Gestion de paquets et DevOps avec Nix, tour d'horizon sur un cas concret
Voir aussi : projet d’exemple - video youtube - video peertube - article linuxfr
Nix et GNU Guix 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 sytèmes 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 paquet fiable, reproductible, multi-version et multi-utilisateur, mais apporte également de nombreuses fonctionnalités supplémentaires : gestion d’un environnement de développement, packaging 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éressante pour le développeur, l’empaqueteur et l’administrateur système. Les exemples sont présentés ici sous NixOS (la distribution Linux basée sur Nix) mais devraient être utilisables également avec l’outil Nix installé sur une distribution Linux classique, ou avec GNU Guix.
Projet d’exemple : mymathserver
Le projet mymathserver est un serveur web. La route “/” retourne un texte d’accueil et la route “/mul2” permet de multiplier un entier par 2.
Le projet est codé en C++. Il contient une bibliothèque de base src/mymath.hpp
:
#pragma once
int mul2(int x) {
return 2*x;
}
Un exécutable de tests unitaires src/mymathtest.cpp
(on ne sait jamais):
#include <gtest/gtest.h>
#include "mymath.hpp"
(Mymath, mul2_1) {
TEST(0, mul2(0));
ASSERT_EQ}
// ...
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Et enfin le serveur web src/mymathserver.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 {
.reply(web::http::status_codes::NotFound);
req}
}
public:
(std::string url) : web::http::experimental::listener::http_listener(url) {
App(web::http::methods::GET,
support(&App::handleGet, this, std::placeholders::_1));
bind}
};
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;
(address);
App app// ...
return 0;
}
Le tout est configuré, de façon très classique, avec un CMakeLists.txt
:
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
${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} )
cpprest
install( TARGETS mymathserver mymathtest DESTINATION bin )
Packager 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.
Toutes ces dépendances sont classiques et donc présentes dans la plupart des
logithèques 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ême 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
) :
{ 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.
Packager le projet
De la même façon qu’on a empaqueté cpprestsdk
, on peut créer un fichier
nix/mymathserver.nix
pour empaqueter notre projet :
{ 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 connait pas le code exact des dépendances. Il connait 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 :
{ 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 packaging plus modulaire. Par exemple, on pourrait quasiment
intégrer directement nix/mymathserver.nix
dans le dépôt
nixpkgs et notre projet serait disponible
dans la logithèque Nix officielle !
Lancer un environnement virtuel
A partir de notre fichier default.nix
, on peut lancer un environnement
virtuel :
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 :
mkdir mybuild
cd mybuild
cmake ..
make
./mymathserver
Construire et installer le paquet du projet
Le fichier default.nix
permet également de construire automatiquement le
paquet du projet :
nix-build
./result/bin/mymathserver
On peut également installer notre projet comme un logiciel classique de la logithèque système.
nix-env -i f .
mymathserver
Et on peut même installer en récupérant directement une archive du dépôt git distant :
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
:
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 :
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.
build:
image: nixos/nix
script:
- nix-build nix/release.nix
- ./result/bin/mymathtest
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 packagé nous-même. Nix propose des outils pour mettre en place 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, qui permet d’uploader 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élerer l’intégration continue précédente, on peut
construire cpprestsdk
avec notre configuration release
, uploader les
paquets binaires correspondants dans le dépôt nokomprendo
puis utiliser ce
cache dans le processus d’intégration continue (.gitlab-ci.yml
) :
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 20 minutes à moins de 2 minutes.
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
:
{ 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 :
nix-build nix/docker.nix
docker load < result
docker run --rm -it -p 3000:3000 mymathserver:latest
Cette image Docker peut être déployée, par exemple sur Heroku :
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
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
:
{
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
:
nixops create -d myvm nix/virtualbox.nix
nixops deploy -d myvm --force-reboot
A l’issu du déploiement, une adresse IP est fournie et permet d’accèder à notre serveur sur la machine virtuelle.
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
contient un attribut configureFlags
qui contient les options de compilation à
utiliser. Si on veut utiliser une version de zlib
compilée avec l’option
--zprefix
, on peut utiliser le fichier nix/custom0.nix
suivant :
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
a une option enableSSL2
, qu’on peut spécifier comme dans le fichier
nix/custom1.nix
suivant :
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) :
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 :
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 paquet. 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 120 lignes de code Nix, le projet d’exemple mymathserver
:
- package une dépendance manquante (
nix/cpprestsdk.nix
) - package 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 release (
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.
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