One Block, Three Machines: How count Expands a Resource Into Indexed Addresses
Your team wants the same small EC2 box in two availability zones. You could copy the resource block twice and rename them web_a and web_b, but then every reference, every tag, and every future change gets written twice. count removes the duplication: write the block once, and Terraform builds that many instances from it.
resource "aws_instance" "web" {
count = 2
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
subnet_id = var.subnet_ids[count.index]
tags = {
Name = "web-${count.index}"
}
}
count accepts a whole number, and Terraform creates that many instances of the block. The moment it does, the block stops being a single object and becomes a tuple of instances addressed by integers starting at 0. That index is written into state, printed in terraform plan, and used to decide what to create, update, or destroy.
| Reference | What it resolves to |
|---|---|
aws_instance.web |
the whole tuple — a collection, not one machine |
aws_instance.web[0] |
the first instance, an individual resource |
aws_instance.web[1].private_ip |
one attribute of the second instance |
count.index |
inside the block: this instance's own index |
Inside the block, count.index is how instances differ from each other — pick a subnet from a list, stamp a numbered Name tag, select a settings entry. Outside, you reach attributes with bracket syntax: aws_instance.web[0].id. Ask for aws_instance.web.subnet_id and there is nothing to index into, so Terraform rejects the reference. Without an index, a counted resource is a tuple, and a tuple has no attributes of the instances inside it. To fan out over the whole set you iterate — a for expression or the [*] splat covered in Splat and Attribute Access Expressions.
count behaves identically on modules and data blocks:
module "network" {
count = length(var.regions)
source = "./modules/network"
region = var.regions[count.index]
}
data "aws_ami" "alma" {
count = var.lookup_ami ? 1 : 0
most_recent = true
owners = ["646666666666"]
}
Addresses keep the same shape: module.network[1].vpc_id, data.aws_ami.alma[0].id. Run terraform state list after an apply and you see aws_instance.web[0] and aws_instance.web[1] as separate entries — the index is part of the object's identity in state, which is exactly what makes the renumbering problem in the next chapter expensive.
Interviewers open with count because it quickly separates people who understand instance addressing from people who have only copied examples. Expect: "how do you reference the second server?" (aws_instance.web[1].private_ip), "what does the bare aws_instance.web give you?" (a tuple), and "what happens if the list shrinks?" — the last one is the answer that costs money, and it is next.
One framing to carry from Promoting Changes Across Environments: workspaces duplicate an entire configuration across environments, while count duplicates one block within a single state file. Different axis, different blast radius.
The addresses created by count are zero-based, so a count of three yields addresses ending in [0], [1], and [2]. Do not treat these indexes as durable identities if the collection can change. A plan that removes element one may also replace the object formerly at index two because that object now occupies index one. Review address changes carefully before applying them, especially for stateful resources such as databases or disks. If identity belongs to a name or key, model that identity with for_each instead.
Deleting index 1 Destroys index 2: The Cost of Positional Identity
You deploy one instance per subnet:
variable "subnet_ids" {
type = list(string)
default = ["subnet-aaa", "subnet-bbb", "subnet-ccc"]
}
resource "aws_instance" "web" {
count = length(var.subnet_ids)
ami = "ami-0c55b159cbfafe1f0"
subnet_id = var.subnet_ids[count.index]
}
State holds aws_instance.web[0], [1], [2]. A teammate removes subnet-bbb from the middle of the list. The intuitive plan would be: destroy the one machine on subnet-bbb and leave the others alone. That is not what Terraform prints:
# aws_instance.web[2] will be destroyed
- resource "aws_instance" "web" {
- id = "i-0ccc..." -> null
- subnet_id = "subnet-ccc" -> null
}
# aws_instance.web[1] must be replaced
-/+ resource "aws_instance" "web" {
~ subnet_id = "subnet-bbb" -> "subnet-ccc" # forces replacement
}
Read that carefully. subnet-ccc's machine (old index 2) is destroyed, while old index 1 is rebuilt on subnet-ccc — and the machine you actually wanted gone, the one on subnet-bbb, is the one being replaced in place. You got two destroys and a create where you wanted one destroy.
The reason is a single rule: count identifies instances by position, not by content. New index 1 must match old index 1, whatever is in it. Terraform has no way to know that the value which used to live at index 2 should now occupy index 1 — values move silently, addresses do not.
What is safe and what is not:
| Change to the list | Plan result |
|---|---|
| Append at the end (3 → 4) | create [3] only — nothing else moves |
| Drop the last element (4 → 3) | destroy [3] only |
| Insert, delete, or reorder in the middle | neighbours rewritten, wrong instance destroyed |
Shrink to count = 0 |
every instance destroyed |
Appending is the only operation that is always harmless, which is why lists feeding count should be append-only or already stable. If identity must survive reordering — the list came from a directory, a discovery API, or a human editing YAML — positional keys are simply the wrong identity model, and for_each over Maps and Sets is the fix. Terraform can also remap addresses after the fact with moved blocks, but that is a repair, not a design.
count doubles as a kill switch: count = var.enable_flow_logs ? 1 : 0 creates or destroys one resource on a flag. Two consequences follow. Going to 0 destroys real infrastructure, so treat that variable as production-affecting. And any reference to aws_instance.flow_log[0].id while count = 0 points at an instance that does not exist — you get an Invalid index diagnostic during plan, so gate those references with try(...) or a conditional.
Interview question to have ready: "how do you add a fourth server without touching the first three?" Append to the end of the list — never insert — and say out loud why: with count, index is identity, so a mid-list edit rewrites neighbours.
When a resource uses count, its reference is a list of instances. A downstream resource can use the same count and index, or consume a splat expression when it needs all instance attributes. Keep the relationship explicit: two independently calculated counts can diverge and produce an invalid index or an unintended association. For a small fixed pair of regional resources, a numeric index may be acceptable; for named regions, derive the resource instances from a map keyed by region so configuration changes do not renumber identities.
count Must Be Known Before Apply: Reading the 'Invalid count argument' Diagnostic
You wire count to something Terraform cannot compute until the infrastructure exists:
data "external" "hosts" {
program = ["bash", "./lookup-hosts.sh"]
query = { config_arn = aws_kms_key.repo.arn } # key does not exist yet
}
resource "aws_instance" "web" {
count = length(jsondecode(data.external.hosts.result.list))
ami = "ami-0c55b159cbfafe1f0"
subnet_id = var.subnet_ids[0]
}
terraform plan stops with:
Error: Invalid count argument
on main.tf line 24, in resource "aws_instance" "web":
24: count = length(jsondecode(data.external.hosts.result.list))
The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
Terraform is not being slow — it is refusing to guess. Instance addresses must exist before Terraform builds the dependency graph: to draw aws_instance.web[0] and aws_instance.web[1], it has to know the number is 2, and to tell you "one will be destroyed" it must know the number used to be 3. An unknown count means an unknown set of addresses, and an unknown set of addresses means a plan that could silently destroy production machines. It fails the plan instead.
Three ways out, best first:
- Make the input a variable.
count = var.worker_countis known at plan time from-var, a.tfvarsfile, or the pipeline. Discovery that must happen at apply time belongs in alocalcomputed from configuration you already control. - Split the apply. Create the dependency in one run, let the next run see a readable value. Terraform's error text suggests
-targetas a workaround; it works, but it leaves a half-applied state behind and the second apply has to reconcile it — use it as a last resort, not a habit. - Change the shape. If the list length genuinely varies at runtime, ask whether you need N resources at all. One resource with repeated nested blocks — generated by
dynamicin dynamic Blocks for Repeatable Nesting — varies without changing the instance count.
The same law covers for_each, and there it bites harder: not just the number but every key must be known before apply, or you get Invalid for_each argument with the same "cannot be determined until apply" wording. Both meta-arguments also reject values from impure functions — timestamp(), uuid(), bcrypt() — because they change every evaluation and would make instance addresses unstable between plan and apply. for_each additionally refuses sensitive values as keys, since addresses are printed in UI output.
When count is the right tool, the docs' rule is short: use it when you want nearly identical instances. Two NAT gateways in two AZs, a fixed pair of cache nodes, a boolean flag resource — all sets where the size is small, fixed, and known before the plan runs. A dynamic list of unknown size, or any set whose members carry names that must stay stable, is a signal you want keys rather than indices — which is where we go next in for_each over Maps and Sets.
A count value must be known during planning because Terraform needs to construct addresses before apply. A count computed from a resource attribute that is unknown until creation can therefore fail with an unknown value error. Move the decision into a plan-time input or use a resource design that does not depend on an apply-time result for its instance count. A conditional count of zero or one can be convenient for an optional resource, but the conditional must be predictable before apply and the resulting indexed reference must handle the zero-instance case safely.
Quiz
1. You set `count = 3` on `aws_instance.web`. Which expression reads the private IP of the second instance?
2. count = length(var.subnet_ids) with three entries. You delete the middle entry from the list. What does terraform plan?
3. A plan fails with `Invalid count argument: The "count" value depends on resource attributes that cannot be determined until apply`. What is the root cause?
4. Which requirement is the best signal that you should reach for count rather than anything else?
Flashcards
Question
With count = 5 on aws_instance.web, what is the address of the third instance, and what does the bare block reference mean?
Click to reveal answer
Answer
The third instance is aws_instance.web[2] — count indices are zero-based and become part of the instance identity in state. The bare aws_instance.web refers to the whole tuple of instances, so it has no attributes of its own; you index it or iterate it.
Question
What object does count expose inside a block, and where is it valid?
Click to reveal answer
Answer
count.index — the zero-based index of the current instance. It exists only inside blocks that carry count (resource, data, module, ephemeral, list) while Terraform expands them, and it is how one expanded instance differs from its siblings (subnet choice, name suffix, settings entry).
Question
What happens when you delete the middle element of a list feeding count, and which edit is always safe?
Click to reveal answer
Answer
Instances are keyed by position, so Terraform rewrites the surviving indices to the new values — neighbours get updated or force-replaced — and destroys whatever index fell off the end. Appending at the end is the only edit that never disturbs existing instances; mid-list insert/delete/reorder must be avoided or repaired with moved blocks.
Question
When does Terraform raise 'Invalid count argument'?
Click to reveal answer
Answer
When the count value is not known before apply — for example it depends on a resource or data attribute created during the same apply, or comes from an impure function like timestamp()/uuid(). Terraform cannot predict the instance addresses, so it refuses to plan; fix it with a plan-time variable, a split apply, or a different resource shape.
Question
What is the documented rule for choosing count versus for_each?
Click to reveal answer
Answer
Use count for nearly identical instances where the index is enough; use for_each when instances need distinct values that cannot be derived from an integer index (names, keys, per-member settings). You cannot set both on the same block, and for_each additionally requires every key to be known before apply.
Revision Notes
Key Takeaways
- 1. count turns one block into a tuple of instances addressed `[0]`, `[1]`, …; the bare block reference is a collection and carries no instance attributes
- 2. Index is identity in state — appending is safe, mid-list edits renumber neighbours and destroy the wrong machine
- 3. `count = var.flag ? 1 : 0` is a legitimate kill switch, but every downstream reference needs `[0]` handling via try() or a conditional
- 4. count must be known before apply, or plan fails with 'Invalid count argument' rather than guessing at addresses
- 5. Pick count for small fixed sets of nearly identical instances (two AZs, a boolean flag); anything with stable per-member names belongs to for_each
Interview Tips
- • "How do you reference the second of three counted instances?" — `aws_instance.web[1].private_ip`, and add that `aws_instance.web` alone is a tuple; this separates index-fluent candidates from those who only copy examples
- • "You deleted the middle subnet from a count-fed list — what does plan do?" — walk through the positional rewrite: surviving indices take the new values, the dropped index is destroyed; then say you would append, or migrate to for_each with moved blocks
- • "Why did my plan fail with Invalid count argument?" — count is not known at apply time, addresses cannot be built; name the three fixes in order of preference and flag `-target` as a workaround that leaves partial state
- • "When is count the right choice?" — near-identical instances, small fixed count, known before plan; immediately contrast with a map of named servers to show you know when the answer flips to for_each
Cheat Sheet
The count Meta-argument
Addressing
| Form | Meaning |
|---|---|
aws_instance.web |
the tuple of all instances (iterate or splat it) |
aws_instance.web[0] |
one instance — this is its identity in state |
aws_instance.web[1].private_ip |
an attribute of that instance |
count.index |
zero-based index inside the expanded block |
module.net[1].vpc_id / data.aws_ami.x[0].id |
same shape on modules and data blocks |
Edits to the list feeding count
| Change | Plan |
|---|---|
| append | create new last index only |
| drop last | destroy last index only |
| insert / delete / reorder mid-list | neighbours rewritten, wrong instance destroyed |
count = 0 |
destroys every instance; references to [0] then raise Invalid index |
Plan-time rule
Error: Invalid count argument
The "count" value depends on resource attributes that cannot be
identified until apply, so Terraform cannot predict how many
instances will be created.
Fixes in order: plan-time variable → split the apply (-target only as last resort) → reshape into one resource with dynamic nested blocks. The same rule applies to for_each, where every key must be known, and impure functions (timestamp(), uuid(), bcrypt()) are rejected in both.