Aller au contenu
Infrastructure as Code medium

terraform state mv et le bloc moved : refactorer sans détruire

25 min de lecture

logo terraform

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.

  • 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 moved déjà appliqué

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

Fenêtre de terminal
terraform plan
Plan: 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.

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.

Fenêtre de terminal
terraform state mv random_pet.web random_pet.frontend
Move "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.web en random_pet.frontend, jamais en random_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.this
Plan: 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 :

Fenêtre de terminal
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"
]
}

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.

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.

  1. 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
  2. Retirez le bloc, puis replanifiez

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

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. »

SituationMéthodePourquoi
Le code est déjà refactoré, le state est en retardstate mvil n'y a rien à déclarer, juste à réconcilier
Le refactoring part du code, en revuebloc movedil se relit, se revoit, se rejoue
Un module partagé, consommé par d'autresbloc moved, conservéles consommateurs ont besoin du chemin
Terraform antérieur à 1.1state mvseule option disponible

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ômeCause probableSolution
Le plan annonce X to add, X to destroy après un renommageLe state n'a pas été réconciliéstate mv, ou un bloc moved
Le déplacement échoue sur le genre d'objetInstance vers module, ou type différentMême genre, même type de ressource
plan -detailed-exitcode rend 2 après réconciliationUn déplacement a été oubliéComparer state list et le code, adresse par adresse
Un collègue a détruit l'objet entre-tempsLa fenêtre de course de state mvCode puis commande, sans intervalle ; ou bloc moved
Après suppression du bloc, le plan veut détruireC'est le breaking change documentéRemettre le bloc, et le conserver
Le plan ne porte aucun previous_addressLe déplacement a été fait en impératifNormal : state mv ne le produit pas

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.

  • Un renommage non réconcilié vaut un destroy + create : Terraform compare des adresses, il ne devine pas.
  • terraform state mv ne 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 mv vaut pour toute l'équipe.
  • Le bloc moved déclare le déplacement dans le code, depuis la 1.1, et c'est le mode normal du refactoring selon la doc.
  • previous_address dans le plan JSON n'apparaît que grâce à un bloc moved, avec l'action no-op.
  • Ne supprimez pas un bloc moved appliqué : c'est un breaking change, et le plan repasse en delete + create.

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.

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