Deploying NixOps automatically to any server provider

Deploying NixOps automatically to any server provider

NixOps is a great tool for deploying both locally and to the cloud. There's also a few tools to automatically deploy a NixOps configuration to a lot of cloud providers, such as Google, Hetzner, DigitalOcean, AWS, Azure and more.

But NixOps is still in a beta and it's functionality is still heavily developed, so things have a tendency to break. At the time of writing this post, one of those "broken" implementations is the DigitalOcean plugin, the DO API specifications have changed since the NixOps deployment plugin was written. So now it's suddenly not working! Uhoh!

So how do you deploy to DigitalOcean (or any other KVM/VPS/Server provider) without being able to use the plugins? Well, the good news is that after a little bit of preparation, it's still pretty easy to do! NixOS (not NixOps) has a long list of configurations for server providers (including generic configurations which should work with all providers), so we can take advantage of that.

What is required to get this to work?

  • The provider has to support uploading custom OS images.
  • Nix-shell
  • SSH keypair, we will only use the public key (never share your secrets!)

That's it!

So you may wonder how this is gonna work? Well first we will make a minimal custom NixOS image with our SSH keys added to it. Sounds complicated? It's not! Nix comes with a featureset that let's us define a OS declaration and build a custom image within minutes by just running a few simple commands.

Building the OS

First let's get a build script to actually build our OS image that we can use to upload our custom image to DigitalOcean, the following has proven handy for me:

GitHub - Hoverbear-Consulting/nixos-digitalocean: A minimal NixOS image builder for DigitalOcean.
A minimal NixOS image builder for DigitalOcean. Contribute to Hoverbear-Consulting/nixos-digitalocean development by creating an account on GitHub.

You can either use git to clone it or just download it as a zip.

The only changes I've made is to add my own public key to the configuration.nix - I'm not 100% sure if this is actually neccessary since the custom NixOS image should get the selected private key when creating a droplet. If you are interested in what the digitalocean configuration looks like, you can head over to:
https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/digital-ocean-config.nix

Either way, going on with what I have tested and I know works, an example of adding your own SSH key could be as follows:

users.users."root".openssh.authorizedKeys.keys = [
     "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAA ...rest of the key"
]

For those who want a full example config, here you go! (Only change from the original repo is the private key.

# Based off the amazing article by
# https://nixos.mayflower.consulting/blog/2018/09/11/custom-images/

{
  nixpkgs ? <nixpkgs>,
  configMixin ? null
}:

# The default configurations of this image.
# Want to make your own? You should **definitely** start from the minimal.
let configurations = {
  minimal = passedPkgs: {
    imports = [
      "${nixpkgs}/nixos/modules/virtualisation/digital-ocean-image.nix"
    ];

    networking.hostName = "nixos";
    services.openssh = {
      enable = true;
      passwordAuthentication = false;
      
      permitRootLogin = "yes";
    };
    users.users."root".openssh.authorizedKeys.keys = [
      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAA ...rest of the key"
    ];
    environment.systemPackages = with nixpkgs; [
    ];
  };
};

mkConfiguredNixOsForDigitalOceanImage = configuration:
builtins.trace "Got config:"
builtins.trace configuration
(
  let instantiatedNixOs = import "${nixpkgs}/nixos" {
      inherit configuration;
      system = "x86_64-linux";
  };
  in
    instantiatedNixOs.config.system.build.digitalOceanImage
);
in {
  minimal =
    builtins.trace "Building minimal image..."
    mkConfiguredNixOsForDigitalOceanImage (configurations.minimal nixpkgs);
}

Now it's time to build, first run the command nix-shell --pure --keep NIX_PATH -p nixUnstable to enter a shell with nixUnstable.
Then run the the bash script from the root of the repository sh ./build.sh
This should build an image for you and put it into result/nixos.qcow2.gz.

Troubleshooting

Currently I've stumbled upon two "bugs", one is related to virtualbox and kvm. If you got a virtualbox or kvm instance running, the OS build may fail. Make sure to exit those programs and try again.

The second "bug" is having to add the --extra-experimental-features nix-command flag to the build script (the build command will tell you and fail), such as:

set -e -o verbose

nix build \
    --show-trace \
    --file default.nix \
    --extra-experimental-features nix-command \
    minimal

Uploading the custom image to digitalocean

This has to be done through the digitalocean dashboard. Digitalocean actually has a really solid tutorial on how to do this over at:
https://docs.digitalocean.com/products/images/custom-images/how-to/upload/

Uploading custom images could be automated, but it would require you to upload the image somewhere due to the DO API only being able to pull images from a URL (you can create a DO spaces and upload it there programmatically and delete it after). If you use the dashboard you can upload it directly in the browser.

After uploading your custom imamge, head over to create a new droplet. Choose your custom image and create your new droplet with the desired parameters. The NixOS configuration will take care of i.e installing the DO metric agent, setting hostname settings and desite not having cloud-init, the configuration is made to work without it. Which is actually really cool!

So now you got a droplet/server set up, now what? Aren't we supposed to deploy something?

Starting to deploy with NixOps

Locally

I suggest first testing on your local computer before deploying to the server we just created. There's already a few NixOps tutorials on how to do this but let's set up a simple HTTP server which returns us the NixOS docs.
First we add the logic, let's name this logic httpdocs-logic.nix

{
    network.description = "Web server";

    webserver =
    { config, pkgs, ... }:
    { 
        services.nginx.enable = true;
        
        services.nginx.virtualHosts."example" = {
            locations."/" = {
            root = "${config.system.build.manual.manualHTML}/share/doc/nixos/";
            };
        };

        networking.firewall.allowedTCPPorts = [ 22 80 443 ];
    };
}

Next, let us set up the hardware config (make sure you got virtualbox installed!), let us call this httpdocs-vbox.nix

{
  webserver =
    { config, pkgs, nixpkgs, ... }:
    { 
      # Deployment target environment
      deployment.targetEnv = "virtualbox";
      # Memory size in case of VM
      deployment.virtualbox.memorySize = 512;
      # Amount of vCPUs assigned to VM
      deployment.virtualbox.vcpu = 1;
      deployment.virtualbox.headless = true;
    };
}

Now let's create a deployment by running the following commands

  1. nixops create httpdocs-logic.nix httpdocs-vbox.nix -d httpdocs.local
  2. nixops deploy -d httpdocs.local --force-reboot (force reboot is set because virtualbox crashes at the initial deployment, it's a bug)

After that, nixops should deploy the webserver. The logs should show an IP address that you can go to and the docs should be available.

In the garden

girl-planting-lily

Sorry you can't use Nixops to plant plants. Yet.

DigitalOcean

But we can deploy Nix to DigitalOcean! After confirming the logic and local deployment works, let's do a remote deployment to Digitalocean with our new droplet. It's actually pretty easy, let's create a new hardware configuration called httpdocs-digitalocean.nix (replace the IP address 127.0.0.1 with your droplet IP address.

{
  webserver = 
    { config, pkgs, ... }:
    {
      imports = [
        <nixpkgs/nixos/modules/virtualisation/digital-ocean-config.nix>
      ];
      deployment.targetHost = "127.0.0.1"; # Replace with your droplet IP address!
    };
}

Now run the previous commands, just adjust for the new hardware config:

  1. nixops create httpdocs-logic.nix httpdocs-digitalocean.nix -d httpdocs.digitalocean
  2. nixops deploy -d httpdocs.digitalocean (will ask you for the SSH key password, if you got one set. Make sure the SSH key you set previously is the same as your default ssh keypair.

After doing that, congrats, you've hopefully deployed to digitalocean successfully!