NixOS 20.09 - Présentation, installation, configuration, utilisation

Voir aussi : dépôt de code - video youtube - video peertube

NixOS est une distribution linux, plutôt atypique, basée sur le gestionnaire de paquets Nix. Elle utilise une approche déclarative pour définir des environnements logiciels, de façon reproductible et composable.

À l’occasion de la sortie récente de la version 20.09, cet article propose un aperçu de NixOS, de son installation jusqu’à son utilisation, d’un point de vue de développeur.

Présentation

Nix, Nixpkgs, NixOS…

Au commencement était Nix. Il s’agit à la fois d’un langage de programmation, permettant de décrire un environnement logiciel, et à la fois d’un programme, permettant de gérer de tels environnements logiciels.

Puis vint Nixpkgs. Il s’agit d’un ensemble de fichiers Nix formant une logithèque complète, comprenant des programmes, bibliothèques, modules pour de nombreux langages, etc.

Et enfin NixOS fût. Il s’agit d’une distribution linux basée sur Nix et Nixpkgs. Ainsi le système peut être entièrement décrit et géré de manière déclarative, via des fichiers Nix : noyau et modules, système de boot, services, utilisateurs, logiciels, etc.

En fait, l’écosystème Nix est encore plus riche. Il comporte également le système de construction-continue Hydra, l’outil de déploiement de machines Nixops, l’outil de déploiement de services distribués Disnix, le service de cache Cachix, le service d’intégration continue Hercules CI, etc. À noter également que Nix a inspiré le projet GNU Guix, qui suit une approche assez similaire.

Nouveautés de NixOS 20.09

Heu… non. Cette nouvelle version n’apporte pas de révolution particulière mais plutôt des classiques mises à jour (Gcc 9.3.0, Glibc 2.31, Mesa 20.1.7, Plasma 5.18.5, Gnome 3.36…), un nouvel environnement graphique (Cinnamon), des nouveaux modules (Jitsi Meet, Podman…), des nouveaux services (Steam, Openvpn…), etc. Tout cela est détaillé dans l’annonce de sortie et dans les notes de version.

En fait, la plus grosse nouveauté du projet est peut-être une refonte de son site web, avec de nouvelles ressources, des vidéos thématiques, etc.

Pourquoi utiliser NixOS ?

Avant de répondre à la question “Pourquoi utiliser NixOS ?”, il faut se poser la question “Quand utiliser NixOS ?”. Et la réponse est : jamais.

Oui bon à la limite, si vous êtes développeur ou administrateur système, que vous jonglez entre les projets et que vous avez une bonne centaine de Go à consacrer à votre partition système, alors là oui, peut-être que NixOS peut changer votre vie (notez que je n’ai pas précisé si c’était en bien ou en mal…). Pour une utilisation “bureautique classique”, NixOS n’a pas vraiment d’intérêt et sera certainement inutilement compliqué.

Et donc “Pourquoi utiliser NixOS dans les cas qui peuvent s’y prêter ?” : parce que Nix permet de définir et reproduire des configurations de machines et de projets. Typiquement, il suffit d’ajouter à un projet un fichier Nix, décrivant ses dépendances et sa procédure de construction, pour que ça-juste-marche™.

Il est également possible d’installer Nix + Nixpkgs sur une distribution Linux classique (Debian, Fedora, etc). Cette méthode permet de profiter de l’écosystème Nix sans avoir à installer NixOS. Cependant, elle ne permet pas d’utiliser tout l’écosystème et peut nécessiter un peu de configuration supplémentaire, notamment pour lancer des applications graphiques.

Installation de NixOS

NixOS s’installe via quelques lignes de commande, un peu comme ArchLinux mais en beaucoup plus simple. Différents supports sont proposés pour installer, tester ou déployer un système : ISO graphique ou minimale, image VirtualBox, image Amazon EC2.

Ici, je considère qu’on a téléchargé l’image ISO minimale et qu’on fait une installation complète sur une machine virtuelle. La procédure pour installer sur une machine réelle est quasiment la même.

Installer le système de base

sudo su
loadkeys fr
cfdisk /dev/sda
mkswap -L swap /dev/sda2
swapon /dev/sda2
mkfs.ext4 -L nixos /dev/sda3
mount /dev/sda3 /mnt
nixos-generate-config --root /mnt
nano /mnt/etc/nixos/configuration.nix
{ config, pkgs, ... }: {

  imports = [ ./hardware-configuration.nix ];

  boot.loader.grub = {
    enable = true;
    device = "/dev/sda";
    version = 2;
  };  

  i18n.defaultLocale = "fr_FR.UTF-8";
  system.stateVersion = "20.09";
  time.timeZone = "Europe/Paris";
  virtualisation.docker.enable = true;

  console = {
    font = "Lat2-Terminus16";
    keyMap = "fr";
  };

  environment.systemPackages = with pkgs; [
    firefox
    git
    vim
  ];

  networking = {
    hostName = "nixos";
    useDHCP = false;
    interfaces.enp0s3.useDHCP = true;
  };

  services = {
    xserver = {
      enable = true;
      layout = "fr";
      displayManager.lightdm.enable = true;
      desktopManager.xfce.enable = true;
    };
  };

  users.users.toto = {
    isNormalUser = true;
    extraGroups = [
      "docker"
      "wheel"
    ];
  };
}
nixos-install

Modifier la configuration système

Attention, avec NixOS, on a rarement besoin de modifier la configuration système. Par exemple, il vaut mieux installer les logiciels via la configuration utilisateur. Modifier la configuration système n’est généralement nécessaire que pour configurer des services systèmes, configurer le matériel (drivers vidéo, réseau, etc), changer de version de NixOS… La procédure est la suivante.

$ sudo nano /etc/nixos/configuration.nix
$ sudo nixos-rebuild switch

À noter que les configurations sont reproductibles, ce qui signifie qu’on peut très facilement annuler une mise à jour, revenir ponctuellement sur une configuration précise ou même reprendre une configuration pour une autre machine.

Configuration utilisateur avec home-manager

Une des grosses différences avec les distributions Linux classiques est que NixOS permet aux utilisateurs d’installer eux-mêmes des logiciels de la logithèque, sans passer par root ni par sudo.

Il y a plusieurs méthodes possibles pour configurer son environnement utilisateur. Généralement, on utilise une approche déclarative, qui se résume à mettre un ou plusieurs fichiers Nix dans le dossier ~/.config/nixpkgs/ et à lancer une commande de mise à jour. Depuis quelques temps, l’outil home-manager est assez populaire pour gérer sa configuration utilisateur avec Nix.

Installer home-manager

$ nix-channel --add https://github.com/nix-community/home-manager/archive/release-20.09.tar.gz home-manager
$ nix-channel --update
$ nix-shell '<home-manager>' -A install

Modifier sa configuration utilisateur

Avec home-manager, le fichier de configuration principal est ~/.config/nixpkgs/home.nix. Ce fichier permet non seulement de spécifier les logiciels à installer mais également de les configurer.

{ pkgs, ... }: {

  home = {
    username = "toto";
    homeDirectory = "/home/toto";
    stateVersion = "20.09";

    packages = with pkgs; [
      cabal-install
      geany
      (python3.withPackages (ps: with ps; [ numpy ]))
    ];
  };

  programs = {
    home-manager.enable = true;

    git = {
      enable = true;
      userName = "toto";
      userEmail = "toto@example.com";
    };
  };
}
$ home-manager switch

Comme pour les configurations systèmes, les configurations utilisateurs sont reproductibles; on peut donc facilement annuler une configuration ou revenir à une configuration précise.

Personnaliser la logithèque avec les overlays

Les overlays permettent de modifier la logithèque pour, par exemple, ajouter des nouveaux logiciels, recompiler automatiquement des logiciels existants avec d’autres options ou patchs, etc.

Ceci est très facile à faire avec Nix car son approche déclarative permet de composer les environnements logiciels. Plus exactement, un “paquet” est en fait une fonction qui modifie l’environnement courant de façon à ajouter le logiciel. On peut donc écrire un “paquet” qui modifie simplement un logiciel déjà présent. C’est ce que permettent de faire les overlays, pour l’environnement utilisateur.

À titre d’exemple, on va écrire un overlay pour ajouter le programme GNU Hello 2.9 dans la logithèque sous le nom myhello.

self: super: {

  myhello = self.stdenv.mkDerivation rec {
    pname = "hello";
    version = "2.9";
    src = self.fetchurl {
      url = "mirror://gnu/hello/${pname}-${version}.tar.gz";
      sha256 = "19qy37gkasc4csb1d3bdiz9snn8mir2p3aj0jgzmfv0r2hi7mfzc";
    };
  };

}

Autre exemple, pour modifier l’option enableNls du paquet nano, on peut écrire l’overlay suivant (puis ajouter le paquet et mettre à jour) :

self: super: {

  nano = super.nano.override {
    enableNls = false;
  };

}

Environnements de développement

Lorsqu’on développe des logiciels, on a souvent différents projets avec différentes dépendances ou versions de dépendances. Il n’est pas toujours facile d’installer un environnement satisfaisant ces contraintes, et encore moins de reproduire cet environnement pour un nouveau projet ou pour un déploiement. C’est dans ce genre de situation que Nix est particulièrement intéressant.

Environnements temporaires

Nix permet d’exécuter des environnements temporaires. Par exemple, la commande suivante lance un environnement temporaire contenant le paquet vlc et y exécute la commande vlc.

$ nix run nixpkgs.vlc -c vlc

Ainsi le logiciel s’exécute et, une fois quitté, on revient dans l’environnement de base, non modifié.

Les environnements temporaires sont également utiles pour lancer un interpréteur avec des modules particuliers, qui ne sont pas forcément déjà installés dans l’environnnement de base. Par exemple, la commande suivante lance python3 dans un environnement contenant le module flask.

$ nix-shell --run python3 -p "python3.withPackages (ps: with ps; [ flask ])" 

Packager un projet

Les environnements temporaires sont pratiques pour des petits travaux rapides. Mais pour travailler sur un vrai projet, on préfèrera écrire un fichier Nix décrivant le projet.

Par exemple, considérons le code python myserver.py suivant (voir le dépôt fourni) :

#!/usr/bin/env python3
from flask import Flask
import sys

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, Nix!"

if __name__ == "__main__":
    print("using {}".format(sys.version))
    app.run(host="0.0.0.0")

Et soit le fichier de configuration setup.py suivant :

from setuptools import setup
setup(
    name='myserver', 
    version='0.1', 
    scripts = ['myserver.py'])

On peut alors écrire un paquet Nix de notre projet avec le fichier default.nix suivant :

{ pkgs ? import <nixpkgs> {} }:

with pkgs.python3Packages; buildPythonApplication {
  pname = "myserver";
  src = ./.;
  version = "0.1";
  propagatedBuildInputs = [ flask ];
}

Ceci permet de construire notre projet et d’exécuter le programme résultat :

$ nix-build
$ ./result/bin/myserver.py

Et même de l’installer dans l’environnement utilisateur :

$ nix-env -i -f .
$ myserver.py

Ainsi, on a un paquet Nix de notre projet, qui fonctionnera automatiquement sur une machine disposant de Nix. Et avec quelques modifications mineures (url du code source…), on pourrait même intégrer notre paquet à la logithèque Nixpkgs.

Environnement de développement

Le fichier default.nix permet également d’exécuter un environnement de développement adapté au projet, via la commande nix-shell. Cette commande ne construit pas le paquet mais charge un environnement contenant toutes les dépendances nécessaires pour travailler sur le projet.

$ nix-shell
$ python3 myserver.py

Le fichier default.nix et la commande nix-shell sont les outils principaux pour développer un projet avec Nix.

Fixer les versions

Le fichier default.nix précédent utilise la logithèque disponible par défaut. Cependant, lorsqu’on veut déployer un projet, on préfère parfois fixer complètement les versions des dépendances utilisées. Pas de soucis avec Nix, il suffit d’écrire un fichier release.nix qui réutilise la configuration default.nix mais en fixant la version précise de la logithèque à utiliser :

let
  rev = "cd63096";
  url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
  pkgs = import (fetchTarball url) {};
in import ./default.nix { inherit pkgs; }

Pour construire le paquet correspondant, il suffit alors de préciser le fichier à utiliser :

$ nix-build nix/release.nix
$ ./result/bin/myserver

Construire une image Docker

Nix permet également de construire des images Docker à partir de nos fichiers Nix. Par exemple, à partir de notre paquet de release, on peut écrire le fichier docker.nix suivant :

{ pkgs ? import <nixpkgs> {} }:
let
  app = import ./release.nix;
  entrypoint = pkgs.writeScript "entrypoint.sh" ''
    #!${pkgs.stdenv.shell}
    $@
  '';
in
  pkgs.dockerTools.buildLayeredImage {
    name = "myserver-py";
    tag = "latest";
    config = {
      WorkingDir = "${app}";
      Entrypoint = [ entrypoint ];
      Cmd = [ "${app}/bin/myserver.py" ];
    };
  }

On peut alors, construire l’image correspondante, la charger dans le service docker et la lancer comme n’importe quelle image Docker classique :

$ nix-build nix/docker.nix
$ docker load -i result
$ docker run --rm -it -p 5000:5000 myserver-py

Environnements automatiques avec nix-direnv

Comme expliqué précédemment, l’outil nix-shell permet de lancer un environnement de développement adapté au projet. L’outil nix-direnv est complémentaire et permet de charger automatiquement l’environnement, de le garder en cache et de garder les dépendances lors des nettoyages du système.

Installer nix-direnv

  # nix-direnv
  nix.extraOptions = ''
    keep-outputs = true
    keep-derivations = true
  '';
  programs = {

    bash.enable = true;

    direnv = {
      enable = true;
      enableNixDirenvIntegration = true;
    };

    home-manager.enable = true;

  };

Utiliser nix-direnv

Par exemple pour le projet précédent, on avait notre code et ses fichiers de configuration dans un dossier myserver-py. À partir du fichier default.nix, il suffit d’écrire un fichier .envrc et d’activer direnv pour ce dossier :

$ echo "use nix" > .envrc
$ allow direnv

Ainsi, l’environnement est automatiquement chargé lorsqu’on va dans le dossier du projet :

$ cd myserver-py/
direnv: loading ~/gitlab/nixos-20.09_config/myserver-py/.envrc
direnv: using nix
direnv: using cached derivation
...

$ python3 myserver.py 
...

Bien entendu, si on modifie la configuration (default.nix), l’environnement est automatiquement reconstruit et rechargé.

Conclusion

Avec son approche déclarative, Nix permet de gérer des environnements logiciels de façon reproductible et composable. Si l’idée initiale vient de travaux de recherche du début des années 2000, l’écosystème n’a cessé d’évoluer depuis. Aujourd’hui, la distribution NixOS, associée à des outils comme home-manager et nix-direnv, fournit un système très pratique pour un développeur ou pour un administrateur système.

Cependant, cette approche necessité une phase de prise en main non négligeable et n’est pas forcément très intéressante pour une utilisation plus “classique” de l’informatique.