Aller au contenu
Infrastructure as Code medium

Le bloc removed Terraform : léguer une ressource sans la détruire

15 min de lecture

logo terraform

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.

  • Écrire un bloc removed et 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_each entier en un seul bloc
  • Reconnaître le cas où seul terraform state rm fonctionne
  • Désenregistrer un objet avant de le lâcher, avec un provisioner

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"
}

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

BlocCe qu'il déclareEffet sur l'objet réel
movedune ressource a changé d'adresseaucun
importun objet existant entre dans le stateaucun
removedune ressource sort du stateaucun 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.

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 still
declared 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.

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 :

ÉcritureAction planifiéeL'objet réel
removed sans bloc lifecycledeletedétruit
removed avec lifecycle { destroy = true }deletedétruit
removed avec lifecycle { destroy = false }forgetconservé

Le plan JSON tranche en un mot, et c'est le contrôle à automatiser en intégration continue :

Fenêtre de terminal
terraform plan -out=migration.tfplan
terraform 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.

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 a
resource instance (e.g. "test_instance.foo[1]").

C'est le cas, et le seul, où la voie impérative reste indispensable :

Fenêtre de terminal
terraform state rm 'local_file.capteurs["nord"]'

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 destroyed

C'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 0s

Seuls 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 a
destroy-time provisioner, use:
when = destroy

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

SituationLe bloc removedterraform state rm
Trace dans le dépôtoui, versionnéeaucune
Relecture avant exécutionoui, par le plannon
Instance précise (["nord"])refuséoui
Module entieroui, en une déclarationadresse par adresse
Destruction de l'objetpossible, et par défautjamais
Terraform antérieur à 1.7indisponibleseule 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.

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ômeCause probableSolution
Removed resource still existsLe 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étruitdestroy = false absentLe défaut détruit : réimporter l'objet, puis corriger le bloc
Resource instance keys not allowedUne clé d'instance dans fromViser la ressource entière, ou passer par terraform state rm
Le plan recrée l'instance retiréeLa clé est restée dans le for_eachAligner la collection sur le retrait
Invalid provisioner blockwhen = destroy manquantSeuls les provisioners de destruction sont acceptés
Le plan ne change rienL'adresse visée n'est pas dans le stateUn bloc orphelin est ignoré en silence : comparer avec state list
Unsupported block type sur removedTerraform antérieur à 1.7Mettre à jour, ou utiliser terraform state rm

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.

  • Le bloc removed déclare qu'une ressource n'est plus gérée, depuis Terraform 1.7. Il complète moved et import.
  • Le bloc resource correspondant 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'action delete en forget.
  • Un seul bloc couvre toutes les instances d'un for_each, avec une action par instance. Les forget ne sont pas comptés dans to destroy.
  • Une clé d'instance est refusée : ce cas passe par terraform state rm, suivi de l'alignement du for_each, sans quoi le plan recrée l'objet.
  • Un from = module.x lègue tout un module en une déclaration.
  • Les provisioners de destruction permettent de désenregistrer un objet avant qu'il parte : when = destroy est obligatoire, et self dé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-exitcode rend 2 tant qu'elle est en attente.

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.

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