Using Nix as a Professional
Summary
There's a chance that if you're reading this, you're aware of NixOS or Nix. If not, welcome to a quick summary.
Nix is both a tool and an outright operating system. Although I love it for a Linux operating system, I am going to assume you are using it as a package management tool. In its most pure sense, it allows you to define what version of software and which software you're using declarative language. It's agnostic to operating systems, languages, or tooling. I personally use it with my Mac so that I can have a consistent environment for each of my projects, which, if you've been reading my blog, you'd be aware travels across a lot of languages and tool.
With that quick summary out of the way, let's talk about how to install it, use it, an excellent repository of simple configurations, and a dive into a living example.
Installing Nix
Installing Nix as a tool is extremely easy, thankfully.
They provide instructions for multiple operating systems, but on a Mac you simply run
sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume --daemon
Mac has an issue with encrypting volumes within volumes for the Nix store. Although it's perfectly safe, you do have to install it in such a way to deal with encrypted volumes.
This installs Nix, pure and simple. You can test it by running
nix-env --version
If you get a response, you're good to go
Nix Shells
Now that you have the tool, how do you use it?
Maybe not the easiest way, but the correct way is to explicitly declare precisely what you need in a given environment. To do this, you write a file called nix.shell
with exactly what you need. Although I have several in personal projects, the master of keeping good nix shells is Seth Doty in his aptly named nix-shells repository. If you grab one of his files and put it in your directory, simply run nix-shell
and Nix will grab the packages from the Nix store and build them. Let's grab an example, his Go Nix Shell.
The file itself builds Go 1.14 and provides no additional shell hooks (meaning Go
works as a command, but no other aliases)
with import <nixpkgs> { };
stdenv.mkDerivation {
name = "go";
buildInputs = [
go_1_14
];
shellHook = "";
}
The opening is consistent across all shell.nix
files where it is importing packages. However, his mkDerivation
flag means that it'll grab the go
package but tell it specifically to build 1.14 instead of whatever is the latest Go package. Pretty straight forward and simple, yeah? Let's dig into one of his better ones, specifically, the Rust Nix Shell.
let
moz_overlay = import (builtins.fetchTarball
"https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz");
nixpkgs = import <nixpkgs> { overlays = [ moz_overlay ]; };
in with nixpkgs;
stdenv.mkDerivation {
name = "moz_overlay_shell";
buildInputs = [
#nixpkgs.latest.rustChannels.nightly.rust
nixpkgs.latest.rustChannels.stable.rust
rustfmt
#rustup
#cargo
];
shellHook = ''
alias help="
echo 'cargo new _'
echo 'cargo build'
echo 'cargo check'
echo 'cargo run'
echo 'cargo build --release'
";
alias setup="
rustup install stable
rustup default stable
";
cd $HOME/workspace/Rust;
'';
}
This leverages the Firefox build overlay (because Mozilla created Rust, effectively, and started using it in Firefox, which builds in a ton of features and functions.) What's more is that not only does it grab the Rust stable version, but is also sets up aliases based on the rustup toolchain. This means whether you grab this file, I grab this file, or Seth grabs this file, we'll have the same aliases, the same latest Stable version, and the same workspace for Rust. It's consistent across the board and Nix ensures that.
He has plenty more files, including Kubernetes which also plugs you into the demo dashboard, Docker and Docker-Compose and others. They are great foundational files as you build your own. As a professional, it's more than acceptable to start with something working and expand it to what you need.
Once you have the shell.nix
of your choice, simply run nix-shell
and it'll load your environment up specific to that shell. It'll download, do the build, and load it up just the way you (or Seth) declared it.
So, what do you do next? Commit it into your repo! By having a shell.nix
directly in your repo, you and your fellow contributors can git clone
a project and immediately run nix shell
to be using the exactly same builds for every member with a single command. It's not just a fringe tool, either, professionals in many industries love it.
When do professionals use nix?
In repositories, so that developers are consistent! Some great examples are
- Waypoint - Hashicorp's tool for automated deployment. (Side note, co-founder of Hashicorp, Mitchell Hashimoto, uses NixOS as his primary development environment and even tweeted about it and keeps his NixOS Configuration public on GitHub.)
- AT&T - An excellent blog post for explaining Nix versus other solutions.
- Red Hat - A great video on how and why they are using it.
If we dig into the Waypoint, the first thing you'll notice is a reference to flake
(
import (fetchTarball https://github.com/edolstra/flake-compat/archive/master.tar.gz) {
src = builtins.fetchGit ./.;
}
).shellNix
What Flake does is it takes nix to the next level, by pinning the exact versions you are on to a flake.nix
and a flake.lock
. How it works is that flake pins to specific repo's /nix/
directory and leverages a combination of *.nix
files and most importantly an overlays.nix
file. Without getting too deep, overlays allow you to apply explicit definitions for overriding packages with a concept of super
arguments. This pins specific versions in Waypoint's overlays.nix
by referencing the other *.nix
files.
As we can see, the overlays.nix file declares which files to import for version
final: prev: {
# This is the pinned protoc version we have for this project.
protobufPin = prev.protobuf3_15;
devShell = final.callPackage ./waypoint.nix { };
# Need to manually do this since 1.16 is still the default
go = final.go_1_17;
go-protobuf = prev.callPackage ./go-protobuf.nix { };
go-protobuf-json = prev.callPackage ./go-protobuf-json.nix { };
go-tools = prev.callPackage ./go-tools.nix { };
go-mockery = prev.callPackage ./go-mockery.nix { };
go-changelog = prev.callPackage ./go-changelog.nix { };
}
And if we go check the go-protobuf.nix file, we can see it very specifically defines which to pull down
{ buildGoModule, fetchFromGitHub }:
buildGoModule rec {
pname = "go-protobuf";
version = "1.5.2";
src = fetchFromGitHub {
owner = "golang";
repo = "protobuf";
rev = "v${version}";
sha256 = "1mh5fyim42dn821nsd3afnmgscrzzhn3h8rag635d2jnr23r1zhk";
};
modSha256 = "0lnk2zpl6y9vnq6h3l42ssghq6iqvmixd86g2drpa4z8xxk116wf";
vendorSha256 = "1qbndn7k0qqwxqk4ynkjrih7f7h56z1jq2yd62clhj95rca67hh9";
subPackages = [ "protoc-gen-go" ];
}
It also includes the SHA checks to make sure that it is precisely the version used - meaning everyone is using exactly the same version.
Conclusion
Nix is a powerful tool to help your developers to stay consistent in building environments and keeping all your developers on the same page. Although the language is a bit rough to learn, it provides a smooth ride for everyone and reduces the "well it works on my machine" issue that many developers have. Alongside other tools, it can be a great way to reduce downtime due to technical issues and keep working on projects the same across the board.