Lambda Layer

Lambda 関数間で共有ライブラリやカスタムランタイムを再利用する仕組み

AWSサーバーレス

Lambda Layer とは

Lambda Layer は、Lambda 関数間で共有ライブラリ、カスタムランタイム、設定ファイルを再利用する仕組みである。共通の依存関係を Layer として切り出すことで、関数コード側のデプロイパッケージを小さく保ち、複数の関数で同じライブラリバージョンを統一できる。実行環境に組み込まれている SDK のバージョンを固定したい場合も、必要なバージョンを Layer に入れて明示的に読ませる手が使える。

適用範囲には前提がある。Layer が使えるのは .zip 形式でデプロイした関数だけで、コンテナイメージとして定義した関数には追加できない (ランタイムと依存関係はイメージ側に同梱する)。また Go と Rust については、依存関係もろとも 1 つの実行可能ファイルにコンパイルする形が前提のため、AWS 自身が Layer の使用を推奨していない。依存を Layer へ出すと初期化フェーズで追加のアセンブリを読み込むことになり、コールドスタートが伸びる方向に働く。

Layer の仕組み

Layer は ZIP アーカイブとして Lambda にアップロードされ、関数の実行環境の /opt ディレクトリに展開される。

/opt/
├── nodejs/           ← Node.js の Layer パス
│   └── node_modules/
│       ├── sharp/
│       └── @aws-sdk/
├── python/           ← Python の Layer パス
│   └── lib/python3.12/site-packages/
└── bin/              ← カスタムバイナリ
    └── ffmpeg

Node.js の場合、/opt/nodejs/node_modules が自動的にモジュール解決パス (NODE_PATH) に追加されるため、関数コードから require('sharp') で参照できる。自動で読み込まれるパスはランタイムごとに決まっていて、ZIP の中の階層をそれに合わせる必要がある。

ランタイムZIP 内のパス読み込まれる仕組み
Node.jsnodejs/node_modulesNODE_PATH
Pythonpython / python/lib/python3.x/site-packagessite ディレクトリ
Javajava/libCLASSPATH
Rubyruby/gems/<gem バージョン> / ruby/libGEM_PATH / RUBYLIB
全ランタイム共通binPATH
全ランタイム共通libLD_LIBRARY_PATH

この階層を 1 段間違えるだけで、Layer は正しく追加されているのに import だけが失敗する。原因が関数側に見えないので厄介な部類の障害で、疑うときは /opt 配下を実際に一覧するのが早い。もう 1 つ忘れやすいのがビルド環境で、Lambda の実行環境は Amazon Linux のため、Layer の中身は Linux でビルドできるものでなければならない。ネイティブ拡張を含む依存関係を手元の macOS でそのまま固めると、共有ライブラリの不一致で実行時に落ちる。Docker のような Linux 環境でビルドするのが確実である。

Layer を使うべきケース

Layer を使うべきケースを以下に示す。

ケース理由
ネイティブバイナリ (sharp, ffmpeg)ビルドが重く、複数関数で共有したい
カスタムランタイムDeno、Bun などの非標準ランタイム
共通ユーティリティ認証、ログ、バリデーションの共通コード
埋め込み SDK のバージョン固定実行環境側の SDK 更新に挙動を左右されない

ここで誤解されやすいのが容量の話である。Layer は「デプロイパッケージの上限を増やす」仕組みではない。展開後 250 MB という上限は Layer とカスタムランタイムの中身を含めて計算されるため、依存関係を Layer へ移しても合計は変わらない。小さくなるのは関数コード側のパッケージだけで、上限に張り付いている状況の打開策にはならない。

Layer を使うべきでないケース

  • 関数ごとに異なるバージョンが必要な依存関係
  • 頻繁に更新される依存関係 (Layer の更新は全関数に影響)
  • 小さな依存関係 (Layer のオーバーヘッドに見合わない)
  • コンテナイメージでデプロイする関数 (そもそも Layer を追加できない)
  • Go・Rust の関数 (依存関係は実行可能ファイルに同梱する方が速い)

制約

  • 1 関数に追加できる Layer は最大 5 つまで
  • 展開後の合計サイズは Layer とカスタムランタイムの中身を含めて 250 MB まで (.zip の直接アップロードは 50 MB までで、それを超えるものは Amazon S3 経由で渡す)
  • 使えるのは .zip 形式でデプロイした関数のみ
  • Layer の中身は Linux 環境でビルドできる必要がある
  • 公開した Layer のバージョンは変更できない不変のスナップショットで、関数側にはバージョン番号まで含めた ARN (arn:aws:lambda:ap-northeast-1:123456789012:layer:my-layer:3 の形) を指定する

よくある落とし穴

Layer のバージョン管理

Layer を更新しても、既存の関数は古いバージョンを参照し続ける。関数はバージョン番号まで含めた ARN で紐付いているので、Layer 側を publish しただけでは関数の挙動は変わらない。参照している全関数を新しいバージョン ARN へ張り替えて再デプロイするまでが更新作業である。

この張り替えが自動になるのは、SAM や CloudFormation で「同じテンプレートの中に」Layer を定義し、関数側から !Ref で参照している場合に限る。!Ref が返すのはそのデプロイで作られたバージョンの ARN なので、テンプレートを再デプロイすれば関数の参照先も新しいバージョンへ移る。テンプレートの外で作った既存の Layer や別スタックの Layer を指している場合は自動化されないため、バージョン ARN をパラメータで渡す設計にしておくと張り替えの手間が減る。なお SAM の AWS::Serverless::LayerVersion は更新時にリソースの論理 ID を変換して旧バージョンを消さずに残す仕様なので、古いバージョンを参照したままの関数が残っていても即座に壊れることはない。

コールドスタートへの影響

Layer のサイズが大きいと、コールドスタート時の展開に時間がかかる。テストランナーや型定義、ドキュメントまで丸ごと詰め込んだ Layer は典型的な失敗で、本番の実行時に import されるものだけに絞れば中身は大きく縮む。全関数の依存を 1 つの巨大な Layer にまとめる構成も、使わない依存の展開コストを全関数が負担することになるため、用途ごとに分けた方がコールドスタートには有利である。

基礎から学ぶなら関連書籍が手がかりになる。

この記事は役に立ちましたか?

関連用語

関連する記事