for_each and count in Terraform are both used to create multiple instances of a resource, but they operate on fundamentally different principles, and choosing the wrong one can lead to significant pain when your infrastructure needs to change.

Let’s see them in action. Imagine you want to create a few identical S3 buckets.

Using count:

resource "aws_s3_bucket" "example_count" {
  count = 3
  bucket = "my-unique-bucket-name-${count.index}"
  tags = {
    Environment = "Dev"
    ManagedBy   = "Terraform"
  }
}

Here, Terraform creates three S3 buckets. The count.index attribute is an integer starting from 0, so the buckets will be named my-unique-bucket-name-0, my-unique-bucket-name-1, and my-unique-bucket-name-2.

Now, using for_each with a set of strings:

resource "aws_s3_bucket" "example_for_each_set" {
  for_each = toset(["app-logs", "audit-trail", "temp-storage"])
  bucket = "my-unique-bucket-name-${each.value}"
  tags = {
    Environment = "Dev"
    ManagedBy   = "Terraform"
  }
}

This also creates three S3 buckets, but they’ll be named my-unique-bucket-name-app-logs, my-unique-bucket-name-audit-trail, and my-unique-bucket-name-temp-storage. Notice how each.value directly uses the elements from the set.

Using for_each with a map is even more powerful:

resource "aws_s3_bucket" "example_for_each_map" {
  for_each = {
    logs    = "app-logs"
    audit   = "audit-trail"
    temp    = "temp-storage"
  }
  bucket = "my-unique-bucket-name-${each.key}-${each.value}"
  tags = {
    Environment = "Dev"
    ManagedBy   = "Terraform"
  }
}

This will create buckets named my-unique-bucket-name-logs-app-logs, my-unique-bucket-name-audit-audit-trail, and my-unique-bucket-name-temp-temp-storage. The each.key refers to the map key, and each.value refers to the map value.

The core problem for_each and count solve is the need to provision multiple identical or near-identical resources without manually writing a block for each one. This is common for things like multiple EC2 instances, S3 buckets, security group rules, or even multiple subnets within a VPC.

The fundamental difference lies in how Terraform identifies and manages each instance. With count, Terraform uses an implicit, zero-based index. When you change the count value, Terraform re-evaluates based on that index. If you remove an item from the middle of a list you’re iterating over with count, Terraform will likely destroy and recreate resources because the indices of subsequent resources will shift. For example, if you had count = 3 and then changed it to count = 2, aws_s3_bucket.example_count[2] would be destroyed. If you had count = 3 and removed the first item (conceptually, by changing the source data to count = 2), aws_s3_bucket.example_count[0] and aws_s3_bucket.example_count[1] would be destroyed and recreated because their indices would change. This is problematic because it can lead to unintended resource destruction and recreation cycles.

for_each, on the other hand, uses explicit keys (from sets or maps) to identify each resource instance. When you use for_each, Terraform generates a unique key for each resource instance based on the input. For a set, the key is the value itself. For a map, the key is the map’s key. This means that if you add or remove an element from the middle of your for_each input, Terraform can precisely identify which instance needs to be created or destroyed without affecting others. For instance, if you have for_each = toset(["a", "b", "c"]) and change it to for_each = toset(["a", "c", "d"]), Terraform will destroy the instance associated with "b" and create a new instance for "d", leaving "a" and "c" untouched. This makes for_each much more resilient to changes.

The key difference is that count relies on the order and position of elements, while for_each relies on identity.

This is why for_each is generally preferred over count for creating multiple resources, especially when the source of your list/map might change in ways that don’t preserve the order or indices of all elements. for_each provides more stable and predictable resource management.

You should use count when the number of instances is dynamic and doesn’t map to a specific set of identifiers, or when you genuinely need to manage resources based on their sequential index, which is rare. For example, creating a dynamic number of EBS volumes where their specific attachment point isn’t tied to a stable identifier.

You should use for_each when you have a collection of distinct items (strings, numbers, maps) that you want to iterate over to create resources. Each item in the collection will correspond to a distinct resource instance. This is the most common scenario for creating multiple similar resources like S3 buckets, security group rules, or subnets.

The most surprising thing about for_each is how it handles changes to the iteration expression. If you use for_each = ["a", "b", "c"] (a list), Terraform still treats it like count internally for keying purposes, meaning changing the list order or inserting/deleting in the middle can cause unexpected recreation. To get the true benefit of for_each’s stable identity, you must use toset() on a list or, more commonly, iterate directly over a map.

The next concept to explore is how to manage complex configurations for resources created with for_each, often involving nested maps or dynamic blocks.

Want structured learning?

Take the full Terraform course →