linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/Resources & data sources/tf-count-for-each

kb/resources ── Resources & data sources ── beginner

count and for_each: many resources from one block

count creates N identical resources by index 0..N-1. for_each creates resources keyed from a set or map. Rule of thumb: count for identical copies, for_each when each one has its own settings. When in doubt, use for_each.

view as markdown

Why you need them

Say you need five S3 buckets: logs-dev, logs-staging, logs-prod, data-dev, data-prod. You can write five resource blocks. Or you can write one with count or for_each.

This is about repeated resources. When things differ only by an index or a key, move them into count/for_each instead of duplicating code.

count is the simplest way

hcl
resource "aws_s3_bucket" "logs" {
  count  = 3
  bucket = "logs-bucket-${count.index}"
}

This creates three buckets:

  • aws_s3_bucket.logs[0] named logs-bucket-0
  • aws_s3_bucket.logs[1] named logs-bucket-1
  • aws_s3_bucket.logs[2] named logs-bucket-2

Inside the block you have the count.index variable, the current number from 0 to N-1.

To change the quantity, change the number. Terraform sees "it was 3, now it should be 5" and creates two more.

for_each, when each one has its own key

hcl
resource "aws_s3_bucket" "regional" {
  for_each = toset(["us", "eu", "ap"])
  bucket = "data-bucket-${each.key}"
}

This creates three buckets:

  • aws_s3_bucket.regional["us"] named data-bucket-us
  • aws_s3_bucket.regional["eu"] named data-bucket-eu
  • aws_s3_bucket.regional["ap"] named data-bucket-ap

You have each.key (the key) and each.value (the value). for_each accepts a set or a map:

hcl
# set, plain keys, each.key == each.value
for_each = toset(["us", "eu", "ap"])
# map, key-value pairs, each.key and each.value differ
for_each = {
  us = { region = "us-east-1", tier = "primary" }
  eu = { region = "eu-central-1", tier = "secondary" }
}
# then: each.key, "us"/"eu", each.value.region, each.value.tier

The main difference: what happens when the list changes

Say you have three resources. Remove the middle one.

With count:

hcl
count = 3   →   count = 2

Terraform reasons like this: "there were 3 elements at indexes 0, 1, 2. Now there should be 2." It removes the element at index 2. That holds even if you meant to remove element 1. Terraform does not understand that; it just shifts: index 2 disappears, and elements 0 and 1 stay the same.

The problem: when you remove an element from the middle of a list with count, Terraform recreates everything after it, because the indexes shift.

With for_each:

hcl
for_each = toset(["us", "eu", "ap"])  →  for_each = toset(["us", "ap"])

Terraform sees "there was a key eu, and it is gone now," and removes only aws_s3_bucket.regional["eu"]. It leaves the rest untouched.

Rule of thumb: if the list can change in the middle, use for_each. Use count only when the count grows or shrinks from the end, or when the elements really are interchangeable.

When to use which

  • count for three identical VMs in an auto-scaling-like setup, for fault tolerance through duplication, and for resources where order does not matter.
  • for_each for almost everything else. Buckets for different environments, IAM roles for different services, sg-rules with different ports.

When you are not sure, use for_each. It is more explicit and safer.

References and outputs

hcl
resource "aws_s3_bucket" "regional" {
  for_each = toset(["us", "eu", "ap"])
  bucket   = "data-${each.key}"
}
# one specific bucket
output "us_bucket_arn" {
  value = aws_s3_bucket.regional["us"].arn
}
# all ARNs as a list
output "all_arns" {
  value = [for k, b in aws_s3_bucket.regional : b.arn]
}
# all ARNs keyed by region
output "arns_by_region" {
  value = { for k, b in aws_s3_bucket.regional : k => b.arn }
}

Gotchas

  • You cannot use count and for_each together. Only one of them per resource.

  • count = 0 or for_each = [] is valid. Terraform creates zero resources. This is a legal way to disable a block conditionally: count = var.create ? 1 : 0.

  • for_each needs statically-knowable keys. The keys must be known at plan time. You cannot write for_each = data.aws_some_thing.dynamic when that data becomes known only after another resource is applied. Fix it with dependencies or static values.

  • Migrating from count to for_each means recreation. Terraform treats it as a different resource. The addresses change (x[0] → x["key"]). Fix it with a moved {} block (advanced).

  • Map keys are always strings. for_each = { 1 = "a", 2 = "b" } complains: numbers must be converted to strings: { "1" = "a", "2" = "b" }.

  • A large for_each means a slow plan. If your for_each runs over 500 elements, plan will hit the API 500 times. Think about scale.

§ команды

bash
terraform state list

Shows all resources with their indexes/keys: aws_s3_bucket.regional["us"], aws_s3_bucket.regional["eu"]...

bash
terraform state show 'aws_s3_bucket.regional["us"]'

One specific element from for_each. Note the single quotes around it: without them the shell eats the double quotes.

bash
terraform plan -target='aws_s3_bucket.regional["us"]'

A targeted plan for just one for_each element.

§ см. также

  • tf-resource-blockResource block: the main building block of TerraformA resource block tells Terraform "create this thing in the cloud." It has three parts: the resource type (what it is), the name (how you refer to it internally), and the arguments (how to configure it). Writing these blocks is what you spend 90% of your time doing in Terraform.
  • tf-referencesReferences in HCL: how to read aws_s3_bucket.demo.bucketAny value in HCL is reachable through an address: var.x, local.x, aws_s3_bucket.demo.arn, module.net.vpc_id, data.aws_region.current.name. Knowing this syntax is half of your productivity in Terraform.
  • hcl-typesData types in HCL: string, number, list, map, objectHCL supports primitives (string, number, bool) and complex types: list, set, map, tuple, object. This article covers the syntax of each one and the difference between the look-alikes (list vs tuple, map vs object).
  • tf-localslocals: computed internal nameslocals is a block with names visible only inside HCL (not input, not output). Useful for DRY: compute a common prefix or tag set once, then use it everywhere via local.x. Do not confuse with variable (input) and output (output).
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies