IaC Testi Sorunu

Altyapıyı test etmek, uygulama kodunu test etmekten temel olarak farklıdır. Bir Terraform modülünün birim testi, kaynakların fiili olarak oluşturulmasını gerektirebilir AWS, paraya ve zamana mal olur (milisaniye değil, dakikalar). Geri bildirim döngüsü uzundur, Testler, doğru şekilde ele alınmadığı takdirde potansiyel olarak yıkıcı olabilir ve bağımlılıklar Entegrasyon testlerinin bulut ortamında yapılması kaçınılmazdır.

Bu zorluklara rağmen IaC testi vazgeçilmezdir: Terraform modülü denenmemiş bir saatli bombadır. Bu makale bir piramit IaC testlerinin sayısı üç seviyeli: statik testler (hızlı, ucuz), Terraform testleri (orta) ve Terratest ile entegrasyon testleri (yavaş, derinlemesine).

IaC için Piramidi Test Edin

  • Seviye 1 - Statik (saniye): fmt, doğrula, tflint, tfsec/checkov. Sıfır AWS maliyeti.
  • Seviye 2 - Birim/Sahne (dakika): Mock_provider ile terraform testi. Sıfır öğe oluşturuldu.
  • Seviye 3 - Entegrasyon (5-30 dakika): Terratest: gerçek varlıklar, uçtan uca doğrulama.

Terraform Testi: Yerel Çerçeve (1.6'dan GA)

Terraform 1.6'dan beri çerçeve terraform test Genel Kullanılabilirliktir. Test dosyalarının uzantısı var .tftest.hcl ve test etmenize izin verin iki modlu modül arayüzü (giriş/çıkış): komut uygula (gerçek kaynaklar yaratın) e komuta planı (yalnızca planlama, sıfır maliyet).

# 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

Sahte Sağlayıcılar: Bulut Kimlik Bilgileri Olmadan Test Yapma

Terraform 1.7'den itibaren, sahte sağlayıcılar simüle etmenize izin verin gerçek kimlik bilgileri olmadan AWS/Azure/GCP sağlayıcısı. Hesaplanan değerler (ARN gibi, ID) sahte olarak otomatik olarak oluşturulur veya yapılandırılır.

# 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 ile Entegrasyon Testi

Terratest (github.com/gruntwork-io/terratest) bir Go kütüphanesidir Gerçek bulut kaynakları oluşturan ve davranışlarını doğrulayan entegrasyon testleri yazmak ve bittiğinde onları yok edin. Karmaşık Terraform modüllerini test etmek için altın standarttır.

# 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)")
}

Sözleşme Testi: Modül Arayüzlerinin Doğrulanması

Il sözleşme testi Terraform modülleri için şunları doğrular: sözleşme Modülün (giriş ve çıkış) tüketici beklentilerini karşılaması. Şunda faydalıdır: Değişikliklerin tüketicileri rahatsız etmemesini sağlamak için paylaşılan modül depoları.

# 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 ile Entegrasyon: Test için GitHub Eylemleri

# .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

Entegrasyon Test Sürelerini Optimize Edin

Test Maliyetlerini ve Sürelerini Azaltmaya Yönelik Stratejiler

  • t.Paralel(): azaltmak için Terratest testlerini paralel olarak çalıştırın. toplam süre. Her biri 5 dakikalık 10 test ile paralel olarak 5 dakika yeterlidir 50 yerine.
  • Ekonomik bölgeleri kullanın: us-east-1 en AWS Bölgesidir Ekonomik ve mümkün olan en fazla sayıda hizmetle.
  • Agresif Temizleme: Amerika defer terraform.Destroy() sağlamak için her zaman seçenekleri oluşturduktan sonraki ilk işlem olarak panik durumunda bile yıkım.
  • Yalnızca değiştirilmiş modülleri test edin: Amerika paths GitHub Eylemlerinde testleri yalnızca PR'de gerçekten değiştirilen modüller için çalıştırmak.
  • Komutu tercih et = plan: Çoğu test için mantık, command = plan Terraform testinde yeterlidir ve kaynak yaratmaz.

Ekipler için Kapsamlı Test Stratejisi

Seviye Enstrüman Nereye dönüyor Süre AWS maliyeti
Statik fmt, doğrulama, tflint Her PR, yerel 10-30'lar $0
Güvenlik Checkov, Trivy Her halkla ilişkiler 30-60'lar $0
Birim (plan) terraform testi -plan Her halkla ilişkiler 1-3 dakika $0
Birim (sahte) terraform testi - sahte Her halkla ilişkiler 1-5 dakika $0
Entegrasyon Terratest Halkla ilişkiler → ana 10-30 dakika 0,10-2,00$
Sözleşme terraform testi - sahte Her bir PR modülü 2-5 dakika $0

Sonuçlar ve Sonraki Adımlar

Üç katmanlı bir IaC test stratejisi (statik, birim/sahte ve entegrasyon) Geri bildirim hızını doğrulama derinliğiyle dengeleyin. Yeni çerçeve terraform test native temel testleri erişilebilir hale getiriyor harici bağımlılıklar olmadan Terratest tercih edilen araç olmaya devam ediyor Altyapının gerçek davranışını doğrulayın.

Serideki Sonraki Yazılar

  • Madde 6: IaC Güvenliği — Checkov, Trivy ve OPA Kod Olarak Politika: ön başvuru kapısında otomatik güvenlik taraması ve organizasyon politikaları.
  • Madde 7: Terraform Çoklu Bulut — AWS + Azure + GCP ile Paylaşılan Modüller: soyutlama katmanı ve çoklu sağlayıcı yönetimi.