Terraform count and for_each

By Vishvesh Patel · DevOps EngineerUpdated September 13, 2026

How Terraform creates multiple resources from one block. Why count uses positional identity, why that destroys things, and when each meta-argument is correct.

Two meta-arguments create multiple instances from one resource block. They look interchangeable and are not, and choosing wrongly is one of the most common ways to destroy infrastructure by accident.

The difference is how instances are identified.

countfor_each
TakesA numberA map or set of strings
Identifies instances byPosition ([0], [1])Key (["web"], ["api"])
Iteratorcount.indexeach.key, each.value
Removing a middle itemShifts everything after itAffects only that item

count

resource "aws_instance" "worker" {
  count         = 3
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"

  tags = { Name = "worker-${count.index}" }
}

Three instances, addressed aws_instance.worker[0] through [2].

Reference them individually by index, or all at once with a splat:

aws_instance.worker[0].id
aws_instance.worker[*].id      # a list of all IDs

The problem with count

This is the part that matters, and it is worth understanding precisely.

variable "buckets" {
  default = ["logs", "artifacts", "backups", "reports"]
}

resource "aws_s3_bucket" "this" {
  count  = length(var.buckets)
  bucket = var.buckets[count.index]
}

State now holds:

[0] = logs
[1] = artifacts
[2] = backups
[3] = reports

Remove "artifacts" from the middle of that list and everything shifts down:

[0] = logs          unchanged
[1] = backups       was artifacts  → destroy and recreate
[2] = reports       was backups    → destroy and recreate
[3] = (gone)        was reports    → destroy

One removal, three resources destroyed. Terraform is behaving correctly, index [1] genuinely changed from artifacts to backups, but the result is that deleting one bucket destroys two unrelated ones.

On stateless resources this is wasteful. On databases, volumes, or anything holding data, it is an outage.

for_each

for_each keys instances by a stable string instead:

resource "aws_s3_bucket" "this" {
  for_each = toset(["logs", "artifacts", "backups", "reports"])
  bucket   = each.key
}

State now holds:

["logs"]       ["artifacts"]       ["backups"]       ["reports"]

Remove "artifacts" and exactly one resource is destroyed. Everything else is untouched, because nothing else's key changed.

With a map

A map is the more useful shape, because the key names the instance and the value carries its configuration:

variable "subnets" {
  type = map(object({
    cidr = string
    az   = string
  }))
  default = {
    public-a  = { cidr = "10.0.1.0/24", az = "eu-west-1a" }
    public-b  = { cidr = "10.0.2.0/24", az = "eu-west-1b" }
  }
}

resource "aws_subnet" "this" {
  for_each          = var.subnets
  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az

  tags = { Name = each.key }
}

Reference by key: aws_subnet.this["public-a"].id. For all of them, use a for expression. The splat syntax that works with count does not apply, because for_each produces a map rather than a list:

[for s in aws_subnet.this : s.id]
values(aws_subnet.this)[*].id

Converting a list

for_each needs a map or a set, so a list must be converted:

for_each = toset(var.names)                            # list of strings
for_each = { for s in var.subnets : s.name => s }      # list of objects

Never key on the index. { for i, s in var.subnets : i => s } reintroduces exactly the positional-identity problem for_each exists to solve.

When count is still right

Two cases.

Conditional creation: still the idiomatic pattern:

resource "aws_s3_bucket" "logs" {
  count  = var.enable_logging ? 1 : 0
  bucket = "${var.name}-logs"
}

Reference it as aws_s3_bucket.logs[0], and handle the zero case when referencing, one(aws_s3_bucket.logs[*].id) returns the value or null rather than erroring.

A genuine quantity of identical things, where the index carries no meaning and nothing distinguishes one from another. Three interchangeable workers, for instance.

Everything else should be for_each.

Migrating from count to for_each

Changing the meta-argument changes every state address, so Terraform plans to destroy and recreate everything. Use moved blocks:

moved {
  from = aws_s3_bucket.this[0]
  to   = aws_s3_bucket.this["logs"]
}

One per instance. Tedious, but it converts a rebuild into a no-op. Run a plan afterwards and confirm it is empty before applying.

Explore further in this section

Sub-topicWhat you will learn
countcount.index, conditional creation, splat expressions
for_eachMaps, sets, each.key and each.value, referencing

Quick reference

TaskSyntax
N identical resourcescount = 3
Conditional resourcecount = var.enabled ? 1 : 0
From a list of stringsfor_each = toset(var.names)
From a mapfor_each = var.config_map
List of objects to mapfor_each = { for s in var.list : s.name => s }
Reference by indexaws_x.this[0].id
Reference by keyaws_x.this["name"].id
All IDs (count)aws_x.this[*].id
All IDs (for_each)[for v in aws_x.this : v.id]
Safe optional referenceone(aws_x.this[*].id)

Final Thoughts

Default to for_each. Use count for conditional creation and for genuinely interchangeable quantities, and nowhere else. The question worth asking before choosing is simple: could an item ever be removed from the middle? If yes, positional identity will eventually destroy something you did not intend, and no amount of care at apply time will catch it.


Frequently Asked Questions

What is the difference between count and for_each in Terraform?

count takes a number and identifies instances by position, [0], [1], [2]. for_each takes a map or set and identifies them by key, ["web"], ["api"]. That difference in identity determines what happens when the collection changes: with count, removing an item shifts every subsequent index and causes Terraform to destroy and recreate unrelated resources; with for_each, each instance is bound to its key, so removing one affects only that one.

Why did removing one item from my list destroy several resources?

Because you used count, which identifies resources by their position in the list. Removing the second of four items means index [1] now holds what used to be at [2], [2] holds what was at [3], and [3] no longer exists, so Terraform correctly plans to change two resources and destroy one. Switching to for_each with stable keys prevents this entirely, since identity no longer depends on ordering.

Should I always use for_each instead of count?

Nearly always, with two exceptions. Conditional creation reads more clearly as count = var.enabled ? 1 : 0, and a genuine quantity of interchangeable resources, three identical workers where nothing distinguishes them, is a legitimate use of count. Anything driven by a list that could gain or lose entries should use for_each.

How do I use for_each with a list?

You cannot directly, because for_each requires stable keys and a list has none. Convert a list of unique strings with toset(var.names), after which each.key and each.value are both the string. Convert a list of objects with a for expression keyed on a stable field, such as { for s in var.subnets : s.name => s }. Never key on the list index, which reintroduces the positional problem.

How do I create a resource conditionally in Terraform?

count = var.enabled ? 1 : 0. Zero instances means the resource is not created. Reference it as resource.name[0], and use one(resource.name[*].id) when the reference must tolerate the resource not existing, since indexing directly errors when count is zero. The same effect can be achieved with for_each over a map that is empty when disabled, which avoids index-based referencing entirely.

How do I convert existing resources from count to for_each?

Add a moved block for each instance, mapping the old indexed address to the new keyed one, from = aws_s3_bucket.this[0], to = aws_s3_bucket.this["logs"]. Without them Terraform sees every address change and plans a full destroy and recreate. It is tedious for a large set, but it turns a rebuild into a no-op. Always run a plan afterwards and confirm it reports no changes before applying.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Terraform Playground

Run init, plan, apply and destroy against simulated infrastructure.

When it breaks

Related Resources