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 = destroyet le debug SSH, préférer user_data, cloud-init, SSM, AMI bakée ou Ansible, et encapsuler un hook dansnull_resource/terraform_data(triggers_replace). Mini labca-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 : #1679Statut : 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-identityOK) - 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 danscommand/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.
- API / user_data / SSM / AMI / Ansible avant tout provisioner ?
- Connaître file, remote-exec, local-exec +
on_failure/-replace. when = destroy= fragile.- Préférer user_data / cloud-init / SSM / AMI.
- Hook →
null_resource/terraform_data+ triggers. - Jamais de secrets dans
command/inline/connection. - 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.
fileuniquement
3. Pour un hook exceptionnel, où encapsuler le provisioner ?
- A. Sur toutes les ressources AWS sans trigger
- B. Dans
null_resource/terraform_dataavec 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
- Doc : Provisioners, Creation/Destroy-time,
null_resource,terraform_data - Sur ce site : Workspaces, Modules, IAM ; suite EBS
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.



