Lambda Layer の pandas 1.3.4 で numpy 2.0 ImportError、ABI 非互換の原因と対処
こんにちは、データ事業本部のまっきーです。
Lambda 関数で使っている依存ライブラリを更新するために、Layer の requirements.txt を見直して再ビルドする機会がありました。
ビルド自体は問題なく通ったのですが、デプロイ後に関数を実行すると ImportError が出るようになっていました。
調べていったら、Layer の中身が次のような組み合わせになっていました。
- pandas 1.3.4(自分で
requirements.txtに指定したバージョン) - numpy 2.0.2(pandas の依存解決で、pip が勝手に入れた最新版)
この pandas と numpy の組み合わせで ABI 非互換 が起きて、Lambda の起動時にロード失敗していた、というのが今回の現象です。
本記事では、その事象の整理と、対処までの流れを残しておきます。
結論を先に
何が起きていたか
requirements.txt で pandas を == 固定していても、その pandas が裏で使っている numpy(間接依存)の上限が指定されていない場合、pip は最新の numpy を一緒にインストールします。今回はそれが numpy 2.0 系で、pandas の C 拡張と ABI 不整合を起こして動かなくなっていました。
どう直したか
間接依存の numpy も requirements.txt に明示的に == で固定する、再現性のために sam build --use-container でコンテナビルドする、の 2 点で対処しました。
numpy 2.0 系で何が変わったのか
numpy は 2024 年 6 月にメジャーバージョン 2.0 をリリースしました。このバージョンで C 拡張ライブラリの ABI(Application Binary Interface)が変更されています。numpy 1.x 系を前提にコンパイルされた C 拡張ライブラリを numpy 2.0 系のランタイムで import すると ImportError 系のエラーで失敗するようになりました。
公式リリースノート(NumPy 2.0.0 Release Notes)でも np.float_ や np.unicode_ といった旧 API の削除など、後方互換性を破る変更が明示されています。
ABI(Application Binary Interface)とは
私自身も今回の調査で初めてちゃんと向き合った概念なので、整理しておきます。
ABI は 「コンパイル済みのプログラム同士が、メモリレベルで会話するための約束事」 です。「メモリ上でこの構造体はこの順序で並んでいて、どのオフセットに長さ情報があって、データへのポインタはここにある」といった、バイナリレベルの取り決めを指します。
普段 Python で書くコードからは見えづらいのですが、numpy や pandas のような C 拡張ライブラリ(内部に C 言語のバイナリを含むライブラリ) は、内部で C で書かれたバイナリと連携しています。pandas の C 拡張は、numpy のメモリ構造を前提にコンパイルされています。
numpy 2.0 ではその内部のメモリ構造の並びが変わりました。すると、
pandas: 「numpy の配列はこの並びで、ここに長さ、ここにデータがあるはず」
↓ メモリにアクセス
numpy 2.0:「いや、その並び変わったよ」
↓
ImportError 💥
という形で、ソースコード上は同じ numpy を import しているのに、バイナリの中身が噛み合わずにロードが失敗します。
ピュア Python のライブラリ(中身が Python のみで書かれているもの)であれば ABI は関係ないので、pip install するだけで普通に動きます。ABI 互換性が問題になるのは numpy / pandas / pyarrow のような C 拡張ライブラリで、これらはメジャーバージョンアップで ABI が変わると、依存先のライブラリも一緒に壊れる、という構造になっています。
どんな事象が起きたか
私が確認した症状は、Lambda 関数の起動時に次のようなエラーが出るというものでした。
A module that was compiled using NumPy 1.x cannot be run in
NumPy 2.x.x as it may crash.
ライブラリによっては、もう少し古典的な次の形でも出ます。
ImportError: numpy.core.multiarray failed to import
Lambda Layer の中身を確認すると、requirements.txt には pandas のバージョンだけが書かれていて、numpy のバージョン指定はありませんでした。
# layers/data_capture/requirements.txt(変更前)
pandas==1.3.4
(numpy の記載なし)
pandas 1.3.4 のメタデータを PyPI で確認すると、Requires-Dist: numpy (>=1.17.3) となっていて 上限指定がありません。なので pip は最新の numpy(2.0 系)を選んで一緒にインストールしてしまいます。
結果として、Layer の中身は次のような組み合わせになっていました。
| 項目 | 値 |
|---|---|
| pandas | 1.3.4(2021 年リリース、当時 numpy 2.x は未リリース) |
| numpy | 2.0.2(pip が自動で選んで入った最新版) |
pandas 1.3.4 が numpy 2.0 に正式対応していないので、ロード時に ABI 不整合で落ちる、という構図でした。なお pandas が numpy 2.0 に正式対応したのは pandas 2.2.2(2024-04-10)以降です(pandas What's new)。
事象が起きた流れを図にすると、次のようになります。
どう直したか
対処は、numpy のバージョンを明示的に固定することでした。
# layers/data_capture/requirements.txt(修正後)
pandas==1.3.4
numpy==1.26.4
修正後の流れを図にすると、次のようになります。
最初は numpy<2 という上限のみの指定でも事象は再現しなくなったのですが、レビューで「他のパッケージも == で完全固定しているなら numpy も合わせて == で固定した方が一貫性がある」という指摘をもらって、numpy==1.26.4 の完全固定に変更しました。
同じリポジトリの別 Layer(l2_fot_product_convert_parquet_to_csv)も、pandas が無指定 + pyarrow==11.0.0 という組み合わせで動いていました。pyarrow 11 は numpy 2 を完全には想定していないバージョンなので、こちらは合わせて以下を固定しました。
# layers/l2_fot_product_convert_parquet_to_csv/requirements.txt(修正後)
pyarrow==11.0.0
pandas==1.5.3
numpy==1.26.4
ビルド環境を Lambda 実行環境と揃える(sam build --use-container)
ライブラリのバージョンを固定するだけでは、もう一つの落とし穴が残ります。ビルド環境と Lambda 実行環境が同じアーキテクチャ / OS で揃っているか、というポイントです。
問題:ローカル開発機と Lambda 実行環境がアーキ違いになることがある
Lambda 関数は、関数のアーキテクチャ設定によって以下のいずれかで動きます。
- x86_64(デフォルト)
- arm64(AWS Graviton2)
numpy や pandas のような C 拡張ライブラリ(内部に C 言語のバイナリを含むライブラリ)は、ビルド時点で「このアーキ用のバイナリ」を取得する仕組みになっています。pip は「今動いているマシンに合った wheel(ビルド済みバイナリ)」を選んで取りに行きます。
つまり以下の組み合わせで事故が起きます。
| ビルド環境 | Lambda 実行環境 | 結果 |
|---|---|---|
| Intel Mac / Linux(x86_64) | x86_64 Lambda | ✅ 動く |
| Apple Silicon Mac(arm64) | x86_64 Lambda | ❌ アーキ不一致で動かない |
| Apple Silicon Mac(arm64) | arm64 Lambda | ✅ 動く |
私のローカル開発機は Apple Silicon Mac で、Lambda 関数は x86_64 構成です。何も対策せず sam build で済ませると、アーキ不一致で動かない Layer が出来上がる組み合わせになっています。
そこで今回のビルドでは、最初から後述の sam build --use-container を使う形で進めました。結果としてアーキ不一致の問題は起きませんでしたが、これは偶然そうなったわけではなく、意図的にこのオプションを使っていたためです。
解決:sam build --use-container でコンテナビルドする
--use-container オプションを付けると、sam build は AWS が公式に提供している Lambda 互換コンテナイメージ(Amazon Linux ベースの x86_64 環境)をローカルに pull して、そのコンテナの中で pip install を実行します。
sam build --use-container
これによって、
- ホストマシンが Apple Silicon Mac であっても
- コンテナの中は Linux x86_64 環境
- pip は「Linux x86_64 用の wheel」を取りに行く
- 結果として、Lambda 実行環境(x86_64)と同じバイナリの Layer ができる
という流れで、ホスト OS / アーキに依存せず、Lambda 実行環境と同じ Layer をローカルで作れるようになります。チームで複数人がビルドしても、誰がやっても同じ結果になる(= 再現性が確保される)のもこのオプションの強みです(参考:Building applications with sam build)。
template.yaml で意図を残す
template.yaml の Layer Metadata に BuildArchitecture を明示しておくと、「この Layer は x86_64 用にビルドする」という意図がテンプレートからも読めます。
# template.yaml(抜粋)
Resources:
MyLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: my-layer
ContentUri: ./layer/
CompatibleRuntimes:
- python3.12
Metadata:
BuildMethod: python3.12
BuildArchitecture: x86_64
BuildArchitecture 自体はターゲット宣言なので、これだけで実行環境が固定されるわけではありませんが、後から触る人に「意図」を残せます。--use-container と組み合わせて、両方揃えると安心です。
今回の事象との関係
念のため整理しておくと、今回の ImportError の原因は「ライブラリのバージョン固定漏れ」で、アーキ不一致ではありません。前述のとおり --use-container を使っていたので、アーキ不一致のほうは予防できていました。
逆に言えば、もし --use-container を使わずにビルドしていたら、ライブラリ固定漏れに加えてアーキ不一致まで同時に踏んでいた可能性があります。Lambda Layer の事故は 「ライブラリのバージョン管理」と「ビルド環境の管理」の両軸で予防策を取らないと、片方を対策してももう片方で詰む構造になっています。
ここから得た学び
今回の事象を整理して、自分用のチェックリストに 3 つ追加しました。
1. 間接依存も含めて固定しないと、いつメジャーバージョンが跨がれるかわからない
requirements.txt で pandas を == 固定していても、その pandas が裏で使っている numpy のような間接依存が >= 1.17.3 のような上限なし指定だと、pip 解決のタイミング次第でメジャーバージョンを跨いだ最新版が入ってしまいます。
直接書くライブラリだけでなく、間接依存も含めて固定が必要、というのが今回の一番の学びでした。Poetry の poetry.lock のように依存ツリー全体をロックする仕組みを使うか、それが無理なら pip freeze の結果を requirements.txt に丸ごとコミットしておくのが現実的です。
2. Lambda Layer は中身が見えづらいので、定期的に pip freeze で棚卸し
Lambda Layer に固めてしまうと、デプロイ後の中身は zip を取り出して中を見ないとわからなくなります。Layer をビルドした時点で pip freeze > layer-versions.txt のような形で artifact を残し、Git にコミットしておくと、後から「いつの時点で何が入っていたか」を追跡できます。
3. sam build はデフォルトでローカル環境を使う、--use-container を明示する
sam build をオプションなしで叩くと、ローカルマシンの Python と pip が使われます。これは Apple Silicon と Linux/x86_64 のアーキテクチャ差で事故りやすいポイントなので、--use-container を README / Makefile / CI で標準化しておくと安全です。
おわりに
依存パッケージのメジャーバージョン更新で動かなくなる、というのはどの言語でも避けられない話です。ただ、Lambda Layer のように 中身が見えづらいパッケージ で起こると、原因に辿り着くまでに時間がかかります。
私の場合は「ABI 非互換」というキーワードに辿り着くまでが一番時間を使った部分でした。本記事が、似たエラーで詰まっている方のデバッグ時間を少しでも減らせれば嬉しいです。
同じように依存ライブラリ周りで踏みやすい落とし穴があれば、また記事にまとめていきたいです。
参考
- NumPy 2.0.0 Release Notes ー ABI 変更の公式アナウンス
- pandas What's new ー pandas 2.2.2 で numpy 2.0 に正式対応
- Building applications with sam build ー
--use-containerの挙動 - Building Lambda layers ー Layer ビルドの公式手順







