Configuration d'un registre Docker pour un GitLab Runner

Publié le 25/04/2021 à 19:50.

Ou comment contourner les limites d'utilisation de Docker Hub. Et sans oublier Docker-in-Docker !

Configuration d'un registre Docker pour un GitLab Runner

Photo de Ludovic Charlet sur Unsplash.

Contexte

Comme pour mon tout premier article, je décide de faire face à de nouvelles restrictions: cette fois ce sont les limites de téléchargement d'images sur Docker Hub:

  • 100 pulls par tranche de 6 heures pour utilisateurs non authentifiés
  • 200 si vous êtes authentifiés avec un compte gratuit

J'ai pensé moi aussi que ce serait suffisant, jusqu'à ce que ma pipeline s'arrête lors d'un déploiement en production.

Ce problème ne se pose pas si vous passez par les shared runners de GitLab, ce qui n'est pas mon cas puique j'ai mon propre runner.

La solution

L'objectif est d'utiliser mon propre registre Docker. Pour ceux qui ne verraient pas de quoi il s'agit, c'est là où sont stockées les images. Il existe Docker Hub, votre registre local, mais vous pouvez faire le vôtre si ça vous chante: il existe l'image registry !

Sauf que je ne vais pas simplement créer mon registre mais en faire un mirroir de Docker Hub, un "pull through cache":

  • si une image n'existe pas, elle sera récupérée de Docker Hub et enregistrée dans mon registre
  • si elle est déjà présente, on n'aura pas besoin de passer par Docker Hub !
  • si elle est présente mais qu'elle a été mise à jour sur Docker Hub, elle sera mise à jour dans mon registre

Pour des raisons de simplicité, le registre sera installé sur le même serveur que le runner. Rien ne vous empêche de les séparer.
Mais pour pouvoir m'adapter aux éventuelles limitations futures et modifier facilement ma configuration, j'ai créé un dépôt GitLab avec une pipeline, spécialement pour le GitLab Runner (ce qui est complètement facultatif).

Dépôt & CI

GitLab Runner

Le but du dépôt peut être de mettre à jour le GitLab Runner, on peut y enregistrer le fichier config.toml qui sera mis à jour.

Aussi, puisque la pipeline peut potentiellement faire redémarrer le runner, elle ne doit pas être exécutée par celui-ci mais par les "shared runners".
Pour cela il faudra aller dans Settings > CI/CD > Runners. Dans la colonne Shared runners, cocher la case « Enable shared runners for this project » et cliquer sur « Disable group runners ».

Note: côté performances, la pipeline dure en moyenne environ 30 secondes, pas de quoi s'inquiéter.

SSH

Pour que la pipeline puisse accéder au serveur, on configure une paire de clés SSH.
Pour utiliser la clé privée, on l'enregistre en tant que variable dans Settings > CI/CD > Variables. Chez moi ce sera SSH_PRIVATE_KEY.

On peut ensuite commencer la création de la pipeline. Aucun besoin spécifique, j'utilise donc une image d'Alpine Linux. Mais puisque cette image ne dispose pas d'un client SSH, il faudra en installer un:

image: alpine:3

variables:
    RUNNER_IP: "123.1.2.3"

stages:
    - deploy

deploy-runner:
    stage: deploy
    only:
        - master
    before_script:
        - apk add --no-cache openssh-client
        - mkdir -p ~/.ssh
        - chmod 700 ~/.ssh
        - eval $(ssh-agent -s)
        - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -

On a donc une pipeline avec un unique job. Notez également qu'il ne s'exécute que pour notre branche master et que l'adresse du runner est enregistrée dans une variable à utiliser plus tard.

Mise à jour du GitLab Runner

La première étape du job deploy-runner sera de mettre à jour le fichier de configuration du runner. Si le fichier a été modifié, on le met à jour et on relance le runner:

deploy-runner:
    ...
	script:
	    - cat ./config.toml | ssh user@$RUNNER_IP "cat > ./config.toml"
        - ssh user@$RUNNER_IP "[[ ./config.toml -ef /etc/gitlab-runner/config.toml ]] || { mv ./config.toml /etc/gitlab-runner/config.toml && gitlab-runner restart; }"

Note: Pour la mise en place du registre, vous n'aurez pas forcément besoin de changer la configuration du runner.

Registre

Mise en place

Identifiants Docker Hub

Dans les différentes documentations que j'ai pu lire pour réaliser ce projet, il est mentionné que des identifiants Docker Hub sont utiles pour pouvoir télécharger des images privées. Cependant je n'ai réussi à faire fonctionner le registre sans, donc je vous conseille d'en générer.

Dans un premier temps, créez un compte sur Docker Hub ou connectez-vous.
Une fois connecté, rendez-vous dans la section sécurité des paramètres du compte: Account Settings > Security.
Vous pourrez créer un jeton d'accès en cliquant sur « New Access Token ». Indiquez une description et gardez de côté le jeton fourni pour la suite.

Démarrage du registre

Le registre s'exécute en tant que conteneur. Pour faciliter sa gestion, j'ai choisi de passer par Docker Compose que j'ai donc installé sur la machine.
Et voici le docker-compose.yml:

version: "3.7"

services:
    registry:
        image: registry:2.7
        restart: always
        ports:
            - 5000:5000
        volumes:
            - "./registry-config.yml:/etc/docker/registry/config.yml"

On expose le port 5000 et on spécifie un volume pour le fichier de configuration du registre.

La section qui nous intéresse dans ce fichier est proxy, qu'on rajoute donc après la configuration par défaut (que vous pourrez trouver dans l'image de base):

version: 0.1
log:
  fields:
    service: registry
storage:
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3

proxy:
    remoteurl: https://registry-1.docker.io
    username: USERNAME
    password: DOCKER_HUB_ACCESS_TOKEN

La clé proxy.remoteurl va configurer le registre pour qu'il agisse comme un "pull through cache" en se basant sur celui de Docker Hub.
Remplacez la valeur de proxy.username par votre nom d'utilisateur Docker Hub, et proxy.password par le jeton d'accès précédemment créé.

Dans notre job GitLab CI, on rajoute quelques commandes pour le démarrage du registre:

...
deploy-runner:
    ...
	script:
	    ...
		- cat ./docker-compose.yml | ssh user@$RUNNER_IP "cat > ./docker-compose.yml"
        - cat ./registry-config.yml | ssh user@$RUNNER_IP "cat > ./registry-config.yml"
        - ssh user@$RUNNER_IP "docker-compose up -d"

La commande docker-compose up démarre ou met à jour des services, parfait pour notre besoin.

Utilisation

Il faut maintenant indiquer au Docker daemon d'utiliser le registre.

Daemon de l'hôte

Dans une configuration simple utilisant le shell executor ou le docker executor avec docker socket binding il s'agit du daemon de la machine hôte.
On peut configurer ce dernier avec un fichier de configuration JSON dans lequel on indiquera le registre mirroir à utiliser. Il faudra ensuite relancer le Docker daemon, il est donc conseillé d'activer le Live Restore pour ne pas arrêter les conteneurs démarrés:

{
    "registry-mirrors": ["http://123.1.2.3:5000"],
    "live-restore": true
}

Note: Veillez à bien indiquer le protocole HTTP si vous n'avez pas configuré le HTTPS, et à ne pas oublier le numéro de port.

Dans le job GitLab CI, on indiquera de relancer le Docker daemon si le fichier de configuration a changé:

...
deploy-runner:
    ...
	script:
	    ...
		- cat ./docker-daemon.json | ssh user@$RUNNER_IP "cat > ./docker-daemon.json"
        - ssh user@$RUNNER_IP "[[ ./docker-daemon.json -ef /etc/docker/daemon.json ]] || { mv ./docker-daemon.json /etc/docker/daemon.json && systemctl reload docker; }"

Service Docker-in-Docker

Dans mon cas, j'utilise le DinD pour que les jobs puissent exécuter des commandes docker. Le DinD est configuré en tant que service dans les pipelines, on va donc ajouter une option au service pour qu'il utilise le registre.
Cela peut se faire directement dans le fichier de configuration du runner, ou bien dans le fichier gitlab-ci.yml:

services:
    -   name: docker:20.10.6-dind
        command: ["--registry-mirror", "http://123.1.2.3:5000"]
        alias: docker

Attention il ne s'agit pas du fichier de la pipeline du runner mais bien d'un projet dont les pipelines s'exécuteront sur celui-ci.

Vérifier l'utilisation du registre

Puisque l'utilisation du registre est transparente, on peut se demander si tout fonctionne comme prévu. Exécuter la commande docker image ls n'est pas suffisant car vous n'aurez aucune certitude du registre utilisé.
Téléchargez des images avec un simple docker pull ou en exécutant une pipeline si vous utilisez le DinD.

Un registre peut exposer les images qu'il possède via une route HTTP, dans du JSON:

curl http://123.1.2.3:5000/v2/_catalog
[output]{"repositories":["library/docker", "library/alpine"]}

Si vous retrouvez les images utilisées dans votre pipeline, c'est tout bon !

Conclusion

Beaucoup de manipulations pour contourner une limitation, mais ça peut vous permettre d'en découvrir un peu plus sur le fonctionnement de Docker. Pour l'instant je ne saurai dire si ce registre aura un impact sur la performance des pipelines.
Une chose est sûre, ça permet d'avoir un peu plus le contrôle sur l'environnement de la CI.

Commentaires: 0

Invasion robot en provenance de robohash.org