Python Lambda - コンテナ上でのローカルデバッグ環境構築（AWSサービス（S3）のモック化）

# 内容

[前回記事](https://qiita.com/takmot/items/648f35c04e0717a4a6fe)の続き

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

上記を[前回記事](https://qiita.com/takmot/items/648f35c04e0717a4a6fe)で実施したので、  
LocalStackでAWSサービス（S3）のモック化を行う

以下は別記事で対応する  

- [Amazon RDSを別途PostgreSQLサーバー用コンテナで対応する](https://qiita.com/takmot/items/257c4448a4ca9b9df9f8)

# フォルダ構成

基本は[前回記事](https://qiita.com/takmot/items/648f35c04e0717a4a6fe#%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E6%A7%8B%E6%88%90)通り  
モック化したS3間でput/getするテスト用ファイルを `test/sample-function` に追加

```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_data.txt         // S3間でput/getするファイル
 │    │            └─ test.py               // lib/handlers/sample-function/python/sample_function.pyのテストコード
 │    │
 │    ├─ docker-compose.yml
 │    └─ Dockerfile-lambda-python           // Python Lambda実行用のコンテナDockerfile
 …
 │
 ├─ poetry.lock
 ├─ pyproject.toml
 │

```

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

[前回記事](https://qiita.com/takmot/items/648f35c04e0717a4a6fe#%E3%83%86%E3%82%B9%E3%83%88%E5%AF%BE%E8%B1%A1lambda%E9%96%A2%E6%95%B0%E3%82%B3%E3%83%BC%E3%83%89)より、  
S3からファイルをダウンロードする処理を追加している  
LocalStackにアクセスするため `boto3.client` に `endpoint_url` を指定している  
環境変数で `ENDPOINT_URL` が指定されている場合のみ適用

```python:lib/handlers/sample-function/python/sample_function.py
import os
import boto3
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")
endpoint_url = os.getenv("ENDPOINT_URL", default=None)


def handler(event, context):
    print(f"event: {event}")
    print(f"test_env_value: {test_env_value}")
    print(f"endpoint_url: {endpoint_url}")

    print_message("test message 1")

    df = pd.DataFrame([["A", 170, 60], ["B", 160, 50], ["C", 165, 58]])
    print(f"df: {df}")

    tokyo_dt = datetime.now(timezone("Asia/Tokyo"))
    
    # S3 get object
    s3_client = boto3.client('s3', endpoint_url=endpoint_url)
    response = s3_client.get_object(Bucket=event["bucket"], Key=event["key"])
    file_data = response["Body"].read().decode("utf-8")
    print(f"file_data: {file_data}")

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

## テストコード

[前回記事](https://qiita.com/takmot/items/648f35c04e0717a4a6fe#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89)より、  
S3にBucket作成～ファイルアップロード処理を追加している  
LocalStackにアクセスするため `boto3.client` に `endpoint_url` を指定している  

```python:test/sample-function/test.py
import os
import boto3
import botocore

os.environ["TEST_ENV_VALUE"] = "test env value"
os.environ["ENDPOINT_URL"] = "http://localstack:4566"   # LocalStackアクセス用のEndpoint

import sample_function

# S3 Bucket作成
def s3_init(bucket_name):
    try:
        endpoint_url = os.getenv("ENDPOINT_URL", default=None)
        s3_client = boto3.client("s3", endpoint_url=endpoint_url)
        location = {"LocationConstraint": "ap-northeast-1"}
        s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location)
        return s3_client
    
    except botocore.exceptions.ClientError as error:
        if error.response['Error']['Code'] == 'BucketAlreadyOwnedByYou':
            print("Bucket Already Owned By You")
            return s3_client
        else:
            raise


def test_1(s3_client, test_file, bucket_name, key):
    # S3 put object
    s3_client.upload_file(test_file, bucket_name, key)
    
    event = {"bucket": bucket_name, "key": key}
    context = {}

    sample_function.handler(event, context)


test_file = "test/sample-function/test_data.txt"
bucket_name = "test-bucket-1"
key = "test_folder/test_data.txt"

s3_client = s3_init(bucket_name)
test_1(s3_client, test_file, bucket_name, key)
```

# docker-compose

Python Lambda 実行用のコンテナについては、[前回記事](https://qiita.com/takmot/items/648f35c04e0717a4a6fe#python-lambda-%E5%AE%9F%E8%A1%8C%E7%94%A8%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A)参照  
localstackサービスを追加している

```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
  localstack:
    image: localstack/localstack:2.3.2
    ports:
      - "4566:4566"
    environment:
      - DEBUG=1
      - DOCKER_HOST=unix:///var/run/docker.sock
```

# Remote Development でのデバッグ

手順は[前回記事](https://qiita.com/takmot/items/648f35c04e0717a4a6fe#remote-development-%E3%81%A7%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0)通り  

![2023-11-07-13-39-34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/6fd79f17-630f-0734-6840-b05222b990ee.png)

# Amazon RDSを別途PostgreSQLサーバー用コンテナで対応する

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

# 参考

- [Docker ComposeでLocalStackを立ち上げてLambda関数のテストをやってみた](https://dev.classmethod.jp/articles/launched-localstack-with-docker-compose-tested-lambda-function/)
- [LocalStackを使ってローカル環境でS3を立ち上げ、Lambdaでファイルをアップロードしてみた](https://qiita.com/yasomaru/items/fa708a1f21a79e637868)
- [LocalStackをDockerコンテナ内で使う場合に、S3のバケット名がサブドメインになって接続できなくて困った話（解決済み）](https://qiita.com/imunew/items/91f01a78a19a29caf3b1)