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

ЁЯз▒ рдзрдбрд╛ 02 тАФ Resources рдЖрдгрд┐ providers: рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд рдХрд╛рдп рд▓рд┐рд╣рд┐рддрд╛ рдпреЗрддреЗ

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


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

рдзрдбрд╛ 01, рдЖрдгрд┐ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдЪрд╛ рд╢рдмреНрджрд╕рдВрдЧреНрд░рд╣. рдПрдХ resource block campus рд╡рд░реАрд▓ рдПрдХрд╛ рдЧреЛрд╖реНрдЯреАрдЪреЗ рд╡рд░реНрдгрди рдХрд░рддреЛ. рдПрдХрд╛ provider рд▓рд╛ рддреНрдпрд╛ рдкреНрд░рдХрд╛рд░рдЪреА рдЧреЛрд╖реНрдЯ рдХрд╢реА рдмрд╛рдВрдзрд╛рдпрдЪреА рддреЗ рдорд╛рд╣реАрдд рдЕрд╕рддреЗ рдЖрдгрд┐ рддреЛ рдПрдХ schema рдкреНрд░рд╕рд┐рджреНрдз рдХрд░рддреЛ: рдХреЛрдгрддреЗ arguments required рдЖрд╣реЗрдд, рдХреЛрдгрддреНрдпрд╛рдВрдирд╛ defaults рдЖрд╣реЗрдд, рдХреЛрдгрддреЗ campus рд╕реНрд╡рддрдГ рднрд░рддреЛ (computed, рдЬрд╕реЗ id), рдЖрдгрд┐ рдХреЛрдгрддреЗ рдирд╡реАрди object рдмрд╛рдВрдзрд▓реНрдпрд╛рд╢рд┐рд╡рд╛рдп рдмрджрд▓рддрд╛ рдпреЗрдд рдирд╛рд╣реАрдд (ForceNew). iac/demo.py рдордзреАрд▓ resources(), iac/cloud.py рдордзреАрд▓ SCHEMAS рдЖрдгрд┐ FakeCloud, iac/engine.py рдордзреАрд▓ to_hcl() рдЖрдгрд┐ validate().

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

рдЖрд░рд╛рдЦрдбрд╛ рдПрдХрд╛ рдЦрд╛рд╕ form рд╡рд░ рд▓рд┐рд╣рд┐рд▓рд╛ рдЬрд╛рддреЛ. ЁЯУЛ Form рд╡рд░реАрд▓ рдкреНрд░рддреНрдпреЗрдХ рдЪреМрдХрдЯ рдХрд╛рдп рдмрд╛рдВрдзрд╛рдпрдЪреЗ рддреЗ рд╕рд╛рдВрдЧрддреЗ:

Room called lab ┬╖ name: chem-lab ┬╖ floor: 2 ┬╖ seats: 30

рдЦреЛрд▓реНрдпрд╛ рдмрд╛рдВрдзрдгрд╛рд▒реНрдпрд╛ рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░рд╛рдХрдбреЗ рдПрдХ рдирд┐рдпрдо-рдкреБрд╕реНрддрдХ рдЕрд╕рддреЗ. рддреНрдпрд╛рдд рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ рдЕрд╕рддреЗ:

Gate рдЪреНрдпрд╛ form рдордзреНрдпреЗ room рдирд╛рд╡рд╛рдЪреА рдПрдХ рдЪреМрдХрдЯ рдЕрд╕рддреЗ. рддрд┐рдереЗ рддреБрдореНрд╣реА рдирдВрдмрд░ рд▓рд┐рд╣реАрдд рдирд╛рд╣реА. рддреБрдореНрд╣реА "lab рдЪрд╛ id" рдЕрд╕реЗ рд▓рд┐рд╣рд┐рддрд╛, рдЖрдгрд┐ lab рддрдпрд╛рд░ рдЭрд╛рд▓реНрдпрд╛рд╡рд░ рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рдЦрд░рд╛ рдирдВрдмрд░ рднрд░рддреЛ.

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

flowchart LR
    plans["ЁЯУЬ the plans<br/>resource school_room lab<br/>name, floor, seats"]
    schema["ЁЯУЦ provider schema<br/>name: required<br/>floor: default 1, ForceNew<br/>seats: default 30<br/>id: computed"]
    prov["ЁЯС╖ provider<br/>(a plugin)"]
    api["ЁЯПл campus API<br/>POST /rooms"]
    plans --> prov
    schema --- prov
    prov -->|"create"| api
    api -->|"id = room-01"| prov

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг schema рдореБрд│реЗрдЪ рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░ рди рдмрд╛рдВрдзрддрд╛ plan рдХрд░реВ рд╢рдХрддреЛ. рддреНрдпрд╛рд▓рд╛ рдорд╛рд╣реАрдд рдЕрд╕рддреЗ рдХреА seats рдЬрд╛рдЧреАрдЪ рдмрджрд▓реВ рд╢рдХрддреЗ рдкрдг floor рдирд╛рд╣реА, рдЖрдгрд┐ id campus рдХрдбреВрди рдпреЗрддреЛ. рддреЛ рдХреЛрдгрддрд╛рд╣реА API call рдХрд░рдгреНрдпрд╛рдЖрдзреА рдЪреВрдХ рдирд╛рдХрд╛рд░реВ рд╢рдХрддреЛ: рдЕрдиреЛрд│рдЦреА argument, рд╣рд░рд╡рд▓реЗрд▓рд╛ required argument, рдЖрдХрдбреНрдпрд╛рдЪреНрдпрд╛ рдЬрд╛рдЧреА рдордЬрдХреВрд░. Provider рд╡реЗрдЧрд│рд╛ рдЕрд╕рд▓реНрдпрд╛рдореБрд│реЗ Terraform core рддреЗрдЪ рд░рд╛рд╣рддреЗ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ API рд╕рд╛рдареА рдПрдХ plugin рдмреЛрд▓рдгреНрдпрд╛рдЪреЗ рдХрд╛рдо рдХрд░рддреЛ.

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

iac/cloud.py рдордзреАрд▓ SCHEMAS рд╣реЗ рддреАрди types рд╕рд╛рдареАрдЪреЗ provider schema рдЖрд╣реЗ. with_defaults() рдЖрд░рд╛рдЦрдбреНрдпрд╛рдиреЗ рд╕реЛрдбрд▓реЗрд▓реЗ defaults рднрд░рддреЗ. FakeCloud.create() object рд╕рд╛рдард╡рддреЗ, room-01 рд╕рд╛рд░рдЦрд╛ id рдирд┐рд╡рдбрддреЗ, рдЖрдгрд┐ API call cloud.calls рдордзреНрдпреЗ рдиреЛрдВрджрд╡рддреЗ. Engine рдЪреЗ validate() config рд▓рд╛ SCHEMAS рд╡рд┐рд░реБрджреНрдз рддрдкрд╛рд╕рддреЗ. to_hcl() config dict HCL рдореНрд╣рдгреВрди print рдХрд░рддреЗ, рдореНрд╣рдгрдЬреЗ рддреБрдореНрд╣реА рддреЗ рдЦрд▒реНрдпрд╛ .tf file рд╢реА рддрд╛рдбреВрди рдкрд╛рд╣реВ рд╢рдХрддрд╛.

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

python3 iac/demo.py resources
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud, with_defaults
from engine import plan, apply, validate
from demo import CAMPUS
print("defaults:", with_defaults("school_locker", {"room_id": "room-01", "label": "a"}))
cloud = FakeCloud(); st, steps, msg = apply(plan(CAMPUS, None, cloud), cloud)
print(msg)
for call in cloud.calls: print("  ", call)
print("gate points at:", st["resources"]["school_gate.main"]["attrs"]["room_id"])
print(validate({"resource": {"school_room": {"x": {"name": "x", "id": "room-99"}}, "school_pool": {"p": {}}}}))
EOF

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

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

   name   required ┬╖ string
   floor  default 1 ┬╖ number ┬╖ forces a new room if changed
   seats  default 30 ┬╖ number
   tags   default {} ┬╖ map
   id     computed ┬╖ string
тФАтФА apply тЖТ Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
   the provider called the campus API: ['POST /rooms тЖТ room-01']

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

defaults: {'room_id': 'room-01', 'label': 'a', 'size': 'm'}
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
   POST /rooms тЖТ room-01
   POST /gates тЖТ gate-01
   POST /lockers тЖТ locker-01
   POST /lockers тЖТ locker-02
   POST /lockers тЖТ locker-03
gate points at: room-01
['school_room.x: An argument named "id" is not expected here.', 'Invalid resource type: the provider does not support resource type "school_pool".']

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

рдЖрд░рд╛рдЦрдбреНрдпрд╛рдиреЗ рдХрдзреАрдЪ room-01 рдореНрд╣рдЯрд▓реЗ рдирд╛рд╣реА; campus рдиреЗ рддреЗ рдирд┐рд╡рдбрд▓реЗ, рдЖрдгрд┐ рдЦреЛрд▓реА рддрдпрд╛рд░ рдЭрд╛рд▓реНрдпрд╛рд╡рд░ gate рдЪрд╛ room_id рддреНрдпрд╛рдиреЗ рднрд░рд▓рд╛ рдЧреЗрд▓рд╛. рдЦреЛрд▓реА gate рдЖрдгрд┐ lockers рдЪреНрдпрд╛ рдЖрдзреА рддрдпрд╛рд░ рдЭрд╛рд▓реА (рдХрд╛ рддреЗ рдзрдбрд╛ 06 рд╕рдордЬрд╛рд╡рддреЛ). рдЖрдгрд┐ validate рдиреЗ campus рд▓рд╛ рдЕрдЬрд┐рдмрд╛рдд call рди рдХрд░рддрд╛ рджреЛрди рдЪреБрдХрд╛ рдирд╛рдХрд╛рд░рд▓реНрдпрд╛: id computed рдЖрд╣реЗ, рдореНрд╣рдгреВрди рдЖрд░рд╛рдЦрдбрд╛ рддреЗ рдард░рд╡реВ рд╢рдХрдд рдирд╛рд╣реА, рдЖрдгрд┐ school_pool рд╣рд╛ provider рд▓рд╛ рдорд╛рд╣реАрдд рдЕрд╕рд▓реЗрд▓рд╛ type рдирд╛рд╣реА.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдЦрд░рд╛ provider рдЖрдгрд┐ рдЦрд░рд╛ resource. terraform init provider download рдХрд░рддреЗ рдЖрдгрд┐ рдиреЗрдордХреА version .terraform.lock.hcl рдордзреНрдпреЗ рдиреЛрдВрджрд╡рддреЗ (рддреА file commit рдХрд░рд╛):

terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"          # any 6.x, never 7.0 by surprise
    }
  }
}

provider "aws" {
  region = "ap-south-1"
}

resource "aws_s3_bucket" "timetables" {
  bucket = "school-timetables-example"   # changing a bucket's name forces a new bucket
  tags   = { owner = "works-office" }
}

resource "aws_s3_bucket_versioning" "timetables" {
  bucket = aws_s3_bucket.timetables.id  # a reference: plain HCL, no quotes
  versioning_configuration { status = "Enabled" }
}
terraform init            # installs hashicorp/aws 6.x, writes .terraform.lock.hcl
terraform providers       # which providers this config needs
terraform validate        # "Success! The configuration is valid."

registry.terraform.io рд╡рд░реАрд▓ provider documentation рдореНрд╣рдгрдЬреЗ рд╡рд╛рдЪрддрд╛ рдпреЗрдгреНрдпрд╛рдЬреЛрдЧреНрдпрд╛ рд╕реНрд╡рд░реВрдкрд╛рддреАрд▓ schema: рдкреНрд░рддреНрдпреЗрдХ argument рд╕рд╛рдареА рддреЗ Required/Optional рд╕рд╛рдВрдЧрддреЗ рдЖрдгрд┐ рдмрджрд▓рд╛рдореБрд│реЗ рдирд╡реАрди resource рдХреЗрд╡реНрд╣рд╛ рд▓рд╛рдЧрддреЛ рддреЗ рдиреЛрдВрджрд╡рддреЗ.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: resource рд▓рд┐рд╣рд┐рдгреНрдпрд╛рдЖрдзреА provider docs рдордзреАрд▓ рддреНрдпрд╛рдЪреЗ рдкрд╛рди рд╡рд╛рдЪрд╛: рдХреЛрдгрддреЗ arguments replacement рдШрдбрд╡рддрд╛рдд, рдХреЛрдгрддреЗ computed рдЖрд╣реЗрдд, рдЖрдгрд┐ рддреЛ рдХреЛрдгрддрд╛ import id рдЕрдкреЗрдХреНрд╖рд┐рдд рдзрд░рддреЛ. рдкреБрдврдЪрд╛ рдмрджрд▓ рдкрдЯрдХрди рд╣реЛрдгрд╛рд░рд╛ update рдЕрд╕реЗрд▓ рдХреА рдкреБрдиреНрд╣рд╛ рдмрд╛рдВрдзрдХрд╛рдо, рд╣реЗ рддреЗ рдкрд╛рдирдЪ рдард░рд╡рддреЗ.

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

рдЖрддрд╛ рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░рд╛рд▓рд╛ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдЪрд╛ рдЕрд░реНрде рдХрд│рддреЛ. рдкреБрдвреЗ: рддреЛ рдХрд╛рдп рдХрд░рд╛рдпрдЪреЗ рддреЗ рдХрд╕реЗ рдард░рд╡рддреЛ тАФ рдореНрд╣рдгрдЬреЗ plan: create, update, replace рдХрд┐рдВрд╡рд╛ destroy.

git checkout lesson-03-plan-apply

ЁЯз▒ Lesson 02 тАФ Resources & providers: what the plans may say

ЁЯУН You are here: Lesson 02 of 12 ┬╖ Previous: lesson-01-why-iac ┬╖ Next: lesson-03-plan-apply


ЁЯУж What's in this branch

Lesson 01, plus the vocabulary of the plans. A resource block describes one thing on the campus. A provider knows how to build that kind of thing and publishes a schema: which arguments are required, which have defaults, which the campus fills in itself (computed, like the id), and which cannot be changed without building a new object (ForceNew). resources() in iac/demo.py, SCHEMAS and FakeCloud in iac/cloud.py, to_hcl() and validate() in iac/engine.py.

ЁЯзТ Explain like I'm 5

The plans are written on a special form. ЁЯУЛ Each box on the form says what to build:

Room called lab ┬╖ name: chem-lab ┬╖ floor: 2 ┬╖ seats: 30

The contractor who builds rooms has a rule book. It says:

A gate form has a box called room. You don't write a number there. You write "the id of the lab", and the contractor fills in the real number after the lab exists.

ЁЯЧ║я╕П Diagram

flowchart LR
    plans["ЁЯУЬ the plans<br/>resource school_room lab<br/>name, floor, seats"]
    schema["ЁЯУЦ provider schema<br/>name: required<br/>floor: default 1, ForceNew<br/>seats: default 30<br/>id: computed"]
    prov["ЁЯС╖ provider<br/>(a plugin)"]
    api["ЁЯПл campus API<br/>POST /rooms"]
    plans --> prov
    schema --- prov
    prov -->|"create"| api
    api -->|"id = room-01"| prov

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

тЭУ What

ЁЯдФ Why

Because the schema is what lets the contractor plan without building. It knows that seats can change in place but floor cannot, and that id comes from the campus. It can also reject a mistake before any API call: an unknown argument, a missing required one, text where a number belongs. The provider split means Terraform core stays the same while one plugin per API does the talking.

ЁЯФз How (in this repo)

SCHEMAS in iac/cloud.py is the provider schema for the three types. with_defaults() fills in defaults the plans left out. FakeCloud.create() stores the object, chooses an id like room-01, and records the API call in cloud.calls. The engine's validate() checks a config against SCHEMAS. to_hcl() prints a config dict as HCL, so you can compare it with a real .tf file.

ЁЯзк Try it

python3 iac/demo.py resources
python3 - <<'EOF'
import sys; sys.path.insert(0, "iac")
from cloud import FakeCloud, with_defaults
from engine import plan, apply, validate
from demo import CAMPUS
print("defaults:", with_defaults("school_locker", {"room_id": "room-01", "label": "a"}))
cloud = FakeCloud(); st, steps, msg = apply(plan(CAMPUS, None, cloud), cloud)
print(msg)
for call in cloud.calls: print("  ", call)
print("gate points at:", st["resources"]["school_gate.main"]["attrs"]["room_id"])
print(validate({"resource": {"school_room": {"x": {"name": "x", "id": "room-99"}}, "school_pool": {"p": {}}}}))
EOF

тЬЕ Verify тАФ what you should see

resources prints:

   name   required ┬╖ string
   floor  default 1 ┬╖ number ┬╖ forces a new room if changed
   seats  default 30 ┬╖ number
   tags   default {} ┬╖ map
   id     computed ┬╖ string
тФАтФА apply тЖТ Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
   the provider called the campus API: ['POST /rooms тЖТ room-01']

Your snippet prints:

defaults: {'room_id': 'room-01', 'label': 'a', 'size': 'm'}
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
   POST /rooms тЖТ room-01
   POST /gates тЖТ gate-01
   POST /lockers тЖТ locker-01
   POST /lockers тЖТ locker-02
   POST /lockers тЖТ locker-03
gate points at: room-01
['school_room.x: An argument named "id" is not expected here.', 'Invalid resource type: the provider does not support resource type "school_pool".']

ЁЯПБ What you just proved

The plans never said room-01; the campus chose it, and the gate's room_id was filled in with it after the room existed. The room was created before the gate and the lockers (lesson 06 explains why). And validate refused two mistakes without calling the campus at all: id is computed, so the plans may not set it, and school_pool is not a type the provider knows.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ a real provider and a real resource. terraform init downloads the provider and records the exact version in .terraform.lock.hcl (commit that file):

terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"          # any 6.x, never 7.0 by surprise
    }
  }
}

provider "aws" {
  region = "ap-south-1"
}

resource "aws_s3_bucket" "timetables" {
  bucket = "school-timetables-example"   # changing a bucket's name forces a new bucket
  tags   = { owner = "works-office" }
}

resource "aws_s3_bucket_versioning" "timetables" {
  bucket = aws_s3_bucket.timetables.id  # a reference: plain HCL, no quotes
  versioning_configuration { status = "Enabled" }
}
terraform init            # installs hashicorp/aws 6.x, writes .terraform.lock.hcl
terraform providers       # which providers this config needs
terraform validate        # "Success! The configuration is valid."

The provider documentation on registry.terraform.io is the schema in readable form: for each argument it says Required/Optional and notes when a change forces a new resource.

ЁЯПн Why this matters in production: before writing a resource, read its page in the provider docs: which arguments force replacement, which are computed, and what import id it expects. That page decides whether a later change is a quick update or a rebuild.

тПня╕П Next

The contractor now knows what the plans mean. Next: how it decides what to do тАФ the plan: create, update, replace or destroy.

git checkout lesson-03-plan-apply
тЖР Previouswhy iacNext тЖТplan apply

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