Test Terraform
Le framework de test intégré de Terraform valide que les mises à jour de configuration n'introduisent pas de changements critiques. Les tests s'exécutent sur des ressources temporaires, protégeant l'infrastructure existante et les fichiers d'état.
Fichiers de référence
references/MOCK_PROVIDERS.md— Syntaxe des fournisseurs simulés, paramètres par défaut courants, quand utiliser les simulacres (Terraform 1.7.0+ uniquement — à ignorer si la version de l'utilisateur est antérieure)references/CI_CD.md— Exemples de pipelines GitHub Actions et GitLab CIreferences/EXAMPLES.md— Suite de tests d'exemple complète (tests unitaires, d'intégration et avec simulacres pour un module VPC)
Consultez le fichier de référence approprié quand l'utilisateur pose des questions sur les simulacres, l'intégration CI/CD, ou souhaite un exemple complet.
Concepts fondamentaux
- Fichier de test (
.tftest.hcl/.tftest.json) : Contient des blocsrunqui valident votre configuration - Bloc run : Un scénario de test unique avec variables, fournisseurs et assertions optionnels
- Bloc assert : Conditions qui doivent être vraies pour que le test réussisse
- Fournisseur simulé : Simule le comportement du fournisseur sans infrastructure réelle (Terraform 1.7.0+)
- Modes de test :
apply(par défaut, crée des ressources réelles) ouplan(valide uniquement la logique)
Structure de fichier
my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── defaults_unit_test.tftest.hcl # mode plan — rapide, sans ressources
├── validation_unit_test.tftest.hcl # mode plan
└── full_stack_integration_test.tftest.hcl # mode apply — crée des ressources réelles
Utilisez *_unit_test.tftest.hcl pour les tests en mode plan et *_integration_test.tftest.hcl pour les tests en mode apply afin qu'ils puissent être filtrés séparément en CI.
Structure de fichier de test
# Optionnel : paramètres au niveau du test
test {
parallel = true # Active l'exécution parallèle pour tous les blocs run (par défaut : false)
}
# Optionnel : variables au niveau du fichier (plus haute priorité, remplace toutes les autres sources)
variables {
aws_region = "us-west-2"
instance_type = "t2.micro"
}
# Optionnel : configuration du fournisseur
provider "aws" {
region = var.aws_region
}
# Obligatoire : au moins un bloc run
run "test_default_configuration" {
command = plan
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Instance type should be t2.micro by default"
}
}
Bloc run
run "test_name" {
command = plan # ou apply (par défaut)
parallel = true # optionnel, depuis v1.9.0
# Remplace les variables au niveau du fichier
variables {
instance_type = "t3.large"
}
# Référence un module spécifique
module {
source = "./modules/vpc" # local ou registry uniquement (pas git/http)
version = "5.0.0" # modules de registry uniquement
}
# Contrôle l'isolation d'état
state_key = "shared_state" # depuis v1.9.0
# Comportement de plan
plan_options {
mode = refresh-only # ou normal (par défaut)
refresh = true
replace = [aws_instance.example]
target = [aws_instance.example]
}
# Assertions
assert {
condition = aws_instance.example.id != ""
error_message = "Instance should have a valid ID"
}
# Échecs attendus (le test réussit si ceux-ci échouent)
expect_failures = [
var.instance_count
]
}
Motifs de test courants
Valider les outputs
run "test_outputs" {
command = plan
assert {
condition = output.vpc_id != null
error_message = "VPC ID output must be defined"
}
assert {
condition = can(regex("^vpc-", output.vpc_id))
error_message = "VPC ID should start with 'vpc-'"
}
}
Ressources conditionnelles
run "test_nat_gateway_disabled" {
command = plan
variables {
create_nat_gateway = false
}
assert {
condition = length(aws_nat_gateway.main) == 0
error_message = "NAT gateway should not be created when disabled"
}
}
Comptes de ressources
run "test_resource_count" {
command = plan
variables {
instance_count = 3
}
assert {
condition = length(aws_instance.workers) == 3
error_message = "Should create exactly 3 worker instances"
}
}
Étiquettes
run "test_resource_tags" {
command = plan
variables {
common_tags = {
Environment = "production"
ManagedBy = "Terraform"
}
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "Environment tag should be set correctly"
}
assert {
condition = aws_instance.example.tags["ManagedBy"] == "Terraform"
error_message = "ManagedBy tag should be set correctly"
}
}
Sources de données
run "test_data_source_lookup" {
command = plan
assert {
condition = data.aws_ami.ubuntu.id != ""
error_message = "Should find a valid Ubuntu AMI"
}
assert {
condition = can(regex("^ami-", data.aws_ami.ubuntu.id))
error_message = "AMI ID should be in correct format"
}
}
Règles de validation
run "test_invalid_environment" {
command = plan
variables {
environment = "invalid"
}
expect_failures = [
var.environment
]
}
Tests séquentiels avec dépendances
run "setup_vpc" {
command = apply
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
run "test_subnet_in_vpc" {
command = plan
variables {
vpc_id = run.setup_vpc.vpc_id
}
assert {
condition = aws_subnet.example.vpc_id == run.setup_vpc.vpc_id
error_message = "Subnet should be in the VPC from setup_vpc"
}
}
Options de plan (refresh-only, ciblé)
run "test_refresh_only" {
command = plan
plan_options {
mode = refresh-only
}
assert {
condition = aws_instance.example.tags["Environment"] == "production"
error_message = "Tags should be refreshed correctly"
}
}
run "test_specific_resource" {
command = plan
plan_options {
target = [aws_instance.example]
}
assert {
condition = aws_instance.example.instance_type == "t2.micro"
error_message = "Targeted resource should be planned"
}
}
Modules parallèles
run "test_networking_module" {
command = plan
parallel = true
module {
source = "./modules/networking"
}
assert {
condition = output.vpc_id != ""
error_message = "VPC should be created"
}
}
run "test_compute_module" {
command = plan
parallel = true
module {
source = "./modules/compute"
}
assert {
condition = output.instance_id != ""
error_message = "Instance should be created"
}
}
Partage de clé d'état
run "create_foundation" {
command = apply
state_key = "foundation"
assert {
condition = aws_vpc.main.id != ""
error_message = "Foundation VPC should be created"
}
}
run "create_application" {
command = apply
state_key = "foundation"
variables {
vpc_id = run.create_foundation.vpc_id
}
assert {
condition = aws_instance.app.vpc_id == run.create_foundation.vpc_id
error_message = "Application should use foundation VPC"
}
}
Ordre de nettoyage (objets S3 avant bucket)
run "create_bucket" {
command = apply
assert {
condition = aws_s3_bucket.example.id != ""
error_message = "Bucket should be created"
}
}
run "add_objects" {
command = apply
assert {
condition = length(aws_s3_object.files) > 0
error_message = "Objects should be added"
}
}
# Le nettoyage détruit dans l'ordre inverse : objets d'abord, puis bucket
Plusieurs fournisseurs avec alias
provider "aws" {
alias = "primary"
region = "us-west-2"
}
provider "aws" {
alias = "secondary"
region = "us-east-1"
}
run "test_with_specific_provider" {
command = plan
providers = {
aws = provider.aws.secondary
}
assert {
condition = aws_instance.example.availability_zone == "us-east-1a"
error_message = "Instance should be in us-east-1 region"
}
}
Conditions complexes
assert {
condition = alltrue([
for subnet in aws_subnet.private :
can(regex("^10\\.0\\.", subnet.cidr_block))
])
error_message = "All private subnets should use 10.0.0.0/8 CIDR range"
}
Nettoyage
Les ressources sont détruites dans l'ordre inverse des blocs run après la fin du test. Cela importe pour les dépendances (par ex., objets S3 avant bucket). Utilisez terraform test -no-cleanup pour ignorer le nettoyage lors du débogage.
Exécuter les tests
terraform test # tous les tests
terraform test tests/defaults.tftest.hcl # fichier spécifique
terraform test -filter=test_vpc_configuration # par nom de bloc run
terraform test -test-directory=integration-tests # répertoire personnalisé
terraform test -verbose # sortie détaillée
terraform test -no-cleanup # ignorer le nettoyage des ressources
Bonnes pratiques
- Nommage :
*_unit_test.tftest.hclpour le mode plan,*_integration_test.tftest.hclpour le mode apply - Nommage des tests : Utilisez des noms de blocs run explicites qui expliquent le scénario testé
- Par défaut plan : Utilisez
command = plansauf si vous devez tester le comportement réel des ressources - Utilisez les simulacres pour les dépendances externes — plus rapide et aucune accréditation requise (voir
references/MOCK_PROVIDERS.md) - Messages d'erreur : Rendez-les assez spécifiques pour diagnostiquer les échecs sans relancer le test
- Tests négatifs : Utilisez
expect_failurespour vérifier que les règles de validation rejettent les mauvaises entrées - Couverture des variables : Testez différentes combinaisons de variables pour valider tous les chemins de code — les variables de test ont la plus haute priorité et remplacent toutes les autres sources
- Sources de modules : Les fichiers de test ne supportent que les chemins locaux et les modules de registry — pas les URLs git ou HTTP
- Exécution parallèle : Utilisez
parallel = truepour les tests indépendants avec des fichiers d'état différents - Nettoyage : Les tests d'intégration détruisent automatiquement les ressources dans l'ordre inverse des blocs run ; utilisez
-no-cleanuppour le débogage - CI/CD : Exécutez les tests unitaires sur chaque PR, les tests d'intégration au moment de la fusion (voir
references/CI_CD.md)
Dépannage
| Problème | Solution |
|---|---|
| Échecs d'assertion | Utilisez -verbose pour voir les valeurs réelles par rapport aux valeurs attendues |
| Accréditations manquantes | Utilisez les fournisseurs simulés pour les tests unitaires |
| Source de module non supportée | Convertissez les sources git/HTTP en modules locaux |
| Tests qui s'interfèrent | Utilisez state_key ou des modules séparés pour l'isolation |
| Tests lents | Utilisez command = plan et les simulacres ; exécutez les tests d'intégration séparément |