Use for_each over count for flexibility
Learn when and how to use for_each instead of count in Terraform for more flexible and maintainable infrastructure. This lesson explains the core concept, provides a hands-on walkthrough, and compares options to help you choose the right approach.
Focus: use for_each over count for flexibility
Stuck managing a list of similar resources with count? You've probably hit the wall: change the order of your list and Terraform wants to destroy and recreate everything. Or you need to remove one item from the middle and watch your state file get torn apart. This is the classic count pain point. In this lesson, you'll learn why using for_each is the more flexible and maintainable choice for most scenarios, and you'll see exactly how to make the switch.
The problem this lesson solves
When you use count to create multiple resources, Terraform treats them as an ordered list. This seems simple at first, but it creates a hidden dependency on the order of your input. Let's say you have a variable that's a list of subnets:
variable "subnets" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
resource "aws_subnet" "main" {
count = length(var.subnets)
cidr_block = var.subnets[count.index]
vpc_id = aws_vpc.main.id
}
Now imagine you need to reorder that list — maybe you want "10.0.2.0/24" first for some reason. Because count uses index numbers, Terraform now thinks the subnet at index 0 (which was 10.0.1.0/24) is the same resource as the one now at index 0 (10.0.2.0/24). The subnet's CIDR block would be updated in place, but if anything else depends on those subnet IDs, you could get unexpected updates or even destruction. Worse, if you remove the second item, the subnet at index 0 and index 1 get renumbered, causing Terraform to plan a destroy and recreate of resources that didn't actually change.
This problem only gets worse as your infrastructure grows. Managing a handful of resources with count is fine, but for anything beyond a static, ordered list, count becomes a maintenance nightmare.
Core concept / mental model
Think of count as numbered parking spots — each spot is identified by its position in the row. If you move a car (resource) to a different spot, it's a different spot, and the old spot might get reused. In contrast, for_each is like named parking spots — each spot has a unique name, like "Spot-A" or "Spot-B" — and moving things around doesn't change which spot is which.
In Terraform terms:
countcreates resources indexed by an integer:[0, 1, 2, ...]for_eachcreates resources keyed by a unique string or set:{ "key1" = ..., "key2" = ... }
These keys are the identity of each resource. With for_each, Terraform tracks each resource by its key, so even if you reorder or remove items from your input map or set, Terraform only touches the affected resources.
This mental model is crucial: the key is the identity. When you use for_each, you are telling Terraform, "This resource is named 'subnet-a' and should be managed as such, regardless of its position in a list."
How it works step by step
Here's how you transition from count to for_each:
- Change your input from a list to a map or set.
for_eachaccepts a map or a set of strings. If you have a list, you can convert it to a set withtoset()or to a map with{ for idx, val in list : val => val }— but prefer a map for clarity. - Replace
count = length(...)withfor_each = var.subnets. - Replace
count.indexwitheach.key(andeach.value). In a map,each.keyis the map key, andeach.valueis the value. In a set,each.keyandeach.valueare the same. - Reference resources using the key, not the index. Instead of
aws_subnet.main[0].id, you useaws_subnet.main["10.0.1.0/24"].idorvalues(aws_subnet.main)[*].id. - Update any references in your configuration. This includes outputs, other resources, and module calls.
Let's see this in action.
Hands-on walkthrough
Let's rewrite the subnet example with for_each. We'll use a map to give each subnet a meaningful name.
variable "subnets" {
type = map(string)
default = {
"subnet-a" = "10.0.1.0/24"
"subnet-b" = "10.0.2.0/24"
"subnet-c" = "10.0.3.0/24"
}
}
resource "aws_subnet" "main" {
for_each = var.subnets
cidr_block = each.value
vpc_id = aws_vpc.main.id
tags = {
Name = each.key
}
}
Notice how we use each.key for the tag — that's the flexible part. If you later change the map's keys, Terraform will rename the tags appropriately, but it won't destroy and recreate subnets unless the value (CIDR block) changes.
Now, let's see how to reference these resources in outputs:
output "subnet_ids" {
value = { for k, v in aws_subnet.main : k => v.id }
}
output "all_subnet_ids" {
value = values(aws_subnet.main)[*].id
}
The first output gives you a map of key-to-ID, which is super handy for passing to other modules or for use in loops. The second gives you a list of all IDs, regardless of key.
Let's also show how to conditionally create resources with for_each using a set — a very common pattern:
variable "enable_metrics" {
type = bool
default = true
}
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
for_each = var.enable_metrics ? toset(["cpu"]) : toset([])
alarm_name = "high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors ec2 cpu utilization"
}
Here, if enable_metrics is true, the for_each evaluates to a set with one key "cpu", creating the alarm. If false, it's an empty set, creating nothing. This pattern is much cleaner than the count = var.enable ? 1 : 0 trick and avoids the [0] indexing.
Pro tip: When you use
for_eachwith a set, remember thateach.keyandeach.valueare the same. This can be confusing initially, so keep that in mind.
Compare options / when to choose what
| Scenario | count |
for_each |
|---|---|---|
| Static, ordered list of identical resources (e.g., three identical VMs) | ✅ Clean and idiomatic | ✅ Also works but adds key complexity |
| Multiple resources with distinct attributes (e.g., subnets with different CIDRs) | ❌ Fragile — order matters | ✅ Natural — key is the identity |
| Removing or reordering items | ❌ Destroys/recreates | ✅ Only touches affected resources |
| Conditional creation based on a bool | ✅ count = var.enable ? 1 : 0 but needs [0] |
✅ toset() trick works cleanly |
Passing arguments to modules (module for_each) |
✅ Works, but module instances are indexed | ✅ Better — instances keyed by name |
When to choose count:
- You have a simple list of identical resources and you don't expect to reorder or remove items.
- You need to reference resources by index (e.g.,
[0]for the first) and that's semantically meaningful. - You're dealing with a list that is genuinely ordered and positional (like nodes in a cluster, where index 0 is the primary).
When to choose for_each:
- You have a map or set of resources that are identified by a name or key.
- You want to reorder or remove items without causing churn.
- You need conditional creation in a clean way.
- You need to pass per-resource configuration easily.
In general, for_each is the more flexible choice and is often recommended by the Terraform community for anything beyond simple count scenarios.
Troubleshooting & edge cases
Here are common pitfalls and how to fix them:
- Error: "Invalid for_each argument" — The
for_eachvalue must be a map or a set of strings. If you pass a list, wrap it withtoset(), or convert it to a map with{ for idx, val in list : val => val }. - Error: "Missing resource instance key" — You're referencing a resource with
for_eachwithout a key, likeaws_subnet.main.id. You need to useaws_subnet.main["subnet-a"].idorvalues(aws_subnet.main)[*].id. - Sensitive values in
for_each— If your map keys are sensitive, Terraform may warn about the key being exposed in state. Consider using a non-sensitive key (likesubnet-a) and keep the sensitive value in the value. - For_each with resources that have
nameortagsthat must be unique — If you change a key, Terraform might create a new resource with a new physical name, and the old one may remain. Make sure to plan carefully. - Complex values in sets —
for_eachwith a set of complex objects is not allowed; you must use a set of strings. Convert your object list to a set of IDs first. - Empty
for_each— If your map or set is empty, Terraform creates zero resources. That's usually what you want, but check for unintended consequences in references that expect at least one instance.
What you learned & what's next
In this lesson, you learned how to use for_each over count for flexibility in Terraform. You now understand the core concept: for_each uses keys to identify resources, making your infrastructure more resilient to changes. You completed a hands-on exercise that transformed a count-based resource into a for_each-based one, and you saw how to reference those resources cleanly.
With this skill, you can confidently manage sets of related resources — subnets, instances, IAM policies, and more — without fearing reordering or deletion. This is a critical step in writing clean, maintainable Terraform.
Your next lesson in the Terraform foundations path will likely dive into modules and how to pass complex inputs — a perfect companion to for_each, since you can use for_each on module blocks too. Keep this lesson in mind; it'll pay off big time when you start building reusable infrastructure components.
Happy Terraforming!
Practice recap
To solidify this lesson, take a Terraform configuration you've written with count and convert it to for_each. Start by changing the variable from a list to a map, then update the resource block and any references. Run terraform plan and observe how the output changes — you should see that reordering the map keys no longer triggers destructive changes. This hands-on practice will make the concept stick.
Common mistakes
- Using
countwith a list and then reordering the list causes Terraform to destroy and recreate resources, even if nothing changed — switch tofor_eachto avoid this. - Forgetting to wrap a list in
toset()when usingfor_each—for_eachrequires a map or a set of strings, not a list. - Referencing a
for_eachresource without a key, likeaws_subnet.main.id— you needaws_subnet.main["subnet-a"].idorvalues(aws_subnet.main)[*].id. - Using
countfor conditional creation and then referencing[0]— this breaks if the condition isfalse; prefer thetoset([])trick.
Variations
- Use
for_eachdirectly on module blocks to create multiple module instances, each with its own key. - Use
dynamic "block"withfor_eachinside a resource to create nested blocks (e.g., multipleingressrules in a security group). - Combine
for_eachwithflatten()to create resources from a nested structure, like multiple environments and services.
Real-world use cases
- Managing a set of subnets in a VPC where each subnet has a unique CIDR and tag, and administrators may add or remove subnets without affecting others.
- Creating IAM policies for multiple services from a map of service name to JSON policy document, allowing independent updates.
- Rolling out EC2 instances for different environments (dev, staging, prod) using a map keyed by environment, enabling per-environment configuration.
Key takeaways
for_eachuses keys to identify resources, providing flexibility and stability compared tocount's index-based approach.- Use
for_eachwhen you have a map or set of resources that are semantically named or need to be reordered/removed independently. - Always reference
for_eachresources with their key (resource.name[key]) or usevalues()to get all instances. - Favor
for_eachfor conditional creation using thetoset()trick overcountwith[0]indexing. - Choose
countonly for static, positional lists of identical resources where index-based access is meaningful.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.