Date: Sep. 24, 2019
By: Robert Prije

NixOps’ declarative configuration is statically defined. But sometimes we want parts of the configuration to be dynamic. At Cross Compass there were two classes of configurations we wanted to be dynamic: the repository revisions to build our platform from, and a set of secrets to be used.

The revisions need to be dynamic because we intend to automatically trigger a redeployment using the latest available revisions of our codebases. Ideally we don’t want to generate a full new NixOps configuration each time we do this: we want the NixOps configuration to stay mostly static and amenable to being checked into a version control repository of its own.

We don’t want that checked in configuration to contain secrets both because we don’t want secrets in version control, and because secrets in nix expressions get copied into the world-readable /nix/store.

Fortunately, NixOps supports passing arguments to expressions defined in network files. This is described in the Network Arguments section of the NixOps Manual.

Along with providing code for following along with this blog post, the repository git@github.com:xc-jp/blog-post-code.git will serve as a test for dynamically passing Git revisions and secrets to NixOps. Included in the DynArgs directory is a Nix expression which builds a small script my-app. This script simply takes a file containing a secret and prints the contents within a message.

{ pkgs ? import <nixpkgs> {}}:

pkgs.writeScriptBin "my-app" ''
  #!${pkgs.runtimeShell}
  set -euo pipefail
  SECRET=''${1:-}

  [[ -n $SECRET ]] || (echo "Usage: $0 SECRET_FILE" && exit 1)

  echo "Running with secret: $(cat $SECRET)"
''

Here’s an example of a network file describing the software to install and run on a NixOS host. It includes the derivation for my-app:

dynargs.nix:

{ myApp, secret }:

let
  myAppSrc = builtins.fetchGit {
    url = "git@github.com:xc-jp/blog-post-code.git";
    inherit (myApp) rev ref;
  };
in

{
  network.description = "Example";

  example =
    { pkgs, lib, ... }:
    let
      # This turns a string into an absolute or relative
      # nix path conditional on whether the string begins with a '/'
      toPath = s:
        if lib.hasPrefix "/" s
        then /.  + s
        else ./. + "/${s}";
    in

    {
      environment.systemPackages =
        [ (import "${myAppSrc}/DynArgs" {inherit pkgs;}) ];
      deployment.keys.my-app-secret = {
        text = builtins.readFile (toPath secret);
      };
    };
}

Notice that this logical file takes an argument { myApp, secret }. This turns the network file into a function taking a dynamic argument that can be supplied from nixops set-args.

We’ll try it out using example-vbox.nix which was described in a previous blog post:

example-vbox.nix:

{
  example =
    { config, pkgs, ... }:
    { deployment.targetEnv = "virtualbox";
      deployment.virtualbox.memorySize = 1024; # megabytes
      deployment.virtualbox.vcpu = 2; # number of cpus
    };
}

Note if you have already run nixops create -d example once (either from an earlier run of the code in this blog post, or while following along with the previous blog post ) then you already have an example deployment and may need nixops modify instead of nixops create here:

$ echo "my password" > /tmp/secret
$ nixops create -d example ./example-vbox.nix ./dynargs.nix
$ nixops set-args -d example \
    --arg myApp '{ref = "master"; rev = "675b7705d46dfc567c768f0f725eb2bbc55b0675"; }' \
    --argstr secret /tmp/secret
$ nixops deploy -d example --allow-reboot
example>
[...]
example> deployment finished successfully
$ nixops ssh -d example example
# my-app /var/run/keys/my-app-secret
Running with secret: my password

You can stop the running instance with nixops stop -d example --include example.

A few things were introduced here.

First, we used builtins.fetchGit to checkout a commit from git@github.com:xc-jp/blog-post-code.git which contains our my-app.

The Git commit revision and Git reference defining the commit to build against is given through the NixOps argument under the myApp attribute: --arg myApp '{ ref = "master"; rev = "675b7705d46dfc567c768f0f725eb2bbc55b0675"; }' This value is then used by builtins.fetchGit: inherit (myApp) rev ref;

We can see my-app producing the output we expected when run against the file /var/run/keys/my-app-secret.

/var/run/keys/my-app-secret itself was deployed from nixops via the use of deployment.keys. deployment.keys is documented in the NixOps manual under Managing Keys. In brief, it provides a way of deploying secrets to a NixOS machine without those secrets being copied to the world-readable Nix store. The keys are deployed to a volume residing in volatile memory ensuring that the keys are not persistently stored on a disk controlled by a third-party cloud provider.

We populated the text of deployment.keys.my-app-secret by using a combination of builtins.readFile and toPath to ensure the path to the secret file remains a string for as long as possible minimising the time it exists as a Nix path and the risk of it getting copied into the Nix store.

The path used in deployment.keys.my-app-secret is defined in the secret argument we provided to NixOps: --argstr secret /tmp/secret.

Note that we used --argstr rather than --arg. This guarantees that the argument will be passed to our function as a string and further reduce the likelihood that we unintentionally copy the contents into the world-readable nix store.

In this way, we have successfully passed run-time arguments to NixOps setting both the revision of our repository to build our application against, and a secret to be used by our application.

Should we want to change the secret path or the repository revision, we will need to make another call to nixops set-args.

If you’d like to see what files and arguments NixOps has registered for a deployment, you can use nixops info.

One behaviour I noticed that may be counterintuitive is that if paths passed as arguments do not change, no update will take place even if the contents of the path changes. As such, a more realistic example of /tmp/secrets should use something like mktemp to ensure a new file path is created each time. The file can then be removed after the deployment.

Updated: