Declarative Configuration, Imperative Interface
-
Programs that have a declarative interface for their configuration like Kubernetes, Docker Compose, systemd, and OpenTofu are all easier to read through and reason about than their counterparts. Kamal takes a different approach and says commands should be obvious and imperative. Where is that line?
Kamal still has a config file where you declare how to build your container image, where to send it, where your hosts live, and any additional sidecar containers you want. Then it gives you imperative commands to get them there. Once you have Kamal configured, you just call kamal deploy to get a new version of your app deployed.
I can’t claim to be a Kubernetes expert (barely a novice), but once you have the configuration done, it seems to me that you just call kubectl -f config.yaml apply which is not as easy English as Kamal’s deploy but doesn’t it do the same thing? Both get your production service to a new state don’t they? Kamal had to roll their own proxy to get rolling, zero-downtime releases, so now they’re doing multi-container management. Kubernetes does that too, but has a lot of options and contributors for your use.
A big difference I see is that Kubernetes seems to guarantee an eventual consistency with the applied state through their operator pattern whereas Kamal has the new state up and running by the time the command finishes (or errors, and it rolls back). And, of course, K8s are a beast to run. But K3s seem easier to run yourself.
Both of these systems are way better than a bunch of shell scripts. Maybe better than Ansible too, I don’t have any experience there but that yaml scares me insofar as it reminds me of GitHub Actions. In both, you can declare things about the state of your system in a configuration file then both give you a standard set of commands for interacting with what’s been created with that configuration. I’m currently in the process of moving from shell script deployments for this static site to containerizing it and running it with Podman Quadlets.
Systemd gets a special mention here because I’ve been playing with Podman Quadlets. I’ve found their unit files and systemctl to work a very similar way. Declarative configuration with imperative commands. There are differences: INI syntax instead of yaml, containers running as processes, and it’s much closer to the UNIX system it comes from overall. But you can declare dependencies and start/reload commands that systemd then knows how to run with its standard interface. The same way Kubernetes and Kamal seem to work. It can do rolling releases and health checks. Has a decent structured logging interface out of the gate, and doesn’t require the overhead of a Kubernetes cluster.
I’m writing a follow up post about my experience setting up Quadlets. It’s been a little bumpy, but I really like the place they fit in. Systemd can be controversial, but I think it’s here to stay. It’s more standard than Kamal and less heavy than Kubernetes. The declarative nature of those Unit files reminds me so much of Docker Compose (which I have plenty of experience in, especially comapred to kube yaml). And that declarative nature gives me something much easier to read and is more repeatable to deploy than the shell scripts I had been using. I could set up the system from scratch by uploading a Container Unit and image to any server running systemd and podman (5.8+). That setup still has some shell scripts for now, but it’s a step in a direction I’m very happy with (and I can still build it on my own computer!).