Terraform Count and For Each Creating Multiple Resources

Writing one resource block per server works fine when you need two servers. It does not work when you need twenty. Terraform provides two meta-arguments — count and for_each — that let a single resource block create multiple resources. This topic explains both, when to use each one, and how to reference specific instances.

The Problem: Repetitive Resource Blocks

Without count or for_each, creating three identical web servers requires three separate resource blocks:

resource "aws_instance" "web_1" { ... }
resource "aws_instance" "web_2" { ... }
resource "aws_instance" "web_3" { ... }

This does not scale. It also creates three separately named resources that are hard to manage together. Count and for_each turn this into a single, manageable block.

Using count

The count meta-argument tells Terraform how many copies of a resource to create. Terraform creates them all from the same block.

resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "web-server-${count.index}"
  }
}

This creates three EC2 instances. Inside the block, count.index gives the current iteration number starting from 0: 0, 1, 2.

Referencing count Instances

# All instances as a list
aws_instance.web[*].public_ip

# A specific instance (index 0)
aws_instance.web[0].id

# Second instance
aws_instance.web[1].id

Using count with a Variable

variable "server_count" {
  type    = number
  default = 3
}

resource "aws_instance" "web" {
  count         = var.server_count
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

Change server_count from 3 to 5, run apply, and Terraform adds two more instances. Reduce it to 1, and Terraform destroys two.

The Problem with count: Index-Based Identity

Count identifies resources by index (0, 1, 2). This causes a painful problem when you remove an item from the middle of a list.

Diagram: The Index Shift Problem

Before removal:
  web[0] = server-A
  web[1] = server-B   <-- you delete this
  web[2] = server-C

After removal:
  web[0] = server-A   (unchanged)
  web[1] = server-C   (shifted from index 2)

Terraform sees:
  web[1] changed (server-B → server-C) → DESTROY and RE-CREATE server-C!
  web[2] removed → DESTROY this index

Removing server-B causes server-C to be destroyed and recreated unnecessarily — because its index changed. This is where for_each excels.

Using for_each

The for_each meta-argument creates one resource instance per item in a map or set. Each instance is identified by a unique key — not by a number. This makes it immune to the index shift problem.

variable "servers" {
  type = map(string)
  default = {
    web    = "t3.micro"
    api    = "t3.small"
    worker = "t3.medium"
  }
}

resource "aws_instance" "app" {
  for_each      = var.servers
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = each.value

  tags = {
    Name = each.key
  }
}

Inside the block, each.key is the map key ("web", "api", "worker") and each.value is the map value ("t3.micro", etc.).

Referencing for_each Instances

# A specific instance by key
aws_instance.app["web"].public_ip
aws_instance.app["api"].id

# All instances as a map
aws_instance.app

for_each with a Set of Strings

resource "aws_iam_user" "team" {
  for_each = toset(["alice", "bob", "carol"])
  name     = each.key
}

The toset() function converts a list to a set, removing duplicates and making it suitable for for_each.

count vs for_each — When to Use Which

SituationUse
Creating N identical resources (same config, just more)count
Creating resources with unique names, sizes, or configsfor_each
The list of items might change (items could be removed)for_each
Resources are truly interchangeable (load balancer backends)count

When in doubt, prefer for_each. It is safer and more explicit about which instance is which.

Key Points

  • count creates multiple resource instances identified by index (0, 1, 2…); use count.index inside the block.
  • Removing a middle item from a count list shifts indexes and can cause unexpected destroy-and-recreate operations.
  • for_each creates instances identified by a unique string key; use each.key and each.value inside the block.
  • for_each is immune to the index shift problem because keys are stable even when items are removed.
  • Use toset() to convert a list of strings to a set before passing it to for_each.

Leave a Comment