
Why do we need Looping in Terraform?
When managing Infrastructure-as-code (IaC) with the Terraform CLI, one often encounters scenarios where multiple resources that are similar but not identical need to be separately created.
This could range from deploying several instances across different availability zones, setting up multiple DNS records, or managing numerous user accounts. Writing out configurations manually for each resource becomes tedious and introduces a higher chance of errors and inconsistencies.
This is where looping in Terraform comes into play. Looping constructs, like the for expression, for_each, and count meta-arguments, provide a way to generate similar resources dynamically based on a collection or count.
Meta-arguments and Expressions for Terraform Looping
Meta-arguments, in a nutshell, are unique arguments that can be defined for all Terraform resources, altering specific behaviors of resources, such as their lifecycle, how they are provisioned, and their relationship with other resources.
Expressions in Terraform are used to reference or compute values within your infrastructure configuration (like dynamic calculations, data access, and resource referencing).
These are mainly used in a Terraform resource block or a module block.
There are five different types of meta-arguments for Terraform resources, but we are going to focus on expressions and meta-arguments that help in looping for Terraform resources:
1.for_each - a meta-argument used to create multiple instances of a resource or module. As the name implies, the for_each argument takes a map or a set of strings, creating an instance for each item. It provides more flexibility than count by allowing you to use complex data structures and access each instance with a unique identifier.
2.count - a meta-argument allows you to create multiple instances of a resource based on the given count. This is useful for creating similar resources without having to duplicate configuration blocks. For example, if count=5 for an EC2 instance resource configuration, Terraform creates five of those instances in your cloud environment.
3.for - a versatile expression for iterating over and manipulating collections such as lists, sets, and maps. The for expression can be used to iterate over elements in a collection and apply a transformation to each element, optionally filtering elements based on a condition.
For example:
This creates an even_set containing only the even numbers from original_set.
for vs. for_each vs. count
Here are some key points that differentiate the for expression from the for_each and count meta arguments.
How does for_each work?
Let us take a real-world example to better understand the for_each meta-argument.
Say you have a map of instance configurations where the string value for each key is an identifier for the EC2 instance, and the value is another map containing the instance type and AMI ID.
Let’s break everything down:
variable "instances"defines a map where each element represents an EC2 instance configuration. For instance, "amzlinux" and "ubuntu" are identifiers for these configurations, each specifying an AMI ID and an instance type.resource "aws_instance" "servers"usesfor_eachto iterate over each element in thevar.instancesmap. For each element, it creates an EC2 instance with the specified AMI and instance type.each.keyin this context refers to the key in the map (e.g., "amzlinux", "ubuntu"), which we use to uniquely name each instance with theNametag.each.value.amiandeach.value.instance_typeaccess the nested values for each instance's configuration.
After successfully running the Terraform workflow (init->plan->apply), we have provisioned these two instances using for_each.

Collections for for_each
Maps
Maps are collections of key-value pairs. When using for_each with maps, each iteration gives you access to both the map key and the value of the current item. Maps are ideal when you need to associate specific attributes or configurations with unique identifiers.
Sets
Sets are collections of unique values. When iterating over a set with for_each, the value for each.key and each.value will be the same since sets do not have key-value pairs but just a list of unique values.
Sets are useful when you need to ensure uniqueness and don't require associated values.
Lists
Directly, for_each cannot iterate over lists because lists do not provide a unique key for each item.
However, you can use the tomap() or toset() function to convert a list into a set or a map, allowing for_each to iterate over it.
Practical Use Cases for for_each
1. Resource Chaining
Resource chaining involves creating dependencies between resources where the configuration of one resource depends on the output of another. This is common in infrastructure setups where certain resources must be provisioned sequentially.
Let us take a common scenario of setting up a VPC and deploying subnets within the VPC, to better understand resource chaining.
In this example, each VPC is created based on the networks map, and then a subnet is created within each VPC. The aws_subnet resource uses the ID of the aws_vpc created in the same for_each loop, demonstrating resource chaining.
2. Tagging Resources Dynamically
Dynamic tagging allows you to assign metadata to resources based on their configuration or other dynamic inputs, improving resource management, billing, and automation.
We can take an example of dynamically tagging s3 buckets using for_each:
This setup dynamically applies tags to each S3 bucket based on the tags defined in the buckets map.
3. Deploying Resources to Multiple Regions
Deploying resources across multiple regions can enhance disaster recovery and reduce latency. for_each can be used to manage such deployments efficiently.
We can deploy s3 buckets to multiple regions using for_each like this example below.
In this example, S3 buckets are created in both the us-east-1 and eu-central-1 regions, using separate provider instances for each region. The provider attribute dynamically selects the correct provider based on the region key from the regions map.
Advantages of using for_each
1. Dynamic Resource Management
for_each enables the dynamic creation, management, and destruction of resources based on collections (maps or sets). This dynamic approach allows infrastructure to adjust automatically to changes in the input data without requiring manual updates to the Terraform configuration.
For example, you can write an IaC for infrastructure provisioning to provision a dynamic number of S3 buckets based on a list of project names like the example below:
2. Conditional Resource Creation
Combined with Terraform's conditional expressions, for_each can be used to conditionally create resources based on specific criteria within the data it iterates over. This allows for more granular control over which resources are created, updated, or destroyed.
For instance, you can tailor your IaC in such a way that creates an S3 bucket only if create=true:
3. Improved Code Reusability
Instead of duplicating resource blocks for each instance of a resource, for_each allows you to define a single resource (or a module) block that can be applied to each item in a collection (like a map or set).
This approach abstracts the common configuration elements into a single, parameterized block, where the specific details for each resource instance are dynamically derived from the collection it iterates over.
For example, you can incorporate for_each with the use of modules to keep your code DRY:
Nested for_each: Working with Complex Data
Everything above assumes a flat map or set: one level of instances, one key per instance. Real infrastructure is often nested instead. A set of networks might each have their own subnets. A set of applications might each have their own environments. A single for_each block can't loop through two nested levels at once, so the nested data has to be reshaped into a flat structure first.
Say you have networks, and each network has its own subnets:
That's a reasonable way to describe the infrastructure, but a subnet resource needs one flat item per subnet, not a map of maps. This is where flatten() and a locals block come in: loop through the parent map, loop through each child map, and flatten the result into a single list.
The second block converts that flat list into a map keyed by a stable, meaningful string, which is what for_each actually needs on a resource:
That's the pattern for nested for_each in general: keep the input readable, reshape it with flatten() and a for expression, convert the result into a map with stable keys, then hand that map to for_each.
for_each vs. Dynamic Blocks: Which One Do You Need?
for_each at the resource level and a dynamic block look similar on the surface, both generate repeated configuration from a collection, but they solve different problems. Resource-level for_each creates separate, independently-managed resource instances, each with its own address and state entry. A dynamic block instead generates repeated nested blocks inside a single resource, like several ingress rules inside one security group.
The question to ask is whether the repeated item needs its own lifecycle. If each item should be creatable, updatable, or destroyable on its own, independent of the others, it belongs in a separate resource with for_each. If the repeated items only ever exist as part of one parent resource and are never managed independently, a dynamic block is the simpler fit.
Here, each ingress rule is just a block of configuration attached to one security group resource, not a resource in its own right, so a dynamic block is the right tool. Reach for it for things like security group rules or lifecycle rules; reach for resource-level for_each when each item should become its own Terraform-managed resource.
Common for_each Mistakes to Avoid
A handful of mistakes account for most of the problems teams run into with for_each. Here is what to watch for.
Renaming a key replaces the resource
Terraform uses the for_each key to track each instance's identity in state. If a key changes, Terraform sees it as a different instance entirely, not the same instance with a new name, and will plan to destroy the old one and create a new one in its place.
Choose keys based on stable, long-term identity, not a label that might get tidied up later.
for_each values must be known at plan time
Terraform needs the full for_each collection before it can perform remote actions. A value that only becomes known after a resource is created, like an attribute generated during apply, can't be used to build the for_each keys for that same plan.
Sensitive values can't be used as for_each keys
Terraform for_each keys are used to identify instances and can surface in plan output, so Terraform does not allow sensitive values there. Use a non-sensitive identifier as the key, and keep the sensitive value itself inside each.value if you need it.
Where env zero Fits In
for_each is good at eliminating repetition, but that same leverage cuts both ways: one change to a map can affect every resource built from it, and a single renamed key can trigger a wave of destroy-and-recreate operations across resources that were never meant to change. That is worth catching in review, before it reaches production, not after.
env zero sits around Terraform and OpenTofu to handle that layer: plan-and-apply workflows with approval gates, RBAC, policy checks, drift detection, and cost visibility across teams and environments. A for_each-driven module can scale to dozens of resources from a single map, and env zero is what makes sure that scale stays reviewable rather than becoming a blind spot.
Commonly Asked Questions/FAQs
Q. Can I use Terraform for_each and count for the same resource?
No, for_each and count cannot be used together within the same resource or module block. You must choose one based on your use case.
count is used to create a specified number of identical resources. for_each iterates over items in a map or set to create multiple resources with different configurations.
Q. What is the use of count.index in Terraform?
The count.index in Terraform is a built-in variable that starts with a current zero-based index of the resource created in a block where the count meta-argument is used.
It's primarily used when you're creating multiple instances of a resource with count and need to differentiate between these individual instances, with a numeric identifier.
Q. Can for_each be used with modules?
Yes, for_each can be used with Terraform modules, enabling you to create multiple instances of a module based on the items in a map or a set. When using for_each with a module, you can define a collection (map or set) containing the values you want to iterate over.
Q. Can count be used to conditionally create a resource?
Yes, count can be used to create a resource in Terraform conditionally. You can specify whether to create a resource based on a condition by leveraging the count meta-argument. For example, if the condition evaluates to true, create a single instance of the resource, and prevent the resource creation if it evaluates to false.
Q. Is it possible to migrate from count to for_each?
Yes, it is possible to migrate from count to for_each in Terraform, but the process requires careful planning and execution. You can check here for detailed information on migrating count to for_each.
Q. Can for_each loop through nested objects directly?
Not in a single step. Terraform needs the for_each value to be a flat map or set. For nested data, such as networks with subnets, the common approach is to flatten the nested structure into a flat list with flatten(), then convert that list into a map with stable keys before handing it to for_each.
Q. Should I use a dynamic block or nested for_each?
Use a dynamic block when the repeated item only exists as configuration inside one resource, like security group rules. Use nested for_each (flattened into a map) when each item should become its own independently managed resource with its own lifecycle.







.avif)

