Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

count Terraform : N instances, et le piège de l'index

Par mis à jour le

Améliorer cette page

Logo Terraform

Le méta-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.

  • Créer N instances avec count, et les personnaliser via count.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 count et for_each, et l'interdiction de les cumuler
  • Migrer de count vers for_each sans rien détruire, avec moved
  • Prouver chaque changement dans le plan JSON

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].

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"
running = true
# ... type et os comme dans la première infrastructure
devices = {
disks = [
{
source = { file = { file = libvirt_volume.disk[count.index].path } }
target = { dev = "vda", bus = "virtio" }
driver = { type = "qcow2" }
}
]
}
}

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

Voici la raison d'être de ce guide. Avec count, l'identité d'une instance est sa position. Prenons trois services nommés d'après une liste, matérialisés ici par des fichiers pour que l'exemple tourne sans hyperviseur :

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 :

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

Les mêmes services, mais indexé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 de la documentation officielle : 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 : des clés connues au plan, jamais sensibles, et une liste convertie en set. Elles sont détaillées, messages d'erreur compris, dans la leçon for_each.

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 cette correspondance à Terraform, une instance après l'autre, et le plan redevient entièrement no-op. La méthode pas à pas et sa preuve par le plan JSON sont dans la leçon for_each, qui traite ce réadressage du point de vue de la ressource d'arrivée.

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.

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.

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 :

Fenêtre de terminal
terraform plan -out=tfplan
terraform show -json tfplan | jq -c '.resource_changes[] | {address, actions: .change.actions, prev: .previous_address}'
  • resource_changes[].actions distingue ["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_address n'apparaît que si l'adresse a changé, typiquement via un bloc moved. C'est la preuve qu'une migration est bien un déplacement, et non une destruction suivie d'une création.

Ces symptômes se lisent tous dans le plan, avant l'apply.

SymptômeCauseSolution
Des instances détruites/recréées au retrait d'un élément du milieuDécalage d'index : l'identité count est positionnellePasser à for_each indexé par une valeur stable
count.index refuséUtilisé hors d'un bloc portant countDéclarer count sur la ressource, ou retirer count.index
Invalid combination of "count" and "for_each"Les deux méta-arguments sur le même blocN'en garder qu'un
The "count" value depends on resource attributes that cannot be determined until applycount dérive d'un attribut known after applyLe fonder sur une variable ou une data source lue au plan
Migration count vers for_each qui détruit toutAucun bloc moved pour relier index et clésAjouter un moved { from = ...[i] to = ...["clé"] } par instance

count indexe par position, et cette position ment dès que la liste bouge : retirer un élément décale tous les suivants, donc les détruit et les recrée. Vous indexez un ensemble par nom avec for_each, migrez une ressource count vers for_each grâce aux blocs moved sans rien détruire, et gardez count là où il reste légitime, pour des copies interchangeables.

  1. count = N crée N instances indexées par entier ; count.index démarre à 0.
  2. La valeur de count doit être connue au plan, jamais known after apply.
  3. Le décalage d'index au retrait d'un élément du milieu est un remplacement (["delete","create"]), pas une simple mise à jour.
  4. count pour des instances interchangeables ; for_each dès qu'elles ont une identité propre. Jamais les deux dans un même bloc.
  5. Migrer count vers for_each sans moved détruit tout ; avec moved, le plan est no-op et previous_address trace le déplacement.
  6. count = cond ? 1 : 0 rend une ressource optionnelle, exposée par one().
  7. Le splat [*] marche sur count (liste), pas sur for_each (map).
  8. La preuve d'un changement est dans resource_changes[].actions, pas dans la sortie humaine.

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.

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn