
Le bloc removed déclare, dans le code, qu'une ressource n'est plus gérée
par Terraform. Disponible depuis la version 1.7, il remplace la commande
terraform state rm par une opération versionnée, relue en revue et rejouée à
l'identique par toute l'équipe. Ce guide s'adresse à qui doit léguer une
partie de son infrastructure : la confier à une autre équipe, la sortir d'un
dépôt, la faire changer d'outil. Vous y verrez sa syntaxe, son argument
destroy qui décide de la vie ou de la mort de l'objet, son comportement sur un
for_each et sur un module, et le moment où il cède la place à la ligne de
commande.
Tous les comportements de ce guide ont été exécutés sur Terraform v1.15.4
avec les providers local et random : sorties, erreurs et
plans JSON compris.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Écrire un bloc
removedet la condition qu'il impose au code - Décider du sort de l'objet réel avec
destroy, dont le défaut surprend - Traiter un
for_eachentier en un seul bloc - Reconnaître le cas où seul
terraform state rmfonctionne - Désenregistrer un objet avant de le lâcher, avec un provisioner
Prérequis
Section intitulée « Prérequis »- Terraform 1.7 ou plus récent (installer Terraform) : le bloc n'existe pas avant.
- Connaître la voie impérative,
terraform state rm, dont ce bloc est le pendant déclaratif. - Savoir lire une adresse avec
terraform state list.
Les exemples reposent sur une configuration jetable, sans cloud ni hyperviseur :
terraform { # Le bloc `removed` exige la 1.7 : la contrainte fait echouer tot, avec un # message clair, plutot que sur une erreur de syntaxe incomprehensible. required_version = ">= 1.7" required_providers { local = { source = "hashicorp/local", version = ">= 2.5" } random = { source = "hashicorp/random", version = ">= 3.6" } }}
# Un fichier unique : le cas simple.resource "local_file" "inventaire" { filename = "${path.root}/inventaire.csv" content = "ref,qte\nvis-8,120\n"}
# Une ressource multipliee : deux instances, adressees par leur cle.resource "local_file" "capteurs" { for_each = toset(["nord", "sud"]) filename = "${path.root}/capteur-${each.key}.txt" content = "capteur ${each.key}\n"}
# Un module local, pour montrer le legs d'un ensemble.module "legs" { source = "./modules/legs"}À quoi sert le bloc removed ?
Section intitulée « À quoi sert le bloc removed ? »À déclarer une sortie de gestion : l'objet continue d'exister, Terraform cesse de le suivre. C'est le troisième membre d'une famille de blocs de refactoring, et les trois se lisent ensemble :
| Bloc | Ce qu'il déclare | Effet sur l'objet réel |
|---|---|---|
moved | une ressource a changé d'adresse | aucun |
import | un objet existant entre dans le state | aucun |
removed | une ressource sort du state | aucun ou destruction |
Le problème qu'il résout n'est pas technique, il est collectif. La commande
terraform state rm n'existe qu'au moment où quelqu'un la tape sur son
poste : rien à relire, rien à discuter, aucune trace dans le dépôt. Un bloc
removed, lui, se pousse en revue, se commente, s'applique quand l'équipe
est d'accord, et reste comme archive du cycle de vie de la ressource. C'est
la raison pour laquelle la documentation recommande les blocs removed et
import pour toute nouvelle migration.
Comment écrire un bloc removed ?
Section intitulée « Comment écrire un bloc removed ? »Le bloc porte une adresse et une intention. L'adresse s'écrit comme une
référence, sans guillemets, exactement comme dans un bloc moved :
removed { # La ressource visee. Pas de guillemets : c'est une reference, pas une chaine. from = local_file.capteurs
lifecycle { # `false` : sortir du state SANS toucher a l'objet reel. destroy = false }}Le bloc lifecycle est le même méta-argument que celui des ressources,
dont les sept règles
gouvernent la création et la destruction ; ici, un seul argument nous
intéresse, destroy.
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
This statement declares that local_file.capteurs was removed, but it is stilldeclared in configuration.Cette erreur dit l'essentiel du mécanisme : un bloc removed n'est pas un
interrupteur, c'est la trace d'une suppression déjà faite dans le code.
On retire d'abord le bloc resource, on déclare ensuite ce qu'il advient de
l'objet.
destroy = false, ou l'objet disparaît
Section intitulée « destroy = false, ou l'objet disparaît »Voici le point que la syntaxe ne signale pas, et qui coûte cher : ni le bloc
lifecycle ni l'argument destroy ne sont obligatoires, et le défaut est
la destruction. Trois écritures, deux résultats :
| Écriture | Action planifiée | L'objet réel |
|---|---|---|
removed sans bloc lifecycle | delete | détruit |
removed avec lifecycle { destroy = true } | delete | détruit |
removed avec lifecycle { destroy = false } | forget | conservé |
Le plan JSON tranche en un mot, et c'est le contrôle à automatiser en intégration continue :
terraform plan -out=migration.tfplanterraform show -json migration.tfplan | jq '.resource_changes[] | select(.change.actions != ["no-op"]) | {address, actions: .change.actions}'forget signe un legs, delete une suppression. Aucune autre lecture
n'est fiable.
Comment le bloc traite-t-il un for_each ?
Section intitulée « Comment le bloc traite-t-il un for_each ? »Un seul bloc suffit pour toutes les instances : le from désigne la
ressource, pas ses clés. Le plan les détaille une par une :
# local_file.capteurs["nord"] will no longer be managed by Terraform, but will not be destroyed # local_file.capteurs["sud"] will no longer be managed by Terraform, but will not be destroyed
Plan: 0 to add, 0 to change, 0 to destroy.Et le JSON rend une action par instance :
[ { "address": "local_file.capteurs[\"nord\"]", "actions": ["forget"] }, { "address": "local_file.capteurs[\"sud\"]", "actions": ["forget"] }]Regardez la ligne de synthèse : 0 to destroy, alors que deux objets
sortent de la gestion. Les forget ne sont pas comptés comme des
destructions, ce qui est exact mais rend le compteur trompeur si on ne lit
que lui. Après l'apply, les deux fichiers sont toujours sur le disque et
les deux adresses ont quitté le state.
Pourquoi le bloc refuse-t-il une instance précise ?
Section intitulée « Pourquoi le bloc refuse-t-il une instance précise ? »Parce que sa granularité s'arrête à la ressource entière. Viser une clé échoue, et le message nomme la contrainte :
Error: Resource instance keys not allowed
Resource address must be a resource (e.g. "test_instance.foo"), not aresource instance (e.g. "test_instance.foo[1]").C'est le cas, et le seul, où la voie impérative reste indispensable :
terraform state rm 'local_file.capteurs["nord"]'Peut-on retirer un module entier du state ?
Section intitulée « Peut-on retirer un module entier du state ? »Le from accepte une adresse de module, ce qui n'a pas d'équivalent simple
en ligne de commande :
removed { from = module.legs
lifecycle { destroy = false }}Le plan rend alors un forget par ressource contenue dans le module :
# module.legs.random_pet.interne will no longer be managed by Terraform, but will not be destroyedC'est la façon propre de faire migrer un module vers un autre dépôt : une seule déclaration, relue en revue, quel que soit le nombre de ressources dedans.
Comment désenregistrer un objet avant qu'il quitte Terraform ?
Section intitulée « Comment désenregistrer un objet avant qu'il quitte Terraform ? »Un objet qui change de main a souvent besoin d'être désenregistré :
sortir d'un inventaire, d'un annuaire, d'une supervision. Le bloc
removed accepte des provisioners pour cela, et self y désigne la
ressource concernée :
removed { from = local_file.inventaire
lifecycle { destroy = true }
provisioner "local-exec" { when = destroy command = "echo desenregistrement ${self.filename} >> journal-migration.txt" }}À l'apply, le provisioner s'exécute avant la destruction :
local_file.inventaire: Provisioning with 'local-exec'...local_file.inventaire (local-exec): Executing: ["/bin/sh" "-c" "echo desenregistrement ./inventaire.csv >> journal-migration.txt"]local_file.inventaire: Destruction complete after 0sSeuls les provisioners de destruction sont acceptés ici, et l'oubli de
when est refusé avec la correction dans le message :
Error: Invalid provisioner block
Only destroy-time provisioners are valid in "removed" blocks. To declare adestroy-time provisioner, use: when = destroyUne précision de la documentation évite un blocage classique : une expression
de connection ne peut pas référencer sa ressource parente par son
adresse, puisque celle-ci n'existe plus dans la configuration. Il faut passer
par self, sous peine de cycle.
Pourquoi préférer le bloc à terraform state rm ?
Section intitulée « Pourquoi préférer le bloc à terraform state rm ? »Parce qu'il se prévisualise. Un bloc écrit et poussé en revue décrit une
opération qui n'a pas encore eu lieu : terraform plan la montre, l'équipe
la discute, l'apply la joue plus tard. Tant qu'il n'est pas appliqué,
terraform plan -detailed-exitcode sort en code 2, ce qui signale un
changement en attente dans n'importe quelle chaîne d'intégration
continue.
| Situation | Le bloc removed | terraform state rm |
|---|---|---|
| Trace dans le dépôt | oui, versionnée | aucune |
| Relecture avant exécution | oui, par le plan | non |
Instance précise (["nord"]) | refusé | oui |
| Module entier | oui, en une déclaration | adresse par adresse |
| Destruction de l'objet | possible, et par défaut | jamais |
| Terraform antérieur à 1.7 | indisponible | seule option |
Une fois la migration faite, garder le bloc est sans conséquence : le
plan suivant rend No changes. La documentation en fait une option,
« 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.
Dépannage
Section intitulée « Dépannage »Le réflexe qui évite la majorité de ces situations : lire le plan avant d'appliquer, et vérifier l'action réelle dans le JSON plutôt que le compteur de synthèse.
| Symptôme | Cause probable | Solution |
|---|---|---|
Removed resource still exists | Le bloc resource est toujours déclaré | Le supprimer : le bloc removed en est la trace, pas l'interrupteur |
| L'objet réel a été détruit | destroy = false absent | Le défaut détruit : réimporter l'objet, puis corriger le bloc |
Resource instance keys not allowed | Une clé d'instance dans from | Viser la ressource entière, ou passer par terraform state rm |
| Le plan recrée l'instance retirée | La clé est restée dans le for_each | Aligner la collection sur le retrait |
Invalid provisioner block | when = destroy manquant | Seuls les provisioners de destruction sont acceptés |
| Le plan ne change rien | L'adresse visée n'est pas dans le state | Un bloc orphelin est ignoré en silence : comparer avec state list |
Unsupported block type sur removed | Terraform antérieur à 1.7 | Mettre à jour, ou utiliser terraform state rm |
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab le bloc removed confie un lot d'artefacts en fin de vie avec, pour
chacun, une décision différente : deux rapports à léguer, un cache à
détruire, un seul bac sur trois à sortir, et une migration à préparer sans
l'appliquer. Le fichier de migration porte une note volontairement fausse, que
le plan dément. Les tests lisent le state et le disque, jamais votre code, et
vérifient que le fichier légué porte encore l'identifiant écrit au premier
apply. Il se joue hors ligne.
À retenir
Section intitulée « À retenir »- Le bloc
removeddéclare qu'une ressource n'est plus gérée, depuis Terraform 1.7. Il complètemovedetimport. - Le bloc
resourcecorrespondant doit avoir disparu du code : sinon,Removed resource still exists. - Son défaut est de détruire. Seul
lifecycle { destroy = false }conserve l'objet réel, et transforme l'actiondeleteenforget. - Un seul bloc couvre toutes les instances d'un
for_each, avec une action par instance. Lesforgetne sont pas comptés dansto destroy. - Une clé d'instance est refusée : ce cas passe par
terraform state rm, suivi de l'alignement dufor_each, sans quoi le plan recrée l'objet. - Un
from = module.xlègue tout un module en une déclaration. - Les provisioners de destruction permettent de désenregistrer un objet
avant qu'il parte :
when = destroyest obligatoire, etselfdésigne la ressource. - Le vrai avantage sur la commande est la prévisualisation : le plan montre
l'opération avant qu'elle ait lieu, et
plan -detailed-exitcoderend 2 tant qu'elle est en attente.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous portent sur ce qui bloque ou coûte le plus : l'objet détruit alors qu'on voulait le léguer, l'instance qu'on ne parvient pas à viser, et le sort du bloc une fois la migration faite.
Le pendant déclaratif de terraform state rm
removed {
from = local_file.capteurs
lifecycle {
destroy = false
}
}
Le bloc appartient à une famille de trois, tous consacrés au refactoring :| Bloc | Ce qu'il déclare |
|---|---|
moved |
une ressource a changé d'adresse |
import |
un objet existant entre dans le state |
removed |
une ressource sort du state |
removed et import pour toute nouvelle migration.Le mot removed ne signifie pas « retiré du state »
La documentation est explicite : « By default, Terraform removes the resource from state and destroys the actual resource. »| Écriture | Action | L'objet réel |
|---|---|---|
sans bloc lifecycle |
delete |
détruit |
lifecycle { destroy = true } |
delete |
détruit |
lifecycle { destroy = false } |
forget |
conservé |
forget pour un legs, delete pour une suppression.Déclaratif contre impératif
| Situation | Bloc removed |
terraform state rm |
|---|---|---|
| Trace dans le dépôt | oui, versionnée | aucune |
| Relecture avant exécution | oui, par le plan | non |
Instance précise (["nord"]) |
refusé | oui |
| Module entier | en une déclaration | adresse par adresse |
| Destruction de l'objet | possible, et par défaut | jamais |
terraform plan -detailed-exitcode sort en code 2 : la migration est visible en intégration continue avant d'être jouée.Deux gestes, jamais un seul
Error: Resource instance keys not allowed
Resource address must be a resource (e.g. "test_instance.foo"), not a
resource instance (e.g. "test_instance.foo[1]").
La granularité du bloc s'arrête à la ressource entière. Pour une clé précise :terraform state rm 'local_file.capteurs["nord"]'
Puis il faut aligner la collection. Tant que la clé reste dans le for_each, la configuration redemande l'objet : # local_file.capteurs["nord"] will be created
Plan: 1 to add, 0 to change, 0 to destroy.
Le fichier que vous vouliez léguer serait écrasé.Une déclaration pour tout un ensemble
removed {
from = module.legs
lifecycle {
destroy = false
}
}
# module.legs.random_pet.interne will no longer be managed by Terraform, but will not be destroyed
Vérifié sur Terraform 1.15.4 : l'apply sort en 0 added, 0 changed, 0 destroyed, et chaque ressource du module rend une action forget dans le plan JSON. En ligne de commande, il faudrait traiter les adresses une par une.Un provisioner de destruction dans le bloc
removed {
from = local_file.inventaire
lifecycle {
destroy = true
}
provisioner "local-exec" {
when = destroy
command = "echo desenregistrement ${self.filename} >> journal.txt"
}
}
Le provisioner s'exécute avant la destruction : Provisioning with 'local-exec'... puis Destruction complete.L'oubli de when est refusé, avec la correction dans le message : « Only destroy-time provisioners are valid in "removed" blocks. » Attention aussi aux blocs connection : ils doivent passer par self, l'adresse de la ressource n'existant plus.Le garder ne coûte rien
Vérifié sur Terraform 1.15.4 : après l'apply, le bloc conservé laisse terraform plan en No changes et plan -detailed-exitcode en code 0.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 : toute configuration référençant encore l'ancienne adresse repasserait en destruction plus création.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Diagnostiquer le state : Mesure l'écart entre state, code et réel après un
removed. - Quiz Gérer le state Terraform : Vérifie les acquis de la section state sur un QCM de synthèse.
- Séparer dev, staging et prod : Mène une migration de ressources sans casser les autres environnements.