In today’s blog post, will cover terraform for_each
meta argument. In particular, we will explain why we need a for_each
construct, how to use for_each
meta argument with examples, and what is the difference between a for_each
and count
meta argument.
Introduction: Why for_each?
Imagine you need to provision 10 identical EC2 instances. You could write 10 separate aws_instance
blocks, but that is repetitive, error-prone, and hard to maintain. Now imagine those 10 instances are not identical – they need different names, different instance types, or different tags. Manually managing this would be a nightmare.
Historically, Terraform provided the count
meta-argument for simple numerical repetition. While useful for creating fungible (interchangeable) resources, it fell short when each resource needed a unique identity or configuration.
Enter for_each
. Introduced in Terraform 0.12.6, for_each
revolutionized dynamic resource provisioning by allowing you to iterate over a collection of items (a map or a set of strings) and create a distinct instance of a resource, data source, or module for each item. This provides more flexibility and significantly reduces code duplication, making your configurations more scalable and maintainable.
What is the for_each Meta-Argument?
The for_each
meta-argument is a special argument that can be added to any resource
, data
, or module
block. Its primary function is to create multiple instances of that block, where each instance is uniquely identified by a key from the collection you provide.
When you use for_each
, Terraform iterates over the elements of the supplied map or set of strings. And for each element, it creates a separate instance of the block, and importantly, each instance gets a distinct infrastructure object associated with it. This means you can manage each created resource independently.
Inside a block using for_each
, you gain access to a special object called each
. This object provides two attributes:
each.key
: Which represents the map key or the set member corresponding to the current iteration.each.value
: Which represents the map value or (if a set was provided) the same aseach.key
.
for_each vs. count: The Key Differences and When to Use Each
This is one of the most sought after question in any terraform interview and sadly most of the terraform developers though have used them both in their code but can not explain the difference clearly. While both for_each
and count
enable creating multiple resources, their underlying mechanisms and ideal use cases differ significantly.
count
Meta-Argument:
- Mechanism: Iterates a specified number of times (from “0” to “
count - 1
“). - Addressing: Resources are addressed by a numerical index (e.g.,
aws_instance.my_server[0]
,aws_instance.my_server[1]
). - Stability: Less stable. If you remove an item from the middle of the list that
count
is based on, all subsequent resources’ indices will shift. Terraform might interpret this as needing to destroy and recreate resources, even if their underlying configuration has not changed. This can lead to unnecessary downtime or data loss. - Use Cases:
- Creating a fixed, small number of truly identical resources where no unique identifier is needed.
- Conditional resource creation (e.g.,
count = var.enable_resource ? 1 : 0
).
for_each
Meta-Argument:
- Mechanism: Iterates over the elements of a map or a set of strings.
- Addressing: Resources are addressed by the map key or set member (e.g.,
aws_instance.my_server["web"]
,aws_instance.my_server["db"]
). - Stability: More stable. If you add or remove an item, only the corresponding resource is affected. The existing resources identified by other keys remain unchanged, preventing unnecessary recreation.
- Use Cases:
- Creating resources that require unique identifiers (e.g., names, specific configurations).
- Provisioning resources based on dynamic input collections where each item has unique attributes.
- Managing environments (dev, staging, prod) where each environment needs a set of distinct resources.
TL;DR: Prefer for_each
when your resources are not fungible and require unique identification. Use count
for simple, numerical repetitions or conditional provisioning of a single resource.
How to Use for_each with Different Data Types (Examples)
for_each
expects a map or a set of strings as its input. Let us look at practical examples.
Using for_each with a Set of Strings
When for_each
is given a set of strings, both each.key
and each.value
will represent the current string in the iteration. This is useful for creating resources where the unique identifier is simply the name or a simple string.
Scenario: Create multiple S3 buckets, each with a unique name.
# variables.tf
variable "bucket_names" {
description = "A set of unique S3 bucket names."
type = set(string)
default = [
"my-app-logs-bucket-prod",
"my-app-data-bucket-prod",
"my-app-backup-bucket-prod"
]
}
# main.tf
resource "aws_s3_bucket" "app_buckets" {
for_each = var.bucket_names
bucket = each.value
# The bucket name is the value from the set
acl = "private"
tags = {
Name = each.key
# The tag name is also the key from the set
Environment = "production"
}
}
Explanation:
- The
var.bucket_names
is aset(string)
, ensuring unique names. for_each = var.bucket_names
tells Terraform to create anaws_s3_bucket
instance for each string in thebucket_names
set.- Inside the
aws_s3_bucket
block,each.value
gives us the current bucket name (“my-app-logs-bucket-prod”, “my-app-data-bucket-prod”, etc.). each.key
is identical toeach.value
when iterating over a set of strings.
Using for_each with a Map of Strings/Objects
Using a map is for_each
‘s most powerful application, as it allows you to associate distinct configurations with each resource instance. each.key
will be the map key, and each.value
will be the corresponding map value.
Scenario 1: Map of Strings – Creating EC2 Instances with Different Types
# variables.tf
variable "instance_configs" {
description = "Map of EC2 instance names to their instance types."
type = map(string)
default = {
"web-server-01" = "t2.medium",
"db-server-01" = "m5.large",
"jenkins-node" = "t3.xlarge"
}
}
# main.tf
resource "aws_instance" "app_servers" {
for_each = var.instance_configs
ami = "ami-0abcdef1234567890" # Example AMI ID
instance_type = each.value
# Instance type comes from the map value (t2.medium, m5.large, t3.xlarge)
tags = {
Name = each.key
# Server name comes from the map key (web-server-01, db-server-01, jenkins-node
}
}
Explanation:
var.instance_configs
is amap(string)
where keys are instance names and values are instance types.for_each = var.instance_configs
creates anaws_instance
for each entry.each.key
provides the instance name (e.g., “web-server-01”), used for theName
tag.each.value
provides the instance type (e.g., “t2.medium”).
Scenario 2: Map of Objects – More Complex Configurations
This is incredibly flexible, allowing you to pass structured data for each resource.
# variables.tf
variable "vpc_configurations" {
description = "Configurations for different VPCs."
type = map(object({
cidr_block = string
tags = map(string)
enable_dns_hostnames = bool
}))
default = {
"development" = {
cidr_block = "10.10.0.0/16"
tags = {
Environment = "Dev"
Project = "MyApp"
}
enable_dns_hostnames = true
},
"production" = {
cidr_block = "10.20.0.0/16"
tags = {
Environment = "Prod"
Project = "MyApp"
}
enable_dns_hostnames = false
}
}
}
# main.tf
resource "aws_vpc" "my_vpcs" {
for_each = var.vpc_configurations
cidr_block = each.value.cidr_block
enable_dns_hostnames = each.value.enable_dns_hostnames
tags = merge(
each.value.tags,
{ "Name" = "VPC-${each.key}" }
# Add a dynamic Name tag
)
}
Explanation:
var.vpc_configurations
is amap(object)
where each key (e.g., “development”, “production”) maps to an object containing detailed VPC settings.each.value.cidr_block
,each.value.tags
, andeach.value.enable_dns_hostnames
allow direct access to the nested attributes of each object.- The
merge
function is used to combine thetags
defined in the variable with a dynamically generatedName
tag, showing howeach.key
can be used for naming.
Using for_each with a List (and toset() or for expressions)
for_each
strictly requires a map or a set of strings. If you have a list
, you need to transform it.
Scenario 1: Converting a List of Strings to a Set with toset()
If the order does not matter and you just need unique elements, toset()
is your friend.
# variables.tf
variable "availability_zones" {
description = "List of AZs to create subnets in."
type = list(string)
default = ["ap-south-1a", "ap-south-1b"]
}
# main.tf
resource "aws_subnet" "regional_subnets" {
for_each = toset(var.availability_zones)
# Assuming you have an aws_vpc.main resource
vpc_id = aws_vpc.main.id
# Example CIDR, might need more complex logic
cidr_block = "10.0.0.0/24"
availability_zone = each.value
tags = {
Name = "subnet-${each.key}"
}
}
Explanation:
toset(var.availability_zones)
converts the list into a set, allowingfor_each
to iterate over it.- Again,
each.key
andeach.value
are the same (the AZ string).
Scenario 2: Transforming a List of Objects into a Map with a for
Expression
This is common when you have a list of complex objects and need to create a unique key for for_each
.
# variables.tf
variable "server_specs" {
description = "List of server specifications."
type = list(object({
name = string
instance_type = string
ami = string
}))
default = [
{
name = "web-01"
instance_type = "t3.micro"
ami = "ami-0abcdef1234567890"
},
{
name = "api-01"
instance_type = "t3.small"
ami = "ami-0abcdef1234567890"
}
]
}
# main.tf
resource "aws_instance" "dynamic_servers" {
# Transform list to map
for_each = { for s in var.server_specs : s.name => s }
ami = each.value.ami
instance_type = each.value.instance_type
tags = {
Name = each.key
}
}
Explanation:
- The
for
expression{ for s in var.server_specs : s.name => s }
creates a map where:- The key is
s.name
(e.g., “web-01”, “api-01”). This must be unique across your list. - The value is
s
itself (the entire object for that server).
- The key is
for_each
then iterates over this newly created map.each.key
gives you the server name, andeach.value
gives you the entire server object, allowing access toeach.value.instance_type
,each.value.ami
, etc.
Advanced for_each Concepts and Use Cases
Referring to Instances
When resources are created with for_each
, they form a collection, and you reference individual instances using square brackets []
with the key:
output "bucket_arns" {
value = { for k, v in aws_s3_bucket.app_buckets : k => v.arn }
}
output "web_server_id" {
value = aws_instance.app_servers["web-server-01"].id
}
Chaining for_each Between Resources
You can create dependencies where one for_each
resource uses outputs from another, maintaining the instance-specific mapping.
Scenario: Create VPCs, and then subnets within each VPC, all dynamically.
# variables.tf
variable "network_configs" {
type = map(object({
vpc_cidr = string
# Map of subnet names to CIDRs within this VPC
subnet_cidrs = map(string)
}))
default = {
"app-vpc" = {
vpc_cidr = "10.0.0.0/16"
subnet_cidrs = {
"public" = "10.0.1.0/24"
"private" = "10.0.2.0/24"
}
},
"db-vpc" = {
vpc_cidr = "10.10.0.0/16"
subnet_cidrs = {
"data" = "10.10.1.0/24"
}
}
}
}
# main.tf
resource "aws_vpc" "networks" {
for_each = var.network_configs
cidr_block = each.value.vpc_cidr
tags = { Name = "VPC-${each.key}" }
}
resource "aws_subnet" "subnets" {
# This nested for_each iterates over each VPC (each.key) and then each of its subnets (subnet_name)
for_each = {
for vpc_key, vpc_config in aws_vpc.networks :
for subnet_name, subnet_cidr in vpc_config.subnet_cidrs :
"${vpc_key}-${subnet_name}" => {
vpc_id = vpc_config.id
cidr_block = subnet_cidr
name = subnet_name
}
}
vpc_id = each.value.vpc_id
cidr_block = each.value.cidr_block
tags = {
Name = "${each.value.name}-${each.key}"
# Example: "public-app-vpc-public"
}
}
Explanation:
- The
aws_vpc.networks
resource usesfor_each
to create multiple VPCs. - The
aws_subnet.subnets
resource uses a complexfor
expression withinfor_each
to flatten the nested map structure into a single map suitable for iteration. The key"vpc_key-subnet_name"
ensures uniqueness across all subnets. each.value.vpc_id
correctly references the ID of the parent VPC created by theaws_vpc.networks
resource, maintaining the relationship.
Dynamic Blocks with for_each
dynamic
blocks allow you to generate nested configuration blocks within a resource based on a collection. for_each
is often used here.
Scenario: Create an EC2 instance with multiple, dynamically configured EBS volumes.
# variables.tf
variable "instance_with_volumes" {
type = map(object({
instance_type = string
volumes = list(object({
device_name = string
volume_size = number
}))
}))
default = {
"web-instance" = {
instance_type = "t3.medium"
volumes = [
{ device_name = "/dev/sdb", volume_size = 50 },
{ device_name = "/dev/sdc", volume_size = 100 }
]
}
}
}
# main.tf
resource "aws_instance" "server_with_disks" {
for_each = var.instance_with_volumes
ami = "ami-0abcdef1234567890" # Example AMI ID
instance_type = each.value.instance_type
tags = {
Name = each.key
}
dynamic "ebs_block_device" {
# Iterate over the 'volumes' list for each instance
for_each = each.value.volumes
content {
device_name = ebs_block_device.value.device_name
volume_size = ebs_block_device.value.volume_size
}
}
}
Explanation:
- The outer
for_each
creates individualaws_instance
resources. - The
dynamic "ebs_block_device"
block then uses its ownfor_each
to iterate over thevolumes
list within the current instance’s configuration. - Inside the
dynamic
block,ebs_block_device.value
(whereebs_block_device
is the implicit iterator name) refers to the current volume object, allowing access todevice_name
andvolume_size
.
Limitations and Troubleshooting
While powerful, for_each
has a few limitations:
- Values Must Be Known at Plan Time: The most common hurdle. The value provided to
for_each
must be known before Terraform performs any remote resource actions (i.e., during theterraform plan
phase). You cannot use outputs from resources that have not been created yet as thefor_each
input.- Workaround: If you encounter “
The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created.
“, you might need to:- Split your deployment into multiple Terraform configurations (e.g., one to create the unknown value, another to use it).
- Use
terraform apply -target=
to apply only the dependent resources first (not ideal for CI/CD). - Rethink your architecture if possible to avoid this dependency.
- Workaround: If you encounter “
- Sensitive Values:
for_each
cannot accept sensitive values directly. This is because the values used infor_each
are used to identify resource instances and will always be disclosed in UI output and state. - List vs. Set: Remember
for_each
requires a map or set. Lists must be converted usingtoset()
orfor
expressions to create a map.
Best Practices for Using for_each
- Prefer
for_each
overcount
for Non-Fungible Resources: If resources have unique characteristics, identities, or lifecycles,for_each
provides superior stability and manageability. - Use Descriptive Map Keys: When creating a map for
for_each
, use keys that are meaningful and uniquely identify the resource instance (e.g., environment names, server roles, unique IDs). - Validate Input Variables: Ensure your input variables for
for_each
are well-defined with appropriate types (map(string)
,map(object)
,set(string)
) and include descriptions. - Keep
for_each
Expressions Simple: Whilefor
expressions can be powerful, keep the logic used to generate thefor_each
map or set as straightforward as possible to improve readability. Complex transformations can be handled inlocals
blocks first. - Modularize: When dynamic blocks or complex
for_each
scenarios become too intricate, consider extracting the logic into dedicated modules to improve organization and reusability. - Avoid Nested Loops in a Single Resource Block: If you find yourself needing deeply nested
for_each
logic within a single resource, consider if you can flatten the data structure or break it into multiple resources or modules for clarity. - Review
terraform plan
Output Carefully: Always inspect theterraform plan
output when usingfor_each
to ensure Terraform intends to create, modify, or destroy the resources as you expect, especially when making changes to the input collection.
Conclusion
The for_each
meta-argument is a key component of advanced Terraform usage. By mastering its application with various data structures, you can write more precise, flexible, and robust infrastructure code. This leads to reduced boilerplate, fewer errors, and a more maintainable and scalable infrastructure-as-code repository. Embrace for_each
to unlock the full potential of dynamic resource provisioning with Terraform!
Related Items:
provider
– https://ckdbtech.com/mastering-terraform-provider-meta-argument/depends_on
– https://ckdbtech.com/mastering-terraform-depends_on-meta-argument/lifecycle
– https://ckdbtech.com/mastering-terraform-lifecycle-meta-argument/count
– https://ckdbtech.com/mastering-terraform-count-meta-argument/
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com