feat(curriculum): Phase 6C module upgrades, security assessment bank, ROOT 11 orientation

Upgrades six PARTIAL modules to the gold-module standard (lesson/demonstration/
knowledge-check/guided-exercise/lab(s)/reflection), preserving every existing lab
activity_id and scenario_id: root-06-m03 (jobs/cronjobs/daemonsets), root-07-m04
(ingress/networkpolicy), root-07-m02 (configuration/secrets, converted in place
from a bare 5-lab bank), root-09-m02 (serviceaccounts/rbac), root-09-m03
(network/supply-chain security), root-10-m03 (troubleshooting framework).

root-05-m01 (20-task kubectl-fluency bank) gets a minimal bookending lesson +
reflection rather than a full redesign, since each task already carries its own
explanation. root-12-m01, root-capstone-m01, and root-09-m04 are promoted to
READY via documented exceptions (no new activities) where adding one would
misrepresent their intentional design.

Wires a new rootnode-security-assessment (exam, 60min, hint_policy disabled)
using the existing Phase 3B assessment engine: 3 required hands-on CORE
scenarios + 4 random-draw pools covering SecurityContext, RBAC, network
policy, and the 7 previously-unwired CKS MCQs. Wires the previously-unwired
readiness-probe-task scenario as an OPTIONAL practice lab in root-10-m01,
using the existing is_required model.

Adds root-11-m01-devops-platform-thinking — an honest, lesson-only ROOT 11
orientation module naming DevOps/platform engineering practices already
exercised in ROOT 05-10/12, explicit that GitOps/CI-CD/IaC/observability need
lab infrastructure this platform doesn't yet provide. No fake labs.

Adds infra/scripts/validate-course-content.js: cross-checks the DB against
scenario/content files for duplicate ids, orphans, invalid references, cycles,
sequence collisions, instructor-notes leakage, and (new) self-paced content
deferring explanation to a live instructor. 0 errors against the resulting
22 courses/modules, 163 activities, 98 scenario files.

No application code changed — every gap was satisfiable with existing tables
(course_modules, module_activities, assessment_definitions) and the existing
content-delivery pipeline.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-26 11:20:17 -05:00
co-authored by Claude Sonnet 5
parent 2db136875f
commit 7f6e1857c0
26 changed files with 1716 additions and 0 deletions
@@ -0,0 +1,72 @@
# Demonstration: Watching a Job Actually Complete
## Creating the Job
```
$ kubectl apply -f pi-job.yaml
job.batch/pi-job created
$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
pi-job 0/1 3s 3s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-job-8x2kq 1/1 Running 0 3s
```
The Job's own status (`0/1` completions) is tracked separately from its Pod's status — this
matters because a Job can retry a FAILED Pod attempt without that looking like the healthy
restart behavior you'd see on a Deployment.
## Once the Pod's process actually exits successfully
```
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-job-8x2kq 0/1 Completed 0 8s
$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
pi-job 1/1 7s 8s
```
`STATUS: Completed` — not `Running`, not `CrashLoopBackOff`. This is the ONE status a
long-running Deployment's Pod is never supposed to reach on its own; for a Job, it's success.
The Job object itself now reports `1/1` and stops — nothing recreates this Pod, because
"finished successfully" is the desired end state here, unlike a Deployment where exiting is
always treated as something to fix.
## What a FAILING Job attempt looks like, briefly
```
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
bad-job-abc12 0/1 Error 0 5s
bad-job-def34 0/1 Error 0 2s
$ kubectl describe job bad-job
...
Events:
Warning BackoffLimitExceeded Job has reached the specified backoff limit
```
Two DIFFERENT Pods (`abc12`, `def34`), not one Pod restarting twice — each failed attempt gets
its own new Pod, up to `backoffLimit`. Once that limit is hit, the Job stops retrying and
reports the failure via Events rather than trying forever.
## Instructor Notes
**Demo script:** Run the successful case live; showing the FAILING case is optional if time is
short, but the `Completed` vs `Running`/`Error` distinction is worth pausing on regardless.
**Approximate timing:** 8 minutes live; 5 minutes self-paced.
**Common misconception to preempt:** students who've only seen Deployments sometimes expect
`kubectl get pods` to show the Job's Pod disappear once it's done. It doesn't, by default — it
stays as `Completed` until manually cleaned up or a TTL is configured, which is worth mentioning
briefly (not a hands-on topic here).
**Optional extension:** ask what `kubectl get pods` would show for a CronJob's Pods across
several scheduled runs (answer: one Completed Pod per past run, accumulating — the same
"nothing auto-cleans this up" behavior, multiplied by the schedule).
@@ -0,0 +1,50 @@
# Guided Exercise: Choosing the Right Workload Primitive
Paper-and-thinking exercise before the Labs.
## The scenario
Your team needs four things running in the cluster:
1. A nightly database backup that must run at 2am and finish before morning.
2. A log-shipping agent that must run on every node, including nodes added later.
3. A one-time script to backfill a new database column, run once during this deploy.
4. The main web application, always up, serving live traffic.
## Your task
For each of the four, name the correct primitive (Deployment, Job, CronJob, or DaemonSet) and
answer: what would go wrong if you used a Deployment for it instead?
## A worked answer to check yourself against
1. **CronJob.** A Deployment has no concept of "run on a schedule" — you'd need external
tooling to trigger it repeatedly, reinventing what CronJob already does. Using a bare Job
would run it once; CronJob is what recreates that Job on schedule.
2. **DaemonSet.** A Deployment's replica count is a number, not "one per node" — nothing
guarantees even coverage, and a newly added node wouldn't automatically get a copy the way a
DaemonSet guarantees.
3. **Job.** A Deployment expects its Pod to run forever; a script that's SUPPOSED to exit when
done would be treated as a crash and restarted forever, which is wrong for a one-time task.
4. **Deployment.** This is the actual always-on, N-replicas case the other three are explicitly
NOT designed for.
## Self-check
If you answered "Job" for #4 or "Deployment" for #1-3, revisit the lesson's decision table
before the Labs — getting this choice right the first time avoids building something that
technically runs but behaves wrong in a way that's easy to miss until it causes a real problem
(a backup CronJob accidentally deployed as a Job that only ever runs once, for example).
## Instructor Notes
**Facilitation:** Good as a quick individual exercise before revealing the answer — the "what
would go wrong" framing forces reasoning about the CONSEQUENCE of a wrong choice, not just
recall of a definition.
**Approximate timing:** 8-10 minutes live with discussion; 8 minutes self-paced.
**Optional extension:** ask what would need to be true for #1 (the nightly backup) to actually
be SAFE to run as a plain Job triggered manually once, instead of a CronJob — a good check that
students understand CronJob's value is specifically the SCHEDULING, not something inherent to
backups.
@@ -0,0 +1,40 @@
# Reflection: The Right Primitive Is a Correctness Decision, Not a Style Choice
You just built a one-shot Job and a scheduled CronJob for real. Take a few minutes to connect
this to production thinking.
## Reflection questions
1. If your team accidentally deployed the database backup script as a Deployment instead of a
CronJob, what's the FIRST symptom you'd likely notice — and would it look like a bug in the
backup script itself, or something else?
2. A CronJob that fails silently every night for a week is a real, common incident pattern. What
would you want monitored to catch this quickly?
3. Why does "it ran once and worked" not prove a CronJob is configured correctly?
## Real Ops takeaway
**Workload-primitive mistakes often look like application bugs at first.** A backup script
"crashing" every few minutes on a Deployment isn't a bug in the script — the script did exactly
what it was told to do (exit when finished) and the Deployment is "correctly" restarting it
according to its own contract. Recognizing "wrong primitive," not "buggy code," as the root
cause saves real debugging time.
**A schedule you never verify is a schedule you can't trust.** A CronJob failing every single
run since deployment is easy to miss if nobody checks — unlike a Deployment's crash-looping
Pods, which are visible in `kubectl get pods` by default, a CronJob's failed history requires
someone to actually look at `kubectl get jobs` or the CronJob's own status. Real teams monitor
CronJob success/failure explicitly rather than assuming silence means success.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 is worth pushing on — many students'
first instinct is to debug the "crashing" script rather than question the workload type it's
running under.
**Approximate timing:** 8 minutes live discussion; 5 minutes self-paced writing.
**Optional extension:** ask students to name one alerting rule they'd add for a critical nightly
CronJob (e.g. "alert if no successful completion recorded in the last 25 hours").