Terraform count
What is Terraform count?
The Terraform count meta-argument lets a single resource, module, or data block produce more than one instance, or none at all. Rather than writing the same resource block three separate times to create three near-identical servers, you write it once and tell Terraform how many copies you want with the count argument.
Basic Terraform count syntax
The count meta-argument is available on resource, module, and data blocks, and you use it to set how many instances of a specified block to create. Set count to 3 on a resource block for aws_instance, and Terraform creates three separate infrastructure objects from that single resource block, rather than writing out three separate blocks by hand.
The count argument accepts a literal number, an input variable, or any expression that resolves to a whole number, making both fixed multiples and conditional creation possible from the same meta-argument.
Every instance you create using count gets its own address, built from the resource type, the resource name, and a zero-based, numeric index in square brackets, so Terraform (and you) can uniquely identify which instance is which without reading the whole configuration.
The first instance of a resource named server will be addressed as aws_instance.server[0], the second as aws_instance.server[1], and so on.
Here's a simple example:
This single resource block, once applied, produces three separate EC2 instances, each with its own resource attributes, and each addressable individually as aws_instance.server[0] through aws_instance.server[2].
The basic syntax stays the same wherever count appears, which is one reason it remains such a commonly used feature across Terraform projects and Terraform workflows of every size.
What Terraform count actually does
Terraform builds one infrastructure object for every number in the count range, indexing each one so it can be tracked, updated, and destroyed independently of the others.
Terraform then uses that index internally to decide which instance in state corresponds to which instance in configuration (a mechanism that causes the problems covered later in this entry).
Count shows up across several places in Terraform configurations, though resource, module, and data blocks remain by far the most common. ephemeral blocks and, more recently, action blocks and query list blocks are also valid places for count, though the mechanism is the same wherever it appears.
On a resource block, count creates multiple resource instances from a single resource definition. This use case is the most common, and it covers everything from a fixed number of identical EC2 instances to a handful of subnets spread across a VPC.
On a module block, count creates multiple copies of an entire module, so every resource declared inside that module gets duplicated once per count value.
This application is useful when the same collection of infrastructure (a VPC with its subnets and route tables, for example) needs to be stood up multiple times with only small variations between copies.
On a data block, count reads multiple existing resources rather than creating new ones. This example is encountered when a data source needs to look up more than one existing object, such as reading several existing subnets by a list of IDs, rather than a single resource at a time.
Wherever count is set, Terraform automatically makes an index variable called count.index available inside that block, which is the mechanism most people mean when they talk about the Terraform count index. It holds the position of the current resource instance in the sequence, starting at 0 for the first instance and counting up from there.
This variable turns a set of otherwise identical resources into a set of distinct ones. Without it, three server instances created by count would be indistinguishable except by their internal Terraform address.
With count.index folded into a name or tag, each one gets a label that matches its position and can be read by a human.
Applying this configuration creates three instances tagged server-0, server-1, and server-2. The count.index variable can be used anywhere inside the block, not just in tags.
It works equally well in a name field, a CIDR block, an availability zone lookup, or any other place a resource needs a value that varies by position.
An example of Terraform count in action
Differences start to appear when you need to reference something the module outputs.
A counted module is really a list of module instances rather than a single module, so any output has to be read with an index, just like a counted resource. Referencing module.app.url will not work once count is set on the module block. It has to be module.app[0].url, module.app[1].url, and so on for each instance.
Inside the module block itself, count.index works exactly the way it does on a resource block, which makes it possible to pass each copy of the module a distinct name, tag, or configuration value. The module's own source code never sees count.index. It only receives the values the block passes in."
An equally common use of count has nothing to do with module outputs at all. Because count accepts any whole-number expression, setting it to a ternary that evaluates to either 1 or 0 turns an entire resource on or off, based on a variable, a feature flag, or an environment check.
Setting count to var.enable_monitoring ? 1 : 0 creates exactly one instance of the resource when enable_monitoring is true, and creates zero instances, meaning the resource does not exist at all, when it is false.
That variable's default value is false, so the monitoring stack simply doesn't exist until someone turns it on.
Flip enable_monitoring to true, and the next terraform plan shows one instance to add. Flip it back to false, and that same instance shows up for destruction instead.
Some infrastructure only belongs in certain environments, like a backup instance that's only needed in production, or a monitoring stack you'd skip in a short-lived test environment.
Common mistakes users make with Terraform count
The main issue with count is that it does not track instances by anything durable, instead tracking them purely by their numeric position in a list, an index that only means something in relation to the other indexes around it.
As soon as something is removed from the middle of it, or reordered, you'll run into issues because every index after the change point shifts down by one, and Terraform has no way to tell that the resource at index 1 today is meant to be the same resource that was at index 2 yesterday. All it sees is that index 0 and index 1 now hold different configurations, and that index 2 no longer exists, so the plan changes the first two and destroys the third.
Here is an example with three named users created from a list:
This code creates aws_iam_user.team[0] for Alice, aws_iam_user.team[1] for Bob, and aws_iam_user.team[2] for Carol.
Remove Alice from the list, leaving only Bob and Carol, and the index positions shift exactly the way a person reading the list would expect. Bob moves from index 1 to index 0, and Carol moves from index 2 to index 1. The problem is what Terraform makes of that shift.
Terraform does not see two IAM users that stayed the same and simply moved position. It sees index 0 changing from Alice to Bob, index 1 changing from Bob to Carol, and index 2 disappearing entirely.
The resulting execution plan destroys and recreates Bob and Carol's resources under their new indexes, as well as removing Alice's, even though the actual intent was to remove one user and leave the other two completely untouched.
For infrastructure resources with any real state attached – e.g., an IAM user with existing access keys, a database holding data, or a load balancer with an established DNS record – this is unnecessary destruction and the recreation of resources that were never meant to change.
The nearest alternatives to Terraform count
One constraint applies no matter which block type count is set on. Count and for_each cannot be used together on the same block. Terraform raises an error if both meta-arguments appear on the same resource, module, or data block, so the choice has to be made per block, not layered on top of each other.
The clearest way to decide between them is working out whether or not the instances are fungible: interchangeable enough that it genuinely wouldn't matter which one got destroyed if the count dropped by one.
A fixed pool of otherwise-identical worker nodes is fungible in that sense. A named user, a named environment, a specific region, or a security group tied to one application is not, and that's exactly the boundary for_each was built to track.
Use for_each whenever resources need a stable identity that isn't just a position in a list.
Rewriting the same users example with for_each keyed on a set removes the index dependency entirely. Each user is now tracked by their own name rather than their position, so removing Alice from the list leaves Bob and Carol's resources completely untouched.
Stategraph's perspective on Terraform count
Reading a plan carefully before applying it is the only real safeguard Terraform gives you against an index shift like the one above, and it's easy to miss on a large state with a lot of other changes happening in the same apply.
It is exactly this kind of change Stategraph makes visible.
Stategraph maintains a graph of how every resource in state actually connects, so an index-driven shift shows up clearly as part of the blast radius of a change, rather than being buried in a long, undifferentiated plan output. Stategraph's own state layer makes that concrete rather than abstract: every resource instance it tracks carries an index_key value (the same count.index or for_each key Terraform itself assigned), stored as a queryable column rather than a line buried in JSON.
Query the instances table for index_key values, and an index shift stops being something you have to notice by eye, instead becoming a row in a result set
Rather than replacing Terraform's own state or plan output, Stategraph adds a layer of structure on top of both, so a change like an index shift is something you can see before running apply, not something you discover after the fact. If you want to see how that works against your own state, Stategraph's Inventory is a good place to start, or try Stategraph free and run the same query against your own state.
Related Terraform terms
- Terraform map variables: The
for_each-over-a-map pattern is the direct replacement for a length-drivencountlist once instances need names instead of positions. - How to use the Terraform
movedblock to refactor safely: Migrating an existingcount-based resource or module tofor_eachneeds exactly themovedblocks this covers, so state doesn't destroy and recreate everything mid-migration. - Blast radius in Terraform: The index shift covered above is exactly a blast-radius scenario in miniature, one small reorder cascading into a plan full of unrelated destroys.
- Terraform state list: Filtering state shows exactly the indexed addressing
countproduces (aws_instance.bar[0],[1], and so on) alongside the keyed addressingfor_eachproduces instead.