fresh-eyes pass: the Linux bridge (container is a process, namespaces/cgroups, overlayfs upper dir, rootless podman), control plane internals + Deployment->ReplicaSet->Pod lineage, oc-vs-kubectl note, registry-qualified push, de-slop (VIII title formula, flowery headings)

This commit is contained in:
2026-08-17 19:13:50 +10:00
parent 98037d7550
commit 985c442feb
+49 -11
View File
@@ -55,9 +55,9 @@
<div class="crt" role="img" aria-label="A terminal transcript: podman build, podman push,
oc apply, oc get pods showing Running - then the question: but what
actually just happened?">
<p class="line">$ podman build -t shop/api:1.4.2 .</p>
<p class="line">$ podman build -t ghcr.io/shop/api:1.4.2 .</p>
<p class="line ok">=&gt; exported image sha256:9f2c41...</p>
<p class="line">$ podman push shop/api:1.4.2</p>
<p class="line">$ podman push ghcr.io/shop/api:1.4.2</p>
<p class="line">$ oc apply -f deploy.yaml</p>
<p class="line ok">deployment.apps/api created</p>
<p class="line">$ oc get pods</p>
@@ -68,6 +68,10 @@
</div>
</div>
<div class="prose">
<p class="aside">A note on the commands: this site uses <span class="mono">oc</span>,
OpenShift's CLI. Everything shown works identically with <span class="mono">kubectl</span> -
<span class="mono">oc</span> is a superset that adds OpenShift's own verbs. Read
<span class="mono">kubectl</span> in the docs, type whichever your cluster gives you.</p>
<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>
@@ -95,15 +99,32 @@
</div>
</div>
<div class="prose">
<h3>A stack of diffs wearing a name</h3>
<h3>A container is a process, not a machine</h3>
<p>If you arrived here from Linux, take this translation first: a running container is an
<b>ordinary process on your kernel</b>. No guest OS, no hypervisor. The kernel gives it
<b>namespaces</b> so it sees its own PID tree, mounts, network and hostname, and
<b>cgroups</b> so its CPU and memory can be capped. <span class="mono">ps</span> on the host
lists it. <span class="mono">kill</span> on the host kills it. Podman leans into that: no
daemon sits in the middle - the container is a child of your own shell - and rootless mode
maps your user onto root <i>inside</i> the container through a user namespace, so root in
there is an unprivileged UID out here.</p>
<p>So what does the image provide? The <b>filesystem that process sees</b>. That is the whole
job, and it is why an image is a stack of layers rather than a disk image.</p>
<h3>What the image actually is</h3>
<p>An image is not a copy of a machine. It is a stack of <b>read-only</b> layers, each
recording only what changed from the one beneath. Layers are content-addressed, so an
identical layer is stored once and reused by every image that references it, so a pull only
fetches the layers you do not already have. A base sits at the bottom, your dependencies on it,
your code - usually the smallest layer, always the most volatile - above that.</p>
<p>The top slab in the scene is the odd one out: the <b>writable layer is not part of the
image</b>. The runtime adds it when it creates the <i>container</i>, and discards it when
that container dies. Nothing written there ships, and nothing written there survives.</p>
<p>Those layers become one filesystem through a <b>union mount</b> - overlayfs, the same
kernel feature you can mount by hand. The read-only image layers are the lower dirs; the
container gets a fresh <b>upper dir</b> of its own. Writes land in the upper, and editing an
existing file copies it up there first, leaving the image layer untouched underneath.</p>
<p>That upper dir is the top slab in the scene, and it is the odd one out: the <b>writable
layer is not part of the image</b>. The runtime creates it with the <i>container</i> and
discards it when that container dies. Nothing written there ships, and nothing written there
survives - which is the entire reason volumes exist.</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
@@ -159,8 +180,23 @@
<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.
<h3>What the control plane is made of</h3>
<p>"Control plane" is four processes and a database, and naming them makes every later error
message readable. The <b>api-server</b> is the only door: everything authenticates, is
authorised and is admitted there, and it is the only component allowed to touch <b>etcd</b> -
the key-value store holding the entire cluster state. Lose etcd and you have lost the
cluster, which is why backing it up is the homework nobody should skip. The <b>scheduler</b>
decides which node a new pod belongs on and writes that decision down; it never starts
anything. The <b>controller-manager</b> runs the reconciliation loops. Nothing talks
sideways - every component watches the api-server.</p>
<p>One of those loops is the chain you will debug most: a <b>Deployment</b> creates a
<b>ReplicaSet</b>, and the ReplicaSet creates <b>pods</b>. That is why the deploy in the
aperitif printed <span class="mono">deployment.apps/api created</span> and you then went
looking for a pod - and why, when a rollout is stuck with no pod at all, the answer is
upstream in that chain rather than on any node.</p>
<p class="sayit">etcd is the truth. Everything else is a cache of it.</p>
<p class="closeline">Everything the delivery arc teaches from Course VI onward is the same
loop at a bigger scale - 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;
@@ -496,7 +532,7 @@
<section class="course" id="c8" data-scene="helm" aria-labelledby="c8h">
<header class="chead">
<p class="eyebrow">Course VIII &middot; Helm</p>
<h2 id="c8h" class="thesis">A chart is a function.<br><em>The values file is the cluster speaking.</em></h2>
<h2 id="c8h" class="thesis">Stop copying YAML between clusters.<br><em>Ship the function instead.</em></h2>
<p class="lede">The default way to run one app on five clusters is five copies of the YAML, and
the default result is five slightly different apps. The inversion: stop copying outputs and
ship the function. A chart is a template with holes; each cluster supplies one small values
@@ -514,7 +550,7 @@
</div>
</div>
<div class="prose">
<h3>It's Go under the braces</h3>
<h3>You are writing a program, not YAML</h3>
<p>Helm templates are Go text/template: <span class="mono">{{ .Values.device.address }}</span>
is a pipeline walking a values object, <span class="mono">_helpers.tpl</span> holds the
<b>named templates</b> - the partials every manifest <span class="mono">include</span>s, not
@@ -648,7 +684,7 @@
</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>
state, the reconciliation loop, pods, services, and the controllers that keep the 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>
@@ -764,6 +800,8 @@ body.fx .crt .line.on{opacity:1;transform:none}
.prose{max-width:66ch;margin:0 auto;padding:5vh 24px 9vh}
.prose .aside{font-size:.88rem !important;border-left:2px solid var(--line);padding-left:18px;
color:var(--dim) !important}
.prose h3{font-size:1.18rem;font-weight:600;letter-spacing:-.01em;margin:2.6rem 0 .8rem;color:var(--ink)}
.prose p{font-size:.98rem;line-height:1.75;color:var(--dim);margin:0 0 1.1rem}
.sayit{font-weight:600;color:var(--cyan) !important;border-left:2px solid var(--cyan);padding-left:18px}