the complete menu: aperitif CRT terminal + courses III node/pod, IV traffic, V orbit - five animation personalities, clip-split and reverse slice modes

This commit is contained in:
2026-08-17 16:06:11 +10:00
parent a201af9d29
commit b12e435003
20 changed files with 3663 additions and 9 deletions
+240 -6
View File
@@ -10,8 +10,13 @@
<nav class="toc" aria-label="index">
<p class="kt">Index</p>
<ol>
<li><a href="#c0"><b>Course 00</b><span>Aperitif - three commands, and what actually just happened</span></a></li>
<li><a href="#c1"><b>Course I</b><span>Docker - an image is a stack of frozen diffs</span></a></li>
<li><a href="#c2"><b>Course II</b><span>Kubernetes - a cluster is a promise, not a place</span></a></li>
<li><a href="#c3a"><b>Course III</b><span>The node - where intent becomes a process</span></a></li>
<li><a href="#c3b"><b>Course III</b><span>The pod - one IP, shared fate</span></a></li>
<li><a href="#c4"><b>Course IV</b><span>The traffic - pods are cattle, Services are the brand</span></a></li>
<li><a href="#c5"><b>Course V</b><span>OpenShift - Kubernetes with opinions, and a security guard</span></a></li>
<li><a href="#c6"><b>Course VI</b><span>GitOps - nobody deploys anything; the cluster syncs itself</span></a></li>
<li><a href="#c7"><b>Course VII</b><span>The image supply chain - a tag is a promise, a digest is a fact</span></a></li>
<li><a href="#c8"><b>Course VIII</b><span>Helm - a chart is a function; the values file is the cluster speaking</span></a></li>
@@ -26,13 +31,50 @@
<p>The Exploded Cluster teaches how modern container platforms work by taking them apart -
literally. Each course is one machine drawn as a single exploded illustration, sliced into its
real components and wired to your scroll, so the architecture moves while the words explain it.
Start at Course I with what an image actually is; finish knowing how a change travels from a
Start with the aperitif&#39;s three terminal commands; finish knowing how a change travels from a
git commit to a running, secret-fed, digest-pinned workload on a fleet. Every claim comes from
running these systems for real, and the library at the end links only to official
documentation.</p>
</section>
<!-- ============ COURSE 00 ============ -->
<section class="course" id="c0" data-scene="surround" aria-labelledby="c0h">
<header class="chead">
<p class="eyebrow">Course 00 &middot; Aperitif</p>
<h2 id="c0h" class="thesis">Three commands.<br><em>What actually just happened?</em></h2>
<p class="lede">The whole ceremony of shipping software fits in four lines of terminal. They
work on your first day and stay mysterious for years. Scroll - the shell comes off first.</p>
</header>
<div class="pin">
<div class="viewport solo">
<figure class="scene">
<img class="flat" src="__HERO_SURROUND__"
alt="Six dark armour fragments with neon seams framing a large empty centre - the
casing of a machine caught the instant before it comes apart.">
<div class="crt" role="img" aria-label="A terminal transcript: docker build, docker push,
kubectl apply, kubectl get pods showing Running - then the question: but what
actually just happened?">
<p class="line">$ docker build -t shop/api:1.4.2 .</p>
<p class="line ok">=&gt; exported image sha256:9f2c41...</p>
<p class="line">$ docker push shop/api:1.4.2</p>
<p class="line">$ kubectl apply -f deploy.yaml</p>
<p class="line ok">deployment.apps/api created</p>
<p class="line">$ kubectl get pods</p>
<p class="line ok">api-7d4b9c6-x2k4f&nbsp;&nbsp;1/1&nbsp;&nbsp;Running</p>
<p class="line q">...but what actually just happened?</p>
</div>
</figure>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<p>An image got <b>built</b> - of what, exactly? <b>Pushed</b> - to where, and what travelled?
<b>Applied</b> - which is not the same as launched. <b>Running</b> - according to whom?
Every course below takes one of those words apart. The armour is already loose.</p>
</div>
</section>
<!-- ============ COURSE I ============ -->
<section class="course" id="c1" data-scene="docker" aria-labelledby="c1h">
<header class="chead">
@@ -126,6 +168,171 @@
</div>
</section>
<!-- ============ COURSE III - THE NODE ============ -->
<section class="course" id="c3a" data-scene="node" aria-labelledby="c3ah">
<header class="chead">
<p class="eyebrow">Course III &middot; The node</p>
<h2 id="c3ah" class="thesis">Where intent<br><em>becomes a process.</em></h2>
<p class="lede">Everything so far was decision. This is the machine where a pod stops being
paperwork and starts being a process - and the chain assembles link by link as you scroll.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_NODE__"
alt="A diagonal chain of node machinery: a visor-lit kubelet module, a layered
container-runtime engine, a ported CNI ring, a magenta routing prism - and a
small glowing pod capsule descending toward the engine.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>One component starts containers. One.</h3>
<p>Every node runs a <b>kubelet</b> - and in the entire system it is the only component that
actually starts your container. It speaks CRI to <b>containerd</b> or CRI-O, which pulls the
image (through the mirror of Course VII) and launches it. A <b>CNI plugin</b> hands the pod
a real IP, and <b>kube-proxy</b> - or an eBPF datapath replacing it - makes Service
addresses route to real pods. The kubelet drives, the runtime executes, the CNI wires, the
proxy routes.</p>
<p class="sayit">The kubelet is the only component that actually starts your container.</p>
<p class="fieldnote"><b>Field note.</b> Node NotReady? Check kubelet-to-API-server
connectivity before anything else - a node that cannot phone home is presumed lost.</p>
<p class="closeline">The control plane never touches your workload. It writes intent; the
kubelet reads it and makes it flesh. Authority and execution meet exactly here, nowhere
else.</p>
<p class="reads"><b>Pre-reads:</b> C-II
&ensp;<b>Further:</b> <a href="https://kubernetes.io/docs/concepts/architecture/">cluster architecture</a> &middot;
<a href="https://kubernetes.io/docs/concepts/architecture/#node-components">node components</a></p>
</div>
</section>
<!-- ============ COURSE III - THE POD ============ -->
<section class="course" id="c3b" data-scene="pod" aria-labelledby="c3bh">
<header class="chead">
<p class="eyebrow">Course III &middot; The pod</p>
<h2 id="c3bh" class="thesis">One IP,<br><em>shared fate.</em></h2>
<p class="lede">A pod is not a container - it is a jacket around one or more. Scroll, and the
capsule opens like a clamshell: shells apart, contents on display.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_POD__"
alt="A pod capsule blown open: two frosted shell halves floating apart, a cyan app
container column standing on an amber-lit init gate, a smaller magenta sidecar
beside it, and a stack of translucent volume discs.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>The jacket, not the container</h3>
<p>Everything inside the jacket shares a <b>network namespace</b>: one IP, localhost between
friends, volumes mountable by all. <b>initContainers</b> run first, in order, to completion -
gatekeepers under everything. <b>Sidecars</b> ride along with their own containers and their
own jobs: proxy, logs, reload.</p>
<h3>Three probes, three different jobs</h3>
<p><b>startup</b> owns warm-up, <b>readiness</b> gates traffic, <b>liveness</b> restarts the
truly hung. Confusing them is how healthy pods get executed - a slow start killed by an
impatient liveness probe looks exactly like a crash.</p>
<p class="sayit">containerPort is documentation - the app still has to bind the port itself.</p>
<p class="fieldnote"><b>Field note.</b> Exit 137 with reason Error is the kubelet's probe
kill; OOMKilled is memory. Same exit code, different crimes - read the reason.</p>
<p class="closeline">The pod is the smallest schedulable unit - the jacket, never the
container. Once that distinction lands, half of Kubernetes networking stops being
mysterious.</p>
<p class="reads"><b>Pre-reads:</b> C-III's node scene above
&ensp;<b>Further:</b> <a href="https://kubernetes.io/docs/concepts/workloads/pods/">pods</a> &middot;
<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">the three probes</a></p>
</div>
</section>
<!-- ============ COURSE IV ============ -->
<section class="course" id="c4" data-scene="traffic" aria-labelledby="c4h">
<header class="chead">
<p class="eyebrow">Course IV &middot; The traffic</p>
<h2 id="c4h" class="thesis">Pods are cattle.<br><em>Services are the brand.</em></h2>
<p class="lede">Pods die, respawn and change addresses - and traffic still arrives. Scroll,
and the delivery route assembles checkpoint by checkpoint; watch what happens to the pod
that stops answering.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_TRAFFIC__"
alt="A delivery route in the void: a glowing client orb, a fanned load-balancer
wedge, an open ingress doorframe, a Service prism with a bright core, a lit
ready pod - and a dark unlit pod fallen out of the line.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>The stable name in front of the churn</h3>
<p>A <b>Service</b> is the fixed point: a ClusterIP inside, a LoadBalancer at the edge,
Ingress or the Gateway API doing host- and path-routing above. Clients hold the name; the
pods behind it come and go without anyone being told.</p>
<h3>Readiness decides membership</h3>
<p>A pod failing its readiness probe <b>silently leaves the pool</b>. No error, no event at
the client - traffic simply stops arriving. That is the feature: broken instances remove
themselves. It is also the first place to look when traffic "disappears".</p>
<p class="sayit">Never talk to a pod; talk to a Service.</p>
<p class="fieldnote"><b>Field note.</b> "The network is broken" after a deploy is usually
readiness telling the truth about your app - not the network lying about your packets.</p>
<p class="closeline">The dark pod in the scene is not an error state - it is the system
working. Membership is earned per probe, continuously, forever.</p>
<p class="reads"><b>Pre-reads:</b> C-III's pod scene (readiness lives there)
&ensp;<b>Further:</b> <a href="https://kubernetes.io/docs/concepts/services-networking/">Services and networking</a></p>
</div>
</section>
<!-- ============ COURSE V ============ -->
<section class="course" id="c5" data-scene="orbit" aria-labelledby="c5h">
<header class="chead">
<p class="eyebrow">Course V &middot; OpenShift</p>
<h2 id="c5h" class="thesis">Kubernetes with opinions -<br><em>and a security guard.</em></h2>
<p class="lede">OpenShift is a distribution of Kubernetes: same engine, opinionated chassis.
Scroll, and the opinions bloom outward from the core they orbit.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_ORBIT__"
alt="A glowing geodesic core ringed by six satellites: a magenta admission shield,
an open route arch, interlocking operator rings, a handheld console, an
amber-lit stack of machine-config plates and a compact single-node box.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>The doorman interviews every pod</h3>
<p>The <b>SCC</b> is admission deciding what a pod may BE. The default, restricted-v2, runs
your container as a <b>random non-root UID</b> - your image has to cope. Workloads that
genuinely need privilege get a dedicated ServiceAccount bound to a minimal custom SCC,
never the stock one.</p>
<h3>The platform runs itself</h3>
<p><b>Routes</b> predate Ingress and still rule here. <b>Operators</b> run the platform's own
machinery. The OS underneath is immutable - changed by <b>MachineConfig</b>, never by SSH.
And <b>SNO</b> puts the entire promise on one box at the edge. At fleet scale the labels
from Course VI decide which of these boxes runs what.</p>
<p class="sayit">On OpenShift, admission is the interview - the SCC is the dress code.</p>
<p class="fieldnote"><b>Field note.</b> Deployment stuck at 0/1 with no pod at all? The
refusal happened above scheduling - read the ReplicaSet events. The error lives a level
up.</p>
<p class="closeline">Everything in the ring is opinion wrapped around the same core you
already know. Learn the core once; the opinions are just house rules.</p>
<p class="reads"><b>Pre-reads:</b> C-II
&ensp;<b>Further:</b> <a href="https://docs.redhat.com/en/documentation/openshift_container_platform">OpenShift documentation</a> &middot;
<a href="https://www.redhat.com/en/technologies/cloud-computing/openshift">Red Hat OpenShift</a></p>
</div>
</section>
<!-- ============ COURSE VI ============ -->
<section class="course" id="c6" data-scene="gitops" aria-labelledby="c6h">
<header class="chead">
@@ -516,6 +723,18 @@ b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cya
.legend .t{font-size:.92rem;font-weight:600;margin:0 0 .22rem;letter-spacing:-.01em}
.legend .b{font-size:.76rem;line-height:1.5;color:var(--dim);margin:0}
.viewport.solo{grid-template-columns:1fr;max-width:980px}
.crt{position:absolute;inset:22% 26%;display:flex;flex-direction:column;justify-content:center;
gap:.35rem;padding:clamp(10px,2.5vw,26px);border:1px solid var(--line);border-radius:10px;
background:rgba(5,8,16,0.88);box-shadow:0 0 40px rgba(63,186,245,0.07) inset}
.crt .line{font-family:var(--mono);font-size:clamp(.55rem,1.35vw,.85rem);line-height:1.5;
color:var(--ink);margin:0;white-space:nowrap;overflow:hidden}
.crt .line.ok{color:var(--dim)}
.crt .line.q{color:var(--magenta);margin-top:.5rem}
body.fx .crt .line{opacity:0;transform:translateY(4px);transition:opacity .3s ease,transform .3s ease}
body.fx .crt .line.on{opacity:1;transform:none}
@media (max-width:700px){.crt{inset:16% 12%}}
.bench{width:min(720px,100%);margin:16px auto 0;text-align:center}
.bench input{width:100%;accent-color:var(--cyan)}
.bench .hud{font-family:var(--mono);font-size:.66rem;letter-spacing:.06em;color:var(--dim);margin:.45rem 0 0}
@@ -562,7 +781,7 @@ body:not(.fx) .bench{display:none}
<script>
(() => {
"use strict";
const SCENES = { docker: __MAN_DOCKER__, cluster: __MAN_CLUSTER__, gitops: __MAN_GITOPS__, supply: __MAN_SUPPLY__, helm: __MAN_HELM__ };
const SCENES = { surround: __MAN_SURROUND__, docker: __MAN_DOCKER__, cluster: __MAN_CLUSTER__, node: __MAN_NODE__, pod: __MAN_POD__, traffic: __MAN_TRAFFIC__, orbit: __MAN_ORBIT__, gitops: __MAN_GITOPS__, supply: __MAN_SUPPLY__, helm: __MAN_HELM__ };
const RM = matchMedia("(prefers-reduced-motion: reduce)").matches;
const units = [];
@@ -571,7 +790,7 @@ body:not(.fx) .bench{display:none}
const scene = sec.querySelector(".scene");
const flat = scene.querySelector(".flat");
const legend = sec.querySelector(".legend");
man.parts.forEach(p => {
if (legend) man.parts.forEach(p => {
const li = document.createElement("li");
li.innerHTML = '<p class="t">' + p.title + '</p><p class="b">' + p.blurb + '</p>';
legend.appendChild(li);
@@ -594,15 +813,18 @@ body:not(.fx) .bench{display:none}
return d;
});
u.flat.style.visibility = "hidden";
const lis = [...u.legend.children];
const lis = u.legend ? [...u.legend.children] : [];
const n = lis.length;
u.apply = t => {
// stagger > 0 = cascade: parts complete their travel one after another in list order
const sg = u.man.stagger || 0;
u.man.parts.forEach((p, i) => {
const pt = sg ? Math.min(1, Math.max(0, t * (1 + sg * (n - 1)) - sg * i)) : t;
const k = u.man.parts.length;
const pt = sg ? Math.min(1, Math.max(0, t * (1 + sg * (k - 1)) - sg * i)) : t;
// reverse scenes sit assembled at rest and FLY APART as the scrub advances
const e = u.man.reverse ? (1 - pt) : pt;
u.slabs[i].style.transform =
"translate3d(" + (p.dx * (1 - pt)).toFixed(3) + "%," + (p.dy * (1 - pt)).toFixed(3) + "%,0)";
"translate3d(" + (p.dx * (1 - e)).toFixed(3) + "%," + (p.dy * (1 - e)).toFixed(3) + "%,0)";
});
u.scene.style.transform = "scale(" + (0.95 + 0.05 * t).toFixed(4) + ")";
// the side list lights in reading order, top to bottom, as the scrub advances
@@ -654,6 +876,18 @@ body:not(.fx) .bench{display:none}
});
if (!RM) ScrollTrigger.refresh();
};
// the aperitif terminal types its transcript once, when it first enters view
document.querySelectorAll(".crt").forEach(crt => {
const lines = [...crt.querySelectorAll(".line")];
if (RM) { lines.forEach(l => l.classList.add("on")); return; }
const io = new IntersectionObserver(es => {
if (!es.some(x => x.isIntersecting)) return;
io.disconnect();
lines.forEach((l, i) => setTimeout(() => l.classList.add("on"), 350 * i + 200));
}, { threshold: 0.4 });
io.observe(crt);
});
Promise.all(units.map(u => u.flat.decode ? u.flat.decode().catch(() => {}) : Promise.resolve()))
.then(() => requestAnimationFrame(() => requestAnimationFrame(boot)));
})();