Test Terraform
Le framework de test intégré de Terraform valide que les mises à jour de configuration n'introduisent pas de changements cassants. Les tests s'exécutent contre 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, valeurs par défaut courantes, quand utiliser les simulacres (Terraform 1.7.0+ uniquement — ignorez si la version de l'utilisateur est infé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 de simulacre pour un module VPC)
Consultez le fichier de référence pertinent quand l'utilisateur pose des questions sur les simulacres, l'intégration CI/CD, ou veut 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 la logique uniquement)
Structure de fichiers
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 du fichier de test
# Optionnel : paramètres au niveau du test
test {
parallel = true # Activer l'exécution parallèle pour tous les blocs run (défaut : false)
}
# Optionnel : variables au niveau du fichier (plus haute précédence, remplacent toutes les autres sources)
variables {
aws_region = "us-west-2"
instance_type = "t2.micro"
}
# Optionnel : configuration du fournisseur
provider "aws" {
region = var.aws_region
}
# Requis : 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 (défaut)
parallel = true # optionnel, depuis v1.9.0
# Remplacer les variables au niveau du fichier
variables {
instance_type = "t3.large"
}
# Référencer un module spécifique
module {
source = "./modules/vpc" # local ou registry uniquement (pas git/http)
version = "5.0.0" # registry modules only
}
# Contrôler l'isolation d'état
state_key = "shared_state" # depuis v1.9.0
# Comportement du plan
plan_options {
mode = refresh-only # ou normal (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"
}
# Défaillances attendues (le test réussit si celles-ci échouent)
expect_failures = [
var.instance_count
]
}
Modèles 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"
}
}
Nombre 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"
}
}
Tags
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
Fournisseurs multiples 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. C'est important pour les dépendances (par exemple, objets S3 avant bucket). Utilisez terraform test -no-cleanup pour ignorer le nettoyage lors du débogage.
Exécution des 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 descriptifs qui expliquent le scénario testé
- Préférez le plan : Utilisez
command = plansauf si vous devez tester le comportement réel des ressources - Utilisez les simulacres pour les dépendances externes — plus rapides et aucune credential nécessaire (voir
references/MOCK_PROVIDERS.md) - Messages d'erreur : Rendez-les assez spécifiques pour diagnostiquer les défaillances sans relancer le test
- Tests négatifs : Utilisez
expect_failurespour vérifier que les règles de validation rejettent les mauvaises entrées - Couverture de variables : Testez différentes combinaisons de variables pour valider tous les chemins de code — les variables de test ont la plus haute précédence 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 différents fichiers d'état - 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 : Lancez les tests unitaires sur chaque PR, les tests d'intégration à la fusion (voir
references/CI_CD.md)
Dépannage
| Problème | Solution |
|---|---|
| Défaillances d'assertion | Utilisez -verbose pour voir les valeurs réelles vs attendues |
| Credentials 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 ; lancez les tests d'intégration séparément |