Une application Multi Architecture en Golang dans Docker

Mis a jours le 20 Nov 2019 à 13:19 · 1022 mots · Lecture en 5 minutes tutoriel multiple-architecture docker golang

Ce n’est pas un exemple qui fonctionne !

Faire une application en Go et le mettre dans un docker est très important pour la portabilité.

Un Dockerfile simple pour une application:

FROM scratch
COPY static/ /srv/static/
COPY app /srv/app
CMD ["/srv/app"]

Et le docker-compose qui va avec:

version '2'
service:
  app:
    build: .
    port:
      - '80:80'

Golang compilation

J’ai récupéré sur se gist une liste des GOOS/GOARCH supportés par go:

  • darwin/386
  • darwin/amd64
  • dragonfly/amd64
  • freebsd/386
  • freebsd/amd64
  • freebsd/arm
  • linux/386
  • linux/amd64
  • linux/arm
  • linux/arm64
  • linux/ppc64
  • linux/ppc64le
  • linux/mips
  • linux/mipsle
  • linux/mips64
  • linux/mips64le
  • linux/s390x
  • nacl/386
  • nacl/amd64p32
  • nacl/arm
  • netbsd/386
  • netbsd/amd64
  • netbsd/arm
  • openbsd/386
  • openbsd/amd64
  • openbsd/arm
  • plan9/386
  • plan9/amd64
  • plan9/arm
  • solaris/amd64
  • windows/386
  • windows/amd64

On peux donc construire un script shell simple qui permet de compiler l’application:

#!/bin/bash
os_archs=( darwin/386 darwin/amd64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm linux/arm64 linux/ppc64 linux/ppc64le linux/mips linux/mipsle linux/mips64 linux/mips64le linux/s390x nacl/386 nacl/amd64p32 nacl/arm netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 openbsd/arm plan9/386 plan9/amd64 plan9/arm solaris/amd64 windows/386 windows/amd64 )
build="go build -o app"

for os_arch in "${os_archs[@]}"; do
    echo $os_arch
    GOARCH=${os_arch#*/} GOOS=${os_arch%/*} CGO_ENABLED=0 $build
done

Docker manifest

Et comme le dit la doc docker

This command is experimental on the Docker client.

(Cette commande est expérimentale sur le client docker)

Pour activer les fonctions expérimentales du client cli de Docker, il faut ajouter cette ligne "experimental": "enabled" dans le fichier de configuration Docker qui à tendance à être ici: $HOME/.docker/config.json.

Sur Windows, il faut sélectionner l’icône Docker et sélectionner Settings. Il faut ensuite sélectionner Daemon puis cliquer sur la checkbox Experimental Features pour les activer

Mais à quoi ça sert ?

La solution simple pour avoir plusieurs architectures pour une même application est d’utiliser les tags(ou repository différents, mais à éviter encore plus). Il faut donc construire une image par GOOS/GOARCH supportés. Et on aurait une liste de tags comme ça:

  • my-app/app:latest
  • my-app/app:latest-windows-amd64
  • my-app/app:XXXXXXX-windows-amd64

Soit faire:

#!/bin/bash
os_archs=( darwin/386 darwin/amd64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm linux/arm64 linux/ppc64 linux/ppc64le linux/mips linux/mipsle linux/mips64 linux/mips64le linux/s390x nacl/386 nacl/amd64p32 nacl/arm netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 openbsd/arm plan9/386 plan9/amd64 plan9/arm solaris/amd64 windows/386 windows/amd64 )
build="go build -o app"
dk_app=my-app/app
hash=$(git rev-parse --short HEAD)

for os_arch in "${os_archs[@]}"; do
    echo $os_arch

    # Compiling
    GOARCH=${os_arch#*/}
    GOOS=${os_arch%/*}
    GOARCH=${GOARCH} GOOS=${GOOS} CGO_ENABLED=0 $build

    # Building and push docker image
    dk_name="${dk_app}:${hash}-${GOOS}-${GOARCH}"
    dk_name_latest="${dk_app}:latest-${GOOS}-${GOARCH}"
    docker build -t ${dk_name} .
    docker tag ${dk_name} ${dk_name_latest}
    docker push ${dk_name} ${dk_name_latest}
done

Mais il serait mieux de ne pas remplir la liste des tags comme ça. Et cela ne permet pas d’être vraiment multi architecture: sur une machine ARM, il faut un tag différent d’une machine x86_64.

Les manifest de Docker permettent de créer une image qui soit architecture agnostique: Que l’on récupère l’image sur un ARM ou sur un x86_64, la commande est la même:

docker pull my-app/app

Par exemple, sur ma machine amd64:

$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

Et sur ma machine arm:

$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm32v7)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Où la différence se trouve juste après la deuxième étape où on peux lire:

The Docker daemon pulled the [...] image [...] (amd64) puis

The Docker daemon pulled the [...] image [...] (arm32v7).

Création du manifest

Attention, il faut avoir push les images sur un registery avant de créer le manifest.

$ docker manifest create -h
Flag shorthand -h has been deprecated, please use --help

Usage:  docker manifest create MANIFEST_LIST MANIFEST [MANIFEST...]

Create a local manifest list for annotating and pushing to a registry

Options:
  -a, --amend      Amend an existing manifest list
      --insecure   Allow communication with an insecure registry

Il faut donc faire la commande docker manifest create suivi du nom de l'image à créer et enfin suivi des noms des différentes images précédemment crées à ajouter dans le manifest.

Ce qui donne, à la main:

$ docker push my-app/app:latest-windows-amd64 my-app/app:latest-windows-arm
$ docker manifest create my-app/app:latest my-app/app:latest-windows-amd64 my-app/app:latest-windows-arm ...
Created manifest list docker.io/my-app/app:latest

Et on peux donc faire:

$ docker manifest inspect my-app/app:latest
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 42,
         "digest": "sha256:4224",
         "platform": {
            "architecture": "amd64",
            "os": "windows"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 42,
         "digest": "sha256:4224",
         "platform": {
            "architecture": "arm",
            "os": "windows"
         }
      }
   ]
}

Script Shell

#!/bin/bash
os_archs=( darwin/386 darwin/amd64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm linux/arm64 linux/ppc64 linux/ppc64le linux/mips linux/mipsle linux/mips64 linux/mips64le linux/s390x nacl/386 nacl/amd64p32 nacl/arm netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 openbsd/arm plan9/386 plan9/amd64 plan9/arm solaris/amd64 windows/386 windows/amd64 )
build="go build -o app"
dk_app=my-app/app
hash=$(git rev-parse --short HEAD)

for os_arch in "${os_archs[@]}"; do
    echo $os_arch

    # Compiling
    GOARCH=${os_arch#*/}
    GOOS=${os_arch%/*}
    GOARCH=${GOARCH} GOOS=${GOOS} CGO_ENABLED=0 $build

    # Building and push docker image
    dk_name="${dk_app}:${hash}-${GOOS}-${GOARCH}"
    dk_name_latest="${dk_app}:latest-${GOOS}-${GOARCH}"
    docker build -t ${dk_name} --platform ${GOOS} . -q
    docker tag  ${dk_name} ${dk_name_latest}
    docker push ${dk_name}
    docker push ${dk_name_latest}
    manifests_latest="${manifests_latest} ${dk_name_latest}"
    manifests="${manifests} ${dk_name}"
done


docker manifest create tommoulard/app:latest ${manifests_latest} --amend
docker manifest create tommoulard/app:${hash} ${manifests} --amend

docker manifest push tommoulard/app:latest
docker manifest push tommoulard/app:${hash}

Ce n’est pas un exemple qui fonctionne !

Image de l'auteur Tom Moulard

L'auteur:  Tom Moulard

Depuis mon enfance, je suis captivé par les articles de science et de technologie. Un jour, j'ai décidé de faire partie de ce monde : j'ai pris ma calculatrice programmable (une TI-82 stat).... La suite, sur mon site

Vous avez vu une erreur ? Quelque chose ne va pas ? Vous pouvez contribuer à cette page sur GitHub ou laisser un commentaire en dessous. Merci d'être passé par là :)