L’infrastructure as code sans tests, c’est du code sans tests. Terratest permet d’écrire des tests Go qui créent de vraies ressources cloud, vérifient qu’elles fonctionnent correctement, puis les détruisent.
Pourquoi Terratest plutôt que terraform validate ?
terraform validate vérifie la syntaxe. terraform plan vérifie la logique. Terratest vérifie que l’infrastructure fonctionne : le bucket S3 est accessible, le serveur répond sur le bon port, le VPC a les bonnes routes.
Structure d’un test Terratest
// test/aws_vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVpcModule(t *testing.T) {
t.Parallel() // lancer les tests en parallèle
awsRegion := "eu-west-1"
terraformOptions := &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"vpc_cidr": "10.0.0.0/16",
"environment": "test",
"region": awsRegion,
},
}
// Détruire les ressources à la fin du test, même si le test échoue
defer terraform.Destroy(t, terraformOptions)
// Créer les ressources
terraform.InitAndApply(t, terraformOptions)
// Récupérer les outputs
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
// Vérifier que les ressources existent et ont les bonnes propriétés
vpc := aws.GetVpcById(t, vpcId, awsRegion)
assert.Equal(t, "10.0.0.0/16", vpc.CidrBlock)
assert.Equal(t, 3, len(privateSubnetIds), "Devrait y avoir 3 subnets privés (1 par AZ)")
// Vérifier qu'aucun subnet privé n'est exposé sur Internet
for _, subnetId := range privateSubnetIds {
subnet := aws.GetSubnetById(t, subnetId, awsRegion)
assert.False(t, subnet.MapPublicIPOnLaunch,
"Les subnets privés ne doivent pas assigner d'IPs publiques")
}
}
Tester un serveur EC2
func TestWebServer(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../examples/web-server",
Vars: map[string]interface{}{
"instance_type": "t3.micro",
"environment": "test",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
publicIp := terraform.Output(t, terraformOptions, "public_ip")
url := fmt.Sprintf("http://%s:8080/health", publicIp)
// Retry jusqu'à ce que le serveur réponde (max 5 minutes)
http_helper.HttpGetWithRetry(t, url, nil, 200, "OK", 30, 10*time.Second)
}
Idempotence et drift detection
func TestIdempotence(t *testing.T) {
t.Parallel()
options := &terraform.Options{TerraformDir: "../modules/database"}
defer terraform.Destroy(t, options)
terraform.InitAndApply(t, options)
// Un deuxième apply ne doit faire aucun changement
exitCode := terraform.PlanExitCode(t, options)
assert.Equal(t, 0, exitCode, "Deuxième plan doit retourner 0 (no changes)")
}
Intégration dans GitLab CI
terraform-test:
image: golang:1.22
before_script:
- apt-get install -y unzip
- wget -O terraform.zip https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
- unzip terraform.zip -d /usr/local/bin/
script:
- cd test
- go mod download
- go test -v -timeout 30m ./...
variables:
AWS_ACCESS_KEY_ID: $TEST_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: $TEST_AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION: eu-west-1
rules:
- changes:
- modules/**
- test/**
Notre formation Terraform couvre les tests d’infrastructure avec Terratest et l’intégration CI/CD.