🤖

GitHub Actionsでarm64 runnerが利用できるようになったので、QEMUを使わずにDockerイメージをビルドする

2025/01/21に公開

フリープランでもARM64 Runnerが使えるようになった

GitHub Team と Enterprise Cloud plansに限られていたARM64 Runnerが、2025/01/16からパブリックレポジトリならフリープランでも利用できるようになりました。

https://github.blog/news-insights/product-news/arm64-on-github-actions-powering-faster-more-efficient-build-systems/

これにより、ワークフロー内のruns-onubuntu-24.04-armubuntu-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でのビルドに比べると結構面倒な手順になります。)

https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners

何はともあれ、基本は上記の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=ghacache-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