- 公開日
- 最終更新日
複数の Lambda 関数で使う共通コードを Lambda Layer にまとめてみた
この記事を共有する
目次
はじめに
こんにちは!パーソル&サーバーワークスの野間です。
AWS Lambda(以下、Lambda)で関数を増やしていくうちに、同じ処理を別の関数に何度も書いている自分に気づきました。
Amazon DynamoDB(以下、DynamoDB)からデータを取り出す処理を、関数を追加するたびにコピーして持ち込んでいたのです。
関数がいくつか増えたころには、1 か所直すたびに同じ数だけ直す状態になっていました。
同じように共通処理をコピーして困っている方向けに、1 か所にまとめた方法を紹介します。
増えていく重複コード
きっかけは、DynamoDB からデータを読み書きするコードでした。
最初はひとつの関数に書いていたのですが、別の関数でも同じ処理が必要になり、ファイルごとコピーして持ち込みました。
その後もコピーは続き、同じ内容の db_client.py が複数の関数のフォルダに並ぶことになりました。
.
├── src/
│ ├── function_a/
│ │ ├── app.py
│ │ └── db_client.py ← コピー
│ └── function_b/
│ ├── app.py
│ └── db_client.py ← 同じ内容のコピー
└── template.yaml
この状態だと、テーブルの持ち方をひとつ変えるだけで、コピーした全部のファイルを直すことになります。
直し忘れた関数だけ古い挙動のまま残る、という危うさもありました。
共通コードは Lambda Layer にまとめられる
Lambda には Lambda Layer(以下、Layer)という仕組みがあります。
複数の関数で使うコードを Layer にまとめておくと、各関数はそれを参照するだけでよくなります。
Layer に入れられるのは外部ライブラリだけではなく、自分で書いた Python のコードも置けます。
Layer は実行時に /opt へ展開され、その中の python フォルダが import の対象になります。
そのため、共通コードを python フォルダに置いておけば、各関数から通常どおり import できます。
重複していた db_client.py を Layer 側の python フォルダに 1 つだけ置き、各関数からは削除しました。
.
├── layers/
│ ├── common/
│ │ └── python/
│ │ └── db_client.py ← 共通コードはここ 1 か所
│ └── dependencies/
│ └── requirements.txt ← 外部ライブラリ
├── src/
│ ├── function_a/
│ │ └── app.py ← import して呼ぶだけ
│ └── function_b/
│ └── app.py
└── template.yaml
私のプロジェクトは AWS Serverless Application Model(以下、SAM)で管理しているので、Layer もテンプレートに定義しました。
自作コード用の Layer は次のように書きます。
CommonLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: example-common
ContentUri: layers/common/
CompatibleRuntimes:
- python3.12
ContentUri に指定したフォルダの下に python フォルダを作り、そこへ共通コードを置きます。
Layer のフォルダ構成は Lambda レイヤーのドキュメント にまとまっています。
Layer のコードを呼び出す
一番気になるのは、Layer に置いたコードをどう呼ぶのか、だと思います。
結論としては、特別な書き方は要りません。python フォルダ直下に置いたファイルは import のパスに乗るので、普通に import できます。
Layer 側のコードはこうなっています。
# layers/common/python/db_client.py
import boto3
_table = boto3.resource("dynamodb").Table("example-table")
def get_item(item_id):
resp = _table.get_item(Key={"id": item_id})
return resp.get("Item")
呼び出す側の関数は、そのファイル名で import して呼ぶだけです。
# src/function_a/app.py
from db_client import get_item
def lambda_handler(event, context):
item = get_item(event["id"])
return {"name": item["name"]}
各関数からコピーしていた db_client.py を消し、この import に置き換えました。
DynamoDB へのアクセスや整形処理を Layer に移した結果、ある関数の app.py はコードが 3 分の 1 以下になりました。
関数側に残ったのは、受け取ったイベントを共通処理に渡し、結果を返すだけのつなぎのコードです。
Layer は役割ごとに分ける
ここが今回いちばん考えたところです。
Layer は 1 つにまとめることもできますが、私は 自作の共通コード用と外部ライブラリ用の 2 つに分けました。
理由は、この 2 つがビルドの仕方も更新の頻度も違うからです。
外部ライブラリの Layer は、requirements.txt から SAM がインストールして作ります。
そのためテンプレートにビルド方法を指定します。
DependenciesLayer:
Type: AWS::Serverless::LayerVersion
Metadata:
BuildMethod: python3.12
Properties:
ContentUri: layers/dependencies/
一方、自作コードの Layer はインストールが要らず、置いたファイルがそのまま入ります。
先ほどの CommonLayer に BuildMethod が無いのはこのためです。
分けておくと、自作コードを直したときに外部ライブラリの再インストールが走りません。
共通コードは開発中に何度も変わりますが、外部ライブラリはたまにしか変えないので、変更のたびに重い処理を挟まずに済みます。
逆に、更新の理由がまったく違うものを 1 つの Layer に混ぜると、片方を直すだけで全体を作り直すことになります。
全関数への適用は Globals でまとめる
SAM では、すべての関数に共通する設定を Globals に書けます。
Layer もここに書けば、関数ごとに指定を繰り返さずに済みます。
Globals:
Function:
Layers:
- !Ref DependenciesLayer
- !Ref CommonLayer
これで、以降に関数を追加しても Layer は自動で付きます。
共通処理をコピーして持ち込む動機そのものが無くなるので、最初の重複コードの再発も防げました。
すべての関数が共通の Layer を使う設計であれば Globals に書くのが便利ですが、一部の関数だけ Layer を外したい場合は Globals に Layers を書かず、各関数に個別指定する構成を選ぶことになります。
今回のプロジェクトでは全関数が同じ共通コードと外部ライブラリを使うため、Globals にまとめています。
まとめ
今回は複数の Lambda 関数にコピーしていた共通コードを、Lambda Layer に 1 か所へまとめました。
学んだのは、Layer は「まとめる」だけでなく「どう分けるか」が効いてくるということでした。
更新の理由が違うものを分けておくと、あとの開発が軽くなります。
共通処理を関数ごとにコピーしている方は、まず 1 か所にまとめるところから試してみてください。
この記事は私が書きました
野間 太一
記事一覧猫とCloudFormationが好きです。