# Terraform for_each vs count: Which One to Use and Why

> Learn the difference between the Terraform and OpenTofu count and for_each meta-arguments, with AWS examples, and why for_each avoids destroying resources.

- Source: https://www.itwonderlab.com/terraform-for-each-vs-count/
- Published: 2026-02-20
- Updated: 2026-02-20
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## When to use for_each and when to use count

`count` and `for_each` are **meta-arguments** that create several instances of the same resource, data source or module from a single block. Both work in Terraform and in OpenTofu, and both are part of [HCL](https://www.itwonderlab.com/hcl/). The difference is how each instance is identified in the [Terraform state](https://www.itwonderlab.com/terraform-state/), and that difference decides which one you should use.

### count: instances identified by a number

`count` creates `N` instances addressed as `resource.name[0]`, `resource.name[1]`...

```hcl title="count.tf"
variable "subnet_cidrs" {
  type    = list(string)
  default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}

resource "aws_subnet" "private" {
  count      = length(var.subnet_cidrs)
  vpc_id     = aws_vpc.main.id
  cidr_block = var.subnet_cidrs[count.index]

  tags = {
    Name = "private-${count.index}"
  }
}
```

The problem appears when the list changes. If you remove `10.0.1.0/24` from the middle of the list, the item that was at index 1 moves to index 0, and so on. [Terraform](https://www.itwonderlab.com/tutorials/terraform/) sees that `aws_subnet.private[0]` now has a different CIDR block and **destroys and recreates** it, and does the same with every following item.

### for_each: instances identified by a key

`for_each` accepts a map or a set of strings and addresses each instance by its key: `resource.name["key"]`.

```hcl title="for_each.tf"
variable "subnets" {
  type = map(string)
  default = {
    a = "10.0.1.0/24"
    b = "10.0.2.0/24"
    c = "10.0.3.0/24"
  }
}

resource "aws_subnet" "private" {
  for_each   = var.subnets
  vpc_id     = aws_vpc.main.id
  cidr_block = each.value

  tags = {
    Name = "private-${each.key}"
  }
}
```

Removing the key `b` only destroys `aws_subnet.private["b"]`. The other instances do not change because their keys do not change.

### Comparison

| | `count` | `for_each` |
|---|---|---|
| Input | A whole number | A map or a set of strings |
| Instance address | `name[0]`, `name[1]` | `name["key"]` |
| Removing an item in the middle | Re-indexes and recreates the following items | Only removes that item |
| Reference to current item | `count.index` | `each.key` and `each.value` |
| Best for | Create 0 or 1 instance (a switch), or identical copies | Collections of different items |

### Using count as a switch

The most common good use of `count` is to create a resource only when a condition is true:

```hcl title="count-switch.tf"
resource "aws_eip" "bastion" {
  count  = var.create_bastion ? 1 : 0
  domain = "vpc"
}
```

To reference it, use the splat or an index: `aws_eip.bastion[0].id`, or `one(aws_eip.bastion[*].id)` which returns `null` when there is no instance.

### Converting a list to a set for for_each

`for_each` does not accept lists. Convert them with `toset`, or build a map with a `for` expression:

```hcl title="list-to-for_each.tf"
variable "bucket_names" {
  type    = list(string)
  default = ["logs", "backups", "assets"]
}

resource "aws_s3_bucket" "this" {
  for_each = toset(var.bucket_names)
  bucket   = "ditwl-${each.key}"
}
```

> [!IMPORTANT]
> The keys of `for_each` must be known at plan time. If they depend on a resource that is created in the same run (for example an ID generated by AWS), Terraform fails with *"The for_each map includes keys derived from resource attributes that cannot be determined until apply"*. Use values that you define (names, CIDRs) as keys, not generated IDs.

### Migrating from count to for_each without destroying anything

Change the code to `for_each` and tell Terraform that the old addresses are now the new ones with `moved` blocks (see [import, moved and removed](https://www.itwonderlab.com/terraform-import-moved-removed/)):

```hcl title="moved.tf"
moved {
  from = aws_subnet.private[0]
  to   = aws_subnet.private["a"]
}

moved {
  from = aws_subnet.private[1]
  to   = aws_subnet.private["b"]
}
```

Run `plan` and check that it only reports the move, with nothing to destroy or create.

### Rule of thumb

- Need one instance or none? Use `count = var.enabled ? 1 : 0`.
- Need several different items? Use `for_each` with a map or a set.
- Need a number of identical items that nobody will remove from the middle? `count` is acceptable.

Continue with [dynamic blocks](https://www.itwonderlab.com/terraform-dynamic-blocks/) and the [lifecycle meta-argument](https://www.itwonderlab.com/terraform-lifecycle-meta-argument/).
