AWS SAM で関数とレイヤーを別々に管理（Python）

# 概要
現在 AWS Lambda で複数の関数を運用しているが、共通するライブラリやコードが存在しているため、[Lambda レイヤー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/chapter-layers.html)で共通部分をくくり出したい
Lambda 関数については [AWS SAM](https://aws.amazon.com/jp/serverless/sam/) で実装しているため、レイヤーも同様に実装・管理を行いたい
ただしレイヤーはどの関数も参照するため、どこかの関数と一緒にするよりも独立させ、個別に管理やデプロイを行いたい

このような要望があり、色々調べたのだが、まとまった方法が見つからなかったため、ここにまとめる

# 環境
* Python 3.11.1
* aws-cli/2.13.12
* SAM CLI, version 1.95.0

# 結論
いきなり結論から書くが、以下のような形で実装した

## ファイル構成（レイヤー）
```
.
├─ python
|  ├─ __init__.py
|  ├─ utils_layer.py
│  └─ requirements.txt
├─ tests
|  ├─ unit
|  |  ├─ __init__.py
|  |  └─ test_utils_layer.py
|  ├─ __init__.py
│  └─ requirements.txt
├─ samconfig.toml
└─ template.yaml
```
README などは割愛

## テンプレート（レイヤー）
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Utils Layer

Resources:
  UtilsLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      Description: Utils Layer
      LayerName: utils_layer
      ContentUri: 'python/'
      RetentionPolicy: Retain
      CompatibleRuntimes:
        - python3.11
    Metadata:
      BuildMethod: python3.11
  SSMLatestArn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub /lambda-layer/latest/${AWS::StackName}
      Type: String
      Value: !Ref UtilsLayer

Outputs:
  Name:
    Description: Stack Name.
    Value: !Ref AWS::StackName
    Export:
      Name: !Ref AWS::StackName

  UtilsLayerArn:
    Description: Published UtilsLayer ARN.
    Value: !Ref UtilsLayer
    Export:
      Name: !Sub ${AWS::StackName}-arn
```

## テンプレート（関数）
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Get Function

Parameters:
  LayerArn:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /lambda-layer/latest/utils-layer

Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: get_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Layers:
        - !Ref LayerArn
      Architectures:
        - x86_64
```
必要な部分だけ抜粋

## コード（関数）
```py
import utils_layer as utils

def lambda_handler(event, context):
    utils.test()

    return {
        "statusCode": 200,
        "body": "OK"
    }
```

# 解説
基本方針として、sam init で作成したテンプレートからなるべく少ない変更で実装を行う

まずレイヤーのファイル構成だが、共有コードや共有ライブラリを登録した requirements.txt を置くフォルダ名は `python` 固定
ここを別の名前にしてしまうと import しても読み込んでくれず、訳の分からない No module エラーに悩まされることになる
そして共有関数を記述するファイル名は init で作成された app.py のままでも問題ないのだが、import 時にファイル名を指定することになるため、わかりやすい名前に変更しておくほうが良い

続いてテンプレート
基本的には上記をベースに必要な要素を追加してやれば問題なく動くはず
解説が必要な部分としてはレイヤーの読み込み部分だろう
レイヤーの読み込みは関数側のテンプレートにレイヤーの ARN を指定することになるのだが、この ARN はバージョンを含める必要がある
そのため、今回はレイヤーのデプロイ時に最新バージョンを含めた ARN をパラメータストアに保存し、関数側でその値を参照する形を取っている
[クロススタックの参照](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html)を使っても問題ない
その場合は以下のようになる
```yaml
      Layers:
        - !ImportValue utils_layer_arn
```
ただし、上記のように直接指定する場合は1つ注意が必要
ImportValue にしろ、パラメータストアにしろ、デプロイは問題ないのだが、ローカル環境での実行の場合は値を取ってきてくれないのだ
なので、読み込んだ値は一旦パラメーターにデフォルトとして指定しておき、ローカル実行時にはパラメーターをオーバーライドできるようにしておくと使い勝手が良い
以下のようなコマンドで実行できる
```shell
sam local invoke --parameter-overrides LayerArn=arn:aws:lambda:region:XXXXXX:layer:utils_layer:XX
```

ここまでやってレイヤーをデプロイすると、関数から何食わぬ顔で import できるようになる
ただしここでも1つ注意が必要で、ライブラリはともかく、作成した関数の場合、ユニットテストなどを行ったときに No module エラーを食らう場合がある
なのでそういった場合は import をレイヤーを使う関数内で行うようにすると良い
```py
def lambda_handler(event, context):
    import utils_layer as utils

    utils.test()

    return {
        "statusCode": 200,
        "body": "OK"
    }
```
見栄えが悪い？そうね……