TFLAB11-for_each: Creating Resources from Maps and Sets

for_each: Creating Resources from Maps and Sets

🔧 Terraform Core ⭐ Beginner for_each each.key each.value toset() count vs for_each

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.

Your Objectives
  • 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 destroy
✓ Expected Output After Apply
Apply 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"