
Renommer un bloc, ou le pousser dans un module, change son adresse. Or
l'adresse est l'identité d'un objet dans le state : par défaut, Terraform lit
ce changement comme « l'ancien a disparu, un nouveau apparaît », donc comme un
destroy suivi d'un create. Deux mécanismes l'évitent, et ils ne se valent
pas : terraform state mv répare un state en retard, le bloc moved
déclare le déplacement dans le code. Ce guide montre lequel employer, comment
prouver lequel a servi, et le piège qui coûte le plus cher.
Tous les comportements de ce guide ont été exécutés sur Terraform v1.15.4
avec le provider random : sorties, codes de retour et plans JSON compris.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Pourquoi un renommage non réconcilié vaut une destruction
terraform state mv: réparer un state en retard, et sa fenêtre de course- Le bloc
moved: déclarer le déplacement dans le code, depuis Terraform 1.1 previous_address: la preuve machine de la méthode employée- Pourquoi on ne supprime jamais un bloc
moveddéjà appliqué
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
Le bloc
movedexiste depuis la 1.1. - Savoir lire une adresse avec
terraform state listet inspecter une instance avecterraform state show.
Le problème : Terraform ne devine pas un renommage
Section intitulée « Le problème : Terraform ne devine pas un renommage »Prenons un projet appliqué, dont trois ressources ont été renommées dans le code sans que personne ne touche au state. Le plan suivant est sans ambiguïté :
terraform planPlan: 3 to add, 0 to change, 3 to destroy.Trois créations et trois destructions, alors que rien n'a changé dans l'infrastructure. Terraform compare des adresses : il ne retrouve plus les anciennes, il en découvre trois nouvelles, et il en tire la seule conclusion possible sans information supplémentaire.
La documentation le formule ainsi : « By default Terraform will understand moving or renaming a resource configuration as a request to delete the old object and create a new object at the new address. » Sur un mot de passe généré, une base de données ou un volume, cette lecture est un incident.
terraform state mv : réparer un state en retard
Section intitulée « terraform state mv : réparer un state en retard »La commande déplace une entrée dans le state. Elle ne touche jamais l'infrastructure réelle : elle ne change que le lien entre le state et l'objet distant.
terraform state mv random_pet.web random_pet.frontendMove "random_pet.web" to "random_pet.frontend"Successfully moved 1 object(s).Deux contraintes ne s'inventent pas, et la documentation les pose sans détour :
- les deux adresses doivent désigner le même genre d'objet : une instance vers une instance, un module entier vers un module entier ;
- pour une ressource, le type doit être identique. On renomme
random_pet.webenrandom_pet.frontend, jamais enrandom_string.frontend.
L'option -dry-run affiche ce qui serait déplacé sans rien écrire. La
commande accepte aussi -lock, -lock-timeout et -ignore-remote-version, plus
les options héritées -state, -state-out, -backup et -backup-out, ces
deux dernières ne fonctionnant que sur un state local.
Le bloc moved : déclarer le déplacement dans le code
Section intitulée « Le bloc moved : déclarer le déplacement dans le code »Depuis Terraform 1.1, le déplacement se déclare. Deux références, sans guillemets :
moved { from = random_string.db_secret to = module.secret.random_string.this}Au plan suivant, Terraform annonce un déplacement au lieu d'un remplacement :
# random_string.db_secret has moved to module.secret.random_string.thisPlan: 0 to add, 0 to change, 0 to destroy.L'apply finalise sans rien détruire ni créer. Et surtout, ce déplacement laisse
une trace versionnée : le bloc se relit en revue, se rejoue à l'identique par
toute l'équipe, et traverse les environnements. Une commande impérative, elle, ne
laisse rien dans le dépôt.
previous_address : la preuve de la méthode employée
Section intitulée « previous_address : la preuve de la méthode employée »Le plan JSON porte un champ que l'on ne trouve nulle part ailleurs :
terraform plan -out=plan.tfplanterraform show -json plan.tfplan | jq '.resource_changes[] | select(.previous_address) | {address, previous_address, actions: .change.actions}'{ "address": "module.secret.random_string.this", "previous_address": "random_string.db_secret", "actions": [ "no-op" ]}Terraform n'écrit previous_address que si un bloc moved a été pris en
compte. Après un terraform state mv, ce champ n'apparaît jamais : le state est
déjà déplacé au moment où le plan se calcule, il n'y a plus rien à signaler.
C'est le discriminant exploitable entre les deux méthodes, utile en revue comme
en test automatisé.
Notez l'action : no-op. Un déplacement ne détruit ni ne crée, il réétiquette.
Ne supprimez jamais un bloc moved appliqué
Section intitulée « Ne supprimez jamais un bloc moved appliqué »Voici le piège qui coûte le plus cher, et c'est un conseil que l'on lit régulièrement à l'envers. Une fois le déplacement appliqué, le bloc paraît inutile. Retirez-le, et toute configuration qui référence encore l'ancienne adresse planifie une destruction.
-
Ramenez l'objet à son ancienne adresse, pour reproduire la situation :
Fenêtre de terminal terraform state mv module.secret.random_string.this random_string.db_secret -
Retirez le bloc, puis replanifiez
Fenêtre de terminal rm moved.tfterraform plan -out=sans-moved.tfplanterraform show -json sans-moved.tfplan | jq '.resource_changes[]| select(.change.actions != ["no-op"]) | {address, actions: .change.actions}'{"address": "random_string.db_secret","actions": ["delete"]}{"address": "module.secret.random_string.this","actions": ["create"]}
Le no-op est devenu un delete plus un create. La documentation est
explicite : retirer un bloc moved est un breaking change, et elle
recommande de conserver tout l'historique des blocs pour préserver le chemin
de mise à jour. La seule tolérance concerne un module privé dont on est
certain que tous les usagers ont appliqué.
Laquelle des deux méthodes, et quand
Section intitulée « Laquelle des deux méthodes, et quand »La hiérarchie officielle n'est pas celle que l'on suppose. La page « Refactor
modules » fait du bloc moved le mode normal, et de state mv la porte de
sortie pour les versions trop anciennes : « Terraform v1.1 and later is required
to use moved blocks to explicitly refactor module addresses. Instead, use
the terraform state mv CLI command. »
| Situation | Méthode | Pourquoi |
|---|---|---|
| Le code est déjà refactoré, le state est en retard | state mv | il n'y a rien à déclarer, juste à réconcilier |
| Le refactoring part du code, en revue | bloc moved | il se relit, se revoit, se rejoue |
| Un module partagé, consommé par d'autres | bloc moved, conservé | les consommateurs ont besoin du chemin |
| Terraform antérieur à 1.1 | state mv | seule option disponible |
Dépannage
Section intitulée « Dépannage »Le réflexe : lire l'adresse exacte avec state list avant toute commande, et
vérifier après coup que le plan ne propose plus rien.
| Symptôme | Cause probable | Solution |
|---|---|---|
Le plan annonce X to add, X to destroy après un renommage | Le state n'a pas été réconcilié | state mv, ou un bloc moved |
| Le déplacement échoue sur le genre d'objet | Instance vers module, ou type différent | Même genre, même type de ressource |
plan -detailed-exitcode rend 2 après réconciliation | Un déplacement a été oublié | Comparer state list et le code, adresse par adresse |
| Un collègue a détruit l'objet entre-temps | La fenêtre de course de state mv | Code puis commande, sans intervalle ; ou bloc moved |
| Après suppression du bloc, le plan veut détruire | C'est le breaking change documenté | Remettre le bloc, et le conserver |
Le plan ne porte aucun previous_address | Le déplacement a été fait en impératif | Normal : state mv ne le produit pas |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab terraform state mv remet un projet dont le code a été refactoré sans
que personne ne touche au state : trois objets à rattacher, deux renommages à
régler en impératif et un passage dans un module à régler en déclaratif.
Les tests comparent les identifiants à une copie figée du state de départ, où
une seule divergence signerait une recréation, et rejouent le déplacement pour
exiger un previous_address. Il se joue hors ligne.
À retenir
Section intitulée « À retenir »- Un renommage non réconcilié vaut un destroy + create : Terraform compare des adresses, il ne devine pas.
terraform state mvne touche pas l'infrastructure : il change le lien entre le state et l'objet. Même genre d'objet, même type de ressource.- Changez le code d'abord, la commande ensuite : entre les deux s'ouvre une fenêtre où un collègue peut appliquer la destruction.
- Sur un backend partagé, une seule exécution de
state mvvaut pour toute l'équipe. - Le bloc
moveddéclare le déplacement dans le code, depuis la 1.1, et c'est le mode normal du refactoring selon la doc. previous_addressdans le plan JSON n'apparaît que grâce à un blocmoved, avec l'actionno-op.- Ne supprimez pas un bloc
movedappliqué : c'est un breaking change, et le plan repasse endelete+create.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui bloque le plus souvent : le plan qui veut tout détruire, le choix entre les deux méthodes, et le bloc qu'on croit pouvoir nettoyer. Chaque réponse donne la vérification correspondante.
Terraform compare des adresses, il ne devine rien
La documentation le dit sans detour : « By default Terraform will understand moving or renaming a resource configuration as a request to delete the old object and create a new object at the new address. »terraform plan
# Plan: 3 to add, 0 to change, 3 to destroy.
Trois creations et trois destructions alors que rien n'a change dans l'infrastructure. Deux mecanismes reparent cela : terraform state mv quand le state est en retard sur le code, et le bloc moved quand le refactoring part du code.Elle ne touche que le state
terraform state mv random_pet.web random_pet.frontend
Move "random_pet.web" to "random_pet.frontend"
Successfully moved 1 object(s).
Deux contraintes s'appliquent : les deux adresses doivent designer le meme genre d'objet (une instance vers une instance, un module entier vers un module entier), et pour une ressource le type doit etre identique. L'option -dry-run montre ce qui serait deplace sans rien ecrire.Une seule execution suffit sur un backend partage
state mv ecrit dans le state, qui est commun. Rien a rejouer poste par poste.Le risque documente est une fenetre de course, et c'est le seul avertissement de la page officielle : entre votre changement de code et votre state mv, quiconque lance un plan ou un apply voit un objet a detruire et un autre a creer, et peut appliquer cette lecture.D'ou l'ordre a respecter : le code d'abord, la commande ensuite, sans rien entre les deux.Impératif contre déclaratif
| Situation | Methode |
|---|---|
| Le code est deja refactore, le state est en retard | state mv |
| Le refactoring part du code, en revue | bloc moved |
| Un module partage, consomme par d'autres | bloc moved, conserve |
| Terraform anterieur a 1.1 | state mv, seule option |
moved le mode normal, et de state mv la porte de sortie pour les versions trop anciennes. La raison est simple : une commande imperative ne laisse aucune trace dans le depot.previous_address est le discriminant
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan | jq '.resource_changes[]
| select(.previous_address) | {address, previous_address, actions: .change.actions}'
{
"address": "module.secret.random_string.this",
"previous_address": "random_string.db_secret",
"actions": [
"no-op"
]
}
Apres un state mv, le state est deja deplace quand le plan se calcule : il n'y a plus rien a signaler, donc pas de previous_address. Notez l'action no-op : un deplacement ne detruit ni ne cree, il reetiquette.Le retirer est un breaking change
Verifie sur Terraform 1.15.4 : on ramene l'objet a son ancienne adresse, on retire le bloc, on replanifie.rm moved.tf
terraform plan -out=sans-moved.tfplan
terraform show -json sans-moved.tfplan | jq '.resource_changes[]
| select(.change.actions != ["no-op"]) | {address, actions: .change.actions}'
{
"address": "random_string.db_secret",
"actions": [
"delete"
]
}
{
"address": "module.secret.random_string.this",
"actions": [
"create"
]
}
Le no-op est devenu un delete plus un create. La doc recommande de conserver tout l'historique des blocs pour preserver le chemin de mise a jour. Seule tolerance : un module prive dont on est certain que tous les usagers ont applique.Un bloc moved, avec le prefixe de module
moved {
from = random_string.db_secret
to = module.secret.random_string.this
}
Les deux valeurs sont des references, sans guillemets. Au plan suivant : # random_string.db_secret has moved to module.secret.random_string.this
Plan: 0 to add, 0 to change, 0 to destroy.
L'apply finalise le deplacement sans rien detruire ni creer. La meme operation est possible en imperatif avec terraform state mv random_string.db_secret module.secret.random_string.this, mais elle ne laissera aucune trace dans le depot.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Séparer les environnements : refactorer sans casser les states des autres environnements.
- Sauvegarder et restaurer le state : le filet avant toute manipulation du state.
- Diagnostiquer le state : quand le state ne colle plus au code ni au réel.
- Refactoring de modules : la référence officielle : le bloc
movedet l'avertissement sur sa suppression. - terraform state mv : la référence officielle : les options et la fenêtre de course.
- Format JSON du plan :
previous_addressetresource_changes.