Dockerの「マルチCPUアーキテクチャ」に対応したイメージをビルドする
みなさん、こんにちは!
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
お馴染みとなった (?) 以下の内容でソースコードを保存します。
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を保存します。
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
コマンドの実行時にシェル変数GOOS
やGOARCH
を指定しません。
つまり、ビルドを実行する環境の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アーキテクチャが自動的に指定されます。
go build
コマンドの実行時にシェル変数GOOS
やGOARCH
を指定しない」と書きました。
「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テンプレート (クリックすると展開します)
--- 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」コマンドの他の機能についても、調べて&試してみたいと思います。