State, backend distant, verrouillage, modules et plan d’exécution : l’infrastructure comme code en entretien.
Cliquez sur une question pour dérouler la réponse attendue.
Le state est la carte entre une adresse dans votre code (aws_s3_bucket.logs) et l’objet réel chez le fournisseur (l’ARN, l’identifiant, les attributs lus à la dernière synchronisation). HashiCorp le décrit ainsi : sans cette correspondance, Terraform ne peut pas décider de manière fiable quoi créer, modifier ou détruire.
Ce n’est pas un cache facultatif. C’est la mémoire du graphe. Le plan compare trois choses : la configuration (ce que vous voulez), le state (ce que Terraform croit posséder) et le monde réel (après un refresh). Enlevez le state et Terraform revoit chaque ressource comme nouvelle : il tente de recréer, ou il refuse, selon les collisions d’identifiants.
Pas la phrase « c’est un fichier JSON ». Il veut que vous disiez à quoi il sert au moment du plan : conserver les IDs, les dépendances, les métadonnées privées du provider, et éviter de redemander à l’API « qui es-tu ? » pour chaque objet.
La phrase qui tranche : le code décrit l’intention, le state décrit la possession. Deux dépôts identiques sans le même state ne gèrent pas la même infrastructure.
Deux laptops, deux fichiers, deux vérités. Sans state partagé ni verrou, le dernier apply écrase l’autre et personne ne sait plus ce qui existe vraiment.
Lire la réponse détailléeUn backend distant n’est pas « un bucket ». HashiCorp attend un service qui tient cinq promesses : une seule copie du state, un verrou pendant plan/apply, le chiffrement au repos et en transit, un historique (versionnement) pour restaurer, et un contrôle d’accès (IAM, rôles CI, pas une clé dans le dépôt).
Sur AWS, le motif classique reste S3 (stockage + versioning + encrypt) et un verrou : table DynamoDB historique, ou fichier de lock natif S3 selon la version du CLI. HCP Terraform / Terraform Cloud fournit la même chose en produit : state, lock, politiques, et souvent l’exécution distante.
Pas la syntaxe du bloc backend "s3". Il veut que vous sépariez ce que le backend doit faire de l’implémentation du jour. Si vous ne citez que S3 sans lock, vous avez décrit un disque partagé, pas un backend d’équipe.
Mentionnez l’œuf et la poule : le bucket de state doit exister avant le premier init qui l’utilise. On le bootstrap à part, on ne le crée pas dans le même module que l’application — Terraform ne peut pas ranger son state dans une ressource qu’il est en train de naître.
Sans verrou, chacun lit le même state, applique, puis le dernier écrase la carte de l’autre. Avec verrou, le second attend ou échoue : une seule écriture à la fois.
Lire la réponse détailléeLe state stocke les attributs renvoyés par l’API, souvent en clair. sensitive = true masque l’affichage ; il n’efface ni le JSON d’état ni les journaux d’un backend mal chiffré.
Lire la réponse détailléeOn ne lit pas « tout est vert ». On lit le verbe et le risque de chaque ligne.
-/+) : remplacement. Terraform détruit puis recrée. Identifiant nouveau, disque neuf, IP neuve. Un changement d’argument ForceNew se lit ici, pas dans le tilde.En bas, le décompte : Plan: 2 to add, 1 to change, 0 to destroy. Un destroy non prévu = on n’applique pas, on explique.
Que vous cherchez les moins avant les plus. Qu’un remplacement n’est pas « une update ». Que vous savez dire pourquoi une ressource est ForceNew (AMI, certain name, AZ) plutôt que de réciter les symboles.
Le plan affiché sur une pull request est spéculatif : refresh à l’instant T, configuration de la branche, souvent un rôle en lecture. Entre ce commentaire et le bouton Apply, le monde a pu changer : un collègue a appliqué, quelqu’un a cliqué dans la console, un certificat a tourné.
Le plan final est celui qui est calculé juste avant l’apply, ou mieux : le fichier de plan produit dans le pipeline d’apply (terraform plan -out=… puis terraform apply planfile). HashiCorp pousse ce couple pour que l’humain approuve ce diff, pas un souvenir de lundi.
En entretien, la phrase utile : un plan n’est pas une promesse, c’est une photographie. Appliquer sans recalcul ni fichier sauvé, c’est signer un autre contrat. D’où le refus d’un apply laptop « j’ai déjà vu le plan sur GitHub ».
On module ce qu’on a déjà recopié et dont l’interface s’est stabilisée. Un module pour une seule resource, ou trop tôt, cache le graphe et multiplie les versions à maintenir.
Lire la réponse détailléeLe module racine est le répertoire où vous lancez terraform init / plan / apply. C’est lui qui déclare le backend, les providers (ou les passe), et compose les enfants. Il n’y a qu’un apply, donc un state, pour cette racine.
Un module enfant est appelé par un bloc module. Il n’a pas de backend à lui. Ses resources ont une adresse préfixée (module.reseau.aws_vpc.this). Il reçoit des variables, expose des outputs. Vous ne l’appliquez pas tout seul — sauf pendant un développement isolé, ce qui n’est pas la prod.
Que vous ne mettez pas un bloc backend dans un module réutilisable. Que vous savez lire une adresse module.* dans un plan. Que « casser un module en deux » change les adresses — donc risque de recréation si vous n’avez pas moved ou state mv.
.terraform.lock.hcl épingle les versions exactes des providers et leurs empreintes. Ce n’est pas le verrou du state (Alice et Bob). C’est la reproductibilité : le plan de lundi et l’apply de mardi parlent au même plugin.
On commite ce fichier. On ne le gitignore pas « parce que généré ». En CI Linux alors que vous avez locké sous Windows, les hashes manquent : terraform providers lock pour les plateformes du pipeline.
required_providers pose une contrainte (~> 5.0). Le lock file choisit 5.82.1 jusqu’à ce que quelqu’un fasse terraform init -upgrade volontairement, puis revue le plan provider. L’intervieweur veut cette séparation : contrainte dans le code, épingle dans le lock, upgrade explicite.
Trois rôles, trois directions.
sensitive = true masque l’affichage, pas le state.En entretien : une valeur qui change selon l’environnement est une variable ; une convention de nommage est un local ; un identifiant dont un autre stack a besoin est un output (ou un data source / remote state, selon le découpage).
Dans Git : les exemples (terraform.tfvars.example, dev.tfvars sans secret) et la liste des clés attendues. Ça documente le contrat.
Hors Git : tout fichier qui porte un mot de passe, un token, une clé d’accès, un compte de prod recopié. *.auto.tfvars et terraform.tfvars sont souvent dans .gitignore précisément parce que les gens y collent des secrets « pour que ça marche en local ».
La CI n’a pas besoin d’un tfvars commité secret : TF_VAR_…, un store (Vault, SSM, variables de pipeline masquées), ou un fichier généré sur le runner et détruit après. L’intervieweur écoute le réflexe Git : si vous commitez prod.tfvars avec le CIDR, ça va ; avec le mot de passe RDS, c’est éliminatoire — et ce mot de passe serait de toute façon recopié dans le state.
count indexe par entier. Les adresses sont aws_instance.web[0], [1]. Retirer l’élément du milieu décale les indices : Terraform voit des remplacements en cascade. C’est le piège classique, et c’est pour ça que la question existe.
for_each indexe par clé (set ou map). Les adresses sont aws_instance.web["alpha"]. Supprimer bravo ne touche pas alpha. HashiCorp pousse for_each dès que les instances ont une identité métier.
count reste légitime pour « zéro ou un » (count = var.enabled ? 1 : 0), pas pour une liste d’environnements que quelqu’un va réordonner. En entretien, raconter le décalage d’indice vaut plus que la syntaxe.
Terraform construit déjà un graphe dès que vous référencez un attribut (subnet_id = aws_subnet.a.id). Cette arête est visible, minimale, et suit la donnée.
depends_on ajoute une arête opaque : « attends que tout ça soit fini », sans dire pourquoi. Le plan devient plus séquentiel, les destructions plus nerveuses, et le prochain lecteur ne sait pas si c’était une vraie contrainte ou de la peur. HashiCorp le documente comme exception : effets de bord sans attribut (propagation IAM, DNS pas encore servi, un provider qui ment sur ses IDs).
En entretien : d’abord l’interpolation, ensuite un data source qui attend, ensuite seulement depends_on commenté. Une forêt de depends_on signale un graphe que l’auteur ne maîtrise plus.
Une resource est un objet dont Terraform possède le cycle de vie : create, update, destroy, et une entrée dans le state.
Une data source lit quelque chose qui existe déjà (AMI récente, VPC partagé, secret dans SSM). Elle n’est pas censée le créer. Si l’objet disparaît, le plan échoue à la lecture, il ne le recrée pas.
L’erreur d’entretien : data sur une resource que votre root gère déjà — double vérité, courses, drifts. Ou l’inverse : recréer un VPC « lookup » alors qu’il appartient à un autre state. On lit avec data (ou remote state) ce qu’on ne possède pas ; on déclare resource ce qu’on a le droit de détruire.
La dérive (drift), c’est l’écart entre le state et le monde réel : quelqu’un a changé un security group dans la console, un autoscaling a modifié une capacité, un autre outil a retouché un tag.
Terraform la voit au refresh (implicite au plan, sauf -refresh=false) : le provider relit l’objet, met à jour le state en mémoire, puis compare à la configuration. Le plan affiche alors un tilde ou un remplacement que personne n’a demandé dans le code.
Ce que l’intervieweur veut : vous ne « corrigez » pas la dérive en éditant le state à la main. Soit vous appliquez pour reconverger vers le code (le code gagne), soit vous changez le code pour adopter le réel (le clic avait raison), soit vous import / ajustez si l’objet n’était pas géré. Ignorer la dérive, c’est mentir au prochain apply.
import adopte un objet déjà existant : il écrit l’association adresse ↔ ID dans le state, sans le recréer. Cas typiques : infra née à la console, outil précédent, recovery.
Depuis 1.5, le bloc import dans le code + un plan est préférable à la commande impérative : la revue voit le geste. Après import, le premier plan doit être lu comme un audit : idéalement vide, souvent quelques attributs à aligner (tags, defaults du provider). Un plan qui veut détruire juste après import, c’est une adresse ou un ID faux — on n’applique pas.
Ce n’est pas une migration magique : il faut écrire la resource qui correspond. Importer sans configuration, c’est un state orphelin au prochain refresh.
-replace=ADRESSE force un remplacement même si le plan aurait fait une update — ou pour recréer un objet dont le provider ne peut pas réparer l’intérieur (instance « pourrie », rotation d’une resource sans attribut ForceNew propre).
C’est le successeur assumé de terraform taint (déprécié) : le geste est dans le plan, visible, pas une marque cachée dans le state.
On s’en sert rarement, et toujours en lisant le -/+ et les dépendants qui suivent. Ce n’est pas un « restart » : c’est détruire et recréer, avec nouvel ID. En prod, autant qu’un destroy ciblé : sauvegarde, fenêtre, et un plan sauvé, pas un réflexe de dépannage sur le laptop.
Le graphe est tronqué : Terraform applique un îlot et ignore le reste. Les dépendances non ciblées restent dans l’ancien monde, le state ment, le prochain plan complet surprend.
Lire la réponse détailléeLes workspaces partagent le même code et changent surtout la clé de state : un mauvais workspace et c’est la prod. Des dossiers (ou des roots) rendent l’environnement explicite, au prix de un peu de duplication.
Lire la réponse détailléeUn state, un apply, un rayon d’explosion. On sépare ce qui ne doit pas se détruire ensemble : réseau durable, données, application. Trop de morceaux, et les dépendances deviennent un plat de nouilles.
Lire la réponse détailléeprevent_destroy = true fait échouer le plan si Terraform veut détruire cette resource. Ce n’est pas un verrou cloud : on peut encore détruire à la console, ou retirer le lifecycle et réappliquer. C’est un frein d’équipe pour les objets irremplaçables (base, bucket d’audit, KMS). Sur tout le root, ça devient du théâtre : plus rien ne peut se refactorer.
ignore_changes dit à Terraform : ne reconverge pas ces attributs. Cas honnêtes : une capacité d’ASG que l’autoscaling possède, un tag posé par un autre système, un mot de passe rotaté hors bande. Cas pourris : ignorer parce que le plan fait peur. Vous instituez de la dérive officielle. HashiCorp le présente comme exception.
En entretien : nommer le propriétaire réel de l’attribut. S’il n’est plus Terraform, soit vous ignore_changes ciblé, soit vous sortez la resource du state. Pas les deux en silence.
Les provisioners (local-exec, remote-exec, file) sortent du déclaratif. Terraform ne sait plus décrire l’état d’un script bash : si le create réussit et le provisioner échoue, la resource est souvent marquée failed / tainted, et le retry est un cauchemar. Ils cassent aussi le plan : rien dans le diff ne montre « apt-get va tourner ».
HashiCorp le dit depuis des années : dernier recours. Préférer user_data / cloud-init, une image (Packer), un outil de configuration après que l’infra existe, ou l’API native du provider. Un local-exec pour appeler kubectl ou ansible dans le même apply, c’est ce que l’intervieweur attend que vous refusiez en prod — sauf bootstrap vraiment sans alternative, isolé, et idempotent.
La phrase utile : Terraform crée des objets ; il ne remplace pas Ansible.
Parce que le code est copié : Git, forks, CI logs, extraits collés dans un ticket. Un secret en clair dans un .tf ou un tfvars commité a déjà fuité, même après un revert.
Ensuite, même injecté proprement, beaucoup de secrets atterrissent dans le state (et parfois dans le plan). D’où backend chiffré, accès restreint, et, quand c’est possible, secret manager + références, ou valeurs éphémères pour ne pas persister.
En entretien, la checklist courte : pas dans le dépôt, pas dans l’image Docker du runner, pas dans un output non protégé affiché sur un job public, rotation si ça a fuité. sensitive = true n’est pas cette checklist — c’est un masque d’affichage.
La PR ne fait que formater, valider et montrer un plan. L’apply n’a lieu que sur la branche protégée, avec le fichier de plan approuvé, jamais depuis un laptop de prod.
Lire la réponse détailléeTerraform est déclaratif sur des API : vous décrivez des objets (VPC, DNS, rôle IAM), il calcule un graphe et converge via le provider. Son cœur est le state.
Ansible est surtout convergente sur des machines (et des API, oui) : playbooks, idempotence par module, inventaire. Pas de state mondial comparable ; la vérité est « le nœud ressemble-t-il au play ? ».
On les empile plus qu’on les oppose : Terraform pose le VM et le security group ; Ansible (ou cloud-init, ou une image) installe le paquet. L’erreur d’entretien : tout faire en provisioners Terraform, ou gérer un VPC entier en playbooks impératifs. Dire « déclaratif contre impératif » est trop court — Ansible a des modules idempotents — mais possession par state contre convergence par SSH/API répétée tient.
required_version borne le binaire Terraform (>= 1.8, ~> 1.9). Un langage et un state trop nouveaux pour un vieux CLI, ou l’inverse, cassent l’init. On l’aligne sur ce que la CI installe.
required_providers borne les plugins (hashicorp/aws ~> 5.0). Ce sont eux qui parlent à l’API et qui décident ForceNew, arguments, bugs. Le lock file épingle l’exact.
Deux contraintes, deux calendriers d’upgrade. En entretien : on n’upgrade pas le provider AWS au milieu d’un vendredi sans plan ; on n’installe pas Terraform 1.x.y « latest » sur le laptop et 1.2 en CI. Le désaccord CLI/provider est une source classique de « ça passe chez moi ».
D’abord : on ne lance presque jamais terraform destroy sur un root de prod. On retire des resources du code, on lit un plan qui ne contient que les moins voulus, on applique ce plan.
Si destroy il y a (environnement éphémère, fin de projet) : le plan liste-t-il uniquement ce root ? Le backend est-il le bon ? prevent_destroy a-t-il sauté pour une bonne raison ? Sauvegardes (RDS snapshot, bucket versionné) réellement restaurables, pas « on verra » ? Les dépendants d’autres states (remote state) sont-ils déjà débranchés, sinon l’apply d’à côté explosera après ?
Pas d’-auto-approve. Pas de destroy ciblé improvisé pour « juste le cluster » si le graphe tire la base. L’intervieweur veut de la peur utile : compter les moins, nommer les irremplaçables, et savoir dire non.
terraform state mv renomme une adresse dans le state sans toucher l’objet cloud. Cas légitime : vous sortez une resource vers un module (aws_vpc.main → module.reseau.aws_vpc.main) et, sans ça, le plan ferait destroy + create.
C’est rare parce que c’est de la chirurgie : un mauvais source/dest, et Terraform croit qu’il faut recréer, ou le state pointe vers le vide. On sauvegarde d’abord (state pull), on verrouille, on mv, on plan — le plan doit être vide ou minuscule.
Depuis 1.1, les blocs moved dans le code sont souvent préférables : le geste est revu en PR, reproductible en CI, plus un lore de commandes laptop. state rm / édition JSON : encore plus rare, incident seulement.
En entretien : « je refactor à la main le JSON » est le mauvais récit. « moved, plan vide, puis on merge » est le bon.
On écrit l’intention, on calcule un plan, un humain lit surtout les destructions, on applique ce plan-là. Terraform n’est pas une console plus verbeuse : c’est cette boucle de confiance.
Lire la réponse détaillée