ЁЯПл The SchoolтА║ЁЯУЬ TerraformтА║ЁЯФБ рдзрдбрд╛ 04 тАФ Variables, outputs рдЖрдгрд┐ loops: рдПрдХ рдЖрд░рд╛рдЦрдбрд╛, рдЕрдиреЗрдХ рдЦреЛрд▓реНрдпрд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯФБ рдзрдбрд╛ 04 тАФ Variables, outputs рдЖрдгрд┐ loops: рдПрдХ рдЖрд░рд╛рдЦрдбрд╛, рдЕрдиреЗрдХ рдЦреЛрд▓реНрдпрд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: рдзрдбрд╛ 04 / 12 ┬╖ рдорд╛рдЧреЗ: lesson-03-plan-apply ┬╖ рдкреБрдвреЗ: lesson-05-state


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ03, рдЖрдгрд┐ рдЖрд░рд╛рдЦрдбрд╛ рдкреБрдиреНрд╣рд╛ рд╡рд╛рдкрд░рддрд╛ рдпреЗрдгреНрдпрд╛рдЬреЛрдЧрд╛ рдХрд░рдгрд╛рд░реЗ рднрд╛рд╖реЗрдЪреЗ рднрд╛рдЧ: input variables (рддреБрдореНрд╣реА рднрд░рддрд╛ рддреНрдпрд╛ рд░рд┐рдХрд╛рдореНрдпрд╛ рдЬрд╛рдЧрд╛), locals (рдПрдХрджрд╛ рдореЛрдЬрд▓реЗрд▓реНрдпрд╛ рдореВрд▓реНрдпрд╛рдВрд╕рд╛рдареА рдирд╛рд╡реЗ), outputs (рдЖрд░рд╛рдЦрдбрд╛ рдкрд░рдд рдХрд╛рдп рд╕рд╛рдВрдЧрддреЛ) рдЖрдгрд┐ рджреЛрди loops, count рдЖрдгрд┐ for_each. рдореБрдЦреНрдп рдзрдбрд╛: count рдЖрдкрд▓реНрдпрд╛ рдкреНрд░рддреАрдВрдирд╛ рдЬрд╛рдЧреЗрдиреБрд╕рд╛рд░ рдирдВрдмрд░ рджреЗрддреЛ, for_each рдирд╛рд╡рд╛рдиреЗ тАФ рдЖрдгрд┐ рдПрдЦрд╛рджреА рдкреНрд░рдд рдХрд╛рдврд▓реНрдпрд╛рд╡рд░ рдХрд╛рдп рд╣реЛрддреЗ рддреЗ рдпрд╛рд╡рд░реВрдирдЪ рдард░рддреЗ. iac/demo.py рдордзреАрд▓ variables(); iac/engine.py рдордзреАрд▓ expand() рдЖрдгрд┐ resolve().

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

Lab рдЪреНрдпрд╛ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд рдЖрддрд╛ рд░рд┐рдХрд╛рдореНрдпрд╛ рдЬрд╛рдЧрд╛ рдЖрд╣реЗрдд: "town: ____ ┬╖ seats: ____". ЁЯЦКя╕П рддреНрдпрд╛ "dev, 30" рдиреЗ рднрд░рд╛ рдЖрдгрд┐ рддреБрдореНрд╣рд╛рд▓рд╛ рдЫреЛрдЯреА lab рдорд┐рд│рддреЗ. "prod, 40" рдиреЗ рднрд░рд╛ рдЖрдгрд┐ рдореЛрдареА рдорд┐рд│рддреЗ. рдЖрд░рд╛рдЦрдбрд╛ рддреЛрдЪ.

Lab рд▓рд╛ lockers a, b рдЖрдгрд┐ c рд╕реБрджреНрдзрд╛ рд╣рд╡реЗ рдЖрд╣реЗрдд. рд╣реЗ рд▓рд┐рд╣реВрди рдареЗрд╡рдгреНрдпрд╛рдЪреЗ рджреЛрди рдорд╛рд░реНрдЧ рдЖрд╣реЗрдд.

рдорд╛рд░реНрдЧ 1 тАФ рдирдВрдмрд░ (count). "Locker #0 рдореНрд╣рдгрдЬреЗ a, #1 рдореНрд╣рдгрдЬреЗ b, #2 рдореНрд╣рдгрдЬреЗ c." рдордЧ locker b рдЪреА рдЧрд░рдЬ рд╕рдВрдкрддреЗ. рдЖрддрд╛ рдпрд╛рджреА "a, c" рдЖрд╣реЗ. рдореНрд╣рдгреВрди #0 рдореНрд╣рдгрдЬреЗ a, рдЖрдгрд┐ #1 рдЖрддрд╛ c рдЖрд╣реЗ. рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рд╡рд┐рдЪрд╛рд░ рдХрд░рддреЛ: "#1 рд╡рд░ рдЖрдзреА b рд▓рд┐рд╣рд┐рд▓реЗ рд╣реЛрддреЗ, рдЖрддрд╛ c рдЖрд╣реЗ тАФ рд▓реЗрдмрд▓ рдмрджрд▓рд╛. рдЖрдгрд┐ #2 рдЖрддрд╛ рдпрд╛рджреАрдд рдирд╛рд╣реА тАФ рддреЛ рдХрд╛рдвреВрди рдЯрд╛рдХрд╛." рдкрдг #2 рд╣рд╛ рдЦрд░рд╛ locker c рд╣реЛрддрд╛, рдЖрдд рдХреЛрдгрд╛рдЪреА рддрд░реА рдкреБрд╕реНрддрдХреЗ рдЕрд╕рд▓реЗрд▓рд╛! ЁЯУЪЁЯШ▒

рдорд╛рд░реНрдЧ 2 тАФ рдирд╛рд╡реЗ (for_each). "a рдирд╛рд╡рд╛рдЪрд╛ locker, b рдирд╛рд╡рд╛рдЪрд╛ locker, c рдирд╛рд╡рд╛рдЪрд╛ locker." b рдХрд╛рдврд╛, рдЖрдгрд┐ рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рдлрдХреНрдд locker b рдХрд╛рдврддреЛ. рдмрд╛рдХреА рдХрд╛рд╣реАрдЪ рд╣рд▓рдд рдирд╛рд╣реА.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    subgraph cnt["count = length(var.labels) тАФ by POSITION"]
      c0["row[0] a"] --> c0b["row[0] a тЬУ"]
      c1["row[1] b"] --> c1b["row[1] c  ~ relabel"]
      c2["row[2] c"] --> c2b["row[2]  - destroy"]
    end
    subgraph fe["for_each = var.labels тАФ by NAME"]
      fa["row["a"]"] --> fa2["тЬУ"]
      fb["row["b"]"] --> fb2["- destroy"]
      fc["row["c"]"] --> fc2["тЬУ"]
    end

ЁЯЧ║я╕П рд░реЗрдЦрд╛рдЯрд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/terraform/lesson-diagrams.html#l04

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг variables рд╢рд┐рд╡рд╛рдп dev рдЖрдгрд┐ prod рдЪреЗ рдЖрд░рд╛рдЦрдбреЗ рджреЛрди рдкреНрд░рддреА рдЕрд╕рддрд╛рдд рдЬреНрдпрд╛ рд╣рд│реВрд╣рд│реВ рд╡реЗрдЧрд│реНрдпрд╛ рд╣реЛрддрд╛рдд. Loops рд╢рд┐рд╡рд╛рдп рд╡реАрд╕ lockers рдореНрд╣рдгрдЬреЗ copy рдХреЗрд▓реЗрд▓реЗ рд╡реАрд╕ blocks. рдЖрдгрд┐ count рд╡ for_each рдпрд╛рдВрддреАрд▓ рдирд┐рд╡рдб рдореНрд╣рдгрдЬреЗ "рдордзрд▓реА item рдХрд╛рдврдгреЗ" рд╣рд╛ рдЫреЛрдЯрд╛ рдмрджрд▓ рдард░рдгреЗ рдЖрдгрд┐ рдЪреБрдХреАрдЪреНрдпрд╛ objects рдЪреА рд▓реЗрдмрд▓реЗ рдмрджрд▓рдгрд╛рд░реА, рддреНрдпрд╛рдВрдирд╛ рдкреБрдиреНрд╣рд╛ рдмрд╛рдВрдзрдгрд╛рд░реА рдХрд┐рдВрд╡рд╛ destroy рдХрд░рдгрд╛рд░реА рдЙрд▓рдерд╛рдкрд╛рд▓рде рдард░рдгреЗ рдпрд╛рдВрддреАрд▓ рдлрд░рдХ. Data рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЦрд▒реНрдпрд╛ objects рд╕рд╛рдареА тАФ disks, users, DNS records тАФ рддреА рдЙрд▓рдерд╛рдкрд╛рд▓рде рдореНрд╣рдгрдЬреЗ outage.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

iac/engine.py рдордзреАрд▓ expand() рдЖрдзреА variable рд╡рд╛рдЪрддреЗ (default рдЖрдгрд┐ рдРрдЪреНрдЫрд┐рдХ validation рд╕рд╣), рдордЧ locals, рдордЧ рдкреНрд░рддреНрдпреЗрдХ resource. count рдЕрд╕рд▓реЗрд▓реНрдпрд╛ block рдЪреЗ instances [0], [1], тАж рд╣реЛрддрд╛рдд; for_each рд╕рд╣ instances ["key"]. resolve() ${var.x}, ${local.x}, ${count.index}, ${each.key}, ${var.labels[count.index]}, ${length(var.labels)} рдЖрдгрд┐ references рднрд░рддреЗ. Outputs apply рдирдВрддрд░ resolve рд╣реЛрддрд╛рдд; show_outputs() рддреЗ terraform output рд╕рд╛рд░рдЦреЗ print рдХрд░рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 iac/demo.py variables
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply, ConfigError
def cfg(kind):
    lock = ({"count": "${length(var.labels)}", "label": "${var.labels[count.index]}"} if kind == "count"
            else {"for_each": "${var.labels}", "label": "${each.key}"})
    return {"variable": {"labels": {"default": ["a", "b", "c", "d"]},
                         "seats": {"default": 30, "validation": (lambda v: 10 <= v <= 60, "seats must be between 10 and 60")}},
            "resource": {"school_room": {"lab": {"name": "lab", "seats": "${var.seats}"}},
                         "school_locker": {"row": dict(lock, room_id="${school_room.lab.id}")}}}
for kind in ("count", "for_each"):
    cloud = FakeCloud(); st, _, _ = apply(plan(cfg(kind), None, cloud), cloud)
    p = plan(cfg(kind), st, cloud, vars={"labels": ["b", "c", "d"]})
    print(f"{kind:<8} remove 'a' тЖТ {p.summary()}")
    for line in p.lines(): print("        ", line.strip())
try: plan(cfg("count"), None, FakeCloud(), vars={"seats": 500})
except ConfigError as e: print("seats=500 тЖТ", e)
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

variables рдЕрд╕реЗ print рдХрд░рддреЗ:

   output lab_name = "dev-school-lab"
   output locker_ids = ["locker-01", "locker-02", "locker-03"]
тФАтФА -var env=prod -var seats=40 тЖТ ~ school_room.lab  name: 'dev-school-lab' тЖТ 'prod-school-lab' ┬╖ seats: 30 тЖТ 40
тФАтФА count: remove 'b' from [a, b, c] тЖТ ~ school_locker.row[1]  label: 'b' тЖТ 'c' ┬╖ - school_locker.row[2] (destroy) тЖТ Plan: 0 to add, 1 to change, 1 to destroy.
тФАтФА for_each: remove 'b' тЖТ - school_locker.row["b"] (destroy) тЖТ Plan: 0 to add, 0 to change, 1 to destroy.

рддреБрдордЪрд╛ snippet рдЕрд╕реЗ print рдХрд░рддреЛ:

count    remove 'a' тЖТ Plan: 0 to add, 3 to change, 1 to destroy.
         ~ school_locker.row[0]  label: 'a' тЖТ 'b'
         ~ school_locker.row[1]  label: 'b' тЖТ 'c'
         ~ school_locker.row[2]  label: 'c' тЖТ 'd'
         - school_locker.row[3] (destroy)
for_each remove 'a' тЖТ Plan: 0 to add, 0 to change, 1 to destroy.
         - school_locker.row["a"] (destroy)
seats=500 тЖТ Invalid value for variable "seats": seats must be between 10 and 60

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

count рд╕рд╣ рдЪрд╛рд░ lockers рдкреИрдХреА рдкрд╣рд┐рд▓рд╛ рдХрд╛рдврд▓реНрдпрд╛рд╡рд░ рдЪрд╛рд░рд╣реА addresses рдирд╛ рдзрдХреНрдХрд╛ рд▓рд╛рдЧрд▓рд╛: рддреАрди рд▓реЗрдмрд▓-рдмрджрд▓ рдЖрдгрд┐ рдПрдХ destroy тАФ рдЖрдгрд┐ destroy рдЭрд╛рд▓реЗрд▓рд╛ object рдЦрд░рд╛ locker d рд╣реЛрддрд╛, a рдирд╛рд╣реА. for_each рд╕рд╣ рддреЛрдЪ рдмрджрд▓ рдореНрд╣рдгрдЬреЗ рдиреЗрдордХрд╛ row["a"] рдЪрд╛ рдПрдХ destroy. рдпрд╛ campus рдордзреНрдпреЗ рд▓реЗрдмрд▓ рдмрджрд▓рдгреЗ рдЬрд╛рдЧреАрдЪ update рдЖрд╣реЗ; рд╕рд░рдХрд▓реЗрд▓реНрдпрд╛ field рдордзреНрдпреЗ ForceNew argument рдЕрд╕рддрд╛ рддрд░ рдкреНрд░рддреНрдпреЗрдХ рд▓реЗрдмрд▓-рдмрджрд▓ replace рдард░рд▓рд╛ рдЕрд╕рддрд╛. Validation рдиреЗ рдХреЛрдгрддрд╛рд╣реА plan рдмрдирдгреНрдпрд╛рдЖрдзреАрдЪ seats = 500 рдерд╛рдВрдмрд╡рд▓реЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ validation рдЕрд╕рд▓реЗрд▓реЗ typed variables, рдПрдХ .tfvars file, рдЖрдгрд┐ map рд╡рд░ for_each:

variable "env" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.env)
    error_message = "env must be dev, staging or prod."
  }
}

variable "lockers" {
  type    = map(object({ size = string }))
  default = { a = { size = "m" }, b = { size = "s" } }
}

locals {
  prefix = "${var.env}-school"
}

resource "aws_s3_bucket" "locker" {
  for_each = var.lockers
  bucket   = "${local.prefix}-locker-${each.key}"
  tags     = { size = each.value.size }
}

output "locker_buckets" {
  value = { for k, b in aws_s3_bucket.locker : k => b.arn }
}
terraform plan -var-file=prod.tfvars           # prod.tfvars:  env = "prod"
TF_VAR_env=dev terraform plan                  # the same through an environment variable
terraform output -json                         # outputs for scripts
terraform console                              # try expressions: > length(var.lockers)

рдЖрдзреАрдЪ count рд╡рд╛рдкрд░рд▓реЗ рдЖрд╣реЗ рдЖрдгрд┐ рдмрджрд▓рд╛рдпрдЪреЗ рдЖрд╣реЗ? moved block (рдзрдбрд╛ 09) aws_s3_bucket.locker[0] рдЪреЗ aws_s3_bucket.locker["a"] рдЕрд╕реЗ рдирд╛рд╡ рдкреБрдиреНрд╣рд╛ рди рдмрд╛рдВрдзрддрд╛ рдмрджрд▓рддреЛ.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: data рдХрд┐рдВрд╡рд╛ рдУрд│рдЦ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ resources рд╡рд░реАрд▓ count = рд╕рд╛рдареА рддреБрдордЪрд╛ code grep рдХрд░рд╛. рдкреНрд░рддреНрдпреЗрдХ рдЬрдг рднрд╡рд┐рд╖реНрдпрд╛рддреАрд▓ "рдордзрд▓реА item рдХрд╛рдврд╛" incident рдЖрд╣реЗ; list рдЕрдЬреВрди рд▓рд╣рд╛рди рдЕрд╕рддрд╛рдирд╛рдЪ рддреНрдпрд╛рдВрдирд╛ moved blocks рд╕рд╣ for_each рд╡рд░ рд╣рд▓рд╡рд╛.

тПня╕П рдкреБрдвреЗ

рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рдЧреБрдкрдЪреВрдк рдПрдХ рд╡рд╣реА рдареЗрд╡рдд рдЖрд▓рд╛ рдЖрд╣реЗ: рдХреЛрдгрддрд╛ address рдХреЛрдгрддрд╛ рдЦрд░рд╛ object рдЖрд╣реЗ. рдкреБрдвреЗ: state тАФ рддреА рдХрд╛рдп рдЖрд╣реЗ, рдЖрдгрд┐ рддрд┐рдЪреЗ рд╕рдВрд░рдХреНрд╖рдг рдХрд╛ рдХрд░рд╛рдпрд▓рд╛рдЪ рд╣рд╡реЗ.

git checkout lesson-05-state

ЁЯФБ Lesson 04 тАФ Variables, outputs & loops: one set of plans, many rooms

ЁЯУН You are here: Lesson 04 of 12 ┬╖ Previous: lesson-03-plan-apply ┬╖ Next: lesson-05-state


ЁЯУж What's in this branch

Lessons 01тАУ03, plus the parts of the language that make plans reusable: input variables (blanks you fill in), locals (names for values you compute once), outputs (what the plans report back) and the two loops, count and for_each. The key lesson: count numbers its copies by position, for_each by name тАФ and that decides what happens when you remove one. variables() in iac/demo.py; expand() and resolve() in iac/engine.py.

ЁЯзТ Explain like I'm 5

The lab plans now have blanks: "town: ____ ┬╖ seats: ____". ЁЯЦКя╕П Fill them in with "dev, 30" and you get a small lab. Fill them in with "prod, 40" and you get a big one. Same plans.

The lab also needs lockers a, b and c. There are two ways to write that down.

Way 1 тАФ numbers (count). "Locker #0 is a, #1 is b, #2 is c." Then locker b is no longer needed. Now the list is "a, c". So #0 is a, and #1 is now c. The contractor thinks: "#1 used to say b, now it says c тАФ relabel it. And #2 is not on the list any more тАФ take it away." But #2 was the real locker c, with someone's books inside! ЁЯУЪЁЯШ▒

Way 2 тАФ names (for_each). "Locker called a, locker called b, locker called c." Remove b, and the contractor removes only locker b. Nothing else moves.

ЁЯЧ║я╕П Diagram

flowchart LR
    subgraph cnt["count = length(var.labels) тАФ by POSITION"]
      c0["row[0] a"] --> c0b["row[0] a тЬУ"]
      c1["row[1] b"] --> c1b["row[1] c  ~ relabel"]
      c2["row[2] c"] --> c2b["row[2]  - destroy"]
    end
    subgraph fe["for_each = var.labels тАФ by NAME"]
      fa["row[&quot;a&quot;]"] --> fa2["тЬУ"]
      fb["row[&quot;b&quot;]"] --> fb2["- destroy"]
      fc["row[&quot;c&quot;]"] --> fc2["тЬУ"]
    end

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/terraform/lesson-diagrams.html#l04

тЭУ What

ЁЯдФ Why

Because without variables, dev and prod plans are two copies that drift apart. Without loops, twenty lockers are twenty copied blocks. And the choice between count and for_each is the difference between "remove the middle item" being a small change and being a shuffle that relabels, rebuilds or destroys the wrong objects. For real objects with data тАФ disks, users, DNS records тАФ that shuffle is an outage.

ЁЯФз How (in this repo)

expand() in iac/engine.py reads variable (with default and an optional validation), then locals, then each resource. A block with count becomes instances [0], [1], тАж; with for_each, instances ["key"]. resolve() fills in ${var.x}, ${local.x}, ${count.index}, ${each.key}, ${var.labels[count.index]}, ${length(var.labels)} and references. Outputs are resolved after apply; show_outputs() prints them like terraform output.

ЁЯзк Try it

python3 iac/demo.py variables
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply, ConfigError
def cfg(kind):
    lock = ({"count": "${length(var.labels)}", "label": "${var.labels[count.index]}"} if kind == "count"
            else {"for_each": "${var.labels}", "label": "${each.key}"})
    return {"variable": {"labels": {"default": ["a", "b", "c", "d"]},
                         "seats": {"default": 30, "validation": (lambda v: 10 <= v <= 60, "seats must be between 10 and 60")}},
            "resource": {"school_room": {"lab": {"name": "lab", "seats": "${var.seats}"}},
                         "school_locker": {"row": dict(lock, room_id="${school_room.lab.id}")}}}
for kind in ("count", "for_each"):
    cloud = FakeCloud(); st, _, _ = apply(plan(cfg(kind), None, cloud), cloud)
    p = plan(cfg(kind), st, cloud, vars={"labels": ["b", "c", "d"]})
    print(f"{kind:<8} remove 'a' тЖТ {p.summary()}")
    for line in p.lines(): print("        ", line.strip())
try: plan(cfg("count"), None, FakeCloud(), vars={"seats": 500})
except ConfigError as e: print("seats=500 тЖТ", e)
EOF

тЬЕ Verify тАФ what you should see

variables prints:

   output lab_name = "dev-school-lab"
   output locker_ids = ["locker-01", "locker-02", "locker-03"]
тФАтФА -var env=prod -var seats=40 тЖТ ~ school_room.lab  name: 'dev-school-lab' тЖТ 'prod-school-lab' ┬╖ seats: 30 тЖТ 40
тФАтФА count: remove 'b' from [a, b, c] тЖТ ~ school_locker.row[1]  label: 'b' тЖТ 'c' ┬╖ - school_locker.row[2] (destroy) тЖТ Plan: 0 to add, 1 to change, 1 to destroy.
тФАтФА for_each: remove 'b' тЖТ - school_locker.row["b"] (destroy) тЖТ Plan: 0 to add, 0 to change, 1 to destroy.

Your snippet prints:

count    remove 'a' тЖТ Plan: 0 to add, 3 to change, 1 to destroy.
         ~ school_locker.row[0]  label: 'a' тЖТ 'b'
         ~ school_locker.row[1]  label: 'b' тЖТ 'c'
         ~ school_locker.row[2]  label: 'c' тЖТ 'd'
         - school_locker.row[3] (destroy)
for_each remove 'a' тЖТ Plan: 0 to add, 0 to change, 1 to destroy.
         - school_locker.row["a"] (destroy)
seats=500 тЖТ Invalid value for variable "seats": seats must be between 10 and 60

ЁЯПБ What you just proved

Removing the first of four lockers with count touched all four addresses: three relabels and one destroy тАФ and the destroyed object was the real locker d, not a. With for_each, the same edit was exactly one destroy of row["a"]. In this campus a label is an in-place update; with a ForceNew argument in the shifted field, each relabel would be a replace. The validation stopped seats = 500 before any plan was made.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ typed variables with validation, a .tfvars file, and for_each over a map:

variable "env" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.env)
    error_message = "env must be dev, staging or prod."
  }
}

variable "lockers" {
  type    = map(object({ size = string }))
  default = { a = { size = "m" }, b = { size = "s" } }
}

locals {
  prefix = "${var.env}-school"
}

resource "aws_s3_bucket" "locker" {
  for_each = var.lockers
  bucket   = "${local.prefix}-locker-${each.key}"
  tags     = { size = each.value.size }
}

output "locker_buckets" {
  value = { for k, b in aws_s3_bucket.locker : k => b.arn }
}
terraform plan -var-file=prod.tfvars           # prod.tfvars:  env = "prod"
TF_VAR_env=dev terraform plan                  # the same through an environment variable
terraform output -json                         # outputs for scripts
terraform console                              # try expressions: > length(var.lockers)

Already used count and need to switch? A moved block (lesson 09) renames aws_s3_bucket.locker[0] to aws_s3_bucket.locker["a"] without a rebuild.

ЁЯПн Why this matters in production: grep your code for count = on resources that hold data or identity. Each one is a future "remove the middle item" incident; move them to for_each with moved blocks while the list is still short.

тПня╕П Next

The contractor has been quietly keeping a notebook: which address is which real object. Next: the state тАФ what it is, and why it must be protected.

git checkout lesson-05-state
тЖР Previousplan applyNext тЖТstate

This page is the lesson's README from the lesson-04-variables-loops branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.