lesson ── terraform-beginner ── ~12 мин ── 3 шагов
Sometimes you need N identical resources: three buckets for different
regions, five security groups for different ports. Copying resource
blocks by hand is painful and fragile. Terraform handles this with
count and for_each.
These two tools look alike, but there is an important difference that surfaces when the list changes. See tf-count-for-each.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
Create main.tf:
resource "aws_s3_bucket" "many" {count = 3
bucket = "linuxlab-count-${count.index}-${random_id.suffix.hex}" tags = {Index = tostring(count.index)
}
}
resource "random_id" "suffix" {byte_length = 4
}
Here:
count = 3, create three instances.count.index, the index of the current one (0, 1, 2).tostring(count.index), convert the number to a string for the tag.cd /home/student/tf-count
terraform init -input=false
terraform apply -auto-approve -input=false
In state you will see:
aws_s3_bucket.many[0]
aws_s3_bucket.many[1]
aws_s3_bucket.many[2]
If apply complains "duplicate bucket name": random_id.suffix needs to be substituted in. Check that the random_id resource is present in the HCL.
✓ Three buckets with indexes 0, 1, 2: count works.
Add one more resource to main.tf:
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "eu", "ap"])
bucket = "linuxlab-regional-${each.key}-${random_id.suffix.hex}" tags = {Region = each.key
}
}
Here:
for_each = toset([...]), create one for each element of the set.each.key, the current key ("us", "eu" or "ap").each.value, the same thing for a set; for a map it differs.terraform apply -auto-approve
State gains:
aws_s3_bucket.regional["us"]
aws_s3_bucket.regional["eu"]
aws_s3_bucket.regional["ap"]
These are not indexes, they are named keys. Much more stable when the list changes.
toset is required: for_each does not accept a plain list, only a set or a map. Drop the toset and it will fail.
✓ for_each created three buckets keyed by us, eu, ap.
OpenTofu keeps the CLI and state compatible with Terraform for the
commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On a first switch, though, make
a backup of the state and do a run on a feature branch, the
differences cluster in the newer features (variables in backend,
state encryption, OCI registry-backed modules). See
tf-opentofu-parity for the full matrix.
Right now you have 6 buckets: 3 from count, 3 from for_each. Let's remove the middle one in each case. Edit the HCL:
resource "aws_s3_bucket" "many" {count = 2 # was 3
bucket = "linuxlab-count-${count.index}-${random_id.suffix.hex}" tags = {Index = tostring(count.index)
}
}
resource "aws_s3_bucket" "regional" {for_each = toset(["us", "ap"]) # removed "eu"
bucket = "linuxlab-regional-${each.key}-${random_id.suffix.hex}" tags = {Region = each.key
}
}
Run plan:
terraform plan
What you will see:
aws_s3_bucket.many[2]. That is the last one by index, not the
middle. If you actually wanted the middle one gone, count does not
"understand" that.aws_s3_bucket.regional["eu"]. Precisely the one you took
out. us and ap are untouched.This is the key difference: for_each is more stable.
terraform apply -auto-approve
If plan shows destroy + create for count: that is expected, count.index shifts. For middle deletes, switch to for_each.
✓ Removed 2 buckets (one from each block), 4 remain. for_each did it cleanly.
If the resources really are identical and the count only grows or
shrinks from the end, count is fine. If each one has its own meaning
(region, name, environment): for_each. When in doubt, take for_each.
Refactoring count to for_each is a painful cost: you need moved {}
blocks or a manual state mv. Better to do it right from the start.
You created three buckets with count (addressed by index [0], [1], [2]) and three with for_each (addressed by key ["us"], ["eu"], ["ap"]). You saw that for_each is more stable when the list changes, it does not shift all the other indexes.
команды
terraform state listsee the indexes and keys of every elementterraform state show 'aws_s3_bucket.regional["us"]'one item from for_eachterraform plan -target='aws_s3_bucket.regional["eu"]'a targeted plan for a single elementконцепции