
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Choosing between count and for_each is one of the most consequential decisions you make when writing reusable Terraform modules. Getting this wrong leads to destructive state drift, accidental resource deletion during refactors, and painful migrations that stall delivery. This guide on Terraform for_each vs count explained provides the definitive mental model and practical patterns you need to write stable, production-grade Infrastructure as Code.
for_each for named resources where identity matters (users, buckets, DNS records) to ensure stable state keys. Reserve count only for identical, interchangeable resources or simple conditional toggles. In modern Terraform for_each vs count explained workflows, for_each prevents accidental destruction during list reordering.Before diving into loop mechanics, remember that iteration strategy directly impacts how you manage Terraform state management and remote backends. When state keys shift unexpectedly due to poor looping choices, your remote state file becomes a liability rather than a source of truth. Understanding the structural difference between index-based and key-based addressing is foundational to avoiding these operational hazards.
How does Terraform count work and when is it safe?
The count meta-argument accepts a whole number and creates that many identical instances of a resource. Each instance receives a zero-based index accessible via count.index. This mechanism is simple but carries inherent risk because Terraform tracks these resources by their numeric position in the state file, not by any semantic identifier.
Safe use cases for count
- Conditional resource creation: Using
count = var.enable_feature ? 1 : 0remains the cleanest pattern for toggling single resources. The index never shifts because there is at most one instance. - Truly identical resources: Creating three identical worker nodes behind a load balancer where no individual node has unique configuration or external dependencies. If node [1] dies and is replaced by what was previously node [2], nothing breaks.
- Fixed-count infrastructure: Resources where the quantity is static and defined by architecture constraints (e.g., exactly 3 control plane nodes) rather than dynamic business data.
# Safe: Conditional toggle with count
resource "aws_cloudwatch_log_group" "audit" {
count = var.enable_audit_logging ? 1 : 0
name = "/audit/${var.environment}"
retention_in_days = 365
}
# Safe: Identical workers with no unique identity
resource "aws_instance" "worker" {
count = 3
ami = var.worker_ami
instance_type = "t3.medium"
tags = {
Name = "worker-${count.index}"
Role = "compute"
}
} A common mistake I see in code reviews across teams in Nepal and globally is using count with var.user_list or similar dynamic collections. The moment someone removes an item from the middle of that list, every subsequent resource gets re-indexed. Terraform interprets this as "destroy resource [n] through [end]" and "create new resource [n] through [end]," even though the underlying cloud resources haven't actually changed. For databases like those covered in PostgreSQL administration essentials, this could mean accidentally dropping and recreating user roles or schemas during a routine config update.
Why is for_each preferred for named resources?
The for_each meta-argument accepts either a map or a set of strings. Crucially, Terraform tracks each instance by its key (the map key or string value), not by position. This means adding, removing, or reordering elements in your input collection only affects the specific resources whose keys changed. Everything else remains untouched in state.
Converting lists to sets for for_each
If your input is a list but you want for_each safety, convert it explicitly. Lists are ordered and allow duplicates; sets are unordered and unique. Terraform requires sets or maps for for_each precisely to enforce key stability.
# Convert list to set for safe iteration
variable "bucket_names" {
type = list(string)
default = ["logs", "backups", "assets"]
}
resource "aws_s3_bucket" "this" {
for_each = toset(var.bucket_names)
bucket = "${var.prefix}-${each.value}"
tags = {
ManagedBy = "terraform"
Purpose = each.value
}
}
# Accessing values: each.key == each.value for sets
# For maps: each.key is the map key, each.value is the map value When working with complex objects, use a map keyed by a stable identifier. This is especially important when integrating with systems discussed in Kubernetes secrets management done right, where secret names must remain constant across deployments to avoid application outages.
# Map of objects with stable keys
variable "app_secrets" {
type = map(object({
description = string
value = string
}))
default = {
"db-password" = {
description = "Primary database credential"
value = "sensitive-value-here"
}
"api-key" = {
description = "External API authentication"
value = "another-sensitive-value"
}
}
}
resource "kubernetes_secret" "app" {
for_each = var.app_secrets
metadata {
name = each.key
}
data = {
value = each.value.value
}
} What are the key differences between for_each and count?
Understanding the trade-offs systematically prevents costly mistakes. The following comparison captures the operational realities I've encountered managing infrastructure across AWS, Azure, and GCP environments.
| Criterion | count | for_each |
|---|---|---|
| State Address | resource.type[index] | resource.type["key"] |
| Input Type | Whole number | Map or set(string) |
| Add/Remove Safety | Unsafe for ordered lists; safe for toggles | Safe; only affected key changes |
| Reference Syntax | count.index | each.key, each.value |
| Splat Expressions | resource.type[*].attr | [for k, v in resource.type : v.attr] |
| Conditional Toggle | Clean (? 1 : 0) | Verbose (filter map/set) |
| Module Iteration | Supported but fragile | Recommended for module reuse |
| Best For | Identical resources, feature flags | Named resources, user-defined collections |
The splat expression difference deserves emphasis. With count, you can use aws_instance.worker[*].private_ip to get all IPs. With for_each, you must use a for expression: [for k, v in aws_instance.worker : v.private_ip]. This is slightly more verbose but gives you explicit control over ordering and filtering, which matters when building outputs for reusable Terraform modules.
How do you migrate from count to for_each safely?
Migrating existing infrastructure from count to for_each is a state surgery operation. Terraform will treat this as destroying all old indexed resources and creating new keyed resources unless you explicitly move state entries. Never run terraform apply without first completing these steps.
- Backup your state. Whether using S3, GCS, or Terraform Cloud, create a versioned backup before any migration. This is non-negotiable.
- Update HCL first. Change
counttofor_eachin your configuration. Usetoset()or construct a map with stable keys derived from your original list. - Run
terraform plan. Confirm it shows destroys and creates. Do NOT apply yet. - Execute
terraform state mvcommands. Move each indexed resource to its new keyed address. For example:terraform state mv 'aws_instance.worker[0]' 'aws_instance.worker["alice"]'. - Re-run
terraform plan. Verify zero changes. The plan should show "No changes. Your infrastructure matches the configuration." - Apply only after confirmation. If the plan is clean, apply to confirm state consistency. If not, debug the moves before proceeding.
# Example migration commands
# Before: count = 3, workers named alice, bob, charlie
# After: for_each with map keyed by name
terraform state mv 'aws_instance.worker[0]' 'aws_instance.worker["alice"]'
terraform state mv 'aws_instance.worker[1]' 'aws_instance.worker["bob"]'
terraform state mv 'aws_instance.worker[2]' 'aws_instance.worker["charlie"]'
# Verify clean plan
terraform plan # Should output: No changes. This process is tedious but necessary. I've seen teams skip step 4 and lose production databases because Terraform destroyed the indexed resource and created a new keyed one with a fresh empty volume. Automation helps here: scripts can generate state mv commands from your current state and desired mapping, reducing human error during high-pressure migrations.
Make the Right Choice for Production Stability
The distinction between Terraform for_each vs count explained ultimately comes down to resource identity. If each instance has a name, a purpose, or external references that depend on its existence, use for_each. If instances are fungible widgets or you're flipping a single boolean, count is appropriate. Default to for_each for new code; the slight verbosity pays off in operational safety and refactor flexibility.
If you're auditing existing modules or designing new infrastructure and want a second pair of eyes on your looping strategy, reach out to discuss your Terraform architecture. Stable state starts with correct abstractions.