Builder simplement des images Docker avec Gitlab-CI (sans DinD)

Temps de lecture : 7 minutes

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 :

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">
    http://script.js
  </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"

Commentaires :

A lire également sur le sujet :