L’objectif de ce billet est de présenter comment utiliser le système de cache de GitLab CI/CD et les artifacts pour diminuer le temps de construction d’une application Maven.

Dan un premier temps, nous allons utiliser le cache pour éviter de re-télécharger l’ensemble des dépendances du projet Maven. Ensuite, les artifacts, soit les résultats d’une opération, seront mis en oeuvre afin de ne pas recompiler les classes entre les différents “stages” d’un même “pipeline”.

GitLab CI/CD: pipeline, stage et job

L’outil GitLab CI/CD ( Continuous Integration / Continuous Deployment) se configure à partir d’un fichier manifeste .gitlab-ci.yml:

  • il permet de définir le pipeline, soit l’ensemble des opérations à lancer automatiquement lorsque une modification est effectuée dans les sources du projet.
  • Un pipeline est découpé en stages qui sont lancés séquentiellement. Par défaut, 3 stages sont utilisés: build -> test -> deploy: le stage test sera lancé à la fin du stage précédent, soit build, si ce dernier est en “réussite” ( il est toutefois possible de modifier ce comportement).
  • Finalement, un stage est composé de jobs qui sont les plus petites unités de travail. Un job appartient à un seul stage et les jobs d’un même stage sont lancés en parallèle.

En image:

Rapport des tests unitaires

Pour plus de détails, vous retrouverez toute la documentation sur le site officiel de GitLab.

Le Manifeste de départ

Voici un exemple de fichier .gitlab-ci.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
image: maven:latest

variables:
  MAVEN_CLI_OPTS: "--batch-mode"

build_job:
  stage: build
  script:
    - mvn $MAVEN_CLI_OPTS compile

test_job:
  stage: test
  script:
    - mvn $MAVEN_CLI_OPTS test

L’image Docker à utiliser est définie au début du fichier. Ensuite, la variable d’environnement MAVEN_CLI_OPTS est définie. Finalement, 2 jobs sont déclarés

  • build_job : la compilation du projet associé au stage build
  • test_job : lancement des tests associé au stage test

Pour simplifier l’exemple, nous n’avons pas défini de job attaché au stage deploy. En général, les noms des jobs ne contiennent pas le préfixe _job qui a été ajouté pour bien séparer les notions de job et stage.

Cette première version n’utilise pas les fonctionnalités de cache: à chaque lancement du pipeline, les dépendances sont toutes téléchargées et les classes sont recompilées par le job test_job.

Utilisation du cache pour les dépendances

Pour mettre en place ce cache, il faut activer le cache de GitLab CI et configurer Maven pour stocker le repository à une emplacement connu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
image: maven:latest

variables:
  MAVEN_CLI_OPTS: "--batch-mode"
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

cache:
  paths:
    - .m2/repository/

build_job:
  stage: build
  script:
    - mvn $MAVEN_CLI_OPTS compile

test_job:
  stage: test
  script:
    - mvn $MAVEN_CLI_OPTS test
    

A la ligne 5, on indique le chemin du repository à Maven via une variable d’environnement. Ensuite, on demande à GitLab CI de cacher ce répertoire (lignes 7 à 9). Cette première optimisation est simple.

Par contre, en parcourant les logs, nous constatons que le projet est recompilé par le job de test_job: par défaut, les stages ne partagent rien. La prochaine étape est d’éviter de recompiler les fichiers java en partageant les résultats (artifacts) entre les stages.

Utiliser le cache pour partager les artifacts: une mauvaise idée

Pour partager ces artifacts, on pourrait envisager d’utiliser la fonctionnalité de cache. Hors, la documentation de Gitlab concernant le cache décrit bien la différence entre la fonctionnalité de cache et les artifacts:

  • le cache doit être utilisé pour cacher les dépendances du projet
  • les artifacts sont utilisés pour partager des résultats d’opération ( .class, ….) entre les stages.

Pour illustrer l’importance de cette distinction, supposons que nous utilisons le cache pour partager des artifacts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
image: maven:latest

variables:
  MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

cache:
  paths:
    - .m2/repository/
    # Pas une bonne idée...
    - target/

build_job:
...

Une conséquence directe: le dossier target étant en cache, il sera restauré à chaque lancement du pipeline. Si une ressource est supprimée du référentiel (Git), elle sera quand même rétablie dans le dossier target par le cache et donc livrée avec les artifacts suivants… Si c’est un fichier de configuration, cela peut occasionner des séances de debug fastidieuses…

Activation des artifacts

Pour partager l’artifact, le contenu du dossier target, voici les modifications à apporter:

12
13
14
15
16
17
18
19
20
...
build_job:
  stage: build
  script:
    - mvn $MAVEN_CLI_OPTS compile
  artifacts:
    expire_in: 10 min
    paths: 
      - target/     

On demande de considérer le dossier target comme un artifact. On définit également une durée de rétention de 10 minutes ce qui sera suffisant pour partager les artifacts entre les stages. Après 10 min, ces résultats intermédiaires ne seront plus disponibles.

Cette configuration est tout à fait adaptée dans le cas d’un projet Maven avec un seul module. Par contre, dans le cas de multi-modules la configuration pourrait être plus verbeuse:

18
19
20
21
22
23
    ...
    paths: 
      - target/
      - moduleA/target/
      - moduleB/target/
      - etc...

Pour éviter cela, il est possible d’utiliser des wildcards pour ces sous-dossiers (attention à la notation):

18
19
20
21
22
    ...
    paths:
      - target/ 
      # il faut utiliser des " et pas de / à la fin...
      - "*/target"

Les classes sont toujours recompilées !

Malgré cette nouvelle configuration, le résultat n’est pas concluant car les logs indiquent que les classes sont toujours recompilées:

[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ springboot-java-insecure ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 9 source files to /builds/capedev-labs/springboot-java-unsecure/target/classes

Une explication:

entre les 2 stages, une opération git force la mise à jour des dates de modification des fichiers java qui deviennent plus récents que les fichiers .class correspondants ( information trouvée ici). Une solution est d’actualiser la date de modification des fichiers .class avec un touch:

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
image: maven:latest

variables:
  MAVEN_CLI_OPTS: "--batch-mode"
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

cache:
  paths:
    - .m2/repository/

build_job:
  stage: build
  script:
    - mvn $MAVEN_CLI_OPTS compile
  artifacts:
    expire_in: 10 min
    paths: 
      - target/
      - "*/target"

test_job:
  stage: test
  script:
    - find . -name "*.class" -exec touch {} \+
    - mvn $MAVEN_CLI_OPTS test

La ligne 44 a été ajoutée: au début du job test_job, un script remet à jour les dates de modifications des fichiers .class. Suite à cette modification, les classes java ne sont plus recompilées:

[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ springboot-java-insecure ---
[INFO] Nothing to compile - all classes are up to date

Cette ligne sera à ajouter pour chaque job réutilisant les fichiers .class ( ce qui n’est pas une solution optimale…).

Conclusion

Le cache et la gestion des artifacts de GitLab CI/CD permettent de diminuer le temps de construction d’un projet Maven. Ces 2 fonctionnalités ont des cas d’usage différents à respecter.

Des liens vers la documentation: