
September 10, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
You open a Terraform plan and three unrelated resources want to be destroyed because someone reordered a list. That pain is exactly why Terraform for_each vs count Explained matters on every real project. Both meta-arguments let you create multiple resource instances from one block, but they address state differently and fail in different ways. If you treat infrastructure as code with Terraform as production software, the choice between them is not stylistic. It controls whether a variable edit renames a server or quietly replaces the wrong one.
for_each when each instance has a stable map or set key you control. Use count only for zero-or-one toggles or fixed numeric replicas. for_each survives list reordering; count ties identity to index and often forces destructive replacement.What is the difference between Terraform for_each and count?
Both are meta-arguments on a resource, module, or data source. They tell Terraform how many instances to create from a single configuration block. The syntax looks similar. The state addressing model is not.
count takes a whole number. Terraform creates instances numbered from zero upward: aws_instance.web[0], aws_instance.web[1], and so on. The index is the identity.
for_each takes a map or a set of strings. Each instance gets a string key: aws_instance.web["prod"], aws_instance.web["staging"]. The key is the identity.
On production VPS stacks I maintain with Deployer and GitLab CI, Terraform usually runs before the app lands. A bad iteration choice shows up as surprise downtime during a routine variable edit. That is the practical gap between the two options.
Minimal count example
variable "subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
resource "aws_subnet" "private" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidrs[count.index]
availability_zone = element(["ap-south-1a", "ap-south-1b"], count.index)
tags = {
Name = "private-${count.index}"
}
} Minimal for_each example
variable "subnets" {
type = map(object({
cidr = string
az = string
}))
default = {
app = { cidr = "10.0.1.0/24", az = "ap-south-1a" }
db = { cidr = "10.0.2.0/24", az = "ap-south-1b" }
}
}
resource "aws_subnet" "private" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = {
Name = "private-${each.key}"
}
} Inside the block, count.index is a number. With for_each, you use each.key and each.value. Those names are reserved. Do not declare variables with those names in the same module.
When should you use Terraform for_each instead of count?
Default to for_each whenever instances represent named things: environments, regions, tenants, or DNS records keyed by hostname. If removing one key should destroy exactly that instance and leave the rest untouched, you want a map or set key.
I reach for for_each on almost every multi-environment module. Sister sites on shared EC2 infrastructure each get a stable key like notary-nepal or court-marriage. Reordering the input map never shuffles which server Terraform thinks belongs to which name.
- Input is a map or can become one with explicit keys.
- Each instance has a business meaning beyond its position in a list.
- You expect keys to be added or removed independently over time.
- You need to pass a subset to a child module via
for_eachon modules. - You want
terraform state mvto rename keys without replacement when possible.
Convert a list to a keyed map safely
Teams often receive a plain list from an API or legacy config. Do not feed that list directly to count if order can change. Build a map first.
variable "server_names" {
type = list(string)
default = ["web-01", "web-02", "web-03"]
}
locals {
servers = { for name in var.server_names : name => name }
}
resource "digitalocean_droplet" "web" {
for_each = local.servers
name = each.key
region = "blr1"
size = "s-1vcpu-1gb"
image = "ubuntu-24-04-x64"
} The list order no longer defines identity. The server name does. That pattern appears constantly in reusable Terraform modules shared across client projects.
When is Terraform count the right choice?
count still earns its place. The two legitimate patterns are conditional creation and fixed-size replica groups where index identity is acceptable.
Pattern 1: feature flag as zero or one resource
variable "enable_nat_gateway" {
type = bool
default = true
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? 1 : 0
allocation_id = aws_eip.nat[0].id
subnet_id = aws_subnet.public[0].id
} Writing for_each = var.enable_nat_gateway ? { main = true } : {} works too. Many teams prefer count here because the intent reads clearly: one optional resource, not a collection.
Pattern 2: identical replicas with no stable name
Three identical worker nodes behind a load balancer may not need individual keys. If losing node 1 and gaining a new node 1 is acceptable, count is fine. Document that index shifts cause replacement.
Do not use count for DNS records, IAM bindings keyed by username, or anything referenced elsewhere by index. Those references break silently when the list changes. Related reading: Terraform dynamic blocks in practice for nested iteration inside a single resource.
How do for_each and count affect Terraform state and plans?
State stores each instance at a unique address. That address is how Terraform matches configuration to real infrastructure on the next plan. Change the address without a proper state mv, and Terraform plans a destroy plus create.
Consider a count-based list ["alpha", "beta", "gamma"]. Index 0 is alpha. You remove alpha from the list. Beta slides to index 0. Terraform now thinks index 0 should be beta. It destroys the old index-0 alpha resource and creates a new index-0 beta resource. Beta was already running at index 1. You get an unnecessary replacement.
With for_each on { alpha = "...", beta = "...", gamma = "..." }, removing alpha destroys only ["alpha"]. Beta and gamma addresses stay put.
Remote state is non-negotiable for teams. Local state on a laptop cannot survive handoffs. See manage Terraform state safely and remote backend configuration for locking and bucket setup.
Referencing instances from other blocks
# count reference — fragile if count changes
resource "aws_route53_record" "web" {
count = length(var.server_names)
zone_id = aws_route53_zone.main.zone_id
name = var.server_names[count.index]
type = "A"
records = [aws_instance.web[count.index].private_ip]
}
# for_each reference — stable
resource "aws_route53_record" "web" {
for_each = local.servers
zone_id = aws_route53_zone.main.zone_id
name = each.key
type = "A"
records = [aws_instance.web[each.key].private_ip]
} Splat expressions differ too. aws_instance.web[*].id returns a list for count. For for_each, use values(aws_instance.web)[*].id or a comprehension. Mixing the two styles in one module causes confusing type errors at plan time.
How do you migrate from count to for_each without downtime?
Refactoring live infrastructure requires state surgery. Never run terraform apply on a blind conversion and hope Terraform deduplicates correctly. It will not.
The safe sequence:
- Change HCL from
counttofor_eachwith keys that match your naming intent. - Run
terraform planand confirm Terraform wants mass destroy/create. - For each instance, run
terraform state mv 'aws_instance.web[0]' 'aws_instance.web["web-01"]'. - Re-run plan until only in-place updates remain.
- Apply during a maintenance window if any replacement still appears.
# Example state moves after converting count to for_each
terraform state mv 'aws_subnet.private[0]' 'aws_subnet.private["app"]'
terraform state mv 'aws_subnet.private[1]' 'aws_subnet.private["db"]'
# Verify
terraform plan Back up state before moving addresses. Copy the remote state object in S3 or export with terraform state pull > backup.tfstate. Pair this with prevent_destroy lifecycle rules on critical resources during migration. Details sit in Terraform lifecycle meta-arguments explained.
CI pipelines should run terraform plan on every pull request. A refactor that skips state moves shows up as a red plan with dozens of replacements. Tools like Checkov catch misconfigurations but not address drift. See Checkov scan for Terraform misconfigurations and Terraform CI/CD with GitHub Actions.
Modules accept both meta-arguments
Calling a module with for_each creates multiple module instances in state. Each gets its own output namespace. This pattern scales well in Terragrunt DRY layouts where each site or region is a keyed module instance.
module "site" {
for_each = var.sites
source = "./modules/laravel-vps"
domain = each.value.domain
php_version = "8.3"
db_engine = "mysql"
} You cannot use both count and for_each on the same block. Terraform rejects it at validation. Pick one per resource, module, or data source.
What are the common mistakes with for_each and count?
These show up repeatedly in code review and production incident postmortems.
- Using
counton a list sourced fromsort()or an API with unstable ordering. - Passing a list of objects to
for_each— it requires a map or set of strings, not a list. - Using non-string keys in maps passed to
for_each— keys must be strings; usetostring()if needed. - Referencing
each.keyinside a block that usescount, or vice versa. - Assuming
terraform apply -targetfixes a bad iteration model — it only limits scope. - Duplicating keys in a map comprehension — Terraform errors on duplicate keys at plan time.
Validate inputs early with variable validation blocks. Catch empty maps before they produce zero resources silently.
variable "environments" {
type = map(object({
instance_type = string
cidr = string
}))
validation {
condition = length(var.environments) > 0
error_message = "At least one environment key is required."
}
} For debugging complex expressions, paste intermediate locals output into a JSON formatter during development. Run terraform console and evaluate local.servers before applying.
Official reference material from HashiCorp covers edge cases: the for_each meta-argument documentation, the count meta-argument documentation, and the for expressions guide for building maps from lists.
| Criteria | for_each | count |
|---|---|---|
| Accepted input types | Map or set of strings | Non-negative integer (often length(list)) |
| State address format | resource.name["key"] | resource.name[index] |
| Identity stability on list reorder | Stable if keys unchanged | Unstable — index is identity |
| Best for optional single resource | Possible but verbose | Ideal: count = bool ? 1 : 0 |
| Best for named multi-instance | Default choice | Avoid unless index OK |
| Iterator variables | each.key, each.value | count.index |
| Module iteration | Strong fit for keyed environments | Works for N identical copies |
| Refactor cost from wrong choice | Low if keys were always stable | High — often needs state mv per index |
Verdict: Use for_each as the default for any collection where instance identity matters. Reserve count for boolean toggles and homogeneous replicas where index-based replacement is acceptable. That single habit prevents most accidental production replacements.
Context from related posts helps round out the picture. Variables, locals, and outputs feed the maps you pass to for_each. Workspaces and environments solve a different problem — do not confuse workspace name with instance key. If you evaluate OpenTofu as a fork, iteration behaviour matches Terraform; see Terraform vs Pulumi vs OpenTofu. For first VPS provisioning, start with Terraform for VPS provisioning before layering complex iteration.
On the application side, the same identity problem appears in Laravel queue design. A cron job vs queue worker choice also hinges on whether work items have stable identity. Infrastructure and app layers reward the same thinking.
When Terraform runs on Linux servers I administer for clients, PHP version pins and Linux system administration sit downstream of correct provisioning. A booking platform like Adventure Third Pole Trek needs stable DNS and VPS names across deploys. Wrong iteration causes churn that no application code can fix.
Pin provider versions while refactoring. A provider upgrade plus address migration in one PR is painful to debug. Follow Terraform provider version pinning and keep HCL refactors separate from provider bumps.
Key Takeaways
- Default to
for_eachwith map or set keys whenever instances have meaningful names. - Use
countonly forcondition ? 1 : 0toggles or index-identical replica groups. - List reorder with
countchanges state addresses and triggers unwanted destroy/create cycles. - Migrate live resources with
terraform state mv, not blind apply, after converting count to for_each. - Validate map inputs and run
terraform planin CI before every apply to catch address drift early. - Reference instances by key with
each.key, never by index, when other resources depend on them.
People Also Ask
Can you use for_each and count on the same resource?
No. Terraform allows exactly one iteration meta-argument per resource, module, or data source block. Choose for_each or count, not both. Attempting both produces a configuration error during terraform validate.
Does for_each work with a list in Terraform?
Not directly. for_each requires a map or a set of strings. Convert a list with a comprehension: { for x in var.list : x => x } when values are unique strings, or assign explicit keys in a locals block.
What happens if you change a for_each key?
Renaming a key changes the state address. Terraform plans to destroy the old address and create the new one unless you run terraform state mv 'resource["old"]' 'resource["new"]'. Treat key names as part of your API contract.
Is count deprecated in Terraform?
No. HashiCorp still documents and supports count. It is the right tool for optional single resources and simple numeric replication. The community default shifted toward for_each for named collections because it produces safer plans.
Pick the right meta-argument on your next module
Terraform for_each vs count Explained boils down to one question: does each instance have a stable name that should survive edits to the input collection? If yes, use for_each. If you only need zero or one of something, use count. Getting this wrong costs more than a long plan diff. It costs downtime on infrastructure your application depends on.
If you are provisioning servers, wiring CI, or refactoring modules across environments and want a second pair of eyes on the state moves, contact us or review custom software and infrastructure development services. Solid iteration choices today save emergency state mv sessions tomorrow.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

