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 !