【Serverless Framework】PythonランタイムのLambdaでrequirements.txtと独自モジュールをレイヤー化する

# はじめに

Lambda Layerを使うと、複数のLambdaで共通して使用するライブラリとその他の依存関係をパッケージ化することができます。

今回はServerless Frameworkを利用してLayerをデプロイし、開発環境、Lambda環境で利用する方法を解説します。

# Layerの基礎知識

押さえておくべきポイントは以下2つです。

:::note
Lmabda環境においてLayerは`/opt`以下に展開される。
:::

:::note
Lambda環境の`PYTHONPATH`には`/opt/python`が含まれている。
※ PYTHONPATHとは、Pythonが import 文で利用するモジュールを探す際のパスです。
:::


以下のような`utils.py`を共通化する場合を想定します。
```utils.py
def hello_func():
    return "Hello!"
```
上記のポイント2つを組み合わせ、Lambda内で`/opt/python/utils.py`となるようにレイヤーを配置することで、このようにLambda内部からLayerを利用することができます。

```api.py
from util import hello_func
```


# ディレクトリ構成
今回はLambdaごとにディレクトリを分けた構造を想定しています。

ルートに置いた`requirements.txt`に記載したPythonパッケージと`layers/custom_packages/python`以下の自前のPythonモジュールをLayer化し、`hello_api/api.py`から呼び出します。

**`開発環境`**
```text
.
├── hello_api
│   ├── api.py
│   └── serverless.yml
├── layers
│   ├── custom_packages ←ここの名前はなんでも良い
│   │   └── python  ← ここをpythonにすることで/opt/pythonに展開される
│   │       ├── __init__.py
│   │       └── utils.py
│   └── serverless.yml
└── requirements.txt
```

**`Lambda環境`**

```text
.
└── api.py
```

# デプロイ

## 自前のモジュールをLayer化する

Serverless frameworkにはLayerをデプロイする機能がデフォルトで備わっているため、これをそのまま利用します。

https://www.serverless.com/framework/docs/providers/aws/guide/layers

```serverless.yml
service: myService
 
provider:
  name: aws
 
layers:
  hello:
    path: layer-dir # required, path to layer contents on disk
    name: ${sls:stage}-layerName # optional, Deployed Lambda layer name
    description: Description of what the lambda layer does # optional, Description to publish to AWS
    compatibleRuntimes: # optional, a list of runtimes this layer is compatible with
      - python3.8
    compatibleArchitectures: # optional, a list of architectures this layer is compatible with
      - x86_64
      - arm64
    licenseInfo: GPLv3 # optional, a string specifying license information
    # allowedAccounts: # optional, a list of AWS account IDs allowed to access this layer.
    #   - '*'
    # note: uncommenting this will give all AWS users access to this layer unconditionally.
    retain: false # optional, false by default. If true, layer versions are not deleted as new ones are created
```

今回の例では、このようになります。

```layers/serverless.yml
service: layers

provider:
  name: aws
  region: ap-northeast-1
  runtime: python3.9

layers:
  utils:
    path: custom_packages
    name: utils
    compatibleRuntimes:
      - python3.9
```

## `requirements.txt`をLayer化する

`serverless-python-requirements`を使うことで、`requirements.txt`に記述したライブラリを全てLayer化することができます。

https://www.npmjs.com/package/serverless-python-requirements


```layers/serverless.yml
custom:
  pythonRequirements:
    fileName: "../requirements.txt"
    layer:
      name: python-requirements
    compatibleRuntimes:
      - python3.9
plugins:
  - serverless-python-requirements
```

# Layerを利用する

## 開発環境でLayerを読み込む

`pip`等でインストールしたPythonパッケージは通常通り読み込むことができます。

```hello/api.py
import flask
```

一方で`utils.py`で定義するような自前のモジュールは、Pythonのインポートの制約を受けます。

今回のように全く別のディレクトリで定義したものを呼び出す際は`PYTHONPATH`を追加する必要があります。

追加方法は環境によりますが、例えばDocker、venvではそれぞれこのようになります。
```Dockerfile
# Dockerfile
ENV PYTHONPATH "${PYTHONPATH}:/app/layers/custom_packages/python"
```

```conf
# venv/bin/activate
export PYTHONPATH="../../layers/custom_packages/python"
```

:::note
これによりエラーを起こさず読みとれるようになりますが、IDEでは警告が出る場合もあります。
Pycharmでは、画像のように「プロジェクト構造」で`python/`に「ソース」を設定することでうまく認識するようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532021/a662171a-806f-7048-7659-be7ced9ab5e5.png)
:::


## LambdaでLayerを読み込む

LambdaからLayerを利用する際は、読み込むLayerを指定する必要があります。

```yml
functions:
  hello:
    handler: handler.hello
    layers:
      - arn:aws:lambda:region:XXXXXX:layer:LayerName:Y # arnで指定するパターン
```

同一の`serverless.yml`内部であれば、Serverless Frameworkの`!Ref`を使って参照することができます。
この際、タイトルケース化した`Layerの名前`+`LambdaLayer`を使って指定します。

```yml
layers:
  test: # 参照時のキーはここに依存し、testであればTestになる
    path: layer

functions:
  hello:
    handler: handler.hello
    layers:
      - !Ref TestLambdaLayer # Test + LambdaLayer
```

しかし、今回はLayerとLambdaで別の`serverless.yml`を使っており`!Ref`では参照できません。
そこで、CludFormationのOutputs機能を利用し、Layer作成時に、Layerの`arn`を別のCloudFormationのスタックから参照可能にします。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html

```yml
resources:
  Outputs:
    UtilsLayerExport:
      Value:
        Ref: UtilsLambdaLayer # Layerの名前に依存
      Export:
        Name: UtilsLambdaLayer # 自由に変えて良い
    PythonRequirementsLambdaLayerExport:
      Value:
        Ref: PythonRequirementsLambdaLayer # 変更不可
      Export:
        Name: PythonRequirementsLambdaLayer # 自由に変えて良い
```

Outputした値を読み込む`serverless.yml`はこのようになります。

```hello_api/serverless.yml
functions:
  api:
    handler: wsgi_handler.handler
    name: hello_api
    layers:
      - 'Fn::ImportValue': UtilsLambdaLayer # layers/serverless.ymlで指定したキー
      - 'Fn::ImportValue': PythonRequirementsLambdaLayer # layers/serverless.ymlで指定したキー
```


# まとめ

これにより、開発環境、Lambda環境のどちらでも共通の処理、ライブラリをまとめることができるようになります。

ここまでの要素をまとめた`serverless.yml`は以下の様になります。

```layers/serverless.yml
service: layers

provider:
  name: aws
  region: ap-northeast-1
  runtime: python3.9

layers:
  utils:
    path: custom_packages
    name: utils
    compatibleRuntimes:
      - python3.9

custom:
  pythonRequirements:
    fileName: "../requirements.txt"
    layer:
      name: python-requirements
    compatibleRuntimes:
      - python3.9

resources:
  Outputs:
    UtilsLayerExport:
      Value:
        Ref: UtilsLambdaLayer
      Export:
        Name: UtilsLambdaLayer
    PythonRequirementsLambdaLayerExport:
      Value:
        Ref: PythonRequirementsLambdaLayer
      Export:
        Name: PythonRequirementsLambdaLayer

plugins:
  - serverless-python-requirements
```


```hello_api/serverless.yml
functions:
  api:
    handler: wsgi_handler.handler
    name: hello_api
    layers:
      - 'Fn::ImportValue': UtilsLambdaLayer
      - 'Fn::ImportValue': PythonRequirementsLambdaLayer
```