GitLab CI : utiliser son propre Runner dédié à Maven avec des settings personnalisés

Contexte

GitLab est un serveur d’hébergement de code source (SCM) dédié à Git. GitLab peut être utilisé avec une instance publique sur Internet (https://gitlab.com/), de façon similaire à GitHub (https://github.com/), ou bien avec une instance hébergée chez soi ou dans une entreprise (on premise).

GitLab intègre une partie « Intégration Continue », connue sous le nom de GitLab CI. Cela permet de vérifier que le logiciel peut être construit correctement, éventuellement d’exécuter des tests unitaires, voire de publier le logiciel. Et bien d’autres choses encore.

La particularité de GitLab CI est de déléguer l’exécution du pipeline d’intégration continue, découpé en tâches (nommées jobs), à d’autres machines, appelées Runners. Ces runners peuvent être mis à disposition d’une instance GitLab via des shared runners, mais on peut également dédier des runners spécifiques à son projet ou son groupe de projets.

On peut ainsi exécuter des runners sur sa propre machine, en installant des GitLab Runners, que l’on soit sous Windows, Mac, Linux, ou même avec une infrastructure plus complexe comme Kubernetes.

Cas d’utilisation

L’objectif ici est de permettre de construire un projet Java dans GitLab CI, à l’aide de Maven.

Bien souvent en entreprise on va aussi utiliser notre propre dépôt (repository) de librairies Java compatible avec Maven notamment. Cela permet à la fois de servir de cache et de proxy pour tous les développeurs qui auraient besoin d’accéder à des librairies externes, disponibles sur Internet. Cela va également permettre d’héberger nos propres librairies ou artefacts, sur un serveur privé. Pour ce faire, on peut par exemple utiliser Sonatype Nexus. Il existe aussi d’autres solutions, comme Artifactory.

Pour faciliter les choses, on souhaite également utiliser Docker, une solution à base de containers.

L’enjeu sera essentiellement ici de pouvoir exécuter les tâches Maven dans GitLab CI, en configurant le settings.xml de Maven afin d’utiliser un repository Nexus interne.

L’exécution de Maven se fera dans chaque job à l’aide d’un executor défini dans le GitLab Runner et configuré pour utiliser Docker. Ainsi, chaque exécution de job consistera à instancier un container Docker et exécuter un script à l’intérieur. On tentera donc au maximum d’utiliser les images Maven officielles présentes sur Docker Hub pour cela. Un des avantages de Docker est notamment d’éviter l’installation de toute une série d’outils sur la machine hôte, comme un JDK et une version de Maven dans ce cas. Ces derniers sont simplement packagés dans l’image Docker correspondante, en isolation de la machine hôte.

Environnement

Dans cet article on va considérer que l’on travaille sur une machine Linux. C’est possible de le faire fonctionner sous Windows, mais cela rajoute quelques contraintes, et le cas de Linux est sûrement plus pertinent en production.

Installation du runner

Si ce n’est déjà fait, il faut installer le GitLab Runner qui fonctionnera comme un service.
Globalement on va suivre la procédure expliquée à cette page : https://docs.gitlab.com/runner/install/linux-manually.html. Pour Ubuntu / Debian on aura donc un package nommé gitlab-runner_x86_64.deb, à installer avec la commande dpkg.
Il est aussi possible de télécharger le binaire pour d’autres architectures comme expliqué dans le lien plus haut.

Pour vérifier que le service est actif, exécuter au choix :

  • sudo gitlab-runner status
  • service gitlab-runner status

Si le service n’est pas démarré, exécuter :

  • sudo gitlab-runner start

Configuration du runner

Pour configurer le runner, on va en général utiliser le fichier config.toml. Voir : https://docs.gitlab.com/runner/configuration/.
Par exemple le paramètre concurrent va définir le nombre de jobs exécutés simultanément.

Enregistrement des runners

Ensuite, un ou plusieurs runners peuvent être enregistrés. Dans notre cas, on utilisera les runners spécifiques à un projet. Il faudra au préalable aller dans le projet GitLab dans la section Settings > CI/CD et ouvrir Runners pour récupérer l’URL et le token.
Par exemple, on va enregistrer un runner spécifique aux jobs Maven.

sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --registration-token "PROJECT_REGISTRATION_TOKEN" \
  --executor "docker" \
  --docker-image maven:latest \
  --description "Runner Maven" \
  --tag-list "maven" \
  --run-untagged="false" \
  --locked="false" \
  --access-level="not_protected"

La valeur de registration-token doit être valorisée avec la valeur du token du projet.

On veut aussi restreindre les images Docker utilisables dans ce runner. On va donc rajouter

--allowed_images="maven:*"

pour n’utiliser que des images Docker Maven officielles.

Lister les runners enregistrés

sudo gitlab-runner list

La liste des runners configurés doit s’afficher.

Tester l’exécution du pipeline dans le runner

Normalement à ce stade le runner est enregistré et prêt à exécuter des jobs du projet. Si un fichier .gitlab-ci.yml a déjà été ajouté aux sources du projet, vous pouvez tester la bonne exécution du pipeline en rajoutant un commit dans le projet ou en forçant son exécution.

Comme on a associé un tag à notre runner, il ne faut pas oublier de le spécifier dans la déclaration du job, comme par exemple :

build:
  stage: build
  image: maven:3.6-jdk-8
  script:
    - mvn $MAVEN_CLI_OPTS verify
  tags:
    - maven

Problème de proxy ?

Bon si vous êtes derrière un proxy, il est probable de rencontrer des problèmes. Il faudra peut-être regarder ici : Running GitLab Runner behind a proxy.

Utilisation d’un registry Docker privé ?

Il est aussi possible que vous deviez passer par un registry Docker privé pour le téléchargement des images Docker. Dans ce cas GitLab Runner devra être configuré en conséquence, voir Using a private container registry. On pourra également restreindre l’origine des images autorisées (allowed_images) à un registry privé.

Voir les détails spécifiques à Docker dans Define an image from a private Container Registry et en particulier la définition de la variable DOCKER_AUTH_CONFIG (credentials + registry).

Concernant la configuration spécifique à Docker pour l’utilisation d’un registry privé en mode proxy, voir Registry as a pull through cache.

Voir aussi ces issues :

Etape suivante : utiliser un repository Maven privé

Ensuite nous souhaitons que notre job, exécuté dans un container Docker avec une image Maven utilise un repository Maven spécifique.

Avant tout, on va démarrer un serveur Sonatype Nexus pour se mettre en conditions réelles.

Avec Docker c’est assez simple :

docker run -d -p 8081:8081 --name nexus sonatype/nexus3

Une fois démarré, un serveur Nexus doit être accessible à http://localhost:8081.

Configuration des settings Maven

On va ensuite définir des settings personnalisés pour Maven avec un fichier custom-settings.xml comme suit :

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
	<localRepository/>
	<mirrors>
		<mirror>
			<id>private-nexus-repo</id>
			<name>My Nexus Maven Repository</name>
			<url>http://localhost:8081/repository/maven-public/</url>
			<mirrorOf>central</mirrorOf>
		</mirror>
	</mirrors>
</settings>

Ces settings doivent ensuite être passés au conteneur Docker au moyen d’un bind mount en utilisant les volumes Docker. Les volumes sont configurés au niveau du runner GitLab.

Dans la configuration config.toml, on ajoutera ou modifiera donc cette section :

  [runners.docker]
    ...
    volumes = ["/cache", "/host/path/custom-settings.xml:/root/.m2/settings.xml:ro"]

En pratique, on peut valider que les settings Maven sont corrects en exécutant simplement un container Docker comme ici :

docker run --rm -v $(pwd)/custom-settings.xml:/root/.m2/settings.xml:ro maven:3 mvn help:effective-settings

Au minimum le plugin help devrait être téléchargé via Nexus, ainsi que ses dépendances. Cela permet aussi de valider que le montage de volume du fichier settings.xml est correct et bien pris en compte dans le container. Un chemin absolu est nécessaire quand on passe par Docker CLI (contrairement à Docker Compose).
Le résultat de la commande précédente devrait ressembler à çà au final :

[INFO] 
Effective user-specific configuration settings:
<?xml version="1.0" encoding="UTF-8"?>
<!-- ====================================================================== -->
<!--                                                                        -->
<!-- Generated by Maven Help Plugin on 2021-03-02T18:12:40Z                 -->
<!-- See: http://maven.apache.org/plugins/maven-help-plugin/                -->
<!--                                                                        -->
<!-- ====================================================================== -->
<!-- ====================================================================== -->
<!--                                                                        -->
<!-- Effective Settings for 'root' on '19791eb218d0'                        -->
<!--                                                                        -->
<!-- ====================================================================== -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
  <localRepository>/root/.m2/repository</localRepository>
  <mirrors>
    <mirror>
      <mirrorOf>central</mirrorOf>
      <name>My Nexus Maven Repository</name>
      <url>http://172.17.0.1:8081/repository/maven-public/</url>
      <id>private-nexus-repo</id>
    </mirror>
  </mirrors>
  <pluginGroups>
    <pluginGroup>org.apache.maven.plugins</pluginGroup>
    <pluginGroup>org.codehaus.mojo</pluginGroup>
  </pluginGroups>
</settings>
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  59.360 s
[INFO] Finished at: 2021-03-02T18:12:40Z
[INFO] ------------------------------------------------------------------------

Accès au serveur Nexus à partir du conteneur Docker

Attention si le conteneur souhaite accéder à un service hébergé sur son hôte, il ne pourra pas résoudre localhost, et il faudra remplacer par exemple localhost par son ip résolue. Voir :

Tester avec cette commande par exemple :

ip addr show docker0

Résultat attendu

Normalement à ce point, tout devrait fonctionner. Dans le log du job Maven du pipeline on devrait avoir quelque chose comme çà :

Running with gitlab-runner 13.9.0 (2ebc4dc4)
  on Runner Maven XHUSESdz
Preparing the "docker" executor
00:06
Using Docker executor with image maven:3.6-jdk-8 ...
Pulling docker image maven:3.6-jdk-8 ...
Using docker image sha256:75cbe9c11be24bae1cb8064cbc33c26df79321b5047c30ff8e29abcd2e5289a9 for maven:3.6-jdk-8 with digest maven@sha256:d4a4bb5d83a95c012615c5294f665ee042e721298c941eea32db3ea0ca588e9b ...
Preparing environment
00:02
Running on runner-xhusesdz-project-23778885-concurrent-0 via guillaume-F3JP...
Getting source from Git repository
00:05
Fetching changes with git depth set to 50...
Reinitialized existing Git repository in /builds/ghusta/maven-git-flow-test/.git/
Checking out c170a61a as feature/gitlab-ci-tags-custom-runner...
Removing .m2/
Removing target/
Skipping Git submodules setup
Restoring cache
00:03
Checking cache for default...
No URL provided, cache will not be downloaded from shared cache server. Instead a local version of cache will be extracted. 
Successfully extracted cache
Executing "step_script" stage of the job script
00:10
Using docker image sha256:75cbe9c11be24bae1cb8064cbc33c26df79321b5047c30ff8e29abcd2e5289a9 for maven:3.6-jdk-8 with digest maven@sha256:d4a4bb5d83a95c012615c5294f665ee042e721298c941eea32db3ea0ca588e9b ...
$ mvn $MAVEN_CLI_OPTS verify
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /usr/share/maven
Java version: 1.8.0_282, vendor: Oracle Corporation, runtime: /usr/local/openjdk-8/jre
Default locale: en, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-66-generic", arch: "amd64", family: "unix"
3361 [INFO] Error stacktraces are turned on.
3477 [INFO] Scanning for projects...
...
7330 [INFO] ------------------------------------------------------------------------
7336 [INFO] BUILD SUCCESS
7337 [INFO] ------------------------------------------------------------------------
7340 [INFO] Total time:  3.910 s
7345 [INFO] Finished at: 2021-02-28T21:08:20Z
7348 [INFO] ------------------------------------------------------------------------
Saving cache for successful job
00:01
Creating cache default...
.m2/repository: found 977 matching files and directories 
Archive is up to date!                             
Created cache
Cleaning up file based variables
00:01
Job succeeded

Et voilà çà marche !

Et n’importe quelle image Docker Maven peut être configurée dans le fichier .gitlab-ci.yml, au choix du développeur. Comme par exemple :

  • maven
  • maven:3.6
  • maven:3-jdk-8
  • maven:3-openjdk-11
  • maven:3-openjdk-17-slim
  • etc…