LocalStack に Lambda 関数を作成して実行する

## このエントリについて

- [こちらのエントリ](https://qiita.com/outerlet/items/8199c2f4eb7cbc678f16) と同じく、以前残したメモを供養する目的で書きました
- LocalStack はv3がリリース済みですが、ここに書いたのはv2の使用時に調べた設定やコマンドです。v3のコンテナでも通用することを確認したうえで書いていますが、v3に最適化したわけでないことはご承知おきください
- LocalStack の CloudWatch Logs を使って動作確認していますが、操作は全てCLIでやっています。コンソールなどで確認できるのかも知れませんが、そのあたりは調べきれておらず、割愛させてください
- `aws lambda` のコマンドリファレンスは [こちら](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/index.html "https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/index.html") でご確認下さい

## ソースコード

以下の関数を用意
関数名を "simple_function" に、ファイル名を "simple_function.py" とした

```
import json

def lambda_handler(event, context):
    value1 = event['value1']
    value2 = event['value2']

    print(f'[CALLED] simple_function: value1 = {value1}, value2 = {value2}')

    try:
        print(f'value1 + value2 = {int(value1) + int(value2)}')
        result = True
    except ValueError as e:
        print(f'ERROR: {e}')
        result = False

    return json.dumps({"result": result})
```

## 関数の作成

zipファイルを使って関数を1つ作成する方法で実施
とはいえ基本的には、以下のどちらでやるか選ぶだけで、やることは実環境と変わらない

- `aws` コマンド実行時に `--profile` と `--endpoint-url` を指定
- `aws` ではなく `awslocal` コマンドで `lambda` や `logs` を実行

### 作成

1. `zip` コマンドで関数のソースコードをアーカイブ
    - パッケージを含めないなら、対象のファイルだけを `zip` すればよい
2. `lambda create-function` を発行して、zipファイルから関数をデプロイ
    - `aws lambda --profile xxxxx --endpoint-url xxxxx create-function`
    - `awslocal lambda create-function` 

#### `create-function` 実行時の注意点

- `--role` をARNで指定（必須）
    - 指定する文字列は、ARNのフォーマットにさえ合致していれば適当でOK
    - LocalStack v1 時代はフォーマット関係なく任意の文字列でOKだった。v1の頃のサンプルもネットに転がっているが、それらはv2以降だと使えないことに注意
        - 詳しくは[こちら](https://docs.localstack.cloud/user-guide/aws/lambda/#migrating-to-lambda-v2 "https://docs.localstack.cloud/user-guide/aws/lambda/#migrating-to-lambda-v2")を参照
        - 上記のページには『The legacy Lambda implementation has been removed since LocalStack 3.0 (Docker `latest` since 2023-11-09)』とあるので、原則ARNを指定すると考えておいた方がよさそう
- `--timeout` の指定（推奨）
    - 実環境の `lambda create-function` と同じく、指定しないとデフォルトである 3秒 が設定される。これより大きな値を設定しておいた方が無難
    - 実行にかかった時間が `--timeout` に設定した時間を超過すると、「Task timed out after 3.00 seconds」と出るだけで結果が分からない
    - 例外の発生源に `try` - `except` を仕掛けてログ出しなどをしても、そこに到達するまで3秒を超えるとタイムアウトするので、ログが出ず何が起きて失敗したのかが判別できない
    - 他のAWSリソースに接続するなら3秒を超えることは十分あり得る。デバッグに支障が出る可能性を考慮すると、ある程度の時間を設定するのが無難かと

### 関数の状態を確認

以下のコマンドを使って最新のデプロイ状態を確認できる

- `lambda list-functions` 
    - デプロイ済みの関数一覧を表示
- `lambda get-function --function-name xxxxx` 
    - 任意の関数について、関数名を指定してその詳細を表示

### （例） 関数の作成〜状態確認

コンテナの外から `aws` コマンドで実行

1. `aws lambda list-functions` で、既存の関数が無いことを確認
2. `aws lambda create-function` で、zipファイルから関数を作成
3. `aws lambda list-functions` を改めて実行し、目的の関数が作成されていることを確認
4. `aws lambda get-function` で目的の関数の詳細を確認
    - `create-function` 直後の `State` は `Pending` なので、ここで `Active` になっていることを確認

```
% aws lambda --profile localstack --endpoint-url http://localhost:4566 list-functions
{
    "Functions": []
}

% aws lambda create-function \
--profile localstack \
--endpoint-url http://localhost:4566 \
--function-name simple_function \
--runtime python3.12 \
--zip-file fileb:///Users/paratroops74/workspaces/aws/LocalLambda/simple_function.zip \
--handler simple_function.lambda_handler \
--role arn:aws:iam::000000000000:role/lambda-role \
--timeout 30
{
    "FunctionName": "simple_function",
    "FunctionArn": "arn:aws:lambda:ap-northeast-3:000000000000:function:simple_function",
    ...
    "State": "Pending",
    ...
}

% aws lambda --profile localstack --endpoint-url http://localhost:4566 list-functions
{
    "Functions": [
        {
            "FunctionName": "simple_function",
            "FunctionArn": "arn:aws:lambda:ap-northeast-3:000000000000:function:simple_function",
            ...
            "LoggingConfig": {
                "LogFormat": "Text",
                "LogGroup": "/aws/lambda/simple_function"
            }
        }
    ]
}

% aws lambda --profile localstack --endpoint-url http://localhost:4566 get-function --function-name simple_function
{
    "Configuration": {
        "FunctionName": "simple_function",
        "FunctionArn": "arn:aws:lambda:ap-northeast-3:000000000000:function:simple_function",
        ...
        "State": "Active",
        ...
    },
    "Code": {
        ...
    }
}
```

コンテナに入って `awslocal` コマンドで実行

1. `awslocal lambda list-functions` で、コンテナの外から実行したのと同じ結果が得られることを確認

```
% docker container exec -it localstack-main /bin/bash 
# awslocal lambda list-functions
{
    "Functions": [
        {
            "FunctionName": "simple_function",
            "FunctionArn": "arn:aws:lambda:ap-northeast-3:000000000000:function:simple_function",
            "Runtime": "python3.12",
            "Role": "arn:aws:iam::000000000000:role/lambda-role",
            "Handler": "simple_function.lambda_handler",
            "CodeSize": 409,
            "Description": "",
            "Timeout": 30,
            ...
        }
    ]
}
```

## 関数の実行と結果確認

これも `aws` に `--profile` と `--endpoint-url` をつけて実行するか、 `aws` でなく `awslocal` で実行するか選ぶだけで、基本的にやることは実環境と変わらない

### 実行

1. `lambda invoke` で関数を実行
    - ここでは `--invocation-type RequestResponse` として、同期的に実行している

  

#### （例） 関数の実行とレスポンスの確認

コンテナに入って `awslocal` で実行

- 正常終了する値を与えた場合

```
# awslocal lambda invoke \
--function-name simple_function \
--invocation-type RequestResponse \
--payload '{"value1":"10","value2":"20"}' \
/tmp/response_simple_function.json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

# cat /tmp/response_simple_function.json 
"{\"result\": true}"
```

- 例外になる値を与えた場合

```
# awslocal lambda invoke \
--function-name simple_function \
--invocation-type RequestResponse \
--payload '{"value1":"hoge","value2":"fuga"}' \
/tmp/response_simple_function_err.json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

# cat /tmp/response_simple_function_err.json 
"{\"result\": false}"
```

### 結果の確認

CloudWatch Logs で出力を確認したければ、 `SERVICES` 環境変数に `logs` を含めること

```
SERVICES=lambda,logs
```

1. `lambda invoke` の実行時、レスポンスの出力先に指定したファイルの中身を確認
    - 期待したレスポンスが返ってきているか
2. `logs` コマンドで CloudWatch Logs に出力された内容を確認
    - 今回はシンプルに `print()` でログに出力しているので、期待した内容が出力されているか
    - `describe-log-groups` でロググループを、 `describe-log-streams` でログストリームを特定して、 `get-log-events` でログイベントの内容を確認

#### （例） 実行確認の確認

コンテナに入って `awslocal` で実行

- 正常終了する値を与えた場合

```
# awslocal logs describe-log-groups
{
    "logGroups": [
        {
            "logGroupName": "/aws/lambda/simple_function",
            "creationTime": 1721635186611,
            ...
        }
    ]
}

# awslocal logs describe-log-streams --log-group-name '/aws/lambda/simple_function'
{
    "logStreams": [
        {
            "logStreamName": "2024/07/22/[$LATEST]30951136d89e941f531bdc375a500dfc",
            "creationTime": 1721635186613,
            ...
        }
    ]
}

# awslocal logs get-log-events --log-group-name '/aws/lambda/simple_function' --log-stream-name '2024/07/22/[$LATEST]30951136d89e941f531bdc375a500dfc'
{
    "events": [
        {
            "timestamp": 1721635186581,
            "message": "START RequestId: 8744aa43-547c-4140-b9af-dbcd349f7183 Version: $LATEST",
            "ingestionTime": 1721635186615
        },
        {
            "timestamp": 1721635186586,
            "message": "[CALLED] simple_function: value1 = 10, value2 = 20",
            "ingestionTime": 1721635186615
        },
        {
            "timestamp": 1721635186592,
            "message": "value1 + value2 = 30",
            "ingestionTime": 1721635186615
        },
        {
            "timestamp": 1721635186597,
            "message": "END RequestId: 8744aa43-547c-4140-b9af-dbcd349f7183",
            "ingestionTime": 1721635186615
        },
        {
            "timestamp": 1721635186603,
            "message": "REPORT RequestId: 8744aa43-547c-4140-b9af-dbcd349f7183\tDuration: 5.89 ms\tBilled Duration: 6 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t",
            "ingestionTime": 1721635186615
        }
    ],
    ...
}
```

- 例外になる値を与えた場合
    - 正常終了させたあとに実行したのでロググループは既存。そのため `describe-log-groups` は割愛

```
# awslocal logs describe-log-streams --log-group-name '/aws/lambda/simple_function'
{
    "logStreams": [
        {
            "logStreamName": "2024/07/22/[$LATEST]30951136d89e941f531bdc375a500dfc",
            "creationTime": 1721635186613,
            ...
        },
        {
            "logStreamName": "2024/07/22/[$LATEST]c98be5efdc03dd4338b92565869e6013",
            "creationTime": 1721636530097,
            ...
        }
    ]
}

# awslocal logs get-log-events --log-group-name '/aws/lambda/simple_function' --log-stream-name '2024/07/22/[$LATEST]c98be5efdc03dd4338b92565869e6013'
{
    "events": [
        {
            "timestamp": 1721636530093,
            "message": "START RequestId: 162e74ab-8465-4d74-a4d3-bdb70539a1d9 Version: $LATEST",
            "ingestionTime": 1721636530098
        },
        {
            "timestamp": 1721636530093,
            "message": "[CALLED] simple_function: value1 = hoge, value2 = fuga",
            "ingestionTime": 1721636530098
        },
        {
            "timestamp": 1721636530094,
            "message": "ERROR: invalid literal for int() with base 10: 'hoge'",
            "ingestionTime": 1721636530098
        },
        {
            "timestamp": 1721636530095,
            "message": "END RequestId: 162e74ab-8465-4d74-a4d3-bdb70539a1d9",
            "ingestionTime": 1721636530098
        },
        {
            "timestamp": 1721636530095,
            "message": "REPORT RequestId: 162e74ab-8465-4d74-a4d3-bdb70539a1d9\tDuration: 6.02 ms\tBilled Duration: 7 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t",
            "ingestionTime": 1721636530098
        }
    ],
    ...
}
```