<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Zwindler's Reflection</title><link>https://blog.zwindler.fr/en/</link><description>Recent content on Zwindler's Reflection</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Licensed under CC BY-SA 4.0</copyright><lastBuildDate>Tue, 17 Mar 2026 18:00:00 +0100</lastBuildDate><atom:link href="https://blog.zwindler.fr/en/index.xml" rel="self" type="application/rss+xml"/><item><title>GenAI and software development, episode 2: kubectl-debug-pvc, from idea to krew in 2x30 minutes</title><link>https://blog.zwindler.fr/en/2026/03/17/genai-and-software-development-episode-2-kubectl-debug-pvc-from-idea-to-krew-in-2x30-minutes/</link><pubDate>Tue, 17 Mar 2026 18:00:00 +0100</pubDate><guid>https://blog.zwindler.fr/en/2026/03/17/genai-and-software-development-episode-2-kubectl-debug-pvc-from-idea-to-krew-in-2x30-minutes/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/kubectl-debug-pvc.webp" alt="Featured image of post GenAI and software development, episode 2: kubectl-debug-pvc, from idea to krew in 2x30 minutes" /&gt;&lt;h2 id="previously-on-genai-and-dev"&gt;Previously, on &amp;ldquo;GenAI and dev&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;In &lt;a class="link" href="https://blog.zwindler.fr/en/2026/03/08/genai-and-software-development-lessons-learned-with-podsweeper/" &gt;my previous article&lt;/a&gt;, I talked about my experience with PodSweeper, a project developed with OpenCode and Claude Opus. The outcome was mixed: impressive raw speed, but race conditions, lax error handling, and a constant need for human supervision (among other disappointments).&lt;/p&gt;
&lt;p&gt;Today, I&amp;rsquo;m talking about a second project, much simpler, born from a real production need. And the takeaway is quite different.&lt;/p&gt;
&lt;h2 id="the-incident-that-started-it-all"&gt;The incident that started it all
&lt;/h2&gt;&lt;p&gt;You may know this situation: a pod in production, running fine, using a PVC in &lt;code&gt;ReadWriteOnce&lt;/code&gt;. You need to look at the contents of that volume. Production best practice: the pod in question has no shell (we reduce the attack surface). No &lt;code&gt;/bin/sh&lt;/code&gt;, no &lt;code&gt;/bin/bash&lt;/code&gt;, nothing.&lt;/p&gt;
&lt;p&gt;No big deal, we have &lt;code&gt;kubectl debug&lt;/code&gt; for that, right?&lt;/p&gt;
&lt;p&gt;Ok, let&amp;rsquo;s create an ephemeral container in the pod and&amp;hellip; oh wait. &lt;code&gt;kubectl debug&lt;/code&gt; doesn&amp;rsquo;t allow mounting the pod&amp;rsquo;s volumes in the ephemeral container. It simply doesn&amp;rsquo;t expose the &lt;code&gt;volumeMounts&lt;/code&gt; option for ephemeral containers.&lt;/p&gt;
&lt;p&gt;We could kill the pod, which would allow mounting the RWO PVC in another pod with a shell, but we don&amp;rsquo;t want to — it&amp;rsquo;s production.&lt;/p&gt;
&lt;p&gt;My colleague Maxime (him again!) suggested a workaround. The manual procedure is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create an ephemeral container on the pod with &lt;code&gt;kubectl debug&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Manually build a JSON patch to add &lt;code&gt;volumeMounts&lt;/code&gt; to the ephemeral container we just created&lt;/li&gt;
&lt;li&gt;In another terminal, connect directly to the kube API with &lt;code&gt;kubectl proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Apply the patch via a curl to the Kubernetes API&lt;/li&gt;
&lt;li&gt;Wait for the container to be ready, attach to the container&lt;/li&gt;
&lt;li&gt;Wonder if there&amp;rsquo;s an easier career than patching containers with curl&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For those who think this is doable, &amp;ldquo;check out this patch&amp;rdquo; as the kids say:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;curl&lt;/span&gt; &lt;span class="err"&gt;http:&lt;/span&gt;&lt;span class="c1"&gt;//localhost:8001/api/v1/namespaces/&amp;lt;namespace&amp;gt;/pods/&amp;lt;pod&amp;gt;/ephemeralcontainers \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;-X&lt;/span&gt; &lt;span class="err"&gt;PATCH&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;-H&lt;/span&gt; &lt;span class="err"&gt;&amp;#39;Content-Type:&lt;/span&gt; &lt;span class="err"&gt;application/strategic-merge-patch+json&amp;#39;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;-d&lt;/span&gt; &lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;spec&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ephemeralContainers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;debugger&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;image&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ubuntu&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/bin/sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;targetContainerName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;target-container&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;stdin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tty&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;volumeMounts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;volume-name&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mountPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/debug/&amp;lt;volume-name&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you think this is error prone, you&amp;rsquo;re right. And if you think it&amp;rsquo;s painful to do under pressure during a production incident, you&amp;rsquo;re even more right.&lt;/p&gt;
&lt;h2 id="side-note"&gt;Side note
&lt;/h2&gt;&lt;p&gt;The more seasoned among you with recent Kubernetes versions may have heard about &lt;a class="link" href="https://kep.k8s.io/2590" target="_blank" rel="noopener"
&gt;KEP-2590&lt;/a&gt;, which introduces a (relatively) new &lt;code&gt;--subresource&lt;/code&gt; flag for &lt;code&gt;kubectl&lt;/code&gt;. Indeed, ephemeral containers created by &lt;code&gt;kubectl debug&lt;/code&gt; are not &lt;em&gt;resources&lt;/em&gt; (a pod, a deployment, &amp;hellip;) but &lt;em&gt;subresources&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Until version 1.33, it was literally impossible to perform operations on subresources with &lt;code&gt;kubectl&lt;/code&gt;, only resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes v1.33: Octarine&lt;/strong&gt; brings support for the &amp;ndash;subresource flag, &lt;strong&gt;but only&lt;/strong&gt; for 3 subresources for now: &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;scale&lt;/code&gt; and &lt;code&gt;resize&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/#subresource-support-in-kubectl" target="_blank" rel="noopener"
&gt;https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/#subresource-support-in-kubectl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/reference/kubectl/conventions/#subresources" target="_blank" rel="noopener"
&gt;https://kubernetes.io/docs/reference/kubectl/conventions/#subresources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No solution on that front either&amp;hellip;&lt;/p&gt;
&lt;h2 id="the-meeting-prompt"&gt;The meeting prompt
&lt;/h2&gt;&lt;p&gt;I had this idea in mind since the incident. Monday morning, at the start of a meeting (while people were still making jokes), I launched OpenCode with Claude Opus and typed a prompt describing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The problem: &lt;code&gt;kubectl debug&lt;/code&gt; doesn&amp;rsquo;t mount PVC volumes if they&amp;rsquo;re RWO&lt;/li&gt;
&lt;li&gt;The manual procedure I was doing by hand (the painful steps described above)&lt;/li&gt;
&lt;li&gt;What I wanted: a tool that automates all of that&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OpenCode + Opus asked me 2 questions (and I added one instruction):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&amp;ldquo;Which language?&amp;rdquo;&lt;/em&gt; → &lt;strong&gt;Go&lt;/strong&gt; (I insist)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&amp;ldquo;CLI only or TUI?&amp;rdquo;&lt;/em&gt; → &lt;strong&gt;Both&lt;/strong&gt; (non-interactive mode for scripting, TUI for daily use)&lt;/li&gt;
&lt;li&gt;And I asked it to always run the &lt;code&gt;golangci-lint&lt;/code&gt; linter every time it considers being done (trauma from my previous test with OpenCode)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It went on its own with the idea of a TUI using &lt;a class="link" href="https://github.com/charmbracelet/bubbletea" target="_blank" rel="noopener"
&gt;Bubble Tea&lt;/a&gt;. Then I stopped watching and followed my meeting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;~30 minutes later, end of the meeting&lt;/strong&gt;, I looked at the result. The POC was functional.&lt;/p&gt;
&lt;h2 id="what-opus-produced-in-30-minutes"&gt;What Opus produced in 30 minutes
&lt;/h2&gt;&lt;p&gt;Without any intervention on my part, the LLM scaffolded a complete Go project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clean structure&lt;/strong&gt;: &lt;code&gt;cmd/&lt;/code&gt; for the Cobra CLI, &lt;code&gt;pkg/k8s/&lt;/code&gt; for all Kubernetes interaction, &lt;code&gt;pkg/tui/&lt;/code&gt; for the Bubble Tea TUI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The core of the matter&lt;/strong&gt;: a direct call to the Kubernetes API to patch the &lt;code&gt;ephemeralcontainers&lt;/code&gt; subresource of the pod with a strategic merge patch including the &lt;code&gt;volumeMounts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-interactive mode&lt;/strong&gt;: &lt;code&gt;-n namespace -p pod -v volume:/mount/path&lt;/code&gt;, ready for scripting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complete TUI&lt;/strong&gt;: vim-style navigation (&lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt;), fuzzy filtering (&lt;code&gt;/&lt;/code&gt;), multi-selection of volumes, loading spinner&amp;hellip;&lt;/li&gt;
&lt;li&gt;Makefile with a whole bunch of targets (vet, lint, test, build, install)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I asked it to add:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Smart discovery&lt;/strong&gt;: instead of scanning all pods in all namespaces (slow on one of my large clusters), the tool first lists PVCs cluster-wide (a single API call) to identify relevant namespaces, THEN the pods in the selected namespace. This drastically reduces the number of calls to the kube API server and the TUI only shows namespaces and pods that have PVC volumes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SecurityContext inheritance&lt;/strong&gt;: the ephemeral container copies the &lt;code&gt;securityContext&lt;/code&gt; from the target container, allowing it to pass PodSecurity policies (&lt;code&gt;restricted&lt;/code&gt;, &lt;code&gt;baseline&lt;/code&gt;&amp;hellip;). This was a case I hadn&amp;rsquo;t anticipated in my initial prompt and it immediately failed on a properly configured cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without these small improvements the tool was already usable (except when you have SecurityContext configured), but it&amp;rsquo;s much more comfortable in practice (because yes, I do use it).&lt;/p&gt;
&lt;p&gt;The code for the core mechanism (the API patch) is a few hundred lines of clean Go. It retrieves the pod, builds the JSON patch with the ephemeral container and its &lt;code&gt;volumeMounts&lt;/code&gt;, applies it via &lt;code&gt;Patch()&lt;/code&gt; on the subresource, waits for the container to be Running, then launches &lt;code&gt;kubectl attach&lt;/code&gt;. Nothing fancy, it&amp;rsquo;s just what&amp;rsquo;s needed, done right the first time.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-golang" data-lang="golang"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ephemeralContainer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;corev1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EphemeralContainer&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;EphemeralContainerCommon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;corev1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EphemeralContainerCommon&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;containerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/bin/sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stdin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TTY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;VolumeMounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SecurityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;targetSecurityContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TargetContainerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;targetContainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Clientset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CoreV1&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Pods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Namespace&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PodName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StrategicMergePatchType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;patchBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;metav1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PatchOptions&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ephemeralcontainers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-following-iterations-30-minutes"&gt;The following iterations (~30 minutes)
&lt;/h2&gt;&lt;p&gt;Once the POC was validated, I chained a few targeted prompts. I asked it what could be improved. It suggested (and implemented):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A few minor code fixes it had gotten wrong (but non-blocking)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Warnings&lt;/strong&gt;: if the inherited SecurityContext has &lt;code&gt;readOnlyRootFilesystem&lt;/code&gt;, &lt;code&gt;runAsNonRoot&lt;/code&gt;, or other security measures, the tool warns the user before attach&lt;/li&gt;
&lt;li&gt;Added a complete CI based on &lt;code&gt;goreleaser&lt;/code&gt; and GitHub Actions&lt;/li&gt;
&lt;li&gt;Added documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-cherry-on-top-publishing-on-krew"&gt;The cherry on top: publishing on Krew
&lt;/h2&gt;&lt;p&gt;Once I had a clean version, I asked Opus how to make the plugin easier to install. It suggested &lt;code&gt;brew&lt;/code&gt;, or &lt;a class="link" href="https://krew.sigs.k8s.io/" target="_blank" rel="noopener"
&gt;Krew&lt;/a&gt;, the kubectl plugin manager.&lt;/p&gt;
&lt;p&gt;I asked if the plugin acceptance process was complex. It said no, I said &amp;ldquo;I dare you!&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;It &lt;strong&gt;completed the entire process&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Created a fork of &lt;a class="link" href="https://github.com/kubernetes-sigs/krew-index" target="_blank" rel="noopener"
&gt;krew-index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Wrote the Krew manifest (the &lt;code&gt;.yaml&lt;/code&gt; file describing the plugin)&lt;/li&gt;
&lt;li&gt;Took into account all the best practices requested by krew-index maintainers (download URL format, short descriptions, SHA256 checksums, etc.)&lt;/li&gt;
&lt;li&gt;Prepared the PR, directly on krew-index&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I just watched. The PR was merged 12 hours later by the maintainers.&lt;/p&gt;
&lt;p&gt;Result: &lt;code&gt;kubectl krew install debug-pvc&lt;/code&gt; works.&lt;/p&gt;
&lt;h2 id="minor-failure--delegating-the-demo-too"&gt;Minor failure — delegating the demo too
&lt;/h2&gt;&lt;p&gt;Emboldened by this success with the krew-index PR, I wondered if we could make a visual demo (a GIF to add to the project&amp;rsquo;s README.md showing how it works) with the LLM.&lt;/p&gt;
&lt;p&gt;I asked if it could do it with &lt;code&gt;asciinema&lt;/code&gt; and the LLM answered &amp;ldquo;yes&amp;rdquo; (yes, I chat with the LLM like I do with my colleagues). Deal, we tried.&lt;/p&gt;
&lt;p&gt;The result was &lt;em&gt;almost&lt;/em&gt; good, I could have settled for it if I weren&amp;rsquo;t such a nitpicker: it was a bit slow. I felt the LLM&amp;rsquo;s responsiveness in its actions was uneven, which made the viewing experience a bit unpleasant. I could have iterated until I got something correct, but I ultimately recorded a video myself, converted to GIF with &lt;code&gt;ffmpeg&lt;/code&gt;. It was smoother and faster than iterating.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/kubectl-debug-pvc-demo.gif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="the-difference-with-podsweeper"&gt;The difference with PodSweeper
&lt;/h2&gt;&lt;p&gt;The difference in experience between this project and PodSweeper is striking. Where PodSweeper was a constant fight (race conditions, LLM amnesia, out-of-spec features), &lt;code&gt;kubectl-debug-pvc&lt;/code&gt; went smoothly without any notable hiccups.&lt;/p&gt;
&lt;p&gt;Why? I think it comes down to the nature of the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One single thing to do&lt;/strong&gt;, clearly defined: patch a Kubernetes subresource with volumeMounts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No concurrent logic&lt;/strong&gt;: we make an API request, we wait, we attach&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A well-documented domain&lt;/strong&gt;: the Kubernetes API, Cobra, Bubble Tea. The LLM knows all of this by heart&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No complex shared state&lt;/strong&gt;: no goroutines stepping on each other, no graceful shutdown to manage, no multiple microservices&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited scope&lt;/strong&gt;: the entire project fits in about fifteen files, CI and documentation included&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is probably the ideal playground for an LLM. A well-defined problem, a well-charted technical domain, a linear solution.&lt;/p&gt;
&lt;h2 id="what-do-i-think-about-it"&gt;What do I think about it?
&lt;/h2&gt;&lt;p&gt;Objectively, this kind of &amp;ldquo;simple&amp;rdquo; project (one thing to do, easy to understand) works &lt;strong&gt;really well&lt;/strong&gt; with OpenCode + Opus. I think it&amp;rsquo;s because of this kind of small project that the hype is so strong around agentic development. You have an idea you were too lazy to dev, you test it, it works. &amp;ldquo;WOW&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;But I don&amp;rsquo;t want to downplay the work done either. The fact that a functional, clean tool, published on Krew and usable by anyone could emerge from a prompt launched at the beginning of a meeting is still pretty wild.&lt;/p&gt;
&lt;p&gt;A bit scary too, thinking that an insane number of micro tools are going to appear in the coming months, with probably uneven quality.&lt;/p&gt;
&lt;h2 id="the-project"&gt;The project
&lt;/h2&gt;&lt;p&gt;The code is available here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/zwindler/kubectl-debug-pvc" target="_blank" rel="noopener"
&gt;https://github.com/zwindler/kubectl-debug-pvc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Installation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Via Krew (recommended)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl krew install debug-pvc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Interactive usage (TUI)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl debug-pvc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Non-interactive usage&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl debug-pvc -n my-namespace -p my-pod-0 -v data:/debug/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you&amp;rsquo;ve ever struggled to inspect a PVC on a pod without a shell, give it a try. And if you find bugs, you can blame the LLM 😄.&lt;/p&gt;</description></item><item><title>Flannel and NetworkPolicies: how to add support with Cilium in CNI chaining mode</title><link>https://blog.zwindler.fr/en/2026/03/15/flannel-and-networkpolicies-how-to-add-support-with-cilium-in-cni-chaining-mode/</link><pubDate>Sun, 15 Mar 2026 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/03/15/flannel-and-networkpolicies-how-to-add-support-with-cilium-in-cni-chaining-mode/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/03/flannel-cilium-chaining.webp" alt="Featured image of post Flannel and NetworkPolicies: how to add support with Cilium in CNI chaining mode" /&gt;&lt;h2 id="flannel-flannel-flannel"&gt;Flannel, flannel, flannel&amp;hellip;
&lt;/h2&gt;&lt;p&gt;Flannel is a simple and popular CNI 😢.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the default CNI in k3s, the one half the kubeadm tutorials use, and you&amp;rsquo;ll find it in quite a few managed offerings too.&lt;/p&gt;
&lt;p&gt;OK, it&amp;rsquo;s simple, it routes packets between pods, it supports VXLAN and WireGuard, it takes 2 minutes to set up. What more could you ask for?&lt;/p&gt;
&lt;p&gt;Well, exactly. There&amp;rsquo;s one thing flannel does &lt;strong&gt;not&lt;/strong&gt; do: NetworkPolicies. (And that&amp;rsquo;s a big deal).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Flannel is focused on networking. For network policy, other projects such as Calico can be used.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Edit]&lt;/strong&gt; Contrary to what I wrote here, flannel actually does support NetworkPolicies and has for at least 2 years, via the reference implementation &lt;a class="link" href="https://github.com/kubernetes-sigs/kube-network-policies" target="_blank" rel="noopener"
&gt;kube-network-policies&lt;/a&gt; from the Kubernetes project. The option isn&amp;rsquo;t prominently featured in the README and is probably not more recommended for production, but it does exist. See the &lt;a class="link" href="https://github.com/flannel-io/flannel/blob/master/Documentation/netpol.md" target="_blank" rel="noopener"
&gt;official documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And the trap is that if you haven&amp;rsquo;t read that one little line in the &lt;strong&gt;README.md&lt;/strong&gt;, nothing tells you explicitly.&lt;/p&gt;
&lt;p&gt;You can perfectly well create &lt;code&gt;NetworkPolicy&lt;/code&gt; objects in your cluster, &lt;code&gt;kubectl apply&lt;/code&gt; won&amp;rsquo;t complain, &lt;code&gt;kubectl get netpol&lt;/code&gt; will happily list them. Except&amp;hellip; they&amp;rsquo;re not enforced. Traffic still flows. Your deny-all doesn&amp;rsquo;t deny anything at all.&lt;/p&gt;
&lt;h2 id="lets-verify-to-be-sure"&gt;Let&amp;rsquo;s verify to be sure
&lt;/h2&gt;&lt;p&gt;Before solving anything, let&amp;rsquo;s verify the problem exists. Starting from the prerequisite that we have a Kubernetes cluster with flannel as the CNI and everything is working, we&amp;rsquo;ll deploy two pods in two different namespaces: a client (curl) and a server (nginx).&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s check that the client can reach the server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n netpol-test-a client -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s --max-time &lt;span class="m"&gt;5&lt;/span&gt; http://server.netpol-test-b.svc.cluster.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We get the nginx welcome page. So far, so normal. Now, let&amp;rsquo;s apply a &lt;em&gt;deny-all&lt;/em&gt; NetworkPolicy on the server&amp;rsquo;s namespace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# 01-deny-all-ingress.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;networking.k8s.io/v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;NetworkPolicy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;deny-all-ingress&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;netpol-test-b&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;podSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;{}&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;policyTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;Ingress&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f 01-deny-all-ingress.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And let&amp;rsquo;s test again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n netpol-test-a client -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s --max-time &lt;span class="m"&gt;5&lt;/span&gt; http://server.netpol-test-b.svc.cluster.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Result: the nginx page still shows up.&lt;/strong&gt; The NetworkPolicy was created (&lt;code&gt;kubectl get netpol -n netpol-test-b&lt;/code&gt; shows it), but it&amp;rsquo;s not enforced. Traffic flows as if nothing happened.&lt;/p&gt;
&lt;p&gt;This is the expected behavior with flannel (by default). Flannel only does L3 routing (VXLAN or WireGuard overlay). It doesn&amp;rsquo;t implement a NetworkPolicy controller. The objects exist in etcd, but nobody translates them into filtering rules.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s clean up the policy before moving on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl delete -f 01-deny-all-ingress.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="alternatives-for-adding-support"&gt;Alternatives for adding support
&lt;/h2&gt;&lt;p&gt;For a long time I thought this was just the way it was. I still think it&amp;rsquo;s not a good choice for any production.&lt;/p&gt;
&lt;p&gt;BUT recently, I discovered that it was possible to chain CNIs within the same cluster, and thus have one CNI handling most tasks (here Flannel) and another one taking care of other tasks, such as enforcing NetworkPolicies.&lt;/p&gt;
&lt;p&gt;This is actually the principle behind Canal, which I knew by name but had never explored. In fact, it&amp;rsquo;s nothing more than a manifest that deploys Flannel as the main CNI with Calico for NetworkPolicy enforcement!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Canal was the name of Tigera and CoreOS&amp;rsquo;s project to integrate Calico and flannel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Note: &lt;a class="link" href="https://github.com/projectcalico/canal?tab=readme-ov-file" target="_blank" rel="noopener"
&gt;the GitHub project was archived in October 2025&lt;/a&gt; but in theory it should still work, if you can find the correct documentation (I couldn&amp;rsquo;t, the links are broken, but I didn&amp;rsquo;t really look that hard either).&lt;/p&gt;
&lt;p&gt;So you get the idea: we&amp;rsquo;re going to add a component that will &lt;strong&gt;watch&lt;/strong&gt; NetworkPolicy objects and translate them into effective filtering rules (iptables, eBPF, nftables&amp;hellip;), &lt;strong&gt;without touching the existing flannel setup&lt;/strong&gt;. This is called &amp;ldquo;CNI chaining&amp;rdquo; or &amp;ldquo;policy-only&amp;rdquo; mode.&lt;/p&gt;
&lt;p&gt;There are several solutions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Calico (Canal)&lt;/strong&gt;; Historically, the flannel + Calico combination is called &amp;ldquo;Canal&amp;rdquo;, which I just mentioned. &lt;strong&gt;But&lt;/strong&gt; the official Canal manifest bundles its own flannel in the same DaemonSet as calico-node. If your flannel is already installed and managed (by you, by an operator, by a provider&amp;hellip;), you probably don&amp;rsquo;t want to replace it. And the Tigera operator (the &amp;ldquo;official&amp;rdquo; Helm method) doesn&amp;rsquo;t support policy-only deployment on an existing flannel either. In short, it&amp;rsquo;s doable but requires some effort. Can&amp;rsquo;t be bothered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;kube-router&lt;/strong&gt;; kube-router can run in firewall-only mode (&lt;code&gt;--run-firewall=true&lt;/code&gt;) and only needs iptables/ipset. This is actually what k3s uses by default for NetworkPolicies. It&amp;rsquo;s the lightest solution (supposedly ~50 MB of RAM per node). Make sure your kernel has the &lt;code&gt;ip_set&lt;/code&gt; module, otherwise it won&amp;rsquo;t work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cilium in CNI chaining mode&lt;/strong&gt;; This is the solution I chose and that we&amp;rsquo;ll detail here. Cilium attaches to the veth interfaces created by flannel and adds its eBPF programs for policy enforcement. No dependency on iptables or ipset, and as a bonus we get Hubble for network observability.&lt;/p&gt;
&lt;h2 id="installing-cilium-in-cni-chaining-mode"&gt;Installing Cilium in CNI chaining mode
&lt;/h2&gt;&lt;h3 id="prerequisites"&gt;Prerequisites
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;A working Kubernetes cluster with flannel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;helm&lt;/code&gt; v3+&lt;/li&gt;
&lt;li&gt;A kernel &amp;gt;= 4.19 (ideally &amp;gt;= 5.10 for all eBPF features)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="retrieve-flannels-cni-configuration"&gt;Retrieve flannel&amp;rsquo;s CNI configuration
&lt;/h3&gt;&lt;p&gt;Cilium in chaining mode needs to know the existing CNI configuration. Let&amp;rsquo;s retrieve it from a node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl debug node/&lt;span class="k"&gt;$(&lt;/span&gt;kubectl get nodes -o &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{.items[0].metadata.name}&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -it --image&lt;span class="o"&gt;=&lt;/span&gt;busybox -- cat /host/etc/cni/net.d/10-flannel.conflist
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On my cluster, this gives:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;cbr0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;cniVersion&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.3.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;plugins&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;flannel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;delegate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hairpinMode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;isDefaultGateway&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;portmap&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;capabilities&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;portMappings&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Note the &lt;code&gt;name&lt;/code&gt; field&lt;/strong&gt; (here &lt;code&gt;cbr0&lt;/code&gt;). We&amp;rsquo;ll need it.&lt;/p&gt;
&lt;h3 id="create-the-chaining-configmap"&gt;Create the chaining ConfigMap
&lt;/h3&gt;&lt;p&gt;We&amp;rsquo;ll create a ConfigMap that takes the flannel config and adds the &lt;code&gt;cilium-cni&lt;/code&gt; plugin in chaining mode:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ConfigMap&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cni-configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;kube-system&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cni-config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|-&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;name&amp;#34;: &amp;#34;cbr0&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;cniVersion&amp;#34;: &amp;#34;0.3.1&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;plugins&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;type&amp;#34;: &amp;#34;flannel&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;delegate&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;hairpinMode&amp;#34;: true,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;isDefaultGateway&amp;#34;: true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;type&amp;#34;: &amp;#34;portmap&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;capabilities&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;portMappings&amp;#34;: true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;type&amp;#34;: &amp;#34;cilium-cni&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;#34;chaining-mode&amp;#34;: &amp;#34;generic-veth&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: the &lt;code&gt;name&lt;/code&gt; field must match your flannel &lt;em&gt;conflist&lt;/em&gt;. If yours is called &lt;code&gt;cni0&lt;/code&gt; or something else, adjust accordingly.&lt;/p&gt;
&lt;p&gt;Before applying, also verify that your CNI uses &lt;strong&gt;veth&lt;/strong&gt; interfaces (this is the default with flannel, but better safe than sorry). From a node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip -d link &lt;span class="p"&gt;|&lt;/span&gt; grep veth
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see veth-type interfaces corresponding to your pods, for example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;103: lxcb3901b7f9c02@if102: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; ...
veth addrgenmode eui64 numtxqueues 1 numrxqueues 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If that&amp;rsquo;s the case, Cilium&amp;rsquo;s &lt;code&gt;generic-veth&lt;/code&gt; mode will work. Let&amp;rsquo;s apply:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f cilium-cni-configmap.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="install-cilium-via-helm"&gt;Install Cilium via Helm
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm repo add cilium https://helm.cilium.io/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here are the values for chaining mode:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# cilium-values.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;cni&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;chainingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;generic-veth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;customConf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;configMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cni-configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;install&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;routingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;native&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;enableIPv4Masquerade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;enableIPv6Masquerade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hubble&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The important points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cni.chainingMode: generic-veth&lt;/code&gt;, this is chaining mode, Cilium attaches to existing veth interfaces&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cni.customConf: true&lt;/code&gt; + &lt;code&gt;cni.configMap&lt;/code&gt;, we provide our own CNI config&lt;/li&gt;
&lt;li&gt;&lt;code&gt;routingMode: native&lt;/code&gt;, flannel handles routing, not Cilium&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enableIPv4Masquerade: false&lt;/code&gt;, flannel handles masquerading&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hubble.enabled: true&lt;/code&gt;, network observability, the big bonus of Cilium&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;helm install cilium cilium/cilium --version 1.19.1 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --namespace kube-system &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -f cilium-values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wait for everything to be ready:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl rollout status daemonset/cilium -n kube-system --timeout&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="verification"&gt;Verification
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n kube-system ds/cilium -- cilium status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What we&amp;rsquo;re looking for:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Kubernetes: Ok 1.35 (v1.35.0) [linux/amd64]
CNI Chaining: generic-veth
Cilium: Ok 1.19.1
Hubble: Ok
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;CNI Chaining: generic-veth&lt;/code&gt; line confirms that Cilium is running in &lt;em&gt;chaining&lt;/em&gt; mode and isn&amp;rsquo;t replacing flannel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: pods that existed before Cilium was installed are not automatically managed by Cilium. You need to restart them so that Cilium can attach its eBPF programs. Remember to &lt;code&gt;kubectl rollout restart&lt;/code&gt; your test workloads (or recreate them).&lt;/p&gt;
&lt;h2 id="testing-networkpolicies"&gt;Testing NetworkPolicies
&lt;/h2&gt;&lt;p&gt;This is the moment of truth. Let&amp;rsquo;s re-apply our deny-all:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f 01-deny-all-ingress.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n netpol-test-a client -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s --max-time &lt;span class="m"&gt;5&lt;/span&gt; http://server.netpol-test-b.svc.cluster.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Result: timeout&lt;/strong&gt;! This time, the NetworkPolicy is properly enforced. Traffic is blocked.&lt;/p&gt;
&lt;p&gt;I followed up with the other classic NetworkPolicy scenarios, and everything works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Selective ingress allow by namespace&lt;/strong&gt;; by adding a policy that allows traffic from &lt;code&gt;netpol-test-a&lt;/code&gt; only, curl works from that namespace but remains blocked from &lt;code&gt;default&lt;/code&gt;. Namespace isolation works.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deny-all egress&lt;/strong&gt;; by blocking all outgoing traffic from the client, even DNS resolution is blocked (immediate timeout).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selective egress allow&lt;/strong&gt;; by allowing only DNS (port 53) and the server (port 80 in the &lt;code&gt;netpol-test-b&lt;/code&gt; namespace), curl to the server works but &lt;code&gt;curl http://example.com&lt;/code&gt; remains blocked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, ingress, egress, selective by namespace, everything works as expected.&lt;/p&gt;
&lt;h2 id="bonus-hubble-network-observability"&gt;Bonus: Hubble, network observability
&lt;/h2&gt;&lt;p&gt;This is for me the real advantage of Cilium over the alternatives. Hubble lets you see network flows and policy verdicts in real time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; -n kube-system ds/cilium -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; hubble observe --namespace netpol-test-b --last &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Mar 15 13:20:42.287: netpol-test-a/client:40066 (ID:9745) -&amp;gt;
netpol-test-b/server:80 (ID:22271)
policy-verdict:none ALLOWED (TCP Flags: SYN)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see the source pod, destination pod, port, Cilium identity, and policy verdict. When you&amp;rsquo;re debugging a NetworkPolicy that isn&amp;rsquo;t behaving as expected, this is incredibly useful.&lt;/p&gt;
&lt;h2 id="how-much-does-it-cost-in-resources"&gt;How much does it cost in resources?
&lt;/h2&gt;&lt;p&gt;On my cluster (3 nodes), here&amp;rsquo;s what Cilium consumes right after installation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Per node&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cilium agent&lt;/td&gt;
&lt;td&gt;yes (DaemonSet)&lt;/td&gt;
&lt;td&gt;~160 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cilium-envoy&lt;/td&gt;
&lt;td&gt;yes (DaemonSet)&lt;/td&gt;
&lt;td&gt;~22 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cilium-operator&lt;/td&gt;
&lt;td&gt;no (2 replicas)&lt;/td&gt;
&lt;td&gt;~42 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubble-relay&lt;/td&gt;
&lt;td&gt;no (1 replica)&lt;/td&gt;
&lt;td&gt;~16 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubble-ui&lt;/td&gt;
&lt;td&gt;no (1 replica)&lt;/td&gt;
&lt;td&gt;~21 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That&amp;rsquo;s roughly &lt;strong&gt;180 MB per node&lt;/strong&gt; for the agent + envoy. I don&amp;rsquo;t have a point of comparison with Calico or kube-router, but it seems acceptable to me, and being able to have full visibility into all network flows with Hubble more than justifies the overhead (in my opinion).&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;If you have no choice and have to work with flannel, and you want to plug the gaping security hole that is the lack of NetworkPolicy enforcement, know that it is possible to chain another CNI to handle it.&lt;/p&gt;
&lt;p&gt;Short of having it as CNI for everything, Cilium in CNI chaining mode (generic-veth) is a pretty nice solution to fill this gap. It doesn&amp;rsquo;t touch the existing flannel setup, it grafts onto it. And as a bonus, you get Hubble for network observability, which is genuinely valuable.&lt;/p&gt;</description></item><item><title>Podcasts &amp; Lives</title><link>https://blog.zwindler.fr/en/podcasts-lives/</link><pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.zwindler.fr/en/podcasts-lives/</guid><description/></item><item><title>Publications</title><link>https://blog.zwindler.fr/en/publications/</link><pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.zwindler.fr/en/publications/</guid><description/></item><item><title>GenAI and software development: lessons learned with PodSweeper</title><link>https://blog.zwindler.fr/en/2026/03/08/genai-and-software-development-lessons-learned-with-podsweeper/</link><pubDate>Sun, 08 Mar 2026 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/03/08/genai-and-software-development-lessons-learned-with-podsweeper/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/opencode.webp" alt="Featured image of post GenAI and software development: lessons learned with PodSweeper" /&gt;&lt;h2 id="genai-is-it-fantastic"&gt;GenAI, is it fantastic?
&lt;/h2&gt;&lt;p&gt;Quite a few people have shared their takes on GenAI for development in a very short time, so I realize it&amp;rsquo;s more than time I post this draft I started over two weeks ago 🙃.&lt;/p&gt;
&lt;p&gt;For work, I use AI assistants more and more to help me with my daily tasks. In 2024, it was mainly for automating tedious tasks (scripting stuff, making a painful list of repetitive tasks without coding it properly). Throughout 2025, I tested it several times for Ops work, and each time the results were mediocre, both for designing coherent, reliable and efficient infrastructure and for incident resolution assistance (see &lt;a class="link" href="https://blog.zwindler.fr/en/2025/08/15/ops-disparition-suite/#et-dans-linfra-" &gt;my article on the topic&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;In 2026, on that last point, I feel we still haven&amp;rsquo;t progressed, though I&amp;rsquo;ll admit there are specialized tools I haven&amp;rsquo;t tested enough yet, open source or not. I can mention &lt;a class="link" href="https://k8sgpt.ai/" target="_blank" rel="noopener"
&gt;k8sgpt&lt;/a&gt; and &lt;a class="link" href="https://github.com/HolmesGPT/holmesgpt" target="_blank" rel="noopener"
&gt;HolmesGPT&lt;/a&gt; for open source, and &lt;a class="link" href="https://www.anyshift.io/" target="_blank" rel="noopener"
&gt;Anyshift&lt;/a&gt; with its SRE agent for root cause detection and incident resolution.&lt;/p&gt;
&lt;p&gt;Note: fun fact, in his post &amp;ldquo;&lt;a class="link" href="https://alex.balmes.co/fr/blog/mon-positionnement-sur-l-intelligence-artificielle-generative#pour-les-ops" target="_blank" rel="noopener"
&gt;My position on Generative Artificial Intelligence&lt;/a&gt;&amp;rdquo; posted just before mine, Alex cites a long list of professions that will benefit from GenAI, and ops folks get nothing more than yet another argument to switch to Kubernetes 😭.&lt;/p&gt;
&lt;p&gt;On the other hand, I&amp;rsquo;ve started using GenAI to build several web projects from scratch (&lt;em&gt;vibe coding&lt;/em&gt;?), which I&amp;rsquo;ve already told you about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2025/12/02/jai-donn%C3%A9-1-heure-%C3%A0-des-agents-copilot-pour-migrer-un-site-de-bloggrify-%C3%A0-hugo/" &gt;Converting a blog from Bloggrify to Hugo (french only)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2026/02/09/101-ways-to-deploy-kubernetes-a-brand-new-ui-to-explore-118-solutions/" &gt;Creating a &amp;ldquo;pretty&amp;rdquo; website&lt;/a&gt; (way beyond my skills) to host the research I&amp;rsquo;ve done on the different ways to deploy Kubernetes&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2026/02/20/securite-headers-http-observatory-hugo/" &gt;Improving my website&amp;rsquo;s security. (french only)&lt;/a&gt; by automating modifications to the Hugo theme I use, to get the best score on Mozilla HTTP Observatory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In all 3 cases, the result is there. Or rather, it &lt;em&gt;seems&lt;/em&gt; to be for the layman that I am.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve never hidden that I have zero front-end skills, and maybe for a domain expert, what I did with AI is horrible (or not?). In any case, it can&amp;rsquo;t be worse than the UIs I made without it. Small example 😄:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/groroti.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="or-rather-gêneai-sorry-that-a-french-play-on-word-i-cant-translate"&gt;or rather: GêneAI (sorry that a french play on word I can&amp;rsquo;t translate)?
&lt;/h2&gt;&lt;p&gt;This is the kind of feedback you see from people who are &lt;em&gt;actually&lt;/em&gt; experts in a domain when they watch novices get excited. We have good examples with AI-generated videos: if you&amp;rsquo;re not paying attention, you think it&amp;rsquo;s incredible. But anyone with a slightly critical eye immediately sees &lt;strong&gt;BIG&lt;/strong&gt; consistency problems. Same goes for image generation, or music.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also true for code, and we have great examples of &lt;em&gt;vibe coded&lt;/em&gt; projects that are horrific spaghetti messes and cybersecurity sieves. In short, you get it: even though I&amp;rsquo;m a daily user, I&amp;rsquo;m not &amp;ldquo;amazified&amp;rdquo; (as my kids would say) by LLMs, even the best ones.&lt;/p&gt;
&lt;p&gt;Yet, I have several friends who swear by them, including people far more brilliant/intelligent (call it what you will) than me. So I thought &lt;em&gt;&amp;ldquo;OK, maybe I&amp;rsquo;m doing it wrong. Let&amp;rsquo;s try with the best there is&amp;rdquo;&lt;/em&gt;: a &amp;ldquo;Claude Code&amp;rdquo;-type IDE and an Opus model.&lt;/p&gt;
&lt;h2 id="the-editor"&gt;The editor
&lt;/h2&gt;&lt;p&gt;After a quick market survey, you realize the options are plentiful: Claude Code, OpenCode, Amp Code, &amp;hellip;&lt;/p&gt;
&lt;p&gt;The problem with Claude Code is the entry price. I&amp;rsquo;m not yet ready to spend 100 or 200€ per month for the use I have today. I don&amp;rsquo;t know if the 20€ plan would be enough. Beyond the rare personal side projects I mentioned above, I don&amp;rsquo;t code much. Most of my geeky activities are infrastructure: installing Kubernetes clusters and virtualization OSes. Stuff that&amp;rsquo;s hard to automate with an LLM.&lt;/p&gt;
&lt;p&gt;Pierre (mostly) recommended &lt;a class="link" href="https://ampcode.com/" target="_blank" rel="noopener"
&gt;Amp Code&lt;/a&gt; for personal use, mainly because it has a free tier with a certain number of tokens and if you&amp;rsquo;re not too greedy, it&amp;rsquo;s quite effective. I preferred to do things my own way and test &lt;strong&gt;OpenCode&lt;/strong&gt; instead, which has the advantage of being more flexible with LLM providers and token consumption. It&amp;rsquo;s even possible to use local models (free, therefore) or &lt;a class="link" href="https://opencode.ai/docs/fr/zen/" target="_blank" rel="noopener"
&gt;models included for free (at least for now) such as GLM 5 Free&lt;/a&gt;, which performs quite well among dev models (most importantly, it&amp;rsquo;s free&amp;hellip;).&lt;/p&gt;
&lt;p&gt;OK, we have the editor. Now, the code?&lt;/p&gt;
&lt;p&gt;To get a sense of the relevance of code generated by my favorite LLM (Sonnet and Opus, for a while now), I need a language &lt;strong&gt;and&lt;/strong&gt; a use case that I master, so I can tell it &lt;em&gt;&amp;ldquo;no, that&amp;rsquo;s absolute nonsense!?&amp;rdquo;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;A use case I master&amp;hellip; as you might guess, there will inevitably be some &lt;strong&gt;Kubernetes&lt;/strong&gt; in there.&lt;/p&gt;
&lt;p&gt;A language I master: I learned Go in 2022 thanks to a colleague at Deezer (thanks again Martial!) and I&amp;rsquo;ve published a few tools and made minor contributions. I wrote a large part of the &lt;a class="link" href="https://github.com/deezer/GroROTI" target="_blank" rel="noopener"
&gt;GroROTI&lt;/a&gt; tool and also started (but never finished) developing an RPG in Go, heavily inspired by Castle of the Winds, a game from my childhood: &lt;a class="link" href="https://github.com/zwindler/gocastle" target="_blank" rel="noopener"
&gt;gocastle&lt;/a&gt;. And I have professional-level proficiency (even if it&amp;rsquo;s not the core of my job) in my day-to-day work.&lt;/p&gt;
&lt;h2 id="after-the-context-the-project"&gt;After the context, the project
&lt;/h2&gt;&lt;p&gt;OK, we have the technical context: Kube + Go. Now we need the idea to implement. The project needs to be large enough for the test to be meaningful, fun enough that I want to spend personal time on it, but still somewhat useful so I&amp;rsquo;d want to talk about it and show progress. And above all, a project whose business logic stays within my reach: to honestly evaluate the quality of AI-generated code, I need to be able to read and critique it.&lt;/p&gt;
&lt;p&gt;And there, in my overflowing drawer of dumb ideas, I remembered this &amp;ldquo;pitch&amp;rdquo; from 2023-2024, which I&amp;rsquo;d left to rot for lack of time:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;PodSweeper&lt;/strong&gt;: the most complex, over-engineered and chaotic way to play Minesweeper.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/Wat8.webp"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="dont-leave-youll-see-its-fun-really"&gt;Don&amp;rsquo;t leave, you&amp;rsquo;ll see it&amp;rsquo;s fun. Really!
&lt;/h2&gt;&lt;p&gt;Instead of a visual grid where you click on cells hoping not to hit a &amp;ldquo;mine&amp;rdquo;, we have a virtual &amp;ldquo;grid&amp;rdquo; where each cell is a Pod and the click is a &lt;code&gt;kubectl delete&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Yes, it&amp;rsquo;s dumb. I like it a lot :).&lt;/p&gt;
&lt;p&gt;Beyond the deliberate trolling (over-engineering from hell) of this game, there&amp;rsquo;s actually an educational objective behind it.&lt;/p&gt;
&lt;p&gt;Really, there is.&lt;/p&gt;
&lt;p&gt;I designed the game as an &lt;strong&gt;introduction to Kubernetes security&lt;/strong&gt;, with difficulty levels to unlock, CTF-style.&lt;/p&gt;
&lt;p&gt;At the beginning, you can do &lt;strong&gt;everything&lt;/strong&gt;. You can of course play normally (delete a Pod, see if there are mines nearby) to get the hang of it. You can also automate actions (a script that &amp;ldquo;clicks a cell&amp;rdquo; at random, retrieves proximity hints for mines, clicks somewhere safe, &amp;hellip;).&lt;/p&gt;
&lt;p&gt;But you can also &lt;strong&gt;cheat&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s the whole point of the game. In the first difficulty levels, the game&amp;rsquo;s Kubernetes manifests are deliberately vulnerable. So you can win effortlessly, if you know where to look.&lt;/p&gt;
&lt;p&gt;However, levels quickly progress and restrictions come with them. Pretty soon, you reach production-grade best practices that prevent any &amp;ldquo;cheating&amp;rdquo;. And if you find unintended vulnerabilities in the game code itself&amp;hellip; well, that&amp;rsquo;s bonus 😈.&lt;/p&gt;
&lt;h2 id="the-initial-process"&gt;The initial process
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s probably a mistake, but I did the entire &lt;strong&gt;ideation phase with Gemini&lt;/strong&gt; before launching OpenCode. The experience would probably have been better (at least more representative) if I&amp;rsquo;d started directly with OpenCode.&lt;/p&gt;
&lt;p&gt;I dumped everything I had in mind, along with a big shapeless block of dozens of lines from my notes from when I had the idea in 2023, and asked it to produce a &lt;a class="link" href="https://github.com/zwindler/PodSweeper/blob/main/SPECIFICATION.md" target="_blank" rel="noopener"
&gt;SPECIFICATIONS.md&lt;/a&gt; file with all the important information, put back in order.&lt;/p&gt;
&lt;p&gt;Once the specs were written, I asked it to detail the different levels and the difficulties we would progressively add, in &lt;a class="link" href="https://github.com/zwindler/PodSweeper/blob/main/GAMEPLAY.md" target="_blank" rel="noopener"
&gt;GAMEPLAY.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, I asked it to break down the project into &amp;ldquo;issues&amp;rdquo; by priority, so I could get an MVP quickly: &lt;a class="link" href="https://github.com/zwindler/PodSweeper/blob/main/ISSUES_PRIORITY.md" target="_blank" rel="noopener"
&gt;ISSUES_PRIORITY.md&lt;/a&gt;. My idea was to give OpenCode the very detailed, finely sliced battle plan and let it run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First disappointment&lt;/strong&gt;: Gemini 3 Pro is pretty bad at this. The tasks were credible(-ish) but ordered randomly (task dependencies not respected). So I launched OpenCode for the first time in my repo and told it &lt;em&gt;&amp;ldquo;here&amp;rsquo;s the context, here&amp;rsquo;s what we came up with for tasks. What do you think?&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="the-good-opencode"&gt;The good (open)code&amp;hellip;
&lt;/h2&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/opencode.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Among the pretty nice things: OpenCode + Claude Opus immediately saw that the plan created by Gemini was flawed, and asked me questions and proposed a breakdown that was still imperfect but already more coherent. This is probably where the main value of these tools lies. They embed knowledge to guide software development and above all (ABOVE ALL) bring structure.&lt;/p&gt;
&lt;p&gt;Pretty quickly, we agree on a plan I like. OpenCode updates the documents and starts developing (scaffolding, creating pipelines, first empty binaries and Docker images). We have a project ready to develop in a snap, which is quite exciting, at first glance.&lt;/p&gt;
&lt;p&gt;LLM assistants are very eager to jump into code, this remains true with OpenCode+Opus. Even though we hadn&amp;rsquo;t finished discussing the plan and possible options, it was spontaneously proposing to move to code. That said, it was the same (or worse) with Gemini (to whom I had specifically said we wouldn&amp;rsquo;t be writing any code at all).&lt;/p&gt;
&lt;p&gt;Very quickly, file creations pile up. I struggle to keep up and at each pause (when the LLM has finished a task and asks if it can move to the next), I spend long minutes reading what the LLM produced. It&amp;rsquo;s both exhilarating and exhausting (re-reading all that is hard on my rusty brain).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The raw speed is impressive.&lt;/strong&gt; In 3 sessions of 1 to 2 hours, I have a working MVP. I understand why people regularly talk about 10x engineering when discussing code generation with recent LLMs. If we&amp;rsquo;re not talking about raw code generation (which doesn&amp;rsquo;t really make sense), roughly speaking, functional code is generated 3x to 10x faster than what I could have done alone. Features that end to end would have taken me hours to implement (grid generation, reveal logic, game state management) land in minutes. You&amp;rsquo;ve read it elsewhere, I&amp;rsquo;m saying the same thing — the bottleneck is no longer the code I type: it&amp;rsquo;s the code I have to review.&lt;/p&gt;
&lt;p&gt;From what I&amp;rsquo;ve read, the code is correct, particularly in the first iterations, a bit less so after a while. But when I say &amp;ldquo;correct&amp;rdquo;, I might be underselling it: the code produced is &lt;strong&gt;idiomatic Go&lt;/strong&gt;. Good package structure, naming conventions respected, error handling in Go style (when it&amp;rsquo;s there&amp;hellip;), appropriate use of interfaces. This isn&amp;rsquo;t just code that compiles: it&amp;rsquo;s code a Go reviewer wouldn&amp;rsquo;t reject outright. We&amp;rsquo;re aiming for an MVP so the business logic remains very simple, but the foundation is clean.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything is tested&lt;/strong&gt;, very quickly the project has perfect coverage and 100+ tests while we&amp;rsquo;re not even doing anything yet. If this were human-written code, I wouldn&amp;rsquo;t know if that&amp;rsquo;s a good thing. We&amp;rsquo;re absolutely not doing TDD, a lot of code tests useless things. In the case of LLM-generated code, it&amp;rsquo;s probably for the best, because the LLM tends to introduce regressions fairly regularly (we&amp;rsquo;ll come back to that). So it&amp;rsquo;s interesting here, because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;it costs almost nothing in dev time (it&amp;rsquo;s so fast to generate)&lt;/li&gt;
&lt;li&gt;it allows the LLM to realize its code introduced a regression, and fix it on its own, without waiting.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="-and-the-bad-opencode"&gt;&amp;hellip; and the bad (open)code
&lt;/h2&gt;&lt;p&gt;On the tool and model side first, a few &lt;strong&gt;irritants&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;By default, OpenCode doesn&amp;rsquo;t encourage the LLM to run &lt;code&gt;golangci-lint&lt;/code&gt; on every commit. You fix this with a pre-commit hook and/or the famous AGENTS.md / skills, but it SHOULD have been part of the basic kit for a golang project (tests are there&amp;hellip;).&lt;/li&gt;
&lt;li&gt;The LLM (Claude Opus on OpenCode, but this is a common bias even outside of it) &lt;strong&gt;loves&lt;/strong&gt; software versions that existed at the time of its training. You have to constantly remind it that the versions it suggests are outdated. This is true for everything: Go code, dependencies, GitHub Actions versions.&lt;/li&gt;
&lt;li&gt;You very often hit the 100k token limit. You waste time &amp;ldquo;compacting&amp;rdquo; and lose precision. Typically: the LLM asks if it can &lt;code&gt;git commit&lt;/code&gt; changes, the context compacts before I answer, and it commits without my permission while re-unreeling the context. For code this low-stakes, it&amp;rsquo;s fine. But in prod, that would be a real problem.&lt;/li&gt;
&lt;li&gt;The LLM is often amnesiac: it forgets it has access to a kind cluster for real testing (even though it did it just before the previous compaction, for example).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the &lt;strong&gt;code quality&lt;/strong&gt; side, it&amp;rsquo;s more concerning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lax error handling.&lt;/strong&gt; Some errors that should have returned a FATAL (controller that can&amp;rsquo;t initialize properly!) are simply logged as ERROR. You end up shipping versions that fail silently. Again, this can probably be fine-tuned with an AGENTS.md.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Race conditions.&lt;/strong&gt; I ran into race conditions fairly quickly, pretty silly ones. In my case, pods stuck in terminating impacted the next game (poor graceful shutdown handling). Opus went into a loop without understanding the root cause. I had to stop it and specify that it should never start a new game without making sure the previous one was finished.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code generation outside the lines.&lt;/strong&gt; Sometimes, without warning, the LLM adds a feature that wasn&amp;rsquo;t requested, is useless, or even contradicts the previously written SPECIFICATION. You have to slap it back into line.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once put back on track, the LLM rolls again, but these episodes clearly show that for concurrent code or outside of ultra-strict user stories (which goes beyond OpenCode&amp;rsquo;s default workflow), human supervision remains essential.&lt;/p&gt;
&lt;p&gt;People will tell me I&amp;rsquo;m a beginner with these tools, and I could have avoided some pitfalls by better configuring my environment (AGENTS.md, pre-commit hooks, etc.). That&amp;rsquo;s true.&lt;/p&gt;
&lt;p&gt;But that&amp;rsquo;s exactly where the shoe pinches: one of the hyped promises of GenAI applied to development is to enable non-developers (or beginners) to produce quality software at high speed, so they can ship complete software. If getting the most out of it requires being an experienced developer who knows how to configure a complex environment (with the specifics of these AI tools), anticipate race conditions and review concurrent code&amp;hellip; we haven&amp;rsquo;t democratized development, we&amp;rsquo;ve just given another tool to people who already knew how to code. My profile (ops who codes, not a pure dev) was precisely the target audience of this promise. And today, the math doesn&amp;rsquo;t check out.&lt;/p&gt;
&lt;h2 id="where-am-i-at"&gt;Where am I at?
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been talking about PodSweeper for a while now. But where is this project?&lt;/p&gt;
&lt;p&gt;After 3 sessions of 1 to 2 hours max, not counting ideation, I have a working MVP. As I said earlier, the productivity gain is undeniable, for someone who isn&amp;rsquo;t a &amp;ldquo;developer&amp;rdquo; by trade.&lt;/p&gt;
&lt;p&gt;The code is probably &amp;ldquo;overall&amp;rdquo; better quality than the Go code I could have written, &lt;strong&gt;but&lt;/strong&gt; the LLM lets gaping holes through in the business logic (simply because it doesn&amp;rsquo;t think, while I do. Well, normally). Hard on trust&amp;hellip; You really have to remain very wary of everything it produces.&lt;/p&gt;
&lt;p&gt;The code is available at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/zwindler/PodSweeper/tree/main" target="_blank" rel="noopener"
&gt;https://github.com/zwindler/PodSweeper/tree/main&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/03/podsweeper.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;You can go check out the code to form your own opinion. If you&amp;rsquo;re not afraid of spoilers, you can read the &lt;a class="link" href="https://github.com/zwindler/PodSweeper/blob/main/SPECIFICATION.md" target="_blank" rel="noopener"
&gt;SPECIFICATION.md&lt;/a&gt; (spoilers), or even &lt;a class="link" href="https://github.com/zwindler/PodSweeper/blob/main/GAMEPLAY.md" target="_blank" rel="noopener"
&gt;GAMEPLAY.md&lt;/a&gt; (I detail all the levels there so it&amp;rsquo;s mega spoilers).&lt;/p&gt;
&lt;p&gt;You can (normally) play the first difficulty levels. It&amp;rsquo;s still fairly basic, if you&amp;rsquo;ve already used &lt;code&gt;kubectl&lt;/code&gt; you should find the solution very quickly. My goal is to add levels over time, I&amp;rsquo;ve already imagined 10, some quite devious 😈 and others might come later.&lt;/p&gt;
&lt;p&gt;The code is under MPL v2.0 because it&amp;rsquo;s a copyleft license I like, &lt;a class="link" href="https://www.tldrlegal.com/license/mozilla-public-license-2-0-mpl-2" target="_blank" rel="noopener"
&gt;since it&amp;rsquo;s both not very restrictive, OSI compliant and still requires contributing changes back if you make them&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There you go :) if you also find it fun, feel free to test and/or give me feedback.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ kubectl apply -k https://github.com/zwindler/podsweeper//deploy/base?ref&lt;span class="o"&gt;=&lt;/span&gt;v0.1.4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;namespace/podsweeper-game created
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; --for&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready pod -l app.kubernetes.io/name&lt;span class="o"&gt;=&lt;/span&gt;podsweeper &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -n podsweeper-game --timeout&lt;span class="o"&gt;=&lt;/span&gt;60s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pod/gamemaster-54f4dddcc4-6m88z condition met
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Have fun!&lt;/p&gt;</description></item><item><title>Kyverno killed my API Server. Again.</title><link>https://blog.zwindler.fr/en/2026/02/26/kyverno-killed-my-api-server.-again./</link><pubDate>Thu, 26 Feb 2026 08:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/02/26/kyverno-killed-my-api-server.-again./</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/0_days_without_kyverno.webp" alt="Featured image of post Kyverno killed my API Server. Again." /&gt;&lt;h2 id="the-revenge-strikes-back"&gt;The revenge strikes back
&lt;/h2&gt;&lt;p&gt;You may have read &lt;a class="link" href="https://blog.zwindler.fr/en/2023/11/30/kubernetes-error-etcdserver-mvcc-database-space-exceeded/" &gt;my previous article about etcd 3 years ago&lt;/a&gt;. If you remember correctly, the crash was etcd, but the real culprit was Kyverno. I love Kyverno. It&amp;rsquo;s really a piece of software I&amp;rsquo;m very fond of. Mainly because it&amp;rsquo;s incredibly powerful. I even wrote &lt;a class="link" href="https://blog.zwindler.fr/2022/08/01/vos-politiques-de-conformite-sur-kubernetes-avec-kyverno/" &gt;an introductory article&lt;/a&gt; and &lt;a class="link" href="https://blog.zwindler.fr/2022/09/05/vos-politiques-de-conformite-sur-kubernetes-avec-kyverno-part2/" &gt;a second one that goes deeper&lt;/a&gt; on the topic (both in french, though).&lt;/p&gt;
&lt;p&gt;But the sheer number of incidents and weird side effects it causes. Mamamia&amp;hellip; This isn&amp;rsquo;t the first incident of the year I&amp;rsquo;ve had with Kyverno (yes, we&amp;rsquo;re in February) but since this one is entertaining, I&amp;rsquo;m sharing it with you.&lt;/p&gt;
&lt;p&gt;During a routine maintenance operation to upgrade a Kubernetes cluster to version &lt;strong&gt;1.34&lt;/strong&gt; (from 1.32), we ended up facing the dreaded scenario for any kube admin: a completely unreachable API Server after restarting the Control Plane nodes.&lt;/p&gt;
&lt;p&gt;What initially looked like a typical network error turned out to be a subtle &lt;strong&gt;deadlock&lt;/strong&gt; between new native Kubernetes networking features and our dear Kyverno 😘.&lt;/p&gt;
&lt;p&gt;Spoiler: it wasn&amp;rsquo;t a network issue. It&amp;rsquo;s never a network issue. Well, sometimes it is. But not this time.&lt;/p&gt;
&lt;h2 id="the-upgrade-that-starts-well"&gt;The upgrade that starts well
&lt;/h2&gt;&lt;p&gt;Alright, a Kubernetes upgrade has become pretty routine at this point. We do it regularly, we have our procedures, we&amp;rsquo;re pros (I swear). We jump from 1.32 to 1.34 in a single commit, skipping the hop through 1.33.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;YOLO.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the technical context I&amp;rsquo;m talking about, everything is managed as code. From machine provisioning all the way to Talos deployment, including MachineConfigs (the CustomResources to modify&amp;hellip; well, the machine).&lt;/p&gt;
&lt;p&gt;For more details, see &lt;a class="link" href="https://docs.siderolabs.com/talos/v1.12/reference/configuration/v1alpha1/config#machineconfig" target="_blank" rel="noopener"
&gt;the Talos documentation on Machine Configs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The first cluster we test has only one control plane (don&amp;rsquo;t ask me why, it probably wouldn&amp;rsquo;t have changed anything). Talos restarts the API Server with the new version and then&amp;hellip; nothing.&lt;/p&gt;
&lt;p&gt;The &amp;ldquo;weird&amp;rdquo; API Server logs (technical term) speak for themselves:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;I0224 15:32:50.979280 1 default_servicecidr_controller.go:166] Creating default ServiceCIDR with CIDRs: [10.1.0.0/20]
W0224 15:32:50.984784 1 dispatcher.go:225] rejected by webhook &amp;#34;validate.kyverno.svc-fail&amp;#34;:
admission webhook &amp;#34;validate.kyverno.svc-fail&amp;#34; denied the request:
Get &amp;#34;https://10.1.0.1:443/api&amp;#34;: dial tcp 10.1.0.1:443: connect: operation not permitted
I0224 15:32:50.985342 1 event.go:389] &amp;#34;Event occurred&amp;#34; kind=&amp;#34;ServiceCIDR&amp;#34;
apiVersion=&amp;#34;networking.k8s.io/v1&amp;#34; type=&amp;#34;Warning&amp;#34;
reason=&amp;#34;KubernetesDefaultServiceCIDRError&amp;#34;
message=&amp;#34;The default ServiceCIDR can not be created&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;😬😬😬&lt;/p&gt;
&lt;h2 id="the-root-cause-a-magnificent-vicious-circle"&gt;The root cause: a magnificent vicious circle
&lt;/h2&gt;&lt;p&gt;After investigation, we discovered that the incident was the result of a collision between a Kubernetes core evolution and our Kyverno configuration. A textbook deadlock case.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s break down the mechanism:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The new &lt;code&gt;ServiceCIDR&lt;/code&gt; Kind&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In recent versions (v1.33+), Kubernetes migrates service IP range management to dedicated objects named &lt;code&gt;ServiceCIDR&lt;/code&gt;. On the first boot after the upgrade, the API Server automatically tries to create the default object (e.g., &lt;code&gt;10.1.0.0/20&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;For the curious, &lt;a class="link" href="https://github.com/kubernetes/enhancements/issues/1880" target="_blank" rel="noopener"
&gt;KEP-1880&lt;/a&gt; and the &lt;a class="link" href="https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/service-cidr-v1/" target="_blank" rel="noopener"
&gt;official ServiceCIDR documentation&lt;/a&gt; detail this evolution.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s new, it&amp;rsquo;s clean, it&amp;rsquo;s well designed. Except that&amp;hellip;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Interception by the Kyverno Webhook&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kyverno, configured with &lt;code&gt;failurePolicy: Fail&lt;/code&gt; (because we&amp;rsquo;re serious people who don&amp;rsquo;t let just anything through in prod), is set up to intercept resource creations to validate them, and &lt;strong&gt;fail the call if Kyverno doesn&amp;rsquo;t respond&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Including the &lt;code&gt;ServiceCIDR&lt;/code&gt; freshly created by the API Server itself.&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Deadlock&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And this is where it gets beautiful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The API Server pauses the &lt;code&gt;ServiceCIDR&lt;/code&gt; creation waiting for Kyverno&amp;rsquo;s &amp;ldquo;OK&amp;rdquo;&lt;/li&gt;
&lt;li&gt;To contact the Kyverno service, the API Server needs to route the request through the Kubernetes service IP (typically &lt;code&gt;10.1.0.1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;But&lt;/strong&gt; the network layer (service routing) can&amp;rsquo;t initialize until the &lt;code&gt;ServiceCIDR&lt;/code&gt; object is validated and created&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s the chicken and the egg, &amp;ldquo;I locked my keys inside the car&amp;rdquo; edition.&lt;/p&gt;
&lt;p&gt;PTSD. Yes, that actually happened to me. In the desert. With no cell service.&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Profit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The API Server times out or returns a &lt;code&gt;connect: operation not permitted&lt;/code&gt; error when trying to reach the webhook, blocking its own initialization. CrashLoopBackOff on the API Server. :D&lt;/p&gt;
&lt;h2 id="breaking-out-of-the-deadlock"&gt;Breaking out of the deadlock
&lt;/h2&gt;&lt;p&gt;To escape this deadlock, you need to temporarily bypass the admission layer. Easy, right?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &amp;ldquo;usual&amp;rdquo; workaround: useless&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Deadlocks with Kyverno, we&amp;rsquo;re used to them at this point. Normally, since &lt;code&gt;kube-system&lt;/code&gt; is ignored, you can simply connect with a break-glass kubeconfig (we normally use OIDC) that has the cluster-admin cluster role and delete the Kyverno validating webhooks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl delete validatingwebhookconfiguration kyverno-resource-validating-webhook-cfg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Except here&lt;/strong&gt;, the API Server won&amp;rsquo;t even start. My &lt;code&gt;kubectl&lt;/code&gt; isn&amp;rsquo;t going to work, obviously!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The real workaround: disable webhooks at boot&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The solution we chose was to modify the API Server configuration to temporarily disable validation webhooks at startup. My esteemed colleague Maxime hot-edited the machine config (using break-glass &lt;code&gt;talosctl&lt;/code&gt; access) &lt;a class="link" href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#how-do-i-turn-off-an-admission-controller" target="_blank" rel="noopener"
&gt;to add the following flag&lt;/a&gt; directly in the API server&amp;rsquo;s &lt;code&gt;extraArgs&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;--disable-admission-plugins=ValidatingAdmissionWebhook
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For those unfamiliar with admission control in Kubernetes, just know that there&amp;rsquo;s a list of &amp;ldquo;default&amp;rdquo; plugins but everything can be toggled off. I might do a deep dive on Kubernetes admission control someday, it&amp;rsquo;s fascinating ;).&lt;/p&gt;
&lt;p&gt;With this flag, the API Server can finally create its &lt;code&gt;ServiceCIDR&lt;/code&gt; objects without asking anyone for permission (completely bypassing all validation mechanisms that Kyverno or similar tools &lt;em&gt;enforce&lt;/em&gt;), the network initializes, Kyverno starts, and then you can remove the flag and restart cleanly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &amp;ldquo;funny&amp;rdquo; option we didn&amp;rsquo;t try&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Personally, I thought it would be hilarious to go directly into the etcd database and delete the webhook key causing the issue (also through &lt;code&gt;talosctl&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Example via etcdctl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;etcdctl del /registry/admissionregistration.k8s.io/validatingwebhookconfigurations/kyverno-resource-validating-webhook-cfg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My colleagues were less enthusiastic: &amp;ldquo;Yeah but you know, if we break etcd it&amp;rsquo;s gonna be painful&amp;rdquo;. We played it safe with the flag. I&amp;rsquo;m deeply disappointed we didn&amp;rsquo;t try 😂.&lt;/p&gt;
&lt;h2 id="the-permanent-fix-matchconditions"&gt;The permanent fix: MatchConditions
&lt;/h2&gt;&lt;p&gt;OK, now that the cluster is back up, how do we make sure this doesn&amp;rsquo;t happen again on the next upgrade?&lt;/p&gt;
&lt;p&gt;The clean solution is to use &lt;code&gt;matchConditions&lt;/code&gt; (introduced in Kubernetes 1.27) on the &lt;code&gt;ValidatingWebhookConfiguration&lt;/code&gt;. This allows you to exclude critical network bootstrap resources &lt;strong&gt;before&lt;/strong&gt; the request even attempts to leave the API Server toward the Kyverno pod.&lt;/p&gt;
&lt;p&gt;See &lt;a class="link" href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchconditions" target="_blank" rel="noopener"
&gt;the official documentation on matchConditions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We were already using this option to throttle the sometimes excessive Kyverno traffic (if you manage Kyverno, you know what I&amp;rsquo;m talking about) on a number of events (we&amp;rsquo;d overwhelm the API server or Kyverno, in CPU or RAM, depending on the case). We just had to add exclusions for the new types:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Exclude network bootstrap resources to prevent the deadlock&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;matchConditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;exclude-ServiceCIDR&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!(request.kind.kind == &amp;#34;ServiceCIDR&amp;#34;)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;exclude-IPAddress&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!(request.kind.kind == &amp;#34;IPAddress&amp;#34;)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this, when the API Server creates a &lt;code&gt;ServiceCIDR&lt;/code&gt; at boot, the request no longer goes through the Kyverno webhook. No circular dependency, no deadlock, everyone&amp;rsquo;s happy.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;As the current French president would say about something that was painfully predictable:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;who could have predicted this?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK fine, all we had to do was read the Kubernetes 1.33 release notes. That said, we have a staging cluster, that&amp;rsquo;s what it&amp;rsquo;s for. We broke staging, no big deal.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/bigdeal.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Context: this is the &amp;ldquo;Big deal&amp;rdquo; TV Game Mascot&lt;/p&gt;
&lt;p&gt;Maybe we&amp;rsquo;ll actually read them next time?&lt;/p&gt;</description></item><item><title>101 ways to deploy Kubernetes: a brand new UI to explore 118+ solutions</title><link>https://blog.zwindler.fr/en/2026/02/09/101-ways-to-deploy-kubernetes-a-brand-new-ui-to-explore-118-solutions/</link><pubDate>Mon, 09 Feb 2026 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/02/09/101-ways-to-deploy-kubernetes-a-brand-new-ui-to-explore-118-solutions/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/02/101-kubernetes-ui-screenshot.webp" alt="Featured image of post 101 ways to deploy Kubernetes: a brand new UI to explore 118+ solutions" /&gt;&lt;h2 id="from-google-sheet-to-a-real-web-application"&gt;From Google Sheet to a real web application
&lt;/h2&gt;&lt;p&gt;You might remember my previous posts about this project: first a &lt;a class="link" href="https://blog.zwindler.fr/en/2025/11/02/93-ways-to-deploy-kubernetes-ive-cataloged-almost-all-existing-methods/" target="_blank" rel="noopener"
&gt;simple Google Sheet with 93 methods&lt;/a&gt;, then a &lt;a class="link" href="https://blog.zwindler.fr/en/2026/01/04/101-ways-to-deploy-kubernetes-v2/" target="_blank" rel="noopener"
&gt;GitHub repository with over 100 entries&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Today, I can present the latest iteration of this project: &lt;strong&gt;a real web interface&lt;/strong&gt; to explore all these solutions!&lt;/p&gt;
&lt;p&gt;👉 &lt;a class="link" href="https://zwindler.github.io/101-ways-to-deploy-kubernetes/" target="_blank" rel="noopener"
&gt;101-ways-to-deploy-kubernetes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/101-kubernetes-ui-screenshot.avif"
loading="lazy"
alt="Web interface for the 101 ways to deploy Kubernetes project"
&gt;&lt;/p&gt;
&lt;h2 id="why-a-ui"&gt;Why a UI?
&lt;/h2&gt;&lt;p&gt;The Markdown table on GitHub was already better than the Google Sheet for collaboration, but barely. Hard to parse, hard to add columns without it becoming a mess (it already was, haha), and above all, incredibly UGLY.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/01/101-kubernetes-v2-screenshot.avif"
loading="lazy"
alt="Old Markdown table on GitHub, hard to read"
&gt;&lt;/p&gt;
&lt;p&gt;I therefore decided to turn all of this into a modern and intuitive interface, with the help of an LLM.&lt;/p&gt;
&lt;h2 id="tech-stack-astro--tailwind"&gt;Tech stack: Astro + Tailwind
&lt;/h2&gt;&lt;p&gt;For this project, I chose a simple but effective stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://astro.build/" target="_blank" rel="noopener"
&gt;Astro&lt;/a&gt;&lt;/strong&gt;: a modern framework that generates ultra-fast static sites&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://tailwindcss.com/" target="_blank" rel="noopener"
&gt;Tailwind CSS&lt;/a&gt;&lt;/strong&gt;: for hassle-free responsive design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result? A lightweight, fairly fast site that works on both &lt;strong&gt;desktop&lt;/strong&gt; and &lt;strong&gt;mobile&lt;/strong&gt; (though the desktop experience remains more comfortable given the amount of data).&lt;/p&gt;
&lt;h2 id="features"&gt;Features
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Cards for each solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No more spreadsheet-style table worthy of a backend dev (no, worse, a kube engineer&amp;hellip;)! Each tool now has its own animated &amp;ldquo;card&amp;rdquo; with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The project logo&lt;/li&gt;
&lt;li&gt;The license type (OSS or proprietary)&lt;/li&gt;
&lt;li&gt;The GitHub star count&lt;/li&gt;
&lt;li&gt;Direct links to the project and third-party resources (independent blogs, experience reports, tutorials&amp;hellip;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/02/101-kubernetes-zoom.avif"
loading="lazy"
alt="Detailed view of a Kubernetes solution card with logo, license, and GitHub stars"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Powerful filters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Looking only for open source solutions? Tools for local development? Management platforms?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt; filters make navigation easy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Desktop (local development)&lt;/li&gt;
&lt;li&gt;Managed (cloud offerings)&lt;/li&gt;
&lt;li&gt;Self-hosted (on-premise automation)&lt;/li&gt;
&lt;li&gt;Infra As Code&lt;/li&gt;
&lt;li&gt;Kubernetes OS (specialized operating systems)&lt;/li&gt;
&lt;li&gt;Management Platform&lt;/li&gt;
&lt;li&gt;Kubernetes in Kubernetes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also filter by &lt;strong&gt;status&lt;/strong&gt; (active, abandoned) or show only &lt;strong&gt;open source&lt;/strong&gt; or &lt;strong&gt;production ready&lt;/strong&gt; solutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A search bar&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Know what you&amp;rsquo;re looking for? Just type the name in the search bar to find the solution instantly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tags for refinement&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Beyond categories, tags help quickly identify underlying technologies (kubeadm, k3s, k0s&amp;hellip;).&lt;/p&gt;
&lt;h2 id="did-you-know-at-least-18-tools-use-kubeadm"&gt;Did you know? At least 18 tools use kubeadm!
&lt;/h2&gt;&lt;p&gt;While compiling all this data, I discovered something fascinating: &lt;strong&gt;at least 18 tools&lt;/strong&gt; use &lt;code&gt;kubeadm&lt;/code&gt; as a backend to deploy Kubernetes! 🤯&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s not even counting the managed Kubernetes offerings from cloud providers!&lt;/p&gt;
&lt;p&gt;This is exactly the kind of information you can now visualize instantly thanks to this new interface.&lt;/p&gt;
&lt;h2 id="a-collaborative-project"&gt;A collaborative project
&lt;/h2&gt;&lt;p&gt;The project remains &lt;strong&gt;100% open source&lt;/strong&gt; and collaborative. The data is still stored in the GitHub repository, and the UI is automatically generated from that data (I even added PR previews).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is a tool or provider missing?&lt;/strong&gt; Spotted a bug? Feel free to &lt;a class="link" href="https://github.com/zwindler/101-ways-to-deploy-kubernetes/issues" target="_blank" rel="noopener"
&gt;open an issue&lt;/a&gt; or submit a Pull Request!&lt;/p&gt;
&lt;p&gt;The project now lists &lt;strong&gt;118 solutions&lt;/strong&gt; (and I know there are certainly more missing), each with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Up-to-date links&lt;/li&gt;
&lt;li&gt;Project status&lt;/li&gt;
&lt;li&gt;External references (tutorials, experience reports&amp;hellip;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="try-it-comment-share"&gt;Try it, comment, share
&lt;/h2&gt;&lt;p&gt;Head over to &lt;a class="link" href="https://zwindler.github.io/101-ways-to-deploy-kubernetes/" target="_blank" rel="noopener"
&gt;zwindler.github.io/101-ways-to-deploy-kubernetes&lt;/a&gt; to explore all these solutions!&lt;/p&gt;
&lt;p&gt;OK, this is a shameless &amp;ldquo;call to action&amp;rdquo; like you see on every social network. Fair enough.&lt;/p&gt;
&lt;p&gt;However, I can&amp;rsquo;t know if this is useful (or not) if you don&amp;rsquo;t tell me. I can leave it as is (it&amp;rsquo;s not a big deal, I have plenty of other projects waiting for my spare time) or keep it alive, if you like it / find it useful.&lt;/p&gt;
&lt;p&gt;And if you do find this project useful, please:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Star&lt;/strong&gt; the project on &lt;a class="link" href="https://github.com/zwindler/101-ways-to-deploy-kubernetes" target="_blank" rel="noopener"
&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Share&lt;/strong&gt; it with your colleagues in the Cloud Native community&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contribute&lt;/strong&gt; by adding missing tools or fixing errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks in advance! 🙏&lt;/p&gt;</description></item><item><title>Installing a Cluster HAT with Raspberry Pi 5 and Pi Zero</title><link>https://blog.zwindler.fr/en/2026/01/27/installing-a-cluster-hat-with-raspberry-pi-5-and-pi-zero/</link><pubDate>Tue, 27 Jan 2026 18:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/01/27/installing-a-cluster-hat-with-raspberry-pi-5-and-pi-zero/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/01/pi_cluster_hat_with_4_pi_zero.webp" alt="Featured image of post Installing a Cluster HAT with Raspberry Pi 5 and Pi Zero" /&gt;&lt;h2 id="introduction-what-is-a-cluster-hat"&gt;Introduction, What Is a Cluster HAT?
&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;What have you found for us this time???&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You&amp;rsquo;ve seen me play with Raspberry Pis and install Kubernetes on them, &lt;a class="link" href="https://blog.zwindler.fr/en/2025/12/23/installing-alpine-linux-in-headless-mode-on-raspberry-pi/" &gt;or just simply Alpine Linux&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So what &lt;em&gt;on earth&lt;/em&gt; (yes, &amp;ldquo;on earth,&amp;rdquo; let&amp;rsquo;s not mince words) are we talking about today???&lt;/p&gt;
&lt;p&gt;While searching the depths of the Internet to see if it was possible to install a Kubernetes cluster on Raspberry Pi Zeros, I came across 3 different people who had failed at this task:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.andreveiga.dev/rpizero-k3s-cluster/" target="_blank" rel="noopener"
&gt;Setting up a Kubernetes cluster using Raspberry Pi Zero 2 W&amp;rsquo;s&lt;/a&gt; and &lt;a class="link" href="https://www.reddit.com/r/homelab/comments/urz31s/kubernetes_cluster_on_raspberry_pi_zero_2_ws_sort/?tl=fr" target="_blank" rel="noopener"
&gt;the associated reddit post with a photo&lt;/a&gt; (important for the rest of the article)&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/abelperezok/kubernetes-raspberry-pi-cluster-hat" target="_blank" rel="noopener"
&gt;github.com/abelperezok/kubernetes-raspberry-pi-cluster-hat&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.reddit.com/r/homelab/comments/qta8yd/a_tiny_cluster_based_on_4x_raspberry_pi_zero_2_w/" target="_blank" rel="noopener"
&gt;A tiny cluster based on 4x Raspberry Pi Zero 2 W&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In all 3 cases, people tried to install a cluster of RPi zeros, mounted on some kind of additional board I didn&amp;rsquo;t know existed: the Cluster HAT&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/01/ClusterHAT-v2-in-use-sm.avif"
loading="lazy"
alt="Cluster HAT v2 in use with Raspberry Pi Zeros"
&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://clusterhat.com/" target="_blank" rel="noopener"
&gt;Official Cluster HAT website clusterhat.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The Cluster HAT (Hardware Attached on Top) which interfaces a (Controller) Raspberry Pi A+/B+/2/3 with 4 Raspberry Pi Zeros configured to use USB Gadget mode is an ideal tool for teaching, testing or simulating small scale clusters.&lt;/p&gt;
&lt;p&gt;The Cluster HAT can be used with any mix of Pi Zero 1.2, Pi Zero 1.3 and Pi Zero W.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;USB Gadget Mode: Ethernet and Serial Console.&lt;/li&gt;
&lt;li&gt;Onboard 4 port USB 2.0 hub.&lt;/li&gt;
&lt;li&gt;Raspberry Pi Zeros powered via Controller Pi GPIO (USB optional).&lt;/li&gt;
&lt;li&gt;Individual Raspberry Pi Zero power controlled via Controller Pi GPIO (I2C).&lt;/li&gt;
&lt;li&gt;Connector for Controller Serial Console (FTDI Basic).&lt;/li&gt;
&lt;li&gt;Controller Pi can be rebooted without interrupting power to Pi Zeros, network recovers on boot.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id="problems"&gt;Problem(s)
&lt;/h2&gt;&lt;p&gt;Well, obviously, it&amp;rsquo;s absurd, so it&amp;rsquo;s yet another project for me!!&lt;/p&gt;
&lt;p&gt;First problem: the Cluster HAT theoretically cannot support Pi Zero 2Ws. At least that&amp;rsquo;s what you read everywhere (on reseller websites, and what you can deduce from the introduction I included just above). And that&amp;rsquo;s really unfortunate because that&amp;rsquo;s the model I have (I have two) and I&amp;rsquo;d have been bummed to have to buy 4 in version 1.3 just to make sure it works.&lt;/p&gt;
&lt;p&gt;Fortunately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Several comments found on the net indicate that in reality, it works if the power supply (27w for the official one) of the Pi 5 is powerful enough; moreover, &lt;a class="link" href="https://8086.support/content/23/121/en/can-the-pi-zero-2-w-be-used-in-the-cluster-hat.html" target="_blank" rel="noopener"
&gt;the FAQ says it actually works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Quentin kindly gave me 2 Pi Zero 1.3s, which completes my collection. I&amp;rsquo;ll have 2 of each, that should be fine power-wise.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Second problem, finding one (Cluster HAT)! The last version of this product was manufactured several years ago. The official site doesn&amp;rsquo;t ship to France, the product no longer exists on Kubii.fr. I found a few on The Pi Hut (in the United Kingdom and there are none left today), for about forty euros, shipping included.&lt;/p&gt;
&lt;p&gt;Last point, I had to find a Pi 5 at a reasonable price and it hurt to pay €70 for a second-hand Pi 5 with only 4 GB.&lt;/p&gt;
&lt;p&gt;So with accessories (SD cards) and the various boards, we&amp;rsquo;re looking at a side project of about €200.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a bit expensive for an absurd project, but I&amp;rsquo;m funding it with an article (on another topic) that will be published in a magazine in a few weeks. Those who know, know ;-P.&lt;/p&gt;
&lt;h2 id="hardware-installation"&gt;Hardware Installation
&lt;/h2&gt;&lt;p&gt;Probably the most fun part when you like (like me) tinkering with computer components and electronic boards.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Insert the small screws to raise the &amp;ldquo;Hat&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Insert the Cluster HAT into the GPIO ports of the Raspberry Pi 5&lt;/li&gt;
&lt;li&gt;Insert the 4 Pi Zeros into the USB ports of the HAT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Power Supply:&lt;/strong&gt; You must use a sufficiently powerful power supply, ideally the official Pi 5 power supply (27W USB-C). The Cluster HAT draws its energy from the Pi 5 to power the 4 Zeros.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SD Cards:&lt;/strong&gt; You need 5 MicroSD cards in total (1 for the Pi 5, 4 for the Zeros). The site mentions booting the Pi zeros without MicroSD cards (experimental feature). I might look into it?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/01/cluster-hat-1.avif"
loading="lazy"
alt="Cluster HAT assembled on a Raspberry Pi 5 with 4 Pi Zeros"
&gt;&lt;/p&gt;
&lt;p&gt;OK, that was trivial. But it proves the product is well designed!&lt;/p&gt;
&lt;h2 id="3d-case"&gt;3D Case
&lt;/h2&gt;&lt;p&gt;Since I have a 3D printer, I modified an existing model for RPis with an &amp;ldquo;M.2 hat&amp;rdquo; (another additional board, this one for connecting an M.2 SSD) by making big HOLES everywhere so the Pi zeros could fit.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://makerworld.com/fr/models/1063966-raspberry-pi-5-ai-m-2-hat-snap-case#profileId-1052672" target="_blank" rel="noopener"
&gt;Raspberry Pi 5 AI/M.2 Hat+ Snap Case&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/01/pi_cluster_hat_with_4_pi_zero.avif"
loading="lazy"
alt="Cluster HAT in its 3D-printed case with 4 Pi Zeros"
&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately, I cannot share the modified profile because it&amp;rsquo;s against the 3D file license (Standard Digital File License, which prohibits modifications).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I spent more time looking for models, modifying them, and testing prototypes than actually playing with the cluster hat itself.&lt;/p&gt;
&lt;h2 id="preparing-the-installation"&gt;Preparing the Installation
&lt;/h2&gt;&lt;p&gt;The first thing to do to start enjoying this magnificent Raspberry cluster is to install and configure our Pi 5.&lt;/p&gt;
&lt;p&gt;Even though it&amp;rsquo;s theoretically possible to do everything yourself, the cluster hat manufacturer recommends starting with the preconfigured images available on their site &lt;a class="link" href="https://8086.net" target="_blank" rel="noopener"
&gt;8086.net&lt;/a&gt;. The latest version is available for download here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://dist1.8086.net/clusterctrl/bookworm/2025-11-24/" target="_blank" rel="noopener"
&gt;dist1.8086.net/clusterctrl/bookworm/2025-11-24/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Looking through the list, we note that there are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6 versions for the &amp;ldquo;master&amp;rdquo; RPi (Bookworm lite, normal and full, with CNAT or CBRIDGE mode for each)&lt;/li&gt;
&lt;li&gt;preconfigured Rpi OS lite images for the Pi Zeros (called Px, x being the position on the cluster hat), also with CBRIDGE / CNAT variants, armhf (32-bit) and arm64&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Out of laziness, I just plugged in the Pi 5, I don&amp;rsquo;t have an Ethernet cable within reach. In this case, if like me you connect your Pi via WiFi, you need to go with &lt;strong&gt;CNAT&lt;/strong&gt; mode (NATed). On the contrary, CBRIDGE mode (bridged, which requires the Ethernet port) means you&amp;rsquo;ll see all 5 machines on your LAN directly.&lt;/p&gt;
&lt;p&gt;Second piece of info (reminder), in my case, I have a mix of 32 and 64-bit machines, so I need to be careful to select the right OS for the right machines.&lt;/p&gt;
&lt;h2 id="installing-the-raspberry-pi-5-our-controller"&gt;Installing the Raspberry Pi 5 (Our Controller)
&lt;/h2&gt;&lt;p&gt;For the Raspberry Pi 5, in my case the file is &lt;code&gt;2025-11-24-2-bookworm-ClusterCTRL-arm64-lite-CNAT.img.xz&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://dist1.8086.net/clusterctrl/bookworm/2025-11-24/2025-11-24-2-bookworm-ClusterCTRL-arm64-lite-CNAT.img.xz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="the-headless-mode-problem"&gt;The Headless Mode Problem
&lt;/h3&gt;&lt;p&gt;Even though it&amp;rsquo;s possible, I don&amp;rsquo;t want to connect the Pi to a monitor (having to get out a keyboard and a monitor&amp;hellip; exhausting) so I&amp;rsquo;m going to do everything in &lt;strong&gt;headless&lt;/strong&gt; mode. And unfortunately, the latest versions of Raspberry Pi Imager (2.0.0) don&amp;rsquo;t allow (anymore?) customization of third-party OSes.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/01/clusterhat1.avif"
loading="lazy"
alt="Raspberry Pi Imager 2.0 no longer allows third-party OS customization"
&gt;&lt;/p&gt;
&lt;p&gt;In theory, once the image is flashed to the microSD, you&amp;rsquo;ll need to manually configure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WiFi&lt;/li&gt;
&lt;li&gt;a basic user&lt;/li&gt;
&lt;li&gt;enable SSH&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I tried, it failed, despite a &lt;code&gt;wpa_supplicant.conf&lt;/code&gt; file, a properly formatted &lt;code&gt;userconf.txt&lt;/code&gt;, and an empty &lt;code&gt;ssh&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/01/headless.avif"
loading="lazy"
alt="Failed headless configuration with wpa_supplicant and userconf"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Solution: Raspberry Pi Imager 1.9.6&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After getting quite frustrated, I simply went back to the previous version of the imager (1.9.6) which works very well with the image provided by 8086.net.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; however, for installing recent RPi OS versions, I had quite a few failures. So use sparingly.&lt;/p&gt;
&lt;h2 id="installing-the-4-raspberry-pi-zeros-the-nodes"&gt;Installing the 4 Raspberry Pi Zeros (The Nodes)
&lt;/h2&gt;&lt;p&gt;Here, it&amp;rsquo;s a bit simpler on the network side, because in CNAT mode, the Pi Zeros will communicate via the USB cable of the Cluster HAT. BUT you need to pay attention to the 32 vs 64-bit architecture issues, in my somewhat hybrid case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P1&lt;/strong&gt; (a Pi Zero 2): &lt;code&gt;...arm64-lite-CNAT-p1.img.xz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2&lt;/strong&gt; (a Pi Zero 1.3): &lt;code&gt;...armhf-lite-CNAT-p2.img.xz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P3&lt;/strong&gt; (a Pi Zero 1.3): &lt;code&gt;...armhf-lite-CNAT-p3.img.xz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P4&lt;/strong&gt; (a Pi Zero 2): &lt;code&gt;...arm64-lite-CNAT-p4.img.xz&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For customization, we leave SSH and the user/password, but no need to set up WiFi. We could also modify the boot parameters to remove the Bluetooth and WiFi kernel modules to save a few MB of RAM.&lt;/p&gt;
&lt;h2 id="boot-up"&gt;Boot Up
&lt;/h2&gt;&lt;p&gt;Once everything is ready and the RPi 5 is working, you can log in via SSH and run the command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo clusterctrl on
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From there, the cluster hat LEDs should start blinking, and the RPi Zeros should gradually turn on. It looks a bit like Christmas lights but I like it.&lt;/p&gt;
&lt;p&gt;If you only want to turn on some of them, it&amp;rsquo;s possible with these commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo clusterctrl on p1 &lt;span class="c1"&gt;# to turn on only p1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo clusterctrl off p2 p3 p4 &lt;span class="c1"&gt;# to turn off p2, p3 and p4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="network-configuration"&gt;Network Configuration
&lt;/h3&gt;&lt;p&gt;In NAT mode, the IP addresses of the Pi Zeros are assigned as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;on the RPi 5 side (the controller) we have a bridge &lt;code&gt;br0&lt;/code&gt; with address &lt;code&gt;172.19.181.254/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;on the Pi Zero side, &lt;code&gt;172.19.181.x&lt;/code&gt; with x ranging from 1 to 4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s as simple as that. From the controller, we can try to log in via SSH:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2026/01/connect_p1.avif"
loading="lazy"
alt="SSH connection to Pi Zero p1 via Cluster HAT NAT network"
&gt;&lt;/p&gt;
&lt;p&gt;Even though p1 is NATed, the NAT works since we have Internet access:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zwindler@p1:~ $ ping 8.8.8.8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PING 8.8.8.8 &lt;span class="o"&gt;(&lt;/span&gt;8.8.8.8&lt;span class="o"&gt;)&lt;/span&gt; 56&lt;span class="o"&gt;(&lt;/span&gt;84&lt;span class="o"&gt;)&lt;/span&gt; bytes of data.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;64&lt;/span&gt; bytes from 8.8.8.8: &lt;span class="nv"&gt;icmp_seq&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="nv"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;115&lt;/span&gt; &lt;span class="nv"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12.5 ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;64&lt;/span&gt; bytes from 8.8.8.8: &lt;span class="nv"&gt;icmp_seq&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="nv"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;115&lt;/span&gt; &lt;span class="nv"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;17.1 ms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;I lost quite a bit of time flashing images until I found the right way to have both the cluster HAT custom ROM &lt;strong&gt;and&lt;/strong&gt; the OS customizations to run headless.&lt;/p&gt;
&lt;p&gt;But once I had that, it was relatively trivial to get this little cluster working.&lt;/p&gt;
&lt;p&gt;Now, all that&amp;rsquo;s left is to find what I&amp;rsquo;m going to run on it ;) I&amp;rsquo;m opening the bets!&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://andypiper.medium.com/building-a-compact-pi-cluster-44217f6a9cf5" target="_blank" rel="noopener"
&gt;Building a compact Pi cluster&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.reddit.com/r/homelab/comments/qta8yd/a_tiny_cluster_based_on_4x_raspberry_pi_zero_2_w/?tl=fr" target="_blank" rel="noopener"
&gt;A tiny cluster based on 4x Raspberry Pi Zero 2 W&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://medium.com/@dhuck/the-missing-clusterhat-tutorial-45ad2241d738" target="_blank" rel="noopener"
&gt;The missing ClusterHAT tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://clusterctrl.com/setup-software" target="_blank" rel="noopener"
&gt;ClusterCTRL Setup Software&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/burtyb/clusterhat-image/tree/master" target="_blank" rel="noopener"
&gt;ClusterHAT Image GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://dist1.8086.net/clusterctrl/bookworm/2025-11-24/" target="_blank" rel="noopener"
&gt;ClusterCTRL Downloads&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>