
If you are writing Terraform configurations for more than one resource, environment, user, subnet, bucket, or module instance, you will eventually run into the same problem: repetition.
You can copy and paste resource blocks, but that gets difficult to maintain. You can use count, but index-based resources can become fragile when items are added, removed, or reordered. This is where terraform for each becomes one of the most useful tools in the Terraform language.
The for_each meta-argument lets teams create multiple resource or module instances from a map or set, while keeping each instance tied to a stable key. That makes Terraform code cleaner, easier to review, and safer to scale across environments.
This guide explains how for_each works, when to use it instead of count, how to use it with maps and sets, how to work around list limitations, how to use for_each with modules, and how to avoid common errors.
What Is Terraform for_each?
Terraform for_each is a meta-argument that allows a resource, module, or supported block to create one instance for each item in a map or set of strings. Each item becomes a separate Terraform-managed instance with its own address, lifecycle, and state entry. HashiCorp's Terraform documentation explains that for_each accepts a map or set of strings and creates an instance for each item in that collection.
In simple terms, for_each lets you write one resource block and use it multiple times. Instead of writing three separate S3 bucket resources, three IAM users, or three network resources, you define the pattern once and pass Terraform the collection of items to create.
A basic example looks like this:
resource "aws_iam_user" "users" {
for_each = toset(["alice", "bob", "carol"])
name = each.key
}
Terraform creates one IAM user for each name in the set. Inside the resource block, each.key refers to the current item. When using a set, each.key and each.value are the same.
The real value of for_each is not just shorter code. It gives each resource instance a meaningful key, which makes Terraform plans and state easier to understand.
Why Infrastructure Teams Use for_each
Infrastructure teams use for_each because cloud environments are rarely made of one resource.
A platform team may need to create multiple subnets across availability zones. A security team may need multiple IAM roles. An application team may need one storage bucket per service. A DevOps team may need the same module deployed across development, staging, and production.
Without for_each, teams often duplicate resource blocks or rely on count. Both approaches can work, but they become harder to manage as configurations grow.
for_each helps solve that by making repeated infrastructure more intentional. Each item has a clear key, and each generated instance can be referenced by that key.
With for_each, accessing resources is easier to understand:
output "alice_arn" {
value = aws_iam_user.users["alice"].arn
}
This way, Terraform resource references are descriptive and easier to maintain. This matters during reviews. A Terraform plan that says aws_iam_user.users["alice"] is being updated is much easier to understand than a plan that refers only to index 0.
for_each vs count
Terraform count and for_each both allow teams to create multiple instances from one block. The difference is how Terraform identifies each instance.
count uses numeric indexes starting from zero. Terraform's documentation explains that count.index identifies each instance by its position.
for_each uses keys. Those keys come from a map or set.
Use count when you need a simple number of nearly identical resources.
Use for_each when each instance needs a meaningful identity or different values.
Sometimes count is fine for multiple identical resources:
resource "aws_instance" "web" {
count = 3
ami = var.ami_id
instance_type = "t3.micro"
}
But if each instance has unique attributes like name, size, owner, or environment-specific values, for_each is usually better:
resource "aws_instance" "web" {
for_each = var.instances
ami = var.ami_id
instance_type = each.value.instance_type
tags = {
Name = each.key
}
}
This approach keeps your configuration readable, maintainable, and descriptive.
The biggest practical difference is stability. If you remove one item from a list used with count, Terraform may shift indexes and plan changes to resources that should not change. With for_each, each resource is tied to its key, so removing one item does not automatically shift the identity of the others.
HashiCorp's guidance is straightforward: use count for nearly identical instances, and use for_each when instance arguments require distinct values that cannot be derived cleanly from an integer index.
Using for_each With Maps and Sets
Maps are one of the best inputs for Terraform for_each because each item already has a key and a value. Here is a simple map example:
resource "aws_iam_user" "team" {
for_each = {
alice = "admin"
bob = "developer"
}
name = each.key
path = "/${each.value}/"
}
Terraform creates one instance for each key in the map, using each.key as the instance name and each.value as its role. A map gives you both an identifier and an associated value in the same iteration, which is why maps are the better choice whenever your resources need distinct configuration per item, not just a unique name.
If you don't need a distinct value per item, just a unique identifier, sets are the leaner option. You can use for_each with a set of strings to create multiple IAM users:
resource "aws_iam_user" "users" {
for_each = var.user_names
name = each.key
}
With this input:
variable "user_names" {
type = set(string)
default = ["alice", "bob", "carol"]
}
Terraform will create one IAM user for each name in the set, and each.key refers to the current item. Sets are unordered collections. Terraform does not treat them like lists with indexes. HashiCorp's documentation notes that sets do not support direct index access because they are unordered.
That is one reason sets work well with for_each. The goal is not to rely on position. The goal is to create one instance per unique string.
The for_each List Workaround
A common Terraform error happens when teams try to use a list directly with for_each.
Be careful: the following example will not work as expected if var.user_list is a list:
variable "user_list" {
type = list(string)
default = ["alice", "bob", "carol"]
}
resource "aws_iam_user" "users" {
for_each = var.user_list
name = each.key
}
In this case, each.key does not refer to the actual string values in the list. To fix this, convert the list to a toset() or use a map:
resource "aws_iam_user" "users" {
for_each = toset(var.user_list)
name = each.key
}
This ensures each.key works as expected, and Terraform creates one resource per item.
Terraform requires for_each to receive a map or a set of strings. It does not automatically convert lists or tuples to sets for for_each. HashiCorp's documentation states that Terraform does not implicitly convert lists or tuples to sets for for_each, so teams must explicitly convert when needed.
The simple workaround for a list of simple strings is to use toset():
for_each = toset(var.user_list)
This works well when the list contains simple strings. For a list of objects, you usually need to convert it into a map using a for expression:
locals {
users_map = { for u in var.user_list : u.name => u }
}
resource "aws_iam_user" "users" {
for_each = local.users_map
name = each.value.name
}
This ensures each object in the list is mapped by a unique key, allowing each.key and each.value to work as expected. This is a clean pattern because the resource key becomes the instance name.
Nested for_each
Nested for_each comes up when infrastructure has parent-child relationships, for example multiple applications that each need their own subnets, roles, or rules. Terraform can handle this, but the data has to be reshaped first: nested structures generally need to be flattened into a single map before for_each can consume them, and it's easy for that transformation to turn into something the next person can't follow.
The actual flattening technique, how to keep keys stable across runs, and when to reach for a dynamic block instead of a nested for_each are covered in full in Terraform for_each Patterns.
Related reading: Nested for_each generates repeated resources from flattened data. When you need to generate repeated nested blocks inside a single resource instead, that's a job for Terraform Dynamic Blocks, which covers when to reach for one pattern over the other.
Terraform for_each Module Example
Terraform for_each is not limited to resources. It can also be used with modules.
This is useful when you want to deploy the same module multiple times with different inputs.
You can also use for_each with modules to dynamically create multiple instances with different configurations:
module "environment" {
source = "./modules/environment"
for_each = var.environments
name = each.key
instance_type = each.value.instance_type
}
A matching input for this variable may look like:
variable "environments" {
type = map(object({
instance_type = string
}))
default = {
dev = { instance_type = "t3.micro" }
prod = { instance_type = "t3.large" }
}
}
This approach allows you to create multiple module instances dynamically, each configured for a specific environment.
Terraform creates one module instance for each environment. Those module instances can be referenced by key:
output "prod_environment_id" {
value = module.environment["prod"].id
}
This pattern is helpful for platform teams because it supports standardization without forcing every environment to be identical.
The module defines the approved infrastructure pattern. The input map defines how each environment should be configured.
Chaining for_each Between Resources
Terraform can also chain for_each between resources.
This means one resource using for_each can become the input for another resource. HashiCorp's documentation describes this as a useful pattern when there is a one-to-one relationship between objects, such as creating one internet gateway for each VPC.
Another common example is creating dependent resources using for_each. Here, we create VPCs and their associated Internet Gateways:
resource "aws_vpc" "this" {
for_each = var.vpcs
cidr_block = each.value.cidr_block
}
resource "aws_internet_gateway" "this" {
for_each = aws_vpc.this
vpc_id = each.value.id
}
In this example:
aws_vpc.thisusesfor_eachto create multiple VPCs fromvar.vpcs.aws_internet_gateway.thisalso usesfor_eachto create one IGW per VPC, referencingeach.value.id.- This approach ensures dependent resources are dynamically mapped to their parent resources.
This tells Terraform that each VPC should have a matching internet gateway.
Chaining can make relationships easier to express because Terraform understands the connection between the resources.
It also keeps keys consistent across related resources.
Common for_each Errors and Fixes
- Using a list directly: the fix is to use
toset()for a list of strings or convert a list of objects into a map. - Using values that are not known until apply: Terraform needs the for_each collection before it can perform remote resource operations, so the keys must be known during planning. HashiCorp's documentation explains that for_each values must be known before Terraform performs remote actions.
- Using sensitive values in for_each: Terraform does not allow sensitive values as for_each arguments because the keys are used to identify instances and may appear in UI output.
- Changing keys without understanding the impact: if a key changes, Terraform treats it as a different instance, which may cause Terraform to destroy the old instance and create a new one.
For example, you can rename keys in a map to make them more descriptive. Changing:
resource "aws_iam_user" "users" {
for_each = {
"0" = "alice@example.com"
"1" = "bob@example.com"
}
name = each.value
}
To something more meaningful, like:
resource "aws_iam_user" "users" {
for_each = {
alice = "alice@example.com"
bob = "bob@example.com"
}
name = each.value
}
This ensures that each.key in for_each references a clear, readable name, improving maintainability, but may cause Terraform to replace the resource because the key changed.
This is why stable keys matter. Choose keys that represent long-term identity, not temporary labels.
Best Practices for Terraform for_each
- Use maps when resources need unique settings: maps make it clear which values belong to which resource.
- Use sets when each item is just a unique string: this is useful for users, simple names, or IDs.
- Avoid unstable keys: keys should not depend on generated IDs, timestamps, random values, or values that may change often.
- Prefer meaningful keys: a key like
prod-apiis easier to review than an index like2. - Keep input variables strongly typed: use
map(object(...))orset(string)instead of loose types where possible. - Be careful when renaming keys: a key change can affect resource identity.
- Review Terraform plans closely: a small input change can create, update, or destroy multiple instances.
- Use for_each for repeatable patterns, not for hiding complex logic: if a loop makes the configuration harder to understand, the design may need to be simplified.
The Governance Angle for env zero
Terraform for_each helps teams reduce duplication, but it also increases the importance of review and governance.
One change to a map can affect many resources. One renamed key can cause replacement. One new environment entry can trigger an entire module deployment. These changes may be valid, but teams need a clear way to review them before they are applied.
That is where env zero fits into the Terraform workflow.
env zero sits around Terraform and OpenTofu to help teams manage plan-and-apply workflows, approval gates, RBAC, policy controls, drift detection, cost visibility, and standardized execution across teams and environments.
For for_each workflows, this matters because reusable infrastructure patterns often scale quickly. A platform team may define one module, but many teams may use it across many environments. Governance ensures those changes are visible, reviewed, and controlled.
Terraform defines the infrastructure pattern. for_each helps repeat that pattern safely. env zero helps teams govern how those repeated changes move through the organization.
Summary
Terraform for_each is one of the most useful features for teams that want to reduce duplication in infrastructure as code.
It allows teams to create multiple resources or module instances from maps and sets, while giving each instance a stable and meaningful identity. Compared with count, for_each is often safer and easier to review when each instance has different values.
The best use cases include repeated cloud resources, environment-based modules, IAM users, security rules, networking components, and standardized platform modules.
The key is to use for_each carefully. Choose stable keys, use clear input types, avoid unnecessary complexity, and always review the plan before applying changes.
For infrastructure teams scaling Terraform in 2026, for_each is more than a loop. It is a practical way to make Terraform configurations cleaner, more reusable, and easier to govern across teams.
Once you’re ready to put this into practice, the Terraform for_each Patterns lays out the full structured model.
.webp)