TECHSTEP

ITインフラ関連の記事を公開してます。

O'Reilly IaC本を読んでIaCのテストについて学ぶ

今回はO'Reilly社から2020年に出版された Infrastructure as Code (2nd Edition) を読み、IaCに対するテストについて個人的に気になった箇所などを書き留めていきます。

www.oreilly.com

目的

これまで業務や勉強の中でクラウド、Infrastructure as Code (IaC) を扱うことは何度もありました。その中で IaCに対するテストってどう扱えば良いのかわからないなあ と感じており、ある程度網羅的にIaCのテスト体系を学びたいと思っていました。 IaC本といえば2017年に日本語訳されたものも出版されていますが、あちらは2016年に出版された第1版が原著なので、せっかくなら新しいほうにしようと思い、こちらを読むことにしました。

前提

IaCとは

IaCがどんなものかは色々と表現方法があると思いますが、本書では ソフトウェア開発のプラクティスに基づくインフラストラクチャ自動化のアプローチ と記載しています。IaCを使ったインフラ管理では、まずIaCコードに変更を加え、その変更をテストしてシステムに適用するために自動化プロセスを使用します。現代のクラウド全盛の時代 (本書では Cloud Age と記載) では、システムに変更を加え続けることがシステムの安定性につながるので、IaCを使うことは変更に適応するためにも適切なアプローチであるといえます。

また、Cloud Ageはリソースの用意までに数十秒から数分で済むものがほとんどのため、オンプレ全盛の時代に比べるとリソースを再作成しやすくなり、何か問題が発生しても問題解決しやすくなりました。そのため、リソースの設定や構成を変更することにかかるコストは以前と比べて大きく下がり、変更作業はシステムやリソースなどに対する学びと改善の機会を与えるイベントとなりました。このような状況では、何か問題が発生することを恐れて変更の機会を減らすより、変更を繰り返して改善速度を最大化するほうが適します。また変更の機会を増やすには、小さな変更を短期間に繰り返すことが重要となります。

こういった背景もあり、インフラにより安全に変更を加え続けるためにも、継続的に自動テストを実行することが重要であるといえるでしょう。

2つ目のCore Practice

本書ではインフラストラクチャをより簡単に、より安全に変更するための原則として、3つのCore Practiceをメインに紹介しています。3つのCore Practiceはそれぞれ以下の通りです。

  • Define everything as code
  • Continuously test and deliver all work in progress
  • Build small, simple pieces that you can change independently

これら3つのPracticeはいずれもテストに関わりがありますが、特に2つ目は直接関連するもののため、今回はここから多くの事項を取り上げています。このPracticeを実践するべき理由は、より早くシステムやコード上の問題を発見し、より少ない時間で問題に対応できるからです。また、継続的に問題を解消することで技術的負債の蓄積を避けることにもつながります。

IaCをテストするうえでの課題

上述した2つ目のCore Practiceでは、IaCコードに対して継続的にテストすることを掲げていますが、一方でこれを実践することが難しくなってしまう要因があります。

Challenge: Tests for Declarative Code Often Have Low Value

IaCを実現するツールは数多くありますが、本書では主にDeclarative Code / Imperative Code という2種類に注目しています。簡単に言えば、Declarative Codeはインフラリソースの種類や状態を宣言的に定義する、 CloudFormation/Terraform/Ansible/Chef/Puppetなどで定義するコードを指し、Imperative Codeは汎用的なプログラミング言語の中でリソースを定義する、 AWS CDK/Pulumiなどを指しています。

ここではDeclarative Codeでテストを実施するケースを考えます。Declarative Codeではインフラリソースの状態を宣言しますが、例えば以下のような形になります。

subnet:
  name: private_A
  address_range: 192.168.0.0/16

これに対する低レベルなテスト(ここではユニットテスト相当のもの)を実装すると、IaCで定義したものと同じ情報をテストコードにも書くことになります。

    assert:
      subnet("private_A").exists
    assert:
      subnet("private_A").address_range is("192.168.0.0/16")

※上記いずれも本書より引用

Declarative Codeに対してこのようなテストを実行することには、幾つかの課題があります。

まず、上記のようなテストを行おうとすると、IaCファイルを変更するたびにテストコードも修正しなければならず、テストが成功するよう帳尻を合わせる作業に時間を使うことになります。

次に、このテストをやるとどんなリスクが検知できるかを考えてみても、テストから検知できるリスク自体の価値が低く、そもそもこのテストをやらなくてもよい場合も多々あると主張しています。

  • 該当のコードが一度も環境に適用されていない : (実際にこういった問題はあるのかもしれませんが) 該当のコードの一部をテストすればこの問題は検出できるでしょうし、何度もテストをするものではありません。
  • コードは適用されたが、エラーを返すことなく失敗している : これはコードではなくツール側のバグです。ツール側のバグを検知するために、ここで細かくテストをする必要はないでしょう。
  • 誰かがインフラを修正したがテストを修正することを忘れていた : これはそもそもこのテストを実行しなければ除かれるリスクです。

一方で、Declarative Codeへのテストで有効なケースとして、input情報などによって作成されるリソースの種類や設定が異なる場合や、複数のリソースを組み合わせて複雑な構成のインフラを作成する場合などを挙げています。

※感想

この視点は理解ができる一方で、現実にはこれに近いことをやりたいケースも多々あると思っています。

例えばWAFやSecurity Groupなどは、ちょっとした設定ミスが即セキュリティインシデントにつながる可能性もあります。このようなリソースのパラメータに間違いがないか、目視のチェックだけでなくユニットテストまで実行できれば、より安全にチェックができる気もします。

こういった場合にどう対応するのがよいのかは気になりました。

Challenge: Testing Infrastructure Code is Slow

インフラリソースをテストするには、対象のリソースをクラウド環境などに作成し、起動中のリソースを用意する必要があります。この場合、テストを開始するには実際のリソースが用意されるまで待つ必要があるため、準備時間が長くなることを意味します (クラウドプラットフォームを利用する場合は特に長くなる傾向にあります)。結果としてテスト完了までの時間が長くなり、デプロイパイプラインやCI/CDを用意していても、自動テストから素早くフィードバックを得ることが難しくなります。

ただしこれを軽減する方法も本書では案内されており、ここでは2つの方法を取り上げます。

  1. 1つのファイルで管理するインフラリソース数を少なくする: テストのたびに複数のリソースをまとめて作成するとどうしても時間がかかるので、1つのファイルで管理するインフラの単位を小さくします。こうすると1度に作られるリソースが少ないので作成までの時間は短くなり、またテスト自体を書きやすくもなります。テスト対象のコードが別のリソースに依存する場合はTest doubleの利用も検討します。
  2. そもそもプロビジョニングしない: プロビジョニングに時間がかかるのなら、そもそもプロビジョニングしなくてもできるテストを考えます。本書では オフラインテスト という名称で紹介されており、例えばLinterなどの静的解析などがこれに当たります。

Challenge: Dependencies Complicate Testing Infrastructure

ひとつ前のチャレンジでも触れましたが、あるリソースを作成するために別のリソースが必要な場合があります。こういった依存するリソースも併せて作成するとなると、プロビジョニングの処理時間はさらに増加し、ますますテスト完了までの時間が長くなります。

ここでの有効な対策としては、依存関係をTest doubleに置き換えることです。mock/fake/stubといったTest doubleを利用することで、依存するリソースを作成することなく目的のリソースをテストできます。 なお、本書執筆時点では LocalStack / Moto などのツールが紹介されており、これらを使うことでローカルで幾つかのテストを実行できます。

※感想

IaCコードに対してTest doubleを使ってテストするケースとは、具体的にどのような場合かが気になりました。

よく見るケースはAWS Lambda関数から別のAWSリソースを操作する場合ですが、これ以外でTest doubleを使っている例があまり思い浮かばず。。。

IaCでのテスト項目

IaCのコードに問題がないかを確認するためには、様々な観点から、そのコードを適用することで生じるリスクをチェックする必要があります。この観点は幅広く、本書ではコードの品質、機能性、セキュリティ、コンプライアンス、パフォーマンス、拡張性、可用性、運用性について触れています。

そのうえで、前述のテストの難しさを踏まえ、それでも効率的なテストを実現するためにどんなことができるのか、本書では幾つかのポイントを挙げています。その一つが オフライン/オンラインテストを適切に実施すること です。

オフラインテスト

オフラインテストとオンラインテストは、実際にクラウドプロバイダーにリソースを作成するか否かで分かれます。実際にリソースは作成せずローカルで実施するものをオフライン、そうでないものをオンラインとしています。

オフラインテストは大きく3種類あります。

  • Syntax Checking: タイプミスや構文エラーなどを検知します。多くのIaCツールにはDry Run機能が備わっており、例えばこれを利用してテストを行います。

  • Static Code Analysis: 誤ったコードスタイルやポリシー、セキュリティの問題などを検知します。tflint などのlinterや tfsec / cfn_nagなどのセキュリティ系ツールを使用します。

  • Tesintg with a Mock API: Mockインスタンスを作成してStatic Code Analysisなどでは見つからないコードのエラーを検知します。LocalStack / Azurite などの一部ツールでしかこれは実現できず、またDeclarative Codeにはあまり有効なテストが実施できない傾向にあります。

オンラインテスト

オンラインテストにはより多くの種類がありますが、ここでは3つの観点からテストを紹介しています。

Preview: Seeing What Changes Will Be Made

実際にリソースを作成する前に、テスト対象のコードを適用することで、Stack (1つ以上のリソースを定義した集まり) にどんな変更が生じるかを確認します。 terraform plan はこの代表例ですが、変更予定の内容を適用前に確認することで、想定外のリソース削除が行われないか、などのリスクを管理できます。

なお、管理リソースが多いと terraform plan の実行結果が大量に出力されることもあり、この結果を人の目で全て確認するのが適切でない場合もあります。この場合は、出力結果に対して自動テストを実行することで、例えばポリシーの違反やリソースの誤削除などを検知するテストが実行できます。

※参考: SpeakerDeck - terraform plan 結果の検証を自動化するぞ! with Conftest

Verification: Making Assertions About Infrastructure Resources

コードを適用して実際にStackを更新した後は、各リソースに対するAssertionを通じて、そのリソースが問題なく動作することを確認できます。こういったテストはawspec / Chef InSpec / TaskCat / Terratest などのツールがサポートしており、例えばそのリソースが起動しているか、ヘルスチェックをパスするか、などをテストできます。

※参考:

ただし、Challenge: Tests for Declarative Code Often Have Low Value にあったようなシンプルなAssertionをテストしても、その多くはリソースの状態を再定義しているにすぎないため、あまり効果は得られません。本書では効果のあるテストの例として、仮想マシンの作成後に is_running passes_healthcheck などをテストすることを挙げています。これらのテストは、何らかの理由でVMが正常に起動しない場合をテストできるため、一定の効果が期待できます。

また、input情報によって結果の変わるような動的なコードの場合も、Assertionテストは効果があるでしょう。

Outcomes: Proving Infrastructure Works Correctly

Stackを更新してリソースを用意することで、テスト対象のリソースは想定通りに機能するか (いわゆる機能テスト) を確認できます。ただし機能テストは1リソースで完結しないことも多く、テスト実施のために専用のインフラリソース (Test Fixture) を作成する必要があります。

テストを構成するその他要素

これ以外にも、テストを効率よく実施するために検討すべき事項は様々あります。

テスト環境のライフサイクル

オンラインテストを実行するには、実際にリソースを作成する必要があります。そのため、多くのプロジェクトではテスト専用の環境を用意するでしょう。このテスト環境のリソースをどのようなライフサイクルで管理するかは、検討する余地があります。

Cloud Age以前は静的で長命なテスト環境を維持し続けていましたが、今ではテストのたびにまっさらな新しいインスタンスを作成することも簡単にできます。ただし毎回インスタンスを作り直すのもデメリットがあり、どんなライフサイクルで管理するかはトレードオフとの相談になります。

  • テスト用のリソースを常に起動する場合: 既存のリソースを更新するのでテスト準備時間は少なくなり、結果的にテスト実行時間は短くなります。ただしリソースが長い間起動し続けることで、起動中のリソース内に変更が蓄積される場合もあり、それが原因で更新時にリソースが壊れる場合があります。こういった事態が続く場合は、別パターンの採用を検討すべきでしょう。
  • テストのたびに新しいリソースを作る場合: 上記のような変更の蓄積はないので、それ以前のテスト実行結果を気にする必要はなくなります。ただしこちらはテスト開始までの時間が延びるため、フィードバックを得るまでの時間が長くなります。

こういったトレードオフを考慮しながら、オンラインテスト用のインスタンスのライフサイクルを決定すべきでしょう。

パイプライン設計

継続的にテストを実施するには、インフラストラクチャのデリバリーパイプラインの中にテストを組み込む必要があります。

Progressive Testing (段階的にテストを実施すること、後述) を意識すると、パイプラインの初期ステージでは、スコープが小さく完了までが短いテストを実施し、スコープの広く時間のかかるテストはステージ後半に実施します。

テスト対象のリソースの中で別のリソースに依存するものが含まれる場合、パイプラインの初期ステージでは依存するリソースが不要なもの、もしくはMockを利用できるものをテストします。

対象のリソースがクラウドプロバイダーなどに依存する場合も、パイプラインの前半ではプラットフォームに依存しないテストを行うよう意識します。

※デプロイパイプラインのステージとテストスコープ・テスト完了時間の関係性

テスト要素の組み合わせ

上記以外にもテストを構成する要素は様々ありますが、最終的にはこれら要素をまとめ、テスト環境を作る必要があります。本書では2つのガイドラインが記載されています。

Support Local Testing

1つ目はローカル環境でのテストをサポートすることです。インフラコードにかかわらずですが、コードを作成・変更する作業者は、共通のパイプラインや環境にコードを配置するより前に、作業者自身でテストを実行するべきです。

ローカル環境でテストをするには、テストに必要な環境を用意することが必要です。この方法は様々で、専用のスクリプトを使って各環境にツールなどをインストールする場合もあれば、コンテナや仮想マシンを使ってパッケージングされたテスト環境を作成する場合もあるでしょう。

またローカルでのテストを有効にするポイントとして、ローカル環境とパイプラインとで使うテスト用のスクリプトを揃えることを挙げています。こうすることで、どの場所でも一貫してテストのセットアップと実行が保障された状態を作ることができます。

Avoid Tight Coupling with Pipeline Tools

多くのCIやパイプラインツールは独自のプラグインを提供しており、それを使うことでテストの設定と実施が便利になるものもあります。しかしこういったプラグインを使うと、同じようなテストをパイプライン以外の環境で実行することが難しくなります。

こういったプラグインの使用は避け、どの環境からでも利用できるツールを採用します。

テストに向けた心得

本書ではテストをうまく実施するための心得が随所に記載されています。その中で大事と感じたものをいくつか取り上げます。

Core Practice: Define Everything as Code

本書の1つ目のCore Practiceでは、すべてをコードで定義することを挙げています。これを実践することで得られる利点は大きく3つ紹介されており、コードの再利用性・リソースの一貫性・透明性の向上があります。

何をコードで定義すべきかというと、IaC定義ファイルだけでなく、サーバーの設定ファイルやサーバーイメージ、デプロイに使うパイプラインの設定ファイル、自動テストに使うバリデーションルールファイルなど、インフラの作成や運用管理にかかわるすべての要素が対象となります。

またこのプラクティスを実現するためのポイントも紹介されています。

  • Choose Tools with Externalized Configuration: インフラの定義情報がコードで管理されず、特有のインターフェイス (GUI/API/CLIなど) からしか参照できないようなツールの利用を避けます。
  • Manage Your Code in a Version Control System: VCSに入れることで、追跡性・可視性などを獲得し、いざというときのロールバックもやりやすくなります。
  • Separate Declarative and Imperative Code: Declarative codeとImperative codeは性質が異なるため、2種類のコードが混在している状況は、ソースコードに何か重要な問題が潜んでいることを暗示しています。
  • Treat Infrastructure Code Like Real Code: コードをデザイン・管理して理解しやすいものに維持し、コードレビューやペアプロ・自動テストなどを利用してコードの品質を保つようにし、技術的負債に気づいたらそれを最小化するよう努めることが求められます。

Core Practice: Small, Simple Pieces

本書の最後のCore Practiceでは、システムを小さくシンプルなピースで構成することを挙げています。これはパフォーマンスの高いチームに共通で見られる特性で、こうすると変更しやすく、テストしやすいシステムを作ることにつながります。

本書では変更しやすいシステムを作るため、モジュラー性を持たせることを目指します。ほとんどのIaCツールは、モジュールやライブラリなどをサポートする機能がありますが、ここではモジュラー性を獲得するためのコツも紹介されています。

またこのPracticeでは自動テストを用意することの利点も挙げています。あるシステムの設計が不十分でモジュラー性に乏しい場合、このシステムに対して自動テストを作成し実行することは困難となります。つまり継続的なテストが実行できない場合、システム設計に何かしら課題があることを示唆しており、設計を見直し改善する機会を与えてくれます。

自動化の仕組みの導入をシステム構築作業に含める

IaCの導入などを進めようとするときに「システムの構築を先に終わらせてから自動化するべき」と主張する人もいますが、自動化を後回しにすると良くない理由がいくつかあります。

まず、自動化によって、構築したものに対する自動テストを簡単に書くことができるようになります。これによりシステムの抱える問題の検知と修正・再構築が容易になります。ビルドプロセスの一部としてこれを行うことで、より良いインフラを構築することができます。

また、既存のシステムを自動化するのは、とても難しいことです。自動化は、システムの設計と実装の一部であり、自動化なしで構築されたシステムに自動化を追加するには、そのシステムの設計と実装を大幅に変更する必要があります。これは、自動化されたテストとデプロイメントにも言えることです。

これらは検証としてシステムを作る時も同様です。検証で良さそうに見えると、すぐに次の段階に移ることを求められるため、自動化できないまま進んでしまうことがままあります。そのため、自動化の仕組みを導入することを検証の一部としていれるべきです。

Progressive Testingを意識する

すでに出てきたワードですが、シンプルなオフラインテストから始め、段階的にテストを行うことを意識します。こうすると最初にスコープの狭い範囲でのエラーを検知し、問題箇所を特定しやすいところから修正することができます。加えて異なるレベルのテストで同じ内容のテストを実施しないようにすることで、テストスイートを管理しやすくすることもできます。

なお、Progressive Testを行う上で、どのテストをどのステージで実施すべきかを判断する必要がありますが、これには通常テストピラミッドが役立ちます。ただしDeclarative Codeの場合、アプリケーションコードの時とは異なり、テストピラミッドがそれほど有効でない、とも指摘しています。

Declarative Codeツールで管理するStackは、ユニットテストを実施するにはサイズが大きく、かつそれはしばしばクラウドプラットフォームに依存します。またDeclarative Codeは Challenge: Tests for Declarative Code Often Have Low Value の通りユニットテストをする価値が低く、実際のインフラリソースがなければ有用なテストを実施するのが難しい傾向もあります。こういった理由から、たとえユニットテストがあるとしても、その総量は従来のテストピラミッドが示すほど多くはなく、どちらかというとダイアモンド状になるだろうと記載しています。

※本書中の図を一部改変

なお、これがImperative Codeの場合、Declarativeよりはもっと多くのユニットテストが必要になります。

テストツールは悩みの種

IaCコードに対するテストをする上で難しい点として、最後にツールの選定の難しさを挙げています。

例えばIaCテスト全体をオーケストレーションするために、多くのチームでは専用のカスタムスクリプトを作成しています。こういったテストをサポートするツールは現状あまりなく、本書でも Test Kitchen / Moleclue という2つを上げるにとどまっています。こういったツールは特定のパターンを想定していることが多く、自分たちのやりたいパターンに合わせてカスタマイズをしなければならない場合も多くあるため、結局は自分たちで一からスクリプトを作成した方が良いと考える人も多いとのことです。現状では使えるツールを探しつつ、自分たちがやりたいこととのギャップを埋めるため、カスタムスクリプトを用意するという過程を繰り返さなければならないでしょう。

またそれ以外の部分でも、IaCコードに対するテストに利用できるツール自体はいくつかあります (本書でもいくつか紹介されています) が、そもそもインフラストラクチャを効率的に開発する手法、例えばTDD (Test-Driven Development) ・CI・自動テストが (本書出版時点では) あまり確立されていません。今後の技術的発展などにより、こういった課題の改善されることが期待されます。