You need to create 50 identical microservices infrastructure components in AWS using Terraform, but you don't want to copy-paste your code 50 times. Why should you avoid using count here, and what is the better approach?
Quick Answer
count identifies each resource instance purely by its numeric index (resource[0], resource[1]...), so if you ever remove a microservice from the middle of the list, every resource after it shifts down one index and Terraform plans to destroy and recreate all of them — a devastating blast radius for something as simple as decommissioning one service. The better approach is for_each over a map keyed by microservice name, wrapped in a reusable module, so each resource is addressed by a stable string key that never shifts regardless of what's added or removed elsewhere in the list.
Detailed Answer
Imagine a parking garage that assigns spots purely by arrival order — car 1 in spot 1, car 2 in spot 2, and so on. If the third car ever leaves, garage management doesn't leave spot 3 empty; they shove every car behind it forward by one spot to keep things tidy. Every car now has a new spot number, even though most of them never moved their actual car. That's exactly what count does to your infrastructure: the identity of a resource is just its position in a list, not anything meaningful about the resource itself.
Terraform was designed so that each resource in state needs a stable address to track across applies. count.index gives you a cheap way to create N similar resources, and it's fine for genuinely interchangeable things like N identical subnets in a CIDR range. But the moment the members of the list have individual identity — 50 microservices each with their own name, own image, own environment variables — count conflates 'position in the list' with 'identity of the thing,' and Terraform has no way to tell the difference between 'this microservice was removed' and 'everything after index 12 shifted.'
Internally, when you change a list count is iterating over — say removing 'checkout-worker' from position 12 out of 50 — Terraform's plan diff compares the old state addresses (microservice_ecs_service[12] through [49]) against the new ones. Since 'checkout-worker' is gone, positions 13-49 now map to different services than before, so Terraform sees resource[12] changing from checkout-worker's config to notifications-worker's config, and proposes destroying and recreating every single downstream resource just to relabel them — even though 37 of those services never actually changed.
for_each solves this by keying resources with a map, for example for_each = var.microservices where microservices is a map like { "payments-api" = {...}, "checkout-worker" = {...} }. Now each resource address is aws_ecs_service.svc["checkout-worker"], a string key tied to the service's actual name, completely independent of ordering or what else exists in the map. Removing checkout-worker from the map only destroys that one resource; every other service's address, and therefore its state entry, is untouched.
At production scale for 50 microservices specifically, the real answer goes one level further: wrap the for_each block inside a reusable module (module "microservice" { for_each = var.services source = "./modules/ecs-microservice" ... }), so the actual ECS service, task definition, target group, and autoscaling policy definitions live in one place and get parameterized per service via a values map — often sourced from a YAML or JSON file, or even a separate repo per team, so each microservice team owns its own entry without needing to touch the shared root module. The non-obvious gotcha: switching an existing count-based resource to for_each is itself a breaking, destructive change unless you first run terraform state mv for every resource to remap old numeric addresses to the new string-keyed addresses — engineers who skip this step and just swap count for for_each in the code will watch Terraform plan to destroy and recreate everything on the very next apply.