はじめに
LocalStack を使用した AWS Lambda 開発について、Docker Desktop の利用から Docker Engine に移行した際、 コードは全く同じなのにエラーが発生し実行できなくなってしまいました。
今回はその原因と解決策を共有します。
先に結論
- 異なるアーキテクチャでビルドされたシステムを Docker 環境で動かす場合、Docker Desktop なら勝手になんとかしてくれる(マルチアーキテクチャ)
- Docker Engine では勝手になんとかできないので、他アーキテクチャで動かすためのコンテナイメージを使う(tonistiigi/binfmt 等)
以下で詳しく説明していきます。
環境
- OS: Windows 11 (x86_64 / amd64)
- 移行前: Docker Desktop
- 移行後: Docker Engine (Ubuntu on WSL2)
- 対象システム: AWS Lambda (ランタイムは arm64 でビルド)
- 検証ツール: LocalStack
発生したエラー
色々あって、元々 Docker Desktop を使用し開発していたところを Docker Engine を利用することになりました。
既に Docker Desktop で構築した環境でサンプルコードの動作も確認済の状況で切り替えたのですが、いつもの通りビルド後に Lambda を動かすと、以下のエラーにより関数が正常に実行できなくなりました。
Runtime.InvalidEntrypoint
また変な環境問題?と焦りを覚えるも、詳細なログの確認やその他の調査をしていく中で、自分の環境では arm64 でビルドした Lambda って動かせないのでは?と当たり前の問題にたどりつきました。
原因:マルチアーキテクチャ対応の違い
原因は、Docker Desktop と Docker Engine の「マルチアーキテクチャ対応」のデフォルト動作の違いにありました。
Docker Desktop では自身のパソコンのアーキテクチャとコードのアーキテクチャの不一致を binfmt_misc(実行ファイルの関連付け機能) と QEMU(CPUエミュレータ) の仕組みでうまいこと解決してくれており、これによって arm64 でビルドされたコードも x86_64(amd64) 環境でも動かせていたということのようです。
ここで安直な自分、「じゃあ arm64 対応の Docker Engine いれれば何とかできるのかな~」なんて謎に思って実行しましたが、当然のごとく以下のようにエラーになりました。
# arm64 を取得する設定
$ echo \
"deb [arch=arm64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# x86_64 環境下で arm64 を取得しようとするのでエラーになります
$ sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:
The following packages have unmet dependencies:
containerd.io:arm64 : Depends: libc6:arm64 (>= 2.38) but it is not installable
Depends: libseccomp2:arm64 (>= 2.5.0) but it is not installable
docker-ce:arm64 : Depends: libseccomp2:arm64 (>= 2.3.0) but it is not installable
Depends: libc6:arm64 (>= 2.34) but it is not installable
Depends: libsystemd0:arm64 but it is not installable
Recommends: apparmor:arm64 but it is not installable
Recommends: docker-ce-rootless-extras:arm64 but it is not going to be installed
Recommends: libltdl7:arm64 but it is not installable
Recommends: pigz:arm64 but it is not installable
docker-ce-cli:arm64 : Depends: libc6:arm64 (>= 2.34) but it is not installable
Recommends: docker-buildx-plugin:arm64 but it is not going to be installed
Recommends: docker-compose-plugin:arm64 but it is not going to be installed
E: Unable to correct problems, you have held broken packages.
そうですよね、最初 amd64 とわざわざ指定して Docker Engine をインストールしていたんでした。ご、ごめんって…思いつつ、Docker Engine では自分の環境のコードしか実行できないの?となりますが、Docker Desktop が勝手にやってくれている QEMU の設定を Docker Engine でも行えばよいようです。
解決策: QEMU の有効化
まず、今後のために Docker の拡張ビルドツールである buildx をインストールしておきます(※今回調べたところ必須ではないようですが、推奨設定とのことです)。
$ sudo apt install -y docker-buildx-plugin
次に、本題である「エミュレータ(QEMU)」を有効化します。今回は compose ファイルを使用するので、以下の設定を compose.yml ファイルに記載します。
services:
emulator:
image: tonistiigi/binfmt # QEMU と登録スクリプトが入ったイメージ
container_name: emulator
privileged: true # ホスト OS のカーネル設定を書き換える権限
command: --install all # 全アーキテクチャ用の通訳を登録(もし arm64 のバイナリが来たら QEMU で実行する)
network_mode: bridge
restart: "no"
これにより arm64 のコードを起動した際に OS がこの設定を使用するようになるため、エラー無く Lambda が動かせるようになりました。
※ binfmt は Binary Formats Miscellaneous(バイナリ形式のその他のもの)の略で、「このファイルはこのプログラムで開く」というルールを実行ファイル(バイナリ)に対して設定できる Linux カーネルの機能とのことです。
ここでは tonistiigi/binfmt コンテナを privileged: true(特権モード) で実行することで、コンテナの中からホスト OS の設定を書き換え、「もしファイルの先頭が『arm64 特有の並び』で始まっていたら、CPU で直接実行せず QEMU という通訳アプリに渡してね」という指示を登録しています(「.xlsx は Excel で開く」という設定をするのと同じイメージです)。
まとめ
Docker Desktop は起動時に裏側で binfmt_misc の設定と QEMU のロードを自動的に行ってくれますが、Docker Engine で別のアーキテクチャのコードを動かす場合、手動で QEMU のセットアップが必要と知りました。知らないうちに対応してくれていた Docker Desktop に感謝ですね。
また、知識の浅さにより引っかかってしまったこの環境問題ですが、環境構築の際にアーキテクチャの違いを考慮する大事さが身に沁みました(一例ですが、devContainer 環境構築の際、npm パッケージがアーキテクチャによって異なるディレクトリ(/arm64 や /x86_64 など)に配置されたため、シェルスクリプトでそのパッケージのパスを固定して書いている場合にファイルが見つからずエラーになる、といった経験もありました。Mac と Windows が混在するチーム開発では、こうしたアーキテクチャの違いが思わぬ落とし穴になるため、考慮ポイントは色々ありますね。)
以上です。ここまでお読みくださってありがとうございました!