今回はManning Publicationsから2022年に出版された Infrastructure as Code Practices and Patterns の中から、IaCのテストに関連する部分を取り上げます。
前提
本書ではテストに限らずIaCの広範なテーマを取り上げており、その中で実践的なテクニックやIaCのパターンなどを紹介しています。今回はIaCのテストを取り上げる第6章を中心に、いくつかのトピックを取り上げています。
また、本書では Python / TerraformをIaCツールとして利用し、Google Cloud上にリソースを提供することを想定しています。
以前取り上げた O'Reilly社のIaC本 では具体的なツールの例を意図的に省いていましたが、本書ではPythonなどのサンプルコードも多く紹介されています。
テストのサイクル
本書ではまず、IaC定義ファイルに修正を加えた後、テストを実行するサイクルについて、以下のように想定しています。
- IaC定義ファイルを修正し、インフラ設定を更新する
- インフラの変更を適用する前に、インフラの設定に対する静的解析を行う
- 実際のインフラに変更を適用する
- インフラへ変更を適用後、インフラの機能に対する動的解析を行う
ここでは2種類のテストを紹介しており、ほぼすべてのテストは2種類のいずれかに分類することができます。
静的解析
実際のリソースに適用する前にインフラの設定をチェックすることで、想定する正しい値やリソースが作成・更新されるかをチェックします。静的解析を行うには起動中のリソースは不要のため、テストの実行と完了までの時間が短く、フィードバックを高速に得られます。
筆者は実際に行うテストの例として、インフラの命名規約や依存関係のチェックを挙げています。このテストを行うことで、企業やプロジェクトの規約に従わない名称や設定のリソースがないか素早くチェックできます。
動的解析
変更内容を反映したリソースに対し動的解析を行うことで、システムの機能が想定通り実現できているかをチェックします。本番環境に変更を適用する前にテスト環境で動的解析を行うことで、本番環境で問題が発覚するより前にバグを検知・修正する機会を得られます。
またテスト環境については、可能であれば以下の要件を満たすよう構築・管理することが推奨されます。
- 本番環境とできる限り同じ設定であること: 本番環境との差分を少なくすることで、より本番環境と近いテストの挙動を得られます
- アプリケーションの開発環境とは別の環境であること: アプリケーション占有環境に対してテスト前の変更をいきなり適用し、アプリケーション開発に影響することを防ぐため。
- テストのたびに構築・削除をしないpersistentな環境であること: 起動中のインフラに変更を適用することで、より実際の本番環境への影響を確認できる
ただし3つ目の要求は、環境を維持するためのコストがかかることを課題として挙げており、それについては第12章で軽減策を提示しています。
※3つ目の要求は前回のIaC本でも紹介されておりましたが、あちらではコストに加え、変更蓄積による想定外の障害リスクがある、という課題を指摘しています。
テストの種類
IaCのテストは静的解析・動的解析の2つに分けられますが、本書ではそれらに含まれるいくつかのテストを紹介しています。
- 静的解析
- unit test
- contract test
- 動的解析
- integration test
- end-to-end test
- (continuous test)
- (regression test)
unit test
unit testはインフラの設定や状態をチェックするテストです。例えばネットワークリソースを定義するTerraformの定義ファイルに対し、想定通りのリソース名とIPアドレスが指定されているかを確認したり、インフラ設定ファイルを生成するpythonコードが想定通りの設定情報が生成されるか、test fixtureとpytestを使って検証する、などが該当します。
unit testを用意すべき条件として、本書では3つのケースを取り上げています。
インフラ設定ファイルを生成するロジックをチェックする時: 設定ファイルの生成にIaCツール内のロジック (例えばCloudFormationのConditionsなど) やPythonを利用している場合、想定通りのファイルが生成されるかをチェックするべきです。特にloop/if-elseに対してはバグも混入しやすいためテストするべきでしょう。
誤った、もしくは問題のある設定が含まれないかの確認: 実際にデプロイする前の時点で、インフラ設定に不適切なものが含まれているかチェックすることができます。例えばセキュリティ的に問題のある設定がないか、ほかのリソースで既に利用しているIPアドレスを指定してないか、などが該当します。なお、IaCをセキュアに利用する方針として、本書では「アクセスとSecretを管理すること」「リソースの監査・管理のためにタグ付けすること」「Policy as Codeを利用すること」を挙げています。
チームの基準に従っているかのチェック: ひとつ前に近いですが、組織やチームで定めたルールに従っているかをチェックすることにも有用です。チームが定めた命名規約に沿ったリソース名やタグになっているかなどをチェックします。
またここではDomain-Specific language (DSL) をテストする場合のpracticeとして、dry-runの実行結果に対してunit testを実行することも紹介しています。これが有効な場合は、DSLなツールの設定ファイルに、テストしたい全ての設定情報がファイルに含まれないケースが挙げられます。
contract test
contract testはモジュールのinput/outputを比較し、想定通りの値やフォーマットになっているかをチェックするテストです。例えば、あるモジュールAのoutput情報を別のモジュールBが利用している場合、何かの拍子にモジュールAのoutputが別物に置き換わればモジュールBは機能しなくなります。こういった不具合は扱うパラメータとモジュールが増えるほど増加しやすく、また人の目ではチェックしきれなくなるため、contract testを作成するのが有効になります。
またここではcontract testが特に有効なモジュールのパターンを3つ紹介しています。
factoryパターン
: input情報のセットを受け取って、input情報+デフォルト値をもとにしたリソース群を作成するモジュールprototypeパターン
: 他のモジュールに利用される固定値を生成するモジュールbuilderパターン
: 複数のリソース定義を含むが、それらを有効化するか無効化するか選べるモジュール
integration test
integration testは、テスト環境などに変更を適用したのち、moduleや設定に行った変更が正しく機能していることを確認するため、起動中のインフラに対して実行します。
本書ではintegration testを行う2つのケースを取り上げています。
モジュール: モジュールのテストは、特定の設定を与えたリソースを作成しテストを行うことで実施します。そのため既存のテスト用リソースに影響しないよう、可能ならモジュールのテスト専用の環境を用意して行います。
インフラ設定: インフラの設定に対するテストは、特に簡単には削除できないようなリソースを扱うこともあるため、既存のテスト環境に更新内容を適用後、テストを実行するという手順が案内されています。
またintegration testを実施するうえで2つのチャレンジングな面も紹介しています。
どの設定パラメータをテストするか: IaCファイルが巨大化するにしたがって扱うパラメータも増加しやすくなり、どのパラメータをテストするべきか判断に迷うケースも出てきます。筆者はここで、すべてのパラメータをテストする必要はない、としています。個別のリソースの作成や更新・削除といった挙動は、IaCツール開発時の受入テストで確認されており、わざわざそこを繰り返し確認する必要はありません。むしろ、モジュール等や自前の自動化スクリプトを使って複数のリソースを同時に扱う場合に、その設定や依存関係が正しいかをテストすることに価値があるとしています。
テストのたびにリソースを削除するか、それとも維持するか: 多くのリソースを維持することでコストも発生するため、必要なリソースだけを維持することを検討します。一般に依存性が少なく、作成や削除に時間がかからないモジュールや設定変更は、毎回作成と削除をすることが推奨されます。これに加えて筆者は具体的な例として、ネットワークやDNSなどのリソースは維持することを推奨し、Application Deployment / SaaSなどは毎回の作成と削除を推奨しています。
end-to-end test
end-to-end testはリソースやシステムの機能性に着目し、IaCファイルの変更によりリソースの果たすべき機能が損なわれていないかをチェックします。特に、ある変更がupstreamな機能を損なう結果になっていないかを確認するうえでは必須なテストとなります。
具体的なテストの例ですが、筆者の場合、end-to-end testのほとんどは、ネットワークおよびコンピューティングリソースに対して書くと紹介しています。またもう一つのケースとして、(おそらくKubernetesなどの) Workload Orchestratorに対してJobを投げ、その処理が完了することを確認することも挙げています。
なお、end-to-end testはこれまで上げたテストの中で最も高価なテストなため、どの環境で実施をするかは検討が必要となります。
continuous test
continuous testは短い周期で頻繁に繰り返し行うテストで、例えばある環境のメトリクスの値が想定通りのものかチェックする、というテストを行います。continuous testは監視システムのメトリクスとセキュリティイベントを含んでおり、何か問題があればアラートを挙げる形で通知することが多いです。
regression test
regression testはある程度の長い期間ごとに実行するテストで、インフラリソースの設定や状態が想定通りかを確認します。これにより、構成ドリフトがないかをチェックし、手動での変更などによるドリフトがあればそれを検知することができます。
テスト戦略
ここまで複数のテストが出てきましたが、これらのテストをいつどのように使えばよいか考える必要があります。本書では大きく2つのケースに分けて、テスト戦略を紹介します。
テストピラミッド
筆者はまずIaCに対する各テストをピラミッド状に配置し(テストピラミッド)、どのテストをどの程度の量実施すべきか、大まかに示しています。通常のソフトウェアテストの原則と同様、Unit testほど多く、End-to-end testほど少ない量テストすることを推奨します。
また、メンテナンスが困難になるようなコードを使ってまでテストを自動化しようとせず、必要に応じて手動テストも行うよう設計することも重要です。
モジュールに対するテスト戦略
モジュールに対しては、unit test / contract test / integration testを実施することで十分に動作を保証できると記載しています。
テストを実施する大きな流れは以下の通りです。
- モジュールへの変更をソースコードリポジトリにコミットする
- unit testを実行し、フォーマットや設定をチェックする
- contract testを実行し、モジュールへのinput/outputが想定通りになることをチェックする
- テスト用の環境にリソースを作成・更新する
- 起動中のリソースにintegration testを実行し、モジュールへの変更が想定通りに機能することをチェックする
- 以降は必要に応じてタグ付けやパッケージング、ドキュメントの更新などを行う
インフラ設定に対するテスト戦略
インフラの設定に対しては、unit test / integration testに加え、end-to-end testを実行することで、機能レベルまで含めて設定に問題がないことを確認できます。また品質を担保するため、テスト環境と本番環境で同じテストを繰り返し実施することを推奨していますが、環境間で差分がある場合は一部テストをスキップすることも可能です。
テスト実施の大きな流れは以下の通りです。本書に記載されたものと若干順番を並べ替えています。
- インフラの設定への変更をソースコードリポジトリにコミットする
- unit testを実行し、フォーマットや設定をチェックする
- テスト用の環境に変更を加えたリソースを作成・更新する
- テスト環境のリソースにintegration testを実行し、変更を加えたリソースが問題なく動作することをチェックする
- テスト環境のリソースにend-to-end testを実行し、リソースが想定通りの機能を果たしていることをチェックする
- テスト環境と同じ流れを本番環境にも適用し、各テストを実行する