What Are Count and For Each in Terraform?
count and for_each are meta-arguments — special arguments that work on any resource or module block, regardless of its type — that let you create more than one copy of that resource from a single block of code, instead of copy-pasting the same block five times with different names.
Picture it like a print job. count is like printing five identical pages numbered 1 through 5 — same content, just numbered. for_each is like printing one labelled folder per employee name from a list — each copy is tied to something meaningful, not just a number.
At CRED, the platform team needed eight S3 buckets — one per data domain: transactions, rewards, kyc, analytics, and so on. Writing eight separate resource "aws_s3_bucket" blocks would mean eight places to fix a bug later. Using for_each over a list of bucket names, they wrote the block once. Adding a ninth bucket later meant adding one line to a list — not writing a new resource block.
Count — Identical Copies by Number
count takes a number and creates that many copies. Inside the block, count.index gives you the current copy's position, starting at 0:
resource "aws_instance" "worker" { count = 3 # creates 3 identical EC2 instances ami = "ami-0f5ee92e2d63afc18" instance_type = "t3.medium" tags = { Name = "worker-${count.index}" # worker-0, worker-1, worker-2 }}Each instance is addressed in state by its index: aws_instance.worker[0], aws_instance.worker[1], aws_instance.worker[2].
COMMON MISTAKE / WARNINGCommon mistake: removing an item from the middle of a `count`-based list. If you delete `worker-1` from a list that feeds `count`, Terraform does not just remove that one instance — it shifts every index after it down by one, which means it destroys and recreates everything that came after. This is the single biggest reason engineers switch from `count` to `for_each`.
For Each — Keyed Copies from a Map or Set
for_each takes a map or a set of strings. Inside the block, each.key and each.value give you the current item:
locals { data_buckets = toset([ "transactions", "rewards", "kyc", "analytics" ])} resource "aws_s3_bucket" "domain_data" { for_each = local.data_buckets # one bucket PER item in the set bucket = "cred-${each.value}-data-prod" # each.value is the string itself for a set}With a map, each.key is the map key and each.value is the map's value — useful when each item needs more than one piece of data:
locals { services = { orders = { = 512, memory = 1024 } payments = { = 1024, memory = 2048 } kyc = { = 256, memory = 512 } }} resource "aws_ecs_task_definition" "service" { for_each = local.services family = each.key # "orders", "payments", "kyc" = each.value. # reads the nested value memory = each.value.memory # reads the nested memory value}Each instance is addressed in state by its key, not a number: aws_ecs_task_definition.service["payments"]. Remove "kyc" from the map and only the kyc task definition is destroyed — everything else stays exactly as it is.
When to Use Count vs For Each
| Situation | Use |
|---|---|
| You need N identical, interchangeable copies | count |
| Items have meaningful names you want preserved in state | for_each |
| You will ever delete an item from the middle of the list | for_each — avoids the index-shift recreate problem |
| Creating zero or one of a resource conditionally | count = var.create_resource ? 1 : 0 |
Conditional Resources with Count
A very common pattern is using count to create a resource only when a condition is true:
resource "aws_instance" "bastion" { count = var.create_bastion_host ? 1 : 0 # 1 if true, 0 if false — creates it or skips it ami = data.aws_ami.amazon_linux.id instance_type = "t3.micro"}Using For Each with Modules
for_each works on module blocks too — useful when you need the same module pattern applied to several environments or services at once:
module "ecs_service" { for_each = local.services source = "./modules/ecs-service" service_name = each.key cpu = each.value.cpu}Converting Between Count and For Each
INFORMATIONSecurity/safety note: switching an existing resource from `count` to `for_each` (or back) is NOT a simple find-and-replace. Terraform sees it as destroying the old indexed resources and creating brand-new keyed ones — because the resource address changes completely. Always run `terraform plan` first and review carefully; for stateful resources like databases, use `terraform state mv` to rename the address in state instead of letting Terraform destroy and recreate.
# Renaming state address after switching count to for_each — avoids destroy/recreateterraform state mv 'aws_instance.worker[0]' 'aws_instance.worker["orders"]'Quick Reference
| Syntax | Refers To |
|---|---|
count.index |
Current copy's number (0, 1, 2...) inside a count block |
each.key |
Current item's map key (or value itself, for a set) inside for_each |
each.value |
Current item's map value inside for_each |
resource_type.name[0] |
Addressing a specific count instance |
resource_type.name["key"] |
Addressing a specific for_each instance |
| Error | Root Cause | Fix |
|---|---|---|
Invalid for_each argument |
Used a list instead of a map or set | Wrap the list in toset() first |
| Plan shows unrelated resources being destroyed | Removed an item from the middle of a count list |
Switch to for_each, or use terraform state mv to fix addressing |
each.value is null |
Map value missing for that key | Check every key in your map has a value defined |
Duplicate object key |
Two items resolve to the same map key | Make sure your for_each map/set has unique keys |