URL:     https://linuxfr.org/news/gestion-de-paquets-et-devops-avec-nix-tour-d-horizon-sur-un-cas-concret
Title:   Gestion de paquets et DevOps avec Nix, tour d’horizon sur un cas concret
Authors: nokomprendo
        ZeroHeure, Davy Defaud, Ysabeau et palm123
Date:    2020-01-23T11:38:47+01:00
License: CC by-sa
Tags:
Score:   4


[Nix](https://nixos.org/nix/) et [GNU Guix](http://guix.gnu.org/) sont des gestionnaires de paquets « fonctionnels », au sens de la programmation fonctionnelle. Cette approche de la gestion de paquets est très différente de l’approche habituellement utilisée par les systèmes GNU/Linux ou BSD, à base de collections de _ports_ ou de dépôts de paquets.


Cette approche fonctionnelle apporte de nombreux avantages. Non seulement, elle permet de fournir une gestion de paquets fiable, reproductible, multi‑version et multi‑utilisateur, mais elle apporte également de nombreuses fonctionnalités supplémentaires : gestion d’un environnement de développement, empaquetage décentralisé, construction d’images Docker ou de machines virtuelles, personnalisation de tout l’environnement logiciel, etc.

Cet article part d’un projet de code (un serveur Web) et illustre progressivement différentes fonctionnalités de Nix intéressantes pour le développeur, l’empaqueteur et l’administrateur système.  Les exemples sont présentés ici sous [NixOS](https://nixos.org/) (la distribution GNU/Linux basée sur Nix), mais devraient être utilisables également avec l’outil Nix installé sur une distribution GNU/Linux classique, ou avec GNU Guix.

----

[Journal à l’origine de la dépêche](https://linuxfr.org/users/nokomprendo-3/journaux/gestion-de-paquets-et-devops-avec-nix-tour-d-horizon-sur-un-cas-concret)
[Le projet servant d’exemple, sur GitLab](https://gitlab.com/nokomprendo/mymathserver)
[Vidéo sur YouTube](https://youtu.be/Z4PhRtA-9gU)
[Vidéo sur PeerTube](https://peertube.fr/videos/watch/28955633-7053-456b-a955-38ff98e9e98e)

----

Voir aussi : le [projet d’exemple](https://gitlab.com/nokomprendo/mymathserver), la [vidéo YouTube](https://youtu.be/Z4PhRtA-9gU) et la [vidéo PeerTube](https://peertube.fr/videos/watch/28955633-7053-456b-a955-38ff98e9e98e)


# Projet d’exemple : mymathserver
Le projet [mymathserver](https://gitlab.com/nokomprendo/mymathserver) est un serveur Web. La route `/` retourne un texte d’accueil et la route `/mul2` permet de multiplier un entier par deux.

![URL de mymathserver](https://nokomprendo.gitlab.io/posts/tuto_fonctionnel_44/images/mymathserver.gif)


Le projet est codé en C++. Il contient une bibliothèque de base `src/mymath.hpp` :

```cpp
#pragma once

int mul2(int x) {
   return 2*x;
}
```



Un exécutable de tests unitaires `src/mymathtest.cpp` (on ne sait jamais) :

```cpp
#include <gtest/gtest.h>

#include "mymath.hpp"

TEST(Mymath, mul2_1) {
   ASSERT_EQ(0, mul2(0));
}

// ...

int main(int argc, char **argv) {
   ::testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
}
```



Et enfin, le serveur Web `src/mymathserver.cpp` :

```cpp
#include "mymath.hpp"
#include <cpprest/http_listener.h>
// ...

class App : public web::http::experimental::listener::http_listener {
   private:
       void handleGet(web::http::http_request req) {
           // ...
           if (path == "/") {
               // ...
           }
           else if (splitPath.size() == 2 and splitPath[0] == "mul2") {
               // ...
           }
           else {
               req.reply(web::http::status_codes::NotFound);
           }
       }

   public:
       App(std::string url) : web::http::experimental::listener::http_listener(url) {
           support(web::http::methods::GET,
                   bind(&App::handleGet, this, std::placeholders::_1));
       }
};

int main() {
   const char * portEnv = std::getenv("PORT");
   const std::string port = portEnv == nullptr ? "3000" : portEnv;
   const std::string address = "http://0.0.0.0:" + port;
   App app(address);
   // ...
   return 0;
}
```



Le tout est configuré de façon très classique avec un `CMakeLists.txt` :

```cmake
cmake_minimum_required( VERSION 3.0 )
project( mymathserver )

find_package( GTest REQUIRED )
add_executable( mymathtest src/mymathtest.cpp )
target_include_directories( mymathtest PRIVATE ${GTEST_INCLUDE_DIRS} )
target_link_libraries( mymathtest
   ${GTEST_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} )

find_package( Boost REQUIRED system )
find_package ( Threads REQUIRED )
find_package ( OpenSSL REQUIRED )
add_executable( mymathserver src/mymathserver.cpp )
target_link_libraries( mymathserver PRIVATE
  cpprest ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} )

install( TARGETS mymathserver mymathtest DESTINATION bin )
```



# Empaqueter une dépendance manquante
On a donc le programme de tests unitaires `mymathtest`, qui utilise la bibliothèque `gtest`, et le programme serveur `mymathserver` qui utilise `boost`, `openssl` et [`cpprestsdk`](https://github.com/microsoft/cpprestsdk). Toutes ces dépendances sont classiques et donc présentes dans la plupart des logithèques GNU/Linux. Cependant, `cpprestsdk` est beaucoup moins connue que les autres dépendances et risque de manquer lors de la compilation de notre projet. Pour éviter ce problème, on peut laisser l’utilisateur installer ou compiler `cpprestsdk` lui‑même (ce qui peut être compliqué ou fastidieux), ou intégrer `cpprestsdk` directement à notre projet via les sous‑modules `git` (ce qui est peu efficace et rend le projet dépendant de `git`).

Avec Nix, on peut créer nous‑mêmes un paquet pour `cpprestsdk`. Certaines bibliothèques peuvent être complexes à empaqueter mais, généralement, cela se résume à indiquer l’adresse du code source et la liste des dépendances (`nix/cpprestsdk.nix`) :

```nix
{ stdenv, fetchFromGitHub, cmake, boost, openssl, websocketpp, zlib }:

stdenv.mkDerivation {

 name = "cpprestsdk";

 src = fetchFromGitHub {
   owner = "Microsoft";
   repo = "cpprestsdk";
   rev = "v2.10.14";
   sha256 = "0z1yblqszs7ig79l6lky02jmrs8zmpi7pnzns237p0w59pipzrvs";
 };

 buildInputs = [ boost cmake openssl websocketpp zlib ];
}
```


On remarquera qu’on n’a écrit aucune directive de compilation. En effet, comme on a indiqué `cmake` dans les dépendances de `cpprestsdk`, Nix sait qu’il faut compiler avec les commandes `cmake` classiques.


# Empaqueter le projet
De la même façon que l’on a empaqueté `cpprestsdk`, on peut créer un fichier `nix/mymathserver.nix` pour empaqueter notre projet :

```nix
{ stdenv, cpprestsdk, boost, cmake, gtest, openssl }:

stdenv.mkDerivation {
 name = "mymathserver";
 version = "0.1";
 src = ../.;
 buildInputs = [ boost cmake cpprestsdk gtest openssl ];
}
```



De même que pour `cpprestsdk`, comme on a mis `cmake` dans les dépendances, Nix sait qu’il faut utiliser `cmake` et notre fichier `CMakeLists.txt` pour compiler le projet.

Pour l’instant, Nix ne connaît pas le code exact des dépendances. Il connaît juste les noms, et ce sont des paramètres du paquet (la première ligne du fichier). D’où la fameuse « approche fonctionnelle » de Nix.


Pour terminer, on écrit un fichier `default.nix`, qui est le point d’entrée de notre configuration de projet et fait le lien avec les fichiers précédents :

```nix
{ pkgs ? import <nixpkgs> {} }:

let
 cpprestsdk = pkgs.callPackage ./nix/cpprestsdk.nix {};
 mymathserver = pkgs.callPackage ./nix/mymathserver.nix { inherit cpprestsdk; };

in mymathserver
```



Ce fichier a également un paramètre (`pkgs`) mais avec une valeur par défaut (la logithèque système `nixpkgs`). Il suffit alors d’appeler nos paquets `cpprestsdk` et `mymathserver` sur la « logithèque » `pkgs`. Les paramètres des paquets sont fixés aux valeurs présentes dans `pkgs`, sauf pour le paramètre `cpprestsdk` dans `mymathserver` où c’est le paquet créé juste avant qui est utilisé (grâce au `inherit`). Ainsi, le `default.nix` permet de fournir concrètement le code à utiliser à travers les paramètres des descriptions de paquets précédentes, et donc de construire réellement les paquets correspondants.


On notera qu’on aurait pu se passer du fichier `nix/mymathserver.nix` et tout mettre dans le fichier `default.nix`. L’avantage d’utiliser deux fichiers est de rendre notre empaquetage plus modulaire. Par exemple, on pourrait quasiment intégrer directement `nix/mymathserver.nix` dans le [dépôt nixpkgs](https://github.com/NixOS/nixpkgs) et notre projet serait disponible dans la logithèque Nix officielle !


# Lancer un environnement virtuel
À partir de notre fichier `default.nix`, on peut lancer un environnement virtuel :

```sh
nix-shell
```



Ceci installe les dépendances, compile `cpprestsdk` et initialise l’environnement. On peut alors travailler sur le code du projet et le compiler avec les commandes `cmake` classiques :

```sh
mkdir mybuild
cd mybuild
cmake ..
make
/mymathserver
```



![lancement du projet](https://nokomprendo.gitlab.io/posts/tuto_fonctionnel_44/images/mymathserver-shell.png)


# Construire et installer le paquet du projet
Le fichier `default.nix` permet également de construire automatiquement le paquet du projet :

```sh
nix-build
/result/bin/mymathserver
```



On peut également installer notre projet comme un logiciel classique de la logithèque système :

```sh
nix-env -i f .
mymathserver
```



Et on peut même l’installer en récupérant directement une archive du dépôt Git distant :

```sh
nix-env -if "https://gitlab.com/nokomprendo/mymathserver/-/archive/master/mymathserver-master.tar.gz"
```



# Fixer une version
Le fichier `default.nix` construit le projet à partir de la logithèque système, qui peut varier dans le temps ou selon les utilisateurs. Nix permet de fixer une logithèque précise, et donc d’avoir un paquet reproductible. Par exemple, avec le fichier `nix/release.nix` :

```nix
let
 rev = "1c92cdaf7414261b4a0e0753ca9383137e6fef06";

 pkgs-src = fetchTarball {
   url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
   sha256 = "0d3fqa1aqralj290n007j477av54knc48y1bf1wrzzjic99cykwh";
 };

 pkgs = import pkgs-src {};

in pkgs.callPackage ../default.nix {}
```



Encore une fois, c’est l’approche fonctionnelle de Nix qui permet de composer les fichiers de configuration entre eux. Ici, `nix/release.nix` se contente de récupérer un `pkgs` particulier et s’en sert comme paramètre de `default.nix` pour construire tout le projet et ses dépendances à partir de ce `pkgs` particulier.

On peut alors construire ce paquet reproductible avec la commande :

```sh
nix-build nix/release.nix
```



# Intégration continue
Comme notre configuration Nix décrit complètement l’environnement et la construction de notre projet, il est très pratique de l’utiliser dans un processus d’intégration continue. Par exemple, avec `gitlab-ci`, on peut ajouter le fichier `.gitlab-ci.yml` suivant pour lancer la compilation du projet et les tests unitaires :

```yaml
build:
   image: nixos/nix
   script:
       - nix-build nix/release.nix
       - ./result/bin/mymathtest
```



![Compilation depuis GitLab](https://nokomprendo.gitlab.io/posts/tuto_fonctionnel_44/images/mymathserver-gitlab0.png)


# Cache binaire
Nix permet d’utiliser des paquets de binaires pour éviter d’avoir à tout compiler soi‑même. Ainsi, la logithèque `nixpkgs` fournit un cache binaire. Cependant, `cpprestsdk` n’est pas disponible dans ce cache car on l’a empaqueté nous‑mêmes. Nix propose des outils pour installer des serveurs d’intégration continue et de cache mais ceci est assez lourd à mettre en place.

Une autre possibilité est d’utiliser le service de cache [cachix](https://cachix.org), qui permet de téléverser des paquets Nix compilés puis de les télécharger sur un autre système. L’utilisation de cachix est très simple. Il suffit de créer un compte et un dépôt de cache. On peut ensuite envoyer des paquets binaires avec la commande `cachix push` et activer le téléchargement depuis un dépôt de cache avec la commande `cachix use`.


Par exemple, pour accélérer l’intégration continue précédente, on peut construire `cpprestsdk` avec notre configuration `release`, téléverser les paquets binaires correspondants dans le dépôt `nokomprendo`, puis utiliser ce cache dans le processus d’intégration continue (`.gitlab-ci.yml`) :

```yaml
build:
   image: nixos/nix
   script:
       - nix-env -iA nixpkgs.cachix
       - cachix use nokomprendo
       - nix-build nix/release.nix
       - ./result/bin/mymathtest
```



Ceci fait passer l’exécution d’un processus d’intégration continue de vingt minutes à moins de deux minutes.

![Comparaison d’intégration continue avec et sans cachix](https://nokomprendo.gitlab.io/posts/tuto_fonctionnel_44/images/mymathserver-gitlab1.png)


# Construire et déployer une image Docker
Nix permet de construire des images Docker. Par exemple, le fichier `nix/docker.nix` suivant définit une image Docker à partir de notre configuration `release` :

```nix
{ pkgs ? import <nixpkgs> {} }:

let
 mymathserver = import ./release.nix;

 entrypoint = pkgs.writeScript "entrypoint.sh" ''
   #!${pkgs.stdenv.shell}
   $@
 '';

in pkgs.dockerTools.buildImage {
 name = "mymathserver";
 tag = "latest";
 config = {
   WorkingDir = "${mymathserver}";
   Entrypoint = [ entrypoint ];
   Cmd = [ "${mymathserver}/bin/mymathserver" ];
 };
}
```



On peut alors construire l’image, la charger dans Docker et la tester :

```sh
nix-build nix/docker.nix
docker load < result
docker run --rm -it -p 3000:3000 mymathserver:latest
```



![Compilation Nix et lancement avec Docker](https://nokomprendo.gitlab.io/posts/tuto_fonctionnel_44/images/mymathserver-docker.png)


Cette image Docker peut être déployée, par exemple sur [Heroku](https://heroku.com/) :

```sh
heroku login
heroku container:login
heroku create nokomprendo-mymathserver
docker tag mymathserver:latest registry.heroku.com/nokomprendo-mymathserver/web
docker push registry.heroku.com/nokomprendo-mymathserver/web
heroku container:release web --app nokomprendo-mymathserver
heroku logs --tail
```



L’application est alors accessible à l’adresse <http://nokomprendo-mymathserver.herokuapp.com>.

![Le projet Nixi en Docker dans Heroku](https://nokomprendo.gitlab.io/posts/tuto_fonctionnel_44/images/mymathserver-heroku.png)


# Construire et déployer une machine virtuelle
Nix permet de définir et de déployer des machines virtuelles, avec `nixops`. Par exemple, le fichier `nix/virtualbox.nix` suivant reprend notre programme `mymathserver`, le lance dans un service `systemd` et déploie le tout dans une `virtualbox` :

```nix
{
 network.description = "mynetwork";

 myserver = { config, pkgs, ... }:

 let
   myapp = import ./release.nix;

 in {
   networking.firewall.allowedTCPPorts = [ 3000 ];

   systemd.services.myservice = {
     wantedBy = [ "multi-user.target" ];
     after = [ "network.target" ];
     script = "${myapp}/bin/mymathserver";
   };

   deployment = {
     targetEnv = "virtualbox";
     virtualbox = {
       memorySize = 512;
       vcpu = 1;
       headless = true;
     };
   };
 };
}
```



Les commandes suivantes créent et déploient la machine virtuelle sous le nom `myvm` :

```sh
nixops create -d myvm nix/virtualbox.nix
nixops deploy -d myvm --force-reboot
```



À l’issue du déploiement, une adresse IP est fournie et permet d’accèder à notre serveur sur la machine virtuelle.

![Machine virtuelle avec Nix](https://nokomprendo.gitlab.io/posts/tuto_fonctionnel_44/images/mymathserver-virtualbox.png)


# Personnaliser des paquets existants
Enfin, une fonctionnalité originale de Nix, liée à son « approche fonctionnelle », est la possibilité de modifier les paramètres des paquets et de répercuter automatiquement et efficacement ces modifications dans l’ensemble de l’environnement logiciel.

Les paquets peuvent être surchargés, c’est‑à‑dire remplacés par des versions modifiées. Par exemple, le paquet de la bibliothèque [zlib](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/libraries/zlib/default.nix) contient un attribut `configureFlags` qui contient les options de compilation à utiliser. Si l’on veut utiliser une version de `zlib` compilée avec l’option `--zprefix`, on peut utiliser le fichier `nix/custom0.nix` suivant :

```nix
let
 pkgs = import <nixpkgs> {};

 zlib = pkgs.zlib.overrideDerivation (attrs: {
    configureFlags = [ attrs.configureFlags "--zprefix" ];
 });

 cpprestsdk = pkgs.callPackage ./cpprestsdk.nix { inherit zlib; };

 mymathserver = pkgs.callPackage ./mymathserver.nix { inherit cpprestsdk; };

in pkgs.stdenv.mkDerivation rec {
 name = "mymathserver-custom0";
 src = ./.;
 buildPhase = "";
 installPhase = ''
   mkdir -p $out/bin
   cp ${mymathserver}/bin/mymathserver $out/bin/${name}
 '';
}
```



Les paramètres des paquets Nix peuvent également contenir des options paramétrables.  Par exemple, la bibliothèque [openssl](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/libraries/openssl/default.nix) a une option `enableSSL2`, qu’on peut spécifier comme dans le fichier `nix/custom1.nix` suivant :

```nix
let
 pkgs = import <nixpkgs> {};

 openssl = pkgs.openssl.override { enableSSL2 = true; };

 cpprestsdk = pkgs.callPackage ./cpprestsdk.nix { inherit openssl; };

 mymathserver = pkgs.callPackage ./mymathserver.nix {
   inherit cpprestsdk;
   inherit openssl;
 };

in pkgs.stdenv.mkDerivation rec {
 name = "mymathserver-custom1";
 src = ./.;
 buildPhase = "";
 installPhase = ''
   mkdir -p $out/bin
   cp ${mymathserver}/bin/mymathserver $out/bin/${name}
 '';
}
```



Enfin, pour éviter tout risque d’incompatibilité d’ABI, on peut surcharger la configuration lors du chargement de la logithèque. Ainsi, tous les paquets dépendant des paquets surchargés seront recompilés en prenant en compte ces modifications. Par exemple, le fichier `nix/custom2.nix` suivant surcharge `openssl` via la configuration de la logithèque (il existe aussi un système d’_overlays_ pour cela) :

```nix
let

 config = {
   packageOverrides = pkgs: {
     openssl = pkgs.openssl.override {
       enableSSL2 = true;
     };
   };
 };

 pkgs = import <nixpkgs> { inherit config; };

 mymathserver = pkgs.callPackage ../default.nix {};

in pkgs.stdenv.mkDerivation rec {
 name = "mymathserver-custom2";
 src = ./.;
 buildPhase = "";
 installPhase = ''
   mkdir -p $out/bin
   cp ${mymathserver}/bin/mymathserver $out/bin/${name}
 '';
}
```



Nix installe chaque paquet dans un dossier spécifique, ce qui signifie qu’on peut utiliser en même temps plusieurs versions ou plusieurs configurations d’un même logiciel. Par exemple, dans les trois fichiers de personnalisation précédents, on a renommé l’exécutable `mymathserver` produit. On peut donc les installer et les exécuter en même temps dans l’environnement utilisateur courant :

```sh
nix-env -i -f nix/custom0.nix
nix-env -i -f nix/custom1.nix
nix-env -i -f nix/custom2.nix
mymathserver-custom0
..
```



# Conclusion
L’approche fonctionnelle de Nix constitue une base solide pour la gestion de paquets. Elle permet de construire des paquets de façon fiable, reproductible et personnalisable. Sur cette base, Nix propose également des fonctionnalités intéressantes pour le développeur, l’empaqueteur et l’administrateur système.

Ainsi, en cent vingt lignes de code Nix, le projet d’exemple `mymathserver` :

- empaquette une dépendance manquante (`nix/cpprestsdk.nix`) ;
- empaquette le projet de base (`nix/mymathserver.nix`) ;
- définit un point d’entrée pour créer des paquets et des environnements
 virtuels (`default.nix`) ;
- définit une publication (`nix/release.nix`) ;
- définit une image Docker (`nix/docker.nix`) ;
- définit une machine virtuelle (`nix/virtualbox.nix`) ;
- définit trois personnalisations différentes de l’environnement logiciel
 (`nix/custom0.nix`, etc.) ;
- permet de réaliser de l’intégration continue, d’utiliser un cache binaire,
 d’installer le projet depuis une archive GitLab, etc.

```sh
mymathserver/
├── CMakeLists.txt
├── default.nix
├── nix
│   ├── cpprestsdk.nix
│   ├── custom0.nix
│   ├── custom1.nix
│   ├── custom2.nix
│   ├── docker.nix
│   ├── mymathserver.nix
│   ├── release.nix
│   └── virtualbox.nix
└── src
   ├── mymath.hpp
   ├── mymathserver.cpp
   └── mymathtest.cpp
```