Testování IaC: Terratest, Terraform Native Testing a smluvní testování
Komplexní testovací strategie pro Terraform: Moduly testování jednotek s nativní testovací rámec terraform (GA v Terraform 1.6+), integrační test s Terratestem v Go a smluvním testováním rozhraní modulu.
Problém testování IaC
Testovací infrastruktura se zásadně liší od testovacího kódu aplikace. Jednotkový test modulu Terraform může vyžadovat skutečné vytvoření zdrojů AWS, které stojí peníze a čas (minuty, nikoli milisekundy). Zpětná vazba je dlouhá, testy jsou potenciálně destruktivní, pokud se s nimi nepracuje správně, a závislosti v cloudovém prostředí jsou pro testování integrace nevyhnutelné.
Navzdory těmto výzvám je testování IaC nepostradatelné: modul Terraform nevyzkoušeno je to časovaná bomba. Tento článek představuje a pyramida testů pro IaC se třemi úrovněmi: statické testy (rychlé, levné), testy s terraform testy (střední) a integrační testy s Terratestem (pomalé, hloubkové).
Testovací pyramida pro IaC
- Úroveň 1 – Statická (v sekundách): fmt, validate, tflint, tfsec/checkov. Nulové náklady na AWS.
- Úroveň 2 – Jednotka/Mock (minuty): terraform test s mock_provider. Byl vytvořen nulový majetek.
- Úroveň 3 – Integrace (5–30 minut): Terratest: reálná aktiva, end-to-end ověřování.
Terraform Test: The Native Framework (GA od 1.6)
Od Terraform 1.6, framework terraform test je to obecná dostupnost.
Testovací soubory mají příponu .tftest.hcl a umožní vám testovat
rozhraní modulu (vstup/výstup) se dvěma režimy: příkaz použít
(vytvářet skutečné zdroje) e velitelský plán (pouze plánování, nulové náklady).
# 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
Mock Providers: Testování bez cloudových přihlašovacích údajů
Od Terraform 1.7, tj zesměšňovat poskytovatele vám umožní simulovat poskytovatele AWS/Azure/GCP bez skutečných přihlašovacích údajů. Vypočítané hodnoty (jako ARN, ID) jsou automaticky generovány nebo konfigurovány v maketě.
# 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: Integrační test s Go
Terratest (github.com/gruntwork-io/terratest) je knihovna Go napsat integrační testy, které vytvoří skutečné cloudové zdroje a ověří jejich chování a po dokončení je zničit. Je to zlatý standard pro testování komplexních modulů 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)")
}
Smluvní testování: Ověření modulových rozhraní
Il testování smlouvy pro moduly Terraform ověřuje, že smlouva modulu (vstup a výstup) splňuje očekávání spotřebitelů. Je užitečné v Sdílená úložiště modulů zajišťující, že změny nezlomí spotřebitele.
# 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"
}
}
Integrace s CI: GitHub Actions for Testing
# .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
Optimalizujte doby integračního testu
Strategie pro snížení nákladů a zkrácení doby testování
- t.Parallel(): Pro snížení spusťte paralelně testy Terratest celkový čas. Při 10 testech po 5 minutách stačí 5 minut paralelně místo 50.
- Použijte ekonomické regiony: us-východ-1 je nejvíce AWS regionem ekonomické a s největším počtem dostupných služeb.
-
Agresivní čištění: USA
defer terraform.Destroy()vždy jako první operaci po vytvoření možností, aby se zajistilo zničení i v případě paniky. -
Testovat pouze upravené moduly: USA
pathsv akcích GitHubu spustit testy pouze pro moduly skutečně změněné v PR. -
Preferovat příkaz = plán: Pro většinu testů
logika,
command = planv terraformním testu je dostačující a nevytváří zdroje.
Komplexní testovací strategie pro týmy
| Úroveň | Nástroj | Kam se to otočí | Trvání | Náklady na AWS |
|---|---|---|---|---|
| Statický | fmt, ověřit, tflint | Každé PR, místní | 10-30s | $0 |
| Zabezpečení | Checkov, Trivy | Každé PR | 30-60s | $0 |
| Jednotka (plán) | plán testu terraform | Každé PR | 1-3 min | $0 |
| jednotka (falešná) | terraform test -zesměšňovat | Každé PR | 1-5 min | $0 |
| Integrace | Terratest | PR → hlavní | 10-30 min | 0,10–2,00 USD |
| Smlouva | terraform test -zesměšňovat | Každý modul PR | 2-5 min | $0 |
Závěry a další kroky
Třívrstvá testovací strategie IaC – statická, jednotková/falešná a integrační –
rychlost zpětné vazby vyvážení s hloubkou ověření. Nový
rámec terraform test nativní zpřístupňuje základní testování
bez externích závislostí, zatímco Terratest zůstává nástrojem volby
ověřit skutečné chování infrastruktury.
Další články v seriálu
- Článek 6: Zabezpečení IaC — Checkov, Trivy a OPA Policy-as-Code: automatické bezpečnostní skenování a organizační zásady v bráně před aplikací.
- Článek 7: Terraform Multi-Cloud — AWS + Azure + GCP s Sdílené moduly: abstraktní vrstva a správa více poskytovatelů.







