persona-read fixes: Service selectors + ClusterIP as a DNAT rule + CoreDNS + kube-proxy (the networking Linux bridge), cgroup limits and the describe/logs commands, 1/1 explained, second de-slop pass

This commit is contained in:
2026-08-17 19:16:40 +10:00
parent 985c442feb
commit 2e38b27d5f
+34 -16
View File
@@ -15,7 +15,7 @@
<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 IIIa</b><span>The node - where intent becomes a process</span></a></li>
<li><a href="#c3b"><b>Course IIIb</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="#c4"><b>Course IV</b><span>The traffic - pods die constantly, the address does not</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>
@@ -72,6 +72,8 @@
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>That <span class="mono">1/1</span> reads as containers-ready over containers-wanted: a pod
can hold more than one, which is Course IIIb.</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>
@@ -165,7 +167,7 @@
</div>
</div>
<div class="prose">
<h3>Desired against actual, forever</h3>
<h3>Desired against actual, on a loop</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">oc apply</span>
didn't launch anything; it filed paperwork. The machine took it from there, and it never
@@ -230,8 +232,8 @@
runtime pulls the image (through the mirror of Course VII) and actually creates and starts
the container, handing the low-level work to runc or crun. 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 and the runtime executes - and it is the
runtime, not the kubelet, that calls the CNI plugin to wire the pod up.</p>
addresses route to real pods - and it is the runtime, not the kubelet, that calls the CNI
plugin to wire the pod up.</p>
<p class="sayit">The kubelet decides what should run. The runtime is what starts it.</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>
@@ -280,7 +282,11 @@
It does not say by whom. The kernel's OOM killer surfaces as reason OOMKilled; a failed
liveness probe shows up in the pod's events; eviction and node pressure look different again.
One exit code, several possible crimes - read the termination reason and the events, never
the number alone.</p>
the number alone: <span class="mono">oc describe pod &lt;name&gt;</span> shows both together,
and <span class="mono">oc logs --previous</span> shows what the dead container said on its
way out. Worth connecting to what you already know: a memory limit becomes a
<b>cgroup ceiling</b>, and the kernel's OOM killer enforces it exactly as it would for any
other process on the box.</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>
@@ -294,7 +300,7 @@
<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>
<h2 id="c4h" class="thesis">Pods die constantly.<br><em>The address does not.</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>
@@ -315,6 +321,20 @@
<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>How a Service actually finds its pods</h3>
<p>A Service holds no list of pods. It holds a <b>label selector</b> - match
<span class="mono">app: api</span> - and a controller continuously matches that against every
pod in the namespace, keeping the passing set in an <b>EndpointSlice</b>. Labels are how
everything here finds everything else, from a Service picking pods to a fleet hub picking
whole clusters (Course VI). Wear the label and you are eligible; readiness decides whether
you stay.</p>
<p>And the ClusterIP is worth a Linux translation: <b>no interface owns that address</b>.
Nothing answers ARP for it. It exists as a rule - <b>kube-proxy</b> programs iptables (or
IPVS) on every node so packets aimed at the virtual IP are DNATed to one of the ready pod
IPs, and an eBPF dataplane does the same job further down without the rule tables. Cluster
DNS - <b>CoreDNS</b> - resolves <span class="mono">api.myns.svc.cluster.local</span> to that
VIP. If you have ever written a DNAT rule by hand, you have already built a small Service.</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
@@ -327,7 +347,7 @@
<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>
working. Membership is re-earned every few seconds, by every pod, for as long as it serves.</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>
@@ -369,8 +389,8 @@
<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="closeline">Everything in the ring wraps the same core you already know. What
OpenShift adds is a house style with teeth - and admission is where it bites first.</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>
@@ -385,8 +405,7 @@
<p class="lede">The mental model everyone arrives with: someone with credentials pushes manifests
at the cluster. In the architecture this course teaches, nothing is pushed: a repository holds
the desired state, an agent inside the cluster watches it, and the cluster pulls its own future
from git. oc is for
looking; git is for changing.</p>
from git.</p>
</header>
<div class="pin">
<div class="viewport">
@@ -517,8 +536,8 @@
the through-line.</p>
<p class="sayit">If the digest changed, it is not a promotion - it is a new candidate.</p>
<p class="closeline">Names that can move are convenient exactly until they move. Address content
by what it is, and the supply chain stops being a chain of trust and becomes a chain of
arithmetic.</p>
by what it is, and the supply chain stops resting on trust: it rests on a hash anyone can
check.</p>
<p class="reads"><b>Pre-reads:</b> C-I &middot;
<a href="https://kubernetes.io/docs/concepts/containers/images/">Kubernetes images</a>
&ensp;<b>Further:</b> <a href="https://zotregistry.dev/">zot</a> &middot;
@@ -578,9 +597,8 @@
later as enforcement pointed at nothing.</p>
<p class="fieldnote"><b>Field note.</b> The failure you want is the render that refuses. It
costs a red pipeline. The alternative reports healthy the whole time.</p>
<p class="closeline">Fleet consistency is not a discipline problem, it is a construction
problem. Divergence stops being something you police and becomes something the tooling cannot
express.</p>
<p class="closeline">Consistency stops being something you police and becomes something the
tooling cannot express.</p>
<p class="reads"><b>Pre-reads:</b> C-II &middot;
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/">Kubernetes objects</a>
&ensp;<b>Further:</b> <a href="https://helm.sh/docs/">Helm docs</a> &middot;