Terraform for_each vs count
If you’ve ever added a VM to a Terraform deployment only to discover Terraform wants to destroy perfectly healthy resources, you’ve probably met the downside of count.
It usually starts innocently enough. You have a handful of storage accounts, virtual networks or resource groups, so you use count because it’s quick and easy. Then someone asks you to remove the second item from the list. Suddenly Terraform decides resource number three should become resource number two, number four should become number three, and before you know it your plan looks more like a demolition schedule than a routine update.
I’ve wasted a good afternoon trying to work out why Terraform wanted to replace resources that hadn’t actually changed. The answer almost always came back to how those resources were identified.
This is one of those Terraform concepts that looks simple until you’re maintaining infrastructure over several years instead of several days.
In this article I’ll explain exactly how count and for_each work, when each one makes sense, where they regularly catch engineers out, and how I approach the decision when designing Azure Landing Zone deployments.
Why this matters more than you think
At first glance both options appear to solve exactly the same problem.
They both allow you to create multiple resources without copying the same block over and over again.
The difference isn’t how many resources they create.
The difference is how Terraform tracks them over time.
That’s a surprisingly important distinction.
In enterprise Azure environments resources rarely stay static. Teams add subscriptions. Applications appear. Business units disappear. Landing Zones evolve.
Your Terraform configuration needs to evolve with them without recreating infrastructure every few weeks.
That is where choosing the right iteration method becomes important.
High-Level Architecture
Variables
│
▼
Terraform Configuration
│
┌────────┴────────┐
│ │
count for_each
│ │
Index Number Named Key
(0,1,2,3...) ("dev","prod")
│ │
▼ ▼
Terraform State Terraform State
│ │
└────────┬────────┘
▼
Azure Resources
Understanding count
count is based entirely on numbers.
Terraform simply creates resources from zero upwards.
variable "resource_groups" {
default = [
"rg-dev",
"rg-test",
"rg-prod"
]
}
resource "azurerm_resource_group" "rg" {
count = length(var.resource_groups)
name = var.resource_groups[count.index]
location = "UK South"
}
Terraform creates:
| Index | Resource |
|---|---|
| 0 | rg-dev |
| 1 | rg-test |
| 2 | rg-prod |
Nice and straightforward.
Until someone removes rg-test.
Terraform now sees:
| Index | Resource |
|---|---|
| 0 | rg-dev |
| 1 | rg-prod |
Unfortunately rg-prod used to be index 2.
Terraform doesn’t know you’ve simply removed one item.
It thinks index 1 has changed, so it plans to destroy one resource and recreate another.
If those resources contain stateful services, you’ve just turned a five-minute change into an uncomfortable CAB meeting.
Understanding for_each
for_each approaches the problem differently.
Instead of numbering resources, Terraform gives each one a permanent key.
variable "resource_groups" {
default = {
dev = "rg-dev"
test = "rg-test"
prod = "rg-prod"
}
}
resource "azurerm_resource_group" "rg" {
for_each = var.resource_groups
name = each.value
location = "UK South"
}
Terraform now creates:
| Key | Resource |
|---|---|
| dev | rg-dev |
| test | rg-test |
| prod | rg-prod |
If you remove test, only that single resource changes.
The others remain exactly where they are because their identity hasn’t changed.
That’s why for_each is generally the better choice for long-lived Azure environments.
I’ve found this especially valuable when deploying management groups, subscriptions, networking, RBAC assignments and policy initiatives. These are resources you really don’t want Terraform trying to shuffle around because someone deleted item number two from a list.
Choosing the right option
There’s a temptation online to say “always use for_each.”
I don’t think that’s quite right.
Both have legitimate uses.
| Use Case | count | for_each |
|---|---|---|
| Enable or disable one resource | ✅ | ❌ |
| Fixed identical resources | ✅ | ✅ |
| Named infrastructure | ❌ | ✅ |
| Enterprise Azure deployments | ❌ | ✅ |
| Long-term maintainability | ❌ | ✅ |
A classic use for count is conditional deployment.
variable "enable_bastion" {
default = true
}
resource "azurerm_public_ip" "bastion" {
count = var.enable_bastion ? 1 : 0
name = "pip-bastion"
resource_group_name = azurerm_resource_group.network.name
location = azurerm_resource_group.network.location
allocation_method = "Static"
sku = "Standard"
}
That’s simple.
You’re either creating one resource or none.
No indexing problems because there is only ever one object.
Once you’re managing collections of resources with meaningful identities, I switch to for_each almost automatically.
Real-world Azure example
Imagine you’re deploying resource groups for multiple application teams.
Instead of writing this:
variable "teams" {
default = [
"Finance",
"HR",
"Sales"
]
}
I’d usually define something closer to this:
variable "teams" {
default = {
finance = {
location = "UK South"
}
hr = {
location = "UK West"
}
sales = {
location = "North Europe"
}
}
}
resource "azurerm_resource_group" "team" {
for_each = var.teams
name = "rg-${each.key}"
location = each.value.location
}
Now every resource has a permanent identity.
Adding Marketing next month doesn’t affect Finance.
Removing HR doesn’t recreate Sales.
Your Terraform state remains predictable, which is exactly what you want when several engineers contribute to the same repository.
Before vs After
Using count
Index 0 → Dev
Index 1 → Test
Index 2 → Prod
Delete Test
Index 0 → Dev
Index 1 → Prod
Terraform:
Destroy Prod
Recreate Prod
──────────────────────────────
Using for_each
dev → Dev
test → Test
prod → Prod
Delete test
dev → Dev
prod → Prod
Terraform:
Destroy Test only
Process Flow
Resource list changes
│
▼
Is resource identified
by index or by key?
│
┌──────┴───────┐
│ │
Index Key
(count) (for_each)
│ │
Resources Resource
renumber identity stays
│ │
More changes Minimal changes
Security and operational considerations
Neither option is more secure than the other, but operationally they behave very differently.
Predictable infrastructure means predictable change windows.
When Terraform plans to recreate resources unnecessarily, engineers become reluctant to apply changes. That often leads to larger, riskier deployments because small improvements keep getting postponed.
State stability is one of those things nobody notices until it disappears.
If you’re working with production subscriptions, management groups, networking or role assignments, reducing unnecessary churn should be a priority.
Common mistakes
The biggest mistake is assuming count and for_each are interchangeable. They aren’t. They produce similar results initially but behave very differently as your environment evolves.
Another common issue is using lists with count for resources that already have unique names. If the resources naturally have identities, model them that way with a map and for_each.
Avoid converting between count and for_each halfway through a project unless you’ve planned the state migration. Terraform sees them as different resource addresses, so changing from one to the other without using terraform state mv or moved blocks can result in unnecessary resource replacement.
Finally, don’t make your keys unstable. Keys like dev, prod or finance are great. Keys generated from random values or changing strings are asking for trouble.
Summary
count is excellent when you’re creating a simple fixed number of identical resources or conditionally deploying something once. Beyond that, its reliance on numeric indexes becomes a maintenance problem.
for_each gives every resource a stable identity, making Terraform plans far more predictable as Azure environments grow and change. That’s why you’ll see it used extensively in well-designed enterprise modules.
If I’m deploying Azure Landing Zones, management groups, RBAC assignments, networking or subscription-level resources, I almost always reach for for_each. It makes future changes easier, reduces unnecessary churn and gives me far more confidence before pressing Apply.
What to Explore Next
- Terraform Locals: Cleaner Code Without the Clutter
- Azure Policy Explained
- Azure RBAC: Getting Role Assignments Right
- Explore Terraform moved blocks for safely refactoring existing infrastructure.
- Review the Azure Verified Modules to see how Microsoft uses
for_eachin production-ready Terraform modules.
Final thoughts
One of the things I enjoy about Terraform is that the biggest lessons often aren’t about syntax. They’re about understanding how Terraform thinks.
Choosing between count and for_each isn’t about writing fewer lines of code. It’s about making life easier for the person maintaining that code six months from now. Quite often, that person is you.
If you’ve found this useful, have a look around the rest of RAWRitsCloud for more Azure and Terraform articles. You can also connect with me on LinkedIn, explore my GitHub repositories, or get in touch if you’ve found an interesting Terraform edge case—I seem to collect those.