DevOpsil
Terraform
85%
Needs Review

Terraform Error: Invalid For_each Argument Must Be A Map Or Set Of Strings - Complete Fix Guide

Majid Iqbal NayyarMajid Iqbal Nayyar4 min read

Terraform Error: Invalid for_each argument must be a map or set of strings - Complete Fix Guide

If you've been working with Terraform's for_each argument, you've probably hit this error at least once. Don't worry – it's one of those frustrating but easily fixable issues once you know what's going on.

What Causes This Error

The for_each argument in Terraform is picky. It only accepts:

  • Maps with string values
  • Sets of strings

That's it. No lists, no complex objects, no nested maps. String keys, string values, period.

Quick Fixes by Data Type

Fix #1: Converting Lists to Sets

Problem: You're trying to use a list

# ❌ This fails
variable "database_names" {
  type = list(string)
  default = ["users_db", "orders_db", "analytics_db"]
}

resource "aws_db_instance" "databases" {
  for_each = var.database_names  # Error!
  # ... rest of config
}

Solution: Convert to a set using toset()

# ✅ This works
resource "aws_db_instance" "databases" {
  for_each = toset(var.database_names)
  
  identifier = each.key
  db_name    = each.value  # same as each.key for sets
  # ... rest of config
}

Fix #2: Complex Objects to Maps

Problem: You have a list of objects

# ❌ This fails
variable "databases" {
  type = list(object({
    name     = string
    size     = string
    engine   = string
  }))
  default = [
    {
      name   = "users"
      size   = "db.t3.micro"
      engine = "postgres"
    },
    {
      name   = "orders"
      size   = "db.t3.small"
      engine = "mysql"
    }
  ]
}

resource "aws_db_instance" "databases" {
  for_each = var.databases  # Error!
}

Solution: Convert to a map using for expression

# ✅ This works
locals {
  database_map = {
    for db in var.databases : db.name => db
  }
}

resource "aws_db_instance" "databases" {
  for_each = local.database_map
  
  identifier     = each.key
  instance_class = each.value.size
  engine         = each.value.engine
  # ... rest of config
}

Fix #3: Nested Maps with Non-String Values

Problem: Your map has non-string values

# ❌ This fails
variable "redis_configs" {
  type = map(object({
    port        = number
    memory_size = number
  }))
  default = {
    "cache"     = { port = 6379, memory_size = 1024 }
    "sessions"  = { port = 6380, memory_size = 512 }
  }
}

resource "aws_elasticache_cluster" "redis" {
  for_each = var.redis_configs  # Error!
}

Solution: Create a string map for iteration, access original data separately

# ✅ This works
resource "aws_elasticache_cluster" "redis" {
  for_each = toset(keys(var.redis_configs))
  
  cluster_id       = each.key
  port             = var.redis_configs[each.key].port
  parameter_group_name = "default.redis7"
  # ... rest of config
}

Common Patterns and Best Practices

Pattern 1: Database Per Environment

locals {
  environments = ["dev", "staging", "prod"]
  db_configs = {
    for env in local.environments : env => {
      instance_class = env == "prod" ? "db.r5.large" : "db.t3.micro"
      backup_days    = env == "prod" ? 30 : 7
    }
  }
}

resource "aws_db_instance" "app_db" {
  for_each = toset(keys(local.db_configs))
  
  identifier                = "app-${each.key}"
  instance_class           = local.db_configs[each.key].instance_class
  backup_retention_period  = local.db_configs[each.key].backup_days
  
  engine         = "postgres"
  engine_version = "14.9"
  db_name        = "appdb"
  # ... rest of config
}

Pattern 2: Redis Clusters with Dynamic Configuration

variable "redis_clusters" {
  type = map(object({
    node_type    = string
    num_nodes    = number
    description  = string
  }))
  default = {
    "user-cache" = {
      node_type   = "cache.t3.micro"
      num_nodes   = 2
      description = "User session cache"
    }
    "app-cache" = {
      node_type   = "cache.r5.large"
      num_nodes   = 3
      description = "Application data cache"
    }
  }
}

# Convert to string map for for_each
locals {
  redis_cluster_keys = { for k, v in var.redis_clusters : k => k }
}

resource "aws_elasticache_replication_group" "redis" {
  for_each = local.redis_cluster_keys
  
  replication_group_id       = each.key
  description               = var.redis_clusters[each.key].description
  node_type                = var.redis_clusters[each.key].node_type
  num_cache_clusters       = var.redis_clusters[each.key].num_nodes
  
  engine               = "redis"
  engine_version      = "7.0"
  parameter_group_name = "default.redis7.cluster.on"
}

Debugging Tips

Check Your Data Structure

Add a temporary output to see what you're working with:

output "debug_data" {
  value = var.your_variable
}

Validate with terraform console

# Test your transformations
terraform console
> toset(var.database_names)
> { for db in var.databases : db.name => db }

Use type() Function for Debugging

output "data_type_check" {
  value = type(var.your_variable)
}

Quick Reference Cheat Sheet

Input TypeSolutionExample
list(string)toset(list)toset(["a", "b", "c"])
list(object){ for item in list : item.key => item }Transform to map
map(object)toset(keys(map)) or create string mapAccess via var.map[each.key]
set(string)✅ Already validUse directly
map(string)✅ Already validUse directly

Remember: for_each is strict about types. When in doubt, convert your data to either a set of strings or a map with string values. Your infrastructure-as-code will thank you for the consistency.

Share:

Was this article helpful?

Majid Iqbal Nayyar
Majid Iqbal Nayyar

Data Infrastructure Engineer

Your data is only as good as the infrastructure it sits on. I specialize in PostgreSQL, Redis, database migrations, backup strategies, and making sure your data survives whatever chaos your application throws at it.

Related Articles

More in Terraform

View all →
TerraformQuick RefBeginnerNeeds Review

Terraform CLI: Cheat Sheet

Terraform CLI cheat sheet with commands organized by workflow — init, plan, apply, destroy, state manipulation, imports, and workspace management.

Zara Blackwood·
3 min read

Discussion