TypeScriptでAWS lambdaのローカル開発を試す

# 前置き

仕事でAWS LambdaとTypeScriptで開発をすることになった
ただAWSの知識はそんなに持ってないし開発環境どうすればいいんだ…
と悩んでいたところに以下の記事を発見。

https://qiita.com/px_y_ohara/items/3866922daeba12315d79

更に公式でDockerイメージが提供されており使用方法の説明もあった。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/typescript-image.html

じゃあ両方うまいこと合わせたらいい感じの環境を作れるか？

# 環境構築

最終的にプロジェクトの構成は以下のようにした。
`/src`配下の`ts`をビルドして`/dist`配下に生成し
`/dist`をdocker上にマウントして動作させるという構成。

```
/project_dir
 ┣ /src
 ┃  ┗ index.ts
 ┣ /dist
 ┃  ┣ index.js
 ┃  ┗ index.js.map
 ┣ /node_module
 ┣ Dockerfile
 ┣ docker-compose.yaml
 ┣ package.json
 ┣ package-lock.json
 ┣ lambda-exec.sh
 ┗ docker-command.sh
```

## nodeインストール

作業環境は WSL 上の ubuntu を利用することとして
デフォルトだと node のバージョンが低いので nvm を導入

https://github.com/nvm-sh/nvm

nvm 入れたらとりあえず node が最新版になったので
AWS lambda 公式マニュアルに従い node プロジェクトを作成する。

```console
$ mkdir ~/project_dir
$ cd ~/project_dir
$ npm init
$ npm install -D @types/aws-lambda esbuild
```

`packege.json` にビルド用コマンドを追記
マニュアルから少し変更し `index.ts` を `src/index.ts` と指定

```json:package.json
  "scripts": {
    "build": "esbuild src/index.ts --bundle --minify --sourcemap --platform=node --target=es2020 --outfile=dist/index.js"
  }
```

## TypeScriptコード記述

`/src`ディレクトリを作成し、その下にコードを記述。
このコードはマニュアルをそのまま利用。

```console
$ mkdir src
$ touch src/index.ts
```

```typescript:index.ts
import { Context, APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';

export const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
    console.log(`Event: ${JSON.stringify(event, null, 2)}`);
    console.log(`Context: ${JSON.stringify(context, null, 2)}`);
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: 'hello world',
        }),
    };
};
```

`index.ts`が作成出来たらとりあえずビルド。
ビルドすると `/dist` 配下に `index.js` が作成される。
```console
$ pwd
/project_dir
$ npm run build
```

## Docker準備

`Dockerfile` と `docker-compose.yaml` を作る。
冒頭の記事を参考にしてなんかいい感じに調整。
もっといい書き方とか不要な記述はあるかもしれない

```console
$ pwd
/project_dir
$ touch Dockerfile
$ touch docker-compose.yaml
```

```Dockerfile:Dockerfile
FROM public.ecr.aws/lambda/nodejs:22 as builder
WORKDIR ${LAMBDA_TASK_ROOT}
COPY package.json ./
RUN npm install
```

```yaml:docker-compose.yaml
services:
  lambda-local:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      LAMBDA_TASK_ROOT: /var/task
    command: [ "index.handler" ]
    restart: always
    volumes:
      - ./dist:/var/task/
    ports:
      - "9000:8080"
    logging:
      options:
        max-size: "5m"
        max-file: "10"
```

ファイルを作ったらとりあえずビルドして起動。
```console
$ docker compose build --no-cache
$ docker compose up -d
```

## 動作確認

Dockerをビルドして問題なく起動できたら
マニュアルの curl コマンドを使って動くか試す

```console
$ curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
```

以下のようなレスポンスが返ってくれば成功
```console
{"statusCode":200,"body":"{\"message\":\"hello world\"}"}
```

## 変更を反映する

これで環境ができたのでコードの編集を行っていく。
変更した場合はまた `index.ts` をビルドして dockerを再起動する必要がある。

```console
$ pwd
/project_dir
$ npm run build
$ docker compose restart lambda-local
```

nodeなら自動ビルドもあるけど一旦それは置いといて・・・


## シェルスクリプトを作っておく

書いた関数の動作確認に長い curl コマンド覚えられないし
変更を反映させるために毎回コマンドを実行するのはちょっとめんどくさいので
簡単なシェルスクリプトを作っておいてちょっと楽をしたい。

```shell:lambda-exec.sh
#!/bin/bash
curl -X POST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
```

```shell:docker-command.sh
#!/bin/bash
cd ~/project_dir
param=$1
if [ -z "$param" ]; then
    echo option is "up" "restart" "down" "rebuild"
elif [ $param = "up" ]; then
    npm run build
    docker compose up -d
elif [ $param = "restart" ]; then
    npm run build
    docker compose restart lambda-local
elif [ $param = "down" ]; then
    docker compose down
elif [ $param = "rebuild" ]; then
    docker compose build --no-cache
fi
```

これでお試しできる環境が一通り揃ったと思う。

## 番外編：POSTでパラメータを渡す

ちょっと詰まったので備忘録として

POSTでパラメータを渡したとき、lambda関数上でどう受け取るか
マニュアルの curl コマンドだと上手く表示できなかったので色々試した

まずは`index.ts`を編集してパラメータを表示するように修正
```index.ts
import { Context, APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';

export const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
    const requestBody = event.body ? JSON.parse(event.body) : null;
    console.log('Request Body:', requestBody);

    const response: APIGatewayProxyResult = {
        statusCode: 200,
        body: JSON.stringify({
            message: 'Received the request body',
            data: requestBody,
        }),
    };

    return response;
};
```

変更後、動作を確認するために以下のcurlコマンドでうまく行った。
`APIGatewayEvent` という型を使った場合、大元を`body`とする必要があった。
```console
// 実行
$ curl -X POST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"body": "{\"payload\":\"hello world!\"}"}'

// レスポンス
{"statusCode":200,"body":"{\"message\":\"Received the request body\",\"data\":{\"payload\":\"hello world! APItester\"}}"}
```

上手くできたら`lambda-exec.sh`を更新しておく。