Aller au contenu
Infrastructure as Code medium

Diagnostiquer le state Terraform

30 min de lecture

logo terraform

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.

  • 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

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

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.

ÉcartNomRéparation
entre le réel et le statedériveapply -refresh-only
entre le state et le codechangement à appliquerapply
objet réel absent du stateorphelinebloc 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 :

Fenêtre de terminal
echo "corrige a la main" > sortie/bulletin.txt

Pourquoi 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 :

Fenêtre de terminal
terraform plan -out=ordinaire.tfplan
terraform 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.

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 :

Fenêtre de terminal
terraform plan -refresh-only -out=derive.tfplan
terraform show -json derive.tfplan > derive-plan.json
jq '{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.

Fenêtre de terminal
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é.

-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 :

CommandeCe que le code 2 signifie
terraform plan -detailed-exitcodele réel ne correspond pas au code
terraform plan -refresh-only -detailed-exitcodele 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 imported
Plan: 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 exist

Mais Terraform sait écrire ce bloc pour vous, depuis la 1.5, avec l'option -generate-config-out :

Fenêtre de terminal
terraform plan -generate-config-out=genere.tf
# random_string.credential will be imported
Plan: 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 :

CommandeCode de sortie
terraform plan -detailed-exitcode2
terraform plan -refresh=false -detailed-exitcode0
terraform plan -refresh-only -detailed-exitcode2

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
}
}

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ômeCause probableSolution
Le plan veut recréer un objet qui existedérive, ou orphelineplan -refresh-only pour trancher
resource_drift rempli, resource_changes videdérive confirméeapply -refresh-only, puis apply
plan -detailed-exitcode rend 2 après réconciliationle réel diverge du codeUn apply ordinaire, ou corriger le code
La CI ne détecte aucune dérive-refresh=false est actifRetirer l'option, contrôler en refresh-only
Configuration for import target does not existBloc import sans bloc resourceÉcrire le bloc, ou -generate-config-out
L'import annonce must be replacedAttributs déclarés ignorés à l'importAligner le code, ou ignore_changes
La valeur importée a changé après l'applyLe remplacement a été applique sans lectureRestaurer la valeur, puis poser le garde-fou

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.

  • 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_drift et dans resource_changes : il ne tranche rien.
  • plan -refresh-only laisse resource_changes vide : c'est la signature du diagnostic, à enregistrer avec -out puis à convertir en JSON.
  • Le code 2 de -detailed-exitcode signale un diff non vide, pas une dérive. Le signal fiable est plan -refresh-only -detailed-exitcode.
  • -refresh=false rend le plan aveugle à la dérive : mesuré, il rend 0 là où le plan ordinaire rend 2.
  • terraform refresh est dépréciée et équivaut à apply -refresh-only -auto-approve, donc sans confirmation.
  • Le bloc import adopte une orpheline ; -generate-config-out écrit le bloc resource à 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_changes protège une valeur héritée.

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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn