for_each: Creating Resources from Maps and Sets
Scenario
Your team stores configuration values in AWS Systems Manager Parameter Store. You need to create several parameters with different names and values. Using count would be fragile — adding or removing a parameter in the middle of the list would shift indices and cause unnecessary destroy/recreate cycles.
for_each solves this by keying instances on stable identifiers (map keys or set values) rather than numeric indices. Adding or removing an item only affects that specific instance — everything else is untouched.
- Use for_each with a map to create SSM parameters. Use each.key for the parameter name and each.value for the value.
- Use for_each with a set (via toset()) to create S3 buckets from a list of names.
- Reference a specific instance by its key: aws_ssm_parameter.config["db_host"].
- Observe what happens when you add/remove a key — only that resource changes.
Additional Context
for_each accepts a map or a set of strings. It does NOT accept a list directly — use toset() to convert a list to a set first.
count vs for_each: Use count when you need N identical resources and removal is always from the end. Use for_each when each resource has a unique identity (name, key) and you want adding/removing to be surgical, not index-shifting.
variables.tf
# MAP variable — each entry becomes one SSM parameter
variable "app_config" {
description = "Application configuration key-value pairs."
type = map(string)
default = {
db_host = "db.example.com"
db_port = "5432"
cache_ttl = "3600"
log_level = "info"
}
}
# LIST variable — will be converted to a set with toset()
variable "bucket_names" {
description = "List of S3 bucket names to create."
type = list(string)
default = ["uploads", "exports", "backups"]
}
variable "aws_region" { type = string; default = "us-east-1" }main.tf
# main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = var.aws_region }
data "aws_caller_identity" "current" {}
# ─── for_each WITH A MAP ───
# Creates one SSM parameter per map entry.
# each.key = "db_host", each.value = "db.example.com"
resource "aws_ssm_parameter" "config" {
for_each = var.app_config
name = "/myapp/config/${each.key}"
type = "String"
value = each.value
tags = {
ConfigKey = each.key
ManagedBy = "terraform"
}
}
# ─── for_each WITH A SET ───
# Lists must be converted to sets: toset(["uploads", "exports", "backups"])
# each.key and each.value are identical for sets.
resource "aws_s3_bucket" "app" {
for_each = toset(var.bucket_names)
bucket = "app-${each.value}-${data.aws_caller_identity.current.account_id}"
tags = {
Name = each.value
ManagedBy = "terraform"
}
}outputs.tf
# Reference by key — stable, regardless of order
output "db_host_parameter" {
value = aws_ssm_parameter.config["db_host"].value
}
# All SSM parameter ARNs as a map
output "all_parameter_arns" {
value = { for k, v in aws_ssm_parameter.config : k => v.arn }
}
# All bucket names as a map
output "all_bucket_names" {
value = { for k, v in aws_s3_bucket.app : k => v.id }
}Workflow Commands
terraform init && terraform apply
# Add a new config entry — only 1 new resource created (no disruption)
terraform apply -var='app_config={"db_host":"db.example.com","db_port":"5432","cache_ttl":"3600","log_level":"info","api_key":"abc123"}'
# Remove a config entry — only 1 resource destroyed
terraform apply -var='app_config={"db_host":"db.example.com","db_port":"5432","log_level":"info"}'
terraform destroyApply complete! Resources: 7 added, 0 changed, 0 destroyed.
Outputs:
all_bucket_names = {
"backups" = "app-backups-123456789012"
"exports" = "app-exports-123456789012"
"uploads" = "app-uploads-123456789012"
}
all_parameter_arns = {
"cache_ttl" = "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/config/cache_ttl"
"db_host" = "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/config/db_host"
"db_port" = "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/config/db_port"
"log_level" = "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/config/log_level"
}
db_host_parameter = "db.example.com"
