TT Lab
はじめる
学ぶ 学習パス コース

Terraform実戦

モジュールを直すたび手で確認していて、一つ見落とした

TT Labで続きを見る

目標

テストできる形のモジュールを作り、プランだけを立てる安価なテストと、実際に作る高価なテストを、それぞれ書き、runをつなげ、止まるべきものが止まるかをテストし、失敗がどんなメッセージを出すかを見て、mockで値だけを確認したあと、CIが呼び出す1行にまとめます。

なぜ重要なのか

再利用するモジュールは、呼び出す場所が増えるほど、直すのが怖くなります。「この値を変えると、どこが壊れるか」を、人が毎回確認するからです。テストを付けておけば、その確認がコマンド1行になり、そのときから、モジュールを直せるようになります。重要なのは、テストを2種類に分けて使う感覚です。プランだけを立てるテストは、速くて安価なので、名前のルール・条件分岐・デフォルト値のように、値を見ればよいものをすべてカバーでき、実際に作るテストは、遅いですが、「本当にそれがそのようにできるのか」を確認する唯一の方法です。ここに、「止まるべきものが止まるか」を確認するテストを加えれば、validationを誤って直したときに、黙って通過することがなくなります。最後に、これらすべてが、CIの終了コード1つにつながってこそ、価値が生まれます。

ステップ

  1. /root/tfa-test/main.tfに、var.env(dev・stage・prodだけを許可するvalidation)と、var.replicas(デフォルト1、1–10だけを許可)を受け取って、out/app-<env>.confにname=とreplicas=の2行を書くlocal_file.confを置き、出力name・conf_path・replicasの3つを宣言してください。initして、tofu validateで確認します。
  2. /root/tfa-test/tests/basic.tftest.hclに、ファイルレベルのvariablesでenv = "dev"を置き、command = planのrun "name_is_prefixed"で、出力nameがapp-devかをアサートしてください。tofu testを、このファイルだけに絞って回し、出力を/root/tfa-test/results/basic.txtに保存します。
  3. /root/tfa-test/tests/apply.tftest.hclに、env = "stage"、replicas = 3で、command = applyのrun "file_is_written"を置き、file(output.conf_path)の内容が、正確にname=app-stageとreplicas=3の2行かを、アサートしてください。このファイルだけに絞って回した出力を、/root/tfa-test/results/apply.txtに保存します。
  4. /root/tfa-test/tests/chain.tftest.hclに、command = applyのrun "make_dev"と、そのrunの出力を参照して値を決める、command = planのrun "reuse_previous_output"を置いてください。2つ目のrunは、出力nameがapp-prodかをアサートします。このファイルだけに絞って回した出力を、/root/tfa-test/results/chain.txtに保存してください。
  5. /root/tfa-test/tests/validation.tftest.hclに、env = "qa"で動くrun "bad_env_is_rejected"と、replicas = 99で動くrun "too_many_replicas_is_rejected"を置き、それぞれexpect_failuresで、該当の変数のvalidationが止めるのが正常であることを宣言してください。このファイルだけに絞って回した出力を、/root/tfa-test/results/validation.txtに保存します。
  6. /root/tfa-test/tests/fail.tftest.hclに、わざと間違ったアサーション(出力nameがapp-nopeか)を置いて、tofu testを回してください。出力は/root/tfa-test/results/fail.txtに、終了コードは/root/tfa-test/results/fail.rcに1行で保存します。そのあと、そのファイルを/root/tfa-test/broken/fail.tftest.hclに移して、テストスイートが再び通過するようにしてください。
  7. /root/tfa-test/tests/mock.tftest.hclにmock_provider "local" {}を置き、env = "prod"でcommand = applyのrun "mocked_apply"で、出力nameがapp-prodかをアサートしてください。このファイルだけに絞って回した出力を、/root/tfa-test/results/mock.txtに保存します。
  8. /root/tfa-test/ci.shを作成してください。自分が置かれたディレクトリに移動して、tofu init -input=falseを回し(失敗したら2で終了)、続けてtofu testを回して、出力を/root/tfa-test/results/ci.txtに残し、テストの終了コードをそのまま返す必要があります。作ったあと、1回回して、0で終了するかを確認してください。

参考

テストを付ける対象を作る

/root/tfa-test/main.tfに、var.env(dev・stage・prodだけを許可するvalidation)と、var.replicas(デフォルト1、1–10だけを許可)を受け取って、out/app-<env>.confにname=とreplicas=の2行を書くlocal_file.confを置き、出力name・conf_path・replicasの3つを宣言してください。initして、tofu validateで確認します。

テストを付けるには、まずテストできる形である必要があります。外から値を入れる場所(変数)と、結果を取り出す場所(出力)があってこそ、アサーションを書けます。validationは、あとでexpect_failuresでテストする対象になります。

プランだけを立てる安価なテスト

/root/tfa-test/tests/basic.tftest.hclに、ファイルレベルのvariablesでenv = "dev"を置き、command = planのrun "name_is_prefixed"で、出力nameがapp-devかをアサートしてください。tofu testを、このファイルだけに絞って回し、出力を/root/tfa-test/results/basic.txtに保存します。

テストファイルは、現在のディレクトリかtestsディレクトリから探されます。runブロックのcommandがplanなら、何も作らず、プランだけを立ててアサートするので速いです。名前のルールや条件分岐のように、値を見ればよいものは、すべてこちらで書きます。

実際に作って確認する、高価なテスト

/root/tfa-test/tests/apply.tftest.hclに、env = "stage"、replicas = 3で、command = applyのrun "file_is_written"を置き、file(output.conf_path)の内容が、正確にname=app-stageとreplicas=3の2行かを、アサートしてください。このファイルだけに絞って回した出力を、/root/tfa-test/results/apply.txtに保存します。

applyテストは、本物を作り、終わるとツールが自分で削除します。そのため、安価ではありませんが、「本当にそのファイルが、その内容でできるのか」は、こうしてしか確認できません。テストが終わったあと、out/を開いて、何が残ったかを見てください。

前のテストの結果を、次のテストが使う

/root/tfa-test/tests/chain.tftest.hclに、command = applyのrun "make_dev"と、そのrunの出力を参照して値を決める、command = planのrun "reuse_previous_output"を置いてください。2つ目のrunは、出力nameがapp-prodかをアサートします。このファイルだけに絞って回した出力を、/root/tfa-test/results/chain.txtに保存してください。

run名で、前のrunの出力を参照できます。実務では、「まずネットワークを作り、そのIDでアプリをプランする」のように、ステップがつながるテストを、こう書きます。ファイルの中のrunは、上から下へ、順番に動きます。

止まるべきものが止まるかをテストする

/root/tfa-test/tests/validation.tftest.hclに、env = "qa"で動くrun "bad_env_is_rejected"と、replicas = 99で動くrun "too_many_replicas_is_rejected"を置き、それぞれexpect_failuresで、該当の変数のvalidationが止めるのが正常であることを宣言してください。このファイルだけに絞って回した出力を、/root/tfa-test/results/validation.txtに保存します。

expect_failuresは、「このrunは、失敗して初めて合格」と逆から言う仕組みです。止める仕組みを作っておいて、本当に止まるかを確認しないと、ある日条件を誤って直しても、誰も気づきません。リストには、失敗すべき対象のアドレスを書きます。

失敗するテストが何を示すかを見る

/root/tfa-test/tests/fail.tftest.hclに、わざと間違ったアサーション(出力nameがapp-nopeか)を置いて、tofu testを回してください。出力は/root/tfa-test/results/fail.txtに、終了コードは/root/tfa-test/results/fail.rcに1行で保存します。そのあと、そのファイルを/root/tfa-test/broken/fail.tftest.hclに移して、テストスイートが再び通過するようにしてください。

失敗メッセージが何を示すかが重要です。アサーションがあった行と、そのとき値が実際に何だったかが、一緒に出ます。そのため、error_messageに値を繰り返し書く必要がありません。終了コードは、CIが読む唯一のシグナルです。

プロバイダーをまねて、値だけをテストする

/root/tfa-test/tests/mock.tftest.hclにmock_provider "local" {}を置き、env = "prod"でcommand = applyのrun "mocked_apply"で、出力nameがapp-prodかをアサートしてください。このファイルだけに絞って回した出力を、/root/tfa-test/results/mock.txtに保存します。

mockプロバイダーは、実際には作らず、もっともらしい値を返します。遅かったりお金がかかったりするプロバイダーをテストするときに、価値が大きいです。ただし、まねなので、「本当にそのファイルができるか」は確認できません。ステップ3の本物のapplyテストとは、役割が違います。

CIが呼び出す1行にまとめる

/root/tfa-test/ci.shを作成してください。自分が置かれたディレクトリに移動して、tofu init -input=falseを回し(失敗したら2で終了)、続けてtofu testを回して、出力を/root/tfa-test/results/ci.txtに残し、テストの終了コードをそのまま返す必要があります。作ったあと、1回回して、0で終了するかを確認してください。

CIが読むシグナルは、終了コード1つだけです。テストが失敗したのに、スクリプトが0で終われば、パイプラインは緑のまま通過します。テストがないより悪いです。採点ツールは、このスクリプトをコピーの中で2回回します。いまそのままのときと、失敗するテストを1つ挟んだときです。