はじめに
IaC にもテストを入れたいと思いつつ、「テスト=実際にリソースを作って確認」だと思っていて、課金が怖くてずっと後回しにしていました。
でも Terraform 標準の terraform test は、command = plan を指定すれば実リソースを作らずに設定を検証できます。実リソースを作らないので、課金リスクをかなり抑えてテストが書けました。
先に結論:terraform test は既定だと apply(実際に作る)ですが、各テストに command = plan を書けばプランの計算だけで評価され、リソースは作られません。
「ほぼ $0」と書いているのは、plan でも provider の認証や data source 参照で AWS の API 呼び出しは発生するためです。通常は大きな課金要因ではありませんが、「絶対に 0 円」と言い切らずにおきます。
環境
- Terraform(ネイティブの
terraform test/.tftest.hcl) - 対象は AWS 向けの構成(Lambda・S3・API Gateway・CloudFront など)
- リージョン: ap-northeast-1(東京)
きっかけ:直したものが、また戻らないように守りたい
直前に「Lambda のランタイムが古くなる」対応をして、現行の nodejs22.x に上げたばかりでした。こういう修正は、あとで誰か(未来の自分含む)がうっかり戻すと困ります。
そこで「この構成ではランタイムに nodejs22.x を使い続ける」をテストで固定することにしました。これが最初の1本です。
# tests/serverless.tftest.hcl
run "lambda_uses_supported_runtime" {
command = plan # ← 実リソースを作らない
module {
source = "./modules/lambda" # ← モジュール単体を検証
}
variables {
project = "aws-study"
function_name = "hello"
}
assert {
condition = aws_lambda_function.this.runtime == "nodejs22.x"
error_message = "Lambda runtime は、この構成の標準である nodejs22.x を使う"
}
}
terraform test で実行すると、これが通ります。何も作らずに「ランタイムは想定どおりか?」だけを確認できました。
どのランタイムが現行/非推奨かは時期で変わります。実際に固定する版は、公開時点の AWS Lambda の対応ランタイム表(参考リンク)で確認してください。
なお、module { source = "./modules/lambda" } の相対パスは、テストを実行する構成ディレクトリ(terraform test を叩く場所)を基準に解決されます。私はそこを基準に確認しました。
plan と apply の違い(ここが安く済む肝)
公式ドキュメントによると、run ブロックの command は**省略すると既定は apply**で、実際にリソースを作って検証します。command = plan を指定すると、プランの計算結果に対してアサートするので、リソースは作られません。
command = apply (既定)… 実際に作る → 課金・後片付けが要る
command = plan … 作らない → ほぼ $0
plan テストで向いていること / apply が要ること
全部 plan で済むわけではないので、最初に切り分けておくと迷いません。
plan テストで向いていること
- 変数の validation が効くこと
- IAM ポリシーや公開設定など、設定値の確認
- ランタイム・タグ・命名規則の固定
- モジュールの入力と出力の確認
apply テストが必要になりやすいこと
- 実際の AWS サービス連携
- Lambda の実行結果
- API Gateway 経由の疎通
- CloudFront 配信後の挙動
「設定が意図どおりか」を確かめたいだけなら plan、「実際に動くか」を見たいなら apply、という住み分けです。
いろいろな検証を plan で書く
モジュール内部を検証する(module {})
「S3 は公開を全ブロックしているか」。
run "s3_blocks_all_public_access" {
command = plan
module { source = "./modules/static_site" }
variables {
project = "aws-study"
suffix = "test"
}
assert {
condition = (
aws_s3_bucket_public_access_block.this.block_public_acls &&
aws_s3_bucket_public_access_block.this.block_public_policy &&
aws_s3_bucket_public_access_block.this.ignore_public_acls &&
aws_s3_bucket_public_access_block.this.restrict_public_buckets
)
error_message = "S3 バケットは公開を全ブロックすべき"
}
}
「不正な入力はちゃんと弾かれる」を検証する(expect_failures)
正しく動くことだけでなく、「ダメな入力をちゃんと拒否する」のもテストできます。SSH 元 IP に全開放(0.0.0.0/0)を渡したら、variable の validation で弾かれてほしい。
run "rejects_wide_open_my_ip" {
command = plan
variables {
my_ip = "0.0.0.0/0"
}
expect_failures = [
var.my_ip, # ← この変数の検証が失敗することを“期待”する
]
}
expect_failures は「ここは失敗するのが正しい」という宣言です。逆に、正しい値(203.0.113.10/32)なら通ることも、別の run で確認しています。
こうして、ランタイム・S3 公開ブロック・API Gateway の構成・CloudFront の既定ルート・入力バリデーションなどを plan のまま 8 本書いて、全部 pass しました。
ハマったところ:依存を足したら init し直し
途中で、テストに新しい module { source = "..." } を追加したら、「初期化されていない」と怒られました。テストが参照するモジュールやプロバイダは、実行前に取得されている必要があります。新しいモジュールや provider の依存を追加・変更したら、terraform init を再実行して依存を取得・更新してから terraform test を回す、で解決しました。
学び
-
terraform testは既定が apply。command = planを書けば実リソースを作らず、ほぼ $0。 - 「設定値が意図どおりか」は plan、「実際に動くか」は apply、と住み分ける。
-
module {}でモジュール単体を、assertで値を、expect_failuresで「不正入力を弾くこと」を検証できる。 - 依存(モジュール/provider)を足したら
terraform initを忘れずに。 - 「戻したくない修正」は、テストで固定しておくと安心。
おわりに
「テスト=お金がかかる」という思い込みで避けていたのが、plan モードを知って一気に書けるようになりました😊 まずは1本、「この値はこうあってほしい」を plan で固定するところから始めると、とっつきやすいと思います🙌
参考
- Terraform 公式: Tests(
.tftest.hcl/run/command/expect_failures)
https://developer.hashicorp.com/terraform/language/tests - Terraform 公式:
terraform testコマンド
https://developer.hashicorp.com/terraform/cli/commands/test - AWS公式: Lambda ランタイム(対応・非推奨スケジュール。公開時点で要確認)
https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html