infra / terraform
Provisioners et pourquoi les éviter
Explication
Ce que vous allez apprendre
- Distinguer
local-exec(exécuté sur ta machine) deremote-exec(exécuté sur la ressource distante) - Expliquer les quatre raisons pour lesquelles les provisioners cassent le modèle déclaratif
- Utiliser
user_data/cloud-init comme alternative native et idempotente - Comprendre pourquoi Packer (images pré-provisionnées) est souvent préférable en production
- Reconnaître les rares cas où un provisioner reste justifié, avec
on_failure
Dans quel contexte ?
Une équipe veut qu'une instance EC2 installe automatiquement Nginx dès sa création. Le premier réflexe, naturel, est d'ajouter un bloc provisioner "remote-exec" qui se connecte en SSH et lance apt install nginx. Ça fonctionne... jusqu'au jour où ce même apply est rejoué sur une instance déjà provisionnée, et où le script échoue de façon imprévisible.
D'abord, il faut distinguer les deux types de provisioners
local-exec exécute une commande sur la machine qui LANCE Terraform elle-même — utile pour écrire un fichier local, notifier un webhook, ou déclencher un script externe. remote-exec, à l'inverse, se connecte à la ressource distante fraîchement créée (via SSH ou WinRM) pour y exécuter des commandes, exactement comme un administrateur le ferait manuellement.
| Type | S'exécute où | Cas d'usage typique |
|---|---|---|
local-exec | Sur la machine qui lance terraform apply | Écrire un inventaire local, notifier un webhook |
remote-exec | Sur la ressource distante, via SSH/WinRM | Installer un paquet, démarrer un service |
Prérequis
Cette leçon suppose une bonne compréhension du cycle de vie apply/destroy (voir la deuxième leçon du cours) : les provisioners s'exécutent à des moments précis de ce cycle, qu'il faut déjà connaître pour comprendre pourquoi ils posent problème.
Une fois ces deux formes comprises, pourquoi sont-elles considérées comme un anti-pattern ?
Quatre raisons s'accumulent. D'abord, Terraform ne peut absolument rien savoir de ce que fait un script arbitraire : il ne peut donc jamais produire un plan fiable sur son contenu. Ensuite, un script comme apt install nginx n'est pas idempotent par défaut : le rejouer sur une instance déjà configurée peut échouer ou dupliquer des actions.
Piège fréquent
Un provisioner qui échoue marque immédiatement la ressource entière comme "tainted" (corrompue), ce qui force Terraform à la RECRÉER intégralement au prochain apply — même si la ressource elle-même fonctionne parfaitement, seul le script de configuration a échoué. C'est une réaction disproportionnée qui surprend souvent les débutants la première fois qu'elle survient.
Voici comment on résout ce problème : user_data et le cloud-init
user_data transmet un script à l'instance elle-même, qui l'exécute via cloud-init AU PREMIER DÉMARRAGE, sans jamais dépendre d'une connexion SSH initiée par Terraform. C'est une approche déclarative et native au cloud : le script fait partie de la définition de la ressource, pas d'une étape séparée et fragile.
Bonne pratique
Pour une configuration plus poussée qu'un simple script de démarrage, préfère construire une image pré-provisionnée avec Packer plutôt que de complexifier un user_data. Terraform se contente alors de déployer cette image déjà prête, sans exécuter le moindre script de configuration lui-même — une séparation nette entre "construire l'image" et "déployer l'infrastructure".
Le dernier recours, à utiliser en connaissance de cause
Certains cas exotiques (systèmes legacy, API cloud incomplète) justifient encore un provisioner. Dans ce cas, on_failure = fail (comportement par défaut) garantit qu'un échec de provisioner reste visible plutôt que d'être silencieusement ignoré.
Après cette mise en garde sur les provisioners, la prochaine leçon revient sur un sujet central déjà entrevu : la mise en place complète et sécurisée d'un backend S3 avec verrouillage DynamoDB.
Commandes & code
Provisioners et pourquoi les éviter
# local-exec : exécute une commande sur la machine qui lance Terraform (pas sur la ressource distante)
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
provisioner "local-exec" {
command = "echo ${self.private_ip} >> inventaire.txt" # ajoute l'IP à un fichier local
}
}
# remote-exec : exécute des commandes SUR la ressource distante, via SSH/WinRM
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
key_name = "ma-cle-ssh"
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"sudo apt update",
"sudo apt install -y nginx",
"sudo systemctl start nginx",
]
}
}# --- Pourquoi les provisioners sont un ANTI-PATTERN en usage courant ---
# 1. Non déclaratifs : Terraform ne peut pas savoir ce que fait un script -> pas de "plan" fiable dessus
# 2. Pas idempotents par défaut : rejouer un remote-exec peut échouer ou dupliquer des actions
# 3. Couplent la création d'infra ET la configuration applicative -> violent la séparation des responsabilités
# 4. Aucune reprise sur erreur propre : si le provisioner échoue, la ressource est marquée "tainted"# --- Alternative recommandée n°1 : user_data / cloud-init (natif, déclaratif, idempotent) ---
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
user_data = templatefile("${path.module}/init.sh.tpl", {
app_version = var.app_version
})
# -> exécuté par le cloud-init DE L'INSTANCE elle-même au premier démarrage, sans dépendance à Terraform/SSH
}# --- Alternative recommandée n°2 : outil de configuration dédié (Ansible, Packer) ---
# Packer construit une IMAGE (AMI) déjà provisionnée -> Terraform ne fait QUE déployer cette image
packer build image.pkr.hcl
# Terraform utilise ensuite l'AMI résultante, sans aucun provisioner# Si un provisioner est VRAIMENT nécessaire (dernier recours), le marquer explicitement
resource "aws_instance" "legacy" {
# ...
provisioner "remote-exec" {
inline = ["echo bootstrap"]
on_failure = fail # ou "continue" pour ignorer l'échec (rarement souhaitable)
}
}Résumé
- Les provisioners (
local-exec,remote-exec) exécutent des scripts impératifs : ils cassent le modèle déclaratif de Terraform. user_data/cloud-init est la meilleure alternative native pour du bootstrap simple, exécuté par l'instance elle-même.- Packer (images pré-provisionnées) ou Ansible (configuration dédiée) remplacent avantageusement les provisioners en production.
Exercices pratiques
Mission : réparer une installation Nginx qui casse au second apply
Objectif : Remplacer un provisioner fragile par une alternative native idempotente, et comprendre pourquoi Terraform ne peut jamais garantir un provisioner.
Contexte
Un provisioner "remote-exec" lance sudo apt install -y nginx sur aws_instance.app au premier apply : ça fonctionne. Un second apply, plus tard, relancé sur cette même instance déjà provisionnée, échoue avec une erreur inattendue liée au gestionnaire de paquets déjà à jour. La ressource se retrouve marquée "tainted".