午前2時のアーキテクチャの悪夢
午前2時、P1インシデントのアラートでスマホが震えました。開発者が重要なホットフィックスをプッシュしたばかりで、CI/CDパイプラインは正常終了(緑色)していましたが、本番ノードの半分が CrashLoopBackOff で停止していました。ログを確認すると、不可解でフラストレーションの溜まるエラーが表示されていました:standard_init_linux.go:211: exec user process caused "exec format error"。
問題は単純ですが致命的でした。最近、コストパフォーマンスを40%向上させるためにワーカーノードをAWS Graviton (ARM64) に移行したばかりだったのです。しかし、ビルドサーバーは依然として従来のIntel (AMD64) マシンでした。x86の命令セットを、その言語を理解できないARMプロセッサにプッシュしてしまったのです。これは現代のDevOpsにおける典型的な罠です。私たちはマルチアーキテクチャの世界に生きていますが、ビルドツールは過去に囚われたままになりがちなのです。
なぜ単一アーキテクチャのイメージは失敗するのか
すべてがAMD64で動作していた頃はシンプルでした。しかし今や、ARM64はどこにでもあります。AppleのM3チップ、AWS Graviton3、Google CloudのTau T2Aインスタンスなどで採用されています。ローカルのMacBookでビルドしたイメージをLinuxサーバーにプッシュすると、不一致が発生するリスクがあります。CIランナーがビルドしたイメージが、最終的にエッジデバイスやRaspberry Piにデプロイされる場合も同様です。
「Exec Format Error」の正体
exec format error が発生した場合、コンテナ内のバイナリがホストCPUの命令セットと互換性がないことを意味します。これを無理やり動かすことはできません。デプロイ先のハードウェアが必要とする特定のバイナリをパッケージ化したコンテナイメージが必要です。マルチアーキテクチャイメージは、複数のプラットフォームを単一のタグにまとめることで、この問題を解決します。
解決策:Docker BuildxとQEMU
このために別々のDockerfileを管理したり、複雑なパイプラインを構築したりする必要はありません。代わりに、Buildx と QEMU を使用して、重労働を自動化します。
Buildxとは?
Buildxは、標準の docker build コマンドを置き換えるDocker CLIプラグインです。Moby BuildKitエンジンを活用して「ビルダーインスタンス」を作成します。これらのインスタンスは、複数のプラットフォーム向けに同時にコンパイルし、それらを単一のマニフェストリストにまとめることができます。
QEMUが架け橋となる仕組み
AMD64マシンは、ネイティブでARM命令を実行することはできません。QEMU (Quick Emulator) は翻訳レイヤーとして機能します。これにより、Buildxはビルドプロセス中に非ネイティブなコマンドを実行できるようになります。例えば、QEMUを使用することで、Intelホスト上でARMターゲット向けの npm install や apt-get を、ソフトウェア的にARM環境を模倣して実行できるのです。
実践的な実装:両方の環境向けにビルドする
私はこのワークフローを本番環境で2年以上使用しています。手動の介入なしに、イメージがどこでも実行できることを保証する最も安定した方法です。
ステップ1:マルチアーキテクチャビルダーの初期化
Dockerのデフォルトドライバーはマルチアーキテクチャビルドをサポートしていません。これらの機能を解放するには、docker-container ドライバーを使用して新しいビルダーインスタンスを作成する必要があります。
# 'mybuilder'という名前の新しいビルダーを作成
docker buildx create --name mybuilder --use
# ビルダーを起動して機能をチェック
docker buildx inspect --bootstrap
出力結果でサポートされているプラットフォームを確認してください。もし linux/arm64 が含まれていない場合は、次のステップでエミュレーションを有効にする必要があります。
ステップ2:QEMUによるエミュレーションの有効化
ほとんどのLinuxディストリビューションや標準のGitHub Actionsランナーでは、QEMUハンドラーを登録する必要があります。これにより、カーネルに他プラットフォームのバイナリの処理方法を教えます。
# binfmt_miscハンドラーを登録
docker run --privileged --rm tonistiigi/binfmt --install all
再び docker buildx ls を実行すると、linux/amd64、linux/arm64、さらには linux/riscv64 など、サポートされているプラットフォームの膨大なリストが表示されるはずです。
ステップ3:ビルドとプッシュ
ここがオーケストレーションの本番です。docker build を docker buildx build に置き換えます。--platform フラグでターゲットを定義し、--push でパッケージ全体をレジストリに送信します。
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t your-registry.com/my-app:v1.0.2 \
--push .
Dockerはイメージを2回ビルドします(各アーキテクチャにつき1回)。その後、両方のバージョンをマニフェストリストと共にレジストリにプッシュします。ユーザーが my-app:v1.0.2 をプルすると、Dockerエンジンが自動的にホストのCPUを検出し、正しいバージョンを取得します。まさに「魔法のように」動作します。
GitHub Actionsによる自動化
手動ビルドはローカルでのテストには適していますが、本番環境には自動化が必要です。GitHub Actionsには、このセットアップを簡単にする公式モジュールが用意されています。
以下は、本番環境に対応したワークフロー(.github/workflows/docker-build.yml)の例です:
name: マルチアーキテクチャイメージのビルド
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: コードのチェックアウト
uses: actions/checkout@v4
- name: QEMUのセットアップ
uses: docker/setup-qemu-action@v3
- name: Docker Buildxのセットアップ
uses: docker/setup-buildx-action@v3
- name: DockerHubへのログイン
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: ビルドとプッシュ
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: user/app:latest
避けるべき一般的な落とし穴
適切なツールを使用しても、ビルドを壊したり速度を低下させたりするいくつかの罠があります。
- ビルド速度: エミュレーションはリソースを大量に消費します。QEMU経由でAMD64ランナー上でARM64イメージをビルドすると、ネイティブビルドより5倍遅くなることがあります。5分のビルドが突然25分かかるようになった場合は、ネイティブなARM64ランナーの使用を検討してください。
- ハードコードされたバイナリ: Dockerfile内で
RUN curl -O https://example.com/tool-x86_64のような記述は避けてください。これはARMバージョンでエラーになります。TARGETARCH引数を使用して、動的に正しいバイナリを取得するようにします。
# アーキテクチャを動的に処理
ARG TARGETARCH
RUN curl -L https://github.com/some/tool/releases/download/v1.0/tool-${TARGETARCH} -o /usr/local/bin/tool
結論
マルチアーキテクチャビルドは、現代のクラウドインフラストラクチャにおいて必須要件です。自分のマシンがM3 Macで、サーバーがXeonプロセッサである場合に起こる「自分のマシンでは動くのに」という言い訳を排除できます。BuildxとQEMUをCI/CDパイプラインに統合することで、ソフトウェアのポータビリティを高め、将来に備えることができます。アーキテクチャに起因する障害に怯えるのはもうやめて、機能の開発に集中しましょう。

