
Le meta-argument count crée plusieurs instances d'une même ressource à
partir d'un nombre. Il est facile à écrire, et coûteux à défaire : ses instances
sont identifiées par leur position, pas par leur identité. Retirer un élément
du milieu d'une liste décale tous les suivants, et Terraform détruit alors des
objets que personne n'avait demandé de toucher.
Ce guide part de la base, count et count.index, puis traite le piège du
décalage d'index, le choix entre count et for_each, la migration non
destructrice par bloc moved, et le count conditionnel exposé par
one(). Tous les comportements ont été vérifiés sur Terraform v1.15.4, plan
JSON à l'appui.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Créer N instances avec
count, et les personnaliser viacount.index - Les adresser (
ressource.nom[i]), et les exposer (splat[*],one()) - Le piège du décalage d'index, et pourquoi c'est un remplacement
- Choisir entre
countetfor_each, et l'interdiction de les cumuler - Migrer de
countversfor_eachsans rien détruire, avecmoved - Prouver chaque changement dans le plan JSON
Prérequis
Section intitulée « Prérequis »- Blocs
resourceet cycle de vie (déclarer des ressources Terraform) - Variables et types (variables Terraform)
- Terraform 1.15.x, la série stable courante
count : N instances indexées par entier
Section intitulée « count : N instances indexées par entier »count = N crée N instances identiques d'une ressource, adressées
ressource.nom[0], ressource.nom[1], etc. Dans le bloc, count.index
donne l'index courant, un entier qui démarre à 0 :
resource "libvirt_volume" "disk" { count = 3
name = "worker-${count.index}.qcow2" pool = "default"}Trois volumes sont créés : disk[0], disk[1], disk[2]. La valeur de count
doit être connue avant l'apply : elle ne peut pas dépendre d'un attribut qui
n'existe qu'une fois une ressource créée (known after apply). Une variable, une
data source lue au plan, ou une constante conviennent.
count accepte aussi bien un bloc resource qu'un bloc module, data ou
ephemeral : la documentation les liste tous dans « Supported constructs ».
Créer trois instances d'un module se fait de la même façon,
count = length(local.noms) puis name = local.noms[count.index].
Adresser et exposer les instances
Section intitulée « Adresser et exposer les instances »Avec count, une ressource devient une liste d'instances, adressables par
index. On accède à l'une par ressource.nom[i], et à l'instance courante dans le
même bloc par ressource.nom[count.index] :
resource "libvirt_domain" "vm" { count = 3
name = "worker-${count.index}" memory = 512 memory_unit = "MiB"
devices = { disks = [ { source = { file = { file = libvirt_volume.disk[count.index].path } } target = { dev = "vda", bus = "virtio" } } ] }}Ce bloc référence déjà libvirt_volume.disk[count.index].path : la
dépendance est implicite, et ajouter un depends_on = [libvirt_volume.disk]
serait redondant, en plus de rendre le plan plus conservateur. Pour exposer
tous les attributs d'une ressource count, l'opérateur splat [*] extrait
un attribut de chaque instance et rend une liste :
output "vm_names" { value = libvirt_domain.vm[*].name # ["worker-0", "worker-1", "worker-2"]}Le splat fonctionne avec count parce que la ressource est une liste. Il ne
s'applique pas à une ressource for_each, qui est une map : là, il faut
une expression for.
for_each : indexé par clé, jamais avec count
Section intitulée « for_each : indexé par clé, jamais avec count »for_each prend une map ou un set et crée une instance par élément,
adressée par sa clé : ressource.nom["web"]. C'est l'autre façon de créer
plusieurs instances, et un même bloc ne peut pas porter les deux :
resource "local_file" "bad" { count = 2 for_each = toset(["a", "b"])}Error: Invalid combination of "count" and "for_each"La différence tient en un mot : count indexe par entier (la position),
for_each par clé (l'identité). Ce mot décide de tout ce qui suit.
Le piège : le décalage d'index
Section intitulée « Le piège : le décalage d'index »Voici la raison d'être de ce guide. Avec count, l'identité d'une instance est
sa position. Prenons trois VMs nommées d'après une liste :
variable "services" { type = list(string) default = ["web", "api", "cache"]}
resource "local_file" "service" { count = length(var.services) filename = "service-${var.services[count.index]}.txt" content = "service=${var.services[count.index]}\n"}Retirez api, celle du milieu. On s'attend à ne perdre que api. Le plan
JSON dit autre chose :
terraform plan -out=tfplan -var 'services=["web","cache"]'terraform show -json tfplan | jq -c '.resource_changes[] | {address, actions: .change.actions}'{"address":"local_file.service[0]","actions":["no-op"]}{"address":"local_file.service[1]","actions":["delete","create"]}{"address":"local_file.service[2]","actions":["delete"]}service[1], qui portait api, est détruite puis recréée avec cache, et
service[2] est détruite. Ce n'est pas « un simple changement » sur
service[1] : c'est un remplacement (["delete","create"]). Retirer un
service en a recréé un autre. Sur un fichier, c'est indolore ; sur une base de
données, c'est un incident.
Choisir entre count et for_each
Section intitulée « Choisir entre count et for_each »Les mêmes services, mais keyés par nom, ne décalent rien. Retirer api ne
touche que api :
resource "local_file" "service" { for_each = toset(var.services) filename = "service-${each.key}.txt" content = "service=${each.key}\n"}{"address":"local_file.service[\"web\"]","actions":["no-op"]}{"address":"local_file.service[\"api\"]","actions":["delete"]}{"address":"local_file.service[\"cache\"]","actions":["no-op"]}D'où la règle, reprise mot pour mot de la documentation : utilisez count quand
les instances sont presque identiques et interchangeables (des workers, des
replicas) ; utilisez for_each dès qu'elles ont une identité propre, ou que
leurs arguments viennent de valeurs distinctes qu'un ajout ou un retrait ne
doit pas déplacer.
for_each a ses propres contraintes : ses clés doivent être connues au plan
(pas dérivées d'un attribut known after apply), il n'accepte pas de valeur
sensible comme clé, ni de fonction impure (uuid, timestamp, bcrypt), et
une liste doit être convertie en set (toset(...)) pour éviter que ses index
ne redeviennent positionnels.
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 point qui sépare l'usage débutant de l'usage professionnel. Vous avez
appliqué une ressource en count, et vous voulez passer à for_each pour tuer
le piège. Changer le bloc sans précaution détruit et recrée tout : les
adresses passent de service[0] à service["web"], et Terraform ne devine pas
la correspondance. Un plan de migration nu annonce alors 3 to add, 3 to destroy.
Le bloc moved donne la correspondance à Terraform :
resource "local_file" "service" { for_each = toset(var.services) filename = "service-${each.key}.txt" content = "service=${each.key}\n"}
moved { from = local_file.service[0] to = local_file.service["web"]}
moved { from = local_file.service[1] to = local_file.service["api"]}
moved { from = local_file.service[2] to = local_file.service["cache"]}Le plan devient entièrement no-op, et chaque changement d'adresse est tracé
par previous_address dans le JSON :
terraform show -json tfplan | jq -c '.resource_changes[] | {address, prev: .previous_address, actions: .change.actions}'{"address":"local_file.service[\"web\"]","prev":"local_file.service[0]","actions":["no-op"]}Un cas plus simple se gère tout seul depuis les versions récentes de
Terraform : ajouter count = 1 à une ressource qui n'en avait pas migre
service vers service[0] sans destruction (has moved to, 0 to destroy).
Mais dès qu'il faut deviner des clés, comme pour for_each, le bloc moved
est obligatoire.
count conditionnel : 0 ou 1 instance
Section intitulée « count conditionnel : 0 ou 1 instance »Le seul usage vraiment courant de count reste le on/off : une ressource
présente ou absente selon un booléen, avec le pattern count = condition ? 1 : 0.
resource "libvirt_domain" "monitoring" { count = var.enable_monitoring ? 1 : 0
name = "monitoring"}À false, aucune instance n'existe. Passer de 0 à 1 crée monitoring[0] et ne
détruit rien d'autre : le « count 0 vers 1 détruit les voisins » est un mythe.
Chaque instance a son propre objet, créé, mis à jour ou détruit
indépendamment.
Pour exposer l'attribut d'une ressource qui a 0 ou 1 instance, la liste splat
monitoring[*].name a une longueur 0 ou 1. La fonction one() la réduit à
une valeur unique, ou null si elle est vide, et lève une erreur au-delà d'un
élément :
output "monitoring_name" { value = one(libvirt_domain.monitoring[*].name)}Une sortie dont la valeur est null est simplement omise de
terraform output, ce qui est le comportement attendu quand l'option est
désactivée.
Prouver un changement dans le plan JSON
Section intitulée « Prouver un changement dans le plan JSON »terraform state list et la sortie humaine du plan se lisent trop vite. La seule
preuve non ambiguë d'un changement de count est le plan au format JSON :
terraform plan -out=tfplanterraform show -json tfplan | jq -c '.resource_changes[] | {address, actions: .change.actions, prev: .previous_address}'resource_changes[].actionsdistingue["no-op"],["create"],["update"],["delete"]et le remplacement["delete","create"]. C'est lui qui révèle les destructions parasites d'un décalage d'index.previous_addressn'apparaît que si l'adresse a changé, typiquement via un blocmoved. C'est la preuve qu'une migration est bien un déplacement, et non une destruction suivie d'une création.
Dépannage
Section intitulée « Dépannage »Ces symptômes se lisent tous dans le plan, avant l'apply.
| Symptôme | Cause | Solution |
|---|---|---|
| Des instances détruites/recréées au retrait d'un élément du milieu | Décalage d'index : l'identité count est positionnelle | Passer à for_each keyé par une valeur stable |
count.index refusé | Utilisé hors d'un bloc portant count | Déclarer count sur la ressource, ou retirer count.index |
Invalid combination of "count" and "for_each" | Les deux meta-arguments sur le même bloc | N'en garder qu'un |
The "count" value depends on resource attributes that cannot be determined until apply | count dérive d'un attribut known after apply | Le fonder sur une variable ou une data source lue au plan |
Migration count vers for_each qui détruit tout | Aucun bloc moved pour relier index et clés | Ajouter un moved { from = ...[i] to = ...["clé"] } par instance |
À retenir
Section intitulée « À retenir »count = Ncrée N instances indexées par entier ;count.indexdémarre à 0.- La valeur de
countdoit être connue au plan, jamaisknown after apply. - Le décalage d'index au retrait d'un élément du milieu est un
remplacement (
["delete","create"]), pas une simple mise à jour. countpour des instances interchangeables ;for_eachdès qu'elles ont une identité propre. Jamais les deux dans un même bloc.- Migrer
countversfor_eachsansmoveddétruit tout ; avecmoved, le plan est no-op etprevious_addresstrace le déplacement. count = cond ? 1 : 0rend une ressource optionnelle, exposée parone().- Le splat
[*]marche surcount(liste), pas surfor_each(map). - La preuve d'un changement est dans
resource_changes[].actions, pas dans la sortie humaine.
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous reprennent les confusions les plus fréquentes autour de
count : le choix avec for_each, le piège du décalage d'index, et la migration
sans destruction.
Le critère
count indexe par entier (la position), for_each par clé (l'identité).count: des instances interchangeables, un nombre de copies (workers, replicas, disques).for_each: des instances à identité propre, ou dont les arguments viennent de valeurs distinctes qu'un ajout ou un retrait ne doit pas déplacer.
Pourquoi ça compte
Aveccount, retirer un élément du milieu d'une liste décale tous les suivants et provoque des remplacements en cascade. for_each keyé par une valeur stable ne touche que l'élément retiré.Le décalage d'index
Aveccount, l'identité d'une instance est son index. Retirez le service du milieu d'une liste ["web","api","cache"] :{"address":"local_file.service[1]","actions":["delete","create"]}
{"address":"local_file.service[2]","actions":["delete"]}
service[1] (ex-api) est remplacée par cache, et service[2] est détruite.Ce n'est pas une mise à jour
Le plan JSON montre["delete","create"], un remplacement, pas un simple changement. Sur une base de données ou un volume, l'objet est recréé. La parade est for_each keyé par nom.Un mythe tenace
Passercount de 0 à 1 crée uniquement l'instance [0] et ne détruit rien d'autre. La documentation est explicite : « Each instance has a distinct infrastructure object associated with it, and each is separately created, updated, or destroyed when the configuration is applied. »Vérifié sur 1.15.4
Le plan JSON d'un passage 0 vers 1 montre une seule action["create"] sur [0], et ["no-op"] sur toutes les ressources voisines.Le bloc moved
Changercount en for_each sans précaution détruit tout : les adresses passent de service[0] à service["web"], et Terraform ne devine pas la correspondance. Un bloc moved par instance la donne :moved {
from = local_file.service[0]
to = local_file.service["web"]
}
La preuve
Le plan devient entièrement no-op, et chaque instance porte unprevious_address en local_file.service[N]. Sans les blocs moved, le même plan annonce 3 to add, 3 to destroy.Interdit, et vérifié
resource "local_file" "bad" {
count = 2
for_each = toset(["a", "b"])
}
Error: Invalid combination of "count" and "for_each"
Comment choisir
Un seul meta-argument par bloc.count pour des instances interchangeables indexées par entier, for_each pour des instances distinctes indexées par clé.Le patron optionnel
Une ressourcecount = var.actif ? 1 : 0 a 0 ou 1 instance. Le splat ressource[*].attr a donc une longueur 0 ou 1, et one() la réduit :output "nom" {
value = one(local_file.rapport[*].filename)
}
- 0 instance :
null - 1 instance : la valeur unique
- plus de 1 : une erreur (garde-fou)
À noter
Une sortie valantnull est omise de terraform output, ce qui est le comportement attendu quand l'option est désactivée.count est une liste, for_each une map
Le splatressource[*].attr extrait un attribut de chaque élément d'une liste : c'est la forme que produit count.Une ressource for_each est une map indexée par clé. Le splat n'y produit pas le résultat attendu.La bonne forme
Utilisez une expressionfor sur la map :output "chemins" {
value = { for k, f in local_file.service : k => f.filename }
}
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Les boucles for : Transforme une collection avant de la passer à
countou àfor_each. - Blocs dynamiques : Répète un bloc imbriqué, ce que
countne sait pas faire. - Le bloc lifecycle : Encadre les remplacements de ressources, fréquents quand l'index se décale.