Lors de mon passage sur plusieurs projets, j'ai souvent pu faire le constat suivant que je résumerai par cette mise en situation :
- GitLab CI/CD a été mis en place par un membre de l'équipe de développement qu'on nommera arbitrairement Michel par simplicité pour la suite
- L'équipe de développement utilise GitLab CI/CD au quotidien, notamment pour les merge requests et les déploiements
- Soudain, l'intégration/déploiement continu ne fonctionne plus
- Michel est en vacances/sous l'eau/en pause café pour une durée indéterminée
- Le reste de l'équipe de développement ne sait pas du tout comment corriger le problème
Comme vous pouvez vous en douter, cette situation n'est clairement pas enviable que ce soit pour Michel ou le reste de l'équipe de développement. Elle met en évidence un manque de maîtrise de GitLab CI/CD au-délà de la simple utilisation des processus déjà mis en place.
L'idée de cet article est donc de tenter de clarifier le fonctionnement de GitLab CI/CD en présentant ses grands principes et plus précisémment comment définir/modifier un pipeline.
Rappel : GitLab CI/CD, c'est quoi ?
C'est une des fonctionnalités de la suite logicielle GitLab qui permet de rapidement automatiser l'intégration et même le déploiement d'un projet présent sur un dépôt git.
Cette fonctionnalité se repose sur trois notions clés :
Pipeline
Un pipeline est constitué de différentes étapes séquentielles nommées <span class="css-span">stage</span> et chaque stage pourra contenir N tâches que l'on appelera <span class="css-span">job</span> dans le contexte GitLab, qui pourront s'exécuter en parallèle au sein d'un stage.
Dans l'exemple ci-dessus, nous avons 2 stages:
- <span class="css-span">test</span>, avec un unique job <span class="css-span">test</span>
- <span class="css-span">build</span>, avec deux jobs <span class="css-span">buildA</span> et <span class="css-span">buildB</span>.
L'ordre d'exécution de ces jobs sera donc le suivant :
- <span class="css-span">test</span>
- <span class="css-span">buildA</span> en parallèle de <span class="css-span">buildB</span>
Job
Un job consiste à réaliser une ou plusieurs opérations avec un périmètre limité sur les sources d'un projet. Il est systématiquement rattaché à un stage.
Voici une liste non exhaustive d'actions pouvant être réalisées par un job :
- tester le code
- construire un livrable
- déployer le projet
Runner
Un job s'exécute via un <span class="css-span">GitLab Runner</span>, qui est une application indépendante de GitLab.
Un runner va déléguer l'exécution d'un job à un exécuteur.
Et là vous me dites : "GitLab, Gitlab Runner et exécuteur, c'est quoi la différence entre tout ça ?" et bien un schéma (Mermaidjs) vaut mieux qu'un long discours :
Cet exécuteur peut être de plusieurs types :
- SSH
- Shell
- Parallels
- VirtualBox
- Docker
- Docker Machine (auto-scaling)
- Kubernetes
Le type d'exécuteur fera varier le contexte d'exécution du job. Pour la suite de l'article, nous nous concentrerons sur le cas d'un exécuteur Docker.
Il est possible et conseillé d'avoir plusieurs runners pour palier une charge importante ou une indisponibilité potentielle.
.gitlab-ci.yml
C'est le fichier qui va contenir la description du pipeline au sein de notre dépôt git.
Il est au format YAML, ce qui permet une lecture et une modification simplifiée.
Le fait que ce fichier soit versionné au sein du dépôt git apporte les avantages suivants :
- traçabilité des modifications
- possibilité de revenir à des situations antérieures
- maintenance facilitée car évolution en même temps que le code
Syntaxe minimaliste
Pour reproduire l'exemple du pipeline précédent, ces quelques lignes suffisent :
<pre><code>stages :
- test
- build
test :
stage : test
script : echo "Tests"
buildA :
stage : build
script : echo "Build A"
buildB :
stage : build
script :
- echo "Build B"
- echo "Build B en cours"
</code></pre>
Il faut donc définir à minima, au sein du fichier, les élément suivants :
- un stage, dans la partie <span class="css-span">stages</span>
- un job en lui indiquant un nom (ex: <span class="css-span">buildA</span>), et les attributs <span class="css-span">stage</span> et <span class="css-span">script</span>
La propriété <span class="css-span">script</span> correspond au script qui sera exécuté lors de l'exécution du job. Il est possible de mettre plusieurs instructions successives car <span class="css-span">script</span> accepte soit une chaîne de caractères (cf <span class="css-span">buildA</span>) soit un tableau de chaînes de caractères (cf <span class="css-span">buildB</span>).
L'exhaustivité de la syntaxe permise par ce fichier est décrite dans la documentation officielle.
Bénéficier d'un contexte grâce à Docker
Il existe plusieurs types de runners mais une des pratiques les plus communes est d'exécuter les jobs de nos pipelines sur un runner Docker. Cette solution permet de lancer un job dans un conteneur Docker qui aura déjà un certain contexte.
Concrètement, cela se fait tout simplement de la manière suivante :
<pre><code>stages :
- build
buildA :
# Image avec un JDK 17
image : openjdk:17-jdk-alpine
stage : build
# Possible d'utiliser Gradle (via Gradle Wrapper ) car le JDK est disponible
script : ./gradlew build
</code></pre>
Variables
Il est possible de définir des variables que ce soit au niveau du pipeline, c-à-d accessible pour tous les jobs du pipeline ou pour un job bien spécifique.
<pre><code>stages :
- test
variables :
GLOBAL : "global"
test :
stage : test
variables :
SPECIAL : "special"
script :
- echo "I'm $SPECIAL !"
- echo "I'm $testing $CI_COMMIT_BRANCH"
- echo "I'm $GLOBAL !"
</code></pre>
En complément, GitLab CI/CD propose des variables prédéfinies.
L'ensemble des variables, définies par nos propres soins et prédéfinies par GitLab, est accessible lors de l'exécution du job, ce qui permet une grande flexibilité d'usage comme accéder au nom de la branche sur laquelle le job est exécuté ou plus simplement d'afficher un message variabilisé comme ci-dessus.
Il est également possible de définir des variables au niveau du projet et/ou du groupe dans GitLab.
Conditions
Au quotidien, il est bien souvent nécessaire de conditionner l'exécution de certains jobs. Ceci est possible grâce au mot-clé <span class="css-span">rules</span> associé aux conditions <span class="css-span">if</span>, <span class="css-span">exists</span>, <span class="css-span">changes</span>.
<pre><code>stages :
- test
test :
image : openjdk:17-jdk-alpine
stage : test
script : ./gradlew test
rules :
- if : $CI_PIPELINE_SOURCE == "merge_request_event"
changes :
- src/**/*
exists :
- build.gradle
</code></pre>
Dans l'exemple ci-dessus, le job <span class="css-span">test</span> sera exécuté uniquement sur une merge request et si un fichier a été modifié dans <span class="css-span">src</span> ou un de ses sous-répertoires et si un fichier <span class="css-span">build.gradle</span> est bien présent.
Il est bien sûr possible d'utiliser des règles moins complexes. L'exemple ci-dessus permet surtout de présenter la granularité des conditions possibles.
Réutilisation
Plusieurs jobs peuvent bénéficier de paramètres identiques. Par conséquent, il peut être avantageux de factoriser ces comportements. GitLab CI/CD permet, grâce à la syntaxe <span class="css-span">extends</span>, d'hériter des informations d'un autre job.
<pre><code>stages :
- build
.build :
stage : build
variables :
VERSION : 1.0
script :
- echo "$BUILD_NAME"
- echo "$VERSION"
buildA :
extends : .build
variables :
BUILD_NAME : "Build A"
buildB :
extends : .build
variables :
BUILD_NAME : "Build B"
VERSION : 1.1
</code></pre>
Lorsque GitLab va analyser ce fichier <span class="css-span">.gitlab-ci.yml</span>, il ne va pas considérer le job <span class="css-span">.build</span> comme effectif car il est préfixé par un . .
Ensuite, il va fusionner le contenu du job <span class="css-span">.build</span> avec celui des jobs <span class="css-span">buildA</span> et <span class="css-span">buildB</span>. Lors de cette fusion, les informations les plus spécifiques à un job seront conservées en cas de conflit. Exemple, <span class="css-span">buildB</span> aura le dernier mot sur <span class="css-span">.build</span> concernant la variable <span class="css-span">VERSION</span>.
Au final, après ses différentes opérations, GitLab analyse l'équivalent suivant :
<pre><code>stages :
- build
buildA :
stage : build
variables :
BUILD_NAME : "Build A"
VERSION : 1.0
script :
- echo "$BUILD_NAME"
- echo "$VERSION"
buildB :
stage : build
variables :
BUILD_NAME : "Build B"
VERSION : 1.1
script :
- echo "$BUILD_NAME"
- echo "$VERSION"
</code></pre>
Il est également possible d'étendre de plusieurs jobs. Pour cela, le mot-clé <span class="css-span">extends</span> accepte une chaîne de caractères ou un tableau de chaînes de caractères. Lors de la fusion, ce sera le dernier job étendu qui aura la priorité parmi les jobs étendus.
Par conséquent le fichier suivant :
<pre><code>stages :
-build
.java :
image : openjdk:17-jdk-alpine
script :
- java -version
.build :
stage : build
script :
- ./gradlew build
buildA :
extends :
- .java
- .build
</code></pre>
sera interprété comme ci-dessous:
<pre><code>stages :
- build
buildA :
image : openjdk:17-jdk-alpine
stage : build
script :
- ./gradlew build
</code></pre>
Vous pouvez remarquer que c'est le script du job <span class="css-span">.build</span> qui a été appliqué et non celui de <span class="css-span">.java</span> car <span class="css-span">.build</span> est renseigné en second.
Depuis la version 13.9 de GitLab, il est possible de visualiser le fichier effectif (après fusion) depuis l'interface de GitLab via l'entrée du menu CI/CD->Editor puis l'onglet View merged YAML.
Découpage et inclusion
Le fait de pouvoir extraire certaines configurations communes au sein de jobs dédiés permet de mieux séparer les responsabilités. Pour aller plus loin, il est possible d'extraire des éléments du fichier <span class="css-span">.gitlab-ci.yml</span> dans d'autres fichiers YAML et les intégrer avec le mot-clé <span class="css-span">include</span>.
<pre><code># Fichier .build.yml
.build
stage : build
script : echo "$BUILD_NAME"
# Fichier .gitlab-ci.yml
include :
- local : '.build.yml'
stages :
- build
buildA :
extends : .build
variables :
BUILD_NAME : "Build A"
</code></pre>
La logique de fusion évoquée précédemment est également appliquée, avec la subtilité suivante : ce qui sera déclaré dans le fichier <span class="css-span">.gitlab-ci.yml </span>aura la priorité en cas de fusion. Cela permet si besoin de surcharger des comportements présents dans le fichier inclus.
Il est également possible d'inclure des fichiers d'un autre dépôt (via <span class="css-span">include:file</span> et <span class="css-span">include:project</span>), ce qui offre un tout nouvel éventail de possibilités pour des dépôts similaires qui auraient intérêt à bénéficier d'éléments communs au niveau CI/CD (micro-services par exemple).
Conclusion
Nous avons pu parcourir ensemble les grands principes de GitLab CI/CD, des pipelines aux runners tout en explorant plus en détails la syntaxe du fichier <span class="css-span">.gitlab-ci.yml</span>.
Grâce à cette syntaxe, nous avons pu :
- profiter d'un contexte d'une image Docker
- appliquer des conditions à nos jobs
- factoriser et réutiliser des jobs
- découper notre fichier <span class="css-span">.gitlab-ci.yml</span>
Florian a le goût du travail bien fait et vite fait. Évidemment, ça nécessite d'être rigoureux, ce n'est pas pour rien que Java est sa techno préférée. Et si vous lui demandez de choisir un langage front, ça pourrait bien être TypeScript "qui apporte un peu de rigueur dans le monde Javascript". Son outil de prédilection : GitLab CI/CD !
Parce que produire des travaux de qualité, c'est bien, mais produire des travaux avec une forte valeur ajoutée pour le client, c'est mieux !
Pilier de l'équipe de soccer de l'agence rennaise, Florian est également un grand amateur de sensations fortes. Son dernier projet fou en tête ? Un saut en parachute !