terraform-test について
terraform-test は、Terraform の組み込みテストフレームワークで設定の動作や変更を確認するためのスキルです。Terraform 設定の保守担当者向けに、plan・apply テスト、アサーション、モックプロバイダー、CI/CD での実行方法を扱います。
主な機能
- 01plan と apply のテストシナリオ
- 02設定確認用アサーション
- 03モックプロバイダーの指針
- 04変数と失敗のテストパターン
- 05テストの絞り込みとクリーンアップ
- 06CI/CD の参考情報
活用シーン
- 01モジュールの出力と条件の確認
- 02無効な入力による検証ルールのテスト
- 03高速な単体テストと統合テストの分離
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ hashicorp/ agent-skills --skill terraform-testインストール後、Claude Code で/terraform-testを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/CI_CD.mdEXAMPLES.mdMOCK_PROVIDERS.md詳細
Terraform テスト
Terraform に組み込まれたテストフレームワークは、設定の更新によって破壊的変更が生じないことを検証します。テストは一時的なリソースに対して実行され、既存のインフラストラクチャとステートファイルを保護します。
リファレンスファイル
references/MOCK_PROVIDERS.md— モックプロバイダーの構文、一般的なデフォルト、モックを使用するタイミング(Terraform 1.7.0 以降のみ — ユーザーのバージョンが 1.7 未満の場合は読み飛ばしてください)references/CI_CD.md— GitHub Actions と GitLab CI のパイプライン例references/EXAMPLES.md— VPC モジュール用の完全なテストスイートの例(ユニットテスト、統合テスト、モックテスト)
ユーザーがモックや CI/CD 統合について質問した場合、または完全な例を求めた場合は、該当するリファレンスファイルを読みます。
主な概念
- テストファイル(
.tftest.hcl/.tftest.json):設定を検証するrunブロックを含みます - Run ブロック:オプションの変数、プロバイダー、アサーションを含む単一のテストシナリオです
- Assert ブロック:テストに合格するために満たす必要がある条件です
- モックプロバイダー:実際のインフラストラクチャを使わずにプロバイダーの動作をシミュレートします(Terraform 1.7.0 以降)
- テストモード:
apply(デフォルト。実際のリソースを作成)またはplan(ロジックのみを検証)
ファイル構成
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── defaults_unit_test.tftest.hcl # plan mode — fast, no resources
├── validation_unit_test.tftest.hcl # plan mode
└── full_stack_integration_test.tftest.hcl # apply mode — creates real resources
CI で個別にフィルタリングできるように、プランモードのテストには *_unit_test.tftest.hcl を、apply モードのテストには *_integration_test.tftest.hcl を使用します。
テストファイルの構成
# Optional: test-wide settings
test {
parallel = true # Enable parallel execution for all run blocks (default: false)
}
# Optional: file-level variables (highest precedence, override all other sources)
variables {
aws_region = "us-west-2"
instance_type = "t2.micro"
}
# Optional: provider configuration
provider "aws" {
region = var.aws_region
}
# Required: at least one run block
run "test_default_configuration" {
command = plan
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Instance type should be t2.micro by default"
}
}
Run ブロック
run "test_name" {
command = plan # or apply (default)
parallel = true # optional, since v1.9.0
# Override file-level variables
variables {
instance_type = "t3.large"
}
# Reference a specific module
module {
source = "./modules/vpc" # local or registry only (not git/http)
version = "5.0.0" # registry modules only
}
# Control state isolation
state_key = "shared_state" # since v1.9.0
# Plan behavior
plan_options {
mode = refresh-only # or normal (default)
refresh = true
replace = [aws_instance.example]
target = [aws_instance.example]
}
# Assertions
assert {
condition = aws_instance.example.id != ""
error_message = "Instance should have a valid ID"
}
# Expected failures (test passes if these fail)
expect_failures = [
var.instance_count
]
}
一般的なテストパターン
出力を検証する
run "test_outputs" {
command = plan
assert {
condition = output.vpc_id != null
error_message = "VPC ID output must be defined"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID should start with 'vpc-'"
}
}
条件付きリソース
run "test_nat_gateway_disabled" {
command = plan
variables {
create_nat_gateway = false
}
assert {
condition = length(aws_nat_gateway.main) == 0
error_message = "NAT gateway should not be created when disabled"
}
}
リソース数
run "test_resource_count" {
command = plan
variables {
instance_count = 3
}
assert {
condition = length(aws_instance.workers) == 3
error_message = "Should create exactly 3 worker instances"
}
}
タグ
run "test_resource_tags" {
command = plan
variables {
common_tags = {
Environment = "production"
ManagedBy = "Terraform"
}
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "Environment tag should be set correctly"
}
assert {
condition = aws_instance.example.tags["ManagedBy"] == "Terraform"
error_message = "ManagedBy tag should be set correctly"
}
}
データソース
run "test_data_source_lookup" {
command = plan
assert {
condition = data.aws_ami.ubuntu.id != ""
error_message = "Should find a valid Ubuntu AMI"
}
assert {
condition = can(regex("^ami-", data.aws_ami.ubuntu.id))
error_message = "AMI ID should be in correct format"
}
}
検証ルール
run "test_invalid_environment" {
command = plan
variables {
environment = "invalid"
}
expect_failures = [
var.environment
]
}
依存関係のある順次テスト
run "setup_vpc" {
command = apply
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
run "test_subnet_in_vpc" {
command = plan
variables {
vpc_id = run.setup_vpc.vpc_id
}
assert {
condition = aws_subnet.example.vpc_id == run.setup_vpc.vpc_id
error_message = "Subnet should be in the VPC from setup_vpc"
}
}
プランのオプション(refresh-only、targeted)
run "test_refresh_only" {
command = plan
plan_options {
mode = refresh-only
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "Tags should be refreshed correctly"
}
}
run "test_specific_resource" {
command = plan
plan_options {
target = [aws_instance.example]
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Targeted resource should be planned"
}
}
並列モジュール
run "test_networking_module" {
command = plan
parallel = true
module {
source = "./modules/networking"
}
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
run "test_compute_module" {
command = plan
parallel = true
module {
source = "./modules/compute"
}
assert {
condition = output.instance_id != ""
error_message = "Instance should be created"
}
}
ステートキーの共有
run "create_foundation" {
command = apply
state_key = "foundation"
assert {
condition = aws_vpc.main.id != ""
error_message = "Foundation VPC should be created"
}
}
run "create_application" {
command = apply
state_key = "foundation"
variables {
vpc_id = run.create_foundation.vpc_id
}
assert {
condition = aws_instance.app.vpc_id == run.create_foundation.vpc_id
error_message = "Application should use foundation VPC"
}
}
クリーンアップの順序(バケットより先に S3 オブジェクト)
run "create_bucket" {
command = apply
assert {
condition = aws_s3_bucket.example.id != ""
error_message = "Bucket should be created"
}
}
run "add_objects" {
command = apply
assert {
condition = length(aws_s3_object.files) > 0
error_message = "Objects should be added"
}
}
# Cleanup destroys in reverse: objects first, then bucket
複数のエイリアス付きプロバイダー
provider "aws" {
alias = "primary"
region = "us-west-2"
}
provider "aws" {
alias = "secondary"
region = "us-east-1"
}
run "test_with_specific_provider" {
command = plan
providers = {
aws = provider.aws.secondary
}
assert {
condition = aws_instance.example.availability_zone == "us-east-1a"
error_message = "Instance should be in us-east-1 region"
}
}
複雑な条件
assert {
condition = alltrue([
for subnet in aws_subnet.private :
can(regex("^10\\.0\\.", subnet.cidr_block))
])
error_message = "All private subnets should use 10.0.0.0/8 CIDR range"
}
クリーンアップ
テスト完了後、リソースは Run ブロックの順序と逆の順序 で破棄されます。依存関係がある場合(例:バケットより先に S3 オブジェクトを破棄する場合)は、この点が重要です。デバッグのためにクリーンアップをスキップするには、terraform test -no-cleanup を使用します。
テストの実行
terraform test # all tests
terraform test tests/defaults.tftest.hcl # specific file
terraform test -filter=test_vpc_configuration # by run block name
terraform test -test-directory=integration-tests # custom directory
terraform test -verbose # detailed output
terraform test -no-cleanup # skip resource cleanup
ベストプラクティス
- 命名:プランモードには
*_unit_test.tftest.hcl、apply モードには*_integration_test.tftest.hclを使用します - テストの命名:テストするシナリオを説明する、わかりやすい名前を Run ブロックに付けます
- デフォルトは plan:実際のリソースの動作をテストする必要がない限り、
command = planを使用します - 外部依存関係には モックを使用 します。実行が速く、認証情報も不要です(
references/MOCK_PROVIDERS.mdを参照) - エラーメッセージ:テストを再実行しなくても失敗の原因を診断できるよう、具体的な内容にします
- 否定テスト:
expect_failuresを使って、検証ルールが不正な入力を拒否することを確認します - 変数の網羅性:さまざまな変数の組み合わせをテストして、すべてのコードパスを検証します。テスト変数は最も優先順位が高く、他のすべての値の取得元を上書きします
- モジュールソース:テストファイルでサポートされるのはローカルパスとレジストリモジュールのみです。git または HTTP の URL はサポートされません
- 並列実行:ステートファイルが異なる独立したテストには
parallel = trueを使用します - クリーンアップ:統合テストでは、リソースは Run ブロックの順序と逆の順序で自動的に破棄されます。デバッグには
-no-cleanupを使用します - CI/CD:ユニットテストはすべての PR で実行し、統合テストはマージ時に実行します(
references/CI_CD.mdを参照)
トラブルシューティング
| 問題 | 解決策 |
|---|---|
| アサーションの失敗 | -verbose を使用して、実際の値と期待値を確認します |
| 認証情報がない | ユニットテストではモックプロバイダーを使用します |
| サポートされていないモジュールソース | git/HTTP のソースをローカルモジュールに変換します |
| テストが干渉する | 分離するために state_key または別々のモジュールを使用します |
| テストが遅い | command = plan とモックを使用し、統合テストは別途実行します |
参考資料
コメント
Markdown 対応