技術紹介

Fixing NixPkgs in NixOps

Fixing Nixpkgs in NixOps

When building with Nix, one of the typical things developers will want to do is
fix the version of Nixpkgs being used. For those not already familiar,
Nixpkgs is the official repository of Nix
derivations (the Nix equivalent of dpkg’s and rpm’s packages). When the version
of Nixpkgs is fixed, it guarantees that the same derivation will build exactly
the same output every time. This frees developers from being concerned about
changes in upstream dependencies while developing their applications. Or worse,
dependencies changing and breaking their application after development and
testing but before building and deploying.

That need to keep the development environment the same as the
deployment build environment means that if NixOps is to be used for building
and deploying applications, its Nixpkgs version needs to be fixed to the same
version used during development.

Fixing Nixpkgs in NixOps is not well documented.
The official manual, sadly, doesn’t describe
how to do it. To summarise, it is done by setting the
nixpkgs prefix in the NIX_PATH environment variable
. Before describing
how we did this for our environment, I’d like to describe how something similar
is achieved from within Nix itself, and why it’s not quite as rigourous as using
NIX_PATH.

We’ll use a simple NixOS machine with the dhall
package installed to illustrate. I will be assuming a lot of what’s already
covered in the NixOps Manual. If a command
or configuration setting is unclear, I recommend checking the manual for
clarification.

First we’ll define a simple VirtualBox physical specification to deploy to.
I’ve opted for VirtualBox here to keep things simple but the choice of
platform isn’t important to this post. If you want to follow along you can
install VirtualBox and use this physical specification, or you can refer
to the NixOps Manual on how to retarget
the physical specification to a different platform:

example-vbox.nix:

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

The logical specification of the box to deploy with dhall installed and
without a fixed Nixpkgs:

example.nix:

{
network.description = "Example";

example =
{ pkgs, ... }:
{ environment.systemPackages = with pkgs; [ pkgs.dhall ];
};
}

Trying it out:

$ nixops create -d example ./example.nix example-vbox.nix
created deployment ‘994abedc-c273-11e9-950b-d89ef34b67c0’
994abedc-c273-11e9-950b-d89ef34b67c0
$ nixops deploy -d example
example> creating VirtualBox VM...
[...]
example> activation finished successfully
example> deployment finished successfully
$ nixops ssh -d example example
# readlink $(which dhall)
/nix/store/fk9433yg8hr71pzrm8gvakp6mfhnrdf0-dhall-1.19.1/bin/dhall
# readlink $(which bash)
/nix/store/mn4jdnhkz12a6yd6jg6wvb4mqpxf8q1f-bash-interactive-4.4-p23/bin/bash

The machine is up, we have SSH access to it and dhall is installed.

Going forward I will continue the convention introduced above of using a
$ prompt when executing commands on the local machine, and a # prompt when
executing commands as root on the deployed virtual machine.

We want to fix Nixpkgs to a specific version. Let’s try to do it
the way we ordinarily might when setting up a build environment (I’m pinning to
an older 18.09 version of nixos for illustration purposes):

example-2.nix:

let
nixpkgs_src =
builtins.fetchTarball {
# nixos-18.09
url = "https://github.com/NixOS/nixpkgs/archive/a7e559a5504572008567383c3dc8e142fa7a8633.tar.gz";
sha256 = "16j95q58kkc69lfgpjkj76gw5sx8rcxwi3civm0mlfaxxyw9gzp6";
};
fixedpkgs = import nixpkgs_src {};
in

{
network.description = "Example";

example =
{ pkgs, ... }:
{ environment.systemPackages = with pkgs; [ fixedpkgs.dhall ];
};
}

Redeploy and check the versions:

$ nixops modify -d example ./example-vbox.nix ./example-2.nix
$ nixops deploy -d example
building all machine configurations...
[...]
example> activation finished successfully
example> deployment finished successfully
$ nixops ssh -d example example
# readlink $(which dhall)
/nix/store/8m0bqimml5malpm02yajf35z5b9hqv8n-dhall-1.15.1/bin/dhall
# readlink $(which bash)
/nix/store/mn4jdnhkz12a6yd6jg6wvb4mqpxf8q1f-bash-interactive-4.4-p23/bin/bash

This has successfully changed the version of dhall to the one in our pinned
version of Nixpkgs. However, notice that everything else, including bash,
remains the same as the unpinned version. What if we want to pin the whole
operating system?

Our strategy of loading up the nix tarball within example-2.nix won’t work.

{ pkgs, ...}:
{ environment.systemPackages = with pkgs; [ fixedpkgs.dhall ];
};

The above configuration is a
NixOS module
which is defined as the following:

  • a function taking a set (which includes a pkgs attribute)
  • the function produces a NixOS configuration specifying how to build the machine

The pkgs attribute is given by NixOps itself. We can use individual packages
from a different, pinned version of Nixpkgs in cases where we explicitly refer
to something in Nixpkgs (as we did for our dhall installation). However, the
operating system itself is implicitly built off whatever NixOps chose to pass
as that pkgs attribute.

To tell NixOps which Nixpkgs to use, the NIX_PATH environment variable must
be used. Since we’re now pinning the Nixpkgs passed as pkgs, we’ll revert to
the original example.nix configuration without the specially defined
fixedpkgs:

$ nix-prefetch-url https://github.com/NixOS/nixpkgs/archive/a7e559a5504572008567383c3dc8e142fa7a8633.tar.gz --unpack
unpacking...
[14.4 MiB DL]
path is '/nix/store/qbzbhgq78m94j4dm026y7mi7nkd4lgh4-a7e559a5504572008567383c3dc8e142fa7a8633.tar.gz'
16j95q58kkc69lfgpjkj76gw5sx8rcxwi3civm0mlfaxxyw9gzp6

$ nixops modify -d example ./example-vbox.nix ./example.nix
$ NIX_PATH="nixpkgs=/nix/store/qbzbhgq78m94j4dm026y7mi7nkd4lgh4-a7e559a5504572008567383c3dc8e142fa7a8633.tar.gz" nixops deploy -d example
building all machine configurations...
[...]
example> activation finished successfully
example> deployment finished successfully
$ nixops ssh -d example example
# readlink $(which dhall)
/nix/store/8m0bqimml5malpm02yajf35z5b9hqv8n-dhall-1.15.1/bin/dhall
# readlink $(which bash)
/nix/store/6sczmwmyx81z1h88v2x434jr3s8qd1vz-bash-interactive-4.4-p23/bin/bash

And now we see that not just dhall, but the whole operating system has been
pinned to the version of Nixpkgs set in NIX_PATH.

Having to set NIX_PATH for every invocation of nixops deploy is not very
user friendly or robust. And we’d like to be able to check our fixed Nixpkgs in
to a version control system. So we set up a shell.nix to take care of it all
for us:

shell.nix:

{ config ? {}
, nixpkgs ? null
} :

let

nixpkgs_src =
if nixpkgs == null
then builtins.fetchTarball {
# nixos-18.09
url = "https://github.com/NixOS/nixpkgs/archive/a7e559a5504572008567383c3dc8e142fa7a8633.tar.gz";
sha256 = "16j95q58kkc69lfgpjkj76gw5sx8rcxwi3civm0mlfaxxyw9gzp6";
}
else nixpkgs;

pkgs = import nixpkgs_src { inherit config; };

in

pkgs.mkShell {
buildInputs = [ pkgs.nixops ];
NIX_PATH = "nixpkgs=" + nixpkgs_src;
NIXOPS_DEPLOYMENT = "example";
}

Getting into that Nix shell and redeploying:

$ nix-shell
$ # we are now in the above nix shell
$ nixops deploy
building all machine configurations...
[..]
example> activation finished successfully
example> deployment finished successfully

Now the NIX_PATHand the NIXOPS_DEPLOYMENT (the equivalent of the
-d flag we’ve been passing to nixops) variables are set in our shell. Users
no longer even need to install nixops themselves. So long as they have nix
installed, the shell will take care of ensuring nixops is available.

As long as nix-shell is run before any nixops commands, the correct version
of Nixpkgs will always be used.

References