These questions are Help in a philosophical sense. Not throw me the lifebuoy.
I’m curious about your mental models for nix and terraform.
I’ve used nix for declarative systems.
I’ve heard of terraform for declaratively provisioning cloud infrastructure. I have not used terraform.
I feel like putting nix and terraform into a general, similar category.
For those who have experience using either or both: how do you think about those tools? Or what mental model do you use for one but not the other?
nix (the programming language + the tool) describes how to package a program, in NixOS, nix can describe a system, so it would feel more like ansible but without the drawbacks.
you could also use nix for infrastructure as code, but you would have to write all the modules to do anything significant, I think it would be a pain. Nix is easier to use with fixed pure inputs, IaaC need states to now the differences between your code and what’s deployed, Nix isn’t good at this IMO.
And that’s why it doesn’t have drawback of ansible, on NixOS, it just doesn’t care about the current configuration, it recreates a new one from scratch, while ansible job is to modify a live system, but if you add a package/file and stop adding it, ansible won’t remove that extra file, NixOS configuration system wouldn’t include it anymore because the old file wouldn’t be part of the new system.
I actually find this statement interesting because state can be just encoded as a Nix expression anyway. It’s not like IaaC tooling does not encode state in files, be it binary or text ones.
terraform is there to setup your infrastructure (VMs, networks, load-balancers, buckets, projects, users, etc.). nix is there to declaritively orchestrate, deploy, or configure a compute instance (VM, container, dedicated server, etc.).
nix is more comparable to ansible, saltstack, chef, puppet, etc. It does have a language called nix or nix-lang, which can be used to write nix modules to do stuff like terraform, but I’d argue that no sane individual or group would do so. nix-lang simply is too cumbersome of a development experience to do so:
substantial lack of documentation
confusing error stacks (if you thought GCC’s compiler errors for generics were bad…)
UefiPls gave a good direct comparison, but I want to take a swing the “mental model” question.
Caveat: I use nix a lot, terraform and ansible 1 or 2 times, and years back I used chef a bit. I don’t think I’ve used anything I didn’t mention (puppet, salt, any of the Nix-ecosystem deployment toolchains, etc.).
AFAIK all of these systems are at least a little declarative, so I think it helps to unpack ~how and at what level(s) they are, and how some other concepts affect that. Maybe these 4 are a good start:
idempotence
atomicity
state(full|less)ness
I don’t think I’ve seen anyone name it, so I’ll hastily call it exclusivity, by which I mean roughly taking complete control of a resource or namespace.
Imagine we have a simple tool for managing which GUI applications are open. It has a simple file format with one application name (or absolute path) per line. We want to run it every morning with a spec like:
Firefox
WireShark
Terminal
Calculator
This file format is declarative on some level, but it doesn’t mean the tool is. Let’s imagine a few things the tool might do when we run it with the declaration above:
Just try to open all 4 apps. If FF were missing, WireShark failed to load, and Terminal was already open, we may end up with two open instances of Terminal and one of Calculator. I’d call this tool imperative, non-idempotent, non-atomic, stateless, and non-exclusive.
Ensure all or none of the listed apps are open by the time the command terminates (i.e., close successfully-opened apps if one failed). I’d say this tool is trying to be declarative. The extent to which it can be idempotent or atomic depends on what it says it does and how well it handles edge-cases such as already-open programs or programs that launch but then fail to close. It is stateless and non-exclusive.
Ensure the only GUI apps open are the ones in the list (i.e., close apps not in the list, but only if/once it successfully opens the set in the list). It’s trying to be declarative, idempotent, atomic, stateless, and exclusive–but as in #2 the extent it succeeds will depend on what it says it does and how it handles the same edge cases.
To make sure we follow the implications, I want to rotate this idea around by thinking through what each tool would do if run on an empty file:
This tool should do nothing.
If it’s actually declarative, stateless, and non-exclusive, this tool should also do nothing (it doesn’t have exclusive control, and it doesn’t know which programs it previously started).
If that sounds ~wrong or undesirable, the tool would need to be stateful in order to know what to close. If we make the tool stateful to cope with this, we’ll also have to weigh how it needs to behave when something else affects the state or what it describes. (Maybe you do this by PID, but they might reboot before they run it, or the app may self-update and relaunch, or the app may crash but the PID gets reused later. Or someone deletes the tool’s state file(s).)
Since it’s trying to be exclusive, this tool can close everything that isn’t in the list with a straight face and without needing to resort to some kind of stateful tracking. Assuming they all close, at least. (I’m saying “trying to be exclusive” rather than “is exclusive” because there are still other ways to open and close applications.)
I think the exercise (hopefully) illustrates a few things:
A literal answer to whether a language/component/tool is declarative can leave a lot of important information out of frame.
Without help from the system(s) it operates on/in, there’s a limit on how actually-exclusive a tool can be.
It’s tricky for declarative tools to be idempotent without severely limiting their scope, tracking state, or having exclusive control of the relevant resources/namespaces.
Exclusivity can simplify a declarative + idempotent tool–at the cost of having to specify all relevant details.
Atomic changes can greatly simplify the edge-case conditions an idempotent tool has to cope with.
I think we reach for declarative tools to limit the states we have to reason about, and these issues show that you need a lot more than just “declarative” to end up with toolchains that are actually capable of reliably converging on a limited number of expected states.
There are at least two really good auto-formatters out there, and there are also two LSP servers that, while indeed arguably incomplete, are genuinely getting there these days.