for_each in Terraform
for_each iterates over a map or set to create multiple resource instances, each with a stable identity based on its key. Unlike count, for_each resources are identified by name — removing an item only removes that specific resource, without touching others.
for_each with a Set
variable "environments" { type = set(string) default = ["dev", "staging", "production"]}
resource "aws_s3_bucket" "per_env" { for_each = var.environments bucket = "mycompany-${each.key}-data"
tags = { Environment = each.key }}
# Each instance is identified as:# aws_s3_bucket.per_env["dev"]# aws_s3_bucket.per_env["staging"]# aws_s3_bucket.per_env["production"]
output "bucket_names" { value = { for k, v in aws_s3_bucket.per_env : k => v.bucket }}for_each with a Map
variable "users" { type = map(object({ email = string groups = list(string) })) default = { alice = { email = "alice@company.com", groups = ["admin", "developers"] } bob = { email = "bob@company.com", groups = ["developers"] } carol = { email = "carol@company.com", groups = ["analysts"] } }}
resource "aws_iam_user" "team" { for_each = var.users name = each.key # "alice", "bob", "carol"
tags = { Email = each.value.email }}
# each.key = the map key ("alice", "bob", "carol")# each.value = the object value ({ email = ..., groups = [...] })toset() and tomap() for Conversion
# Convert a list to a set for for_each# (lists can't be used directly — for_each needs set or map)variable "region_list" { type = list(string) default = ["us-east-1", "eu-west-1", "ap-southeast-1"]}
resource "aws_cloudwatch_log_group" "per_region" { for_each = toset(var.region_list) name = "/myapp/${each.key}"
provider = aws.by_region[each.key] # requires provider alias map}# Build a map from a list of objects for for_eachvariable "services" { type = list(object({ name = string port = number })) default = [ { name = "api", port = 8080 }, { name = "auth", port = 8081 }, { name = "metrics", port = 9090 } ]}
resource "aws_security_group_rule" "service_ports" { for_each = { for s in var.services : s.name => s }
type = "ingress" security_group_id = aws_security_group.app.id from_port = each.value.port to_port = each.value.port protocol = "tcp" cidr_blocks = ["10.0.0.0/8"] description = "Allow ${each.key} traffic"}Accessing for_each Resources
# Get the bucket for a specific keyaws_s3_bucket.per_env["production"].arn
# All values as a map{ for k, v in aws_s3_bucket.per_env : k => v.arn }
# All keyskeys(aws_s3_bucket.per_env) # ["dev", "production", "staging"]
# All values (list)values(aws_s3_bucket.per_env)[*].arnModule for_each
for_each also works on module blocks — create multiple module instances:
variable "services" { type = map(object({ image = string cpu = number memory = number desired_count = number })) default = { api = { image = "mycompany/api:v3.2.1" cpu = 512 memory = 1024 desired_count = 3 } worker = { image = "mycompany/worker:v1.5.0" cpu = 1024 memory = 2048 desired_count = 5 } }}
module "services" { for_each = var.services source = "./modules/ecs-service"
name = each.key image = each.value.image cpu = each.value.cpu memory = each.value.memory desired_count = each.value.desired_count
vpc_id = module.networking.vpc_id subnet_ids = module.networking.private_subnet_ids cluster_id = aws_ecs_cluster.main.id}
# Access module outputs per instanceoutput "service_names" { value = { for k, v in module.services : k => v.service_name }}for_each vs count: The Stability Difference
# With count — fragile orderingvariable "subnet_cidrs" { default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]}
resource "aws_subnet" "public" { count = length(var.subnet_cidrs) cidr_block = var.subnet_cidrs[count.index]}# Resources: aws_subnet.public[0], [1], [2]# Remove "10.0.1.0/24" from the list → [0] becomes "10.0.2.0/24"# Terraform destroys and recreates [0] and [1], only [2] is "deleted"
# With for_each — stable by keyresource "aws_subnet" "public" { for_each = toset(var.subnet_cidrs) cidr_block = each.key}# Resources: aws_subnet.public["10.0.1.0/24"], ["10.0.2.0/24"], ["10.0.3.0/24"]# Remove "10.0.1.0/24" → only that subnet is destroyed, others untouchedRule: use for_each when each instance has a meaningful, stable identity. Use count only for truly homogeneous resources where you want them ordered by index.
Filtering with for_each
# Only create resources for enabled servicesvariable "services" { type = map(object({ enabled = bool image = string port = number }))}
resource "aws_lb_target_group" "services" { # Filter: only services where enabled = true for_each = { for k, v in var.services : k => v if v.enabled }
name = each.key port = each.value.port protocol = "HTTP" vpc_id = aws_vpc.main.id}