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

ЁЯМкя╕П рдзрдбрд╛ 09 тАФ Drift рдЖрдгрд┐ import: рдХреЛрдгреАрддрд░реА console рдордзреНрдпреЗ click рдХреЗрд▓реЗ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 09 ┬╖ рдорд╛рдЧреАрд▓: lesson-08-remote-state-locking ┬╖ рдкреБрдвреАрд▓: lesson-10-environments


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

рдзрдбреЗ 01тАУ08, рдЖрдгрд┐ рддреНрдпрд╛рд╕реЛрдмрдд рдЦрд░реЗ рдЬрдЧ. рд▓реЛрдХ рдЕрдЬреВрдирд╣реА console рдордзреНрдпреЗ click рдХрд░рддрд╛рдд. Campus рдЖрд░рд╛рдЦрдбреНрдпрд╛рдкрд╛рд╕реВрди рдЖрдгрд┐ рдиреЛрдВрджрд╡рд╣реАрдкрд╛рд╕реВрди drift рд╣реЛрддреЛ тАФ рджреВрд░ рд╕рд░рдХрддреЛ. рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ refresh рдХрд░реВрди drift рд╢реЛрдзрддреЗ, рдЖрдгрд┐ рдкреБрдврдЪрд╛ plan рдЧреЛрд╖реНрдЯреА рдкреВрд░реНрд╡реАрд╕рд╛рд░рдЦреНрдпрд╛ рдХрд░рддреЛ тАФ рдЬреЛрдкрд░реНрдпрдВрдд рддреБрдореНрд╣реА рддреНрдпрд╛рд▓рд╛ ignore_changes рд╕рд╛рдВрдЧрдд рдирд╛рд╣реА. рд╣рд╛рддрд╛рдиреЗ рдмрд╛рдВрдзрд▓реЗрд▓реА рдЧреЛрд╖реНрдЯ import block рдиреЗ рд╕реНрд╡реАрдХрд╛рд░рддрд╛ рдпреЗрддреЗ, рдЖрдгрд┐ рдирд╛рд╡ рдмрджрд▓рд▓реЗрд▓рд╛ address рдкреБрдиреНрд╣рд╛ рдмрд╛рдВрдзрдгреНрдпрд╛рдРрд╡рдЬреА moved block рдиреЗ рдЯрд┐рдХрд╡рддрд╛ рдпреЗрддреЛ. drift() рд╣реЗ iac/demo.py рдордзреНрдпреЗ рдЖрд╣реЗ; refresh() рдЖрдгрд┐ import / moved рдЪреА рд╣рд╛рддрд╛рд│рдгреА iac/engine.py рдордзреАрд▓ plan() рдордзреНрдпреЗ.

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

рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд lab рдордзреНрдпреЗ 30 seats рдЖрд╣реЗрдд рдЕрд╕реЗ рд▓рд┐рд╣рд┐рд▓реЗ рдЖрд╣реЗ. рдордВрдЧрд│рд╡рд╛рд░реА рджреАрдкрд┐рдХрд╛рд▓рд╛ рдПрдХрд╛ рдкрд░реАрдХреНрд╖реЗрд╕рд╛рдареА рдЬрд╛рд╕реНрдд рдЦреБрд░реНрдЪреНрдпрд╛ рд╣рд╡реНрдпрд╛ рдЕрд╕рддрд╛рдд, рдореНрд╣рдгреВрди рддреА рд╕реНрд╡рддрдГрдЪ рдЖрдгрдЦреА 15 рдЦреБрд░реНрдЪреНрдпрд╛ рдЖрдд рдЖрдгрддреЗ. ЁЯкС рдЖрддрд╛ lab рдордзреНрдпреЗ 45 рдЖрд╣реЗрдд. рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд рдЕрдЬреВрдирд╣реА 30 рд▓рд┐рд╣рд┐рд▓реЗ рдЖрд╣реЗ. рдиреЛрдВрджрд╡рд╣реАрддрд╣реА рдЕрдЬреВрди 30 рдЖрд╣реЗ.

рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рдкрд░рдд рдпреЗрддреЗ рддреЗрд╡реНрд╣рд╛ рддреА рдЖрдзреА рдЬрд╛рдЧрд╛ рдлрд┐рд░реВрди рдкрд╛рд╣рддреЗ ЁЯСА: "Room-01 рдордзреНрдпреЗ 45 seats рдЖрд╣реЗрдд, рдиреЛрдВрджрд╡рд╣реА 30 рд╕рд╛рдВрдЧрддреЗ тАФ рдХреЛрдгреАрддрд░реА рд╣рд╛рддрд╛рдиреЗ рдмрджрд▓рд▓реЗ рдЖрд╣реЗ." рдордЧ plan: "рдЖрд░рд╛рдЦрдбрд╛ 30 рд╕рд╛рдВрдЧрддреЛ. 15 рдЦреБрд░реНрдЪреНрдпрд╛ рдмрд╛рд╣реЗрд░ рдХрд╛рдврд╛." рдЦреБрд░реНрдЪреНрдпрд╛ рддрд┐рдереЗрдЪ рд░рд╛рд╣рд╛рд╡реНрдпрд╛рдд рдЕрд╕реЗ рд╢рд╛рд│реЗрд▓рд╛ рд╡рд╛рдЯрдд рдЕрд╕реЗрд▓, рддрд░ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд 45 рд▓рд┐рд╣рд╛рдпрд▓рд╛ рд╣рд╡реЗ тАФ рдХрд┐рдВрд╡рд╛ "рдпрд╛ рдЦреЛрд▓реАрддреАрд▓ seats рдХрдбреЗ рд▓рдХреНрд╖ рджреЗрдК рдирдХрд╛" рдЕрд╕реЗ рд╕рд╛рдВрдЧрд╛рдпрд▓рд╛ рд╣рд╡реЗ (ignore_changes).

рдордЧ рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ рдПрдХ рдорд╛рдЧрдЪреЗ рдлрд╛рдЯрдХ рд╕рд╛рдкрдбрддреЗ рдЬреЗ рдХреЛрдгреАрддрд░реА рдорд╛рдЧрдЪреНрдпрд╛ рд╡рд░реНрд╖реА рд╣рд╛рддрд╛рдиреЗ рдмрд╛рдВрдзрд▓реЗ рд╣реЛрддреЗ. рддреЗ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд рдирд╛рд╣реА, рдиреЛрдВрджрд╡рд╣реАрдд рдирд╛рд╣реА. рддрд┐рдиреЗ рддреЗ рдлрдХреНрдд рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд рдЬреЛрдбрд▓реЗ, рддрд░ рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рджреБрд╕рд░реЗ рдорд╛рдЧрдЪреЗ рдлрд╛рдЯрдХ рдмрд╛рдВрдзреЗрд▓. рддреНрдпрд╛рдРрд╡рдЬреА рддреА рд▓рд┐рд╣рд┐рддреЗ: "school_gate.back рдирд╛рд╡рд╛рдЪреА рдЧреЛрд╖реНрдЯ рдореНрд╣рдгрдЬреЗрдЪ gate-02 рдЖрд╣реЗ." тЬНя╕П рдпрд╛рд▓рд╛рдЪ import рдореНрд╣рдгрддрд╛рдд. рдЖрддрд╛ рдиреЛрдВрджрд╡рд╣реАрд▓рд╛ рддреЗ рдорд╛рд╣реАрдд рдЖрд╣реЗ, рдЖрдгрд┐ рдХрд╛рд╣реАрд╣реА рдирд╡реАрди рдмрд╛рдВрдзрд▓реЗ рдЬрд╛рдд рдирд╛рд╣реА.

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

flowchart LR
    click["ЁЯЦ▒я╕П console click<br/>seats 30 тЖТ 45"] --> campus["ЁЯПл room-01"]
    campus -->|"refresh"| drift["drift: seats 30 тЖТ 45"]
    drift --> plan["plan: ~ seats 45 тЖТ 30"]
    drift --> ign["ignore_changes = [seats]<br/>тЖТ No changes"]
    hand["ЁЯЦРя╕П gate-02 built by hand"] --> imp["import { to = school_gate.back<br/>id = gate-02 }"]
    imp --> p2["Plan: 1 to import, 0 to add"]
    ren["rename lab тЖТ science"] --> mv["moved { from, to }<br/>тЖТ 0 to add, 0 to destroy"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг drift рдордзреНрдпреЗрдЪ incidents рд▓рдкрд▓реЗрд▓реЗ рдЕрд╕рддрд╛рдд. рдкреБрдврдЪреЗ, рдХрд╛рд╣реАрд╣реА рд╕рдВрдмрдВрдз рдирд╕рд▓реЗрд▓реЗ apply рд░рд╛рддреНрд░реА 2 рд╡рд╛рдЬрддрд╛ рдХреЛрдгреАрддрд░реА рдХреЗрд▓реЗрд▓рд╛ hotfix рдЧреБрдкрдЪреВрдк рдЙрд▓рдЯрд╡рддреЗ, рдХрд┐рдВрд╡рд╛ plan рдордзреНрдпреЗ рдХреЛрдгреАрдЪ рди рд▓рд┐рд╣рд┐рд▓реЗрд▓рд╛ рдмрджрд▓ рджрд┐рд╕рддреЛ. Drift рд▓рд╡рдХрд░ рд╢реЛрдзрд▓реНрдпрд╛рдиреЗ (рдПрдХ scheduled plan -refresh-only) рддреНрдпрд╛рдЪреЗ рд░реВрдкрд╛рдВрддрд░ рдЪрд░реНрдЪреЗрдд рд╣реЛрддреЗ. Import рдореБрд│реЗ рдЬреБрдиреЗ, рд╣рд╛рддрд╛рдиреЗ рдмрд╛рдВрдзрд▓реЗрд▓реЗ infrastructure downtime рд╢рд┐рд╡рд╛рдп рдЖрд░рд╛рдЦрдбреНрдпрд╛рдЦрд╛рд▓реА рдпреЗрддреЗ. рдЖрдгрд┐ moved рдиреЗ рддреБрдореНрд╣реА рдкреБрдиреНрд╣рд╛ рди рдмрд╛рдВрдзрддрд╛ refactor рдХрд░рддрд╛ тАФ рдирд╛рд╡ рдмрджрд▓рдгреЗ, module рдордзреНрдпреЗ рд╣рд▓рд╡рдгреЗ, count рд╡рд░реВрди for_each рд╡рд░ рдЬрд╛рдгреЗ.

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

iac/engine.py рдордзреАрд▓ refresh(state, cloud) рдкреНрд░рддреНрдпреЗрдХ id рд╡рд╛рдЪрддреЗ; рдЧрд╛рдпрдм рд╡рд╕реНрддреВ state рдордзреВрди рдХрд╛рдврд▓реНрдпрд╛ рдЬрд╛рддрд╛рдд, рдмрджрд▓рд▓реЗрд▓реЗ attributes drift рдУрд│реА рдореНрд╣рдгреВрди рдиреЛрдВрджрд╡рд▓реЗ рдЬрд╛рддрд╛рдд. plan(..., refresh_only=True) рддрд┐рдереЗрдЪ рдерд╛рдВрдмрддреЗ. lifecycle.ignore_changes рддреНрдпрд╛ keys diff рдордзреВрди рдХрд╛рдвреВрди рдЯрд╛рдХрддреЗ. cfg["import"] diff рдЖрдзреА рд╡рд╕реНрддреВ id рдиреЗ state рдордзреНрдпреЗ рд╡рд╛рдЪрддреЗ рдЖрдгрд┐ рдмрджрд▓рд╛рд▓рд╛ imported рдЕрд╢реА рдЦреВрдг рдХрд░рддреЗ. cfg["moved"] рдЗрддрд░ рдХрд╢рд╛рдЪреНрдпрд╛рд╣реА рдЖрдзреА state рдиреЛрдВрджреАрдВрдЪреА рдирд╛рд╡реЗ рдмрджрд▓рддреЗ. iac/cloud.py рдордзреАрд▓ FakeCloud.click() рдЖрдгрд┐ click_create() рдореНрд╣рдгрдЬреЗрдЪ console.

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

python3 iac/demo.py drift
python3 - <<'EOF'
import sys, copy; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply
cfg = {"resource": {"school_room": {"lab": {"name": "chem-lab", "floor": 2, "seats": 30}},
                    "school_gate": {"main": {"name": "main-gate", "room_id": "${school_room.lab.id}"}}}}
def fresh():
    cloud = FakeCloud(); st, _, _ = apply(plan(cfg, None, cloud), cloud); return cloud, st
cloud, st = fresh(); cloud.delete(st["resources"]["school_gate.main"]["id"])
print("gate deleted by hand  тЖТ", plan(cfg, st, cloud, refresh_only=True).drift, "тЖТ", plan(cfg, st, cloud).summary())
cloud, st = fresh(); cloud.click(st["resources"]["school_room.lab"]["id"], floor=3)
p = plan(cfg, st, cloud)
print("floor clicked 2 тЖТ 3   тЖТ", p.summary())
for line in p.lines(): print("     ", line.strip())
cloud, st = fresh()
h = cloud.click_create("school_gate", name="side-gate", room_id="room-01", open_hours="07-19")
imp = copy.deepcopy(cfg)
imp["resource"]["school_gate"]["side"] = {"name": "side-gate", "room_id": "${school_room.lab.id}"}
imp["import"] = [{"to": "school_gate.side", "id": h}]
p = plan(imp, st, cloud)
print(f"import {h} (built with open_hours 07-19, plans say the default 08-18) тЖТ", p.summary())
for line in p.lines(): print("     ", line.strip())
EOF

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

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

   plan -refresh-only тЖТ ['school_room.lab has changed outside of Terraform: seats 30 тЖТ 45']
   plan тЖТ ~ school_room.lab  seats: 45 тЖТ 30 тЖТ Plan: 0 to add, 1 to change, 0 to destroy.
   with lifecycle ignore_changes = [seats] тЖТ No changes. Your infrastructure matches the configuration.
   plan with the gate in the plans but no import тЖТ Plan: 1 to add, 0 to change, 0 to destroy.  (a second gate)
   Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
   Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.
   without a moved block тЖТ Plan: 3 to add, 0 to change, 3 to destroy.  (the lab AND both gates would be rebuilt)
   with moved { from = school_room.lab, to = school_room.science } тЖТ school_room.lab has moved to school_room.science ┬╖ Plan: 0 to add, 0 to change, 0 to destroy.

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

gate deleted by hand  тЖТ ['school_gate.main has been deleted outside of Terraform'] тЖТ Plan: 1 to add, 0 to change, 0 to destroy.
floor clicked 2 тЖТ 3   тЖТ Plan: 2 to add, 0 to change, 2 to destroy.
      -/+ school_room.lab  floor: 3 тЖТ 2  # forces replacement
      -/+ school_gate.main  room_id: 'room-01' тЖТ (known after apply)  # forces replacement
import gate-02 (built with open_hours 07-19, plans say the default 08-18) тЖТ Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
      тЖР school_gate.side will be imported (id gate-02)
      ~ school_gate.side  open_hours: '07-19' тЖТ '08-18'

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

Plan рдиреЗрд╣рдореА рдЦрд░рд╛ campus рдкрд╛рд╣реВрдирдЪ рд╕реБрд░реВ рд╣реЛрддреЛ, рдореНрд╣рдгреВрди drift рджрд┐рд╕рддреЛрдЪ тАФ рдЖрдгрд┐ "рддреЗ рдкреВрд░реНрд╡реАрд╕рд╛рд░рдЦреЗ рдХрд░рдгреЗ" рд╕реНрд╡рд╕реНрдд рдЕрд╕реВ рд╢рдХрддреЗ (seats, рдПрдХ update) рдХрд┐рдВрд╡рд╛ рдорд╣рд╛рдЧ: ForceNew field рдкрд░рдд рдлрд┐рд░рд╡рдгреЗ рдореНрд╣рдгрдЬреЗ replace, рдЖрдгрд┐ рддреНрдпрд╛ рдЦреЛрд▓реАрдЪрд╛ reference рдЕрд╕рд▓реЗрд▓реЗ gate рд╕реБрджреНрдзрд╛ рддреНрдпрд╛рд╕реЛрдмрдд replace рд╣реЛрддреЗ (2 to add, 2 to destroy). (рд╢рд┐рдХрд╡рдгреНрдпрд╛рдЪрд╛ campus console рд▓рд╛ ForceNew field рдмрджрд▓реВ рджреЗрддреЛ; рдЦрд▒реНрдпрд╛ cloud рд╡рд░ рдЕрд╕рд╛ рдмрджрд▓ рд╕рд╣рд╕рд╛ рдирд╡реНрдпрд╛ id рд╕рд╣ рд╣рд╛рддрд╛рдиреЗ рдХреЗрд▓реЗрд▓реА рдкреБрдирд░реНрдмрд╛рдВрдзрдгреА рдЕрд╕рддреЗ, рдЬреА refresh рд▓рд╛ "deleted" рджрд┐рд╕рддреЗ.) Import рдиреЗ рджреБрд╕рд░реЗ gate рдмрд╛рдВрдзрдгреНрдпрд╛рдРрд╡рдЬреА рдЕрд╕реНрддрд┐рддреНрд╡рд╛рдд рдЕрд╕рд▓реЗрд▓реЗ gate рд╕реНрд╡реАрдХрд╛рд░рд▓реЗ тАФ рдЖрдгрд┐ plan рдиреЗ рд╣реЗрд╣реА рджрд╛рдЦрд╡рд▓реЗ рдХреА рд╣рд╛рддрд╛рдиреЗ рдмрд╛рдВрдзрд▓реЗрд▓реЗ gate рдЖрд░рд╛рдЦрдбреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдХреБрдареЗ рд╡реЗрдЧрд│реЗ рдЖрд╣реЗ. moved block рдиреЗ 3-рдЖрдгрд┐-3 рдкреБрдирд░реНрдмрд╛рдВрдзрдгреАрдЪреЗ рд░реВрдкрд╛рдВрддрд░ рд╢реВрдиреНрдп рдмрджрд▓рд╛рдВрдд рдХреЗрд▓реЗ.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ drift рд╢реЛрдзрд╛, рд╕реНрд╡реАрдХрд╛рд░рд╛, рдирд╛рд╡ рдмрджрд▓рд╛:

terraform plan -refresh-only                   # "Objects have changed outside of Terraform"
terraform apply -refresh-only                  # accept the drift into the state (no cloud changes)
terraform plan -detailed-exitcode              # exit 0 = no changes, 2 = changes: good for a nightly drift job
import {
  to = aws_s3_bucket.reports
  id = "school-reports-2019"       # the id format is on the resource's docs page, under "Import"
}

moved {
  from = aws_s3_bucket.locker[0]
  to   = aws_s3_bucket.locker["a"]
}

resource "aws_autoscaling_group" "web" {
  # ...
  lifecycle { ignore_changes = [desired_capacity] }   # the autoscaler owns this number
}
terraform plan -generate-config-out=generated.tf   # with import blocks: draft config for them (Terraform 1.5+, marked experimental)

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ state рд╕рд╛рдареА schedule рд╡рд░ terraform plan -detailed-exitcode рдЪрд╛рд▓рд╡рд╛ рдЖрдгрд┐ exit code 2 рдЖрд▓рд╛ рдХреА alert рджреНрдпрд╛. рдкреНрд░рддреНрдпреЗрдХ alert рдореНрд╣рдгрдЬреЗ рдПрдХрддрд░ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд рд▓рд┐рд╣рд╛рдпрдЪрд╛ рдмрджрд▓, рдХрд┐рдВрд╡рд╛ рдерд╛рдВрдмрд╡рд╛рдпрдЪрд╛ click.

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

рдЖрддрд╛ рддреНрдпрд╛рдЪ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдиреЗ dev, staging рдЖрдгрд┐ prod рдмрд╛рдВрдзрд╛рдпрдЪреЗ рдЖрд╣реЗрдд тАФ рдПрдХрд╛рдиреЗ рджреБрд╕рд▒реНрдпрд╛рд▓рд╛ рд╣рд╛рдд рди рд▓рд╛рд╡рддрд╛. рдкреБрдвреЗ: environments.

git checkout lesson-10-environments

ЁЯМкя╕П Lesson 09 тАФ Drift & import: someone clicked in the console

ЁЯУН You are here: Lesson 09 of 12 ┬╖ Previous: lesson-08-remote-state-locking ┬╖ Next: lesson-10-environments


ЁЯУж What's in this branch

Lessons 01тАУ08, plus the real world. People still click in the console. The campus drifts away from the plans and the register. The contractor finds drift with a refresh, and the next plan puts things back тАФ unless you tell it to ignore_changes. A thing built by hand can be adopted with an import block, and a renamed address can be kept with a moved block instead of a rebuild. drift() in iac/demo.py; refresh() and the import / moved handling in plan() in iac/engine.py.

ЁЯзТ Explain like I'm 5

The plans say the lab has 30 seats. On Tuesday, Dipika needs more chairs for an exam, so she carries in 15 more herself. ЁЯкС Now the lab has 45. The plans still say 30. The register still says 30.

When the contractor comes back, she walks the site first ЁЯСА: "Room-01 has 45 seats, the register says 30 тАФ someone changed it by hand." Then the plan: "The plans say 30. Take 15 chairs out." If the school wants the chairs to stay, the plans must say 45 тАФ or say "don't watch the seats in this room" (ignore_changes).

Then Aishwarya finds a back gate that someone built by hand last year. It's not in the plans, not in the register. If she just adds it to the plans, the contractor will build a second back gate. Instead she writes: "the thing called school_gate.back is gate-02." тЬНя╕П That's an import. Now the register knows it, and nothing new is built.

ЁЯЧ║я╕П Diagram

flowchart LR
    click["ЁЯЦ▒я╕П console click<br/>seats 30 тЖТ 45"] --> campus["ЁЯПл room-01"]
    campus -->|"refresh"| drift["drift: seats 30 тЖТ 45"]
    drift --> plan["plan: ~ seats 45 тЖТ 30"]
    drift --> ign["ignore_changes = [seats]<br/>тЖТ No changes"]
    hand["ЁЯЦРя╕П gate-02 built by hand"] --> imp["import { to = school_gate.back<br/>id = gate-02 }"]
    imp --> p2["Plan: 1 to import, 0 to add"]
    ren["rename lab тЖТ science"] --> mv["moved { from, to }<br/>тЖТ 0 to add, 0 to destroy"]

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

тЭУ What

ЁЯдФ Why

Because drift is where incidents hide. The next unrelated apply quietly reverts a hotfix someone made at 2 a.m., or a plan shows a change nobody wrote. Finding drift early (a scheduled plan -refresh-only) turns it into a conversation. Import is how old, hand-built infrastructure comes under the plans without downtime. And moved is how you refactor тАФ rename, move into a module, switch count to for_each тАФ without rebuilding.

ЁЯФз How (in this repo)

refresh(state, cloud) in iac/engine.py reads every id; missing objects are dropped from the state, changed attributes are recorded as drift lines. plan(..., refresh_only=True) stops there. lifecycle.ignore_changes removes those keys from the diff. cfg["import"] reads the object by id into the state before the diff and marks the change imported. cfg["moved"] renames state entries before anything else. FakeCloud.click() and click_create() in iac/cloud.py are the console.

ЁЯзк Try it

python3 iac/demo.py drift
python3 - <<'EOF'
import sys, copy; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply
cfg = {"resource": {"school_room": {"lab": {"name": "chem-lab", "floor": 2, "seats": 30}},
                    "school_gate": {"main": {"name": "main-gate", "room_id": "${school_room.lab.id}"}}}}
def fresh():
    cloud = FakeCloud(); st, _, _ = apply(plan(cfg, None, cloud), cloud); return cloud, st
cloud, st = fresh(); cloud.delete(st["resources"]["school_gate.main"]["id"])
print("gate deleted by hand  тЖТ", plan(cfg, st, cloud, refresh_only=True).drift, "тЖТ", plan(cfg, st, cloud).summary())
cloud, st = fresh(); cloud.click(st["resources"]["school_room.lab"]["id"], floor=3)
p = plan(cfg, st, cloud)
print("floor clicked 2 тЖТ 3   тЖТ", p.summary())
for line in p.lines(): print("     ", line.strip())
cloud, st = fresh()
h = cloud.click_create("school_gate", name="side-gate", room_id="room-01", open_hours="07-19")
imp = copy.deepcopy(cfg)
imp["resource"]["school_gate"]["side"] = {"name": "side-gate", "room_id": "${school_room.lab.id}"}
imp["import"] = [{"to": "school_gate.side", "id": h}]
p = plan(imp, st, cloud)
print(f"import {h} (built with open_hours 07-19, plans say the default 08-18) тЖТ", p.summary())
for line in p.lines(): print("     ", line.strip())
EOF

тЬЕ Verify тАФ what you should see

drift prints:

   plan -refresh-only тЖТ ['school_room.lab has changed outside of Terraform: seats 30 тЖТ 45']
   plan тЖТ ~ school_room.lab  seats: 45 тЖТ 30 тЖТ Plan: 0 to add, 1 to change, 0 to destroy.
   with lifecycle ignore_changes = [seats] тЖТ No changes. Your infrastructure matches the configuration.
   plan with the gate in the plans but no import тЖТ Plan: 1 to add, 0 to change, 0 to destroy.  (a second gate)
   Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
   Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.
   without a moved block тЖТ Plan: 3 to add, 0 to change, 3 to destroy.  (the lab AND both gates would be rebuilt)
   with moved { from = school_room.lab, to = school_room.science } тЖТ school_room.lab has moved to school_room.science ┬╖ Plan: 0 to add, 0 to change, 0 to destroy.

Your snippet prints:

gate deleted by hand  тЖТ ['school_gate.main has been deleted outside of Terraform'] тЖТ Plan: 1 to add, 0 to change, 0 to destroy.
floor clicked 2 тЖТ 3   тЖТ Plan: 2 to add, 0 to change, 2 to destroy.
      -/+ school_room.lab  floor: 3 тЖТ 2  # forces replacement
      -/+ school_gate.main  room_id: 'room-01' тЖТ (known after apply)  # forces replacement
import gate-02 (built with open_hours 07-19, plans say the default 08-18) тЖТ Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
      тЖР school_gate.side will be imported (id gate-02)
      ~ school_gate.side  open_hours: '07-19' тЖТ '08-18'

ЁЯПБ What you just proved

A plan always starts by looking at the real campus, so drift shows up тАФ and "putting it back" can be cheap (seats, an update) or expensive: reverting a ForceNew field is a replace, and the gate that references the room is replaced with it (2 to add, 2 to destroy). (The teaching campus lets the console change a ForceNew field; on a real cloud, such a change is usually a hand-made rebuild with a new id, which a refresh sees as "deleted".) Import adopted an existing gate instead of building a second one тАФ and the plan also showed where the hand-built gate differs from the plans. A moved block turned a 3-and-3 rebuild into zero changes.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ find drift, adopt, rename:

terraform plan -refresh-only                   # "Objects have changed outside of Terraform"
terraform apply -refresh-only                  # accept the drift into the state (no cloud changes)
terraform plan -detailed-exitcode              # exit 0 = no changes, 2 = changes: good for a nightly drift job
import {
  to = aws_s3_bucket.reports
  id = "school-reports-2019"       # the id format is on the resource's docs page, under "Import"
}

moved {
  from = aws_s3_bucket.locker[0]
  to   = aws_s3_bucket.locker["a"]
}

resource "aws_autoscaling_group" "web" {
  # ...
  lifecycle { ignore_changes = [desired_capacity] }   # the autoscaler owns this number
}
terraform plan -generate-config-out=generated.tf   # with import blocks: draft config for them (Terraform 1.5+, marked experimental)

ЁЯПн Why this matters in production: run terraform plan -detailed-exitcode on a schedule for every state and alert on exit code 2. Every alert is either a change to write into the plans or a click to stop.

тПня╕П Next

The same plans now need to build dev, staging and prod тАФ without one touching another. Next: environments.

git checkout lesson-10-environments
тЖР Previousremote state lockingNext тЖТenvironments

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