Terraform for_each vs count: Which One to Use and Why

· 3 min read · Terraform & OpenTofu Tutorials

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. The difference is how each instance is identified in the 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]...

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 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"].

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:

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:

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}"
}

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):

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 and the lifecycle meta-argument.

#Terraform #OpenTofu #Hcl #AWS