Tout le monde a entendu parler d'intégration continue et de déploiement continue. Mais comment réellement la mettre en place.
En partant du postulat que l'on dispose déjà d'un Gitlab et de connaissances en Docker, voici la procédure pour se préparer à mettre en place l'intégration continue et le déploiement continu (CI/CD) ainsi que des exemples type pour la configuration d'un projet multi-technologies.

Généralités sur l'intégration et le déploiement continu

La philosophie générale de la CI/CD est définie par l'exécution d'une action ou liste d'actions lors d'un événement particulier sur dépôt GIT. Des runners sont alors missionnés pour exécuter une tâche à partir des sources disponibles dans le dépôt à un état donné (un commit ou un tag précis ...).

Généralement, c'est le serveur qui prévient les runners qu'une nouvelle tâche est à réaliser or, dans la philosophie de Gitlab, ce sont les runners qui vont aller vérifier régulièrement auprès du serveur s'il n'y a pas de nouvelle tâche à prendre en charge. Cela permet entre-autre de disposer de runner dans un espace sécurisé, sur un réseau privé par exemple, et surtout sur n'importe quel type de machine.

Mise en place d'un runner

Comme annoncé dans le postulat de départ, nous partons du principe qu'une instance de Gitlab est déjà en place et qu'elle est fonctionnelle.
Nous allons donc configurer un gitlab-runner et le configurer pour qu'il puisse aller récupérer des tâches/jobs sur le serveur. Pour cela, nous allons utiliser Docker
L'ajout d'un runner se déroule en deux étapes :

  • Il faut le configurer et le référencer dans un premier temps
  • Ensuite il faut le démarrer

Configuration et référencement d'un runner

Dans un premier temps, il est nécessaire de récupérer token d'authentification auprès de notre instance Gitlab. Il est possible de récupérer deux types de tokens :

  • Un token projet qui permettra de référencer un runner dans le scope d'un unique projet. Il ne pourra alors exécuter que les tâches associées au projet pour lequel il a été référencé.
  • Un token global qui permettra de référencer un runner dans le scope global de Gitlab. Cela veut dire qu'il sera en mesure de réaliser les tâches de n'importe quel projet pour peu que sa configuration soit compatible.

Dans cet article, nous allons nous limiter au scope d'un projet aussi, pour récupérer notre token, il suffit d'aller, dans notre projet, dans le menu Paramètres > Intégration et livraison continue puis d'étendre la section Exécuteurs.
Dans le colonne Exécuteurs spécifiques nous pouvons voir les informations nécessaires à la configuration de notre nouveau runner :

  • L'adresse de notre instance Gitlab
  • Le token d'authentification spécifique à notre projet

Le référencement d'un runner peut se faire de manière intéractive ou directement en ligne de commande. Quoi qu'il en soit, il est important de sauvegarder la configuration du runner pour s'en reservir ultérieurement.
Afin de faciliter un peu la procédure, Nous allons réaliser un petit script qui permet d'enregistrer un runner. Pour cela, il faut créer le fichier register.sh :

#!/bin/sh

docker run --rm -it \
    -v $(pwd)/config:/etc/gitlab-runner:rw \
    -v /var/run/docker.sock:/var/run/docker.sock \
    gitlab/gitlab-runner:latest register \
    --non-interactive \
    --executor "docker" \
    --docker-image alpine:3.8 \
    --docker-privileged \
    --url "https://<GITLAB_INSTANCE>/" \
    --registration-token "<GITLAB_TOKEN>" \
    --description "docker-runner" \
    --tag-list "docker" \
    --run-untagged \
    --locked="false"

Plus de détails sur les paramètres de la commande docker :

  • -v $(pwd)/config:/etc/gitlab-runner:rw permet de sauvegarder la configuration du runner dans le dossier config du répertoire courant.
  • --non-interactive désactive le mode intéractif
  • --executor "docker" définit le type d'éxécuteur en mode docker
  • --docker-image alpine:3.8 définit l'image de base à partir de laquelle seront réalisées les tâches
  • --docker-privileged permet de donner des droits plus étendus au runner dont celui d'exécuter un daemon docker
  • --url "https://<GITLAB_INSTANCE>/" précise l'URL de notre instance Gitlab pour pouvoir vérifier si de nouvelles tâches doivent être exécutées
  • --registration-token "<GITLAB_TOKEN>" précise le token d'authentication de notre runner
  • --description "docker-runner" définit le nom du runner
  • --tag-list "docker" définit une liste de tag, séparés par une virgule, qui peuvent être utilisés pour sélectionner un runner lors de la définition d'une tâche
  • --run-untagged précise que le runner peut exécuter des tâche qui n'ont pas été taggées

Si l'on souhaite pouvoir créer des images Docker dans un runner, il peut être également intéressant d'ajouter le flag --privileged.

Lorsque la commande a été exécutée, nous devrions nous retrouver avec le fichier ./config/config.toml dont le contenu est le suivant :

concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "docker-runner"
  url = "https://<GITLAB_INSTANCE>/"
  token = "<GITLAB_TOKEN>"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "alpine:3.8"
    privileged = true
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]
    shm_size = 0
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

Exécution du runner

Pour exécuter le runner, nous procédons de la même manière. Nous créons le script run.sh dont le contenu est le suivant :

#!/bin/sh

docker rm -f gitlab-runner
docker run -d --name gitlab-runner \
    -v $(pwd)/config:/etc/gitlab-runner \
    -v /var/run/docker.sock:/var/run/docker.sock \
    --restart always \
    --privileged \
    gitlab/gitlab-runner:latest

Ce script supprime dans un premier temps tout conteneur nommé gitlab-runner puis en démarre un nouveau qui s'appellera à son tour gitlab-runner.
Voici également le détail des paramètres de la commande docker run :

  • -v $(pwd)/config:/etc/gitlab-runner permet de monter dans le conteneur un volume qui contient la configuration créée précédemment
  • -v /var/run/docker.sock:/var/run/docker.sock permet de monter le socker docker afin de pouvoir exécuter des commandes docker depuis le runner
  • --restart always permet de redémarrer le conteneur en cas d'erreur afin de reprendre les travaux en cours
  • --privileged permet au runner de créer des conteneurs

Maintenant que nous avons mis en place notre runner, on doit pouvoir le retrouver sur la page d'administration de notre projet.

L'intégration continue sur un projet Web

Préparation du projet

Avant de rentrer dans le vEn partant du postulat quif du sujet, il est nécessaire de préparer le projet (au sens Gitlab) pour l'intégration continue. Rien de bien compliqué mais il faut tout de même préparer les variables d'intégration continue.
Dans notre cas, nous allons faire en sorte de générer des images docker et de les pousser sur un registry local.

Pour cela nous avons besoin de déclarer quelques variables dans notre projet. Il faut donc se rendre dans la section Paramètres > Intégration et livraison continue puis étendre la section Variables.
Nous allons saisir les variables suivantes :

  • CI_REGISTRY_HOST : Adresse du registry docker
  • CI_BUILD_IMAGE : Nom de l'image docker que l'on va builder
  • CI_REGISTRY_TOKEN : Token d'authentification au registry pour un utilisateur donné (ici on utilisera l'utilisateur gitlab)

Made with Canon 5d Mark III and loved analog lens, Leica Elmarit-R 1:2.8 / 28mm (Year: 1978)
Photo by Markus Spiske / Unsplash

Définition des règles d'intégration continue

Afin de définir les étapes de notre intégration continue, il est nécessaire d'ajouter à notre projet un fichier de configuration nommé gitlab-ci.yml. Ce fichier va contenir les instructions nécessaires au runner pour réaliser les tâches que nous lui demanderons de faire, selon le formalisme décrit dans la documentation Gitlab.
L'ensemble des instructions définies se nomme un pipeline et est découpé en étapes (build, test, ...) et en jobs.

Dans l'exemple que je vais donner et expliquer ci-dessous, je pars du postulat que le projet sur lequel nous travaillons est déjà configuré pour être packagé dans une image docker. Il y a donc dans l'arborescence un fichier .Dockerfile qui contient les instructions nécessaire au build de notre image.

Voici donc un exemple relativement simple de fichier gitlab-ci.yml :

image: docker:stable

# Uncomment if runner version lower than 11.11
# See : https://docs.gitlab.com/ce/ci/docker/using_docker_build.html#use-docker-socket-binding
# variables:
#   DOCKER_HOST: tcp://docker:2376/
#   DOCKER_DRIVER: overlay2

# Uncomment if runner version lower than 11.11
# See : https://docs.gitlab.com/ce/ci/docker/using_docker_build.html#use-docker-socket-binding
# services:
#   - docker:dind

stages:
  - package

docker-build:
  stage: package
  script:
    - docker login -u gitlab -p ${CI_REGISTRY_TOKEN} ${CI_REGISTRY_HOST}
    - docker build --build-arg VERSION=${CI_COMMIT_TAG:-nightly} -t ${CI_REGISTRY_HOST}/${CI_BUILD_IMAGE}:${CI_COMMIT_TAG:-nightly} .
    - docker push ${CI_REGISTRY_HOST}/${CI_BUILD_IMAGE}:${CI_COMMIT_TAG:-nightly}
  tags:
    - docker

Ce fichier permet de définir la configuration de notre pipeline. Dans le détail, voici la description des différentes étapes qui sont réalisées :

  • image: docker:stable : permet de demander au runner d'instancier un conteneur à partir de l'image docker:stable pour réaliser les jobs qui vont être décris pas la suite
  • variables : Ce bloc permet d'ajouter des variables qui seront disponible dans le scope global de l'intégration continue. Il est également possible d'en définir dans chacun des jobs. Elles ne seront alors pas disponibles en dehors du scope du job dans lequel elles ont été définies.
  • services : Permet de définir un service complémentaire qui sera instancé et accessible pendant l'exécution du pipeline. Ici nous souhaitons utiliser le service docker:dind (docker in docker) afin de pouvoir exécuter n'importe quelle commande docker.
  • stages : Permet de définir les étapes de notre pipeline. Elles seront traitées dans l'ordre de définition
  • docker-build : Ce n'est pas un mot clé. Nous commençons ici la définition d'un job nommé docker-build
    • stage : Permet d'associer un job à une étape du pipeline.
      Attention, les jobs associés à une même étape sont traités dans un ordre arbitraire.
    • script : Permet de définir une liste de commandes qui seront exécutées par le conteneur.
  • tags : Permet de définir quel runner sera utilisé pour exécuter le job, en l'occurence n'importe quel runner disposant du tag docker.

Concentrons-nous un peu plus sur le contenu de la section script.
Nous nous connectons dans un premier temps à notre registry CI_REGISTRY_HOST avec le compte gitlab et le token CI_REGISTRY_TOKEN. Ensuite nous buildons notre image en définissant le paramètre VERSION avec la valeur CI_COMMIT_TAG, une variable de Gitlab correspondant au tag du commit, ou la valeur nightly dans le cas où cette variable n'est pas renseignée puis nous taggons l'image.
Enfin nous poussons l'image générées sur le registry.

C'est un pipeline tout à fait élémentaire mais il présente bien le principe d'intégration continue. Il existe tout un tas d'options de configuration complémentaires qui permettent d'affiner le comportement du runner, en cas d'erreur ou de succès d'un job par exemple.

Il est également possible de compléter un pipeline avec des étapes complémentaire comme les tests ou le déploiement continue car au final, ce ne sont que des commandes qui sont exécutées dans une contexte bien précis.

Références