
Un plan qui propose des changements alors que personne n'a touché au code
recouvre deux situations très différentes : l'objet réel a été modifié en
dehors de Terraform, c'est une dérive, ou bien il existe sans avoir jamais
été enregistré, c'est une orpheline. Ce guide s'adresse à qui doit trancher
entre les deux, puis réparer : plan -refresh-only pour isoler la dérive,
apply -refresh-only pour la réconcilier, et le bloc import pour
adopter une orpheline. Vous verrez aussi pourquoi un plan lancé en intégration
continue peut ne rien voir du tout.
Tous les comportements de ce guide, sorties, codes de retour et champs
JSON, ont été rejoués sur Terraform v1.15.4 avec les providers
hashicorp/local et hashicorp/random.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Distinguer une dérive d'une orpheline, qui ne se réparent pas pareil
- Isoler la dérive avec un plan refresh-only, enregistré et relisible
- Réconcilier le state sans que l'infrastructure bouge
- Adopter une ressource existante, et faire générer sa configuration
- Lire ce que disent vraiment les deux codes de sortie du plan
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
Le bloc
importexiste depuis la 1.5. - Savoir ce qu'est la dérive et comment le refresh la révèle : comprendre le state pose le concept, ce guide en fait une procédure.
- Pour la même opération sur une infrastructure cloud réelle, importer et détecter le drift sur AWS reprend ces gestes avec une instance EC2.
Les exemples reposent sur une configuration jetable, sans cloud :
terraform { # Version epinglee : le bloc `import` exige la 1.5, et les sorties citees # plus bas sont celles de la branche 1.15.x. required_version = ">= 1.7" required_providers { local = { source = "hashicorp/local", version = ">= 2.5" } random = { source = "hashicorp/random", version = ">= 3.6" } }}
# Un fichier sur le disque : quelqu'un ira le corriger a la main, et c'est# exactement la derive qu'on veut savoir diagnostiquer.resource "local_file" "bulletin" { filename = "${path.root}/sortie/bulletin.txt" content = "bulletin du jour\n"}Qu'est-ce qu'une dérive, exactement ?
Section intitulée « Qu'est-ce qu'une dérive, exactement ? »Un écart entre le state et le monde réel, pas entre le state et le code. La distinction paraît subtile, elle décide de tout : Terraform compare trois choses, votre code, son state, et les objets réels qu'il interroge à chaque plan lors du refresh.
| Écart | Nom | Réparation |
|---|---|---|
| entre le réel et le state | dérive | apply -refresh-only |
| entre le state et le code | changement à appliquer | apply |
| objet réel absent du state | orpheline | bloc import |
Appliquez la configuration ci-dessus, puis provoquez l'incident que tout le monde a déjà vécu, la correction faite directement sur le serveur :
echo "corrige a la main" > sortie/bulletin.txtPourquoi un plan ordinaire ne suffit pas au diagnostic
Section intitulée « Pourquoi un plan ordinaire ne suffit pas au diagnostic »Parce qu'il mélange les deux informations. Il vous dit qu'il veut recréer le fichier, sans dire si c'est le réel qui a bougé ou le code. Le JSON du plan le montre bien, la même adresse est rangée deux fois :
terraform plan -out=ordinaire.tfplanterraform show -json ordinaire.tfplan | jq '{ drift: [.resource_drift[]?.address], changes: [.resource_changes[]?.address]}'{ "drift": ["local_file.bulletin"], "changes": ["local_file.bulletin"]}Le champ resource_drift décrit ce que le refresh a découvert ;
resource_changes décrit ce que l'apply ferait. Un plan ordinaire
remplit les deux, et c'est pour cela qu'il ne tranche rien.
Isoler la dérive avec plan -refresh-only
Section intitulée « Isoler la dérive avec plan -refresh-only »Le mode refresh-only ne propose aucune modification de l'infrastructure : il décrit uniquement le rattrapage du state. Enregistrez-le, puis convertissez le en JSON :
terraform plan -refresh-only -out=derive.tfplanterraform show -json derive.tfplan > derive-plan.jsonjq '{drift: [.resource_drift[]?.address], changes: [.resource_changes[]?.address]}' derive-plan.json{ "drift": ["local_file.bulletin"], "changes": []}resource_changes est vide : c'est la signature du diagnostic. Vous tenez la
preuve que l'écart vient du réel, et vous la tenez sous une forme archivable.
Un fichier de plan étant un binaire, la conversion en JSON n'a rien de
cosmétique : c'est elle qui rend la dérive lisible en revue, attachable à un
ticket, vérifiable par un script.
Réconcilier, puis remettre le réel en conformité
Section intitulée « Réconcilier, puis remettre le réel en conformité »terraform apply -refresh-only inscrit la dérive dans le state, sans toucher
à l'infrastructure. Il demande confirmation, contrairement à l'ancienne
commande terraform refresh, que la documentation déclare dépréciée et
équivalente à apply -refresh-only -auto-approve.
terraform apply -refresh-onlyÀ ce stade, le state dit la vérité : le fichier contient bien
corrige a la main. Mais cette vérité ne correspond pas au code. Un apply
ordinaire tranche alors dans le sens du dépôt, et remet le fichier en
conformité.
Que disent vraiment les deux codes de sortie ?
Section intitulée « Que disent vraiment les deux codes de sortie ? »-detailed-exitcode rend 0 sans changement, 2 avec, 1 en cas
d'erreur. Le 2 signale un diff non vide, pas une dérive : c'est la nuance qui
fait écrire de fausses alertes en intégration continue. Appliqué aux deux formes
de plan, il devient en revanche un vrai outil de diagnostic :
| Commande | Ce que le code 2 signifie |
|---|---|
terraform plan -detailed-exitcode | le réel ne correspond pas au code |
terraform plan -refresh-only -detailed-exitcode | le state ne correspond pas au réel |
Mesuré sur la séquence ci-dessus : après l'apply -refresh-only, le refresh-only
rend 0 alors que le plan ordinaire rend encore 2. Les deux ne tombent à
0 qu'une fois le fichier remis en conformité.
Adopter une ressource existante avec un bloc import
Section intitulée « Adopter une ressource existante avec un bloc import »Le bloc import, déclaratif depuis Terraform 1.5, rattache un objet
existant à une adresse du state. Deux attributs, l'adresse visée et
l'identifiant de l'objet chez le provider :
resource "random_string" "credential" { length = 22}
import { to = random_string.credential id = "V3ryS3cretL3gacyStr1ng"}Le plan annonce l'adoption, avec un compteur qui lui est propre :
# random_string.credential will be importedPlan: 1 to import, 0 to add, 0 to change, 0 to destroy.Côté JSON, l'entrée porte "actions": ["no-op"] et un champ importing :
rien n'est créé ni détruit, l'objet change de statut. Le format de l'id
appartient au provider : un ARN chez AWS, un identifiant numérique ailleurs, et
pour random_string, la valeur elle-même.
Faire générer la configuration depuis le bloc import
Section intitulée « Faire générer la configuration depuis le bloc import »Un bloc import seul, sans bloc resource, échoue :
Error: Configuration for import target does not existMais Terraform sait écrire ce bloc pour vous, depuis la 1.5, avec l'option
-generate-config-out :
terraform plan -generate-config-out=genere.tf # random_string.credential will be importedPlan: 1 to import, 0 to add, 0 to change, 0 to destroy.Terraform has generated configuration and written it to genere.tf.Le fichier produit porte les valeurs réelles de l'objet, en-tête compris :
# __generated__ by Terraform from "V3ryS3cretL3gacyStr1ng"resource "random_string" "credential" { keepers = null length = 22 lower = true ... special = true upper = true}L'affirmation courante selon laquelle « le bloc resource doit exister avant
l'import » ne vaut donc que pour l'ancienne commande terraform import. Avec
un bloc, la génération fait le travail, et la doc rappelle que la
fonctionnalité reste expérimentale : relisez ce qu'elle écrit avant de le
committer.
Pourquoi un plan en intégration continue peut ne rien voir
Section intitulée « Pourquoi un plan en intégration continue peut ne rien voir »Voici le piège qui rend une surveillance silencieuse. L'option
-refresh=false, souvent ajoutée en CI pour gagner du temps, demande à
Terraform de ne pas interroger les objets réels. Le plan compare alors le
code au state, et rien d'autre. Mesuré sur la dérive de tout à l'heure :
| Commande | Code de sortie |
|---|---|
terraform plan -detailed-exitcode | 2 |
terraform plan -refresh=false -detailed-exitcode | 0 |
terraform plan -refresh-only -detailed-exitcode | 2 |
Un plan sans refresh est aveugle à la dérive. Une chaîne d'automatisation qui
surveille la dérive doit donc laisser le refresh actif, et poser ses contrôles
sur plan -refresh-only -detailed-exitcode, avec -input=false, -lock-timeout
sur un backend partagé, et -out pour conserver l'artefact plutôt que de
jeter la sortie.
Pourquoi un import annonce parfois un remplacement
Section intitulée « Pourquoi un import annonce parfois un remplacement »Parce que les attributs déclarés dans la configuration sont ignorés à
l'import. La documentation du provider random est explicite : les valeurs
présentes dans la configuration « will be ignored during import », les
défauts du schéma s'appliquent, et cela « can trigger resource replacement ».
Déclarez length = 32 pour une chaîne qui en fait 22, et le plan devient :
# random_string.credential must be replaced # (imported from "V3ryS3cretL3gacyStr1ng") # Warning: this will destroy the imported resource ~ length = 22 -> 32 # forces replacement ~ result = "V3ryS3cretL3gacyStr1ng" -> (known after apply)Plan: 1 to import, 1 to add, 0 to change, 1 to destroy.L'avertissement est explicite et le JSON le confirme,
"actions": ["delete", "create"] avec "replace_paths": [["length"]]. Mais il
n'apparaît qu'au plan : un apply -auto-approve lancé sans lire emporte la
valeur, et le plan suivant annonce paisiblement No changes. La perte devient
alors indétectable.
Deux remèdes, selon l'intention : aligner le code sur les valeurs réelles, ou poser un garde-fou quand la valeur héritée ne peut pas être regénérée.
resource "random_string" "credential" { length = 32
lifecycle { ignore_changes = all }}Dépannage
Section intitulée « Dépannage »Le réflexe qui évite la plupart de ces situations : lire le plan avant d'appliquer, et laisser un artefact derrière chaque diagnostic.
| Symptôme | Cause probable | Solution |
|---|---|---|
| Le plan veut recréer un objet qui existe | dérive, ou orpheline | plan -refresh-only pour trancher |
resource_drift rempli, resource_changes vide | dérive confirmée | apply -refresh-only, puis apply |
plan -detailed-exitcode rend 2 après réconciliation | le réel diverge du code | Un apply ordinaire, ou corriger le code |
| La CI ne détecte aucune dérive | -refresh=false est actif | Retirer l'option, contrôler en refresh-only |
Configuration for import target does not exist | Bloc import sans bloc resource | Écrire le bloc, ou -generate-config-out |
L'import annonce must be replaced | Attributs déclarés ignorés à l'import | Aligner le code, ou ignore_changes |
La valeur importée a changé après l'apply | Le remplacement a été applique sans lecture | Restaurer la valeur, puis poser le garde-fou |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab diagnostiquer le state vous fait provoquer la dérive vous-même, puis la prouver par un plan refresh-only converti en JSON, avant d'adopter un jeton hérité qu'un service tiers connaît, donc impossible à regénérer. Les tests comparent le jeton du state au fichier d'origine : la réponse incomplète, celle qui oublie le garde-fou, produit un plan parfaitement propre mais perd la valeur. Il se joue hors ligne.
À retenir
Section intitulée « À retenir »- Une dérive est un écart entre le state et le réel ; une orpheline est un objet absent du state. Les réparations diffèrent.
- Un plan ordinaire range la dérive dans
resource_driftet dansresource_changes: il ne tranche rien. plan -refresh-onlylaisseresource_changesvide : c'est la signature du diagnostic, à enregistrer avec-outpuis à convertir en JSON.- Le code 2 de
-detailed-exitcodesignale un diff non vide, pas une dérive. Le signal fiable estplan -refresh-only -detailed-exitcode. -refresh=falserend le plan aveugle à la dérive : mesuré, il rend 0 là où le plan ordinaire rend 2.terraform refreshest dépréciée et équivaut àapply -refresh-only -auto-approve, donc sans confirmation.- Le bloc
importadopte une orpheline ;-generate-config-outécrit le blocresourceà votre place, en expérimental. - Les attributs déclarés sont ignorés à l'import : un écart déclenche un
remplacement, que seul le plan annonce.
ignore_changesprotège une valeur héritée.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui trompe le plus : le code de sortie qu'on interprète comme une alerte de dérive, la CI qui ne voit rien, et l'import qui détruit ce qu'il devait reprendre.
Le plan refresh-only isole la dérive
terraform plan -refresh-only -out=derive.tfplan
terraform show -json derive.tfplan > derive-plan.json
{
"drift": ["local_file.bulletin"],
"changes": []
}
Un plan ordinaire range la même adresse dans les deux listes, resource_drift et resource_changes : il ne permet donc pas de trancher entre « le réel a bougé » et « le code a changé ».La conversion en JSON n'est pas cosmétique : un fichier de plan est un binaire, illisible en revue comme dans un diff.Deux commandes, deux significations
| Commande | Ce que le code 2 signifie |
|---|---|
terraform plan -detailed-exitcode |
le réel ne correspond pas au code |
terraform plan -refresh-only -detailed-exitcode |
le state ne correspond pas au réel |
apply -refresh-only, le refresh-only rend 0 alors que le plan ordinaire rend encore 2. Le state dit alors la vérité, mais cette vérité ne correspond pas au code.Une alerte de surveillance branchée sur le mauvais code de sortie crie donc au drift à chaque changement en attente.Un plan sans refresh est aveugle
Mesuré sur Terraform 1.15.4, face à une dérive bien réelle :| Commande | Code de sortie |
|---|---|
terraform plan -detailed-exitcode |
2 |
terraform plan -refresh=false -detailed-exitcode |
0 |
terraform plan -refresh-only -detailed-exitcode |
2 |
plan -refresh-only -detailed-exitcode, avec -input=false, -lock-timeout sur un backend partagé, et -out pour conserver l'artefact.Terraform peut écrire le bloc pour vous
Un blocimport seul échoue : Error: Configuration for import target does not exist. Mais depuis la 1.5 :terraform plan -generate-config-out=genere.tf
# __generated__ by Terraform from "V3ryS3cretL3gacyStr1ng"
resource "random_string" "credential" {
length = 22
special = true
upper = true
}
La fonctionnalité reste expérimentale : relisez ce que Terraform écrit avant de le committer, et déplacez le bloc dans vos fichiers de configuration.L'avertissement n'apparaît qu'au plan
# random_string.credential must be replaced
# (imported from "V3ryS3cretL3gacyStr1ng")
# Warning: this will destroy the imported resource
~ length = 22 -> 32 # forces replacement
Le JSON confirme, "actions": ["delete", "create"] avec "replace_paths": [["length"]]. Un apply -auto-approve lancé sans lire emporte la valeur, et le plan suivant annonce paisiblement No changes : la perte devient indétectable.Deux remèdes : aligner le code sur les valeurs réelles, ou poser lifecycle { ignore_changes = all } quand la valeur héritée ne peut pas être regénérée.Un alias sans garde-fou
La doc officielle la déclare dépréciée et donne son équivalent exact,terraform apply -refresh-only -auto-approve. L'alias explique en une ligne pourquoi elle ne demande rien.Le risque documenté va loin : avec des identifiants mal configurés, « Terraform may be misled into thinking that all managed objects have been deleted, causing it to remove tracked objects without confirmation ».Employez le couple prévu : plan -refresh-only pour examiner, apply -refresh-only pour réconcilier, avec confirmation.Deux symptômes proches, deux réparations
| Écart | Nom | Réparation |
|---|---|---|
| entre le réel et le state | dérive | apply -refresh-only |
| entre le state et le code | changement à appliquer | apply |
| objet réel absent du state | orpheline | bloc import |
plan -refresh-only qui tranche, en remplissant resource_drift pour une dérive et en restant muet pour une orpheline, que Terraform ne connaît pas encore.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Les workspaces Terraform : Isole plusieurs states dans un même projet sans dupliquer le code.
- Séparer dev, staging et prod : Cloisonne les states pour qu'une dérive n'en touche qu'un.
- Organiser un dépôt Terraform : Range les configurations pour que chaque state ait un périmètre clair.