Aller au contenu
Infrastructure as Code medium

terraform state rm et le bloc removed : cesser de gérer sans détruire

25 min de lecture

logo terraform

Retirer une ressource du state Terraform ne la détruit pas : l'objet continue d'exister, Terraform cesse simplement de le suivre. Deux mécanismes le font, terraform state rm en ligne de commande et le bloc removed dans le code depuis Terraform 1.7. Ce guide s'adresse à toute personne qui doit sortir une ressource de son périmètre, parce qu'elle change d'équipe, de dépôt ou d'outil. Vous y verrez les deux mécanismes en action, comment vérifier qu'aucun objet n'a été détruit, et pourquoi un bloc removed mal écrit supprime votre infrastructure sans avertissement.

Tous les comportements de ce guide ont été exécutés sur Terraform v1.15.4 avec les providers local et random : sorties, codes de retour et plans JSON compris.

  • Retirer une entrée du state avec terraform state rm, sans toucher à l'objet
  • Vérifier qu'un retrait n'a rien détruit, côté state et côté disque
  • Nettoyer le code pour éviter une ressource orpheline
  • Déclarer un retrait dans le code avec un bloc removed
  • Distinguer forget et delete dans le plan JSON avant tout apply

Les exemples reposent sur une configuration jetable, sans cloud ni hyperviseur, que vous pouvez reproduire dans n'importe quel répertoire vide :

terraform {
# Le bloc `removed` exige la 1.7 : on le declare pour que Terraform
# refuse d'executer ce code sur une version trop ancienne.
required_version = ">= 1.7"
required_providers {
# Providers epingles : `local` ecrit des fichiers, `random` genere des
# valeurs. Aucun compte ni credential n'est necessaire.
local = { source = "hashicorp/local", version = ">= 2.5" }
random = { source = "hashicorp/random", version = ">= 3.6" }
}
}
# Une ressource simple : elle materialise un fichier sur le disque, ce qui
# rend toute destruction immediatement visible.
resource "local_file" "meteo" {
filename = "${path.root}/meteo.csv"
content = "ville,temp\nlille,12\n"
}
# Une ressource multiple : `count` en cree trois instances, adressees
# local_file.lot[0], [1] et [2]. Elle servira a montrer la granularite.
resource "local_file" "lot" {
count = 3
filename = "${path.root}/lot-${count.index}.txt"
content = "lot ${count.index}\n"
}
# Une ressource sans fichier : son identifiant change a chaque creation,
# ce qui rend une recreation accidentelle facile a reperer.
resource "random_pet" "demo" {
length = 2
}

Cesser de gérer une ressource, est-ce la détruire ?

Section intitulée « Cesser de gérer une ressource, est-ce la détruire ? »

Non, et c'est toute la raison d'être de ces mécanismes. Une ressource Terraform existe à trois endroits en même temps, et chaque opération n'en touche pas les mêmes :

Ce que c'estQui l'écrit
Le codele bloc resource qui décrit l'intentionvous
Le statel'enregistrement qui relie cette intention à un objetTerraform
L'objet réelle fichier, la machine virtuelle, le bucketle provider

Un terraform destroy supprime l'objet réel et son enregistrement. Retirer une ressource du state ne touche que le lien du milieu : l'objet continue d'exister, mais Terraform ne sait plus qu'il lui appartient. Il ne le rafraîchira plus, ne le modifiera plus, ne le détruira plus.

C'est précisément ce qu'il faut quand une ressource change de propriétaire : l'équipe réseau reprend le pare-feu dans son propre dépôt, un module devient autonome, ou un composant sort du périmètre de l'automatisation. Sans ce mécanisme, la seule façon de le faire sortir de votre configuration serait de le détruire puis de le recréer à la main, avec l'interruption de service que cela suppose.

L'opération inverse existe et porte un nom que vous croiserez souvent : terraform import rattache un objet existant à un enregistrement du state. Les deux se répondent, et savoir faire l'aller permet de faire le retour.

Retirer une ressource du state avec terraform state rm

Section intitulée « Retirer une ressource du state avec terraform state rm »

La commande retire une entrée du state et ne touche jamais l'objet réel. Prenez l'habitude de l'essayer à blanc d'abord, surtout sur un state partagé par une équipe :

Fenêtre de terminal
terraform state rm -dry-run local_file.meteo

La sortie annonce ce qui serait fait, sans rien écrire :

Would remove local_file.meteo

Le retrait réel s'obtient en enlevant l'option :

Fenêtre de terminal
terraform state rm local_file.meteo
Removed local_file.meteo
Successfully removed 1 resource instance(s).

Deux lignes, deux informations distinctes : l'adresse retirée, puis le nombre d'instances concernées. Ce compteur devient utile dès que l'adresse visée couvre plusieurs objets.

Comment vérifier qu'un retrait n'a rien détruit ?

Section intitulée « Comment vérifier qu'un retrait n'a rien détruit ? »

En regardant les deux côtés : le state, qui doit avoir perdu l'entrée, et le disque, qui doit avoir gardé l'objet. Côté state :

Fenêtre de terminal
terraform state list

L'adresse local_file.meteo a bien disparu de la liste, les autres sont intactes :

local_file.lot[0]
local_file.lot[1]
local_file.lot[2]
random_pet.demo

Côté objet réel, le fichier est toujours là, avec sa taille et son contenu d'origine :

Fenêtre de terminal
ls -l meteo.csv && cat meteo.csv
-rwxrwxr-x 1 bob bob 20 juil. 28 12:17 meteo.csv
ville,temp
lille,12

C'est la vérification qui compte, et la seule qui distingue un retrait d'une destruction. Sur une infrastructure réelle, l'équivalent est une requête au fournisseur : la machine répond toujours, le bucket est toujours listé. Terraform a seulement oublié qu'il en était responsable.

La commande accepte plusieurs adresses en un seul appel, et elle descend jusqu'à l'instance quand la ressource porte un count ou un for_each :

Fenêtre de terminal
terraform state rm 'local_file.lot[0]' 'local_file.lot[2]'
Removed local_file.lot[0]
Removed local_file.lot[2]
Successfully removed 2 resource instance(s).

Les crochets se quotent systématiquement : sans guillemets, le shell les lit comme un motif de fichiers et la commande ne reçoit jamais l'adresse voulue. Au-delà de -dry-run, terraform state rm expose -lock, -lock-timeout et -ignore-remote-version, plus les options héritées -state, -state-out et -backup, réservées au backend local. Le verrou n'est pas un détail de confort : la commande écrit dans le state, donc elle le verrouille comme n'importe quelle écriture.

Une adresse qui ne correspond à rien n'échoue pas en silence, elle sort en code 1 avec une erreur explicite :

Error: Invalid target address
No matching objects found. To view the available instances, use "terraform
state list". Please modify the address to reference a specific instance.

Pourquoi le plan veut-il recréer la ressource après un state rm ?

Section intitulée « Pourquoi le plan veut-il recréer la ressource après un state rm ? »

Parce que le bloc resource est resté dans la configuration. Pour Terraform, la lecture est sans ambiguïté : le code décrit un objet que le state ne connaît pas, donc il faut le créer.

Fenêtre de terminal
terraform plan
# local_file.meteo will be created
Plan: 1 to add, 0 to change, 0 to destroy.

On appelle cette ressource une orpheline : elle existe pour de bon, mais plus aucun enregistrement ne la rattache à votre configuration. La documentation officielle le formule sobrement, un terraform plan ultérieur inclura une action de création pour chaque instance oubliée. Sur une vraie infrastructure, le prochain apply lancé par n'importe qui recrée donc un objet qui existe déjà, avec les conséquences propres au fournisseur : un doublon, un conflit de nom, ou une adresse IP déjà prise.

Le retrait n'est donc complet qu'une fois le bloc resource supprimé du code, et avec lui toutes les expressions qui référencent encore ses attributs ailleurs dans la configuration. C'est une exigence explicite de la documentation : une seule référence oubliée suffit à faire échouer le plan.

Le bloc removed : déclarer le retrait dans le code

Section intitulée « Le bloc removed : déclarer le retrait dans le code »

Depuis Terraform 1.7, le retrait se déclare au lieu de s'exécuter. L'adresse s'écrit comme une référence, sans guillemets, et le bloc lifecycle porte l'intention :

removed {
# L'adresse de la ressource qui doit sortir du state.
from = random_pet.demo
lifecycle {
# `false` : sortir du state SANS detruire l'objet reel.
destroy = false
}
}

Une condition est impérative : le bloc resource correspondant doit avoir disparu de la configuration. Les deux ne coexistent jamais, et Terraform refuse de planifier tant que c'est le cas :

Error: Removed resource still exists
on main.tf line 20:
20: resource "random_pet" "demo" {
This statement declares that random_pet.demo was removed, but it is still
declared in configuration.

Le bloc resource retiré, le plan annonce un retrait sans destruction. Notez le marqueur de tête, un point, bien distinct du - d'une suppression :

# random_pet.demo will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)
. resource "random_pet" "demo" {
id = "intent-stallion"
# (2 unchanged attributes hidden)
}
Plan: 0 to add, 0 to change, 0 to destroy.
Warning: Some objects will no longer be managed by Terraform
If you apply this plan, Terraform will discard its tracking information for
the following objects, but it will not delete them:
- random_pet.demo

L'apply confirme que rien n'a bougé côté infrastructure : Apply complete! Resources: 0 added, 0 changed, 0 destroyed. L'objet a quitté le state, il n'a pas quitté l'existence. Contrairement à la commande, ce retrait laisse une trace versionnée dans le dépôt : il se relit en revue de code et se rejoue à l'identique par toute l'équipe.

forget ou delete : lire l'action réelle dans le plan JSON

Section intitulée « forget ou delete : lire l'action réelle dans le plan JSON »

La sortie destinée aux humains ne s'automatise pas. Le plan enregistré puis converti en JSON tranche en un mot :

Fenêtre de terminal
terraform plan -out=oubli.tfplan
terraform show -json oubli.tfplan | jq '.resource_changes[]
| {address, actions: .change.actions}'
{
"address": "random_pet.demo",
"actions": [
"forget"
]
}

forget est l'action d'un retrait sans destruction, delete celle d'une suppression. C'est le contrôle à poser en revue ou en intégration continue avant tout apply qui porte un bloc removed, et il tient en une comparaison de chaîne. Précision utile : la page officielle du format JSON ne liste pas encore forget parmi les actions possibles, la valeur se constate donc à l'exécution, comme ici sur la 1.15.4.

Pourquoi un bloc removed peut détruire votre infrastructure

Section intitulée « Pourquoi un bloc removed peut détruire votre infrastructure »

Parce que la destruction est son comportement par défaut, et que la syntaxe ne le signale pas. Ni le bloc lifecycle ni l'argument destroy ne sont obligatoires :

removed {
from = local_file.lot
}

Terraform accepte ce bloc sans le moindre avertissement, et planifie l'inverse de ce que le mot « removed » laisse imaginer :

# local_file.lot[2] will be destroyed
# (because local_file.lot is not in configuration)
- resource "local_file" "lot" {
- filename = "./lot-2.txt" -> null
- id = "f62fb50f2c6a58ed24da0bd2901ea4151b9a1890" -> null
}
Plan: 0 to add, 0 to change, 3 to destroy.

À l'apply, les trois fichiers disparaissent du disque : Apply complete! Resources: 0 added, 0 changed, 3 destroyed. Un bloc de trois lignes, écrit avec la meilleure intention, a supprimé les objets qu'il devait préserver.

Ce que from accepte : une ressource, un module, jamais une instance

Section intitulée « Ce que from accepte : une ressource, un module, jamais une instance »

Les deux mécanismes n'ont pas la même granularité, et c'est souvent ce qui décide lequel employer. Une clé d'instance, que terraform state rm traite sans broncher, est refusée par le bloc :

Error: Resource instance keys not allowed
on oubli.tf line 2, in removed:
2: from = local_file.lot[1]
Resource address must be a resource (e.g. "test_instance.foo"), not a
resource instance (e.g. "test_instance.foo[1]").

Un bloc removed porte donc sur la ressource entière, toutes instances comprises : c'est exactement ce qu'on observe plus haut, où un seul bloc a emporté les trois instances du count. En revanche il accepte un module entier, ce qui n'a pas d'équivalent plus simple en ligne de commande :

removed {
from = module.stock
lifecycle {
destroy = false
}
}

Le plan rend alors un forget par ressource contenue dans le module, ici module.stock.random_pet.interne, et l'apply sort en 0 added, 0 changed, 0 destroyed. C'est la façon propre de faire migrer un module vers un autre dépôt.

Faut-il supprimer le bloc removed une fois appliqué ?

Section intitulée « Faut-il supprimer le bloc removed une fois appliqué ? »

Non, et c'est une différence notable avec le bloc moved, dont le retrait est un changement cassant. Un bloc removed déjà appliqué est inoffensif : le plan suivant rend No changes et terraform plan -detailed-exitcode sort en code 0. Un bloc qui vise une adresse jamais gérée est même ignoré en silence, sans erreur ni avertissement.

La documentation en fait donc une option, pas une obligation : « After you complete the migration you can optionally remove the removed and import blocks, or choose to keep them as a record of the resource's lifecycle. » Conservez-le si vous tenez à garder trace du cycle de vie de la ressource dans le dépôt, supprimez-le si vous préférez un code allégé.

La hiérarchie officielle n'est pas celle qu'on suppose : la documentation recommande les blocs removed et import pour toute nouvelle migration, et la page de la commande renvoie elle aussi vers le bloc. terraform state rm est la voie héritée, pas un égal.

SituationMéthodePourquoi
Retrait d'une instance précise ([1], ["web"])state rmle bloc refuse les clés d'instance
Retrait qui doit être revu et tracébloc removedil vit dans le dépôt, en revue
Migration d'un module vers un autre dépôtbloc removedfrom = module.x retire tout le module
Réparation ponctuelle d'un state incohérentstate rmrien à déclarer, juste à corriger
Terraform antérieur à 1.7state rmseule option disponible

La raison de fond tient en une phrase : une commande impérative ne laisse aucune trace dans le dépôt, là où un bloc versionné se relit, se revoit en équipe et se rejoue à l'identique.

Le réflexe qui évite la majorité de ces situations : lire l'adresse exacte avec terraform state list, puis relire le plan avant de l'appliquer.

SymptômeCause probableSolution
Invalid target addressL'adresse n'existe pas dans le stateVérifier avec terraform state list
Le plan veut créer la ressource après un state rmLe bloc resource est encore dans le codeSupprimer le bloc, et toute référence à ses attributs
Removed resource still existsLe bloc resource et le bloc removed coexistentSupprimer le bloc resource
L'objet réel a été détruitBloc removed sans destroy = falseRecréer ou réimporter, puis ajouter le garde-fou
Resource instance keys not allowedUne clé d'instance dans fromViser la ressource entière, ou passer par state rm
Le plan ne change rien après ajout du blocL'adresse visée n'est pas dans le stateComparer avec state list : un bloc orphelin est ignoré
Un apply a recréé un objet existantRessource orpheline jamais nettoyéestate rm puis nettoyage du code, ou terraform import

Le lab terraform state rm remet un projet à quatre ressources dont deux doivent cesser d'être gérées sans que leurs fichiers disparaissent, une par chaque voie. Les tests lisent le state et le disque, jamais votre code, et rejouent l'expérience dans une copie pour prouver les deux pièges : la ressource orpheline planifiée en create, et le bloc removed privé de son garde-fou planifié en delete. Il se joue hors ligne.

  • terraform state rm retire une entrée du state, l'objet réel n'est jamais touché. -dry-run simule, et la commande accepte plusieurs adresses.
  • La commande descend jusqu'à l'instance (local_file.lot[1]), le bloc removed non : il refuse les clés d'instance.
  • Une ressource retirée du state mais toujours déclarée est orpheline : le prochain plan veut la recréer.
  • Nettoyez le bloc resource et toutes les expressions qui référencent ses attributs.
  • Le bloc removed exige que le bloc resource ait disparu : sinon, Removed resource still exists.
  • Le défaut du bloc removed est de détruire. Sans lifecycle { destroy = false }, l'objet réel est supprimé, sans le moindre avertissement.
  • Dans le plan JSON, forget signe un retrait sans destruction, delete une suppression : c'est le seul contrôle fiable avant un apply.
  • Garder un bloc removed appliqué est sans conséquence : le plan suivant rend No changes.

Les questions ci-dessous portent sur ce qui coûte le plus cher : le fichier détruit alors qu'on voulait seulement cesser de le gérer, la ressource orpheline qui revient au plan suivant, et le choix entre la commande et le bloc. 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