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.
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.
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 !
Écrit par
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 🧠
Sujets