Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

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_each
variable "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 key
aws_s3_bucket.per_env["production"].arn
# All values as a map
{ for k, v in aws_s3_bucket.per_env : k => v.arn }
# All keys
keys(aws_s3_bucket.per_env) # ["dev", "production", "staging"]
# All values (list)
values(aws_s3_bucket.per_env)[*].arn

Module 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 instance
output "service_names" {
value = { for k, v in module.services : k => v.service_name }
}

for_each vs count: The Stability Difference

# With count — fragile ordering
variable "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 key
resource "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 untouched

Rule: 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 services
variable "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
}