Terraform · lesson 8 of 17
Beginner
7- 1 Terraform Fundamentals (IaC)
- 2 Terraform Workflow (init → validate → plan → apply)
- 3 Terraform State (tfstate, remote state, locking)
- 4 Terraform Variables and Outputs
- 5 Providers and Resources (the building blocks of Terraform)
- 6 Terraform Locals: Computed Values Without Repetition
- 7 Terraform Modules (reuse, composition, and clean structure)
Intermediate
7- 8 Terraform count and for_each (repeat resources safely)
- 9 The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes
- 10 Data Sources and Dependencies (read existing infrastructure)
- 11 sensitive = true: What It Hides, and What It Doesn't
- 12 Remote State Safety (locking, drift, and recovery)
- 13 Workspaces and Environments (dev/stage/prod patterns)
- 14 Terraform CI/CD with GitHub Actions (plan/apply)
Advanced
3Also part of Terraform
4Terraform 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
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.