LocalStack を使って dockerise した Lambda を実行する方法

# はじめに
AWS上で動かしているアプリをどうにかしてローカルで実行できないかと思って
色々試してみてどうにか Lambda を動かすことができたのでそのまとめ。

# LocalStack を使った構成

## 使っている技術など
* Docker
* Docker Compose
* LocalStack
* Python3

## ディレクトリ構成

```
.
├── docker-compose.yml
├── app  # ここにAPIのコードが入っている感じ。今回は説明対象外
└── docker
    ├── api
    │   └── Dockerfile # 今回は説明対象外
    ├── lambda
    │   └── Dockerfile # AWS で実行するやつ。今回は説明対象外
    └── localstack
        ├── create_lambda.sh
        ├── dummy_function.py # 別にここにある必要はないけど、同じところにあった方が管理しやすいので
        └── dummy_function.zip
```

## ソースコード

全部載せると量が半端ないので、必要なところだけ抜粋。

```yaml: docker-compose.yml
version: '3'
services:
  db:
    container_name: db
    image: postgres:14.2-alpine
    volumes:
      - postgres:/var/lib/postgresql
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: password
      POSTGRES_DB: test
      PGPASSWORD: password
  api:
    container_name: api
    build:
      context: ./docker/api
      dockerfile: Dockerfile
    environment:
      - LAMBDA_FUNCTION_NAME: dummy_function
      - AWS_LAMBDA_ENDPOINT: http://localstack:4566
      - AWS_ACCESS_KEY_ID: test # localstack内部で使っている KEY ID
      - AWS_SECRET_ACCESS_KEY: test # localstack内部で使っている ACCESS KEY
      - AWS_DEFAULT_REGION: us-east-1  # localstackと一致していればどこでもよい
  localstack:
    container_name: localstack
    image: localstack/localstack:0.14.2
    volumes:
      - ./docker/localstack:/docker-entrypoint-initaws.d
      - localstack:/localstack
    environment:
      - DEBUG=${DEBUG- }
      - DEFAULT_REGION=us-east-1
      - SERVICES=${SERVICES- }
      - HOSTNAME_EXTERNAL=localstack
      - LS_LOG=info
      - DATA_DIR=/localstack
      - LAMBDA_HOST=hoge_lambda:8080
  hoge_lambda:
    container_name: hoge_lambda
    build:
      context: ./lambda
      dockerfile: Dockerfile # 実際に AWS 上で動かす Lambda の Dockerfile
    environment:
      AWS_ACCESS_KEY_ID: test
      AWS_SECRET_ACCESS_KEY: text
      AWS_DEFAULT_REGION: us-east-1
volumes:
  localstack:
  postgres:
```

```python: dummy_function.py
import http.client
import json
import os

LAMBDA_HOST = os.getenv("LAMBDA_HOST", "hoge_lambda:8080")


def lambda_handler(event, context):
    conn = http.client.HTTPConnection(LAMBDA_HOST)
    conn.request(
        "POST", "/2015-03-31/functions/function/invocations", json.dumps(event), {"Content-type": "application/json"}
    )
    response = conn.getresponse()
    if response.status != 200:
        print(
            "Failed to execute request. Status: {}, Message: {}, data: {}".format(
                response.status, response.reason, response.read().decode("utf8")
            )
        )
        raise Exception()

    body = response.read().decode("utf8")
    return json.loads(body)
```

`LAMBDA_HOST` を `docker-compose.yml` の `environment` などで設定すると、
任意の Lambda にリクエストを送ることが可能。
あと、本当なら `requests` などより使いやすい http クライアントを使った方がよいのだと思うのだけど、
単なるリクエストの proxy で且つ外部ライブラリのインストールは極力避けたかったのでこの方法を採用。

```shell: create_lambda.sh
#!/bin/bash

awslocal lambda create-function \
  --function-name dummy_function \
  --zip-file fileb:///docker-entrypoint-initaws.d/dummy_function.zip \
  --handler dummy_function.lambda_handler \
  --runtime python3.8 \
  --role test-role

echo "Finished."
```

ここで指定している `dummy_function.zip` は `dummy_function.py` だけを入れた Zip ファイルで、
予め作成して `./docker/localstack` などに置いておく。

```python: aws_lambda_service.py
import json
import os
import boto3


class AwsLambdaService:
    LAMBDA_FUNCTION_NAME = os.getenv("LAMBDA_FUNCTION_NAME")
    AWS_LAMBDA_ENDPOINT = os.getenv("AWS_LAMBDA_ENDPOINT")

    def invoke(self, payload):
        client = self._get_client()
        client.invoke(
            FunctionName=self.LAMBDA_FUNCTION_NAME, InvocationType="Event", Payload=json.dumps(payload),
        )

    @staticmethod
    def _get_client():
        return boto3.client(
            "lambda",
            endpoint_url=AWS_LAMBDA_ENDPOINT,
        )
```

`AWS_LAMBDA_ENDPOINT` を指定することで localstack 側の Lambda にリクエストを送ることが可能で、
ここが `None` だと AWS の Lambda にリクエストを送る。
なので、ローカルと本番で同じコードを使うことが可能。

# おわりに
この方法だと、アプリと Lambda の間に proxy 用の Lambda が入るだけで他は同じものを使える点がよいと思っている。

あと、今回はアプリケーションから Lambda を呼び出すパターンに対応したけど、
S3 のファイル作成をトリガーとして実行されるパターンも基本的には問題ないはず
（ localstack で実行できれば。。。)