チームでTerraformモジュールを共有していると、誰かの変更が別のモジュールを静かに壊すリグレッションが避けられません。手動で確認するのには限界があり、本番環境への適用後に発覚するケースも珍しくないのが現実です。
Terraform 1.6から、外部ライブラリなしで使える公式のネイティブテスト機能が正式リリースされました。
.tftest.hclファイルを作成してterraform testコマンドを実行するだけで、モジュールの入出力やリソース属性を自動的に検証できます。この記事では、terraform testの基本構文と実行方法を押さえたうえで、CI/CDへの組み込みやtflintとの役割分担まで実践的に解説します。前提バージョンはTerraform 1.6以降です。
この記事のポイント
・terraform testはTerraform 1.6から正式搭載されたネイティブテスト機能
・.tftest.hclにrunブロックとassertを書くだけでモジュール検証ができる
・tflintの静的解析とterraform testの動的テストは役割が異なる二軸
・GitHub Actionsへの組み込みでリグレッションを自動防止できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
terraform testとは何か?1.6以前との違い
Terraformのテスト手法は、バージョンによって大きく変わってきました。1.6以前は、モジュールのテストにTerraformの標準機能は存在しませんでした。GoのライブラリであるTerratest(HashiCorp外のOSSプロジェクト)や、Conftest(OPA/Regoポリシーで静的検査)を使うのが主流でしたが、どちらも学習コストが高く、Terraformとは別の言語・ツールチェーンが必要でした。
Terraform 1.5でアルファ版として導入された
terraform testコマンドは、1.6で安定版(GA)になりました。特徴をまとめると次のとおりです。・HCLで書ける:テストコードにGoやPythonが不要。Terraformエンジニアがそのまま書ける
・実際のプランと適用を実行する:静的解析ではなく、実際にplan/applyを走らせて結果を検証する
・assertブロックで条件チェック:outputの値やリソース属性を条件式で検証し、失敗時にエラーメッセージを表示する
・独立したstate管理:テスト用に別stateが自動作成され、テスト終了後にdestroyされる
「テストのためだけにGoを覚えなければならない」という壁がなくなったのは、現場での採用ハードルを大きく下げています。
基本構文と動かし方
1. テストファイルを作る
テストファイルの拡張子は.tftest.hclです。モジュールのルートディレクトリにtests/ディレクトリを作り、その中に配置するのが慣例です。# ディレクトリ構成例 modules/ s3_bucket/ main.tf variables.tf outputs.tf tests/ main.tftest.hcl # テストファイル
terraform testコマンドは、実行ディレクトリ内のtests/フォルダと*.tftest.hclファイルを自動で検索します。明示的にパスを指定する場合は-filterオプションを使います。2. runブロックとassertブロックを書く
テストケースはrunブロックで定義します。runブロックの中にassertブロックを置いて条件を記述します。# tests/main.tftest.hcl # テスト用変数の設定 variables { bucket_name_prefix = "test-app" environment = "stg" } run "s3_bucket_name_check" { command = plan # plan のみ実行(実リソースは作成しない) assert { condition = output.bucket_name == "test-app-stg" error_message = "バケット名が期待値と一致しません: ${output.bucket_name}" } } run "s3_versioning_enabled" { command = apply # 実際に apply して属性を確認 assert { condition = aws_s3_bucket_versioning.this.versioning_configuration[0].status == "Enabled" error_message = "バージョニングが有効になっていません" } }
command = planにするとterraform planの結果を検証し、実際のリソースは作成されません。command = applyにすると実際にリソースを作成してからassertを評価します。コストを抑えたい場合はplanで済む範囲はplanに留めるのが実務上の鉄則です。3. terraform testを実行する
テストの実行はterraform testコマンドのみです。通常のterraform init完了後に実行できます。$ cd modules/s3_bucket $ terraform init $ terraform test tests/main.tftest.hcl... in progress run "s3_bucket_name_check"... pass run "s3_versioning_enabled"... pass tests/main.tftest.hcl... timed out Success! 2 passed, 0 failed.
error_messageに設定したメッセージとともにどのassertが失敗したかが明示されます。tests/main.tftest.hcl... in progress run "s3_bucket_name_check"... fail Failure! 0 passed, 1 failed. --- Failure in run "s3_bucket_name_check" in tests/main.tftest.hcl Error: Test assertion failed on tests/main.tftest.hcl line 15, in run "s3_bucket_name_check": 15: condition = output.bucket_name == "test-app-stg" バケット名が期待値と一致しません: my-app-stg
モジュールテストの実践パターン
1. 入力変数のバリデーションをテストで確認する
variableブロックにvalidationを書いて入力制約を設けている場合、その制約が正しく機能するかをterraform testで確認できます。# variables.tf(テスト対象モジュール側) variable "environment" { type = string description = "デプロイ環境(prod/stg/dev のみ許可)" validation { condition = contains(["prod", "stg", "dev"], var.environment) error_message = "environment は prod / stg / dev のいずれかを指定してください。" } }
# tests/validation.tftest.hcl run "invalid_environment_should_fail" { command = plan variables { environment = "production" # 無効な値 } expect_failures = [ var.environment, ] }
expect_failuresに失敗を期待する変数・リソースを指定することで、「この値を渡したらエラーになるはず」という否定ケースのテストも書けます。バリデーション漏れを防ぐ重要な検証パターンです。2. outputの値をassertで確認する
モジュールが返すoutputが仕様どおりかを確認するのが、最も基本的なテストパターンです。# tests/outputs.tftest.hcl variables { bucket_name_prefix = "myapp" environment = "dev" } run "check_bucket_arn_format" { command = plan assert { condition = startswith(output.bucket_arn, "arn:aws:s3:::") error_message = "バケットARNのフォーマットが不正です: ${output.bucket_arn}" } assert { condition = output.bucket_name == "myapp-dev" error_message = "バケット名が命名規則に従っていません: ${output.bucket_name}" } }
runブロックに複数のassertを並べることができます。論理的にひとまとまりの検証はまとめておくと、失敗時の原因特定が楽になります。3. Terraform 1.7以降のmock_providerを活用する
Terraform 1.7からmock_providerブロックが追加されました。プロバイダーをモック化することで、実際のAWSリソースを一切作成せずにテストを実行できます。# tests/mock.tftest.hcl mock_provider "aws" { mock_resource "aws_s3_bucket" { defaults = { arn = "arn:aws:s3:::mock-bucket" region = "ap-northeast-1" } } } run "name_check_with_mock" { command = plan assert { condition = output.bucket_name == "myapp-dev" error_message = "バケット名が期待値と異なります: ${output.bucket_name}" } }
command = planとの組み合わせで「ロジックのテスト」に特化するのが実用的な使い方です。なお、
mock_providerはTerraform 1.7以降の機能です。本記事のrunブロック・assertブロック・expect_failuresはTerraform 1.6以降で使用できます。バージョンはterraform versionコマンドで確認してください。
>> Terraform実践セミナーの詳細はこちら
tflintとの使い分け — 静的解析と動的テストの二軸
terraform testと混同されがちなのが、Terraformの静的解析ツールであるtflintです。両者は役割が根本的に異なります。| ツール | 実行タイミング | 確認できること | 実リソース作成 |
|---|---|---|---|
| tflint | plan前(コード解析) | 命名規則違反・非推奨引数・型ミス | なし |
| terraform test(plan) | plan時 | planの出力値・output値の正確さ | なし |
| terraform test(apply) | apply時 | 実際のリソース属性・APIの応答 | あり(テスト後destroy) |
実務では、CI/CDパイプラインに両方を組み込むのが理想的です。tflintでコード品質を担保しながら、terraform testでモジュールの契約(インターフェース)が壊れていないことを確認します。
CI/CDパイプラインへの組み込み
1. GitHub Actionsに組み込む
# .github/workflows/terraform-test.yml name: Terraform Test on: pull_request: paths: - 'modules/**' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.9.0" - name: Terraform Init run: terraform init working-directory: modules/s3_bucket - name: Terraform Test run: terraform test working-directory: modules/s3_bucket env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: ap-northeast-1
pathsフィルターでmodules/**に変更があったプルリクエストのみテストを実行するのがポイントです。全ブランチの全コミットでapplyテストを走らせると、クラウドコストが膨らみます。2. mock_providerを使うとシークレット不要になる
Terraform 1.7以降でmock_providerを使うケースでは、AWSのクレデンシャルがなくてもテストを実行できます。- name: Terraform Test (mock) run: terraform test -filter=tests/mock.tftest.hcl working-directory: modules/s3_bucket # AWS クレデンシャル不要(mock_provider 使用時)
-filterオプションで特定のテストファイルだけを実行できます。mockテストと実リソーステストを分離しておくと、CI/CDの段階ごとに使い分けられます。よくあるエラーと対処法
「No test files」と表示される
terraform testを実行したがテストファイルが見つからないパターンです。$ terraform test No test files
・
.tftest.hclの拡張子が間違っている(例:.hclや.tftestのみ)・テストファイルが
tests/ディレクトリ外に置かれている・コマンドを実行しているディレクトリがモジュールのルートでない
拡張子は必ず
.tftest.hclにし、tests/ディレクトリはモジュールのルートディレクトリに置くことを確認してください。「Reference to undeclared output value」が出る
assertブロックで参照しているoutput名がモジュールのoutputs.tfに定義されていない場合に発生します。Error: Reference to undeclared output value on tests/main.tftest.hcl line 15, in run "check_name": 15: condition = output.bucket_id == "..." An output value with the name "bucket_id" has not been declared.
outputs.tfを確認し、テストで参照したいoutputが定義されているかを確認します。モジュールのリファクタリング後にoutput名が変わった場合、テストコードも同時に更新する必要があります。これ自体がterraform testの有用性を示す典型例です。applyテスト後にリソースが残る
command = applyのテストでエラーが発生した場合、destroyフェーズが走らずにリソースが残ることがあります。# テスト用stateファイルの確認 $ ls tests/*.tfstate # テスト用リソースを手動でdestroy $ terraform destroy -state=tests/terraform.tfstate
本記事のまとめ
| やりたいこと | terraform testの書き方 |
|---|---|
| planの出力値を検証する | run "name" { command = plan; assert { ... } } |
| 実リソースの属性を確認する | run "name" { command = apply; assert { ... } } |
| 変数バリデーションの失敗を確認する | run "name" { expect_failures = [var.xxx] } |
| 実リソースなしでテストする | mock_provider "aws" { ... }(Terraform 1.7以降) |
| 特定のテストファイルだけ実行する | terraform test -filter=tests/xxx.tftest.hcl |
.tftest.hclファイルを用意するだけで、モジュールの契約をHCLで表現できます。tflintが「コードの書き方」を静的に検査するのに対して、terraform testは「コードの動作」を動的に検証する、という二軸の品質保証体制を整えることが、チームでのTerraform運用の安定性につながります。CI/CDに組み込んでプルリクエストのたびにテストが走るようにしておけば、「前は動いていたのに」というリグレッションを未然に防げます。まずは小さなモジュールに1本テストを書いてみるところから始めてみてください。
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます