Les dépendances : à l'intérieur ou à l'extérieur du dépôt ?

tout-est-intriqué

To git or not to git

Le problème

Faut-il, ou non, placer les dépendances d'une application à l'intérieur du dépôt git de cette même application ?

Prenons, pour l’exemple, une application Node.js, avec laquelle nous souhaiterions retoucher des photos, plus précisément ajouter un nez rouge aux personnes présentes sur une photo. Pour ce faire, on décide de s'interfacer avec le logiciel ImageMagick. Notre application nécessite à la fois des dépendances installées via node package manager (NPM), comme express, et des logiciels présents sur la machine sur lequel le serveur va s'exécuter, comme imageMagick.

D'une part, ces dépendances sont nécessaires et vitales au bon fonctionnement de l'application ; d’autre part, elles n'appartiennent pas à proprement parler à son code source.

De là, une hésitation : de quelle façon faut-il enregistrer ses dépendances ? Est-il plus prudent de les enregistrer de façon permanente à l’intérieur du dépôt git ou peut-on se contenter de conserver seulement un pointeur vers un registre ?

L'état de l'art

En apparence, le débat est clos. De façon unanime, pour une dépendance, on enregistre aujourd'hui un pointeur vers un registre. Dans notre exemple précédent, on ajoute express en version 4.17.3 dans le package.json, et on mentionne l'installation d'imageMagick dans un Dockerfile.

L'accord est unanime, la pratique universelle. Si bien que si l'on commence un projet angular avec la commande :

ng new nez-rouges

Le dossier créé contiendra un fichier .gitignore où les node_modules seront mentionnés dans les fichiers à ignorer.

Les fissures

Cette pratique généralisée ouvre cependant immédiatement deux failles de sécurité :

  • Que se passe-t-il si, temporairement ou définitivement, une dépendance n’est plus accessible ? Si, par exemple, le registre npm n'est plus accessible à cause d'une panne de réseau ? Ou bien si le code de l'application doit vivre quatre à cinq ans et que, durant cette durée de vie, un des registres de dépendance disparaît ?

  • Que se passe-t-il si une modification malveillante à une dépendance est apportée ?

Mentionnons trois scénarios qui peuvent causer problème :

  • Comme tout service, un registre peut être temporairement indisponible. NPM n'échappe pas à cette règle. Si, par suite, cet incident correspond au moment où vous souhaitiez corriger un bug majeur, ou bien ajouter un second serveur, tant pis pour vous, il vous faudra prendre votre mal en patience.
  • Si l'on prévoit que la durée de vie du code s'étendra sur plusieurs années, il est fort probable qu'une dépendance vienne à s'éteindre. Cela peut particulièrement être le cas d'un package ubuntu, dont soit l'adresse de registre changerait, soit la version ne serait plus délivrée. Lorsque cela advient, il devient nécessaire de réécrire tout un bout de l'application.
  • Enfin, on pense garantir la permanence du code en mentionnant la version des dépendances à télécharger. Nous indiquons par exemple à npm de charger la version 4.17.13 d'express. Mais le propriétaire du dépôt possède encore la possibilité de modifier cette même version, ce qui lui ouvre la possibilité d'une attaque. En février 2022 par exemple, la modification des modules colors.js et fakers.js par leur concepteur mécontent a provoqué le blocage d'un bon millier d'applications d'internet. Là encore, cela empêche tout déploiement pendant un certain laps de temps ; il ne reste plus qu'à espérer que cela ne survienne pas à un moment critique.

Recommandation

À l'instar de différents articles, nous recommandons donc de maintenir une archive des dépendances. Pour les modules, il est bon de les placer directement à l'intérieur du dépôt git et d'enlever node_modules du .gitignore. Pour les dépendances logicielles, nous recommandons de se servir d'un docker registry. Si la deuxième pratique est courante, la première l'est moins, car elle présente, à son tour, des inconvénients manifestes.

  • Elle augmente la complexité du suivi du code source
  • Elle oublie que parfois certains modules sont construits différemment selon l'environnement d'installation et que la portabilité d'un environnement à l'autre n'est pas toujours garantie.
  • Surtout, dans les procès de développement moderne, git sert non seulement de logiciel de contrôle de version mais aussi, et même principalement, d'outil collaboratif. Son rôle est de permettre l'interaction entre les équipes, de faciliter la relecture du code, de visualiser la contribution des uns et des autres. Mettre dans un commit les milliers de ligne de code d'une dépendance rend alors illisible les modifications fonctionnelles apportées. Cela complexifie l'échange sur les codes et rompt avec les modes de travail en vigueur.

Le contrefort

Pour résoudre ce problème de façon pérenne, à la webcapsule, nous avons mis en place un cache automatisé des dépendances. Nous disposons d'un dépôt git parallèle au dépôt git originel. Pour chaque commit ajouté sur la branche de production du dépôt principal, nous nous chargeons d'enregistrer les dépendances aptes à faire tourner l'application dans notre dépôt miroir. Pour les modules, ils sont pleinement sauvegardés dans le dépôt git ; pour les dépendances, elles sont sauvegardées via la construction d'une image Docker, ensuite stockée dans un Docker Registry.

Cela permet aux développeurs de ne pas enregistrer leur dépendance directement à l'intérieur du dépôt git, tout en ayant la certitude de toujours pouvoir disposer, si besoin, de l'ensemble des dépendances dans un état fonctionnel.

Ainsi, sur notre exemple de départ, celle d'une application Node.js utilisant imageMagick, nous disposons d'une version du code source où la version d'express 4.17.13 est enregistrée et figée. Par ailleurs, nous stockons une image docker contenant la version d'imageMagick permettant d'exécuter l'application. À chaque instant, nous sommes donc capables de construire un environnement d'exécution fonctionnel de l'application, hermétique aux indisponibilités ou aux modifications indues des dépendances.