はじめに
AWS SAM を使って複数の Lambda 関数を1つのコードベースで管理することはよくあります。開発が進むと、複数の Lambda 関数間で共通のメソッドやクラスを使用したい場面も増えてきます。今回は、AWS SAM でこのような共通コードを効率的に管理するための手法について検討してみました。
想定ケース
1つの AWS SAM スタックで管理している複数の Lambda 関数で共通のメソッドを使用するケースを想定しています。
なお、テンプレートでは共通のプレフィックスや設定は省略しています。
手法
その1 各lambda関数で同じファイルを作成する方法
.
├── src/
│ ├── lambda1/
│ │ ├── common/
│ │ │ ├── __init__.py
│ │ │ └── hogehoge.py
│ │ ├── lambda_function.py
│ │ └── requirements.txt
│ └── lambda2/
│ ├── common/
│ │ ├── __init__.py
│ │ └── hogehoge.py
│ ├── lambda_function.py
│ └── requirements.txt
├── samconfig.toml
└── template.yml
Resources:
# Lambda関数1の定義
Lambda1Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda1/
# Lambda関数2の定義
Lambda2Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda2/
各 Lambda 関数に common
ディレクトリを配置する方法です。しかし、共通コードの修正が複数箇所に必要になるため、管理が煩雑です。
記事の目的の一つは、このような構成を避けることにあります。
その2 lambda関数と同じディレクトリにlibを作成する方法
.
├── src/
│ ├── common/
│ │ ├── __init__.py
│ │ └── hogehoge.py
│ ├── lambda1/
│ │ ├── lambda_function.py
│ │ └── requirements.txt
│ └── lambda2/
│ ├── lambda_function.py
│ └── requirements.txt
├── samconfig.toml
└── template.yml
Resources:
# Lambda関数1の定義
Lambda1Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda1.lambda_function.handler
Runtime: python3.8
CodeUri: src/
# Lambda関数2の定義
Lambda2Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda2.lambda_function.handler
Runtime: python3.8
CodeUri: src/
この方法では、src
ディレクトリをすべてアップロードすることで共通コードを共有します。シンプルな解決方法ですが、冗長なコードが含まれる可能性があります。
その3 lambda layer を活用する方法
.
├── layers/
│ └── common/
│ └── python/
│ ├── __init__.py
│ └── hogehoge.py
├── src/
│ ├── lambda1/
│ │ ├── lambda_function.py
│ │ └── requirements.txt
│ └── lambda2/
│ ├── lambda_function.py
│ └── requirements.txt
├── samconfig.toml
└── template.yml
Resources:
# Layerリソースの定義
CommonLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: CommonLayer
Description: Common code layer for Lambda functions
ContentUri: layers/common
CompatibleRuntimes:
- python3.8 # 使用しているPythonのバージョンに合わせて設定
RetentionPolicy: Retain
# Lambda関数1の定義
Lambda1Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda1.lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda1
Layers:
- !Ref CommonLayer
# Lambda関数2の定義
Lambda2Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda2.lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda2
Layers:
- !Ref CommonLayer
共通コードを Layer として分離し、各 Lambda 関数から参照します。きれいな構成ですが、Layer の追加管理が必要で、特に CI/CD 環境では Layer イメージのビルドを保守する手間がかかる点がデメリットです。
私自身あまりlambda layerを利用した経験がないのですが、同僚の話を聞く限りだと管理が意外と大変らしいです(「CI/CDを組んでいると、layer imageを build する環境を整備し続けなきゃいけないので大変」と話していました)。
その4 build時にパッケージをコピーするスクリプトを書く
.
├── src/
│ ├── common/
│ │ ├── __init__.py
│ │ └── hogehoge.py
│ ├── lambda1/
│ │ ├── lambda_function.py
│ │ └── requirements.txt
│ └── lambda2/
│ ├── lambda_function.py
│ └── requirements.txt
├── build.sh
├── samconfig.toml
└── template.yml
#!/bin/bash
cp -r src/common src/lambda1/
cp -r src/common src/lambda2/
sam build
rm -rf src/lambda1/
rm -rf src/lambda2/
Resources:
# Lambda関数1の定義
Lambda1Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda1/
# Lambda関数2の定義
Lambda2Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda2/
ビルド時に common
ディレクトリをコピーする方法です。ただし、この方法では sam sync
に対応できないため、samの機能を最大限発揮できなくなるというデメリットがあります。
その5 commonをrequirements.txtで読み込めるようにする方法
.
├── src/
│ ├── lambda1/
│ │ ├── lambda_function.py
│ │ └── requirements.txt
│ └── lambda2/
│ ├── lambda_function.py
│ └── requirements.txt
├── samconfig.toml
└── template.yml
common
Resources:
# Lambda関数1の定義
Lambda1Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda1/
# Lambda関数2の定義
Lambda2Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda2/
共通コードをパッケージ化し、requirements.txt
に記載する方法です。パッケージを Pypi に公開するかプライベートリポジトリを構築する必要があるため、導入には手間がかかります。
少し古いですが、プライベートリポジトリの作成方法についてはNTT Communicationsのブログで紹介されていました。
よりシンプルに管理したい場合は、こちらの記事が参考になりました。
番外編 commonを1つのlambda関数として分離する
.
├── src/
│ ├── common/
│ │ └── lambda_function.py
│ │ └── requirements.txt
│ ├── lambda1/
│ │ ├── lambda_function.py
│ │ └── requirements.txt
│ └── lambda2/
│ ├── lambda_function.py
│ └── requirements.txt
├── samconfig.toml
└── template.yml
Resources:
# Lambda関数commonの定義
Lambda1Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/common/
# Lambda関数1の定義
Lambda1Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda1/
# Lambda関数2の定義
Lambda2Function:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.handler
Runtime: python3.8
CodeUri: src/lambda2/
commonのパッケージを1つのlambda関数にしてしまう方法です。
今後ほかのシステムからも呼び出される可能性のあるメソッドならこの方法でもよいかもしません。
ただし、料金やレイテンシなどの問題もあるので、パッケージ1つにここまでするのは少々大げさでしょう。また、commonパッケージ事態が1つの責務(業務ロジックやドメインと呼べれるもの)を保有していない限りはこの構成を維持していくのは難しいように思えます。
結論
いろいろな事情を考慮した結果、「その2 lambda関数と同じディレクトリにlibを作成する方法」で進めていくことにしました。
理由としては、AWS SAMでリソースを管理することを考えるとlambdaに冗長なコードがあるデメリットが少ないことと、プロジェクト自体がそこまで大きくなる予定はないからです。
layerを利用した方法やプライベートリポジトリを作成する方法はかなり魅力を感じるので、後日時間のある時に試したみたいと思います。