GitHub Actionsでarm64 runnerが利用できるようになったので、QEMUを使わずにDockerイメージをビルドする
フリープランでもARM64 Runnerが使えるようになった
GitHub Team と Enterprise Cloud plansに限られていたARM64 Runnerが、2025/01/16からパブリックレポジトリならフリープランでも利用できるようになりました。
これにより、ワークフロー内のruns-on
でubuntu-24.04-arm
とubuntu-22.04-arm
を指定するだけでARM64のRunnerで処理がおこなわれます。
この新しいRunnerを使うことでARM向けバイナリを用意する際に、従来のQEMUのアーキテクチャエミュレートを使ったクロスコンパイルが不要になります。個人的には、ActionsのDockerのARM64環境向けのイメージ生成が爆速化されたのが大きいです。
ActionsでGHCRにマルチアーキテクチャ対応のDockerイメージをビルドする
従来はamd64(x86_64)のRunner内でQEMUを展開して、複数のアーキテクチャのイメージを生成させるだけでしたが、Runner自体で分岐する必要があります。
Dockerの公式ドキュメント内で、「Distribute build across multiple runners」の項で触れられていますが、job内でstrategy.matrix
を指定して、個別にイメージを生成し、digestsを識別子としてartifactに出力させて、最後にマージという作業が必要となります。(QEMUでのビルドに比べると結構面倒な手順になります。)
何はともあれ、基本は上記のDockerの公式ドキュメントのテンプレを参考に実際に使ってみて困ったことをまとめます。
Dockerイメージには大文字が使えない問題
元になったのWorkflow
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
~~~省略~~~
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
公式ドキュメントから抜粋した項目ですが、GitHubのユーザー名やレポジトリ名には大文字が利用できますが、Dockerのイメージ名としては大文字が利用できません。
変更後のWorkflow
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
echo "REGISTRY_IMAGE=`echo ghcr.io/ghcr.io/${{github.repository}} | tr '[:upper:]' '[:lower:]'`" >> ${GITHUB_ENV}
~~~省略~~~
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
Prepareのstepでシュル芸をして、大文字は小文字に変換した状態でREGISTRY_IMAGE
を定義します(GHCRはGitHubのユーザー名が小文字状態でも受け付けます)。
最後にビルドが終わったプラットフォームしかキャッシュが効かない問題
docker/build-push-action
内でcache-from: type=gha
やcache-to: type=gha,mode=max
などと指定するとGitHub Actionsのキャッシュが利用できるようになりますが、この指定方法では不十分です。buildxのキャッシュを示すの識別子が被るため、indexのキャッシュが上書きされてしまいます。
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=platform-${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=platform-${{ env.PLATFORM_PAIR }}
scope=platform-${{ env.PLATFORM_PAIR }}
のようにプラットフォーム事にスコープを付加して対応します。
試行錯誤した結果、汎用的に使えるようにしたWorkflowのテンプレート
name: build docker image
run-name: build docker image - ${{ github.event_name }}
on:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: read
packages: write
jobs:
BuildDockerImage:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: linux/amd64
- os: ubuntu-24.04-arm
platform: linux/arm64
runs-on: ${{ matrix.os }}
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
echo "REGISTRY_IMAGE=`echo ghcr.io/${{github.repository}} | tr '[:upper:]' '[:lower:]'`" >> ${GITHUB_ENV}
- name: Checkout code
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Build and push by digest
uses: docker/build-push-action@v6
id: build
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=platform-${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=platform-${{ env.PLATFORM_PAIR }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- BuildDockerImage
steps:
- name: Prepare
run: |
echo "REGISTRY_IMAGE=`echo ghcr.io/${{github.repository}} | tr '[:upper:]' '[:lower:]'`" >> ${GITHUB_ENV}
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=ref,prefix=pr-,suffix=,event=pr
type=sha
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
Discussion