Connectes-toi pour publier un commentaire.
Publié le 26/09/2020 à 19:14. Mise à jour le 25/04/2021 à 13:28.
J'ai décidé de revoir ma pipeline actuelle et d'utiliser mon propre GitLab Runner. Ce fût plus compliqué que prévu.
Publié le 26/09/2020 à 19:14. Mise à jour le 25/04/2021 à 13:28.
J'ai décidé de revoir ma pipeline actuelle et d'utiliser mon propre GitLab Runner. Ce fût plus compliqué que prévu.
Photo par Chris Liverani sur Unsplash
Récemment, Docker Hub a annoncé sa nouvelle politique de rétention d'images visant à supprimer les images inutilisées depuis 6 mois et GitLab la baisse des minutes de build passant à 400 minutes.
Bon, ça ne concerne que les comptes utilisateurs gratuits et ces contraintes ne sont pas bloquantes pour mes projets, mais ça m'a motivé à mettre en place mon propre runner et à me débarasser de Docker Compose en CI.
Pour ceux qui ne seraient pas familier avec les technologies mentionnées ici:
Et Docker in Docker (abregé « DinD » 🦃) consiste à faire tourner Docker dans un conteneur Docker. C'est intéressant puisque je souhaite construire une image Docker durant ma pipeline qui s'exécute elle-même dans un conteneur.
Jusqu'à présent j'utilisais les shared runners de GitLab CI avec l'exécuteur Docker, en DinD avec une image créée par mes soins qui embarquait Docker Compose en plus: aymdev/dind-compose.
L'intérêt étant de démarrer une stack Docker Compose pour certains jobs (tests fonctionnels notamment, qui ont besoin d'une base de données en plus de l'application), et parce que l'application (ce site) est déployée dans un cluster Docker Swarm, donc autant tester directement avec l'image qui sera déployée.
Le problème c'est que mon image n'était qu'un essai que je ne maintiens pas, qui n'a pas beaucoup d'intérêt, et que démarrer toute la stack juste pour mes tests fonctionnels est un peu lourd (4 conteneurs au total pour 2 utilisés). Et mes fichiers docker-compose.yml
étaient multipliés, ce qui n'arrangeait pas la maintenabilité du code.
Lors de l'installation de GitLab Runner vient le choix de l'executor. Ayant la main sur la machine, j'ai pensé que le shell executor serait une bonne idée, et pour plusieurs raisons. Celui-ci exécute simplement les jobs sur la machine du runner (l'hôte), donc:
Mais c'était sans compter sur 1 problème: la concurrence.
En effet, tous les jobs se partagent le même environnement, donc si plusieurs d'entre eux s'exécutent en parallèle ils risquent de se perturber. Il faut faire attention à:
Et c'est sans compter sur la gestion de l'espace de stockage (images qui s'accumulent), la gestion des conteneurs laissés démarrés parce que la pipeline a planté, etc.
Bref, j'ai réinitialisé mon instance.
L'instance utilisée tourne sous Debian 10. Je pense qu'il est préférable d'utiliser une machine optimisée au niveau de la mémoire et de délaisser l'espace disque, surtout si votre budget est limité.
Connecté en SSH sur l'instance:
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
export GITLAB_RUNNER_DISABLE_SKEL=true; sudo -E apt-get install gitlab-runner
Source: Install GitLab Runner using the official GitLab repositories
Puisque j'utilise Debian, j'ai également suivi la recommandation de l'APT pinning.
Et on installera que Docker, pas Docker Compose:
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
Source: Install Docker Engine on Debian
Avant d'enregistrer le runner, on a besoin de récupérer un token afin de lier notre projet ou groupe de projets au runner. Pour trouver celui-ci, depuis l'interface de GitLab, aller dans Settings > CI/CD et ouvrir la section Runners.
Note: depuis cette section, pour un projet (pas au niveau d'un groupe), il est possible de désactiver les shared runners pour forcer l'utilisation de vos propres runners.
Une fois copié, on peut lancer l'enregistrement du runner. La commande suivante sera interactive:
sudo gitlab-runner register
Pour l'URL du coordinateur, n'utilisant pas ma propre instance GitLab, c'est donc https://gitlab.com
.
On passe ensuite le token, une description (sera affichée dans l'interface de CI/CD), des tags (facultatif), et enfin, l'exécuteur docker
.
Une dernière question demande une image par défaut, on choisira docker:19.03.13
.
La documentation de GitLab propose 3 manières de permettre Docker in Docker.
La technique du Docker socket binding consiste à monter /var/run/docker.sock
en volume entre hôte et conteneurs.
Bien qu'elle ait l'air pratique, elle revient à ce que tous les conteneurs partagent le même daemon Docker. Cela signifie que l'on retrouverait une bonne partie des problèmes de concurrence du shell executor (noms uniques, etc).
Solution viable mais à utiliser lorsque l'on ne peut pas faire autrement.
GitLab: « Par exemple, si vous n'avez aucun contrôle sur la configuration du GitLab Runner que vous utilisez. »
On a justement la main sur la configuration.
C'est l'option recommandée, pour des raisons de sécurité. Ouvrir le fichier de configuration du runner: /etc/gitlab-runner/config.toml
:
concurrent = 1
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "GitLab Runner"
url = "https://gitlab.com/"
token = "super-secret-token"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "docker:19.03"
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0
Dans la section [runners.docker]
, rajouter privileged = true
, le mode d'exécution privilégié est nécessaire pour du DinD. Dans la même section, en ce qui concerne TLS, rajouter le volume "/certs/client"
.
Pour éviter que le runner n'exécute qu'un seul job à la fois, modifier la clé
concurrent
en tout début de fichier.
Le fichier devrait ressembler à ça (raccourci):
concurrent = 4
...
[[runners]]
...
[runners.docker]
...
privileged = true
volumes = ["/cache", "/certs/client"]
...
Il ne devrait pas être nécessaire de redémarrer le runner mais vous pouvez le faire avec les commandes suivantes:
gitlab-runner restart
sudo service gitlab-runner restart
Côté runner, c'est terminé !
Maintenant il faut s'occuper de notre fichier .gitlab-ci.yml
.
Si besoin, préciser l'image à utiliser par défaut:
image: docker:19.03.13
L'intérêt est de pouvoir changer de version sans avoir à changer la version par défaut dans la configuration du runner.
Dans les variables par défaut, rajouter une variable pour TLS:
variables:
DOCKER_TLS_CERTDIR: "/certs"
Et enfin, pour utiliser Docker in Docker, ajouter un service docker
:
services:
- name: docker:19.03.13-dind
alias: docker
Pour éviter de devoir reconstruire son image Docker à chaque job, on peut la construire dans un 1er job, l'envoyer dans le registre d'images fourni par GitLab, et la récupérer dans les jobs suivants.
Le nom de cette image devra être unique par pipeline. Puisqu'il sera utilisé dans chaque job, on rajoute une autre variable en utilisant des variables d'environnement prédéfinies:
variables:
...
DOCKER_CI_IMAGE: "$CI_REGISTRY_IMAGE:ci-$CI_COMMIT_SHORT_SHA"
Il sera nécessaire de se connecter au registre à chaque job, autant créer une ancre YAML à référencer dans la clé before_script
de chaque job:
.docker-login: &docker-login
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
Un premier job consistera à créer l'image de la pipeline:
docker-build:
before_script:
- *docker-login
script:
- docker build --pull -t "$DOCKER_CI_IMAGE" -f ./Dockerfile .
after_script:
- docker push $DOCKER_CI_IMAGE
Les suivants auront seulement besoin de se connecter au registre et l'image sera téléchargée sur demande:
example-job:
before_script:
- *docker-login
script:
- docker run -d $DOCKER_CI_IMAGE
Pour certains jobs, il sera suffisant de démarrer 1 conteneur de l'application à tester et d'y exécuter quelques commandes à l'aide de docker exec
.
Pour d'autres, ça peut être plus compliqué: tests fonctionnels nécessitant une base de données.
« Alors on ajoute un service au job concerné avec la clé
services
appropriée ... »
Justement non, car ce service sera accessible pour le conteneur du job, pas pour celui de notre application qui se trouve à l'interieur du premier. Et les manipulations pour réussir à contacter le service sont plus complexes que ce que j'ai à proposer.
Docker Compose permet de démarrer plusieurs conteneurs facilement, mais il ne propose pas de fonctionnalité en plus de Docker seul. On peut donc en quelques commandes recréer un environnement de test idéal.
Petit exemple:
test-suite:
variables:
POSTGRES_PASSWORD: postgres
DATABASE_URL: "pgsql://postgres:postgres@postgres/postgres"
before_script:
- *docker-login
- docker network create app-net
- docker run -d --network app-net --name postgres -e POSTGRES_PASSWORD postgres:11.7-alpine
- docker run -d --network app-net --name app -e DATABASE_URL $DOCKER_CI_IMAGE
- docker exec app ./path/to/database-migrations
script:
- docker exec app ./path/to/your/tests
Si vous avez lu jusqu'ici, merci !
Bon, l'utilisation de Docker in Docker fonctionne bien, mais a aussi ses désavantages, notamment au niveau du cache de construction d'images qui demande des manipulations supplémentaires.
Je n'ai pas abordé la structure complète d'une pipeline ou comment réduire sa durée d'exécution mais ce sera probablement le sujet d'un prochain article. J'espère que ce premier article vous aura plu, pour ma part je me suis bien amusé !
Connectes-toi pour publier un commentaire.