Terraform for_each vs count Explained

Khimananda Oli 8 min read Virtualization
Terraform for_each vs count Explained

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.

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.

count (Index-Based)Resource [0] → "alice"Resource [1] → "bob"Resource [2] → "charlie"⚠ Remove "alice" from listAll indices shift → Destroys bob & charliefor_each (Key-Based)Resource ["alice"] → "alice"Resource ["bob"] → "bob"Resource ["charlie"] → "charlie"✓ Remove "alice" keyOnly "alice" destroyed. Others stable.
Terraform for_each vs count explained: index-based addressing causes cascading destruction when lists reorder, while key-based addressing preserves resource identity.

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 : 0 remains 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
  }
}
Variable Inputmap / set(string)for_each ExpansionIterate keysGenerate resource blocksBind each.key / each.valueState Keysresource.type["key"]Cloud ProviderCreate / Update / No-opKey Stability GuaranteeRemoving key "X" only destroys resource["X"]. All others unchanged.
Terraform for_each evaluation flow: variable input expands into keyed resource instances, generating stable state addresses that isolate changes to modified keys only.

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.

Criterioncountfor_each
State Addressresource.type[index]resource.type["key"]
Input TypeWhole numberMap or set(string)
Add/Remove SafetyUnsafe for ordered lists; safe for togglesSafe; only affected key changes
Reference Syntaxcount.indexeach.key, each.value
Splat Expressionsresource.type[*].attr[for k, v in resource.type : v.attr]
Conditional ToggleClean (? 1 : 0)Verbose (filter map/set)
Module IterationSupported but fragileRecommended for module reuse
Best ForIdentical resources, feature flagsNamed 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.

  1. Backup your state. Whether using S3, GCS, or Terraform Cloud, create a versioned backup before any migration. This is non-negotiable.
  2. Update HCL first. Change count to for_each in your configuration. Use toset() or construct a map with stable keys derived from your original list.
  3. Run terraform plan. Confirm it shows destroys and creates. Do NOT apply yet.
  4. Execute terraform state mv commands. Move each indexed resource to its new keyed address. For example: terraform state mv 'aws_instance.worker[0]' 'aws_instance.worker["alice"]'.
  5. Re-run terraform plan. Verify zero changes. The plan should show "No changes. Your infrastructure matches the configuration."
  6. 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.

Start: Need Loop?ConditionalToggle Only?YesUse countNoResourcesIdentical?YescountNoUse for_eachDefault RecommendationPrefer for_each unless count has clear advantage
Decision framework for Terraform for_each vs count explained: start with conditional check, then assess resource identity to select the appropriate meta-argument.

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.

Frequently Asked Questions

Count creates resources by index position, while for_each uses unique map keys or set values. For_each prevents resource recreation when list order changes, making it safer for production infrastructure management in 2026.

Use count only for identical resources where ordering matters or when creating a fixed number of indistinguishable instances. Avoid count for named resources like IAM users or S3 buckets where identity stability is critical during updates.

Yes, but you must refactor state using terraform state mv commands to remap indexed addresses to keyed addresses. Skipping this step causes Terraform to destroy and recreate all affected resources during the next apply cycle.

No, for_each requires maps or sets. Convert lists using toset() or zipmap() functions first. Using toset() removes duplicates and loses ordering, so prefer zipmap() when preserving original list indices as keys is necessary.

Count references resources by numeric index. Reordering the source list shifts indices, causing Terraform to misidentify existing resources. This triggers unnecessary destroys and creates, risking data loss and downtime in stateful infrastructure components.

For_each assigns stable identities via keys, preventing accidental credential rotation or permission drift caused by index shifting. Named resources maintain consistent ARNs and IDs, reducing security risks during team collaboration and automated pipeline executions.

No, both have identical API call costs. Performance differences are negligible in 2026 Terraform versions. Choose based on safety and maintainability, not cost, as plan and apply execution times remain virtually equivalent.

Yes, nested for_each works in Terraform 1.9+ using module-level iteration. Each nested level requires distinct keys to avoid state conflicts. Flatten complex structures before iterating to maintain readable configurations and predictable dependency graphs.

Use conditional expressions inside for_each maps with filtering syntax like {for k,v in var.items : k => v if v.enabled}. This dynamically excludes disabled entries without altering the underlying variable structure or requiring separate preprocessing steps.

Terraform accepts most UTF-8 characters in keys but avoids slashes and dots which conflict with address parsing. Use alphanumeric keys with underscores for safe state addressing and reliable CLI operations across different shell environments.

Yes, but mark sensitive map values explicitly using sensitive() function. Terraform redacts these in logs and plans. Never use sensitive data as map keys since keys appear unmasked in resource addresses and state files.

Run terraform console to inspect transformed maps before applying. Use try() functions to handle missing keys gracefully. Check plan output carefully for unexpected key mappings that indicate incorrect filtering or type conversion issues.

No, Terraform prohibits using both meta-arguments simultaneously on the same resource block. Choose one approach per resource. Split logic into separate locals or child modules if you need both iteration patterns together.

Use descriptive, immutable identifiers like instance names or environment tags rather than arbitrary indices. Stable keys prevent state churn during refactors and make terraform state mv operations intuitive when restructuring infrastructure code.

For_each with empty maps or sets creates zero resources without errors. Unlike count which requires conditional length checks, for_each naturally handles empty inputs, simplifying optional resource patterns and reducing boilerplate validation logic.