GitOps Delivery Platform
A curriculum exercise that turned into the most useful thing I've built for understanding delivery: three stages that go from two bare virtual machines to a cluster that reverts your manual changes because Git didn't ask for them.
- 2 → 1 → 0
- VMs needed, as each stage gets closer to containers
- 1 command
- from empty machine to synced cluster
- prune + self-heal
- drift reverted without human action
What it is
Three stages, each one removing a layer of machinery. The first provisions two virtual machines and joins them into a K3s cluster — one server, one agent. The second drops to a single node and puts three applications behind one ingress, routed by hostname. The third abandons virtual machines entirely for K3s-in-Docker and hands control to Argo CD, which watches a Git repository and makes the cluster match it.
A bonus stage replaces GitHub with a self-hosted GitLab installed by Helm, with Postgres, Redis and object storage running outside the cluster, so the Git server being synced from is itself part of the infrastructure.
The part that was actually hard
Not the Kubernetes. The networking. This was built on an Apple Silicon machine, which means QEMU rather than VirtualBox, and the Vagrant QEMU provider has no high-level networking primitives at all — no private network you can declare, nothing that hands a VM an address.
So the private network between the two nodes is built out of a TCP socket. The server VM's second network card opens a listener; the worker's connects to it. The socket is the Ethernet cable. That has a consequence most Vagrant setups never encounter: boot order becomes load-bearing, because the listener has to exist before the other end tries to connect, so parallel provisioning is disabled deliberately rather than by accident.
Nothing on that segment hands out addresses either, so each node configures its own — from a single template rendered per VM, matching the interface by MAC address and renaming it, so the configuration survives the kernel enumerating devices in a different order on the next boot.
- Addresses are static because no DHCP server exists on a socket-backed LAN — there is nothing there but the two VMs.
- The environment parser is hand-written in the Vagrantfile, because the plugin that would normally do it breaks on modern Ruby.
- Argo CD is installed with server-side apply, because its manifests exceed the annotation size limit that client-side apply imposes.
Proving the loop actually closes
It is easy to install Argo CD and believe you have GitOps. The test is whether the cluster follows Git when Git changes, and whether it fights you when you change the cluster by hand.
The application definition enables automated sync with pruning and self-healing, so resources Git stops mentioning are removed and manual edits are reverted rather than tolerated. The watched repository's history is the demonstration: it is a sequence of image tag changes, moved to a new version and then back again, each one landing in the cluster without anybody running a deploy command.
One thing I changed my mind about mid-project is in that history too. The application originally tracked the highest Git tag in a version range, which sounds tidier than tracking the branch head. In practice it made the question 'what is deployed right now' require resolving a tag glob, so it went back to tracking the head — less clever, immediately answerable.
What I'd fix before calling it production
It is worth being precise about the gap between this and something you would run a business on, because the gap is mostly one thing: nothing is version-pinned. Every component installs from a 'stable' or 'latest' URL, and the container images have no tags. It works today and is not guaranteed to work next month, which is the opposite of what reproducible provisioning is for.
The bonus stage is also unfinished — it has a hardcoded address and an unsubstituted placeholder in its application definition. I'd rather say that than present it as complete.
Built with
- K3s
- k3d
- Argo CD
- Vagrant
- QEMU
- Traefik
- Helm
- GitLab
A curriculum exercise, published as-is. It demonstrates the delivery mechanics faithfully; it is not pinned or hardened for production use.