Terraform for_each vs count: Which One to Use and Why
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]...
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"].
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:
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:
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 {
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_eachwith a map or a set. - Need a number of identical items that nobody will remove from the middle?
countis acceptable.
Continue with dynamic blocks and the lifecycle meta-argument.