前回のエントリでは、最初の一歩を整理したので、今回はプロジェクトのページに載っているベストプラクティスについて自分なりに整理したい。英語を読める人は下記に載っている。
ベストプラクティスは下記の10個
- ユニットテスト、インテグレーションテスト、E2Eテスト
- テスト環境
- ネームスペース
- クリーンアップ
- タイムアウトとロギング
- デバッグと、余計なものが挟まったテストアウトプット
- テストのキャッシュを避ける
- エラーハンドリング
- Dockerを使って、ローカル実行する
- テストステージを使って繰り返し実行する
ユニットテスト、インテグレーションテスト、E2Eテスト
下記の動画で細かく述べられている。下記に動画の内容を少しだけ記述しておきます。動画でも、ベストプラクティスでも説明している内容があるので、それは割愛しておきます。
ユニットテストとは?
インフラの場合は、必ず外部リソースにアクセスするので、通常のプログラミング言語のユニットテストではない。ユニットテストは1つのユニット(リソース)例えば、WebAppとか、SQLServerとかそういった単位でテストをすること。
flaky テストと、リトライ
各テストで、コードが正確でも、クラウドプロバイダーに対してデプロイを行うので、エラーがどうしても発生してしまいます。特にE2Eに近づけば近づくほど、エラーの割合が増えてしまいます。その場合、リトライを行うのが一つの対策です。(terratestにリトライの実装が含まれています)
E2Eのコツ
上記のように実行時間が長くなりまくるので、E2Eテストなどは、環境をプロビジョンしておいて、インクリメンタルにアップデートするなどがあった。(全部デプロイするとめっちゃ時間かかるため)
全体的なTips
これも動画のスクショです。全体のテストの性質について述べられています。
テスト環境
テスト環境に関しては、本番と同じクラウドプロバイダのアカウントを使わずに、完全に本番と分けたアカウントを使いましょう。これは、テストによって、本番の環境に影響することを防ぐためです。例えばAzureとかでも、デプロイしておくと、様々なクオータの制限に引っかかるかもしれず、そうなったら、本番のほうが問題なくても影響を受けてしまいます。
ネームスペース
テストで、ネームスペースを分けましょうという話です。また、テストをパラレル実行しても良い構造にしておくことも重要です。Azure とかだと、Resource Group を分割したり、Kubernetes とかなら、namespace があるのでそれを使う感じです。もちろん本番環境との分離という意味合いもあります。あと、クラウドのデプロイメントでありがちなのが、リソースがグローバルユニークである必要があるものがあります。例えばAzure だと、storage account の名前はグローバルユニークである必要があるので、そのリソースで同じ名前を使うとかぶってテストがこけます(ちなみにクリーンしても1時間ぐらい使えなかったりもします)だから、terratestだと、random.UniqueId()
の関数が用意されているのでそうならないようにしましょう。
クリーンアップ
テストとは言え、本物の環境にデプロイするので、クリーンアップしましょう。Goの defer
を使って、毎回削除しておくのがよいでしょう。
// Ensure cleanup always runs
defer terraform.Destroy(t, options)
// Deploy
terraform.Apply(t, options)
// Validate
checkServerWorks(t, options)
ちなみに、クリーンアップしておいても、失敗することが起こるらしいので、別の方法でクリーンアップする必要があるみたいです。AWSだとライブラリがあるらしいですが、例えば、Azureだと、ResourceGroupのネーミングルールを決めておいて、AzureAutomationなどでクリーンアップのスクリプトを書いておけば良いでしょう。
タイムアウトとロギング
go のテストのデフォルトタイムアウトは10分で、インフラのタイムアウトだと、余裕で引っかかるので、タイムアウトを長くしましょう。
$ go test -timeout 30m
多くのCIシステムは、ログが一定の期間出力されない場合、実行を終了してしまうものが多いそうです(CircleCIは、10分のようです)t.Log
や t.Logf
を使うと、ログがテスト実行の最後までバッファされてしまいます。だから、ロングランニングテストをする場合、terratest の logger.Log
や logger.Logf
をつかって、直ちにstdout
に出力しましょう。
それらのライブラリを使ってstdout
に書いたとしても、Goは、パッケージごとにテストが終了するまで、キャッシュしてしまいます。その場合ワークアラウンドとして、各パッケージをシリアルでテストする方法があります。
$ go test -timeout 30m -p 1 ./...
デバッグと、余計なものが挟まったテストアウトプット
これは前回のポストで説明しましたが、普通にログを吐くと特にパラレルテストの時にログが挟まってみにくくなったり、CIの時にJUnit形式のテストレポートを出したくても、出せなかったりしますので、terratest_log_parser
のライブラリを使います。詳細は下記まで。
テストのキャッシュを避ける
1.10 以降、GO は、テストの結果を自動でキャッシュします。テストコードを変えてもテストが実行されないことがあります。特にTerraformのコードのみを変えた時は。その場合、キャッシュをオフにしましょう。-count=1
がそれにあたります。
$ go test -count=1 -timeout 30m -p 1 ./...
エラーハンドリング
通常は、terraform init
や terraform apply
を実施したときにエラーになった場合、テストもエラーで終了してほしいと思いますが、エラーがあっても引き続きテスト実行してほしいケースがあります。
通常のコード(エラーが発生したらテスト実行終了)
terraform.Init(t, terraformOptions)
terraform.Apply(t, terraformOptions)
url := terraform.Output(t, terraformOptions, "url")
エラーを許容するコード
語尾にEが付いたメソッドを使うと、エラーを許容しますので、その後の処理が書けます。
if _, err := terraform.InitE(t, terraformOptions); err != nil {
// Do something with err
}
if _, err := terraform.ApplyE(t, terraformOptions); err != nil {
// Do something with err
}
url, err := terraform.OutputE(t, terraformOptions, "url")
if err != nil {
// Do something with err
}
Dockerを使って、ローカル実行する
スクリプトがデプロイメントに含まれる場合、Dockerがあれば、ローカルでテストを行うことができます。Dockerのビルドは、本物のサーバーに比べて10倍速いですし、100倍速くスタートします。Package Docker Exampleにサンプルがあるようです。
テストステージを使って繰り返し実行する
例えば次のような、「ステージ」があったとします。
- Packerを使ってVMのイメージを作成する
- VM のイメージを terraform を使ってデプロイする
- VM をバリデーションする
- VM を terraform を使って削除する
このようなテストの場合、それぞれの実行に時間がかかります。またある特定の箇所を何回も実行したくなるかもしれません。その場合は、terratest の test_structure
パッケージが便利です。環境変数を設定することで、いくつかのステージをdisableにすることができます。
(試していませんが、サンプルを見ると、ステージを実行すると、そこで得られたIDを次のステージに引き渡すことができるみたいなので、この場合、おそらく、前のステージはデプロイされているので、何回も作成・削除したくない、そこで環境変数を設定すると、前のステージの実行がおこなわれなくなり、つまりデプロイされなくなって、既にデプロイしたときの、ID は後続のステージで使えるので、問題なくテストが行えるということかと思います。次回以降で試してみます。)
サンプルはこちら
なぜ terratest なのか?
他にも似たようなことが出来るツールがありますが、terratest の特徴はなんでしょうか?今までのツールは、コンフィグのプロパティを確認するのがテストでした。例えば、httpd
がインストールされて動作しているかなど。terratest は「インフラストラクチャが本当に動いてるか?」という問いに答えるようにしています。先ほどの例だと、実際にHTTPリクエストを送信して、期待する結果を得て、データベースにデータがストアされて、それが読めて、新しいバージョンのDockerコンテナがデプロイされていて、オーケストレーションツールがダウンタイムなしに新しいコンテナをデプロイしたか?などです。
また、terratest は、単一のサーバーだけではなく、システム全体のテストにも使います。つまりE2Eのインフラテストにも使われます。
terratest を作ったgruntworkは、沢山の IaC ツールを書いてきたらしく、インフラのコードの信頼性を上げるためには、リアルワールドのE2Eテストしかないという結論にたどり着いたらしいです。また、最近のDevOps環境は、いろんなものがものすごく速く変わりやすいです。そういうのを発見するのもテストです。彼らは、AWSやAzure、TerraformやPackerなどのバグ夜間実行のテストで何回も見つけてきたらしいです。
今回のまとめ
今回は、ベストプラクティスを理解するために、ほぼ日本語訳みたいなことをしてみました。全体像がわかったので、次回はKubernetesをデプロイするスクリプトを作成して、テストを書いてみたいと思います。