Vos Builds dans des images Docker – Part #1 – Quelques changements d’habitudes

À 4SH, Google Cloud Build est devenu la plateforme préconisée pour l’ensemble de nos builds projet.
Cette plateforme, comme beaucoup de plateformes de Builds dans le Cloud, propose d’exécuter vos workflows de Builds dans des images Docker, et cela demande de changer certaines de nos habitudes.
L’objectif de cette série d’articles est de parcourir les astuces que nous avons mise en place lors de ce changement.

Cet article est le premier article d’une série de 4 articles sur les changements d’habitudes à avoir quand on build dans une image Docker :

  • #1 – Quelques changements dans nos habitudes
  • #2 – Comment tirer parti du cache docker sur un projet Maven
  • #3 – Comment tirer parti du cache docker sur un projet Angular
  • #4 – Et les descripteurs Google Cloud Build dans tout ça ?

Le contexte : qu’allons-nous construire ?

L’application dont nous parlons ici est une application disposant :

  • D’un frontend en Angular

    Artefact produit ici : un zip contenant l’ensemble des assets du front en mode prod (concaténé/minifié/rev-vé)
  • D’un backend composé de plusieurs applications (modules) Java Sprint Boot.

    Le cycle de vie de ces applications Java est le même : si aucun changement n’est appliqué à un module, il sera tout de même releasé.
    C’est un choix discutable, mais que nous avons pris pour nous épargner une trop grande complexité lors de chaque release.

    Artefacts produits ici : plusieurs fichiers war

Quelques shifts dans les habitudes/bonnes pratiques de build

Pendant longtemps, j’ai eu l’habitude de releaser mes projets Java avec Maven et le maven-release-plugin.
Par extension, j’avais pris l’habitude de porter ce workflow de release à mon front via un ensemble de scripts bash :

  • je “fige” la version dans mon descripteur, je commit, je tagge
  • je build mon artefact
  • … puis je re-snapshot la version dans mon descripteur, je commit, je push

Cette habitude est contre-productive dans un contexte de multi-stage build Docker où il est possible de gagner un temps conséquent de build grace au cache Docker (pour de plus amples détails, je vous invite à lire ce très bon blog post sur les bonnes pratiques dans les Dockerfile).

Une release maven/npm modifiant systématiquement le descripteur de dépendances (pom.xml ou package.json) vous ne bénéficiez donc pas du cache Docker pendant le build.
=> À chaque release, le build docker re-téléchargera l’ensemble des dépendances ce qui n’est bon ni pour notre portefeuille, ni pour notre efficacité, et encore moins pour la planète !

C’est donc le premier shift que nous avons eu sur ce projet : la version qu’on est en train de builder est dorénavant injectée dans nos descripteurs au moment du build, plutôt que commitée en amont dans les sources de l’application.

Cela ne vous est jamais arrivé de faire une Release Candidate à un moment, puis de vouloir promouvoir exactement la même photo de votre RC car elle avait passé la barrière des tests d’acceptance ?

Dans notre cas, nous n’avons plus de “commits de release” : le tag de la RC et le tag de la release stabilisée sont sur le même commit : votre graphe Git n’en sera que plus heureux !

Concrètement : quels impacts sur mon pom.xml ?…

La solution à ces problèmes coté Maven, c’est de décrire la version sous forme de placeholder, qu’on vient valoriser/surcharger pendant le build docker qui génère notre artefact :

  1. Toutes les versions des artefacts sont “variabilisées” (une variable différente par artefact)
  2. Une valeur par défaut est positionnée dans chaque pom.
    Cette valeur par défaut est utile, notamment en DEV, lorsqu’aucun paramètre en -D n’est passé au build pour la valoriser.

    Lorsqu’on souhaite générer un artefact avec une version particulière, il suffit donc de faire un :
    mvn -Dcmbl-aggregator.version=1.0 clean package
  3. Cette première variable par défaut (spécifique à un artefact) peut elle-même être alimentée à partir d’une valeur par défaut plus générale.
    Par exemple une variable projet, pour centraliser les versions qui suivent un cycle de vie commun.
    Dans le cas de notre applicatif ici, nous avons un projet multi-module où tous les artefacts ont un cycle de release commun : toutes les versions sont valorisées à partir d’une seule et même propriété commune cmbl.version.

    Ce mécanisme permet :
    – De définir très facilement, via un -D, la version de tous mes artefacts :
     mvn -Dcmbl.version=1.0 clean package 
    => je peux packager la version 1.0 de tous mes artefacts avec 1 seul -D
    – Si, dans des cas très rares, je veux pouvoir faire une release d’un artefact séparément des autres, je peux le faire en faisant par exemple un mvn -Dcmbl-domain.version=1.0.1 clean package (depuis le module cmbl-domain)
  4. La version utilisée en dev est DEV-SNAPSHOT
    DEV : pour bien montrer qu’on est en dev, en ne faisant sciemment pas apparaître de numéro de version et ainsi en évitant la tentation de changer cette valeur lors d’une release.
    SNAPSHOT : pour éviter que Maven se repose sur des artefacts potentiellement installés dans son cache local

… et quels impacts coté Front ?

Les conseils préconisés ici serviront pour tous les fichiers package.json / package-lock.json / yarn-lock.json.

Si on souhaite conserver un comportement similaire coté front, les choses doivent se faire un peu plus manuellement : l’idée est de venir remplacer certaines valeurs de nos fichiers JSON pendant le build, de manière à y injecter nos numéros de versions :

  1. Le champ “version” doit renseigner une version 0.0.0
    Pourquoi 0.0.0 ?
    J’aurais été tenté de suivre les mêmes guidelines que pour la partie Maven (utiliser DEV-SNAPSHOT)…
    Le problème : si votre version n’est pas un numéro sémantique valide, alors la plupart des versions de npm n’installent pas vos dépendances lors d’un npm ci !
    C’était le cas avec la version 10 de node qu’on utilise sur notre projet. Le pire étant que ça ne plantait absolument pas !… Juste un warning perdu dans les logs alors qu’aucune dépendance n’était installée !
  2. L’interpolation lors du build se fait avec une simple commande node (exemple avec le package.json, mais cela fonctionne de la même manière avec tous les descripteurs JSON…) :
    node -p -e "const p = require('./package.json'); p.version = '$VERSION'; JSON.stringify(p)" > package.new.json
    mv package.new.json package.json # obligation de le faire en deux fois, pour ne pas scier la branche sur laquelle on est assis pendant la commande ci-dessus

Ces étapes sur la partie front ne sont pas primordiales du moment que votre projet front n’est référencé via un système de gestion de dépendances (on pourrait imaginer ne jamais altérer le package.json lors d’une release, et tout le temps utiliser le même numéro de version … mais cela me paraît plus homogène avec la partie back de gérer une version dans le descripteur front)

Pour récapituler

Le cache Docker nous impose certaines contraintes si on souhaite tirer un maximum parti de son cache : l’habitude que nous avions d’altérer en amont les descripteurs contenant (aussi) les dépendances est antagoniste avec le mécanisme de cache docker.
Il est donc encouragé d’injecter les versions applicatives durant le build docker et non en-dehors.

Cela se traduit par une variabilisation des artefacts Maven qu’on build, ainsi que par une altération de nos descripteurs npm pendant le build coté front.

La ligne de commande node nous permet de très facilement altérer nos fichiers JSON en exécutant un script node dédié à cette problématique. C’est une astuce qui peut servir dans bien d’autres cas que la release d’artefacts, gardez-la en tête :-).

Cet article a été publié dans 4SH Develop.

%d blogueurs aiment cette page :