4 points par GN⁺ 2024-03-17 | 1 commentaires | Partager sur WhatsApp
  • Si votre environnement utilise déjà les images Docker comme unité de déploiement, Nix constitue un point d’entrée pour expérimenter les builds déterministes et le verrouillage des dépendances sans bouleverser fortement le workflow
  • Un docker build classique dépend facilement de l’état de l’Internet public, par exemple des dépôts Ubuntu ou de téléchargements externes, ce qui rend difficile la reproduction de la même image longtemps après
  • Nix connaît les dépendances nécessaires au niveau des paquets, ce qui permet avec dockerTools.buildLayeredImage de ne retéléverser que les couches modifiées
  • L’exemple douglas-adams-quotes, basé sur Go, montre qu’on peut construire le binaire avec pkgs.buildGoModule, puis le charger et le déployer comme une image Docker classique avec nix build.#docker, docker load <./result
  • Si plusieurs services d’un monorepo partagent leurs couches et le cache Nix, on peut réduire le temps et le coût de build lors des déploiements répétés et de la reproduction d’images issues d’anciens commits

Pourquoi ajouter Nix à la construction d’images Docker

  • Nix réunit trois dimensions : gestionnaire de paquets, langage et système d’exploitation
    • Les instructions de build des paquets sont écrites en langage Nix
    • Le gestionnaire de paquets Nix peut, à partir de ces instructions, produire des logiciels, des outils, des images NixOS, des images de conteneurs, etc.
  • Introduire Nix dans un pipeline CI/CD existant n’est pas simple
    • Nix diffère de la manière de travailler à laquelle beaucoup de développeurs sont habitués
    • Sauf dans un environnement où tout peut être reparti de zéro, comme une nouvelle startup ou un homelab, la barrière à l’adoption est élevée
  • Les organisations qui utilisent déjà Docker peuvent commencer par appliquer Nix à la construction d’images de conteneurs afin d’expérimenter sans casser fortement leur flux de déploiement existant

Là où les builds Docker deviennent fragiles

  • Docker et la conteneurisation sont largement adoptés, si bien que les images Docker servent de facto de format de paquet universel sur Internet
    • Des plateformes comme Fly.io, Railway ou Render utilisent des images Docker plutôt que des images VM arbitraires ou des programmes Linux sous forme de tarball
  • Les builds Docker ne sont pas toujours déterministes
    • La plupart des Dockerfiles vus sur Internet buildent correctement, mais la petite part qui échoue peut se transformer en problème d’exploitation
  • L’une des plus grandes faiblesses tient au fait que le build Docker accède à l’Internet public
    • C’est nécessaire pour télécharger des paquets depuis les dépôts Ubuntu
    • Lorsqu’il faut régénérer la même image plus tard, il est difficile de revenir à l’état exact du dépôt Ubuntu de l’époque
    • Ubuntu 18.04 approche de sa fin de support cette année-là, et l’on peut ne découvrir qu’après incident ce qui dépend encore de cette version
  • Ajouter des paquets à une image Docker de manière simple peut aussi générer du gaspillage d’espace
    • Exécuter apt-get upgrade au début du build peut remplacer des fichiers présents dans l’image du conteneur
    • Les fichiers d’avant remplacement peuvent rester dans les couches et s’accumuler comme des copies fantômes

La manière dont Nix gère les couches Docker

  • Nix connaît à l’avance les dépendances nécessaires et peut les répartir sur le moins possible de couches Docker
    • Changer une seule ligne de code ne nécessite pas de réinstaller les dépendances via apt ou npm
    • Seul le minimum réellement modifié doit être répercuté dans la mise à jour de l’image
  • dockerTools est une boîte à outils qui place des paquets Nix et leurs dépendances dans des images Docker
  • Les images Docker produites avec Nix se divisent en deux grandes approches
    • Image non stratifiée : le programme, ses dépendances et des éléments supplémentaires comme les certificats racine TLS sont placés dans un seul dossier puis exposés comme une image à couche unique
    • Image stratifiée : chaque dépendance est placée dans une couche distincte afin de ne téléverser que ce qui a réellement changé
  • Docker dispose lui aussi d’un stockage adressé par contenu, mais il est difficile d’en tirer pleinement parti avec docker build seul
    • Les images stratifiées de Nix utilisent des couches par paquet, donc une dépendance comme glibc n’a besoin d’être téléversée qu’une seule fois
    • En revanche, si cette bibliothèque doit être modifiée, sa couche devra être renvoyée

Exemple de service Go : Douglas Adams Quotes

bin = pkgs.buildGoModule {
  pname = "douglas-adams-quotes";
  inherit version;
  src = ./.;
  vendorHash = null;
};
  • Cette définition utilise le template de modules Go pour configurer le compilateur Go, le compilateur C pour CGo et le téléchargement des dépendances externes
    • pname est le nom du paquet
    • version est générée automatiquement à partir du commit Git du service
    • src = ./.; utilise le code source du répertoire de travail courant
    • S’il n’y a pas de dépendances au-delà de la bibliothèque standard, on peut laisser vendorHash = null
    • S’il y a des dépendances externes, il faut spécifier le hash de toutes les dépendances ou utiliser gomod2nix
  • Le paquet se build avec la commande suivante
nix build .#bin
  • Une image Docker en couches peut être créée avec dockerTools.buildLayeredImage
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • En indiquant le binaire serveur compilé dans config.Cmd, les éléments nécessaires sont copiés avec lui
    • glibc et les composants requis à l’exécution sont également inclus
  • Des paquets supplémentaires comme les certificats racine CA peuvent être placés dans contents
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  contents = with pkgs; [ cacert ];
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • L’image se construit puis se charge dans le démon Docker avec les commandes suivantes
nix build .#docker
docker load < ./result
  • En l’ouvrant avec dive, on voit que chaque couche ajoute des paquets différents de nixpkgs, puis qu’à la fin les éléments sont reliés symboliquement à la racine de l’image

L’effet du partage de couches dans un monorepo

  • Si plusieurs services se trouvent dans le même dépôt, les images Docker produites avec Nix peuvent partager leurs couches
    • Ce partage se fait sans configuration supplémentaire
    • Avec Docker seul, obtenir le même résultat conduit souvent à créer plusieurs images de base communes, qui peuvent inclure des outils et du contenu inutiles pour certains services
  • Par exemple, le dépôt Xe/x est un monorepo qui rassemble dix ans de side projects, d’expérimentations et d’outils
    • Plusieurs projets y sont déployés sur environ trois plateformes
    • Un travail a été mené pour les empaqueter en images Docker afin de converger vers une base de déploiement commune
  • Lorsqu’une mise à jour d’un service est poussée, une partie des mises à jour partagées avec la plupart des autres services est poussée en même temps
    • Cette approche fait gagner du temps et de l’argent sur plusieurs projets

Ce que le cache Docker seul couvre difficilement

  • En théorie, l’utilisation du cache Docker peut permettre de construire des images presque aussi efficacement que Nix
  • Mais obtenir le même effet avec une approche Docker peut rendre les étapes de build difficiles à maintenir
    • Il faut installer les bibliothèques partagées dans des couches séparées
    • Il faut réactiver la pile réseau pendant le build, ce qui affaiblit la reproductibilité
    • Il faut réajuster les chemins de recherche, les flags du compilateur et la configuration liée à CGo
  • dockerTools.buildLayeredImage de Nix prend en charge les détails de mise en conteneur des paquets, ce qui simplifie la configuration

Reproduire un état passé et tirer parti du cache Nix

  • L’un des grands avantages de Nix est de pouvoir reconstruire exactement un logiciel à un instant passé
    • Si l’on doit reproduire plus tard un bug dans l’environnement d’un client précis, on peut recréer la même image Docker
    • Le build des paquets fige l’état complet du logiciel et de ses dépendances à un moment donné
  • XeDN est un projet développé depuis plusieurs années, et sa version d’il y a 14 mois peut être buildée avec la commande suivante
nix build github:Xe/x/567fdc2#xedn-docker
  • Cette commande construit la cible xedn-docker à partir d’un commit précis du dépôt GitHub repo Xe/x
    • Une fois chargée dans le démon Docker, elle produit une image identique octet pour octet à celle de l’époque
    • L’exemple inclut aussi Go 1.19
  • Reproduire la même chose avec un build Docker classique peut nécessiter beaucoup de stockage
  • Le cache Nix enregistre les sorties des commandes Nix pour éviter de les reconstruire plus tard
    • Chaque laptop de développeur n’a pas besoin de rebuilder un paquet comme nokogiri
    • Il peut récupérer un résultat déjà construit dans le cloud
  • Garnix est utilisé comme CI pour tous les projets flakes et remonte l’état des builds à chaque commit
    • La configuration des machines de homelab est aussi buildée avec Garnix, ce qui permet de récupérer la configuration la plus récente depuis le cache Garnix lors des mises à jour du soir au lieu de tout rebuilder localement

Conclusion en tant qu’artefact de déploiement

  • On peut considérer Nix comme un meilleur constructeur d’images Docker que le constructeur d’images de Docker
  • Nix pousse à décrire le résultat attendu, plutôt qu’à lister manuellement toute la procédure pour y parvenir
  • Dans un environnement qui utilise déjà Docker, commencer par la construction d’images Docker avec Nix constitue une voie d’adoption réaliste
  • Les images Docker produites avec Nix peuvent partager leurs couches entre différentes parties d’un monorepo
  • Grâce au cache binaire, il n’est pas nécessaire de reconstruire du code déjà buildé par le passé
  • Le résultat final reste une image de conteneur standard, déployable sur des plateformes comme AWS, Google Cloud ou Fly.io

1 commentaires

 
GN⁺ 2024-03-17
Commentaires sur Hacker News
  • J’ai essayé plusieurs fois d’aimer Nix, mais j’ai l’impression qu’il faut que j’abandonne
    J’ai deux systèmes qui tournent sous Nix et j’ai peur d’y toucher ; il m’est déjà arrivé de casser les deux et de devoir tout réinstaller depuis zéro. En théorie, Nix est idempotent et déterministe, mais en pratique il faut comprendre par rapport à quoi il est déterministe, et si on ne connaît pas en profondeur le fonctionnement de toutes les briques dont il dépend, on se retrouve avec des résultats étranges, des erreurs bizarres et inutiles, ou parfois aucune information en retour
    La documentation, même quand elle est complète et techniquement correcte, est affreusement obscure, et les tutoriels ne vous emmènent qu’à 80 % du chemin. Au-delà, il faut construire le reste soi-même, ce qui demande des années d’expérience accumulée et de frustration, et je n’ai plus envie de supporter ça. L’avantage de Docker, au contraire, c’est presque son chaos : avec une connaissance approximative du shell et du gestionnaire de paquets de la distribution, on peut construire presque n’importe quoi, et quand il y a un problème, les outils qui existent depuis des décennies rendent le diagnostic et la correction bien plus simples
    Nix, c’est un peu comme Emacs : avec de la patience et une connaissance profonde, presque visionnaire, on peut tout faire, mais on a l’impression qu’il faut soit s’y consacrer entièrement, soit rester à distance et regarder

    • J’ai suivi le même chemin, et même si j’aime les builds déterministes, je pense que le plus gros problème des Dockerfile, pour le développeur moyen, c’est qu’ils ont l’air déterministes
      Si on build deux fois de suite sur la même machine, on obtient le même résultat ; mais après un certain temps, quand le gestionnaire de paquets et les tags de l’image de base ont été mis à jour, une reconstruction un mois plus tard peut donner un résultat complètement différent. Quand une équipe gère une quarantaine de conteneurs, la maintenance de ces conteneurs finit par représenter une grosse partie du travail
      Donc, en théorie, Nix devrait être parfait, mais c’est tellement différent que les outils des éditeurs ne tournent pas sous Nix, et quand une erreur survient il est difficile de trouver rapidement une solution sur le web. C’est cette frustration qui m’a poussé à créer https://www.stablebuild.com, qui fournit des builds déterministes basés sur Docker au-dessus de conteneurs Ubuntu, Debian et Alpine. Le système repose sur un cache pull-through immuable pour Docker Hub, des instantanés complets quotidiens des dépôts de paquets Ubuntu/Debian/Alpine, des instantanés quotidiens des PPA populaires et de l’index PyPI, ainsi qu’un cache immuable pour des fichiers et URL arbitraires
      En pratique, c’est facile à utiliser, facile à déboguer et compatible avec un large éventail de logiciels ; dans les conteneurs migrés vers StableBuild, nous n’avons eu zéro incident lié au non-déterminisme
    • Le problème n’est pas seulement que la documentation est obscure : en particulier du côté du nouveau CLI et des flakes, elle est souvent tout simplement fausse, et pas uniquement dans des cas limites
      J’ai déjà passé du temps à essayer de comprendre pourquoi nix develop ne se comportait pas comme indiqué dans la documentation, et comment faire pour qu’il se comporte ainsi. La doc explique qu’on entre, par défaut, dans l’environnement du moment du build, mais en réalité le comportement par défaut n’est pas hermétique, et même les noms des options en ligne de commande prêtent à confusion, au point qu’il a fallu aller extraire l’information dans le code source
      J’aimerais aussi qu’on traite plus explicitement les cas limites qui cassent la reproductibilité. Le code en virgule flottante est sensible à l’ordre des opérations, et l’état peut fuiter à cause de la préemption du système d’exploitation ; même les choses évidentes devraient être précisées, sinon les gens finissent par se tirer une balle dans le pied
    • Ce n’est pas si terrible que ça, mais même avec une configuration NixOS standard, il reste énormément d’état non reproductible dans les comptes utilisateur et à l’intérieur du système
      Avec une configuration « Erase your darlings », on peut éliminer la majeure partie de l’état non reproductible hors des comptes utilisateur. C’est un peu pénible, mais en même temps, qu’est-ce qui ne l’est pas sur NixOS ?
      https://grahamc.com/blog/erase-your-darlings/
      Je ne m’occupe pas de l’intérieur des comptes utilisateur, et je n’aime pas Home Manager
    • Au travail, on utilise à la fois Docker et NixOS, mais je n’ai jamais rencontré les problèmes évoqués plus haut
      Docker est correct, mais les performances sur Mac ne sont pas terribles, et j’apprécie le fait qu’avec Nix il soit trivial d’installer plusieurs machines de la même manière et de les faire fonctionner de façon identique. La documentation de Nix est médiocre, mais ChatGPT-4 a été excellent pour résoudre les problèmes liés à Nix
      J’ai l’impression que 90 % des problèmes avec Nix venaient du fait qu’on essayait de faire quelque chose qui n’était pas « la manière Nix »
    • Ça vaut le coup d’essayer une distribution immuable comme Fedora Atomic
      J’ai utilisé presque tous les gestionnaires de paquets des distributions, et j’ai réussi à tous les casser d’une manière ou d’une autre, même sans rien faire de spécial. Fedora Kinoite, elle, tient bien le coup malgré l’ajout et la suppression de couches, les mises à jour quotidiennes, et même un rebase depuis Silverblue. Personnellement, je pense que rpm-ostree finira par remplacer Nix
  • Je considère que Nix et NixOS en sont à peu près là où git en était avant GitHub
    L’idée de base repose sur une informatique plus sérieuse que les approches existantes comme SVN ou Docker, et même si la plomberie interne a encore des problèmes, ce n’est pas pire ; en revanche, les outils orientés utilisateur et la documentation ne sont pas encore au niveau d’une adoption grand public
    Il est possible que la sortie de flox ait changé cela : https://flox.dev. C’est presque fluide, ce qui n’a rien de surprenant venant d’anciens de DE Shaw
    Nix n’a pratiquement pas de sens sans les flakes et nix-command, mais tous deux sont documentés comme expérimentaux et désactivés par défaut. La documentation s’améliore, mais reste insuffisante, et le nixlang est excellent une fois bien appris, sans pour autant être acceptable comme barrière d’entrée pour le grand public. nix-env -iA foo ne correspond en pratique presque jamais à ce qu’on veut réellement faire ; du coup, Nix tient plus de l’arme secrète pour les entreprises capables d’assumer une expertise interne que du gestionnaire de paquets tel qu’il est présenté
    flox abaisse le seuil pour obtenir une « meilleure expérience dès le premier essai ». Nix/NixOS, ou quelque chose du même genre, finira par reléguer Docker au second plan comme Subversion, mais cela n’arrivera pas avant un moment à la GitHub — puis tout se produira d’un coup
    La plupart des reproches faits à Nix dans ce fil sont techniquement faux, mais tout à fait compréhensibles, et plus important encore, ce n’est pas la faute des utilisateurs. Je sais qu’il y a de moins en moins de gens qui ont connu le monde d’avant git/GitHub, mais ça vaudrait la peine d’entendre Linus expliquer à des personnes qui auraient passé les critères d’embauche du Google des débuts pourquoi elles devraient s’intéresser à un outil perçu comme complexe
    https://youtu.be/MjIPv8a0hU8?si=QC0UnHXRdMpp2tI4

    • C’est Ron de flox.dev, et toute l’équipe a bien ri grâce à ce post
      Ça fait un moment qu’on construit ça, et j’aimerais savoir s’il y a des points à mettre en priorité pour l’améliorer
    • Pour qu’un outil de développement réussisse, je pense qu’il faut que, pour les tâches les plus courantes, un ingénieur puisse mal utiliser l’outil d’au moins trois façons différentes tout en réussissant quand même à « finir », quitte à laisser derrière lui de la dette technique implicite
      git le permet, mais Nix pas encore, donc je ne sais pas si un moment à la GitHub aiderait vraiment
  • Ce billet de blog n’explique pas pourquoi les couches Docker partagées sont utiles
    La raison, c’est le cache : plus d’images partagent les mêmes couches, plus on peut mettre en cache de choses, ce qui accélère le démarrage des conteneurs
    Si Docker est faible sur ce point, c’est parce que pour bénéficier du cache, il faut que la construction d’une image produise des couches aussi proches que possible des couches existantes. Par exemple, si j’exécute apt-get install python3 aujourd’hui et qu’il n’y a pas de nouvelle mise à jour, il faudrait obtenir exactement la même couche qu’hier ; or les couches Docker sont mises en cache via le hash des fichiers, donc tous les fichiers doivent être strictement identiques, métadonnées comme l’heure de création comprises
    Nix stocke déjà les dépendances par hash, donc avec la même version et la même configuration, la couche est toujours identique

    • Je pense que c’est plus juste de le formuler ainsi : le format Dockerfile impose une relation hiérarchique entre les couches
      Or les dépendances forment généralement un graphe plutôt qu’un arbre, ce qui devient vite pénible. Des outils alternatifs comme Nix, ou probablement Bazel, n’ont pas cette contrainte et peuvent projeter le graphe de dépendances sur les couches Docker pour obtenir un cache plus fin. C’est impossible à exprimer avec un Dockerfile
    • Avec Docker, si une couche est déjà en cache, la couche contenant apt-get n’est pas invalidée automatiquement
      Elle ne change que si on utilise --no-cache ou s’il y a un changement dans une couche parente
  • J’ai passé les 2 ou 3 derniers jours à me battre pour construire une image Docker sur Darwin, et cet article donne l’impression que l’univers se moque de moi
    Nix est clairement le meilleur outil pour l’objectif visé, mais il a aussi des recoins sombres et abandonnés qui vous aspirent l’âme. J’adore ça, mais parfois j’ai l’impression d’être Morty entraîné dans une aventure de Rick au pays des compilateurs

    • Le gros problème, c’est la conception même de Docker
      Docker est fondamentalement plus proche d’une prison pour binaires Linux. Sous Linux, il suffit de compiler le binaire et de le mettre dans le conteneur, et le code Nix reste simple. Si vous savez compiler le code, créer le conteneur n’est qu’une étape de plus
      Mais Docker exige des binaires Linux, et sur Mac, Docker Desktop lance une VM Linux, fait tout le travail dedans, puis masque ce fait. Nix ne procède pas ainsi, donc il y a deux options
      La première consiste à faire de la compilation croisée, mais il faut pouvoir croiser jusqu’à glibc, et même si la plupart des dépendances communautaires le permettent, certains paquets peuvent ne pas avoir été pensés par leur auteur pour la compilation croisée. En plus, Hydra, qui alimente le cache Nix standard, ne produit pas de builds en compilation croisée, donc on peut compiler longtemps pour finir en échec
      La seconde consiste à attacher un builder Linux au Mac et à lui envoyer les builds x86_64-linux. Cela peut être une machine physique, une VM, ou même un conteneur Docker NixOS
      La solution n°1 semble être la bonne en théorie, mais la n°2 est plus pratique. Si vous rencontrez ce genre de problème, c’est probablement parce que vous essayez la n°1, ce qui demande non seulement Nix mais aussi une solide expérience de la compilation croisée. Ce serait bien si Hydra construisait aussi les compilations croisées de Darwin vers Linux pour les mettre en cache et éviter les régressions, mais cela augmenterait aussi les coûts. Mieux vaut sans doute essayer la solution n°2
      Il me semble qu’il y avait une solution officielle : https://ryantm.github.io/nixpkgs/builders/special/darwin-bui...
    • Avec Orbstack, ça fonctionne parfaitement
      Sur certaines stacks, la compilation croisée n’est pas totalement robuste, donc je fais de la compilation croisée {aarch64,amd64} x {linux,darwin} avec Docker. Je fais tout compiler en lançant plusieurs Docker sur Darwin aarch64, et l’expérience a été bonne
    • Je suppose qu’on parle de ce genre d’aventure de 20 minutes
      https://github.com/gytis-ivaskevicius/high-quality-nix-conte...
    • macOS est clairement plus rude, et là j’utilise colima, qui s’en sort plutôt bien
      Il y a un ou deux bugs, mais ils semblent surtout liés aux volumes, et pour la construction d’images Docker, ça se passe correctement. Le côté le plus rude, c’est la vitesse, à cause du cumul entre le matériel et le fait que Docker doive émuler une VM Linux
  • L’expérience de création d’images Docker pour des applications Java avec Nix n’a pas été très bonne
    Après l’abandon de gradle2nix, il ne semble plus y avoir d’alternative évidente pour les applications Java basées sur Gradle. J’avais autrefois proposé à un ami d’essayer de produire une image Docker aussi petite que possible pour une simple application Spring Boot, mais le résultat avec Nix faisait deux fois la taille d’une image créée sans Nix
    Le code est visible ici : https://github.com/jossephus/Docker_challenge/blob/main/flak...

    • La raison est qu’il y a deux JDK
      zulu et le JDK inclus par gradle via l’argument jdk se retrouvent tous les deux dans l’image. Un coup d’œil à gradleGen dans nixpkgs permet de comprendre. Désolé pour gradle2nix, je travaille sur une amélioration moins bricolée
    • Je n’ai pas utilisé Java depuis plus de 10 ans, donc je ne peux pas beaucoup aider, mais j’ai déjà réussi à mettre une application dans un conteneur de 70 Mo avec Python, toutes ses dépendances, busybox et tini
      C’était grosso modo comme ça : https://gist.github.com/takeda/17b6b645ad4758d5aaf472b84447b...
      Tout était lié avec musl, Python avait été compilé en désactivant tous les paquets inutilisés par l’application, et les parties non utilisées de boto3/botocore avaient été retirées. À eux seuls, boto3/botocore dépassent les 100 Mo
      Les paquets Nix ciblent par défaut le système d’exploitation NixOS, donc dans une situation générale où l’espace disque n’est pas un problème, on veut que toutes les fonctionnalités soient activées. Cela entraîne beaucoup de dépendances inutiles. À l’inverse, les images Alpine sont conçues pour Docker, avec pour objectif de désactiver les fonctionnalités annexes dans les paquets, donc le résultat est plus petit
      Pour produire une petite image, il faut désactiver ce qui n’est pas nécessaire via override. Par exemple, zulu inclut alsa, fontconfig, freetype, xorg, cups, gtk, cairo, ffmpeg, etc. Mon ami a extrait avec précaution uniquement les fichiers nécessaires pour les mettre dans le conteneur, alors que côté Nix cela revenait à embarquer tout le paquet zulu et toutes ses dépendances
      Il faut d’abord corriger cela pour n’inclure qu’un seul JDK, puis il est possible de réduire davantage la taille du JDK avec les méthodes ci-dessus. Utiliser openjdk_headless pourrait simplifier les choses
      https://github.com/NixOS/nixpkgs/blob/master/pkgs/developmen...
    • Pour un conteneur OCI Java minimal, il est difficile de battre jib
      https://github.com/GoogleContainerTools/jib/tree/master/jib-...
    • openjdk_headless évite les dépendances GTK et X inutiles pour Spring
    • J’ai participé moi-même au défi et j’ai un peu nettoyé le code Nix ; l’essentiel semble être de fabriquer un JRE très petit
      J’ai remplacé Zulu par la build OpenJDK headless de Nixpkgs afin de supprimer les dépendances aux bibliothèques graphiques, puis j’ai créé un JRE minimal personnalisé avec pkgs.jre_minimal et jlink
      La taille de l’image est de 161 Mo, donc légèrement plus grande que l’image demo_jlink. C’est parce que le JRE fait environ 90 Mo, puisqu’il inclut tous les modules réellement nécessaires à l’exécution de l’application. L’appel à jdeps dans Dockerfile_jlink ne détectait pas tous les modules et construisait un JRE avec seulement java.base. Si mon JRE minimal ne contenait lui aussi que java.base, il ferait environ 50 Mo, et l’image de conteneur cassée ferait 117 Mo avec Podman
      J’ai aussi supprimé un copyToRoot incorrect dans l’appel à dockerTools.buildImage. Le simple contexte de chaîne de config.Cmd suffit déjà à faire entrer l’application dans l’image, et le code existant copiait donc l’application une seconde fois. Je suis également passé à dockerTools.buildLayeredImage, qui place chaque store path dans une couche d’image distincte ; c’est intéressant pour la mutualisation des dépendances entre plusieurs images de conteneur, mais cela ne change rien pour une expérience sur une image unique
      Ce qui reste concerne surtout l’optimisation de la taille du JRE. glibc est la dépendance suivante la plus volumineuse, à environ 30 Mo, apparemment parce que Nixpkgs compile glibc avec la prise en charge de nombreuses locales et encodages de caractères. Je ne sais pas s’il est possible, ni si ce serait pratique, de séparer cela dans une derivation ou une output distincte pour le rendre optionnel. Si l’on construit plusieurs images avec dockerTools.buildLayeredImage, alors glibc et toutes les autres dépendances sont partagées, à condition d’utiliser le même commit Nixpkgs
      https://github.com/max-privatevoid/hackernews-docker-challen...
  • Si vous avez déjà adopté Nix, c’est excellent, et j’aimerais que la gestion de paquets déclarative comme Nix ou Guix se répande davantage.
    Si vous utilisez déjà Docker mais souhaitez introduire Nix progressivement, cette présentation propose aussi cette approche : https://youtu.be/l17oRkhgqHE
    Au lieu de migrer immédiatement toute la configuration et la construction des conteneurs vers Nix, on peut conserver le Dockerfile et lui faire construire la configuration Nix. Le principal inconvénient est qu’on ne profite absolument pas des layers, et l’avantage est qu’on peut réutiliser l’infrastructure ou l’automatisation Docker existante tout en faisant évoluer le Dockerfile progressivement.

    • Cet article utilise aussi la même approche
      [1] https://mitchellh.com/writing/nix-with-dockerfiles
    • Un des axes de l’article est que les builds Docker ne sont pas reproductibles alors que Nix l’est.
      Cela dit, j’ai l’impression qu’une grande partie de cette non-reproductibilité vient du fait qu’il n’y a aucune garantie qu’un des layers Docker reste disponible en permanence. Dans ce cas, je me demande aussi si Nix garantit que les versions de paquets restent indéfiniment dans le dépôt.
  • C’est un peu hors sujet, mais l’illustratrice Annie Ruygt, qui a réalisé les visuels de ces slides, a aussi créé les logos et le brand art de deux startups du YC, RethinkDB (rethinkdb.com) et Pachyderm (pachyderm.io).
    J’ai travaillé chez Pachyderm, et Pachyderm a été fondée par d’anciens ingénieurs de RethinkDB. Son travail est vraiment excellent.

  • Il y a relativement peu de temps, j’ai passé environ une demi-journée à essayer de construire une image de base CI avec Nix sur recommandation de l’équipe infra, mais l’image était énorme et certaines parties ne fonctionnaient pas à cause de problèmes de liens.
    Ce qui m’a particulièrement agacé, c’est qu’en créant une image multi-architecture, il essayait réellement d’exécuter quelque chose d’une autre architecture, tout en ne prenant en charge que la virtualisation matérielle via qemu. Ma machine de build et mon poste de travail sont des VM, donc je n’ai pas ça, même si j’ai binfmt-misc. S’il avait fork/exec un mkdir arm64 pour créer /tmp, ça aurait fonctionné, mais un layer Docker n’est qu’un fichier tar, donc on peut créer un répertoire comme ci-dessous
    echo "tmp uid=0 gid=0 time=0 mode=0755 type=dir" | bsdtar -cf - @-
    Il est même très probable que ce layer exact existe déjà quelque part, auquel cas l’utilisateur n’aurait même pas besoin de le télécharger.
    Chaque fois que j’essaie Nix, j’ai l’impression que dans quelques mois je pourrai l’utiliser régulièrement. nixpkgs contient presque tous les paquets que je veux, et l’installation sur poste de travail fonctionne bien. Mais un besoin du type « il me faut bash, python, build-essential et Bazel » ne semble pas être l’objectif d’un constructeur d’images Docker. Si c’est juste pour mettre un binaire Go dans une image Docker, il n’y a pas besoin de Nix. Il suffit de prendre une distroless et de mettre l’application dans un fichier tar, et cela devient un conteneur. Personnellement, j’utilise rules_oci de Bazel, qui ne fait au fond pas beaucoup plus que ça, avec juste un peu plus d’intelligence pour construire des binaires pour plusieurs architectures, générer un YAML d’index d’images et les pousser vers un registre.

    • On devrait pouvoir faire de la cross-compilation pour des binaires d’autres architectures sans réellement les exécuter.
      Bien sûr, il faut que les fichiers de build du paquet le prennent en charge. Et l’idée que qemu ne prend en charge que la virtualisation matérielle ne me semble pas correcte non plus. On peut utiliser qemu aussi pour des architectures où seule l’émulation logicielle est possible.
      Un exemple minimal est présenté ici : https://discourse.nixos.org/t/how-do-i-get-a-shell-nix-with-...
      Je n’irais pas jusqu’à dire que c’est aussi simple que d’utiliser pkgCross, mais je suis curieux de savoir quel problème concret a été rencontré dans le flux habituel.
      https://nix.dev/tutorials/cross-compilation.html
    • Tu parles sans doute de l’image Docker officielle de Nix, et elle est effectivement grosse.
      Je l’ai utilisée pendant des années sur plusieurs projets, mais si la taille est un problème, on peut créer des images très petites ne contenant que ce qui est spécifié, comme mentionné dans l’article.
      [1] https://hub.docker.com/r/nixos/nix/tags
    • La cross-compilation vers des images Docker était précisément mon cas d’usage de Nix.
      J’utilisais même musl, et cela produisait en CI les plus petites images, plus vite et plus régulièrement que tout ce que j’avais fait avec d’autres outils, avec un cache qui fonctionnait bien. Je ne comprends pas bien ce qui était censé avoir été exécuté.
    • Je serais curieux de voir à quoi ressemblaient au final les fichiers Nix et Docker, et s’il a fallu utiliser buildFHSEnv pour prendre en charge un binaire tiers bizarre.
      On dirait que Nix a besoin de plus d’articles expliquant comment passer en douceur, morceau par morceau, depuis un système existant.
  • En tant qu’ingénieur plateforme, j’aimerais apprécier Nix, mais ce n’est pas simple pour tout le monde.
    Je trouve aussi que l’expérience développeur reste assez mauvaise. Par exemple, je préfère l’expérience développeur de devbox, où l’on peut ajouter des paquets avec devbox add python@3.11.
    Quand on regarde un flake.nix de 120 lignes, il est difficile de dire que c’est précisément « plus simple ».
    https://github.com/Xe/douglas-adams-quotes/blob/main/flake.n...

    • Cette comparaison est un peu injuste.
      Le flake lié définit la manière de compiler un binaire Go, définit une image Docker qui utilise ce binaire comme point d’entrée, et définit aussi un module NixOS qui crée un service systemd exécutant ce binaire Go. Il inclut également un test NixOS pour vérifier que ce module NixOS crée bien le service systemd attendu.
      Le framework de test NixOS est assez impressionnant, et les tests s’exécutent dans une VM QEMU faisant tourner NixOS. Dans l’article lié, ce qui est pertinent, c’est la définition du binaire Go et de l’image Docker, ainsi qu’une partie du boilerplate autour.
    • Ces 120 lignes sont difficiles à considérer comme représentatives, et pour un service unique, cela aurait sans doute été écrit en ligne plutôt que sous forme d’un nouveau module.
      En outre, de nombreuses lignes correspondent directement à la description du service systemd en ligne, donc elles ne disparaîtraient avec aucun autre système. Il y a aussi une manière élégante de déclarer plusieurs systèmes avec des overrides.
      Cet exemple relève davantage de « faisons une chose banale comme s’il s’agissait d’un gros projet sérieux », et dans un cas ponctuel, cela pourrait probablement être réduit à environ 40 lignes.
    • Ces 120 lignes font quand même pas mal de choses.
    • Il faut 120 lignes de code pour gérer un seul binaire et une configuration systemd.
      Ce binaire et ce fichier de configuration sont ce qui se rapproche le plus d’un « flat pack » qu’on puisse obtenir sous Linux, et cet exemple montre bien ce que nous avons raté entre la forêt et les arbres.
  • Guix a aussi une option Docker simple et correcte avec guix pack -f docker.
    Guix a aussi l’avantage d’utiliser Guile/Scheme, un langage déjà employé ailleurs, au lieu de son propre langage dédié.
    [1] https://guix.gnu.org/manual/en/html_node/Invoking-guix-pack....