Loadbalacing

Pipelines dynamiques pour Gitlab CI, une nouvelle ère

Public cible : développeurs , opérateurs CI/CD, Jenkins afficionados

Par Max Gautier, Consultant Cloud & DevOps @ ObjectifLibre

Le Moyen Age

Les pipelines Gitlab CI sont simples, faciles d’utilisation, bien intégrées avec l’UI Gitlab, et il est possible de les faire s’exécuter sur un cluster Kubernetes. Que demander de plus ? Eh bien, une chose : des pipelines dynamiques. Le cas d’utilisation est le suivant : nous avons besoin d’exécuter plusieurs jobs similaires (par exemple, un ensemble de tests). Cela nous oblige à écrire une entrée par job dans notre .gitlab-ci.yml ! Ce qui implique beaucoup de copier-coller… On peut utiliser des alias yaml, mais cela nous laisse toujours beaucoup de boilerplate.

...

test_template: &test
  script: echo "Testing $SOMETHING"
  stage: test

test_A:
  variables:
   SOMETHING: A
  <<: *test

test_B:
  variables:
   SOMETHING: B
  <<: *test

test_C:
  variables:
   SOMETHING: C
  <<: *test

test_D:
  variables:
   SOMETHING: D
  <<: *test

...

Vous imaginez la suite.

Ce n’est pas très pratique, mais cela reste cependant faisable, soit en ressortant effectivement à du copier-coller, ou en abusant de la directive include, qui nous permet d’importer un fichier depuis une source externe.

En revanche, on ne peut pas définir de jobs dynamiquement1 (c’est-à-dire dépendant du contexte d’exécution de notre pipeline). Par exemple, si nous avons un répertoire tests, dans lequel sont tous nos tests (sous forme de scripts shell) :

tests/
├── test_A.sh
├── test_B.sh
├── test_C.sh
├── test_D.sh
├── test_E.sh
├── test_F.sh
├── test_G.sh
├── test_H.sh
├── test_I.sh
├── test_J.sh
├── test_K.sh
├── test_L.sh
├── test_M.sh
├── test_N.sh
├── test_O.sh
├── test_P.sh
├── test_Q.sh
├── test_R.sh
├── test_S.sh
├── test_T.sh
├── test_U.sh
├── test_V.sh
├── test_W.sh
├── test_X.sh
├── test_Y.sh
└── test_Z.sh

Nous souhaitons lancer chacun des ces tests dans son propre job2, Nous pourrions réutiliser notre approche précédente :

...

test_template: &test
script: tests/test_$SOMETHING
stage: test

test_A:
variables:
SOMETHING: A
<<: *test

test_B:
variables:
SOMETHING: B
<<: *test

...
# and so on

Outre la répétition, il y a deux problèmes avec cette façon de faire : – la répétition. A chaque fois que nous devons ajouter (ou supprimer un test), il faut également modifier le fichier .gitlab-ci.yml. – Les tests doivent être connus au moment de l’écriture du code. Par exemple, on ne pourrait pas utiliser une base de données de vulnérabilités récupérés lors de l’exécution pour et avoir un job par scan de vulnérabilité.

Il y a donc certaines limitations à ce modèle

Un Nouvel Espoir

Dans sa release 12.7, Gitlab a annoncé les pipelines parent-enfant.

job_1:
trigger:
include:
   - local: path/to/pipeline.yml

Ce job créé une nouvelle pipeline dans le même projet, qui est définie dans path/to/pipeline.yml. L’include utilise la syntaxe mentionnée précédemment.

Par défaut, Gitlab considère ce job réussi dès que la pipeline a été déclenché. Pour changer cela, il faut ajouter strategy: depend dans notre section trigger. Cela permet de lier le statut du job à celui de la pipeline (si elle est en erreur, le job également).

Cette feature seule ne nous permet pas d’avoir des pipelines véritablement dynamiques. Mais la releas 12.9 de Gitlab permet au jobs trigger d’utiliser un nouveau type d’include : artifact. Celui-ci permet d’inclure un fichier stocké dans un artifact, qui a été généré par un job précédent.

Ce qui correspond exactement à notre besoin

Tentons d’appliquer cela à notre exemple. Tout d’abord, créons un template pour notre pipeline. Nous utiliserons un script shell par simplicité, mais notre générateur pourrait être n’importe quoi (par exemple, utilisez des templates Jinja) :

#!/bin/sh

for test in tests/*
do 
    cat <<EOF
job_${test##*/}:
  stage: test
  script: ./$test

EOF
done

Dans notre .gitlab-ci.yml, nous créons le générateur :

generator:
  stage: generate
  image: ubuntu
  script: 
   - ./generate-pipeline.sh > generated-pipeline.yml
  artifacts:
    paths:
      - generated-pipeline.yml

Et enfin, nous incluons le yaml généré pour déclencher une pipeline enfant :

generator:
  stage: generate
  image: ubuntu
  script: 
   - ./generate-pipeline.sh > generated-pipeline.yml
  artifacts:
    paths:
      - generated-pipeline.yml

Le résultat :

Gitlab UI avec représentation des différents jobs générés

Nous pouvons donc maintenant générer dynamiquement nos pipelines Gitlab CI ; ce qui veut dire que nous se sommes plus restreint par l’expressivité de la syntaxe .gitlab.ci.yml.

Bien entendu, utilisez cette méthode de génération est plus complexe qu’une simple pipeline. Cependant, si notre processus de de CI est complexe, cette méthode, si elle ne fait pas disparaître cette complexité, nous donne au moins le moyen de la gérer.


  1. Bien entendu, il est possible de générer un fichier yaml approprié en utilisant un include remote et en pointant celui-ci sur un serveur web qui peut éventuellement accepter des paramètres. Mais cela ajoute des éléments dans notre architecture et n’a pas une très bonne scalabilité.↩︎
  2. Dans le cas où la séparation ou l’opération en parallèle n’importe pas, on pourrait bien entendu faire une simple boucle for test in tests/*; do ./$test; done↩︎