コンテナイメージを使用した AWS Lambda 関数の開発環境構築をする（自動ユニットテスト / デバッグモード）

# はじめに

[過去の記事 その１](https://qiita.com/Yu-Chihuahua/items/3a84b320633a88f3662b)で、Docker コンテナ内の Web アプリケーションを Window 環境の VSCode からデバッグできるようにしました。
また、[過去の記事 その２](https://qiita.com/Yu-Chihuahua/items/1e450d3c084609e7de35)では、テストレポートおよびカバレッジレポートを出力できる、自動ユニットテスト環境を構築しました。

しかし、２つの記事は Web アプリケーション内に構築する前提としてしまったために、やりたいことをピンポイントで伝える記事としてはイマイチ(？)だと思えてきました。
今回は、コンテナイメージを使用した AWS Lambda 関数（Python）の開発環境構築をする（自動ユニットテスト / デバッグモード）手順を、新しい記事として作り直すことにしました。

# 前提
[過去の記事 その３](https://qiita.com/Yu-Chihuahua/items/02cb5dfddb8b24393f14)の通りに、Windows に Docker の開発環境を構築する手順は実施済みの前提です。

## 今回のゴール
- 自動ユニットテスト
    - Docker コンテナ内で起動している AWS Lambda 関数の自動ユニットテスト環境を構築する
    - テストレポート（HTML形式／JUnit XML形式／Text形式）およびカバレッジレポート（HTML形式）を出力する
- デバッグモード
    - Docker コンテナ内で起動している AWS Lambda 関数を VSCode からデバッグする

## ディレクトリ構成
今回のディレクトリ構成は下記のようになります。
```diff_shell:WSL2 Ubuntu
  /home/appuser

  ├── workspace
  │   ├── docker-compose.yaml
+ │   ├── .vscode
+ │   │   └── launch.json
+ │   └── lambda001
+ │        ├── Dockerfile
+ │        ├── opt
+ │        │   ├── bootstrap_debug
+ │        │   ├── lambda-entrypoint_debug.sh
+ │        │   └── pytest
+ │        │        ├── run_pytest.sh
+ │        │        └── reports
+ │        │             └ {yyyyMMddHHmmss}
+ │        └── var
+ │             └── task
+ │                  ├── app.py
+ │                  ├── pytest.ini
+ │                  └── tests
+ │                       └── test_app_001.py
```

## 手順
### 資材作成
#### 自動ユニットテスト / デバッグモード 共通
1. ```docker-compose.yaml```ファイルを修正
    ```docker-compose.yaml```ファイルを下記のように修正します。
    ```yaml:docker-compose.yaml
    version: '3'

    services:
      ##############################################################################
      # Lambda container
      ##############################################################################
      lambda001:
        build: "./lambda001/"
        entrypoint: "/bin/bash" # Lambda アプリケーションが自動的に起動しないようにする設定
        tty: true
        volumes:
          - "./lambda001/var/task/:/var/task/" # Lambda アプリケーションファイルの同期
          - "./lambda001/opt/pytest/:/opt/pytest/" # pytest 関連ファイルの同期
        ports:
          - "8080:8080" # Lambda port
          - "5678:5678" # Debug port
        image: "lambda001:develop"
        container_name: "lambda001"
    ```
    <br>

1. ```Dockerfile```ファイルを作成
    ```Dockerfile```ファイルを下記のように作成します。
    ```shell:Dockerfile
    # Python 3.12.1
    FROM public.ecr.aws/lambda/python:3.12.2024.03.04.10-x86_64

    # Python のライブラリをインストール
    RUN pip3 install pytest==8.1.*
    RUN pip3 install pytest-cov==4.1.*
    RUN pip3 install pytest-html==4.1.*
    RUN pip3 install debugpy==1.8.*

    # 資材を配置する
    COPY ./var/task/ /var/task/
    RUN mkdir -p /opt/pytest/reports/
    COPY ./opt/pytest/ /opt/pytest/
    COPY ./opt/bootstrap_debug /opt
    COPY ./opt/lambda-entrypoint_debug.sh/ /opt/

    # パーミッションを変更する
    RUN chmod 755 /opt/bootstrap_debug
    RUN chmod 755 /opt/lambda-entrypoint_debug.sh

    # 起動時のコマンドを設定する
    CMD ["app.handler"]
    ```
    <br>

1. Lambda ソースコードファイルを作成
    Lambda ソースコード```app.py```ファイルを下記のように作成します。
    ```python:app.py
    import json

    def handler(event, context):
        param1 = event["param1"]
        if param1 == "param001":
            return {
                "statusCode": 200,
                "body": json.dumps("OK")
            }
        else:
            return {
                "statusCode": 500,
                "body": json.dumps("ERROR")
            }
    ```

#### 自動ユニットテスト
1. テストコードファイルを作成
    テストコード```test_app_001.py```ファイルを下記のように作成します。
    ```python:test_app_001.py
    # coding: UTF-8

    import sys
    sys.path.append("/var/task/")

    import importlib
    import app as __app


    def test_app_001_xxxxx():
        """
        自動テストのサンプル
        """
        try:
            event = {"param1": "param001"}
            _app = importlib.reload(__app)
            res = _app.handler(event=event, context={})
            assert res["statusCode"] == 200

        except AssertionError as ae:
            raise ae

        except Exception as e:
            msg = str(e)
            assert False, f"Exception occurred. {msg}"


    def test_app_002_xxxxx():
        """
        自動テストのサンプル
        """
        try:
            event = {"param1": "param999"}
            _app = importlib.reload(__app)
            res = _app.handler(event=event, context={})
            assert res["statusCode"] == 500

        except AssertionError as ae:
            raise ae

        except Exception as e:
            msg = str(e)
            assert False, f"Exception occurred. {msg}"
    ```
    <br>

1. pytest 設定ファイルを作成
    pytest 設定ファイル```pytest.ini```を下記のように作成します。
    ```ini:pytest.ini
    [pytest]
    env =
        TZ=Asia/Tokyo
        AppEnv=TEST
    ```
    <br>

1. テストレポートディレクトリを作成
    権限を appuser にする都合上、テストレポートディレクトリを先に作成しておきます。
    ```shell:WSL2 Ubuntu
    mkdir -p /home/appuser/workspace/lambda001/opt/pytest/reports
    ```
    <br>

1. pytest 起動シェルを作成
    pytest 起動シェル```run_pytest.sh```を作成します。
    ```shell:run_pytest.sh
    #!/bin/bash

    # pytest をカバレッジレポートを出力するモードで起動するシェルスクリプト
    datetime=$(date '+%Y%m%d%H%M%S')
    path=/opt/pytest/reports
    out_path=$path/$datetime

    mkdir -p $out_path/report_text/
    mkdir -p $out_path/report_xml/
    mkdir -p $out_path/report_html/
    mkdir -p $out_path/coverage_html/

    cd /var/task/
    python -m pytest -c pytest.ini -s -v \
        --junitxml=$out_path/report_xml/report.xml \
        --html=$out_path/report_html/report.html \
        --cov-report=html:$out_path/coverage_html/ \
        --cov=/var/task/ \
        --cov-branch /var/task/ > $out_path/report_text/report.txt
    ```

#### デバッグモード
1. デバッグモードで起動するシェルを作成
    AWS Lambda 関数をデバッグモードで起動するために修正したシェル```bootstrap_debug```と```lambda-entrypoint_debug.sh```を作成します。
    AWS Lambda のコンテナに配置されているファイルを改造したものです。
    5678 ポートでリッスンするようにしています。
    ```shell:bootstrap_debug
    #!/bin/bash

    # AWS Lambda 関数をデバッグモードで起動するために修正したシェルスクリプト
    export AWS_EXECUTION_ENV=AWS_Lambda_python3.12

    if [ -z "$AWS_LAMBDA_EXEC_WRAPPER" ]; then
      exec /var/lang/bin/python3.12 -m debugpy --listen 0.0.0.0:5678 /var/runtime/bootstrap.py
    else
      wrapper="$AWS_LAMBDA_EXEC_WRAPPER"
      if [ ! -f "$wrapper" ]; then
        echo "$wrapper: does not exist"
        exit 127
      fi
      if [ ! -x "$wrapper" ]; then
        echo "$wrapper: is not an executable"
        exit 126
      fi
        exec -- "$wrapper" /var/lang/bin/python3.12 -m debugpy --listen 0.0.0.0:5678 /var/runtime/bootstrap.py
    fi
    ```
    ```shell:lambda-entrypoint_debug.sh
    #!/bin/sh
    
    # AWS Lambda 関数をデバッグモードで起動するために修正したシェルスクリプト
    if [ $# -ne 1 ]; then
      echo "entrypoint requires the handler name to be the first argument" 1>&2
      exit 142
    fi
    export _HANDLER="$1"
    
    RUNTIME_ENTRYPOINT=/opt/bootstrap_debug
    if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
      exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
    else
      exec $RUNTIME_ENTRYPOINT
    fi
    ```
    <br>

1. VSCode のデバッグ設定```launch.json```ファイルを作成
    VSCode のデバッグ設定```launch.json```ファイルを下記のように作成します。
    - コンテナ内とローカル環境のソースディレクトリをマッピング
    - デバッグ連携用に5678番ポートを公開する
    ```json:launch.json
    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "lambda001 Debugger",
                "type": "debugpy",
                "request": "attach",
                "connect": {
                    "host": "localhost",
                    "port": 5678
                },
                "pathMappings": [
                    {
                        "localRoot": "${workspaceFolder}/lambda001/var/task",
                        "remoteRoot": "/var/task"
                    }
                ]
            }
        ]
    }
    ```
    <br>

1. VSCode 設定
    VSCode 設定は、[過去の記事 その１](https://qiita.com/Yu-Chihuahua/items/3a84b320633a88f3662b#vscode-%E8%A8%AD%E5%AE%9A)の通りですので、こちらを参照してください。

### 動作確認
#### 自動ユニットテスト / デバッグモード 共通
1. Docker Compose でビルド＆起動
    Docker Compose でカスタムイメージをビルドして起動します。
    ```shell:WSL2 Ubuntu
    cd /home/appuser/workspace
    docker-compose build
    docker-compose up -d
    ```
    <br>

1. 起動した Docker コンテナに入る
    ```docker exec```コマンドで、起動した Docker コンテナに入ります。
    ```shell:WSL2 Ubuntu
    docker exec -it lambda001 bash
    ```

#### 自動ユニットテスト
1. pytest を起動する
    シェルを実行して pytest を起動します。
    ```shell:lambda001
    sh /opt/pytest/run_pytest.sh
    ```
    <br>

1. テストレポートを確認する
    Windows 環境から、出力されたテストレポートを確認します。
    ```\\wsl$\Ubuntu-22.04\home\appuser\workspace\lambda001\opt\pytest\reports\{yyyyMMddHHmmss}```に下記のテストレポートが出力されています。
    - カバレッジレポート（HTML形式）：```coverage_html\index.html```
    ![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/2c0d4269-c3fc-2abf-3969-75db65289226.png)
    - テストレポート（HTML形式）：```report_html\report.html```
    ![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/85804b12-fd2c-3432-a263-e0df007a22b6.png)
    - テストレポート（JUnit XML形式）：```report_xml\report.xml```
    - テストレポート（Text形式）（デフォルト）：```report_text\report.txt```

#### デバッグモード
1. AWS Lambda 関数をデバッグモードで起動
    シェルを実行して、AWS Lambda 関数をデバッグモードで起動します。
    ```shell:lambda001
    sh /opt/lambda-entrypoint_debug.sh app.handler
    ```
    <br>

1. VSCode でデバッグ待機
    VSCode の「実行とデバッグ（Ctrl+Shift+D）」タブから、```lambda001 Debugger```を起動します。
    また、```app.py```にブレークポイントを設定します。
    ![スクリーンショット 2025-02-11 133517.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/5ccd6963-b3de-83d7-5685-ba1e228907b7.png)
    <br>

1. AWS Lambda 関数へリクエスト送信
    AWS Lambda 関数へリクエスト送信します。
    今回は、ローカル環境（Ubuntu）で、Curl コマンドを実行するものとします。
    ```shell:WSL2 Ubuntu
    curl -X POST http://localhost:8080/2015-03-31/functions/function/invocations \
     -H "Content-Type: application/json" \
     -d '{"param1": "param001"}'
    ```
    <br>

1. デバッグ操作を確認
    ブレークポイントで処理が止まることを確認します。
    また、ステップオーバー（F10）などの操作ができること、変数の値を参照できることを確認します。
    ![スクリーンショット 2025-02-11 133517.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/0f82e4d2-144a-9622-372a-e2099b94c4bc.png)
    ![ddd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3767768/5084071e-3800-d490-e4db-cfd0a791966c.png)

## 最後に
今回は、今まで投稿してきたことの応用を、コンテナイメージを使用した AWS Lambda 関数の開発環境構築に適用してみました。
これで開発スピードをアップすることができるかと期待しています。