mcpField
T
terraform-test

terraform-test

github.com/hashicorpテスト・品質9098.8k 回インストール今日更新
リポジトリ

terraform-test について

terraform-test は、Terraform の組み込みテストフレームワークで設定の動作や変更を確認するためのスキルです。Terraform 設定の保守担当者向けに、plan・apply テスト、アサーション、モックプロバイダー、CI/CD での実行方法を扱います。

主な機能

  1. 01plan と apply のテストシナリオ
  2. 02設定確認用アサーション
  3. 03モックプロバイダーの指針
  4. 04変数と失敗のテストパターン
  5. 05テストの絞り込みとクリーンアップ
  6. 06CI/CD の参考情報

活用シーン

  1. 01モジュールの出力と条件の確認
  2. 02無効な入力による検証ルールのテスト
  3. 03高速な単体テストと統合テストの分離

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/hashicorp/agent-skills --skill terraform-test

インストール後、Claude Code で/terraform-testを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
CI_CD.md
EXAMPLES.md
MOCK_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

ベストプラクティス

  1. 命名:プランモードには *_unit_test.tftest.hcl、apply モードには *_integration_test.tftest.hcl を使用します
  2. テストの命名:テストするシナリオを説明する、わかりやすい名前を Run ブロックに付けます
  3. デフォルトは plan:実際のリソースの動作をテストする必要がない限り、command = plan を使用します
  4. 外部依存関係には モックを使用 します。実行が速く、認証情報も不要です(references/MOCK_PROVIDERS.md を参照)
  5. エラーメッセージ:テストを再実行しなくても失敗の原因を診断できるよう、具体的な内容にします
  6. 否定テスト:expect_failures を使って、検証ルールが不正な入力を拒否することを確認します
  7. 変数の網羅性:さまざまな変数の組み合わせをテストして、すべてのコードパスを検証します。テスト変数は最も優先順位が高く、他のすべての値の取得元を上書きします
  8. モジュールソース:テストファイルでサポートされるのはローカルパスとレジストリモジュールのみです。git または HTTP の URL はサポートされません
  9. 並列実行:ステートファイルが異なる独立したテストには parallel = true を使用します
  10. クリーンアップ:統合テストでは、リソースは Run ブロックの順序と逆の順序で自動的に破棄されます。デバッグには -no-cleanup を使用します
  11. CI/CD:ユニットテストはすべての PR で実行し、統合テストはマージ時に実行します(references/CI_CD.md を参照)

トラブルシューティング

問題解決策
アサーションの失敗-verbose を使用して、実際の値と期待値を確認します
認証情報がないユニットテストではモックプロバイダーを使用します
サポートされていないモジュールソースgit/HTTP のソースをローカルモジュールに変換します
テストが干渉する分離するために state_key または別々のモジュールを使用します
テストが遅いcommand = plan とモックを使用し、統合テストは別途実行します

参考資料

コメント