Terraform Error: Invalid For_each Argument Must Be A Map Or Set Of Strings - Complete Fix Guide
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 Type | Solution | Example |
|---|---|---|
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 map | Access via var.map[each.key] |
set(string) | ✅ Already valid | Use directly |
map(string) | ✅ Already valid | Use 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.
Was this article helpful?
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
Terraform "Error: No Valid Credential Sources Found" On AWS — Fixing Authentication And Provider Configuration Issues
If you've spent any time with Terraform and AWS, you've almost certainly run into this gem at some point: It's one of those errors that looks simple but ca...
Terraform "Error: Cycle" In Resource Dependencies — How To Detect And Break Circular References
If you've spent any meaningful time with Terraform, you've probably encountered this at the worst possible moment: Everything was working fine, you added o...
Fix Terraform 'Error Acquiring the State Lock' with DynamoDB
Resolve Terraform DynamoDB state lock errors caused by permission issues, missing tables, or misconfigured backends.
Fix Terraform State Lock Stuck After Failed Apply
Safely unlock Terraform state when a lock is stuck after a crashed apply, CI timeout, or interrupted operation.
Terraform Dynamic Blocks For Managing Variable Infrastructure Resources
If you've been working with Terraform for a while, you've probably hit that wall where you need to create resources with configurations that vary based on...
Prometheus Target Down Error: Debugging Failed Scrapes And Network Connectivity Issues
You've deployed Prometheus, configured your targets, and everything looks perfect in your YAML files. Then you check the Prometheus UI and see those dreade...
More in Terraform
View all →Writing Reusable Terraform Modules: A Practical Guide
Learn how to write Terraform modules that are actually reusable — with input validation, sensible defaults, and clean interfaces across environments.
Terraform CLI: Cheat Sheet
Terraform CLI cheat sheet with commands organized by workflow — init, plan, apply, destroy, state manipulation, imports, and workspace management.
Terraform from Zero to Production: Project Structure, Modules, State, and CI/CD
Build production-grade Terraform infrastructure — project structure, module design, state management, testing, and CI/CD pipeline integration.
Testing Terraform with Terratest: A Practical Guide
How to write unit and integration tests for Terraform modules using Terratest — because untested infrastructure is a liability.