Fixing Nixpkgs in NixOps
Date: Aug. 29, 2019
By: Robert Prije
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_PATH
and 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.