Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

Go言語のクロスコンパイルを活かすDocker BuildxのBUILDPLATFORM・TARGETARCH

Go言語で作ったアプリケーションをコンテナイメージにするとき、以下のようにマルチステージビルドを利用したDockerfileを書くことが多いでしょう。

FROM golang:1.21-bookworm as builder

WORKDIR /opt/app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -ldflags="-s -w" -o server ./cmd/server

FROM gcr.io/distroless/base-debian12:nonroot

COPY --from=builder --chown=nonroot:nonroot /opt/app/server /server

ENV PORT 8000
EXPOSE $PORT

ENTRYPOINT ["/server"]

ここで、ベースイメージである golang:1.21-bookwormgcr.io/distroless/base-debian12:nonroot はマルチアーキテクチャイメージなので、--platform オプションを利用することでこのDockerfileのままホストのアーキテクチャと異なるイメージをビルドすることができます。

例えばGitHub Actions上でbuildすると基本的にはamd64のランナーを使うことになりますが、docker/setup-qemu-actiondocker/setup-buildx-actiondocker/build-push-actionといったactionを利用することで、arm64向けのイメージをビルドすることができます。もちろんマルチアーキテクチャイメージを作ることも可能です。

docs.docker.com

ここで、amd64のホスト上で先ほどのDockerfileからarm64向けのイメージを作ろうとすると、最初のbuilderステージからQEMUを利用してarm64をエミュレートした環境でgo buildが行われます。エミュレーションなのでとても遅いです。自分の個人開発プロジェクトで作ったちょっとしたLambdaのイメージビルドに7分30秒もかかっていました。

今回は、これを1分30秒まで短縮する方法をご紹介します。

本画像は https://github.com/7474/NantoNBai をお借りして生成しました。分数の計算合ってる?

Go言語はクロスコンパイルが得意な言語なのに、先ほどのdocker buildの方法ではそれが活かせていないと言えます。しかし、かといってdockerの外でbuildしたバイナリをCOPYするような仕組みにするのも美しくありません。

Docker wayから外れずにGo言語の良さを活かすためのBuildxの機能がBUILDPLATFORM, TARGETARCH引数です。これらの引数はBuildKitを使う際に自動で設定されます。BUILDPLATFORMはbuild環境のplatform(例: linux/amd64)、TARGETARCHはビルドしたい対象のアーキテクチャ(例: arm64)です。

docs.docker.com

以下のようにDockerfileを書き換えることで、builderステージは必ずビルド環境と同じプラットフォームで実行されます。つまりこのステージでのエミュレーションが不要になり、ビルドが高速になります。一方、go build時にアーキテクチャを明示する必要が出てきたため、TARGETARCHをGOARCHとして渡してあげています。

-FROM golang:1.21-bookworm as builder
+FROM --platform=$BUILDPLATFORM golang:1.21-bookworm as builder
+ARG TARGETARCH

 WORKDIR /opt/app

 COPY go.mod go.sum ./
 RUN go mod download

 COPY . .
-RUN go build -ldflags="-s -w" -o server ./cmd/server
+RUN GOARCH=${TARGETARCH} go build -ldflags="-s -w" -o server ./cmd/server

 FROM gcr.io/distroless/base-debian12:nonroot

 COPY --from=builder --chown=nonroot:nonroot /opt/app/server /server

 ENV PORT 8000
 EXPOSE $PORT

 ENTRYPOINT ["/server"]