Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

GitHub Actions で amd64/arm64 両対応の Docker イメージをビルド

PHP 開発環境の Docker イメージとして公開している shin1x1/php-dev イメージの arm64 対応を行いました。従来の amd64 も必要なので、マルチアーキテクチャビルドでイメージを生成するようにしています。

shin1x1/php-dev については下記を参照で。 blog.shin1x1.com

Docker Buildx による multi-arch ビルド

Docker Buildx は Buildkit でビルド機能を拡張する Docker CLI プラグインです。Buildx にはマルチアーキテクチャビルド機能があるので、これを利用します。

docs.docker.com

実行の流れを掴むために M1 Mac の Docker Desktop で Buildx を利用してビルドしてみます。Docker Desktop 4.3.1 には Buildx が同梱されており、デフォルトで buildx コマンドが有効となっていました。

$ docker buildx version
github.com/docker/buildx v0.7.1 05846896d149da05f3d6fd1e7770da187b52a247

マルチアーキテクチャビルドを実行するために新規ビルダーを作成します。下記では multi-arch という名前のビルダーを作成し、有効にしています。

$ docker buildx create --use --name multi-arch
multi-arch
$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS   PLATFORMS
multi-arch *    docker-container
  multi-arch0   unix:///var/run/docker.sock inactive
desktop-linux   docker
  desktop-linux desktop-linux               running  linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default         docker
  default       default                     running  linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

multi-archドライバが inactive になっているので起動します。

$ docker buildx inspect --builder multi-arch --bootstrap
[+] Building 3.2s (1/1) FINISHED
 => [internal] booting buildkit                                                                                                                                                      3.2s
 => => pulling image moby/buildkit:buildx-stable-1                                                                                                                                   2.8s
 => => creating container buildx_buildkit_multi-arch0                                                                                                                                0.4s
Name:   multi-arch
Driver: docker-container

Nodes:
Name:      multi-arch0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

ビルドするイメージの Dockerfile を作成します。ここでは、alpine イメージにファイル /hello を保存しただけのものです。これで準備ができました。

FROM alpine

RUN echo 'Hello' > /hello

ビルドするには、docker buildx build コマンドを利用します。--platformオプションでターゲットアーキテクチャを指定します。ここでは、linux/amd64linux/arm64を指定しています。--pushオプションを付けることでビルドしたイメージをそのまま Docker Hub にプッシュします。

$ docker buildx build --platform linux/amd64,linux/arm64 -t shin1x1/test:latest --push .
(snip)
 => => pushing layers                                                                                                                                                                8.5s
 => => pushing manifest for docker.io/shin1x1/test:latest@sha256:32400fa23ce020b150c88c107e3995de7ace477f51c6fae2ea6c51b6329f0e4a                                                    3.9s
 => [auth] shin1x1/test:pull,push token for registry-1.docker.io                                                                                                                     0.0s
 => [auth] shin1x1/test:pull,push token for registry-1.docker.io

実行完了後に Docker Hub にアクセスすると、イメージタグに linux/amd64linux/arm64が表示されており、両アーキテクチャのイメージがプッシュされていることが分かります。

f:id:shin1x1:20211214105825p:plain

Buildx を利用することで簡単に両アーキテクチャに対応したイメージをビルド、プッシュできます。この操作を GitHub Actions で自動実行します。

GitHub Actions で multi-arch ビルド & プッシュ

GitHub Actions では、Docker Buildx でマルチアーキテクチャビルドするためのアクションとして docker/build-push-action が Docker から公開されています。こちらにセットアップを含めた利用例があるので、これを参照にアクションを記述しました。

github.com

記述したアクションが下記です。上から順に、QEMU、Docker Buildx、Docker Hub ログインとセットアップを進めています。その後、docker/build-push-action を利用して Docker イメージのビルド、プッシュを行います。platforms キーではターゲットのアーキテクチャとして linux/amd64linux/arm64を指定し、tags キーでイメージとタグ名を指定しています。

このアクションがトリガーされると Docker イメージがビルドされ、Docker Hub にプッシュされます。

https://github.com/shin1x1/docker-php-dev/blob/master/.github/workflows/build-and-push.yml

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - run: echo ${{github.ref_name}}

      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: .
          file: Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: shin1x1/php-dev:${{github.ref_name}}

実際に公開されているのが下記です。amd64arm64のイメージが公開されています。

https://hub.docker.com/r/shin1x1/php-dev/tags

f:id:shin1x1:20211214105846p:plain

ビルドが遅い

GitHub Actions でマルチアーキテクチャビルドを実行してみると、完了するまで 30 分から 40 分(!) かかりました。ビルド時間は下記のようになっており、linux/arm64 のビルドでは約 40 分かかっています。

  • linux/amd64: 148.1s *1
  • linux/arm64: 2391.8s *2

これは M1 Pro Mac でも同様で、同じ Dockerfile をマルチアーキテクチャビルドしてみると、amd64 イメージについては 900s(15m) ほどかかりました。

今回のイメージでは PHP 拡張のビルド処理が入っており、こうした処理をホストとは異なるアーキテクチャのイメージ内で実行すると、QEMU によるオーバーヘッドが発生します。これは仕組み上致し方ないですが、マルチアーキテクチャビルドの場合、ビルド時間については認識しておく必要があります。

f:id:shin1x1:20211214110412p:plain

ビルド速度改善アイデア

このビルド速度を改善するアイデアとして、2つの方法が Docker Blog で紹介されていました。

案 1 は、複数ホストによるビルドです。

Buildx では、QEMU を利用した単一ホストによる多アーキテクチャビルドだけでなく、複数のホストによるビルドもサポートしています。これを利用すれば、linux/amd64 イメージは x86_64 環境、linux/arm64 イメージは arm64 環境と専用の環境でビルドできます。CircleCI には arm64 の VM もあるようなので、どうにかできないかなと考えたりもします。

www.docker.com

案 2 は、クロスコンパイルでターゲットアーキテクチャのバイナリを生成する方法です。

下記では、ホストアーキテクチャイメージ内で Go アプリケーションをビルドして、ターゲットアーキテクチャのバイナリを生成しています。つまり、x86_64 環境であれば build ステージは常に amd64 イメージで実行して、go buildコマンドでターゲットアーキテクチャを指定することで目的のバイナリを生成します。最後に生成したバイナリをターゲットアーキテクチャのイメージにコピーするという流れです。

FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS build
WORKDIR /src
COPY . .
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/myapp .

FROM alpine
COPY --from=build /out/myapp /bin

shin1x1/php-dev ではこの方法は難しそうですが、Go や Rust のようにクロスコンパイルが容易なアプリケーションであれば単一のホストで実現でき、効果も大きいので良さそうです。

www.docker.com

GitHub Actions 並列実行

上記では 1 ビルドが遅いと書きましたが、GitHub Actions では並列に実行できるので、複数イメージをビルドする場合でもトータルの時間は 1 ビルド時間 + α 程度で完了します。shin1x1/php-dev では、Git タグを push すると GitHub Action が実行されてビルド、プッシュを行うようにしています。28 タグを一気に更新したところ、28 並列で実行され、トータルでは 1h 弱で全てのイメージを Docker Hub にプッシュできました。28 並列が一気に起動して動くのは爽快でした :)

f:id:shin1x1:20211214110532j:plain

この対応の前に利用していた Docker Hub の autobuild では、1 イメージのビルドは 1-3m 程度で終わる(amd64 のみ)のですが、直列にビルドが実行されるので、トータルの時間ではこちらの方が遅かったかもしれません。

これだけの並列にアクションを一度に動かして、それが無料*3というのはすごいですね。ありがたい。

さいごに

M1 Mac の登場により、現時点ではチーム内で利用するアーキテクチャが混在する場面が想定されます。また、いずれ開発環境は arm64 に統一されても、本番環境が x86_64 のままということも十分にありえるでしょう。今後、複数アーキテクチャの Docker イメージが必要な場面が増えてきそうです。

現状でも、Buildx を使うと手軽にビルドできるのですが、ビルド速度が遅いのがネックなので、GitHub Actions や CircleCI などの CI サービスで上手い具合に対応してもらえると嬉しいですね。