Boucles, conditions et blocs dynamiques
Pourquoi
L'architecture de Signalements contient des ressources qui vont par séries : deux instances, une par zone ; plusieurs règles dans un groupe de sécurité ; des enregistrements DNS par environnement ; bientôt trois métropoles, chacune avec son bucket. La première tentation est de copier-coller le bloc resource et de changer le nom. À deux exemplaires, cela tient ; à six, chaque modification doit être faite six fois, et le jour où l'on en oublie une, la sixième métropole a une règle de pare-feu de moins que les autres. Le code n'est plus la description fidèle de l'intention.
HCL n'est pas un langage de programmation, mais il a de quoi décrire des séries : deux méta-arguments, count et for_each, qui multiplient une ressource ; des expressions qui transforment des listes et des tables ; des blocs dynamiques qui génèrent des sous-blocs. Bien utilisés, ils rendent une configuration plus courte et plus sûre. Mal utilisés, ils produisent l'un des accidents les plus classiques de Terraform : retirer un élément au milieu d'une liste et voir le plan proposer de remplacer tous ceux qui suivent. Cette leçon montre les deux côtés, avec des sorties réelles produites par Terraform 1.16.1 sur des ressources terraform_data, qui ne coûtent rien.
Les concepts
count : un nombre d'exemplaires
count prend un nombre entier et crée autant d'instances de la ressource. Chacune est désignée par son index : terraform_data.instance[0], terraform_data.instance[1]... Dans le bloc, count.index donne l'index de l'instance en cours.
resource "scaleway_instance_server" "app" {
count = 2
name = "sig-app-${count.index + 1}"
# ...
}La documentation de Terraform le résume : count convient pour des instances presque identiques. Le piège vient quand on l'utilise pour parcourir une liste de choses différentes, parce que l'identité de chaque instance est alors sa position dans la liste, et non ce qu'elle représente.
for_each : une instance par clé
for_each prend une table (map) ou un ensemble de chaînes (set) et crée une instance par élément. L'instance est désignée par sa clé : terraform_data.instance["sig-app-1"]. Dans le bloc, each.key donne la clé, each.value la valeur (pour un ensemble, each.value est égal à each.key).
resource "scaleway_instance_server" "app" {
for_each = {
"sig-app-1" = "fr-par-1"
"sig-app-2" = "fr-par-2"
}
name = each.key
zone = each.value
# ...
}L'identité de chaque instance est cette fois sa clé, stable, choisie par vous. Retirer sig-app-1 ne touche pas sig-app-2. La documentation conseille for_each dès que les instances ont des valeurs qui ne se déduisent pas d'un simple entier.
Deux contraintes, documentées :
- Une table ou un ensemble, pas une liste. Une liste a un ordre et peut contenir des doublons, ce qui ne fait pas une clé.
toset()convertit une liste de chaînes en ensemble (doublons supprimés),tomap()ou une expressionforfabriquent une table. - Des clés connues au moment du plan. Toutes les valeurs que parcourt
for_each(et la valeur decount) doivent être connues avant toute opération sur l'infrastructure. Une clé fabriquée à partir d'un attribut que l'API ne donnera qu'à la création (un identifiant, un nom aléatoire) est refusée. Une valeur sensible l'est aussi, puisque la clé apparaît en clair dans l'adresse de l'instance.
On ne peut pas mettre count et for_each dans le même bloc.
Les expressions for
Une expression for fabrique une collection à partir d'une autre, comme une compréhension de liste en Python :
[for nom, i in var.instances : nom]produit une liste (crochets) ;{for nom, i in var.instances : nom => i.zone}produit une table (accolades, avec=>) ;- un
iffinal filtre :[for nom, i in var.instances : nom if i.zone != "fr-par-3"].
C'est l'outil qui transforme les données « comme on les pense » (une table d'instances avec leurs attributs) en données « comme for_each les veut » (une table à clés stables).
Les splats
Un splat [*] est un raccourci pour extraire un attribut de tous les éléments d'une liste : terraform_data.instance[*].output vaut [for i in terraform_data.instance : i.output]. Il fonctionne sur une ressource à count (qui est une liste), pas directement sur une ressource à for_each (qui est une table) : il faut passer par values().
Les blocs dynamiques
Certaines ressources contiennent des blocs imbriqués répétés : les règles inbound_rule d'un groupe de sécurité Scaleway, les frontend d'un répartiteur. Un bloc dynamic génère ces sous-blocs à partir d'une collection :
dynamic "inbound_rule" {
for_each = var.regles_entrantes
content {
port = inbound_rule.value.port
ip_range = inbound_rule.value.ip_range
# ...
}
}L'étiquette ("inbound_rule") dit quel type de bloc générer ; dans content, la variable d'itération porte par défaut le même nom (l'argument iterator permet d'en choisir un autre). La documentation fixe deux limites : un bloc dynamique ne génère que des blocs propres à la ressource, jamais les blocs de méta-arguments comme lifecycle ou provisioner ; et il doit rester exceptionnel. Elle recommande d'écrire les blocs littéralement chaque fois que c'est possible, et de réserver dynamic aux modules réutilisables qui doivent cacher des détails à leurs utilisateurs, parce que leur abus rend la configuration difficile à lire et à maintenir.
Les conditions
HCL a une expression conditionnelle : condition ? valeur_si_vrai : valeur_si_faux, où les deux valeurs doivent être du même type. Combinée aux méta-arguments, elle crée une ressource seulement si une condition est vraie :
count = var.avec_bastion ? 1 : 0: zéro ou une instance ;for_each = var.avec_ip_publique ? var.zones : {}: toutes les instances, ou aucune.
En pratique
Le piège de count
Trois instances décrites par une liste de noms. Pour rendre le piège visible, la « fausse instance » terraform_data met son nom dans triggers_replace, qui force un remplacement quand la valeur change, comme le ferait le nom d'une ressource qu'une API ne sait pas renommer :
variable "instances" {
type = list(string)
default = ["sig-app-1", "sig-app-2", "sig-app-3"]
}
# terraform_data joue le rôle d'une instance : changer son nom force son remplacement.
resource "terraform_data" "instance" {
count = length(var.instances)
triggers_replace = var.instances[count.index]
}$ terraform apply -auto-approve -no-color
...
$ terraform state list
terraform_data.instance[0]
terraform_data.instance[1]
terraform_data.instance[2]
L'équipe décommissionne sig-app-1, la plus ancienne. Elle la retire de la liste :
$ terraform plan -no-color -var 'instances=["sig-app-2","sig-app-3"]'
...
Terraform will perform the following actions:
# terraform_data.instance[0] must be replaced
-/+ resource "terraform_data" "instance" {
~ id = "01cae795-27d5-ce01-3705-4798d43e929e" -> (known after apply)
~ triggers_replace = "sig-app-1" -> "sig-app-2"
}
# terraform_data.instance[1] must be replaced
-/+ resource "terraform_data" "instance" {
~ id = "d59be8b0-8865-8a07-590d-99b95b438e56" -> (known after apply)
~ triggers_replace = "sig-app-2" -> "sig-app-3"
}
# terraform_data.instance[2] will be destroyed
# (because index [2] is out of range for count)
- resource "terraform_data" "instance" {
- id = "5fcd009c-383b-4b7a-e198-f7932a0e9daa" -> null
- triggers_replace = "sig-app-3" -> null
}
Plan: 2 to add, 0 to change, 3 to destroy.
On voulait supprimer une instance ; Terraform propose d'en détruire trois et d'en recréer deux. Lisez la raison : l'index [0] s'appelait sig-app-1, il s'appelle maintenant sig-app-2 ; l'index [1] passe de sig-app-2 à sig-app-3 ; l'index [2] n'existe plus. Pour Terraform, l'identité est la position, et toutes les positions ont glissé. Sur de vraies instances, appliquer ce plan coupe le service pendant que sig-app-2 et sig-app-3 sont recréées, pour rien.
La même chose avec for_each
La même série, décrite par une table dont les clés sont les noms :
variable "instances" {
type = map(object({
zone = string
type = string
}))
default = {
"sig-app-1" = { zone = "fr-par-1", type = "PRO2-XXS" }
"sig-app-2" = { zone = "fr-par-2", type = "PRO2-XXS" }
"sig-app-3" = { zone = "fr-par-3", type = "PRO2-XXS" }
}
}
resource "terraform_data" "instance" {
for_each = var.instances
triggers_replace = "${each.key}@${each.value.zone}"
input = each.value.type
}
output "par_zone" {
value = { for nom, i in var.instances : i.zone => nom }
}$ terraform apply -auto-approve -no-color
...
Outputs:
par_zone = {
"fr-par-1" = "sig-app-1"
"fr-par-2" = "sig-app-2"
"fr-par-3" = "sig-app-3"
}
$ terraform state list
terraform_data.instance["sig-app-1"]
terraform_data.instance["sig-app-2"]
terraform_data.instance["sig-app-3"]
Les adresses portent maintenant les noms. Retirons sig-app-1, avec un fichier de valeurs moins.tfvars qui ne garde que les deux autres :
$ terraform plan -no-color -var-file=moins.tfvars
...
Terraform will perform the following actions:
# terraform_data.instance["sig-app-1"] will be destroyed
# (because key ["sig-app-1"] is not in for_each map)
- resource "terraform_data" "instance" {
- id = "fc16f1a7-a6e5-fd79-09cf-d105fdf5b261" -> null
- input = "PRO2-XXS" -> null
- output = "PRO2-XXS" -> null
- triggers_replace = "sig-app-1@fr-par-1" -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
Une destruction, celle que l'on voulait. C'est la raison pour laquelle ce cours décrit les instances de Signalements par une table : le plan dit exactement ce que l'on a changé.
Tip
Une configuration existante écrite avec count se convertit en for_each sans rien détruire, grâce à des blocs moved qui disent à Terraform que terraform_data.instance[0] s'appelle désormais terraform_data.instance["sig-app-1"]. C'est l'objet de la leçon 11.
Deux erreurs de for_each
Une liste passée à for_each :
$ terraform plan -no-color
Error: Invalid for_each argument
on main.tf line 7, in resource "terraform_data" "instance":
7: for_each = var.zones
├────────────────
│ var.zones is a list of string
The given "for_each" argument value is unsuitable: the "for_each" argument
must be a map, or set of strings, and you have provided a value of type list
of string.
La correction est for_each = toset(var.zones).
Une clé inconnue au moment du plan, ici un bucket dont le nom contient un suffixe aléatoire :
resource "random_pet" "suffixe" {}
resource "terraform_data" "bucket" {
for_each = toset(["signalements-pj-${random_pet.suffixe.id}"])
input = each.key
}$ terraform plan -no-color
...
Error: Invalid for_each argument
on main.tf line 13, in resource "terraform_data" "bucket":
13: for_each = toset(["signalements-pj-${random_pet.suffixe.id}"])
├────────────────
│ random_pet.suffixe.id is a string, known only after apply
The "for_each" set includes values derived from resource attributes that
cannot be determined until apply, and so Terraform cannot determine the full
set of keys that will identify the instances of this resource.
When working with unknown values in for_each, it's better to use a map value
where the keys are defined statically in your configuration and where only
the values contain apply-time results.
Alternatively, you could use the -target planning option to first apply only
the resources that the for_each value depends on, and then apply a second
time to fully converge.
Le message donne la bonne solution : des clés statiques, écrites dans le code, et des valeurs qui peuvent venir de l'apply. Ici, for_each = { photos = "signalements-pj-${random_pet.suffixe.id}" } fonctionne, puisque la clé photos est connue. L'autre solution proposée, -target, est un contournement à éviter dans un usage courant : elle produit des états intermédiaires que personne n'a relus.
Transformer les collections
La console de Terraform (terraform console) évalue des expressions dans le contexte de la configuration. Chaque expression ci-dessous a été passée à la console, dans le répertoire de l'exemple for_each :
> [for nom, i in var.instances : nom if i.zone != "fr-par-3"]
[
"sig-app-1",
"sig-app-2",
]
> { for nom, i in var.instances : nom => upper(i.zone) }
{
"sig-app-1" = "FR-PAR-1"
"sig-app-2" = "FR-PAR-2"
"sig-app-3" = "FR-PAR-3"
}
> values(terraform_data.instance)[*].input
[
"PRO2-XXS",
"PRO2-XXS",
"PRO2-XXS",
]
> toset(["fr-par-1", "fr-par-2", "fr-par-1"])
toset([
"fr-par-1",
"fr-par-2",
])
Le troisième résultat montre le splat sur une ressource à for_each : values() d'abord, pour passer de la table à une liste. Le quatrième montre toset() qui retire le doublon.
Pour générer une série à deux dimensions (chaque environnement dans chaque zone), trois fonctions servent :
> setproduct(["preprod", "prod"], ["fr-par-1", "fr-par-2"])
tolist([
[
"preprod",
"fr-par-1",
],
[
"preprod",
"fr-par-2",
],
[
"prod",
"fr-par-1",
],
[
"prod",
"fr-par-2",
],
])
> flatten([for env in ["preprod", "prod"] : [for z in ["fr-par-1", "fr-par-2"] : "${env}-${z}"]])
[
"preprod-fr-par-1",
"preprod-fr-par-2",
"prod-fr-par-1",
"prod-fr-par-2",
]
> zipmap(["sig-app-1", "sig-app-2"], ["172.16.20.11", "172.16.20.12"])
{
"sig-app-1" = "172.16.20.11"
"sig-app-2" = "172.16.20.12"
}
setproductdonne toutes les combinaisons ;flattenaplatit une liste de listes, ce qui rend les expressionsforimbriquées utilisables ;zipmapassocie deux listes de même longueur en une table.
Le motif complet, pour for_each, est une table dont la clé combine les deux dimensions :
locals {
instances = {
for p in setproduct(["preprod", "prod"], ["fr-par-1", "fr-par-2"]) :
"${p[0]}-${p[1]}" => { env = p[0], zone = p[1] }
}
}La console donne pour local.instances une table à quatre clés, preprod-fr-par-1 à prod-fr-par-2, chacune avec son env et sa zone. Les clés sont lisibles dans le plan, et retirer une zone ne touche que ses deux instances.
Une ressource conditionnelle
variable "avec_bastion" {
type = bool
default = false
}
resource "terraform_data" "bastion" {
count = var.avec_bastion ? 1 : 0
input = "gw-signalements"
}
output "bastion" {
value = one(terraform_data.bastion[*].output)
}Avec la valeur par défaut, rien n'est créé, et la sortie vaut null (elle n'est donc pas affichée). Avec -var avec_bastion=true :
$ terraform apply -auto-approve -no-color -var avec_bastion=true
...
terraform_data.bastion[0]: Creating...
terraform_data.bastion[0]: Creation complete after 0s [id=47cd7c2c-0bd8-2699-aff3-6a571f192448]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
bastion = "gw-signalements"
La fonction one() est faite pour ce cas : elle renvoie l'unique élément d'une liste, null si la liste est vide, et une erreur si elle en contient plusieurs. Elle évite le fragile terraform_data.bastion[0].output, qui échoue quand count vaut zéro.
Signalements chez Scaleway : instances par zone et règles dynamiques
La configuration suivante a été validée par terraform validate avec le fournisseur Scaleway 2.84.0 ; elle n'a pas été appliquée pour ce cours. Elle génère les instances depuis une table, un groupe de sécurité avec des règles dynamiques, et des adresses publiques optionnelles :
terraform {
required_providers {
scaleway = {
source = "scaleway/scaleway"
version = "~> 2.84"
}
}
}
variable "regles_entrantes" {
description = "Ports ouverts vers l'interface publique, par plage d'adresses."
type = list(object({
port = number
ip_range = string
}))
default = [
{ port = 22, ip_range = "198.51.100.7/32" },
{ port = 443, ip_range = "0.0.0.0/0" },
]
}
resource "scaleway_instance_security_group" "sig_app" {
name = "sg-sig-app"
zone = "fr-par-1"
stateful = true
inbound_default_policy = "drop"
outbound_default_policy = "accept"
dynamic "inbound_rule" {
for_each = var.regles_entrantes
content {
action = "accept"
protocol = "TCP"
port = inbound_rule.value.port
ip_range = inbound_rule.value.ip_range
}
}
}
variable "zones" {
type = map(string)
default = {
"sig-app-1" = "fr-par-1"
"sig-app-2" = "fr-par-2"
}
}
variable "avec_ip_publique" {
type = bool
default = false
}
resource "scaleway_instance_ip" "publique" {
for_each = var.avec_ip_publique ? var.zones : {}
zone = each.value
}
resource "scaleway_instance_server" "app" {
for_each = var.zones
name = each.key
zone = each.value
type = "PRO2-XXS"
image = "ubuntu_noble"
ip_id = var.avec_ip_publique ? scaleway_instance_ip.publique[each.key].id : null
}$ terraform validate -no-color
Success! The configuration is valid.
Quelques points :
- Les règles du groupe sont des blocs
inbound_rule(le schéma du fournisseur les déclare comme une liste de blocs imbriqués) : c'est le cas d'usage légitime dedynamic. L'argumentipde ces règles est déprécié au profit deip_rangedans le fournisseur 2.84. - Les adresses publiques et les instances partagent les mêmes clés (
sig-app-1,sig-app-2) : c'est ce qui permet d'écrirescaleway_instance_ip.publique[each.key]dans l'instance. Aligner les clés de séries liées est la règle qui rendfor_eachlisible. ip_id = ... : null:nullsignifie « argument absent », et laisse le fournisseur appliquer son comportement par défaut. La configuration complète de la leçon 10 attache en plus chaque instance au réseau privé et au groupe de sécurité de sa zone ; ici, le groupe n'existe qu'enfr-par-1, ce qui suffit à illustrerdynamic.
Sous le capot
L'expansion. Pendant le plan, Terraform évalue d'abord count ou for_each, puis développe le bloc en autant d'instances, chacune avec son adresse ([0] ou ["sig-app-1"]). C'est pourquoi ces valeurs doivent être connues avant tout le reste : sans elles, Terraform ne sait pas combien de nœuds mettre dans le graphe de la leçon 8. Dans la sortie de terraform graph -type=plan, le suffixe (expand) des nœuds désigne précisément cette étape.
L'état garde la clé. Chaque instance est enregistrée dans l'état avec sa clé d'index ("index_key": 0 ou "index_key": "sig-app-1"). Au plan suivant, Terraform rapproche les instances de la configuration et celles de l'état par cette clé, et seulement par elle. Il ne compare pas les contenus pour deviner qu'une instance a « glissé » d'un index à l'autre : d'où le comportement de count vu plus haut.
Les blocs dynamiques n'existent pas après l'évaluation. Un bloc dynamic est développé en blocs ordinaires avant d'être transmis au fournisseur ; le fournisseur reçoit une liste de règles, sans savoir qu'elle a été générée. Si le schéma déclare ces blocs comme une liste ordonnée (c'est le cas des règles du groupe de sécurité Scaleway), insérer une règle en tête de la collection décale toutes les autres dans le plan, comme count ; si c'est un ensemble, l'ordre ne compte pas.
Pièges courants
count sur une liste de choses qui ont une identité. Le piège démontré plus haut. count pour des copies interchangeables ou pour une condition (0 ou 1) ; for_each pour tout ce qui a un nom.
Des clés qui changent. Avec for_each, la clé est l'identité. Renommer une clé (corriger une faute de frappe, changer de convention) détruit et recrée l'instance, sauf si un bloc moved accompagne le changement.
Une clé construite à partir d'un attribut calculé. Erreur Invalid for_each argument. Les clés viennent du code ou des variables, les valeurs peuvent venir de l'apply.
Le splat sur une ressource à for_each. terraform_data.instance[*].input échoue ou ne donne pas ce que l'on attend : c'est une table, il faut values(...)[*].
[0] sur une ressource conditionnelle. terraform_data.bastion[0] échoue quand count vaut zéro. one(...[*]), ou une condition autour de la référence.
Les blocs dynamiques partout. Un dynamic pour trois règles fixes rend la configuration plus difficile à lire que les trois blocs écrits en clair. La documentation le recommande explicitement : écrire les blocs littéralement quand c'est possible.
Des boucles imbriquées illisibles. Une expression for sur trois niveaux, dans un for_each, dans un dynamic : le plan reste exact, mais plus personne ne sait le relire. Un locals nommé qui prépare la structure, puis un for_each simple sur cette structure, se relit bien mieux.
Sécurité
Les règles générées se relisent sur le plan, pas sur le code. Une variable
regles_entrantesmodifiée dans un fichier.tfvarspeut ouvrir un port à Internet sans que le code.tfchange. La revue doit porter sur le plan, où chaque règle apparaît en clair (leçon 12).Une précondition sur les données qui alimentent une boucle évite qu'une valeur dangereuse soit générée en série. Par exemple, refuser toute règle sur le port 22 dont la plage vaut
0.0.0.0/0:variable "regles_entrantes" { # ... validation { condition = alltrue([for r in var.regles_entrantes : !(r.port == 22 && r.ip_range == "0.0.0.0/0")]) error_message = "SSH ne s'ouvre jamais à tout Internet." } }Le bloc
validationd'une variable (leçon 5) est ici plus adapté qu'une précondition : il échoue dès la lecture des variables.Les clés de
for_eachne peuvent pas être sensibles, et elles apparaissent dans l'état, dans les plans et dans les journaux de l'intégration continue. N'en fabriquez pas à partir d'une adresse électronique, d'un nom de client confidentiel ou d'un identifiant interne que vous ne publieriez pas.
En production
- Une table de description par environnement, dans un fichier
.tfvarsou unlocals, et unfor_eachdessus : ajouter une métropole cliente, c'est ajouter une entrée relue en demande de fusion, et le plan montre exactement les ressources qu'elle crée. - Des conventions de clés stables dès le départ (
sig-app-1,prod-fr-par-1), parce que les changer plus tard demande des blocsmovedpour chaque instance. - Mesurez la lisibilité des plans. Une boucle qui génère quarante ressources rend chaque plan long ; c'est souvent le signe qu'il faut découper en modules (cours Terraform avancé : modules, tests, état), chacun avec un plan plus court.
- Évitez
-targetcomme réponse aux clés inconnues : il est fait pour les situations exceptionnelles, et la documentation de Terraform le présente ainsi.
Exercices
1. count ou for_each (niveau 200). Pour chaque cas, choisissez count, for_each, ou ni l'un ni l'autre : (a) trois enregistrements DNS A pour trois sous-domaines différents ; (b) un bastion présent seulement en production ; (c) cinq workers identiques derrière une file, sans nom propre ; (d) un bucket par métropole cliente.
Solution
(a) for_each sur une table sous-domaine vers adresse : chaque enregistrement a une identité. (b) count = var.env == "prod" ? 1 : 0, avec one() pour y faire référence. (c) count = 5 convient, puisque les instances sont interchangeables ; mais si l'on prévoit d'en retirer une précise, for_each sur un ensemble de noms reste plus sûr. (d) for_each sur l'ensemble des métropoles : retirer une cliente ne doit toucher que son bucket, et un bucket recréé est un bucket vide.
2. Corriger une clé inconnue (niveau 200). Réécrivez la ressource terraform_data.bucket de la leçon (celle qui échouait sur random_pet.suffixe.id) pour qu'elle fonctionne en un seul apply, puis vérifiez avec terraform plan.
Solution
resource "terraform_data" "bucket" {
for_each = { photos = "signalements-pj" }
input = "${each.value}-${random_pet.suffixe.id}"
}La clé photos est écrite dans le code, donc connue au plan ; le nom complet, qui contient le suffixe aléatoire, est une valeur, calculée à l'apply. Le plan affiche input = (known after apply) et réussit.
3. Deux dimensions (niveau 200). Écrivez un locals qui produit, pour les environnements preprod et prod et les zones fr-par-1 et fr-par-2, une table utilisable par for_each, dont les clés sont de la forme prod-fr-par-1, et dont la valeur contient env, zone et un nom de la forme sig-app-prod-1. Vérifiez avec terraform console.
Solution
locals {
zones = ["fr-par-1", "fr-par-2"]
instances = {
for p in setproduct(["preprod", "prod"], local.zones) :
"${p[0]}-${p[1]}" => {
env = p[0]
zone = p[1]
nom = "sig-app-${p[0]}-${index(local.zones, p[1]) + 1}"
}
}
}Dans la console, local.instances["prod-fr-par-2"] doit donner env = "prod", zone = "fr-par-2", nom = "sig-app-prod-2". La fonction index renvoie la position d'un élément dans une liste.
4. Une règle de trop (niveau 200). Dans la configuration Scaleway de la leçon, on insère en tête de regles_entrantes une règle pour le port 80. Que montrera le plan pour le groupe de sécurité, et pourquoi ?
Solution
Une mise à jour sur place du groupe de sécurité, mais dont le détail montre les règles décalées : la règle d'index 0 passe de 22 à 80, celle d'index 1 de 443 à 22, et une troisième apparaît pour 443. Le schéma du fournisseur déclare inbound_rule comme une liste ordonnée : le bloc dynamic génère les règles dans l'ordre de la collection, et Terraform compare par position. Le résultat final est correct, mais le plan est difficile à relire ; ajouter les nouvelles règles à la fin de la liste garde des plans lisibles.
Récapitulatif
countcrée N instances désignées par leur index ; il convient aux copies interchangeables et aux conditions (? 1 : 0). Sur une liste de choses nommées, retirer un élément au milieu remplace tous ceux qui suivent.for_eachcrée une instance par clé d'une table ou d'un ensemble ; la clé est l'identité, stable, et retirer un élément ne touche que lui. Les clés doivent être connues au plan et non sensibles ; une liste se convertit avectoset().- Les expressions
forfabriquent listes ([...]) et tables ({... => ...}), avec filtreif; le splat[*]extrait un attribut (viavalues()pour unfor_each). setproduct,flattenetzipmapconstruisent des séries à plusieurs dimensions ;one()lit une ressource conditionnelle.- Les blocs
dynamicgénèrent des sous-blocs (règles, frontends), jamaislifecycleniprovisioner, et seulement quand écrire les blocs en clair n'est pas raisonnable. - Une boucle se relit sur le plan : des clés lisibles, des structures préparées dans
locals, et des validations sur les données qui l'alimentent.
Pour aller plus loin
- Les pages des méta-arguments
countetfor_eachde la documentation de Terraform, en particulier la section qui compare les deux. - La page des blocs dynamiques, et sa section sur les bonnes pratiques.
- Le chapitre 5 de Terraform: Up & Running, qui consacre une longue section aux boucles et à leurs pièges.
- La leçon 10, qui assemble l'architecture complète de Signalements avec ces techniques, puis la leçon 11, qui montre comment passer de
countàfor_eachsans rien détruire.
Sources
- HashiCorp, Terraform : le méta-argument count
- HashiCorp, Terraform : le méta-argument for_each
- HashiCorp, Terraform : blocs dynamiques
- HashiCorp, Terraform : expressions for
- HashiCorp, Terraform : expressions splat
- HashiCorp, Terraform : fonctions flatten, setproduct, zipmap, one
- Fournisseur Terraform de Scaleway : scaleway_instance_security_group
- OpenTofu, méta-arguments count et for_each
- Yevgeniy Brikman, Terraform: Up & Running (3e éd., O'Reilly, 2022), chapitre 5 : Terraform Tips and Tricks (boucles et conditions)