TerraformとPyTestを使ったクラウドインフラの自動テストツールの作成
2020年3月27日
0 分で読めます編集者注
このブログ記事は、もともとfugue.coに掲載されていました。Fugueは2022年にSnykの一員となり、Snyk IaCの重要な構成要素となっています。
最近、Fugue向けの自動テストツールを作成する仕事を任されました。Fugueはクラウドリソースのコンプライアンスとセキュリティを監視するサービスです。Fugueのスキャン結果全体が正しいことを検証する方法が必要でした。ローカル環境またはCIで実行でき、構成可能なインフラをデプロイし、Fugueでスキャンして、その結果を検証する自動システムの作成が目標でした。この記事では、社内自動テストツールとなったautotestの設計と実装のプロセスを紹介します。
ここではTerraformとFugue APIを使用しますが、フィクスチャの設計はあらゆるCLIコマンドやAPIに応用できます。そのため、ここで紹介する考え方は、ほかのユースケースにも簡単に適用できます。
設計
本格的にプロジェクトに着手する前に、数日かけて調査し、課題を理解しようとしました。Google検索でいくつか調べた結果、このツールの基盤にpytestフレームワークを採用することにしました。以前、pytestをユニットテストで使ったことはありましたが、今回はその仕組みを詳しく調べ、機能テストの基盤として活用する方法を学ぶ絶好の機会でした。
テスト手順は、次の3つのステップに分けられます。
Terraformでモジュール式のインフラをデプロイする
このインフラを参照するFugue環境を作成してスキャンする
スキャン結果を取得して検証する
ステップ1にはTerraformを使うと決めていました。既存のインフラテスト用Terraform構成がいくつかあり、TerraformはAWS、Azureをはじめ、多数のクラウドサービスプロバイダーに対応しているためです。ステップ2では、Fugue APIを使って環境を作成し、スキャンを開始できます。最後に、ステップ3でもFugue APIを使い、スキャン結果全体を取得して検証できます。
大まかな設計が固まったので、pytestを調べ、設計を実装するための使い方を確認しました。

PyTest
pytestは、テストの検出、Python組み込みの<code>assert</code>文を使ったアサーション、依存関係を管理するフィクスチャを備えたPython用のテストフレームワークです。
フィクスチャはpytestの真価を発揮する機能です。テスト依存関係のライフサイクルを管理するデコレートされた関数です。従来ならsetUp()やtearDown()メソッドに記述していたセットアップとクリーンアップのコードを、フィクスチャに記述します。フィクスチャのスコープを指定すると、その出力をクラス、モジュール、セッション間で再利用できます。
例を見てみましょう。ここでは、APIにアクセスするフィクスチャを定義して使用します。ユニットテストではモックAPIオブジェクトを使いますが、機能テストでは実際のAPIを使う必要があります。
api_clientフィクスチャは@pytest.fixtureデコレーターで宣言します。また、デコレーターのscope引数を使って、フィクスチャのscopeを設定できます。APIクライアントはすべてのテストで安全に再利用できるため、sessionスコープにしました。フィクスチャはテスト関数やクラスと同じモジュール内に定義することも、conftest.pyに記述してすべてのテストモジュールから使用できるようにすることもできます。
テスト関数の引数にフィクスチャ名を指定するだけで、フィクスチャを使えます。この例では、2つのテストでapi_clientフィクスチャを使用しています。フィクスチャのスコープをsessionにしたため、pytestは各テストセッション中にフィクスチャが一度だけ呼び出されるようにします。
api_clientフィクスチャが入力パラメーターcredsを受け取っていることに気づいたかもしれません。fugue_api関数を直接呼び出してはいないのに、credsにはどのように値が設定されるのでしょうか。
creds自体がフィクスチャだと思った方には、金星を差し上げます。⭐️ フィクスチャから別のフィクスチャを呼び出せるため、複雑な依存関係もフィクスチャを連鎖させて構築できます。credsの定義を見てみましょう。
セキュリティの基本を守るため、credsではcredstashを使ってシークレット値を保存、取得しています。シークレット名はpytestのコマンドラインオプションとして渡します。オプションはpytest_addoptionで定義し、requestフィクスチャを介して参照します。credsフィクスチャを使うことで、認証情報を取得する実装を簡単に抽象化し、各テストやフィクスチャに必要な入力に集中できます。認証情報の取得方法を後で変更しても、修正が必要なのはcredsだけです。
pytestフィクスチャの基本を理解したところで、フィクスチャを使って設計を実装する方法を見ていきましょう。
ステップ1:Terraformを使ったインフラのデプロイ
ツールの最初のステップでは、スキャン対象となるインフラを作成し、ステップ2と3で検証の基盤として使用します。Terraformでインフラをデプロイすることは決めていましたが、具体的にどう実現すればよいでしょうか。
設計
このコード例は、既存のテスト構成の構造を示しています。これらの構成は、aws/ルートからterraform applyを実行するか、aws/api_gatewayなどのサービスサブディレクトリから実行して、手動でデプロイできます。テストするサービスを選べるよう、このモジュール性は維持したいと考えました。既存の構造でTerraformを実行することもできますが、それは避けるべきです。ファイルシステム上の処理はすべて固有の一時ディレクトリで実行し、テストを制御された環境で行う必要があります。そのため、一時的なワークスペースを管理する方法が必要です。また、Terraformを呼び出して出力を記録し、エラーを確認する方法も必要です。Terraformは実際の(高額な)インフラを作成するため、問題が起きた場合にインフラを削除するエラー処理が不可欠です。
まとめると、最初のステップで必要なことは次のとおりです。
一時的なワークスペースを作成、管理する
テスト構成をワークスペースにコピーする
Terraformを呼び出し、エラー時には作成済みのインフラを削除する
1と2はフィクスチャで処理できそうですが、Terraformを使ったインフラ作成のように複雑でエラーが起きやすい処理も、フィクスチャにできるのでしょうか。
「できる」と答えた方にも、金星を差し上げます。⭐️ ワークスペースとTerraformのフィクスチャを定義する方法を詳しく見てみましょう。
ワークスペースのフィクスチャ
6行のworkspaceには多くの処理が詰まっています。まず、workspaceがyield_fixtureとして定義されている点に注目してください。これは、関数がreturnではなくyieldを使って呼び出し元に制御を戻すという意味です。その間、フィクスチャの実行は一時停止し、コンテキストマネージャーは開いたままになります。制御がフィクスチャに戻ると、yield文の後にあるコードが実行されます。この例ではWorkspaceコンテキストマネージャーが終了します。pytestはyield_fixtureから返された値を処理するため、通常のフィクスチャと同じように扱えます。workspaceはクラススコープのフィクスチャなので、テストクラスごとに新しいワークスペースが作成されます。これにより、AWSとAzureのテストをクラスごとに分け、プロバイダーごとに新しいワークスペースを確実に作成できます。
次に、workspaceが3つのフィクスチャ、tmpdir_factory、provider、servicesに依存していることがわかります。tmpdir_factoryは、一意の一時ディレクトリを返す組み込みフィクスチャです。--basetemp=DIRECTORYオプションでベースディレクトリを手動設定でき、デバッグに便利です。providerとservices は独自に定義したフィクスチャで、現在のテスト対象プロバイダーと、入力で指定されたサービスのリストを返します。
フィクスチャの気に入っている点の一つは、明確な抽象化を促してくれることです。テストやフィクスチャへの入力は関数定義というインターフェースを通じて指定され、実装はフィクスチャの定義内に隠されます。フィクスチャのモジュール性のおかげで、workspaceの入力ごとに個別のフィクスチャをすばやく簡単に作成できます。そのため、workspace内で値を組み立てる誘惑にかられずに済みます。
最後に、Workspaceがコンテキストマネージャークラスである点にも注目してください。これにより、フィクスチャ自体の終了時にワークスペースが自動的にクローズされます。前述のとおり、yield文を使うと、フィクスチャの終了時に制御が戻るまでコンテキストマネージャーは終了しません。autotestの設計全体を通じて、この強力なパターンを採用しました。使用するTerraform構成のワークスペースへのコピーはinitialize_workspaceで行います。ここでは読者への課題として残しておきます。
Terraformのフィクスチャ
Terraformのフィクスチャは、terraform、infra、terraform_resourcesの3つで構成されます。フィクスチャの連鎖によって、Terraformのstateファイルから読み取ったTerraformリソースの辞書が得られます。terraformはワークスペース内でTerraformを初期化し、infraはterraform planとterraform applyを実行してインフラを構築し、terraform_resourcesはリソースマップを取り出します。
初期化とリソースマップの抽出は簡単ですが、terraform_resourcesをyield_fixtureにする必要がある点に注意してください。infraがyield_fixtureだからです。terraform_resourcesが値を返すと、infraが開いたままにしているコンテキストが閉じられ、クリーンアップコードが実行されてしまいます。
コンテキストマネージャークラスの代わりに、infraでは関数ベースのコンテキストマネージャーbuild_infraを使います。定義は次のとおりです。
build_infraはpython-terraformパッケージを使ってTerraformを呼び出し、出力を収集します。すべての操作でエラーコードを確認し、エラーがあれば独自の例外を発生させます。t.planとt.applyは、それぞれterraform planとterraform applyを呼び出してインフラを計画、作成します。terraform applyは、インフラの作成が一部完了した状態で失敗することがあります。terraformは作成済みのインフラを自動で削除しないため、TerraformApplyErrorを捕捉し、t.destroyを手動で呼び出してから、元の例外を再度発生させます。
build_infraはジェネレーターです。returnの代わりにyieldを呼び出すことで、@contextmanagerデコレーターを使ってコンテキストマネージャーに変換できます。yieldを囲むtry...finallyブロックにより、呼び出し元のコンテキストで例外が発生してもクリーンアップコードが実行されます。
最後に、exceptブロックとfinallyブロックのコードが似ている点に注目してください。どちらも、実行中に何が起きてもインフラを削除する処理です。finallyブロック内のコードは、例外が発生した場合も、発生しなかった場合も実行されるため、tryブロックをまとめられます。
さらに、t.planはリソースを作成しないため、tryブロックの外に移動できます。一般に、tryブロックの範囲はできるだけ狭く保つのがよい習慣です。最終的なbuild_infraコンテキストマネージャーは次のようになります。
ステップ2:Fugue環境の作成とスキャン
大まかな設計の次のステップでは、Fugue APIを使って、ステップ1でインフラを作成したアカウント内に環境を作成し、スキャンします。
設計
Fugue APIはSwaggerを使って定義されたREST APIです。そのため、HTTP呼び出しではなくネイティブの関数や型を使ってAPIにアクセスできるクライアントを簡単に生成できます。クライアントが動作するようになれば、環境を作成してスキャンできます。この処理は3つのステップに分けられます。
Fugue APIのクライアントを生成して設定する
クライアントを使って環境を作成する
クライアントを使ってスキャンを開始し、完了を待つ
Swagger APIクライアント
swagger-codegenを使い、Fugue API定義に基づいてクライアントを生成しました。
APIクライアント用のPythonパッケージ全体が生成されましたが、必要なかったので、クライアントコードをそのままautotestにコピーしました。次に、以下のようにクライアントを初期化しました。
ここでcredsはフィクスチャではなく、単なる入力パラメーターです。initialize_clientはフィクスチャでもテストでもないためです。initialize_clientは、credsフィクスチャに依存するフィクスチャから呼び出されることを思い出してください。
APIクライアントは終了処理が不要なので、environments APIのフィクスチャ定義は簡単です。Swaggerで生成されたクライアントには、APIへの接続を管理するベースAPIクライアントと、APIの各トップレベルの区分に対応する個別のクラスがあります。各クラスのメソッドとして、API操作を利用できます。
抽象化について:apiモジュールを介して、Swaggerクライアントの詳細を抽象化しています。これはインターフェースの両側を抽象化するという原則に沿ったもので、クライアントのインターフェースに変更が生じた場合にも柔軟に対応できます。APIを使った環境の作成について説明する際に、これをさらに詳しく見ていきます。
環境の作成とスキャン
最初に作成した環境フィクスチャは、次のようなものでした。
これは、environments APIのcreate_environmentメソッドをそのまま適用したものです。aws_creds、survey_resource_types、servicesの各フィクスチャから認証情報と入力パラメーターを取得します。クラウドプロバイダーごとにクラスを分けてテストを整理しているため、このフィクスチャのスコープはクラス単位です。最後に、テスト終了時に環境を自動的に削除できるよう、環境をyieldします。
このフィクスチャは設計がシンプルで、必要なことを実行できます。このままにして先に進むこともできました。しかし、このシンプルなアプローチにはデメリットがあります。create_environmentへの入力には多数のフィールドがあり、一部はプロバイダーによって異なるため、扱いが煩雑です。フィクスチャ内でenvironments APIを直接使うと、巨大で読みにくい関数を作らずにcreate_environmentの入力パラメーターを準備する柔軟性が限られます。読みやすさと再利用性を両立させるのが難しかったため、再利用性をあきらめ、プロバイダーをハードコードしたうえで、Azure環境用にenvironmentフィクスチャを複製しましたが、これは良い解決策ではありませんでした。本当に必要なのは、フィクスチャのコードとenvironments APIの間にもう一つのレイヤーを設け、扱いにくい入力の準備をカプセル化し、フィクスチャとAPIの関係を管理することです。幸い、apiモジュールがまさにその役割を果たします。
入力の準備とAPIの呼び出しを切り分け、apiモジュール内の関数にまとめました。
この関数は長いものの、AWSとAzureの両方に対応しており、environmentフィクスチャを基本的な処理だけに絞れます。
同じアプローチで、環境内のスキャンを開始できます。
出力を使わなくてもterraform_resourcesフィクスチャを含めることで、スキャン開始前にリソースが作成されていることを保証できます。次に、apiモジュールでは次のようにします。
Terraformを使ったリソースの作成と、Fugue環境の作成およびスキャンを行うフィクスチャが揃いました。これで、テストを書く準備はほぼ完了です。
ステップ3:スキャン結果の取得と検証
テストを書く前に、Fugue APIからスキャン結果を取得できるようにする必要があります。スキャン結果は、APIのカスタムルールテスト入力エンドポイントから取得できます。このフィクスチャの作成には、上で使った概念をそのまま適用できます。
get_scan_resourcesは、単に呼び出しをそのまま受け渡すだけです。
このような単純な関数では、抽象化レイヤーを省略してAPI呼び出しをフィクスチャに直接記述したくなるかもしれません。実際、この関数の最初のバージョンではそうしていました。しかし、これはリーキーな抽象化、つまり隠すはずの基盤機能を一部露出させてしまう抽象化を生み出します。リーキーな抽象化は、抽象化を使う際の認知負荷を増大させ、軽減するどころではありません。
scan_resourcesフィクスチャができたので、テストに必要な入力はすべて揃いました。前のセクションで説明した準備や抽象化をすべて経て、テスト自体はわずか数行のコードで済みます。
checkは、失敗として扱われないアサーションを提供するpytestプラグインのフィクスチャです。verify_resource_idは、2つのリソースを比較し、リソースIDが同じ場合にtrueを返す単純な検証関数です。
より複雑なテストを書きたい場合は、リソースの詳細情報をすべて利用できますが、今のところはインフラを立ち上げ、正しくスキャンされることを検証するだけで十分です。テストを実行すると、次の出力が得られます。
まとめ
この記事では、pytestを使って、自動インフラテストツールautotestを作成する方法を紹介しました。クリーンで強力な抽象化を簡単に構築できる、pytestのモジュール式フィクスチャの威力について説明しました。また、Fugue APIを使って環境とスキャンを作成し、それらの処理をフィクスチャにカプセル化する方法も取り上げました。さらに、Fugue APIのカスタムルールテスト入力エンドポイントを使って、スキャン内のリソースの詳細情報を取得する方法を説明しました。最後に、これらをすべて組み合わせてテストを作成しました。
最後にもうひとつ…
Fugueは、エンジニアのためにエンジニアが作った、クラウドセキュリティとコンプライアンスのソリューションです。Fugueを使うと、次のことができます。
動的な可視化とコンプライアンスレポートにより、クラウドインフラ環境とセキュリティ態勢を包括的に可視化できます。
CIS Foundations Benchmark、HIPAA、PCI、SOC 2、NIST 800-53、ISO 27001、GDPR、および独自のポリシーについて、ソフトウェア開発ライフサイクルのあらゆる段階でコンプライアンスを検証できます。
ベースラインの適用によりクラウドの設定ミスを防ぎ、セキュリティ上重要なクラウドインフラを自己修復できるようにします。
開発者のために設計されたIaCセキュリティ
Snykは、統合されたポリシー・アズ・コードエンジンにより、SDLCからクラウドでの実行時までInfrastructure as Codeを保護します。すべてのチームが安全に開発、デプロイ、運用できるよう支援します。


