Dockerでマルチプラットフォームビルド
ARM64とAMD64の両方に対応するマルチプラットフォームイメージのビルドを試してみる。
参考
公式ドキュメントに従ってやってみる
まずはローカルのMac(M2)で。Docker Desktop for Macを使用している。
作成した記憶がないのだが、ビルダーは作成済みだった。
docker buildx ls --no-trunc
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
desktop-linux* docker
\_ desktop-linux \_ desktop-linux running v0.17.3 linux/arm64, linux/amd64, linux/amd64/v2, linux/riscv64, linux/ppc64le, linux/s390x, linux/386
QEMUエミュレーションを使ったマルチプラットフォームビルド
作業ディレクトリ作成
mkdir multi-platform && cd multi-platform
以下のDockerfileをビルドする。
FROM alpine
RUN uname -m > /arch
linux/amd64
とlinux/arm64
向けにビルド
docker build --platform linux/amd64,linux/arm64 -t multi-platform .
コンテナを起動してみる
docker run --rm multi-platform cat /arch
aarch64
エミュレーションになってしまうが、AMD64想定で起動してみる
docker run --rm --platform linux/amd64 multi-platform cat /arch
x86_64
DockerHubにpushしてみる
docker tag multi-platform:latest kun432/multi-platform:latest
docker push kun432/multi-platform:latest
マルチプラットフォームに対応したイメージになっているのがわかる
QEMUエミュレーションについては、これぐらいだと時間の違いがわからないので、上の「ぽよめも」様の記事をベースに「miyajan」様が作成されたDockerfileのサンプルを使用させていただく。
FROM golang:1.22.3 AS builder
ARG VERSION=latest
RUN go install github.com/Code-Hex/Neo-cowsay/cmd/v2/cowsay@${VERSION}
FROM gcr.io/distroless/static
COPY /go/bin/cowsay /usr/bin/cowsay
ENTRYPOINT ["/usr/bin/cowsay"]
docker build --platform linux/amd64,linux/arm64 -t cowsay .
確かにgo installしている箇所の時間が違う。Mac上でビルドしているのでlinux/arm64
のほうが早くlinux/amd64
のほうが時間がかかっている。ここでQEMUのエミュレーションが行われているということになる。
=> [linux/arm64 builder 2/2] RUN go install github.com/Code-Hex/Neo-cowsay/cmd/v2/cowsay@latest 7.3s
=> [linux/amd64 builder 2/2] RUN go install github.com/Code-Hex/Neo-cowsay/cmd/v2/cowsay@latest 16.7s
マルチネイティブノード
プラットフォームごとにビルダーを用意してそれぞれでビルドさせることで、QEMUエミュレーションを回避する。
以下の記事がわかりやすい。
linux/arm64
はMac、linux/amd64
は、x86_64なUbuntu 22.04サーバでビルドすることとする。
パスワード無しでログインできる環境である必要がある
docker -H ssh://kun432@REMOTE_HOST info
Client:
Version: 27.4.0
Context: default
Debug Mode: false
(snip)
ローカル用・リモート用、それぞれのノードをlocal_remote_builder
というビルダーで定義
docker buildx create \
--name local_remote_builder \
--node arm64 \
--platform linux/arm64 \
--driver-opt env.BUILDKIT_STEP_LOG_MAX_SIZE=10000000 \
--driver-opt env.BUILDKIT_STEP_LOG_MAX_SPEED=10000000
append
で追加
docker buildx create \
--name local_remote_builder \
--append \
--node amd64 \
--platform linux/amd64,linux/amd64/v2,linux/386 \
ssh://kun432@REMOTE_HOST \
--driver-opt env.BUILDKIT_STEP_LOG_MAX_SIZE=10000000 \
--driver-opt env.BUILDKIT_STEP_LOG_MAX_SPEED=10000000
確認
docker buildx ls --no-trunc
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
local_remote_builder docker-container
\_ arm64 \_ desktop-linux inactive linux/arm64*
\_ amd64 \_ ssh://kun432@REMOTE_HOST inactive linux/amd64*, linux/amd64/v2*, linux/386*
desktop-linux* docker
\_ desktop-linux \_ desktop-linux running v0.17.3 linux/arm64, linux/amd64, linux/amd64/v2, linux/riscv64, linux/ppc64le, linux/s390x, linux/386
ステータスがinactive
になってるけどいいのかな???
とりあえずこれを使ってビルドしてみる。
docker build \
--platform linux/amd64,linux/arm64 \
--builder local_remote_builder \
-t cowsay \
--no-cache .
どうやら初回のみ?ビルド用のコンテナがローカル・リモートの両方で作成されるので少し時間がかかる模様。ただgo installしている箇所はQEMUエミュレーションと違って高速になっている。
=> [linux/arm64 builder 2/2] RUN go install github.com/Code-Hex/Neo-cowsay/cmd/v2/cowsay@latest 7.3s
=> [linux/amd64 builder 2/2] RUN go install github.com/Code-Hex/Neo-cowsay/cmd/v2/cowsay@latest 7.8s
DockerHubにpushして確認
docker tag cowsay:latest kun432/cowsay:latest
docker push kun432/cowsay:latest
Mac側
docker inspect kun432/cowsay:latest | jq -r '.[0] | .Os + "/" + .Architecture'
linux/arm64
Ubuntu側
docker pull kun432/cowsay
docker inspect kun432/cowsay:latest | jq -r '.[0] | .Os + "/" + .Architecture'
linux/amd64
クロスコンパイル
クロスコンパイルが可能な環境ではDockerfileでマルチプラットフォームビルドを吸収することができる。色々やり方がある模様だが、基本的にはマルチステージビルドと組み合わせる感じっぽい。
ここはあまり知見がないので割愛。
マニフェスト
自分は過去意識したことがなかったのだけども、以下の記事を見ながらやってみる。
Dockerにおけるマニフェストは、レイヤー・サイズ・ダイジェストなどの情報を含むイメージに関する情報を指す。マニフェストの目的は以下。
-
イメージ情報
マニフェストは、レイヤー・サイズ・ダイジェストなど、そのイメージの詳細な情報を持つ。これによりイメージをダウンロードしなくても、ユーザーやツールがそのイメージの構成や特性を把握することができる。 -
マルチプラットフォームのサポート
マニフェストリスト(「マルチアーキテクチャイメージ」とも言う)を使うことで、1つのイメージで複数のプラットフォームをサポートできる。これにより、さまざまなハードやソフトで実行できるポータブルなアプリケーションを作成することができる。 -
レジストリとの通信
Dockerクライアントがイメージをプルすると、レジストリはマニフェストのリストを返す。これを元にクライアントはホストのアーキテクチャに適したバリアントを自動的に選択することができる。 -
メタデータストレージ
イメージに関する追加のコンテキストや情報など、イメージのメタデータをマニフェストに保存することができる。 -
バージョン管理とタグ付け
上と同様に、イメージの異なるバージョンやタグをマニフェストに保存することができる。 -
セキュリティ
マニフェストにはイメージのダイジェスト情報が含まれており、転送中や保存中のイメージの改ざん等が行われていないか、イメージの完全性を検証することができる。 -
効率的な配布
マニフェストリストを使うことで、レジストリは単一の参照でイメージの複数のバリエーションを保存できる。マルチアーキテクチャイメージの配布プロセスを合理化できる。
1つのプラットフォームにしか対応していないイメージの場合は以下となる。
referred from https://docs.docker.com/build/building/multi-platform/#difference-between-single-platform-and-multi-platform-images and translated into Japanese by kun432
マルチプラットフォームに対応したイメージは、各プラットフォームごとのマニフェストを持つマニフェストリストを持つ。
referred from https://docs.docker.com/build/building/multi-platform/#difference-between-single-platform-and-multi-platform-images and translated into Japanese by kun432
マニフェストの操作にはdocker manifest
コマンドを使用する。実際に複数のプラットフォームごとのイメージからマニフェストリストを作成してみる。上の方で使用したmultiplatformのDockerfileをビルドしてDocker Hubにプッシュする。
FROM alpine
RUN uname -m > /arch
docker build --platform linux/arm64 -t kun432/sample-arm64:latest .
docker push kun432/sample-arm64:latest
マニフェストを確認
docker manifest inspect kun432/sample-arm64:latest
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 668,
"digest": "sha256:acd826a5bb28ca16799ced9b39e1283f7c859b062dfdc2267a8d955ab6167481",
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 566,
"digest": "sha256:f482f546c9da4582944894967caddbc1cbb32c476af5abf76f7707b84bf8dfac",
"platform": {
"architecture": "unknown",
"os": "unknown"
}
}
]
}
すでにこの時点でマニフェストリストになっていて、プラットフォームが"unknown"になっているのだが、Provenance attestationsというものらしい。詳しくは以下が参考になる。
今回はそこはあまり触れずに進める。
次にUbuntuでも。
docker build --platform linux/amd64 -t kun432/sample-amd64:latest .
docker push kun432/sample-amd64:latest
マニフェストを確認
docker manifest inspect kun432/sample-amd64:latest
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 818,
"digest": "sha256:676507ffea28737fdff56dab8f22aeae30af713ae2b557eda0805ddc882827ad"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 3642247,
"digest": "sha256:f18232174bc91741fdf3da96d85011092101a032a93a388b79e99e69c2d5c870"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 137,
"digest": "sha256:f18de9172e4f52a2f2ccf2277d7e2950266d8b09ea63b5e005cba2de13545836"
}
]
}
全然フォーマットが違うなー。Docker Desktopの設定でかなり変わるってことかなー。まあ、そこも一旦おいておく。
この2つのイメージをまとめて1つのマニフェストにする。
docker manifest create kun432/sample-multi:latest \
kun432/sample-arm64:latest \
kun432/sample-amd64:latest
docker.io/kun432/sample-arm64:latest is a manifest list
どうもマニフェストリストとマニフェストを1つのマニフェストリストにはまとめれない様子。
MacのDocker Desktopの設定を変更することもできるけども、--provenance=false
を使えばProvenance attestationsを無効化できるはず。
再度ビルドし直し
docker rmi kun432/sample-arm64:latest
docker build --platform linux/arm64 --provenance=false -t kun432/sample-arm64:latest . --no-cache
docker push kun432/sample-arm64:latest
マニフェストを確認
docker manifest inspect kun432/sample-arm64:latest
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:a711b39d196b0597b491c75f705b1c7cc2b089e4a95d82552bb12447fe9e5845",
"size": 809
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:6e771e15690e2fabf2332d3a3b744495411d6e0b00b2aea64419b58b0066cf81",
"size": 3993029
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:1dd6892e0663966a7f1f0ada81ac64a4aea77118165be05749c57c2a1f7266f6",
"size": 105
}
]
}
うむ、通常のマニフェストになっている。
再度マニフェストリストを作成。
docker manifest create kun432/sample-multi:latest \
kun432/sample-arm64:latest \
kun432/sample-amd64:latest
Created manifest list docker.io/kun432/sample-multi:latest
今度は問題なし。Docker Hubにpushして、マニフェストリストも確認。
docker manifest inspect kun432/sample-multi:latest
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 734,
"digest": "sha256:044c54279f351ae6d84da7f035b56c7fac1e4a1daacaacc6532ee8bdb8bd1af4",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 696,
"digest": "sha256:8359f4c0c1371d53b9edbf45bfd0641d53299bf4a9111bb0e4018549061ddf61",
"platform": {
"architecture": "arm64",
"os": "linux"
}
}
]
}
マルチプラットフォームなマニフェストリストになっているのがわかる。
Docker Hubにpush。マニフェストリストの場合はdocker manifest push
になる。
docker manifest push kun432/sample-multi:latest
Docker Hub上でもマルチプラットフォーム対応になっているのがわかる。
そういえば上の方でMac上でマルチプラットフォームイメージは--provenance=false
とかつけてなかったけど作成できたよな、と思っていたら、以下の記事にも書いてあった。
デフォルトのimage storeでも、Buildxであればマルチプラットフォームイメージをビルドできるじゃないか!?と思われた方もいると思いますが、Buildx経由の場合、image storeは経由せず、driver独自のキャッシュストレージでイメージは管理されます。Buildx経由であればマルチイメージプラットフォームイメージをビルドできるのはそのためです。
Unlike when using the default docker driver, images built using other drivers aren't automatically loaded into the local image store. If you don't specify an output, the build result is exported to the build cache only.
-- https://docs.docker.com/build/builders/drivers/#loading-to-local-image-store
この記事、本当にいろいろ参考になった。
以下の記事を参考に、マルチプラットフォームビルドして、ghcr.ioにpushするGitHub ActionsのサンプルをChatGPTに書いてもらった。
name: Build and push multi-platform image
on:
push:
tags:
- 'v*.*.*'
jobs:
build_push:
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image for ${{ matrix.arch }}
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: ${{ matrix.platform }}
tags: ghcr.io/${{ github.repository }}:${{ matrix.arch }}-${{ github.ref_name }}
push: true
merge:
name: Create and push manifest list
runs-on: ubuntu-latest
needs: build_push
permissions:
contents: read
packages: write
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Create manifest list and push
run: |
IMAGE="ghcr.io/${{ github.repository }}"
TAG="${{ github.ref_name }}"
echo "Creating manifest list for ${IMAGE}:${TAG}"
DIGEST_AMD64=$(docker buildx imagetools inspect ${IMAGE}:amd64-${TAG} --raw | jq -r '.manifests[] | select(.platform.architecture=="amd64") | .digest')
DIGEST_ARM64=$(docker buildx imagetools inspect ${IMAGE}:arm64-${TAG} --raw | jq -r '.manifests[] | select(.platform.architecture=="arm64") | .digest')
echo "amd64 digest: $DIGEST_AMD64"
echo "arm64 digest: $DIGEST_ARM64"
docker manifest create ${IMAGE}:${TAG} \
${IMAGE}@${DIGEST_AMD64} \
${IMAGE}@${DIGEST_ARM64}
docker manifest annotate ${IMAGE}:${TAG} ${IMAGE}@${DIGEST_ARM64} --os linux --arch arm64 --variant v8
docker manifest annotate ${IMAGE}:${TAG} ${IMAGE}@${DIGEST_AMD64} --os linux --arch amd64
docker manifest push ${IMAGE}:${TAG}
上の方で使ったcowsayのDockerfileをpushして、タグをpushすると自動的にビルドされる。
そしてパッケージとして利用できるようになる。
実際にMacとUbuntuの両方で試して問題ないことを確認できた。
docker run --rm ghcr.io/kun432/docker-multiplatform-build-github-action:v0.0.1
__
< >
--
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
なお、参考にさせていただいた記事ではdocker buildx imagetools
を使っている様子。
サンプルのレポジトリ