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

ЁЯзк рдзрдбрд╛ 11 тАФ Testing рдЖрдгрд┐ policy: рдЖрдзреА рдЖрд░рд╛рдЦрдбрд╛ рд╡рд╛рдЪрдгрд╛рд░реЗ рддрдкрд╛рд╕рдиреАрд╕

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 11 ┬╖ рдорд╛рдЧреАрд▓: lesson-10-environments ┬╖ рдкреБрдвреАрд▓: lesson-12-iac-cicd


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

рдзрдбреЗ 01тАУ10, рдЖрдгрд┐ рдХрд╛рд╣реАрд╣реА рдмрд╛рдВрдзрдгреНрдпрд╛рдЪреНрдпрд╛ рдЖрдзреА рдЪрд╛рд▓рдгрд╛рд▒реНрдпрд╛ рдЖрдкреЛрдЖрдк рддрдкрд╛рд╕рдгреНрдпрд╛: validate (рдЖрдХрд╛рд░ рдмрд░реЛрдмрд░ рдЖрд╣реЗ рдХрд╛?), tests (plan рдЖрдкрд▓реНрдпрд╛рд▓рд╛ рдЕрдкреЗрдХреНрд╖рд┐рдд рддреЗрдЪ рдХрд░рддреЛ рдХрд╛?), policy (рдпрд╛рд▓рд╛ рдкрд░рд╡рд╛рдирдЧреА рдЖрд╣реЗ рдХрд╛?) рдЖрдгрд┐ cost (рдпрд╛рд▓рд╛ рдХрд┐рддреА рдЦрд░реНрдЪ рдпреЗрдИрд▓?). рддрдкрд╛рд╕рдиреАрд╕ plan рд╡рд╛рдЪрддрд╛рдд, campus рдирд╛рд╣реА. policy() рд╣реЗ iac/demo.py рдордзреНрдпреЗ рдЖрд╣реЗ; validate() рд╣реЗ iac/engine.py рдордзреНрдпреЗ; policy_check(), cost() рдЖрдгрд┐ run_tests() рд╣реЗ iac/checks.py рдордзреНрдпреЗ.

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

рдХреЛрдгрддрд╛рд╣реА plan рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░рд╛рдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪрдгреНрдпрд╛рдЖрдзреА рддреЛ рдЪрд╛рд░ рддрдкрд╛рд╕рдиреАрд╕рд╛рдВрдЪреНрдпрд╛ рд╕рдореЛрд░реВрди рдЬрд╛рддреЛ. ЁЯХ╡я╕ПЁЯХ╡я╕ПЁЯХ╡я╕ПЁЯХ╡я╕П

  1. Form рддрдкрд╛рд╕рдгрд╛рд░реА рд▓рд┐рдЦрд╛рдг рдкрд╛рд╣рддреЗ: "рдпрд╛ рдЦреЛрд▓реАрд▓рд╛ рдирд╛рд╡ рдирд╛рд╣реА. 'thirty' рд╣рд╛ рдЖрдХрдбрд╛ рдирд╛рд╣реА. рдЦреЛрд▓реАрдЪреНрдпрд╛ form рд╡рд░ colour рдирд╛рд╡рд╛рдЪрд╛ рд░рдХрд╛рдирд╛рдЪ рдирд╛рд╣реА. рдЖрдгрд┐ рд╣реЗ gate рдЕрд╢рд╛ pool рдХрдбреЗ рдмреЛрдЯ рджрд╛рдЦрд╡рддреЗ рдЬреЛ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдд рдирд╛рд╣реАрдЪ." рддрд┐рд▓рд╛ рдХреЛрдгрддреНрдпрд╛рд╣реА рдЬрд╛рдЧреА рдЬрд╛рдгреНрдпрд╛рдЪреА рдЧрд░рдЬ рдирд╕рддреЗ.
  2. Tester рд╡рд┐рдЪрд╛рд░рддреЗ "plan рддреБрдореНрд╣рд╛рд▓рд╛ рдЕрдкреЗрдХреНрд╖рд┐рдд рддреЗрдЪ рдХрд░рддреЛ рдХрд╛?" "рддреБрдореНрд╣рд╛рд▓рд╛ lab рдордзреНрдпреЗ 30 seats рд╣рд╡реНрдпрд╛ рд╣реЛрддреНрдпрд╛ тАФ рд╣реЛ. 2 lockers тАФ рд╣реЛ. рд░рд╛рддреНрд░реА gates рдмрдВрдж тАФ рдирд╛рд╣реА, рд░рд╛рддреНрд░реАрдЪреЗ gate 00-24 рдЙрдШрдбреЗ рдЖрд╣реЗ."
  3. рдирд┐рдпрдо рдкрд╛рд│рдгрд╛рд░реА рдХрдбреЗ рд╢рд╛рд│реЗрдЪреЗ рдирд┐рдпрдо рдЕрд╕рддрд╛рдд: рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрд▓рд╛ рдПрдХ owner рд╣рд╡рд╛; рд░рд╛рддреНрд░реА gates рдмрдВрдж рд╡реНрд╣рд╛рдпрд▓рд╛ рд╣рд╡реАрдд; рдЦрд▒реНрдпрд╛ campus рд╢рд┐рд╡рд╛рдп рдЗрддрд░рддреНрд░ extra-large lockers рдирд╛рд╣реАрдд. рддреА рддреАрди рд╡реЗрд│рд╛ DENY рдореНрд╣рдгрддреЗ.
  4. рд╣рд┐рд╢реЗрдмрдиреАрд╕ рдмреЗрд░реАрдЬ рдХрд░рддреЗ: рд╣рд╛ plan рджрд░ рдорд╣рд┐рдиреНрдпрд╛рд▓рд╛ тВ╣2830 рдЬрд╛рд╕реНрдд рдЦрд░реНрдЪ рдХрд░рддреЛ.

рдЪрд╛рд░рд╣реА рддрдкрд╛рд╕рдгреНрдпрд╛ рдкрд╛рд░ рдХрд░рдгрд╛рд░рд╛ plan рдЪ рдХрдВрддреНрд░рд╛рдЯрджрд╛рд░рд╛рдХрдбреЗ рдЬрд╛рддреЛ.

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

flowchart LR
    plans["ЁЯУЬ plans"] --> v["ЁЯЦКя╕П validate<br/>shape: 4 errors"]
    v --> plan["ЁЯУЛ plan<br/>5 to add"]
    plan --> t["ЁЯзк tests<br/>2 passed, 1 failed"]
    plan --> pol["ЁЯЪл policy<br/>3 DENY"]
    plan --> c["ЁЯТ░ cost<br/>+тВ╣2830/month"]
    t & pol & c --> gate{"all green?"}
    gate -->|"yes"| apply["apply"]
    gate -->|"no"| fix["fix the plans"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг plans рд╡рд╛рдЪрдгрд╛рд▒реНрдпрд╛ рдорд╛рдгрд╕рд╛рдВрдХрдбреВрди рдЧреЛрд╖реНрдЯреА рд╕реБрдЯрддрд╛рдд, рдЖрдгрд┐ рдХрд╛рд╣реА рдЪреБрдХрд╛ рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рддреНрдпрд╛рдЪ рдЕрд╕рддрд╛рдд: public bucket, рдЧрд╣рд╛рд│ tag, рд░рд╛рддреНрд░рднрд░ рдЙрдШрдбреЗ gate, dev рдордзреНрдпреЗ рдкреНрд░рдЪрдВрдб instance. рдкреНрд░рддреНрдпреЗрдХ plan рд╡рд░ рдЪрд╛рд▓рдгрд╛рд░реА рддрдкрд╛рд╕рдгреА рддреНрдпрд╛ review рдЖрдзреАрдЪ рдкрдХрдбрддреЗ, рдЖрдгрд┐ рдирд┐рдпрдо рдХреЛрдгрд╛рдЪреНрдпрд╛ рддрд░реА рдбреЛрдХреНрдпрд╛рдд рди рд░рд╛рд╣рддрд╛ code рдордзреНрдпреЗ рджрд┐рд╕рддрд╛рдд. Plan рд╡рд░рдЪреНрдпрд╛ рддрдкрд╛рд╕рдгреНрдпрд╛ рд╕реНрд╡рд╕реНрдд рдЕрд╕рддрд╛рдд: apply рдирд╛рд╣реА, рд╡рд╛рдЯ рдкрд╛рд╣рдгреЗ рдирд╛рд╣реА, рдирдВрддрд░рдЪреА рд╕рд╛рдлрд╕рдлрд╛рдИ рдирд╛рд╣реА. command = apply рд╕рд╣ terraform test рдкреБрдвреЗ рдЬрд╛рдКрди рдЦрд▒реНрдпрд╛ рдЧреЛрд╖реНрдЯреА рдмрд╛рдВрдзрддреЗ, рдЬреЗ рд╣рд│реВ рдЕрд╕рддреЗ рдЖрдгрд┐ рдкреИрд╕реЗ рдЦрд░реНрдЪ рдХрд░рддреЗ, рдореНрд╣рдгреВрди рдмрд╣реБрддреЗрдХ teams рддреЗ modules рд╕рд╛рдареАрдЪ рдареЗрд╡рддрд╛рдд.

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

iac/engine.py рдордзреАрд▓ validate(cfg) рдкреНрд░рддреНрдпреЗрдХ block SCHEMAS рдиреБрд╕рд╛рд░ рддрдкрд╛рд╕рддреЗ, Terraform рдЪреНрдпрд╛рдЪ error рд╢рдмреНрджрд░рдЪрдиреЗрдд. iac/checks.py рдордзреАрд▓ POLICIES рд╣реА (name, rule) рдЪреА рдпрд╛рджреА рдЖрд╣реЗ; рдкреНрд░рддреНрдпреЗрдХ rule рдПрдХ planned рдмрджрд▓ (рдЖрдгрд┐ environment) рдкрд╛рд╣рддреЛ рдЖрдгрд┐ рдирд╛рдХрд╛рд░рдгреНрдпрд╛рдЪреЗ рдХрд╛рд░рдг рдкрд░рдд рджреЗрддреЛ тАФ Python рдордзреНрдпреЗ рд▓рд┐рд╣рд┐рд▓реЗрд▓реНрдпрд╛ Rego deny rule рдЪрд╛рдЪ рдЖрдХрд╛рд░. cost(p) plan рдЪреНрдпрд╛ рдЖрдзреА рдЖрдгрд┐ рдирдВрддрд░рдЪреНрдпрд╛ resources рдЪреА рдХрд╛рд▓реНрдкрдирд┐рдХ рд░реБрдкрдпрд╛рдВрдЪреНрдпрд╛ рдХрд┐рдВрдорддреАрдВрдиреА рдХрд┐рдВрдордд рдХрд╛рдврддреЗ. run_tests() рд░рд┐рдХрд╛рдореНрдпрд╛ рдиреЛрдВрджрд╡рд╣реАрд╡рд░ plan рдХрд░рддреЗ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ assertion рддрдкрд╛рд╕рддреЗ, terraform test рдкреНрд░рдорд╛рдгреЗрдЪ Success! рдХрд┐рдВрд╡рд╛ Failure! рдЫрд╛рдкрддреЗ.

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

python3 iac/demo.py policy
python3 - <<'EOF'
import sys, copy; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan
from checks import policy_check, cost, POLICIES
from demo import POLICY
p = plan(POLICY, None, FakeCloud())
for env in ("dev", "prod"): print(f"{env:<5} тЖТ {len(policy_check(p, env))} denied")
fixed = copy.deepcopy(POLICY)
fixed["resource"]["school_room"]["gym"]["tags"] = {"owner": "aishwarya"}
fixed["resource"]["school_gate"]["night"]["open_hours"] = "06-22"
fixed["resource"]["school_locker"]["big"]["size"] = "m"
q = plan(fixed, None, FakeCloud())
print("fixed тЖТ", policy_check(q, "dev"), "┬╖ cost", cost(q))
print(len(POLICIES), "rules")
EOF

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

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

   school_room.gym: Inappropriate value for attribute "seats": a number is required.
   school_room.gym: An argument named "colour" is not expected here.
   school_room.gym: The argument "name" is required, but no definition was found.
   school_gate.g: Reference to undeclared resource "school_room.pool".
   DENY school_room.gym: every room needs an owner tag (tags has no 'owner')
   DENY school_gate.night: gates must close at night (open_hours = '00-24')
   DENY school_locker.big: no xl lockers outside prod (size = 'xl' in dev)
тФАтФА cost estimate: тВ╣0/month тЖТ тВ╣2830/month (+2830) тАФ made-up prices, the shape of an Infracost comment
     run "the lab has 30 seats"... pass
     run "the lab gets 2 lockers"... pass
     run "gates close at night"... fail
   Failure! 2 passed, 1 failed.

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

dev   тЖТ 3 denied
prod  тЖТ 2 denied
fixed тЖТ [] ┬╖ cost (0, 2780)
3 rules

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

Validate рд▓рд╛ campus, рдиреЛрдВрджрд╡рд╣реА рдХрд┐рдВрд╡рд╛ plan рд╢рд┐рд╡рд╛рдп 4 рдЪреБрдХрд╛ рд╕рд╛рдкрдбрд▓реНрдпрд╛. рддреЛрдЪ plan dev рдордзреНрдпреЗ 3 рд╡реЗрд│рд╛ рдЖрдгрд┐ prod рдордзреНрдпреЗ 2 рд╡реЗрд│рд╛ рдирд╛рдХрд╛рд░рд▓рд╛ рдЧреЗрд▓рд╛ тАФ policy environment рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЕрд╕реВ рд╢рдХрддреЗ (prod рдордзреНрдпреЗ xl lockers рдирд╛ рдкрд░рд╡рд╛рдирдЧреА рдЖрд╣реЗ). рддреАрди рдПрдХ-рдУрд│реА рджреБрд░реБрд╕реНрддреНрдпрд╛рдВрдирдВрддрд░ рдХрд╛рд╣реАрдЪ рдирд╛рдХрд╛рд░рд▓реЗ рдЧреЗрд▓реЗ рдирд╛рд╣реА, рдЖрдгрд┐ рдЦрд░реНрдЪ тВ╣2830 рд╡рд░реВрди тВ╣2780 рд╡рд░ рдЖрд▓рд╛ рдХрд╛рд░рдг xl locker m рдЭрд╛рд▓рд╛. рдЕрдкрдпрд╢реА test рдиреЗ policy рдиреЗ рдкрдХрдбрд▓реЗрд▓реЗрдЪ рд░рд╛рддреНрд░реАрдЪреЗ gate рдкрдХрдбрд▓реЗ: tests рд╕рд╛рдВрдЧрддрд╛рдд рдХреА рд╣реЗ config рдХрд╛рдп рдХрд░рд╛рдпрд▓рд╛рдЪ рд╣рд╡реЗ; policies рд╕рд╛рдВрдЧрддрд╛рдд рдХреА рдХреЛрдгрддреЗрд╣реА config рдХрд╛рдп рдХрд░реВ рд╢рдХрддреЗ.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ pipeline рдЪрд╛рд▓рд╡рддреЗ рддреНрдпрд╛ рддрдкрд╛рд╕рдгреНрдпрд╛, рдХреНрд░рдорд╛рдиреЗ:

terraform fmt -check -recursive
terraform init -backend=false && terraform validate
tflint --recursive
terraform plan -out=tfplan && terraform show -json tfplan > plan.json
conftest test plan.json --policy policy/         # OPA / Rego rules
checkov -f plan.json                             # built-in rules for common cloud mistakes
infracost breakdown --path plan.json             # monthly cost of the plan
terraform test                                   # runs tests/*.tftest.hcl (Terraform 1.6+)

рдПрдХ test file (tests/lab.tftest.hcl):

run "locker_names_start_with_env" {
  command = plan
  variables { env = "dev" }
  assert {
    condition     = aws_s3_bucket.locker["a"].bucket == "dev-school-locker-a"
    error_message = "locker bucket names must start with the environment"
  }
}

рдПрдХ conftest rule (policy/tags.rego, Rego v1 syntax):

package main

deny contains msg if {
  rc := input.resource_changes[_]
  rc.type == "aws_s3_bucket"
  not rc.change.after.tags.owner
  msg := sprintf("%s: every bucket needs an owner tag", [rc.address])
}

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рддреБрдордЪреНрдпрд╛ рд╢реЗрд╡рдЯрдЪреНрдпрд╛ рддреАрди incidents рд╢реА рдЬреБрд│рдгрд╛рд▒реНрдпрд╛ рддреАрди policies рдиреЗ рд╕реБрд░реБрд╡рд╛рдд рдХрд░рд╛, рддреНрдпрд╛ merge рдерд╛рдВрдмрд╡рддреАрд▓ рдЕрд╕реЗ рдХрд░рд╛, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХреАрд▓рд╛ рджреБрд░реБрд╕реНрддреАрдЪрд╛ рд╕реНрдкрд╖реНрдЯ message рджреНрдпрд╛.

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

рд╕рдЧрд│реЗ conveyor belt рд╡рд░ рдареЗрд╡рд╛рдпрд▓рд╛ рддрдпрд╛рд░ рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ pull request рд╡рд░ plan, merge рд╡рд░ apply, рд╕рд╛рдард╡рд▓реЗрд▓реНрдпрд╛ keys рдирд╛рд╣реАрдд. рдкреБрдвреЗ: CI/CD рдордзреАрд▓ IaC тАФ рдЖрдгрд┐ рд╕рдВрдкреВрд░реНрдг рдЪрд┐рддреНрд░.

git checkout lesson-12-iac-cicd

ЁЯзк Lesson 11 тАФ Testing & policy: inspectors who read the plans first

ЁЯУН You are here: Lesson 11 of 12 ┬╖ Previous: lesson-10-environments ┬╖ Next: lesson-12-iac-cicd


ЁЯУж What's in this branch

Lessons 01тАУ10, plus automatic checks that run before anything is built: validate (is the shape right?), tests (does the plan do what we meant?), policy (is it allowed?) and cost (what will it cost?). The inspectors read the plan, not the campus. policy() in iac/demo.py; validate() in iac/engine.py; policy_check(), cost() and run_tests() in iac/checks.py.

ЁЯзТ Explain like I'm 5

Before any plan reaches the contractor, it goes past four inspectors. ЁЯХ╡я╕ПЁЯХ╡я╕ПЁЯХ╡я╕ПЁЯХ╡я╕П

  1. The form checker looks at the writing: "This room has no name. 'thirty' is not a number. There is no box called colour on a room form. And this gate points at a pool that isn't in the plans." She doesn't need to visit any site.
  2. The tester asks "does the plan do what you meant?" "You meant the lab to have 30 seats тАФ yes. 2 lockers тАФ yes. Gates closed at night тАФ no, the night gate is open 00-24."
  3. The rule keeper has the school's rules: every room needs an owner; gates close at night; no extra-large lockers except in the real campus. She says DENY three times.
  4. The accountant adds it up: this plan costs тВ╣2830 a month more.

Only a plan that passes all four goes to the contractor.

ЁЯЧ║я╕П Diagram

flowchart LR
    plans["ЁЯУЬ plans"] --> v["ЁЯЦКя╕П validate<br/>shape: 4 errors"]
    v --> plan["ЁЯУЛ plan<br/>5 to add"]
    plan --> t["ЁЯзк tests<br/>2 passed, 1 failed"]
    plan --> pol["ЁЯЪл policy<br/>3 DENY"]
    plan --> c["ЁЯТ░ cost<br/>+тВ╣2830/month"]
    t & pol & c --> gate{"all green?"}
    gate -->|"yes"| apply["apply"]
    gate -->|"no"| fix["fix the plans"]

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

тЭУ What

ЁЯдФ Why

Because humans reading plans miss things, and some mistakes are the same every time: a public bucket, a missing tag, a gate open all night, a huge instance in dev. A check that runs on every plan catches them before review, and makes the rules visible in code instead of in someone's head. Checks on the plan are cheap: no apply, no waiting, no cleanup. terraform test with command = apply goes further and builds real things, which is slower and costs money, so most teams keep it for modules.

ЁЯФз How (in this repo)

validate(cfg) in iac/engine.py checks each block against SCHEMAS, using Terraform's own error wording. POLICIES in iac/checks.py is a list of (name, rule); each rule looks at one planned change (and the environment) and returns a reason to deny тАФ the shape of a Rego deny rule, written in Python. cost(p) prices the resources before and after the plan with made-up rupee prices. run_tests() plans against an empty register and checks each assertion, printing Success! or Failure! like terraform test.

ЁЯзк Try it

python3 iac/demo.py policy
python3 - <<'EOF'
import sys, copy; sys.path.insert(0, "iac")
from cloud import FakeCloud
from engine import plan
from checks import policy_check, cost, POLICIES
from demo import POLICY
p = plan(POLICY, None, FakeCloud())
for env in ("dev", "prod"): print(f"{env:<5} тЖТ {len(policy_check(p, env))} denied")
fixed = copy.deepcopy(POLICY)
fixed["resource"]["school_room"]["gym"]["tags"] = {"owner": "aishwarya"}
fixed["resource"]["school_gate"]["night"]["open_hours"] = "06-22"
fixed["resource"]["school_locker"]["big"]["size"] = "m"
q = plan(fixed, None, FakeCloud())
print("fixed тЖТ", policy_check(q, "dev"), "┬╖ cost", cost(q))
print(len(POLICIES), "rules")
EOF

тЬЕ Verify тАФ what you should see

policy prints:

   school_room.gym: Inappropriate value for attribute "seats": a number is required.
   school_room.gym: An argument named "colour" is not expected here.
   school_room.gym: The argument "name" is required, but no definition was found.
   school_gate.g: Reference to undeclared resource "school_room.pool".
   DENY school_room.gym: every room needs an owner tag (tags has no 'owner')
   DENY school_gate.night: gates must close at night (open_hours = '00-24')
   DENY school_locker.big: no xl lockers outside prod (size = 'xl' in dev)
тФАтФА cost estimate: тВ╣0/month тЖТ тВ╣2830/month (+2830) тАФ made-up prices, the shape of an Infracost comment
     run "the lab has 30 seats"... pass
     run "the lab gets 2 lockers"... pass
     run "gates close at night"... fail
   Failure! 2 passed, 1 failed.

Your snippet prints:

dev   тЖТ 3 denied
prod  тЖТ 2 denied
fixed тЖТ [] ┬╖ cost (0, 2780)
3 rules

ЁЯПБ What you just proved

Validate found 4 mistakes without a campus, a register or a plan. The same plan was denied 3 times in dev and 2 times in prod тАФ policy can depend on the environment (xl lockers are allowed in prod). After three one-line fixes, nothing was denied, and the cost dropped from тВ╣2830 to тВ╣2780 because the xl locker became an m. The failing test caught the same night gate as the policy: tests say what this config must do; policies say what every config may do.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ the checks a pipeline runs, in order:

terraform fmt -check -recursive
terraform init -backend=false && terraform validate
tflint --recursive
terraform plan -out=tfplan && terraform show -json tfplan > plan.json
conftest test plan.json --policy policy/         # OPA / Rego rules
checkov -f plan.json                             # built-in rules for common cloud mistakes
infracost breakdown --path plan.json             # monthly cost of the plan
terraform test                                   # runs tests/*.tftest.hcl (Terraform 1.6+)

A test file (tests/lab.tftest.hcl):

run "locker_names_start_with_env" {
  command = plan
  variables { env = "dev" }
  assert {
    condition     = aws_s3_bucket.locker["a"].bucket == "dev-school-locker-a"
    error_message = "locker bucket names must start with the environment"
  }
}

A conftest rule (policy/tags.rego, Rego v1 syntax):

package main

deny contains msg if {
  rc := input.resource_changes[_]
  rc.type == "aws_s3_bucket"
  not rc.change.after.tags.owner
  msg := sprintf("%s: every bucket needs an owner tag", [rc.address])
}

ЁЯПн Why this matters in production: start with three policies that match your last three incidents, make them block the merge, and add a clear fix message to each.

тПня╕П Next

Everything is ready to put on a conveyor belt: plan on every pull request, apply on merge, no stored keys. Next: IaC in CI/CD тАФ and the whole picture.

git checkout lesson-12-iac-cicd
тЖР PreviousenvironmentsNext тЖТiac cicd

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