Aller au contenu
Infrastructure as Code medium

for_each Terraform : instances nommées avec une map

35 min de lecture

logo terraform

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.

  • for_each avec une map : each.key et each.value
  • for_each avec un set de chaînes : la nuance qui fait échouer les débutants
  • Les quatre contraintes qui rendent un for_each invalide
  • Migrer count vers for_each avec les blocs moved, sans rien détruire
  • Prouver qu'un plan ne détruit rien, avec show -json

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.

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.key et each.value sont identiques, tous deux valant la chaîne.

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.

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 string

La correction tient en un appel : for_each = toset(var.noms).

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 argument

Convertissez 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 apply

Mê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.

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 value

Conséquence contre-intuitive : marquer une variable sensitive « par prudence » peut casser une configuration qui fonctionnait.

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 :

Fenêtre de terminal
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.

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 cles
output "par_cle" {
value = { for cle, r in random_pet.service : cle => r.id }
}
# values() rend une liste, sur laquelle le splat redevient valide
output "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.

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.

  1. 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.index laisse la place à each.key.

  2. Déclarer un bloc moved par 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"]
    }
  3. 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.

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 :

Fenêtre de terminal
terraform plan -out=tfplan
terraform show -json tfplan | jq '.resource_changes[] | {address, previous_address, actions: .change.actions}'

Deux signatures à savoir lire :

Ce que vous lisezCe que cela signifie
"actions": ["no-op"] avec un previous_addressl'instance a changé d'adresse sans être touchée, le réadressage fonctionne
"actions": ["create"] sans previous_addressune 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.

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
}
Critèrecountfor_each
Instances réellement interchangeables✅ adaptéverbeux
Instances avec une identité distincte❌ fragile✅ adapté
Insertion au milieu de la collectiondécale les index suivants, donc destructions✅ aucune autre instance touchée
Ajout en fin de listesans conséquence✅ sans conséquence
Ressource optionnelle (0 ou 1)count = cond ? 1 : 0inadapté
Adressage dans le stateressource.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.

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ômeCause probableSolution
Invalid for_each argument avec « is a list of string »Une liste passée directementtoset(var.liste)
Invalid for_each set argumentUn set de nombres ou d'objetsConvertir 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 ressourceDé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 sensitiveRetirer le marquage sur la collection, ou nonsensitive() en dernier recours
Invalid combination of "count" and "for_each"Les deux dans le même blocIls sont exclusifs, n'en garder qu'un
Unsupported attribute sur ressource[*].attrLe splat sur une ressource for_eachvalues(ressource.nom)[*].attr ou [for k, v in ressource.nom : v.attr]
Ressources détruites après passage à for_eachChangement d'adresse non déclaréAjouter un bloc moved par instance
each.key indisponibleBloc sans for_eachVérifier que le méta-argument est bien présent
  1. for_each accepte une map ou un set de chaînes, jamais une liste.
  2. Les clés doivent être connues au plan : impossible de les dériver d'un attribut créé à l'apply.
  3. Une valeur sensible est refusée en argument de for_each.
  4. count et for_each sont mutuellement exclusifs dans un même bloc.
  5. Le splat est invalide sur une ressource for_each : utilisez une expression for ou values().
  6. Migrer depuis count exige des blocs moved, sinon Terraform détruit et recrée tout.
  7. Conservez les blocs moved : les supprimer est un changement cassant.
  8. La preuve d'un plan sans destruction se lit dans terraform show -json, pas dans la sortie humaine.

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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn