Retour au cours

infra / terraform

Provisioners et pourquoi les éviter

Leçon 121 exercice

Explication

Ce que vous allez apprendre

  • Distinguer local-exec (exécuté sur ta machine) de remote-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.

TypeS'exécute oùCas d'usage typique
local-execSur la machine qui lance terraform applyÉcrire un inventaire local, notifier un webhook
remote-execSur la ressource distante, via SSH/WinRMInstaller 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

hcl
# 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",
    ]
  }
}
bash
# --- 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"
hcl
# --- 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
}
bash
# --- 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
hcl
# 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

1 disponible
1

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".

Résoudre l’exercice →