| 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.
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.
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:
mkdir -p ~/terraform-labs/terraform-count-for-each/basic-count
cd ~/terraform-labs/terraform-count-for-each/basic-countWrite a three-instance configuration:
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
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample 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:
terraform state listterraform_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:
mkdir -p ~/terraform-labs/terraform-count-for-each/conditional-count
cd ~/terraform-labs/terraform-count-for-each/conditional-countWrite the conditional resource:
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
}
EOFInitialize and apply with the default enabled = true:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample 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:
terraform plan -var='enabled=false' -input=false -no-colorSample 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:
mkdir -p ~/terraform-labs/terraform-count-for-each/basic-foreach
cd ~/terraform-labs/terraform-count-for-each/basic-foreachWrite keyed instances from a set:
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]
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample output:
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
server_inputs = [
"api",
"db",
"web",
]State addresses use string keys:
terraform state listterraform_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:
mkdir -p ~/terraform-labs/terraform-count-for-each/map-foreach
cd ~/terraform-labs/terraform-count-for-each/map-foreachWrite a map-driven configuration where keys and values differ:
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 }
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample 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:
for_each = {
web = { tier = "frontend", port = 80 }
api = { tier = "backend", port = 8080 }
}
input = each.value.tierWhen your source data is a list of objects with unique names, build a map first:
{ 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():
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:
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:
mkdir -p ~/terraform-labs/terraform-count-for-each/index-shift-count
cd ~/terraform-labs/terraform-count-for-each/index-shift-countWrite configuration driven by a four-element list:
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 }
}
EOFInitialize and apply the four instances:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample output:
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.Remove the middle element by editing the list default to ["one", "two", "three"]:
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 }
}
EOFPlan after the list shrinks — watch what happens to index 2 and 3:
terraform plan -input=false -no-colorSample 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:
mkdir -p ~/terraform-labs/terraform-count-for-each/index-shift-foreach
cd ~/terraform-labs/terraform-count-for-each/index-shift-foreachStart with a set containing four members:
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 }
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample output:
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.Remove "remove-me" from the set default:
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 }
}
EOFPlan shows only the removed key is destroyed:
terraform plan -input=false -no-colorSample 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:
mkdir -p ~/terraform-labs/terraform-count-for-each/modules-demo/child
cd ~/terraform-labs/terraform-count-for-each/modules-demoWrite a minimal child module:
cat > child/main.tf <<'EOF'
variable "name" {
type = string
}
resource "terraform_data" "this" {
input = var.name
}
output "input" {
value = terraform_data.this.input
}
EOFCall it once per service name with for_each:
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 }
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample output:
Outputs:
module_inputs = {
"api" = "api"
"worker" = "worker"
}count on a module uses the same count.index semantics:
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:
data.<type>.<name>[0] # count
data.<type>.<name>["key"] # for_eachUse 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:
mkdir -p ~/terraform-labs/terraform-count-for-each/errors
cd ~/terraform-labs/terraform-count-for-each/errorsWrite a minimal root module and initialize:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
EOFRun init in the errors directory:
terraform init -input=falseList passed directly to for_each
Write a list-typed variable into for_each:
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
}
EOFValidate surfaces the type mismatch:
terraform validate -no-colorError: 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:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
input = each.value
}
EOFRun validate to confirm each is rejected without for_each:
terraform validate -no-colorError: each.value cannot be used in this context
A reference to "each.value" has been used in a context in which it is
unavailableeach exists only inside blocks that declare for_each.
count.index outside count
Reference count.index on a resource without count:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
input = "idx-${count.index}"
}
EOFValidate flags count.index on a non-counted resource:
terraform validate -no-colorError: 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:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "x" {
count = 2
for_each = toset(["a"])
input = "bad"
}
EOFValidate rejects both meta-arguments on one block:
terraform validate -no-colorError: Invalid combination of "count" and "for_each"
The "count" and "for_each" meta-arguments are mutually-exclusiveRemove 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:
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 || trueReferences
- Resources — Meta-arguments
countandfor_each - The
for_eachmeta-argument - Module blocks —
countandfor_each - Provider configuration
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.

