ENGINEER BLOG ENGINEER BLOG
  • 公開日
  • 最終更新日

複数の 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 はインストールが要らず、置いたファイルがそのまま入ります。
先ほどの CommonLayerBuildMethod が無いのはこのためです。

分けておくと、自作コードを直したときに外部ライブラリの再インストールが走りません。
共通コードは開発中に何度も変わりますが、外部ライブラリはたまにしか変えないので、変更のたびに重い処理を挟まずに済みます。
逆に、更新の理由がまったく違うものを 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が好きです。

野間 太一

この記事を共有する

クラウドのご相談

CONTACT

クラウド導入や運用でお悩みの方は、お気軽にご相談ください。
専門家がサポートします。

サービス資料ダウンロード

DOWNLOAD

ビジネスをクラウドで加速させる準備はできていますか?
今すぐサービス資料をダウンロードして、詳細をご確認ください。