Testowanie IaC: Terratest, testy natywne dla Terraform i testy kontraktowe
Kompleksowa strategia testowania dla Terraform: moduły testów jednostkowych z natywny framework testowy terraform (GA w Terraform 1.6+), test integracyjny z Terratest in Go i testowaniem kontraktowym interfejsu modułu.
Problem testowania IaC
Infrastruktura testowa zasadniczo różni się od testowania kodu aplikacji. Test jednostkowy modułu Terraform może wymagać faktycznego utworzenia zasobów AWS, które kosztują pieniądze i czas (minuty, a nie milisekundy). Pętla sprzężenia zwrotnego jest długa, testy są potencjalnie destrukcyjne, jeśli nie są właściwie obsługiwane, oraz zależności w środowisku chmury są nieuniknione w przypadku testów integracyjnych.
Pomimo tych wyzwań niezastąpione są testy IaC: moduł Terraform niesprawdzone, to bomba zegarowa. W artykule przedstawiono A piramida testów dla IaC z trzema poziomami: testy statyczne (szybkie, tanie), testy z testy terraformowe (średnie) i testy integracyjne z Terratestem (powolne, dogłębne).
Piramida testowa dla IaC
- Poziom 1 – Statyczny (sekundy): fmt, sprawdź, tflint, tfsec/checkov. Zerowe koszty AWS.
- Poziom 2 – Jednostka/Makieta (minuty): test terraformowy z mock_provider. Utworzono zero zasobów.
- Poziom 3 – Integracja (5-30 minut): Terratest: realne aktywa, kompleksowa weryfikacja.
Test Terraform: Natywna platforma (GA od 1.6)
Od wersji Terraform 1.6 framework terraform test jest to ogólna dostępność.
Pliki testowe mają rozszerzenie .tftest.hcl i pozwolić Ci przetestować
interfejs modułu (wejście/wyjście) z dwoma trybami: polecenie zastosuj
(stwórz prawdziwe zasoby) e plan dowodzenia (tylko planowanie, koszty zerowe).
# 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
Próbni dostawcy: testowanie bez poświadczeń w chmurze
Od Terraforma 1.7, tj fałszywych dostawców pozwalają na symulację dostawca AWS/Azure/GCP bez prawdziwych poświadczeń. Obliczone wartości (takie jak ARN, ID) są automatycznie generowane lub konfigurowane w makiecie.
# 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: Test integracji z Go
Terratest (github.com/gruntwork-io/terratest) to biblioteka Go napisać testy integracyjne, które stworzą realne zasoby chmurowe i weryfikują ich zachowanie i zniszcz je, gdy skończysz. Jest to złoty standard w testowaniu złożonych modułów 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)")
}
Testowanie kontraktowe: weryfikacja interfejsów modułów
Il testowanie kontraktu dla modułów Terraform sprawdza, czy plik umowa modułu (wejście i wyjście) spełnia oczekiwania konsumentów. Przydaje się w Udostępnione repozytoria modułów, aby mieć pewność, że zmiany nie psują konsumentów.
# 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"
}
}
Integracja z 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
Zoptymalizuj czas testów integracyjnych
Strategie mające na celu zmniejszenie kosztów i czasu testowania
- t.Równolegle(): Aby zmniejszyć, uruchom równolegle testy Terratest całkowity czas. Przy 10 testach po 5 minut każdy wystarczy 5 minut równolegle zamiast 50.
- Skorzystaj z regionów gospodarczych: us-east-1 jest najbardziej regionem AWS ekonomiczne i oferujące największą liczbę dostępnych usług.
-
Agresywne czyszczenie: USA
defer terraform.Destroy()zawsze jako pierwsza operacja po utworzeniu opcji, aby się upewnić zniszczenia nawet w przypadku paniki. -
Testuj tylko zmodyfikowane moduły: USA
pathsw akcjach GitHuba aby uruchomić testy tylko dla modułów faktycznie zmienionych w PR. -
Preferuj polecenie = plan: W przypadku większości testów
logika,
command = planw teście terraformowym jest to wystarczające i nie tworzy zasobów.
Kompleksowa strategia testów dla zespołów
| Poziom | Instrument | Gdzie się obraca | Czas trwania | Koszt AWS-a |
|---|---|---|---|---|
| Statyczny | fmt, zatwierdź, tflint | Każdy PR, lokalny | 10-30 s | $0 |
| Bezpieczeństwo | Checkov, Trivy | Każdy PR | 30-60 lat | $0 |
| Jednostka (plan) | plan testu terraformowego | Każdy PR | 1-3 minuty | $0 |
| Jednostka (próba) | test terraformowy - próbny | Każdy PR | 1-5 minut | $0 |
| Integracja | Terratest | PR → główny | 10-30 minut | 0,10-2,00 USD |
| Umowa | test terraformowy - próbny | Każdy moduł PR | 2-5 minut | $0 |
Wnioski i dalsze kroki
Trójpoziomowa strategia testowania IaC — statyczna, jednostkowa/próbna i integracyjna —
zrównoważyć prędkość sprzężenia zwrotnego z głębokością weryfikacji. Nowy
ramy terraform test natywny sprawia, że podstawowe testy stają się dostępne
bez zewnętrznych zależności, podczas gdy Terratest pozostaje narzędziem z wyboru
zweryfikować rzeczywiste zachowanie infrastruktury.
Następne artykuły z serii
- Artykuł 6: Bezpieczeństwo IaC — Checkov, Trivy i OPA Policy-as-Code: automatyczne skanowanie bezpieczeństwa i zasady organizacyjne w bramce przed złożeniem wniosku.
- Artykuł 7: Terraform Multi-Cloud — AWS + Azure + GCP z Moduły współdzielone: warstwa abstrakcji i zarządzanie wieloma dostawcami.







