Fix Terraform Error - Invalid for_each Argument
Fix the Terraform invalid for_each argument error when keys depend on unknown values. Covers static keys, targeting, locals, and for_each best practices.
Troubleshooting
Fix the Terraform each.value unsupported attribute error. Covers for_each map structures, lookup defaults, try functions, and type-safe variable definitions.
You're referencing an attribute on each.value that doesn't exist in your for_each map. Check that every entry in the map has the attribute you're accessing, or use lookup() / try() with a default value for optional attributes.
When running terraform plan, you encounter:
Error: Unsupported attribute
on main.tf line 12, in resource "aws_instance" "servers":
12: instance_type = each.value.instance_type
This object does not have an attribute named "instance_type".Error: Unsupported attribute
on main.tf line 8, in resource "aws_security_group_rule" "rules":
8: description = each.value.description
This value does not have any attributes.Some entries in your map are missing the attribute you reference:
variable "servers" {
default = {
web = {
instance_type = "t3.micro"
ami = "ami-12345"
}
db = {
instance_type = "r5.large"
# Missing "ami" — accessing each.value.ami will fail
}
}
}When for_each iterates over a set of strings, each.value is a string — not an object:
# each.value is a STRING here, not an object
resource "aws_iam_user" "users" {
for_each = toset(["alice", "bob", "carol"])
name = each.value # OK — it's a string
# name = each.value.name # ERROR — string has no .name attribute
}The variable type doesn't enforce the expected structure:
# BAD — map(any) doesn't enforce object shape
variable "servers" {
type = map(any)
}
# GOOD — enforce the expected attributes
variable "servers" {
type = map(object({
instance_type = string
ami = string
}))
}Trying to access nested attributes that don't exist at the expected depth:
# each.value.tags.Name when tags might not exist
tags = each.value.tags.Name # ERROR if tags is nullUse an object type constraint to enforce required attributes:
variable "servers" {
type = map(object({
instance_type = string
ami = string
subnet_id = string
}))
}
resource "aws_instance" "servers" {
for_each = var.servers
ami = each.value.ami # Guaranteed to exist
instance_type = each.value.instance_type # Guaranteed to exist
subnet_id = each.value.subnet_id # Guaranteed to exist
tags = {
Name = each.key
}
}variable "servers" {
type = map(map(string))
default = {
web = {
instance_type = "t3.micro"
}
db = {
instance_type = "r5.large"
volume_size = "100"
}
}
}
resource "aws_instance" "servers" {
for_each = var.servers
ami = var.ami_id
instance_type = each.value.instance_type
root_block_device {
# Use lookup with a default for optional attributes
volume_size = lookup(each.value, "volume_size", "20")
}
}resource "aws_instance" "servers" {
for_each = var.servers
ami = each.value.ami
instance_type = each.value.instance_type
# try() returns the default if the attribute doesn't exist
monitoring = try(each.value.monitoring, false)
tags = {
Name = each.key
Environment = try(each.value.environment, "dev")
CostCenter = try(each.value.cost_center, "shared")
}
}variable "servers" {
type = map(object({
instance_type = string
ami = string
monitoring = optional(bool, false)
volume_size = optional(number, 20)
environment = optional(string, "dev")
}))
}
# Now all attributes are accessible — optionals use their defaults
resource "aws_instance" "servers" {
for_each = var.servers
ami = each.value.ami
instance_type = each.value.instance_type
monitoring = each.value.monitoring # defaults to false
root_block_device {
volume_size = each.value.volume_size # defaults to 20
}
}# When iterating over a set of strings
variable "user_names" {
type = set(string)
default = ["alice", "bob"]
}
resource "aws_iam_user" "users" {
for_each = var.user_names
# each.key and each.value are both the string
name = each.value # "alice", "bob" — NOT each.value.name
}
# When iterating over a map of objects
variable "users" {
type = map(object({
email = string
role = string
}))
}
resource "aws_iam_user" "users" {
for_each = var.users
name = each.key # The map key
tags = {
Email = each.value.email # Object attribute
Role = each.value.role # Object attribute
}
}Add an output to inspect what for_each receives:
output "debug_servers" {
value = var.servers
}
# Or inspect a specific entry
output "debug_first" {
value = var.servers["web"]
}terraform plan -var-file=dev.tfvars
# Check the output to see the actual structure| Code | Problem | Fix |
|---|---|---|
each.value.name on toset(["a","b"]) | each.value is a string | Use each.value directly |
each.value.ami with map(string) | Map values are strings | Use each.value as the string |
each.value.optional_key | Key might not exist | lookup(each.value, "key", "default") |
each.value.nested.deep | Nested object might be null | try(each.value.nested.deep, "default") |
for_each? Set of strings, map of strings, or map of objects?each.value.attr on a string set? (It should be each.value)optional() type modifier for non-required fields?for_each variablesoptional() with defaults (Terraform 1.3+) for non-required attributestry() over lookup() for nested attribute accessterraform validate to catch type errors before planThe "unsupported attribute" error on each.value means the data structure doesn't match your code's expectations. Define type-safe variables with object() types, use optional() for non-required fields, and use try() or lookup() with defaults for flexibility. Always check whether for_each is iterating over strings or objects.
Fix the Terraform invalid for_each argument error when keys depend on unknown values. Covers static keys, targeting, locals, and for_each best practices.
Fix the Terraform 'Backend configuration changed' error. Migrate state between backends (local to S3, S3 to S3), resolve lock conflicts
Fix Terraform provider version conflicts between modules, lock files, and constraint mismatches. Resolve dependency lock errors, upgrade providers safely
Fix the Terraform 'Reference to undeclared resource' error. Causes include typos, missing resources, wrong module references, and for_each/count confusion.