Terraform for_each
Learn how Terraform for_each works with maps, sets, and transformed collections, how to reference named instances, and when for_each is better than count.
What Is the for_each Meta-Argument?
The for_each meta-argument lets Terraform create multiple instances from one block by iterating over a map or a set of strings. Like count, it reduces repetition. Unlike count, it identifies each instance by a key rather than a numeric position.
That difference is what makes for_each so valuable for real infrastructure.
Here is a simple example:
resource "aws_s3_bucket" "logs" {
for_each = toset(["dev", "staging", "prod"])
bucket = "myapp-${each.key}-logs"
}
Terraform creates three bucket instances, but instead of naming them by index, it names them by key:
aws_s3_bucket.logs["dev"]aws_s3_bucket.logs["staging"]aws_s3_bucket.logs["prod"]
This is usually easier to read and safer to maintain than numeric indexing.
Why for_each Exists
If count already creates multiple resources, why does Terraform also need for_each?
Because many infrastructure collections are not really just "three copies." They are named things:
- environments like
dev,stage, andprod - applications like
api,worker, andfrontend - buckets with specific names
- IAM users for distinct teams
- subnets with different roles
When the objects have their own identity, a name or key is a better model than a position in a list.
That is the core benefit of for_each: it gives each instance a stable logical identity.
Basic for_each Syntax
A for_each block looks like this:
resource "TYPE" "NAME" {
for_each = SOME_MAP_OR_SET
# use each.key and maybe each.value
}
Terraform creates one instance per element in the collection.
Inside the block, Terraform exposes an each object:
each.keygives the current keyeach.valuegives the current value
For a set of strings, each.key and each.value are effectively the same string.
For a map, each.key is the map key and each.value is the mapped value.
for_each with a Set of Strings
One of the easiest ways to start with for_each is to iterate over a set of strings.
Example:
variable "environments" {
type = list(string)
default = ["dev", "staging", "prod"]
}
resource "aws_s3_bucket" "artifacts" {
for_each = toset(var.environments)
bucket = "myapp-${each.key}-artifacts"
tags = {
Environment = each.key
}
}
Why toset() is used
for_each does not accept a plain list directly for this pattern. A common beginner approach is to convert a list of strings to a set using toset().
for_each = toset(var.environments)
A set means:
- duplicate values are removed
- order is not important
- each string becomes the identity key
That matches the for_each model well.
Resulting references
Terraform creates instances like:
aws_s3_bucket.artifacts["dev"]aws_s3_bucket.artifacts["staging"]aws_s3_bucket.artifacts["prod"]
Those references are much more meaningful than [0], [1], and [2].
for_each with a Map
Maps are where for_each becomes even more useful. A map lets each instance carry both a key and a value.
Example:
variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.medium"
}
}
resource "aws_instance" "app" {
for_each = var.instance_types
ami = data.aws_ami.ubuntu.id
instance_type = each.value
tags = {
Name = "app-${each.key}"
Environment = each.key
}
}
Here:
each.keyisdev,staging, orprodeach.valueis the instance type for that environment
This makes configuration expressive and self-documenting.
Why maps are powerful
Maps let you attach configuration to identity. Instead of keeping separate lists aligned by position, you can say:
- key: the stable name of the instance
- value: the configuration for that instance
That is usually easier to reason about and safer when the collection changes.
Using each.key and each.value
Understanding each.key and each.value is central to mastering for_each.
each.key
Use each.key when you need the current item name.
Typical uses:
- resource naming
- tagging
- outputs
- conditional logic based on the item key
Example:
bucket = "company-${each.key}-logs"
each.value
Use each.value when the collection contains per-item configuration.
Example:
instance_type = each.value
Or, with a map of objects:
subnet_id = each.value.subnet_id
Together, each.key and each.value let one resource block describe many distinct instances cleanly.
for_each with a Map of Objects
A very common real-world pattern is iterating over a map of objects.
Example:
variable "servers" {
type = map(object({
instance_type = string
subnet_id = string
}))
default = {
api = {
instance_type = "t3.small"
subnet_id = "subnet-123"
}
worker = {
instance_type = "t3.micro"
subnet_id = "subnet-456"
}
}
}
resource "aws_instance" "server" {
for_each = var.servers
ami = data.aws_ami.ubuntu.id
instance_type = each.value.instance_type
subnet_id = each.value.subnet_id
tags = {
Name = each.key
}
}
This pattern is powerful because each instance can have structured settings while still keeping a stable name.
for_each with a List of Objects
Sometimes your input starts as a list of objects rather than a map. Terraform can still use for_each, but you first transform the list into a map so each object gets a unique key.
This is a common pattern:
variable "buckets" {
type = list(object({
name = string
acl = string
}))
}
resource "aws_s3_bucket" "buckets" {
for_each = { for item in var.buckets : item.name => item }
bucket = each.key
acl = each.value.acl
}
The expression:
{ for item in var.buckets : item.name => item }
means:
- iterate through the list
- use
item.nameas the map key - store the full object as the map value
That converts a list into the exact structure for_each wants.
Why this transformation matters
Lists alone do not provide stable identity when order changes. By transforming the list into a map keyed by item.name, you tell Terraform that the name is the durable identity.
That is much safer than relying on list position.
Referencing for_each Resources
Resources created with for_each are referenced by key.
Example:
aws_s3_bucket.buckets["prod"].arn
This is one of the biggest advantages of for_each. The reference tells you exactly which instance is being used.
Output example
output "prod_bucket_arn" {
value = aws_s3_bucket.buckets["prod"].arn
}
Getting values from all instances
You can also build expressions from all instances using for expressions.
Example:
output "bucket_arns" {
value = {
for name, bucket in aws_s3_bucket.buckets :
name => bucket.arn
}
}
That produces a map from bucket name to ARN.
Dynamic Resource Naming with each.key
A very common beginner use of for_each is dynamic naming.
resource "aws_s3_bucket" "logs" {
for_each = toset(["dev", "staging", "prod"])
bucket = "myapp-${each.key}-logs"
tags = {
Name = "myapp-${each.key}-logs"
Environment = each.key
}
}
Here, one short block becomes three clearly named resources.
This keeps names:
- predictable
- consistent
- easy to understand in both Terraform and AWS
for_each with Modules
Like count, for_each can also be used with modules.
module "environment_app" {
for_each = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.medium"
}
source = "./modules/app"
environment = each.key
instance_type = each.value
}
Terraform creates:
module.environment_app["dev"]module.environment_app["staging"]module.environment_app["prod"]
This is often a clean way to instantiate the same child module for multiple environments or services.
Why for_each Is Safer Than count for Named Items
The biggest practical benefit of for_each is stable identity.
Suppose you use:
for_each = toset(["app", "api", "worker"])
Terraform tracks instances by those names. If you remove api, Terraform removes only the api instance. The app and worker instances keep their identity.
With count, the same collection might be tied to indexes [0], [1], and [2]. Removing the middle item can shift indexes and cause replacement of later resources.
That makes for_each a much better choice for long-lived named infrastructure.
for_each vs count
Here is the most important comparison.
| Topic | count | for_each |
|---|---|---|
| Collection style | Number or list-position pattern | Map or set-based pattern |
| Instance identity | Numeric index | Stable key |
| Best for | Simple repetition | Named distinct instances |
| Reordering risk | High when list order changes | Low when keys stay stable |
| Readability | resource.name[0] | resource.name["prod"] |
| Per-item configuration | More awkward with aligned lists | Natural with maps/objects |
Quick decision guide
Use count when:
- you need N copies
- the instances are nearly identical
- optional creation with
0or1is the main goal
Use for_each when:
- each instance has a name
- the collection may change over time
- you want stable references
- each item has its own configuration
Best Practices for for_each
1. Choose keys carefully
The key becomes the identity of the instance. Pick values that are stable and meaningful, such as environment names, team names, or bucket names.
2. Prefer maps when instances have custom configuration
If each resource has its own settings, a map or map of objects is usually the clearest model.
3. Convert lists to maps intentionally
If your input starts as a list of objects, use a for expression to create a map keyed by a unique field.
4. Avoid duplicate keys
Every key in for_each must be unique. If two objects generate the same key, Terraform cannot distinguish them.
5. Keep keys stable over time
Renaming a key is effectively changing instance identity. Terraform may destroy the old instance and create a new one.
Common Beginner Mistakes
Mistake 1: Passing a plain list directly without understanding identity
for_each works naturally with maps and sets. If you start with a list, think carefully about what the stable key should be.
Mistake 2: Confusing each.key and each.value
For maps, each.key is the name and each.value is the configuration. Use the right one in the right place.
Mistake 3: Using unstable keys
If keys depend on transient data, resource identity becomes fragile.
Mistake 4: Using count when you really want names
If the collection is conceptually dev, stage, and prod, forcing it into indexes is usually a sign that for_each is the better fit.
Practical Example: Per-Environment Buckets
Let us build a clean, realistic example.
variable "bucket_configs" {
type = map(object({
versioning = bool
}))
default = {
dev = {
versioning = false
}
staging = {
versioning = true
}
prod = {
versioning = true
}
}
}
resource "aws_s3_bucket" "logs" {
for_each = var.bucket_configs
bucket = "myapp-${each.key}-logs"
tags = {
Environment = each.key
}
}
resource "aws_s3_bucket_versioning" "logs" {
for_each = var.bucket_configs
bucket = aws_s3_bucket.logs[each.key].id
versioning_configuration {
status = each.value.versioning ? "Enabled" : "Suspended"
}
}
This example shows several good habits:
- stable keys by environment name
- custom per-environment settings via
each.value - readable references like
aws_s3_bucket.logs[each.key].id
That is the kind of structure that scales much better than index-based repetition.
Final Thoughts
Terraform for_each is one of the best tools for building maintainable infrastructure when you have a collection of named items. It reduces copy-paste like count, but it also improves clarity by giving each instance a meaningful key.
The most important ideas to keep in mind are:
for_eachiterates over maps or setseach.keyidentifies the current itemeach.valuecarries the current item's data- references use named keys such as
resource["prod"] - stable keys make infrastructure changes safer over time
If count answers, "How many copies do I need?" then for_each answers, "Which named things do I need?" For most long-lived, real-world collections, that second model is often the better one.
Knowledge Check
Question 1: Stable Identity
What is one major advantage of for_each over count?
Question 2: each.key and each.value
In a resource that uses for_each with a map, what does each.value usually represent?
Question 3: Referencing Instances
How do you reference the ARN of a for_each-created S3 bucket instance keyed by prod?