Guillaume Fenollar DevOps et SysAdmin Freelance

Guillaume FENOLLAR

Ingénieur Linux/DevOps Indépendant

− Montpellier −

Gitlab CI: templates, reference... comment appliquer les principes DRY

Un de mes principes favoris en programmation, qui part des fois jusqu'à l'obsession saine (du moins j'ose espérer), c'est le DRY. Il faut parfois prendre du recul et se demander si dans un cas précis le prix du DRY n'est pas trop cher à payer (comprendre : complexifie trop largement un développement) et s'adapter. C'est ça aussi être développeur après tout, s'adapter au contexte et aux besoins au delà de ses propres réflexes.

Dans le cas de Gitlab CI, la brique d’intégration continue de Gitlab, il est tout à fait possible d’appliquer ces principes-là, mais certaines solutions sont assez mal documentées, d’où mon idée de créer ce billet de blog sur ce sujet.

Ne perdons pas plus de temps, allons droit au but et listons les différents mécanismes DRY (ne vous répétez pas) de Gitlab et la façon de les utiliser.

Petit rappels de vigueur

Pour ceux qui auraient passé quelque temps dans une grotte:

  • Le fichier .gitlab-ci.yaml (a la racine d’une projet gitlab) est le point de départ de tout pipeline Gitlab.
  • Il contient des jobs (actions à effectuer) et des stages (groupes de jobs).
  • Par défaut, trois stages existent implicitement, dans l’ordre: build, test et deploy. Il est possible de les écraser avec une directive “stages”.
  • Tout job dont le nom commence par un point sera ignoré. Ça nous servira par contre pour faire des jobs de template.

Le-mot clé extends

Extends est probablement la directive la plus connue. Elle permet de spécifier un job parent à hériter. Le job courant prendra alors toutes les directives du job parent spécifié (qui devient un template), et pourra alors les écraser selon les besoins.

.create_container:
  image: docker:20.10
  stage: build
  variables:
    TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  script:
    - docker build --pull --tag ${TAG} .

create_container_myapp:
  extends: .create_container

create_container_myotherapp:
  extends: .create_container
  variables:
    TAG: my-custom-tag-name

C’est donc une des armes favorites pour appliquer la loi du DRY en ce bas-monde, vous l’aurez compris. L’intérêt étant bien évidemment d’avoir plus de jobs enfants que parents sinon ça n’a pas tellement d’intérêt :D

Le mot clé !reference

Derrière ce nom un peu étrange se cache une feature extrêmement pratique, permettant de piocher à loisir dans certaines directives déjà contenues dans d’autres jobs, que ce soit des jobs de templates ou non.

.docker_registry_login:
  script:
    - echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY

.create_container:
  variables:
    TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  script:
    - docker build --pull --tag ${TAG} .
    - !reference[.docker_registry_login, script]
    - docker push ${TAG}

Nous faisons ici un usage intensif des variables Gitlab. Voici un lien vers la doc de ces variables définies par défaut, sachant que des variables peuvent être rajoutées niveau projet, niveau .gitlab-ci.yaml, globalement, etc…

“!reference” peut s’apparenter un peu aux ancres (anchor) en YAML, mais est plus flexible car permet de “cherry-pick” l’élément d’une configuration qui nous intéresse.

Enfin, “!reference” est documenté ici

Le mot-clé global include

Comme vous pouvez l’imaginer, include permet d’inclure des fichiers de configuration CI dans le fichier existant. Mais ce qui m’intéresse le plus personnellement, c’est d’inclure des templates depuis un autre projet Gitlab, ce qui me permet d’avoir un projet central dont vont hériter tous mes jobs d’applications. DRY encore une fois. :)

# À spécifier tout en haut de votre .gitlab-ci.yaml
include:
- project: 'mygroup/ci-templates'
  file:
  - 'common.yml'
  - 'create_container.yml'
  - 'deployment.yml'

Le mot-clé include comme son nom l’indique inclut totalement les fichiers mentionnés dans le courant. Si nous avez eu le reflexe de n’écrire que des jobs de template dont les noms commencent par des points, alors il ne se passera rien de particulier, il nous restera à piocher dans les jobs de template à dispo grâce aux mots-clé déjà décrit ci-dessus. Ou alors vous pouvez appeler un fichier contenant déjà des “vrais” jobs et ceux-ci seront automatiquement exécutés dans le projet courant, ce qui évite encore une fois la duplication du code.

Dans l’exemple ci-dessus, j’ai donc un fichier include qui s’occupe de toute la dockerization, un autre pour la partie deploiement, et un autre pour ce qui est commun entre plusieurs jobs et réutilisé régulièrement. À vous de choisir l’arborescence qui vous va le mieux.

Comment gérer des variables dépendamment du contexte ?

Imaginons que j’ai maintenant un ensemble de jobs bien répartis qui ne se répètent pas, mais du coup le même bloc de code pour mes branches de développement et celle de production. Seulement, les besoins en variables sont différents et nous permettront de spécifier des comportements spécifiques. Pour ce besoin particulier, nous pouvons utiliser le mot clé rules, documenté ici.

.create_container_nodejs:
  extends: .create_container
  rules:
    - if: $CI_COMMIT_REF_NAME == "master"
      exists:
        - Dockerfile
      variables:
        NODE_ENV: production
        REF: latest
    - if: $CI_COMMIT_REF_NAME != "master"
      exists:
        - Dockerfile
      variables:
        NODE_ENV: develop
        REF: $CI_COMMIT_REF_SLUG

Ici, nous spécifions deux règles différentes. Dans la première, nous spécifions notre tag d’image à latest et une variable supplémentaire dans le cas où nous avons exécuter notre pipeline sur la branche “master”. Dans le cas contraire, nous considérons être sur une branche de developpement, et nous agissons différemment.

Enfin, pour vous féliciter d’être arrivé au bout de ce billet, un petit bonus exterieur au sujet mais permettant de simplement éviter la construction de pipelines dans le cas où un commit contient le terme “nobuild”:

workflow:
  rules:
    - if: $CI_COMMIT_TITLE =~ /nobuild/
      when: never
    - if: $CI

Ne pas oublier le dernier “if” qui semble perdu, il est nécessaire pour que le workflow passe aux jobs suivants et ne reste pas coincé sur la première comparaison infructueuse.

J’espère que ce petit tour d’horizon de quelques techniques DRY vous aura été utile !