Terraform count vs for_each with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user; sudo only if Terraform is not installed yet
Scope Terraform count and for_each meta-arguments — numeric vs keyed instances, count.index, conditional count, each.key and each.value, maps and sets, list conversion, state addresses, index-shift demonstration, module repetition, data source note, provider limitation, decision table, and common errors. Does not cover dynamic blocks, for expressions in depth, module design, or provider alias creation.
Related guides Terraform resource block
Terraform expressions
Terraform variables
Terraform state
Terraform Associate certification course

count and for_each are Terraform meta-arguments for creating multiple instances of supported blocks. On resources, count creates numeric-indexed instances while for_each creates keyed instances. They look similar in configuration, but they assign different instance identities in state — and that difference drives what happens when your input collection changes.

text
count     → numeric instances   terraform_data.server[0]
for_each  → keyed instances     terraform_data.server["web"]

Neither choice is universally better. count fits fixed numeric repetition and simple conditional creation. for_each fits stable names, per-instance values, and collections whose membership changes. The index-shift lab later shows why that distinction matters in practice.

Each hands-on demo uses its own subdirectory under ~/terraform-labs/terraform-count-for-each/ so state from one exercise does not contaminate the next.

NOTE
Use the Terraform lab environment on Ubuntu. Run terraform init in each new directory before your first plan. Examples use the built-in terraform_data resource so you do not need cloud credentials.

How count and for_each differ

Both meta-arguments turn one resource block into many instances. Terraform records each instance separately in state under a distinct address.

Meta-argument Input type Instance address Identity
count Non-negative number resource.name[0], [1], … Numeric index
for_each Map or set of strings resource.name["key"] String key

A single block uses one meta-argument — never both. Terraform rejects a block that sets count and for_each together.

for_each keys and set members must be known during planning. You cannot drive for_each from resource attributes that stay unknown until apply — Terraform needs the instance map before remote operations begin. The same rule applies to count: its value must also be known during planning and cannot depend on a resource attribute that remains unknown until apply.


Terraform count

count tells Terraform how many numeric-indexed instances to create. Set it to a whole number — commonly length(var.list) or a literal. The count value must be known during planning; it cannot depend on a resource attribute that remains unknown until apply.

Create multiple instances

Create an isolated directory for the basic count demo:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/basic-count
cd ~/terraform-labs/terraform-count-for-each/basic-count

Write a three-instance configuration:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "server" {
  count = 3

  input = "server-${count.index}"
}

output "server_inputs" {
  value = terraform_data.server[*].input
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

server_inputs = [
  "server-0",
  "server-1",
  "server-2",
]

List state addresses — each instance uses a bracketed index:

bash
terraform state list
output
terraform_data.server[0]
terraform_data.server[1]
terraform_data.server[2]

count = 3 created three instances at indexes 0, 1, and 2. The index is part of the resource identity in state.

Use count.index

Inside a counted block, count.index is the zero-based position of the current instance. The lab above uses it in input = "server-${count.index}" — with count = 3, indexes run 0, 1, 2.

You can also reference another counted resource by index — aws_instance.web[count.index].id — when two counted resources stay aligned by position.

count.index exists only inside blocks that declare count. Using it elsewhere triggers a validation error (covered in Common count and for_each errors).

Conditional creation with count

A common pattern creates zero or one instance from a boolean. Use a separate directory so this demo does not share state with the three-server exercise:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/conditional-count
cd ~/terraform-labs/terraform-count-for-each/conditional-count

Write the conditional resource:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

variable "enabled" {
  type    = bool
  default = true
}

resource "terraform_data" "optional" {
  count = var.enabled ? 1 : 0

  input = "feature-on"
}

output "optional_input" {
  value = length(terraform_data.optional) > 0 ? terraform_data.optional[0].input : null
}
EOF

Initialize and apply with the default enabled = true:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

optional_input = "feature-on"

count = var.enabled ? 1 : 0 is the classic conditional-resource idiom. When enabled is false, Terraform creates no instances — the resource type becomes an empty list in expressions.

References to a counted resource must account for zero instances. The output uses length(terraform_data.optional) > 0 before reading [0]. See Terraform expressions for splat and try() alternatives.

Pass enabled=false on the command line to plan the teardown without a second variable block:

bash
terraform plan -var='enabled=false' -input=false -no-color

Sample output:

output
# terraform_data.optional[0] will be destroyed
  # (because index [0] is out of range for count)

-var overrides the default for this plan only — no duplicate variable "enabled" declaration is needed.


Terraform for_each

for_each creates one instance per key in a map or set of strings.

Use each.key and each.value

Inside a for_each block, Terraform exposes:

  • each.key — the instance key (map key or set element)
  • each.value — the map value (for a set, same as the key)

Create a directory for the set-based demo:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/basic-foreach
cd ~/terraform-labs/terraform-count-for-each/basic-foreach

Write keyed instances from a set:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "server" {
  for_each = toset(["web", "api", "db"])

  input = each.value
}

output "server_inputs" {
  value = [for k, r in terraform_data.server : r.input]
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

server_inputs = [
  "api",
  "db",
  "web",
]

State addresses use string keys:

bash
terraform state list
output
terraform_data.server["api"]
terraform_data.server["db"]
terraform_data.server["web"]

For a set of strings, each.key and each.value are identical — both are the set element.

for_each with maps

Maps are the natural for_each input when each instance needs its own configuration value. Use another directory for the map demo:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/map-foreach
cd ~/terraform-labs/terraform-count-for-each/map-foreach

Write a map-driven configuration where keys and values differ:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

locals {
  roles = {
    web = "frontend"
    api = "backend"
  }
}

resource "terraform_data" "role" {
  for_each = local.roles

  input = "${each.key} -> ${each.value}"
}

output "role_inputs" {
  value = { for k, r in terraform_data.role : k => r.input }
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Outputs:

role_inputs = {
  "api" = "api -> backend"
  "web" = "web -> frontend"
}

each.key is the map key (web, api). each.value is the corresponding value (frontend, backend). State addresses follow keys: terraform_data.role["web"].

For richer per-instance settings, map values can be objects:

hcl
for_each = {
  web = { tier = "frontend", port = 80 }
  api = { tier = "backend",  port = 8080 }
}

input = each.value.tier

When your source data is a list of objects with unique names, build a map first:

hcl
{ for svc in var.services : svc.name => svc }

for_each with sets

for_each accepts a set of strings. Convert a list of unique names with toset():

hcl
for_each = toset(var.service_names)

Sets deduplicate values — toset(["api", "api"]) collapses to one instance. If you need duplicate values preserved, use a map with distinct keys instead.

Convert lists for for_each

for_each requires a map or a set of strings. Lists are not valid directly — Terraform does not implicitly convert them:

text
The given "for_each" argument value is unsuitable: the "for_each" argument
must be a map, or set of strings, and you have provided a value of type list
of string.

Common conversions:

Goal Approach
Unique string names toset(var.names)
Stable key per list item { for name in var.names : name => name }
Index-based identity Use count instead — accept index semantics

Do not confuse for_each (meta-argument on a block) with a for expression (value transformation inside an argument). for expressions belong in Terraform expressions; this lesson covers instance repetition only.


count vs for_each resource identity

This is the most important difference between the two meta-arguments. Count identity follows numeric position — when you remove a middle list element, indexes shift and Terraform may update or destroy instances you did not intend to touch. for_each identity follows the key — removing one member destroys only that keyed instance.

Whether a changed argument results in an in-place update or a replacement depends on the resource and provider schema. The lab below focuses on address and index shifting, which is the core reason to choose one meta-argument over the other.

Index shifts with count

Create an isolated directory for the count identity demo:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/index-shift-count
cd ~/terraform-labs/terraform-count-for-each/index-shift-count

Write configuration driven by a four-element list:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

variable "names" {
  type    = list(string)
  default = ["one", "two", "remove-me", "three"]
}

resource "terraform_data" "item" {
  count = length(var.names)

  input = var.names[count.index]
}

output "items" {
  value = { for i, r in terraform_data.item : i => r.input }
}
EOF

Initialize and apply the four instances:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Remove the middle element by editing the list default to ["one", "two", "three"]:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

variable "names" {
  type    = list(string)
  default = ["one", "two", "three"]
}

resource "terraform_data" "item" {
  count = length(var.names)

  input = var.names[count.index]
}

output "items" {
  value = { for i, r in terraform_data.item : i => r.input }
}
EOF

Plan after the list shrinks — watch what happens to index 2 and 3:

bash
terraform plan -input=false -no-color

Sample output:

output
# terraform_data.item[2] will be updated in-place
  ~ resource "terraform_data" "item" {
      ~ input  = "remove-me" -> "three"
    }

  # terraform_data.item[3] will be destroyed
  # (because index [3] is out of range for count)

Index 2 previously held "remove-me" but now receives "three" from the shortened list — Terraform updates it in place. Index 3 is destroyed because count dropped from four to three. Numeric identity followed position, not the string value.

Stable keys with for_each

Repeat the exercise with keyed instances in a separate directory:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/index-shift-foreach
cd ~/terraform-labs/terraform-count-for-each/index-shift-foreach

Start with a set containing four members:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

variable "names" {
  type = set(string)
  default = ["one", "two", "remove-me", "three"]
}

resource "terraform_data" "item" {
  for_each = var.names

  input = each.value
}

output "items" {
  value = { for k, r in terraform_data.item : k => r.input }
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Remove "remove-me" from the set default:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

variable "names" {
  type = set(string)
  default = ["one", "two", "three"]
}

resource "terraform_data" "item" {
  for_each = var.names

  input = each.value
}

output "items" {
  value = { for k, r in terraform_data.item : k => r.input }
}
EOF

Plan shows only the removed key is destroyed:

bash
terraform plan -input=false -no-color

Sample output:

output
# terraform_data.item["remove-me"] will be destroyed
  # (because key ["remove-me"] is not in for_each map)

one, two, and three keep the same state addresses. No unrelated instance receives an in-place update. Keyed identity tracked the string, not list position.


When to use count vs for_each

Reach for count when:

  • You need a fixed number of nearly identical instances (count = 3)
  • You want a simple conditional resource (count = var.enabled ? 1 : 0)
  • The numeric index is meaningful or acceptable — aligned replicas where position does not change often

Reach for for_each when:

  • Each instance needs a stable name in state (["web"], ["api"])
  • Values come from a map with per-key configuration
  • Collection membership changes — adding or removing one member should affect only that instance
  • You iterate a list of unique strings via toset() or a derived map

Avoid count when the driving collection is an ordered list that gains and loses members in the middle. Index shifts cause surprising updates and destroys, as the lab above demonstrated. for_each is usually the safer default for user-defined names, service catalogs, and role maps.

Decision table

Scenario Prefer
Three identical replicas, index does not matter count
Enable optional monitoring resource count = var.enabled ? 1 : 0
One resource per service name from a map for_each on the map
List of unique hostnames that may shrink or grow for_each = toset(var.hosts)
List order changes frequently for_each with explicit keys — not count
Need both count and for_each on one block Not supported — pick one

Where count and for_each are supported

count and for_each work on resource, data, and module blocks. Syntax and addressing rules are the same across block types.

Modules

Module blocks accept the same meta-arguments as resources:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/modules-demo/child
cd ~/terraform-labs/terraform-count-for-each/modules-demo

Write a minimal child module:

bash
cat > child/main.tf <<'EOF'
variable "name" {
  type = string
}
resource "terraform_data" "this" {
  input = var.name
}
output "input" {
  value = terraform_data.this.input
}
EOF

Call it once per service name with for_each:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

module "svc" {
  source   = "./child"
  for_each = toset(["api", "worker"])

  name = each.value
}

output "module_inputs" {
  value = { for k, m in module.svc : k => m.input }
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Outputs:

module_inputs = {
  "api" = "api"
  "worker" = "worker"
}

count on a module uses the same count.index semantics:

hcl
module "replica" {
  source = "./child"
  count  = 2

  name = "replica-${count.index}"
}

Module instance addresses follow the same bracket rules: module.svc["api"], module.replica[0]. This lesson shows syntax only — module composition patterns are out of scope.

Data sources

data blocks accept count and for_each with the same rules as resource blocks. Reference counted and keyed data sources with the same address pattern:

text
data.<type>.<name>[0]       # count
data.<type>.<name>["key"]   # for_each

Use the same decision criteria: count for numeric repetition, for_each for stable keys. Data sources only read remote objects — they do not create infrastructure. See Terraform data sources for query workflows.

Why provider configuration is different

In standard Terraform configuration (.tf files), provider configuration blocks do not accept count or for_each. Declare explicit provider configurations and aliases instead, then pass the alias into each resource that needs a non-default provider.

Terraform Stacks use a different provider configuration model where for_each can appear on Stack provider blocks. Stacks are outside this article's scope — the Associate course focuses on ordinary .tf modules.


Common count and for_each errors

The examples below run in ~/terraform-labs/terraform-count-for-each/errors/. Each writes a minimal broken configuration.

Create the error demo directory:

bash
mkdir -p ~/terraform-labs/terraform-count-for-each/errors
cd ~/terraform-labs/terraform-count-for-each/errors

Write a minimal root module and initialize:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
EOF

Run init in the errors directory:

bash
terraform init -input=false

List passed directly to for_each

Write a list-typed variable into for_each:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
variable "names" {
  type    = list(string)
  default = ["a", "b"]
}
resource "terraform_data" "x" {
  for_each = var.names
  input = each.value
}
EOF

Validate surfaces the type mismatch:

bash
terraform validate -no-color
output
Error: Invalid for_each argument

The given "for_each" argument value is unsuitable: the "for_each" argument
must be a map, or set of strings, and you have provided a value of type list
of string.

Wrap with toset() or build a map.

each used outside for_each

Reference each.value without declaring for_each:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
  input = each.value
}
EOF

Run validate to confirm each is rejected without for_each:

bash
terraform validate -no-color
output
Error: each.value cannot be used in this context

A reference to "each.value" has been used in a context in which it is
unavailable

each exists only inside blocks that declare for_each.

count.index outside count

Reference count.index on a resource without count:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
  input = "idx-${count.index}"
}
EOF

Validate flags count.index on a non-counted resource:

bash
terraform validate -no-color
output
Error: Reference to "count" in non-counted context

The "count" object can only be used in "module", "resource", and "data"
blocks, and only when the "count" argument is set.

Add count = … to the block, or remove the count.index reference.

count and for_each on the same block

Set both meta-arguments:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
  count    = 2
  for_each = toset(["a"])
  input = "bad"
}
EOF

Validate rejects both meta-arguments on one block:

bash
terraform validate -no-color
output
Error: Invalid combination of "count" and "for_each"

The "count" and "for_each" meta-arguments are mutually-exclusive

Remove one meta-argument.

Quick troubleshooting reference

Symptom Likely cause Fix
Invalid for_each argument (wrong type) List passed to for_each Use a map or toset() on unique strings
Invalid for_each argument (unknown values) Keys depend on values known only after apply Derive keys from configuration inputs known during planning
each.value cannot be used each referenced outside for_each block Add for_each or remove each reference
Reference to "count" in non-counted context count.index without count Add count or stop using count.index
Invalid combination of "count" and "for_each" Both set on one block Use only one meta-argument
Unexpected destroy/update after list edit count index shift Switch to for_each with stable keys
Fewer instances than list items with toset Duplicate strings collapsed Use a map with distinct keys

Cleanup

Destroy resources in every subdirectory you created:

bash
cd ~/terraform-labs/terraform-count-for-each/basic-count && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-count-for-each/conditional-count && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-count-for-each/basic-foreach && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-count-for-each/map-foreach && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-count-for-each/index-shift-count && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-count-for-each/index-shift-foreach && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-count-for-each/modules-demo && terraform destroy -auto-approve -input=false 2>/dev/null || true

References


Summary

count and for_each both create multiple instances from one block, but they assign different identities in state. count addresses instances by numeric index — terraform_data.server[0] — and count.index identifies position inside the block. for_each addresses instances by string key — terraform_data.server["web"] — with each.key and each.value available inside the block.

You practiced basic repetition, conditional creation with count = var.enabled ? 1 : 0, map-driven for_each, and set conversion with toset() — each in its own lab directory so state stayed isolated. The index-shift lab is the critical takeaway: removing a middle list element under count reassigns values to lower indexes and destroys the trailing instance, while removing one set member under for_each destroys only that key. Count identity follows numeric position; for_each identity follows the key.

Module and data blocks accept the same meta-arguments. In standard .tf configuration, provider blocks do not — declare explicit aliases instead. for_each keys must be known during planning, not derived from attributes that stay unknown until apply. When a list of unique names may change membership, prefer for_each with stable keys over count on length(var.list). For expression syntax used inside arguments, continue with Terraform expressions; for dynamic nested blocks built from collections, see the dynamic blocks lesson next in the course.


Frequently Asked Questions

1. What is the difference between Terraform count and for_each?

count creates a fixed number of resource instances addressed by numeric index, such as terraform_data.server[0]. for_each creates one instance per map or set element addressed by key, such as terraform_data.server["web"]. Choose count for simple numeric repetition or conditional creation; choose for_each when stable names matter or collection membership changes.

2. When should I use count in Terraform?

Use count when you need a fixed number of nearly identical instances, a simple on-or-off conditional resource with count = var.enabled ? 1 : 0, or when the numeric index itself is acceptable for identity. Avoid count when list order changes often, because removing a middle element shifts indexes and can update unrelated instances.

3. When should I use for_each in Terraform?

Use for_each when each instance needs a stable key, per-instance values from a map, or membership in a set that changes over time. Removing one key destroys only that instance. Convert lists to sets with toset() or build a map when you need distinct keys.

4. Can Terraform count and for_each be used together?

No. A single resource, data, or module block may use count or for_each, but not both. Pick one meta-argument per block to make instance count explicit.

5. Can for_each use a list in Terraform?

No. for_each requires a map or a set of strings. Lists are not valid directly. Convert a list of unique strings with toset(var.names) or build a map with distinct keys, such as { for name in var.names : name => name }.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)