The mostly technical DevOps blog

I write about things. Mostly music, and DevOps related topics

View My GitHub Profile

Fuel my coffee addiction https://cafecito.app/skyok

14 June 2020

The NixOps Experience: Are you deployed?

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)

Overview

“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.

Installation and configuration (AWS)

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>

Usage by example

Objective

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.

NixOS Machine

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.

Target: AWS with custom Security Groups

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"; }
    ];
  };
}

Project layout

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:

If 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).

Example execution

The “standard” cycle for a NixOps execution would be:

  1. Create deployment (specifying a machine, its target, and a name)
  2. Execute said deployment
  3. Once no longer needed, tear down the deployment
  4. If deprecated or no longer to be used, delete the deployment

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

Other notes

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).

tags: