Un repo pour les builder tous

Au fil de ce post, nous allons vous montrer comment est découpé notre repo, l’implémentation des child-pipelines de Gitlab à l’aide d’un simple script bash et d’un template de job et pour finir, quelques petites subtilités.

Un repo pour les builder tous

Aujourd’hui, je vous partage un choix et une exploration de Gitlab.

Nous avons pas mal de petits conteneurs très simples, avec des Dockerfile tout petits qui ne font qu’ajouter un package, ou configurer quelque chose.

On avait envie d’automatiser les builds de ces conteneurs, même si nous ne le faisons que très rarement.

La première intuition était de dire, on fait un repository par conteneur, et avec la CI de Gitlab, on fait le build, mais bon, un repo pour juste un Dockerfile… On préfère avoir un repo avec tous les conteneurs qui ne nécessitent pas ou très peu de code/modifications.

Je ne sais pas s’il y a une bonne pratique admise pour ça, je n’ai pas trouvé de consensus clair en cherchant rapidement, et nous sommes plusieurs à préférer faire le pull d’un seul repo quand il est question d’ajouter quelques lignes dans quelques fichiers.

Au fil de ce billet, je vais vous montrer comment est découpé notre repo, l’implémentation des child-pipelines de Gitlab à l’aide d’un simple script bash et d’un template de job. Pour finir, quelques petites subtilités.

Un repo et un sous-dossier par conteneur :

mon-beau-conteneur/
Dockerfile
Readme.md

un-autre/
Dockerfile
src/app.js

projet/conteneur1/
Dockerfile
projet/conteneur2/
Dockerfile

Jusque-là tout va bien ! Ensuite, ça se complique, comment faire pour faire le build ?

Solution de base, à chaque push de modifications, je refais tous les builds, j’ai donc un job par sous-répertoire.

Je pars du template de build docker :

docker-build:
  # Use the official docker image.
  image: docker:latest
  stage: build
  services:
    – docker:dind
  before_script:
    – docker login -u « $CI_REGISTRY_USER » -p « $CI_REGISTRY_PASSWORD » $CI_REGISTRY
  script:
  

    – docker build –pull t « $CI_REGISTRY_IMAGE:${tag} » mon-beau-conteneur/
    – docker push « $CI_REGISTRY_IMAGE:${tag} »


Fonctionnel mais pas fantastique, et surtout inutile.

La solution ce sont les parent-child pipelines :

Dans la CI parent, je lance un bash qui cherche quels sont les Dockerfiles modifiés, et qui génère un fichier avec l’ensemble des jobs pour build uniquement les concernés.

Le fichier .gitlab-ci.yml ressemble à ça :

stages:
– generate-build-jobs
– triggers

search-for-updates:
image: debian:bullseye-slim
stage: generate-build-jobs
before_script:
– chmod +x ./generate-jobs.sh
script: ./generate-jobs.sh
artifacts:
paths:
– « generated-jobs.yml »
rules:
– if: $CI_COMMIT_BRANCH
exists:
– « **/Dockerfile »

builds:
stage: triggers
trigger:
include:
– artifact: « generated-jobs.yml »
job: search-for-updates

Avec la règle, “if : changes”, le script se lance uniquement lorsqu’au moins un Dockerfile a changé.

J’aime bien debian, et j’ai besoin de bash, donc j’ai pris bulleye-slim comme image de base.

Le job lance le script, et lorsqu’il termine, il upload le fichier “generated-jobs.yml” en tant qu’“artifact”.

Il sera disponible pour le job “builds” qui grâce au mot-clef “trigger” lance tous les jobs contenus dans le fichier.

Voilà à quoi ressemble le script “generate_jobs.sh” :

#!/bin/bash
apt-get update
apt-get install -y git
# needed to do the diff between branches
git fetch –depth 50 origin main:main

if
] ; then

updatedFiles=$(git diff –name-only main…) # new branch
else
updatedFiles=$(git diff –name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA)
fi

for updatedFile in ${updatedFiles
}; do

echo « FILEPATH= »$updatedFile
if
] ; then # if I find a dockerfile

slashes=${updatedFile//
}

numberOfSlashes=$((${#slashes})) # to manage subdir
for ((depth=1;depth<=numberOfSlashes;depth++)); do currentDir=$(echo $updatedFile|cut -d/ -f$depth) if (( (depth > 1) && (depth <= numberOfSlashes) )); then name=$name »- » dirToBuild=$dirToBuild »/ » echo « name=> « $name
echo « dirSoBuild=> « $dirToBuild
fi
name=$name$currentDir
dirToBuild=$dirToBuild$currentDir
echo « name =>  » $name
echo « dirToBuild =>  » $dirToBuild
done
echo « name=> « $name
sed « s|==DIRTOBUILD==|${dirToBuild}|g » gitlab-ci-tpl.yml >> generated-jobs.yml
sed -i « s|==JOBNAME==|${name}|g » generated-jobs.yml
name= » »
dirToBuild= » »
fi
done

Le bash vérifie la liste des fichiers modifiés et pour chaque Dockerfile ajoute un job dans la child-pipeline : le fichier ”generated-jobs.yml”.

Pour ça, il fait juste un “sed“ pour le sous-répertoire à gérer. Il vérifie les fichiers modifiés entre les commit ou lorsque c’est une nouvelle branche, il compare avec la branche par défaut (variable disponible dans Gitlab ci $CI_DEFAULT_ BRANCH).

Mémo rapide

Le bash vérifie la liste des fichiers modifiés et pour chaque Dockerfile ajoute un job dans la child-pipeline : le fichier ”generated-jobs.yml”.

Pour ça, il fait juste un “sed“ pour le sous-répertoire à gérer.

Mémo rapide

Le bash vérifie la liste des fichiers modifiés et pour chaque Dockerfile ajoute un job dans la child-pipeline : le fichier ”generated-jobs.yml”.

Pour ça, il fait juste un “sed“ pour le sous-répertoire à gérer.

Un repo pour les builder tous

Pour finir mon template de job :

Pour finir, voici mon template de job, très simple et basé sur un template de base :

docker-build-generated-==JOBNAME==:
# Use the official docker image.
image: docker:latest
stage: build
services:
– docker:dind
before_script:
– export DOCKER_BUILDKIT=1
– export CI_REGISTRY_IMAGE= »registry _name/==JOBNAME== »
– docker login -u « $CI_REGISTRY_USER » -p « $CI_REGISTRY_PASSWORD » $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
– |
if
] ; then

tag= » »
echo « Running on default branch ‘$CI_DEFAULT_BRANCH’: tag = ‘master' »
else
tag= »$CI_COMMIT_REF_SLUG »
echo « Running on branch ‘$CI_COMMIT_BRANCH’: tag = $tag »
fi
– tag=$(date +’%Y%m%d%H%M%S’)$tag
– docker build –pull -t « $CI_REGISTRY_IMAGE:${tag} » ==DIRTOBUILD==
– docker push « $CI_REGISTRY_IMAGE:${tag} »
– echo ‘docker push « $CI_REGISTRY_IMAGE:${tag} »‘

Petites subtilités :

  • Il faut un nom de job unique
  • Il faut penser à mettre à jour le chemin de la registry
  • Il faut que les noms de répertoires soient simples et propres (pas d’espaces, caractères spéciaux… Mais ça devrait toujours être le cas, non ? 😉)

Pour les petits malins; oui, je vous ai vu, vous qui vous dites, il est sympas, mais si je modifie que “un-autre/src/app.js” ça ne va pas faire le build, et vous avez raison !

Rappelons le contexte, nous avons mis dans ce repo les conteneurs qui ne bougent pas ou presque, et qui pour la plus grosse partie sont des images de base avec quelques packages ou configuration supplémentaires.

Donc, si votre source bouge régulièrement, il est certain qu’un repository de code spécifique est plus adapté; ceci étant dit, il y a quand même au moins deux solutions :

  • Adapter le bash pour qu’une modification de **/src/* ajoute un job pour le sous répertoire concerné
  • Simplement ajouter un commentaire au Dockerfile (date de dernière modification, version, incrément de build …) que vous pourrez modifier pour forcer le build

Conclusion

Voilà notre solution pour gérer nos petits conteneurs de la manière la plus automatisée et simple possible, sans multiplier les repository git, en utilisant au maximum la CI/CD de Gitlab, tout en restant sobre sur les ressources (éco-logique/nomique-ment plus sérieux).

Vos retours et vos remarques sur nos produits sont les bienvenues !

Sources

Passionné par le numérique et grand amateur d'écriture qui apprécie tout particulièrement transmettre ses connaissances à d'autres personnes.