HeavyCookie

Accélérer le build des containers avec Gitlab Runner

Pour faire suite au dernier article sur GitLab, l'utilisation d'un runner Docker de ce type a un gros désavantage : la vitesse. Les builds sont faites à partir d'un environnement vide à chaque fois : on ne bénéficie plus du système de cache des builds de Docker.

Les runners ont bien un système de cache partagé mais il est déconseillé de partager le même système de fichiers pour plusieurs daemon Docker.

J'ai trouvé deux principales pistes pour accélérer les builds Docker : mettre en place un proxy-cache pour le registry Docker officiel et re-télécharger les anciennes builds à partir du registry de notre instance GitLab.

Proxy du registry officiel

On met donc en place un registry server officiel en suivant une des procédures disponible ici par exemple, puis on ajoute la configuration pour l'utiliser comme proxy du registry officiel.

"Parfait" me direz-vous, il n'y a plus qu'a configurer le daemon docker pour prendre en compte ce mirroir ! Oui, sauf dans le cas où on utilise un service docker:dind, cela demande un peu de gymnastique cérébrale (et de le customiser).

Il faut donc que l'on build notre propre docker:dind pour lui ajouter l'option --registry-mirror en lui précisant l'adresse de notre registry, par exemple :

# Dockerfile.dind
FROM docker:dind
CMD ["--registry-mirror=https://docker-registry-proxy:5000/"]

Pour le builder, on peut ajouter à notre précédent .gitlab-ci.yml le contenu suivant :

# .gitlab-ci.yml
# [...]
build:dind:
  stage: build
  script:
    - docker build -t registry.example.com/ci/docker:dind -f Dockerfile.dind .
    - docker push registry.example.com/ci/docker:dind

Mais ce n'est pas fini : les services sont linké à notre image de base avec un host spécifique qui suit ces quelques règles.

Notre image registry.example.com/ci/docker:dind est donc accessible via le host registry.example.com__ci__docker (ou registry.example.com-ci-docker).

Il faut donc aussi modifier notre image basée sur docker:latest en re-définissant l'URL de docker. Notre Dockerfile ressemblera donc à ça :

FROM docker:latest
RUN apk add --update python py-pip python-dev && pip install docker-compose
ENV DOCKER_HOST tcp://registry.example.com__ci__docker:2375

Dernière étape : il faut modifier le fichier TOML de configuration du Runner en redéfinissant notre service :

[[runners]]
  # [...]
  [runners.docker]
    # [...]
    services = ["registry.example.com/ci/docker:dind"]

Il y a probablement quelque chose à faire pour cacher de multiples registry Docker sans en avoir plusieurs instances (avec un nginx_proxy par exemple) mais je n'ai pas encore été voir de ce côté là (et pourtant, quand on a des containers ElasticSearch, ce serait bien utile vue la rapidité de leur registry).

Cache des précédentes builds Docker

Je vous préviens, vos .gitlab-ci.yml vont prendre de l'embonpoint avec cette méthode. L'idée est de tagger chaque build de plusieurs manières.

GitLab CI donne accès à de multiples variables d'environnement mais je vais surtout en retenir deux : $CI_COMMIT_REF_SLUG et $CI_COMMIT_SHA.

Avant chaque commande docker build, on va donc aller chercher les dernières builds du repository (et prévoir le fait que la référence n'existe pas en y ajoutant un || true) puis les builder en utilisant le cache des précédents builds :

Par exemple :

variables:
  IMAGE: registry.example.com/namespace/project
 
before_script:
  - echo $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin registry.example.com
 
stages:
  - build
 
build:
  stage: build
  script:
    - docker pull $IMAGE:latest || true
    - docker pull $IMAGE:$CI_COMMIT_REF_SLUG || true
    - docker build
      --cache-from $IMAGE:latest
      --cache-from $IMAGE:$CI_COMMIT_REF_SLUG
      --tag $IMAGE:$CI_COMMIT_REF_SLUG
      --tag $IMAGE:$CI_COMMIT_SHA
      .
    - docker push $IMAGE:$CI_COMMIT_REF_SLUG
    - docker push $IMAGE:$CI_COMMIT_SHA
    - ([ "$CI_COMMIT_REF_SLUG" == "master" ] && docker tag $IMAGE:$CI_COMMIT_SHA $IMAGE:latest && docker push $IMAGE:latest) || true

Et voilà ! Vos images sont maintenant taggées par branches et par le SHA de votre dernier commit !
Et si votre image est sur la branche master, on y ajoute le tag latest.

N'oubliez pas de nettoyez vos tags Docker de temps en temps pour économiser un peu de place. J'ai dans les tuyaux un script pour ça, ça arrive bientôt ;)

*[registry]: Service permettant de stocker et distribuer les images Docker *[ElasticSearch]: Base de donnée distribuée orientée recherche