DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Terraform Tutorial

Introduction to Terraform
Installing Terraform
The Terraform Core Workflow
Terraform Variables and Outputs
Terraform State and Backends
Terraform Modules
Terraform Data Sources
Terraform Count and for_each
Terraform Expressions and Functions
Terraform Dynamic Blocks
Terraform Workspaces
Terraform with AWS
Terraform Provisioners
Terraform CI/CD Pipeline
Terraform Security

Terraform for_each

PreviousPrev
Next

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, and prod
  • applications like api, worker, and frontend
  • 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.key gives the current key
  • each.value gives 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.key is dev, staging, or prod
  • each.value is 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.name as 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.

Topiccountfor_each
Collection styleNumber or list-position patternMap or set-based pattern
Instance identityNumeric indexStable key
Best forSimple repetitionNamed distinct instances
Reordering riskHigh when list order changesLow when keys stay stable
Readabilityresource.name[0]resource.name["prod"]
Per-item configurationMore awkward with aligned listsNatural with maps/objects

Quick decision guide

Use count when:

  • you need N copies
  • the instances are nearly identical
  • optional creation with 0 or 1 is 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_each iterates over maps or sets
  • each.key identifies the current item
  • each.value carries 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

Exercise

Question 1: Stable Identity

What is one major advantage of for_each over count?

Exercise

Question 2: each.key and each.value

In a resource that uses for_each with a map, what does each.value usually represent?

Exercise

Question 3: Referencing Instances

How do you reference the ARN of a for_each-created S3 bucket instance keyed by prod?

PreviousPrev
Next

Continue Learning

Terraform Data Sources

Learn how Terraform data sources read existing infrastructure, how data blocks work, how outputs are referenced, and when to use data sources instead of resources.

12 min read·Easy

Terraform Count and for_each

Get a quick overview of Terraform count and for_each so you can choose the right repetition pattern before learning each meta-argument in detail.

5 min read·Easy

Terraform Count

Learn how Terraform count works, how count.index is used, how to create conditional resources, how to reference counted resources, and when count is a good fit.

10 min read·Easy

Explore Related Topics

AW

AWS Tutorials

Provision AWS infrastructure with Terraform

CI

CI/CD Tutorials

Run terraform plan/apply in CI pipelines

Try the Tool

YAML Validator

Validate Terraform and Kubernetes YAML configs before applying.

On This Page

What Is the `for_each` Meta-Argument?Why `for_each` ExistsBasic `for_each` Syntax`for_each` with a Set of StringsWhy `toset()` is usedResulting references`for_each` with a MapWhy maps are powerfulUsing `each.key` and `each.value``each.key``each.value``for_each` with a Map of Objects`for_each` with a List of ObjectsWhy this transformation mattersReferencing `for_each` ResourcesOutput exampleGetting values from all instancesDynamic Resource Naming with `each.key``for_each` with ModulesWhy `for_each` Is Safer Than `count` for Named Items`for_each` vs `count`Quick decision guideBest Practices for `for_each`1. Choose keys carefully2. Prefer maps when instances have custom configuration3. Convert lists to maps intentionally4. Avoid duplicate keys5. Keep keys stable over timeCommon Beginner MistakesMistake 1: Passing a plain list directly without understanding identityMistake 2: Confusing `each.key` and `each.value`Mistake 3: Using unstable keysMistake 4: Using `count` when you really want namesPractical Example: Per-Environment BucketsFinal ThoughtsKnowledge Check