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

ЁЯкг рдзрдбрд╛ 03 тАФ Toil: рдЖрдзреА рдмрд╛рджрд▓реНрдпрд╛ рдореЛрдЬрд╛, рдордЧ рддреНрдпрд╛рдВрдирд╛ automate рдХрд░рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 03 ┬╖ рдорд╛рдЧреЗ: lesson-02-error-budget-policy ┬╖ рдкреБрдвреЗ: lesson-04-on-call


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

рдзрдбрд╛ 02, рдЕрдзрд┐рдХ рдкрдердХрд╛рдЪреА toil рдЪреА рдиреЛрдВрджрд╡рд╣реА. Toil рдореНрд╣рдгреВрди рдХрд╛рдп рдореЛрдЬрд▓реЗ рдЬрд╛рддреЗ (рдЖрдгрд┐ рдХрд╛рдп рдирд╛рд╣реА), рддреЗ рдХрд╕реЗ рдореЛрдЬрд╛рдпрдЪреЗ, рдЖрдгрд┐ рдХреЛрдгрддреЗ automation рдЖрдзреА рдмрдирд╡рд╛рдпрдЪреЗ рд╣реЗ рддреНрдпрд╛рдЪреНрдпрд╛ payback time рд╡рд░реВрди рдХрд╕реЗ рдард░рд╡рд╛рдпрдЪреЗ тАФ рдирд╡реНрдпрд╛ tool рд▓рд╛ рд╕реНрд╡рддрдГрд▓рд╛ рд▓рд╛рдЧрдгрд╛рд▒реНрдпрд╛ upkeep рд╕рдХрдЯ. sre/crew.py рдордзреАрд▓ ToilLedger рдЖрдгрд┐ sre/demo.py рдордзреАрд▓ toil().

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

рджреАрдкрд┐рдХрд╛ рдПрдХ рд╡рд╣реА рдареЗрд╡рддреЗ. ЁЯУТ рдкрдердХ рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдПрдЦрд╛рджреЗ рдХрдВрдЯрд╛рд│рд╡рд╛рдгреЗ рдХрд╛рдо рд╣рд╛рддрд╛рдиреЗ рдХрд░рддреЗ, рддреЗрд╡реНрд╣рд╛ рддреА рддреЗ рд▓рд┐рд╣реВрди рдареЗрд╡рддреЗ: рдХрд╛рдо рдХрд╛рдп рд╣реЛрддреЗ, рдХрд┐рддреА рд╡реЗрд│ рд▓рд╛рдЧрд▓рд╛, рддреЗ рдХрд┐рддреА рд╡реЗрд│рд╛ рд╣реЛрддреЗ.

рдорд╣рд┐рдиреНрдпрд╛рднрд░рд╛рдиреЗ рддреА рдмреЗрд░реАрдЬ рдХрд░рддреЗ. "рдЖрдкрдг рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдВрдЪреА locked accounts рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ 60 рд╡реЗрд│рд╛ reset рдХрд░рддреЛ. рдкреНрд░рддреНрдпреЗрдХреА рджрд╣рд╛ рдорд┐рдирд┐рдЯреЗ. рдореНрд╣рдгрдЬреЗ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ 10 рддрд╛рд╕ тАФ рдХрддрд░рд┐рдирд╛рдЪреНрдпрд╛ рдЖрдард╡рдбреНрдпрд╛рддрд▓рд╛ рдПрдХ рдкреВрд░реНрдг рджрд┐рд╡рд╕, рджрд░ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛."

рдордЧ рддреА рд╡рд┐рдЪрд╛рд░рддреЗ: "рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдВрдиреА рдЖрдкрд▓реЗ account рд╕реНрд╡рддрдГ reset рдХрд░рд╛рд╡реЗ рдЕрд╕реЗ рдмрдЯрдг рдмрдирд╡рд╛рдпрд▓рд╛ рдХрд┐рддреА рд╡реЗрд│ рд▓рд╛рдЧреЗрд▓?" рдХрддрд░рд┐рдирд╛ рдореНрд╣рдгрддреЗ: "16 рддрд╛рд╕." рдореНрд╣рдгрдЬреЗ рджреЛрди рдЖрдард╡рдбреНрдпрд╛рдВрдкреЗрдХреНрд╖рд╛ рдХрдореА рдХрд╛рд│рд╛рдд рдмрдЯрдгрд╛рдиреЗ рдЖрдкрд▓реА рдХрд┐рдВрдордд рд╡рд╕реВрд▓ рдХреЗрд▓реА. рддреНрдпрд╛рдирдВрддрд░ рддреЛ рдореЛрдХрд│рд╛ рд╡реЗрд│ рдЖрд╣реЗ.

рдкрдг рдХрд╛рд╣реА рдХрд╛рдореЗ рдХреНрд╡рдЪрд┐рддрдЪ рдпреЗрддрд╛рдд. Certificate рдЪреЗ рдиреВрддрдиреАрдХрд░рдг рд╡рд░реНрд╖рд╛рддреВрди рдЪрд╛рд░ рд╡реЗрд│рд╛ рд╣реЛрддреЗ. рддреНрдпрд╛рд╕рд╛рдареА tool рдмрдирд╡рд╛рдпрд▓рд╛ 12 рддрд╛рд╕ рд▓рд╛рдЧрддрд╛рдд рдЖрдгрд┐ рддреЗ рдЬрд╡рд│рдкрд╛рд╕ рдХрд╛рд╣реАрдЪ рд╡рд╛рдЪрд╡рдд рдирд╛рд╣реА. рд╡рд╣реАрдиреБрд╕рд╛рд░ рддреЗ рдлрд╛рдпрджреНрдпрд╛рдЪреЗ рдирд╛рд╣реА. (рддрд░реАрд╣реА рджреАрдкрд┐рдХрд╛ рддреЗ рдХрд░рддреЗ тАФ рдХрд╛рд░рдг рддреЗ рдПрдХрджрд╛ рдЬрд░реА рд╡рд┐рд╕рд░рд▓реЗ, рддрд░ рд╕рдВрдкреВрд░реНрдг рдЗрдорд╛рд░рдд рдЕрдВрдзрд╛рд░рд╛рдд рдЬрд╛рддреЗ. рд╡рд╣реА рд╣реАрдЪ рдПрдХрдореЗрд╡ рдореЛрдЬрдгреНрдпрд╛рдЪреА рдЧреЛрд╖реНрдЯ рдирд╛рд╣реА.)

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

flowchart LR
    ledger["ЁЯУТ toil ledger<br/>24.2 h/week"] --> rank["ЁЯУК rank by payback<br/>build ├╖ (saved тИТ upkeep)"]
    rank --> a["backups 1.2 wk"]
    rank --> b["account resets 1.6 wk"]
    rank --> c["disk resize 2.8 wk"]
    rank --> d["print queue 3.5 wk<br/>(next quarter)"]
    rank --> e["TLS cert 137 wk<br/>(use ACME anyway)"]
    a & b & c --> done["ЁЯФз 40 h budget spent: 30 h<br/>toil 24.2 тЖТ 6.2 h/week"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг toil рдореНрд╣рдгрдЬреЗрдЪ рдмрд╛рджрд▓реНрдпрд╛. рддреЗ рд╢рд╛рд│реЗрд╕реЛрдмрдд рд╡рд╛рдврддреЗ, рдЖрдгрд┐ рдЬреНрдпрд╛ engineering рдиреЗ рддреЗ рдХрдореА рдЭрд╛рд▓реЗ рдЕрд╕рддреЗ рддрд┐рд▓рд╛ рдмрд╛рдЬреВрд▓рд╛ рд╕рд╛рд░рддреЗ. рддреЗ рд▓реЛрдХрд╛рдВрд╕рд╛рдареАрд╣реА рд╡рд╛рдИрдЯ рдЖрд╣реЗ: рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛ рд╣реЛрдгрд╛рд░реЗ, рд╡реНрдпрддреНрдпрдпрд╛рдВрд╡рд░ рдЪрд╛рд▓рдгрд╛рд░реЗ рдХрд╛рдо рдердХрд╡рдгрд╛рд░реЗ рдЖрдгрд┐ рдЪреБрдХрд╛рдВрдирд╛ рдЖрдордВрддреНрд░рдг рджреЗрдгрд╛рд░реЗ рдЕрд╕рддреЗ тАФ 60 рдкреИрдХреА 58 рд╡реЗ account reset рдХрд░рдгрд╛рд░реА рд╡реНрдпрдХреНрддреА рдЪреБрдХреАрдЪреЗ account reset рдХрд░рдгреНрдпрд╛рдЪреА рд╢рдХреНрдпрддрд╛ рдЬрд╛рд╕реНрдд рдЕрд╕рддреЗ. рдЖрдгрд┐ рдХреЛрдгреА рдореЛрдЬрд▓реНрдпрд╛рд╢рд┐рд╡рд╛рдп рддреЗ рджрд┐рд╕рдд рдирд╛рд╣реА. рдиреЛрдВрджрд╡рд╣реА "рдЖрдореНрд╣реА рдиреЗрд╣рдореА рд╡реНрдпрд╕реНрдд рдЕрд╕рддреЛ" рдпрд╛рд▓рд╛ рдЖрдзреА рдХрд╛рдп рджреБрд░реБрд╕реНрдд рдХрд░рд╛рдпрдЪреЗ рдпрд╛рдЪреНрдпрд╛ рдХреНрд░рдорд╡рд╛рд░ рдпрд╛рджреАрдд рдмрджрд▓рддреЗ.

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

sre/crew.py рдордзреАрд▓ ToilLedger(tasks) (name, minutes each, times per week, kind, hours to automate, upkeep hours per week) рдЪреНрдпрд╛ rows рдШреЗрддреЗ. hours(kind) рдЖрдард╡рдбреНрдпрд╛рдЪреНрдпрд╛ рддрд╛рд╕рд╛рдВрдЪреА рдмреЗрд░реАрдЬ рдХрд░рддреЗ; roi() toil rows рдирд╛ payback рдиреБрд╕рд╛рд░ рдХреНрд░рдо рд▓рд╛рд╡рддреЗ; plan(budget_hours, max_payback_weeks=26) build budget рд╕рдВрдкреЗрдкрд░реНрдпрдВрдд payback рдХреНрд░рдорд╛рдиреЗ automations рдирд┐рд╡рдбрддреЗ, рдЖрдгрд┐ рдЬреНрдпрд╛рдЪрд╛ payback 26 рдЖрдард╡рдбреНрдпрд╛рдВрдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдЕрд╕реЗрд▓ рддреЗ рд╡рдЧрд│рддреЗ.

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

python3 sre/demo.py toil
python3 - <<'EOF'
import sys; sys.path.insert(0, "sre"); from crew import ToilLedger
L = ToilLedger([("reset locked pupil accounts", 10, 60, "toil", 16, 0.25),
                ("restart the stuck print queue", 15, 24, "toil", 20, 0.25),
                ("answer 'is it down?' chat questions", 5, 40, "toil", 10, 0.5),
                ("weekly crew meeting", 60, 3, "overhead", 0, 0)])
print(f"toil {L.hours('toil'):.2f} h/week ┬╖ overhead {L.hours('overhead'):.1f} h/week")
for name, saved, build, upkeep, pay in L.roi():
    print(f"{name:<36} saves {saved:5.2f} h/week ┬╖ pays back in {pay:4.1f} weeks")
for budget in (16, 30, 50):
    print(f"budget {budget:>2} h тЖТ", L.plan(budget))
EOF

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

toil рд╣реЗ print рдХрд░рддреЗ:

тФАтФА the toil ledger: toil 24.2 h/week ┬╖ overhead 3.0 h ┬╖ engineering 8.0 h
   copy grades to the backup drive   5.00 h/week ┬╖ build  6 h ┬╖ upkeep 0.10 h/week тЖТ pays back in    1.2 weeks
   reset locked pupil accounts      10.00 h/week ┬╖ build 16 h ┬╖ upkeep 0.25 h/week тЖТ pays back in    1.6 weeks
   grow a full disk by hand          3.00 h/week ┬╖ build  8 h ┬╖ upkeep 0.10 h/week тЖТ pays back in    2.8 weeks
   restart the stuck print queue     6.00 h/week ┬╖ build 20 h ┬╖ upkeep 0.25 h/week тЖТ pays back in    3.5 weeks
   renew a TLS certificate by hand   0.19 h/week ┬╖ build 12 h ┬╖ upkeep 0.10 h/week тЖТ pays back in  137.1 weeks
   toil falls from 24.2 to 6.2 h/week, plus 0.45 h/week of upkeep for the new tools

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

toil 19.33 h/week ┬╖ overhead 3.0 h/week
reset locked pupil accounts          saves 10.00 h/week ┬╖ pays back in  1.6 weeks
restart the stuck print queue        saves  6.00 h/week ┬╖ pays back in  3.5 weeks
answer 'is it down?' chat questions  saves  3.33 h/week ┬╖ pays back in  3.5 weeks
budget 16 h тЖТ (['reset locked pupil accounts'], 16)
budget 30 h тЖТ (['reset locked pupil accounts', "answer 'is it down?' chat questions"], 26)
budget 50 h тЖТ (['reset locked pupil accounts', 'restart the stuck print queue', "answer 'is it down?' chat questions"], 46)

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

рддреАрд╕ рддрд╛рд╕рд╛рдВрдЪреНрдпрд╛ рдмрд╛рдВрдзрдгреАрдиреЗ рджрд░ рдЖрдард╡рдбреНрдпрд╛рдЪреЗ 18 рддрд╛рд╕ toil рдХрд╛рдвреВрди рдЯрд╛рдХрд▓реЗ (24.2 тЖТ 6.2), рддреНрдпрд╛рдмрджрд▓реНрдпрд╛рдд рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ 0.45 рддрд╛рд╕ upkeep. рдХреНрд░рдо "рд╕рд░реНрд╡рд╛рдд рдореЛрдареЗ рдХрд╛рдо рдЖрдзреА" рдЕрд╕рд╛ рдирд╛рд╣реА тАФ account resets рд╕рд░реНрд╡рд╛рдд рдЬрд╛рд╕реНрдд рд╡рд╛рдЪрд╡рддрд╛рдд, рдкрдг backup copy рд╕рд░реНрд╡рд╛рдд рд▓рд╡рдХрд░ рд╡рд╕реВрд▓ рд╣реЛрддреЗ. 30 рддрд╛рд╕рд╛рдВрдд planner рдиреЗ print queue (20 h) рд╡рдЧрд│рд▓реА рдЖрдгрд┐ рддреНрдпрд╛рдРрд╡рдЬреА рд╕реНрд╡рд╕реНрдд "is it down?" item рдШреЗрддрд▓рд╛; 50 рддрд╛рд╕рд╛рдВрдд рддреНрдпрд╛рдиреЗ рддрд┐рдиреНрд╣реА рдШреЗрддрд▓реЗ. рдЖрдгрд┐ certificate рдиреЛрдВрджрд╡рд╣реАрдЪреА рдорд░реНрдпрд╛рджрд╛ рджрд╛рдЦрд╡рддреЗ: рддрд╛рд╕рд╛рдВрдЪреНрдпрд╛ рд╣рд┐рд╢реЛрдмрд╛рдиреЗ рд╡рд╕реБрд▓реАрд▓рд╛ 137 рдЖрдард╡рдбреЗ, рддрд░реАрд╣реА рдПрдХрджрд╛ рд╡рд┐рд╕рд░рд▓реЗрд▓реЗ рдиреВрддрдиреАрдХрд░рдг рдореНрд╣рдгрдЬреЗ outage. рдзреЛрдХрд╛рд╣реА рдореЛрдЬрд▓рд╛ рдЬрд╛рддреЛ.

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

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

рдХрд╛рдо рдЬрд┐рдереЗ рдЖрдзреАрдЪ рд╣реЛрддреЗ рддрд┐рдереЗрдЪ toil рдиреЛрдВрджрд╡рд╛. Tickets рдирд╛ рд▓рд╛рдЧрд▓реЗрд▓реНрдпрд╛ рдорд┐рдирд┐рдЯрд╛рдВрд╕рд╣ toil tag рд▓рд╛рд╡рд╛; рдорд╣рд┐рдиреНрдпрд╛рддреВрди рдПрдХрджрд╛ рдХрд╛рдорд╛рдиреБрд╕рд╛рд░ рдмреЗрд░реАрдЬ рдХрд░рд╛. рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдЙрджрд╛рд╣рд░рдгрд╛рд░реНрде, рддреБрдордЪреНрдпрд╛ ticket system рдЪреНрдпрд╛ CSV export рдордзреВрди:

# columns: task,minutes
awk -F, 'NR > 1 { m[$1] += $2 } END { for (t in m) printf "%6.1f h  %s\n", m[t] / 60, t }' toil-2026-09.csv | sort -rn

Kubernetes рд╡рд░ cert-manager (ACME, Let's Encrypt) рд╡рд╛рдкрд░реВрди certificate рдЪреЗ рдиреВрддрдиреАрдХрд░рдг рдХрд╛рдпрдордЪреЗ рдХрд╛рдвреВрди рдЯрд╛рдХрд╛; рддреЗ expiry рдЪреНрдпрд╛ рдЖрдзреАрдЪ рдиреВрддрдиреАрдХрд░рдг рдХрд░рддреЗ:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata: {name: timetable-tls, namespace: timetable}
spec:
  secretName: timetable-tls
  dnsNames: [timetable.school.example]
  issuerRef: {name: letsencrypt-prod, kind: ClusterIssuer}
  renewBefore: 720h            # renew 30 days before the certificate expires

рд╕рдВрд░рдХреНрд╖рдХ рдХрдардбреНрдпрд╛рдВрд╕рд╣ self-service рдореНрд╣рдгрдЬреЗ рдмрд╣реБрддреЗрдХ рд╡реЗрд│рд╛ рдПрдХ рдЫреЛрдЯреЗ runbook-automation job (Rundeck, рдПрдЦрд╛рджреЗ рдЕрдВрддрд░реНрдЧрдд portal, рдХрд┐рдВрд╡рд╛ chat command) рдЬреЗ рдХреЛрдг рдорд╛рдЧрдд рдЖрд╣реЗ рддреЗ рддрдкрд╛рд╕рддреЗ, рдкреНрд░рддреНрдпреЗрдХ run log рдХрд░рддреЗ рдЖрдгрд┐ рдлрдХреНрдд рдПрдХрдЪ рдХрд╛рдо рдХрд░рддреЗ.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдпрд╛ рдЖрдард╡рдбреНрдпрд╛рдд toil рдЪреА рдиреЛрдВрджрд╡рд╣реА рд╕реБрд░реВ рдХрд░рд╛. рдорд╣рд┐рдиреНрдпрд╛рднрд░рд╛рдиреЗ рд╡рд░рдЪреНрдпрд╛ рддреАрди rows рд╣рд╛ рддреБрдордЪрд╛ automation рдЪрд╛ рдЖрд░рд╛рдЦрдбрд╛ рдЕрд╕реЗрд▓, рдЖрдзреАрдЪ рдХреНрд░рдо рд▓рд╛рд╡рд▓реЗрд▓рд╛, рдЖрдгрд┐ рддреЗ рдЖрдХрдбреЗ рддреБрдордЪреНрдпрд╛ manager рд╕рдореЛрд░ рддреНрдпрд╛ рд╡реЗрд│реЗрдЪреЗ рд╕рдорд░реНрдерди рдХрд░рддрд╛рдд.

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

рдХрд╛рд╣реА рдмрд╛рджрд▓реНрдпрд╛ рдкрд╣рд╛рдЯреЗ 3 рд╡рд╛рдЬрддрд╛ рдпреЗрддрд╛рдд. рдкреБрдврдЪрд╛ рдзрдбрд╛ рдбреНрдпреБрдЯреАрдЪреНрдпрд╛ рдлреЛрдирдмрджреНрджрд▓ рдЖрд╣реЗ: рддреЛ рдХреЛрдг рд╕рд╛рдВрднрд╛рд│рддреЗ, рддреЛ рдХрд┐рддреА рд╡реЗрд│рд╛ рд╡рд╛рдЬрддреЛ, рдЖрдгрд┐ рддреЗ рдЯрд┐рдХрд╛рдК рдХрд╕реЗ рдареЗрд╡рд╛рдпрдЪреЗ.

git checkout lesson-04-on-call

ЁЯкг Lesson 03 тАФ Toil: measure the buckets, then automate them

ЁЯУН You are here: Lesson 03 of 12 ┬╖ Previous: lesson-02-error-budget-policy ┬╖ Next: lesson-04-on-call


ЁЯУж What's in this branch

Lesson 02, plus the crew's toil ledger. What counts as toil (and what does not), how to measure it, and how to decide which automation to build first by its payback time тАФ including the upkeep the new tool itself will need. ToilLedger in sre/crew.py and toil() in sre/demo.py.

ЁЯзТ Explain like I'm 5

Dipika keeps a notebook. ЁЯУТ Every time the crew does a boring job by hand, she writes it down: what it was, how long it took, how often it happens.

After a month she adds it up. "We reset locked pupil accounts 60 times a week. Ten minutes each. That is 10 hours a week тАФ a whole day of Katrina's week, every week."

Then she asks: "How long to build a button so pupils reset their own account?" Katrina says: "16 hours." So in less than two weeks the button has paid for itself. After that, it is free time.

But some jobs are rare. Renewing the certificate happens four times a year. Building a tool for it takes 12 hours and saves almost nothing. By the notebook, it is not worth it. (Dipika does it anyway тАФ because if they forget it once, the whole building goes dark. The notebook is not the only thing that counts.)

ЁЯЧ║я╕П Diagram

flowchart LR
    ledger["ЁЯУТ toil ledger<br/>24.2 h/week"] --> rank["ЁЯУК rank by payback<br/>build ├╖ (saved тИТ upkeep)"]
    rank --> a["backups 1.2 wk"]
    rank --> b["account resets 1.6 wk"]
    rank --> c["disk resize 2.8 wk"]
    rank --> d["print queue 3.5 wk<br/>(next quarter)"]
    rank --> e["TLS cert 137 wk<br/>(use ACME anyway)"]
    a & b & c --> done["ЁЯФз 40 h budget spent: 30 h<br/>toil 24.2 тЖТ 6.2 h/week"]

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

тЭУ What

ЁЯдФ Why

Because toil is the buckets. It grows with the school, and it crowds out the engineering that would shrink it. It is also bad for people: repetitive, interrupt-driven work is tiring and error-prone тАФ a person resetting account 58 of 60 is more likely to reset the wrong one. And it is invisible unless someone measures it. The ledger turns "we are always busy" into a ranked list of what to fix first.

ЁЯФз How (in this repo)

ToilLedger(tasks) in sre/crew.py takes rows of (name, minutes each, times per week, kind, hours to automate, upkeep hours per week). hours(kind) sums the weekly hours; roi() ranks the toil rows by payback; plan(budget_hours, max_payback_weeks=26) picks automations in payback order until the build budget runs out, and skips anything that would take longer than 26 weeks to pay back.

ЁЯзк Try it

python3 sre/demo.py toil
python3 - <<'EOF'
import sys; sys.path.insert(0, "sre"); from crew import ToilLedger
L = ToilLedger([("reset locked pupil accounts", 10, 60, "toil", 16, 0.25),
                ("restart the stuck print queue", 15, 24, "toil", 20, 0.25),
                ("answer 'is it down?' chat questions", 5, 40, "toil", 10, 0.5),
                ("weekly crew meeting", 60, 3, "overhead", 0, 0)])
print(f"toil {L.hours('toil'):.2f} h/week ┬╖ overhead {L.hours('overhead'):.1f} h/week")
for name, saved, build, upkeep, pay in L.roi():
    print(f"{name:<36} saves {saved:5.2f} h/week ┬╖ pays back in {pay:4.1f} weeks")
for budget in (16, 30, 50):
    print(f"budget {budget:>2} h тЖТ", L.plan(budget))
EOF

тЬЕ Verify тАФ what you should see

toil prints:

тФАтФА the toil ledger: toil 24.2 h/week ┬╖ overhead 3.0 h ┬╖ engineering 8.0 h
   copy grades to the backup drive   5.00 h/week ┬╖ build  6 h ┬╖ upkeep 0.10 h/week тЖТ pays back in    1.2 weeks
   reset locked pupil accounts      10.00 h/week ┬╖ build 16 h ┬╖ upkeep 0.25 h/week тЖТ pays back in    1.6 weeks
   grow a full disk by hand          3.00 h/week ┬╖ build  8 h ┬╖ upkeep 0.10 h/week тЖТ pays back in    2.8 weeks
   restart the stuck print queue     6.00 h/week ┬╖ build 20 h ┬╖ upkeep 0.25 h/week тЖТ pays back in    3.5 weeks
   renew a TLS certificate by hand   0.19 h/week ┬╖ build 12 h ┬╖ upkeep 0.10 h/week тЖТ pays back in  137.1 weeks
   toil falls from 24.2 to 6.2 h/week, plus 0.45 h/week of upkeep for the new tools

Your snippet prints:

toil 19.33 h/week ┬╖ overhead 3.0 h/week
reset locked pupil accounts          saves 10.00 h/week ┬╖ pays back in  1.6 weeks
restart the stuck print queue        saves  6.00 h/week ┬╖ pays back in  3.5 weeks
answer 'is it down?' chat questions  saves  3.33 h/week ┬╖ pays back in  3.5 weeks
budget 16 h тЖТ (['reset locked pupil accounts'], 16)
budget 30 h тЖТ (['reset locked pupil accounts', "answer 'is it down?' chat questions"], 26)
budget 50 h тЖТ (['reset locked pupil accounts', 'restart the stuck print queue', "answer 'is it down?' chat questions"], 46)

ЁЯПБ What you just proved

Thirty hours of building removed 18 hours of toil every week (24.2 тЖТ 6.2), for 0.45 hours a week of upkeep. The ranking is not "the biggest task first" тАФ the account resets save the most, but the backup copy pays back fastest. With 30 hours the planner skipped the print queue (20 h) and took the cheaper "is it down?" item instead; with 50 it took all three. And the certificate shows the ledger's limit: 137 weeks to pay back by hours, yet one forgotten renewal is an outage. Risk counts too.

тЪая╕П Common mistakes

ЁЯПн In production

Log toil where the work already happens. Tag tickets toil with minutes spent; once a month, sum by task. On a real account тАФ for example, from a CSV export of your ticket system:

# columns: task,minutes
awk -F, 'NR > 1 { m[$1] += $2 } END { for (t in m) printf "%6.1f h  %s\n", m[t] / 60, t }' toil-2026-09.csv | sort -rn

Remove a certificate renewal for good with cert-manager on Kubernetes (ACME, Let's Encrypt), which renews before expiry:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata: {name: timetable-tls, namespace: timetable}
spec:
  secretName: timetable-tls
  dnsNames: [timetable.school.example]
  issuerRef: {name: letsencrypt-prod, kind: ClusterIssuer}
  renewBefore: 720h            # renew 30 days before the certificate expires

Self-service with guard rails is often a small runbook-automation job (Rundeck, an internal portal, or a chat command) that checks who asks, logs every run and does one thing only.

ЁЯПн Why this matters in production: start a toil ledger this week. After a month, the top three rows are your automation roadmap, already ranked, and the numbers justify the time to your manager.

тПня╕П Next

Some of the buckets arrive at 3 a.m. The next lesson is about the duty phone: who holds it, how often it rings, and how to keep that sustainable.

git checkout lesson-04-on-call
тЖР Previouserror budget policyNext тЖТon call

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