ЁЯПл The SchoolтА║ЁЯЫдя╕П Platform EngineeringтА║ЁЯз░ рдзрдбрд╛ 05 тАФ Self-service infrastructure: рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдХрдбреЗ рдорд╛рдЧрд╛, рд▓рдЧреЗрдЪ рдорд┐рд│рд╡рд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯз░ рдзрдбрд╛ 05 тАФ Self-service infrastructure: рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдХрдбреЗ рдорд╛рдЧрд╛, рд▓рдЧреЗрдЪ рдорд┐рд│рд╡рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-service-catalog ┬╖ рдкреБрдвреЗ: lesson-06-preview-environments


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

рдзрдбреЗ 01тАУ04, рдЖрдгрд┐ рдзрдбрд╛ 02 рдордзрд▓рд╛ рд╕рд░реНрд╡рд╛рдд рдореЛрдард╛ рддреНрд░рд╛рд╕: ticket рд╢рд┐рд╡рд╛рдп database рдорд┐рд│рд╡рдгреЗ. Team рдПрдХрд╛ рдард░рд▓реЗрд▓реНрдпрд╛ menu рдордзреВрди (рдХрд╛рд░реНрдпрд╛рд▓рдп рджреЗрдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЧреЛрд╖реНрдЯреАрдВрдЪрд╛ catalogue) resource рдорд╛рдЧрддреЗ; рд╡рд┐рдирдВрддреА team рдЪреНрдпрд╛ quota рд╢реА рдЖрдгрд┐ рдирд╛рд╡рд╛рдЪреНрдпрд╛ рдирд┐рдпрдорд╛рд╢реА рддрдкрд╛рд╕рд▓реА рдЬрд╛рддреЗ; рддреА рдмрд╕рд▓реА рддрд░ owner tags рд╕рд╣ рд▓рдЧреЗрдЪ provision рд╣реЛрддреЗ; рдирд╛рд╣реА рдмрд╕рд▓реА рддрд░ рдирдХрд╛рд░ рдХрд╛рдп рдХрд░рд╛рдпрдЪреЗ рддреЗ рд╕рд╛рдВрдЧрддреЛ. idp/selfserve.py рдордзрд▓реЗ OFFERINGS рдЖрдгрд┐ Provisioner, idp/demo.py рдордзрд▓реЗ selfservice().

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

рд╕реБрд╡рд┐рдзрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдЪреНрдпрд╛ рднрд┐рдВрддреАрд╡рд░ рдЖрддрд╛ рдПрдХ рдореЗрдиреВ рдЖрд╣реЗ. ЁЯУЛ

рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧрд╛рдЪреА рдПрдХ рдорд░реНрдпрд╛рджрд╛ рдЖрд╣реЗ. рд╡рд┐рдЬреНрдЮрд╛рди рд╡рд┐рднрд╛рдЧрд╛рд▓рд╛ 2 рдХрдкрд╛рдЯреЗ, 3 рдкреЗрдЯреНрдпрд╛, 2 рдорд╛рдВрдбрдгреНрдпрд╛, рдЖрдгрд┐ рдПрдХреВрдг 6 рдлрд░рд╢реНрдпрд╛ рдорд┐рд│реВ рд╢рдХрддрд╛рдд.

рдХрддрд░рд┐рдирд╛ exam results рд╕рд╛рдареА рдореЛрдареЗ рдХрдкрд╛рдЯ рдорд╛рдЧрд╡рддреЗ: тЬЕ рд▓рдЧреЗрдЪ рддрдпрд╛рд░, "owner: science" рдЕрд╢рд╛ рд▓реЗрдмрд▓рд╕рд╣. рдордЧ lab рд╕рд╛рдареА рд▓рд╣рд╛рди рдХрдкрд╛рдЯ: тЬЕ рддрдпрд╛рд░ тАФ рдЖрддрд╛ рд╕рдЧрд│реНрдпрд╛ 6 рдлрд░рд╢реНрдпрд╛ рд╡рд╛рдкрд░рд▓реНрдпрд╛ рдЧреЗрд▓реНрдпрд╛. рдордЧ рддрд┐рд╕рд░реЗ рдХрдкрд╛рдЯ: тЭМ "рд╡рд┐рдЬреНрдЮрд╛рди рд╡рд┐рднрд╛рдЧрд╛рдХрдбреЗ рдЖрдзреАрдЪ 2 рдкреИрдХреА 2 рдХрдкрд╛рдЯреЗ рдЖрд╣реЗрдд тАФ рдПрдХ рдкрд░рдд рдХрд░рд╛, рдХрд┐рдВрд╡рд╛ рдореЛрдареА рдорд░реНрдпрд╛рджрд╛ рдорд╛рдЧрд╛." рдордЧ рдПрдХ "MongoDB wardrobe": тЭМ "рддреЗ рдореЗрдиреВрд╡рд░ рдирд╛рд╣реА тАФ рдореЗрдиреВрд╡рд░ рд╣реЗ рдЖрд╣реЗ, рдХрд┐рдВрд╡рд╛ рдЖрдореНрд╣рд╛рд▓рд╛ рддреЗ рдЬреЛрдбрд╛рдпрд▓рд╛ рд╕рд╛рдВрдЧрд╛."

рдлреЙрд░реНрдо рдирд╛рд╣реАрдд. рджреАрдкрд┐рдХрд╛рдЪреНрдпрд╛ рдврд┐рдЧрд╛рдд рд╡рд╛рдЯ рдкрд╛рд╣рдгреЗ рдирд╛рд╣реА. рдЖрдгрд┐ рдХреЛрдгрддрд╛рдЪ рд╡рд┐рднрд╛рдЧ рд╕рдВрдкреВрд░реНрдг рд╢рд╛рд│рд╛ рдХрдкрд╛рдЯрд╛рдВрдиреА рднрд░реВ рд╢рдХрдд рдирд╛рд╣реА, рдХрд╛рд░рдг рдорд░реНрдпрд╛рджрд╛ рддреНрдпрд╛рд▓рд╛ рдерд╛рдВрдмрд╡рддреЗ.

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

flowchart LR
    k["ЁЯСйтАНЁЯПл science asks:<br/>postgres-medium results-db"] --> m{"ЁЯУЛ on the menu?"}
    m -->|"no: mongodb"| r1["тЭМ offered: postgres-small,<br/>postgres-medium, bucket, queue"]
    m -->|"yes"| q{"ЁЯУП quota left?<br/>databases 2 ┬╖ cpu 6"}
    q -->|"no: 3rd database"| r2["тЭМ 2/2 databases тАФ<br/>delete one or ask for more"]
    q -->|"yes"| p["тЬЕ science-results-db<br/>tags: owner=science,<br/>managed-by=facilities-office"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/platform-engineering/lesson-diagrams.html#l05

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдзрдбрд╛ 01 рдордзрд▓реА ticket queue рдмрд╣реБрддрд╛рдВрд╢реА рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛ рдпреЗрдгрд╛рд▒реНрдпрд╛ рд╡рд┐рдирдВрддреНрдпрд╛рдВрдЪреА рд╣реЛрддреА: "рдорд╛рдЭреНрдпрд╛ service рд╕рд╛рдареА рдПрдХ Postgres", рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛, рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рд╣рд╛рддрд╛рдиреЗ, рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдереЛрдбреА рд╡реЗрдЧрд│реА. рд╕рд░реНрд╡рд╛рдд рд╕рд╛рдорд╛рдиреНрдп рд╡рд┐рдирдВрддреАрд▓рд╛ menu item рдмрдирд╡рд▓реНрдпрд╛рдиреЗ рд╡рд╛рдЯ рдЖрдгрд┐ рдлрд░рдХ рджреЛрдиреНрд╣реА рдирд╛рд╣реАрд╕реЗ рд╣реЛрддрд╛рдд. Quotas рдореБрд│реЗ рдЦрд░реНрдЪ рдЖрдгрд┐ рдзреЛрдХрд╛ рдирд┐рдпрдВрддреНрд░рдгрд╛рдд рдареЗрд╡рдгреНрдпрд╛рд╕рд╛рдареА рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рдирдВрддреА рддрдкрд╛рд╕рд╛рд╡реА рд▓рд╛рдЧрдд рдирд╛рд╣реА тАФ рдирд┐рдпрдо рддреЗ рдХрд░рддреЛ.

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

idp/selfserve.py рдордзрд▓реЗ OFFERINGS рд╣рд╛ menu рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ item рд╕рд╛рдВрдЧрддреЛ рдХреА рддреЛ рдХреЛрдгрддреНрдпрд╛ quota рдордзреВрди рдореЛрдЬрд▓рд╛ рдЬрд╛рддреЛ рдЖрдгрд┐ рдХрд┐рддреА CPU рд╡рд╛рдкрд░рддреЛ. Provisioner(quotas) рдкреНрд░рддреНрдпреЗрдХ team рдЪрд╛ quota рдЖрдгрд┐ рд╡рд╛рдкрд░ рдареЗрд╡рддреЛ. request(team, kind, name) рдпрд╛ рдХреНрд░рдорд╛рдиреЗ рддрдкрд╛рд╕рддреЗ: рддреЗ menu рд╡рд░ рдЖрд╣реЗ рдХрд╛, рдирд╛рд╡ рд╡реИрдз рдЖрд╣реЗ рдХрд╛, team рдХрдбреЗ рддреЛ quota рдЖрд╣реЗ рдХрд╛, рддреНрдпрд╛рддрд▓рд╛ рдПрдЦрд╛рджрд╛ рд╢рд┐рд▓реНрд▓рдХ рдЖрд╣реЗ рдХрд╛, рдкреБрд░реЗрд╕реЗ CPU рд╢рд┐рд▓реНрд▓рдХ рдЖрд╣реЗ рдХрд╛. рдпрд╢ рдорд┐рд│рд╛рд▓реЗ рдХреА <team>-<name> рдирд╛рд╡рд╛рдЪрд╛ resource owner рдЖрдгрд┐ managed-by tags рд╕рд╣ рдкрд░рдд рдорд┐рд│рддреЛ. рддреЗ рдЦрд░реЗ рдХрд╛рд╣реАрдЪ рдмрдирд╡рдд рдирд╛рд╣реА тАФ рд╣реЗ cloud рдЪреЗ рдирд╛рд╣реА, рддрд░ рдирд┐рд░реНрдгрдпрд╛рдЪреЗ рд╢рд┐рдХрд╡рдгреНрдпрд╛рдЪреЗ рдореЙрдбреЗрд▓ рдЖрд╣реЗ.

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

python3 idp/demo.py selfservice
python3 - <<'EOF'
import sys; sys.path.insert(0, "idp"); from selfserve import Provisioner
p = Provisioner({"maths": {"databases": 1, "buckets": 1, "queues": 0, "cpu": 2}})
for kind, name in (("postgres-medium", "timetable-db"), ("postgres-small", "timetable-db"), ("queue", "swaps"),
                   ("bucket", "Old_Papers"), ("bucket", "old-papers"), ("bucket", "more-papers")):
    status, out = p.request("maths", kind, name)
    print(f"{kind:<15} {name:<13} тЖТ {status}: {out['id'] if status == 'ready' else out}")
print("maths used:", p.used["maths"])
EOF

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

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

тФАтФА science's quota: 2 databases, 3 buckets, 2 queues, 6 CPU тАФ requests go to the office's catalogue, not a ticket
   postgres-medium results-db    тЖТ ready: science-results-db {'owner': 'science', 'managed-by': 'facilities-office'}
   postgres-small  lab-db        тЖТ ready: science-lab-db {'owner': 'science', 'managed-by': 'facilities-office'}
   postgres-small  extra-db      тЖТ refused: science already uses 2/2 databases тАФ delete one or ask for a bigger quota
   used now: {'databases': 2, 'buckets': 1, 'queues': 1, 'cpu': 6}

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

postgres-medium timetable-db  тЖТ refused: postgres-medium needs 4 CPU, maths has 2 of 2 left тАФ pick a smaller size or ask for more
postgres-small  timetable-db  тЖТ ready: maths-timetable-db
queue           swaps         тЖТ refused: maths has no queues in its quota тАФ ask the office to add some
bucket          Old_Papers    тЖТ refused: name 'Old_Papers' тАФ use lowercase letters, digits and dashes
bucket          old-papers    тЖТ ready: maths-old-papers
bucket          more-papers   тЖТ refused: maths already uses 1/1 buckets тАФ delete one or ask for a bigger quota
maths used: {'databases': 1, 'buckets': 1, 'queues': 0, 'cpu': 2}

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

рдЧрдгрд┐рдд рд╡рд┐рднрд╛рдЧрд╛рд▓рд╛ рджреЛрди рд╡рд┐рдирдВрддреНрдпрд╛рдВрдд рдЖрдгрд┐ рд╢реВрдиреНрдп tickets рдордзреНрдпреЗ рджреЛрди resources рдорд┐рд│рд╛рд▓реЗ. рддреНрдпрд╛рдЪреЗ рдЪрд╛рд░рд╣реА рдирдХрд╛рд░ рд╡реЗрдЧрд╡реЗрдЧрд│реА рдкреБрдврдЪреА рдкрд╛рдпрд░реА рд╕рд╛рдВрдЧрддрд╛рдд: рд▓рд╣рд╛рди size рдирд┐рд╡рдбрд╛, queues рдорд╛рдЧрд╛, рдирд╛рд╡ рджреБрд░реБрд╕реНрдд рдХрд░рд╛, рдПрдХ bucket рдкрд░рдд рджреНрдпрд╛. рддреАрдЪ рдкрд╣рд┐рд▓реА рд╡рд┐рдирдВрддреА тАФ рдПрдХ medium database тАФ рдЧрдгрд┐рддрд╛рд╕рд╛рдареА (2 CPU) рдирд╛рдХрд╛рд░рд▓реА рдЧреЗрд▓реА рдкрдг рд╡рд┐рдЬреНрдЮрд╛рдирд╛рд╕рд╛рдареА (6 CPU) рдордВрдЬреВрд░ рдЭрд╛рд▓реА: рдирд┐рдпрдо рддреЛрдЪ рдЖрд╣реЗ, quota рдкреНрд░рддреНрдпреЗрдХ team рдЪрд╛ рд╡реЗрдЧрд│рд╛ рдЖрд╣реЗ. Lab рдордзреНрдпреЗ рдХреЛрдгреАрдЪ рдХреЛрдгрддреНрдпрд╛рд╣реА рд╡реНрдпрдХреНрддреАрд▓рд╛ рд╡рд┐рдЪрд╛рд░рд▓реЗ рдирд╛рд╣реА; рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдЪреЗ рдХрд╛рдо рд╡рд┐рдирдВрддреНрдпрд╛ рдХрд░рдгреНрдпрд╛рдкрд╛рд╕реВрди menu рдЖрдгрд┐ рдорд░реНрдпрд╛рджрд╛ рдЖрдЦрдгреНрдпрд╛рдХрдбреЗ рд╕рд░рдХрд▓реЗ.

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

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

Crossplane рд╡рд╛рдкрд░реВрди рдХрд╛рд░реНрдпрд╛рд▓рдп рд╕реНрд╡рддрдГрдЪрд╛ API рдард░рд╡рддреЗ. рдЦрд╛рд▓рдЪрд╛ kind рдЖрдгрд┐ fields рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ CompositeResourceDefinition рдордзреВрди рдпреЗрддрд╛рдд тАФ рддреЗ рдПрдХ рдЙрджрд╛рд╣рд░рдг рдЖрд╣реЗ, Crossplane рдЪрд╛ built-in рдкреНрд░рдХрд╛рд░ рдирд╛рд╣реА. рдЦрд▒реНрдпрд╛ cluster рд╡рд░, team рд╣реЗ apply рдХрд░реЗрд▓:

apiVersion: school.example.org/v1alpha1
kind: PostgreSQLInstance          # defined by the facilities office
metadata:
  name: results-db
  namespace: science
spec:
  size: medium                    # the office's Composition maps this to real cloud settings

(Crossplane v1 рдордзреНрдпреЗ рдЕрд╢рд╛ namespaced рд╡рд┐рдирдВрддреАрд▓рд╛ claim рдореНрд╣рдгрддрд╛рдд; Crossplane v2 рдордзреНрдпреЗ composite resources рд╕реНрд╡рддрдГ namespaced рдЕрд╕реВ рд╢рдХрддрд╛рдд. рддреБрдореНрд╣реА рдХреЛрдгрддреЗ version рдЪрд╛рд▓рд╡рддрд╛ рддреЗ рддрдкрд╛рд╕рд╛.) рдкреНрд░рддреНрдпреЗрдХ team рдЪреА рдорд░реНрдпрд╛рджрд╛ рд╕рд╛рдзрд╛ Kubernetes ResourceQuota рдЕрд╕реВ рд╢рдХрддреЛ:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: science-quota
  namespace: science
spec:
  hard:
    requests.cpu: "6"
    count/postgresqlinstances.school.example.org: "2"

Terraform рд╡рд╛рдкрд░реВрди, menu item рдореНрд╣рдгрдЬреЗ рдХрд╛рд╣реА inputs рдЕрд╕рд▓реЗрд▓рд╛ module:

module "results_db" {
  source = "git::https://github.com/school/terraform-modules.git//postgres?ref=v1.4.0"
  name   = "science-results-db"
  size   = "medium"
  owner  = "science"
}

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рджрд░ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рдЕрдЬреВрдирд╣реА рдХрд┐рддреА infrastructure tickets рдпреЗрддрд╛рдд рддреЗ рдореЛрдЬрд╛. рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛ рдпреЗрдгрд╛рд░рд╛ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рдирдВрддреА-рдкреНрд░рдХрд╛рд░ menu item рдмрдирдгреНрдпрд╛рдЪрд╛ рдЙрдореЗрджрд╡рд╛рд░ рдЖрд╣реЗ.

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

Teams рдирд╛ рдЯрд┐рдХрдгрд╛рд░реЗ resources рдорд┐рд│реВ рд╢рдХрддрд╛рдд. рдкреБрдвреЗ: рдереЛрдбреНрдпрд╛ рдХрд╛рд│рд╛рдЪреЗ resources тАФ рдкреНрд░рддреНрдпреЗрдХ pull request рд╕рд╛рдареА рдПрдХ рд╕рдВрдкреВрд░реНрдг рдЪрд╛рдЪрдгреА environment, рдЬреЗ рдЖрдкреЛрдЖрдк рд╕рд╛рдл рд╣реЛрддреЗ.

git checkout lesson-06-preview-environments

ЁЯз░ Lesson 05 тАФ Self-service infrastructure: ask the office, get it at once

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-service-catalog ┬╖ Next: lesson-06-preview-environments


ЁЯУж What's in this branch

Lessons 01тАУ04, plus the top pain from lesson 02: getting a database without a ticket. A team asks for a resource from a fixed menu (the office's catalogue of offerings); the request is checked against the team's quota and a naming rule; if it fits, it is provisioned at once with owner tags; if not, the refusal says what to do. OFFERINGS and Provisioner in idp/selfserve.py, selfservice() in idp/demo.py.

ЁЯзТ Explain like I'm 5

The facilities office now has a menu on the wall. ЁЯУЛ

Every department has an allowance. Science may have 2 cupboards, 3 boxes, 2 racks, and 6 floor tiles in total.

Katrina orders a big cupboard for exam results: тЬЕ ready at once, with a label "owner: science". Then a small cupboard for the lab: тЬЕ ready тАФ now all 6 tiles are used. Then a third cupboard: тЭМ "Science already has 2 of 2 cupboards тАФ give one back, or ask for a bigger allowance." Then a "MongoDB wardrobe": тЭМ "That is not on the menu тАФ here is what is, or ask us to add it."

No forms. No waiting in Dipika's pile. And no department can fill the whole school with cupboards, because the allowance stops it.

ЁЯЧ║я╕П Diagram

flowchart LR
    k["ЁЯСйтАНЁЯПл science asks:<br/>postgres-medium results-db"] --> m{"ЁЯУЛ on the menu?"}
    m -->|"no: mongodb"| r1["тЭМ offered: postgres-small,<br/>postgres-medium, bucket, queue"]
    m -->|"yes"| q{"ЁЯУП quota left?<br/>databases 2 ┬╖ cpu 6"}
    q -->|"no: 3rd database"| r2["тЭМ 2/2 databases тАФ<br/>delete one or ask for more"]
    q -->|"yes"| p["тЬЕ science-results-db<br/>tags: owner=science,<br/>managed-by=facilities-office"]

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

тЭУ What

ЁЯдФ Why

Because the ticket queue from lesson 01 was mostly repeat requests: "a Postgres for my service", again and again, done by hand each time, a little differently each time. Turning the most common request into a menu item removes the wait and the differences. Quotas mean the office does not have to review every request to keep costs and risk under control тАФ the rule does it.

ЁЯФз How (in this repo)

OFFERINGS in idp/selfserve.py is the menu: each item says which quota it counts against and how much CPU it uses. Provisioner(quotas) keeps each team's quota and usage. request(team, kind, name) checks, in order: is it on the menu, is the name valid, does the team have any of that quota, is there one left, is there enough CPU left. A success returns a resource named <team>-<name> with owner and managed-by tags. It creates nothing real тАФ it is a teaching model of the decision, not of the cloud.

ЁЯзк Try it

python3 idp/demo.py selfservice
python3 - <<'EOF'
import sys; sys.path.insert(0, "idp"); from selfserve import Provisioner
p = Provisioner({"maths": {"databases": 1, "buckets": 1, "queues": 0, "cpu": 2}})
for kind, name in (("postgres-medium", "timetable-db"), ("postgres-small", "timetable-db"), ("queue", "swaps"),
                   ("bucket", "Old_Papers"), ("bucket", "old-papers"), ("bucket", "more-papers")):
    status, out = p.request("maths", kind, name)
    print(f"{kind:<15} {name:<13} тЖТ {status}: {out['id'] if status == 'ready' else out}")
print("maths used:", p.used["maths"])
EOF

тЬЕ Verify тАФ what you should see

selfservice prints:

тФАтФА science's quota: 2 databases, 3 buckets, 2 queues, 6 CPU тАФ requests go to the office's catalogue, not a ticket
   postgres-medium results-db    тЖТ ready: science-results-db {'owner': 'science', 'managed-by': 'facilities-office'}
   postgres-small  lab-db        тЖТ ready: science-lab-db {'owner': 'science', 'managed-by': 'facilities-office'}
   postgres-small  extra-db      тЖТ refused: science already uses 2/2 databases тАФ delete one or ask for a bigger quota
   used now: {'databases': 2, 'buckets': 1, 'queues': 1, 'cpu': 6}

Your snippet prints:

postgres-medium timetable-db  тЖТ refused: postgres-medium needs 4 CPU, maths has 2 of 2 left тАФ pick a smaller size or ask for more
postgres-small  timetable-db  тЖТ ready: maths-timetable-db
queue           swaps         тЖТ refused: maths has no queues in its quota тАФ ask the office to add some
bucket          Old_Papers    тЖТ refused: name 'Old_Papers' тАФ use lowercase letters, digits and dashes
bucket          old-papers    тЖТ ready: maths-old-papers
bucket          more-papers   тЖТ refused: maths already uses 1/1 buckets тАФ delete one or ask for a bigger quota
maths used: {'databases': 1, 'buckets': 1, 'queues': 0, 'cpu': 2}

ЁЯПБ What you just proved

Maths got two resources in two requests and zero tickets. Each of its four refusals says a different next step: pick a smaller size, ask for queues, fix the name, give a bucket back. The same first request тАФ a medium database тАФ was refused for maths (2 CPU) but granted for science (6 CPU): the rule is the same, the quota is per team. Nothing in the lab asked a person; the office's work moved from doing requests to designing the menu and the limits.

тЪая╕П Common mistakes

ЁЯПн In production

With Crossplane, the office defines its own API. The kind and fields below come from the office's own CompositeResourceDefinition тАФ they are an example, not a built-in Crossplane type. On a real cluster, a team would apply:

apiVersion: school.example.org/v1alpha1
kind: PostgreSQLInstance          # defined by the facilities office
metadata:
  name: results-db
  namespace: science
spec:
  size: medium                    # the office's Composition maps this to real cloud settings

(In Crossplane v1 a namespaced request like this is called a claim; Crossplane v2 lets composite resources themselves be namespaced. Check which version you run.) The per-team limit can be a plain Kubernetes ResourceQuota:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: science-quota
  namespace: science
spec:
  hard:
    requests.cpu: "6"
    count/postgresqlinstances.school.example.org: "2"

With Terraform, the menu item is a module with a few inputs:

module "results_db" {
  source = "git::https://github.com/school/terraform-modules.git//postgres?ref=v1.4.0"
  name   = "science-results-db"
  size   = "medium"
  owner  = "science"
}

ЁЯПн Why this matters in production: count how many infrastructure tickets still arrive each week. Every repeat request type is a candidate menu item.

тПня╕П Next

Teams can get lasting resources. Next: short-lived ones тАФ a whole trial environment for every pull request, cleaned up by itself.

git checkout lesson-06-preview-environments
тЖР Previousservice catalogNext тЖТpreview environments

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