ЁЯПл The SchoolтА║ЁЯУЬ TerraformтА║ЁЯПШя╕П рдзрдбрд╛ 10 тАФ Environments: рддреНрдпрд╛рдЪ рдЖрд░рд╛рдЦрдбреНрдпрд╛рд╡рд░реВрди dev рдЖрдгрд┐ prod
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯПШя╕П рдзрдбрд╛ 10 тАФ Environments: рддреНрдпрд╛рдЪ рдЖрд░рд╛рдЦрдбреНрдпрд╛рд╡рд░реВрди dev рдЖрдгрд┐ prod

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 10 ┬╖ рдорд╛рдЧреАрд▓: lesson-09-drift-import ┬╖ рдкреБрдвреАрд▓: lesson-11-testing-policy


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

рдзрдбреЗ 01тАУ09, рдЖрдгрд┐ рддреНрдпрд╛рд╕реЛрдмрдд рдЕрдиреЗрдХ рдЬрд╛рдЧрд╛. рддреЛрдЪ рдЖрд░рд╛рдЦрдбрд╛ рдПрдХ рд▓рд╣рд╛рди dev campus рдЖрдгрд┐ рдПрдХ рдореЛрдард╛ prod campus рдмрд╛рдВрдзрд╛рдпрд▓рд╛ рд╣рд╡рд╛, рдЖрдгрд┐ рдПрдХрд╛рддрд▓рд╛ рдмрджрд▓ рджреБрд╕рд▒реНрдпрд╛рд▓рд╛ рдХрдзреАрдЪ рд╣рд╛рдд рд▓рд╛рд╡рддрд╛ рдХрд╛рдорд╛ рдирдпреЗ. рдпрд╛рдЪреЗ рддреАрди рдиреЗрд╣рдореАрдЪреЗ рдорд╛рд░реНрдЧ: workspaces (рдПрдХ config, рдПрдХ backend, рдкреНрд░рддреНрдпреЗрдХ workspace рдЪреА рд╡реЗрдЧрд│реА state), рд╡реЗрдЧрд│реНрдпрд╛ directories (рдкреНрд░рддреНрдпреЗрдХ environment рд╕рд╛рдареА рдПрдХ folder, рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреА рд╕реНрд╡рддрдГрдЪреА backend key, modules рд╕рд╛рдорд╛рдпрд┐рдХ) рдЖрдгрд┐ рд╡реЗрдЧрд│реЗ accounts (рд╢рд┐рд╡рд╛рдп рд╡реЗрдЧрд│реА state рдЖрдгрд┐ рд╡реЗрдЧрд│реЗ CI roles). environments() рд╣реЗ iac/demo.py рдордзреНрдпреЗ рдЖрд╣реЗ; Backend.path() рд╣реЗ iac/engine.py рдордзреНрдпреЗ.

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

рд╢рд╛рд│реЗрдХрдбреЗ рдПрдХ рд╕рд░рд╛рд╡рд╛рдЪрд╛ campus (dev) рдЖрд╣реЗ рдЬрд┐рдереЗ рдирд╡реНрдпрд╛ рдХрд▓реНрдкрдирд╛ рдХрд░реВрди рдкрд╛рд╣рд┐рд▓реНрдпрд╛ рдЬрд╛рддрд╛рдд, рдЖрдгрд┐ рдПрдХ рдЦрд░рд╛ campus (prod) рдЬрд┐рдереЗ рдореБрд▓реЗ рдЕрд╕рддрд╛рдд. ЁЯПлЁЯПл рдЖрд░рд╛рдЦрдбрд╛ рддреЛрдЪ, рдЖрдХрд╛рд░ рд╡реЗрдЧрд│реЗ: рд╕рд░рд╛рд╡рд╛рдЪреНрдпрд╛ lab рдордзреНрдпреЗ 10 seats рдЖрдгрд┐ 1 locker; рдЦрд▒реНрдпрд╛ lab рдордзреНрдпреЗ 40 seats рдЖрдгрд┐ 3 lockers. рд╣реЗ рдЖрдХрд╛рд░ рджреЛрди рд▓рд╣рд╛рди рдХрд╛рд░реНрдбрд╛рдВрд╡рд░ рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ рдЕрд╕рддрд╛рдд: dev.tfvars рдЖрдгрд┐ prod.tfvars. ЁЯГП

рдорд╛рд░реНрдЧ 1 тАФ workspaces. рдПрдХрдЪ рдиреЛрдВрджрд╡рд╣реА, рдкрдг рддрд┐рд▓рд╛ рджреЛрди tabs: "dev" рдЖрдгрд┐ "prod". рдХрд╛рдо рдХрд░рдгреНрдпрд╛рдЖрдзреА рддреБрдореНрд╣реА рдпреЛрдЧреНрдп tab рдЙрдШрдбрддрд╛. рдХрддрд░рд┐рдирд╛ prod tab рдЙрдШрдбрддреЗ рдкрдг рдЪреБрдХреВрди dev рдЪреЗ рдХрд╛рд░реНрдб рдЙрдЪрд▓рддреЗ. рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ plan рдореЛрдареНрдпрд╛рдиреЗ рд╡рд╛рдЪрддреЛ: "рдЦрд░реА lab: 40 seats тЖТ 10. 2 lockers рдХрд╛рдвреВрди рдЯрд╛рдХрд╛." рдирд╢реАрдм, рдХрддрд░рд┐рдирд╛ рд╣реЛ рдореНрд╣рдгрдгреНрдпрд╛рдЖрдзреА plans рд╡рд╛рдЪрддреЗ. ЁЯШЕ

рдорд╛рд░реНрдЧ 2 тАФ рд╡реЗрдЧрд│реЗ folders. dev рд╕рд╛рдареА рдПрдХ folder, prod рд╕рд╛рдареА рдПрдХ folder, рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреА рд╕реНрд╡рддрдГрдЪреА рдиреЛрдВрджрд╡рд╣реА. рддреБрдореНрд╣реА "рдЪреБрдХреАрдЪреНрдпрд╛ tab рдордзреНрдпреЗ" рдЕрд╕реВрдЪ рд╢рдХрдд рдирд╛рд╣реА тАФ рддреБрдореНрд╣реА рд╡реЗрдЧрд│реНрдпрд╛рдЪ рдЦреЛрд▓реАрдд рдЙрднреЗ рдЕрд╕рддрд╛.

рдорд╛рд░реНрдЧ 3 тАФ рд╡реЗрдЧрд│реЗ campuses. dev рдЖрдгрд┐ prod рдпрд╛ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рдЬрд╛рдЧрд╛ рдЖрд╣реЗрдд, рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рдХрд┐рд▓реНрд▓реНрдпрд╛рдВрд╕рд╣. рд╕рд░рд╛рд╡ рдХрд░рдгрд╛рд▒реНрдпрд╛ рдмрд╛рдВрдзрдХрд╛рдо рдХрд░рдгрд╛рд▒реНрдпрд╛рдВрдЪреНрдпрд╛ рдХрд┐рд▓реНрд▓реНрдпрд╛ рдЦрд▒реНрдпрд╛ campus рдЪреЗ рдлрд╛рдЯрдХрд╣реА рдЙрдШрдбрдд рдирд╛рд╣реАрдд. ЁЯФС

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

flowchart LR
    plans["ЁЯУЬ one set of plans + modules"]
    plans -->|"workspace dev + dev.tfvars"| d["ЁЯУТ env:/dev/campus.tfstate<br/>dev-lab ┬╖ 10 seats ┬╖ 1 locker"]
    plans -->|"workspace prod + prod.tfvars"| p["ЁЯУТ env:/prod/campus.tfstate<br/>prod-lab ┬╖ 40 seats ┬╖ 3 lockers"]
    oops["тЪая╕П prod selected + dev.tfvars"] --> bad["~ seats 40 тЖТ 10<br/>- 2 lockers"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "staging рдордзреНрдпреЗ test рдХреЗрд▓реЗ" рдпрд╛рд▓рд╛ рдЕрд░реНрде рддреЗрд╡реНрд╣рд╛рдЪ рдЕрд╕рддреЛ рдЬреЗрд╡реНрд╣рд╛ staging рддреНрдпрд╛рдЪ рдЖрд░рд╛рдЦрдбреНрдпрд╛рд╡рд░реВрди рдмрд╛рдВрдзрд▓реЗрд▓реЗ рдЕрд╕рддреЗ. рдЖрдгрд┐ рдХрд╛рд░рдг IaC рдордзреАрд▓ рд╕рд░реНрд╡рд╛рдд рдорд╣рд╛рдЧрдбреНрдпрд╛ рдЪреБрдХрд╛ рдореНрд╣рдгрдЬреЗ рдЪреБрдХреАрдЪреНрдпрд╛ environment рд╡рд░ apply рдХреЗрд▓реЗрд▓реЗ рдЖрд░рд╛рдЦрдбреЗ. HashiCorp рдЪреЗ рд╕реНрд╡рддрдГрдЪреЗ documentation рд╕рд╛рдВрдЧрддреЗ рдХреА environments рдирд╛ рд╡реЗрдЧрд│реЗ credentials рдХрд┐рдВрд╡рд╛ access controls рд╣рд╡реЗ рдЕрд╕рддреАрд▓, рддреЗрд╡реНрд╣рд╛ CLI workspaces рдпреЛрдЧреНрдп рдирд╛рд╣реАрдд. Environments рддреНрдпрд╛рдВрдирд╛ рдХреЛрдг рдмрджрд▓реВ рд╢рдХрддреЗ рдпрд╛рдд рдЬрд┐рддрдХреЗ рд╡реЗрдЧрд│реЗ, рддрд┐рддрдХреА рддреНрдпрд╛рдВрдирд╛ рд╡реЗрдЧрд│реНрдпрд╛ directories, backends рдЖрдгрд┐ accounts рдЪреА рдЬрд╛рд╕реНрдд рдЧрд░рдЬ.

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

iac/engine.py рдордзреАрд▓ Backend.path(workspace) default рд╕рд╛рдареА campus.tfstate рдЖрдгрд┐ рдЗрддрд░рд╛рдВрд╕рд╛рдареА env:/<workspace>/campus.tfstate рдкрд░рдд рджреЗрддреЗ. plan(..., workspace=ws) рдореБрд│реЗ ${terraform.workspace} рдЪреЗ рдореВрд▓реНрдп workspace рдЪреЗ рдирд╛рд╡ рд╣реЛрддреЗ. iac/demo.py рдордзреАрд▓ ENV lab рдЪреЗ рдирд╛рд╡ ${terraform.workspace}-lab рдареЗрд╡рддреЗ рдЖрдгрд┐ seats рд╡ lockers TFVARS рдордзреВрди рдШреЗрддреЗ. Demo dev рдЖрдгрд┐ prod apply рдХрд░рддреЛ, рдордЧ dev рдЪреНрдпрд╛ рдореВрд▓реНрдпрд╛рдВрдиреА prod рдЪрд╛ plan рдХрд░рддреЛ.

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

python3 iac/demo.py environments
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply, Backend
from demo import ENV
cloud, be = FakeCloud(), Backend()
vars = {"dev": {"seats": 10, "lockers": 1}, "staging": {"seats": 20, "lockers": 2}, "prod": {"seats": 40, "lockers": 3}}
for ws, v in vars.items():
    st, _, msg = apply(plan(ENV, be.read(be.path(ws)), cloud, vars=v, workspace=ws), cloud, backend=be, key=be.path(ws))
    names = [r["attrs"]["name"] for r in st["resources"].values() if r["type"] == "school_room"]
    print(f"{ws:<8} {be.path(ws):<27} {len(st['resources'])} resources ┬╖ room {names}")
print("campus objects:", len(cloud.inventory()), "┬╖ default workspace empty:", be.read(be.path())["resources"] == {})
EOF

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

environments рд╣реЗ рдЫрд╛рдкрддреЗ:

   the backend now holds ['env:/dev/campus.tfstate', 'env:/prod/campus.tfstate'] ┬╖ the campus has 6 objects
тФАтФА Katrina forgets `workspace select dev` and plans with dev.tfvars while prod is selected:
     ~ school_room.lab  seats: 40 тЖТ 10
     - school_locker.row[1] (destroy)
     - school_locker.row[2] (destroy)
   Plan: 0 to add, 1 to change, 2 to destroy.  тЖР prod shrinks to dev's size

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

dev      env:/dev/campus.tfstate     2 resources ┬╖ room ['dev-lab']
staging  env:/staging/campus.tfstate 3 resources ┬╖ room ['staging-lab']
prod     env:/prod/campus.tfstate    4 resources ┬╖ room ['prod-lab']
campus objects: 9 ┬╖ default workspace empty: True

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

рдПрдХрд╛рдЪ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдиреЗ рддреАрди рд╡реЗрдЧрд│реЗ environments рдмрд╛рдВрдзрд▓реЗ, рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреА рд╕реНрд╡рддрдГрдЪреА рдиреЛрдВрджрд╡рд╣реА key рдЖрдгрд┐ рд╕реНрд╡рддрдГрдЪрд╛ рдЖрдХрд╛рд░ тАФ рдЖрдгрд┐ default workspace рд░рд┐рдХрд╛рдореЗ рд░рд╛рд╣рд┐рд▓реЗ. рдкрдг рддрд┐рдиреНрд╣реА рдПрдХрд╛рдЪ campus рдордзреНрдпреЗ рдЖрдгрд┐ рдПрдХрд╛рдЪ backend рдордзреНрдпреЗ рд░рд╛рд╣рддрд╛рдд, рдЖрдгрд┐ рдирд┐рд╡рдбрд▓реЗрд▓реНрдпрд╛ workspace рд╢рд┐рд╡рд╛рдп рддреНрдпрд╛рдВрдирд╛ рдХрд╛рд╣реАрдЪ рд╡реЗрдЧрд│реЗ рдХрд░рдд рдирд╛рд╣реА: рдЪреБрдХреАрдЪреНрдпрд╛ workspace рд╕реЛрдмрдд рдЪреБрдХреАрдЪреНрдпрд╛ .tfvars рдиреЗ prod рд▓рд╣рд╛рди рдХрд░рдгреНрдпрд╛рдЪрд╛ plan рдХреЗрд▓рд╛ (1 to change, 2 to destroy). рддреНрдпрд╛ рдЖрдгрд┐ outage рдЪреНрдпрд╛ рдордзреНрдпреЗ рдлрдХреНрдд plan review рдЙрднрд╛ рд╣реЛрддрд╛.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ CLI workspaces, рдЬреЗрд╡реНрд╣рд╛ environments рдЪрд╛ access рд╕рд╛рдорд╛рдпрд┐рдХ рдЕрд╕рддреЛ:

terraform workspace new staging
terraform workspace select staging
terraform workspace show                          # always check before apply
terraform plan -var-file=staging.tfvars -out=tfplan

рдкреНрд░рддреНрдпреЗрдХ environment рд╕рд╛рдареА рдПрдХ directory, рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреА рд╕реНрд╡рддрдГрдЪреА backend key, рд╕рд╛рдорд╛рдпрд┐рдХ modules рдмреЛрд▓рд╛рд╡рдгрд╛рд░реА:

infra/
тФЬтФАтФА modules/wing/                 # the shared design
тФФтФАтФА envs/
    тФЬтФАтФА dev/   main.tf  backend.tf (key = "dev/campus.tfstate")   dev.tfvars
    тФФтФАтФА prod/  main.tf  backend.tf (key = "prod/campus.tfstate")  prod.tfvars
# envs/prod/main.tf
module "east" {
  source  = "../../modules/wing"
  wing    = "east"
  lockers = 3
}

рд╡реЗрдЧрд│реНрдпрд╛ accounts рд╕рд╛рдареА, рдкреНрд░рддреНрдпреЗрдХ environment рдЪрд╛ provider рд╡реЗрдЧрд│рд╛ role assume рдХрд░рддреЛ, рдЖрдгрд┐ prod рдЪрд╛ CI job рд╣рд╛рдЪ prod role assume рдХрд░рдгреНрдпрд╛рдЪреА рдкрд░рд╡рд╛рдирдЧреА рдЕрд╕рд▓реЗрд▓рд╛ рдПрдХрдореЗрд╡ рдШрдЯрдХ рдЕрд╕рддреЛ (рдзрдбрд╛ 12). Terragrunt рд╕рд╛рд░рдЦреА tools рдореБрдЦреНрдпрддрдГ рдЕрдиреЗрдХ environment folders рд▓рд╣рд╛рди рдареЗрд╡рдгреНрдпрд╛рд╕рд╛рдареА рдЕрд╕рддрд╛рдд.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: prod рд╕рд╛рдареА рд▓рд┐рд╣реВрди рдареЗрд╡рд╛ рдХреА рддреЗ рдмрджрд▓рдгрд╛рд░реЗ credentials рдХреЛрдг рдЖрдгрд┐ рдХрд╛рдп assume рдХрд░реВ рд╢рдХрддреЗ. рдЙрддреНрддрд░рд╛рдд "dev рдЪрд╛рд▓рд╡реВ рд╢рдХрдгрд╛рд░реЗ рдХреЛрдгреАрд╣реА" рдЕрд╕реЗрд▓, рддрд░ environments рдЕрдЬреВрди рд╡реЗрдЧрд│реЗ рдЭрд╛рд▓реЗрд▓реЗ рдирд╛рд╣реАрдд.

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

рдкреНрд░рддреНрдпреЗрдХ plan рдбреЛрд│реНрдпрд╛рдВрдиреА рд╡рд╛рдЪрдгреЗ рдореЛрдареНрдпрд╛ рдкреНрд░рдорд╛рдгрд╛рд╡рд░ рд╢рдХреНрдп рдирд╛рд╣реА. рдкреБрдвреЗ: рддреБрдордЪреНрдпрд╛рд╕рд╛рдареА рдЖрд░рд╛рдЦрдбрд╛ рд╡рд╛рдЪрдгрд╛рд░реЗ рддрдкрд╛рд╕рдиреАрд╕ тАФ validate, tests, policy рдЖрдгрд┐ cost.

git checkout lesson-11-testing-policy

ЁЯПШя╕П Lesson 10 тАФ Environments: dev and prod from the same plans

ЁЯУН You are here: Lesson 10 of 12 ┬╖ Previous: lesson-09-drift-import ┬╖ Next: lesson-11-testing-policy


ЁЯУж What's in this branch

Lessons 01тАУ09, plus many sites. The same plans must build a small dev campus and a big prod campus, and a change to one must never touch the other. Three common ways: workspaces (one config, one backend, a separate state per workspace), separate directories (one folder per environment, each with its own backend key, sharing modules) and separate accounts (plus separate state and separate CI roles). environments() in iac/demo.py; Backend.path() in iac/engine.py.

ЁЯзТ Explain like I'm 5

The school has a practice campus (dev) where new ideas are tried, and the real campus (prod) where the children are. ЁЯПлЁЯПл Same plans, different sizes: the practice lab has 10 seats and 1 locker; the real lab has 40 seats and 3 lockers. The sizes live on two small cards: dev.tfvars and prod.tfvars. ЁЯГП

Way 1 тАФ workspaces. One register book with two tabs: "dev" and "prod". Before you work, you open the right tab. Katrina opens the prod tab but picks up the dev card by mistake. The contractor reads the plan out loud: "Real lab: 40 seats тЖТ 10. Take away 2 lockers." Luckily Katrina reads plans before saying yes. ЁЯШЕ

Way 2 тАФ separate folders. A folder for dev, a folder for prod, each with its own register. You can't be "in the wrong tab" тАФ you are standing in a different room.

Way 3 тАФ separate campuses. Dev and prod are different sites with different keys. The practice builders' keys don't even open the real campus gate. ЁЯФС

ЁЯЧ║я╕П Diagram

flowchart LR
    plans["ЁЯУЬ one set of plans + modules"]
    plans -->|"workspace dev + dev.tfvars"| d["ЁЯУТ env:/dev/campus.tfstate<br/>dev-lab ┬╖ 10 seats ┬╖ 1 locker"]
    plans -->|"workspace prod + prod.tfvars"| p["ЁЯУТ env:/prod/campus.tfstate<br/>prod-lab ┬╖ 40 seats ┬╖ 3 lockers"]
    oops["тЪая╕П prod selected + dev.tfvars"] --> bad["~ seats 40 тЖТ 10<br/>- 2 lockers"]

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

тЭУ What

ЁЯдФ Why

Because "tested in staging" only means something if staging was built from the same plans. And because the most expensive mistakes in IaC are plans applied to the wrong environment. HashiCorp's own documentation says CLI workspaces are not a good fit when environments need separate credentials or access controls. The more the environments differ in who may change them, the more they need separate directories, backends and accounts.

ЁЯФз How (in this repo)

Backend.path(workspace) in iac/engine.py returns campus.tfstate for default and env:/<workspace>/campus.tfstate for the others. plan(..., workspace=ws) makes ${terraform.workspace} resolve to the workspace name. ENV in iac/demo.py names the lab ${terraform.workspace}-lab and takes seats and lockers from TFVARS. The demo applies dev and prod, then plans prod with dev's values.

ЁЯзк Try it

python3 iac/demo.py environments
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply, Backend
from demo import ENV
cloud, be = FakeCloud(), Backend()
vars = {"dev": {"seats": 10, "lockers": 1}, "staging": {"seats": 20, "lockers": 2}, "prod": {"seats": 40, "lockers": 3}}
for ws, v in vars.items():
    st, _, msg = apply(plan(ENV, be.read(be.path(ws)), cloud, vars=v, workspace=ws), cloud, backend=be, key=be.path(ws))
    names = [r["attrs"]["name"] for r in st["resources"].values() if r["type"] == "school_room"]
    print(f"{ws:<8} {be.path(ws):<27} {len(st['resources'])} resources ┬╖ room {names}")
print("campus objects:", len(cloud.inventory()), "┬╖ default workspace empty:", be.read(be.path())["resources"] == {})
EOF

тЬЕ Verify тАФ what you should see

environments prints:

   the backend now holds ['env:/dev/campus.tfstate', 'env:/prod/campus.tfstate'] ┬╖ the campus has 6 objects
тФАтФА Katrina forgets `workspace select dev` and plans with dev.tfvars while prod is selected:
     ~ school_room.lab  seats: 40 тЖТ 10
     - school_locker.row[1] (destroy)
     - school_locker.row[2] (destroy)
   Plan: 0 to add, 1 to change, 2 to destroy.  тЖР prod shrinks to dev's size

Your snippet prints:

dev      env:/dev/campus.tfstate     2 resources ┬╖ room ['dev-lab']
staging  env:/staging/campus.tfstate 3 resources ┬╖ room ['staging-lab']
prod     env:/prod/campus.tfstate    4 resources ┬╖ room ['prod-lab']
campus objects: 9 ┬╖ default workspace empty: True

ЁЯПБ What you just proved

One set of plans built three separate environments, each with its own register key and its own size тАФ and the default workspace stayed empty. But all three live in one campus and one backend, and nothing but the selected workspace separates them: the wrong .tfvars with the wrong workspace planned to shrink prod (1 to change, 2 to destroy). Only the plan review stood between that and an outage.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ CLI workspaces, when the environments share access:

terraform workspace new staging
terraform workspace select staging
terraform workspace show                          # always check before apply
terraform plan -var-file=staging.tfvars -out=tfplan

A directory per environment, each with its own backend key, calling shared modules:

infra/
тФЬтФАтФА modules/wing/                 # the shared design
тФФтФАтФА envs/
    тФЬтФАтФА dev/   main.tf  backend.tf (key = "dev/campus.tfstate")   dev.tfvars
    тФФтФАтФА prod/  main.tf  backend.tf (key = "prod/campus.tfstate")  prod.tfvars
# envs/prod/main.tf
module "east" {
  source  = "../../modules/wing"
  wing    = "east"
  lockers = 3
}

For separate accounts, each environment's provider assumes a different role, and the CI job for prod is the only thing allowed to assume the prod role (lesson 12). Tools like Terragrunt exist mainly to keep many environment folders short.

ЁЯПн Why this matters in production: write down, for prod, who and what can assume credentials that change it. If the answer includes "anyone who can run dev", the environments are not separate yet.

тПня╕П Next

Reading every plan by eye does not scale. Next: inspectors that read the plans for you тАФ validate, tests, policy and cost.

git checkout lesson-11-testing-policy
тЖР Previousdrift importNext тЖТtesting policy

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