Correction d’erreurs de timeout avec Gitlab Runner, Docker-in-Docker et Kubernetes executor

Correction d’erreurs de timeout avec Gitlab Runner, Docker-in-Docker et Kubernetes executor

Publié le


Do not index
Do not index
Primary Keyword
Lié à Analyse sémantique (Articles liés) 1
Lié à Analyse sémantique (Articles liés)
Statut rédaction
A optimiser SEO
Lié à Analyse sémantique (Articles liés) 2
Chez Keltio, nous rencontrons plein de types d’infrastructures. Des simples, des complexes, mais surtout on en voit beaucoup.
Un cas d’usage qui revient souvent est d’héberger ses propres runners pour faire tourner les pipelines de CI/CD.
Pour des raisons de scalabilité, ces runners sont habituellement dans un cluster Kubernetes, et sont instanciés dynamiquement lorsqu’un nouveau job est lancé.
 
Ce matin, on travaille sur un client assez classique, un cluster Kubernetes déployé via du Terraform, et un répertoire applicatif dont la CI/CD construit puis pousse une image Docker. Le déploiement se fait automatiquement avec du ArgoCD et son extension argocd-image-updater.
 

Découverte du bug

Reprenons, la CI/CD dans le répertoire applicatif s’occupe de construire l’image Docker puis de la pousser vers un registre sur Scaleway.
Pendant la construction de cette image (une application NextJS gérée avec Yarn), une des étapes est d’installer des dépendances via apk (l’image de base est une alpine).
À cette étape, ça plante, il ne se passe plus rien, et au bout d’une heure, le runner s’arrête avec une erreur de type timeout.
 
Ici, apk attend mais jamais rien n’arrive.
Ici, apk attend mais jamais rien n’arrive.

Contexte

Pour remettre un peu de contexte autour de ce bug, revoyons les principales technologies utilisées pour construire cette image Docker.
Le répertoire Git est hébergé par Gitlab, qui s’occupe également de gérer la CI/CD. Le runner, quant à lui, est privé et hébergé dans un cluster Kubernetes sur Scaleway. Pour son déploiement, le chart Helm officiel est utilisé.
Ce runner utilise la technologie Docker-in-Docker (DinD) pour construire des images en utilisant le démon Docker d’un autre service. Ce démon Docker originel est un conteneur lancé en sidecar du runner Gitlab.

Investigation

Comme toujours avec des problèmes de réseau, on avance couche par couche. Dans un premier temps, la résolution DNS. Depuis un pod pris au hasard dans le cluster, ça marche. Depuis le conteneur du gitlab-runner, ça marche, enfin depuis le conteneur qui contient le démon Docker, ça marche aussi. Le problème ne vient donc pas de là.
Maintenant, lorsqu’on essaie de télécharger un fichier, tout se passe sans encombre sauf dans le conteneur DinD. Intéressant.
Fouillons au niveau des interfaces réseau pour trouver des choses intéressantes.
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if16760: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1480 qdisc noqueue state UP
    link/ether 5e:3e:7a:37:14:fb brd ff:ff:ff:ff:ff:ff
    inet 100.65.142.130/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::5c3e:7aff:fe37:14fb/64 scope link
       valid_lft forever preferred_lft forever
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:fc:2f:49:28 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

# docker network inspect bridge
"Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
           },
eth0 correspond à l’interface ethernet de la machine, c’est ici que se fait le transit avec l’internet “extérieur”. docker0 correspond à l’interface réseau entre le service DinD et les runners.
On constate ici que le MTU ne coïncide pas entre l’interface Docker et ethernet, c’est ici la racine de notre problème.
En recherchant des problèmes similaires sur internet, nous trouvons les liens suivants qui retombent sur le même problème :

Explication du problème

Le problème que nous rencontrons est lié à la maximum transmission unit (MTU). Cette valeur est utilisée par les différentes interfaces réseau pour définir “la taille maximale d’un paquet pouvant être transmis en une seule fois”. Voir la page Wikipédia associée.
La différence de MTU entre les différentes interfaces réseau cause le point bloquant. L’interface ethernet accepte des paquets d’une taille de 1480, tandis que l’interface Docker accepte une taille maximale de 1500.
Lors de la construction de l’image Docker, le démon va donc effectuer des requêtes en demandant des paquets d’une taille de 1500 octets au maximum, et les serveurs répondre avec des paquets de cette taille-là, sauf que l’interface ethernet n’accepte que des paquets d’une taille inférieure ou égale à 1480 octets, et donc ne les laisse pas passer.
Le démon Docker n’a donc aucune réponse, causant une erreur timeout après suffisamment de temps.
 
Les paquets sont bloqués par l’interface ethernet qui a un MTU de 1480
Les paquets sont bloqués par l’interface ethernet qui a un MTU de 1480

Solution

Maintenant que nous avons trouvé la cause de ces problèmes, la solution est toute donnée, il faut réduire le MTU du service DinD pour que les paquets puissent passer à travers l’interface ethernet. On modifie donc la déclaration du service DinD avec un MTU égal (ou inférieur) à celui de l’interface ethernet.
Pour en revenir à notre pipeline de CI/CD initiale, voilà les modifications à effectuer :
services:
  - name: docker:dind
    command: ["--mtu=1480"]

Conclusion

Dans cet article, nous avons effectué une investigation pour comprendre pourquoi la construction d’une image Docker échouait avec une erreur timeout dans le contexte d’un runner Gitlab hébergé sur un cluster Kubernetes. Nous avons cherché la cause de ce problème qui résidait dans une mauvaise configuration de l’interface réseau du démon Docker.
Après modification de ce démon pour réduire la taille maximale des paquets transmis, l’interface ethernet du cluster laisse désormais passer les paquets à destination du démon Docker et l’image se construit comme prévu lors des pipelines de CI/CD ! Une nouvelle mission de réussie avec brio, et toujours plus de connaissance emmagasinée !
 
Cet article et les thématiques abordées vous ont plus ? Contactez-nous sur Linkedin et venez travailler avec nous !
 

S'inscrire à la newsletter DevSecOps Keltio

Pour recevoir tous les mois des articles d'expertise du domaine

S'inscrire

Écrit par

Victor Franzi
Victor Franzi

Victor est notre première recrue, il est passionné depuis son plus jeune âge et ne veut jamais s’arrêter d’apprendre 🧠