Count, for_each, Functions, and Stable Identities
Create repeated resources with predictable addresses and avoid accidental changes caused by list indexes.
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.