courses I and II wired: cascade + up-down split animations, fleet-labels move in VI, the library, C-II hero + manifest

This commit is contained in:
2026-08-17 15:18:01 +10:00
parent 5a7b45f1b9
commit 5a56ed6434
10 changed files with 794 additions and 10 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+4 -2
View File
@@ -10,11 +10,13 @@ trap 'printf "build failed at line %s\n" "$LINENO" >&2' ERR
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd -- "$here"
for f in pilot/arc.tpl.html pilot/manifest-gitops.json pilot/manifest-supply.json \
for f in pilot/arc.tpl.html pilot/manifest-docker.json pilot/manifest-cluster.json \
pilot/manifest-gitops.json pilot/manifest-supply.json \
pilot/manifest-helm.json gsap.min.js st.min.js \
assets/dist/course-VI-gitops.webp assets/dist/course-VII-supply-chain.webp \
assets/dist/course-VIII-helm-press.webp assets/dist/appendix-dependency-ledger.webp \
assets/dist/delivery-flow.svg; do
assets/dist/delivery-flow.svg assets/dist/docker-layers.webp \
assets/dist/course-II-cluster.webp; do
[[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; }
done
+155 -6
View File
@@ -1,20 +1,114 @@
<header class="masthead">
<p class="eyebrow">The Exploded Cluster &middot; The Delivery Arc</p>
<h1>The machine was the easy part.<br><em>Now watch how software reaches it.</em></h1>
<p class="lede">Three courses on the toolchain around the cluster: how change travels, how images
are named, and how one definition serves a fleet. Scroll, and each machine comes apart.</p>
<p class="lede">The foundations first - what an image is, what a cluster is - then the toolchain
that delivers to them: how change travels, how images are named, and how one definition serves
a fleet. Scroll, and each machine comes apart.</p>
<p class="scrollcue" aria-hidden="true">scroll<span></span></p>
</header>
<aside class="key" aria-label="reading key">
<p class="kt">Reading key &middot; this page stands alone - anything it leans on is listed here</p>
<ul>
<li><b>C-I</b><span>Docker: an image is a stack of frozen diffs. Referenced by Course VII.</span><i>course lands here soon</i></li>
<li><b>C-II</b><span>Kubernetes: a cluster is a promise - the reconciliation loop. Referenced by Courses VI and VIII.</span><i>course lands here soon</i></li>
<li><b>C-I</b><span>Docker: an image is a stack of frozen diffs. Referenced by Course VII.</span><a href="#c1">on this page</a></li>
<li><b>C-II</b><span>Kubernetes: a cluster is a promise - the reconciliation loop. Referenced by Courses VI and VIII.</span><a href="#c2">on this page</a></li>
<li><b>BLOG</b><span>Consistency by construction beats consistency by discipline - Course VIII's contexts move, lived on a real fleet.</span><a href="https://www.bztmon.com/blog/one-value-many-enforcers/">read it on bztmon.com</a></li>
</ul>
</aside>
<!-- ============ COURSE I ============ -->
<section class="course" id="c1" data-scene="docker" aria-labelledby="c1h">
<header class="chead">
<p class="eyebrow">Course I &middot; Docker</p>
<h2 id="c1h" class="thesis">An image is not a box.<br><em>It is a stack of frozen diffs.</em></h2>
<p class="lede">Scroll, and the thing you keep calling "a container image" comes apart in your
hands. Four layers. Each one only stores what changed from the layer under it - and here they
rise one at a time, bottom up.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_DOCKER__"
alt="An exploded isometric view of a container image: four stacked slabs floating
apart - a heavy metal base, a circuit-etched dependency layer, a magenta-traced
code layer, and a thin frosted-glass writable layer on top.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>A stack of diffs wearing a name</h3>
<p>An image is not a copy of a machine. It is a stack of read-only layers, each recording only
what changed from the one beneath. The base OS is pulled once and shared by every image on
the host; your dependencies sit on it; your code - usually the smallest layer, always the
most volatile - sits above that; and the writable layer is created fresh per container and
thrown away when it dies. Nothing you write there survives.</p>
<p class="sayit">Change a layer and every layer above it must be rebuilt.</p>
<h3>The order is a caching decision</h3>
<p>The builder caches layer by layer, and a cached layer survives only while everything
beneath it is unchanged. Put <span class="mono">COPY . .</span> above your dependency
install and you have told the builder to throw the dependency cache away every time one
line of code changes. Dependencies first, code last - a Dockerfile is a cache policy that
happens to build software.</p>
<p class="fieldnote"><b>Field note.</b> A rebuild that takes twenty minutes and one that
takes twenty seconds are usually the same Dockerfile with the lines swapped.</p>
<p class="closeline">This stack explains the whole ecosystem above it: sharing explains why
pulls are fast, immutability explains why a digest can name the exact bytes (Course VII),
and the throwaway top layer explains why state needs volumes. One idea - frozen diffs -
all the way down.</p>
<p class="reads"><b>Pre-reads:</b> none - start here
&ensp;<b>Further:</b> <a href="https://docs.docker.com/get-started/">Docker get-started</a> &middot;
<a href="https://docs.docker.com/build/">Docker build + layers</a></p>
</div>
</section>
<!-- ============ COURSE II ============ -->
<section class="course" id="c2" data-scene="cluster" aria-labelledby="c2h">
<header class="chead">
<p class="eyebrow">Course II &middot; Kubernetes</p>
<h2 id="c2h" class="thesis">A cluster is a promise,<br><em>not a place.</em></h2>
<p class="lede">You never tell Kubernetes <i>how</i> to run your app. You describe what you
want - declarative intent - and the cluster works, forever, to make it true. Scroll, and
the formation splits: the half that decides rises, the halves that run spread below.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_CLUSTER__"
alt="A small fleet mid-explosion: one wide command slab with a strong cyan seam
hovering above a row of three identical worker blocks, all floating apart in
the void.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>Desired against actual, forever</h3>
<p>The habit underneath everything: the <b>reconciliation loop</b> - compare desired state
against actual state, fix the difference, repeat. That <span class="mono">kubectl apply</span>
didn't launch anything; it filed paperwork. The machine took it from there, and it never
stops taking it from there: kill a pod and it returns, not because something noticed the
crash but because the loop noticed the difference.</p>
<p class="sayit">Kubernetes doesn't run your app - it reconciles it.</p>
<h3>One half decides, one half runs</h3>
<p>The split in the scene is the split that makes everything else possible: a <b>control
plane</b> that decides - holds the truth, schedules, reconciles - and <b>worker nodes</b>
that run pods. The workers are deliberately interchangeable: identical, replaceable, cattle
from day one. Authority does not live where the work happens.</p>
<p class="fieldnote"><b>Field note.</b> If you are SSHing into nodes to "fix" things, you
are arm-wrestling the reconciler - and it does not get tired. Change the desired state
instead.</p>
<p class="closeline">Everything the delivery arc teaches from Course VI onward is this loop
wearing bigger clothes - git as the desired state, whole fleets as reconciled objects.
Learn the promise once; it repeats all the way up.</p>
<p class="reads"><b>Pre-reads:</b> C-I
&ensp;<b>Further:</b> <a href="https://kubernetes.io/docs/concepts/overview/">Kubernetes overview</a> &middot;
<a href="https://kubernetes.io/docs/concepts/architecture/">cluster architecture</a></p>
</div>
</section>
<!-- ============ COURSE VI ============ -->
<section class="course" id="c6" data-scene="gitops" aria-labelledby="c6h">
<header class="chead">
@@ -55,6 +149,15 @@
<p class="fieldnote"><b>Field note.</b> Fighting the reconciler over SSH is arm-wrestling a
machine that does not get tired - C-II's warning, now with a face. The fix is always
upstream, in git.</p>
<h3>At fleet scale, the label is the deploy button</h3>
<p>Run many OpenShift clusters under a hub - RHACM, with edge clusters arriving through
zero-touch provisioning - and nobody applies apps to clusters by hand. Each app carries a
<b>Placement</b> that selects cluster <b>labels</b>; the hub matches placements against the
labels a cluster wears, and the chosen cluster pulls its assignment. Labelling the cluster
<i>is</i> the deploy action: attach the label and the app follows, remove it and the app
leaves. The same pull model as above, one level bigger - a cluster's labels are its
entitlements, reconciled like everything else.</p>
<p class="sayit">Label the cluster; the app follows.</p>
<h3>The one thing git never holds</h3>
<p>Git holds everything except secrets - a secret in git is published, forever, to everyone who
ever clones. So the pattern splits the reference from the value: git carries an ExternalSecret
@@ -275,6 +378,40 @@
</figure>
</section>
<section class="course" id="library" aria-labelledby="libh">
<header class="chead">
<p class="eyebrow">The library</p>
<h2 id="libh" class="thesis">Go to <em>the sources.</em></h2>
<p class="lede">Every technology this site teaches, one sentence each, official documentation
only.</p>
</header>
<ul class="library">
<li><b>Kubernetes</b><span>The container orchestrator underneath everything here: declarative
state, the reconciliation loop, pods, services and the machinery that keeps promises.</span>
<a href="https://kubernetes.io/docs/">kubernetes.io/docs</a></li>
<li><b>Helm</b><span>The package manager and Go-template engine that turns one chart plus a
per-cluster values file into the rendered manifests a cluster actually runs.</span>
<a href="https://helm.sh/docs/">helm.sh/docs</a></li>
<li><b>Red Hat OpenShift</b><span>Kubernetes as an opinionated, supported distribution -
SCCs, Routes and Operators, plus the fleet tooling (RHACM, zero-touch provisioning) that
assigns apps to clusters by label.</span>
<a href="https://docs.redhat.com/en/documentation/openshift_container_platform">docs.redhat.com</a></li>
<li><b>Argo CD</b><span>The GitOps reconciler that watches a repository and keeps the cluster
converged on what it finds there - the pull model that means nothing ever pushes to
production.</span>
<a href="https://argo-cd.readthedocs.io/">argo-cd.readthedocs.io</a></li>
<li><b>GitHub Actions</b><span>CI that builds and tests on every push - in this flow it makes
artefacts and proposes changes, and never holds a credential that can touch a cluster.</span>
<a href="https://docs.github.com/en/actions">docs.github.com/actions</a></li>
<li><b>Azure Key Vault</b><span>The managed vault holding secret values, so git carries only
references and rotation never has to become a commit.</span>
<a href="https://learn.microsoft.com/azure/key-vault/">learn.microsoft.com/key-vault</a></li>
<li><b>Azure Container Registry</b><span>The managed upstream registry CI pushes to - where
the digest is minted that everything downstream pins and promotes.</span>
<a href="https://learn.microsoft.com/azure/container-registry/">learn.microsoft.com/container-registry</a></li>
</ul>
</section>
<footer class="outro">
<p>The Exploded Cluster &middot; the delivery arc &middot; every scene is one generated hero,
sliced and scrubbed - no diagrams were harmed.</p>
@@ -318,6 +455,15 @@ b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cya
.key a{font-family:var(--mono);font-size:.66rem;letter-spacing:.06em;white-space:nowrap}
@media (max-width:700px){.key li{grid-template-columns:44px 1fr}.key i,.key a{grid-column:2}}
.library{max-width:860px;margin:2vh auto 6vh;padding:0 24px;list-style:none;
display:flex;flex-direction:column;gap:12px}
.library li{display:grid;grid-template-columns:190px 1fr auto;gap:16px;align-items:baseline;
border:1px solid var(--line);border-radius:8px;padding:14px 18px;background:#0a1020}
.library b{font-family:var(--mono);font-size:.78rem;letter-spacing:.06em;color:var(--cyan)}
.library span{font-size:.85rem;line-height:1.6;color:var(--dim)}
.library a{font-family:var(--mono);font-size:.68rem;letter-spacing:.04em;white-space:nowrap}
@media (max-width:760px){.library li{grid-template-columns:1fr}.library a{justify-self:start}}
.blueprint{max-width:1220px;margin:2vh auto 4vh;padding:0 24px}
.blueprint img{width:100%;height:auto;display:block;border-radius:12px}
@@ -384,7 +530,7 @@ body:not(.fx) .bench{display:none}
<script>
(() => {
"use strict";
const SCENES = { gitops: __MAN_GITOPS__, supply: __MAN_SUPPLY__, helm: __MAN_HELM__ };
const SCENES = { docker: __MAN_DOCKER__, cluster: __MAN_CLUSTER__, gitops: __MAN_GITOPS__, supply: __MAN_SUPPLY__, helm: __MAN_HELM__ };
const RM = matchMedia("(prefers-reduced-motion: reduce)").matches;
const units = [];
@@ -419,9 +565,12 @@ body:not(.fx) .bench{display:none}
const lis = [...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;
u.slabs[i].style.transform =
"translate3d(" + (p.dx * (1 - t)).toFixed(3) + "%," + (p.dy * (1 - t)).toFixed(3) + "%,0)";
"translate3d(" + (p.dx * (1 - pt)).toFixed(3) + "%," + (p.dy * (1 - pt)).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
+4
View File
@@ -19,12 +19,16 @@ const i = tpl.lastIndexOf("<script>");
const j = tpl.lastIndexOf("</script>");
if (i < 0 || j < i) throw new Error("app script block not found");
let app = tpl.slice(i + 8, j)
.replace("__MAN_DOCKER__", man("docker"))
.replace("__MAN_CLUSTER__", man("cluster"))
.replace("__MAN_GITOPS__", man("gitops"))
.replace("__MAN_SUPPLY__", man("supply"))
.replace("__MAN_HELM__", man("helm"));
let body = tpl.slice(0, i) + tpl.slice(j + 9);
const heroes = {
__HERO_DOCKER__: "docker-layers.webp",
__HERO_CLUSTER__: "course-II-cluster.webp",
__HERO_GITOPS__: "course-VI-gitops.webp",
__HERO_SUPPLY__: "course-VII-supply-chain.webp",
__HERO_HELM__: "course-VIII-helm-press.webp",
+4
View File
@@ -16,9 +16,13 @@ def man(tag):
separators=(",", ":"))
out = (ROOT / "pilot/arc.tpl.html").read_text() \
.replace("__MAN_DOCKER__", man("docker")) \
.replace("__MAN_CLUSTER__", man("cluster")) \
.replace("__MAN_GITOPS__", man("gitops")) \
.replace("__MAN_SUPPLY__", man("supply")) \
.replace("__MAN_HELM__", man("helm")) \
.replace("__HERO_DOCKER__", b64("docker-layers.webp")) \
.replace("__HERO_CLUSTER__", b64("course-II-cluster.webp")) \
.replace("__HERO_GITOPS__", b64("course-VI-gitops.webp")) \
.replace("__HERO_SUPPLY__", b64("course-VII-supply-chain.webp")) \
.replace("__HERO_HELM__", b64("course-VIII-helm-press.webp")) \
+439
View File
@@ -0,0 +1,439 @@
{
"scene": "cluster",
"source": "assets/raw/course-II-cluster.jpeg",
"parts": [
{
"name": "command",
"title": "The control plane",
"blurb": "Decides - holds the truth, schedules, reconciles. The only authority in the room.",
"z": 0,
"points": [
[
70.1,
23.15
],
[
70.02,
28.95
],
[
68.86,
30.8
],
[
63.92,
34.78
],
[
55.0,
42.6
],
[
51.76,
44.47
],
[
49.56,
45.13
],
[
47.87,
44.2
],
[
43.26,
41.53
],
[
32.23,
31.68
],
[
30.85,
30.52
],
[
29.98,
29.1
],
[
29.83,
27.54
],
[
30.63,
21.55
],
[
47.43,
9.27
],
[
49.73,
8.07
],
[
50.71,
8.2
],
[
68.5,
20.84
],
[
69.3,
21.57
]
],
"dx": 0.0,
"dy": 0.0
},
{
"name": "worker-1",
"title": "Worker node",
"blurb": "Runs pods. Identical and replaceable by design - cattle, not pets.",
"z": 0,
"points": [
[
32.59,
76.73
],
[
30.36,
76.47
],
[
23.8,
71.93
],
[
23.29,
71.16
],
[
22.71,
69.64
],
[
22.56,
68.69
],
[
22.35,
51.98
],
[
22.42,
49.83
],
[
22.56,
49.04
],
[
23.73,
46.8
],
[
30.59,
42.2
],
[
31.6,
41.67
],
[
32.52,
41.8
],
[
33.62,
42.47
],
[
39.21,
46.19
],
[
40.01,
47.18
],
[
40.88,
49.36
],
[
41.03,
52.59
],
[
41.1,
68.65
],
[
40.95,
69.59
],
[
40.37,
71.1
],
[
40.01,
71.66
]
],
"dx": 11.06,
"dy": -19.39
},
{
"name": "worker-2",
"title": "Worker node",
"blurb": "Interchangeable with its neighbours - lose one and the promise re-lands its pods elsewhere.",
"z": 0,
"points": [
[
59.78,
62.5
],
[
59.78,
74.77
],
[
59.7,
81.75
],
[
59.63,
82.22
],
[
59.56,
82.53
],
[
58.83,
84.19
],
[
58.54,
84.56
],
[
52.87,
88.87
],
[
51.1,
90.07
],
[
50.85,
90.2
],
[
49.12,
90.2
],
[
48.87,
90.07
],
[
48.29,
89.67
],
[
41.39,
84.65
],
[
41.1,
84.27
],
[
40.44,
82.76
],
[
40.3,
82.12
],
[
40.22,
81.65
],
[
40.15,
72.96
],
[
40.15,
62.14
],
[
40.22,
61.52
],
[
40.37,
60.89
],
[
41.02,
59.38
],
[
41.24,
59.02
],
[
41.53,
58.63
],
[
45.13,
56.07
],
[
49.37,
53.4
],
[
49.7,
53.27
],
[
50.28,
53.27
],
[
50.61,
53.4
],
[
54.86,
56.07
],
[
58.54,
58.92
],
[
58.76,
59.27
],
[
59.63,
61.26
],
[
59.7,
61.73
]
],
"dx": 0.28,
"dy": -26.71
},
{
"name": "worker-3",
"title": "Worker node",
"blurb": "The fleet grows by adding more of exactly this.",
"z": 0,
"points": [
[
58.9,
69.09
],
[
58.97,
50.11
],
[
59.19,
49.18
],
[
60.21,
47.22
],
[
66.51,
42.2
],
[
67.2,
41.8
],
[
68.05,
41.67
],
[
68.73,
41.93
],
[
73.28,
44.6
],
[
76.2,
46.67
],
[
77.22,
48.69
],
[
77.44,
49.51
],
[
77.58,
50.94
],
[
77.51,
58.91
],
[
77.36,
69.28
],
[
77.14,
70.44
],
[
75.71,
72.2
],
[
70.98,
75.4
],
[
69.0,
76.73
],
[
67.19,
76.73
],
[
59.92,
71.54
],
[
59.05,
69.71
]
],
"dx": -11.12,
"dy": -19.34
}
],
"stagger": 0.28
}
+183
View File
@@ -0,0 +1,183 @@
{
"scene": "docker",
"source": "assets/dist/docker-layers.webp",
"stagger": 0.35,
"parts": [
{
"name": "base",
"title": "Base image",
"blurb": "The OS layer everything else is stacked on. Pull it once, share it across every image on the host.",
"z": 0,
"points": [
[
0,
55.66
],
[
27.88,
55.66
],
[
49.48,
75.51
],
[
71.88,
55.6
],
[
100,
55.6
],
[
100,
100
],
[
0,
100
]
],
"dx": 0.0,
"dy": 0.0
},
{
"name": "deps",
"title": "Dependencies",
"blurb": "Your packages and runtime. Change these and every layer above has to be rebuilt.",
"z": 0,
"points": [
[
0,
40.64
],
[
28.38,
40.64
],
[
49.76,
55.45
],
[
71.5,
40.32
],
[
100,
40.32
],
[
100,
56.3
],
[
71.88,
56.3
],
[
49.48,
76.21
],
[
27.88,
56.36
],
[
0,
56.36
]
],
"dx": 0.0,
"dy": 13.49
},
{
"name": "code",
"title": "Application code",
"blurb": "Your actual program \u2014 usually the smallest layer, and the one that changes every single build.",
"z": 0,
"points": [
[
0,
26.54
],
[
27.75,
26.54
],
[
50.77,
40.03
],
[
72.0,
26.17
],
[
100,
26.17
],
[
100,
41.02
],
[
71.5,
41.02
],
[
49.76,
56.15
],
[
28.38,
41.34
],
[
0,
41.34
]
],
"dx": 0.0,
"dy": 28.54
},
{
"name": "writable",
"title": "Writable layer",
"blurb": "Created fresh per container, thrown away when it dies. Anything written here is not in the image.",
"z": 0,
"points": [
[
0,
0
],
[
100,
0
],
[
100,
26.87
],
[
72.0,
26.87
],
[
50.77,
40.73
],
[
27.75,
27.24
],
[
0,
27.24
]
],
"dx": 0.0,
"dy": 40.1
}
]
}
+1 -1
View File
@@ -5,7 +5,7 @@
{
"name": "image",
"title": "The image",
"blurb": "C-I's layers, shipped as one unit - the thing whose name we must say precisely.",
"blurb": "Course I's layers, shipped as one unit - the thing whose name we must say precisely.",
"z": 0,
"points": [
[
+4 -1
View File
@@ -84,6 +84,8 @@ def emit(name, tag, spec):
p["dy"] = round((ay - p["cy"]) * nest, 2)
del p["cx"]; del p["cy"]
man = dict(scene=tag, source=f"assets/raw/{name}", parts=parts)
if "stagger" in spec:
man["stagger"] = spec["stagger"]
out = ROOT / "pilot" / f"manifest-{tag}.json"
out.write_text(json.dumps(man, indent=1))
print(f"manifest-{tag}.json: {len(parts)} parts ->",
@@ -92,7 +94,8 @@ def emit(name, tag, spec):
if __name__ == "__main__":
mode, scenes_json, outdir = sys.argv[1], sys.argv[2], sys.argv[3]
scenes = json.loads(pathlib.Path(scenes_json).read_text()) if mode == "emit" else None
heroes = [("course-VI-gitops.jpeg", "gitops"),
heroes = [("course-II-cluster.jpeg", "cluster"),
("course-VI-gitops.jpeg", "gitops"),
("course-VII-supply-chain.jpeg", "supply"),
("course-VIII-helm-press.jpeg", "helm")]
for name, tag in heroes: