Part VI — Extension and synthesis
Extending Kubernetes, and the Whole Machine (Capstone)
Lesson 12 locked the cluster down: authentication, RBAC, admission, namespaces, quotas, and NetworkPolicy turned a wide-open multi-tenant box into one where every API call and every Pod-to-Pod connection is constrained. But all of that machinery still only governs the built-in nouns — Deployment, Service, ConfigMap, StatefulSet. Those nouns model a generic app. They have nothing to say about your domain: a database that needs ordered failover and nightly backups, a model rollout that must canary then promote. So today, to manage those, you are back to a human running runbooks by hand — which is precisely the manual ops we abolished in lesson 00. This final lesson closes that gap by teaching Kubernetes a new noun (Part A), and then steps back to trace the whole machine end to end and show that the thirteen primitives before it form a single derived chain (Part B).
New capability: a Custom Resource Definition (CRD) teaches the API server a new noun; an operator is a custom controller that watches that noun and reconciles it with the exact loop from lessons 00 and 04 (observe → diff → act). Kubernetes becomes a platform for building platforms — user-extensible all the way down.
Forces next: nothing. This is the last lesson. The chain closes here, and the remaining work is to see the whole machine at once and walk back to lesson 00 with new eyes.
kubectl apply end to end through every component the track built. (5) Synthesis II: trace one external request, draw the full architecture map, and put the entire linear chain in one recap table — then bridge to the GenAI sibling track.1 · The forced fix: teach the API server a new noun
Return to the engine. Kubernetes works because intent is stored as a declarative object and a controller drives reality toward it. The whole reason a Deployment self-heals is that "I want 5 replicas of v2" is a first-class, queryable, watchable object the API server persists. Your domain intent — "I want a 3-node database, primary elected, replicas streaming" — has no such object. It cannot be stored, so it cannot be reconciled, so a human is the controller. To fix this the way Kubernetes fixes everything, you do not write a one-off script: you make your intent a declarative object the API server understands.
That is exactly what a Custom Resource Definition (CRD) is. A CRD is itself an object you kubectl apply; submitting it teaches the API server a brand-new kind — say Database — with a schema you define (an OpenAPI schema validating the fields). From that moment the API server treats your noun like any native one: kubectl get databases works, RBAC rules can grant verbs on it, admission webhooks can guard it, it gets stored in etcd, and clients can watch it. A single instance of the new kind is a custom resource (CR):
apiVersion: acme.io/v1 # your API group + version
kind: Database # the new noun the CRD registered
metadata: { name: orders-db }
spec: # desired state — what YOU want
size: 3 # number of database members
version: "15" # engine version
status: # actual state — what the operator OBSERVES
readyReplicas: 3
primary: orders-db-0
Notice the shape is the universal Kubernetes shape from lesson 03: a spec (desired, written by the user) and a status (actual, written by the controller). The CRD gives you the noun and a place to store both halves of the loop. But a stored noun is inert — exactly like a declaration with no loop behind it (lesson 00, §3). The API server will happily persist a Database{size: 3} and do absolutely nothing about it. Something must watch this noun and close the gap. That something is the operator.
2 · The operator is just a controller over your noun
An operator is not a new mechanism. It is the controller from lessons 00 and 04 — the same observe → diff → act loop — pointed at a custom resource instead of a built-in one. It encodes the operational knowledge a human expert would otherwise apply by hand: how to bring a database up in order, elect a primary, fail over, take a backup. The pattern is so regular that frameworks (controller-runtime, scaffolded by kubebuilder or the Operator SDK) reduce writing one to filling in a single function. Its signature is the loop made literal:
// controller-runtime: called every time the watched object changes,
// AND periodically (re-sync) — because it is level-triggered (§00).
func (r *DatabaseReconciler) Reconcile(ctx, req) (Result, error) {
var db Database
r.Get(ctx, req.NamespacedName, &db) // OBSERVE: read desired (spec)
pods := r.listChildPods(db) // OBSERVE: read actual (the world)
diff := db.Spec.Size - len(pods) // DIFF: desired vs actual
if diff > 0 { r.Create(... diff new Pods) // ACT: create the shortfall
} else if diff < 0 { r.Delete(... -diff) // ACT: delete the surplus
}
return Result{RequeueAfter: 30 * time.Second}, nil // REPEAT: forever
}
That is the entire idea. The reconciler returns, and controller-runtime calls it again on the next event or on the requeue timer. It is level-triggered (it re-reads desired and actual every call, so it never relies on catching a "Pod died" event) and idempotent (a call where diff == 0 does nothing) — the two properties from lesson 00 that make the loop self-heal, now yours to reuse. A real Database operator does more than count Pods, but every action is still a diff-and-act pass:
spec.size vs the live members. size: 3 → 5 creates db-3, db-4 (each its own StatefulSet Pod with its own PVC, lesson 09); 5 → 2 removes the highest ordinals, newest first.status.primary is unreachable; act by promoting a healthy replica, updating status.primary, and re-pointing the headless Service's endpoints (lesson 06) at the new leader.spec; act by creating a Job (lesson 09) that snapshots a replica's PVC to object storage, then recording the time in status.spec.version differs from running members; act by replacing them one ordinal at a time, replica-first, primary-last — the domain-specific safe order a human used to run from a runbook.This is why the phrase "Kubernetes is a platform for building platforms" is precise, not marketing. The control-plane pattern — declarative noun in etcd, a watching loop that reconciles it — is generic and user-extensible. The built-in controllers (Deployment, ReplicaSet, StatefulSet, Job) are not privileged; they are simply the operators the Kubernetes authors shipped. You write your own with the same API, the same loop, the same guarantees. The manual runbook from the source-note becomes a Reconcile function, and the 3am page becomes a self-healing object.
3 · Drive the operator loop yourself
The widget below is the lesson-00 reconciliation loop, grown up. There it counted abstract Pods; here it is a real operator watching a custom Database resource. You edit one field — spec.size — and the operator reconciles by creating or deleting child StatefulSet Pods (db-0, db-1, …) to match. Watch that it is the same observe → diff → act pass: each tick it reads spec.size (desired) and counts live members (actual), computes the diff, and creates or deletes exactly enough to close it — newest ordinals removed first, just like a StatefulSet (lesson 09). Kill a member to simulate a node death and watch the operator recreate it on the next tick, exactly as the ReplicaSet did in lesson 04. The point of the widget is recognition: you have seen this loop before, in the very first lesson. Custom nouns did not add a new engine; they pointed the old engine at a new object.
That recognition is the hinge of the whole track. There was never a pile of unrelated features — there was one loop, applied to nouns the loop turned out to need. The next two sections make that claim literal by tracing the entire machine.
4 · Synthesis I — one kubectl apply, end to end
You type kubectl apply -f deployment.yaml. Follow the request through every component the track built, in order. Each hop is a lesson.
create deployments in this namespace? A Role/RoleBinding must grant it, or the request is 403 (lesson 12).resourceVersion). This write is the only thing that touches etcd (lesson 03).Twelve hops, and every one is a watch-driven, level-triggered reconcile against state in etcd. No component called another directly; each only watched the API server and acted on what it saw. That is the hub-and-spoke design from lesson 03 doing its job: the API server is the single source of truth, and a dozen independent loops converge the cluster on it. You declared a result; the machine computed the steps.
5 · Synthesis II — one external request, the map, and the chain
Now the other direction. An external user opens your app in a browser. Follow the HTTP request inward:
app.example.com to the public IP of the cluster's load balancer / Ingress (lesson 07).Put both directions together and the entire cluster is one picture — a control plane of stored truth plus watching loops, and worker nodes that run the actual Pods:
CONTROL PLANE (the brain)
+-------------------------------------------------------------------+
| API SERVER (only writer of etcd; authn -> authz -> admission) |
| ^ ^ ^ ^ ^ |
| | | | | | +------+ |
| kubectl | scheduler controllers EndpointSlice | | |
| (clients) | (bind Pods) (Deploy/RS/ controller -> | etcd | |
| | StatefulSet/Job/ ^ |(truth| |
| | YOUR OPERATOR) | +------+ |
+------------|-------------------|--------------------------|--------+
| watch/act | bind | watch
v v |
============ NODES (the hands) =========================== |
| NODE A NODE B | |
| kubelet <-- reconciles Pods kubelet | |
| containerd (CRI) runs: containerd runs: | |
| [Pod: app IP .11] <--CNI--> [Pod: app IP .21] | |
| [Pod: db-0 IP .12] [Pod: db-1 IP .22] | |
| kube-proxy (Service VIP rules) kube-proxy | |
========================================================== |
^ |
| external request: DNS -> Ingress -> Service VIP -> kube-proxy -> Pod
[ user ] ------------------------------------------------+
And here is the payoff of the whole linear spine: every primitive exists because the previous one left a specific failure unanswered. Read this table top to bottom and you have re-derived Kubernetes — each row is forced by the row above.
| The failure left open | The primitive forced | Lesson |
|---|---|---|
| Manual ops: intent lives in a human's head, nothing watches the gap | Declarative state + the reconcile loop (observe → diff → act) | 00 |
| A "process" is too slippery to reconcile — dependency hell, noisy neighbors | The container: a frozen image under namespaces + cgroups | 01 |
| A lone container can't co-locate helpers or be scheduled as one thing | The Pod (shared netns/IP) on a Node (kubelet + CRI runtime) | 02 |
| Desired + actual state need one durable, consistent home | etcd + the API server as sole gateway (hub-and-spoke) | 03 |
| A dead Pod stays dead; nothing restores count, nothing tracks "its" Pods | Controllers + the ReplicaSet, matched by label selector | 04 |
| A ReplicaSet pins one version — no safe rollout, no undo | The Deployment: rolling update over ReplicaSets + rollback | 05 |
| Rolling Pods get new IPs; clients can't hold a moving target | Flat Pod network (CNI) + the Service (stable ClusterIP) | 06 |
| A ClusterIP is a bare number; no name, no L7 routing, no TLS edge | CoreDNS names + Ingress / Gateway front door | 07 |
| Config baked into images; a Pod's disk dies with it | ConfigMap/Secret + PersistentVolume/PVC + StorageClass/CSI | 08 |
| Deployment Pods are interchangeable — wrong for databases, finite jobs, per-node agents | StatefulSet, Job/CronJob, DaemonSet | 09 |
| A new Pod is unbound — which of N nodes runs it? | The kube-scheduler: filter → score → bind (requests, taints, spread) | 10 |
| Running ≠ healthy; a fixed replica count is wrong as load moves | Probes (liveness/readiness/startup) + HPA/VPA/Cluster Autoscaler | 11 |
| Many tenants, but any Pod can read any Secret and call any API | Authn → RBAC → admission, Namespaces/Quota, NetworkPolicy | 12 |
| Built-in nouns only model generic apps; domain objects need a human | CRD (a new noun) + Operator (a custom controller — the §00 loop) | 13 |
The bridge to the sibling track. With this chain in hand, the entire Generative AI on Kubernetes track stops being a separate subject and becomes these exact primitives under one extra constraint: GPUs and their scarce HBM. A model server is a Pod (lesson 02) whose readiness probe (lesson 11) must wait for tens of gigabytes of weights to load; "schedule it" is the scheduler (lesson 10) filtering on a GPU resource and node taints (GPU nodes as a platform); autoscaling is the HPA (lesson 11) driven by KV-cache and TTFT signals instead of CPU; multi-tenant GPU fairness is RBAC + quota (lesson 12). The sibling track's opening lesson, Why GenAI Is Different on Kubernetes, even names the same failure modes — a CPU HPA that can't see the bottleneck, a readiness probe that lies — but now you can place each one on a row of the table above. Nothing there is new machinery; it is this machine, bound by HBM instead of CPU.
Failure modes & checklist
Failure modes
- Reaching for an operator too early. Writing a CRD + controller for something a Deployment plus a Job would have handled. Operators are real software you must maintain. Signal: a 2,000-line reconciler that only ever scales a stateless web app.
- An imperative operator. A reconciler that fires once on the "created" event and assumes its work stuck, instead of re-deriving desired-vs-actual every call. Signal: the database never recovers after a node dies because no event re-fired — the §00 edge-triggered trap, now in your code.
- A non-idempotent reconcile. A pass that creates a member instead of ensuring N exist, so requeues pile up duplicates. Signal: member count drifts above
spec.sizewith no scale-up requested. - Confusing spec and status. The operator writing to
spec(the user's intent) or the user editingstatus. Signal: a fight between human and controller, intent silently overwritten each tick. - Treating the GenAI track as a different system. Re-learning "GPU scheduling" or "model autoscaling" from scratch instead of seeing the scheduler and HPA you already know. Signal: you can't say which recap-table row a GenAI feature sits on.
Checklist
- Before writing an operator, ask if built-ins suffice. StatefulSet + Job + a Service often is the answer; reach for a CRD only when real domain logic (failover, ordered upgrade) must be encoded.
- Model intent as
spec, observed reality asstatus. Only the user writes spec; only the controller writes status. - Make every reconcile level-triggered and idempotent. Re-read both states every call; "ensure N," never "create one."
- Lean on the platform you extended. Your operator should create normal Pods/StatefulSets/Jobs and let the scheduler, kubelet, and Services do their jobs — don't reinvent them.
- Trace one
applyend to end for any new workload; if you can't name the twelve hops, you don't yet understand where it can fail.
Checkpoint exercise
spec.size to 5; let it settle (actual 5, state converged — a tick now does nothing, that is idempotence). Kill two members at once and, before it recovers, predict the diff the operator will compute on its next tick — then watch it recreate exactly to 5 (level-triggered, just like lesson 00's "kill all" test). Now scale spec.size from 5 down to 2 and confirm it deletes the highest ordinals first. Finally, write the §4 twelve-hop trace from memory for a brand-new Deployment, and for any three hops name the failure that would occur if that component were missing — then map each hop to its row in the §5 recap table.Where this points next
There is no lesson 15 — the chain closes here. The forced-derivation arc that began with a human typing ./server at 3am ends with that same human's runbook compiled into a Reconcile function the cluster runs forever. The honest next step is to re-read lesson 00 with new eyes: every noun it promised the loop would need — container, Pod, control plane, controllers, Services, scheduler — you have now built, and the operator you just wrote is literally the lesson-00 widget pointed at a custom object. From here, go wide instead of deeper: open the Generative AI on Kubernetes track and watch every one of these primitives reappear under GPU/HBM pressure — the scheduler placing model Pods on tainted GPU nodes, the HPA scaling on KV-cache instead of CPU, the readiness probe waiting on weights. You will not be learning a new system; you will be applying this one to the hardest workload it runs.
Database{size, version}, a ModelRollout) with a spec (desired) and status (actual); an operator is just a custom controller whose Reconcile function runs the exact observe → diff → act loop from lessons 00 and 04 — level-triggered and idempotent — over that noun, creating and deleting child Pods/StatefulSets/Jobs to converge it and encoding failover, backup, and ordered upgrade that a human used to do from a runbook. That is why "Kubernetes is a platform for building platforms": the control-plane pattern is generic and user-extensible, and the shipped controllers are just the operators its authors wrote. The synthesis seals it: one kubectl apply flows through twelve watch-driven, level-triggered hops (kubectl → authn → RBAC → admission → etcd → Deployment ctrl → ReplicaSet ctrl → scheduler → kubelet/CRI → CNI → readiness → EndpointSlice), one external request flows DNS → Ingress → ClusterIP → kube-proxy → Pod, and the recap table shows every primitive as the forced answer to the previous one's failure. The whole GenAI sibling track is then just this machine under one more constraint — scarce GPU HBM instead of CPU.Interview prompts
- What problem does a CRD solve that a Deployment cannot? (§1 — it teaches the API server a new declarative noun (e.g.
Database) with spec+status, so your domain intent becomes a stored, watchable, RBAC-able object that can be reconciled; otherwise domain knowledge lives only in a human runbook and nothing closes the gap.) - How is an operator related to the controllers from earlier lessons? (§2 — it is the same observe → diff → act control loop (lessons 00, 04), level-triggered and idempotent, just pointed at a custom resource;
Reconcile()reads spec, reads the world, diffs, and creates/deletes child objects. The built-in controllers are simply the operators Kubernetes shipped.) - Why is "Kubernetes is a platform for building platforms" a precise claim? (§2 — the control-plane pattern (declarative object in etcd + a watching reconcile loop) is generic and user-extensible; Deployment/StatefulSet controllers aren't privileged, you write your own with the same API and the same guarantees.)
- Trace one
kubectl apply -f deployment.yamlend to end. (§4 — kubectl → API server authn → authz/RBAC → admission → validate+write to etcd → Deployment controller creates a ReplicaSet → ReplicaSet controller creates Pods → scheduler binds Pods to nodes → kubelet pulls image and starts containers via CRI → CNI assigns Pod IP → readiness probe passes → EndpointSlice adds the Pod to its Service.) - Trace one external HTTP request to a live Pod. (§5 — DNS resolves the hostname to the edge LB → Ingress/Gateway terminates TLS and routes by host/path to a Service → the Service ClusterIP (a stable VIP) → kube-proxy rewrites it to a real Pod IP from the EndpointSlice → a live Pod answers.)
- Pick any primitive and name the failure that forced it. (§5 recap table — e.g. the Service is forced because rolling Pods get new IPs so clients can't hold a moving target; the scheduler is forced because a freshly created Pod is unbound; the CRD/operator is forced because built-in nouns don't model domain objects. Each row is the answer to the row above.)
- How does the GenAI track map onto this one? (Bridge — it is the same primitives under scarce GPU/HBM: model server = Pod whose readiness waits on weight load; GPU placement = scheduler filtering on a GPU resource + node taints; serving autoscale = HPA on KV-cache/TTFT not CPU; tenant fairness = RBAC + quota. Nothing new, just this machine bound by HBM instead of CPU.)