I write about things. Mostly music, and DevOps related topics
by ~sky
I love to rave about nix, and how declarative configs should be the norm on Linux distros.
It should be no surprise, then, that I think NixOS should be an industry standard for operations, and all DevOps related woes.
Where’s the catch then? What’s the other side of this oh-so-perfect coin? Deployment.
Due to its unique design and its oddities, NixOS is hard to deploy. Well-known tools are built with “normal” distros like Debian or RedHat (or its derivates of course) in mind, and do not adjust well to NixOS’ oddities.
To deal with this, I’m gonna talk a bit about NixOps (formerly known as Charon)
“NixOps is a tool for deploying to NixOS machines in a network or cloud”, says the repository description.
More specifically, what it means is that you can define your actual NixOS machine on one side, and completely decouple it from its target(s). I’ll get to this in the example, but yes, this means that NixOps heavily simplifies both testing, and multi-cloud management.
Of course, standard limitations of NixOS still apply (that is, services that aren’t packaged on nixpkgs must be packaged and provided to the machine), but for the better part, most common services are provided and are ready to be consumed.
Getting NixOps is dead easy if you’re running on NixOS: simply add the nixops
derivation to the list of system (or user, if you prefer to use home-manager
) packages.
If running nix on a different system, however, the special precaution of running nixops
with sudo might be necessary as well.
Regarding configuration for AWS, profiles created via CLI work out of the box, with no extra setup required. Otherwise, NixOps supports a space-separated format for profiles, via the ~/.ec2-keys
file:
<AWS_ACCESS_KEY> <AWS_ACCESS_SECRET> <PROFILE_NAME>
What I hope to demonstrate with this example is how straightforward a NixOS declaration is, and how simple NixOps’s separation of concerns for targets is handled.
For this, I based my project off of the manual’s “Trivial Example”, which declares a NixOS machine with httpd, and valgrind
’s HTML documentation.
NOTE: Some fixes were necessary, since the example as-is in the NixOps manual uses a deprecated httpd declaration.
In order to demonstrate target separation as well, I’ve declared two AWS targets (to simulate real-world usage), and a local VirtualBox target. It’s also possible to target accessible NixOS machines via IP address, but this was not demonstrated in this project.
The example machine is declared as follows:
{
network.description = "Web Server";
webserver = {
config, pkgs, ...
} : {
services.httpd = {
enable = true;
adminAddr = "someaddress@mail.com";
virtualHosts.localhost.documentRoot = "${pkgs.valgrind.doc}/share/doc/valgrind/html";
};
networking.firewall.allowedTCPPorts = [ 80 ];
};
}
It’s a pretty straightforward machine declaration: on one hand, the httpd service is declared as enabled, and serving valgrind’s builitin documentation as the default site’s root. The default firewall is also extended, in order to allow connections on port 80
I’ll try to talk more about machine declarations in a different entry, but for now, this should explain the example machine well enough.
After some tweaks to the “trivial” AWS target, the working target expression ended up like so:
let
region = "us-west-2";
accessKeyId = "nixops-example"; # create credentials profile with this name
instanceType = "t2.micro";
ec2 =
{ resources, ... }: # Resources is an object containing all supported platforms, and its resources
{
deployment = {
targetEnv = "ec2";
ec2 = {
inherit accessKeyId region instanceType;
# This creates an AWS key pair and then supplies it to the instance
keyPair = resources.ec2KeyPairs.my-key-pair;
# You can optionally specify your own private key to supply
# This must be a .pem file
# privateKey = "/path/to/my-key-pair.pem";
# Optionally, other security groups can be specified as a list
securityGroups = [
resources.ec2SecurityGroups.allow-http-all
resources.ec2SecurityGroups.allow-ssh-all
];
};
};
};
in
{
webserver = ec2;
# these extra resources MUST be declared down here
resources.ec2KeyPairs.my-key-pair = { inherit region accessKeyId; };
resources.ec2SecurityGroups.allow-http-all = {
inherit accessKeyId region; # This allows avoiding repetition of assigning these values
name = "allow-http-all";
description = "lorem ipsum";
rules = [
{ fromPort = 80; toPort = 80; sourceIp = "0.0.0.0/0"; }
];
};
resources.ec2SecurityGroups.allow-ssh-all = {
inherit accessKeyId region;
name = "allow-ssh-all";
description = "lorem ipsum";
rules = [
{ fromPort = 22; toPort = 22; sourceIp = "0.0.0.0/0"; }
];
};
}
In essence, a NixOps project consists of a declared NixOS machine (aka, its configuration.nix
), and one or more targets for it.
The layout for this project is as follows:
/entrypoint.nix
: This is the “main” file for the machine declaration. It contains a collection of resources, which might be explicited there, or on separate files if necessary/targets/
: A directory that contains declarations for the many targets this projects aims to serve
vbox.nix
: VirtualBox targetaws.nix
: Trivial AWS targetaws-with-sg.nix
: As its name suggests, an extended AWS target with custom Security Group declarationsIf the project required custom packaging for certain resources, I’d include a pkgs
directory, with a <resource>.nix
for each one, exposing its resulting derivation.
A tests
directory could be included as well, in case the project is sensible enough to require an automated testing battery against the resulting machine (this is where an independent virtualbox/libvirt target comes in handy).
The “standard” cycle for a NixOps execution would be:
As a way of demonstration, these are copypasted commands I used while building the example project:
# Create the named deployment, specifying its details and deployment target
[reimu@hakurei:~/IaC/NixOps/Example]$ nixops create ./entrypoint.nix targets/aws.nix -d example
# Execute the named deployment
[reimu@hakurei:~/IaC/NixOps/Example]$ nixops deploy -d example
# Access "webserver" instance of the "example" deployment via an ssh wrapper
[reimu@hakurei:~/IaC/NixOps/Example]$ nixops ssh -d example webserver
# Destroy resources related to the "example" deployment
# Note: although nixops prompts confirmation for each resource, this can be
# skipped by passing the --confirm flag
[reimu@hakurei:~/IaC/NixOps/Example]$ nixops destroy -d example
# Get rid of the named deployment, to ensure no further use
[reimu@hakurei:~/IaC/NixOps/Example]$ nixops delete -d example
This is a collection of thoughts and things I considered worth noting that I encountered while building the project
The NixOps experience has been a fun ride so far. Of course, the contents described in this post merely scratch the surface of what this tool is capable, and I’m anxious to continue my jounrey deep down this monster.
All things considered, documentation is lacking for essential parts of the tool, which heavily plays against adoption over its alternatives. If using NixOps interests you, I urge you to get in touch with the community (the NixOS Discord, or its IRC channel).