Dock Stay
マルチアーキテクチャビルドのベストプラクティス
CI/CD

マルチアーキテクチャビルドのベストプラクティス

amd64 と arm64 の両方に配布する必要が出てきたときの buildx 構成。QEMU エミュレーションを避けて時間を短縮する方法も。

マルチアーキテクチャビルドのベストプラクティス diagram

なぜマルチアーキか

単一アーキだけで配布すると、以下で困ります。

  • 開発者の Mac(Apple Silicon = arm64)と CI / 本番サーバー(amd64)でアーキが違う
  • AWS Graviton(arm64)や Ampere Altra ベースのサーバーでコスト最適化したい
  • 公式イメージ(node, python, postgres 等)はほぼ全てマルチアーキ。自分のイメージだけ amd64 単一だとダウンストリームが困る

一方、不要ならやらない ほうが良いです。

ビルド時間は当然伸び、QEMU エミュレーションは遅いのでランナーコストも膨らみます。

まずは本当に arm64 が必要か確認してから導入しましょう。

基本: QEMU エミュレーションでのマルチアーキビルド

最も手軽な方法。

ubuntu-latest(amd64)のランナー上で QEMU を使って arm64 バイナリもエミュレーションで生成します。

セットアップが単純で、追加のランナーも不要です。

欠点は とにかく遅い こと。

特にネイティブコンパイルを含むステップ(npm install でネイティブモジュール、cargo buildgo build のクロスコンパイルなし)は 5〜10 倍遅くなることも珍しくありません。

bash
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

推奨: アーキ別ランナーでネイティブビルドし最後に結合

GitHub Actions は 2024 年以降、arm64 のマネージドランナー(ubuntu-24.04-arm)を提供しています。

amd64 と arm64 をそれぞれネイティブランナーで並列ビルドし、最後に docker buildx imagetools create でマニフェストリストを合成すると、QEMU なしで高速にマルチアーキが作れます。

手順は少し複雑ですが、ビルド時間が 1/5〜1/10 になるプロジェクトも珍しくなく、arm64 を真面目に配布するなら検討する価値があります。

bash
jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: linux/amd64
            runner: ubuntu-latest
          - platform: linux/arm64
            runner: ubuntu-24.04-arm
    runs-on: ${{ matrix.runner }}
    outputs:
      digest-amd64: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: ${{ matrix.platform }}
          outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true
          cache-from: type=gha,scope=${{ matrix.platform }}
          cache-to: type=gha,mode=max,scope=${{ matrix.platform }}

  merge:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Create manifest list
        run: |
          docker buildx imagetools create \
            -t ghcr.io/${{ github.repository }}:${{ github.sha }} \
            -t ghcr.io/${{ github.repository }}:latest \
            $(cat $GITHUB_OUTPUT | grep digest | awk -F= '{print "ghcr.io/${{ github.repository }}@" $2}')

注意したい落とし穴

  • platforms: linux/amd64,linux/arm64 だけ書いて満足しない: ベースイメージが両アーキ対応か必ず確認する。docker buildx imagetools inspect node:20 でマニフェストリストを確認
  • ネイティブ依存のテストがアーキ別で走るか: CI で linux/amd64 のテストしか走らせていないと、arm64 固有バグ(CPU 命令・エンディアン・アライメント)を見落とす
  • ビルド用のアーキ別インストール: apt-get install -y gcc-aarch64-linux-gnu のようにクロスコンパイラを呼ぶ場合、QEMU で arm64 コンテナ内から apt-get install gcc だけすればネイティブ gcc が入る(--platform 指定で勝手にアーキが切り替わる)ので、Dockerfile は基本的にはアーキ非依存で書ける
  • タグの衝突: push-by-digest=true でビルド → imagetools create でマニフェスト合成、という流れを守らないと、別アーキのビルドが後勝ちで同タグを上書きしてしまう
  • キャッシュスコープを分ける: 上の例のように cache-to: ... scope=${{ matrix.platform }} で分けないと、amd64 と arm64 のキャッシュが互いを潰し合う