Python Lambda - コンテナ上でのローカルデバッグ環境構築

# 内容

CDKで実装した Python Lambda のローカルデバッグ環境構築する  
Lambdaの実行環境と合わせるため、Lambdaと同環境のコンテナを用意し、  
Remote Developmentでコンテナに入りデバッグを行う  
本記事で一旦ここまでを実施する  

以下は別記事で対応する  

- [AWSサービス（S3）はLocalStackでモック化する](https://qiita.com/takmot/items/7f244baa8a56f5cd1f63)
- [Amazon RDSを別途PostgreSQLサーバー用コンテナで対応する](https://qiita.com/takmot/items/257c4448a4ca9b9df9f8)

# フォルダ構成

今回以下のようにLambdaごとに外部パッケージをインストールしているが、  
Lambdaのサイズ上限も考慮すると、EFS上に外部パッケージをインストールしてLambdaにアタッチする（[参考](https://valmore.work/aws-labmda-pytorch/)）ほうが良さそうなため、  
後日試してみる予定
 → [AWS CDK - EFS での Python Lambda パッケージ管理](https://qiita.com/takmot/items/346ca1a0fd95737a4445)

```txt
 ├─ .devcontainer
 │     └─ devcontainer.json
 ├─ .vscode
 │     ├─ extensions.json 
 │     └─ settings.json
 │
 ├─ bin/
 │   └─ sample.ts
 │
 ├─ lib/
 │    │
 │    ├─ handlers/                           // Lambda実装
 │    │   │
 │    │   └─ sample-function/
 │    │        ├─ python/
 │    │        │     ├─ pandas/             // 外部パッケージ
 │    │        │     …
 │    │        │     └─ sample_function.py  // Lambda関数実装
 │    │        │
 │    │        └─ requirements.txt          // Lambdaに含める外部パッケージ指定
 │    │
 │    ├─ layers/                            // Layer実装
 │    │   │
 │    │   ├─ python/
 │    │   │     ├─ pytz/                    // 外部パッケージ
 │    │   │     …
 │    │   │     └─ common_layer.py          // layerに含める共通処理実装
 │    │   │
 │    │   └─ requirements.txt               // layerに含める外部パッケージ指定
 │    │
 │    └─ sample-stack.ts
 │
 ├─ test/
 │    │
 │    ├─ sample-function/
 │    │            └─ test.py               // lib/handlers/sample-function/python/sample_function.pyのテストコード
 │    │
 │    ├─ docker-compose.yml
 │    └─ Dockerfile-lambda-python           // Python Lambda実行用のコンテナDockerfile
 …
 │
 ├─ poetry.lock
 ├─ pyproject.toml
 │

```

## テスト対象Lambda関数コード

```python:lib/handlers/sample-function/python/sample_function.py
import os
from datetime import datetime
from pytz import timezone
from common_layer import print_message  # type: ignore | common layer package
import pandas as pd

test_env_value = os.getenv("TEST_ENV_VALUE")


def handler(event, context):
    print(f"event: {event}")  # イベント内容の確認
    print(f"test_env_value: {test_env_value}")  # 環境変数の確認

    print_message("test message 1")   # Layerを使った処理

    df = pd.DataFrame([["A", 170, 60], ["B", 160, 50], ["C", 165, 58]])  # 外部パッケージpandasを使った処理
    print(f"df: {df}")

    tokyo_dt = datetime.now(timezone("Asia/Tokyo"))

    return {"statusCode": 200, "body": tokyo_dt.strftime("%Y-%m-%d %H:%M:%S")}

```

## テストコード

```python:test/sample-function/test.py
import os
from common_layer import print_message

os.environ["TEST_ENV_VALUE"] = "test env value"  # 環境変数設定
import sample_function


def test_1():
    print_message("layer test")
    event = {"key1": "value1", "key2": "value2"}  # Lambda handlerの引数設定
    context = {}

    sample_function.handler(event, context)  # Lambda handlerの実行


test_1()
```

# import パスの追加

テストコード `test/sample-function/test.py` から、  
layer内の外部パッケージ + 共通処理、  
Lambda内の外部パッケージ、handlerが実行できるように  
pyproject.toml内 `[tool.poetry]` ブロックの `packages` でimportパスを追加する  
（`sys.path.append("<path>")` でimportパスを追加するのと同じ）

```text:pyproject.toml
[tool.poetry]
name = "sample"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
packages = [
    { from = "lib/layers/python", include = "*" },
    { from = "lib/handlers/sample-function/python", include = "*" },
]

[tool.poetry.dependencies]
python = "3.11.6"
boto3 = "^1.28.78"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```

Lambdaと同環境のimageにboto3はインストールされているが、  
poetryのinterpreterを使用するため、boto3は必要

# Python Lambda 実行用のコンテナ

Python11のLambdaイメージをベースに、必要なモジュールをインストールする

```text:Dockerfile-lambda-python
FROM public.ecr.aws/lambda/python:3.11

RUN yum update -y
RUN yum install -y unzip tar gzip less
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && unzip awscliv2.zip && ./aws/install
RUN pip install poetry

CMD ["/bin/bash"]
```

# docker-compose

ソースコード一式をコンテナ内の `/lambda-python` に同期する
クレデンシャルも同期する

```text:docker-compose.yml
version: "3.8"
services:
  lambda:
    container_name: lambda-python
    build:
      context: .
      dockerfile: Dockerfile-lambda-python
    volumes:
      - ../:/lambda-python
      - ~/.aws:/root/.aws
    working_dir: /lambda-python
```

# Remote Development でのデバッグ

以下のように `.devcontainer/devcontainer.json` を設定しているが必須ではない

```json:.devcontainer/devcontainer.json
{
  "name": "lambda-python",
  "dockerComposeFile": ["test/docker-compose.yml"],
  "service": "lambda",
  "workspaceFolder": "/lambda-python"
}
```

コンテナ起動後、 `Remote Development` を使用してPython Lambda実行用のコンテナにアタッチする  
Open Folderからソースコード一式を同期した `lambda-python` フォルダを開く  
`poetry install` 実行後、poetryのinterpreterを選択する  

![2023-11-06-13-21-43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/3ab5eebd-ea3a-20a2-312d-1eff5333bfc4.png)

Layer、Lambdaに含める外部パッケージは以下コマンドでpythonフォルダ以下にインストールする

```text
pip install -r requirements.txt -t python/
```

Pythonデバッグに必要な拡張機能 `ms-python.python` をインストールする  

![2023-11-06-13-26-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/ede4d452-374e-76dd-d976-c71574dd38c1.png)

`test/sample-function/test.py` を開いてデバッグ実行を行う  

![2023-11-06-13-23-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/bff2903e-5196-2d0d-4ec4-acbad2f6e85a.png)

![2023-11-06-13-24-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/652b896f-6f21-975a-cf5d-04164fe9ba63.png)

# AWSサービス（S3）をLocalStackでモック化

[Python Lambda - コンテナ上でのローカルデバッグ環境構築（AWSサービス（S3）のモック化）](https://qiita.com/takmot/items/7f244baa8a56f5cd1f63)

# 参考

- [docker と docker-compose の初歩](https://qiita.com/hiyuzawa/items/81490020568417d85e86)
- [Windows+Python+Lambda+Dockerで開発環境づくり](https://blog.tricrow.com/entry/windows/development-environment-container)
- [Python の AWS ベースイメージを使用する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-image.html#python-image-instructions)