Why you need them
Say you need five S3 buckets: logs-dev, logs-staging, logs-prod, data-dev, data-prod. You can write five resource blocks. Or you can write one with count or for_each.
This is about repeated resources. When things differ only by an index or a key, move them into count/for_each instead of duplicating code.
count is the simplest way
resource "aws_s3_bucket" "logs" {count = 3
bucket = "logs-bucket-${count.index}"}
This creates three buckets:
aws_s3_bucket.logs[0]namedlogs-bucket-0aws_s3_bucket.logs[1]namedlogs-bucket-1aws_s3_bucket.logs[2]namedlogs-bucket-2
Inside the block you have the count.index variable, the current number from 0 to N-1.
To change the quantity, change the number. Terraform sees "it was 3, now it should be 5" and creates two more.
for_each, when each one has its own key
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "eu", "ap"])
bucket = "data-bucket-${each.key}"}
This creates three buckets:
aws_s3_bucket.regional["us"]nameddata-bucket-usaws_s3_bucket.regional["eu"]nameddata-bucket-euaws_s3_bucket.regional["ap"]nameddata-bucket-ap
You have each.key (the key) and each.value (the value). for_each accepts a set or a map:
# set, plain keys, each.key == each.value
for_each = toset(["us", "eu", "ap"])
# map, key-value pairs, each.key and each.value differ
for_each = { us = { region = "us-east-1", tier = "primary" } eu = { region = "eu-central-1", tier = "secondary" }}
# then: each.key, "us"/"eu", each.value.region, each.value.tier
The main difference: what happens when the list changes
Say you have three resources. Remove the middle one.
With count:
count = 3 → count = 2
Terraform reasons like this: "there were 3 elements at indexes 0, 1, 2. Now there should be 2." It removes the element at index 2. That holds even if you meant to remove element 1. Terraform does not understand that; it just shifts: index 2 disappears, and elements 0 and 1 stay the same.
The problem: when you remove an element from the middle of a list with count, Terraform recreates everything after it, because the indexes shift.
With for_each:
for_each = toset(["us", "eu", "ap"]) → for_each = toset(["us", "ap"])
Terraform sees "there was a key eu, and it is gone now," and removes only aws_s3_bucket.regional["eu"]. It leaves the rest untouched.
Rule of thumb: if the list can change in the middle, use for_each. Use count only when the count grows or shrinks from the end, or when the elements really are interchangeable.
When to use which
- count for three identical VMs in an auto-scaling-like setup, for fault tolerance through duplication, and for resources where order does not matter.
- for_each for almost everything else. Buckets for different environments, IAM roles for different services, sg-rules with different ports.
When you are not sure, use for_each. It is more explicit and safer.
References and outputs
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "eu", "ap"])
bucket = "data-${each.key}"}
# one specific bucket
output "us_bucket_arn" {value = aws_s3_bucket.regional["us"].arn
}
# all ARNs as a list
output "all_arns" {value = [for k, b in aws_s3_bucket.regional : b.arn]
}
# all ARNs keyed by region
output "arns_by_region" { value = { for k, b in aws_s3_bucket.regional : k => b.arn }}
Gotchas
-
You cannot use count and for_each together. Only one of them per resource.
-
count = 0orfor_each = []is valid. Terraform creates zero resources. This is a legal way to disable a block conditionally:count = var.create ? 1 : 0. -
for_each needs statically-knowable keys. The keys must be known at
plantime. You cannot writefor_each = data.aws_some_thing.dynamicwhen that data becomes known only after another resource is applied. Fix it with dependencies or static values. -
Migrating from count to for_each means recreation. Terraform treats it as a different resource. The addresses change (
x[0]→x["key"]). Fix it with amoved {}block (advanced). -
Map keys are always strings.
for_each = { 1 = "a", 2 = "b" }complains: numbers must be converted to strings:{ "1" = "a", "2" = "b" }. -
A large for_each means a slow plan. If your for_each runs over 500 elements, plan will hit the API 500 times. Think about scale.