IaC テスト: Terratest、Terraform ネイティブ テスト、契約テスト
Terraform の包括的なテスト戦略: を使用した単体テスト モジュール ネイティブ Terraform テスト フレームワーク (Terraform 1.6 以降の GA)、統合テスト Go の Terratest とモジュール インターフェイスのコントラクト テストを使用します。
IaC テストの問題点
インフラストラクチャのテストは、アプリケーション コードのテストとは根本的に異なります。 Terraform モジュールの単体テストでは、実際のリソースの作成が必要になる場合があります AWS では、お金と時間 (ミリ秒ではなく分単位) がかかります。フィードバックループが長いので、 テストは適切に処理されないと破壊的になる可能性があり、依存関係も クラウド環境での統合テストは避けられません。
これらの課題にもかかわらず、IaC テストは不可欠です: Terraform モジュール 検証されていないのは時限爆弾だ。この記事では、 ピラミッド IaC のテストの数 3 つのレベル: 静的テスト (高速、安価)、テスト terraform テスト (中)、Terratest との統合テスト (低速、詳細)。
IaC のテスト ピラミッド
- レベル 1 - 静的 (秒): fmt、検証、tflint、tfsec/checkov。 AWS のコストはゼロです。
- レベル 2 - ユニット/モック (分): mock_provider を使用した terraform テスト。アセットは作成されませんでした。
- レベル 3 - 統合 (5 ~ 30 分): Terratest: 実際の資産、エンドツーエンドの検証。
Terraform テスト: ネイティブ フレームワーク (1.6 から GA)
Terraform 1.6 以降、フレームワーク terraform test それは一般提供です。
テストファイルには拡張子が付いています .tftest.hcl そしてテストできるようにします
モジュール インターフェイス (入力/出力) には 2 つのモードがあります。 コマンド適用
(実際のリソースを作成する) e 指揮計画 (計画のみ、コストはゼロ)。
# modules/s3-static-website/main.tf - Il modulo da testare
variable "bucket_name" {
type = string
description = "Nome univoco del bucket S3"
}
variable "environment" {
type = string
description = "Ambiente: dev, staging, production"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "environment deve essere dev, staging o production"
}
}
variable "enable_versioning" {
type = bool
default = false
}
resource "aws_s3_bucket" "website" {
bucket = var.bucket_name
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_s3_bucket_versioning" "website" {
count = var.enable_versioning ? 1 : 0
bucket = aws_s3_bucket.website.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document { suffix = "index.html" }
error_document { key = "error.html" }
}
output "bucket_id" {
value = aws_s3_bucket.website.id
}
output "website_endpoint" {
value = aws_s3_bucket_website_configuration.website.website_endpoint
}
# modules/s3-static-website/tests/unit.tftest.hcl
# Test con command = "plan" - zero risorse create, zero costi
variables {
bucket_name = "test-website-bucket-12345"
environment = "dev"
}
# Test 1: configurazione base
run "basic_plan" {
command = plan
assert {
condition = aws_s3_bucket.website.bucket == "test-website-bucket-12345"
error_message = "Il nome del bucket non corrisponde alla variabile bucket_name"
}
assert {
condition = aws_s3_bucket.website.tags["Environment"] == "dev"
error_message = "Il tag Environment non corrisponde alla variabile environment"
}
assert {
condition = length(aws_s3_bucket_versioning.website) == 0
error_message = "Il versioning non dovrebbe essere abilitato per default"
}
}
# Test 2: con versioning abilitato
run "with_versioning" {
command = plan
variables {
enable_versioning = true
}
assert {
condition = length(aws_s3_bucket_versioning.website) == 1
error_message = "Il versioning dovrebbe essere abilitato"
}
}
# Test 3: validazione input - deve fallire con ambiente invalido
run "invalid_environment_fails" {
command = plan
variables {
environment = "qa" # Non valido: deve fallire
}
expect_failures = [var.environment]
}
# Esegui i test
# In locale:
terraform test
# Output atteso:
# Success! All tests passed.
# 3 passed, 0 failed
モックプロバイダー: クラウド認証情報を使用しないテスト
Terraform 1.7 以降、 モックプロバイダー シミュレーションできるようにする 実際の認証情報を持たない AWS/Azure/GCP プロバイダー。計算された値 (ARN、 ID) はモック内で自動的に生成または設定されます。
# modules/s3-static-website/tests/mock.tftest.hcl
# Test con mock provider: nessuna credenziale AWS necessaria
mock_provider "aws" {
# Il provider AWS è simulato: nessuna chiamata API reale
mock_resource "aws_s3_bucket" {
defaults = {
id = "test-website-bucket-12345"
arn = "arn:aws:s3:::test-website-bucket-12345"
bucket_domain_name = "test-website-bucket-12345.s3.amazonaws.com"
bucket_regional_domain_name = "test-website-bucket-12345.s3.eu-west-1.amazonaws.com"
region = "eu-west-1"
}
}
mock_resource "aws_s3_bucket_website_configuration" {
defaults = {
website_endpoint = "test-website-bucket-12345.s3-website-eu-west-1.amazonaws.com"
}
}
}
variables {
bucket_name = "test-website-bucket-12345"
environment = "dev"
}
run "mock_apply_succeeds" {
# command = apply con mock provider: crea risorse simulate
command = apply
assert {
condition = output.bucket_id == "test-website-bucket-12345"
error_message = "output.bucket_id deve corrispondere al mock"
}
assert {
condition = can(regex("s3-website", output.website_endpoint))
error_message = "website_endpoint deve contenere 's3-website'"
}
}
Terratest: Go との統合テスト
テラテスト (github.com/gruntwork-io/terratest) は Go ライブラリです 実際のクラウド リソースを作成し、その動作を検証する統合テストを作成する 完了したら破棄します。これは、複雑な Terraform モジュールをテストするためのゴールドスタンダードです。
# Struttura per Terratest
modules/
s3-static-website/
main.tf
variables.tf
outputs.tf
tests/
unit.tftest.hcl # terraform test
integration_test.go # Terratest
go.mod
go.sum
// modules/s3-static-website/tests/integration_test.go
package test
import (
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestS3WebsiteModuleBasic crea il modulo, verifica il comportamento, poi distrugge
func TestS3WebsiteModuleBasic(t *testing.T) {
t.Parallel() // I test possono girare in parallelo
// ID univoco per evitare conflitti di naming tra test run
uniqueId := random.UniqueId()
bucketName := fmt.Sprintf("terratest-website-%s", uniqueId)
terraformOptions := &terraform.Options{
TerraformDir: "../",
// Variabili passate al modulo
Vars: map[string]interface{}{
"bucket_name": bucketName,
"environment": "dev",
"enable_versioning": false,
},
// Mostra tutti i log Terraform durante i test
// NoColor: true, // Disabilita colori per log CI puliti
}
// defer garantisce la distruzione delle risorse anche se il test fallisce
defer terraform.Destroy(t, terraformOptions)
// Init e Apply
terraform.InitAndApply(t, terraformOptions)
// Leggi gli output
bucketId := terraform.Output(t, terraformOptions, "bucket_id")
websiteEndpoint := terraform.Output(t, terraformOptions, "website_endpoint")
// Assertions sugli output
assert.Equal(t, bucketName, bucketId, "bucket_id deve essere uguale al bucket_name")
assert.Contains(t, websiteEndpoint, "s3-website", "website_endpoint deve essere una URL website S3")
// Verifica diretta con AWS SDK che il bucket esista e sia configurato correttamente
sess, err := session.NewSession(&aws.Config{Region: aws.String("eu-west-1")})
require.NoError(t, err)
s3Client := s3.New(sess)
// Verifica che il bucket esista
_, err = s3Client.HeadBucket(&s3.HeadBucketInput{
Bucket: aws.String(bucketName),
})
assert.NoError(t, err, "Il bucket deve esistere in AWS")
// Verifica che il website hosting sia abilitato
websiteOutput, err := s3Client.GetBucketWebsite(&s3.GetBucketWebsiteInput{
Bucket: aws.String(bucketName),
})
require.NoError(t, err)
assert.Equal(t, "index.html", *websiteOutput.IndexDocument.Suffix)
assert.Equal(t, "error.html", *websiteOutput.ErrorDocument.Key)
}
// TestS3WebsiteModuleVersioning testa il versioning
func TestS3WebsiteModuleVersioning(t *testing.T) {
t.Parallel()
uniqueId := random.UniqueId()
bucketName := fmt.Sprintf("terratest-versioned-%s", uniqueId)
terraformOptions := &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"bucket_name": bucketName,
"environment": "dev",
"enable_versioning": true,
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Verifica versioning con AWS SDK
sess, _ := session.NewSession(&aws.Config{Region: aws.String("eu-west-1")})
s3Client := s3.New(sess)
versioningOutput, err := s3Client.GetBucketVersioning(&s3.GetBucketVersioningInput{
Bucket: aws.String(bucketName),
})
require.NoError(t, err)
assert.Equal(t, "Enabled", *versioningOutput.Status)
}
// TestS3WebsiteIdempotent verifica che apply due volte non cambi nulla
func TestS3WebsiteIdempotent(t *testing.T) {
t.Parallel()
uniqueId := random.UniqueId()
bucketName := fmt.Sprintf("terratest-idempotent-%s", uniqueId)
terraformOptions := &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"bucket_name": bucketName,
"environment": "dev",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Secondo apply: non deve cambiare nulla (0 adds, 0 changes, 0 destroys)
exitCode := terraform.PlanExitCode(t, terraformOptions)
assert.Equal(t, 0, exitCode, "Secondo plan deve avere exit code 0 (no changes)")
}
契約テスト: モジュールインターフェイスの検証
Il 契約テスト Terraform モジュールの場合、 契約 モジュール (入力および出力) は消費者の期待に応えます。に役立ちます 変更によって消費者に影響が及ばないようにするための共有モジュール リポジトリ。
# modules/s3-static-website/tests/contract.tftest.hcl
# Contract test: verifica che l'interfaccia del modulo rispetti il contratto
mock_provider "aws" {}
# Contract: output.bucket_id deve sempre essere una stringa non vuota
run "contract_bucket_id_not_empty" {
command = apply
variables {
bucket_name = "contract-test-bucket"
environment = "dev"
}
assert {
condition = length(output.bucket_id) > 0
error_message = "CONTRATTO VIOLATO: output.bucket_id non deve essere vuoto"
}
}
# Contract: output.website_endpoint deve essere un URL valido
run "contract_website_endpoint_format" {
command = apply
variables {
bucket_name = "contract-test-bucket"
environment = "dev"
}
assert {
condition = can(regex("^[a-z0-9-]+\\.s3-website[-.]", output.website_endpoint))
error_message = "CONTRATTO VIOLATO: website_endpoint deve essere nel formato S3 website URL"
}
}
# Contract: il modulo NON deve mai creare risorse con tag Environment mancante
run "contract_environment_tag_required" {
command = plan
variables {
bucket_name = "contract-test-bucket"
environment = "staging"
}
assert {
condition = aws_s3_bucket.website.tags["Environment"] != null
error_message = "CONTRATTO VIOLATO: il tag Environment deve sempre essere presente"
}
}
CI との統合: テスト用の GitHub アクション
# .github/workflows/module-test.yml
name: Test Terraform Modules
on:
pull_request:
paths:
- 'modules/**'
permissions:
id-token: write
contents: read
jobs:
# Livello 1: Test statici (0 costi, secondi)
static:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.7.5'
- run: find modules -name "*.tf" -exec terraform fmt -check {} \;
- run: |
for dir in modules/*/; do
cd "$dir"
terraform init -backend=false
terraform validate
cd -
done
# Livello 2: terraform test con mock providers (0 costi AWS, minuti)
unit-test:
name: Unit Tests (terraform test)
runs-on: ubuntu-latest
needs: static
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.7.5'
- name: Run terraform test for each module
run: |
for dir in modules/*/; do
if ls "$dir"tests/*.tftest.hcl 2>/dev/null; then
echo "Testing module: $dir"
cd "$dir"
terraform init -backend=false
terraform test -filter=mock.tftest.hcl -filter=unit.tftest.hcl
cd -
fi
done
# Livello 3: Terratest (crea risorse AWS reali, 5-30 minuti)
integration-test:
name: Integration Tests (Terratest)
runs-on: ubuntu-latest
needs: unit-test
# Esegui solo su PR verso main, non per ogni push
if: github.base_ref == 'main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/TerratestRole
aws-region: eu-west-1
- name: Run Terratest
run: |
cd modules/s3-static-website/tests
go test -v -timeout 30m -run "TestS3Website"
env:
TF_VAR_environment: dev
統合テスト時間を最適化する
テストのコストと時間を削減する戦略
- t.Parallel(): Terratest テストを並行して実行して、 合計時間。それぞれ 5 分のテストを 10 回行う場合、並行して行うには 5 分で十分です。 50の代わりに。
- 経済地域を使用します。 us-east-1 は最も多くの AWS リージョンです 経済的で、利用可能なサービスが最も豊富です。
-
積極的なクリーンアップ: アメリカ合衆国
defer terraform.Destroy()オプションを作成した後の最初の操作として、常に たとえパニックが起こったとしても破壊します。 -
変更されたモジュールのみをテストします。 アメリカ合衆国
pathsGitHub アクション内 PR で実際に変更されたモジュールに対してのみテストを実行します。 -
コマンド = 計画を優先します: ほとんどのテストでは
ロジック、
command = planterraform テストではこれで十分であり、 リソースは作成されません。
チームのための包括的なテスト戦略
| レベル | 楽器 | どこに転ぶのか | 間隔 | AWSのコスト |
|---|---|---|---|---|
| 静的 | fmt、検証、tflint | あらゆるPR、ローカル | 10~30代 | $0 |
| 安全 | チェコフ、トリビー | あらゆるPR | 30~60代 | $0 |
| 単位(予定) | terraform テスト計画 | あらゆるPR | 1~3分 | $0 |
| ユニット(模擬) | terraform テスト - モック | あらゆるPR | 1~5分 | $0 |
| 統合 | テラテスト | 広報→メイン | 10~30分 | $0.10-2.00 |
| 契約 | terraform テスト - モック | 各PRモジュール | 2~5分 | $0 |
結論と次のステップ
3 層の IaC テスト戦略 (静的、ユニット/モック、統合)
フィードバック速度と検証の深さのバランスを取る。新しい
フレームワーク terraform test ネイティブなので基本的なテストにアクセスできる
外部依存関係がありませんが、Terratest は依然として最適なツールです。
インフラストラクチャの実際の動作を検証します。
シリーズの次の記事
- 第6条: IaC セキュリティ — Checkov、Trivy、OPA コードとしてのポリシー: 事前適用ゲートでの自動セキュリティ スキャンと組織ポリシー。
- 第7条: Terraform マルチクラウド — AWS + Azure + GCP を使用 共有モジュール: 抽象化レイヤーと複数のプロバイダー管理。







