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
countis 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_namesis aset(string), ensuring unique names. for_each = var.bucket_namestells Terraform to create anaws_s3_bucketinstance for each string in thebucket_namesset.- Inside the
aws_s3_bucketblock,each.valuegives us the current bucket name (“my-app-logs-bucket-prod”, “my-app-data-bucket-prod”, etc.). each.keyis identical toeach.valuewhen 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_configsis amap(string)where keys are instance names and values are instance types.for_each = var.instance_configscreates anaws_instancefor each entry.each.keyprovides the instance name (e.g., “web-server-01”), used for theNametag.each.valueprovides 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_configurationsis 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_hostnamesallow direct access to the nested attributes of each object.- The
mergefunction is used to combine thetagsdefined in the variable with a dynamically generatedNametag, showing howeach.keycan 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_eachto iterate over it.- Again,
each.keyandeach.valueare 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
forexpression{ 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
sitself (the entire object for that server).
- The key is
for_eachthen iterates over this newly created map.each.keygives you the server name, andeach.valuegives 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.networksresource usesfor_eachto create multiple VPCs. - The
aws_subnet.subnetsresource uses a complexforexpression withinfor_eachto 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_idcorrectly references the ID of the parent VPC created by theaws_vpc.networksresource, 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_eachcreates individualaws_instanceresources. - The
dynamic "ebs_block_device"block then uses its ownfor_eachto iterate over thevolumeslist within the current instance’s configuration. - Inside the
dynamicblock,ebs_block_device.value(whereebs_block_deviceis the implicit iterator name) refers to the current volume object, allowing access todevice_nameandvolume_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_eachmust be known before Terraform performs any remote resource actions (i.e., during theterraform planphase). You cannot use outputs from resources that have not been created yet as thefor_eachinput.- 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_eachcannot accept sensitive values directly. This is because the values used infor_eachare used to identify resource instances and will always be disclosed in UI output and state. - List vs. Set: Remember
for_eachrequires a map or set. Lists must be converted usingtoset()orforexpressions to create a map.
Best Practices for Using for_each
- Prefer
for_eachovercountfor Non-Fungible Resources: If resources have unique characteristics, identities, or lifecycles,for_eachprovides 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_eachare well-defined with appropriate types (map(string),map(object),set(string)) and include descriptions. - Keep
for_eachExpressions Simple: Whileforexpressions can be powerful, keep the logic used to generate thefor_eachmap or set as straightforward as possible to improve readability. Complex transformations can be handled inlocalsblocks first. - Modularize: When dynamic blocks or complex
for_eachscenarios 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_eachlogic within a single resource, consider if you can flatten the data structure or break it into multiple resources or modules for clarity. - Review
terraform planOutput Carefully: Always inspect theterraform planoutput when usingfor_eachto 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