Dockerの「マルチCPUアーキテクチャ」に対応したイメージをビルドする

またまたDockerの「マルチ〇〇〇〇」についてのお話です。
2020.05.22

みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。

前回、前々回と、ARMアーキテクチャの「Graviton2」と Docker のネタをお送りしましたが、今回は第3弾をお届けします。

前回のブログ記事 の中で、Docker Hubの「マルチCPUアーキテクチャサポート」について紹介しました。

前回は 「マルチCPUアーキテクチャ」に対応しているDocker Hubの公式イメージをプルして利用する という流れでした。

今回は一歩進んで 「マルチCPUアーキテクチャ」に対応するDockerイメージを自分でビルドしてDocker Hub上に公開する という一連の作業を試してみたいと思います。

「マルチCPUアーキテクチャサポート」とは?

Docker Hubにおける「マルチCPUアーキテクチャサポート」とは、x86 (AMD64) やARM64など複数のアーキテクチャ向けのイメージを同一のイメージ名・タグ名で管理することができる仕組みです。

Leverage multi-CPU architecture support | Docker Documentation

Docker Hubで公開されている公式イメージの多くは「マルチCPUアーキテクチャ」に対応しています。

公開されているイメージが「マルチCPUアーキテクチャ」に対応している場合、Docker Hubからプルを実行した際に、特にアーキテクチャを明示しなくても自動的に適切なアーキテクチャのイメージがダウンロードされます。

例えば、次のようなDockerfileを用意します。

FROM alpine:latest
CMD ["echo", "Hello, everyone!"]

このDockerfileはアーキテクチャを特定するような情報を持っていませんが、このDockerfileを使ってビルドを行うと、

  • x86アーキテクチャのDocker上でビルドした場合は「x86アーキテクチャ向けAlpineイメージ」
  • ARMアーキテクチャのDocker上でビルドした場合は「ARMアーキテクチャ向けAlpineイメージ」

が、それぞれベースイメージとしてDocker Hubからプル (ダウンロード) されます。
これにより、環境に応じたDockerイメージをビルドすることができます。

「マルチCPUアーキテクチャサポート」については下記ブログ記事に詳しく書かれていますので、参考にしてください。

Dockerの実験的機能「Buildx」コマンド

Dockerで「マルチCPUアーキテクチャ」に対応したイメージをビルドするには、Dockerの「実験的 (Experimental)」機能として用意されている「Buildx」コマンドを使用します。

「Buildx」コマンドは、現時点では「Docker Desktop for Windows」または「Docker Desktop for Mac」のみで利用することが可能であり、Linuxなど向けの「Docker CE」では利用することができません。
したがって、
今回はWindows/MacのDocker Desktopを使って進めることにします。

※ 今回のブログ執筆では「Docker Desktop for Windows」の「Version 2.3.0.2 (45183)」を使用しました。

※ 2020.05.25 加筆訂正
「『Buildx』コマンドはLinuxなど向けの『Docker CE』では利用することができない」旨の記述は正しくないとのご指摘を頂きました。

Docker CEの場合は、以下のいずれかを行うことで「Buildx」コマンドを利用することができます。

  • 環境変数 DOCKER_CLI_EXPERIMENTALに値enabledをセットする
  • Dockerの設定ファイル~/.docker/config.jsonへ以下のように記述する
    {
      "experimental": "enabled"
    }
    

なお、「Amazon Linux Extras」を使ってインストールしたDocker CEでは、上記の設定を行っても「Buildx」コマンドが利用可能になりませんでした。
理由は分かりませんが、Docekr公式パッケージリポジトリからインストールした場合とは何かしら違うのかもしれません。

Docker Desktopのインストールと「実験的機能」の有効化

Docker公式サイトから Docker Desktop を入手してインストールします。
インストール手順は公式ドキュメント等を参照してください。

既にインストールされている方は、最新のバージョンへアップデートしてください。
※「Buildx」コマンドを使用するためには、Docker Desktopのバージョンが「2.0.4.0 (33772)」以降である必要があります

インストールしましたら「実験的 (Experimental)」機能の有効化を行います。

Docker Desktopの設定 (Settings) 画面を起動して、「Command Line」を選択します。

「Enable experimental features」のスイッチを「オン」にして、設定を保存します。

実験的機能が有効になりましたので、「Buildx」コマンドが使えるようになったことを確認します。

以下のようにdocker --helpコマンドを実行して、「Management Commands」の中に「buildx」が表示されていればOKです。
(「*」が付いているものが実験的機能になります)

$ docker --help

Usage:  docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Options:
(略)

Management Commands:
  app*        Docker Application (Docker Inc., v0.8.0)
  builder     Manage builds
  buildx*     Build with BuildKit (Docker Inc., v0.3.1-tp-docker)
  config      Manage Docker configs
  container   Manage containers
  context     Manage contexts
  image       Manage images
  manifest    Manage Docker image manifests and manifest lists
  mutagen*    Synchronize files with Docker Desktop (Docker Inc., testing)
  network     Manage networks
  node        Manage Swarm nodes
  plugin      Manage plugins
  secret      Manage Docker secrets
  service     Manage services
  stack       Manage Docker stacks
  swarm       Manage Swarm
  system      Manage Docker
  trust       Manage trust on Docker images
  volume      Manage volumes

Commands:
(略)

「ビルダーインスタンス」の準備

「Buildx」コマンドは、Dockerイメージをビルドするために「ビルダーインスタンス」と呼ばれるリソースを使用します。

デフォルトのビルダーインスタンスを確認する

まず、初期状態で用意されているビルダーインスタンスの一覧を見てみましょう。

$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
default * docker
  default default         running linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

「default」という名前のビルダーインスタンスが存在しています。
「default」の横に「*」マークが付いていますが、これは「現在使用するビルダーインスタンス」を示しています。
「DRIVER」欄の「docker」は、「Docker内蔵のビルドエンジンを使用する」という意味です。

段落を下げて表示された行は、ビルダーインスタンスを構成する「ノード」と呼ばれるリソースの情報です。
このビルダーインスタンスは「default」という名前の1ノードで構成されていることが分かります。
また、ノードについての以下の情報が表示されています。

  • STATUS: ノードの状態 (running=実行中)
  • PLATFORMS: ノードが対応しているプラットフォーム (OSおよびCPUアーキテクチャの組み合わせ)

新規のビルダーインスタンスを作成する

今回試す「マルチCPUアーキテクチャ」に対応したイメージのビルドは、デフォルトのビルダーインスタンスを使って行うことはできません。

そこで、以下のコマンドを実行して、新たにビルダーインスタンスを作成します。

$ docker buildx create --name mybuilder
mybuilder

ビルダーインスタンスの一覧を確認します。

$ docker buildx ls
NAME/NODE    DRIVER/ENDPOINT                STATUS   PLATFORMS
mybuilder    docker-container
  mybuilder0 npipe:////./pipe/docker_engine inactive
default *    docker
  default    default                        running  linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

「mybuilder」という名前のビルダーインスタンスが追加されていることが確認できます。
「DRIVER」欄の「docker-container」は、「ビルド用のDockerコンテナを使ってビルドを行う」という意味です。

また、ビルダーインスタンスは「mybuilder0」という名前のノードで構成されています。
ノードの状態は「inactive」と表示されており、まだ無効状態であることを示しています。
(無効状態のためプラットフォームの情報も表示されません)

使用するビルダーインスタンスを選択する

さて、新しいビルダーインスタンスを作成しても「現在使用するビルダーインスタンス」は依然「default」のままになっていますので、「mybuilder」を使用するように変更する必要があります。

使用するビルダーインスタンスを切り替えるには、以下のコマンドを実行します。

$ docker buildx use mybuilder

ビルダーインスタンスの一覧を確認します。

$ docker buildx ls
NAME/NODE    DRIVER/ENDPOINT                STATUS   PLATFORMS
mybuilder *  docker-container
  mybuilder0 npipe:////./pipe/docker_engine inactive
default      docker
  default    default                        running  linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

「mybuilder」の横に「*」マークが移動しました。
これでOKです。

ビルダーインスタンスを起動して有効化する

現在「mybuilder」の状態は「inactive」となっていますので、ビルダーインスタンスを起動して有効化したいと思います。

現在使用中のビルダーインスタンスについての情報を確認するdocker buildx inspectコマンドに--bootstrapオプションを付けて実行することで、ビルダーインスタンスを起動することができます。

$ docker buildx inspect --bootstrap
[+] Building 9.5s (1/1) FINISHED
 => [internal] booting buildkit                                                                                    9.5s
 => => pulling image moby/buildkit:buildx-stable-1                                                                 8.3s
 => => creating container buildx_buildkit_mybuilder0                                                               1.2s
Name:   mybuilder
Driver: docker-container

Nodes:
Name:      mybuilder0
Endpoint:  npipe:////./pipe/docker_engine
Status:    running
Platforms: linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

ビルダーインスタンスの一覧を確認します。

$ docker buildx ls
NAME/NODE    DRIVER/ENDPOINT                STATUS  PLATFORMS
mybuilder *  docker-container
  mybuilder0 npipe:////./pipe/docker_engine running linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default      docker
  default    default                        running linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

「mybuilder」の状態が「running」になりました。
また、プラットフォームの情報も表示されるようになりました。

ここで、DockerイメージとDockerコンテナの一覧を確認してみます。

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
moby/buildkit       buildx-stable-1     f2a88cb62c92        4 weeks ago         82.8MB

$ docker container ls
CONTAINER ID        IMAGE                           COMMAND             CREATED             STATUS              PORTS               NAMES
d75591709d17        moby/buildkit:buildx-stable-1   "buildkitd"         3 minutes ago       Up 3 minutes                            buildx_buildkit_mybuilder0

「moby/buildkit」というDockerイメージがロードされていることが分かります。
また、イメージからDocekrコンテナが起動されていることも分かります。

これが「mybuilder」のビルダーインスタンスの実体であり、このコンテナを使ってビルドが行われる訳です。

実は、、、起動しなくてもOKです!

「Buildx」コマンドでビルドを実行する際、ビルダーインスタンスが起動していない場合は自動的に起動をかけるようになっています。

今回は、ビルダーインスタンスの起動に伴うDockerイメージやDockerコンテナの動きを確認するために敢えて手動で起動しましたが、実際のところはdocker buildx inspect --bootstrapは実行しなくても問題ありません。

「Buildx」コマンドを使ったビルドを試してみる

「マルチCPUアーキテクチャ」に対応したイメージのビルドを行う前に、シンプルなビルドを試してみましょう。

ソースコードを格納するディレクトリを作成します。

$ mkdir go-webserver-sample
$ cd go-webserver-sample

お馴染みとなった (?) 以下の内容でソースコードを保存します。

go-webserver-sample.go

package main

import (
    "fmt"
    "net/http"
    "os"
    "runtime"
)

func handler(w http.ResponseWriter, r *http.Request) {
    hostname, _ := os.Hostname()
    fmt.Fprintf(w, "<h1>Welcome Golang-WebServer!</h1>")
    fmt.Fprintf(w, "<h2>Hostname: %s</h2>", hostname)
    fmt.Fprintf(w, "<h2>OS: %s</h2>", runtime.GOOS)
    fmt.Fprintf(w, "<h2>Architecture: %s</h2>", runtime.GOARCH)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

Dockerfileを保存します。

Dockerfile

FROM golang:latest AS builder
WORKDIR /tmp
COPY ./go-webserver-sample.go /tmp
RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go

FROM alpine:latest
COPY --from=builder /tmp/go-webserver-sample /bin/
CMD ["/bin/go-webserver-sample"]

今回は前回ブログ記事とは異なり、go buildコマンドの実行時にシェル変数GOOSGOARCH指定しません
つまり、ビルドを実行する環境のOS/アーキテクチャに応じてGo言語の実行可能ファイルが生成されます。

(これは、次に説明する「マルチCPUアーキテクチャに対応したイメージのビルド」において重要な点です)

「Buildx」コマンドを使ってDockerイメージのビルドを行うには、以下のコマンドを実行します。

$ docker buildx build -t go-webserver-sample:latest --load .

docker build (またはdocker image build) コマンドの使い方と良く似ていますが、最後に--loadというオプションが付いています。

「Buildx」のビルドコマンドは、生成したDockerイメージの出力先を明示的に指定する必要があります。

出力先を指定するオプションは--outputですが、--loadオプションは--output type=dockerと指定したことと同じ意味になります。
これは「生成したイメージをローカルDockerへ出力する」という意味です。

ビルドコマンドを実行すると、以下のような結果になると思います。

$ docker buildx build -t go-webserver-sample:latest --load .
[+] Building 94.1s (14/14) FINISHED
 => [internal] booting buildkit                                                                                    8.2s
 => => pulling image moby/buildkit:buildx-stable-1                                                                 7.4s
 => => creating container buildx_buildkit_mybuilder0                                                               0.8s
 => [internal] load build definition from Dockerfile                                                               0.2s
 => => transferring dockerfile: 292B                                                                               0.0s
 => [internal] load .dockerignore                                                                                  0.2s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/alpine:latest                                                   9.5s
 => [internal] load metadata for docker.io/library/golang:latest                                                   9.5s
 => [stage-1 1/2] FROM docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed  11.1s
 => => resolve docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4f  0.0s
 => => sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4fb9a54 1.64kB / 1.64kB                     0.0s
 => => sha256:39eda93d15866957feaee28f8fc5adb545276a64147445c64992ef69804dbf01 528B / 528B                         0.0s
 => => sha256:cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08 2.81MB / 2.81MB                     4.8s
 => => sha256:f70734b6a266dcb5f44c383274821207885b549b75c8e119404917a61335981a 1.51kB / 1.51kB                     0.0s
 => => unpacking docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea  2.5s
 => [internal] load build context                                                                                  0.1s
 => => transferring context: 498B                                                                                  0.0s
 => [builder 1/4] FROM docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61  55.7s
 => => resolve docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb  0.0s
 => => sha256:496548a8c952b37bdf149ab3654f9085d721ee126b8c73b16860778be5137f5e 10.00MB / 10.00MB                   7.3s
 => => sha256:7e5e8028e8ecb43c45b9928ab4561de171b28b9a8add11e575405133e4f408e5 5.45kB / 5.45kB                     0.0s
 => => sha256:376057ac6fa17f65688c56977118e2e764b27c348b3f70637fa829cd2a12b200 50.39MB / 50.39MB                  18.7s
 => => sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb543a7 1.91kB / 1.91kB                     0.0s
 => => sha256:8b98da51cbc03732a136fd9ed4449683d5b6976debd9d89403070a3390c1b3d8 1.79kB / 1.79kB                     0.0s
 => => sha256:5a63a0a859d859478f30461786a49c2fca3ae7d89ab5b5ce3c81c54951d30f88 7.81MB / 7.81MB                     6.6s
 => => sha256:0cca3cbecb14d24f4d5dcfd8b8f8865b819f77b9e9fdacc369d886b5b53c2da4 123.70MB / 123.70MB                27.8s
 => => sha256:2adae3950d4d0f11875568c325d3d542d1f2e2d9b49bdd740bb57fcfc1f6478f 51.83MB / 51.83MB                  18.9s
 => => sha256:039b991354af4dcbc534338f687e27643c717bb57e11b87c2e81d50bdd0b2376 68.65MB / 68.65MB                  19.5s
 => => sha256:59c34b3f33f3365509a64a5fd8454551f7bad251213c903e7b59018e6b09a38d 126B / 126B                         3.4s
 => => sha256:496548a8c952b37bdf149ab3654f9085d721ee126b8c73b16860778be5137f5e 10.00MB / 10.00MB                   7.3s
 => => sha256:0cca3cbecb14d24f4d5dcfd8b8f8865b819f77b9e9fdacc369d886b5b53c2da4 123.70MB / 123.70MB                27.8s
 => => unpacking docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3d  24.3s
 => [builder 2/4] WORKDIR /tmp                                                                                     0.4s
 => [builder 3/4] ADD ./go-webserver-sample.go /tmp                                                                0.4s
 => [builder 4/4] RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go                         16.8s
 => [stage-1 2/2] COPY --from=builder /tmp/go-webserver-sample /bin/                                               0.2s
 => exporting to oci image format                                                                                  1.9s
 => => exporting layers                                                                                            1.1s
 => => exporting manifest sha256:e3b1af1ddc44ae9ef7615284529caca3a9b213ec245a62ff6963e4027ef7d4f7                  0.1s
 => => exporting config sha256:52e70dbdf4cc20ba7de04592281c694daf05d2ee5f447ea05701c8642e809a16                    0.1s
 => => sending tarball                                                                                             0.6s
 => importing to docker                                                                                            0.4s

Dockerイメージの一覧を確認します。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED              SIZE
go-webserver-sample   latest              52e70dbdf4cc        About a minute ago   13MB
moby/buildkit         buildx-stable-1     f2a88cb62c92        4 weeks ago          82.8MB

イメージが生成されたことが確認できました。

(イメージからのDocekrコンテナの起動については割愛します)

「Buildx」コマンドを使った「マルチCPUアーキテクチャ」対応イメージのビルド

それでは、いよいよ「マルチCPUアーキテクチャ」に対応したイメージのビルドを行います。

ビルド実行時に「プラットフォーム」を指定する

Go言語のソースコード、Dockerfileは、さきほどと同じものを使います。

「マルチCPUアーキテクチャ」に対応したイメージのビルドを行うには、以下のコマンドを実行します。

$ docker buildx build --platform linux/amd64,linux/arm64 -t go-webserver-sample:latest --load .

さきほどと違うのは--platform linux/amd64,linux/arm64というオプションが指定されていることです。

--platformオプションによって、どのプラットフォーム (OSおよびCPUアーキテクチャの組み合わせ) に対応したイメージを生成するのかを指定します。
ここでは「Linux/AMD64」と「Linux/ARM64」という2通りのプラットフォームを指定しています。

ちなみに、--platformオプションを省略した場合は、ビルドを実行する環境のOS/CPUアーキテクチャが自動的に指定されます。

さきほどDockerfileの説明で「go buildコマンドの実行時にシェル変数GOOSGOARCHを指定しない」と書きました。

「Buildx」コマンドは--platformオプションの指定に従ってOSやCPUアーキテクチャを変えながらイメージのビルドを行いますが、Dockerfile内でOS/アーキテクチャを特定するような記述をすると期待通りの動作を行えなくなるためです。

ビルドを実行してみます。

$ docker buildx build --platform linux/amd64,linux/arm64 -t go-webserver-sample:latest --load .
[+] Building 241.3s (20/20) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.1s
 => => transferring dockerfile: 32B                                                                                0.0s
 => [internal] load .dockerignore                                                                                  0.1s
 => => transferring context: 2B                                                                                    0.0s
 => [linux/arm64 internal] load metadata for docker.io/library/alpine:latest                                       7.8s
 => [linux/amd64 internal] load metadata for docker.io/library/alpine:latest                                       3.5s
 => [linux/arm64 internal] load metadata for docker.io/library/golang:latest                                       7.8s
 => [linux/amd64 internal] load metadata for docker.io/library/golang:latest                                       3.7s
 => [linux/amd64 builder 1/4] FROM docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a5  0.0s
 => => resolve docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb  0.0s
 => [linux/amd64 stage-1 1/2] FROM docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90  0.0s
 => => resolve docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4f  0.0s
 => [internal] load build context                                                                                  0.1s
 => => transferring context: 44B                                                                                   0.0s
 => [linux/arm64 builder 1/4] FROM docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a  45.4s
 => => resolve docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb  0.0s
 => => sha256:9e4953d07f3797c5157db03c421dadd6a327e1bca0704cd2c93a33987666a317 1.79kB / 1.79kB                     0.0s
 => => sha256:0d65fe43068f16f86c33f2ce753ee24de91c3f8f746874ffb972f9d8c24f8543 5.45kB / 5.45kB                     0.0s
 => => sha256:d23bf71de5e1fea32788576972e233e80db7c51d831ed7edb269102dab298bf1 49.17MB / 49.17MB                  15.7s
 => => sha256:f34690136adb76fac1a121f6c4a9b5a63de1def4d65e0b2f99ff84c392ed8244 9.98MB / 9.98MB                     7.0s
 => => sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb543a7 1.91kB / 1.91kB                     0.0s
 => => sha256:87401a001075b35755fe95f6a8a1722c746534b17a4b5c1caf1b22a7ea7d7456 155B / 155B                         3.7s
 => => sha256:d4f6b089b3526111514262a58bbb27c5a292d7ef355184883365cdce9a7c61e5 7.68MB / 7.68MB                     6.3s
 => => sha256:4287f76f52e4c390d78182615c75d2b3d6a5cd5b2c28dc554707a7cfbfa91f2b 52.16MB / 52.16MB                  16.4s
 => => sha256:d5631a7a4659fe13982c83f950545083bec8f0e96ad34c74de171518bae880b7 62.52MB / 62.52MB                  18.1s
 => => sha256:2a7b5302103f4733bdd64ce58c4ab19b7318b3171bc4fd38fb434bc2d3ae2c08 101.04MB / 101.04MB                21.8s
 => => sha256:d5631a7a4659fe13982c83f950545083bec8f0e96ad34c74de171518bae880b7 62.52MB / 62.52MB                  18.1s
 => => unpacking docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3d  20.1s
 => [linux/arm64 stage-1 1/2] FROM docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90  7.9s
 => => sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4fb9a54 1.64kB / 1.64kB                     0.0s
 => => sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280 528B / 528B                         0.0s
 => => sha256:29e5d40040c18c692ed73df24511071725b74956ca1a61fe6056a651d86a13bd 2.72MB / 2.72MB                     3.6s
 => => sha256:c20d2a9ab6869161e3ea6d8cb52d00be9adac2cc733d3fbc3955b9268bfd7fc5 1.51kB / 1.51kB                     0.0s
 => => unpacking docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea  0.7s
 => => sha256:29e5d40040c18c692ed73df24511071725b74956ca1a61fe6056a651d86a13bd 2.72MB / 2.72MB                     3.6s
 => CACHED [linux/amd64 builder 2/4] WORKDIR /tmp                                                                  0.0s
 => CACHED [linux/amd64 builder 3/4] ADD ./go-webserver-sample.go /tmp                                             0.0s
 => CACHED [linux/amd64 builder 4/4] RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go       0.0s
 => CACHED [linux/amd64 stage-1 2/2] COPY --from=builder /tmp/go-webserver-sample /bin/                            0.0s
 => [linux/arm64 builder 2/4] WORKDIR /tmp                                                                         0.5s
 => [linux/arm64 builder 3/4] ADD ./go-webserver-sample.go /tmp                                                    0.3s
 => [linux/arm64 builder 4/4] RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go            186.9s
 => [linux/arm64 stage-1 2/2] COPY --from=builder /tmp/go-webserver-sample /bin/                                   0.1s
 => ERROR exporting to oci image format                                                                            0.0s
------
 > exporting to oci image format:
------
failed to solve: rpc error: code = Unknown desc = docker exporter does not currently support exporting manifest lists

最後の方に「ERROR exporting to oci image format」と出力され、ビルドに失敗しました。

何故ビルドに失敗したのか?

実は、ビルド実行時に--loadオプションを指定してローカルDockerへイメージを出力しようとした場合、ビルド環境と異なるプラットフォームのDockerイメージは出力することができない という制約があります。

ローカルDockerは自身で動作するイメージを扱うことを前提にしていますから「異なる環境のイメージは保持できない」ということなんでしょうね。

※ 将来的にはローカルDockerへの出力ができるようになる可能性もありますが、現時点では「できない」ということを覚えておいてください。

「マルチCPUアーキテクチャ」対応イメージをDocker Hubへ直接出力する

では、どうやって「マルチCPUアーキテクチャ」に対応したイメージをビルドして出力すればよいかと言いますと、答は ビルドしたDockerイメージをローカルに保持することなく「Docker Hub」へ直接出力する です。

まず、予め Docker Hub のアカウントを作成しておきます。

次に、コマンドラインでDocker Hubへのログインを行います。

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: cmaoyagi
Password: ********
Login Succeeded

これで、DockerコマンドがDocker Hubへ書き込みアクセス権限を持つ状態になりました。

この状態で、ビルドを実行します。

改良したビルドのコマンドラインは以下のようになります。

$ docker buildx build --platform linux/amd64,linux/arm64 -t cmaoyagi/go-webserver-sample:latest --push .

さきほど失敗した際のコマンドラインの--loadオプションに代わって、--pushオプションが指定されています。

--pushオプションは--output type=registryと指定したことと同じ意味になります。
これは「生成したイメージをDockerレジストリ (この場合はDocker Hub) へ出力する」という意味です。

ビルドコマンドを実行すると、以下のような結果になります。

$ docker buildx build --platform linux/amd64,linux/arm64 -t cmaoyagi/go-webserver-sample:latest --push .
[+] Building 18.0s (20/20) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.1s
 => => transferring dockerfile: 32B                                                                                0.0s
 => [internal] load .dockerignore                                                                                  0.1s
 => => transferring context: 2B                                                                                    0.0s
 => [linux/arm64 internal] load metadata for docker.io/library/alpine:latest                                       3.4s
 => [linux/amd64 internal] load metadata for docker.io/library/alpine:latest                                       3.6s
 => [linux/arm64 internal] load metadata for docker.io/library/golang:latest                                       3.5s
 => [linux/amd64 internal] load metadata for docker.io/library/golang:latest                                       3.4s
 => [linux/arm64 builder 1/4] FROM docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a5  0.0s
 => => resolve docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb  0.0s
 => [linux/amd64 stage-1 1/2] FROM docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90  0.0s
 => [linux/arm64 stage-1 1/2] FROM docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90  0.0s
 => => resolve docker.io/library/alpine:latest@sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4f  0.0s
 => [internal] load build context                                                                                  0.0s
 => => transferring context: 44B                                                                                   0.0s
 => [linux/amd64 builder 1/4] FROM docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a5  0.0s
 => => resolve docker.io/library/golang:latest@sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb  0.0s
 => CACHED [linux/amd64 builder 2/4] WORKDIR /tmp                                                                  0.0s
 => CACHED [linux/amd64 builder 3/4] ADD ./go-webserver-sample.go /tmp                                             0.0s
 => CACHED [linux/amd64 builder 4/4] RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go       0.0s
 => CACHED [linux/amd64 stage-1 2/2] COPY --from=builder /tmp/go-webserver-sample /bin/                            0.0s
 => CACHED [linux/arm64 builder 2/4] WORKDIR /tmp                                                                  0.0s
 => CACHED [linux/arm64 builder 3/4] ADD ./go-webserver-sample.go /tmp                                             0.0s
 => CACHED [linux/arm64 builder 4/4] RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go       0.0s
 => CACHED [linux/arm64 stage-1 2/2] COPY --from=builder /tmp/go-webserver-sample /bin/                            0.0s
 => exporting to image                                                                                            14.2s
 => => exporting layers                                                                                            0.7s
 => => exporting manifest sha256:e3b1af1ddc44ae9ef7615284529caca3a9b213ec245a62ff6963e4027ef7d4f7                  0.0s
 => => exporting config sha256:52e70dbdf4cc20ba7de04592281c694daf05d2ee5f447ea05701c8642e809a16                    0.0s
 => => exporting manifest sha256:4a2bb69f9f229d47a99ab72043db859650cac9019131703c9b5c33489a51b3e2                  0.0s
 => => exporting config sha256:8a9a7e993885da8c3e52074c1957a98858928f355096c6fe981f4ba09ae784be                    0.0s
 => => exporting manifest list sha256:93172eaafa22d45d5309f9823f5b1c269ae6e0613e6d38ea6c9f9a8a9192688b             0.0s
 => => pushing layers                                                                                              7.9s
 => => pushing manifest for docker.io/cmaoyagi/go-webserver-sample:latest                                          5.4s

今度は正常にビルドが完了しました。

Docker Hub上に出力された (プッシュされた) Dockerイメージを確認しましょう。

自分のDocker Hubアカウントのページに行くと、出力したDockerイメージの名前に合わせて、リポジトリが作成されていることが確認できます。

「Tags」タブを選択すると、Dockerイメージが対応するOS/アーキテクチャの一覧が表示されます。
ちゃんと「linux/amd64」「linux/arm64」の2つが登録されています。

コマンドで確認する方法もあります。

docker buildx imagetools inspectコマンドを使用すると、Dockerレジストリ上にあるDockerイメージの内容を確認することができます。

$ docker buildx imagetools inspect cmaoyagi/go-webserver-sample:latest
Name:      docker.io/cmaoyagi/go-webserver-sample:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:    sha256:93172eaafa22d45d5309f9823f5b1c269ae6e0613e6d38ea6c9f9a8a9192688b

Manifests:
  Name:      docker.io/cmaoyagi/go-webserver-sample:latest@sha256:e3b1af1ddc44ae9ef7615284529caca3a9b213ec245a62ff6963e4027ef7d4f7
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/amd64

  Name:      docker.io/cmaoyagi/go-webserver-sample:latest@sha256:4a2bb69f9f229d47a99ab72043db859650cac9019131703c9b5c33489a51b3e2
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm64

2通りのプラットフォームに対応するイメージの情報 (これをマニフェストと呼びます) が並んでおり、「Platform」欄には「linux/amd64」「linux/arm64」と表示されています。

「マルチCPUアーキテクチャ」対応イメージを利用する

最後に、Docker Hub上の「マルチCPUアーキテクチャ」に対応したDockerイメージを、「x64 (AMD64)」および「ARM64」の各アーキテクチャ環境からプルを行い、コンテナを起動してみましょう。

CloudFormationを使って、AWS上に「x86 (AMD64)」と「ARM64」アーキテクチャのEC2インスタンスを1台ずつ起動します。

CloudFormationテンプレート (クリックすると展開します)

cfn-docker-multiarch.yaml

---
AWSTemplateFormatVersion: "2010-09-09"
Description: "Launch x86(AMD64) and ARM64(Graviton2) EC2 instances with VPC environment"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "General Information"
        Parameters:
          - SystemName
      - Label:
          default: "Network Configuration"
        Parameters:
          - CidrBlockVPC
          - CidrBlockSubnetPublic
          - MyIpAddressCidr
      - Label:
          default: "x86(AMD64) EC2 Instance Configuration"
        Parameters:
          - X86ImageID
          - X86InstanceType
          - X86KeyName
          - X86VolumeType
          - X86VolumeSize
      - Label:
          default: "ARM64(Graviton2) EC2 Instance Configuration"
        Parameters:
          - ARM64ImageID
          - ARM64InstanceType
          - ARM64KeyName
          - ARM64VolumeType
          - ARM64VolumeSize

Parameters:
  SystemName:
    Type: String
    Default: docker-multiarch

  CidrBlockVPC:
    Type: String
    Default: 192.168.0.0/16

  CidrBlockSubnetPublic:
    Type: String
    Default: 192.168.1.0/24

  MyIpAddressCidr:
    Type: String

  X86ImageID:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

  X86InstanceType:
    Type: String
    Default: t3.medium

  X86KeyName:
    Type: AWS::EC2::KeyPair::KeyName

  X86VolumeType:
    Type: String
    Default: gp2

  X86VolumeSize:
    Type: String
    Default: 20

  ARM64ImageID:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2

  ARM64InstanceType:
    Type: String
    Default: m6g.medium
    AllowedValues:
      - m6g.medium
      - m6g.large
      - m6g.xlarge
      - m6g.2xlarge
      - m6g.4xlarge
      - m6g.8xlarge
      - m6g.12xlarge
      - m6g.16xlarge

  ARM64KeyName:
    Type: AWS::EC2::KeyPair::KeyName

  ARM64VolumeType:
    Type: String
    Default: gp2

  ARM64VolumeSize:
    Type: String
    Default: 20

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CidrBlockVPC
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-vpc"
        - Key: System
          Value: !Ref SystemName

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-igw"
        - Key: System
          Value: !Ref SystemName

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  SubnetPublic:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref CidrBlockSubnetPublic
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-subnet"
        - Key: System
          Value: !Ref SystemName

  RouteTablePublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-rtb"
        - Key: System
          Value: !Ref SystemName

  RouteIGW:
    DependsOn:
      - VPCGatewayAttachment
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTablePublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  RouteTableAssociationPublic:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPublic
      RouteTableId: !Ref RouteTablePublic

  SecurityGroupServer:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${SystemName}-server-sg"
      GroupDescription: "Security group for server"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIpAddressCidr
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIpAddressCidr
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-server-sg"
        - Key: System
          Value: !Ref SystemName

  EC2InstanceX86:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref X86ImageID
      InstanceType: !Ref X86InstanceType
      KeyName: !Ref X86KeyName
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: !Ref X86VolumeType
            VolumeSize: !Ref X86VolumeSize
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref SubnetPublic
          GroupSet:
            - !Ref SecurityGroupServer
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum update -y
          amazon-linux-extras install -y docker=latest
          systemctl enable docker.service
          systemctl start docker.service
          usermod -aG docker ec2-user
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-x86"
        - Key: System
          Value: !Ref SystemName

  EC2InstanceARM64:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ARM64ImageID
      InstanceType: !Ref ARM64InstanceType
      KeyName: !Ref ARM64KeyName
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: !Ref ARM64VolumeType
            VolumeSize: !Ref ARM64VolumeSize
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref SubnetPublic
          GroupSet:
            - !Ref SecurityGroupServer
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum update -y
          amazon-linux-extras install -y docker=latest
          systemctl enable docker.service
          systemctl start docker.service
          usermod -aG docker ec2-user
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-arm64"
        - Key: System
          Value: !Ref SystemName

Outputs:
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}::VPC"

  SubnetPublic:
    Value: !Ref SubnetPublic
    Export:
      Name: !Sub "${AWS::StackName}::SubnetPublic"

  SecurityGroupServer:
    Value: !Ref SecurityGroupServer
    Export:
      Name: !Sub "${AWS::StackName}::SecurityGroupServer"

  EC2InstanceX86:
    Value: !Ref EC2InstanceX86
    Export:
      Name: !Sub "${AWS::StackName}::EC2InstanceX86"

  EC2InstanceARM64:
    Value: !Ref EC2InstanceARM64
    Export:
      Name: !Sub "${AWS::StackName}::EC2InstanceARM64"

x86アーキテクチャ環境で実行

「x86 (AMD64)」アーキテクチャのEC2インスタンスへSSH等で接続します。

Docker環境は既にインストール済みですので、さっそく進めていきましょう。

Docker HubからDockerイメージをプルします。

$ docker image pull cmaoyagi/go-webserver-sample:latest
latest: Pulling from cmaoyagi/go-webserver-sample
cbdbe7a5bc2a: Pull complete
12834960e0ba: Pull complete
Digest: sha256:93172eaafa22d45d5309f9823f5b1c269ae6e0613e6d38ea6c9f9a8a9192688b
Status: Downloaded newer image for cmaoyagi/go-webserver-sample:latest
docker.io/cmaoyagi/go-webserver-sample:latest

プルしたDockerイメージを確認します。

$ docker image ls
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
cmaoyagi/go-webserver-sample   latest              52e70dbdf4cc        29 minutes ago      13MB

念のため、Dockerイメージの内容を確認しましょう。
docker image inspectコマンドを使うと、Dockerイメージの対応アーキテクチャなどの情報を参照することができます。

$ docker image inspect cmaoyagi/go-webserver-sample:latest
[
    {
        "Id": "sha256:52e70dbdf4cc20ba7de04592281c694daf05d2ee5f447ea05701c8642e809a16",
        "RepoTags": [
            "cmaoyagi/go-webserver-sample:latest"
        ],
        "RepoDigests": [
            "cmaoyagi/go-webserver-sample@sha256:93172eaafa22d45d5309f9823f5b1c269ae6e0613e6d38ea6c9f9a8a9192688b"
        ],

・・・(略)・・・

        "Architecture": "amd64",
        "Os": "linux",

・・・(略)・・・

    }
]

"Architecture": "amd64"となっていますね。

それでは、Dockerイメージからコンテナを起動しましょう。

$ docker container run -p 80:8080 --rm cmaoyagi/go-webserver-sample:latest

WebブラウザでEC2インスタンスのIPアドレスにアクセスします。

x86アーキテクチャのDockerコンテナが動作していることが確認できました。

ARM64アーキテクチャ環境で実行

「ARM64」アーキテクチャのEC2インスタンスでも同様の手順で行います。

Dockerイメージをプルします。

$ docker image pull cmaoyagi/go-webserver-sample:latest
latest: Pulling from cmaoyagi/go-webserver-sample
29e5d40040c1: Pull complete
12a1e52164ea: Pull complete
Digest: sha256:93172eaafa22d45d5309f9823f5b1c269ae6e0613e6d38ea6c9f9a8a9192688b
Status: Downloaded newer image for cmaoyagi/go-webserver-sample:latest
docker.io/cmaoyagi/go-webserver-sample:latest

プルしたDockerイメージを確認します。

$ docker image ls
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
cmaoyagi/go-webserver-sample   latest              8a9a7e993885        25 minutes ago      12.5MB
$ docker image inspect cmaoyagi/go-webserver-sample:latest
[
    {
        "Id": "sha256:8a9a7e993885da8c3e52074c1957a98858928f355096c6fe981f4ba09ae784be",
        "RepoTags": [
            "cmaoyagi/go-webserver-sample:latest"
        ],
        "RepoDigests": [
            "cmaoyagi/go-webserver-sample@sha256:93172eaafa22d45d5309f9823f5b1c269ae6e0613e6d38ea6c9f9a8a9192688b"
        ],

・・・(略)・・・

        "Architecture": "arm64",
        "Os": "linux",

・・・(略)・・・

    }
]

Dockerイメージからコンテナを起動します。

$ docker container run -p 80:8080 --rm cmaoyagi/go-webserver-sample:latest

Webブラウザでアクセスします。

こちらも問題なく確認できました。

おわりに

Dockerの「Buildx」コマンドには、今回ご紹介した「マルチCPUアーキテクチャ」対応イメージのビルドの他にも、様々な新しい機能があります。

機会があれば、「Buildx」コマンドの他の機能についても、調べて&試してみたいと思います。