terraform-test

Par hashicorp · agent-skills

Guide complet pour écrire et exécuter des tests Terraform. À utiliser lors de la création de fichiers de test (.tftest.hcl), de l'écriture de scénarios de test avec des blocs `run`, de la validation du comportement de l'infrastructure avec des assertions, du mock de providers et de sources de données, des tests de sorties de modules et de configurations de ressources, ou du débogage de la syntaxe et de l'exécution des tests Terraform.

npx skills add https://github.com/hashicorp/agent-skills --skill terraform-test

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 CI
  • references/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 blocs run qui 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) ou plan (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

  1. Nommage : *_unit_test.tftest.hcl pour le mode plan, *_integration_test.tftest.hcl pour le mode apply
  2. Nommage des tests : Utilisez des noms de blocs run explicites qui expliquent le scénario testé
  3. Par défaut plan : Utilisez command = plan sauf si vous devez tester le comportement réel des ressources
  4. Utilisez les simulacres pour les dépendances externes — plus rapide et aucune accréditation requise (voir references/MOCK_PROVIDERS.md)
  5. Messages d'erreur : Rendez-les assez spécifiques pour diagnostiquer les échecs sans relancer le test
  6. Tests négatifs : Utilisez expect_failures pour vérifier que les règles de validation rejettent les mauvaises entrées
  7. 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
  8. Sources de modules : Les fichiers de test ne supportent que les chemins locaux et les modules de registry — pas les URLs git ou HTTP
  9. Exécution parallèle : Utilisez parallel = true pour les tests indépendants avec des fichiers d'état différents
  10. Nettoyage : Les tests d'intégration détruisent automatiquement les ressources dans l'ordre inverse des blocs run ; utilisez -no-cleanup pour le débogage
  11. 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

Références

Skills similaires