Docker buildx multi-arch : arrêter d’expédier de mauvais binaires

Cet article vous a aidé ?

«Erreur de format d’exécution.» Le message le plus honnête que votre conteneur peut vous renvoyer. C’est l’équivalent d’exécution de regarder une porte étiquetée pousser et tirer quand même : vous avez publié le mauvais binaire pour le CPU.

Le multi-arch est censé rendre ça ennuyeux : publiez un seul tag, exécutez partout. Dans des pipelines de production réels, il est facile de publier par erreur une image réservée à amd64 sous un tag que vos nœuds arm64 vont récupérer sans problème. Ou pire : publier une liste de manifests qui prétend qu’arm64 existe alors que le contenu des couches est en réalité amd64. C’est là que vous recevez un réveil à 02:17 pour «Kubernetes ne marche plus». Ce n’est pas Kubernetes. C’est votre build.

Ce que «multi-arch» signifie vraiment (et ce que ce n’est pas)

Le multi-arch dans Docker n’est pas magique. C’est un artifice de packaging : un seul tag d’image pointe vers une liste de manifests (a.k.a. «index») qui contient un manifeste d’image par plateforme. Le registre sert la bonne entrée en fonction de la plateforme demandée par le client.

Voilà tout. Le registre ne valide pas vos binaires. Docker ne lit pas vos en-têtes ELF en disant «hmm, suspect». Si vous poussez un manifeste arm64 qui référence une couche amd64, vous publierez un mensonge avec succès. Le runtime ne se plaindra que lorsque le noyau refusera de l’exécuter.

Termes clés à ne plus balayer d’un revers de main

  • Plateforme : OS + architecture (+ variante optionnelle). Exemple : linux/amd64, linux/arm64, linux/arm/v7.
  • Manifeste : JSON décrivant une image spécifique pour une plateforme : config + couches.
  • Liste de manifests / index : JSON mappant plateformes et manifests. C’est ce que vous voulez que votre tag soit quand vous dites «multi-arch».
  • BuildKit : Le moteur de build derrière docker buildx. Il fait le gros du travail : cross-building, cache, export, et builders distants.
  • Émulation QEMU : Moyen d’exécuter des binaires non natifs pendant le build. Pratique, plus lent, et parfois subtilement cassé.

Si vous n’en retenez qu’une chose : le multi-arch concerne la publication de métadonnées correctes et d’octets corrects. Il vous faut les deux.

Faits rapides et historique qui comptent vraiment

  1. Les «manifest lists» Docker sont arrivées des années après la popularité des images Docker. Docker au début n’avait pas d’histoire multi-arch native ; les gens utilisaient des tags séparés comme :arm et :amd64 en espérant ne pas se tromper.
  2. Les specs OCI ont standardisé ce que les registres stockent. La plupart des registres modernes stockent des manifests compatibles OCI même si vous les appelez «images Docker». Les formes JSON sont bien définies ; c’est votre outillage qui varie.
  3. BuildKit a été un virage architectural majeur. Le docker build classique était lié au daemon et pas conçu pour des builds cross-plateformes à grande échelle. BuildKit a rendu les builds plus parallèles, cacheables et exportables.
  4. La victoire d’arm64 dans les serveurs a changé le modèle de menace. Avant c’était des cartes hobbyistes. Maintenant ce sont des instances cloud mainstream. Tirer une image amd64 sur un nœud arm64 n’est plus «rare».
  5. Kubernetes multi-arch n’est pas spécial. Il se repose sur la même négociation de manifest du registre que Docker. Si votre tag est faux, Kubernetes tirera la mauvaise variante plus vite que vous ne pouvez dire «rollout restart».
  6. Les différences entre musl d’Alpine et glibc de Debian mordent plus fort sous émulation. QEMU peut masquer des incompatibilités d’ABI jusqu’au runtime, et alors vous obtenez des crashs qui ressemblent à des bugs applicatifs.
  7. La «variante» compte pour certaines cibles ARM. linux/arm/v7 vs linux/arm/v6 n’est pas une discussion futile. Si vous publiez la mauvaise variante, le binaire peut s’exécuter puis planter de manière étrange.
  8. Les registres ne valident généralement pas la conformité de la plateforme. Ils stockent ce que vous poussez. Traitez le registre comme un stockage durable, pas comme une porte de contrôle qualité.

Le multi-arch est assez mature pour être ennuyeux—si vous le construisez comme un SRE qui a déjà été brûlé.

Comment vous en arrivez à expédier de mauvais binaires

1) Vous avez poussé une image mono-arch sous un tag «universel»

Quelqu’un a exécuté docker build -t myapp:latest . sur un laptop amd64 et l’a poussée. Vos nœuds arm64 récupèrent :latest, obtiennent des couches amd64, et échouent avec exec format error.

C’est le classique. Ça arrive encore en 2026 parce que les gens ont encore des doigts.

2) Vous avez construit multi-arch, mais seule une plateforme a réellement réussi

Buildx peut construire plusieurs plateformes dans une seule commande. Il peut aussi produire silencieusement des résultats partiels si vous n’êtes pas strict sur les échecs en CI et si vous ne vérifiez pas la liste de manifests ensuite.

3) Votre Dockerfile «utilement» télécharge le mauvais binaire précompilé

La forme la plus courante d’expédition d’un mauvais arch n’est pas la compilation. C’est curl. Un Dockerfile qui fait :

  • curl -L -o tool.tgz ...linux-amd64... quel que soit la plateforme
  • ou qui utilise un script d’installation qui par défaut choisit amd64
  • ou qui suppose que uname -m dans le conteneur de build est l’arch cible

Sous émulation, uname -m peut renvoyer ce que vous attendez, jusqu’à ce que non. En cross-compilation, il peut renvoyer l’architecture de l’environnement de build, pas celle de la cible.

4) Vous avez compté sur QEMU pour des builds «ça ira» et vous avez obtenu «ça ira parfois»

QEMU est super pour progresser. Il n’est pas super pour prétendre être identique à l’exécution native. Certains écosystèmes linguistiques (sans citer de noms) font une détection d’architecture pendant le build. Sous émulation, la détection peut être erronée, lente ou instable.

5) Votre cache a mélangé les architectures et vous ne l’avez pas remarqué

Les caches de build sont adressés par contenu, mais votre logique de build peut quand même produire une contamination cross-arch si vous écrivez dans des chemins partagés, réutilisez des artefacts entre étapes, ou récupérez «latest» sans pinner par arch.

6) Vos runners CI sont multi-arch et vos hypothèses sont mono-arch

Quand votre parc comprend des runners amd64 et arm64, «build where available» devient «ship whatever it built». Le multi-arch exige de l’explicitude : plateformes, provenance et vérification.

Une citation à garder en tête pendant ce travail : L’espoir n’est pas une stratégie. (idée paraphrasée, souvent attribuée aux ingénieurs et opérateurs en fiabilité)

Blague #1 : Les images multi-arch sont comme des adaptateurs secteur—tout semble compatible jusqu’à ce que vous branchiez réellement.

Mode opératoire de diagnostic rapide

Quand quelque chose échoue en production, vous n’obtenez pas de points pour une théorie élégante. Vous en obtenez pour rétablir le service et empêcher une récidive. Voici le chemin le plus rapide que je connaisse.

Premier point : prouver ce qui a été récupéré

  1. Vérifiez l’architecture du nœud. Si le nœud est arm64 et que l’image est seulement amd64, arrêtez-vous là.
  2. Inspectez la liste de manifests du tag. Est-ce que le tag inclut vraiment la plateforme que vous pensez ?
  3. Résolvez le digest réellement récupéré. Les tags bougent. Les digests non. Trouvez le digest que le runtime utilise.

Deuxième point : validez les octets à l’intérieur de l’image

  1. Démarrez un conteneur de debug et inspectez un binaire avec file. S’il indique x86-64 sur une cible arm64, vous avez trouvé la pièce à conviction.
  2. Vérifiez le chargeur dynamique / attentes libc. L’architecture erronée est évidente. L’ABI erronée peut être plus sournoise.

Troisième point : retracez la source de vérité du build

  1. Examinez le Dockerfile pour des téléchargements. Tout ce qui récupère des artefacts précompilés doit être conscient de la plateforme.
  2. Vérifiez les logs buildx pour les étapes par plateforme. Un seul badge de job vert peut cacher un build partiel.
  3. Vérifiez la configuration QEMU/binfmt si l’émulation est impliquée. Si votre build dépend de l’émulation, traitez-la comme une dépendance avec des health checks.

Goulot d’étranglement le plus courant

Ce n’est pas BuildKit. C’est l’absence de vérification dans votre pipeline. Le build «a fonctionné» et a publié un tag cassé. Le système a fait exactement ce que vous lui avez demandé — et c’est le problème.

Tâches pratiques : commandes, sorties, décisions

Ce ne sont pas des commandes «jouet». Ce sont celles que vous exécutez pendant un incident, et celles que vous automatisez ensuite pour éviter les incidents.

Tâche 1 : Confirmez l’architecture de l’hôte (ne devinez pas)

cr0x@server:~$ uname -m
aarch64

Ce que cela signifie : Le nœud est ARM 64-bit. Il attend des images linux/arm64.

Décision : Si le tag d’image n’annonce pas linux/arm64, vous êtes déjà en zone «mauvais binaire».

Tâche 2 : Voir quelles plateformes votre tag revendique

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:latest
Name:      myorg/myapp:latest
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:8c9c2f7b4f8a5b0d5c0a2b1e9c3d1a6e2f4b7a9c0d1e2f3a4b5c6d7e8f9a0b1c

Manifests:
  Name:      myorg/myapp:latest@sha256:111...
  Platform:  linux/amd64
  Name:      myorg/myapp:latest@sha256:222...
  Platform:  linux/arm64

Ce que cela signifie : Le tag est un index multi-arch contenant des variantes amd64 et arm64.

Décision : Si votre plateforme est absente ici, corrigez la publication d’abord. Si elle est présente, passez à la vérification du contenu de l’image arm64.

Tâche 3 : Inspecter la liste de manifests avec la CLI Docker (vue alternative)

cr0x@server:~$ docker manifest inspect myorg/myapp:latest | head -n 20
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.oci.image.index.v1+json",
   "manifests": [
      {
         "mediaType": "application/vnd.oci.image.manifest.v1+json",
         "digest": "sha256:111...",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.oci.image.manifest.v1+json",
         "digest": "sha256:222...",

Ce que cela signifie : Vous regardez la cartographie brute des plateformes.

Décision : En déboguant des bizarreries de registre, le JSON ne ment pas. Si la liste de manifests n’inclut pas votre plateforme, arrêtez de blâmer Kubernetes.

Tâche 4 : Forcer la récupération d’une plateforme spécifique localement

cr0x@server:~$ docker pull --platform=linux/arm64 myorg/myapp:latest
latest: Pulling from myorg/myapp
Digest: sha256:222...
Status: Downloaded newer image for myorg/myapp:latest

Ce que cela signifie : Vous avez demandé la variante arm64 et obtenu un digest spécifique.

Décision : Utilisez ce digest pour une inspection plus approfondie. Si le pull échoue avec «no matching manifest», votre publication multi-arch est incomplète.

Tâche 5 : Confirmer les métadonnées d’architecture dans la config de l’image

cr0x@server:~$ docker image inspect myorg/myapp:latest --format '{{.Architecture}} {{.Os}}'
arm64 linux

Ce que cela signifie : Les métadonnées locales de l’image indiquent arm64.

Décision : Bon signe, pas une preuve. Ensuite, vérifiez les fichiers exécutables réels.

Tâche 6 : Vérifier l’architecture du binaire à l’intérieur du conteneur

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:latest -c 'file /usr/local/bin/myapp'
/usr/local/bin/myapp: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, stripped

Ce que cela signifie : Les octets sont réellement arm64.

Décision : S’il indique «x86-64», vous avez une étape de build qui a produit le mauvais binaire (généralement une logique de téléchargement). Corrigez le Dockerfile, pas le registre.

Tâche 7 : Reproduire en sécurité la signature de l’échec («exec format error»)

cr0x@server:~$ docker run --rm --platform=linux/arm64 myorg/myapp:latest /usr/local/bin/myapp --version
myapp 2.7.1

Ce que cela signifie : La variante arm64 s’exécute et affiche la version.

Décision : Si vous obtenez exec format error, le runtime tente d’exécuter la mauvaise arch (ou le fichier n’est pas exécutable pour cet OS/arch). Revenez à la Tâche 6.

Tâche 8 : Vérifiez quel builder vous utilisez (et s’il est sain)

cr0x@server:~$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS    BUILDKIT   PLATFORMS
default         docker
  default       unix:///var/run/docker.sock running   v0.12.5    linux/amd64
multiarch       docker-container
  multiarch0    unix:///var/run/docker.sock running   v0.12.5    linux/amd64,linux/arm64,linux/arm/v7

Ce que cela signifie : Le builder «default» ne peut faire que amd64 ; le builder docker-container supporte plusieurs plateformes (probablement via binfmt/QEMU).

Décision : Utilisez une instance de builder dédiée pour le multi-arch. Ne faites pas confiance à «default» à moins d’aimer les surprises.

Tâche 9 : Créer et sélectionner un builder multi-arch approprié

cr0x@server:~$ docker buildx create --name multiarch --driver docker-container --use
multiarch

Ce que cela signifie : Buildx lancera un conteneur BuildKit pour un comportement cohérent et des fonctionnalités multi-plateforme.

Décision : En CI, créez/touchez toujours un builder nommé. Cela rend les builds reproductibles et débogables.

Tâche 10 : Vérifier l’enregistrement binfmt/QEMU sur l’hôte

cr0x@server:~$ docker run --privileged --rm tonistiigi/binfmt --info
Supported platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6
Enabled platforms: linux/amd64, linux/arm64, linux/arm/v7

Ce que cela signifie : Le noyau a des handlers binfmt activés pour ces architectures.

Décision : Si votre plateforme cible n’est pas activée, les builds multi-arch nécessitant l’émulation échoueront ou seront silencieusement sautés. Activez la plateforme ou basculez vers des builders natifs par arch.

Tâche 11 : Construire et pousser une image multi-arch (la bonne façon)

cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 -t myorg/myapp:2.7.1 --push .
[+] Building 142.6s (24/24) FINISHED
 => [internal] load build definition from Dockerfile                                  0.0s
 => => transferring dockerfile: 2.12kB                                                0.0s
 => [linux/amd64] exporting to image                                                  8.1s
 => => pushing layers                                                                 6.7s
 => [linux/arm64] exporting to image                                                  9.4s
 => => pushing layers                                                                 7.5s
 => exporting manifest list                                                           1.2s
 => => pushing manifest list                                                          0.6s

Ce que cela signifie : Vous avez construit les deux plateformes et poussé une liste de manifests.

Décision : Si vous ne voyez pas «exporting manifest list», vous n’avez probablement pas poussé un index multi-arch. Corrigez cela avant de vous déclarer victorieux.

Tâche 12 : Valider que le tag poussé est un index, pas un manifeste unique

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:2.7.1 | sed -n '1,20p'
Name:      myorg/myapp:2.7.1
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:4aa...

Manifests:
  Name:      myorg/myapp:2.7.1@sha256:aaa...
  Platform:  linux/amd64
  Name:      myorg/myapp:2.7.1@sha256:bbb...
  Platform:  linux/arm64

Ce que cela signifie : Le registre stocke maintenant un véritable index multi-arch pour ce tag.

Décision : Gatez votre pipeline sur cette vérification. Si ce n’est pas un index, faites échouer le build.

Tâche 13 : Diagnostiquer un Dockerfile qui télécharge le mauvais asset

cr0x@server:~$ docker buildx build --platform=linux/arm64 --progress=plain --no-cache .
#10 [linux/arm64 6/9] RUN curl -fsSL -o /usr/local/bin/helm.tgz "https://example/helm-linux-amd64.tgz"
#10 0.9s curl: (22) The requested URL returned error: 404

Ce que cela signifie : Votre Dockerfile a hardcodé amd64 (et ça échoue sur arm64, ce qui est clément). Souvent ça ne renverra pas 404 et «fonctionnera» tout en expédiant les mauvais octets.

Décision : Remplacez l’architecture codée en dur par les args fournis par BuildKit (TARGETARCH, TARGETOS, TARGETVARIANT) et mappez-les aux noms upstream.

Tâche 14 : Utiliser correctement les arguments de plateforme fournis par BuildKit

cr0x@server:~$ docker buildx build --platform=linux/arm64 --progress=plain --no-cache -t test/myapp:arm64 .
#5 [linux/arm64 3/7] RUN echo "TARGETOS=$TARGETOS TARGETARCH=$TARGETARCH TARGETVARIANT=$TARGETVARIANT"
#5 0.1s TARGETOS=linux TARGETARCH=arm64 TARGETVARIANT=

Ce que cela signifie : BuildKit injecte des valeurs de plateforme cible ; vous devriez baser téléchargements/compilation sur celles-ci, pas sur uname.

Décision : Si votre Dockerfile utilise uname -m pour choisir des binaires, vous êtes à une refactorisation d’un gros problème. Passez aux args BuildKit.

Tâche 15 : Confirmer la plateforme du conteneur en cours d’exécution dans Kubernetes (depuis un nœud)

cr0x@server:~$ kubectl get node -o wide | head -n 3
NAME           STATUS   ROLES    AGE   VERSION   INTERNAL-IP   OS-IMAGE             KERNEL-VERSION
worker-arm01   Ready    <none>   55d   v1.29.1   10.0.3.21     Ubuntu 22.04.3 LTS    5.15.0-91-generic
worker-x86a01  Ready    <none>   55d   v1.29.1   10.0.2.14     Ubuntu 22.04.3 LTS    5.15.0-91-generic

Ce que cela signifie : Vous avez un cluster à architectures mixtes (devenu courant). L’ordonnancement placera les pods sur l’un ou l’autre sauf contrainte.

Décision : Si l’image n’est pas vraiment multi-arch, vous devez pinner selectors/affinités de nœud temporairement, sinon vous aurez la roulette «ça marche sur un nœud».

Tâche 16 : Identifier le digest d’image réellement utilisé par un pod

cr0x@server:~$ kubectl get pod myapp-5f7f6d9c7b-2qk4p -o jsonpath='{.status.containerStatuses[0].imageID}{"\n"}'
docker-pullable://myorg/myapp@sha256:bbb...

Ce que cela signifie : Le pod a résolu le tag en digest (immuable). Ce digest correspond à un manifeste de plateforme.

Décision : Comparez ce digest avec votre liste de manifests. S’il pointe vers l’entrée d’une mauvaise plateforme, vos métadonnées de registre ou la négociation de plateforme client est défaillante.

Tâche 17 : Déboguer le comportement du cache BuildKit (et arrêter d’«optimiser» aveuglément)

cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 \
  --cache-to=type=registry,ref=myorg/myapp:buildcache,mode=max \
  --cache-from=type=registry,ref=myorg/myapp:buildcache \
  -t myorg/myapp:ci-test --push .
[+] Building 98.3s (24/24) FINISHED
 => importing cache manifest from myorg/myapp:buildcache                               2.4s
 => [linux/amd64] CACHED                                                               0.6s
 => [linux/arm64] CACHED                                                               0.8s
 => exporting manifest list                                                            1.1s

Ce que cela signifie : Le cache a été réutilisé pour les deux plateformes, et une liste de manifests a été exportée.

Décision : Si vous voyez une plateforme mise en cache et l’autre reconstruite à zéro à chaque fois, l’ordre du Dockerfile ou les étapes conditionnelles par plateforme annihilent la réutilisation du cache.

Tâche 18 : Attraper en CI le «mono-arch qui se fait passer pour multi-arch»

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:ci-test | grep -E 'MediaType|Platform'
MediaType: application/vnd.oci.image.index.v1+json
  Platform:  linux/amd64
  Platform:  linux/arm64

Ce que cela signifie : Votre tag est un index et inclut les deux plateformes.

Décision : Faites-en une étape requise de pipeline. Si cela affiche application/vnd.oci.image.manifest.v1+json à la place, faites échouer le job.

Une stratégie de build qui se comporte en CI

La stratégie correcte dépend de ce que vous construisez. Les langages compilés se comportent différemment des builds «curl un binaire dans /usr/local/bin». Mais les principes sont cohérents :

Principe 1 : Privilégiez les builds natifs par architecture quand vous le pouvez

Si vous pouvez exécuter un runner arm64 et un runner amd64, faites-le. Les builds natifs évitent les surprises d’émulation et sont généralement plus rapides pour des compilations lourdes.

Cela ne signifie pas que vous ayez besoin de deux pipelines totalement séparés. Cela signifie que vous devriez considérer une topologie de builders :

  • Builders distants par arch (BuildKit supporte ce pattern)
  • Ou jobs CI séparés qui poussent des images par-arch puis publient une liste de manifests

Principe 2 : Si vous utilisez QEMU, traitez-le comme une infrastructure de production

QEMU via binfmt n’est pas «juste une commodité développeur» une fois qu’il est en CI. Il peut casser après des mises à jour noyau, Docker, ou des renforcements de sécurité. Surveillez-le et validez-le.

Principe 3 : Rendez le Dockerfile conscient de la plateforme sans être trop malin

Utilisez les args de BuildKit. Ils existent pour une raison :

  • TARGETOS, TARGETARCH, TARGETVARIANT
  • BUILDOS, BUILDARCH pour l’environnement de build

Puis mappez explicitement aux noms des fournisseurs. Beaucoup d’upstreams utilisent x86_64 au lieu de amd64. Ou aarch64 au lieu de arm64. Ne devinez pas ; mappez.

Principe 4 : Séparez «récupérer les outils» de «construire l’app»

Plus vous mélangez les responsabilités, plus vous risquez la pollution du cache et les confusions de plateforme. Un pattern propre :

  • Stage A : récupérer ou construire les outils spécifiques à la plateforme (clé selon l’arch cible)
  • Stage B : builder votre application (cross-compiler si approprié)
  • Stage C : image runtime minimale (copier exactement ce dont vous avez besoin)

Principe 5 : Toujours vérifier le tag publié

La vérification est peu coûteuse. Les incidents coûtent cher. Après le push, inspectez la liste de manifests, pull chaque variante plateforme, et vérifiez le binaire principal avec file. Automatisez cela.

Blague #2 : Si vous ne vérifiez pas le multi-arch, vous jouez essentiellement à la loterie cross-platform où le prix est une panne.

Trois mini-histoires d’entreprise (anonymisées, douloureusement familières)

Mini-histoire 1 : L’incident causé par une fausse hypothèse

Une entreprise de taille moyenne a migré une partie de ses nœuds Kubernetes vers arm64 pour réduire les coûts. L’équipe plateforme a fait la chose raisonnable : tainté les nouveaux nœuds, migré d’abord les services à faible risque, et surveillé les taux d’erreur. Ça avait l’air correct pendant une semaine.

Puis un redeploy routinier d’une API interne «ennuyeuse» a commencé à échouer seulement sur les nœuds arm. L’on-call a vu des crash loops avec exec format error. Ils ont supposé que c’était un problème d’image de base et ont rollbacké la mise à jour du cluster qu’ils venaient de faire. Rien n’a changé. Ils ont rollbacké l’application. Toujours cassé sur arm.

La mauvaise hypothèse était simple : «Nous utilisons buildx, donc nos images sont multi-arch.» Ils utilisaient bien buildx. Mais le pipeline avait poussé un tag depuis un job docker build unique qui s’exécutait sur un runner amd64. Un autre job produisait arm64 pour les tests, mais ne l’a jamais poussé.

La correction fut tout aussi simple : faire de la publication d’une liste de manifests le seul chemin vers :latest et les tags de release, et gateer le pipeline avec imagetools inspect plus une vérification binaire. La leçon plus profonde était culturelle : aucun tag ne devrait exister sans étape de vérification, même si l’équipe «sait» comment ça marche.

Mini-histoire 2 : L’optimisation qui s’est retournée contre eux

Une autre organisation avait un build lent et a décidé de «l’accélérer» en mettant en cache agressivement et en réutilisant un répertoire d’artefacts partagé entre les builds. Ils ont monté un volume de cache dans le builder et stocké des sorties compilées keyées seulement par le SHA du commit. Ça a réduit le temps de build dramatiquement—pour un temps.

Puis les images arm64 ont commencé à échouer avec des segfaults dans une librairie crypto pendant les poignées de main TLS. Pas immédiatement. Seulement sous charge. Les images x86 allaient bien. Tout le monde a suspecté un bug de compilateur, puis une régression noyau, puis des rayons cosmiques.

La cause racine était banale et rageante : le répertoire de cache partagé contenait des objets compilés pour amd64 qui étaient copiés dans l’étape de build arm64 parce que les scripts utilisaient la logique «si le fichier existe, réutilise-le». Sous QEMU, certaines étapes de build étaient sautées et le cache court-circuitait la compilation de la pire manière. Les métadonnées d’image indiquaient toujours arm64, mais les octets étaient mélangés.

Ils ont corrigé cela en scindant les caches par plateforme et en refusant de partager des répertoires d’artefacts opaques entre plateformes. Le cache registre de BuildKit a résolu le problème initial sans le raccourci dangereux. L’«optimisation» avait créé un problème de chaîne d’approvisionnement cross-architecture à l’intérieur de leur pipeline.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une entreprise du secteur financier avait un contrôle des changements strict et un processus de release un peu pénible. Les ingénieurs se plaignaient des étapes supplémentaires : publier les images par digest, enregistrer les manifests, et garder un petit «log preuve de release» pour chaque déploiement. Ça ressemblait à de la paperasse. Ça signifiait aussi que leurs ingénieurs on-call dormaient mieux.

Pendant une semaine chargée, un service a commencé à planter seulement sur un sous-ensemble de nœuds après un rebuild d’image routinier. Cette fois, la réponse à l’incident fut presque ennuyeuse. Ils ont récupéré le digest imageID du pod, l’ont mis en correspondance avec la liste de manifests, et ont vu immédiatement que le digest arm64 référençait des couches construites deux jours plus tôt. Le digest amd64 était neuf.

Le pipeline de build avait partiellement échoué pour pousser les couches arm64 à cause d’un problème transitoire de permissions sur le registre. Le job avait tout de même publié le tag. Mais parce que l’équipe déploie toujours par digest en production et enregistre systématiquement la cartographie tag → digest → plateforme, ils ont pu rapidement pinner le digest connu bon pour les deux plateformes et rétablir le service pendant qu’ils corrigeaient la CI.

La pratique «ennuyeuse»—promotion par digest et vérification des manifests—n’a pas empêché l’erreur. Elle a réduit le rayon d’explosion et accéléré le diagnostic. Voilà la vraie victoire.

Erreurs courantes : symptôme → cause racine → correction

1) Symptom: exec format error au démarrage du conteneur

Cause racine : Binaire d’une autre architecture dans l’image, ou le tag pointe vers le mauvais manifeste de plateforme.

Correction : Inspectez le tag avec docker buildx imagetools inspect. Pull la plateforme voulue avec docker pull --platform=.... Confirmez l’arch du binaire avec file. Corrigez les téléchargements dans le Dockerfile pour utiliser TARGETARCH.

2) Symptom: Ça marche sur les nœuds amd64, plante sur arm64, mais pas d’exec format error

Cause racine : Composants userland mixtes (plugins, libs partagées) ou incompatibilité d’ABI (musl vs glibc), souvent introduits par des scripts «download latest».

Correction : Vérifiez la sortie file pour chaque binaire copié, pas seulement le principal. Pinner les images de base et versions d’outils. Privilégiez les paquets de distro ou compilez depuis la source par arch.

3) Symptom: Le tag revendique le support arm64, mais le pull arm64 dit «no matching manifest»

Cause racine : Vous avez poussé un manifeste mono-arch, pas une liste de manifests ; ou vous avez écrasé le tag avec un push mono-arch.

Correction : Autorisez la publication de tags uniquement depuis docker buildx build --platform ... --push. Utilisez les permissions CI pour empêcher les pushes manuels sur les tags de release.

4) Symptom: Les builds multi-arch sont douloureusement lents

Cause racine : Émulation QEMU faisant de la compilation lourde ; pas de cache ; Dockerfile qui invalide trop souvent le cache.

Correction : Passez à des builders natifs par arch ou au cross-compile quand c’est approprié. Ajoutez un cache-backed registry. Réordonnez le Dockerfile pour maximiser les hits de cache.

5) Symptom: Le build arm64 «réussit» mais au runtime le chargeur est introuvable

Cause racine : Vous avez copié un binaire dynamiquement lié contre glibc dans une image musl (ou inversement), ou vous avez copié seulement le binaire sans les libs requises.

Correction : Utilisez une famille d’image de base cohérente entre les stages, ou build statique quand approprié. Validez avec ldd (si disponible) ou inspectez avec file et vérifiez le chemin de l’interpréteur.

6) Symptom: La CI affiche vert, mais la prod récupère un digest plus ancien pour une architecture

Cause racine : Push partiel ; liste de manifests mise à jour incorrectement ; étape cache ou push échouée pour une plateforme tandis que l’autre a réussi.

Correction : Gatez sur la vérification du manifest et exigez que les deux plateformes soient présentes et fraîchement construites. Envisagez de pousser d’abord des tags par-arch, puis de créer explicitement la liste de manifests.

7) Symptom: Docker Compose exécute la mauvaise arch localement

Cause racine : L’environnement local par défaut utilise une plateforme ; Compose peut builder localement pour l’arch de l’hôte à moins que platform: soit défini ou que vous n’utilisiez buildx correctement.

Correction : Définissez explicitement platform dans Compose pour tester, ou pull avec --platform. Ne considérez pas le comportement de Compose comme une preuve que votre tag de registre est correct.

Checklists / plan étape par étape

Étape par étape : durcir un pipeline de release multi-arch

  1. Décidez explicitement des plateformes cibles. Pour la plupart des services back-end : linux/amd64 et linux/arm64. Ajoutez linux/arm/v7 seulement si vous le supportez vraiment.
  2. Créez un builder nommé en CI. Utilisez le driver docker-container pour un comportement BuildKit cohérent.
  3. Installez/vérifiez binfmt si vous comptez sur l’émulation. Exécutez la commande d’info binfmt et assurez-vous que les plateformes cibles sont activées.
  4. Rendez le Dockerfile conscient de la plateforme. Remplacez la logique uname -m par un mapping TARGETARCH.
  5. Construisez et poussez multi-arch en une seule opération. Utilisez docker buildx build --platform=... --push.
  6. Vérifiez que le tag publié est un index. Faites échouer le pipeline si le media type n’est pas un index OCI.
  7. Vérifiez le binaire principal de chaque variante plateforme. Pull par plateforme et exécutez file sur le binaire d’entrée.
  8. Promouvez par digest vers la production. Déployez des digests immuables ; gardez des tags pour les humains.
  9. Surveillez le skew de plateforme. Dans les clusters mixtes, surveillez les crash loops corrélés à l’architecture des nœuds.
  10. Restreignez qui peut pousser les tags de release. Empêchez les «fix rapides» qui contournent la vérification.

Checklist : revue de Dockerfile pour la correction multi-arch

  • Des curl/wget ? Si oui, l’URL varie-t-elle selon TARGETARCH ?
  • Des scripts d’installation ? Supportent-ils explicitement arm64 ou par défaut choisissent-ils amd64 ?
  • Des artefacts compilés copiés d’un autre stage ? Les stages sont-ils construits pour le même --platform ?
  • Utilisez-vous uname ou dpkg --print-architecture ? Êtes-vous sûr qu’ils interrogent la cible, pas l’environnement de build ?
  • Verrouillez-vous les versions et checksums par arch ? Sinon, vous faites confiance à Internet en stéréo.

Checklist : gates de vérification de release (sécurité minimale viable)

  • imagetools inspect montre un index OCI et inclut toutes les plateformes requises.
  • Le pull par plateforme réussit.
  • Le conteneur par plateforme exécute --version (ou une commande de santé légère).
  • L’inspection binaire via file correspond à l’architecture attendue.

FAQ

1) Ai-je toujours besoin de QEMU pour construire des images multi-arch ?

Non. Si votre build est purement cross-compilation (par exemple Go avec les bons réglages), vous pouvez construire pour d’autres architectures sans exécuter de binaires étrangers. QEMU est nécessaire quand des étapes de build exécutent des binaires de l’arch cible pendant le build.

2) Quelle est la différence entre docker build et docker buildx build ici ?

buildx utilise les fonctionnalités de BuildKit : sorties multi-plateforme, exporteurs de cache avancés, et builders distants. Le docker build classique est typiquement mono-plateforme et lié à l’architecture du daemon local.

3) Pourquoi le tag affiche le support arm64 mais le binaire est toujours amd64 ?

Parce que les manifests sont des métadonnées. Vous pouvez publier un manifeste qui dit «arm64» alors que la couche contient un binaire amd64. Cela arrive généralement via des téléchargements codés en dur ou une contamination du cache cross-arch.

4) Dois-je construire les deux architectures dans un seul job ou dans des jobs séparés ?

Si vous avez des runners natifs fiables pour chaque arch, des jobs séparés peuvent être plus rapides et déterministes. Si vous comptez sur QEMU, une invocation buildx unique est plus simple mais peut être plus lente. Dans tous les cas, publiez une liste de manifests unique et vérifiez-la.

5) Puis-je «corriger» un tag existant qui est erroné ?

Vous pouvez repusher le tag pour pointer vers une liste de manifests corrigée, mais les caches et rollouts ont peut-être déjà récupéré le digest cassé. En production, préférez déployer par digest afin que la correction soit une décision de rollout explicite, pas une surprise de tag.

6) Pourquoi Kubernetes récupère parfois la mauvaise architecture ?

Généralement il ne le fait pas. Il récupère ce que la négociation de manifeste fournit pour la plateforme du nœud. Quand «il récupère la mauvaise», c’est généralement parce que le tag n’était pas un index multi-arch adéquat, ou que l’entrée de l’index référence le mauvais contenu.

7) Et linux/arm/v7—devrais-je le supporter ?

Seulement si vous avez vraiment des clients sur ARM 32-bit. Le supporter augmente la complexité de build, la taille de la matrice de tests, et le risque d’erreurs de variante. Ne l’ajoutez pas comme trophée.

8) Comment rendre les «outils téléchargés» sûrs sur toutes les architectures ?

Utilisez un mapping basé sur TARGETARCH, pinner les versions, et validez les checksums par architecture. Mieux : utilisez les paquets de la distro ou compilez depuis la source quand c’est pratique.

9) Est-ce acceptable d’utiliser :latest pour le multi-arch ?

C’est acceptable pour la commodité développeur, pas acceptable comme contrat de production. Utilisez des digests immuables ou des tags versionnés pour les releases, et traitez :latest comme un pointeur mouvant.

10) Quel est le meilleur garde-fou unique pour empêcher les mauvais binaires ?

Un job de vérification post-push qui (a) confirme que le tag est un index OCI avec les plateformes requises, et (b) valide l’architecture du binaire d’entrée avec file pour chaque plateforme.

Conclusion : prochaines étapes à faire cette semaine

Si vous gérez des flottes à architectures mixtes—or vous le ferez bientôt—le multi-arch n’est pas un ornement optionnel. C’est une hygiène de release basique. Le registre stockera volontiers vos erreurs, et Kubernetes les déploiera à l’échelle.

  1. Ajoutez une gate de vérification : après le push, exigez que docker buildx imagetools inspect affiche un index OCI avec toutes les plateformes.
  2. Validez les octets : pour chaque plateforme, pull et exécutez file sur l’exécutable principal.
  3. Corrigez les téléchargements dans le Dockerfile : remplacez les assets amd64 codés en dur par un mapping TARGETARCH.
  4. Choisissez une stratégie de builder : builders natifs par arch si possible ; QEMU si nécessaire—puis monitorisez-le comme toute autre dépendance.
  5. Déployez par digest : rendez les rollouts explicites, réversibles et traçables.

Le multi-arch bien fait est silencieux. C’est l’objectif : pas de gestes héroïques, pas de crashs mystérieux, pas de «ça marche sur mon nœud». Juste des binaires corrects, à chaque fois.

← Précédent
Mises à jour sans interruption avec Docker Compose : mythe, réalité et modèles qui fonctionnent
Suivant →
Évolution de la VRAM : du GDDR simple à l’absurdité pure

Laisser un commentaire