Count, for_each, Functions, and Stable Identities

Create repeated resources with predictable addresses and avoid accidental changes caused by list indexes.

Intermediate⏱ 2 min readLesson 7 of 16#terraform#for_each#count#functions#dynamic

Identity matters more than repetition

count creates instances indexed by number. for_each creates instances identified by map keys or set members. Those identifiers become part of the state address.

# Keep the local provider configuration from lesson 2.
variable "services" {
  type = map(object({
    port = number
  }))
  default = {
    api = { port = 8080 }
    web = { port = 3000 }
  }
}

resource "local_file" "service" {
  for_each = var.services
  filename = "${path.module}/${each.key}.json"
  content = jsonencode({
    name = each.key
    port = each.value.port
  })
}

output "service_files" {
  value = { for name, file in local_file.service : name => file.filename }
}

This creates local_file.service["api"] and local_file.service["web"]. Add worker = { port = 9000 }; plan should add one file without changing the other identities.

Why list indexes can surprise you

If a resource uses count with ["api", "web", "worker"], instance zero refers to api. Removing api shifts later entries to earlier indexes. Terraform compares the changed settings at those addresses and may update or replace several objects.

A map avoids index shifts. Renaming a key still changes identity and may trigger deletion plus creation. Use a moved block when you intend to preserve the original object under a new address.

count remains useful for interchangeable instances or a zero-or-one switch:

variable "write_readme" {
  type    = bool
  default = false
}

resource "local_file" "readme" {
  count    = var.write_readme ? 1 : 0
  filename = "${path.module}/README-generated.txt"
  content  = "Generated by Terraform.\n"
}

When present, the address is local_file.readme[0].

Transform values cleanly

Use jsonencode instead of hand-building JSON, templatefile for larger templates, merge for tag maps, and cidrsubnet for subnet math.

locals {
  common_tags = { ManagedBy = "Terraform", Team = "platform" }
  tags        = merge(local.common_tags, { Environment = "dev" })
  subnet_cidr = cidrsubnet("10.42.0.0/16", 8, 1)
}

The subnet expression produces 10.42.1.0/24. Verify expressions with terraform console before embedding complex transformations.

Dynamic blocks

A dynamic block generates repeated nested blocks inside a resource, when that resource's provider schema requires them. It does not create separate resource instances and cannot generate lifecycle meta-arguments. Prefer ordinary resource for_each when separately managed objects exist.

Practice: remove only web from the local example and inspect the plan. Then rename api to backend and compare. Explain why these changes have different identity consequences.

Further reading: for_each, functions.