{"description":"Louis Opter (kalessin) :: Blog","feed_url":"https://www.kalessin.fr/blog/feed.json","home_page_url":"https://www.kalessin.fr/blog","items":[{"content_html":"<p><em>Building custom model planes for fun and cost optimization</em></p>\n<p><a href=\"https://fly.io/\">Fly.io</a> is a “cloud service provider”, with an <a href=\"https://fly.io/docs/reference/architecture/\">interesting architecture</a>: they have a global anycast routing layer over a bunch of points of presence (PoPs) around the world. Each point of presence provides compute, and they have the additional services to expect from a public “cloud” such as TLS termination, databases &amp; storage, etc. Worth mentioning is their S3-compatible blob storage, <a href=\"https://www.tigrisdata.com/\">Tigris</a>, built on top of the Fly.io architecture.</p>\n<p>I liked their networking architecture, and the ability to run VMs, with as little as 256MB of memory, from OCI (Docker) images. However, I did not really like the default model of one process per VM, and I was not interested in their other abstractions e.g. TLS termination, or secrets management, since I already have working parts for that.</p>\n<p>In this post, we will see how <a href=\"https://github.com/NixOS/nixpkgs\">Nixpkgs</a> allowed me to cherry-pick from the Fly.io abstractions, and do more with less. Rather than going deep into how Nixpkgs work, I will keep things superficial, and high-level, so that it can serve as an introduction to some of the concepts. If you have not heard of Nixpkgs before, it is a package manager for Linux and macOS implemented using a functional programming language called <a href=\"https://nix.dev/tutorials/nix-language\">Nix</a>.</p>\n<h2>Edge networking <a href=\"#0-1-edge-networking\" id=\"0-1-edge-networking\"><span class=\"heading-anchor\">#</span></a></h2>\n<p>Fly.io sits at the edge of my personal infrastructure: it is the layer that receives traffic from the Internet, and proxies it to my self-hosted servers. Fly.io has better Internet connectivity than my self-hosted servers, and it is nice to be able to cache my servers’ responses, and keep my home IPs (the “origin” in CDN parlance) private. This immediately presents a few problems:</p>\n<ol>\n<li>My machines on Fly.io somehow need to connect to my backends via the Internet;</li>\n<li>They need to do TLS termination;</li>\n<li>I use Nginx, and it will need some DNS resolver.</li>\n</ol>\n<p>For 1, I have been using <a href=\"https://tailscale.com/\">Tailscale</a>. For 2, I have a <code>vault-agent</code> setup that pulls my Let’s Encrypt certificates from my <a href=\"https://developer.hashicorp.com/vault\">Vault</a> (see also <a href=\"https://openbao.org/\">OpenBao</a>). For 3, I can run <a href=\"https://unbound.docs.nlnetlabs.nl/en/latest/\">unbound</a>.</p>\n<p>Nginx does not run alone: it has all those sidecar processes and they need to be in the same network namespace so that we can route over Tailscale, and that does not fit Fly.io’s model of one process per machine (VM), so we need to bring some kind of process manager, something lighter than systemd, and more modern than <a href=\"https://supervisord.org/\">supervisord</a>.</p>\n<p>My pick here is <a href=\"https://github.com/F1bonacc1/process-compose\">process-compose</a>, think <code>docker compose</code> but without Docker: just regular processes, with a way to setup some dependencies between them.</p>\n<p>Now that we have some kind of bill of materials for an OCI (Docker) image, let’s see how we can build that image <em><code>FROM scratch</code></em>.</p>\n<h2>Pick and place with Nix &amp; Nixpkgs <a href=\"#0-2-pick-and-place-with-nix-nixpkgs\" id=\"0-2-pick-and-place-with-nix-nixpkgs\"><span class=\"heading-anchor\">#</span></a></h2>\n<p>Our image consists of:</p>\n<ul>\n<li>The packages we depend on: Nginx, process-compose, Tailscale…;</li>\n<li>Configuration files for our dependencies;</li>\n<li>Some idempotent script to set a few things up at boot (mainly directories);</li>\n<li>Some user and groups database to have a modicum of privilege separation;</li>\n<li>The configuration for the image itself (think <code>CMD</code>, or <code>ENV</code> in a <code>Dockerfile</code>).</li>\n</ul>\n<p>The next sections will be illustrated with some Nix code from some fairly ugly <code>flake-module.nix</code>. If you are not familiar with Nix, all this file does (in this context) is setting up our Docker image as a Nix package called <code>fly-io-pop</code>.</p>\n<h3>Pull up the dependencies <a href=\"#0-2-1-pull-up-the-dependencies\" id=\"0-2-1-pull-up-the-dependencies\"><span class=\"heading-anchor\">#</span></a></h3>\n<p>You can think of Nixpkgs as one huge tree of JSON objects that describes how to build pretty much any piece of software out there. We can pick the software we are interested in from that tree, and put them in our basket: the list variable I named <a href=\"https://github.com/lopter/clan-destiny/blob/7dfda2c5604639290192ce0271f4293bdc5cc8a9/library/nix/packages/fly-io-pop/flake-module.nix#L54\"><code>basePkgs</code></a>.</p>\n<p>It is worth noting that we modify some packages with those <code>override</code> and <code>overrideAttrs</code> calls instead of picking them straight from the tree. On Linux, Nixpkgs builds packages with the assumption that they are going to be used in a regular server or desktop environment that uses systemd. We are not using systemd here, and since it’s a heavy dependency, we can reduce our image size if we let go of it. Those “overrides” allow us to build custom versions of those packages without systemd.</p>\n<p>The <a href=\"https://github.com/lopter/clan-destiny/blob/7dfda2c5604639290192ce0271f4293bdc5cc8a9/library/nix/packages/fly-io-pop/flake-module.nix#L36-L53\"><code>overrideAttrs</code> on <code>process-compose</code></a> is worth noting: it swaps out the sources for <code>process-compose</code> with my own fork, where I am trying to <a href=\"https://github.com/F1bonacc1/process-compose/pull/331\">implement config validation</a>. This ability you get with Nixpkgs, to basically patch anything at any level in your system, and then easily rebuild, and redistribute software with those changes, is very liberating. For years, I have been stuck on ideas, simply because building, and distributing packages with other package managers is so <em>hard</em>.</p>\n<h3>Configure everything <a href=\"#0-2-2-configure-everything\" id=\"0-2-2-configure-everything\"><span class=\"heading-anchor\">#</span></a></h3>\n<p>One reason why this <code>flake-module.nix</code> file is so ugly is because I pretty much inlined the configuration for everything in it. 🤷</p>\n<p>The configuration files are set up using the Nixpkgs’ function <a href=\"https://nixos.org/manual/nixpkgs/unstable/#trivial-builder-writeTextFile\"><code>writeTextFile</code></a>, which <em>« Write a text file to the Nix store »</em>. Unlike other package managers where all packages share the same directories, usually under <code>/usr</code>, Nix takes a different approach where each package gets its own directory under <code>/nix/store</code>. Each package directory in the <code>/nix/store</code> starts with a cryptographic hash based on the dependency graph of the package. This gives you all the benefits listed in the first page of the <a href=\"https://nix.dev/manual/nix/stable/\">Nix manual</a>.</p>\n<p><code>writeTextFile</code> writes a single file under <code>/nix/store</code>, and it will also start with a hash that uniquely identifies it. The return value of <code>writeTextFile</code> is the absolute path of the file written under <code>/nix/store</code><span id=\"fn-derivation_return_value\"><sup class=\"footnote-reference\"><a href=\"#derivation_return_value\">1</a></sup></span>.</p>\n<p>Since the Nix language maps very well to JSON, it can be used to directly configure many programs. For example, I directly configure <code>process-compose</code> from Nix, which allows me to <a href=\"https://github.com/lopter/clan-destiny/blob/7dfda2c5604639290192ce0271f4293bdc5cc8a9/library/nix/packages/fly-io-pop/flake-module.nix#L142-L150\">define a function <code>mkProcess</code></a> to avoid repeating the same logging configuration for every process.</p>\n<p>The <code>process-compose</code> configuration starts with a <code>postInit</code> command, which is <a href=\"https://github.com/lopter/clan-destiny/blob/7dfda2c5604639290192ce0271f4293bdc5cc8a9/library/nix/packages/fly-io-pop/flake-module.nix#L487-L545\">a shell script</a> written to the Nix store. The script creates a few things at runtime: temporary directories when the VM boots, but also some permanent files under <code>/var</code> which I have configured in fly.io to be mounted on persistent storage. Some directories are owned by the <code>nginx</code> and <code>unbound</code> users, this will have to be backed by some user database.</p>\n<p>For the user-related functions in the libc to work we need the following files: <code>/etc/passwd</code>, <code>/etc/group</code>, and <code>/etc/shadow</code>. Luckily the Docker image builder for the Nix interpreter (<a href=\"https://github.com/NixOS/nix/blob/876f676d90506990406529881239d55898fc86d2/docker.nix\"><code>docker.nix</code></a> from the Nix repository), already has an helpful bit of code for that, and I have extracted it into my own [<code>dockerNssHelper</code> function]. The function takes a couple “JSON objects” / “dictionaries” / “hash maps”, Nix calls them <em>attribute sets</em>, that describe my user and group directories: user or group names are the keys, with properties like <code>uid</code>, <code>gid</code>, <code>home</code>, <code>shell</code>, etc. as values. Since <code>/etc/passwd</code> et al. are not unlike CSV files, it is pretty easy for the function to loop over its arguments, perform some string interpolation, and render the files. They will go under <code>/nix/store</code>, and we will symlink them in <code>/etc</code> in a later step.</p>\n<p>The last thing <code>postInit</code> does is create an SSH identity, and then call this program called <code>sops-install-secrets</code> at the end, let’s look into that, and how we manage TLS certificates.</p>\n<h2>Security considerations <a href=\"#0-3-security-considerations\" id=\"0-3-security-considerations\"><span class=\"heading-anchor\">#</span></a></h2>\n<h3>TLS certificates <a href=\"#0-3-1-tls-certificates\" id=\"0-3-1-tls-certificates\"><span class=\"heading-anchor\">#</span></a></h3>\n<p>I run a <a href=\"https://github.com/lopter/clan-destiny/blob/72c0bfe984806c3aa2ce612e538b11eca722d314/library/nix/nixosModules/certbot-vault.nix\">certbot NixOS module</a><span id=\"fn-nixos\"><sup class=\"footnote-reference\"><a href=\"#nixos\">2</a></sup></span> in my backend that gets certificates from Let’s Encrypt and store them in Vault (or <a href=\"https://openbao.org/\">OpenBao</a>). Whenever a process needs some TLS certificate, I deploy a <a href=\"https://github.com/lopter/clan-destiny/blob/72c0bfe984806c3aa2ce612e538b11eca722d314/library/nix/packages/fly-io-pop/flake-module.nix#L286-L357\"><code>vault-agent</code> instance</a> with it. We need to supply the agent with credentials to fetch the certificates from the vault. The credentials are stored in the <code>/nix/store</code> in a YAML file created with <a href=\"https://getsops.io/docs/\">Sops</a>, if you have not heard about Sops it is some kind of text editor for encrypted files. A sops file has a bunch of key/value pairs, all the values are symmetrically encrypted with a single key, and then that key is encrypted using any number of “master keys” for the file. Sops is nice way to store secrets in git, and still have something diff-able. This <code>sops-install-secrets</code> program called at the end of <code>postInit</code> is part of <a href=\"https://github.com/Mic92/sops-nix\">sops-nix</a>, a project that integrates Sops with NixOS. Let’s see how this plays out.</p>\n<p>We have this <a href=\"https://github.com/lopter/clan-destiny/blob/7dfda2c5604639290192ce0271f4293bdc5cc8a9/library/nix/packages/fly-io-pop/secrets.yaml\"><code>secrets.yaml</code> Sops file</a> in the repository, Nix will copy the file under <code>/nix/store</code>, and it can be decrypted by any of three different (private) keys: my GPG encryption key, or the SSH host private key of each of my two machines on fly.io. When the VM boots, <code>sops-install-secrets</code> is executed, and is given the path of a JSON manifest that tells <code>sops-install-secrets</code> where <code>secrets.yaml</code> is, where is the key that can decrypt it, and where each of the secrets the file contains need to go. How I generate the JSON manifest for <code>sops-install-secrets</code> (<code>sops-nix</code>) is an absolute hack: you are supposed to use <code>sops-nix</code> in NixOS, where this JSON manifest is generated from <a href=\"https://github.com/lopter/clan-destiny/blob/72c0bfe984806c3aa2ce612e538b11eca722d314/library/nix/packages/fly-io-pop/flake-module.nix#L577-L608\">the evaluation</a> of the <code>sops-nix</code> <em>NixOS module</em>. We are not using NixOS here, I just cut off the exact NixOS bits I needed with my hacksaw, and called <code>lib.nixos.evalModules</code>, to evaluate the <code>sops-nix</code> module (slightly modified for the purpose of this hack), with my configuration for it, and as a result we get our JSON manifest for <code>sops-nix</code> to decrypt, and setup the secrets at boot in a ramfs. If you wish, that can serve as quick introduction to the concept of modules in Nix/NixOS, which is really just a design pattern. A module returns an object with three attributes:</p>\n<ul>\n<li><code>imports</code>: a list of other modules to import;</li>\n<li><code>options</code>: used to declare options that are type-checked (e.g. strings, numbers, paths…);</li>\n<li><code>config</code>: definitions of the declared options.</li>\n</ul>\n<p>The result of the evaluation of a list of modules is all the <code>config</code> definitions (deep-)merged together, and where things get really interesting<span id=\"fn-lambda_calculus\"><sup class=\"footnote-reference\"><a href=\"#lambda_calculus\">3</a></sup></span> is that a module can receive the current, in-progress<span id=\"fn-fixed_point\"><sup class=\"footnote-reference\"><a href=\"#fixed_point\">4</a></sup></span>, state of the <code>config</code> evaluation as a parameter, so that it can reference options that have been set (in <code>config</code>) in another module. This design pattern is used extensively in the Nix ecosystem.</p>\n<h3>Dropping privileges <a href=\"#0-3-2-dropping-privileges\" id=\"0-3-2-dropping-privileges\"><span class=\"heading-anchor\">#</span></a></h3>\n<p>The other security related piece is how we drop privileges, and we could do a lot more here, but not running Nginx as root seems like a sensible thing to do. This piece gets interesting because our image is so bare-bones that it does not support PAM, and neither <code>sudo(1)</code>, nor <code>doas(1)</code> will work. Thankfully Nix makes it very easy to roll our own: we can inline a <a href=\"https://github.com/lopter/clan-destiny/blob/7dfda2c5604639290192ce0271f4293bdc5cc8a9/library/nix/packages/fly-io-pop/flake-module.nix#L624-L699\">little bit of C</a> like if it was another shell script, and makeup for the situation with our own <code>runas</code> helper.</p>\n<h2>Assemble and deploy the OCI image <a href=\"#0-4-assemble-and-deploy-the-oci-image\" id=\"0-4-assemble-and-deploy-the-oci-image\"><span class=\"heading-anchor\">#</span></a></h2>\n<p>My issue with the “native” Nixpkgs tooling for OCI images is that it creates one layer per derivation, which is better for caching, but unfortunately hits the practical and low number of COW filesystems that can be stacked on top of each other with decent performance (historically 128). Maybe an option to use bind mounts instead makes sense.</p>\n<p>Instead I have been using <a href=\"https://github.com/nlewo/nix2container/\">nix2container</a>, with the old fashioned way of composing layers yourself, I had it <a href=\"https://github.com/lopter/clan-destiny/commit/c318e81ed0854c5c5e7630db2bc80bad5c166ab5\">wrongly configured</a> the first time. The other cool thing nix2container does is to use skopeo to directly interact with a Docker registry. This allows me to create a couple tasks ([more shell scripts]): <code>pop-deploy</code> to release and/or push to prod, and <code>pop-releases</code> to list all the images (versions) I have uploaded.</p>\n<h2>Profits &amp; Losses <a href=\"#0-5-profits-losses\" id=\"0-5-profits-losses\"><span class=\"heading-anchor\">#</span></a></h2>\n<p>For sure some time was invested, things are missing… It runs for about $15 a month, scaling vertically is easier, I could easily add a 3rd replica manually. It has been fun, and useful to me.</p>\n<p>Did you learn anything? Did something surprise you? Did something feel <em>wrong</em> or <em>good</em>?</p>\n<hr />\n<div class=\"footnote-definition\" id=\"derivation_return_value\"><sup class=\"footnote-definition-label\">1</sup>\n<p>Technically it returns a <a href=\"https://nix.dev/manual/nix/2.24/language/derivations.html\">derivation</a>, that gets automatically converted to a string that represents the derivation’s output path under <code>/nix/store</code>. <a href=\"#fn-derivation_return_value\">↩</a></p>\n</div>\n<div class=\"footnote-definition\" id=\"nixos\"><sup class=\"footnote-definition-label\">2</sup>\n<p>NixOS is a Linux distribution built upon Nix and Nixpkgs. <a href=\"#fn-nixos\">↩</a></p>\n</div>\n<div class=\"footnote-definition\" id=\"lambda_calculus\"><sup class=\"footnote-definition-label\">3</sup>\n<p>And leave the realm of hacking to enter the realm of computer science and lambda calculus. <a href=\"#fn-lambda_calculus\">↩</a></p>\n</div>\n<div class=\"footnote-definition\" id=\"fixed_point\"><sup class=\"footnote-definition-label\">4</sup>\n<p>Sorry if I am butchering the <a href=\"https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fixedPoints.fix\">fixed point</a> concept. <a href=\"#fn-fixed_point\">↩</a></p>\n</div>\n","date_published":"2025-04-23","id":"0002-fly-io-sidecars-and-extra-luggage","tags":["nixpkgs","fly.io","process-compose"],"title":"How to use sidecars and bring extra luggage on fly.io","url":"https://www.kalessin.fr//blog/0002-fly-io-sidecars-and-extra-luggage"}],"language":"en","title":"Louis Opter (kalessin) :: Blog"}