
Terraform nested for each pattern become important when teams move beyond simple resources and start building reusable modules with complex input data.
A basic for_each works well for a map of users, buckets, or instances. But real infrastructure often includes nested objects: networks with subnets, teams with repositories, applications with environments, or policies with multiple rules.
When teams do not understand how to shape nested data, Terraform modules become harder to maintain. Engineers may duplicate blocks, overuse count, or write modules that are too rigid.
The better approach is to use terraform for each with terraform locals, terraform functions such as flatten, and terraform dynamic block patterns where they make sense.
This framework explains how to move from simple for_each usage to nested flatten structures that support scalable Terraform module design.
Why for_each Matters in Terraform Modules
The for_each meta-argument helps Terraform create multiple instances from a map or set. It is often preferred over count when each instance needs a stable identity.
For example, if you create three environments using a map, each instance is tracked by key, such as dev, staging, and prod. This is safer than relying on numeric indexes that may change when items are reordered.
A simple pattern looks like this:
variable "buckets" {
type = set(string)
}
resource "aws_s3_bucket" "example" {
for_each = var.buckets
bucket = each.value
}
This works because each item is simple. The challenge starts when each item contains children.
Simple Map Pattern
A stronger pattern is to use a map of objects. This gives every resource a stable key and structured configuration.
variable "apps" {
type = map(object({
instance_type = string
environment = string
}))
}
resource "aws_instance" "app" {
for_each = var.apps
ami = "ami-123456"
instance_type = each.value.instance_type
tags = {
Name = each.key
Env = each.value.environment
}
}
This is the best starting point for most Terraform modules. It is readable, stable, and easy to extend.
The Nested for_each Problem
Terraform does not allow a resource-level for_each to directly loop through multiple nested levels in one step. If your input includes networks and subnets, Terraform needs one flat collection for the subnet resource.
For example, this type of input is common:
variable "networks" {
type = map(object({
cidr_block = string
subnets = map(object({
cidr_block = string
az = string
}))
}))
}
This is good input design, but a subnet resource needs a flat map where each subnet has a unique key.
Use locals to Reshape Data
terraform locals are useful because they let teams transform input data before using it in resources. Instead of forcing every resource to handle complex nesting, use a local value to create a cleaner structure.
locals {
subnet_list = flatten([
for network_key, network in var.networks : [
for subnet_key, subnet in network.subnets : {
key = "${network_key}-${subnet_key}"
network_key = network_key
subnet_key = subnet_key
cidr_block = subnet.cidr_block
az = subnet.az
}
]
])
}
This creates a flat list of subnet objects. Each object still remembers where it came from.
Convert Flattened Lists Into Maps
A resource for_each usually works best with a map because map keys become stable resource addresses. After flattening the nested list, convert it into a map.
locals {
subnet_map = {
for subnet in local.subnet_list :
subnet.key => subnet
}
}
Now the subnet resource can use for_each cleanly.
resource "aws_subnet" "example" {
for_each = local.subnet_map
vpc_id = aws_vpc.example[each.value.network_key].id
cidr_block = each.value.cidr_block
availability_zone = each.value.az
tags = {
Name = each.key
}
}
This is the core Terraform nested for_each pattern: nested input, flattened local, stable map, then resource for_each.
Use Stable Keys Carefully
Stable keys are one of the most important parts of this pattern.
Terraform uses keys to track resource instances. If keys change, Terraform may think resources must be destroyed and recreated.
Avoid keys based only on list position. Use meaningful names such as environment, network, subnet, region, or team.
Good key:
"${network_key}-${subnet_key}"
Risky key:
index
Stable keys make plans easier to review and reduce unexpected infrastructure changes.
When to Use Dynamic Blocks
A terraform dynamic block is useful when the nested item is part of a resource, not a separate resource.
For example, security group ingress rules, lifecycle rules, or nested configuration blocks may be good candidates.
resource "aws_security_group" "example" {
name = "app-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Use dynamic blocks when Terraform expects repeated nested blocks inside one resource.
Use resource-level for_each when each item should become its own resource instance.
Where Terraform Functions Help
Terraform functions help reshape data before it reaches for_each. The most useful functions for nested patterns include:
- flatten for turning nested lists into one list
- merge for combining maps
- setproduct for generating combinations
- toset for converting lists into sets
- zipmap for building maps from related lists
The goal is not to make clever code. The goal is to make data predictable before Terraform creates resources.
How Terraform Import Fits This Pattern
terraform import becomes easier when resources use stable for_each keys. Imported resources need to map into Terraform state using the correct resource address.
For example, a resource created with for_each may have an address like:
aws_subnet.example["prod-private-a"]
If keys are meaningful, import and state review become easier. If keys are unclear or unstable, import work becomes more error-prone.
Terraform Cloud, Registry, and Team Workflows
These patterns matter even more in Terraform Cloud or team-based workflows.
A module published through the Terraform registry should have predictable inputs, stable keys, and clear examples.
Teams using shared modules need confidence that adding one item will not recreate unrelated infrastructure.
At scale, Terraform code quality becomes a governance issue. Poor data structures create risky plans. Clear for_each patterns make modules safer, easier to review, and easier to reuse.
env0 helps teams run and govern Terraform workflows with approvals, RBAC, drift detection, cost visibility, and auditability.
That platform layer is especially useful when many teams consume shared modules.
Common Mistakes to Avoid
The most common mistake is using lists where maps would be safer. Lists are fine for simple values, but maps usually create more stable resource addresses.
Another mistake is flattening data without creating a unique key. Every item in a for_each map needs a predictable identity.
A third mistake is using dynamic blocks for resources that should be separate. If each child item has its own lifecycle, use resource-level for_each.
Conclusion: Shape the Data Before Using for_each
Terraform nested for_each patterns are easier when teams separate input design from resource creation.
Start with clean nested input, reshape it with locals and functions, convert it into a stable map, then use for_each.
This approach keeps Terraform modules flexible without making them chaotic.
It also helps platform teams create reusable modules that work across environments, teams, and Terraform Cloud workflows.
Build Governed Terraform Workflows With env0
env0’s IaC Platform & Terraform Automation service helps teams manage Terraform workflows with approvals, policy controls, RBAC, drift detection, cost visibility, and audit logs.
Talk to env0 to govern reusable Terraform modules and scale infrastructure delivery safely across teams.
FAQs
What is Terraform nested for_each?
Terraform nested for_each is a pattern for working with nested input data, such as networks with subnets or teams with repositories. Teams usually flatten the nested data into a stable map before using it in a resource-level for_each.
Can Terraform for_each loop through nested objects directly?
Not directly in one resource-level loop. Terraform needs the for_each value to be a suitable collection. For nested data, teams commonly use flatten, for expressions, and locals to reshape the data first.
When should I use flatten with for_each?
Use flatten when your input data contains nested lists or maps that need to become one flat collection for resource creation. After flattening, convert the list into a map with stable keys.
When should I use a dynamic block?
Use a dynamic block when you need to generate repeated nested blocks inside one resource. Use resource-level for_each when each item should become its own Terraform-managed resource.
Why are stable keys important in Terraform for_each?
Stable keys help Terraform track resource instances. If keys change, Terraform may plan to destroy and recreate resources. Meaningful keys make plans, imports, and reviews safer.
How does env0 help with Terraform module workflows?
env0 helps teams govern Terraform workflows with approvals, RBAC, drift detection, cost visibility, audit logs, and policy controls. This is useful when teams share reusable modules across environments.
.webp)