kivikakk.ee

devops? devops! (part 1)

This is part 1 of x in a series.

I have spent most of my life avoiding DevOps-y type things. At GitHub I got familiar enough with kubectl to help debug the applications I had deployed on it, but that was almost a decade ago and I don’t remember a single bit of it.

Most of the things I run I deploy with a really simple systemd unit definition in the Nix module. Here’s an excerpt from the one for the Elixir app this blog ran on:

{
systemd.services.kv = {
description = "kv";
enableStrictShellChecks = true;
wantedBy = [ "multi-user.target" ];
after = [ "kv-migrations.service" ];
requires = [
"postgresql.service"
"kv-migrations.service"
];
script = ''
export KV_STORAGE_ROOT="$STATE_DIRECTORY"
${envVarScript}
${cfg.package}/bin/kv-server
'';
serviceConfig = {
User = cfg.user;
ProtectSystem = "strict";
PrivateTmp = true;
UMask = "0007";
Restart = "on-failure";
RestartSec = "10s";
StateDirectory = "kv";
StateDirectoryMode = "0750";
};
inherit environment;
};
}

It’s very basic, and it worked beautifully! I love that, with NixOS, you can package a reproducible build (with all its dependencies), deployment strategy, and configuration schema all in one place. It’s so damn clean, and it works wonderfully for homelab- or personal services-scale systems. (For more, try Xe’s All Systems Go! talk, Writing your own NixOS modules for fun and (hopefully) profit.)

The downside is that this is not exactly a high-availability setup. When any of the dependencies of a service like this change — such as a new cfg.package, or change in environment — the result is that the existing service is stopped, the service is swapped out, and then the new one is started.

There can often be 10–30 seconds between the stop and start, depending on how much else the nixos-rebuild has to do. And while a failing build won’t leave you with a stopped service — you won’t even get that far — if the build succeeds, but the new service fails to come up for some reason, then you’ll be scrambling fast.

This being NixOS, getting your service back up is as easy as switching to the previous generation, and can be done very fast, but still, it’s not great. Realising this, and still very much wanting to use Nix as a build orchestrator in places where this isn’t an acceptable trade-off, it was time to learn a devops.


Structurally, Kubernetes seems relatively sound, giving us language for defining the shape of a deployed system upon many different axes. It is very YAML and it is very containers, neither of which I am the hugest fan of, but I felt pretty sure there would be tools to help with the former, and Nix my beloved has beautiful solutions for the latter.

If, like me before the start of this exercise, you don’t really know about the model Kubernetes gives you to work with, you might find useful David Ventura’s blog post, A skeptic’s first contact with Kubernetes. If I had found it before and not immediately after coming this far it would’ve been super helpful -_-

One thing worth mentioning is that, as a Very Nix Person (and Very Dissociated Person), I really need my infrastructure to be described in a version-controlled way. Ideally, I would be able to tie all of my infra back into the same place (which is vyx, a Nix flake).

So I decide to start up a cluster and begin experimenting. I hate Docker, Inc. with a passion — I will never forgive them for getting rid of Docker for Mac’s cute whale — plus I want to learn somewhere where I can actually deploy things, so I decide to start with k3s on my VPS. How I chose k3s to begin with, I’m not so sure — maybe because it has relatively few options exposed in its Nix module. Lightweight sounds good, and it’s a “certified Kubernetes distribution”. Whatever that means, it must be good!

NixOS has the option services.k3s.manifests, which is described as “auto-deploying manifests”. Perhaps this is the magic sauce I need to get my infrastructure as code!?

(The answer is, no, it isn’t — the entire cluster is restarted when you change its values, because NixOS. Teehee.)

Nonetheless, I struggled through writing some early manifests this way. Writing YAML in Nix is way better than writing YAML, and very easy to parameterise, extract functions, and so on. I had seen mention of Helm charts here and there, and while I felt like one day I would need to come to terms with them, I preferred to leave that until as late an opportunity as possible. As a bonus, using k3s auto-deploying manifests in this way meant I could write a NixOS module to deploy an application in Kubernetes, without a single line of raw YAML.

So, terrible in many respects — now bringing down an entire cluster on each change instead of just the relevant services (!!!) — but an introduction nonetheless. We are now at the point of siguiente:

  • Decided to turn that homelab server into a gaming PC instead, haha psyche! Instead decided to learn better how to cross-build things and operate k3s without trying to shove everything through a NixOS module.

Part 2 will cover building our own software ready for orchestration (using Nix — we won’t write a single Dockerfile, promise, and as a little bit of a spoiler, we won’t write a single Go template either), and the unique fun presented by developing on aarch64-darwin while largely deploying to x86_64-linux. :)

external-dns-bunny-webhook

Just a quick field report. I wanted to try ExternalDNS; I use bunny.net (obviously), and found external-dns-bunny-webhook! Perfect!

Only, it crash looped on startup. After asking myself whether I was really bothered enough to try getting a development environment setup for this (and then asking Annie, who helped resolve it to a yes), I rewrote the build process and development environment in Nix and got a fork with a fix going.

Given the sorry state of GitHub, I figured it was time to start opening “pull requests” there in the form of issues that contain instructions for fetching from off-site. Included here for posterity.

I really wanted to get this working for me, so I ended up forking the project with my fix and build environment. The webhook currently doesn’t handle root records being handed to it by ExternalDNS correctly, and will panic if that happens.

You can find the source here: https://nossa.ee/~talya/external-dns-bunny-webhook.

If you’d like to pull the relevant commit, this should do the trick:

$ git fetch https://nossa.ee/~talya/external-dns-bunny-webhook acb21849b8292222d7e0c85ac8d3dea913147bad
$ git cherry-pick FETCH_HEAD

The patch is inline here if you’d rather apply it directly with git am:

(elided)

girls kissing, in your area

“It could never work. She doesn’t like Nix flakes.”

íqán — sync Nix flake pins between projects.

I use Nix flakes a lot — both in development, and in how I deploy artefacts. (You can see my primary flake’s 17 inputs!)

One thing that has gotten a bit annoying, though, has been keeping the pins somewhat in sync. I regularly end up with a dozen nixpkgs and fenix and (etc. etc. etc.) variants in my Nix store, meaning I’d also have half a dozen different Rust, Erlang, Elixir and whatever other versions installed too. If I dare nix-collect-garbage -d, then working on any given project might give me a surprise as it turns out it had a slightly older pin and I actually need to wait for its special magical Elixir version.

And so on!

Using inputs.X.follows is all good and well when combining them into a unified whole, but I don’t use (and don’t intend to use) global devShells — I want each project to stand on its own. So, instead, I want a way to compare and optionally sync the locked versions from some reference to my projects. For a while I found it was a good enough hack to just cp ~/g/vyx/flake.lock . and then let the next Nix invocation remove all the irrelevant ones … but then I’d introduce a dependency which hitched its own nixpkgs along for the ride, and for Reasons™ that would get called "nixpkgs" in flake.lock, and my actual primary Nixpkgs pin called "nixpkgs_2". For a while I was then accidentally using that reference everywhere. Goodness.

Here’s íqán. It takes a plan file, which specifies the source (Vyx, for me), and then a list of targets along with which inputs should be synced. Here’s an excerpt of a sample run:

$ iqan ./iqan.json
Source: /Users/kivikakk/g/vyx
Target: /Users/kivikakk/g/a1d
-----------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/chog
------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/cmark-gfm-hs
--------------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/comenzar
----------------------------------
input nixpkgs is ahead of source (!)
source: 1751741127 (2025-07-05 18:45:27 UTC)
target: 1752620740 (2025-07-15 23:05:40 UTC)
(S)ync to source, or (I)gnore? s
Target: /Users/kivikakk/g/comrak
--------------------------------
input nixpkgs is behind source
source: 1751741127 (2025-07-05 18:45:27 UTC)
target: 1748437600 (2025-05-28 13:06:40 UTC)
(S)ync to source, or (I)gnore? s
input "fenix" has an original mismatch
source: github:nix-community/fenix
target: github:nix-community/fenix/monthly
(S)ync to source, or (I)gnore? s
[!!!] update flake.nix please!
Target: /Users/kivikakk/g/iqan
------------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/kivikakk
----------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/koino
-------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/kv
----------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/nossa
-------------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/notes
-------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/outfoxsync
------------------------------------
input "nixpkgs" has an original mismatch
source: github:NixOS/nixpkgs/nixos-25.05
target: github:NixOS/nixpkgs/nixos-24.11
(S)ync to source, or (I)gnore? s
[!!!] update flake.nix please!
$

It does the job. :)

Argon ONE V3 fan/power controller.

Seems almost a rite of passage to write one’s own for this accursed piece of hardware.

(The hardware’s actually quite OK, screw threading notwithstanding, but Argon 40 cannot write software to save themselves.)

https://nossa.ee/~talya/a1d