Builder simplement des images Docker avec Gitlab-CI (sans DinD)
Cela fait plusieurs mois (années ?) que je me dis qu’utiliser DinD (Docker in Docker) pour builder des images n’est vraiment pas idéal. Mais cela fait aussi un moment que je n’ai pas eu l’occasion de (re)construire une CI. Couplé à une flemmingite aiguë en ce qui concerne la reprise des projets persos…
Mais il est temps ! Aujourd’hui nous allons jeter un coup d’oeil à buildah et construire un pipeline Gitlab-CI simple pour s’affranchir de la dépendance à Docker dans nos jobs.
Objectif
C’est la première fois que je me penche sur les alternatives à DinD pour le build. Le but n’est pas de devenir expert mais de remplacer ce fonctionnement par un autre que j’estime plus sain.
L’objectif est donc de créer un pipeline pour construire une image docker puis de publier celle-ci.
Nous utiliserons les éléments suivants :
- Gitlab.com : et ses outils de CI/CD intégrés
- Runner partagés : des serveurs mis à disposition par Gitlab.com pour exécuter nos jobs. Attention au quota.
- .gitlab-ci.yml : le fichier définissant le comportement de la CI Gitlab
- Dockerfile : le fichier contenant les instructions de création pour une image
- Buildah : alternative à Docker pour construire nos images à partir du Dockerfile dans le contexte CI
- quay.io/buildah/stable:v1.21.0 : la dernière version en date de l’image Buildah
- GitLab Container Registry : pour publier nos images
Les étapes seront les suivantes :
- Définition d’un Dockerfile de test à builder automatiquement
- Définition des jobs CI dans le fichier .gitlab-ci.yml pour réaliser le build et la publication sur le registry Gitlab
- Un récap du code complet
Y’a plus qu’à…
Avant de commencer
Nous aurons besoin de quelques prérequis pour atteindre cet objectif :
- Un compte et un repository vierge sur Gitlab.com. N’hésitez pas à jeter un oeil sur les guides si nécessaire Gitlab basics, Gitlab CI quick start
- Un Dockerfile d’exemple
- Nous nous appuierons sur l’image officielle de Buildah pour le contexte d’exécution de nos tâches automatisées.
Le Dockerfile de test
Pour notre exemple choisissons un serveur Nginx servant un fichier statique.
Commençons par cloner notre projet, préalablement créé sur gitlab.com. Dans mon cas :
git clone git@gitlab.com:memorandom/buildah-gitlab-ci.git
cd buildah-gitlab-ci
Créons ensuite un pseudo site web qui sera exposé par Nginx :
# créons le dossier qui contiendra un fichier html basique
mkdir static-html-directory
# puis le fichier nommé index.html
cat <<EOF > static-html-directory/index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Buildah with Gitlab CI</title>
<link rel="stylesheet" href="style.css">
<a href="http://script.js">http://script.js</a>
</head>
<body>
<p>I was built with Buildah!</p>
</body>
</html>
EOF
Puis le fichier Dockerfile qui permettra de construire l’image de notre serveur :
FROM nginx
COPY static-html-directory /usr/share/nginx/html
Commitons les changements. Notre exemple est prêt.
Test en local
Avant d’implémenter la CI testons le processus en local. Démarrons un conteneur avec Buildah :
docker run --security-opt seccomp:unconfined -it -v $PWD:/buildah quay.io/buildah/stable:v1.21.0
Il peut être nécessaire d’adapter la configuration Docker pour que Buildah fonctionne correctement (buildah:issue:1901). Heureusement les runners mis à disposition par Gitlab.com ont l’air compatible ?.
Une fois dans le conteneur, nous avons accès au contenu de notre projet grâce au volume Docker. Exécutons les commandes suivantes :
cd /buildah # Le dossier dans lequel est monté notre projet
export STORAGE_DRIVER=vfs # Option de configuration pour Buildah
export BUILDAH_FORMAT=docker
buildah bud -f Dockerfile -t test:latest # le build avec Dockerfile
buildah images # afficher les images, dont celle nouvellement créée
# "export" de l'image dans un fichier
mkdir archive
buildah push test docker-archive:archive/built-with-buildah-example.tar:latest
# "re-import" de l'image dans Buildah via le même fichier
buildah pull docker-archive:archive/built-with-buildah-example.tar
Nous utilisons buildah bud
(pour Build Using Dockerfile) pour construire l’image. Les paramètres sont simplement le chemin vers le fichier Dockerfile et le nom de la nouvelle image.
La commande buildah push
nous permet d’exporter l’image dans un fichier d’archive. Et celle de pull
permet le re-import d’une image depuis le même type d’archive.
Ces deux dernières commandes nous servirons à isoler notre job de build et notre job de push. L’export sera stocké dans un artifact au moment du build et sera re-importé avant le push vers le registry. Les deux jobs pourront ainsi s’éxécuter indépendamment et l’archive pourra être téléchargée pour consultation si nécessaire.
Le Pipeline
Entrons dans le vif du sujet avec le détail des définitions de la CI. Créons un fichier .gitlab-ci.yml à la racine de notre projet dans lequel nous allons définir nos deux jobs.
Options communes
Le fichier débute par des options communes :
variables: # les variables, notamment de configuration pour Buildah
ARTIFACT_DIR: "./archive"
ARTIFACT_NAME: "built-with-buildah-example.tar"
STORAGE_DRIVER: "vfs"
BUILDAH_FORMAT: "docker"
stages: # les étapes dans lesquelles viendront s'inscrire les jobs
- build
- push
default: # l'export de la variable TAG qui sera exécuté avant chaque job
before_script:
- export TAG="${CI_COMMIT_TAG:-latest}" && echo $TAG
L’export du TAG joue sur les variables pré-définies exposées par Gitlab.com. Si le pipeline est trigger par la création d’un tag, la variable CI_COMMIT_TAG est automatiquement ajoutée par Gitlab. Nous utilisons donc cette valeur pour le TAG si elle existe. Sinon la valeur par défaut latest prime.
Le dossier servant à stocker l’artifact (variable ARTIFACT_DIR) doit être relatif au dossier de build, sans quoi Gitlab.com ne pourra pas créer l’artifact. (issue:15530)
Le build
Définissons ensuite la clef build
qui configure le job du même nom :
build:
stage: "build" # fait partie de l'étape de build du pipeline
image: # utilise l'image officielle de Buildah
name: "quay.io/buildah/stable:v1.21.0"
script: # les commandes pour builder et exporter l'image
- "buildah bud -f Dockerfile -t ${CI_REGISTRY_IMAGE}:${TAG} ."
- "mkdir ${ARTIFACT_DIR}"
- "buildah push ${CI_REGISTRY_IMAGE}:${TAG} docker-archive:${ARTIFACT_DIR}/${ARTIFACT_NAME}:${TAG}"
Plutôt explicite. Notons l’utilisation de la variable pré-définie CI_REGISTRY_IMAGE. Celle-ci contient l’adresse complète de l’image pour le projet courant. Par exemple dans le cas de mon dépôt git nommé memorandom/buildah-gitlab-ci, la valeur associée est registry.gitlab.com/memorandom/buildah-gitlab-ci
.
Rappelons que la variable TAG provient de la définition default.before_script
explicitée précédemment.
L’artifact
Toujours dans le job de build rajoutons le bloc indiquant à Gitlab.com qu’il doit stocker l’export de l’image :
build:
[...]
artifacts: # Gitlab.com gère automatiquement la récupération et le stockage des fichiers spécifiés
name: "archive:${ARTIFACT_NAME}:${CI_JOB_ID}" # le nom
when: "on_success" # seulement si le job ne présente pas d'erreur
expire_in: "6h" # suppression de l'artifact après 6h
paths: # le chemin, relatif au dossier de build
- "${ARTIFACT_DIR}/"
C’est ce qui nous permet de séparer nos deux jobs. L’artifact est automatiquement géré par Gitlab.com. Il sera copié au même endroit dans les jobs suivants avant que la logique de ces derniers ne soit éxécutée.
Pour être plus explicite nous mentionnerons la dépendances lors de la phase de push, mais ce n’est pas obligatoire.
Le push
Finalement ajoutons la tâche permettant de publier l’image :
push:
stage: "push" # fait partie de l'étape de push du pipeline
image: # utilise l'image officielle de Buildah
name: "quay.io/buildah/stable:v1.21.0"
only: # ne s'éxécute que sur la branche principale ou à la création d'un tag
- "tags"
- "main"
script: # les commandes pour importer l'image depuis l'artifact et la publier
- "buildah pull docker-archive:${ARTIFACT_DIR}/${ARTIFACT_NAME}"
- "echo $CI_REGISTRY_PASSWORD | buildah login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY"
- "buildah push ${CI_REGISTRY_IMAGE}:${TAG}"
dependencies:
- "build" # explicite la dépendance avec le job précédent, et donc la récupération de l'artifact
La différence principale (au-delà des commandes de scripts) réside dans l’ajout de la clef only
. Nous spécifions par ce biais que le job n’est lancé que pour la branche main ou pour la création d’un tag. L’intérêt de découpler les deux jobs est de pouvoir s’assurer que le build est fonctionnel en permanence, mais de ne publier que dans certains cas précis.
Nous nous appuyons également de nouveau sur les variables pré-définies de Gitlab qui exposent toute les informations nécessaire pour l’authentification auprès du registry (CI_REGISTRY_USER, CI_REGISTRY_PASSWORD & CI_REGISTRY).
Commitons les changements et poussons sur Gitlab. En consultant l’UI nous pouvons suivre l’exécution du pipeline. Une fois celle-ci terminée nous pouvons également constater l’apparition d’une nouvelle image dans le registry ! ?
Vérification en local
Par acquis de conscience, récupérons et lançons l’image depuis notre poste pour s’assurer que tout est fonctionnel. Dans mon cas :
docker login registry.gitlab.com
docker run -p 8080:80 registry.gitlab.com/memorandom/buildah-gitlab-ci
A l’aide de curl, ou en visitant http://127.0.0.1:8080, nous retrouvons bien le pseudo site web créé initialement.
Pour l’accès au registry Gitlab depuis l’extérieur vous pouvez créer un token personnel.
Le code complet
Nous en avons terminé. N’hésitez pas à me faire vos retours ?. En attendant vous trouverez ci-après le code complet du pipeline.
A la prochaine ?.
.gitlab-ci.yml
variables:
ARTIFACT_DIR: "./archive" # must be relative to the build directory - https://gitlab.com/gitlab-org/gitlab-foss/-/issues/15530
ARTIFACT_NAME: "built-with-buildah-example.tar"
STORAGE_DRIVER: "vfs"
BUILDAH_FORMAT: "docker"
stages:
- build
- push
default:
before_script:
- export TAG="${CI_COMMIT_TAG:-latest}" && echo $TAG # If the pipeline was triggered by a tag, use the tag value, otherwise use "latest"
build:
stage: "build"
image:
name: "quay.io/buildah/stable:v1.21.0"
script:
- "buildah bud -f Dockerfile -t ${CI_REGISTRY_IMAGE}:${TAG} ."
- "mkdir ${ARTIFACT_DIR}"
- "buildah push ${CI_REGISTRY_IMAGE}:${TAG} docker-archive:${ARTIFACT_DIR}/${ARTIFACT_NAME}:${TAG}"
artifacts:
name: "archive:${ARTIFACT_NAME}:${CI_JOB_ID}"
when: "on_success"
expire_in: "6h"
paths:
- "${ARTIFACT_DIR}/"
push:
stage: "push"
image:
name: "quay.io/buildah/stable:v1.21.0"
only:
- "tags"
- "main"
script:
- "buildah pull docker-archive:${ARTIFACT_DIR}/${ARTIFACT_NAME}"
- "echo $CI_REGISTRY_PASSWORD | buildah login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY"
- "buildah push ${CI_REGISTRY_IMAGE}:${TAG}"
dependencies:
- "build"