Importer, déplacer et corriger la dérive
Pourquoi
L'équipe de Signalements a maintenant deux infrastructures. La première a été construite à la main pendant le cours Le cloud : les fondamentaux : elle tourne, des mairies l'utilisent, sa base contient des mois de signalements. La seconde existe en code depuis la leçon 10, sur le papier. Lancer terraform apply créerait une deuxième architecture à côté de la première, avec une nouvelle base vide. Supprimer la première pour la recréer en code imposerait une coupure, une migration de données et un changement d'adresse publique.
Ce qu'il faut, c'est adopter l'existant : dire à Terraform « cette base qui tourne, c'est scaleway_rdb_instance.principale », sans rien recréer. C'est l'import.
Une fois l'infrastructure sous gestion, deux autres besoins arrivent vite. Le code évolue : on renomme une ressource, on passe d'une liste à un dictionnaire, et Terraform ne doit pas en conclure qu'il faut détruire puis recréer. Et la réalité évolue aussi : quelqu'un modifie une règle de groupe de sécurité dans la console pendant un incident, et l'infrastructure ne correspond plus au code. C'est la dérive, que le glossaire définit déjà (dérive de configuration) et que le cours GitOps traitait pour Kubernetes. Cette leçon donne les outils de Terraform pour ces trois situations.
Les concepts
Ce qu'importer veut dire
Importer ne crée rien et ne modifie rien chez le fournisseur. Terraform lit la ressource existante par son API, à partir d'un identifiant, et écrit le résultat dans l'état, à l'adresse choisie. À partir de là, la ressource est gérée comme si Terraform l'avait créée : le plan suivant compare la configuration à ce qui a été lu, et propose de corriger les différences.
D'où une règle simple : après un import, le plan doit être vide. S'il ne l'est pas, la configuration ne décrit pas la ressource telle qu'elle est, et appliquer ce plan la modifierait, parfois en la remplaçant.
Deux façons d'importer
- La commande
terraform import ADRESSE ID, historique : elle écrit immédiatement dans l'état, une ressource à la fois, sans plan à relire. Une erreur d'adresse ou d'identifiant se découvre après coup. - Le bloc
import, apparu avec Terraform 1.5 : il se déclare dans la configuration, avec deux arguments,to(l'adresse de la ressource, qui doit correspondre à un blocresourceexistant) etid(l'identifiant chez le fournisseur). L'import fait alors partie du plan : on le relit, on le fait passer en revue de code, puis on l'applique comme le reste. Depuis Terraform 1.7, un blocimportacceptefor_each, pour importer une série de ressources semblables. La documentation actuelle mentionne aussi un argumentidentity, exclusif deid, pour les fournisseurs qui identifient leurs ressources par plusieurs champs.
Le bloc est la méthode à préférer : il transforme une opération sur l'état en changement relu.
Générer la configuration
Écrire à la main la configuration exacte d'une ressource existante est fastidieux. L'option -generate-config-out=FICHIER de terraform plan écrit, pour chaque bloc import sans bloc resource correspondant, une première version de la configuration, à partir de ce que l'API a renvoyé. La documentation de HashiCorp la décrit comme expérimentale depuis Terraform 1.5 et prévient que le résultat est « la meilleure supposition » de Terraform : pour une ressource au schéma complexe, il peut contenir des arguments incompatibles entre eux, qu'il faudra trier. Elle refuse d'écrire dans un fichier qui existe déjà.
Déplacer sans détruire : moved
Pour Terraform, une ressource est identifiée par son adresse dans la configuration (random_id.suffixe_bucket, local_file.instance[0]). Renommer le bloc, ou passer de count à for_each, change l'adresse : vu de l'état, l'ancienne ressource n'est plus décrite (donc à détruire) et une nouvelle apparaît (donc à créer). Pour une base de données, c'est une catastrophe.
Le bloc moved, avec from et to, dit à Terraform que l'objet a changé d'adresse : au plan, il met à jour l'état au lieu de détruire et recréer. On garde ces blocs dans le code un certain temps, au moins jusqu'à ce que tous les états qui utilisent la configuration aient été mis à jour ; dans un module partagé, longtemps.
Rendre une ressource à la main : removed
L'opération inverse de l'import : sortir une ressource de la gestion de Terraform, sans la détruire, par exemple pour la confier à une autre équipe ou à un autre outil. La commande historique est terraform state rm. Depuis Terraform 1.7, le bloc removed le fait dans la configuration, relu et planifié comme le reste :
removed {
from = scaleway_object_bucket.archives
lifecycle {
destroy = false
}
}Sans destroy = false, la valeur par défaut est true : la ressource est retirée et détruite. C'est la seule différence entre un bloc removed et la simple suppression du bloc resource, et elle vaut d'être écrite explicitement.
La dérive
Une dérive est un écart entre l'infrastructure réelle et ce que Terraform croit savoir d'elle. Elle a trois causes habituelles :
- une modification manuelle, dans la console ou par la CLI, souvent pendant un incident ;
- un autre outil qui gère les mêmes objets (un script, un second dépôt Terraform, un opérateur Kubernetes) ;
- le fournisseur lui-même, qui modifie un attribut (une version de moteur mise à jour, une adresse réattribuée).
À chaque plan, Terraform commence par rafraîchir l'état : il relit chaque ressource par l'API. Les différences entre l'état rafraîchi et la configuration deviennent des actions. La dérive est donc visible dans tout plan, mais mélangée aux changements voulus du code.
Deux options l'isolent :
terraform plan -refresh-onlyne propose aucune modification de l'infrastructure : il montre seulement ce qui a changé dehors depuis le dernier apply, et propose de l'enregistrer dans l'état ;terraform plan -detailed-exitcodechange le code de sortie :0si le plan est vide,1en cas d'erreur,2si le plan contient des changements. C'est ce qui permet à une tâche planifiée de détecter un écart sans lire la sortie.
En pratique
Les démonstrations de cette leçon utilisent les fournisseurs local et random, qui ne créent rien dans le cloud : les sorties montrées ont été produites avec Terraform 1.16.1. Les commandes Scaleway qui suivent sont tirées de la documentation du fournisseur, sans sortie.
Détecter une modification faite dehors
Une configuration minimale gère un fichier de configuration local, image miniature du fichier /etc/signalements/env des instances :
resource "local_file" "env" {
filename = "${path.module}/signalements.env"
content = "APP_VERSION=1.2.0\n"
file_permission = "0640"
}Après un terraform apply, quelqu'un modifie le fichier à la main (echo 'APP_VERSION=9.9.9' > signalements.env). Le plan en mode rafraîchissement seul :
$ terraform plan -refresh-only
local_file.env: Refreshing state... [id=3dc5b0db57348bf79a264991a5a590407f0fcd5a]
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:
# local_file.env has been deleted
- resource "local_file" "env" {
- content = <<-EOT
APP_VERSION=1.2.0
EOT -> null
...
}
This is a refresh-only plan, so Terraform will not take any actions to undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.
(La sortie est abrégée à l'endroit des ... : les sommes de contrôle du contenu.) Le fournisseur local traite un fichier dont le contenu a changé comme un fichier disparu : c'est un choix de ce fournisseur, et chaque fournisseur décide de la manière dont il relit ses ressources. Pour une ressource Scaleway, un attribut modifié dans la console apparaît en général comme un changement de cet attribut (~), pas comme une disparition.
Le plan ordinaire, lui, propose de corriger :
$ terraform plan
...
# local_file.env will be created
+ resource "local_file" "env" {
+ content = <<-EOT
APP_VERSION=1.2.0
EOT
...
Plan: 1 to add, 0 to change, 0 to destroy.
Et le code de sortie détaillé permet de l'automatiser :
$ terraform plan -detailed-exitcode > /dev/null; echo $?
2
2 : des changements. Une tâche planifiée qui lance cette commande chaque matin sur chaque environnement, et alerte sur un 2, est le détecteur de dérive le plus simple qui soit (leçon 12).
Importer avec un bloc et générer la configuration
Le bucket des pièces jointes du cours cloud s'appelle signalements-pj-7f3a : le suffixe avait été tiré au hasard. Pour l'exemple, adoptons ce suffixe comme une ressource random_id existante. Le fournisseur random accepte l'import d'un random_id à partir de sa valeur en base64 pour URL, ici fzo :
import {
to = random_id.suffixe_bucket
id = "fzo"
}Aucun bloc resource n'existe encore pour random_id.suffixe_bucket : demandons à Terraform de l'écrire.
$ terraform plan -generate-config-out=genere.tf
local_file.env: Refreshing state... [id=3dc5b0db57348bf79a264991a5a590407f0fcd5a]
random_id.suffixe_bucket: Preparing import... [id=fzo]
random_id.suffixe_bucket: Refreshing state... [id=fzo]
Terraform will perform the following actions:
# random_id.suffixe_bucket will be imported
# (config will be generated)
resource "random_id" "suffixe_bucket" {
b64_std = "fzo="
b64_url = "fzo"
byte_length = 2
dec = "32570"
hex = "7f3a"
id = "fzo"
}
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
$ cat genere.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.
# __generated__ by Terraform from "fzo"
resource "random_id" "suffixe_bucket" {
byte_length = 2
keepers = null
prefix = null
}
Le plan annonce un import et rien d'autre ; hex = "7f3a" confirme qu'il s'agit bien du suffixe du bucket. Le fichier généré est un point de départ : on retire les arguments à null inutiles, on le range dans le bon fichier, on l'adapte aux conventions du dépôt. Puis :
$ terraform apply
...
random_id.suffixe_bucket: Import complete [id=fzo]
Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.
Le bloc import peut ensuite être retiré : il n'a d'effet que si la ressource n'est pas déjà dans l'état. Le garder un temps ne coûte rien et documente l'opération dans l'historique Git.
Importer l'architecture du cours cloud
Le même mécanisme s'applique aux ressources Scaleway, avec un identifiant propre à chaque ressource. La section Import de la documentation de chaque ressource du fournisseur donne son format ; pour celles de la leçon 10 :
| Ressource | Format de l'identifiant d'import |
|---|---|
scaleway_vpc, scaleway_vpc_private_network, scaleway_rdb_instance, scaleway_ipam_ip | {région}/{id}, par exemple fr-par/11111111-1111-1111-1111-111111111111 |
scaleway_instance_server, scaleway_instance_ip, scaleway_instance_security_group, scaleway_vpc_public_gateway, scaleway_vpc_gateway_network, scaleway_lb, scaleway_lb_backend, scaleway_lb_frontend | {zone}/{id}, par exemple fr-par-1/11111111-1111-1111-1111-111111111111 |
scaleway_object_bucket | {région}/{nom}, suivi de @{id du projet} si le bucket n'est pas dans le projet par défaut de vos identifiants |
scaleway_account_project | {id}, sans localité |
La localité dans l'identifiant reflète ce que la leçon 3 du cours cloud appelait la portée de la ressource : zonale, régionale ou globale. L'oublier ou se tromper de zone donne une ressource introuvable.
Les identifiants se lisent avec la CLI. Par exemple, pour la base et les instances :
$ scw rdb instance list name=sig-db region=fr-par -o json | jq -r '.[0].id'
$ scw instance server list zone=fr-par-1 name=sig-app-1 -o json | jq -r '.[0].id'
Puis un fichier import.tf, relu en demande de fusion comme le reste :
import {
to = scaleway_rdb_instance.principale
id = "fr-par/<id de sig-db>"
}
import {
for_each = {
"sig-app-1" = "fr-par-1/<id de sig-app-1>"
"sig-app-2" = "fr-par-2/<id de sig-app-2>"
}
to = scaleway_instance_server.app[each.key]
id = each.value
}
import {
to = scaleway_object_bucket.pieces_jointes
id = "fr-par/signalements-pj-7f3a@<id du projet signalements>"
}Le plan qui suit, avec les identifiants du projet, est le moment de vérité. Attendez-vous à ce qu'il ne soit pas vide au premier essai, et lisez chaque différence :
- Une différence d'étiquettes ou de description : ajoutez-la au code ou acceptez que Terraform la corrige, c'est sans risque.
- Un
-/+sur une ressource avec état (la base, le bucket) : un argument qui force le remplacement diffère. Exemple réel : la documentation du fournisseur indique qu'un changement deis_ha_clusterou deuser_namerecrée la base. Le code de la leçon 10 active la haute disponibilité en production ; la base du cours cloud a été créée sans. Importée telle quelle, elle serait remplacée par une base vide. Ne laissez jamais passer un remplacement de base par un import : alignez d'abord le code sur la réalité (ici,is_ha_cluster = falsele temps de l'import), puis faites évoluer la réalité par une opération prévue et préparée (la leçon 3 de Scaleway en pratique traite le passage en haute disponibilité), jusqu'à obtenir un plan sans remplacement. - Le point d'accès public de la base : si vous avez suivi la leçon 6 du cours cloud jusqu'au bout, il a été supprimé et la base n'a plus qu'un point d'accès privé, ce que décrit le code. Sinon, le plan proposera de modifier les points d'accès : lisez précisément s'il s'agit d'une modification ou d'un remplacement.
- Un
-/+sur une instance : les instances du cours cloud ont uncloud_initdifférent de celui du modèle de la leçon 10. Les remplacer est acceptable, l'instance étant du bétail, mais faites-le volontairement, une à une, pas comme effet de bord d'un import. - Les ressources qui n'existent pas encore (adresses IPAM réservées, secret) apparaissent en création : c'est normal.
Important
Un import se fait sans -auto-approve, environnement par environnement, et seulement quand le plan ne contient plus aucun remplacement non voulu. L'import en lui-même ne touche à rien ; c'est l'apply du plan qui l'accompagne qui modifie l'infrastructure.
La commande historique reste utile pour une ressource isolée, ou avec un outil qui ne connaît pas les blocs :
$ terraform import 'scaleway_instance_server.app["sig-app-1"]' fr-par-1/<id>
Elle écrit directement dans l'état, sans plan préalable.
Renommer avec moved
Le nom random_id.suffixe_bucket était trop long : renommons le bloc en random_id.suffixe. Sans autre précaution, le plan est sans ambiguïté :
$ terraform plan
...
# random_id.suffixe will be created
# random_id.suffixe_bucket will be destroyed
Plan: 1 to add, 0 to change, 1 to destroy.
Un nouveau suffixe, donc un nouveau nom de bucket au prochain apply de la leçon 10. Avec un bloc moved :
moved {
from = random_id.suffixe_bucket
to = random_id.suffixe
}$ terraform plan
...
Terraform will perform the following actions:
# random_id.suffixe_bucket has moved to random_id.suffixe
resource "random_id" "suffixe" {
id = "fzo"
# (5 unchanged attributes hidden)
}
Plan: 0 to add, 0 to change, 0 to destroy.
Passer de count à for_each
C'est le cas le plus fréquent en pratique : une première version décrit les instances avec count, et l'on découvre (leçon 9) qu'un index numérique fait glisser toutes les instances quand on retire la première. Deux fichiers locaux jouent le rôle des deux instances :
resource "local_file" "instance" {
count = 2
filename = "${path.module}/sig-app-${count.index + 1}.txt"
content = "zone=fr-par-${count.index + 1}\n"
}On réécrit avec for_each :
locals {
instances = {
"sig-app-1" = "fr-par-1"
"sig-app-2" = "fr-par-2"
}
}
resource "local_file" "instance" {
for_each = local.instances
filename = "${path.module}/${each.key}.txt"
content = "zone=${each.value}\n"
}Le plan, sans bloc moved :
$ terraform plan | grep -E '^ # |Plan:'
# local_file.instance[0] will be destroyed
# (because resource does not use count)
# local_file.instance[1] will be destroyed
# (because resource does not use count)
# local_file.instance["sig-app-1"] will be created
# local_file.instance["sig-app-2"] will be created
Plan: 2 to add, 0 to change, 2 to destroy.
Sur de vraies instances, deux destructions et deux créations : une coupure. Avec un bloc moved par élément, qui associe chaque index à sa clé :
moved {
from = local_file.instance[0]
to = local_file.instance["sig-app-1"]
}
moved {
from = local_file.instance[1]
to = local_file.instance["sig-app-2"]
}$ terraform plan | grep -E '^ # |Plan:'
# local_file.instance[0] has moved to local_file.instance["sig-app-1"]
# local_file.instance[1] has moved to local_file.instance["sig-app-2"]
Plan: 0 to add, 0 to change, 0 to destroy.
Rendre une ressource avec removed
Le suffixe ne sert plus à rien en code : le nom du bucket est désormais une variable. On veut sortir random_id.suffixe de l'état sans rien détruire. On supprime son bloc resource, et l'on écrit :
removed {
from = random_id.suffixe
lifecycle {
destroy = false
}
}$ terraform plan
...
Terraform will perform the following actions:
# random_id.suffixe will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)
. resource "random_id" "suffixe" {
id = "fzo"
# (5 unchanged attributes hidden)
}
Plan: 0 to add, 0 to change, 0 to destroy.
If you apply this plan, Terraform will discard its tracking information for
the following objects, but it will not delete them:
- random_id.suffixe
After applying this plan, Terraform will no longer manage these objects. You
will need to import them into Terraform to manage them again.
Après l'apply, terraform state list ne mentionne plus random_id.suffixe.
Traiter une dérive
Quand le plan quotidien signale un écart, trois réponses sont possibles, et le choix est une décision d'équipe, pas un réflexe :
| Situation | Réponse | Comment |
|---|---|---|
| La modification était une erreur, ou un contournement d'incident terminé | Réappliquer le code | terraform apply : la réalité revient à la configuration |
| La modification était bonne (une règle de pare-feu ajoutée en urgence et qu'il faut garder) | Adopter dans le code | Reporter la modification dans le .tf, faire relire, appliquer : le plan doit devenir vide |
| L'attribut est légitimement modifié par un autre acteur (un autoscaler qui change un nombre d'instances, le fournisseur qui met à jour une version mineure) | Ignorer cet attribut | lifecycle { ignore_changes = [...] } (leçon 8) |
terraform apply -refresh-only a un usage plus étroit : enregistrer dans l'état ce que le rafraîchissement a constaté, par exemple après qu'une ressource a été supprimée volontairement hors de Terraform, pour que les sorties et les références reflètent la réalité. Il ne règle pas la dérive : au plan suivant, Terraform proposera de nouveau de recréer ce que le code décrit.
Sous le capot
L'import passe par la lecture du fournisseur. Terraform appelle la fonction d'import du fournisseur avec l'identifiant, qui rend un objet partiel (souvent l'identifiant seul, découpé en localité et UUID), puis appelle la lecture ordinaire de la ressource, la même que celle du rafraîchissement. C'est pourquoi le format de l'identifiant est propre à chaque ressource, et pourquoi ce qui n'est pas lisible par l'API ne peut pas être importé : un mot de passe ou un argument en écriture seule n'est jamais relu, et un import ne peut pas le reconstituer. Après l'import de la base, l'état ne contient donc ni le mot de passe, ni d'historique de sa version : la documentation du fournisseur ne dit pas si le premier apply qui suit enverra le mot de passe éphémère. Lisez le plan, et préparez l'application à relire son secret si c'est le cas.
moved agit sur l'état, pendant le plan. Avant de calculer les actions, Terraform applique les déplacements déclarés à sa copie de l'état : l'objet enregistré sous l'ancienne adresse passe sous la nouvelle. Le calcul des différences voit ensuite un objet existant, à la bonne adresse, conforme à la configuration. Rien n'est envoyé au fournisseur.
Le rafraîchissement n'est qu'une suite de lectures. Pour chaque ressource de l'état, Terraform demande au fournisseur de la relire. Si l'API répond « introuvable », le fournisseur retire l'objet de l'état rafraîchi, d'où le « has been deleted ». Sur une grande configuration, ce sont des centaines d'appels d'API à chaque plan, ce qui explique la durée des plans et l'intérêt de découper l'infrastructure en plusieurs états.
Pièges courants
Importer à la mauvaise adresse. Un import vers scaleway_instance_server.app["sig-app-1"] d'une instance qui est en réalité sig-app-2 produit un plan qui propose de la modifier pour qu'elle ressemble à sig-app-1. Vérifiez le nom dans le bloc relu du plan avant d'appliquer.
Se tromper de zone dans l'identifiant. fr-par-1/<id> pour une instance de fr-par-2 : le fournisseur ne la trouve pas, et l'import échoue. C'est le symptôme classique d'une ressource zonale importée avec la zone par défaut.
Oublier le projet d'un bucket. Sans le suffixe @<id du projet>, le fournisseur cherche le bucket avec le projet par défaut de vos identifiants ; s'il est dans un autre projet, la documentation du fournisseur prévient que l'attribut project_id se comportera mal.
Appliquer un import qui remplace. Le plan dit « 1 to import, 1 to destroy, 1 to add » sur la base : ce n'est pas un import, c'est un remplacement précédé d'un import. On n'applique pas.
Retirer trop tôt les blocs moved. Un état qui n'a pas encore été mis à jour (un autre environnement, un poste qui n'a pas fait de plan depuis) verra de nouveau une destruction et une création. Gardez les blocs moved jusqu'à ce que tous les états aient été appliqués.
Supprimer un bloc resource en croyant le « retirer ». Sans bloc removed avec destroy = false, le plan suivant détruit la ressource. C'est exactement ce que l'on voulait éviter.
Confondre -refresh-only et correction. apply -refresh-only met l'état d'accord avec la réalité, pas la réalité d'accord avec le code.
Sécurité
- Un import relu. Le bloc
importfait de l'adoption d'une ressource un changement de code, relu en demande de fusion. La commandeterraform import, lancée depuis un poste, écrit dans l'état partagé sans trace dans Git : réservez-la aux cas où le bloc n'est pas possible, et notez-la. - La dérive est un signal de sécurité. Une règle de groupe de sécurité ouverte sur
0.0.0.0/0qui apparaît dans le plan quotidien, sans demande de fusion correspondante, est soit un contournement oublié, soit une compromission. Le plan de dérive est un contrôle de détection, au même titre que les alertes d'Audit Trail de Scaleway en pratique, leçon 15, qui disent qui a fait la modification. - Les droits de lecture suffisent pour détecter. Une tâche de détection de dérive n'a besoin que de lire l'infrastructure et l'état ; elle ne doit pas porter les droits d'écriture du pipeline d'apply (leçon 12).
- Ignorer, avec parcimonie. Chaque
ignore_changesest un angle mort du détecteur de dérive. Documentez pourquoi, dans un commentaire à côté.
En production
- Interdire les modifications manuelles, ou presque. Une fois l'infrastructure en code, les membres de l'équipe gardent des droits de lecture sur la production, et les droits d'écriture passent par le pipeline (cours cloud, leçon 7). Un accès de secours (« bris de glace ») reste possible, tracé, et toute modification faite par ce biais est reportée dans le code le jour même. C'est la même discipline que celle du GitOps, où l'agent corrige la dérive tout seul ; Terraform, lui, ne fait que la signaler.
- Importer par étapes. Sur une infrastructure de plusieurs dizaines de ressources, on importe un domaine à la fois (le réseau, puis le calcul, puis la base), chaque étape dans sa propre demande de fusion, avec un plan vide à la fin.
- La dérive mesurée. Le nombre de jours avec une dérive détectée, et le temps pour la résorber, sont de bons indicateurs de la discipline de l'équipe.
- Les outils. HCP Terraform propose une détection de dérive intégrée ; Atlantis et les autres outils d'orchestration (leçon 12) permettent de lancer le plan de dérive dans le même cadre que les autres.
Exercices
1. Lire un plan d'import (niveau 200). Après l'import de sig-db en production, le plan affiche -/+ resource "scaleway_rdb_instance" "principale", avec la mention forces replacement à côté de is_ha_cluster. Que se passe-t-il si vous appliquez ? Que faites-vous ?
Solution
Appliquer détruirait la base de production et en créerait une nouvelle, vide : perte de données. La cause : le code demande la haute disponibilité en production (is_ha_cluster = var.environnement == "prod"), la base réelle n'en a pas, et la documentation du fournisseur indique qu'un changement de cet argument recrée l'instance de base. On n'applique pas. On aligne d'abord le code sur la réalité (is_ha_cluster = false pour cet environnement), on importe avec un plan sans remplacement, puis on prépare le passage en haute disponibilité comme une opération à part entière, avec sauvegarde vérifiée et fenêtre de maintenance.
2. Passer de count à for_each (niveau 200). Une configuration décrit trois groupes de sécurité avec count = length(var.zones), et var.zones = ["fr-par-1", "fr-par-2", "fr-par-3"]. Écrivez la version for_each et les blocs moved nécessaires.
Solution
resource "scaleway_instance_security_group" "app" {
for_each = toset(var.zones)
zone = each.value
# ...
}
moved {
from = scaleway_instance_security_group.app[0]
to = scaleway_instance_security_group.app["fr-par-1"]
}
moved {
from = scaleway_instance_security_group.app[1]
to = scaleway_instance_security_group.app["fr-par-2"]
}
moved {
from = scaleway_instance_security_group.app[2]
to = scaleway_instance_security_group.app["fr-par-3"]
}Le plan doit annoncer trois déplacements et aucune autre action. Les références ailleurs dans le code (scaleway_instance_security_group.app[0].id) doivent aussi être réécrites avec la clé.
3. Concevoir la détection de dérive (niveau 200). Décrivez une tâche qui détecte la dérive de la préproduction et de la production chaque jour ouvré, et ce qu'elle fait de chaque code de sortie.
Solution
Une tâche planifiée (par exemple un schedule de GitHub Actions, leçon 12), une exécution par environnement, avec une clé d'API en lecture : terraform init sur l'état de l'environnement, puis terraform plan -lock=false -detailed-exitcode -var-file=.... Code 0 : rien. Code 2 : alerte à l'équipe (ticket ou message), avec la sortie du plan pour savoir quoi regarder. Code 1 : erreur de la tâche elle-même (identifiants expirés, API indisponible), à traiter comme une panne de la surveillance. -lock=false évite qu'une tâche de lecture bloque un apply légitime ; elle ne modifie pas l'état.
Récapitulatif
- Importer inscrit dans l'état une ressource existante, sans la modifier ; après un import, le plan doit être vide.
- Le bloc
import(to,id,for_eachdepuis 1.7) fait de l'import un changement relu ;-generate-config-outécrit une première configuration, à reprendre à la main. - L'identifiant d'import d'une ressource Scaleway porte sa localité :
{zone}/{id},{région}/{id}, ou{région}/{nom}@{projet}pour un bucket. movedchange l'adresse d'une ressource dans l'état sans rien détruire : renommage, passage decountàfor_each.removedavecdestroy = falsesort une ressource de la gestion de Terraform sans la détruire.- La dérive se voit au rafraîchissement ;
plan -refresh-onlyl'isole,plan -detailed-exitcode(code2) l'automatise. - Face à une dérive : réappliquer, adopter dans le code, ou ignorer un attribut, en le justifiant.
- Les arguments en écriture seule ne sont jamais relus : un import ne les reconstitue pas.
Pour aller plus loin
- La documentation de HashiCorp sur les blocs
import,movedetremoved, et la page sur la génération de configuration. - La section Import de la documentation de chaque ressource du fournisseur Scaleway.
- La leçon 12, qui fait tourner la détection de dérive dans le pipeline.
Sources
- HashiCorp, Terraform : bloc import
- HashiCorp, Terraform : générer la configuration des ressources importées
- HashiCorp, Terraform : bloc removed
- HashiCorp, Terraform 1.7 adds test mocking and config-driven remove (annonce)
- Fournisseur Terraform de Scaleway, section Import de chaque ressource (docs/resources)