Dezelfde Docker container op meerdere architecturen

Maak je gebruik van meer dan één architectuur in je project? Bijvoorbeeld een lokaal cluster van Raspberry Pi’s om je microservices te testen, en een set Azure of AWS machines om productie te draaien? Docker kan dit aan, maar tot eind 2017 moest je een aparte tag hebben voor je AMD64 en ARM images zodat je een werkende versie kon binnenhalen. In september 2017 zijn alle officiële Docker images omgezet zodat ze eenvoudig op meerdere architecturen te draaien zijn. Het is eigenlijk heel makkelijk om dit ook zelf te doen!

Hoewel het eenvoudig te doen is, vereist het wel een aantal stappen om te realiseren. Je zult nog steeds moeten zorgen dat alle containers gebouwd worden en naar de registry gepushed worden. Praktisch heb je de volgende stappen:

  1. Bouw je Docker Container voor architectuur en OS #1 en tag deze (bijv. latest-linux-amd64)
  2. Bouw je Docker Container voor architectuur en OS #2 en tag deze (bijv. latest-windows-amd64)
  3. Doe dit ook voor alle andere architecturen (i386, ARMv7, ARM64, IBM Power of een andere architectuur waar Docker op draait)
  4. Push je containers naar de registry
  5. Maak een manifest aan met alle tags
  6. Push het manifest naar de registry

Stap 1 tot en met 3 bestaan uit weinig meer dan docker build -t <tagnaam> . uitvoeren op de verschillende hosts, vergeet hierbij niet dat je iedere build wél de architectuur moet meegeven. Als je dit vergeet dan ben je constant je image aan het overschrijven, en dat schiet natuurlijk niet op. Stap 4 is docker push <tagnaam>, tot hier is het eigenlijk hetzelfde als een gewone Docker container publiceren.

Manifest aanmaken

Het aanmaken van het manifest vereist wel dat je experimentele features aanzet voor de Docker CLI. In ieder geval in Windows is hier géén GUI voor op het moment van schrijven. Voor macOS en Linux moet je het bestand ~/.docker/config.json van je lokale gebruiker aanpassen. Voor Windows is het bestand te vinden op C:\Users\<username>\.docker\config.json. In de JSON moet de key experimental toegevoegd worden met de waarde enabled, het resultaat is dan iets als:

{
    "credsStore": "wincred",
    "experimental": "enabled",
    "orchestrator": "swarm"
}

Nu je al je containers gepubliceerd hebt en je configuratie goed hebt staan kun je een manifest gaan maken. Hiervoor gebruiken we het commando docker manifest create MANIFEST_LIST MANIFEST [MANIFEST...]. In de praktijk komt dat neer op een commando zoals dit, uitgaande van een Bash shell:

docker manifest create multiarchdocker:latest \
    multiarchdocker:latest-linux-amd64 \
    multiarchdocker:latest-windows-amd64
docker manifest push

Feitelijk geef je de tag die je aan de buitenwereld wilt laten zien op als eerste argument, gevolgd door een lijst van alle tags die daaronder vallen. Docker bevat vervolgens genoeg intelligentie om zelf de besturingssystemen en architecturen uit de images te halen en in het manifest te zetten. Tot slot push je het manifest richting je registry. Nu kun je op alle architecturen waarvoor je een image hebt gemaakt de tag van je manifest pullen.

Manifest updaten

Als je later een extra architectuur toevoegt, bijvoorbeeld omdat je lokaal op een Raspberry Pi wilt testen dan is dit eenvoudig mogelijk. Zorg dat je, net zoals eerder, de container aanmaakt en pushed naar je registry. Vervolgens kun je met onderstaand commando eenvoudig het manifest bijwerken:

docker manifest create --amend multiarchdocker:latest \
    multiarchdocker:latest-linux-armhf

In dit voorbeeld pakken we het eerder aangemaakte manifest, en we geven aan dat we hier iets aan willen toevoegen door --amend als optie mee te geven. Tot slot geven we de nieuwe tag(s) mee.

Manifest inspecteren

Om te controleren dat alles goed gaat kun je op de doelmachine een docker pull van je image doen natuurlijk. Je kunt echter ook met docker manifest inspect <TAG> controleren wat er precies inzit. Als je dit doet op het hello-world image van Docker zelf krijg je het volgende terug:

{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 524,
         "digest": "sha256:d5c74e6f8efc7bdf42a5e22bd764400692cf82360d86b8c587a7584b03f51520",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 525,
         "digest": "sha256:cbeb75b6bcbdaccd2ad450c53a3dee130a859cfff2cba81e2b95e8102a070326",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v5"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 524,
         "digest": "sha256:17c4b0d75c2cd60a2cbe0c3b7e4a98dd81f3089db08317eb2667b756597f19b0",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v7"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 525,
         "digest": "sha256:5d03b9f591e8e5289c1c4433fb8674ad573e705b0f05c77fc6dc8a4b3241cbd0",
         "platform": {
            "architecture": "arm64",
            "os": "linux",
            "variant": "v8"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 527,
         "digest": "sha256:70104ad2e0cbd45dd4147f2081264f48272e9d8cac2dc58f59a034724f9281b2",
         "platform": {
            "architecture": "386",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 525,
         "digest": "sha256:af45cba7d8b2a9ea6b56e48b92b6e866a3eb7d905eaeb709fbb4f844fe869c71",
         "platform": {
            "architecture": "ppc64le",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 525,
         "digest": "sha256:15205d00669e6c8b6d9e8b03248b1d303cec308dbb49489da58a6d3b49efc804",
         "platform": {
            "architecture": "s390x",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1358,
         "digest": "sha256:0bd9af81b2037d02466fb89db8736e936d7790a65a0168305d6bd9e3322640b1",
         "platform": {
            "architecture": "amd64",
            "os": "windows",
            "os.version": "10.0.14393.2189"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1356,
         "digest": "sha256:84c9545e380fcfd7f3935b0d2e007de84467a9ef67f36ef4d4d6c4366bda2860",
         "platform": {
            "architecture": "amd64",
            "os": "windows",
            "os.version": "10.0.16299.371"
         }
      }
   ]
}

Deze gigantische waslijst bevat bijna alle platformen waarop Docker draait, waaronder drie verschillende ARM architecturen, Linux op een 32 bit Intel of AMD CPU en meer. Zoals je kunt zien heeft Windows een extra key in de JSON. Deze heeft een os.version key, welke gebruikt word om een zo goed mogelijke match te vinden. Als de os.version namelijk niet matched met de versie van het OS die je geïnstalleerd hebt dan moet Windows gebruik maken van Hyper-V isolatie, wat de performance niet ten goede komt. Dat is echter iets voor een andere blogpost.