<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ha on bismigel</title><link>https://bismigel.dev/tags/ha/</link><description>Recent content in Ha on bismigel</description><generator>Hugo</generator><language>en-us</language><copyright>bismigel</copyright><lastBuildDate>Wed, 14 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://bismigel.dev/tags/ha/index.xml" rel="self" type="application/rss+xml"/><item><title>One cluster, three nodes</title><link>https://bismigel.dev/posts/2026-10-14-one-cluster-three-nodes/</link><pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate><guid>https://bismigel.dev/posts/2026-10-14-one-cluster-three-nodes/</guid><description/><content:encoded><![CDATA[<blockquote>
<p><strong>Building the lab</strong> &mdash; part 4 of 8. ← <a href="/posts/2026-10-13-secrets-without-plaintext/">Secrets without plaintext</a>
 · <a href="/posts/2026-10-16-deepseek-writes-the-boring-parts/">Where AI fits in the lab →</a>
</p>
</blockquote>
<p>For a while my lab was two machines, each running its own single-node cluster. I told myself it was resilient because each one had a Cloudflare connector, so if one went down the tunnel stayed up. That was wrong, and it took me an embarrassingly long time to see why.</p>
<p>The connector isn&rsquo;t the thing serving my sites. The sites run on one box. If that box dies, the second connector keeps the <strong>tunnel</strong> open and cheerfully returns errors for everything behind it. I&rsquo;d built redundancy for the part that rarely fails and none for the part that matters. So I merged the two clusters into one.</p>
<h1 id="why-three">Why three</h1>
<p>A single-node cluster is one machine. To survive losing a machine you need more than two, because etcd &mdash; the thing that stores the cluster&rsquo;s state &mdash; needs a <strong>majority</strong> of its members to agree before it will do anything. With two, that majority is two: lose either one and the cluster stops. Three is the smallest number that tolerates losing one.</p>
<p>I had two machines. The third turned out to be a 2012 MacBook Pro &mdash; the last one Apple made that you can still open with a screwdriver. A cheap SSD and an Ethernet cable later, it was a fine third vote. It holds only etcd and the control plane; I tainted it so nothing else lands there.</p>
<h1 id="letting-the-ai-drive">Letting the AI drive</h1>
<p>The honest part: I did not want to partition a disk by hand, install NixOS, and persuade a cluster to accept it &mdash; three things I&rsquo;d mess up in one night. So I had a local AI assistant do it. I gave it the MacBook&rsquo;s SSH key and a one-line instruction: install NixOS, match the other two, join the cluster. It wiped the disk, installed NixOS over SSH, gave the machine a fixed address, and joined it. I watched and understood maybe a third of it.</p>
<h1 id="what-bit-me">What bit me</h1>
<ul>
<li><strong>The firewall.</strong> The join kept timing out. The port etcd uses between members wasn&rsquo;t open on the other machines, so the new node could reach the API but not its peers. I stared at pod logs before thinking to look at the host firewall.</li>
<li><strong>The taint.</strong> Keeping the MacBook cheap was right for it, but it also means it can&rsquo;t help <strong>spread</strong> services for redundancy. A control-plane-only node keeps the cluster alive; it doesn&rsquo;t make the apps redundant. Different job.</li>
<li><strong>Storage.</strong> The one I expected least. My workloads keep their state on the local disk of the node they run on, so even with three healthy nodes a pod can&rsquo;t just move &mdash; its data isn&rsquo;t there. Three nodes gave me a resilient control plane, not resilient apps. (I&rsquo;ve since added distributed storage for the parts that need it.)</li>
</ul>
<h1 id="making-the-control-plane-itself-redundant">Making the control plane itself redundant</h1>
<p>The cluster can survive a node, but the thing that <strong>manages</strong> it shouldn&rsquo;t be a single copy. So the GitOps controller now runs two replicas each of its web, repo, and app-set components, and two of its controller (one active, one standing by), spread across the two worker boxes. Its cache is still a single copy &mdash; a known weak point I haven&rsquo;t fixed.</p>
<h1 id="the-lesson">The lesson</h1>
<p>Resilience is a property of a <strong>service</strong>, not of a box or a cable. Adding a second tunnel connector didn&rsquo;t make my sites redundant; it made the tunnel redundant. Once I could say out loud what I actually wanted to survive &mdash; a whole machine going away &mdash; the right design got a lot easier to see. Three machines agreeing is a stranger, more fiddly thing than two machines pretending. It&rsquo;s also the first version that&rsquo;s actually true.</p>
<blockquote>
<p><strong>Building the lab</strong> &mdash; part 4. Previous: <a href="/posts/2026-10-13-secrets-without-plaintext/">Secrets without plaintext</a>
. Next: <a href="/posts/2026-10-16-deepseek-writes-the-boring-parts/">Where AI fits in the lab</a>
.</p>
</blockquote>
]]></content:encoded></item></channel></rss>