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

ЁЯУЛ рдзрдбрд╛ 03 тАФ Plan рдЖрдгрд┐ apply: рдлрдХреНрдд рдлрд░рдХ рдмрд╛рдВрдзрд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: рдзрдбрд╛ 03 / 12 ┬╖ рдорд╛рдЧреЗ: lesson-02-resources-providers ┬╖ рдкреБрдвреЗ: lesson-04-variables-loops


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

рдзрдбреЗ 01тАУ02, рдЖрдгрд┐ Terraform рдЪреЗ рд╣реГрджрдп: plan. рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рддреАрди рдЧреЛрд╖реНрдЯреАрдВрдЪреА рддреБрд▓рдирд╛ рдХрд░рддреЛ тАФ рдЖрд░рд╛рдЦрдбрд╛ (config), рдиреЛрдВрджрд╡рд╣реА (state) рдЖрдгрд┐ campus (рдЦрд░реЗ objects) тАФ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ address рдЪреЗ рддреЛ рдХрд╛рдп рдХрд░реЗрд▓ рдпрд╛рдЪреА рдпрд╛рджреА рдХрд░рддреЛ: create (+), рдЬрд╛рдЧреАрдЪ update (~), replace (-/+, рдЬреЗрд╡реНрд╣рд╛ ForceNew argument рдмрджрд▓рддреЛ) рдХрд┐рдВрд╡рд╛ destroy (-). рдордЧ apply рдиреЗрдордХреЗ рддреЗрдЪ рдХрд░рддреЗ. iac/demo.py рдордзреАрд▓ plan_(), iac/engine.py рдордзреАрд▓ plan() рдЖрдгрд┐ apply().

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

рдЖрд░рд╛рдЦрдбреНрдпрд╛рдЪреА version 1 рдмрд╛рдВрдзреВрди рдЭрд╛рд▓реА рдЖрд╣реЗ: рдПрдХ lab, рдПрдХ art room рдЖрдгрд┐ рдПрдХ рдореБрдЦреНрдп gate.

рдЖрддрд╛ рдРрд╢реНрд╡рд░реНрдпрд╛ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдЪреА version 2 рд▓рд┐рд╣рд┐рддреЗ:

рдХреЛрдгреА рдПрдХрд╣реА рд╡реАрдЯ рд╣рд▓рд╡рдгреНрдпрд╛рдЖрдзреА рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рдЬрд╛рдЧреЗрд╡рд░ рдлрд┐рд░реВрди рдПрдХ рдпрд╛рджреА рд▓рд┐рд╣рд┐рддреЛ: ЁЯУЭ

рдРрд╢реНрд╡рд░реНрдпрд╛ рдпрд╛рджреА рд╡рд╛рдЪрддреЗ. рдордЧ рддреА "рд╣реЛ" рдореНрд╣рдгрддреЗ. рддреНрдпрд╛рдирдВрддрд░рдЪ рдмрд╛рдВрдзрдХрд╛рдо рд╕реБрд░реВ рд╣реЛрддреЗ.

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

flowchart LR
    cfg["ЁЯУЬ plans v2"] --> diff{"ЁЯС╖ compare"}
    st["ЁЯУТ register<br/>(state)"] --> diff
    campus["ЁЯПл campus<br/>(refresh)"] --> st
    diff --> u["~ lab<br/>seats 30 тЖТ 40"]
    diff --> r["-/+ art<br/>floor 1 тЖТ 3<br/>forces replacement"]
    diff --> c["+ library"]
    diff --> d["- gate"]
    u & r & c & d --> sum["Plan: 2 to add,<br/>1 to change, 2 to destroy"]
    sum -->|"yes"| apply["apply"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг plan рд╣рд╛ рдЕрдирд░реНрде рдШрдбрдгреНрдпрд╛рдЖрдзреА рдкрдХрдбрдгреНрдпрд╛рдЪрд╛ рдХреНрд╖рдг рдЖрд╣реЗ. Database рд╡рд░ -/+ рдореНрд╣рдгрдЬреЗ рддреБрдореНрд╣реА рдЖрдзреА рдпреЛрдЬрдирд╛ рдХреЗрд▓реА рдирд╕реЗрд▓ рддрд░ рддреНрдпрд╛рдЪрд╛ data рдЧреЗрд▓рд╛. рдЕрдирдкреЗрдХреНрд╖рд┐рдд - destroy рдореНрд╣рдгрдЬреЗ рдХрд╛рд╣реАрддрд░реА rename рдЭрд╛рд▓реЗ рдХрд┐рдВрд╡рд╛ рдХрд╛рдврд▓реЗ рдЧреЗрд▓реЗ. рдЪрд╛рдВрдЧрд▓реА team рдкреНрд░рддреНрдпреЗрдХ plan рд╡рд╛рдЪрддреЗ рдЖрдгрд┐ replace рдХрд┐рдВрд╡рд╛ destroy рд▓рд╛ рдЙрддреНрддрд░ рд╣рд╡рд╛ рдЕрд╕рд▓реЗрд▓рд╛ рдкреНрд░рд╢реНрди рдорд╛рдирддреЗ. Plan рдореБрд│реЗ рдмрджрд▓ рд▓рд╣рд╛рдирд╣реА рд░рд╛рд╣рддрд╛рдд: рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рдлрдХреНрдд рдлрд░рдХ рдмрд╛рдВрдзрддреЛ, рд╕рдВрдкреВрд░реНрдг campus рдкреБрдиреНрд╣рд╛ рдирд╛рд╣реА.

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

iac/engine.py рдордзреАрд▓ plan(cfg, state, cloud) state refresh рдХрд░рддреЗ, config рдЪреЗ instances рдордзреНрдпреЗ рд╡рд┐рд╕реНрддрд╛рд░ рдХрд░рддреЗ, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ address рд╕рд╛рдареА рд╣рд╡реЗ рдЕрд╕рд▓реЗрд▓реЗ arguments (with_defaults) state рдЪреНрдпрд╛ attributes рд╢реА рддрд╛рдбрддреЗ. рдЬреНрдпрд╛рдЪреНрдпрд╛ schema рдордзреНрдпреЗ force_new рдЖрд╣реЗ рдЕрд╕рд╛ argument рдмрджрд▓рд▓рд╛ рддрд░ рддреЛ рдмрджрд▓ replace рдард░рддреЛ; рдирд╛рд╣реАрддрд░ рддреЛ update рдЕрд╕рддреЛ. State рдордзреНрдпреЗ рдЙрд░рд▓реЗрд▓реЗ addresses delete рдЕрд╕рддрд╛рдд. Plan.lines() diff print рдХрд░рддреЗ рдЖрдгрд┐ Plan.summary() рд╕рд╛рд░рд╛рдВрд╢рд╛рдЪреА рдУрд│. apply(p, cloud) рдХреНрд░рд┐рдпрд╛ рдЪрд╛рд▓рд╡рддреЗ рдЖрдгрд┐ рдирд╡реА state рдкрд░рдд рджреЗрддреЗ.

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

python3 iac/demo.py plan
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply
base = {"name": "lab", "floor": 1, "seats": 30}
for change in ({}, {"seats": 40}, {"name": "science-lab"}, {"floor": 2}, {"seats": 40, "floor": 2}):
    cloud = FakeCloud()
    st, _, _ = apply(plan({"resource": {"school_room": {"lab": base}}}, None, cloud), cloud)
    p = plan({"resource": {"school_room": {"lab": dict(base, **change)}}}, st, cloud)
    print(f"{str(change):<28} тЖТ {p.changes[0]['action']:<8} {p.summary()}")
EOF

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

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

     ~ school_room.lab  seats: 30 тЖТ 40
   -/+ school_room.art  floor: 1 тЖТ 3  # forces replacement
     + school_room.library (create)
     - school_gate.main (destroy)
   Plan: 2 to add, 1 to change, 2 to destroy.
тФАтФА apply тЖТ Apply complete! Resources: 2 added, 1 changed, 2 destroyed.
   the art room is a NEW object: room-02 тЖТ room-03 ┬╖ the lab kept its id room-01

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

{}                           тЖТ no-op    No changes. Your infrastructure matches the configuration.
{'seats': 40}                тЖТ update   Plan: 0 to add, 1 to change, 0 to destroy.
{'name': 'science-lab'}      тЖТ update   Plan: 0 to add, 1 to change, 0 to destroy.
{'floor': 2}                 тЖТ replace  Plan: 1 to add, 0 to change, 1 to destroy.
{'seats': 40, 'floor': 2}    тЖТ replace  Plan: 1 to add, 0 to change, 1 to destroy.

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

рдПрдХрд╛рдЪ рдкреНрд░рдХрд╛рд░рдЪрд╛ рдмрджрд▓ тАФ рдПрдХ рдЖрдХрдбрд╛ рдмрджрд▓рдгреЗ тАФ рдирд┐рд░реБрдкрджреНрд░рд╡реА update (seats) рдЕрд╕реВ рд╢рдХрддреЛ рдХрд┐рдВрд╡рд╛ рдкреБрдиреНрд╣рд╛ рдмрд╛рдВрдзрдХрд╛рдо (floor). рдмрджрд▓ рдХрд┐рддреА рдореЛрдард╛ рдЖрд╣реЗ рдпрд╛рд╡рд░реВрди рдирд╛рд╣реА, schema рдард░рд╡рддреЗ. ForceNew рдмрджрд▓ рдЖрдгрд┐ рд╕рд╛рдзрд╛ рдмрджрд▓ рдПрдХрддреНрд░ рдЖрд▓реЗ рддрд░ рд╕рдВрдкреВрд░реНрдг рдЧреЛрд╖реНрдЯ replace рдард░рддреЗ. рдЖрдгрд┐ replace рдирдВрддрд░ art room рд▓рд╛ рдирд╡рд╛ id рдорд┐рд│рд╛рд▓рд╛ (room-02 тЖТ room-03): рдЖрд░рд╛рдЦрдбреНрдпрд╛рдмрд╛рд╣реЗрд░рдЪреЗ рдЬреЗ рдХрд╛рд╣реА room-02 рд▓рдХреНрд╖рд╛рдд рдареЗрд╡реВрди рд╣реЛрддреЗ рддреЗ рдЖрддрд╛ рдХрд╢рд╛рдХрдбреЗрдЪ рдирд┐рд░реНрджреЗрд╢ рдХрд░рдд рдирд╛рд╣реА.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ plan рд╕рд╛рдард╡рд╛, рд╡рд╛рдЪрд╛, рдиреЗрдордХрд╛ рддреЛрдЪ plan apply рдХрд░рд╛:

terraform plan -out=tfplan               # the plan, saved
terraform show tfplan                    # read it again, as text
terraform show -json tfplan > plan.json  # for tools: policy, cost, comments (lesson 11)
terraform apply tfplan                   # applies THIS plan тАФ no new diff, no "yes" prompt

AWS provider рдХрдбреВрди рдЦрд▒реНрдпрд╛ plan рдордзреАрд▓ рдПрдХ replace (ami рдмрджрд▓рд▓реНрдпрд╛рд╕ рдирд╡рд╛ instance рд▓рд╛рдЧрддреЛ; instance_type рдЬрд╛рдЧреАрдЪ рдмрджрд▓реВ рд╢рдХрддреЛ):

  # aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
      ~ ami = "ami-0aaa..." -> "ami-0bbb..." # forces replacement

lifecycle block рдиреЗ replacement рдирд┐рдпрдВрддреНрд░рд┐рдд рдХрд░рд╛:

resource "aws_instance" "web" {
  ami           = var.ami
  instance_type = "t3.small"
  lifecycle {
    create_before_destroy = true   # build the new one first, then remove the old one
    prevent_destroy       = false  # set true on things that must never be destroyed by a plan
  }
}

terraform plan -replace=aws_instance.web рдореБрджреНрджрд╛рдо replace рдШрдбрд╡рддреЗ (рдмрд┐рдШрдбрд▓реЗрд▓реНрдпрд╛ server рд╕рд╛рдареА), рдЖрдгрд┐ terraform destroy state рдордзреАрд▓ рд╕рд░реНрд╡ рдХрд╛рд╣реА рдХрд╛рдвреВрди рдЯрд╛рдХрдгреНрдпрд╛рдЪрд╛ plan рдХрд░рддреЗ.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: "-/+ рдЖрдгрд┐ - рдУрд│реА рдореЛрдЬрд╛" рд╣реЗ рдкреНрд░рддреНрдпреЗрдХ review рдЪрд╛ рднрд╛рдЧ рдмрдирд╡рд╛. Data рдареЗрд╡рдгрд╛рд▒реНрдпрд╛ databases рдЖрдгрд┐ buckets рд╡рд░ prevent_destroy = true рд▓рд╛рд╡рд╛, рдореНрд╣рдгрдЬреЗ рддреНрдпрд╛рдВрдирд╛ destroy рдХрд░рдгрд╛рд░рд╛ plan рддреНрдпрд╛рдРрд╡рдЬреА fail рд╣реЛрдИрд▓.

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

рдПрдХ lab рд╕реЛрдкреА рдЖрд╣реЗ. рдЕрдиреЗрдХ рдЧрд╛рд╡рд╛рдВрд╕рд╛рдареА, рдереЛрдбреНрдпрд╛ рд╡реЗрдЧрд│реНрдпрд╛ рдЖрдХрд╛рд░рд╛рдВрдЪреНрдпрд╛ рдЕрдиреЗрдХ labs рд╕рд╛рдареА variables, outputs рдЖрдгрд┐ loops рд▓рд╛рдЧрддрд╛рдд.

git checkout lesson-04-variables-loops

ЁЯУЛ Lesson 03 тАФ Plan & apply: build only the difference

ЁЯУН You are here: Lesson 03 of 12 ┬╖ Previous: lesson-02-resources-providers ┬╖ Next: lesson-04-variables-loops


ЁЯУж What's in this branch

Lessons 01тАУ02, plus the heart of Terraform: the plan. The contractor compares three things тАФ the plans (config), the register (state) and the campus (the real objects) тАФ and lists what it would do to each address: create (+), update in place (~), replace (-/+, when a ForceNew argument changes) or destroy (-). Then apply does exactly that. plan_() in iac/demo.py, plan() and apply() in iac/engine.py.

ЁЯзТ Explain like I'm 5

Version 1 of the plans is built: a lab, an art room and a main gate.

Now Aishwarya writes version 2 of the plans:

Before anyone touches a brick, the contractor walks the site and writes a list: ЁЯУЭ

Aishwarya reads the list. Then she says "yes". Only then does the building start.

ЁЯЧ║я╕П Diagram

flowchart LR
    cfg["ЁЯУЬ plans v2"] --> diff{"ЁЯС╖ compare"}
    st["ЁЯУТ register<br/>(state)"] --> diff
    campus["ЁЯПл campus<br/>(refresh)"] --> st
    diff --> u["~ lab<br/>seats 30 тЖТ 40"]
    diff --> r["-/+ art<br/>floor 1 тЖТ 3<br/>forces replacement"]
    diff --> c["+ library"]
    diff --> d["- gate"]
    u & r & c & d --> sum["Plan: 2 to add,<br/>1 to change, 2 to destroy"]
    sum -->|"yes"| apply["apply"]

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

тЭУ What

ЁЯдФ Why

Because the plan is the moment to catch a disaster before it happens. -/+ on a database means its data is gone unless you planned for it. A surprise - destroy means something was renamed or removed. A good team reads every plan and treats a replace or a destroy as a question that needs an answer. The plan also makes changes small: the contractor builds only the difference, not the whole campus again.

ЁЯФз How (in this repo)

plan(cfg, state, cloud) in iac/engine.py refreshes the state, expands the config into instances, and for each address compares the wanted arguments (with_defaults) with the state's attributes. A changed argument whose schema has force_new makes the change a replace; otherwise it is an update. Addresses left in the state are delete. Plan.lines() prints the diff and Plan.summary() the summary line. apply(p, cloud) runs the actions and returns the new state.

ЁЯзк Try it

python3 iac/demo.py plan
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan, apply
base = {"name": "lab", "floor": 1, "seats": 30}
for change in ({}, {"seats": 40}, {"name": "science-lab"}, {"floor": 2}, {"seats": 40, "floor": 2}):
    cloud = FakeCloud()
    st, _, _ = apply(plan({"resource": {"school_room": {"lab": base}}}, None, cloud), cloud)
    p = plan({"resource": {"school_room": {"lab": dict(base, **change)}}}, st, cloud)
    print(f"{str(change):<28} тЖТ {p.changes[0]['action']:<8} {p.summary()}")
EOF

тЬЕ Verify тАФ what you should see

plan prints:

     ~ school_room.lab  seats: 30 тЖТ 40
   -/+ school_room.art  floor: 1 тЖТ 3  # forces replacement
     + school_room.library (create)
     - school_gate.main (destroy)
   Plan: 2 to add, 1 to change, 2 to destroy.
тФАтФА apply тЖТ Apply complete! Resources: 2 added, 1 changed, 2 destroyed.
   the art room is a NEW object: room-02 тЖТ room-03 ┬╖ the lab kept its id room-01

Your snippet prints:

{}                           тЖТ no-op    No changes. Your infrastructure matches the configuration.
{'seats': 40}                тЖТ update   Plan: 0 to add, 1 to change, 0 to destroy.
{'name': 'science-lab'}      тЖТ update   Plan: 0 to add, 1 to change, 0 to destroy.
{'floor': 2}                 тЖТ replace  Plan: 1 to add, 0 to change, 1 to destroy.
{'seats': 40, 'floor': 2}    тЖТ replace  Plan: 1 to add, 0 to change, 1 to destroy.

ЁЯПБ What you just proved

The same kind of edit тАФ change one number тАФ can be a harmless update (seats) or a rebuild (floor). The schema decides, not the size of the edit. When a ForceNew change and a normal change come together, the whole thing is a replace. And after the replace, the art room had a new id (room-02 тЖТ room-03): anything outside the plans that remembered room-02 now points at nothing.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ save the plan, read it, apply exactly that plan:

terraform plan -out=tfplan               # the plan, saved
terraform show tfplan                    # read it again, as text
terraform show -json tfplan > plan.json  # for tools: policy, cost, comments (lesson 11)
terraform apply tfplan                   # applies THIS plan тАФ no new diff, no "yes" prompt

A replace in a real plan, from the AWS provider (changing ami forces a new instance; instance_type can change in place):

  # aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
      ~ ami = "ami-0aaa..." -> "ami-0bbb..." # forces replacement

Control replacement with the lifecycle block:

resource "aws_instance" "web" {
  ami           = var.ami
  instance_type = "t3.small"
  lifecycle {
    create_before_destroy = true   # build the new one first, then remove the old one
    prevent_destroy       = false  # set true on things that must never be destroyed by a plan
  }
}

terraform plan -replace=aws_instance.web forces a replace on purpose (for a broken server), and terraform destroy plans the removal of everything in the state.

ЁЯПн Why this matters in production: make "count the -/+ and - lines" part of every review. Put prevent_destroy = true on databases and buckets that hold data, so a plan that would destroy them fails instead.

тПня╕П Next

One lab is easy. Many labs, for many towns, with slightly different sizes, need variables, outputs and loops.

git checkout lesson-04-variables-loops
тЖР Previousresources providersNext тЖТvariables loops

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