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
- Functions always evaluate the same result value given the same argument value
- 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
- Nix manual
- Interactive tour of Nix
- NixOS manual
- Nix Pills
Can I use Nix in production?
We do. Others too.
Even customers.
Thanks!
Find these slides online:
Any questions?
You can also ask me on Twitter after the talk!
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