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

ЁЯзй рдзрдбрд╛ 07 тАФ Modules: рдПрдХрд╛ wing рдЪреЗ design, рджреЛрдирджрд╛ рдмрд╛рдВрдзрд▓реЗрд▓реЗ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: рдзрдбрд╛ 07 / 12 ┬╖ рдорд╛рдЧреЗ: lesson-06-dependency-graph ┬╖ рдкреБрдвреЗ: lesson-08-remote-state-locking


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

рдзрдбреЗ 01тАУ06, рдЖрдгрд┐ рдкреБрдирд░реНрд╡рд╛рдкрд░. Module рдореНрд╣рдгрдЬреЗ inputs (variables) рдЖрдгрд┐ outputs рдЕрд╕рд▓реЗрд▓рд╛ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдЪрд╛ рдПрдХ folder. рдмрд╛рдВрдзрдХрд╛рдо рдХрд╛рд░реНрдпрд╛рд▓рдп рдПрдХ wing рдПрдХрджрд╛рдЪ рд░реЗрдЦрд╛рдЯрддреЗ тАФ рдПрдХ hall, рдПрдХ gate рдЖрдгрд┐ N lockers тАФ рдЖрдгрд┐ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ inputs рд╕рд╣ east wing рдЖрдгрд┐ west wing рд╕рд╛рдареА рддреЗ call рдХрд░рддреЗ. рдкреНрд░рддреНрдпреЗрдХ call рд▓рд╛ рд╕реНрд╡рддрдГрдЪреЗ addresses (module.east.school_room.this) рдЖрдгрд┐ рд╕реНрд╡рддрдГрдЪреЗ outputs рдорд┐рд│рддрд╛рдд. iac/demo.py рдордзреАрд▓ modules() рдЖрдгрд┐ WING; iac/engine.py рдордзреАрд▓ expand() рдордзрд▓реЗ module handling.

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

рд╢рд╛рд│реЗрдЪреА рдкреНрд░рддреНрдпреЗрдХ wing рд╕рд╛рд░рдЦреАрдЪ рджрд┐рд╕рддреЗ: рдПрдХ рдореЛрдард╛ hall, рддреНрдпрд╛рдд рдЬрд╛рдгрд╛рд░реЗ рдПрдХ gate, рдЖрдгрд┐ lockers рдЪреА рдПрдХ рд░рд╛рдВрдЧ. рдкреНрд░рддреНрдпреЗрдХ wing рд╕рд╛рдареА рд╣реЗ рд╕рдЧрд│реЗ рдкреБрдиреНрд╣рд╛ рд░реЗрдЦрд╛рдЯрдгреЗ рдХрдВрдЯрд╛рд│рд╡рд╛рдгреЗ рдЖрд╣реЗ, рдЖрдгрд┐ рдПрдХреЗ рджрд┐рд╡рд╢реА рдХреЛрдгреАрддрд░реА gate рдЪреБрдХреАрдЪреЗ рдХрд╛рдврддреЗ.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдПрдХрд╛ wing рдЪреЗ design ЁЯУР рд░реЗрдЦрд╛рдЯрддреЗ, рджреЛрди рд░рд┐рдХрд╛рдореНрдпрд╛ рдЬрд╛рдЧрд╛рдВрд╕рд╣: "wing рдЪреЗ рдирд╛рд╡: ____" рдЖрдгрд┐ "рдХрд┐рддреА lockers: ____ (рддреБрдореНрд╣реА рд╕рд╛рдВрдЧрд┐рддрд▓реЗ рдирд╛рд╣реА рддрд░ 2)". рддреА рддреЗ modules/wing рдирд╛рд╡рд╛рдЪреНрдпрд╛ folder рдордзреНрдпреЗ рдареЗрд╡рддреЗ.

рдЖрддрд╛ рд╕рдВрдкреВрд░реНрдг рд╢рд╛рд│реЗрдЪрд╛ рдЖрд░рд╛рдЦрдбрд╛ рдЫреЛрдЯрд╛ рдЖрд╣реЗ:

рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ east рд╕рд╛рдареА 4 рдЧреЛрд╖реНрдЯреА рдЖрдгрд┐ west рд╕рд╛рдареА 5 рдмрд╛рдВрдзрддреЛ. West wing рд▓рд╛ рдЪреМрдерд╛ locker рд╣рд╡рд╛ рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛ рдлрдХреНрдд west wing рдмрджрд▓рддреЗ. рдЖрдгрд┐ design рдЙрддреНрддрд░реЗ рдкрд░рдд рджреЗрдК рд╢рдХрддреЗ тАФ "east gate рдЪрд╛ рдирдВрдмрд░ gate-01 рдЖрд╣реЗ" тАФ рдореНрд╣рдгрдЬреЗ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдЪрд╛ рдмрд╛рдХреАрдЪрд╛ рднрд╛рдЧ рддреА рд╡рд╛рдкрд░реВ рд╢рдХрддреЛ.

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

flowchart LR
    root["ЁЯУЬ root plans"] -->|"wing = east"| e["ЁЯзй module.east<br/>hall ┬╖ gate ┬╖ 2 lockers"]
    root -->|"wing = west, lockers = 3"| w["ЁЯзй module.west<br/>hall ┬╖ gate ┬╖ 3 lockers"]
    design["ЁЯУР modules/wing<br/>inputs: wing, lockers<br/>outputs: gate_id, hall_id"] -.-> e
    design -.-> w
    e -->|"gate_id = gate-01"| out["root outputs"]
    w -->|"hall_id = room-02"| out

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг copy-paste рдиреЗ рдмрд╛рдВрдзрд▓реЗрд▓рд╛ рддреЛрдЪ pattern рд╣рд│реВрд╣рд│реВ рд╡реЗрдЧрд│рд╛ рд╣реЛрдд рдЬрд╛рддреЛ: рдПрдХрд╛ wing рд▓рд╛ рджреБрд░реБрд╕реНрддреА рдорд┐рд│рддреЗ, рдмрд╛рдХреАрдВрдирд╛ рдирд╛рд╣реА. Module рддреНрдпрд╛ pattern рд▓рд╛ рдЫреЛрдЯреНрдпрд╛ interface рд╕рд╣ рдПрдХ review рдЭрд╛рд▓реЗрд▓реА рдЧреЛрд╖реНрдЯ рдмрдирд╡рддреЗ. рддреНрдпрд╛рдореБрд│реЗ platform team рдЪрд╛рдВрдЧрд▓реЗ defaults (encryption рдЪрд╛рд▓реВ, public access рдмрдВрдж) рдкреНрд░рд╕рд┐рджреНрдз рдХрд░реВ рд╢рдХрддреЗ рдЬреЗ рдкреНрд░рддреНрдпреЗрдХ product team рд▓рд╛ рдлреБрдХрдЯ рдорд┐рд│рддрд╛рдд. рдХрд┐рдВрдордд рдореНрд╣рдгрдЬреЗ рдЕрдкреНрд░рддреНрдпрдХреНрд╖рдкрдгрд╛ (indirection): plan рдЖрддрд╛ module.east.тАж addresses рджрд╛рдЦрд╡рддреЛ, рдЖрдгрд┐ module рдмрджрд▓рд▓рд╛ рдХреА рдкреНрд░рддреНрдпреЗрдХ call рдХрд░рдгрд╛рд░рд╛ рдмрджрд▓рддреЛ.

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

iac/engine.py рдордзреАрд▓ expand() module block рд╣рд╛рддрд╛рд│рддрд╛рдирд╛ рддреНрдпрд╛рдЪрд╛ source рдПрдХрд╛ modules dictionary рдордзреНрдпреЗ рд╢реЛрдзрддреЗ, inputs resolve рдХрд░рддреЗ, рдЖрдгрд┐ module рдЪреЗ рд╕реНрд╡рддрдГрдЪреЗ config module.<name>. рдпрд╛ address prefix рд╕рд╣ рд╡рд┐рд╕реНрддрд╛рд░рддреЗ. Module рдЪреЗ output blocks рддреНрдпрд╛рдЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ scope рдордзреНрдпреЗ рдареЗрд╡рд▓реЗ рдЬрд╛рддрд╛рдд; ${module.east.gate_id} рддрд┐рдереЗ resolve рд╣реЛрддреЗ. рд╢рд┐рдХрд╡рдгреНрдпрд╛рд╕рд╛рдареА рдХреЗрд▓реЗрд▓реЗ рд╕реЛрдкреЗ рд░реВрдк: рдЗрдереЗ module inputs variables, locals рдЖрдгрд┐ literals рд╡рд╛рдкрд░реВ рд╢рдХрддрд╛рдд, рдЗрддрд░ resources рдирд╛рд╣реА.

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

python3 iac/demo.py modules
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply, show_outputs
from demo import WING
for n in (0, 1, 2, 3):
    root = {"module": {"north": {"source": "wing", "wing": "north", "lockers": n}},
            "output": {"gate": {"value": "${module.north.gate_id}"}}}
    cloud = FakeCloud(); p = plan(root, None, cloud, modules={"wing": WING}); st, _, msg = apply(p, cloud)
    print(f"lockers={n} тЖТ {msg} ┬╖ {show_outputs(st)[0]}")
print([a for a in st["resources"]])
EOF

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

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

   Plan: 9 to add, 0 to change, 0 to destroy.
   module.east: school_room.this, school_gate.this, school_locker.row[0], school_locker.row[1]
   module.west: school_room.this, school_gate.this, school_locker.row[0], school_locker.row[1], school_locker.row[2]
тФАтФА apply тЖТ Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
   output east_gate = "gate-01"
   output west_hall = "room-02"
тФАтФА west asks for 4 lockers тЖТ + module.west.school_locker.row[3] (create) тЖТ Plan: 1 to add, 0 to change, 0 to destroy.

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

lockers=0 тЖТ Apply complete! Resources: 2 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
lockers=1 тЖТ Apply complete! Resources: 3 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
lockers=2 тЖТ Apply complete! Resources: 4 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
lockers=3 тЖТ Apply complete! Resources: 5 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
['module.north.school_room.this', 'module.north.school_gate.this', 'module.north.school_locker.row[0]', 'module.north.school_locker.row[1]', 'module.north.school_locker.row[2]']

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

рдПрдХ design, рджреЛрди calls, рдирд╛рд╡рд╛рдВрдЪреНрдпрд╛ рд╕рдВрдШрд░реНрд╖рд╛рд╢рд┐рд╡рд╛рдп 9 resources: рджреЛрдиреНрд╣реА wings рдордзреНрдпреЗ school_room.this рдЖрд╣реЗ, рдЬреЗ module.east. рдЖрдгрд┐ module.west. prefix рдиреЗ рд╡реЗрдЧрд│реЗ рдУрд│рдЦрд▓реЗ рдЬрд╛рддрд╛рдд. рдПрдХрд╛ call рдЪрд╛ input рдмрджрд▓рд▓реНрдпрд╛рд╡рд░ рдлрдХреНрдд рддреНрдпрд╛рдЪ call рд▓рд╛ рдзрдХреНрдХрд╛ рд▓рд╛рдЧрд▓рд╛ (1 to add, module.west рдордзреНрдпреЗ). Outputs рдиреЗ ids module рдмрд╛рд╣реЗрд░ рдЖрдгрд▓реЗ, рдЖрдгрд┐ lockers = 0 рджрд╛рдЦрд╡рддреЗ рдХреА module input рднрд╛рдЧ рдкреВрд░реНрдгрдкрдгреЗ рдмрдВрдж рдХрд░реВ рд╢рдХрддреЛ.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдПрдХ рд╕реНрдерд╛рдирд┐рдХ module рдЖрдгрд┐ рдПрдХ pinned registry module:

module "east" {
  source  = "./modules/wing"
  wing    = "east"
  lockers = 2
}

module "network" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"            # pin the major version you tested
  name    = "campus"
  cidr    = "10.20.0.0/16"
  azs     = ["ap-south-1a", "ap-south-1b"]
  private_subnets = ["10.20.1.0/24", "10.20.2.0/24"]
}

output "east_gate" {
  value = module.east.gate_id
}
terraform init -upgrade              # fetch modules and providers again, within the pins
terraform get                        # download modules only
terraform state list | grep '^module.east'

рдмрд╣реБрддреЗрдХ teams module рд╕рд╛рдареА рд╡рд╛рдкрд░рдд рдЕрд╕рд▓реЗрд▓реА folder рд░рдЪрдирд╛: main.tf, variables.tf, outputs.tf, versions.tf, рдПрдХ README.md, рдЖрдгрд┐ рдПрдХ examples/ folder рдЬреЛ рддреНрдпрд╛рдЪрд╛ test рдореНрд╣рдгреВрдирд╣реА рдХрд╛рдо рдХрд░рддреЛ (рдзрдбрд╛ 11).

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ shared module рд▓рд╛ version, changelog рдЖрдгрд┐ tests рджреНрдпрд╛. Prod рдордзреНрдпреЗ module upgrade рдХрд░рдгреЗ рдореНрд╣рдгрдЬреЗ рдПрдХ pull request рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡рд╛ рдЬреНрдпрд╛рдЪрд╛ plan рддреБрдореНрд╣реА рдЗрддрд░ рдХреЛрдгрддреНрдпрд╛рд╣реА plan рд╕рд╛рд░рдЦрд╛рдЪ рд╡рд╛рдЪрддрд╛.

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

рдЖрддрд╛рдкрд░реНрдпрдВрдд рдПрдХрдЪ рд╡реНрдпрдХреНрддреА рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рдЪрд╛рд▓рд╡рддреЗ. рдПрдХрд╛рдЪ рд╡реЗрд│реА рджреЛрди рд▓реЛрдХ, рдкреНрд░рддреНрдпреЗрдХреАрдХрдбреЗ laptop рд╡рд░ рдПрдХ рдиреЛрдВрджрд╡рд╣реА тАФ рдЕрд╕реЗрдЪ campuses рд╣рд░рд╡рддрд╛рдд. рдкреБрдвреЗ: remote state рдЖрдгрд┐ locking.

git checkout lesson-08-remote-state-locking

ЁЯзй Lesson 07 тАФ Modules: one wing design, built twice

ЁЯУН You are here: Lesson 07 of 12 ┬╖ Previous: lesson-06-dependency-graph ┬╖ Next: lesson-08-remote-state-locking


ЁЯУж What's in this branch

Lessons 01тАУ06, plus reuse. A module is a folder of plans with inputs (variables) and outputs. The works office draws a wing once тАФ a hall, a gate and N lockers тАФ and calls it for the east wing and the west wing with different inputs. Each call gets its own addresses (module.east.school_room.this) and its own outputs. modules() and WING in iac/demo.py; the module handling in expand() in iac/engine.py.

ЁЯзТ Explain like I'm 5

Every wing of the school looks the same: a big hall, a gate into it, and a row of lockers. Drawing all of it again for every wing is boring, and one day someone draws the gate wrong.

So Dipika draws one wing design ЁЯУР with two blanks: "wing name: ____" and "how many lockers: ____ (2 if you don't say)". She puts it in a folder called modules/wing.

Now the plans for the whole school are short:

The contractor builds 4 things for east and 5 for west. When the west wing needs a fourth locker, only the west wing changes. And the design can hand back answers тАФ "the east gate's number is gate-01" тАФ so the rest of the plans can use them.

ЁЯЧ║я╕П Diagram

flowchart LR
    root["ЁЯУЬ root plans"] -->|"wing = east"| e["ЁЯзй module.east<br/>hall ┬╖ gate ┬╖ 2 lockers"]
    root -->|"wing = west, lockers = 3"| w["ЁЯзй module.west<br/>hall ┬╖ gate ┬╖ 3 lockers"]
    design["ЁЯУР modules/wing<br/>inputs: wing, lockers<br/>outputs: gate_id, hall_id"] -.-> e
    design -.-> w
    e -->|"gate_id = gate-01"| out["root outputs"]
    w -->|"hall_id = room-02"| out

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

тЭУ What

ЁЯдФ Why

Because the same pattern built by copy-paste drifts: one wing gets the fix, the others do not. A module makes the pattern one reviewed thing, with a small interface. It also lets a platform team publish good defaults (encryption on, public access off) that every product team gets for free. The cost is indirection: a plan now shows module.east.тАж addresses, and changing the module changes every caller.

ЁЯФз How (in this repo)

expand() in iac/engine.py handles a module block by looking up its source in a modules dictionary, resolving the inputs, and expanding the module's own config with the address prefix module.<name>.. The module's output blocks are kept in its own scope; ${module.east.gate_id} resolves there. A teaching simplification: here module inputs may use variables, locals and literals, not other resources.

ЁЯзк Try it

python3 iac/demo.py modules
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply, show_outputs
from demo import WING
for n in (0, 1, 2, 3):
    root = {"module": {"north": {"source": "wing", "wing": "north", "lockers": n}},
            "output": {"gate": {"value": "${module.north.gate_id}"}}}
    cloud = FakeCloud(); p = plan(root, None, cloud, modules={"wing": WING}); st, _, msg = apply(p, cloud)
    print(f"lockers={n} тЖТ {msg} ┬╖ {show_outputs(st)[0]}")
print([a for a in st["resources"]])
EOF

тЬЕ Verify тАФ what you should see

modules prints:

   Plan: 9 to add, 0 to change, 0 to destroy.
   module.east: school_room.this, school_gate.this, school_locker.row[0], school_locker.row[1]
   module.west: school_room.this, school_gate.this, school_locker.row[0], school_locker.row[1], school_locker.row[2]
тФАтФА apply тЖТ Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
   output east_gate = "gate-01"
   output west_hall = "room-02"
тФАтФА west asks for 4 lockers тЖТ + module.west.school_locker.row[3] (create) тЖТ Plan: 1 to add, 0 to change, 0 to destroy.

Your snippet prints:

lockers=0 тЖТ Apply complete! Resources: 2 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
lockers=1 тЖТ Apply complete! Resources: 3 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
lockers=2 тЖТ Apply complete! Resources: 4 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
lockers=3 тЖТ Apply complete! Resources: 5 added, 0 changed, 0 destroyed. ┬╖ gate = "gate-01"
['module.north.school_room.this', 'module.north.school_gate.this', 'module.north.school_locker.row[0]', 'module.north.school_locker.row[1]', 'module.north.school_locker.row[2]']

ЁЯПБ What you just proved

One design, two calls, 9 resources with no name clashes: both wings have a school_room.this, told apart by the module.east. and module.west. prefix. Changing one call's input touched only that call (1 to add, in module.west). The outputs carried ids out of the module, and lockers = 0 shows a module input can switch parts off entirely.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ a local module and a pinned registry module:

module "east" {
  source  = "./modules/wing"
  wing    = "east"
  lockers = 2
}

module "network" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"            # pin the major version you tested
  name    = "campus"
  cidr    = "10.20.0.0/16"
  azs     = ["ap-south-1a", "ap-south-1b"]
  private_subnets = ["10.20.1.0/24", "10.20.2.0/24"]
}

output "east_gate" {
  value = module.east.gate_id
}
terraform init -upgrade              # fetch modules and providers again, within the pins
terraform get                        # download modules only
terraform state list | grep '^module.east'

The folder layout most teams use for a module: main.tf, variables.tf, outputs.tf, versions.tf, a README.md, and an examples/ folder that doubles as its test (lesson 11).

ЁЯПн Why this matters in production: give each shared module a version, a changelog and tests. Upgrading a module in prod should be a pull request whose plan you read like any other.

тПня╕П Next

So far one person runs the contractor. Two people at once, each with a register on a laptop, is how campuses get lost. Next: remote state and locking.

git checkout lesson-08-remote-state-locking
тЖР Previousdependency graphNext тЖТremote state locking

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