AWS SAM で作成した複数のlambda関数で共通のメソッドを使う方法

## はじめに

AWS SAM を使って複数の Lambda 関数を1つのコードベースで管理することはよくあります。開発が進むと、複数の Lambda 関数間で共通のメソッドやクラスを使用したい場面も増えてきます。今回は、AWS SAM でこのような共通コードを効率的に管理するための手法について検討してみました。

## 想定ケース

1つの AWS SAM スタックで管理している複数の Lambda 関数で共通のメソッドを使用するケースを想定しています。

![sam_deploy.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1586001/dd3e87ee-f8f0-c6ed-902a-d49d3e04a86e.png)

なお、テンプレートでは共通のプレフィックスや設定は省略しています。

## 手法


### その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
```

```yaml
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
```

```yaml
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
```

```yaml
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
```

```bash
#!/bin/bash
cp -r src/common src/lambda1/
cp -r src/common src/lambda2/
sam build
rm -rf src/lambda1/
rm -rf src/lambda2/
```

```yaml
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
```

```yaml
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のブログで紹介されていました。

https://engineers.ntt.com/entry/2022/12/23/091853

よりシンプルに管理したい場合は、こちらの記事が参考になりました。

https://qiita.com/silane1001/items/12a8b6f06c38eae2386f

### 番外編 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
```

```yaml
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を作成する方法**](#その2-lambda関数と同じディレクトリにlibを作成する方法)」で進めていくことにしました。
理由としては、AWS SAMでリソースを管理することを考えるとlambdaに冗長なコードがあるデメリットが少ないことと、プロジェクト自体がそこまで大きくなる予定はないからです。
layerを利用した方法やプライベートリポジトリを作成する方法はかなり魅力を感じるので、後日時間のある時に試したみたいと思います。