<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://onatm.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://onatm.dev/" rel="alternate" type="text/html" /><updated>2026-07-16T00:28:03+00:00</updated><id>https://onatm.dev/feed.xml</id><title type="html">Onat Mercan’s Blog</title><subtitle>The blog of Onat Yigit Mercan. Rust enthusiast, cloud infrastructure builder, and occasional explorer of computer science - from assembly to distributed systems.</subtitle><author><name>Onat Yigit Mercan</name></author><entry><title type="html">Let’s Build PlanetScale From Scratch: Infrastructure</title><link href="https://onatm.dev/2026/07/16/homescale-part-1/" rel="alternate" type="text/html" title="Let’s Build PlanetScale From Scratch: Infrastructure" /><published>2026-07-16T00:00:00+00:00</published><updated>2026-07-16T00:00:00+00:00</updated><id>https://onatm.dev/2026/07/16/homescale-part-1</id><content type="html" xml:base="https://onatm.dev/2026/07/16/homescale-part-1/"><![CDATA[<figure>
  <img src="/assets/images/planetscale_at_home.jpg" alt="" />
  <figcaption>Homescale, PlanetScale at home.</figcaption>
</figure>

<p>I worked for a database tools company many years ago and was lucky enough to build a few database cloning tools before PlanetScale was cool.</p>

<p>Those tools were never destined to be as successful as PlanetScale, but they had their uses.</p>

<p>The idea behind those tools was simple: isolate the storage layer from compute and use the best storage technology for cloning the database files.</p>

<p>This idea came back to me a while ago while I was tweeting about an interview I had with an F1 team that was looking for a Rust dev with hands-on Ceph experience. Mentioning Ceph reminded me of a failed product attempt I was part of, and I jokingly told a friend that I was going to build Homescale, PlanetScale at home.</p>

<h2 id="homescale">Homescale</h2>

<blockquote>
  <p>I am building Homescale at <a href="https://github.com/homescale-dev/homescale">github.com/homescale-dev/homescale</a>.</p>
</blockquote>

<p>Homescale creates writable database instances and point-in-time branches from immutable snapshots without copying the full database.</p>

<p>I borrowed Docker’s image and container model to represent the database state: A database image is an immutable starting point. A database container is a writable clone created from that image. A branch is another container created from the current state of an existing container.</p>

<p>The CLI I have in mind looks like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>homescale image create <span class="nt">--engine</span> postgres postgres-base
homescale container create <span class="nt">--image</span> postgres-base dev-db
homescale container connect dev-db

homescale branch create <span class="nt">--container</span> dev-db feature-login
homescale container connect feature-login
</code></pre></div></div>

<p>The examples in this series use Postgres, but <strong>Homescale’s storage model is database agnostic</strong>. The image, container, and branch model can apply to any database engine that keeps its durable state on a filesystem backed by a block device and can be prepared for a recoverable snapshot. Postgres is the first engine I plan to support and gives me something concrete to use while building the storage and orchestration layers.</p>

<p>Engine-specific behavior belongs behind an adapter. That adapter initializes an image, starts the database process, exposes connection details, and prepares the database for a snapshot when necessary. Homescale handles the lifecycle around it: volumes, immutable states, writable clones, workloads, and lineage.</p>

<p>The word <code class="language-plaintext highlighter-rouge">branch</code> describes the relationship between the containers. After running these commands, <code class="language-plaintext highlighter-rouge">dev-db</code> and <code class="language-plaintext highlighter-rouge">feature-login</code> are both writable database containers. <code class="language-plaintext highlighter-rouge">feature-login</code> simply started from the state of <code class="language-plaintext highlighter-rouge">dev-db</code> at the moment the branch was created.</p>

<p>Underneath, the lineage looks like this:</p>

<pre><code class="language-mermaid">flowchart LR
    Image[/postgres-base&lt;br/&gt;image/]
    Dev[dev-db&lt;br/&gt;writable]
    State[/read-only&lt;br/&gt;state/]
    Feature[feature-login&lt;br/&gt;writable]

    Image --&gt;|clone| Dev
    Dev --&gt;|snapshot| State
    State --&gt;|clone| Feature
</code></pre>

<p>The image is already immutable, so Homescale can create <code class="language-plaintext highlighter-rouge">dev-db</code> from it directly. Branching from a writable container needs an intermediate state. Homescale first captures <code class="language-plaintext highlighter-rouge">dev-db</code> at a point in time, then creates <code class="language-plaintext highlighter-rouge">feature-login</code> from that state.</p>

<p>Creating <code class="language-plaintext highlighter-rouge">feature-login</code> cannot mean copying <code class="language-plaintext highlighter-rouge">dev-db</code> byte for byte. A 100 GB database would require another 100 GB of storage before the branch could start.</p>

<p>The branch should initially share its data with the captured state of <code class="language-plaintext highlighter-rouge">dev-db</code>. Only the data that changes afterward should require additional storage.</p>

<p>That requires branching to happen below the database process, in the storage layer.</p>

<h2 id="separating-storage-from-compute">Separating storage from compute</h2>

<p>Separating database storage from compute is not a new idea. Standard Amazon RDS engines store database and log files on EBS volumes. Google Cloud SQL runs the database process in a VM with attached network block storage, using Persistent Disk or Hyperdisk depending on the machine series. The database still sees a normal block device, but its durable data is not tied to the compute host’s local disk.</p>

<p>Aurora, AlloyDB, and Azure SQL Hyperscale take this further. Their compute instances connect to shared or distributed storage systems designed specifically for the database. Compute instances can be replaced or scaled without creating a complete copy of the data for each instance.</p>

<p>Homescale is closer to the first model. A database process reads and writes a filesystem on what looks like a block device. I only need the storage behind that device to have a lifecycle independent of the process using it.</p>

<pre><code class="language-mermaid">flowchart TD
    Database[Database process]
    Filesystem[Filesystem]
    Device[Block device]
    Storage[(Persistent storage)]

    Database --&gt;|file reads and writes| Filesystem
    Filesystem --&gt;|block I/O| Device
    Device --&gt; Storage
</code></pre>

<p>That boundary lets Homescale create the storage before starting a database process and keep it after the process stops. More importantly, it lets branching happen in the storage layer without requiring the database engine to implement branching itself.</p>

<p>The two Postgres processes know nothing about their shared history. Each sees its own writable block device. Another supported engine would see the same storage boundary. The engine adapter would change, but the snapshot, clone, and lineage model would not.</p>

<p>Homescale still needs a way to create a writable block device that shares unchanged data with its parent.</p>

<h2 id="cow-is-the-secret-sauce">COW is the secret sauce</h2>

<p>Copy-on-write (COW) allows a writable clone to share unchanged data with the immutable state it was created from.</p>

<p>When Homescale creates <code class="language-plaintext highlighter-rouge">dev-db</code>, the new container initially reads its data from <code class="language-plaintext highlighter-rouge">postgres-base</code>. Writes made by <code class="language-plaintext highlighter-rouge">dev-db</code> are stored in the container without changing the image.</p>

<p>Creating <code class="language-plaintext highlighter-rouge">feature-login</code> repeats the same process. Homescale captures the current state of <code class="language-plaintext highlighter-rouge">dev-db</code>, then creates a new writable container from it.</p>

<p>The captured state, <code class="language-plaintext highlighter-rouge">dev-db@feature-login</code>, is read-only. It preserves the point where the two containers separated. <code class="language-plaintext highlighter-rouge">dev-db</code> can continue changing, while <code class="language-plaintext highlighter-rouge">feature-login</code> stores its own changes on top of that captured state.</p>

<p>When <code class="language-plaintext highlighter-rouge">feature-login</code> reads data it has not changed, the storage layer follows the chain back to its parent. This can continue through several generations until it finds the data. On the first write to shared data, the storage layer copies the relevant allocation unit into the writable container and applies the change there.</p>

<p>A branch of a 100 GB database therefore appears as a complete 100 GB database without requiring another 100 GB copy up front. Its initial cost is mostly metadata. Storage usage grows as <code class="language-plaintext highlighter-rouge">dev-db</code> and <code class="language-plaintext highlighter-rouge">feature-login</code> diverge, although a small database write may cause a larger storage allocation underneath.</p>

<p>This model also creates dependencies. <code class="language-plaintext highlighter-rouge">feature-login</code> relies on <code class="language-plaintext highlighter-rouge">dev-db@feature-login</code> for data it has not copied into its own container. Homescale must not remove that intermediate state while the branch still depends on it.</p>

<p>Homescale therefore needs persistent block devices, immutable snapshots, and writable COW clones.</p>

<h2 id="ceph">Ceph</h2>

<p>Ceph provides those operations through RBD. The database only sees an ordinary block device.</p>

<p>Ceph provides object, file, and block storage on top of the same underlying system. I only care about the block interface, called RADOS Block Device or RBD. An RBD image is a virtual block device that can be mapped to a machine, formatted with a filesystem, and mounted like a normal disk.</p>

<p>In the earlier storage stack, the persistent storage at the bottom is a Ceph RBD image. The database still reads and writes through the same filesystem and block device interface.</p>

<p>Underneath that RBD image, Ceph splits the device into objects and stores them across its object store. The <a href="https://docs.ceph.com/en/latest/man/8/rbd/">Ceph documentation</a> describes RBD images as block devices striped over objects in RADOS. The default object size is 4 MB, although it is configurable.</p>

<p>This is why a small database write does not necessarily create an equally small allocation in a clone. When a clone first writes to data it still shares with its parent, Ceph may need to copy the corresponding RADOS object into the child before applying the write.</p>

<p>An RBD snapshot is a read-only view of an image at a point in time. A clone is a writable RBD image that refers back to that snapshot for data it does not yet own. Ceph calls this <a href="https://docs.ceph.com/en/latest/rbd/rbd-snapshot/">snapshot layering</a>.</p>

<p>Ceph can snapshot the volume without knowing which database is using it. Whether the database can safely start from that snapshot is a separate problem.</p>

<p>One direct RBD sequence for that operation is:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rbd snap create homescale/dev-db@feature-login
rbd snap protect homescale/dev-db@feature-login
rbd clone <span class="se">\</span>
  homescale/dev-db@feature-login <span class="se">\</span>
  homescale/feature-login
</code></pre></div></div>

<p>Homescale represents that sequence with one command, although Kubernetes and Ceph CSI will perform the backend storage operations:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>homescale branch create <span class="nt">--container</span> dev-db feature-login
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dev-db@feature-login</code> is the read-only state between the two containers. Ceph will not delete it while it is protected. <code class="language-plaintext highlighter-rouge">feature-login</code> is a regular writable RBD image that can be mounted, snapshotted, and branched again.</p>

<h3 id="rados-osds-and-rbds">RADOS, OSDs and RBDs</h3>

<p>RBD sits on top of RADOS, which stores its objects through OSDs:</p>

<pre><code class="language-mermaid">flowchart TB
    RBD[RBD image]
    Objects[RADOS objects]
    PG[Placement group]
    OSDs[Acting OSD set]
    Disks[(Storage devices)]
    Pool[Pool policy&lt;br/&gt;replicas and CRUSH]

    RBD --&gt;|split into| Objects
    Objects --&gt;|hash to| PG
    Pool -.-&gt;|controls placement| PG
    PG --&gt;|maps to| OSDs
    OSDs --&gt;|persist on| Disks
</code></pre>

<p>RADOS is Ceph’s distributed object store. RBD provides the block interface on top of it. When a database writes to an RBD-backed filesystem, RBD turns those block operations into reads and writes against RADOS objects.</p>

<p>Those objects live in a pool. A pool defines how its data is stored, including the number of replicas and the CRUSH rule used for placement.</p>

<p>Ceph does not map every object directly to a disk. It first maps the object to a placement group, then maps that placement group to one or more OSDs. This allows Ceph to move data when OSDs are added, removed, or fail without keeping a central table of every object’s location. The <a href="https://docs.ceph.com/en/latest/architecture/#data-placement">Ceph architecture documentation</a> describes the same path: objects to placement groups, then placement groups to OSDs.</p>

<p>An OSD stores RADOS objects on a storage device and handles their reads, writes, and replication. Ceph can split one device between multiple OSDs, but the <a href="https://docs.ceph.com/en/latest/start/hardware-recommendations/#storage-drives">documented layout is one storage drive per OSD</a>. That is the layout I will use.</p>

<p>Homescale will not talk to RADOS directly. Its storage boundary is RBD: create a volume, take a snapshot, and clone it. The operator will request those operations through Kubernetes PVCs and <code class="language-plaintext highlighter-rouge">VolumeSnapshot</code> resources.</p>

<h2 id="kubernetes-as-the-control-plane">Kubernetes as the control plane</h2>

<p>I do not want Homescale to manage database processes, volume mounts, and network endpoints directly. It will describe the desired database and storage state through Kubernetes resources, then rely on controllers to make that state real.</p>

<p>The CLI talks to the Homescale API. Homescale creates database workloads, PVCs, and <code class="language-plaintext highlighter-rouge">VolumeSnapshot</code> resources. Ceph CSI translates the storage resources into RBD operations, while Rook operates the Ceph cluster itself.</p>

<pre><code class="language-mermaid">flowchart TB
    CLI[Homescale CLI] --&gt; Control

    subgraph Kubernetes
        Control["Homescale"]
        Databases
        StorageAPI["Storage API&lt;br/&gt;PVCs, snapshots, CSI"]
        Ceph["Ceph storage&lt;br/&gt;RBD and OSDs"]

        Control --&gt;|reconciles| Databases
        Control --&gt;|creates| StorageAPI
        StorageAPI --&gt; Ceph
        Databases --&gt; Ceph
    end

    Ceph --&gt; Disks[(Storage devices)]
</code></pre>

<p>Kubernetes controllers reconcile these resources toward their desired state. If a database pod disappears, its controller replaces it. If a PVC requests dynamic provisioning through a compatible <code class="language-plaintext highlighter-rouge">StorageClass</code>, the CSI driver provisions its storage. Homescale can use that machinery instead of implementing its own process supervision and storage attachment logic.</p>

<p>The application-specific resources and the controllers that reconcile them belong to the next part. The important boundary here is the Kubernetes storage API: Homescale requests PVCs and <code class="language-plaintext highlighter-rouge">VolumeSnapshot</code> resources, and Ceph CSI translates those requests into RBD operations.</p>

<h3 id="local-and-replicated-deployments">Local and replicated deployments</h3>

<p>For now, Homescale will run locally on macOS in a single-node Kubernetes cluster, probably using Colima or Lima. This local profile has one Kubernetes node, one Ceph OSD, and one storage device.</p>

<p>I may also deploy Homescale to the <a href="/2026/05/30/provisioning-a-private-talos-kubernetes-cluster-on-hetzner-cloud/">private Talos cluster I run on Hetzner Cloud</a>. The Terraform for that cluster already supports separate worker pools, so I can add a Ceph node pool without putting OSDs beside the existing application workloads.</p>

<p>A replicated pool with <code class="language-plaintext highlighter-rouge">size: 3</code> and <code class="language-plaintext highlighter-rouge">failureDomain: host</code> can place each object’s replicas across those nodes. If a disk or node fails, Ceph can continue serving the volume from the surviving replicas.</p>

<p>Locally, there is one copy of the data. On Hetzner, I can run the same Homescale operator against a replicated Ceph pool spread across the storage nodes.</p>

<h2 id="rook-connects-kubernetes-to-ceph">Rook connects Kubernetes to Ceph</h2>

<p>A <code class="language-plaintext highlighter-rouge">CephCluster</code> resource tells Rook which Ceph version to run, where it can place monitors and managers, and which devices it can consume for OSDs. A <code class="language-plaintext highlighter-rouge">CephBlockPool</code> describes the RBD pool and its replication and failure-domain settings. The Rook operator watches those resources and creates or updates the corresponding Ceph daemons and configuration.</p>

<p>The split looks like this:</p>

<pre><code class="language-mermaid">flowchart TB
    Pod[Database pod] --&gt;|mounts| PVC[PVC]
    Snapshot[VolumeSnapshot] --&gt;|source for| NewPVC[New PVC]
    PVC --&gt; CSI[Ceph CSI]
    NewPVC --&gt; CSI
    CSI --&gt;|provisions in| Ceph[Ceph cluster]

    CRs[Ceph resources] --&gt; Rook[Rook operator]
    Rook --&gt;|reconciles| Ceph
</code></pre>

<p>Rook configures the Ceph CSI drivers used to provision and mount storage. Homescale can use PVCs and <code class="language-plaintext highlighter-rouge">VolumeSnapshot</code> resources without running <code class="language-plaintext highlighter-rouge">rbd</code> commands or carrying Ceph credentials itself. The <a href="https://rook.io/docs/rook/latest-release/Storage-Configuration/Ceph-CSI/ceph-csi-snapshot/">Rook snapshot documentation</a> describes the same restore flow: create a <code class="language-plaintext highlighter-rouge">VolumeSnapshot</code> from a PVC, then use that snapshot as the data source for another PVC.</p>

<p>This keeps Homescale independent of the Ceph deployment shape. The local cluster can use one OSD and a pool with one copy. The Hetzner cluster can use three OSDs and replication across hosts. As long as both expose compatible <code class="language-plaintext highlighter-rouge">StorageClass</code> and <code class="language-plaintext highlighter-rouge">VolumeSnapshotClass</code> resources, the Homescale operator follows the same reconciliation flow.</p>

<h2 id="what-is-next">What is next?</h2>

<p>The next part will cover the Homescale services: the API used by the CLI, the controllers that manage database resources, and the Postgres adapter.</p>

<p>I will start with the image and container path. An image request needs to initialize a Postgres volume and capture it as an immutable <code class="language-plaintext highlighter-rouge">VolumeSnapshot</code>. A container request needs to clone that snapshot into a new PVC, start Postgres, wait for it to become ready, and return connection details.</p>

<p>Once that path works, I can add branching from a running container. That service flow must prepare the source database for a snapshot, create the intermediate state, record its lineage, and reconcile another writable container from it.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="infrastructure" /><category term="kubernetes" /><category term="ceph" /><category term="database" /><summary type="html"><![CDATA[Homescale, PlanetScale at home.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/posts/homescale-part-1.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/posts/homescale-part-1.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Provisioning a Private Talos Kubernetes Cluster on Hetzner Cloud</title><link href="https://onatm.dev/2026/05/30/provisioning-a-private-talos-kubernetes-cluster-on-hetzner-cloud/" rel="alternate" type="text/html" title="Provisioning a Private Talos Kubernetes Cluster on Hetzner Cloud" /><published>2026-05-30T00:00:00+00:00</published><updated>2026-05-30T00:00:00+00:00</updated><id>https://onatm.dev/2026/05/30/provisioning-a-private-talos-kubernetes-cluster-on-hetzner-cloud</id><content type="html" xml:base="https://onatm.dev/2026/05/30/provisioning-a-private-talos-kubernetes-cluster-on-hetzner-cloud/"><![CDATA[<p><em>This is a follow up to <a href="/2026/01/28/private-networking-on-hetzner-cloud-with-tailscale/">Private Networking on Hetzner Cloud with Tailscale</a></em></p>

<hr />

<p>The previous post was about the network. This one is about what I put inside that network: a private Kubernetes cluster running Talos on Hetzner Cloud.</p>

<p>The important part is not just “Kubernetes on Hetzner Cloud”. There are many posts about it. The part I cared about was making the cluster private from the first boot. No public IPs on the control plane. No public IPs on the workers. Access only through the Tailnet.</p>

<p>That made Talos a good fit. No package manager, no SSH. You give it machine configuration, it becomes a Kubernetes node, and that is mostly it.</p>

<p>Mostly.</p>

<h2 id="what-i-wanted-from-the-cluster">What I Wanted from the Cluster</h2>

<ul>
  <li><strong>Private-only nodes</strong>: every Kubernetes node should live only on the Hetzner private network.</li>
  <li><strong>Terraform-managed bootstrap</strong>: machines, Talos config, kubeconfig, and base add-ons should come from code.</li>
  <li><strong>Talos</strong>: no manual server maintenance.</li>
  <li><strong>Separate node pools</strong>: platform components should not fight application workloads.</li>
  <li><strong>GitOps</strong>: Terraform can bootstrap ArgoCD, then ArgoCD owns the platform.</li>
</ul>

<p>The goal was to build something small enough that I could understand every moving part, but powerful enough that I could run actual projects on it.</p>

<h2 id="cluster-shape">Cluster Shape</h2>

<p>The private network from the previous post gives the cluster a <code class="language-plaintext highlighter-rouge">/24</code> to live in. I split that range into explicit chunks:</p>

<ul>
  <li>Control plane: <code class="language-plaintext highlighter-rouge">10.0.128.16/28</code></li>
  <li>Platform workers: <code class="language-plaintext highlighter-rouge">10.0.128.32/27</code></li>
  <li>General workers: <code class="language-plaintext highlighter-rouge">10.0.128.64/27</code></li>
  <li>Service network: <code class="language-plaintext highlighter-rouge">10.0.192.0/21</code></li>
  <li>Pod network: <code class="language-plaintext highlighter-rouge">10.0.200.0/19</code></li>
</ul>

<p>The control plane has three nodes. Platform workers run things like ArgoCD and platform components. General workers run applications like <a href="https://snapbyte.dev">snapbyte.dev</a>.</p>

<pre><code class="language-mermaid">flowchart TB
    Tailnet((Tailnet))
    Internet((Internet))

    subgraph VPC["Private network 10.0.0.0/16"]
        subgraph Subnet["Subnet 10.0.128.0/24"]
            NAT["NAT Gateway"]

            subgraph CP["Control Plane 10.0.128.16/28"]
                CP1["cp-1"]
                CP2["cp-2"]
                CP3["cp-3"]
            end

            subgraph Platform["Platform Workers 10.0.128.32/27"]
                ArgoCD["ArgoCD"]
                PlatformApps["Platform components"]
            end

            subgraph General["General Workers 10.0.128.64/27"]
                PublicApps["Public apps"]
                InternalApps["Internal apps"]
            end
        end
    end

    Tailnet --&gt;|kubectl and talosctl| CP1
    Tailnet --&gt; Platform
    Tailnet --&gt; General
    CP --&gt; NAT
    Platform --&gt; NAT
    General --&gt; NAT
    NAT --&gt; Internet
</code></pre>

<p>The Kubernetes API endpoint is the first control plane node’s private IP:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">cluster_endpoint</span> <span class="o">=</span> <span class="s2">"https://${local.control_plane_private_ips[0]}:6443"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That endpoint is only useful if you are already inside the private network through Tailscale.</p>

<h2 id="building-the-talos-image">Building the Talos Image</h2>

<p>Before Terraform could create any nodes, I needed a Talos image that Hetzner could boot.</p>

<p>I started this cluster on Talos <code class="language-plaintext highlighter-rouge">v1.11.3</code>. The later <code class="language-plaintext highlighter-rouge">v1.12.6</code> upgrade came from an operational incident, not the initial design.</p>

<p>Hetzner does not give you Talos as an image option, so I build my own snapshot with Packer. The flow is based on <a href="https://github.com/hcloud-talos/terraform-hcloud-talos/tree/main/_packer">hcloud-talos/terraform-hcloud-talos</a>.</p>

<p>It starts a temporary Hetzner server, downloads the Talos raw image from the Talos Image Factory, writes it to disk, and saves the result as a snapshot.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"talos_version"</span> <span class="p">{</span>
  <span class="nx">type</span>    <span class="o">=</span> <span class="nx">string</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"v1.11.3"</span>
<span class="p">}</span>

<span class="nx">source</span> <span class="s2">"hcloud"</span> <span class="s2">"talos"</span> <span class="p">{</span>
  <span class="nx">rescue</span>       <span class="o">=</span> <span class="s2">"linux64"</span>
  <span class="nx">image</span>        <span class="o">=</span> <span class="s2">"debian-11"</span>
  <span class="nx">location</span>     <span class="o">=</span> <span class="s2">"nbg1"</span>
  <span class="nx">server_type</span>  <span class="o">=</span> <span class="s2">"cx22"</span>
  <span class="nx">ssh_username</span> <span class="o">=</span> <span class="s2">"root"</span>

  <span class="nx">snapshot_name</span> <span class="o">=</span> <span class="s2">"talos-${var.talos_version}-amd64"</span>
  <span class="nx">snapshot_labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">type</span>    <span class="o">=</span> <span class="s2">"infra"</span>
    <span class="nx">os</span>      <span class="o">=</span> <span class="s2">"talos"</span>
    <span class="nx">version</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">talos_version</span>
    <span class="nx">arch</span>    <span class="o">=</span> <span class="s2">"amd64"</span>
  <span class="p">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>The label part is the important bit. Terraform can later find the image by selector instead of relying on snapshot name:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">data</span> <span class="s2">"hcloud_image"</span> <span class="s2">"talos"</span> <span class="p">{</span>
  <span class="nx">with_selector</span> <span class="o">=</span> <span class="s2">"os=talos,type=infra,version=${var.talos_version},arch=amd64"</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="worker-pools">Worker Pools</h2>

<p>Before creating the machines, I needed a way to describe what kind of nodes I wanted.</p>

<p>This is basically the same idea as node pools in managed Kubernetes offerings. GKE, EKS, and AKS all let you create groups of nodes with different sizes, labels, or taints. I wanted the same mental model.</p>

<p>Each pool also gets its own Hetzner placement group. That tells Hetzner to spread the nodes in that pool across different physical hosts where possible. It does not make the pool highly available, but it avoids the failure mode where every <code class="language-plaintext highlighter-rouge">platform</code> worker ends up on the same machine.</p>

<p>The pool config looks like this:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">worker_pools</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">platform</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">count</span>      <span class="o">=</span> <span class="mi">3</span>
    <span class="nx">sku</span>        <span class="o">=</span> <span class="s2">"cx33"</span>
    <span class="nx">cidr</span>       <span class="o">=</span> <span class="s2">"10.0.128.32/27"</span>
    <span class="nx">datacenter</span> <span class="o">=</span> <span class="s2">"nbg1-dc3"</span>
    <span class="nx">labels</span>     <span class="o">=</span> <span class="p">{</span> <span class="nx">purpose</span> <span class="o">=</span> <span class="s2">"platform"</span> <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">general</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">count</span>      <span class="o">=</span> <span class="mi">3</span>
    <span class="nx">sku</span>        <span class="o">=</span> <span class="s2">"cx23"</span>
    <span class="nx">cidr</span>       <span class="o">=</span> <span class="s2">"10.0.128.64/27"</span>
    <span class="nx">datacenter</span> <span class="o">=</span> <span class="s2">"nbg1-dc3"</span>
    <span class="nx">labels</span>     <span class="o">=</span> <span class="p">{</span> <span class="nx">purpose</span> <span class="o">=</span> <span class="s2">"general"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This makes the Terraform code easier to reason about. It lets me create named groups of machines with known CIDR ranges, placement groups, and labels.</p>

<h2 id="terraform-creates-the-machines">Terraform Creates the Machines</h2>

<p>The node resources are just regular Hetzner servers, but with the public network disabled.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"hcloud_server"</span> <span class="s2">"control_plane"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">control_plane</span><span class="p">.</span><span class="nx">count</span>

  <span class="nx">name</span>        <span class="o">=</span> <span class="s2">"${local.cluster_name}-cp-${count.index + 1}"</span>
  <span class="nx">datacenter</span>  <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">control_plane</span><span class="p">.</span><span class="nx">datacenter</span>
  <span class="nx">image</span>       <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">hcloud_image</span><span class="p">.</span><span class="nx">talos</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">server_type</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">control_plane</span><span class="p">.</span><span class="nx">sku</span>

  <span class="nx">public_net</span> <span class="p">{</span>
    <span class="nx">ipv4_enabled</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="nx">ipv6_enabled</span> <span class="o">=</span> <span class="kc">false</span>
  <span class="p">}</span>

  <span class="nx">network</span> <span class="p">{</span>
    <span class="nx">network_id</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">network_id</span>
    <span class="nx">ip</span>         <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">control_plane_ips</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The worker pool map from the previous section gets flattened into individual servers. The code is not elegant, but the outcome is simple: if I add another worker to the <code class="language-plaintext highlighter-rouge">general</code> pool, it gets the next private IP in that pool and the right labels.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">workers_flat</span> <span class="o">=</span> <span class="nx">merge</span><span class="p">([</span>
    <span class="nx">for</span> <span class="nx">pool_name</span><span class="p">,</span> <span class="nx">pool_config</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">worker_pools</span> <span class="o">:</span> <span class="p">{</span>
      <span class="nx">for</span> <span class="nx">i</span> <span class="nx">in</span> <span class="nx">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nx">pool_config</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">:</span>
      <span class="s2">"${pool_name}-${i}"</span> <span class="p">=</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="nx">pool</span>       <span class="o">=</span> <span class="nx">pool_name</span>
        <span class="nx">index</span>      <span class="o">=</span> <span class="nx">i</span>
        <span class="nx">sku</span>        <span class="o">=</span> <span class="nx">pool_config</span><span class="err">.</span><span class="nx">sku</span>
        <span class="nx">datacenter</span> <span class="o">=</span> <span class="nx">pool_config</span><span class="p">.</span><span class="nx">datacenter</span>
        <span class="nx">labels</span>     <span class="o">=</span> <span class="nx">pool_config</span><span class="p">.</span><span class="nx">labels</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">]...)</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="talos-bootstraps-kubernetes">Talos Bootstraps Kubernetes</h2>

<p>Once the servers exist, the Talos Terraform provider takes over. It generates machine secrets, creates control plane and worker configs, applies patches, bootstraps the first control plane node, waits for Talos cluster health, and gives me a kubeconfig.</p>

<p>There is one base config for control plane nodes, and one worker base config per pool:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">data</span> <span class="s2">"talos_machine_configuration"</span> <span class="s2">"control_plane"</span> <span class="p">{</span>
  <span class="nx">cluster_name</span>       <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">cluster_name</span>
  <span class="nx">cluster_endpoint</span>   <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">cluster_endpoint</span>
  <span class="nx">machine_type</span>       <span class="o">=</span> <span class="s2">"controlplane"</span>
  <span class="nx">machine_secrets</span>    <span class="o">=</span> <span class="nx">talos_machine_secrets</span><span class="p">.</span><span class="nx">this</span><span class="p">.</span><span class="nx">machine_secrets</span>
  <span class="nx">talos_version</span>      <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">talos_version</span>
  <span class="nx">kubernetes_version</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">kubernetes_version</span>
<span class="p">}</span>

<span class="nx">data</span> <span class="s2">"talos_machine_configuration"</span> <span class="s2">"worker"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">worker_pools</span>

  <span class="nx">cluster_name</span>       <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">cluster_name</span>
  <span class="nx">cluster_endpoint</span>   <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">cluster_endpoint</span>
  <span class="nx">machine_type</span>       <span class="o">=</span> <span class="s2">"worker"</span>
  <span class="nx">machine_secrets</span>    <span class="o">=</span> <span class="nx">talos_machine_secrets</span><span class="p">.</span><span class="nx">this</span><span class="p">.</span><span class="nx">machine_secrets</span>
  <span class="nx">talos_version</span>      <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">talos_version</span>
  <span class="nx">kubernetes_version</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">kubernetes_version</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then Terraform applies the patched control-plane config to each control-plane node:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"talos_machine_configuration_apply"</span> <span class="s2">"control_plane"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">control_plane</span><span class="p">.</span><span class="nx">count</span>

  <span class="nx">client_configuration</span>        <span class="o">=</span> <span class="nx">talos_machine_secrets</span><span class="p">.</span><span class="nx">this</span><span class="p">.</span><span class="nx">client_configuration</span>
  <span class="nx">machine_configuration_input</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">talos_machine_configuration</span><span class="p">.</span><span class="nx">control_plane</span><span class="p">.</span><span class="nx">machine_configuration</span>
  <span class="nx">node</span>                        <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">control_plane_private_ips</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">]</span>

  <span class="nx">config_patches</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nx">yamlencode</span><span class="p">(</span><span class="nx">local</span><span class="p">.</span><span class="nx">control_plane_patch</span><span class="p">),</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Workers follow the same pattern, except the patch comes from the worker pool:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"talos_machine_configuration_apply"</span> <span class="s2">"worker"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">workers_flat</span>

  <span class="nx">client_configuration</span>        <span class="o">=</span> <span class="nx">talos_machine_secrets</span><span class="p">.</span><span class="nx">this</span><span class="p">.</span><span class="nx">client_configuration</span>
  <span class="nx">machine_configuration_input</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">talos_machine_configuration</span><span class="p">.</span><span class="nx">worker</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">pool</span><span class="p">].</span><span class="nx">machine_configuration</span>
  <span class="nx">node</span>                        <span class="o">=</span> <span class="nx">flatten</span><span class="p">(</span><span class="nx">hcloud_server</span><span class="p">.</span><span class="nx">worker</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">network</span><span class="p">)[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ip</span>

  <span class="nx">config_patches</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nx">yamlencode</span><span class="p">(</span><span class="nx">local</span><span class="p">.</span><span class="nx">worker_pool_patches</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">pool</span><span class="p">]),</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Nodes need the right installer image, node IP selection, default route, pod and service CIDRs, and CNI behavior.</p>

<p>This is the part I messed up and later caused the first real failure.</p>

<p>A simplified version of the patch, showing the final intent, looks like this:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">common_patch</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">machine</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">install</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">disk</span>  <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">install_disk</span>
      <span class="nx">image</span> <span class="o">=</span> <span class="s2">"factory.talos.dev/installer/${var.talos_schematic_id}:${var.talos_version}"</span>
    <span class="p">}</span>

    <span class="nx">kubelet</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">extraArgs</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s2">"cloud-provider"</span>             <span class="p">=</span> <span class="s2">"external"</span>
        <span class="s2">"rotate-server-certificates"</span> <span class="p">=</span> <span class="kc">true</span>
      <span class="p">}</span>
      <span class="nx">nodeIP</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">validSubnets</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">node_cidrs</span>
      <span class="p">}</span>
    <span class="p">}</span>

    <span class="nx">network</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">interfaces</span> <span class="o">=</span> <span class="p">[</span>
        <span class="p">{</span>
          <span class="nx">interface</span> <span class="o">=</span> <span class="s2">"eth0"</span>
          <span class="nx">routes</span> <span class="o">=</span> <span class="p">[{</span>
            <span class="nx">network</span> <span class="o">=</span> <span class="s2">"0.0.0.0/0"</span>
            <span class="nx">gateway</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">gateway</span>
          <span class="p">}]</span>
          <span class="nx">dhcp</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="p">},</span>
        <span class="p">{</span>
          <span class="nx">interface</span> <span class="o">=</span> <span class="s2">"eth1"</span>
          <span class="nx">ignore</span>    <span class="o">=</span> <span class="kc">true</span>
        <span class="p">}</span>
      <span class="p">]</span>
    <span class="p">}</span>

    <span class="nx">features</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">hostDNS</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">enabled</span>              <span class="o">=</span> <span class="kc">true</span>
        <span class="nx">forwardKubeDNSToHost</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="nx">resolveMemberNames</span>   <span class="o">=</span> <span class="kc">true</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">cluster</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">network</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">podSubnets</span>     <span class="o">=</span> <span class="p">[</span><span class="nx">var</span><span class="p">.</span><span class="nx">pod_ipv4_cidr</span><span class="p">]</span>
      <span class="nx">serviceSubnets</span> <span class="o">=</span> <span class="p">[</span><span class="nx">var</span><span class="p">.</span><span class="nx">service_ipv4_cidr</span><span class="p">]</span>
      <span class="nx">cni</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">name</span> <span class="o">=</span> <span class="s2">"none"</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="nx">proxy</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">disabled</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Most of that is ordinary cluster setup. The important bits are: <code class="language-plaintext highlighter-rouge">nodeIP.validSubnets</code>, the default route, and the interface names. If those are wrong, the cluster does not fail in an obvious way. It half-works, which is worse.</p>

<p>The control plane patch adds the other important private-networking detail:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">cluster</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">etcd</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">advertisedSubnets</span> <span class="o">=</span> <span class="p">[</span><span class="nx">var</span><span class="p">.</span><span class="nx">control_plane</span><span class="p">.</span><span class="nx">cidr</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">controllerManager</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">extraArgs</span> <span class="o">=</span> <span class="p">{</span>
      <span class="s2">"cloud-provider"</span>           <span class="p">=</span> <span class="s2">"external"</span>
      <span class="s2">"node-cidr-mask-size-ipv4"</span> <span class="p">=</span> <span class="s2">"24"</span>
      <span class="s2">"bind-address"</span>             <span class="p">=</span> <span class="s2">"0.0.0.0"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That keeps etcd on the control-plane private CIDR and makes Kubernetes pod CIDR allocation line up with the cluster network.</p>

<p>The worker patch mostly adds the pool labels, so nodes become <code class="language-plaintext highlighter-rouge">purpose=platform</code> or <code class="language-plaintext highlighter-rouge">purpose=general</code>. After the configs are applied, Terraform bootstraps only the first control plane node:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"talos_machine_bootstrap"</span> <span class="s2">"this"</span> <span class="p">{</span>
  <span class="nx">client_configuration</span> <span class="o">=</span> <span class="nx">talos_machine_secrets</span><span class="p">.</span><span class="nx">this</span><span class="p">.</span><span class="nx">client_configuration</span>
  <span class="nx">node</span>                 <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">control_plane_private_ips</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-first-failure-was-networking">The First Failure Was Networking</h2>

<p>The first failure was simple: the nodes did not agree on their own network identity.</p>

<p>Private-only Hetzner machines still need a default route for egress. In my setup, the node route goes to the subnet gateway, and Hetzner’s network route sends outbound traffic to the NAT gateway.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Node -&gt; Subnet Gateway -&gt; NAT Gateway -&gt; Internet
</code></pre></div></div>

<p>In practice, the Talos machine config needs to point that route at the right interface, and kubelet needs to pick the right node IP.</p>

<p>I had a few false starts here. At one point I assumed the private network interface was <code class="language-plaintext highlighter-rouge">enp7s0</code>. Then I tried routes on both <code class="language-plaintext highlighter-rouge">enp7s0</code> and <code class="language-plaintext highlighter-rouge">eth0</code>, with <code class="language-plaintext highlighter-rouge">eth1</code> ignored. Hetzner Cloud VMs were using <code class="language-plaintext highlighter-rouge">eth0</code> for the network path I needed, so <code class="language-plaintext highlighter-rouge">eth0</code> was the path that actually mattered.</p>

<p>The other subtle part was <code class="language-plaintext highlighter-rouge">nodeIP.validSubnets</code>. My first attempt pointed kubelet at the broader subnet. Talos and Kubernetes do better when the allowed node IP ranges are exactly the ranges where nodes live.</p>

<p>So the module builds that list from the control plane CIDR and each worker pool CIDR:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">node_cidrs</span> <span class="o">=</span> <span class="nx">concat</span><span class="p">(</span>
    <span class="p">[</span><span class="nx">var</span><span class="p">.</span><span class="nx">control_plane</span><span class="p">.</span><span class="nx">cidr</span><span class="p">],</span>
    <span class="p">[</span><span class="nx">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">pool_config</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">worker_pools</span> <span class="o">:</span> <span class="nx">pool_config</span><span class="p">.</span><span class="nx">cidr</span><span class="p">]</span>
  <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Configuring networking is not easy. It is a chain of small settings that all need to agree: Hetzner routes, Talos interfaces, kubelet node IPs, etcd advertised subnets, and Kubernetes pod CIDR allocation.</p>

<h2 id="cilium-first-then-cloud-integrations">Cilium First, Then Cloud Integrations</h2>

<p>Once the nodes agreed on their private addresses and routes, the next step was getting the cluster network itself working.</p>

<p>I install Cilium with Helm after Talos bootstraps Kubernetes. It runs in native routing mode and replaces kube-proxy.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"helm_release"</span> <span class="s2">"cilium"</span> <span class="p">{</span>
  <span class="nx">name</span>       <span class="o">=</span> <span class="s2">"cilium"</span>
  <span class="nx">repository</span> <span class="o">=</span> <span class="s2">"https://helm.cilium.io/"</span>
  <span class="nx">chart</span>      <span class="o">=</span> <span class="s2">"cilium"</span>
  <span class="nx">namespace</span>  <span class="o">=</span> <span class="s2">"kube-system"</span>

  <span class="nx">set</span> <span class="p">{</span>
    <span class="nx">name</span>  <span class="o">=</span> <span class="s2">"routingMode"</span>
    <span class="nx">value</span> <span class="o">=</span> <span class="s2">"native"</span>
  <span class="p">}</span>

  <span class="nx">set</span> <span class="p">{</span>
    <span class="nx">name</span>  <span class="o">=</span> <span class="s2">"kubeProxyReplacement"</span>
    <span class="nx">value</span> <span class="o">=</span> <span class="s2">"true"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>After Cilium is in place, the Hetzner Cloud Controller Manager can run. That order matters because the CCM depends on a working cluster network and is responsible for Hetzner-specific behavior like load balancers and node metadata.</p>

<p>The CSI driver and metrics server follow the same idea. Terraform installs the base pieces that make the cluster usable.</p>

<h2 id="gitops-from-day-one">GitOps from Day One</h2>

<p>Terraform installs ArgoCD because something needs to install ArgoCD. After that bootstrap step, the responsibilities are clear: Terraform owns infrastructure and the first bootstrap, ArgoCD owns platform components.</p>

<p>This is where the earlier <code class="language-plaintext highlighter-rouge">platform</code> pool starts to make sense. ArgoCD should not land randomly on the same general workers as application traffic.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">global</span><span class="pi">:</span>
  <span class="na">nodeSelector</span><span class="pi">:</span>
    <span class="na">purpose</span><span class="pi">:</span> <span class="s">platform</span>
</code></pre></div></div>

<p>The Terraform side creates the namespace, repository credentials, and Helm release. ArgoCD itself is exposed on the internal ingress as <code class="language-plaintext highlighter-rouge">argocd.int.noreturn.dev</code>. That keeps the GitOps UI private, but still easy to reach from my laptop through Tailscale.</p>

<p>From there, the platform repository can install <code class="language-plaintext highlighter-rouge">cert-manager</code>, <code class="language-plaintext highlighter-rouge">external-secrets</code>, ingress controllers, monitoring, and the rest.</p>

<h2 id="then-real-workloads-showed-up">Then Real Workloads Showed Up</h2>

<p>The original cluster came up on Talos <code class="language-plaintext highlighter-rouge">v1.11.3</code>. It worked. And then <a href="https://snapbyte.dev">snapbyte.dev</a> started running on the general workers.</p>

<p>That is when the cluster stopped being a learning exercise.</p>

<p>A high-churn set of CronJobs in <code class="language-plaintext highlighter-rouge">snapbyte</code> started leaving the cluster in a bad state. One worker showed <code class="language-plaintext highlighter-rouge">SystemOOM</code>, Grafana had gaps in node metrics, disk I/O looked suspicious, and Talos showed hundreds of <code class="language-plaintext highlighter-rouge">containerd-shim-runc-v2</code> processes on a hot worker.</p>

<p>At first it looked like a disk problem. Then it looked like a workload problem. Then it looked like both.</p>

<p>Restarting the affected workers would probably have been fine, but I got curious around 1am one night and kept digging.</p>

<p>Eventually, the runtime became part of the investigation because those nodes were on Talos <code class="language-plaintext highlighter-rouge">v1.11.3</code> with <code class="language-plaintext highlighter-rouge">containerd 2.1.4</code>. That pointed me to this issue: <a href="https://github.com/containerd/containerd/issues/12344">containerd-shim processes leak during high pod churn</a>.</p>

<p>By 3am, curiosity had turned into an in-place Talos upgrade to <code class="language-plaintext highlighter-rouge">v1.12.6</code>, moving the nodes to <code class="language-plaintext highlighter-rouge">containerd 2.1.6</code>, and a much more careful rollout process than I had planned when I first built this cluster.</p>

<p>But that is its own post.</p>

<h2 id="what-i-learned">What I Learned</h2>

<ul>
  <li>Private-only nodes still need a default route for outbound traffic</li>
  <li>The Talos interface names have to match what the Hetzner VM actually uses</li>
  <li><code class="language-plaintext highlighter-rouge">nodeIP.validSubnets</code> should only include the ranges where Kubernetes nodes actually live: the control plane CIDR and each worker pool CIDR</li>
  <li><code class="language-plaintext highlighter-rouge">etcd.advertisedSubnets</code> should stay on the control-plane private CIDR</li>
  <li>The service and pod CIDRs are not random ranges. I used <code class="language-plaintext highlighter-rouge">10.0.192.0/21</code> for services and <code class="language-plaintext highlighter-rouge">10.0.200.0/19</code> for pods, outside the node ranges in <code class="language-plaintext highlighter-rouge">10.0.128.0/24</code>.</li>
  <li>The pod range is <code class="language-plaintext highlighter-rouge">/19</code> because Kubernetes allocates one <code class="language-plaintext highlighter-rouge">/24</code> pod CIDR per node in this setup. A <code class="language-plaintext highlighter-rouge">/19</code> contains 32 <code class="language-plaintext highlighter-rouge">/24</code> ranges, so the cluster has room for up to 32 nodes without overlapping the node or service ranges.</li>
  <li>Talos passes those ranges to Kubernetes, the controller manager allocates <code class="language-plaintext highlighter-rouge">/24</code> pod CIDRs to nodes from the pod range, and Cilium routes traffic based on that Kubernetes view of the network.</li>
  <li>Running real workloads changes the meaning of the cluster. Once <code class="language-plaintext highlighter-rouge">snapbyte.dev</code> was running there, it was not a learning exercise anymore. A personal cluster is still production if real services depend on it.</li>
</ul>

<h2 id="next">Next</h2>

<p>Next up: the Talos upgrade from <code class="language-plaintext highlighter-rouge">v1.11.3</code> to <code class="language-plaintext highlighter-rouge">v1.12.6</code>, why I did it, what I checked before each node, and how a small <code class="language-plaintext highlighter-rouge">snapbyte</code> incident became a container runtime debugging session.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="infrastructure" /><category term="kubernetes" /><category term="terraform" /><category term="talos" /><category term="hetzner" /><summary type="html"><![CDATA[This is a follow up to Private Networking on Hetzner Cloud with Tailscale]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/posts/provisioning-a-private-talos-kubernetes-cluster-on-hetzner-cloud.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/posts/provisioning-a-private-talos-kubernetes-cluster-on-hetzner-cloud.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">We lost our imagination</title><link href="https://onatm.dev/2026/05/17/we-lost-our-imagination/" rel="alternate" type="text/html" title="We lost our imagination" /><published>2026-05-17T00:00:00+00:00</published><updated>2026-05-17T00:00:00+00:00</updated><id>https://onatm.dev/2026/05/17/we-lost-our-imagination</id><content type="html" xml:base="https://onatm.dev/2026/05/17/we-lost-our-imagination/"><![CDATA[<p>We lost our imagination. It’s not because of AI. Not everybody had it before AI, but the people who had it lost it too.</p>

<p>Now we are constantly witnessing how much regular people lack imagination. Everybody thinks they can get rich by building the exact same habit tracker.</p>

<p>Our clothes are the same, cars look the same, and houses decorated with cheap, soulless decor are the same.</p>

<p>We all go home and watch the same shows. It doesn’t matter what language they are in because we can watch them in our native tongue.</p>

<p>We have more tools than ever, but fewer ideas. Everything is copied, translated, repackaged, and sold back to us.</p>

<p>At what point did convenience stop helping us imagine and start doing the imagining for us?</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="ai" /><category term="life" /><category term="general" /><summary type="html"><![CDATA[We lost our imagination. It’s not because of AI. Not everybody had it before AI, but the people who had it lost it too.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/posts/we-lost-our-imagination.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/posts/we-lost-our-imagination.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">What can change the nature of an AI?</title><link href="https://onatm.dev/2026/03/14/what-can-change-the-nature-of-an-ai/" rel="alternate" type="text/html" title="What can change the nature of an AI?" /><published>2026-03-14T00:00:00+00:00</published><updated>2026-03-14T00:00:00+00:00</updated><id>https://onatm.dev/2026/03/14/what-can-change-the-nature-of-an-ai</id><content type="html" xml:base="https://onatm.dev/2026/03/14/what-can-change-the-nature-of-an-ai/"><![CDATA[<blockquote>
  <p>Lie: “PERSONALITY.md”</p>
</blockquote>

<p>I cannot tell you how much I hate seeing <code class="language-plaintext highlighter-rouge">AGENTS.md</code>, skills, or BS like <code class="language-plaintext highlighter-rouge">PERSONALITY.md</code> files full of crap such as “You are a senior software engineer”. You are just fucking role-playing. You are merely telling an LLM to use the lingo a senior software engineer might use.</p>

<p>They can only improve or change during training, post-training, or fine-tuning. That is it. People confuse surface behavior with actual change. They are only as good as the next benchmark asking whether they should drive to the car wash or walk.</p>

<p>We are experiencing a mass hysteria in a very small echo chamber, thinking we are all doomed and everybody is going to lose their jobs. You know what? The world is not just a Hacker News echo chamber. Most people are using ChatGPT to interpret their dreams or ask questions rather than Googling.</p>

<figure>
  <img src="/assets/images/what-can-change-the-nature-of-a-man.png" alt="What can change the nature of a man" />
  <figcaption>Same question, fewer personality files.</figcaption>
</figure>

<h2 id="that-is-just-context">That is just context</h2>

<p>Your coding agent’s “nature” changes for a few seconds on some random H100 shared by God knows how many people in a desert somewhere in the US. That is not a soul. It is context. That personality, that quirkiness, that thing that makes it call you “dipshit” only happens because a whole wall of text is getting sent back and forth between your computer and the data center.</p>

<p>The worst mistake is treating imitation like conscience.</p>

<p>You cannot expect them to feel remorse when they decide to bomb some random apartment block full of kids in a Middle Eastern country. They don’t reason, think or feel. They are just an imitation, an Artificial Artificial Intelligence (AAI). Of course, for the next chat session you could add a line to WEAPONOFMASSDESTRUCTION[.]md to say “Previously you killed 250 kids. You should feel sad. Consider not killing too many kids”.</p>

<p>You can change the voice, the costume, the script. Not the nature. You are <strong>always</strong> absolutely right.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="ai" /><category term="llm" /><summary type="html"><![CDATA[Lie: “PERSONALITY.md”]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/posts/what-can-change-the-nature-of-an-ai.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/posts/what-can-change-the-nature-of-an-ai.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Private Networking on Hetzner Cloud with Tailscale</title><link href="https://onatm.dev/2026/01/28/private-networking-on-hetzner-cloud-with-tailscale/" rel="alternate" type="text/html" title="Private Networking on Hetzner Cloud with Tailscale" /><published>2026-01-28T00:00:00+00:00</published><updated>2026-01-28T00:00:00+00:00</updated><id>https://onatm.dev/2026/01/28/private-networking-on-hetzner-cloud-with-tailscale</id><content type="html" xml:base="https://onatm.dev/2026/01/28/private-networking-on-hetzner-cloud-with-tailscale/"><![CDATA[<p><em>This is a follow up to <a href="/2025/11/30/why-i-built-my-own-kubernetes-cluster/">Why I Built My Own Kubernetes Cluster</a></em></p>

<hr />

<p>The cluster post was about why I built my own Kubernetes cluster. This one is about the private network that makes the rest possible. I wanted the cluster to live in a VPC-style network with zero public node IPs, reachable only from my own internal network.</p>

<p>Tailscale is the bridge here. I keep Raspberry Pi Zero exit nodes across Europe with friends for VPN use, so I already use Tailscale a lot. That’s why it was the obvious choice for private access.</p>

<h2 id="what-i-wanted-from-the-network">What I Wanted from the Network</h2>

<p>I wanted three things:</p>

<ol>
  <li><strong>Private-only cluster nodes</strong>: no public IPs anywhere except one gateway.</li>
  <li><strong>Tailnet-only kubectl</strong>: cluster access stays inside the Tailnet via subnet routes.</li>
  <li><strong>Separation of public vs. private apps</strong>: <a href="https://snapbyte.dev">snapbyte.dev</a> stays public, internal tools stay private.</li>
</ol>

<p>That meant I needed a VPC-style network that could handle egress through a single NAT gateway, and ingress through an internal load balancer with a private IP address. The cluster could stay dark on the public internet, but still be reachable from my Tailnet.</p>

<h2 id="network-shape">Network Shape</h2>

<p>Hetzner does not call this a VPC, but the architecture is the same idea: an isolated private network with a controlled egress point. I kept the design small and opinionated:</p>

<ul>
  <li>Network CIDR: <code class="language-plaintext highlighter-rouge">10.0.0.0/16</code></li>
  <li>Subnet: <code class="language-plaintext highlighter-rouge">10.0.128.0/24</code></li>
  <li>Single NAT gateway with a public IP for outbound traffic</li>
  <li>Private-only Kubernetes nodes inside the subnet</li>
</ul>

<p>The design is intentionally simple: network + subnet, a minimal firewall, a NAT gateway box, and Tailscale ACLs. The base network shape is small and explicit:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"hcloud_network"</span> <span class="s2">"vpc_network"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"vpc-network"</span>
  <span class="nx">ip_range</span> <span class="o">=</span> <span class="s2">"10.0.0.0/16"</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"hcloud_network_subnet"</span> <span class="s2">"vpc_subnet"</span> <span class="p">{</span>
  <span class="nx">network_id</span>   <span class="o">=</span> <span class="nx">hcloud_network</span><span class="p">.</span><span class="nx">vpc_network</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">type</span>         <span class="o">=</span> <span class="s2">"cloud"</span>
  <span class="nx">ip_range</span>     <span class="o">=</span> <span class="s2">"10.0.128.0/24"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The routing flow is simple and deterministic:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cluster Node -&gt; Subnet Gateway -&gt; NAT Gateway -&gt; Internet
</code></pre></div></div>

<p>The NAT gateway is the only box with a public IP. Everything else is private.</p>

<pre><code class="language-mermaid">flowchart LR
    subgraph Tailnet["Tailnet"]
        Laptop[Laptop]
        Phone[Phone]
    end
    Internet((Internet))

    subgraph VPC["VPC-style network 10.0.0.0/16"]
        subgraph Subnet["Subnet 10.0.128.0/24"]
            subgraph K8S["Kubernetes Nodes"]
                PublicApps["Public apps"]
                InternalApps["Internal apps"]
            end
            PubLB["Public LB"]
            PrivLB["Private LB"]
            NAT[NAT Gateway]
        end
    end

    Internet --&gt; PubLB
    PubLB --&gt; PublicApps
    Tailnet --&gt; PrivLB
    PrivLB --&gt; InternalApps
    Tailnet --&gt;|kubectl over subnet route| K8S
    K8S --&gt;|egress| NAT
    NAT --&gt; Internet
</code></pre>

<h2 id="the-nat-gateway-and-egress">The NAT Gateway and Egress</h2>

<p>The NAT gateway is a Debian 12 server with two jobs: perform <code class="language-plaintext highlighter-rouge">MASQUERADE NAT</code> for the subnet and advertise the subnet into my Tailnet.</p>

<p>The NAT gateway is just a Hetzner server with a public IP and a default route for the network:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"hcloud_server"</span> <span class="s2">"nat_gateway"</span> <span class="p">{</span>
  <span class="nx">name</span>        <span class="o">=</span> <span class="s2">"vpc-nat-gateway"</span>
  <span class="nx">image</span>       <span class="o">=</span> <span class="s2">"debian-12"</span>
  <span class="nx">server_type</span> <span class="o">=</span> <span class="s2">"cx23"</span>
  <span class="nx">user_data</span>   <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">cloudinit_config</span><span class="p">.</span><span class="nx">nat_gateway_cloud_init</span><span class="p">.</span><span class="nx">rendered</span>

  <span class="nx">network</span> <span class="p">{</span>
    <span class="nx">network_id</span> <span class="o">=</span> <span class="nx">hcloud_network</span><span class="p">.</span><span class="nx">vpc_network</span><span class="p">.</span><span class="nx">id</span>
    <span class="nx">ip</span>         <span class="o">=</span> <span class="nx">cidrhost</span><span class="p">(</span><span class="nx">hcloud_network_subnet</span><span class="p">.</span><span class="nx">vpc_subnet</span><span class="p">.</span><span class="nx">ip_range</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"hcloud_network_route"</span> <span class="s2">"nat_gateway_route"</span> <span class="p">{</span>
  <span class="nx">network_id</span>  <span class="o">=</span> <span class="nx">hcloud_network</span><span class="p">.</span><span class="nx">vpc_network</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">destination</span> <span class="o">=</span> <span class="s2">"0.0.0.0/0"</span>
  <span class="nx">gateway</span>     <span class="o">=</span> <span class="nx">flatten</span><span class="p">(</span><span class="nx">hcloud_server</span><span class="p">.</span><span class="nx">nat_gateway</span><span class="p">.</span><span class="nx">network</span><span class="p">)[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ip</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Cloud-init does the two important bits: NAT and Tailscale subnet routing.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#cloud-config</span>
<span class="na">write_files</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/etc/network/interfaces</span>
    <span class="na">content</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">post-up echo 1 &gt; /proc/sys/net/ipv4/ip_forward</span>
      <span class="s">post-up iptables -t nat -A POSTROUTING -s '${network_ipv4_cidr}' -o eth0 -j MASQUERADE</span>

<span class="na">runcmd</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="pi">[</span><span class="s1">'</span><span class="s">tailscale'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">up'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">--auth-key=${tailscale_auth_key}'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">--advertise-routes=${subnet_ip_range}'</span><span class="pi">]</span>
</code></pre></div></div>

<p>The important detail is that cluster nodes never need public IPs. They just route their outbound traffic to the NAT gateway, and the gateway handles the rest.</p>

<p>The gateway sits behind a narrow firewall, so only the minimum network paths exist in and out.</p>

<h2 id="private-access-with-tailscale">Private Access with Tailscale</h2>

<p>Tailscale is not just for logging in. It’s the entry point into the private network.</p>

<p>The network Terraform code manages Tailscale ACLs and subnet routes. When the gateway comes up, it advertises <code class="language-plaintext highlighter-rouge">10.0.128.0/24</code> to the Tailnet. My laptop and phone can reach any node or service inside the subnet.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"tailscale_acl"</span> <span class="s2">"vpc_nat_gateway_acl"</span> <span class="p">{</span>
  <span class="nx">acl</span> <span class="o">=</span> <span class="nx">jsonencode</span><span class="p">({</span>
    <span class="nx">tagOwners</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"tag:gateway"</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"autogroup:admin"</span><span class="p">]</span> <span class="p">}</span>
    <span class="nx">autoApprovers</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">routes</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"10.0.128.0/24"</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"tag:gateway"</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span>
    <span class="nx">acls</span> <span class="o">=</span> <span class="p">[{</span> <span class="nx">action</span> <span class="o">=</span> <span class="s2">"accept"</span><span class="p">,</span> <span class="nx">src</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"autogroup:admin"</span><span class="p">],</span> <span class="nx">dst</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"*:*"</span><span class="p">]</span> <span class="p">}]</span>
  <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This gives me a private network that feels like a LAN, but works across continents.</p>

<h2 id="internal-subdomains-on-a-private-load-balancer">Internal Subdomains on a Private Load Balancer</h2>

<p>The Kubernetes cluster sits behind two ingress controllers: one public, one private. The private one uses a Hetzner load balancer with a private IP on the subnet. That means I can map internal DNS names to that private IP and access internal apps only from my Tailnet.</p>

<p>The load balancers are not created here. They show up when the two different nginx ingress controllers are deployed, and <a href="https://github.com/hetznercloud/hcloud-cloud-controller-manager"><code class="language-plaintext highlighter-rouge">hccm</code></a> provisions them: one with public IP and one with private IP.</p>

<p>So I can run public projects like <code class="language-plaintext highlighter-rouge">snapbyte.dev</code> on the public ingress, while keeping ArgoCD, Grafana, or other internal tools locked to the private ingress. The internal subdomains resolve inside my Tailnet, not on the public internet.</p>

<p>Example DNS split:</p>

<p><code class="language-plaintext highlighter-rouge">grafana.int.noreturn.dev -&gt; 10.0.128.x</code> (private LB IP)</p>

<p><code class="language-plaintext highlighter-rouge">snapbyte.dev -&gt; &lt;public-lb-ip&gt;</code></p>

<h2 id="minimal-exposure">Minimal Exposure</h2>

<p>The firewall rules are intentionally minimal:</p>

<ul>
  <li>Inbound: only WireGuard/Tailscale</li>
  <li>Outbound: DNS, NTP, HTTPS, and STUN</li>
</ul>

<p>There is no SSH open to the internet. The NAT gateway is the only exposed node, and it is still only reachable through Tailscale.</p>

<h2 id="what-i-learned">What I Learned</h2>

<ul>
  <li>A small <code class="language-plaintext highlighter-rouge">/24</code> subnet is enough for a personal cluster when you plan ranges up front.</li>
  <li>Subnet routes make the private network feel local without exposing anything publicly.</li>
</ul>

<h2 id="next">Next</h2>

<p>The network is the base layer. Next up is the Kubernetes provisioning piece: Talos images, node pools, and how I keep public and private ingress separate without leaking the cluster to the internet.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="infrastructure" /><category term="kubernetes" /><category term="networking" /><category term="terraform" /><category term="tailscale" /><category term="hetzner" /><summary type="html"><![CDATA[This is a follow up to Why I Built My Own Kubernetes Cluster]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/posts/private-networking-on-hetzner-cloud-with-tailscale.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/posts/private-networking-on-hetzner-cloud-with-tailscale.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why I Built My Own Kubernetes Cluster</title><link href="https://onatm.dev/2025/11/30/why-i-built-my-own-kubernetes-cluster/" rel="alternate" type="text/html" title="Why I Built My Own Kubernetes Cluster" /><published>2025-11-30T12:00:00+00:00</published><updated>2025-11-30T12:00:00+00:00</updated><id>https://onatm.dev/2025/11/30/why-i-built-my-own-kubernetes-cluster</id><content type="html" xml:base="https://onatm.dev/2025/11/30/why-i-built-my-own-kubernetes-cluster/"><![CDATA[<p><img src="/assets/images/k9s.png" alt="my cluster" /></p>

<p>I’ve been working as a platform engineer at Stack Overflow since 2022, designing platform components, writing documentation, and making developers’ lives easier. Recently, I moved to the infrastructure platform team. Now I’m in unfamiliar territory, trying to abstract away multi-cloud infrastructure details from other platform engineering teams.</p>

<p>For years, I worked with Kubernetes professionally, but mostly at a level that hid the infrastructure details. CIDR planning? Not my thing. DNS Zones? Already done. CNI? Things were already talking to each other. I could implement platform components, deploy applications, and troubleshoot pods. But now I’m working one level below, the landscape where I don’t have much experience.</p>

<p>I needed to learn infrastructure deeply. So I built my own Kubernetes cluster from scratch on Hetzner Cloud. Cost? About £50/month for a production-ready setup with high availability, monitoring, and GitOps. That’s less than just the control plane on AWS, GCP, or Azure.</p>

<h2 id="why-i-needed-my-own-cluster">Why I Needed My Own Cluster</h2>

<p>I’ve been building distributed systems since 2015 when I started at Hepsiburada in Istanbul, one of the largest e-commerce sites in Europe. I led the team that introduced the marketplace platform there and brought containers to the company. Back then, Kubernetes was still in alpha, and I was amazed by it. A decade of this kind of work changes how you approach problems.</p>

<p>Whenever I start a side project, the architecture immediately becomes distributed. I hate when I need a background job processor or some functionality the web framework doesn’t offer. The monolith I promise myself won’t become a distributed system? It always does. I don’t think this is overengineering anymore. <em>It’s just how I think now</em>.</p>

<blockquote>
  <p>My Kubernetes and Distributed Systems Experience is a Curse</p>
</blockquote>

<p>Take my current side project, <a href="https://snapbyte.dev">snapbyte.dev</a>. It’s a personal tech digest service that collects links from HN, /r/programming, lobste.rs, extracts content, categorizes it, generates summaries, and builds personalized digests based on user configuration. Started as a single Phoenix app, but quickly split into multiple services once I hit limitations with Elixir’s content extraction libraries.</p>

<p>For years at work, I’ve been shipping with Kubernetes. It’s my default. When I think about deploying, I think in pods and services. But for personal projects, there’s always been friction: I want to use the tools I’m comfortable with, but I can’t justify £100+ monthly for managed Kubernetes on a side project.</p>

<p>Then I started seeing Hetzner posts on Hacker News. <a href="https://digitalsociety.coop/posts/migrating-to-hetzner-cloud/">This one in particular caught my attention</a>. People were running production Kubernetes clusters on Hetzner for £100 per month. The same setup on AWS, GCP, or Azure would cost 5-10x that amount.</p>

<p>Suddenly, the math worked. Affordable Kubernetes plus infrastructure learning for my day job.</p>

<h2 id="what-i-wanted-to-learn">What I Wanted to Learn</h2>

<p>I needed to fill specific knowledge gaps for my day job at the infrastructure platform team.</p>

<p><strong>Network segmentation and CIDR planning</strong>: How do you actually design a VPC? What CIDR blocks should you allocate for nodes vs. pods vs. services? I’d seen <code class="language-plaintext highlighter-rouge">/16</code> and <code class="language-plaintext highlighter-rouge">/24</code> notations for years but never had to think about planning subnet ranges.</p>

<p><strong>Certificate management</strong>: How does cert-manager work with Let’s Encrypt? What’s a DNS-01 challenge vs. HTTP-01? How do you automate wildcard certificates?</p>

<p><strong>ArgoCD and ApplicationSets</strong>: Stack Overflow uses ArgoCD, but I only used FluxCD at previous companies like Redgate and TrueLayer. I wanted to understand ApplicationSets, and whether ArgoCD’s UI and app-of-apps pattern would be more intuitive for managing platform components.</p>

<p>I also wanted to work with <a href="https://www.talos.dev/">Talos Linux</a>. No SSH, no package manager, no traditional OS at all. Just YAML for managing everything. More YAML. Always more YAML.</p>

<h2 id="what-i-built">What I Built</h2>

<p>Here’s what I ended up with: a private VPC with no public IPs on cluster nodes, dual ingress controllers (public + private via Tailscale), and GitOps from day one. The NAT gateway is the only node exposed to the internet, with SSH blocked and only Tailscale ports open.</p>

<h3 id="architecture">Architecture</h3>

<pre><code class="language-mermaid">flowchart TB
    Tailnet((Tailnet))
    InternetIn((Internet))
    InternetOut((Internet))
    
    subgraph VPC["Private VPC (10.0.0.0/16)"]
        
        subgraph Subnet["Subnet (10.0.128.0/24)"]
        NAT[NAT Gateway - Public IP]
        ExtLB[Hetzner LB - External]
        IntLB["Hetzner LB - Internal (10.0.128.3)"]
        
        subgraph K8S["Kubernetes"]

            subgraph CP["Control Plane (10.0.128.16/28)"]
            end
            
            subgraph PW["Platform Workers (10.0.128.32/27)"]
                PlatformComponents[Platform Components]
            end
            
            subgraph GW["General Workers (10.0.128.64/27)"]
                PublicApps[Public apps]
                PrivateApps[Private apps]
            end

        end
        
        IntLB --&gt; PW
        IntLB --&gt; GW
        ExtLB --&gt; GW
        end
    end
    
    CP --&gt; NAT
    PW --&gt; NAT
    GW --&gt; NAT
    InternetIn --&gt; ExtLB
    Tailnet --&gt; Subnet &amp; IntLB
    NAT --&gt; InternetOut
    
    style Tailnet fill:#d1ecf1,stroke:#0c5460,stroke-width:2px
    style InternetIn fill:#cce5ff,stroke:#004085,stroke-width:3px
    style InternetOut fill:#cce5ff,stroke:#004085,stroke-width:3px
    style VPC fill:#f5f5f5,stroke:#666,stroke-width:2px
    style Subnet fill:#e8f4f8,stroke:#5a6268,stroke-width:2px
    style NAT fill:#ffe6cc,stroke:#ff9933,stroke-width:2px
    style ExtLB fill:#ffe6cc,stroke:#ff9933,stroke-width:2px
    style IntLB fill:#fff9e6,stroke:#ffcc00,stroke-width:2px
    style K8S fill:#e9ecef,stroke:#495057,stroke-width:2px
    style CP fill:#d1e7dd,stroke:#0f5132,stroke-width:2px
    style PW fill:#cfe2ff,stroke:#084298,stroke-width:2px
    style GW fill:#f8d7da,stroke:#842029,stroke-width:2px
    style PlatformComponents fill:#e7f1ff,stroke:#084298
    style PublicApps fill:#f5c6cb,stroke:#842029
    style PrivateApps fill:#f5c6cb,stroke:#842029
</code></pre>

<h3 id="key-decisions">Key Decisions</h3>

<p><strong>IaC</strong>: The whole infrastructure is version controlled. <code class="language-plaintext highlighter-rouge">terraform apply</code> if I need more nodes.</p>

<p><strong>NAT Gateway security</strong>: The only node with a public IP is the NAT gateway. SSH port (22) is blocked via firewall; only Tailscale ports are open. Subnet routes are advertised to my Tailnet from this server.</p>

<p><strong>Talos</strong>: Used <a href="https://registry.terraform.io/providers/siderolabs/talos/latest">Talos Terraform provider</a> to provision the cluster and setup a nodepool logic.</p>

<p><strong>Private-only cluster nodes</strong>: No public IPs on any Kubernetes nodes. All access goes through Tailscale.</p>

<p><strong>GitOps from day one</strong>: ArgoCD manages all platform components: <code class="language-plaintext highlighter-rouge">cert-manager</code>, <code class="language-plaintext highlighter-rouge">external-secrets</code>, <code class="language-plaintext highlighter-rouge">otel-collector</code>, etc. No more <code class="language-plaintext highlighter-rouge">kubectl apply</code>.</p>

<p><strong>Multiple “nodepools”</strong>: Workloads and platform components are deployed into different nodes based on <code class="language-plaintext highlighter-rouge">nodeSelector</code>. Workloads go to <code class="language-plaintext highlighter-rouge">purpose: general</code> and platform components go to <code class="language-plaintext highlighter-rouge">purpose: platform</code>.</p>

<p><strong>Dual ingress</strong>: Two separate ingress controllers, both with Hetzner LoadBalancers. External ingress gets a public IP for public-facing apps. Internal ingress gets a private IP for tools like ArgoCD, Grafana, and OpenWebUI. I use Cloudflare NS records to delegate the <code class="language-plaintext highlighter-rouge">int</code> subdomain to Hetzner’s nameservers, where A and CNAME records point to the internal load balancer’s private IP. Internal services are only accessible via Tailscale.</p>

<h3 id="cost-breakdown">Cost Breakdown</h3>

<p>About £50/month total:</p>
<ul>
  <li>10x servers (NAT Gateway + control plane + workers): ~£35</li>
  <li>1x public ip: ~£0.5</li>
  <li>2x Hetzner Load Balancers: £10</li>
  <li>8x 10GB storage: £4</li>
  <li>Bandwidth: 20TB included</li>
</ul>

<p>Compare this to managed Kubernetes:</p>
<ul>
  <li>AWS EKS: £65/month for control plane alone</li>
  <li>GKE/AKS: Similar pricing</li>
  <li>Add nodes, load balancers, NAT gateway: £150-200/month minimum</li>
</ul>

<p>The savings are real, but so is the learning curve.</p>

<h2 id="whats-next">What’s Next</h2>

<p>I might write more about building this platform: the network design, Talos provisioning, Cilium setup, ArgoCD workflows, and the full cost breakdown. But for now, this covers why I built it and what the architecture looks like.</p>

<p>If you’re interested in running your own Kubernetes cluster affordably, or you’re a platform engineer wanting to understand infrastructure more deeply, hopefully this gives you some inspiration.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="infrastructure" /><category term="kubernetes" /><summary type="html"><![CDATA[]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/why-i-built-my-own-kubernetes-cluster.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/why-i-built-my-own-kubernetes-cluster.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Let’s implement a Bloom Filter</title><link href="https://onatm.dev/2020/08/10/let-s-implement-a-bloom-filter/" rel="alternate" type="text/html" title="Let’s implement a Bloom Filter" /><published>2020-08-10T00:00:00+00:00</published><updated>2020-08-10T00:00:00+00:00</updated><id>https://onatm.dev/2020/08/10/let-s-implement-a-bloom-filter</id><content type="html" xml:base="https://onatm.dev/2020/08/10/let-s-implement-a-bloom-filter/"><![CDATA[<p>I am planning to create a series of blog posts that includes some literature research, implementation of various data structures and our journey of creating a distributed datastore in <a href="https://distrentic.io">distrentic.io</a>.</p>

<p>You might be wondering why I start with a blog post explaining the Bloom Filter while I don’t have single clue about how to create a distributed datastore? My answer is simple: “I like the idea behind it”.</p>

<hr />

<p>Before I get into the details of the Bloom filters, I want to give our backstory that will help you understand <strong>why we started building something we’d enjoy during our spare time that will never be production ready</strong>.</p>

<h4 id="the-backstory">The backstory</h4>

<p>My friend Ibrahim and I are always fascinated by complex software and distibuted systems - We’ve been working together more than 5 years (we got old dude) and we were  lucky enough to work for the largest e-commerce company in Europe. We battled our way solving many different problems that distributed systems can offer. We both moved to Cambridge, UK and still fighting against distributed world villains.</p>

<hr />

<p>Let’s explore the mystic land of probabilistic data structures by implementing a Bloom Filter.</p>

<h2 id="what-the-hell-is-a-bloom-filter">What the hell is a Bloom Filter</h2>

<blockquote>
  <p>You might also want to read <a href="https://gopiandcode.uk/logs/log-bloomfilters-debunked.html">Bloom filters debunked</a>.</p>
</blockquote>

<p>A Bloom filter is a method for representing a set $A = {a_1, a_2,\ldots, a_n}$ of n elements (also called keys) to support membership queries. It was invented by <strong>Burton Bloom</strong> in 1970 and was proposed for use in the web context by Marais and Bharat as a mechanism for identifying which pages have associated comments stored within a <em>CommonKnowledge</em> server. <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>It is a space-efficient probabilistic data structure that is used to answer a very simple question: <strong>is this element a member of a set?</strong>. A Bloom filter does not store the actual elements, it only stores the <strong>membership</strong> of them.</p>

<p>False positive matches are possible, but false negatives are not – in other words, a query returns either “possibly in set” or “definitely not in set”. <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> Unfortunately, this also means <strong>items cannot be removed from the Bloom Filter</strong> (Some other element or group of elements may be hashed to the same indices).</p>

<p>Because of its nature of being probabilistic, the Bloom Filter trades space and performance for accuracy. This is much like the CAP theorem, we choose performance over accuracy.</p>

<p>Bloom filters have some interesting use cases. For example, they can be placed on top of a datastore. When a key is queried for its existence and the filter does not have it, we can skip querying the datastore entirely.</p>

<pre><code class="language-mermaid">flowchart LR
    q1["Do you have&lt;br/&gt;'Key A'?"]
    q2["Do you have&lt;br/&gt;'Key B'?"]
    q3["Do you have&lt;br/&gt;'Key C'?"]

    subgraph filter["FILTER"]
        f1["Filter: No"]
        f2["Filter: Yes"]
        f3["Filter: Yes&lt;br/&gt;(false positive)"]
    end

    subgraph storage["STORAGE"]
        s2["Storage:&lt;br/&gt;Yes"]
        s3["Storage:&lt;br/&gt;No"]
    end

    q1 --&gt; f1
    f1 -.-&gt;|No| q1

    q2 --&gt; f2
    f2 --&gt;|disk access| s2
    s2 -.-&gt;|value of key B| f2
    f2 -.-&gt;|Yes| q2

    q3 --&gt; f3
    f3 --&gt;|disk access| s3
    s3 -.-&gt;|No| f3
    f3 -.-&gt;|No| q3

    style f1 fill:#cc241d,stroke:#fb4934,color:#fbf1c7
    style f2 fill:#98971a,stroke:#b8bb26,color:#fbf1c7
    style f3 fill:#98971a,stroke:#b8bb26,color:#fbf1c7
    style s2 fill:#98971a,stroke:#b8bb26,color:#fbf1c7
    style s3 fill:#cc241d,stroke:#fb4934,color:#fbf1c7
</code></pre>

<p style="text-align: center; font-style: italic; margin-top: 1rem;">Figure 1: Example usage of a Bloom filter. <a href="/assets/images/bloom_filter_example.png">Original drawing</a></p>

<h3 id="how-does-it-work">How does it work</h3>

<p>The idea behind Bloom filter is very simple: Allocate an array $v$ of $m$ bits, each bit in the array is initially set to $0$, and then choose $k$ independent hash functions $h_1, h_2, …, h_k$, each with range ${1,…,m}$.</p>

<p>The Bloom filter has two operations just like a standard set:</p>

<h4 id="insertion">Insertion</h4>

<p>When an element $a \in A$ is added to the filter, the bits at positions $h_1(a), h_2(a), …, h_k(a)$ in $v$ are set to $1$. In simpler words, the new element is hashed by $k$ number of functions and modded by $m$, resulting in $k$ indices into the bit array. Each bit at the respective index is set.</p>

<pre><code class="language-mermaid">flowchart TB
    a[a]

    h1[h₁]
    h2[h₂]
    h3[h₃]

    a --&gt;|hash| h1
    a --&gt;|hash| h2
    a --&gt;|hash| h3

    subgraph "Bit Array"
        b0[1]
        b1[0]
        b2[0]
        b3[0]
        b4[0]
        b5[0]
        b6[0]
        b7[1]
        b8[1]
        b9[0]
    end

    h1 -.-&gt;|set bit 0| b0
    h2 -.-&gt;|set bit 7| b7
    h3 -.-&gt;|set bit 8| b8

    style a fill:#ebdbb2,stroke:#3c3836,stroke-width:2px
    style h1 fill:#b16286,stroke:#d3869b
    style h2 fill:#458588,stroke:#83a598
    style h3 fill:#d65d0e,stroke:#fe8019
</code></pre>

<p style="text-align: center; font-style: italic; margin-top: 1rem;">Figure 2: Adding elements to a Bloom filter ($m = 10$, $k = 3$). <a href="/assets/images/bloom_filter_add.png">Original drawing</a></p>

<h4 id="query">Query</h4>

<p>To query the membership of an element $b$, we check the bits at indices $h_1(b), h_2(b), …, h_k(b)$ in $v$. If any of them is $0$, then certainly $b$ is not in the set $A$. Otherwise, we assume that $b$ is in the set although it’s possible that some other element or group of elements hashed to the same indices. This is called a <strong>false positive</strong>. We can target a specific probability of false positives by selecting an optimal value of $m$ and $k$ for up to $n$ insertions.</p>

<hr />

<p>A Bloom filter eventually reaches a point where all bits are set, which means every query will indicate membership, effectively making the probability of false positives $1$. The problem with this is it requires a priori knowledge of the data set in order to select optimal parameters and avoid “overfilling”. <sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<h3 id="finding-optimal-k-and-m">Finding optimal $k$ and $m$</h3>

<p>We can derive optimal $k$ and $m$ based on $n$ and a chosen probability of false positives $P_{FP}$.</p>

\[k = -\frac{\ln{P_{FP}}}{\ln{2}}
,
m = -\frac{n\ln{P_{FP}}}{(\ln2)^2}\]

<p>If you want to learn about how the above formulae are derived, you might want to pay a visit <a href="https://sagi.io/bloom-filters-for-the-perplexed/#appendix">here</a>.</p>

<h2 id="rust-implementation">Rust implementation</h2>

<blockquote>
  <p>You can find the full implementation <a href="https://github.com/distrentic/plum">here</a>.
Huge thanks to <a href="https://github.com/xfix">@xfix</a> for <a href="https://github.com/distrentic/plum/pull/1">fixing <code class="language-plaintext highlighter-rouge">hashers</code> initialization with the same seed</a> and <a href="https://github.com/dkales">@dkales</a> for spotting <a href="https://github.com/distrentic/plum/issues/2">an issue with the ordering of operations in index calculation</a>.</p>
</blockquote>

<p>Finally! It is time to write some <code class="language-plaintext highlighter-rouge">rust</code> :heart_eyes:. I am simultaneously implementing the bloom filter whilst writing this blog post. If you don’t believe me then check the below command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo new <span class="nt">--lib</span> plum
</code></pre></div></div>

<p>Let’s continue with the dependencies. There is only one dependency and we will use it to create $v$.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">[</span><span class="n">dependencies</span><span class="k">]</span>
<span class="n">bit-vec</span> <span class="o">=</span><span class="w"> </span><span class="s">"0.6"</span>
</code></pre></div></div>

<p>We will declare a new <code class="language-plaintext highlighter-rouge">struct</code> <code class="language-plaintext highlighter-rouge">StandardBloomFilter</code> to encapsulate required fields $k$ (optimal number of hash functions), $m$ (optimal size of the bit array), $v$ (the bit array), hash functions and a marker to tell rust compiler that our <code class="language-plaintext highlighter-rouge">struct</code> “owns” a <code class="language-plaintext highlighter-rouge">T</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="k">crate</span> <span class="n">bit_vec</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">bit_vec</span><span class="p">::</span><span class="n">BitVec</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">collections</span><span class="p">::</span><span class="nn">hash_map</span><span class="p">::{</span><span class="n">DefaultHasher</span><span class="p">,</span> <span class="n">RandomState</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">hash</span><span class="p">::{</span><span class="n">BuildHasher</span><span class="p">,</span> <span class="n">Hash</span><span class="p">,</span> <span class="n">Hasher</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">marker</span><span class="p">::</span><span class="n">PhantomData</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">StandardBloomFilter</span><span class="o">&lt;</span><span class="n">T</span><span class="p">:</span> <span class="o">?</span><span class="nb">Sized</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="n">bitmap</span><span class="p">:</span> <span class="n">BitVec</span><span class="p">,</span>
    <span class="n">optimal_m</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">optimal_k</span><span class="p">:</span> <span class="nb">u32</span><span class="p">,</span>
    <span class="n">hashers</span><span class="p">:</span> <span class="p">[</span><span class="n">DefaultHasher</span><span class="p">;</span> <span class="mi">2</span><span class="p">],</span>
    <span class="n">_marker</span><span class="p">:</span> <span class="n">PhantomData</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Careful readers will think that I made a mistake in the declaration of <code class="language-plaintext highlighter-rouge">hashers</code> array because of the requirement of $k$ independent hash functions. It was indeed intentional here’s why:</p>

<blockquote>
  <p><strong>Why two hash functions?</strong> Kirsch and Mitzenmacher demonstrated in their paper that using two hash functions $h_1(x)$ and $h_2(x)$ to simulate additional hash functions of the form $g_i(x) = h_1(x) + {i}{h_2(x)}$ can be usefully applied to Bloom filters. This leads to less computation and potentially less need for randomness in practice. <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup> This formula may appear similar to the use of pairwise indenpendent hash functions. Unfortunately, there is no formal connection between the two techniques.</p>
</blockquote>

<p>I mentioned earlier that the Bloom Filter has two operations like a standard set: insert and query. We will implement those two operations along with constructor-like <code class="language-plaintext highlighter-rouge">new</code> method.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span><span class="o">&lt;</span><span class="n">T</span><span class="p">:</span> <span class="o">?</span><span class="nb">Sized</span><span class="o">&gt;</span> <span class="n">StandardBloomFilter</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">items_count</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">fp_rate</span><span class="p">:</span> <span class="nb">f64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="k">Self</span> <span class="p">{</span>
        <span class="c1">// ...snip</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">new</code> calculates the size of the <code class="language-plaintext highlighter-rouge">bitmap</code> ($v$) and <code class="language-plaintext highlighter-rouge">optimal_k</code> ($k$) and then instantiates a <code class="language-plaintext highlighter-rouge">StandardBloomFilter</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span><span class="o">&lt;</span><span class="n">T</span><span class="p">:</span> <span class="o">?</span><span class="nb">Sized</span><span class="o">&gt;</span> <span class="n">StandardBloomFilter</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">items_count</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">fp_rate</span><span class="p">:</span> <span class="nb">f64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="k">Self</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">optimal_m</span> <span class="o">=</span> <span class="k">Self</span><span class="p">::</span><span class="nf">bitmap_size</span><span class="p">(</span><span class="n">items_count</span><span class="p">,</span> <span class="n">fp_rate</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">optimal_k</span> <span class="o">=</span> <span class="k">Self</span><span class="p">::</span><span class="nf">optimal_k</span><span class="p">(</span><span class="n">fp_rate</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">hashers</span> <span class="o">=</span> <span class="p">[</span>
            <span class="nn">RandomState</span><span class="p">::</span><span class="nf">new</span><span class="p">()</span><span class="nf">.build_hasher</span><span class="p">(),</span>
            <span class="nn">RandomState</span><span class="p">::</span><span class="nf">new</span><span class="p">()</span><span class="nf">.build_hasher</span><span class="p">(),</span>
        <span class="p">];</span>
        <span class="n">StandardBloomFilter</span> <span class="p">{</span>
            <span class="n">bitmap</span><span class="p">:</span> <span class="nn">BitVec</span><span class="p">::</span><span class="nf">from_elem</span><span class="p">(</span><span class="n">optimal_m</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">,</span> <span class="k">false</span><span class="p">),</span>
            <span class="n">optimal_m</span><span class="p">,</span>
            <span class="n">optimal_k</span><span class="p">,</span>
            <span class="n">hashers</span><span class="p">,</span>
            <span class="n">_marker</span><span class="p">:</span> <span class="n">PhantomData</span><span class="p">,</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// ...snip</span>

    <span class="k">fn</span> <span class="nf">bitmap_size</span><span class="p">(</span><span class="n">items_count</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">fp_rate</span><span class="p">:</span> <span class="nb">f64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">usize</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">ln2_2</span> <span class="o">=</span> <span class="nn">core</span><span class="p">::</span><span class="nn">f64</span><span class="p">::</span><span class="nn">consts</span><span class="p">::</span><span class="n">LN_2</span> <span class="o">*</span> <span class="nn">core</span><span class="p">::</span><span class="nn">f64</span><span class="p">::</span><span class="nn">consts</span><span class="p">::</span><span class="n">LN_2</span><span class="p">;</span>
        <span class="p">((</span><span class="o">-</span><span class="mf">1.0f64</span> <span class="o">*</span> <span class="n">items_count</span> <span class="k">as</span> <span class="nb">f64</span> <span class="o">*</span> <span class="n">fp_rate</span><span class="nf">.ln</span><span class="p">())</span> <span class="o">/</span> <span class="n">ln2_2</span><span class="p">)</span><span class="nf">.ceil</span><span class="p">()</span> <span class="k">as</span> <span class="nb">usize</span>
    <span class="p">}</span>

    <span class="k">fn</span> <span class="nf">optimal_k</span><span class="p">(</span><span class="n">fp_rate</span><span class="p">:</span> <span class="nb">f64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">u32</span> <span class="p">{</span>
        <span class="p">((</span><span class="o">-</span><span class="mf">1.0f64</span> <span class="o">*</span> <span class="n">fp_rate</span><span class="nf">.ln</span><span class="p">())</span> <span class="o">/</span> <span class="nn">core</span><span class="p">::</span><span class="nn">f64</span><span class="p">::</span><span class="nn">consts</span><span class="p">::</span><span class="n">LN_2</span><span class="p">)</span><span class="nf">.ceil</span><span class="p">()</span> <span class="k">as</span> <span class="nb">u32</span>
    <span class="p">}</span>

    <span class="c1">// ...snip</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s run these calculations on <a href="https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2018&amp;gist=f83331e5f3fa5be8ec52545d03afa00f">Rust Playground</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bitmap_size: 9585059
optimal_k: 7
</code></pre></div></div>

<p>This looks really promising! A Bloom Filter that represents a set of $1$ million items with a false-positive rate of $0.01$ requires only $9585059$ bits ($~1.14\mathrm{MB}$)  and 7 hash functions.</p>

<p>We managed to construct a Bloom Filter so far and it is time to implement <code class="language-plaintext highlighter-rouge">insert</code> and <code class="language-plaintext highlighter-rouge">contains</code> methods. Their implementations are dead simple and they share the same code to calculate indexes of the bit array.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span><span class="o">&lt;</span><span class="n">T</span><span class="p">:</span> <span class="o">?</span><span class="nb">Sized</span><span class="o">&gt;</span> <span class="n">StandardBloomFilter</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// ...snip</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">insert</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">item</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">T</span><span class="p">)</span>
    <span class="k">where</span>
        <span class="n">T</span><span class="p">:</span> <span class="n">Hash</span><span class="p">,</span>
    <span class="p">{</span>
        <span class="k">let</span> <span class="p">(</span><span class="n">h1</span><span class="p">,</span> <span class="n">h2</span><span class="p">)</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.hash_kernel</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>

        <span class="k">for</span> <span class="n">k_i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="k">self</span><span class="py">.optimal_k</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">index</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.get_index</span><span class="p">(</span><span class="n">h1</span><span class="p">,</span> <span class="n">h2</span><span class="p">,</span> <span class="n">k_i</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">);</span>

            <span class="k">self</span><span class="py">.bitmap</span><span class="nf">.set</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">contains</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">item</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">T</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">bool</span>
    <span class="k">where</span>
        <span class="n">T</span><span class="p">:</span> <span class="n">Hash</span><span class="p">,</span>
    <span class="p">{</span>
        <span class="k">let</span> <span class="p">(</span><span class="n">h1</span><span class="p">,</span> <span class="n">h2</span><span class="p">)</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.hash_kernel</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>

        <span class="k">for</span> <span class="n">k_i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="k">self</span><span class="py">.optimal_k</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">index</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.get_index</span><span class="p">(</span><span class="n">h1</span><span class="p">,</span> <span class="n">h2</span><span class="p">,</span> <span class="n">k_i</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">);</span>

            <span class="k">if</span> <span class="o">!</span><span class="k">self</span><span class="py">.bitmap</span><span class="nf">.get</span><span class="p">(</span><span class="n">index</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">return</span> <span class="k">false</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="k">true</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The above methods depend on two other methods that we haven’t implemented yet: <code class="language-plaintext highlighter-rouge">hash_kernel</code> and <code class="language-plaintext highlighter-rouge">get_index</code>. <code class="language-plaintext highlighter-rouge">hash_kernel</code> is going to be the one where the actual “hashing” happens. It will return the hash values of $h_1(x)$ and $h_2(x)$.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span><span class="o">&lt;</span><span class="n">T</span><span class="p">:</span> <span class="o">?</span><span class="nb">Sized</span><span class="o">&gt;</span> <span class="n">StandardBloomFilter</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// ...snip</span>

   <span class="k">fn</span> <span class="nf">hash_kernel</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">item</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">T</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="p">(</span><span class="nb">u64</span><span class="p">,</span> <span class="nb">u64</span><span class="p">)</span>
    <span class="k">where</span>
        <span class="n">T</span><span class="p">:</span> <span class="n">Hash</span><span class="p">,</span>
    <span class="p">{</span>
        <span class="k">let</span> <span class="n">hasher1</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="py">.hashers</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="nf">.clone</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">hasher2</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="py">.hashers</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="nf">.clone</span><span class="p">();</span>

        <span class="n">item</span><span class="nf">.hash</span><span class="p">(</span><span class="n">hasher1</span><span class="p">);</span>
        <span class="n">item</span><span class="nf">.hash</span><span class="p">(</span><span class="n">hasher2</span><span class="p">);</span>

        <span class="k">let</span> <span class="n">hash1</span> <span class="o">=</span> <span class="n">hasher1</span><span class="nf">.finish</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">hash2</span> <span class="o">=</span> <span class="n">hasher2</span><span class="nf">.finish</span><span class="p">();</span>

        <span class="p">(</span><span class="n">hash1</span><span class="p">,</span> <span class="n">hash2</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We could’ve used $128$ bit <a href="https://en.wikipedia.org/wiki/MurmurHash#MurmurHash3">MurmurHash3</a> and returned upper $64$ bit as <code class="language-plaintext highlighter-rouge">hash1</code> and the lower as <code class="language-plaintext highlighter-rouge">hash2</code> but to keep this implementation even simpler (this is how <a href="https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilterStrategies.java">Google Guava Bloom Filter implementation</a> currently works) and not to rely on any other additional dependencies I decided to continue with <code class="language-plaintext highlighter-rouge">DefaultHasher</code> - see <a href="https://en.wikipedia.org/wiki/SipHash">SipHash</a></p>

<p>Now, it is time to make the final touch. We are going to implement <code class="language-plaintext highlighter-rouge">get_index</code> by using $g_i(x) = h_1(x) + {i}{h_2(x)}$ to simulate more than two hash functions.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span><span class="o">&lt;</span><span class="n">T</span><span class="p">:</span> <span class="o">?</span><span class="nb">Sized</span><span class="o">&gt;</span> <span class="n">StandardBloomFilter</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// ...snip</span>

    <span class="k">fn</span> <span class="nf">get_index</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">h1</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="n">h2</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="n">k_i</span><span class="p">:</span> <span class="nb">u64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">usize</span> <span class="p">{</span>
        <span class="p">(</span><span class="n">h1</span><span class="nf">.wrapping_add</span><span class="p">((</span><span class="n">k_i</span><span class="p">)</span><span class="nf">.wrapping_mul</span><span class="p">(</span><span class="n">h2</span><span class="p">))</span> <span class="o">%</span> <span class="k">self</span><span class="py">.optimal_m</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span>
    <span class="p">}</span>

    <span class="c1">// ...snip</span>
</code></pre></div></div>

<h3 id="we-are-finally-there">We are finally there</h3>

<p>:tada: :tada: :tada: We’ve just finished implementing <strong>a fast variant of a standard Bloom Filter</strong> but there is still one thing missing - We didn’t write any tests.</p>

<p>Let’s add two simple test cases and validate our implementation.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[cfg(test)]</span>
<span class="k">mod</span> <span class="n">tests</span> <span class="p">{</span>
    <span class="k">use</span> <span class="k">super</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>

    <span class="nd">#[test]</span>
    <span class="k">fn</span> <span class="nf">insert</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">bloom</span> <span class="o">=</span> <span class="nn">StandardBloomFilter</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mf">0.01</span><span class="p">);</span>
        <span class="n">bloom</span><span class="nf">.insert</span><span class="p">(</span><span class="s">"item"</span><span class="p">);</span>
        <span class="nd">assert!</span><span class="p">(</span><span class="n">bloom</span><span class="nf">.contains</span><span class="p">(</span><span class="s">"item"</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="nd">#[test]</span>
    <span class="k">fn</span> <span class="nf">check_and_insert</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">bloom</span> <span class="o">=</span> <span class="nn">StandardBloomFilter</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mf">0.01</span><span class="p">);</span>
        <span class="nd">assert!</span><span class="p">(</span><span class="o">!</span><span class="n">bloom</span><span class="nf">.contains</span><span class="p">(</span><span class="s">"item_1"</span><span class="p">));</span>
        <span class="nd">assert!</span><span class="p">(</span><span class="o">!</span><span class="n">bloom</span><span class="nf">.contains</span><span class="p">(</span><span class="s">"item_2"</span><span class="p">));</span>
        <span class="n">bloom</span><span class="nf">.insert</span><span class="p">(</span><span class="s">"item_1"</span><span class="p">);</span>
        <span class="nd">assert!</span><span class="p">(</span><span class="n">bloom</span><span class="nf">.contains</span><span class="p">(</span><span class="s">"item_1"</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ cargo <span class="nb">test
   </span>Compiling plum v0.1.2 <span class="o">(</span>/Users/onat.mercan/dev/distrentic/plum<span class="o">)</span>
    Finished <span class="nb">test</span> <span class="o">[</span>unoptimized + debuginfo] target<span class="o">(</span>s<span class="o">)</span> <span class="k">in </span>0.99s
     Running target/debug/deps/plum-6fc161db530d5b36

running 2 tests
<span class="nb">test </span>tests::insert ... ok
<span class="nb">test </span>tests::check_and_insert ... ok

<span class="nb">test </span>result: ok. 2 passed<span class="p">;</span> 0 failed<span class="p">;</span> 0 ignored<span class="p">;</span> 0 measured<span class="p">;</span> 0 filtered out
</code></pre></div></div>

<p>I hope you’ve enjoyed reading this post as much as I enjoyed writing it!</p>

<p>If you find anything wrong with <a href="https://github.com/distrentic/plum">the code</a>, you can <a href="https://github.com/distrentic/plum/issues">file an issue</a> or, even better, <a href="https://github.com/distrentic/plum/pulls">submit a pull request</a>.</p>

<p><a href="https://news.ycombinator.com/item?id=24102617">Discuss it on HN</a></p>

<h2 id="further-reading">Further Reading</h2>

<ul>
  <li><a href="https://www.stavros.io/posts/bloom-filter-search-engine/">Writing a full-text search engine using Bloom filters - Stavros’ Stuff</a></li>
  <li><a href="https://llimllib.github.io/bloomfilter-tutorial/">Bloom Filters by Example</a></li>
  <li><a href="https://engineering.indeedblog.com/blog/2013/10/serving-over-1-billion-documents-per-day-with-docstore-v2/">Serving over 1 billion documents per day with Docstore v2 - Indeed Engineering Blog</a></li>
  <li><a href="https://github.com/wiredtiger/wiredtiger/wiki/LSMTrees-Bloom">LSMTrees+Bloom - wiredtiger/wiredtiger</a></li>
  <li><a href="https://www.sciencedirect.com/science/article/pii/S2212017316301591">SLSM - A Scalable Log Structured Merge Tree with Bloom Filters for Low Latency Analytics - ScienceDirect</a></li>
  <li><a href="https://nivdayan.github.io/monkey-journal.pdf">Optimal Bloom Filters and Adaptive Merging for LSM-Trees</a></li>
  <li><a href="http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf">Approximately Detecting Duplicates for Streaming Data
using Stable Bloom Filters</a></li>
</ul>

<h2 id="resources">Resources</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p><a href="http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html">Bloom Filters - the math</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p><a href="https://en.wikipedia.org/wiki/Bloom_filter">Bloom filter - Wikipedia</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p><a href="https://bravenewgeek.com/stream-processing-and-probabilistic-methods/">Stream Processing and Probabilistic Methods: Data at Scale – Brave New Geek</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p><a href="https://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf">Less Hashing, Same Performance: Building a Better Bloom Filter</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Onat Yigit Mercan</name></author><category term="rust" /><category term="data structure" /><category term="bloom filter" /><category term="distributed" /><category term="datastore" /><category term="distrentic" /><summary type="html"><![CDATA[I am planning to create a series of blog posts that includes some literature research, implementation of various data structures and our journey of creating a distributed datastore in distrentic.io.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/let-s-implement-a-bloom-filter.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/let-s-implement-a-bloom-filter.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">What I learned from my failed attempt of writing baremetal android in Rust</title><link href="https://onatm.dev/2019/04/22/what-i-learned-from-my-failed-attempt-of-writing-baremetal-android-in-rust/" rel="alternate" type="text/html" title="What I learned from my failed attempt of writing baremetal android in Rust" /><published>2019-04-22T12:35:34+00:00</published><updated>2019-04-22T12:35:34+00:00</updated><id>https://onatm.dev/2019/04/22/what-i-learned-from-my-failed-attempt-of-writing-baremetal-android-in-rust</id><content type="html" xml:base="https://onatm.dev/2019/04/22/what-i-learned-from-my-failed-attempt-of-writing-baremetal-android-in-rust/"><![CDATA[<blockquote>
  <p>This post is focused mostly on the tools that I use while I failed to write a bootable kernel image in <code class="language-plaintext highlighter-rouge">rust</code>.</p>
</blockquote>

<p>Every year I define a super ambitious goal for my learning process to keep myself motivated on the way. This year I defined my goal as <strong>writing a bootable kernel image for my old HTC One X android smartphone</strong>. I knew it was going to be hard but I never thought I’d fail in the end. It was clearly the <a href="https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect">Dunning–Kruger effect</a> that made me think that I can achieve what I want to do with my limited knowledge/experience on the subject.</p>

<h2 id="prior-work">Prior Work</h2>

<p>Let’s start by looking into the projects that have been done to run <code class="language-plaintext highlighter-rouge">baremetal</code> code on android smartphones. Unfortunately, I managed to find only <strong>two</strong> projects out in the wild.</p>

<p>The first project (<a href="https://github.com/zhuowei/nexus7-baremetal">nexus7-baremetal</a>) made me really excited because I thought nobody would ever care about writing baremetal android and also it was the only resource I had found until I gave up. The project contains some code from <a href="https://github.com/dwelch67/raspberrypi/tree/master/bootloader05">raspberrypi/bootloader05</a>. This is because of the shared type of <strong>CPU</strong> family between <strong>Raspberry Pi 2</strong> and <strong>Nexus 7</strong> (and HTC One X as well) which happens to be <a href="https://en.wikipedia.org/wiki/ARM_Cortex-A7">ARM Cortex-A7</a>.</p>

<p>The second project is <a href="https://github.com/M1cha/lktris"><code class="language-plaintext highlighter-rouge">lktris</code></a>. The only thing makes this project interesting is it is built on top of <a href="https://github.com/littlekernel/lk"><code class="language-plaintext highlighter-rouge">littlekernel</code></a>.</p>

<p>I wanted to try <code class="language-plaintext highlighter-rouge">nexus7-baremetal</code> project before I dive into writing my own code in <code class="language-plaintext highlighter-rouge">rust</code> but I couldn’t manage to run it successfully even though <a href="https://github.com/zhuowei/nexus7-baremetal/issues/1#issuecomment-476751437">I told the author of the project the opposite</a>. I thought it would be rude to make him waste his time on a project that he wrote 6 years ago and I wanted to do more research to understand the issue without any hand-holding.</p>

<p>I spent sometime to refresh my knowledge about <code class="language-plaintext highlighter-rouge">android-ndk</code> and <code class="language-plaintext highlighter-rouge">android-sdk</code> to be able to compile and <em>unsuccessfully</em> run <code class="language-plaintext highlighter-rouge">nexus7-baremetal</code>. It’s a bit pain to install standalone android toolchain on macOS and installing platforms, platform tools and emulators is just whole another story that I don’t want to talk about. The below command just shows how badly android <code class="language-plaintext highlighter-rouge">sdkmanager</code> cli is designed:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sdkmanager <span class="s2">"system-images;android-19;google_apis;armeabi-v7a"</span>
</code></pre></div></div>

<p>If you really want to use android standalone toolchain on macOS, you can run the following:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># install android standalone toolchain</span>
brew <span class="nb">install </span>intel-haxm
brew <span class="nb">install </span>android-sdk
brew <span class="nb">install </span>android-ndk

<span class="c"># update env vars</span>
<span class="nb">export </span><span class="nv">ANDROID_HOME</span><span class="o">=</span>/usr/local/share/android-sdk
<span class="nb">export </span><span class="nv">ANDROID_NDK_HOME</span><span class="o">=</span>/usr/local/share/android-ndk

<span class="c"># update path</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="nv">$ANDROID_HOME</span>/tools:<span class="nv">$PATH</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="nv">$ANDROID_HOME</span>/platform-tools:<span class="nv">$PATH</span>
</code></pre></div></div>

<h1 id="what-i-learned">What I learned</h1>

<h2 id="little-kernel">Little Kernel</h2>

<p>LK (Little Kernel) is a tiny operating system suited for small embedded devices, bootloaders, and other environments where OS primitives like threads, mutexes, and timers are needed. It also initializes the most important hardware such as MMU and UART.</p>

<p>LK is the Android bootloader and is also used in <strong>Android Trusted Execution Environment</strong> - “Trusty TEE” Operating System.</p>

<p>Android bootloader supports specially packed <strong>Android Boot Images</strong> only. These files contain the <strong>kernel</strong>, a <strong>ramdisk</strong> (root filesystem) and some metadata. The file header of these images includes sizes of all packaged files and the loading address of the kernel.</p>

<p>This header has a size of <code class="language-plaintext highlighter-rouge">0x8000</code> bytes followed by the kernel image. That’s why the loading address needs to be set to <code class="language-plaintext highlighter-rouge">KERNEL_LOADING_ADDRESS</code> - <code class="language-plaintext highlighter-rouge">0x8000</code> to get LK to the right place.</p>

<h3 id="0x8000">0x8000</h3>

<p><code class="language-plaintext highlighter-rouge">0x8000</code> (<code class="language-plaintext highlighter-rouge">32K</code>) is in fact the size of an offset that leaves space for the parameter block in ARM architecture.</p>

<p>According to <a href="http://www.simtec.co.uk/products/SWLINUX/files/booting_article.html">the ARM booting procedures</a>:</p>

<blockquote>
  <p>Despite the ability to place zImage anywhere within memory, convention has it that it is loaded at the base of physical RAM plus an offset of 0x8000 (32K). This leaves space for the parameter block usually placed at offset 0x100, zero page exception vectors and page tables. This convention is very common.</p>
</blockquote>

<h2 id="rust-cross-compilation">Rust Cross Compilation</h2>

<p>It’s a bit complicated to cross-compile rust binaries on macOS for <code class="language-plaintext highlighter-rouge">armv7</code> and you probably knew it already. However, I am ignorant and stubborn and I battled my way to get a proper armv7 toolchain for my macbook. All I wanted to do was just to compile my project to <code class="language-plaintext highlighter-rouge">armv7-unknown-linux-gnueabihf</code> platform.</p>

<p>The first thing I’ve done was madly downloading all the packages I’ve found for Homebrew because I didn’t want to deal with <a href="https://github.com/crosstool-ng/crosstool-ng">crosstool-ng</a>. Nevertheless, I end up installing it and after many failed attempts of building <code class="language-plaintext highlighter-rouge">armv7-rpi2-linux-gnueabihf</code>, I realized that <a href="http://crosstool-ng.github.io/docs/os-setup/#macos-aka-mac-os-x-os-x">macOS is no longer supported by crosstool-ng</a>.</p>

<p>I deciced to do what any <em>sane</em> person would do and fired up a <code class="language-plaintext highlighter-rouge">vagrant</code> machine, installed all the toolchains needed and finally, the dysfunctional kernel image was compiled and linked successfully.</p>

<p>Why would I use a VM just to compile a binary? We are in 2019, right? I would have been OK if it was a container but this is a <strong>HUGE</strong> VM!</p>

<p>I went straight back to the list of Homebrew packages and figured out the only way to compile and link my kernel image is targetting <code class="language-plaintext highlighter-rouge">armv7-unknown-linux-musleabihf</code> by installing <code class="language-plaintext highlighter-rouge">arm-linux-gnueabihf-binutils</code>. Some would disagree my decision to use <code class="language-plaintext highlighter-rouge">musl</code> toolchain considering that baremetal code doesn’t need <code class="language-plaintext highlighter-rouge">libc</code> but it was the only viable way for me at that time and if you know a better way (you probably know), please let me know because I don’t have much knowledge about cross-compilation of low-level languages.</p>

<h3 id="rust-targets">Rust targets</h3>

<p>There is a list of all the <a href="https://forge.rust-lang.org/platform-support.html">available supported platforms</a> and you can easily add any of them by using <code class="language-plaintext highlighter-rouge">rustup</code>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rustup target add armv7-unknown-linux-musleabihf
</code></pre></div></div>

<p>To compile your program for a specific target you can either use <code class="language-plaintext highlighter-rouge">cargo</code> with <code class="language-plaintext highlighter-rouge">--target</code> flag:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo build <span class="nt">--target</span><span class="o">=</span>armv7-unknown-linux-musleabihf
</code></pre></div></div>

<p>or create <code class="language-plaintext highlighter-rouge">.cargo/config</code> file:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>build]
target <span class="o">=</span> <span class="s2">"armv7-unknown-linux-musleabihf"</span>
</code></pre></div></div>

<h3 id="cargo-binutils">cargo-binutils</h3>

<p><a href="https://github.com/rust-embedded/cargo-binutils"><code class="language-plaintext highlighter-rouge">cargo-binutils</code></a> is a pretty handy plugin if you need to use <code class="language-plaintext highlighter-rouge">LLVM</code> tools for binary inspection and manipulation. It simply proxies the LLVM tools in the <code class="language-plaintext highlighter-rouge">llvm-tools-preview</code> <code class="language-plaintext highlighter-rouge">rustup</code> component and provides subcommands to invoke any of the tools.</p>

<p>Most of the tools in <code class="language-plaintext highlighter-rouge">llvm-tools-preview</code> are LLVM alternatives to GNU <code class="language-plaintext highlighter-rouge">binutils</code>. The main advantage of these LLVM tools is that they support all the architectures that the Rust compiler supports.</p>

<h3 id="rust-inline-assembly">Rust inline assembly</h3>

<p>Currently, there are two feature gated ways to write assembly: <code class="language-plaintext highlighter-rouge">nasm!</code> (requires <code class="language-plaintext highlighter-rouge">#![feature(asm)]</code>) and <code class="language-plaintext highlighter-rouge">global_asm!</code> (requires <code class="language-plaintext highlighter-rouge">#![feature(global_asm)])</code> macros.</p>

<h4 id="asm">asm</h4>

<p><a href="https://doc.rust-lang.org/1.8.0/book/inline-assembly.html"><code class="language-plaintext highlighter-rouge">nasm!</code></a> uses the same basic format as <code class="language-plaintext highlighter-rouge">GCC</code> uses for its own inline <code class="language-plaintext highlighter-rouge">nasm</code> and restricts your inline assembly to <code class="language-plaintext highlighter-rouge">fn</code> bodies only. The syntax isn’t the best:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">asm!</span><span class="p">(</span><span class="n">assembly</span> <span class="n">template</span>
   <span class="p">:</span> <span class="n">output</span> <span class="n">operands</span>
   <span class="p">:</span> <span class="n">input</span> <span class="n">operands</span>
   <span class="p">:</span> <span class="n">clobbers</span>
   <span class="p">:</span> <span class="n">options</span>
   <span class="p">);</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">assembly template</code> is the only required parameter and must be a literal string. Here’s an example (taken from the rust book):</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#![feature(asm)]</span>

<span class="k">fn</span> <span class="nf">foo</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span>
        <span class="nd">asm!</span><span class="p">(</span><span class="s">"NOP"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// ...</span>
    <span class="nf">foo</span><span class="p">();</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="global_asm">global_asm</h4>

<p><a href="https://doc.rust-lang.org/unstable-book/language-features/global-asm.html"><code class="language-plaintext highlighter-rouge">global_asm!</code></a> gives you ability to write arbitrary assembly without the restriction of <code class="language-plaintext highlighter-rouge">fn</code> bodies.</p>

<p>A simple usage looks like this:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">global_asm!</span><span class="p">(</span><span class="nd">include_str!</span><span class="p">(</span><span class="s">"boot.S"</span><span class="p">));</span>
</code></pre></div></div>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">.section</span> <span class="s">".text.boot"</span>

<span class="nf">.globl</span> <span class="nv">_boot</span>

<span class="nl">_boot:</span>
    <span class="nf">bl</span>      <span class="nv">not_main</span>

<span class="nf">.section</span> <span class="nv">.text</span>

<span class="nf">.globl</span> <span class="nv">_put32</span>

<span class="nl">_put32:</span>
    <span class="nf">str</span>     <span class="nv">r1</span><span class="p">,[</span><span class="nv">r0</span><span class="p">]</span>
    <span class="nf">bx</span>      <span class="nv">lr</span>
</code></pre></div></div>

<h3 id="using-extern-functions-to-call-assembly-code">Using <code class="language-plaintext highlighter-rouge">extern</code> Functions to Call Assembly Code</h3>

<p><code class="language-plaintext highlighter-rouge">extern</code> keyword facilitates the creation and use of a <strong>Foreign Function Interface (FFI)</strong>. The below example demonstrates how to set up an integration with <code class="language-plaintext highlighter-rouge">_put32</code> function in <code class="language-plaintext highlighter-rouge">boot.S</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="s">"C"</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">_put32</span><span class="p">(</span><span class="n">f</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">u32</span><span class="p">,</span> <span class="n">c</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">u8</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="o">!</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span>
        <span class="nf">_put32</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">0xFF002000</span><span class="p">,</span> <span class="o">&amp;</span><span class="mi">72</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">loop</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="calling-rust-functions-from-assembly-code">Calling Rust Functions from Assembly Code</h3>

<p><code class="language-plaintext highlighter-rouge">extern</code> also has another usage that allows us create an interface for other languages to call Rust functions. You need to add <code class="language-plaintext highlighter-rouge">extern</code> keyword and specify the ABI to use just before the <code class="language-plaintext highlighter-rouge">fn</code> keyword. We also need to add a <code class="language-plaintext highlighter-rouge">#[no_mangle]</code> annotation to tell the Rust compiler not to mangle the name of this function.</p>

<p>In the below example, we make <code class="language-plaintext highlighter-rouge">not_main</code> function accessible from <code class="language-plaintext highlighter-rouge">boot.S</code> file:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[no_mangle]</span>
<span class="k">pub</span> <span class="k">unsafe</span> <span class="k">extern</span> <span class="s">"C"</span> <span class="k">fn</span> <span class="nf">not_main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="o">!</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>

<h2 id="epilogue">Epilogue</h2>

<p>That’s it! I consider this work as a huge win even though I failed to write a functional bootable image. I learnt to use quite useful tools on the way and now I have a better understanding around cross compilation.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="rust" /><category term="baremetal" /><category term="assembly" /><category term="android" /><category term="arm" /><summary type="html"><![CDATA[This post is focused mostly on the tools that I use while I failed to write a bootable kernel image in rust.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/what-i-learned-from-my-failed-attempt-of-writing-baremetal-android-in-rust.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/what-i-learned-from-my-failed-attempt-of-writing-baremetal-android-in-rust.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Anatomy of a Hack assembly program - Part 2</title><link href="https://onatm.dev/2019/04/07/anatomy-of-a-hack-assembly-program-part-2/" rel="alternate" type="text/html" title="Anatomy of a Hack assembly program - Part 2" /><published>2019-04-07T18:52:18+00:00</published><updated>2019-04-07T18:52:18+00:00</updated><id>https://onatm.dev/2019/04/07/anatomy-of-a-hack-assembly-program-part-2</id><content type="html" xml:base="https://onatm.dev/2019/04/07/anatomy-of-a-hack-assembly-program-part-2/"><![CDATA[<p><em>This is the second part of ‘Anatomy of a Hack assembly program’ series.</em></p>

<hr />

<ul>
  <li><a href="/2019/04/05/Anatomy-of-a-Hack-assembly-program-Part-1/">First Part</a></li>
  <li>Second Part</li>
</ul>

<hr />

<p>In the first part, we learnt the details about Hack hardware platform. Now, it is a good time to deep dive into <strong>Hack assembly</strong> language before we understand how binary instructions flow through the CPU.</p>

<h1 id="hack-assembly">Hack Assembly</h1>

<p>The Hack Assembly Language is minimal, it consists of 2 types of instructions: <strong>A-Instruction</strong> (Addressing instructions), and <strong>C-Instruction</strong> (Computation instructions). It also allows declaration of symbols.</p>

<h2 id="a-instruction">A-Instruction</h2>

<p>Sets the contents of the A register to the specified value. The value is either a non-negative number (i.e. 3) or a Symbol. If the value is a Symbol, then the contents of the A register is set to the value that the Symbol refers to but not the actual data in that Register or Memory Location.</p>

<h3 id="syntax">Syntax</h3>

<p><code class="language-plaintext highlighter-rouge">@value</code>, where value is either a decimal non-negative number or a Symbol.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">@3</code></li>
  <li><code class="language-plaintext highlighter-rouge">@R3</code></li>
  <li><code class="language-plaintext highlighter-rouge">@SCREEN</code></li>
</ul>

<h3 id="binary-translation">Binary Translation</h3>

<p><code class="language-plaintext highlighter-rouge">0xxxxxxxxxxxxxxx</code>, where <code class="language-plaintext highlighter-rouge">x</code> is a bit, either 0 or 1. A-Instructions always have their MSB set to 0.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">000000000001010</code></li>
  <li><code class="language-plaintext highlighter-rouge">011111111111111</code></li>
</ul>

<h2 id="c-instruction">C-Instruction</h2>

<p>Performs a computation on the CPU and stores the output in a register or memory address, and then either jumps to an instruction location that is usually addressed by a symbol or continues with the next instruction.</p>

<h2 id="symbols">Symbols</h2>

<p>Symbols can be either variables or labels. Variables are symbolic names for memory addresses to make accessing these addresses easier. Labels are instruction addresses that allow jumps in the program easier to handle. There are three ways to introduce symbols into an assembly program: Predefined symbols, label symbols, and variable symbols.</p>

<h3 id="predefined-symbols">Predefined Symbols</h3>

<p>A special subset of <strong>RAM</strong> addresses can be referred to by any assembly program.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SP</code>: RAM address 0</li>
  <li><code class="language-plaintext highlighter-rouge">LCL</code>: RAM address 1</li>
  <li><code class="language-plaintext highlighter-rouge">ARG</code>: RAM address 2</li>
  <li><code class="language-plaintext highlighter-rouge">THIS</code>: RAM address 3</li>
  <li><code class="language-plaintext highlighter-rouge">THAT</code>: RAM address 4</li>
  <li><code class="language-plaintext highlighter-rouge">R0</code>-<code class="language-plaintext highlighter-rouge">R15</code>: Addresses of 16 RAM Registers, mapped from 0 to 15</li>
  <li><code class="language-plaintext highlighter-rouge">SCREEN</code>: Base address of the Screen Map in Main Memory, which is equal to 16384</li>
  <li><code class="language-plaintext highlighter-rouge">KBD</code>: Keyboard Register address in Main Memory, which is equal to 24576</li>
</ul>

<h3 id="label-symbols">Label Symbols</h3>

<p>To declare a label we need to use the command <code class="language-plaintext highlighter-rouge">(LABEL_NAME)</code>, where <strong>LABEL_NAME</strong> can be any name we desire to have for the label, as long as it’s wraped between parentheses.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(LOOP)
// instruction 1
// instruction 2
// instruction 3
@LOOP
0;JMP
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">(LOOP)</code> declares a new label called <strong>LOOP</strong>, it will be resolved to the address of the next instruction on the following line. The instruction <code class="language-plaintext highlighter-rouge">@LOOP</code> is an <strong>A-Instruction</strong> that sets the contents of A Register to the instruction address the label refers to.</p>

<h3 id="variable-symbols">Variable Symbols</h3>

<p>Any user-defined symbol <code class="language-plaintext highlighter-rouge">@variable</code> that is not predefined using <code class="language-plaintext highlighter-rouge">(variable)</code> command is treated as a variable, and is assigned a unique memory address, starting at <strong>RAM</strong> address 16 (0x0010).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@i
M=0
</code></pre></div></div>

<p>The symbol <code class="language-plaintext highlighter-rouge">@i</code> declares a variable <strong>i</strong>, and the instruction <code class="language-plaintext highlighter-rouge">M=0</code> sets the memory location of <strong>i</strong> in RAM to 0, the address <strong>i</strong> is stored in <strong>A-Register</strong>.</p>

<hr />

<p>That’s it for the second part. In the next part I will explain how the CU (<em>control unit</em>) decodes an instruction and how the decoded instruction flows through the CPU.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="hack" /><category term="assembly" /><summary type="html"><![CDATA[This is the second part of ‘Anatomy of a Hack assembly program’ series.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/anatomy-of-a-hack-assembly-program-part-2.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/anatomy-of-a-hack-assembly-program-part-2.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Anatomy of a Hack assembly program - Part 1</title><link href="https://onatm.dev/2019/04/05/anatomy-of-a-hack-assembly-program-part-1/" rel="alternate" type="text/html" title="Anatomy of a Hack assembly program - Part 1" /><published>2019-04-05T15:16:27+00:00</published><updated>2019-04-05T15:16:27+00:00</updated><id>https://onatm.dev/2019/04/05/anatomy-of-a-hack-assembly-program-part-1</id><content type="html" xml:base="https://onatm.dev/2019/04/05/anatomy-of-a-hack-assembly-program-part-1/"><![CDATA[<p><em>This blog series is based on nand2tetris book.</em></p>

<blockquote>
  <p>I don’t have a comprehensive knowledge of hardware nor low-level programming. However, I have been learning this so long mistery part of computers since the last Summer. I will try to do my best to explain how a Hack assembly is translated into binary instructions and how the Hack machine does process a single instruction in a <strong>fetch and execute</strong> loop.</p>
</blockquote>

<hr />

<ul>
  <li>First Part</li>
  <li><a href="/2019/04/07/Anatomy-of-a-Hack-assembly-program-Part-2">Second Part</a></li>
</ul>

<hr />

<p>I was always fascinated by how the operating system orchestrates all the components on a computer but I’ve never previously had the chance to learn the low-level details of this hidden world. Since last summer, I’ve started to explore and uncover the details of this beautiful yet complex landscape and I want to share what I learnt so far from the books I read.</p>

<p>The first book I started to read was <strong>the Elements of Computing Systems</strong> (AKA <em>nand2tetris</em>) which has amazing content that uncovers most of the topics I always wanted to learn. In order to reinforce what I learnt from the book, I decided to write about how a Hack assembly program flows through hardware. I will try to do my best to explain the details an emphasize on the parts that I think really crucial.</p>

<p>Before we dive into a Hack assembly program, let’s look into to specification of the Hack hardware platform.</p>

<h1 id="the-hack-hardware-platform-specification">The Hack Hardware Platform Specification</h1>

<p>The Hack platform is a <strong>16-bit von Neumann machine</strong>, designed to execute programs written in the Hack machine language. In order to do so, the Hack platform consists of a <strong>CPU</strong>, two separate memory modules serving as <strong>instruction memory</strong> and <strong>data memory</strong>, and <strong>two memory-mapped I/O devices</strong>: a screen and a keyboard.</p>

<p>The Hack CPU consists of the <strong>ALU</strong> and three registers called <strong>data register (D), address register (A), and program counter (PC)</strong>. While the <strong>D-register</strong> is used solely for storing data values, the <strong>A-register</strong> serves three different purposes, depending on the context in which it is used: storing a data value (just like the D-register), pointing at an address in the instruction memory, or pointing at an address in the data memory.</p>

<h2 id="cpu---parts">CPU - Parts</h2>

<p>In order to implement the Hack CPU, we need an ALU chip capable of computing arithmetic/logical functions, a set of registers, a program counter, and some additional gates (Control Unit) designed to help decode, execute, and fetch instructions.</p>

<h3 id="alu-arithmetic-logic-unit">ALU (Arithmetic Logic Unit)</h3>

<p>This is the part where actual processing (or the magic) happens. The Hack ALU computes a fixed set of functions <code class="language-plaintext highlighter-rouge">out = fi(x, y)</code> where x and y are the chip’s two 16-bit inputs, <code class="language-plaintext highlighter-rouge">out</code> is the chip’s 16-bit output, and <code class="language-plaintext highlighter-rouge">fi</code> is an arithmetic or logical function selected from 18 possible functions. We instruct the ALU which function to compute by setting six input bits, called control bits. The ALU can potentially compute 64 (2^6) different functions.</p>

<p><a href="https://en.wikipedia.org/wiki/Two%27s_complement">Two’s complement</a> is used as the method of signed number representation. It allows computing of operations such as <code class="language-plaintext highlighter-rouge">x-1</code> with ease: When <code class="language-plaintext highlighter-rouge">zy</code> and <code class="language-plaintext highlighter-rouge">ny</code> bits are <code class="language-plaintext highlighter-rouge">1</code>, the <code class="language-plaintext highlighter-rouge">y</code> input is first zeroed, and then negated bit-wise. Bit-wise negation of zero gives the 2’s complement binary value of <code class="language-plaintext highlighter-rouge">-1</code>.</p>

<h4 id="specification">Specification</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Chip name: ALU

Inputs:    x[16], y[16],                    // Two 16-bit data inputs
           zx,                              // Zero the x input
           nx,                              // Negate the x input
           zy,                              // Zero the y input
           ny,                              // Negate the y input
           f,                               // Function code: 1 for Add, 0 for And
           no                               // Negate the out output

Outputs:   out[16],                         // 16-bit output
           zr,                              // True iff out=0
           ng                               // True iff out&lt;0

Function:  if zx then x = 0                 // 16-bit zero constant
           if nx then x = !x                // Bit-wise negation
           if zy then y = 0                 // 16-bit zero constant
           if ny then y = !y                // Bit-wise negation
           if f then out = x + y            // Integer 2's complement addition
                else out = x &amp; y            // Bit-wise And
           if no then out = !out            // Bit-wise negation
           if out=0 then zr = 1 else zr = 0 // 16-bit eq. comparison
           if out&lt;0 then ng = 1 else ng = 0 // 16-bit neg. comparison

Comment:   Overflow is neither detected nor handled.
</code></pre></div></div>

<p>The above specification gives a clear idea of the implementation of the ALU. We only need a 16-bit Adder chip and a couple of logic gates including 16-bit Multiplexor, 16-bit NOT, 16-bit AND, 8-way OR, OR, and NOT.</p>

<p><img src="/assets/images/hack_alu.png" alt="hack alu" /></p>

<p>Figure 1: Arithmetic Logic Unit. (Taken from The Elements of Computing Systems, <a href="https://docs.wixstatic.com/ugd/44046b_f0eaab042ba042dcb58f3e08b46bb4d7.pdf">Chapter 2</a>)</p>

<p>ALU computes one of the following instructions: <code class="language-plaintext highlighter-rouge">x+y</code>, <code class="language-plaintext highlighter-rouge">x-y</code>, <code class="language-plaintext highlighter-rouge">y-x</code>, <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">-1</code>, <code class="language-plaintext highlighter-rouge">x</code>, <code class="language-plaintext highlighter-rouge">y</code>, <code class="language-plaintext highlighter-rouge">-x</code>, <code class="language-plaintext highlighter-rouge">-y</code>, <code class="language-plaintext highlighter-rouge">!x</code>, <code class="language-plaintext highlighter-rouge">!y</code>, <code class="language-plaintext highlighter-rouge">x+1</code>, <code class="language-plaintext highlighter-rouge">y+1</code>, <code class="language-plaintext highlighter-rouge">x-1</code>, <code class="language-plaintext highlighter-rouge">y-1</code>, <code class="language-plaintext highlighter-rouge">x&amp;y</code>, <code class="language-plaintext highlighter-rouge">x|y</code> on two 16-bit inputs, according to 6 input bits denoted by <code class="language-plaintext highlighter-rouge">zx</code>, <code class="language-plaintext highlighter-rouge">nx</code>, <code class="language-plaintext highlighter-rouge">zy</code>, <code class="language-plaintext highlighter-rouge">ny</code>, <code class="language-plaintext highlighter-rouge">f</code>, <code class="language-plaintext highlighter-rouge">no</code>. In addition, ALU computes two 1-bit outputs: if ALU output is <code class="language-plaintext highlighter-rouge">0</code> then <code class="language-plaintext highlighter-rouge">zr</code> is set to <code class="language-plaintext highlighter-rouge">1</code>, otherwise <code class="language-plaintext highlighter-rouge">zr</code> is set to <code class="language-plaintext highlighter-rouge">0</code>; if <code class="language-plaintext highlighter-rouge">out&lt;0</code> then <code class="language-plaintext highlighter-rouge">ng</code> is set to <code class="language-plaintext highlighter-rouge">1</code> otherwise <code class="language-plaintext highlighter-rouge">ng</code> is set to <code class="language-plaintext highlighter-rouge">0</code>.</p>

<p>The below is an example implementation of the ALU in <a href="https://en.wikipedia.org/wiki/Hardware_description_language">HDL (hardware description language)</a>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/02/ALU.hdl

// Implementation: the ALU manipulates the x and y
// inputs and then operates on the resulting values,
// as follows:
// if (zx==1) set x = 0        // 16-bit constant
// if (nx==1) set x = ~x       // bitwise "not"
// if (zy==1) set y = 0        // 16-bit constant
// if (ny==1) set y = ~y       // bitwise "not"
// if (f==1)  set out = x + y  // integer 2's complement addition
// if (f==0)  set out = x &amp; y  // bitwise "and"
// if (no==1) set out = ~out   // bitwise "not"
// if (out==0) set zr = 1
// if (out&lt;0) set ng = 1

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs
        zx, // zero the x input
        nx, // negate the x input
        zy, // zero the y input
        ny, // negate the y input
        f,  // compute  out = x + y (if 1) or out = x &amp; y (if 0)
        no; // negate the out output

    OUT
        out[16], // 16-bit output
        zr, // 1 if (out==0), 0 otherwise
        ng; // 1 if (out&lt;0),  0 otherwise

    PARTS:

    // if (zx==1) set x = 0
    Mux16(a=x,b=false,sel=zx,out=zxout);

    // if (zy==1) set y = 0
    Mux16(a=y,b=false,sel=zy,out=zyout);

    // if (nx==1) set x = ~x
    // if (ny==1) set y = ~y  
    Not16(in=zxout,out=notx);
    Not16(in=zyout,out=noty);
    Mux16(a=zxout,b=notx,sel=nx,out=nxout);
    Mux16(a=zyout,b=noty,sel=ny,out=nyout);

    // if (f==1)  set out = x + y
    // if (f==0)  set out = x &amp; y
    Add16(a=nxout,b=nyout,out=addout);
    And16(a=nxout,b=nyout,out=andout);
    Mux16(a=andout,b=addout,sel=f,out=fout);

    // if (no==1) set out = ~out
    // 1 if (out&lt;0),  0 otherwise
    Not16(in=fout,out=nfout);
    Mux16(a=fout,b=nfout,sel=no,out=out,out[0..7]=zr1,out[8..15]=zr2,out[15]=ng);

    // 1 if (out==0), 0 otherwise
    Or8Way(in=zr1,out=or1);
    Or8Way(in=zr2,out=or2);
    Or(a=or1,b=or2,out=or3);
    Not(in=or3,out=zr);
}
</code></pre></div></div>

<h3 id="registers">Registers</h3>

<p>I am going to pass the specification and the implementation part for the registers since our subject is only about the computational part of the Hack platform. However, it is still useful to know about the types of registers that reside physically inside the CPU.</p>

<h4 id="data-register">Data Register</h4>

<p>Data Register holds the contents of the memory which are to be transferred from the immediate access storage to other components or vice versa.</p>

<h4 id="addressing-register">Addressing Register</h4>

<p>Addressing Register holds the memory address of data that needs to be accessed.  When reading from memory, data addressed by addressing register is fed into the data register and then used by the CPU.</p>

<h4 id="program-counter-instruction-pointer">Program Counter (Instruction Pointer)</h4>

<p>Program Counter holds the memory address of the next instruction that would be executed.</p>

<h3 id="control-unit">Control Unit</h3>

<p>Control Unit controls the flow of data between the CPU and other components. It is contained within the CPU and reponsible for decoding the instructions, and figuring out which instruction to fetch and execute next.</p>

<h2 id="cpu---specification">CPU - Specification</h2>

<p>Hack platform’s CPU is designed to execute 16-bit instructions according to the Hack machine language specification. The CPU should be connected to two separate memory modules: Instruction memory (ROM) and data memory (RAM).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Chip Name: CPU              // Central Processing Unit
Inputs:    inM[16],         // M value input (M = contents of RAM[A])
           instruction[16], // Instruction for execution
           reset            // Signals whether to restart the current
                            // program (reset=1) or continue executing
                            // the current program (reset=0)
Outputs:   outM[16],        // M value output
           writeM,          // Write to M?
           addressM[15],    // Address of M in data memory
           pc[15]           // Address of next instruction



</code></pre></div></div>

<p>The below figures shows the proposed CPU implementation. It does not show the <em>control logic</em>, except for inputs and outputs of control bits, labeled with a circled “c”.</p>

<p><img src="/assets/images/hack_cpu.png" alt="hack cpu" /></p>

<p>Figure 2: Central Processing Unit. (Taken from The Elements of Computing Systems, <a href="https://docs.wixstatic.com/ugd/44046b_b2cad2eea33847869b86c541683551a7.pdf">Chapter 5</a>)</p>

<p>CPU executes the given instruction according to Hack assembly language specification. <code class="language-plaintext highlighter-rouge">D</code> and <code class="language-plaintext highlighter-rouge">A</code> refer to CPU-resident registers while <code class="language-plaintext highlighter-rouge">M</code> refers to external memory location addressed by <code class="language-plaintext highlighter-rouge">A</code>, i.e. to <code class="language-plaintext highlighter-rouge">RAM[A]</code>. <code class="language-plaintext highlighter-rouge">inM</code> holds the value of this location. If the current instruction needs to write a value to M, the value is placed in <code class="language-plaintext highlighter-rouge">outM</code>, the address of the target location is placed in the <code class="language-plaintext highlighter-rouge">addressM</code> output, and the <code class="language-plaintext highlighter-rouge">writeM</code> control bit is asserted.</p>

<p><code class="language-plaintext highlighter-rouge">outM</code> and <code class="language-plaintext highlighter-rouge">writeM</code> outputs are combinational: they are affected instantaneously by the execution of the current instruction. <code class="language-plaintext highlighter-rouge">addressM</code> and <code class="language-plaintext highlighter-rouge">pc</code> outputs are clocked, they commit to their new values only in the next time unit. If <code class="language-plaintext highlighter-rouge">reset=1</code> then the CPU jumps to address 0 (i.e. sets <code class="language-plaintext highlighter-rouge">pc</code> to 0 in next time unit) rather than to the address resulting from executing the current instruction.</p>

<p>This is an example implementation of the CPU in HDL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/05/CPU.hdl

CHIP CPU {

    IN  inM[16],         // M value input  (M = contents of RAM[A])
        instruction[16], // Instruction for execution
        reset;           // Signals whether to re-start the current
                         // program (reset=1) or continue executing
                         // the current program (reset=0).

    OUT outM[16],        // M value output
        writeM,          // Write into M?
        addressM[15],    // Address in data memory (of M)
        pc[15];          // address of next instruction

    PARTS:
    Mux16(a=instruction,b=ALUout,sel=instruction[15],out=Ain);

    Not(in=instruction[15],out=notinstruction);

    //RegisterA
    //when instruction[15] = 0, it is @value means A should load value
    Or(a=notinstruction,b=instruction[5],out=loadA);//d1
    ARegister(in=Ain,load=loadA,out=Aout,out[0..14]=addressM);

    Mux16(a=Aout,b=inM,sel=instruction[12],out=AMout);

    //Prepare for ALU, if it is not an instruction, just return D
    And(a=instruction[11],b=instruction[15],out=zx);//c1
    And(a=instruction[10],b=instruction[15],out=nx);//c2
    Or(a=instruction[9],b=notinstruction,out=zy);//c3
    Or(a=instruction[8],b=notinstruction,out=ny);//c4
    And(a=instruction[7],b=instruction[15],out=f);//c5
    And(a=instruction[6],b=instruction[15],out=no);//c6

    ALU(x=Dout,y=AMout,zx=zx,nx=nx,zy=zy,ny=ny,f=f,no=no,out=outM,out=ALUout,zr=zero,ng=neg);

    //when it is an instruction, write M
    And(a=instruction[15],b=instruction[3],out=writeM);//d3

    //RegisterD,when it is an instruction, load D
    And(a=instruction[15],b=instruction[4],out=loadD);//d2
    DRegister(in=ALUout,load=loadD,out=Dout);

    //Prepare for jump
    //get positive
    Or(a=zero,b=neg,out=notpos);
    Not(in=notpos,out=pos);

    And(a=instruction[0],b=pos,out=j3);//j3
    And(a=instruction[1],b=zero,out=j2);//j2
    And(a=instruction[2],b=neg,out=j1);//j1

    Or(a=j1,b=j2,out=j12);
    Or(a=j12,b=j3,out=j123);

    And(a=j123,b=instruction[15],out=jump);

    //when jump,load Aout
    PC(in=Aout,load=jump,reset=reset,inc=true,out[0..14]=pc);
}
</code></pre></div></div>

<p>That’s it for the first part! We’ve done a great job so far and I know it was super overwhelming but all of the above information were necessary to understand how the binary instructions flow through the <em>control unit</em>. In the next part I will explain the Hack assembly language and how its instructions are translated into binary.</p>]]></content><author><name>Onat Yigit Mercan</name></author><category term="hack" /><category term="ALU" /><category term="CPU" /><summary type="html"><![CDATA[This blog series is based on nand2tetris book.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://onatm.dev/assets/images/og/anatomy-of-a-hack-assembly-program-part-1.png" /><media:content medium="image" url="https://onatm.dev/assets/images/og/anatomy-of-a-hack-assembly-program-part-1.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>