QEMUエミュレーションを廃止してarm64ネイティブビルドへ移行
従来のDockerイメージビルドでQEMUエミュレーションによって発生していた速度低下を解消するため、arm64イメージのビルドをネイティブのarm64 Ubuntuランナー上で実行するよう変更されました。あわせてワークフロー定義のリファクタリングも行われ、重複の排除とGitHub Actions UIからのトリガー操作の簡略化も実現しています。
背景
QEMUによるクロスアーキテクチャビルドは、単一マシンでamd64/arm64の両イメージを生成できる手軽な手法ですが、エミュレーションのオーバーヘッドによりビルド時間が大幅に増加するという問題がありました。GitHub ActionsではUbuntu 24.04のarm64ランナー(ubuntu-24.04-arm)が利用可能になったことで、エミュレーションなしにarm64イメージをネイティブビルドできる環境が整っています。
各アーキテクチャをそれぞれネイティブランナーでビルドする方式に切り替えることで、QEMUの導入コスト自体を排除できます。ただしアーキテクチャごとに独立したイメージタグがGHCRにプッシュされることになるため、最終的にmulti-archマニフェストを作成してまとめるステップが別途必要になります。
技術的な変更
アクション定義の拡張(build-and-publish-image)
build-and-publish-image アクションに3つの新しい入力パラメータが追加され、シングルアーキテクチャビルドとマニフェスト発行の両モードを1つのアクションで扱えるようになりました。
追加されたパラメータは以下のとおりです:
-
build_platform: ビルド対象のプラットフォーム(linux/amd64またはlinux/arm64) -
platform_suffix: アーキテクチャ固有タグのサフィックス(amd64またはarm64) -
publish_manifest:trueを指定するとビルドを行わずmulti-archマニフェストの発行のみを実行
変更前:
- name: Set up QEMU for multi-architecture builds
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
変更後:
- name: Set up Docker Buildx
if: ${{ inputs.publish_manifest != 'true' }}
uses: docker/setup-buildx-action@v3
with:
platforms: ${{ inputs.build_platform }}
docker/setup-qemu-action のステップが完全に削除され、setup-buildx-action には単一プラットフォームのみを渡すようになっています。また publish_manifest が true の場合はCheckoutやBuildxのセットアップを含む多くのステップが if 条件でスキップされます。入力の整合性は冒頭の Validate required inputs ステップでシェルスクリプトによりチェックされます。
再利用可能ワークフローの新設
新たに .github/workflows/publish-images-reusable.yaml が追加され、ビルドのコアロジックがここに集約されました。このワークフローはビルドジョブのmatrixにアーキテクチャ軸を追加しており、ubuntu-24.04(amd64)と ubuntu-24.04-arm(arm64)の2ランナーが並列実行されます。
matrix:
RUBY_VERSION: ${{ fromJSON(inputs.ruby_versions_json) }}
IMAGE_VERSION: ${{ fromJSON(inputs.image_versions_json) }}
TARGET:
- RUNNER: ubuntu-24.04
BUILD_PLATFORM: linux/amd64
PLATFORM_SUFFIX: amd64
- RUNNER: ubuntu-24.04-arm
BUILD_PLATFORM: linux/arm64
PLATFORM_SUFFIX: arm64
Rubyバージョン・イメージバージョン・ターゲットアーキテクチャの3次元matrixによって、必要な全組み合わせが並列ビルドされます。このワークフローは workflow_call トリガーのみで起動し、既存の2つのワークフロー(タグ起動・手動起動)から uses: で呼び出される構成です。
入力正規化スクリプトの追加
手動トリガーワークフロー(publish-new-ruby-versions.yaml)の入力形式が改善されました。従来はJSONアレイ形式(例: ["3.3.1","3.2.4"])での入力が必要でしたが、カンマまたは改行区切りのテキスト(例: 3.3.1, 3.2.4)を受け付けるようになっています。
module Commands
class PublishInputNormalizer
def call
ruby_versions = normalize_list(ruby_versions_input)
raise Error, "Ruby versions input is empty" if ruby_versions.empty?
images_source = image_versions_input
if blank_list?(images_source)
images_source = latest_tag_fetcher.call(repository)
end
image_versions = normalize_image_versions(normalize_list(images_source))
raise Error, "Image versions input is empty" if image_versions.empty?
{
ruby_versions: ruby_versions,
image_versions: image_versions,
ruby_versions_json: JSON.generate(ruby_versions),
image_versions_json: JSON.generate(image_versions)
}
end
end
end
PublishInputNormalizer クラスは入力文字列の正規化、ruby- プレフィックスの付与、およびimage_versionsが空の場合にリポジトリの最新タグを自動取得する機能を持ちます。latest_tag_fetcher をDIとして外部から注入できる設計になっており、テストでモック差し替えが容易です。bin/normalize-publish-inputs スクリプト経由でGitHub Actionsのステップから呼び出され、出力は GITHUB_OUTPUT 形式でセットされます。
設計判断
アーキテクチャごとにジョブを分割してネイティブランナーで実行するアプローチ が採用されました。
代替案として単一ランナーでQEMUを使いつつ並列化する方法も存在しますが、このPRではネイティブビルドを選択することでエミュレーションオーバーヘッドを根本から排除しています。matrixのアーキテクチャ軸をワークフローYAML内に宣言的に定義することで、将来的なアーキテクチャ追加も行次元のエントリ追加だけで対応できます。
再利用可能ワークフロー(workflow_call)によるロジックの集約は、タグ起動・手動起動の2つのエントリポイントでビルド定義が重複していた問題を解消するものです。また入力正規化のロジックをRubyクラスとして実装することで、シェルスクリプトで実装するよりもテスタビリティを高めており、実際に Minitest によるユニットテストとスクリプトレベルの統合テストが追加されています。
まとめ
QEMUエミュレーションの廃止によりarm64イメージのビルド速度を改善しつつ、再利用可能ワークフローと入力正規化スクリプトの導入でワークフロー定義の重複と運用上の煩雑さを同時に解消した変更です。ネイティブランナーへの移行・宣言的なmatrix定義・テスト付きのRubyクラスへのロジック分離という3つの判断が組み合わさることで、ビルドパイプラインの速度と保守性が同時に向上しています。