CI/CD
マルチアーキテクチャビルドのベストプラクティス
amd64 と arm64 の両方に配布する必要が出てきたときの buildx 構成。QEMU エミュレーションを避けて時間を短縮する方法も。
なぜマルチアーキか
単一アーキだけで配布すると、以下で困ります。
- 開発者の 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 build、go 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 のキャッシュが互いを潰し合う
