外部モジュールを使った AWS Lambda (Python) を Serverless Framework でかんたんデプロイ

[Serverless Framework]:https://www.serverless.com/
[pipenv]:https://pipenv-ja.readthedocs.io/ja/translate-ja/
[numpy]:https://numpy.org/

# はじめに

この記事のターゲットは「NumPy, OpenAI とか外部モジュールに依存する Python コードを書いたけど、AWS Lambda 上で動かしたいな！」という人向けです。

> ※特に最近の話題ではありません ＆ 「別に AWS Lambda とか触らないなぁ」、という人は無視して下さい〜。

# 解決したい課題

AWS Lambda って、サーバーレスなので繰り返し実行するコードを実行するランタイム環境としてはすごく便利ですよね。 Lambda コンソール上で直接コードも書けますし。

![916616de-93e0-ade4-6b43-c18e1a4a828b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/3a641321-fb92-d1cb-b3f3-40b20cab54fe.png)

ただし、多くの人が一度は ↓ で躓きます。

```
あれ？ 外部モジュール (pip) ってどうやって使うんだ？
```

# Lambda 標準のやり方

次の２つのやり方が AWS Lambda 標準であります。どちらも手順が煩雑になりがちなので、開発でも自動化はしたいですね。

|  | ① Zip アップロード | ② Docker イメージ |
|:-----|:-----|:------|
| **説明** | `pip install --target {path}`で事前にローカルインストールしたのを `*.zip` で固めてアップロード | Docker Image を ECR にアップロードして利用 |
| **✅ メリット** | (Docker イメージ方式に比べれば) 簡単で高速 | コード実行上のほぼ全ての要件を Dockerfile 内で解決できる |
| **❌ デメリット** | ImageMagick とか、native の動的ライブラリ、コマンドが実行環境に必要なケースに対応できない | イメージをビルドして、ECR にアップロードして... 大変！ |

特によくハマるのが、 **「numpy の様に コンパイルが必要な Native code (C/C++) を含む pip module」** です。

:::note
実際に numpy のコードを見てみると、主要なコードは C で書かれています。
* https://github.com/numpy/numpy/tree/main/numpy/core/src/common
:::

この場合、Mac (arm64) で `pip install numpy`して入るのは、 **「Mac 用かつ arm64 CPU 用のバイナリ」** です。

つまり、Mac 等のローカルで Packaging して Zip アップロードした場合、**Lambda のランタイムイメージが Linux / x86_64 では動きません。**

![2c4715e1-7cf7-7da1-726a-a9dd060ae236.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/c485b190-a92d-b09e-152b-a37ae2f7f103.png)


# [Serverless Framework] を使って自動化しよう！

ここでは、 **方式 ②「Docker イメージ」** を [Serverless Framework] を使ってやる方法を説明します。

:::note
方式 ①「Zip アップロード」を [Serverless Framework] でするケースはクラメソさんの ↓ が参考になります。

* https://dev.classmethod.jp/articles/serverless-framework-lambda-numpy-scipy/
:::

:::note
たぶんそのまま使える、方式 ②「Docker イメージ」のサンプルコード。
* https://github.com/megmogmog1965/sls-aws-lambda-python
:::

## ローカル環境

ローカルでは [pipenv] で開発し、Lambda へのデプロイは [Serverless Framework] で行うとします。まずは開発 PC に以下が必要です。

| ランタイム環境 | インストール手順 |
|:----------------|:------------------|
| Node.js (v14.21.3) | 直接インストールするか、[nvm](https://github.com/nvm-sh/nvm) 等を使うか |
| [pipenv] | https://pipenv-ja.readthedocs.io/ja/translate-ja/install.html |
| Docker | [Docker Desktop](https://www.docker.com/products/docker-desktop/) か、個人的には無償の [Rancher Desktop](https://rancherdesktop.io/) がオススメ |

## ディレクトリ構造

最終的にこんな感じになります。

```
.
├── .gitignore
├── README.md
├── Dockerfile
├── Pipfile
├── Pipfile.lock
├── package.json
├── package-lock.json
├── serverless.yml
└── app.py
```

> [Serverless Framework] が Node.js 製なので、`package.json`があります。

## 手順

### .gitignore

本サンプルプロジェクトの `.gitignore` はこんな感じです。プロジェクトルートに作成して下さい。

```
# Distribution / packaging
.Python
env/
.env
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# Node.js
node_modules/

# Serverless directories
.serverless
```

### Python code

Python 3.10 ベースで Pipfile ファイルを作ります。

```shell
pipenv --python 3.10
```

外部ライブラリ (e.g. [numpy]) をインストールします。

```shell
pipenv install numpy
```

Lambda ハンドラコード `app.py` を用意します。 (※申し訳程度に [numpy] を使用してます笑)

```python:app.py
#!/usr/bin/env python3 
import numpy as np

def handler(event: dict, context: dict) -> dict:
    arr1 = np.array([10, 20], dtype=np.int32)
    arr2 = np.array([30, 40], dtype=np.int32)

    return {
        'statusCode': 200,
        'body': str(arr1 + arr2)
    }

if __name__ == '__main__':
    ret = handler({}, {})
    print(ret)
```

### Install [Serverless Framework]

まずは [Serverless Framework] を global install します。

```shell
npm install -g serverless
```

とは言え、可能な限り global install したものは使いたくない (チームで開発する場合は特に) ので、`package.json` を作成します。

```json
{
  "name": "sample-project",
  "dependencies": {
    "serverless": "^3.31.0"
  },
  "devDependencies": {},
  "scripts": {
    "deploy": "npx sls deploy"
  }
}
```

npm install しておきましょう。

```shell
npm install
```

[Serverless Framework] を使ったデプロイ設定 `serverless.yml` を作成します。

```yaml
service: sample-project

provider:
  name: aws
  runtime: python3.10
  memorySize: 1024
  region: ap-northeast-1
  architecture: x86_64
  ecr:
    images:
      image_sample_project:
        platform: linux/amd64
        path: ./

functions:
  sample-project:
    name: sample-project
    timeout: 900
    image:
      name: image_sample_project
#   environment:
#     ENV_NAME: ENV_VALUE
```

最後に実行環境となる Docker イメージの `Dockerfile` を作成しましょう。

```Dockerfile
FROM public.ecr.aws/lambda/python:3.10

# 環境にインストールが必要な物があれば、ここで...
# RUN yum update -y && yum install -y gcc make ...

# pip: install python modules.
COPY Pipfile ./
COPY Pipfile.lock ./
RUN pip install pipenv && \
    pipenv requirements > requirements.txt && \
    pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"

# Copy function code
COPY *.py ${LAMBDA_TASK_ROOT}

# Compatible with initial base image
CMD ["app.handler"]
```

それでは早速デプロイしてみましょう。

:::note warn
ECR にイメージをアップロードする為に、環境変数に AWS の認証情報 `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY ` が必要です。 [awsume](https://awsu.me/) 等を使うのも良いでしょう。
:::

:::note warn
Docker 環境の起動も忘れずに！
:::

```
npm run deploy
```

## 実行してみる

AWS Console >> Lambda >> Functions >> sample-project を開いてみて下さい。
「Test」タブでテスト実行をしてみましょう。

![d4e16211-c4e2-a2eb-209e-d1b6825a3f1f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/b1af5840-38b4-af6e-a4aa-bd6324debd35.png)

## トラブルシューティング

色々やってて、なんか動かなくなった場合はキャッシュ要素の初期化を試みましょう。

* [sls コマンドを使う](https://www.serverless.com/framework/docs/providers/aws/cli-reference/remove)
* `.serverless` ディレクトリを消す