<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://arani.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://arani.dev/" rel="alternate" type="text/html" /><updated>2026-07-30T23:52:09+02:00</updated><id>https://arani.dev/feed.xml</id><title type="html">Afshin Arani</title><subtitle>Senior software engineer and solutions architect in Copenhagen. A decade of .NET, distributed systems and cloud platforms inside regulated environments.</subtitle><author><name>Afshin Arani</name><email>afshin@arani.dev</email></author><entry><title type="html">Firecracker, part four: BusyBox init, SquashFS, and a day lost to systemd</title><link href="https://arani.dev/writing/firecracker-part-four-busybox-squashfs/" rel="alternate" type="text/html" title="Firecracker, part four: BusyBox init, SquashFS, and a day lost to systemd" /><published>2025-10-12T00:00:00+02:00</published><updated>2025-10-12T00:00:00+02:00</updated><id>https://arani.dev/writing/firecracker-part-four-busybox-squashfs</id><content type="html" xml:base="https://arani.dev/writing/firecracker-part-four-busybox-squashfs/"><![CDATA[<h2 id="previously">Previously</h2>

<p>Firecracker microVMs running .NET functions per HTTP request, storage optimised with
OverlayFS, sparse function images compressed for transfer, requests routed through a
gateway and queued in Kafka, microVMs launched on demand and talked to over vsock.
Cold starts around ten seconds, much faster warm.</p>

<p>I promised a few things in part three and then got derailed doing cleanup instead.</p>

<h2 id="lets-be-real">Let’s be real</h2>

<ul>
  <li>That crappy init script couldn’t take it any more. It panicked when <code class="language-plaintext highlighter-rouge">dotnet</code> died.
Not ideal.</li>
  <li>Creating runtime images by hand was a massive pain. Also not ideal.</li>
</ul>

<h2 id="fixing-it">Fixing it</h2>

<p>I tried compiling systemd for a full day. Even with its most minimal options it came
out at 200 MB. Screw that — I compiled BusyBox init as a static binary instead.</p>

<p>Then I wrote a <a href="https://github.com/aarani/rootfs-generation/blob/main/dotnet/Dockerfile">Dockerfile</a>
that builds the rootfs inside Docker, so I can use it directly for my VMs.</p>

<p>It also turns out ext4 is a pain as a read-only disk, so I switched to SquashFS, which
is built for exactly that.</p>

<h2 id="tried-it-crashed">Tried it. Crashed.</h2>

<p>BusyBox’s init does nothing unless you tell it to, in <code class="language-plaintext highlighter-rouge">inittab</code>. So, after more
negotiation with Copilot, I added the mounts .NET actually needs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>::sysinit:/bin/mount -t devtmpfs devtmpfs /dev
::sysinit:/bin/mkdir -p /dev/pts
::sysinit:/bin/mount -t devpts devpts /dev/pts
::sysinit:/bin/mount -t proc proc /proc
::sysinit:/bin/mount -t sysfs sys /sys
::sysinit:/bin/mkdir -p /tmp /dev/shm /var/tmp /run
::sysinit:/bin/mount -t tmpfs -o mode=1777,nosuid,nodev tmpfs /tmp
::sysinit:/bin/mount -t tmpfs -o nosuid,nodev tmpfs /dev/shm
::sysinit:/bin/mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run
::sysinit:/bin/chmod 1777 /var/tmp

::respawn:/bin/sh -c 'exec socat TCP-LISTEN:55505,fork,reuseaddr VSOCK-CONNECT:2:80'
::once:/bin/sh -c '/bin/dotnet "/app/$(cat /entrypoint.txt)"; rc=$?; echo "dotnet exited rc=$rc, powering off" &gt;&amp;2; sync; poweroff -f'
</code></pre></div></div>

<h2 id="back-where-we-started--but-better">Back where we started — but better</h2>

<p>Theoretically I can now convert <em>any</em> Docker container into a runtime by injecting my
static init binaries.</p>

<p>Elsewhere:</p>

<ul>
  <li>I started implementing a SquashFS driver in .NET for no good reason, and halfway
through discovered I needed ext4, not SquashFS.</li>
  <li>In-process DEFLATE (de)compression replaced the finicky tar shell-out.</li>
  <li>Endpoints for adding runtimes — thanks to SquashFS I don’t need to compress them at all.</li>
</ul>

<h2 id="roadmap">Roadmap</h2>

<ul>
  <li>Binary protocol for runtime communication</li>
  <li>Dogfooding: FaaS-based overlay image creation</li>
  <li>Non-HTTP triggers (MinIO events and friends)</li>
  <li>A frontend, so no more Postman</li>
  <li>Authorization, authorization, authorization</li>
  <li>Snapshots</li>
  <li>Replace the terrible Kafka consuming logic with kafka-offset-manager</li>
</ul>]]></content><author><name>Afshin Arani</name><email>afshin@arani.dev</email></author><category term="firecracker" /><category term="mangofaas" /><category term="linux" /><summary type="html"><![CDATA[Previously]]></summary></entry><entry><title type="html">Firecracker, part three: a request’s journey through MangoFaaS</title><link href="https://arani.dev/writing/firecracker-part-three-request-journey/" rel="alternate" type="text/html" title="Firecracker, part three: a request’s journey through MangoFaaS" /><published>2025-09-24T00:00:00+02:00</published><updated>2025-09-24T00:00:00+02:00</updated><id>https://arani.dev/writing/firecracker-part-three-request-journey</id><content type="html" xml:base="https://arani.dev/writing/firecracker-part-three-request-journey/"><![CDATA[<h2 id="previously">Previously</h2>

<p>An API gateway mapping HTTP routes to (FunctionId, FunctionVersion), disk bloat from
giving every microVM its own full rootfs, and the fix: OverlayFS with one shared
read-only base and a per-VM sparse writable ext4 layer.</p>

<p>Enough preparation. Let’s walk through <a href="https://github.com/aarani/MangoFaaS">MangoFaaS</a>
from the point of view of everyone involved.</p>

<p><strong>I’m an HTTP request.</strong> My journey starts in MangoFaaS.Gateway. I carry a <em>Host</em> and
a <em>Path</em>; the gateway uses them to work out the tenant and which function version to
invoke.</p>

<p><strong>I’m a function owner.</strong> My journey starts in MangoFaaS.Functions. I register a
function by uploading a ZIP of my .NET application to S3 with metadata: entrypoint and
runtime. A background job discovers new version ZIPs in the bucket, builds a function
overlay image, and pushes it to an images bucket.</p>

<p>Worth noting: those images are sparse files. To avoid shipping all the zero-filled
regions we compress them — currently with GNU tar, because .NET’s Tar implementation
doesn’t preserve sparseness. So we shell out to the lovely GNU tar.</p>

<p><strong>I’m the HTTP request again.</strong> I’ve been routed and I’m sitting in Kafka. I move
toward the first free MangoFaaS.Firecracker.Node. (Future improvement: prefer nodes
that already have my function cached.)</p>

<p><strong>I’m MangoFaaS.Firecracker.Node.</strong> I:</p>

<ul>
  <li>put the request in a pending list;</li>
  <li>download, if not cached, the latest kernel, the runtime base image and the
function’s overlay image;</li>
  <li>launch or prepare a Firecracker microVM and wait for the function runtime to connect
to my Kestrel server over vsock and ask for work.</li>
</ul>

<p><strong>I’m the function runtime.</strong> I connect and poll the Kestrel server over vsock, process
the request, and send the response back down the same channel.</p>

<p>The response then travels back through the node and out via the gateway that took the
inbound request.</p>

<h2 id="early-timings">Early timings</h2>

<table>
  <thead>
    <tr>
      <th>Case</th>
      <th>Time</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Very cold — image downloads required</td>
      <td>~10 s</td>
    </tr>
    <tr>
      <td>Not-so-cold — images cached locally</td>
      <td>~6 s</td>
    </tr>
    <tr>
      <td>Warm — VM already running</td>
      <td>negligible</td>
    </tr>
  </tbody>
</table>

<p>Next: snapshots, prewarming, smarter scheduling, cache affinity.</p>]]></content><author><name>Afshin Arani</name><email>afshin@arani.dev</email></author><category term="firecracker" /><category term="mangofaas" /><category term="architecture" /><summary type="html"><![CDATA[Previously]]></summary></entry><entry><title type="html">Firecracker, part two: one rootfs, many microVMs</title><link href="https://arani.dev/writing/firecracker-part-two-overlayfs/" rel="alternate" type="text/html" title="Firecracker, part two: one rootfs, many microVMs" /><published>2025-09-02T00:00:00+02:00</published><updated>2025-09-02T00:00:00+02:00</updated><id>https://arani.dev/writing/firecracker-part-two-overlayfs</id><content type="html" xml:base="https://arani.dev/writing/firecracker-part-two-overlayfs/"><![CDATA[<h2 id="previously">Previously</h2>

<p>I explored Firecracker, compared it with containers, chose to build a
snapshot-based mini-FaaS, and got a first microVM running with a custom rootfs and
a prebuilt kernel. Networking fixed, VMs booting, and a plan to manage them from C#.</p>

<h2 id="preparation">Preparation</h2>

<p>I have a proof-of-concept API gateway that matches an HTTP request to a
(FunctionId, FunctionVersion) pair using a route table in the database with
in-memory caching.</p>

<h2 id="function-id-function-version-wat">Function id? Function version? Wat?</h2>

<p>Fair. I still have no way to <em>create</em> a function. A function, in FaaS terms, is a
small, stateless, event-triggered piece of code executed on demand in an ephemeral
runtime that scales per invocation. To run that code I need an image — which is where
part one left off.</p>

<p>And here’s the new problem: as soon as I tried to run many microVMs on my
not-so-micro VM, I ran out of disk.</p>

<h2 id="overlayfs">OverlayFS</h2>

<p>OverlayFS is a Linux kernel feature that combines several mount points into one
directory tree containing the files from all of them.</p>

<p>The classic use is overlaying a read/write partition on a read-only one. Which means
every VM can share the <em>same</em> read-only rootfs, and each gets its own thin
read-write layer on top. Disk problem solved.</p>

<h2 id="the-code">The code</h2>

<p>Base image, once:</p>

<ol>
  <li>Create your base rootfs image.</li>
  <li>Inside it: <code class="language-plaintext highlighter-rouge">mkdir -p /mnt/rootfs/overlay/root /mnt/rootfs/overlay/work /mnt/rootfs/mnt /mnt/rootfs/rom</code></li>
  <li>Drop in the <code class="language-plaintext highlighter-rouge">overlay-init</code> script from firecracker-containerd — a separate init that
runs before your main init and does the preparation (you can drop the line that
creates the volumes folder).</li>
  <li><code class="language-plaintext highlighter-rouge">chmod +x</code> it.</li>
  <li>Unmount. Done.</li>
</ol>

<p>Per-VM overlay:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">dd </span><span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>overlay.ext4 <span class="nv">conv</span><span class="o">=</span>sparse <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">count</span><span class="o">=</span>1024
mkfs.ext4 overlay.ext4
</code></pre></div></div>

<p>That’s it. Meanwhile the C# side of the platform is taking shape in
<a href="https://github.com/aarani/mangofaas">MangoFaaS</a>.</p>

<h2 id="sources">Sources</h2>

<ul>
  <li><a href="https://github.com/aarani/mangofaas">https://github.com/aarani/mangofaas</a></li>
  <li><a href="https://github.com/njapke/overlayfs-in-firecracker">https://github.com/njapke/overlayfs-in-firecracker</a></li>
  <li><a href="https://en.wikipedia.org/wiki/OverlayFS">https://en.wikipedia.org/wiki/OverlayFS</a></li>
</ul>]]></content><author><name>Afshin Arani</name><email>afshin@arani.dev</email></author><category term="firecracker" /><category term="mangofaas" /><category term="linux" /><summary type="html"><![CDATA[Previously]]></summary></entry><entry><title type="html">Firecracker, part one: getting a microVM to boot</title><link href="https://arani.dev/writing/firecracker-part-one-first-microvm/" rel="alternate" type="text/html" title="Firecracker, part one: getting a microVM to boot" /><published>2025-08-15T00:00:00+02:00</published><updated>2025-08-15T00:00:00+02:00</updated><id>https://arani.dev/writing/firecracker-part-one-first-microvm</id><content type="html" xml:base="https://arani.dev/writing/firecracker-part-one-first-microvm/"><![CDATA[<p>Series notes for <a href="https://github.com/aarani/MangoFaaS">MangoFaaS</a> — let’s play with Firecracker.</p>

<h2 id="what-is-aws-firecracker">What is AWS Firecracker?</h2>

<p>Firecracker is an open-source virtual machine monitor from AWS that runs
lightweight microVMs on KVM, optimised for secure multi-tenancy, fast startup and
low overhead. It’s what powers Lambda and Fargate: VM-grade isolation with
startup times closer to containers.</p>

<h2 id="so-why-not-just-a-container">So why not just a container?</h2>

<ul>
  <li>Firecracker uses KVM hardware virtualisation with its own guest kernel;
containers share the host kernel. The multi-tenant isolation story is much stronger.</li>
  <li>Containers start in tens of milliseconds. Firecracker cold starts are ~100–300 ms —
and you can optimise the heck out of that down to single-digit ms.</li>
  <li>MicroVMs are light, but they still carry a guest kernel, so density is lower.</li>
  <li>Containers use OCI images; Firecracker wants a kernel + rootfs. You build your own
images. <code class="language-plaintext highlighter-rouge">firecracker-containerd</code> exists, but I wanted to start from scratch.</li>
</ul>

<h2 id="picking-a-project">Picking a project</h2>

<p>I asked Copilot what to build and got three options: a declarative launchpad CLI,
a snapshot-accelerated mini-FaaS, or a hardened multi-tenant code runner. The CLI
was too easy. The code runner reminded me of ACM contest auto-judges — fun, but
niche. So: <strong>mini-FaaS</strong>.</p>

<p>I promptly ignored the plan and started with a WebAPI project that swallows every
request — all paths, hosts and methods — and forwards them to Kafka.</p>

<h2 id="firecracker-comes-into-play">Firecracker comes into play</h2>

<p>I’m on a Mac, which can’t run Firecracker, so I rented a Linux VM from DigitalOcean
(Hetzner doesn’t do nested virtualisation). I needed a kernel and a rootfs. Building
the kernel — even a shallow clone — took forever on a cheap VM, so I gave up and took
Amazon’s prebuilt one:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ARCH</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-m</span><span class="si">)</span><span class="s2">"</span>
<span class="nv">release_url</span><span class="o">=</span><span class="s2">"https://github.com/firecracker-microvm/firecracker/releases"</span>
<span class="nv">latest_version</span><span class="o">=</span><span class="si">$(</span><span class="nb">basename</span> <span class="si">$(</span>curl <span class="nt">-fsSLI</span> <span class="nt">-o</span> /dev/null <span class="nt">-w</span>  %<span class="o">{</span>url_effective<span class="o">}</span> <span class="k">${</span><span class="nv">release_url</span><span class="k">}</span>/latest<span class="si">))</span>
<span class="nv">CI_VERSION</span><span class="o">=</span><span class="k">${</span><span class="nv">latest_version</span><span class="p">%.*</span><span class="k">}</span>
<span class="nv">latest_kernel_key</span><span class="o">=</span><span class="si">$(</span>curl <span class="s2">"http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/</span><span class="nv">$CI_VERSION</span><span class="s2">/</span><span class="nv">$ARCH</span><span class="s2">/vmlinux-&amp;list-type=2"</span> <span class="se">\</span>
    | <span class="nb">grep</span> <span class="nt">-oP</span> <span class="s2">"(?&lt;=&lt;Key&gt;)(firecracker-ci/</span><span class="nv">$CI_VERSION</span><span class="s2">/</span><span class="nv">$ARCH</span><span class="s2">/vmlinux-[0-9]+</span><span class="se">\.</span><span class="s2">[0-9]+</span><span class="se">\.</span><span class="s2">[0-9]{1,3})(?=&lt;/Key&gt;)"</span> <span class="se">\</span>
    | <span class="nb">sort</span> <span class="nt">-V</span> | <span class="nb">tail</span> <span class="nt">-1</span><span class="si">)</span>

wget <span class="s2">"https://s3.amazonaws.com/spec.ccfc.min/</span><span class="k">${</span><span class="nv">latest_kernel_key</span><span class="k">}</span><span class="s2">"</span>
</code></pre></div></div>

<p>Rootfs time — exported straight out of a container image:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull mcr.microsoft.com/dotnet/aspnet:9.0
<span class="nv">CONTAINER_ID</span><span class="o">=</span><span class="si">$(</span>docker create mcr.microsoft.com/dotnet/aspnet:9.0<span class="si">)</span>
docker <span class="nb">export</span> <span class="nv">$CONTAINER_ID</span> <span class="nt">-o</span> rootfs.tar
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>rootfs.ext4 <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">count</span><span class="o">=</span>2048
mkfs.ext4 rootfs.ext4
<span class="nb">mkdir</span> /mnt/rootfs
<span class="nb">sudo </span>mount <span class="nt">-o</span> loop rootfs.ext4 /mnt/rootfs
<span class="nb">sudo tar</span> <span class="nt">-xf</span> rootfs.tar <span class="nt">-C</span> /mnt/rootfs
</code></pre></div></div>

<p>There’s a twist: containers don’t use an init process, so most container images
don’t ship one. I wrote a sample init and moved on.</p>

<h2 id="it-works-it-has-no-internet">It works. It has no internet.</h2>

<p>Neither container image I tried had <code class="language-plaintext highlighter-rouge">ip</code> installed, so I configured networking
through the kernel command line instead, pointed the resolver at 1.1.1.1, and this
time it really worked.</p>

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

<p>Each Firecracker process should serve exactly one tenant — which means I now need to
launch the Firecracker binary from C#, talk to its API, and boot VMs programmatically.</p>]]></content><author><name>Afshin Arani</name><email>afshin@arani.dev</email></author><category term="firecracker" /><category term="mangofaas" /><category term="dotnet" /><summary type="html"><![CDATA[Series notes for MangoFaaS — let’s play with Firecracker.]]></summary></entry></feed>