NixOS

Declarative Linux Distribution & Purely Functional Package Management 

Franz Pletz / @fpletz

Mayflower GmbH

FrOSCon 2016

About the speaker

Package Management 

Let's talk about…

Features of a Typical  Package Manager

  • codifies software build process
  • creates a distributable package
  • manages package lifecycle
  • maintains a package database or repo 
  • ensures package integrity & authenticity
  • version & dependency management

Procedural Approach

Build results depend on inherited state

Package installs modify state

Procedural Approach

Typical WTF Moments

  • Package compiles/runs on machine A but fails on machine B
  • Installing packages fails, system ends up in a broken or unknown state
  • New version of a library changes ABI, all dependent packages are broken
  • User modified a file of installed package, gets overwritten by package upgrade
  1. Functions always evaluate the same result value given the same argument value
  2. Evaluation of the result does not cause any semantically observable side effect

Purely? Functional?

Functional Approach

 Useful properties:

  • Reproducible
  • Atomic
  • Conflict-free
  • Immutable

Nix

The purely functional package manager

What is Nix?

  • PhD thesis of Eelco Dolstra (2006)
    "The Purely Functional Deployment Model"
  • build results only depend on declard inputs
  • implemented in C++
  • uses minimal purely functional language
    • describes package builds
    • defines dependencies
    • lazily evaluated, dynamically typed

Key Features of Nix

  • immutable package store
  • atomic upgrades & rollbacks
  • isolated build environment
  • shell environments
  • runs on POSIX (Linux, *BSD, OS X)
  • multi-version support
  • multi-user support
  • source/binary model

Standard Package Set

nixpkgs

  • Github Repository
  • Lots of abstractions
    • fetch sources from git, mercurial, …
    • a standard build environment
    • wrappers for lots of build systems
  • > 10.000 packages available
  • can be used on other Unix systems & Darwin

Purity in Nix

  • functional language
  • chroot
  • no networking
  • patched shebangs
  • /nix/store mounted read-only
  • patches to tooling
/nix/store/s3fikpaws0z0wdkgs53nym05wj4wjy5w-openssh-7.3p1

A package is an output of a pure function. It depends only on the function inputs, without any side effects.

Purity in Nix

src = fetchurl {
  url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
  sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
};

Fixed Output Derivations

For impure derivations where the output is known before the build.

Guarded by cryptographic hash of output.

src = fetchFromGitHub {
  owner = "ponylang";
  repo = "ponyc";
  rev = "4eec8a9b0d9936b2a0249bd17fd7a2caac6aaa9c";
  sha256 = "184x2jivp7826i60rf0dpx0a9dg5rsj56dv0cll28as4nyqfmna2";
};

Dependencies of openssh

openssh Closure

$ du -sh $(nix-store -qR `nix-build -A openssh`)
19M     /nix/store/10zyyd2m90vqzj8gyqqdikzmg6a4dzqp-glibc-2.23
562K	/nix/store/1zcm7nlw89y9hqq5ihhbkkmncw6s0vm8-bash-4.3-p42
8.4M	/nix/store/6bscdl1q74d16d1c7hp84j77mpy9q5jg-ncurses-6.0
126K	/nix/store/lx142x7lrirv833ckk74niiy3fp5p2wa-ncurses-6.0-man
534K	/nix/store/02fpx56jrmbs8pgkdv0ihql2sdzbaxdq-ncurses-6.0-dev
1.8M	/nix/store/381mr3hs51sbw4r53hc7adixlmard2cv-libressl-2.4.2
66K	/nix/store/lcazrvc9390cm4bi95n458jabrs6y8z4-zlib-1.2.8
815K	/nix/store/67ggrjiphzll3sy60zcv642r7jiv94nj-cracklib-2.9.6
1.8M	/nix/store/g2q9kgmjsr6sbpnjmkbnvq298p89ps3f-linux-pam-1.2.1
209K	/nix/store/zbc4iykrf79hj0gr7s77l56743yf5wml-libedit-20150325-3.1
3.1M	/nix/store/s3fikpaws0z0wdkgs53nym05wj4wjy5w-openssh-7.3p1

openssh Store Path

$ tree `nix-build -A openssh`
/nix/store/s3fikpaws0z0wdkgs53nym05wj4wjy5w-openssh-7.3p1
├── bin
│   ├── scp
│   ├── sftp
│   ├── ssh
│   ├── ssh-add
│   ├── ssh-agent
│   ├── ssh-copy-id
│   ├── sshd
│   ├── ssh-keygen
│   └── ssh-keyscan
├── etc
│   └── ssh
│       ├── moduli
│       ├── ssh_config
│       └── sshd_config
├── libexec
│   ├── sftp-server
│   ├── ssh-keysign
│   └── ssh-pkcs11-helper
└── share
    └── man
        ├── man1
        │   ├── scp.1.gz
        │   ├── sftp.1.gz
        │   ├── ssh.1.gz
        │   ├── ssh-add.1.gz
        │   ├── ssh-agent.1.gz
        │   ├── ssh-copy-id.1.gz
        │   ├── ssh-keygen.1.gz
        │   └── ssh-keyscan.1.gz
        ├── man5
        │   ├── moduli.5.gz
        │   ├── ssh_config.5.gz
        │   └── sshd_config.5.gz
        └── man8
            ├── sftp-server.8.gz
            ├── sshd.8.gz
            ├── ssh-keysign.8.gz
            └── ssh-pkcs11-helper.8.gz

9 directories, 30 files

Dependencies are hardcoded

$ ldd `which ssh`
linux-vdso.so.1 (0x00007ffccb981000)
libcrypto.so.38 => /nix/store/381mr3hs51sbw4r53hc7adixlmard2cv-libressl-2.4.2/lib/libcrypto.so.38
libdl.so.2 => /nix/store/10zyyd2m90vqzj8gyqqdikzmg6a4dzqp-glibc-2.23/lib/libdl.so.2
libutil.so.1 => /nix/store/10zyyd2m90vqzj8gyqqdikzmg6a4dzqp-glibc-2.23/lib/libutil.so.1
libz.so.1 => /nix/store/lcazrvc9390cm4bi95n458jabrs6y8z4-zlib-1.2.8/lib/libz.so.1
libcrypt.so.1 => /nix/store/10zyyd2m90vqzj8gyqqdikzmg6a4dzqp-glibc-2.23/lib/libcrypt.so.1
libc.so.6 => /nix/store/10zyyd2m90vqzj8gyqqdikzmg6a4dzqp-glibc-2.23/lib/libc.so.6
libresolv.so.2 => /nix/store/10zyyd2m90vqzj8gyqqdikzmg6a4dzqp-glibc-2.23/lib/libresolv.so.2
/nix/store/10zyyd2m90vqzj8gyqqdikzmg6a4dzqp-glibc-2.23/lib/ld-linux-x86-64.so.2
$ cat `which iotop`
#! /nix/store/1zcm7nlw89y9hqq5ihhbkkmncw6s0vm8-bash-4.3-p42/bin/bash -e

export PYTHONPATH=/nix/store/hb9pj22cm3xjxjkblqlay3i5f3fdz48r-iotop-0.6/lib/python2.7/site-packages:/nix/store/pfv9m665mg2368hdkldwscxy927851n8-python-2.7.12/lib/python2.7/site-packages:/nix/store/wkhan6yfzljykzlwyd3wli4y990lf2gr-python2.7-setuptools-19.4/lib/python2.7/site-packages:/nix/store/c291g9drj1a6nvs0ils0jfx6rqppc8x9-python-curses-2.7.12/lib/python2.7/site-packages${PYTHONPATH:+:}$PYTHONPATH

export PATH=/nix/store/hb9pj22cm3xjxjkblqlay3i5f3fdz48r-iotop-0.6/bin:/nix/store/pfv9m665mg2368hdkldwscxy927851n8-python-2.7.12/bin:/nix/store/ya9d15pgg63bx6m7m72czvh9fcj5vfr7-less-483/bin:/nix/store/wkhan6yfzljykzlwyd3wli4y990lf2gr-python2.7-setuptools-19.4/bin:/nix/store/hb9pj22cm3xjxjkblqlay3i5f3fdz48r-iotop-0.6/bin${PATH:+:}$PATH

exec -a "$0" /nix/store/hb9pj22cm3xjxjkblqlay3i5f3fdz48r-iotop-0.6/bin/.iotop-wrapped "${extraFlagsArray[@]}" "$@"

Real World Example

openssh

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

Dependencies of this "derivation"

Parameters of a new function

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

Create a derivation using a wrapper

from the "standard environment"

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

Define package name with version

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

Fetch the source code from an

OpenBSD mirror & check hash

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

List of packages available

in the build environment

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

List of flags to pass to the

configure script (GNU autotools)

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

Custom make target to install

openssh (don't generate host keys)

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

 

More meta information

Independent of the package build

 

{ stdenv, fetchurl, fetchpatch, zlib, openssl, libedit, pkgconfig, pam }:

stdenv.mkDerivation rec {
  name = "openssh-${version}";
  version = "7.3p1";

  src = fetchurl {
    url = "mirror://openbsd/OpenSSH/portable/${name}.tar.gz";
    sha256 = "1k5y1wi29d47cgizbryxrhc1fbjsba2x8l5mqfa9b9nadnd9iyrz";
  };
  
  buildInputs = [ zlib openssl libedit pkgconfig pam ];
  
  configureFlags = [
    "--with-mantype=man"
    "--with-libedit=yes"
    (if pam != null then "--with-pam" else "--without-pam")
  ];
  
  enableParallelBuilding = true;

  installTargets = [ "install-nokeys" ];

  meta = with stdenv.lib; {
    homepage = "http://www.openssh.com/";
    description = "An implementation of the SSH protocol";
    license = licenses.bsd2;
    platforms = platforms.unix;
    maintainers = with maintainers; [ eelco ];
  };
}

Profiles & User Environments

nix-shell

Just like 

virtualenv, bundler, rvm

but for all packages!

And for all programming language specific package managers like

npm, cabal, pip, gem, go, rust, nuget, bower

nix-shell

[fpletz@yolovo:~]$ python
python: Command not found

[fpletz@yolovo:~]$ nix-shell -p python

[nix-shell:~/src/nixpkgs]$ python
Python 2.7.12 (default, Jun 25 2016, 21:49:32) 
[GCC 5.4.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 

Imperative Environments

nix-shell

[nix-shell:~]$ python -c 'import requests'
ImportError: No module named requests

[nix-shell:~]$ exit

[fpletz@yolovo:~]$ nix-shell -p python pythonPackages.requests

[nix-shell:~]$ python -c 'import requests'

Imperative Environments

$ python -c 'import requests'
ImportError: No module named requests

$ nix-shell --pure

$ python -c 'import requests'

$ 

Declarative environments for your project

with import <nixpkgs> {}; {
  env = stdenv.mkDerivation {
    name = "pyrequests-env";
    buildInputs = [
      python
      pythonPackages.requests
    ];
  };
}
default.nix

nix-shell

Configuration Management

Let's talk about…

Traditional CfgMgmt

Can be declarative…

like for instance Puppet

package { "openssh":
  ensure => installed,
}

user { "eris":
  ensure => present,
  uid    => 1023,
  home   => "/home/eris",
}

Traditional CfgMgmt

…but only mutates state!

  • apt-get install openssh
  • useradd -u 1023 eris

Undeclared aspects of the target system are unknown and could have unexpected values!

Traditional CfgMgmt

converges to target state

Purely Functional CfgMgmt

rebuilds complete target state

eventual consistency

defined consistency

Remember this?

All over again!

Eliminate the State

Everything is a freaking state problem

So why not take Nix and build…

  • Linux kernel
  • initrd
  • bootloader
  • init system
  • configuration files
  • lots of packages

…to produce a complete operating system?

NixOS

The declarative

Linux Distribution

Declarative Configuration

{
  boot.loader.grub.device = "/dev/sda";
  fileSystems."/".device = "/dev/sda1";

  networking.hostname = "webserver";
  networking.firewall.allowedTCPPorts = [ 80 443 ];

  environment.systemPackages = with pkgs; [ htop vim mtr ];
  
  services =
    { openssh.enable = true;
      nginx =
        { enable = true;
          virtualHosts."service.example.com" =
            { forceSSL = true;
              enableACME = true;
              location."/".proxyPass = "https://backend:3000/";
            };
        };
    };
}

Live Demo

Installing NixOS

Unique Features of NixOS

  • reliable upgrades & consistency
    • immutable OS pinned to a git commit
  • atomic upgrades (just one symlink)
  • rollback
  • multi-user package management
  • same configuration, different targets
    • qemu, Virtualbox, AMIs, ISOs, netboot
    • container tarballs, Docker images
  • ~500 service modules
  • testing framework

NixOS Organisation

  • one stable release every half a year
    • next: 16.09
  • code in the nixpkgs git repository
  • stable branch vs. git master
  • small but growing community
    • ~900 contributors with ~90.000 commits
    • ~13.000 pull requests, ~4.000 issues

Deployment with NixOps

$ cat deploy.nix
{
  webserver = { pkgs, ... }:
    { services.openssh.enable = true;
      services.nginx.enable = true;
      users.extraUsers.root.openssh.authorizedKeys.keys = 
        [ "ssh-ed25519 AAAA…BUV fpletz@yolovo" ];
    };
}

$ cat deploy-vbox.nix
{
  webserver = { ... }:
    { deployment.targetEnv = "virtualbox";
      deployment.virtualbox.memorySize = 1024;
    };
}
$ nixops create ./deploy.nix ./deploy-vbox.nix -d my-web-deployment

$ nixops deploy -d my-web-deployment
creating VirtualBox VM ‘webserver’...

Hydra Build Farm 

Seriously, why Nix?

Quotes by Rob Vermas - http://nixer.ghost.io/why/

Nix protects me against me.

Nix exposes the things I forget.

Nix let's me do things multiple times consistently, even on different machines.

Nix, the one language to rule them all.

Caveats of Nix & NixOS

  • steep learning curve
  • quick "hacks" are hard/impossible
  • documentation is not beginner-friendly
  • flexibility costs disk space
  • no management of application state

Future Improvements

  • private files in nix store
  • service hardening
  • statically typed Nix
  • speed & memory improvements
  • binary determinism
  • P2P distribution of binary packages

Useful Resources

Can I use Nix in production?

We do. Others too.

Even customers.

Thanks!

Any questions?

You can also ask me on Twitter after the talk!

@fpletz

Bonus Slides

The simplest Nix derivation

$ cat default.nix
derivation {
  name = "my-package";
  builder = ./builder.sh;
  system = "x86-64-linux";
  src = /home/user/source.tar.gz;
}
$ cat builder.sh
tar xvfz $src
cd program-1.0/
mkdir -p $out/bin
cp program.sh $out/bin/program
$ nix-build
/nix/store/cpcx3df4s2d51ddm01qm0w9x1nv8mpfq-my-package
$ ./result/bin/program

Sources of Impurity

  • wetware bugs
    • "hotfixes" in production
    • "debugging" in production
  • package upgrades
  • operating system upgrades
  • broken software modifying its own configuration

Sources

NixOS FrOSCon 2016

By fpletz

NixOS FrOSCon 2016

  • 2,885