DevOps Elastic Hayway
Document

SUBSCRIBE TO GET FULL ACCESS TO THE E-BOOKS FOR FREE 🎁SUBSCRIBE NOW

Professional Dropdown with Icon

SUBSCRIBE NOW TO GET FREE ACCESS TO EBOOKS

TerraformLesson 15 / 2412 min readUpdated September 13, 2026

Provisioners Terraform : quand les éviter

À la fin de ce tutoriel, vous saurez pourquoi HashiCorp déconseille les terraform provisioner, distinguer file, remote-exec et local-exec, gérer on_failure, when = destroy et le debug SSH, préférer user_data, cloud-init, SSM, AMI bakée ou Ansible, et encapsuler un hook dans null_resource / terraform_data (triggers_replace). Mini lab ca-central-1 — Terraform ≥ 1.9, AWS provider ~> 5.0. Message clé : provisioners = dernier recours.

Niveau : Intermédiaire · Temps : 30–40 min · Versions : Terraform 1.16.2 (≥ 1.9), AWS ~> 5.0 · Vérifié : 2026-09-11

Slug live : terraform-provisioner · Alias série SEO : terraform-provisioners · Série : Terraform · Fusion : #1679

Statut : Publish HOLD — draft markdown uniquement ; fusion éditoriale #1679 ; pas de publish WordPress / Rank Math. Canonical : /terraform-provisioner/.

Live absorbé (EN Lesson 14/23, 2026-09-10) : 3 types, EC2 file+remote-exec+local-exec, on_failure/-replace, debug SSH/cloud-init, destroy-time, terraform_data, alternatives — réécrit FR, intention conservée.

Prérequis

  • Workspaces (terraform-workspace) : state, naming, apply/destroy
  • Modules registry (terraform-modules-registry) et idéalement Variables
  • Terraform ≥ 1.9, AWS CLI v2, profil lab (aws sts get-caller-identity OK)
  • Notions EC2 / IAM (user_data, SSM)

Coût : 0 € (mini lab local-exec). Variante EC2 = quelques centimes — optionnelle. Destroy si lab cloud. Aucun secret dans HCL/tfvars.

Auth sans secrets. AWS_PROFILE / SSO. Jamais de clés dans le provider ni de secrets dans command / inline.

Ce que nous allons construire

Lab terraform provisioner (ca-central-1) — last resort
  ├── versions.tf / providers.tf  → TF ≥ 1.9, aws ~> 5.0, ca-central-1
  ├── Étapes 1–4                  → why avoid + types + destroy + alternatives
  ├── null_resource / terraform_data + local-exec (0 €)
  ├── (optionnel) EC2 file/remote-exec → destroy immédiat
  └── Checklist + debug + erreurs + quiz

(Alt : « Terraform provisioner : local-exec, remote-exec, file — préférer user_data et SSM — ca-central-1 ».)

Ce guide enrichit #1679 (slug live terraform-provisioner). Focus : last resort, pas bootstrap quotidien.

Étape 1 — Pourquoi HashiCorp déconseille les provisioners

Terraform brille quand l’état désiré est déclaratif et observable via le provider. Un provisioner casse ce contrat : il exécute un script dont le résultat (paquets installés, fichiers déposés) n’apparaît pas comme attribut de resource. Au prochain plan, Terraform croit que rien n’a changé alors que l’OS a dérivé.

Cas réel : une équipe installe nginx via remote-exec au create. Un collègue met à jour nginx à la main ; un autre change le script. Personne ne voit de diff Terraform. Le correctif durable est user_data / cloud-init (premier boot) ou une AMI bakée, pas un SSH depuis la machine d’apply.

Piège : utiliser des provisioners « parce que ça marche en démo ». En CI multi-agents, la connectivité SSH, les clés et les bastions deviennent un enfer. Préférez SSM.

Un terraform provisioner exécute une action hors modèle déclaratif (shell, copie, SSH) au create ou destroy — dernier recours si l’API provider ne suffit pas.

Problème Impact
Hors state État OS post-script invisible à Terraform
Non idempotent Second apply opaque
Couplage réseau SSH/WinRM, bastion, timing boot
Échec opaque Timeout SSH sans drift clair
Drift Config manuelle invisible au plan

Configurer l’OS = user_data / cloud-init, AMI bakée (Packer), SSM ou Ansible hors graph. Un provisioner comble un trou API, pas une stratégie d’image.

Étape 2 — Types : file, remote-exec, local-exec

file et remote-exec exigent une connection (SSH/WinRM) : timeout, user, clé, bastion. local-exec s’exécute sur le runner Terraform — pratique pour un echo de lab, dangereux si la commande suppose des binaires absents en CI. Dans tous les cas, un échec tainte souvent la resource : le recreate peut surprendre.

Type Où Connexion Usage typique (éviter si possible)
file Copie locale → distante Oui Déposer script / config
remote-exec Machine distante Oui Install juste après create
local-exec Machine apply Non Hook CI, inventaire, webhook

Exécution une fois à la création (ou destroy avec when = destroy). Changer le script ne les rejoue pas ; plan n’affiche pas leur effet.

terraform {
  required_version = ">= 1.9.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}
provider "aws" {
  region = "ca-central-1"  # AWS_PROFILE / SSO — jamais de clés
}

resource "null_resource" "demo_local_hook" {
  triggers = { always_run_hint = "change-me-to-force" }
  provisioner "local-exec" {
    command = "echo 'Hook local — ca-central-1' > /tmp/tf-provisioner-demo.txt"
  }
}

Notes : connection requis pour file/remote-exec. on_failure = continue | fail (défaut fail) : échec → ressource tainted (recreate). Rejouer : terraform apply -replace=aws_instance.web (remplace taint). Préférez script/scripts à inline.

Étape 3 — when = destroy : pièges

Les provisioners de destroy sont séduisants (« nettoyer l’inventaire ») mais fragiles : si le script est retiré du code avant le destroy, il ne s’exécutera jamais ; si la resource distante a déjà disparu, le remote destroy échoue. Préférez les mécanismes natifs (force_destroy, lifecycle) et les jobs de cleanup pipeline.

resource "null_resource" "cleanup_hook" {
  provisioner "local-exec" {
    when    = destroy
    command = "echo 'Cleanup local — fragile en prod'"
  }
}

Pièges : ordre non garanti ; script destroy raté → state incohérent ; ressource déjà partie → remote destroy inutile ; CI sans la même clé ; refs limitées à self/count.index/each.key ; code retiré → provisioner jamais exécuté. Préférez force_destroy, lifecycle, SSM.

Étape 4 — Alternatives (user_data, SSM, Packer, Ansible)

user_data avec user_data_replace_on_change rend le bootstrap visible dans le plan (recreate contrôlée). SSM Run Command gère le drift post-boot sans ouvrir le port 22. Packer/Image Builder produit une AMI testée ; Ansible en pipeline applique la config riche après que l’infra existe. Ces outils respectent mieux la séparation des responsabilités que des inline SSH dans le HCL.

Besoin Alternative
Premier boot user_data + cloud-init
Patch récurrents SSM Run Command / State Manager
Image reproductible AMI bakée (Packer / Image Builder)
Config riche Ansible (pipeline post-apply)
Secrets SSM Parameter / Secrets Manager
Hook CI Job pipeline hors HCL
resource "aws_instance" "web" {
  # ami / instance_type / subnet / iam_instance_profile selon lab
  user_data = <<-EOT
    #!/bin/bash
    set -euo pipefail
    dnf install -y nginx && systemctl enable --now nginx
  EOT
  user_data_replace_on_change = true
  tags = { ManagedBy = "terraform", Lab = "terraform-provisioner", Region = "ca-central-1" }
}

Pas de port 22 pour Terraform, pas de clé privée dans state/CI, compatible Auto Scaling.

Étape 5 — null_resource / terraform_data

Si vous devez vraiment un hook (mise à jour kubeconfig, génération d’inventaire), isolez-le dans terraform_data / null_resource avec des triggers explicites. Ainsi vous ne taintez pas une aws_instance critique et vous documentez le caractère exceptionnel du geste. En Terraform ≥ 1.4, triggers_replace est plus clair que d’anciens patterns triggers opaques.

Encapsulez dans null_resource ou terraform_data (≥ 1.4, triggers_replace) — recreate contrôlé, hors ressource AWS critique.

resource "terraform_data" "lab_marker" {
  input = { project = var.project, region = "ca-central-1", note = "last-resort" }
}

# Légitime rare : kubeconfig / inventaire quand un input change
# resource "terraform_data" "kubeconfig" {
#   triggers_replace = [aws_eks_cluster.main.endpoint]
#   provisioner "local-exec" {
#     command = "aws eks update-kubeconfig --name ${aws_eks_cluster.main.name} --region ca-central-1"
#   }
# }

resource "null_resource" "local_marker" {
  triggers = { project = var.project, revision = "1" }
  provisioner "local-exec" {
    command = "printf '%s\n' "project=${var.project}" "region=ca-central-1" "ts=$(date -u +%Y-%m-%dT%H:%MZ)" > ./tf-provisioner-marker.txt"
  }
  provisioner "local-exec" {
    when = destroy
    command = "rm -f ./tf-provisioner-marker.txt || true"
  }
}
variable "project" { type = string, description = "Préfixe lab (sans secret)." }
output "marker_hint" { value = "tf-provisioner-marker.txt" }

Usages encore OK : CLI sans provider ; inventaire/kubeconfig ; labs simples. Prod → souvent job CI.

Étape 6 — Mini lab (+ variante EC2 optionnelle)

Le mini lab local-exec coûte 0 € et suffit à comprendre le cycle create/destroy du marker. La variante EC2 illustre le pattern historique du live #1679 : gardez-la comme contre-exemple pédagogique autant que comme exercice. Si vous l’appliquez, restreignez SSH à votre IP /32, puis destroy immédiat.

mkdir -p ~/terraform-provisioner-lab && cd ~/terraform-provisioner-lab
# Collez versions.tf, providers.tf, blocs étape 5
export AWS_PROFILE=lab AWS_REGION=ca-central-1
PROJECT="votreprenom20260911"
terraform init && terraform fmt && terraform validate
terraform plan  -var="project=${PROJECT}"
terraform apply -auto-approve -var="project=${PROJECT}"
cat ./tf-provisioner-marker.txt
terraform destroy -auto-approve -var="project=${PROJECT}"
cd ~ && rm -rf ~/terraform-provisioner-lab

Variante optionnelle (coûteuse) — EC2 + file + remote-exec + local-exec

Illustre le pattern live #1679 (AL2023, SG SSH restreint, nginx). Non obligatoire — préférez user_data + SSM. Si testée : destroy immédiat.

# Placeholders — pas d’IP/clés réelles dans le dépôt
# data.aws_ami.al2023 + aws_key_pair + aws_security_group (22 = var.my_ip_cidr)
resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"
  key_name      = aws_key_pair.lab.key_name
  vpc_security_group_ids = [aws_security_group.web.id]
  connection {
    type = "ssh", host = self.public_ip, user = "ec2-user"
    private_key = file("~/.ssh/id_ed25519")  # lab local — jamais commit
    timeout = "3m"
  }
  provisioner "file" {
    source = "files/index.html", destination = "/tmp/index.html"
  }
  provisioner "remote-exec" {
    inline = [
      "cloud-init status --wait",
      "sudo dnf install -y nginx",
      "sudo mv /tmp/index.html /usr/share/nginx/html/index.html",
      "sudo systemctl enable --now nginx",
    ]
  }
  provisioner "local-exec" {
    command = "echo '${self.public_ip} ansible_user=ec2-user' >> inventory.ini"
  }
  tags = { Name = "provisioner-lab" }
}

Points : self (pas la ref de la ressource dans son propre bloc) ; connection partagée ; on_failure = continue pour éviter le taint ; rejouer avec -replace.

Étape 7 — Debug et checklist

Debug : SG sans 22 / pas d’IP publique / mauvais user (ec2-user vs ubuntu) — tester SSH manuellement ; cloud-init status --wait en tête d’inline ; TF_LOG=DEBUG ; Windows = WinRM via user_data.

  1. API / user_data / SSM / AMI / Ansible avant tout provisioner ?
  2. Connaître file, remote-exec, local-exec + on_failure / -replace.
  3. when = destroy = fragile.
  4. Préférer user_data / cloud-init / SSM / AMI.
  5. Hook → null_resource / terraform_data + triggers.
  6. Jamais de secrets dans command / inline / connection.
  7. Lab local → destroy ; EC2 optionnel seulement.

Nettoyage

cd ~/terraform-provisioner-lab
PROJECT="votreprenom20260911"
terraform destroy -auto-approve -var="project=${PROJECT}"
rm -f ./tf-provisioner-marker.txt inventory.ini
cd ~ && rm -rf ~/terraform-provisioner-lab

Si EC2 : console ca-central-1 — aucune instance restante.

Erreurs fréquentes

Symptôme Cause Correction
Timeout SSH / « Still creating… » SG / IP / user / boot user_data ; waits + SSM
Ne se rejoue pas Pas de triggers / -replace triggers_replace ou apply -replace
Destroy laisse des artefacts when = destroy KO / absent CI Cleanup natif + pipeline
Clés dans le state connection.private_key SSM Session Manager
Drift OS invisible Config hors modèle AMI bakée + config mgmt
local-exec OK laptop, KO CI Binaire local Conteneur CI / job hors TF
Double bootstrap user_data et remote-exec Une seule stratégie
Package manager locked cloud-init pas fini cloud-init status --wait
WinRM timeout Windows sans bootstrap WinRM Préparer WinRM via user_data ; ou éviter remote-exec
Secret dans inline Token en clair dans HCL SSM Parameter / Secrets Manager + IAM
null provider manquant null_resource sans init provider Ajouter hashicorp/null ou migrer terraform_data

Quiz (3 questions)

1. Posture officielle sur les terraform provisioner ?

  • A. Pattern recommandé pour tout bootstrap EC2
  • B. Dernier recours — préférer user_data, SSM, AMI, Ansible
  • C. Obligatoires avec AWS provider ~> 5.0

2. Quel type s’exécute sur la machine qui lance terraform apply ?

  • A. remote-exec
  • B. local-exec
  • C. file uniquement

3. Pour un hook exceptionnel, où encapsuler le provisioner ?

  • A. Sur toutes les ressources AWS sans trigger
  • B. Dans null_resource / terraform_data avec triggers contrôlés
  • C. Dans le bloc provider "aws"

4. Pourquoi cloud-init status --wait avant dnf install en remote-exec ?

  • A. Cosmétique
  • B. Évite les conflits package manager pendant le premier boot
  • C. Remplace le Security Group

5. Où s’exécute local-exec ?

  • A. Sur chaque instance ASG
  • B. Sur la machine / le runner qui lance Terraform
  • C. Uniquement dans AWS Systems Manager

Réponses : 1‑B · 2‑B · 3‑B · 4‑B · 5‑B

FAQ

Les provisioners sont-ils interdits ? Non — déconseillés comme stratégie principale. Un hook local-exec documenté et rare reste acceptable ; le bootstrap SSH systématique ne l’est pas.

terraform_data remplace-t-il null_resource ? Pour beaucoup de hooks modernes, oui (ressource built-in). null_resource reste répandu dans l’écosystème ; comprenez les deux.

Comment rejouer un provisioner sans destroy complet ? terraform apply -replace=… ou modifier les triggers / triggers_replace. Sinon Terraform ne le rejoue pas.

Pour aller plus loin

Maillage série Terraform (P2)

← Précédent Workspaces (terraform-workspace)
→ Suivant EBS (terraform-ebs)
Aussi Modules (terraform-modules-bases / terraform-modules-registry) · IAM (aws-iam-users-groupes-roles-politiques)
Carte Overview · … · Workspaces · Provisioners · EBS · …

Cas réel — remote-exec qui casse le destroy

Une équipe laisse remote-exec installer nginx via SSH sur chaque apply. Le security group n’ouvre le 22 que depuis le laptop du lundi. Mardi, le apply CI timeout : Terraform marque la ressource tainted ou échoue à mi-création. Le destroy suivant tente when = destroy alors que l’instance n’a plus de route SSH — le state reste sale.

Correctif : plus de bootstrap SSH. user_data (cloud-init) ou SSM + AMI bakée. Si vous devez vraiment d’un hook, préférez local-exec qui parle à l’API AWS (tag, invalidation) sans dépendre du réseau de l’instance. Documentez on_failure = continue seulement si le hook est cosmétique — jamais pour une étape de sécurité.

Piège null_resource / terraform_data : un trigger trop large (timestamp) rejoue le provisioner à chaque plan. Triggers stables (hash d’un script, version d’AMI). En ca-central-1, testez destroy avant de commiter le hook : si destroy a besoin de SSH, le hook n’a pas sa place dans le module.

À retenir : un provisioner qui ne survit pas à un changement de laptop n’est pas de l’IaC. C’est un script collé au state.

Retour parcours Terraform — hub de la série et leçons sœurs.

Share your love

Leave a Reply

Your email address will not be published. Required fields are marked *