Aller au contenu
Boucles, conditions et blocs dynamiques

Boucles, conditions et blocs dynamiques

200 Pratiquer ⏱ 1 h 15 terraformopentofuscalewayiachcl

À la fin, vous saurez

  • Générer plusieurs instances d'une ressource avec count et avec for_each, et choisir entre les deux
  • Expliquer et éviter les remplacements en cascade que provoque count sur une liste
  • Transformer des collections avec les expressions for, les splats et les fonctions flatten, setproduct et zipmap
  • Générer des blocs imbriqués avec dynamic, et savoir quand ne pas le faire
  • Créer une ressource seulement si une condition est vraie

Prérequis

Testé avec random 3.9.1 scaleway 2.84.0 terraform 1.16.1 , vérifié le 5 octobre 2026

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 expression for fabriquent une table.
  • Des clés connues au moment du plan. Toutes les valeurs que parcourt for_each (et la valeur de count) 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 if final 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"
}
  • setproduct donne toutes les combinaisons ;
  • flatten aplatit une liste de listes, ce qui rend les expressions for imbriquées utilisables ;
  • zipmap associe 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 de dynamic. L'argument ip de ces règles est déprécié au profit de ip_range dans 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'écrire scaleway_instance_ip.publique[each.key] dans l'instance. Aligner les clés de séries liées est la règle qui rend for_each lisible.
  • ip_id = ... : null : null signifie « 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'en fr-par-1, ce qui suffit à illustrer dynamic.

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_entrantes modifiée dans un fichier .tfvars peut ouvrir un port à Internet sans que le code .tf change. 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 validation d'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_each ne 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 .tfvars ou un locals, et un for_each dessus : 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 blocs moved pour 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 -target comme 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

  • count cré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_each cré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 avec toset().
  • Les expressions for fabriquent listes ([...]) et tables ({... => ...}), avec filtre if ; le splat [*] extrait un attribut (via values() pour un for_each).
  • setproduct, flatten et zipmap construisent des séries à plusieurs dimensions ; one() lit une ressource conditionnelle.
  • Les blocs dynamic génèrent des sous-blocs (règles, frontends), jamais lifecycle ni provisioner, 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 count et for_each de 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_each sans rien détruire.
Voir ma constellation →

Sources