Il est 3h17. Personne ne regarde l’écran.
Un ticket tombe dans la file, banal entre mille : « Nettoyer les ressources de test
orphelines, namespace sandbox-eu. » Un agent IA le récupère, le lit, le comprend —
ou croit le comprendre. Il a accès au repo Terraform. Il a accès au backend d’état.
On lui a donné les clés, parce que c’était pratique, parce que le sprint était chargé,
parce que « de toute façon il ne touche qu’au sandbox ».
L’agent rédige son plan. Propre. Il cible une poignée de ressources de test avec un
terraform destroy ciblé. Logique imparable. Sauf qu’au fond d’un fichier locals.tf
que personne n’a relu depuis quatorze mois, une expression anodine référence l’une de
ces ressources. Et cette référence est consommée par un for_each qui pilote, lui,
les subnets du data tier. Production comprise.
Le destroy ciblé ne reste pas ciblé. Il remonte la chaîne de dépendances implicites comme une mèche. Pas de cri. Pas d’alerte rouge clignotante. Juste un état qui se contracte, ligne après ligne, dans le silence climatisé du datacenter.
Destroy complete! Resources: 23 destroyed.
Vingt-trois. On en attendait quatre.
Et là, on pourrait raconter la suite : le réveil à 6h, le PagerDuty saturé, le post-mortem. Mais ce n’est pas le sujet.
Le sujet, c’est que l’agent a fait exactement ce qu’on lui a demandé. Il n’a pas halluciné. Il n’a pas dérapé. Il a exécuté une intention mal cadrée avec une obéissance parfaite et une vitesse que nul humain hésitant n’aurait eue. Le drame n’est pas dans la machine. Il est dans le vide qu’on a laissé autour d’elle.
On a déjà perdu le débat « faut-il laisser les agents toucher à l’infra »
Bon. On a pleuré, on respire, on passe en mode atelier.
La vraie question n’est plus « est-ce qu’on autorise un agent à manipuler du Terraform ? ». Cette bataille est finie. Vos collègues le font déjà, dans leur IDE, avec un copilote qui crache du HCL à la volée. Vos pipelines en sont peut-être déjà truffés. La question utile, la seule qui mérite qu’on ouvre un terminal, c’est : comment on donne à cet agent la discipline qu’un bon SRE met dix ans à intérioriser — et que la plupart n’intériorisent jamais complètement ?
Parce qu’un LLM, livré à lui-même sur du Terraform, échoue toujours sur les mêmes lignes de faille. Ce ne sont pas des bugs exotiques. Ce sont des classiques, et c’est précisément ça qui est terrifiant :
- L’identity churn. Le modèle adore le
count. C’est plus court, ça « fait pro ». Sauf quecountindexe par position. Retirez un élément au milieu d’une liste, et toutes les adresses qui suivent se décalent d’un cran : Terraform croit qu’on a détruit puis recréé la moitié du parc. Unfor_eachsur un set aurait gardé les adresses stables. L’agent ne le sait pas. Il pattern-matche sur le tutoriel le plus fréquent de son corpus. - Les secrets dans l’état. Marquer une variable
sensitive = true, c’est masquer l’affichage — pas chiffrer la valeur dans le fichier d’état. Le secret est toujours là, en clair, dans le.tfstate. Le modèle confond les deux avec une assurance désarmante. - Le blast radius. Un seul état monolithique pour le réseau, le compute et la base.
Un seul
applyqui peut tout emporter. Le rayon de souffle est maximal et personne ne l’a découpé. - La CI drift. Le plan tourné en local ne correspond plus à celui de la CI. Versions non figées, providers qui glissent. L’agent applique un plan qu’il a régénéré au lieu de rejouer l’artefact validé. Et entre les deux, le monde a bougé.
Tout ça, ce sont des trous connus. Le problème, c’est qu’un humain expérimenté ralentit instinctivement devant chacun d’eux. L’agent, lui, fonce — sauf si on lui forge le rempart.
Le rempart a un nom : la discipline « diagnostic d’abord »
C’est là qu’intervient une trouvaille que je voulais partager depuis un moment : le
terraform-skill d’Anton Babenko
(oui, le Anton Babenko, celui des modules terraform-aws-modules que vous utilisez
sans le savoir). C’est un skill — un paquet de contexte et de règles qu’on injecte dans
l’agent pour transformer sa manière de raisonner sur l’infra.
L’idée centrale est d’une simplicité brutale : diagnostiquer avant de générer. Au lieu de cracher du HCL au premier prompt venu, l’agent est forcé de catégoriser d’abord la nature du risque : est-ce qu’on est sur de l’identity churn ? De l’exposition de secret ? Du blast radius ? De la corruption d’état ? Une table de routage le contraint à charger uniquement la connaissance pertinente, puis à proposer un correctif avec ses garde-fous.
Et surtout, chaque réponse de l’agent doit se terminer par ce que le skill appelle un Response Contract — un contrat de réponse. En gros, l’agent n’a pas le droit de dire « voilà ton code, bonne chance ». Il doit livrer :
- Ses hypothèses et son plancher de version — quel runtime, quelle version exacte, quel backend d’état, quelle criticité d’environnement. S’il ne sait pas, il l’écrit noir sur blanc au lieu de deviner.
- La catégorie de risque traitée.
- Le choix de remédiation et ses arbitrages — ce qu’on a choisi, ce qu’on a sacrifié, pourquoi.
- Un plan de validation — les commandes exactes (
fmt -check,validate,plan -out, contrôle de policy), calibrées sur le niveau de risque. - Les notes de rollback — pour tout changement destructeur : comment annuler, quelles preuves garder.
Et la règle qui aurait sauvé notre sandbox-eu de 3h17, gravée dans le marbre du
skill : jamais de terraform destroy sans avoir d’abord lancé terraform plan -destroy
et montré à l’humain chaque ressource qui va disparaître — y compris les dépendants
implicites traînés par les locals et les for_each. Et jamais, jamais, de
-auto-approve sur un destroy.
C’est exactement la marche qu’on n’avait pas mise. Le skill, lui, la met d’office.
L’orchestration : un agent ne suffit pas, il en faut trois
La vraie élégance arrive quand on arrête de penser « un agent qui fait tout » pour
penser chaîne d’agents spécialisés, calquée sur les étages d’un pipeline CI/CD propre :
validate → test → plan → apply.
Soyons précis sur ce qui vient d’où, parce que la nuance compte. Le skill, lui, ne parle pas de « trois agents » : ce qu’il impose, ce sont les étapes de ce pipeline, avec protection d’environnement. Découper ces étapes en agents distincts — Planner, Policy, Apply — c’est ma façon de matérialiser sa discipline, pas une prescription gravée dans le skill. L’idée d’orchestrer plusieurs agents spécialisés existe bien chez Babenko, mais ailleurs dans son écosystème : son plugin deliberation fait justement collaborer sept sous-agents experts (Architect, Plan Reviewer, Security Analyst, et compagnie). Ce qui suit est donc un montage — solide, mais assumé comme tel.
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Agent │ │ Agent │ │ Agent │ │ HUMAIN │
│ Planner │───▶│ Policy / │───▶│ Apply │───▶│ (approbation│
│ │ │ Security │ │ (gated) │ │ finale) │
│ génère le │ │ │ │ │ │ │
│ plan + le │ │ checkov, │ │ rejoue │ │ valide │
│ Response │ │ trivy, OPA │ │ l'ARTEFACT │ │ l'artefact │
│ Contract │ │ /Sentinel │ │ validé │ │ de plan │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
Le point crucial — celui que le skill martèle, lui, et que tout le monde oublie :
l’agent qui applique ne régénère jamais le plan. Il rejoue l’artefact de plan validé
produit à l’étape précédente. Pas de re-plan dans le job d’apply. Sinon vous
appliquez un futur que personne n’a relu. L’artefact de plan devient un contrat qui
circule de main en main, signé à chaque étape.
L’agent Policy, lui, passe trivy config . et checkov -d . sur chaque chemin vers
l’apply. Security group ouvert à 0.0.0.0/0 ? Bloqué. Chiffrement at-rest absent ?
Bloqué. Secret en clair dans une tfvars ? Bloqué avant même que l’humain regarde.
Une architecture 3-tiers, vraiment réaliste, sous discipline d’agent
Assez de théorie. Construisons une architecture 3-tiers classique sur AWS — la bonne vieille séparation présentation / applicatif / données — comme un agent correctement skillé la produirait. C’est-à-dire : pas du HCL de démo, du HCL qui survit au contact de la production.
L’arborescence d’abord (parce que c’est là que tout se joue)
Le skill impose de séparer franchement les environnements des modules, et de traiter
les examples/ comme à la fois doc et fixtures de test :
infra/
├── environments/
│ ├── prod/
│ │ ├── main.tf # composition : assemble les modules
│ │ ├── backend.tf # état distant, isolé par composant
│ │ ├── variables.tf
│ │ └── versions.tf
│ ├── staging/
│ └── dev/
└── modules/
├── networking/ # tier réseau : VPC, subnets, SG
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── versions.tf
│ └── examples/
│ ├── minimal/
│ └── complete/
├── web_tier/ # tier 1 — présentation (ALB, public subnets)
├── app_tier/ # tier 2 — applicatif (ASG, private subnets)
└── data_tier/ # tier 3 — données (RDS, isolated subnets)
Découpage du blast radius par état distant : le réseau, le compute et la base n’ont pas le même cycle de vie, donc pas le même fichier d’état. C’est le pattern hybride recommandé.
# environments/prod/backend.tf
terraform {
backend "s3" {
bucket = "acme-tfstate-prod"
key = "prod/data/terraform.tfstate" # un état par composant
region = "eu-west-3"
encrypt = true
use_lockfile = true # verrou natif S3 (Terraform 1.10+, fini DynamoDB)
}
}
Versions figées — sinon la drift vous rattrape
# versions.tf
terraform {
required_version = "~> 1.11" # plancher : on veut write_only sur les secrets
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.90" # write_only sur RDS = provider 5.x récent (~5.88+)
}
}
}
Deux planchers, pas un. required_version = "~> 1.11" n’est pas cosmétique : c’est le
plancher Terraform qui débloque les arguments write_only, indispensables pour le tier
données. Mais il ne suffit pas. Le support de write_only sur aws_db_instance n’est
arrivé que dans une version récente du provider AWS (autour de la 5.88). Un ~> 5.0
laisserait l’agent installer une 5.10 qui ignore purement et simplement password_wo
— et vous repartez avec le secret en clair dans l’état, persuadé d’avoir fait les
choses bien. D’où le ~> 5.90. On y revient.
Tier 1 — Présentation : le for_each, pas le count
Le réflexe naïf de l’agent serait de provisionner les subnets publics avec un count.
Sous discipline, il bascule sur un for_each indexé par AZ, pour que les adresses
restent stables quand on ajoute ou retire une zone.
# modules/web_tier/main.tf
locals {
public_subnets = {
"eu-west-3a" = "10.0.1.0/24"
"eu-west-3b" = "10.0.2.0/24"
"eu-west-3c" = "10.0.3.0/24"
}
}
resource "aws_subnet" "public" {
for_each = local.public_subnets # clé = AZ, adresse stable
vpc_id = var.vpc_id
cidr_block = each.value
availability_zone = each.key
map_public_ip_on_launch = true
tags = {
Name = "acme-public-${each.key}"
Tier = "web"
}
}
resource "aws_lb" "web" {
name = "acme-web-alb"
load_balancer_type = "application"
subnets = [for s in aws_subnet.public : s.id]
security_groups = [aws_security_group.web_alb.id]
enable_deletion_protection = true # le filet sous le funambule
}
Notez enable_deletion_protection = true. C’est précisément le genre de garde-fou
qu’un agent pressé saute et qu’un agent discipliné colle par défaut sur tout ce qui
regarde Internet.
Le security group : règles séparées, jamais en inline
Piège classique : le modèle adore les blocs ingress/egress inline dans
aws_security_group. Le skill l’interdit (sur provider AWS v5+) au profit de ressources
de règles séparées — sinon chaque modif réécrit tout le SG et brouille les diffs.
resource "aws_security_group" "web_alb" {
name_prefix = "acme-web-alb-"
vpc_id = var.vpc_id
# AUCUN bloc ingress/egress ici — on les externalise
}
resource "aws_vpc_security_group_ingress_rule" "web_https" {
security_group_id = aws_security_group.web_alb.id
cidr_ipv4 = "0.0.0.0/0" # HTTPS public : assumé, le reste est fermé
from_port = 443
to_port = 443
ip_protocol = "tcp"
}
Tier 2 — Applicatif : isolé, jamais exposé
L’app tier vit dans des subnets privés. Son SG n’accepte que le trafic venant du SG de l’ALB — pas un CIDR, une référence de SG. Moindre privilège, à la lettre.
resource "aws_vpc_security_group_ingress_rule" "app_from_alb" {
security_group_id = aws_security_group.app.id
referenced_security_group_id = var.web_alb_sg_id # source = l'ALB, point
from_port = 8080
to_port = 8080
ip_protocol = "tcp"
}
Tier 3 — Données : le secret qui ne touche jamais l’état
Le clou du spectacle, et la leçon de notre drame d’ouverture. Sur du Terraform < 1.11,
le mot de passe de la base finit toujours en clair dans le .tfstate, même marqué
sensitive. Sur 1.11+ (et avec un provider AWS assez récent), on a les arguments
write_only : la valeur transite, configure la ressource, et n’est jamais persistée
dans l’état.
# modules/data_tier/variables.tf
variable "db_password_wo" {
description = "Mot de passe maître RDS — write-only, jamais stocké dans l'état"
type = string
sensitive = true
}
variable "db_password_wo_version" {
description = "Incrémenter pour déclencher une rotation du secret"
type = number
default = 1
}
# modules/data_tier/main.tf
resource "aws_db_instance" "main" {
identifier = "acme-prod-db"
engine = "postgres"
instance_class = "db.r6g.large"
username = "acme_admin"
password_wo = var.db_password_wo # write-only
password_wo_version = var.db_password_wo_version # rotation
storage_encrypted = true # chiffrement at-rest non négociable
multi_az = true
backup_retention_period = 30
deletion_protection = true
skip_final_snapshot = false # on garde un snapshot final, toujours
db_subnet_group_name = aws_db_subnet_group.isolated.name
vpc_security_group_ids = [aws_security_group.data.id]
}
Idéalement, ce mot de passe ne vient même pas d’une variable mais d’une lecture runtime
dans AWS Secrets Manager (via une ressource ephemeral, qui ne touche elle non plus ni
l’état ni le plan). Mais si on doit le passer par Terraform, write_only est la
différence entre « un secret de prod traîne dans un bucket S3 versionné » et « il
n’existe nulle part sur le disque ».
Et quand on refactore : moved, pas destroy/recreate
Dernier réflexe d’agent discipliné. Renommer une ressource ? On n’efface pas pour
recréer — on déclare un bloc moved, et terraform plan doit afficher zéro
destruction :
moved {
from = aws_db_instance.database # ancien nom
to = aws_db_instance.main # nouveau nom
}
C’est ça, la différence entre un copilote qui écrit du HCL et un agent qui opère de l’infrastructure. Le premier vous donne du code. Le second vous donne du code plus le raisonnement sur ce que ce code va casser — et refuse d’appuyer sur le bouton rouge sans vous montrer le rayon de souffle.
La chute
On ne va pas se mentir : l’infrastructure moderne reste un chaos entropique. Des couches
sur des couches, des providers qui dérivent, des locals fossilisés qui attendent leur
heure au fond d’un fichier que plus personne n’ose ouvrir. Le legacy ne meurt pas, il
sédimente. Et maintenant, on lâche là-dedans des agents qui amplifient, à la vitesse de
la machine, autant notre rigueur que notre négligence.
Mais voilà la note d’espoir — cynique, mesurée, mais réelle. Le rempart existe. Il ne consiste pas à interdire aux agents de toucher à l’infra : c’est un combat perdu, et c’est aussi passer à côté d’un levier énorme. Le rempart, c’est d’encoder la discipline elle-même — le diagnostic avant la génération, le Response Contract qui force l’agent à dire ses hypothèses, l’artefact de plan qui circule comme un contrat signé, et cette dernière barrière irréductible : un humain qui approuve, en connaissance de cause, parce que l’agent lui a montré les vingt-trois ressources avant d’en toucher une seule.
On ne dompte pas le chaos. On construit des murs assez vite pour rester devant lui.
Et un skill bien écrit, injecté au bon endroit, c’est exactement ça : un mur qu’on n’a à bâtir qu’une fois, et que l’agent porte ensuite dans chacune de ses décisions. À 3h17 du matin, surtout.
Pour creuser : le terraform-skill d’Anton Babenko est sous licence Apache 2.0, avec ses fichiers de référence sur le state management, la CI/CD, la sécurité et les patterns de code. À lire avant de donner les clés de votre prod à quoi que ce soit — agent ou humain.