背景
AWS Lambda Web Adapter (LWA)1という任意のHTTPサーバーをAWS Lambdaアプリに変換する方法を知ってLambdaでデプロイするモチベーションが上がっていたところ、せっかくだからよりコスパがいいArm64環境を使いたいと思いました。AWS Lambdaへはzipファイルとコンテナーイメージを利用する2つの方法があります。マルチプラットフォーム対応のコンテナーを作る方法Dockerの公式doc2にあるように以下の3つの方法があります。
- QEMU(エミュレーター)を使用する
- 複数のnative環境を用意する
- クロスコンパイルする
1はオーバーヘッドが気になり、2は使い捨てのArm64環境を用意するのが面倒なので33のアプローチでやりたいと思います。GoやRustなどはクロスコンパイル情報がよく見つかりますが、Pythonで特にNumPyなどのネイティブバイナリを含んだ場合についてどうするか調べました。
ちなみに自分のプロジェクト自体はpure Pythonの前提です。C/C++/Fortran/Rustを含んでいる場合はそれぞれの言語のクロスコンパイルツールチェーンも必要になります。
クロスビルドのやり方
基本原理
Python自体はインタープリタ言語なのでソースコードを書けばプラットフォームを気にせずに使用できますが、実際にはデータサイエンス関連で使用するNumPy,PolarsやWeb開発で使用するPydanticなどネイティブバイナリを含んだライブラリを使用する場合がほとんどです。これらのライブラリはプラットフォーム依存ですので正しいものをパッケージングする必要があります。幸いにも今では主要なライブラリはすでにx86_64とArm64のLinux向けに事前にビルドしたバイナリを含んだwheelファイルをPyPIで配布していますので自分で毎回コンパイルすることはほとんどありません。正しいファイルをちゃんと選べば事足ります。これは[uv] pip install
するときに--python-platform
で指定することができます。
uv pip install --platform [x86_64|aarch64]-manylinux_2_34 --only-binary :all: --target package numpy
manylinux_x_y
4はざっくいりえばどのようなLinux上で動作するかを規定する規格で、manylinux_2_34
はglibc 2.34以降で動作するものです。Ubuntu 21.10以降やDebian 12以降そしてAWS Lambdaでも利用されるAmazon Linux 2023などに対応します。--only-binary :all:
はネイティブコードを含むパッケージは必ずビルド済みのwheelを選択するというオプションです。 --target
はインストール先を指定するオプションで、venv
内ではなくデプロイするパッケージの中に含めたいのでこれを含めます。
AWS Lambda用zipファイル
最終的にAWS Lambda用のzipファイルを作るスクリプトはこのようになります。
# define target
python_platform="${PYTHON_ARCH}-manylinux_2_34"
pyversion="$(echo py${PYTHON_VERSION} | tr -d '.')"
target_file_name="awslambda_package-${python_arch}-${pyversion}.zip"
target_file="${PWD}/${target_file_name}"
lock_file=".requirements.lock"
tmp_dir=".lambda_tmp"
rm -fr ${target_file} ${tmp_dir} ${lock_file}
# install to tmp_dir
uv export --no-dev --no-emit-workspace --frozen --all-extras > ${lock_file}
uv pip install --python-platform ${python_platform} --python-version ${python_version} --target ${tmp_dir} --only-binary :all: -r ${lock_file}
uv pip install --python-platform ${PYTHON_PLATFORM} --target package .
.
cp run.sh ${tmp_dir}
# create zip package
cd ${tmp_dir}
zip -r --exclude="*__pycache__/*" ${target_file} .
cd ..
# clean up
rm -fr ${tmp_dir} ${lock_file}
PYTHON_ARCH
にはx86_64
かaarch64
、PYTHON_VERISON
には3.13
などの文字列を指定します。pyproject.toml
で管理している前提で、まずuv.lock
ファイルの内容をrequirements.txt形式で書き出し、上で説明したpipコマンドでパッケージ内にインストールし、zipで固めます。run.sh
はLWAで使用するentrypointです5。
なお、AWS Lambda上でLWAを使用して動かす際の設定については公式doc5を参照してください。
AWS Lambda対応Dockerfile
Dockerfileを使用してクロスビルドする場合は基本的にはmulti-stage build機能を使用し、builderはホストのプラットフォーム固定でrunnerは実際にターゲットのプラットフォームになるようにします6。Pythonの場合は以下のようになります。
#---------builder------------
FROM --platform=$BUILDPLATFORM python:3.13-slim AS builder
WORKDIR /project
ARG TARGETOS
ARG TARGETARCH
RUN echo "Building for $TARGETOS/$TARGETARCH"
# install uv
RUN apt update && apt install -y curl
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"
# build package
WORKDIR /project
COPY pyproject.toml uv.lock build.sh /project/
COPY src /project/src
RUN bash build.sh
#---------runner------------
FROM python:3.13-slim-bookworm AS runner
WORKDIR /project
# add AWS Lambda Web Adapter settings
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter
COPY --from=builder /project/package /project
COPY run.sh /project/
SHELL ["/bin/bash", "-c"]
CMD run.sh
#!/bin/bash
# amd64 -> x86_64
# arm64 -> aarch64
PYTHON_ARCH=$(echo ${TARGETARCH} | sed -e 's/amd64/x86_64/' -e 's/arm64/aarch64/')
PYTHON_PLATFORM="${PYTHON_ARCH}-manylinux_2_34"
uv export --no-dev --no-emit-workspace --frozen --all-extras > .requirements.lock
uv pip install --python-platform ${PYTHON_PLATFORM} --target package --only-binary :all: -r .requirements.lock
uv pip install --python-platform ${PYTHON_PLATFORM} --target package .
ビルドの仕方自体は先ほどのzipの時と同じですが、platform指定文字列の変換が必要になります。Dockerfile内でuvをインストールする部分はuvの公式docにあるように
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
と書いてしまうとホストではなくターゲットのプラットフォームが使用されてしまい、現上COPY --from
にはFROM --platform=$BUILDPLATFORM
相当の指定ができないのでおとなしくcurl
をaptで入れて公式のインストールスクリプトを実行します。
あとはdocker build
する際に--platform linux/amd64,linux/arm64
をつければマルチプラットフォーム対応のコンテナーイメージができます。通常はこれでいいのですが、残念ながらAWS Lambdaはマルチプラットフォームイメージには対応しておらず、仕方ないのでlinux/amd64
とlinux/arm64
を別々に異なるtagをつけてbuild/pushする必要がある点に注意してください。また、--provenance=false
オプションも必要になります7。
まとめ
Pythonのデプロイパッケージのクロスビルドの方法を説明しました。
よろしければこちらのプロジェクトテンプレートを使用してください。
https://github.com/lucidfrontier45/python-uv-template
-
https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/ ↩
-
https://docs.docker.com/build/building/multi-platform/#strategies ↩
-
例えばGitHub ActionsではArm64のrunnerは2025年8月時点ではprivate repoにはまだ解放されていない ↩
-
https://github.com/awslabs/aws-lambda-web-adapter?tab=readme-ov-file#lambda-functions-packaged-as-zip-package-for-aws-managed-runtimes ↩ ↩2
-
https://docs.docker.com/build/building/multi-platform/#cross-compilation ↩