
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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
forgetetdeletedans le plan JSON avant toutapply
Prérequis
Section intitulée « Prérequis »- Terraform 1.11 ou plus récent (installer Terraform).
Le bloc
removedexiste depuis la version 1.7. - Savoir lire une adresse avec
terraform state listet inspecter une instance avecterraform state show.
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 :
| Où | Ce que c'est | Qui l'écrit |
|---|---|---|
| Le code | le bloc resource qui décrit l'intention | vous |
| Le state | l'enregistrement qui relie cette intention à un objet | Terraform |
| L'objet réel | le fichier, la machine virtuelle, le bucket | le 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 :
terraform state rm -dry-run local_file.meteoLa sortie annonce ce qui serait fait, sans rien écrire :
Would remove local_file.meteoLe retrait réel s'obtient en enlevant l'option :
terraform state rm local_file.meteoRemoved local_file.meteoSuccessfully 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 :
terraform state listL'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.demoCôté objet réel, le fichier est toujours là, avec sa taille et son contenu d'origine :
ls -l meteo.csv && cat meteo.csv-rwxrwxr-x 1 bob bob 20 juil. 28 12:17 meteo.csvville,templille,12C'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.
Retirer plusieurs adresses, ou une seule instance
Section intitulée « Retirer plusieurs adresses, ou une seule instance »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 :
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 "terraformstate 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.
terraform plan # local_file.meteo will be createdPlan: 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 stilldeclared 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 forthe following objects, but it will not delete them: - random_pet.demoL'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 :
terraform plan -out=oubli.tfplanterraform 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 aresource 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é.
state rm ou bloc removed : lequel choisir ?
Section intitulée « state rm ou bloc removed : lequel choisir ? »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.
| Situation | Méthode | Pourquoi |
|---|---|---|
Retrait d'une instance précise ([1], ["web"]) | state rm | le bloc refuse les clés d'instance |
| Retrait qui doit être revu et tracé | bloc removed | il vit dans le dépôt, en revue |
| Migration d'un module vers un autre dépôt | bloc removed | from = module.x retire tout le module |
| Réparation ponctuelle d'un state incohérent | state rm | rien à déclarer, juste à corriger |
| Terraform antérieur à 1.7 | state rm | seule 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.
Dépannage
Section intitulée « Dépannage »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ôme | Cause probable | Solution |
|---|---|---|
Invalid target address | L'adresse n'existe pas dans le state | Vérifier avec terraform state list |
Le plan veut créer la ressource après un state rm | Le bloc resource est encore dans le code | Supprimer le bloc, et toute référence à ses attributs |
Removed resource still exists | Le bloc resource et le bloc removed coexistent | Supprimer le bloc resource |
| L'objet réel a été détruit | Bloc removed sans destroy = false | Recréer ou réimporter, puis ajouter le garde-fou |
Resource instance keys not allowed | Une clé d'instance dans from | Viser la ressource entière, ou passer par state rm |
| Le plan ne change rien après ajout du bloc | L'adresse visée n'est pas dans le state | Comparer avec state list : un bloc orphelin est ignoré |
Un apply a recréé un objet existant | Ressource orpheline jamais nettoyée | state rm puis nettoyage du code, ou terraform import |
Mettre en pratique
Section intitulée « Mettre en pratique »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.
À retenir
Section intitulée « À retenir »terraform state rmretire une entrée du state, l'objet réel n'est jamais touché.-dry-runsimule, et la commande accepte plusieurs adresses.- La commande descend jusqu'à l'instance (
local_file.lot[1]), le blocremovednon : 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
resourceet toutes les expressions qui référencent ses attributs. - Le bloc
removedexige que le blocresourceait disparu : sinon,Removed resource still exists. - Le défaut du bloc
removedest de détruire. Sanslifecycle { destroy = false }, l'objet réel est supprimé, sans le moindre avertissement. - Dans le plan JSON,
forgetsigne un retrait sans destruction,deleteune suppression : c'est le seul contrôle fiable avant unapply. - Garder un bloc
removedappliqué est sans conséquence : le plan suivant rendNo changes.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »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.
Elle ne touche que le state
Une ressource existe à trois endroits : le code qui la décrit, le state qui l'enregistre, et l'objet réel chez le fournisseur.terraform state rm n'agit que sur le lien du milieu.terraform state rm local_file.meteo
Removed local_file.meteo
Successfully removed 1 resource instance(s).
Le fichier meteo.csv est toujours sur le disque avec son contenu d'origine. Terraform ne le rafraîchira plus, ne le modifiera plus et ne le détruira plus : il a oublié qu'il en était responsable.Vérifiez toujours les deux côtés : terraform state list doit avoir perdu l'adresse, et l'objet doit répondre présent chez le fournisseur.La ressource est devenue orpheline
terraform plan
# local_file.meteo will be created
Plan: 1 to add, 0 to change, 0 to destroy.
Le code décrit un objet que le state ne connaît plus : Terraform en tire la seule conclusion possible, il faut le créer. Sur une infrastructure réelle, le prochain apply lancé par n'importe qui recrée donc un objet qui existe déjà.Le retrait n'est complet qu'une fois le bloc resource supprimé, et avec lui toutes les expressions qui référencent encore ses attributs ailleurs dans la configuration. L'ordre compte : on retire du state d'abord, on nettoie le code ensuite.Impératif contre déclaratif
| Situation | Méthode |
|---|---|
Retrait d'une instance précise ([1], ["web"]) |
state rm |
| Retrait qui doit être revu et tracé | bloc removed |
| Migration d'un module vers un autre dépôt | bloc removed |
| Terraform antérieur à 1.7 | state rm, seule option |
removed et import pour toute nouvelle migration. La raison est simple : une commande impérative ne laisse aucune trace dans le dépôt, là où un bloc versionné se relit en revue et se rejoue à l'identique.Attention toutefois : le bloc removed détruit par défaut, ce que la commande ne fait jamais.Le défaut est la destruction
La documentation est sans détour : « By default, Terraform removes the resource from state and destroys the actual resource. »removed {
from = local_file.lot
}
Ce bloc est accepté sans avertissement et planifie :Plan: 0 to add, 0 to change, 3 to destroy.
| Écriture | Action | L'objet réel |
|---|---|---|
sans bloc lifecycle |
delete |
détruit |
lifecycle { destroy = true } |
delete |
détruit |
lifecycle { destroy = false } |
forget |
conservé |
forget contre delete
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ôté sortie humaine, la ligne à repérer est will no longer be managed by Terraform, but will not be destroyed, avec un point en marqueur au lieu du - d'une suppression.À noter : la page officielle du format JSON ne liste pas encore forget parmi les actions possibles, la valeur se constate à l'exécution.Deux granularités différentes
La commande descend jusqu'à l'instance, et en accepte plusieurs :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 : sans guillemets, le shell les lit comme un motif de fichiers. Le bloc removed, lui, refuse :Error: Resource instance keys not allowed
Il porte donc sur la ressource entière, toutes instances comprises. En revanche il accepte un module entier avec from = module.stock, ce qui n'a pas d'équivalent simple en ligne de commande.Le garder ne coûte rien
Vérifié sur Terraform 1.15.4 : un blocremoved conservé après l'apply laisse le plan suivant en 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.La documentation en fait une option : « 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. »C'est une différence nette avec le bloc moved, dont la suppression est un changement cassant documenté comme tel.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Diagnostiquer le state : quand le state ne colle plus ni au code ni au réel.
- Séparer les environnements : retirer une ressource sans casser les autres environnements.
- terraform state rm : la référence officielle : la signature complète et toutes les options.
- Le bloc removed : la référence officielle : la sémantique de
destroyet les provisioners de destruction. - Retirer une ressource du state : la portée de
fromet le nettoyage des références. - Format JSON du plan :
resource_changeset les actions du plan.