Skip to main content
Terraform · lesson 8 of 17
Terraform intermediate Lesson 8 of 17

Terraform count and for_each (repeat resources safely)

count keys instances by position, for_each by name. See the real plan when an item is removed from the middle, and the for_each error on unknown keys.

By SkillByExample · Updated

terraform count for_each meta-arguments

count and for_each both turn one resource block into many instances. The difference is how instances are identified. That decides what a later edit to the list destroys. Examples use Terraform 1.16.5 and hashicorp/random 3.9.1.

count: instances by position

terraform {
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.9"
    }
  }
}

variable "teams" {
  type    = list(string)
  default = ["alpha", "beta", "gamma"]
}

resource "random_pet" "by_index" {
  count  = length(var.teams)
  prefix = var.teams[count.index]
}

output "by_index" {
  value = random_pet.by_index[*].id
}
$ terraform apply -auto-approve -no-color | tail -12
random_pet.by_index[1]: Creation complete after 0s [id=beta-good-cub]
random_pet.by_index[2]: Creation complete after 0s [id=gamma-closing-cricket]

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

Outputs:

by_index = [
  "alpha-pleasant-horse",
  "beta-good-cub",
  "gamma-closing-cricket",
]

The instances are random_pet.by_index[0], [1] and [2]. The address records a position, not a team. [*] (the splat) collects one attribute from every instance into a list.

Removing an item from the middle

$ terraform plan -var 'teams=["alpha","gamma"]' -no-color
random_pet.by_index[2]: Refreshing state... [id=gamma-closing-cricket]
random_pet.by_index[1]: Refreshing state... [id=beta-good-cub]
random_pet.by_index[0]: Refreshing state... [id=alpha-pleasant-horse]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  - destroy
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # random_pet.by_index[1] must be replaced
-/+ resource "random_pet" "by_index" {
      ~ id        = "beta-good-cub" -> (known after apply)
      ~ prefix    = "beta" -> "gamma" # forces replacement
        # (2 unchanged attributes hidden)
    }

  # random_pet.by_index[2] will be destroyed
  # (because index [2] is out of range for count)
  - resource "random_pet" "by_index" {
      - id        = "gamma-closing-cricket" -> null
      - length    = 2 -> null
      - prefix    = "gamma" -> null
      - separator = "-" -> null
    }

Plan: 1 to add, 0 to change, 2 to destroy.

Changes to Outputs:
  ~ by_index = [
        "alpha-pleasant-horse",
      - "beta-good-cub",
      - "gamma-closing-cricket",
      + (known after apply),
    ]

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

You removed one team but the plan destroys two. gamma moved from index 2 to index 1, so Terraform rebuilds [1] with gamma’s prefix and deletes [2], the instance that really was gamma. With servers instead of pet names, the gamma team’s server gets deleted and rebuilt just because beta left.

for_each: instances by key

resource "random_pet" "by_key" {
  for_each = toset(var.teams)
  prefix   = each.key
}

output "by_key" {
  value = { for team, pet in random_pet.by_key : team => pet.id }
}
$ terraform apply -auto-approve -no-color | tail -8

Outputs:

by_key = {
  "alpha" = "alpha-sterling-mink"
  "beta" = "beta-cool-duckling"
  "gamma" = "gamma-evolving-monkey"
}
$ terraform plan -var 'teams=["alpha","gamma"]' -no-color | sed -n '/Terraform will/,/^Plan:/p'
Terraform will perform the following actions:

  # random_pet.by_key["beta"] will be destroyed
  # (because key ["beta"] is not in for_each map)
  - resource "random_pet" "by_key" {
      - id        = "beta-cool-duckling" -> null
      - length    = 2 -> null
      - prefix    = "beta" -> null
      - separator = "-" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Same edit, one destroy, and it’s the right one. The address random_pet.by_key["gamma"] doesn’t depend on where gamma appears in the list. for_each needs a set or a map, which is why toset() converts the list. A set also can’t contain duplicates, so two teams can’t share a key.

for_each keys must be known at plan time

resource "random_pet" "env" {
  count = 2
}

resource "random_id" "per_env" {
  for_each    = toset(random_pet.env[*].id)
  byte_length = 2
}
$ terraform plan -no-color

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform planned the following actions, but then encountered a problem:

  # random_pet.env[0] will be created
  + resource "random_pet" "env" {
      + id        = (known after apply)
      + length    = 2
      + separator = "-"
    }

  # random_pet.env[1] will be created
  + resource "random_pet" "env" {
      + id        = (known after apply)
      + length    = 2
      + separator = "-"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Error: Invalid for_each argument

  on main.tf line 15, in resource "random_id" "per_env":
  15:   for_each    = toset(random_pet.env[*].id)
    ├────────────────
    │ random_pet.env is tuple with 2 elements

The "for_each" set includes values derived from resource attributes that
cannot be determined until apply, and so Terraform cannot determine the full
set of keys that will identify the instances of this resource.

When working with unknown values in for_each, it's better to use a map value
where the keys are defined statically in your configuration and where only
the values contain apply-time results.

Alternatively, you could use the -target planning option to first apply only
the resources that the for_each value depends on, and then apply a second
time to fully converge.

The pet names don’t exist until apply, so Terraform can’t know the instance keys (the "..." in random_id.per_env["..."]) at plan time. The error message describes the fix: keys written in the config, and apply-time values in the map’s values.

resource "random_pet" "env" {
  for_each = toset(["dev", "prod"])
  prefix   = each.key
}

resource "random_id" "per_env" {
  for_each    = random_pet.env
  prefix      = "${each.value.id}-"
  byte_length = 2
}
$ terraform apply -auto-approve -no-color | grep -E "Creation|Apply"
random_pet.env["dev"]: Creation complete after 0s [id=dev-pet-sunfish]
random_pet.env["prod"]: Creation complete after 0s [id=prod-workable-gator]
random_id.per_env["prod"]: Creation complete after 0s [id=yMg]
random_id.per_env["dev"]: Creation complete after 0s [id=NIc]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Using a for_each resource as another resource’s for_each passes a map whose keys (dev, prod) come straight from the config, while each.value.id can stay unknown until apply. “Chaining” for_each resources like this is the standard pattern.

Practice

1. In the count version, add "delta" at the end of the list instead of removing from the middle. How many resources change?

Solution
$ terraform plan -var 'teams=["alpha","beta","gamma","delta"]' -no-color | grep -E '^  #|Plan:'
  # random_pet.by_index[3] will be created
Plan: 1 to add, 0 to change, 0 to destroy.

Appending to the end leaves every existing index where it was, so count handles it cleanly. count only causes damage when items are inserted or removed earlier in the list, and lists that only ever grow at the end are rare.

2. Make an optional resource that only exists when enable_audit is true, with an output that works either way.

Solution
variable "enable_audit" {
  type    = bool
  default = false
}

resource "random_pet" "audit" {
  count  = var.enable_audit ? 1 : 0
  prefix = "audit"
}

output "audit_name" {
  value = one(random_pet.audit[*].id)
}
$ terraform apply -var enable_audit=true -auto-approve -no-color | grep -E "Creation|audit_name"
  + audit_name = (known after apply)
random_pet.audit[0]: Creation complete after 0s [id=audit-grateful-squid]
audit_name = "audit-grateful-squid"

With the flag off, count = 0 creates nothing and one() of an empty list is null, so the output has no value instead of erroring. With it on, one() unwraps the single instance’s id.

3. Read gamma’s pet name in terraform console from the for_each resource, then try a numeric index.

Solution
$ echo 'random_pet.by_key["gamma"].id' | terraform console -no-color
"gamma-cool-caiman"
$ echo 'random_pet.by_key[1].id' | terraform console -no-color

Error: Invalid index

  on <console-input> line 1:
  (source code not available)

The given key does not identify an element in this collection value. An
object only supports looking up attributes by name, not by numeric index.

The suffix is random, so it differs from the run above. A for_each resource is a map from key to instance, so it has no position 1. Positional access was the whole problem with count, and for_each doesn’t allow it.

Next: the lifecycle block, which changes how Terraform replaces and destroys resources.

Frequently Asked Questions

When should I use for_each instead of count?
Use for_each whenever instances have an identity, like a team, environment or user name. count keys instances by position, so removing an item from the middle of a list shifts every later index and replaces those resources. count is still the right tool for 'zero or one' toggles and for identical copies where position genuinely doesn't matter.
Why does Terraform say the for_each set includes values that cannot be determined until apply?
for_each keys become part of each instance's address, so Terraform must know them at plan time. Keys built from a resource's computed attribute, like random_pet.env[*].id, are unknown until that resource is created. Use keys written in the configuration, and put apply-time results in the map's values instead.
How do I reference one instance created with for_each?
By its key in square brackets: random_pet.by_key["gamma"].id. Numeric indexes only work on count resources; a for_each resource is a map of objects, so random_pet.by_key[1] fails with 'Invalid index'.
How do I make a resource optional?
Set count = var.enabled ? 1 : 0. Refer to it with one(resource.name[*].attr), which returns the single value when the resource exists and null when it doesn't, so outputs and references don't fail in the disabled case.
Can I switch an existing resource from count to for_each?
Yes, without recreating anything, by adding a moved block per instance, for example from = random_pet.by_index[0] and to = random_pet.by_key["alpha"]. Without moved blocks, Terraform sees the old indexed addresses disappear and new keyed addresses appear, and plans to destroy and recreate them all.