📑

PythonでもAWS Lamda用のデプロイパッケージをクロスビルドする

に公開

背景

AWS Lambda Web Adapter (LWA)[1]という任意のHTTPサーバーをAWS Lambdaアプリに変換する方法を知ってLambdaでデプロイするモチベーションが上がっていたところ、せっかくだからよりコスパがいいArm64環境を使いたいと思いました。AWS Lambdaへはzipファイルとコンテナーイメージを利用する2つの方法があります。マルチプラットフォーム対応のコンテナーを作る方法Dockerの公式doc[2]にあるように以下の3つの方法があります。

  1. QEMU(エミュレーター)を使用する
  2. 複数のnative環境を用意する
  3. クロスコンパイルする

1はオーバーヘッドが気になり、2は使い捨てのArm64環境を用意するのが面倒なので[3]3のアプローチでやりたいと思います。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ファイルを作るスクリプトはこのようになります。

build_package.sh
# 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_64aarch64PYTHON_VERISONには3.13などの文字列を指定します。pyproject.tomlで管理している前提で、まずuv.lockファイルの内容をrequirements.txt形式で書き出し、上で説明したpipコマンドでパッケージ内にインストールし、zipで固めます。run.shはLWAで使用するentrypointです[5]

なお、AWS Lambda上でLWAを使用して動かす際の設定については公式doc[5:1]を参照してください。

AWS Lambda対応Dockerfile

Dockerfileを使用してクロスビルドする場合は基本的にはmulti-stage build機能を使用し、builderはホストのプラットフォーム固定でrunnerは実際にターゲットのプラットフォームになるようにします[6]。Pythonの場合は以下のようになります。

Dockerfile
#---------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
build.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/amd64linux/arm64を別々に異なるtagをつけてbuild/pushする必要がある点に注意してください。また、--provenance=falseオプションも必要になります[7]

まとめ

Pythonのデプロイパッケージのクロスビルドの方法を説明しました。
よろしければこちらのプロジェクトテンプレートを使用してください。
https://github.com/lucidfrontier45/python-uv-template

脚注
  1. https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/ ↩︎

  2. https://docs.docker.com/build/building/multi-platform/#strategies ↩︎

  3. 例えばGitHub ActionsではArm64のrunnerは2025年8月時点ではprivate repoにはまだ解放されていない ↩︎

  4. https://github.com/pypa/manylinux ↩︎

  5. https://github.com/awslabs/aws-lambda-web-adapter?tab=readme-ov-file#lambda-functions-packaged-as-zip-package-for-aws-managed-runtimes ↩︎ ↩︎

  6. https://docs.docker.com/build/building/multi-platform/#cross-compilation ↩︎

  7. https://qiita.com/har1101/items/40717ac600559a6cb1bb ↩︎

Discussion