
Vous avez créé trois ressources avec count, et vous devez en insérer une
au milieu de la liste. Terraform décale tous les index suivants : ce qui
était [1] devient [2], et il détruit puis recrée des ressources qui n'avaient
aucune raison de bouger. En production, c'est une coupure.
for_each adresse chaque instance par une clé nommée plutôt que par un
index numérique. L'instance "db" garde son identité quelle que soit sa place
dans la collection. C'est l'approche recommandée dès que les instances ont un
rôle distinct, count restant réservé aux instances réellement
interchangeables.
Tous les exemples de ce guide ont été exécutés sur Terraform v1.15.4 avant
publication, messages d'erreur compris. Ils s'appuient sur les providers
random et local, sans aucune infrastructure : vous pouvez les rejouer tels
quels.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »for_eachavec une map :each.keyeteach.valuefor_eachavec un set de chaînes : la nuance qui fait échouer les débutants- Les quatre contraintes qui rendent un
for_eachinvalide - Migrer
countversfor_eachavec les blocsmoved, sans rien détruire - Prouver qu'un plan ne détruit rien, avec
show -json
Prérequis
Section intitulée « Prérequis »countcompris (count Terraform)- Variables de type
mapetobjectconnues (variables Terraform) - Terraform 1.1 ou plus récent pour les blocs
moved
L'idée derrière for_each
Section intitulée « L'idée derrière for_each »Pensez à une boucle sur un dictionnaire :
services = { "web": {"taille": "small"}, "db": {"taille": "large"}, "cache": {"taille": "small"},}
for nom, config in services.items(): creer(f"svc-{nom}", config["taille"])for_each fait cela en Terraform : « applique ce bloc à chaque entrée de cette
collection ». Chaque instance reste identifiée par sa clé, jamais par un
rang.
Syntaxe minimale
Section intitulée « Syntaxe minimale »resource "random_pet" "service" { for_each = var.services
length = each.value.longueur # each.value : la valeur sous la cle separator = "-" keepers = { nom = each.key } # each.key : la cle ("web", "db"...)}Trois symboles à retenir :
for_each = map_ou_set: la collection à parcourir,each.key: la clé courante,each.value: la valeur associée. Sur un set,each.keyeteach.valuesont identiques, tous deux valant la chaîne.
Les quatre contraintes de for_each
Section intitulée « Les quatre contraintes de for_each »C'est la section qui évite le plus de perte de temps. Chaque message ci-dessous est le message réel de Terraform 1.15.4.
1. Une liste est refusée
Section intitulée « 1. Une liste est refusée »for_each accepte une map ou un set, jamais une liste : une liste
admet des doublons et dépend de l'ordre, deux propriétés incompatibles avec des
clés d'instance stables. Terraform ne convertit pas implicitement.
variable "noms" { type = list(string) default = ["a", "b"]}
resource "random_pet" "n" { for_each = var.noms # refuse length = 2}Error: Invalid for_each argument │ var.noms is a list of stringLa correction tient en un appel : for_each = toset(var.noms).
2. Un set doit contenir des chaînes
Section intitulée « 2. Un set doit contenir des chaînes »La documentation officielle est précise : for_each accepte « a map or a set
of strings ». Un set de nombres est refusé, ce que la plupart des tutoriels
passent sous silence.
resource "random_pet" "n" { for_each = toset([1, 2, 3]) # refuse length = 2}Error: Invalid for_each set argumentConvertissez les valeurs : toset([for n in [1, 2, 3] : tostring(n)]).
3. Les clés doivent être connues au moment du plan
Section intitulée « 3. Les clés doivent être connues au moment du plan »Contrainte la plus coûteuse en conditions réelles. Terraform doit connaître toutes les clés avant la moindre opération distante, puisqu'elles identifient les instances. Une clé dérivée d'un attribut qui n'existera qu'après l'apply est donc impossible.
resource "random_pet" "source" { length = 2 }
resource "random_pet" "derive" { for_each = toset([random_pet.source.id]) # refuse length = 2}Error: Invalid for_each argument │ random_pet.source.id is a string, known only after applyMême impossibilité avec uuid(), timestamp() ou bcrypt(), dont la valeur
n'est pas connue au plan. La parade consiste à dériver les clés de valeurs
statiques (variables, locals) et non de résultats de ressources.
4. Une valeur sensible est interdite
Section intitulée « 4. Une valeur sensible est interdite »Terraform utilise la valeur de for_each pour identifier les instances, et
l'affiche donc toujours dans ses sorties. Une valeur marquée sensible y est
refusée.
variable "services" { type = set(string) default = ["a", "b"] sensitive = true}
resource "random_pet" "n" { for_each = var.services # refuse length = 2}Error: Invalid for_each argument │ var.services has a sensitive valueConséquence contre-intuitive : marquer une variable sensitive « par prudence »
peut casser une configuration qui fonctionnait.
Adressage par clé dans le state
Section intitulée « Adressage par clé dans le state »Chaque instance porte sa clé, jamais un rang :
random_pet.service["cache"]random_pet.service["db"]random_pet.service["web"]Les guillemets font partie de l'adresse, il faut donc les protéger du shell :
terraform state show 'random_pet.service["db"]'L'option -target existe pour agir sur une seule instance, mais la
documentation officielle la réserve aux « circonstances exceptionnelles, comme
se remettre d'une erreur ou contourner une limite de Terraform ». Elle
déconseille son usage courant, qui produit une dérive non détectée entre la
configuration et le réel.
Transformer le résultat : le splat ne marche pas
Section intitulée « Transformer le résultat : le splat ne marche pas »Une ressource pilotée par for_each se comporte comme une map. Or
l'expression splat [*] ne s'applique qu'aux listes, sets et tuples. Sur
une map, elle échoue :
output "res" { value = random_pet.service[*].id }Error: Unsupported attribute This object does not have an attribute named "id".Deux formes correctes, toutes deux vérifiées :
# Expression for : la plus lisible, et elle conserve les clesoutput "par_cle" { value = { for cle, r in random_pet.service : cle => r.id }}
# values() rend une liste, sur laquelle le splat redevient valideoutput "en_liste" { value = values(random_pet.service)[*].id}La différence compte : la première rend une map indexée par service, la seconde une liste dont l'ordre suit les clés triées.
Migrer de count vers for_each sans rien détruire
Section intitulée « Migrer de count vers for_each sans rien détruire »C'est le geste réel, et celui que la plupart des guides passent sous silence.
Changer count en for_each change l'adresse de chaque instance dans le
state. Sans précaution, Terraform ne reconnaît plus rien : il détruit tout et
recrée tout.
Le bloc moved, disponible depuis Terraform 1.1, déclare le réadressage.
-
Remplacer le méta-argument dans les ressources.
resource "random_pet" "service" {for_each = toset(var.services) # etait : count = length(var.services)length = 2}Dans le corps,
count.indexlaisse la place àeach.key. -
Déclarer un bloc
movedpar instance existante, de l'ancienne adresse vers la nouvelle.moved {from = random_pet.service[0]to = random_pet.service["web"]}moved {from = random_pet.service[1]to = random_pet.service["cache"]} -
Vérifier le plan avant d'appliquer. Un réadressage réussi n'annonce aucun changement.
Le bloc moved est préférable à la commande terraform state mv sur trois
points : il est versionné avec le code donc revu en revue, il est rejoué
automatiquement par toute l'équipe et par la CI, et personne n'a à se souvenir
d'exécuter une commande.
Prouver qu'un plan ne détruit rien
Section intitulée « Prouver qu'un plan ne détruit rien »Coller une sortie humaine du type Plan: 2 to add, 0 to destroy ne prouve rien
de façon fiable. La méthode vérifiable passe par le plan enregistré, relu en
JSON :
terraform plan -out=tfplanterraform show -json tfplan | jq '.resource_changes[] | {address, previous_address, actions: .change.actions}'Deux signatures à savoir lire :
| Ce que vous lisez | Ce que cela signifie |
|---|---|
"actions": ["no-op"] avec un previous_address | l'instance a changé d'adresse sans être touchée, le réadressage fonctionne |
"actions": ["create"] sans previous_address | une nouvelle instance, c'est attendu pour un ajout |
"actions": ["delete", "create"] | l'instance est remplacée, le réadressage a échoué |
Pour un contrôle automatisable, terraform plan -detailed-exitcode rend 0
quand le diff est vide, 2 quand des changements sont en attente, et 1 en
cas d'erreur. C'est la brique d'une porte de revue en intégration continue.
for_each ne concerne pas que les ressources
Section intitulée « for_each ne concerne pas que les ressources »Le méta-argument s'applique aussi aux blocs module, data et ephemeral :
module "service" { source = "./modules/service" for_each = toset(var.services)
nom = each.key}
output "modules" { value = { for cle, m in module.service : cle => m.nom }}Quand la relation entre deux ressources est de un pour un, vous pouvez même donner directement la première comme collection de la seconde, ce qui évite de répéter la source des clés :
resource "local_file" "fiche" { for_each = random_pet.service # la ressource elle-meme
filename = "out/${each.key}.txt" content = each.value.id}count ou for_each : comment choisir
Section intitulée « count ou for_each : comment choisir »| Critère | count | for_each |
|---|---|---|
| Instances réellement interchangeables | ✅ adapté | verbeux |
| Instances avec une identité distincte | ❌ fragile | ✅ adapté |
| Insertion au milieu de la collection | décale les index suivants, donc destructions | ✅ aucune autre instance touchée |
| Ajout en fin de liste | sans conséquence | ✅ sans conséquence |
| Ressource optionnelle (0 ou 1) | ✅ count = cond ? 1 : 0 | inadapté |
| Adressage dans le state | ressource.nom[0] | ressource.nom["cle"] |
La nuance de la troisième ligne est souvent mal comprise : avec count, ajouter
un élément à la fin d'une liste ne casse rien. C'est l'insertion au
milieu, ou la suppression d'un élément intermédiaire, qui décale les index et
provoque les recréations en cascade.
Règle simple : si les instances ont des noms ou des configurations
distincts, for_each. Si elles sont vraiment interchangeables, count.
Dépannage
Section intitulée « Dépannage »La plupart des pannes de for_each remontent le même message,
Invalid for_each argument, ce qui rend le diagnostic trompeur. Le texte qui
suit le message porte toute l'information : il désigne soit un type
refusé, soit une valeur inconnue au plan, soit une collection sensible.
Le tableau part donc de ce complément de message plutôt que du code d'erreur.
| Symptôme | Cause probable | Solution |
|---|---|---|
Invalid for_each argument avec « is a list of string » | Une liste passée directement | toset(var.liste) |
Invalid for_each set argument | Un set de nombres ou d'objets | Convertir en chaînes : toset([for n in liste : tostring(n)]) |
Invalid for_each argument avec « known only after apply » | Clé dérivée d'un attribut de ressource | Dériver les clés de variables ou de locals, jamais d'un résultat d'apply |
Invalid for_each argument avec « has a sensitive value » | Collection marquée sensitive | Retirer le marquage sur la collection, ou nonsensitive() en dernier recours |
Invalid combination of "count" and "for_each" | Les deux dans le même bloc | Ils sont exclusifs, n'en garder qu'un |
Unsupported attribute sur ressource[*].attr | Le splat sur une ressource for_each | values(ressource.nom)[*].attr ou [for k, v in ressource.nom : v.attr] |
Ressources détruites après passage à for_each | Changement d'adresse non déclaré | Ajouter un bloc moved par instance |
each.key indisponible | Bloc sans for_each | Vérifier que le méta-argument est bien présent |
À retenir
Section intitulée « À retenir »for_eachaccepte une map ou un set de chaînes, jamais une liste.- Les clés doivent être connues au plan : impossible de les dériver d'un attribut créé à l'apply.
- Une valeur sensible est refusée en argument de
for_each. countetfor_eachsont mutuellement exclusifs dans un même bloc.- Le splat est invalide sur une ressource
for_each: utilisez une expressionforouvalues(). - Migrer depuis
countexige des blocsmoved, sinon Terraform détruit et recrée tout. - Conservez les blocs
moved: les supprimer est un changement cassant. - La preuve d'un plan sans destruction se lit dans
terraform show -json, pas dans la sortie humaine.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous reprennent les messages d'erreur que for_each
produit le plus souvent. Chaque réponse donne la sortie réelle de Terraform
et la correction correspondante, pour que le message serve de point d'entrée au
diagnostic.
La cause
for_each n'accepte qu'un set de chaînes ou une map. Une liste admet les doublons et un ordre, deux propriétés incompatibles avec un adressage par clé stable.resource "null_resource" "env" {
for_each = var.environnements # liste : refuse
}
Error: Invalid for_each argument
on main.tf line 7, in resource "null_resource" "env":
7: for_each = var.environnements
La correction
for_each = toset(var.environnements)
# null_resource.env["dev"] will be created
# null_resource.env["prod"] will be created
# null_resource.env["staging"] will be created
Plan: 3 to add, 0 to change, 0 to destroy.
Vérifié sur Terraform v1.15.4.La cause
count produit une liste, sur laquelle le splat fonctionne. for_each produit une map indexée par clé, que le splat ne sait pas parcourir.output "ids" {
value = null_resource.env[*].id
}
Error: Unsupported attribute
on outputs.tf line 2, in output "ids":
2: value = null_resource.env[*].id
This object does not have an attribute named "id".
Les deux formes correctes
values(null_resource.env)[*].id
[for k, v in null_resource.env : v.id]
La seconde donne accès à la clé en plus de la valeur.Vérifié sur Terraform v1.15.4.La cause
Terraform doit connaître les clés au moment du plan pour construire les adresses des instances. Un attribut d'une ressource non encore créée ne l'est pas.resource "null_resource" "derive" {
for_each = toset([null_resource.source.id])
}
Error: Invalid for_each argument
4: for_each = toset([null_resource.source.id])
│ null_resource.source.id is a string, known only after apply
La parade
Dériver les clés de variables ou de locals, jamais d'un résultat d'apply. Les valeurs elles-mêmes peuvent en revanche rester inconnues : seule la clé doit être déterminée au plan.Vérifié sur Terraform v1.15.4.Non, ils sont exclusifs
Chacun définit un mode d'adressage des instances : un index numérique pourcount, une clé pour for_each. Les deux ensemble n'ont pas de sens.resource "null_resource" "conflit" {
count = 2
for_each = toset(["a"])
}
Error: Invalid combination of "count" and "for_each"
on conflit.tf line 3, in resource "null_resource" "conflit":
3: for_each = toset(["a"])
Que choisir
Utilisezfor_each dès que les instances sont distinguables par un identifiant stable. Réservez count aux cas où les instances sont interchangeables, ou pour activer un bloc conditionnellement avec count = var.actif ? 1 : 0.Vérifié sur Terraform v1.15.4.L'adressage par clé
terraform state list
null_resource.env["dev"]
null_resource.env["prod"]
null_resource.env["staging"]
Pourquoi c'est l'avantage décisif
Aveccount, les instances sont adressées par index : env[0], env[1], env[2]. Retirer le premier élément de la liste décale tous les suivants, et Terraform détruit puis recrée les ressources qui ont changé de position.Avec for_each, retirer "dev" de la collection ne touche que env["dev"]. Les autres instances gardent leur adresse et ne bougent pas.Vérifié sur Terraform v1.15.4.Le problème
Passer decount à for_each change l'adresse de chaque instance, de env[0] à env["dev"]. Terraform y voit une ressource disparue et une nouvelle à créer, donc une destruction.La solution : un bloc moved par instance
moved {
from = null_resource.env[0]
to = null_resource.env["dev"]
}
La vérification qui compte
Avant d'appliquer, exigez du plan qu'il n'annonce aucune destruction :terraform plan -no-color | grep -E "Plan:|to destroy"
Le plan doit indiquer 0 to destroy. Tant que ce n'est pas le cas, il manque un bloc moved.Vérifié sur Terraform v1.15.4.Sur les modules et les data sources
Le méta-argument s'utilise à l'identique, avec le même adressage par clé :module "reseau" {
for_each = var.zones
source = "./modules/reseau"
}
Dans un bloc dynamic
Pour générer des blocs imbriqués répétés, la syntaxe change : on accède aux valeurs par le nom du bloc dynamique, pas pareach.dynamic "regle" {
for_each = var.regles
content {
port = regle.value.port
}
}
C'est regle.key et regle.value, et non each.key et each.value.Vérifié sur Terraform v1.15.4.Pour aller plus loin
Section intitulée « Pour aller plus loin »- Les blocs dynamiques : le
for_eacha l'interieur d'une ressource, pour les blocs imbriques. - Le style guide Terraform : les conventions de nommage des cles d'un
for_each. - Le state Terraform : comment une cle de
for_eachdevient une adresse dans le state. - for_each : la reference officielle : le meta-argument,
each.keyeteach.value. - count : la reference officielle : l'alternative par index, et ce qu'elle coute au renommage.