Chapter 6: Iteration and Conditionals
Topic 35

count

IterationLanguage

count is the oldest way to stamp out multiple copies of a resource: set count = 3 and Terraform creates three instances, addressed resource.name[0], [1], [2], with count.index available inside the block to vary each one. It is index-based and positional, which is exactly its strength for identical-ish replicas and exactly its footgun the moment the middle of the list changes.

The Hatch pipeline reaches for iteration as it outgrows its single raw bucket. count is the simplest construct that gets there, and for a fixed number of interchangeable things it is the right tool. But most real collections are named and mutable rather than fixed and positional, and on those count does something destructive that the next topic, for_each, exists to avoid. Knowing where the line falls is the whole point of this page.

The Basic Form

Setting count = N on a resource tells Terraform to create N instances of it, indexed 0 through N-1. Inside the block, count.index is the current instance's number, so each copy can differ — a numbered name, a zone pulled from a list by index, a label carrying the index. Without count.index you would get N identical resources, which the provider usually rejects because names must be unique.

Three numbered worker instances
resource "google_compute_instance" "worker" {
  count        = 3
  name         = "hatch-worker-${count.index}"   # hatch-worker-0, -1, -2
  machine_type = "e2-standard-2"
  zone         = "us-central1-a"
}

That block produces three instances with distinct names, addressed google_compute_instance.worker[0] through [2]. They are interchangeable workers where the index is just a serial number, which is precisely the case count handles cleanly.

Iterating a List with count

A common pre-for_each pattern is to drive count off the length of a list and index back into that list with count.index. Setting count = length(var.regions) creates one resource per element, and var.regions[count.index] pulls the matching element for each instance. This walks a list to produce "one resource per element," the original idiom before for_each existed.

One bucket per region, by list index
variable "regions" {
  type    = list(string)
  default = ["us-central1", "europe-west1", "asia-east1"]
}

resource "google_storage_bucket" "regional" {
  count    = length(var.regions)
  name     = "hatch-events-${var.regions[count.index]}"
  location = var.regions[count.index]
}

This works, and you will see it in older code. It also carries the positional trap front and center: the buckets are keyed by their position in the list, not by region name, so the list's order is now load-bearing state.

Addressing Instances

Once count is set, the resource is no longer a single object — it is a list of instances. You address one with an index, google_storage_bucket.regional[0].name, and all of them at once with a splat, google_storage_bucket.regional[*].name, which yields the list of every instance's name. A bare reference without an index, google_storage_bucket.regional.name, is an error: Terraform cannot tell which of the instances you mean.

This is the first thing that bites newcomers. Adding count to an existing single resource silently changes its type from one object to a list, and every reference elsewhere in the configuration must be updated to index in. The splat form, covered in its own topic later in this chapter, is how you read an attribute across all instances at once.

The Renumbering Footgun

Here is the behavior that decides whether count is right for a given collection. Instances are keyed by position. Remove the element at index 1 of a three-element list and index 2 shifts down to 1, index 3 shifts to 2 — Terraform compares the new positions to state and reads it as "destroy the resource at index 2 and recreate it, destroy the resource at index 1 and recreate it," not "delete the middle one and leave the rest alone." That destructive churn has dropped buckets and disks in production, taking their contents with them.

Concretely: a list of three source buckets keyed by position, and you remove hatch-events-raw-billing from the middle. The plan does not say "delete one bucket." It proposes destroying and recreating every bucket after the removed one, because each one's positional address now points at different data. The bucket that used to be index 2 is now index 1, and Terraform tears it down to rebuild it at its new slot. Nothing about the change you intended implied data loss, but the positional model produced it.

Two edits to a counted list, two very different plans
Append to the END
A fourth bucket lands at index [3]; indices [0][2] stay put. Plan: one bucket created, nothing else touched. Safe.
Remove a MIDDLE element
Index [1] vanishes, so [2] shifts to [1] and the tail renumbers. Plan: every later bucket destroyed and recreated, contents gone.

When count Is Still Right

count earns its place for a fixed number of truly interchangeable replicas where order never changes — N identical worker VMs behind a load balancer, where instance 0 and instance 2 are indistinguishable and no element is ever removed from the middle. It is also the mechanism behind the conditional count = var.enabled ? 1 : 0 idiom for creating a resource or not, which gets its own treatment in the conditional-expressions topic. Anything keyed, named, or mutable in the middle wants for_each instead.

count vs for_each

count — keys instances by integer position. Right for a fixed number of identical, interchangeable replicas where order is meaningless and stable. Wrong the moment elements can be added or removed from the middle, because that renumbers and churns the tail.

for_each — keys instances by a stable map or set key, so insertions and removals are surgical. The default for any collection of distinct, named things — one bucket per source, one topic per event type. Reach for it unless the replicas are genuinely ordered and interchangeable.

Common Mistakes
  • Using count over a list that grows and shrinks in the middle — removing hatch-events-raw-billing from the middle of a source list re-indexes every bucket after it, and Terraform destroys and recreates them, losing their contents.
  • Referencing a counted resource without an index (google_storage_bucket.raw.name) — once count is set the resource is a list and the bare reference fails to compile.
  • Setting count from a value not known until apply (another resource's computed attribute) — Terraform cannot expand the count during plan and errors with "count cannot be determined."
  • Assuming count = 0 and a later count = 1 resume the same instance — index 0 only exists when count is at least 1, and flipping it on recreates rather than resumes.
  • Reaching for count to create distinct, named resources like one bucket per named source — they are positional, so any reordering of the source list churns everything downstream.
  • Treating the order of the list count walks as cosmetic — that order is now state, and an innocent alphabetical re-sort triggers a destroy-and-recreate across the fleet.
Best Practices
  • Use count only for a fixed number of interchangeable replicas, never for a collection of distinct named things.
  • Switch to for_each the moment elements can be added to or removed from the middle of a collection.
  • Reference counted instances with an explicit index or a splat, and expect the resource to behave as a list everywhere it is used.
  • Keep the count expression knowable at plan time — derive it from variables and locals, not from another resource's computed output.
  • Reserve the conditional count = var.enabled ? 1 : 0 form for create-or-not toggles, and keep "whether" and "how many" out of the same expression.
Comparable approaches Pulumi a real-language loop over an array, no positional state trap Config Connector N distinct manifests or a templating layer Deployment Manager Jinja or Python loops Ansible loop with with_items

Knowledge Check

You have three buckets created with count over a list and you remove the middle element. What does the plan propose?

  • Destroying and recreating every instance after the removed one, because their positional addresses shift down
  • Deleting only the middle bucket and leaving the other two at their original indices untouched
  • Renaming the two remaining buckets in place to close the index gap left behind, with no destroy or recreate at all
  • Nothing at all, since the list length is recomputed automatically on the next plan

When is count the appropriate choice over for_each?

  • A fixed number of truly interchangeable replicas where order is meaningless and never changes
  • A collection of distinctly named resources, like one storage bucket per upstream source system
  • Any collection whose membership is added to or trimmed frequently in the middle
  • Whenever you want each resource addressed by a stable human-readable key

Why does google_storage_bucket.raw.name fail to compile once count is set on the resource?

  • The resource is now a list of instances, so a reference must include an index or a splat
  • The name attribute is no longer exported when count is present
  • Counted resources can only be referenced from within their own block, never from elsewhere
  • The provider strips the name attribute for counted buckets

Why can't count be set from another resource's computed attribute?

  • Terraform must know the number of instances at plan time, and a computed value is unknown until apply
  • Computed attributes are always returned as strings, and count requires a numeric list
  • It can, but only when the source resource happens to live in the very same module as the counted resource
  • Counted resources cannot declare a dependency on any other resource at all

You got correct