Dépendances et cycle de vie
Pourquoi
Dans la configuration de Signalements, le réseau privé doit exister avant que les instances s'y attachent, la base avant que l'application reçoive son adresse, le répartiteur avant ses backends. À la main, avec la CLI scw, c'est vous qui faisiez l'ordre, commande après commande. Avec Terraform, vous écrivez les ressources dans n'importe quel ordre, dans n'importe quel fichier, et l'outil décide. La plupart du temps, il décide bien. Mais trois situations reviennent, et chacune a déjà coûté cher à quelqu'un :
- Un remplacement inattendu. Vous changez une ligne anodine, et le plan annonce
-/+sur la base de données : Terraform va la détruire puis la recréer, vide. Si vous ne savez pas lire la cause dans le plan, vous appliquez. - Une coupure évitable. Une instance doit être remplacée. Par défaut, Terraform détruit l'ancienne avant de créer la nouvelle : pendant ce temps, plus rien ne répond.
- Une dépendance invisible. Une ressource a besoin qu'une autre soit prête, mais rien dans le code ne le dit à Terraform. Il les crée en parallèle, et la création échoue une fois sur deux.
Cette leçon montre comment Terraform construit son ordre, comment lire ce qu'il va faire, et comment le corriger quand son choix par défaut n'est pas le bon. Toutes les sorties ont été produites avec Terraform 1.16.1 et des fournisseurs qui ne créent rien dans le cloud (random, local, terraform_data), pour que vous puissiez les reproduire gratuitement.
Les concepts
Le graphe de dépendances
Terraform ne lit pas vos fichiers de haut en bas. Il construit un graphe de dépendances : chaque ressource est un nœud, et chaque référence d'une ressource à une autre est un arc. Quand l'instance écrit private_network_id = scaleway_vpc_private_network.pn.id, elle dépend du réseau : Terraform ne peut pas connaître la valeur de id avant d'avoir créé le réseau. C'est une dépendance implicite, déduite des expressions.
À partir du graphe, Terraform :
- crée dans l'ordre des arcs (une ressource après celles dont elle dépend) ;
- détruit dans l'ordre inverse (une ressource avant celles dont elle dépend : on détache l'instance avant de supprimer le réseau) ;
- exécute en parallèle tout ce qui ne dépend pas l'un de l'autre, jusqu'à dix opérations à la fois par défaut (option
-parallelismdeplanetapply).
Le graphe doit être acyclique : si A dépend de B et B de A, aucun ordre n'est possible, et Terraform refuse de travailler.
Dépendance implicite, dépendance explicite
Quand une ressource a besoin d'une autre sans lire aucun de ses attributs, aucune référence ne l'indique. Le méta-argument depends_on déclare alors la dépendance à la main :
resource "scaleway_instance_server" "app" {
# ...
depends_on = [scaleway_vpc_public_gateway_ip.sortie]
}La documentation de Terraform le présente comme un dernier recours, à réserver aux dépendances qui ne passent par aucune valeur, et demande d'expliquer chaque usage par un commentaire. Deux raisons :
depends_onest grossier : il porte sur une ressource entière, pas sur un attribut, ce qui rend les plans plus prudents, donc moins précis.- Sur une source de données, il retarde sa lecture jusqu'à l'
applydès que la ressource dont elle dépend a un changement prévu : ses attributs deviennent « connus après l'application », et tout ce qui en dépend aussi. Un plan plein de(known after apply)est souvent la trace d'undepends_onsuperflu.
La règle pratique : si une valeur passe, référencez-la ; depends_on seulement pour un effet de bord qui ne se voit dans aucun attribut (une politique IAM qui doit exister avant qu'un service l'utilise, une passerelle qui doit fournir la sortie Internet avant que cloud-init ne télécharge ses paquets).
Mise à jour sur place ou remplacement
Quand un attribut change, deux issues sont possibles, et c'est le fournisseur qui décide pour chaque attribut :
- Mise à jour sur place (
~) : l'API permet de modifier l'objet existant. Le nom d'une instance Scaleway, ses étiquettes, sa taille de volume bloc en augmentation. - Remplacement (
-/+ou+/-) : l'API ne permet pas la modification, ou le fournisseur a choisi de ne pas la faire. L'ancien objet est détruit, un nouveau est créé, avec un nouvel identifiant, une nouvelle adresse, et, pour un objet qui porte des données, sans les données.
Dans le code d'un fournisseur écrit avec le SDK de HashiCorp, un attribut qui impose le remplacement porte la marque ForceNew. Chez Scaleway, quelques exemples tirés de la documentation et du code du fournisseur 2.84 :
| Ressource | Changement | Effet |
|---|---|---|
scaleway_instance_server | zone | remplacement (le schéma zonal commun à toutes les ressources est ForceNew) |
scaleway_instance_server | image | remplacement |
scaleway_instance_server | type | migration sur place (arrêt, changement, redémarrage), ou remplacement si replace_on_type_change = true |
scaleway_instance_server | root_volume.size_in_gb sur volume local | remplacement ; sur volume bloc, augmentation sur place, diminution par remplacement |
scaleway_rdb_instance | node_type | montée de gamme sur place, sans interruption d'après la documentation |
scaleway_rdb_instance | engine | montée de version par une procédure bleu-vert : nouvelle instance depuis un instantané, bascule des points d'accès ; la documentation avertit que les écritures entre l'instantané et la bascule sont perdues, et que les versions du fournisseur antérieures à 2.61.0 recréaient la base vide |
Ce dernier exemple résume la leçon : le comportement dépend du fournisseur, de sa version, et parfois d'un argument. On ne le devine pas, on le lit dans le plan, qui indique toujours la cause d'un remplacement par un commentaire # forces replacement à côté de l'attribut responsable.
Le méta-argument lifecycle
Chaque ressource accepte un bloc lifecycle qui modifie la façon dont Terraform la crée, la met à jour et la détruit :
| Argument | Effet | Usage typique |
|---|---|---|
create_before_destroy | lors d'un remplacement, créer le nouvel objet avant de détruire l'ancien | instance derrière un répartiteur, certificat, enregistrement DNS |
prevent_destroy | refuser tout plan qui détruirait l'objet | base de données, bucket de données, clé de chiffrement |
ignore_changes | ne jamais proposer de mise à jour pour les attributs listés (ou all) | attribut modifié par un autre système (étiquettes posées par un outil, nombre d'instances géré par l'autoscaling) |
replace_triggered_by | remplacer l'objet quand une autre ressource (ou un de ses attributs) change | recréer une instance quand son cloud-init change |
precondition, postcondition | vérifier une hypothèse avant ou après, avec un message d'erreur | refuser une version mal formée, vérifier qu'une image est bien Ubuntu |
destroy | à false, oublier l'objet au lieu de le détruire (Terraform 1.15 et suivants) | sortir une ressource de la gestion de Terraform en la laissant vivre |
Les sections pratiques montrent chacun à l'œuvre. Trois subtilités, toutes documentées, méritent d'être connues avant :
create_before_destroyse propage : Terraform l'applique aussi aux ressources dont la ressource marquée dépend, et l'on ne peut pas le remettre àfalsesur elles. Et il suppose que l'ancien et le nouvel objet puissent coexister : un nom unique imposé par l'API (un bucket, un espace de noms de registre) fait échouer la création du second.prevent_destroyne protège que tant que le bloc existe : la documentation le souligne, supprimer la ressource du code supprime aussi sa protection, et le plan suivant détruit l'objet.ignore_changesne joue que sur les mises à jour : à la création, les valeurs du code s'appliquent.
Les provisionneurs
Un provisionneur exécute une commande pendant la création ou la destruction d'une ressource : sur votre machine (local-exec) ou sur la machine créée, par SSH (remote-exec). La documentation de Terraform les présente comme un dernier recours, pour deux raisons qu'elle donne elle-même : Terraform ne peut pas modéliser dans un plan ce que fait une commande, et la plupart des provisionneurs exigent un accès réseau direct aux serveurs et des identifiants, ce qui ajoute de la complexité et des risques de sécurité.
Pour Signalements, les remplaçants sont connus : la configuration initiale d'une instance passe par cloud-init dans user_data (leçon 4 du cours cloud), une image préparée à l'avance par Packer (cours Images de machines avec Packer), et la configuration continue par un outil de gestion de configuration (cours Ansible : les fondamentaux). Il reste des usages légitimes et locaux, comme valider un fichier généré ou prévenir un système externe, souvent portés par la ressource terraform_data.
terraform_data
terraform_data est une ressource intégrée à Terraform depuis la version 1.4, sans fournisseur à installer. Elle ne crée rien à l'extérieur : elle stocke une valeur (input, recopiée dans output) et peut être remplacée quand une valeur de triggers_replace change. Elle remplace l'ancienne null_resource du fournisseur null, et sert de support aux provisionneurs et de déclencheur pour replace_triggered_by. Dans cette leçon, elle joue aussi le rôle de « fausse instance » pour observer l'ordre des opérations sans rien dépenser.
En pratique
Lire le graphe
Une configuration minimale, qui imite trois étages de Signalements : un réseau (random_pet, un nom aléatoire), une base qui en dépend (random_id, dont keepers force le remplacement quand le réseau change), et une instance qui dépend des deux (terraform_data) :
terraform {
required_version = ">= 1.10"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.7"
}
}
}
variable "version_app" {
type = string
default = "1.2.0"
}
resource "random_pet" "reseau" {
prefix = "pn"
}
resource "random_id" "base" {
byte_length = 4
keepers = {
reseau = random_pet.reseau.id
}
}
resource "terraform_data" "instance" {
input = {
reseau = random_pet.reseau.id
base = random_id.base.hex
version = var.version_app
}
}
output "instance" {
value = terraform_data.instance.output
}$ terraform graph -no-color
digraph G {
rankdir = "RL";
node [shape = rect, fontname = "sans-serif"];
"random_id.base" [label="random_id.base"];
"random_pet.reseau" [label="random_pet.reseau"];
"terraform_data.instance" [label="terraform_data.instance"];
"random_id.base" -> "random_pet.reseau";
"terraform_data.instance" -> "random_id.base";
}
La sortie est au format DOT de Graphviz : chaque ligne A -> B se lit « A dépend de B ». Depuis Terraform 1.7, terraform graph ne montre par défaut que les relations entre ressources. Notez que l'arc direct de l'instance vers le réseau n'apparaît pas, bien que l'instance référence random_pet.reseau.id : Terraform retire du graphe les arcs déjà impliqués par un autre chemin (instance, base, réseau), ce qui ne change rien à l'ordre. L'option -type=plan donne le graphe complet qu'utilise réellement le moteur, avec les fournisseurs, les variables et les sorties ; extrait :
$ terraform graph -type=plan -no-color
...
"[root] terraform_data.instance (expand)" -> "[root] provider[\"terraform.io/builtin/terraform\"]"
"[root] terraform_data.instance (expand)" -> "[root] random_id.base (expand)"
"[root] terraform_data.instance (expand)" -> "[root] var.version_app"
...
Pour une image, installez Graphviz et redirigez : terraform graph | dot -Tsvg > graphe.svg. Sur une vraie configuration, le graphe complet devient vite illisible ; il sert surtout à comprendre un cas précis, ou un cycle.
Un cycle
Deux ressources qui se référencent mutuellement :
resource "terraform_data" "a" {
input = terraform_data.b.id
}
resource "terraform_data" "b" {
input = terraform_data.a.id
}$ terraform validate -no-color
Error: Cycle:
terraform_data.a
terraform_data.b
Dans une vraie configuration, le cycle passe souvent par trois ou quatre ressources et un depends_on ; l'option -draw-cycles de terraform graph -type=plan colore les arcs du cycle. La correction consiste presque toujours à casser une référence : la valeur que l'une des deux attend peut-elle venir d'ailleurs (une variable, une source de données) ?
Créer, puis observer l'ordre
$ terraform apply -auto-approve -no-color
...
random_pet.reseau: Creating...
random_pet.reseau: Creation complete after 0s [id=pn-verified-goldfish]
random_id.base: Creating...
random_id.base: Creation complete after 0s [id=L8H0EQ]
terraform_data.instance: Creating...
terraform_data.instance: Creation complete after 0s [id=25c258df-17cd-d0d6-929f-e93e093e6400]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
L'ordre suit le graphe. Avec des ressources indépendantes, les lignes Creating... se chevaucheraient : c'est le parallélisme.
Lire un remplacement et sa cause
L'option -replace demande le remplacement d'une ressource précise, ce qui permet de voir la chaîne des conséquences sans rien modifier au code :
$ terraform plan -no-color -replace=random_pet.reseau
...
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement
Terraform will perform the following actions:
# random_id.base must be replaced
-/+ resource "random_id" "base" {
~ b64_std = "L8H0EQ==" -> (known after apply)
~ b64_url = "L8H0EQ" -> (known after apply)
~ dec = "801240081" -> (known after apply)
~ hex = "2fc1f411" -> (known after apply)
~ id = "L8H0EQ" -> (known after apply)
~ keepers = { # forces replacement
~ "reseau" = "pn-verified-goldfish" -> (known after apply)
}
# (1 unchanged attribute hidden)
}
# random_pet.reseau will be replaced, as requested
-/+ resource "random_pet" "reseau" {
~ id = "pn-verified-goldfish" -> (known after apply)
# (3 unchanged attributes hidden)
}
# terraform_data.instance will be updated in-place
~ resource "terraform_data" "instance" {
id = "25c258df-17cd-d0d6-929f-e93e093e6400"
~ input = {
~ base = "2fc1f411" -> (known after apply)
~ reseau = "pn-verified-goldfish" -> (known after apply)
# (1 unchanged attribute hidden)
}
...
Plan: 2 to add, 1 to change, 2 to destroy.
Trois lectures, à faire sur chaque plan de production :
- La légende en tête :
~mise à jour sur place,-/+détruire puis créer. - La phrase sous chaque
#:will be replaced, as requested(vous l'avez demandé),must be replaced(une conséquence),will be updated in-place. - Le commentaire
# forces replacement: il désigne l'attribut responsable. Ici,keepersde la base change parce que le nom du réseau change. Sur une vraie base, c'est l'alerte rouge : si vous lisez# forces replacementà côté d'un attribut descaleway_rdb_instance, arrêtez-vous.
Remarquez aussi que l'instance n'est pas remplacée : un changement de input d'une ressource terraform_data est une mise à jour sur place. Pour qu'elle soit remplacée, il faudrait que la valeur figure dans triggers_replace.
Appliquez, et regardez l'ordre réel :
$ terraform apply -auto-approve -no-color -replace=random_pet.reseau
random_id.base: Destroying... [id=L8H0EQ]
random_id.base: Destruction complete after 0s
random_pet.reseau: Destroying... [id=pn-verified-goldfish]
random_pet.reseau: Destruction complete after 0s
random_pet.reseau: Creating...
random_pet.reseau: Creation complete after 0s [id=pn-workable-moth]
random_id.base: Creating...
random_id.base: Creation complete after 0s [id=ahvStA]
terraform_data.instance: Modifying... [id=25c258df-17cd-d0d6-929f-e93e093e6400]
terraform_data.instance: Modifications complete after 0s [id=25c258df-17cd-d0d6-929f-e93e093e6400]
Apply complete! Resources: 2 added, 1 changed, 2 destroyed.
(Seules les lignes d'action sont reproduites.) Par défaut, Terraform détruit d'abord : la base, qui dépend du réseau, puis le réseau ; ensuite il recrée dans l'ordre du graphe. Entre la première et la dernière ligne, ni réseau ni base n'existent. Sur une instance réelle, c'est une coupure.
Créer avant de détruire
Ajoutez create_before_destroy au réseau :
resource "random_pet" "reseau" {
prefix = "pn"
lifecycle {
create_before_destroy = true
}
}$ terraform apply -auto-approve -no-color -replace=random_pet.reseau
...
-/+ destroy and then create replacement
+/- create replacement and then destroy
# random_id.base must be replaced
-/+ resource "random_id" "base" {
# random_pet.reseau will be replaced, as requested
+/- resource "random_pet" "reseau" {
random_id.base: Destroying... [id=ahvStA]
random_pet.reseau: Creating...
random_id.base: Creating...
terraform_data.instance: Modifying... [id=25c258df-17cd-d0d6-929f-e93e093e6400]
random_pet.reseau (deposed object d829539e): Destroying... [id=pn-workable-moth]
Apply complete! Resources: 2 added, 1 changed, 2 destroyed.
(Extrait : la légende, les en-têtes du plan, puis les lignes d'action.) Le symbole devient +/- pour le réseau : créer le remplaçant, puis détruire l'ancien. Pendant l'intervalle, l'ancien objet est dit déposé (deposed) : il n'est plus l'objet courant de la ressource, mais existe encore, et Terraform le garde dans l'état jusqu'à sa destruction, en dernier. La base, qui n'a pas create_before_destroy, garde l'ordre par défaut -/+.
Pour une instance de Signalements derrière le répartiteur, c'est le réglage qui évite la coupure : la nouvelle instance est créée et ajoutée au backend avant que l'ancienne disparaisse. À une condition : que les deux puissent coexister. Deux instances peuvent porter le même nom chez Scaleway ; deux buckets, non.
Protéger la base
resource "random_id" "base" {
# ...
lifecycle {
prevent_destroy = true
}
}$ terraform destroy -auto-approve -no-color
...
Error: Instance cannot be destroyed
on main.tf line 24:
24: resource "random_id" "base" {
Resource random_id.base has lifecycle.prevent_destroy set, but the plan calls
for this resource to be destroyed. To avoid this error and continue with the
plan, either disable lifecycle.prevent_destroy or reduce the scope of the
plan using the -target option.
Le plan entier est refusé, pas seulement la ressource : rien n'est détruit. La même erreur arrêterait un apply dont un changement forcerait le remplacement de la base, ce qui est exactement ce que l'on veut sur sig-db.
Oublier sans détruire
Terraform 1.15 a ajouté un argument destroy au bloc lifecycle (il figure dans les notes de version de la branche 1.15, publiée au printemps 2026). À false, une destruction devient un oubli : la ressource sort de l'état, l'objet continue d'exister.
resource "terraform_data" "x" {
lifecycle {
destroy = false
}
}$ terraform plan -destroy -no-color
...
Terraform will perform the following actions:
# terraform_data.x will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)
. resource "terraform_data" "x" {
id = "a7e02d83-d252-0d8e-9c2e-2069a86c444c"
}
Plan: 0 to add, 0 to change, 0 to destroy.
Et la même limite que prevent_destroy : si l'on retire le bloc entier du code, le réglage disparaît avec lui. Le même plan, après suppression du bloc, annonce terraform_data.x will be destroyed (because terraform_data.x is not in configuration). Pour sortir une ressource du code sans la détruire, la méthode prévue est le bloc removed, vu à la leçon 11. Cet argument est apparu dans Terraform ; avant de l'utiliser dans une configuration partagée avec OpenTofu, vérifiez qu'il existe dans votre version d'OpenTofu.
Ignorer, déclencher, vérifier
Trois réglages sur l'« instance » :
resource "terraform_data" "instance" {
input = {
reseau = random_pet.reseau.id
base = random_id.base.hex
version = var.version_app
}
lifecycle {
ignore_changes = [input]
replace_triggered_by = [random_id.base]
precondition {
condition = can(regex("^[0-9]+[.][0-9]+[.][0-9]+$", var.version_app))
error_message = "version_app doit être une version sémantique, par exemple 1.2.0."
}
}
}ignore_changes : changer la version ne produit plus aucune mise à jour.
$ terraform plan -no-color -var version_app=1.3.0
...
No changes. Your infrastructure matches the configuration.
C'est utile quand un autre système modifie l'attribut (le nombre d'instances d'un groupe d'autoscaling, les étiquettes posées par un outil d'inventaire) ; c'est dangereux quand on l'oublie, parce que le code ment alors sur la réalité. Un ignore_changes se commente toujours : qui modifie cet attribut, et pourquoi ?
precondition : une version mal formée arrête le plan avant toute action.
$ terraform plan -no-color -var version_app=latest
Planning failed. Terraform encountered an error while generating this plan.
Error: Resource precondition failed
on main.tf line 47, in resource "terraform_data" "instance":
47: condition = can(regex("^[0-9]+[.][0-9]+[.][0-9]+$", var.version_app))
├────────────────
│ var.version_app is "latest"
version_app doit être une version sémantique, par exemple 1.2.0.
Une postcondition s'écrit de la même façon, mais s'évalue après que la ressource est connue, et peut utiliser self : vérifier, par exemple, qu'une source de données d'image a bien trouvé une image Ubuntu, ou que l'instance créée a bien une adresse privée.
replace_triggered_by : quand la base est remplacée, l'instance l'est aussi, alors qu'elle ne l'aurait été sinon que mise à jour.
$ terraform plan -no-color -replace=random_id.base
# random_id.base will be replaced, as requested
-/+ resource "random_id" "base" {
# terraform_data.instance will be replaced due to changes in replace_triggered_by
-/+ resource "terraform_data" "instance" {
Plan: 2 to add, 0 to change, 2 to destroy.
(Seules les lignes d'en-tête sont reproduites. Pour cette démonstration, prevent_destroy de la base a été remis à false.) La documentation limite les références de replace_triggered_by aux ressources gérées et à leurs attributs. Usage typique pour Signalements : replace_triggered_by = [terraform_data.cloud_init], où terraform_data.cloud_init a le contenu du fichier cloud-init dans triggers_replace, pour que toute modification du cloud-init recrée les instances au lieu d'être ignorée (cloud-init ne s'exécute qu'au premier démarrage).
Un provisionneur local
Un usage légitime : vérifier un fichier généré, à chaque changement de son contenu.
resource "local_file" "cloud_init" {
filename = "${path.module}/signalements.yaml"
content = "#cloud-config\npackages: [docker.io]\n"
}
resource "terraform_data" "verification" {
triggers_replace = [local_file.cloud_init.content_sha256]
provisioner "local-exec" {
command = "grep -q '^#cloud-config' signalements.yaml && echo 'cloud-config valide'"
}
}$ terraform apply -auto-approve -no-color
local_file.cloud_init: Creating...
local_file.cloud_init: Creation complete after 0s [id=fabc06f30b837730206e887d3673dbaea5d8a139]
terraform_data.verification: Creating...
terraform_data.verification: Provisioning with 'local-exec'...
terraform_data.verification (local-exec): Executing: ["/bin/sh" "-c" "grep -q '^#cloud-config' signalements.yaml && echo 'cloud-config valide'"]
terraform_data.verification (local-exec): cloud-config valide
terraform_data.verification: Creation complete after 0s [id=ee1847c9-9062-2e95-08e3-f39720c7cbdf]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
La référence à content_sha256 fait deux choses : elle crée la dépendance (la vérification attend le fichier) et elle déclenche un remplacement, donc une nouvelle exécution, à chaque changement du contenu. Sans elle, le provisionneur pourrait s'exécuter avant que le fichier existe, puisque rien ne relierait les deux ressources ; il faudrait alors un depends_on, qui créerait l'ordre sans la réexécution.
Remarquez enfin que le plan n'a rien dit de ce que la commande allait faire : il annonçait seulement la création de terraform_data.verification. C'est la limite que pointe la documentation.
Détruire : l'ordre inverse
$ terraform destroy -auto-approve -no-color
terraform_data.instance: Destroying... [id=25c258df-17cd-d0d6-929f-e93e093e6400]
random_id.base: Destroying... [id=unI5rQ]
random_pet.reseau: Destroying... [id=pn-actual-drake]
Destroy complete! Resources: 3 destroyed.
L'instance d'abord, le réseau en dernier : l'inverse du graphe.
Sous le capot
Plan, puis graphe d'application. Pendant plan, Terraform construit le graphe à partir de la configuration et de l'état, interroge les fournisseurs (lecture de l'existant, calcul du changement prévu) et produit une liste d'actions par instance de ressource. Pendant apply, il construit un second graphe, à partir du plan cette fois, dans lequel chaque action est un nœud : « détruire l'ancienne base » et « créer la nouvelle base » sont deux nœuds distincts, que create_before_destroy ordonne autrement. C'est ce graphe que montre terraform graph -type=apply.
Qui décide du remplacement. Terraform ne sait rien des API de Scaleway. Pendant le plan, il envoie au fournisseur l'état actuel et la configuration voulue ; le fournisseur répond par l'état prévu et la liste des attributs qui exigent un remplacement. Dans les fournisseurs écrits avec le SDK historique de HashiCorp, comme celui de Scaleway, cette liste vient des marques ForceNew du schéma, et de fonctions CustomizeDiff qui décident au cas par cas : dans le code du fournisseur Scaleway, la fonction qui compare les états d'un serveur appelle ForceNew sur image, sur type quand replace_on_type_change est vrai, et sur la taille du volume racine selon son type. D'où les commentaires # forces replacement du plan.
Les objets déposés. Avec create_before_destroy, si la création du remplaçant réussit mais que la destruction de l'ancien échoue (une dépendance externe l'utilise encore), l'ancien reste dans l'état comme objet déposé, et le prochain apply retentera sa destruction.
Le parallélisme. Le moteur parcourt le graphe et lance une opération dès que toutes ses dépendances sont terminées, avec au plus dix opérations simultanées par défaut. Réduire -parallelism ralentit tout mais peut éviter les erreurs de limitation de débit d'une API ; l'augmenter rarement aide, la plupart des créations étant limitées par le fournisseur, pas par Terraform.
Pièges courants
Appliquer un plan avec -/+ sur une ressource qui porte des données. Base, volume, bucket : un remplacement est une perte de données. Cherchez must be replaced et # forces replacement dans chaque plan, et protégez ces ressources par prevent_destroy.
create_before_destroy sur une ressource au nom unique. La création du remplaçant échoue parce que le nom est pris par l'objet encore vivant. Soit un nom généré (préfixe et suffixe aléatoire), soit pas de create_before_destroy.
Croire que prevent_destroy protège d'une suppression du code. Il disparaît avec le bloc. Une revue de code qui voit disparaître une ressource protégée doit alerter.
depends_on par précaution. Il ralentit et rend les plans flous, surtout sur les sources de données. Une référence vaut mieux.
ignore_changes = all pour faire taire un plan. Le plan se tait, la dérive reste. Trouvez qui modifie la ressource, et décidez qui en est le propriétaire.
Un provisionneur remote-exec pour installer l'application. Il exige SSH ouvert depuis l'endroit où tourne Terraform, des identifiants, et ne se rejoue pas. Par défaut, s'il échoue à la création, la ressource est marquée corrompue (tainted) et sera recréée au prochain apply. cloud-init fait le même travail sans ces défauts.
terraform taint. La commande existe encore en 1.16, mais elle modifie l'état sans montrer de plan. terraform apply -replace=<adresse> fait la même chose en montrant ce qui va se passer.
Sécurité
- Le remplacement est une opération destructrice au sens de la sécurité aussi : nouvelle instance, nouvelle clé d'hôte SSH, nouvelle adresse. Les règles de pare-feu, les listes d'adresses autorisées et la confiance des clients SSH doivent le prévoir.
prevent_destroysur ce qui ne se reconstruit pas : bases, buckets de données et de sauvegardes, clés de Key Manager (détruire une clé rend illisible tout ce qu'elle chiffrait, leçon 5 du cours Cloud souverain).- Les provisionneurs exécutent des commandes avec vos droits, sur la machine qui lance Terraform : en intégration continue, avec ceux du pipeline. Une commande construite à partir d'une variable non vérifiée est une injection. Si vous en écrivez, passez les valeurs par l'option
environmentdu provisionneur, pas par concaténation danscommand. - Les préconditions sont un contrôle de sécurité bon marché : refuser une image qui n'est pas celle attendue, une plage d'adresses ouverte à
0.0.0.0/0, un type d'instance hors de la liste approuvée, avec un message qui explique pourquoi.
En production
- Relisez les plans comme du code. En intégration continue, le plan est publié dans la demande de fusion (leçon 12), et le relecteur cherche les
-/+avant tout le reste. Certaines équipes font échouer automatiquement le pipeline si un plan remplace une ressource d'une liste protégée. - Gardez
-parallelismpar défaut, sauf limitation de débit avérée de l'API. - Préférez les remplacements progressifs à
create_before_destroysur un grand nombre d'instances : un groupe d'autoscaling avec un nouveau modèle (leçon 2 du cours Scaleway en pratique) remplace les instances par lots, ce que Terraform ne sait pas faire seul. - Épinglez la version du fournisseur. Le comportement d'un changement (mise à jour ou remplacement) dépend de sa version : l'exemple du moteur de
scaleway_rdb_instance, qui recréait la base vide avant la 2.61.0, suffit à justifier un fichier de verrouillage commité et des montées de version relues (leçon 4).
Exercices
1. Lire un plan (niveau 200). Un plan de production contient ces lignes. Que va-t-il se passer, et que faites-vous ?
# scaleway_instance_server.app["sig-app-1"] must be replaced
-/+ resource "scaleway_instance_server" "app" {
~ zone = "fr-par-1" -> "fr-par-3" # forces replacementSolution
L'instance sig-app-1 va être détruite en fr-par-1 puis recréée en fr-par-3, avec un nouvel identifiant et de nouvelles adresses : la zone est un attribut ForceNew. Par défaut, la destruction précède la création, donc une coupure de cette instance. Avant d'appliquer : vérifier que le changement de zone est voulu ; s'assurer que l'autre instance porte la charge (ou ajouter create_before_destroy) ; vérifier que tout ce qui référence l'instance (backend du répartiteur, règles, DNS) sera mis à jour dans le même plan.
2. Le cloud-init oublié (niveau 200). L'équipe modifie le fichier cloud-init des instances, applique, et constate que rien n'a changé sur les machines. Pourquoi, et quelle configuration corrige le problème ?
Solution
Selon le fournisseur et l'attribut, un changement de user_data est soit une mise à jour sur place des métadonnées (que cloud-init ne relit pas après le premier démarrage), soit ignoré par un ignore_changes. Dans les deux cas, la machine ne se reconfigure pas. Correction : rendre le remplacement explicite, avec une ressource terraform_data dont triggers_replace contient le hachage du fichier, et replace_triggered_by = [terraform_data.cloud_init] sur les instances, idéalement avec create_before_destroy pour éviter la coupure.
3. Le bon réglage (niveau 200). Pour chaque cas, quel argument de lifecycle choisissez-vous ? (a) La base sig-db ne doit jamais être détruite par erreur. (b) Un outil d'inventaire ajoute une étiquette inventaire=... aux instances, et Terraform veut la retirer à chaque plan. (c) Une ancienne instance doit sortir de la gestion de Terraform pour être reprise par une autre équipe, sans être détruite. (d) Le certificat du répartiteur doit être recréé avant que l'ancien soit supprimé.
Solution
(a) prevent_destroy = true, en sachant qu'il ne protège pas d'une suppression du bloc. (b) ignore_changes = [tags], commenté, ou mieux, faire porter l'étiquette par le code. (c) Le bloc removed avec lifecycle { destroy = false } (leçon 11), ou destroy = false dans le lifecycle de la ressource en Terraform 1.15 et plus, suivi d'un apply. (d) create_before_destroy = true, si le nom du certificat peut être dupliqué le temps de la bascule.
4. Un depends_on de trop (niveau 200). Une source de données scaleway_instance_image porte depends_on = [scaleway_vpc_private_network.pn]. Chaque fois que le réseau a un changement prévu, le plan affiche toutes les instances avec image = (known after apply) et annonce leur remplacement. Expliquez.
Solution
Avec depends_on, la source de données ne peut être lue qu'après l'application des changements du réseau : pendant le plan, son résultat est inconnu. L'identifiant de l'image devient donc « connu après l'application », et comme image est un attribut qui force le remplacement d'une instance, Terraform doit supposer qu'il peut changer : il annonce le remplacement. Il suffit de retirer le depends_on, qui n'exprimait aucune dépendance réelle (l'image ne dépend pas du réseau).
Récapitulatif
- Terraform ordonne les opérations par un graphe de dépendances déduit des références ; il crée dans l'ordre, détruit dans l'ordre inverse, et parallélise le reste (dix opérations par défaut). Un cycle est une erreur.
depends_onest un dernier recours, pour les dépendances sans valeur ; sur une source de données, il retarde sa lecture à l'apply.- Le fournisseur décide si un changement est une mise à jour sur place (
~) ou un remplacement (-/+) ; le plan désigne la cause par# forces replacement. Chez Scaleway, la zone et l'image d'une instance forcent le remplacement ; le type migre ; le moteur d'une base passe par une procédure bleu-vert qui peut perdre des écritures. lifecycle:create_before_destroy(se propage, suppose la coexistence),prevent_destroy(disparaît avec le bloc),ignore_changes(mises à jour seulement),replace_triggered_by(ressources gérées seulement),preconditionetpostcondition,destroy = false(Terraform 1.15 et plus).terraform apply -replace=remplacetaint.- Les provisionneurs sont un dernier recours ; cloud-init, Packer et la gestion de configuration les remplacent.
terraform_dataporte les déclencheurs et les rares provisionneurs locaux.
Pour aller plus loin
- La page du méta-argument
lifecycle, à relire avant chaque usage : chaque argument y a ses limites documentées. - Le code du fournisseur Scaleway (
internal/services/<produit>/), pour savoir exactement quels attributs forcent un remplacement ; la documentation de chaque ressource le signale aussi par des encadrés « Important ». - Le chapitre 5 de Terraform: Up & Running, sur les déploiements sans coupure et les pièges de
create_before_destroy. - La leçon suivante, qui génère des ressources en série avec
countetfor_each, et montre comment un mauvais choix de clé provoque des remplacements en cascade.
Sources
- HashiCorp, Terraform : le méta-argument lifecycle
- HashiCorp, Terraform : le méta-argument depends_on
- HashiCorp, Terraform : la commande graph
- HashiCorp, Terraform : provisionneurs
- HashiCorp, Terraform : la ressource terraform_data
- hashicorp/terraform, CHANGELOG (1.2 : replace_triggered_by et conditions ; 1.4 : terraform_data ; 1.7 : graph simplifié ; 1.15 : lifecycle destroy)
- Fournisseur Terraform de Scaleway : scaleway_instance_server et scaleway_rdb_instance
- scaleway/terraform-provider-scaleway, code source (schéma zonal, CustomizeDiff du serveur)
- Yevgeniy Brikman, Terraform: Up & Running (3e éd., O'Reilly, 2022), chapitre 5 : Terraform Tips and Tricks