AWS CDK + SAMでローカル実行しているLambdaにattachしてデバッグ実行したメモ

## 概要
[前回はローカルでMongoを動かした。](https://qiita.com/hibohiboo/items/15db5256624c7a8e8b8c)
今回はローカルで実行しているlambdaにデバッグ実行を試してみる。
[ソースコード](https://github.com/hibohiboo/develop/tree/0466b37b367ea738564395fb82ac6a8dc1d31547/tutorial/lesson/aws/typescript/projects/cdk_sample)



## 環境
* Windows 10
* aws-cdk 1.95.1
* SAM CLI. 1.20.0
* VSCode 1.54.3

## やること
* attachしてデバッグ実行
* launch.jsonの自動生成

## フォルダ階層

```yaml
- .vscode
  - launch.json       # VSCodeのデバッガ設定
- cdk                 # cdk用の設定フォルダ
  - lib
    - sample-stack.ts # デバッグ用の設定
  - cdk.out
    - assets.hoge
      - index.js
      - index.map.js
    - template.yaml
  - cdk.json
  - package.json
- src
  - handlers
- tsconfig.json
```

## 準備
### ソースマップ
デバッグ実行でTypescriptとバンドルされたjsを紐づけるためのソースマップが必要。

```diff:cdk/lib/sample-stack.ts
    const echoFunction = new NodejsFunction(this, 'echo', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: `${entryHandlerDir}/get-echo.ts`,
      functionName: 'get-echo',
      handler: 'echoHandler',
+      bundling: { sourceMap: true },
    });
    new NodejsFunction(this, 'test', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: `${entryHandlerDir}/test.ts`,
      functionName: 'kotatest',
      bundling: {
+        sourceMap: true,
        externalModules: [
          'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
          'date-fns', // Layrerに入れておきたいモジュール
        ],
        define: { // Replace strings during build time
          'process.env.API_KEY': JSON.stringify(`\\"${'xxx-xxx'}\\"`), // バグってそう.エスケープしないとInvalid define valueのエラー
        },

      },
      layers: [nodeModulesLayer],
    });
```

### デバッガ設定
dockerのコンテナ上のソースと、ローカルに出力したソースマップの紐づけがデバッガに必要。
`cdk synth > cdk.out/template.yaml`コマンドでソースを出力した後、紐づけの設定を行う。
以下はAPI用に一つ、invokeで呼び出すように一つ、合計2つの設定をしている状態

```json:.vscode/lauch.json
{
  "version": "0.2.0",
  "configurations": [
      {
          "type": "node",
          "request": "attach",
          "name": "api echo ",
          "port": 5858,
          "address": "localhost",
          "localRoot": "${workspaceFolder}/cdk/cdk.out/asset.eb94322643dddde1077f91e199ef7ddd442fb0064b028e1c9196f8b5130ec4e1",
          "remoteRoot": "/var/task",
          "protocol": "inspector",
      },
      {
        "type": "node",
        "request": "attach",
        "name": "invoke test ",
        "port": 5858,
        "address": "localhost",
        "localRoot": "${workspaceFolder}/cdk/cdk.out/asset.ff62102abf256369f4067436df86939e80532114601e39c34faa7edd1c438306",
        "remoteRoot": "/var/task",
        "protocol": "inspector",
    }
  ]
}
```

### デバッグ実行設定

`-d ポート番号`オプションでデバッグ実行ができるので、スクリプトにしておく。

```diff:cdk/package.json
    "localinvoke": "cd cdk.out && sam.cmd local invoke testAF53AC38 --no-event",
+    "debuginvoke": "cd cdk.out && sam.cmd local invoke -d 5858 testAF53AC38 --no-event",
    "dev-serve": "cd cdk.out && sam.cmd local start-api -n ../env.json",
+    "debug-serve": "cd cdk.out && sam.cmd local start-api -d 5858 -n ../env.json"
```

### ブレークポイント設定

確認したい箇所にブレークポイントを入れておく。

## 実行

### invoke

`npm run debuginvoke`で起動。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/5cd34925-96da-0d90-38ed-4a77055b4260.png)

サイドバーのデバッグから、「invoke test」を選択。実行ボタン|>を押す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/aa07c44c-1750-0a4e-f75f-76d8b8aaa7f0.png)

最初はjsファイルで止まるので、続行してやる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/73ec8ea3-9a84-626b-4487-f11c2e40be68.png)

ブレークポイントで止めることができた。 ここからステップ実行可能となる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/2c27cc1d-3478-5f02-e4fa-766939dcec0e.png)
### API

APIも同様。
`npm debug-serve`。
デバッグしたいAPI(今回の場合はecho)を叩くと、`For help, see: https://nodejs.org/en/docs/inspector`が表示されるので、「api echo」を選択。実行ボタン|>を押す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/f29f1bf3-2f52-ec1d-e942-5d5e0ae435f1.png)

アタッチされるとjsファイルでいったん止まる。続行する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/1867150c-391e-fe69-d296-1602dbf23bda.png)

ブレークポイントで止めることができた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/981fe6d7-f3be-7f0f-3172-99158af30986.png)

[ソースコード](https://github.com/hibohiboo/develop/tree/b16fdc2588755d520018137caa940fd5b4c6f207/tutorial/lesson/aws/typescript/projects/cdk_sample)


## launch.jsonの自動作成
毎回、assetsのパスを確認するのは大変。
なので、バンドルと同時にlaunch.jsonを更新するようにした。

```diff:cdk/bin/sample-index.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { SampleStack } from '../lib/sample-stack';
import { bundleNpm } from '../lib/process/setup';
+ import { createLaunch } from '../lib/process/after';

// pre-process
bundleNpm();

// // create app
const app = new cdk.App();
const stack = new SampleStack(app, 'SampleStack2021');
+ createLaunch(stack)
```

* 愚直にtemplate.jsonから得られる情報をlaunch.jsonに書き込んでいる。

```ts:cdk/lib/process/after.ts
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
import * as fs from 'fs';
import * as path from 'path'
import { SampleStack } from '../sample-stack';
const launchFilePath = path.resolve(process.cwd(), '..', '.vscode', 'launch.json');
const assetIndex = 1;
const debugPort = 5858;
const configuration = {
  "type": "node",
  "request": "attach",
  "name": "api echo ",
  "port": debugPort,
  "address": "localhost",
  "localRoot": "${workspaceFolder}/cdk/cdk.out/ASSET_PATH",
  "remoteRoot": "/var/task",
  "protocol": "inspector",
}
export const createLaunch = async (stack: SampleStack) => {
  const confs = stack.node.children.filter((item): item is NodejsFunction => item instanceof NodejsFunction)
    .map((item: NodejsFunction) => ({
      ...configuration,
      // @ts-ignore
      name: `lambda ${item.physicalName} ${item.permissionsNode.uniqueId.replace(stack.stackName, '')}`, // uniqueIdはdeprecatedだが現在templateに出力されているので使う
      // @ts-ignore
      localRoot: configuration.localRoot.replace('ASSET_PATH', item.node.children[assetIndex].assetPath)
    }))
  const launch = {
    "version": "0.2.0",
    "configurations": confs
  }
  fs.writeFileSync(launchFilePath, JSON.stringify(launch))
}
```

<details><summary>作成されたlaunch.json</summary><div>

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "lambda kotahello hello25C2F4BF",
      "port": 5858,
      "address": "localhost",
      "localRoot": "${workspaceFolder}/cdk/cdk.out/asset.005313309c1a04502205e8341511c4f2d861dd96521f4d707c2b9b49b9c9cd81",
      "remoteRoot": "/var/task",
      "protocol": "inspector"
    },
    {
      "type": "node",
      "request": "attach",
      "name": "lambda kotatest test2F9CF384",
      "port": 5858,
      "address": "localhost",
      "localRoot": "${workspaceFolder}/cdk/cdk.out/asset.ff62102abf256369f4067436df86939e80532114601e39c34faa7edd1c438306",
      "remoteRoot": "/var/task",
      "protocol": "inspector"
    },
    {
      "type": "node",
      "request": "attach",
      "name": "lambda get-echo echoD896FE0C",
      "port": 5858,
      "address": "localhost",
      "localRoot": "${workspaceFolder}/cdk/cdk.out/asset.eb94322643dddde1077f91e199ef7ddd442fb0064b028e1c9196f8b5130ec4e1",
      "remoteRoot": "/var/task",
      "protocol": "inspector"
    },
    {
      "type": "node",
      "request": "attach",
      "name": "lambda get-user getUserD5985AD3",
      "port": 5858,
      "address": "localhost",
      "localRoot": "${workspaceFolder}/cdk/cdk.out/asset.d8b09a14625edbffe651afd290e80d8e761c109eb9f36bf78118877a0887ef4c",
      "remoteRoot": "/var/task",
      "protocol": "inspector"
    },
    {
      "type": "node",
      "request": "attach",
      "name": "lambda post-user postUser43EF2B65",
      "port": 5858,
      "address": "localhost",
      "localRoot": "${workspaceFolder}/cdk/cdk.out/asset.b7ec805956444f4fc600442e05c20ccdb9d8eb8d075babcb68381db04d468b3e",
      "remoteRoot": "/var/task",
      "protocol": "inspector"
    }
  ]
}
```

</div></details>

[ソースコード](https://github.com/hibohiboo/develop/tree/3d24d2fa86721ea5f6e18af170b1ee312835f0f6/tutorial/lesson/aws/typescript/projects/cdk_sample)


### リファクタリング
uniqueIdがdeprecateのため、jsonから必要な情報を読み込むようにリファクタリング

```ts:cdk/lib/process/after.ts
import * as fs from 'fs';
import * as path from 'path'
import { SampleStack } from '../sample-stack';
const launchFilePath = path.resolve(process.cwd(), '..', '.vscode', 'launch.json');
const assetPlaceHolder = 'ASSET_PATH';
const debugPort = 5858;
const configuration = {
  "type": "node",
  "request": "attach",
  "name": "",
  "port": debugPort,
  "address": "localhost",
  "localRoot": "${workspaceFolder}/cdk/cdk.out/" + assetPlaceHolder,
  "remoteRoot": "/var/task",
  "protocol": "inspector",
}
type Config = typeof configuration
export const createLaunch = async (stack: SampleStack) => {
  const buf = fs.readFileSync(path.join(process.cwd(), 'cdk.out', stack.templateFile))
  const template = JSON.parse(buf.toString());
  const resources = template.Resources;
  const confs: Config[] = [];

  for (const prop in resources) {
    if (resources[prop].Type !== 'AWS::Lambda::Function') continue

    confs.push({
      ...configuration,
      name: `lambda ${prop}`,
      localRoot: configuration.localRoot.replace(assetPlaceHolder, resources[prop].Metadata['aws:asset:path'])
    })
  }

  const launch = {
    "version": "0.2.0",
    "configurations": confs
  }
  fs.writeFileSync(launchFilePath, JSON.stringify(launch))
}
````


## エラー
前のローカル実行のゴミが残っていたりすると以下のエラーになることがある。

```
requests.exceptions.HTTPError: 500 Server Error: Internal Server Error for url: http+docker://localnpipe/v1.35/containers/9a7c6cc76fd0736fe91333a05070a69c4c92b6cd52343a438fd7e626af7d1a5e/start
```

以下のように、いったんdockerを消してやるとよい。

```bash
docker stop $(docker ps -q)

# 全てのdockerコンテナを削除
docker rm -f $(docker ps -aq)
```



## 参考
[AWS - Step-through debugging Node.js functions locally](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-using-debugging-nodejs.html)
[AWS - サーバーレスアプリケーションをデバッグするための設定オプション](https://docs.aws.amazon.com/ja_jp/toolkit-for-vscode/latest/userguide/serverless-apps-run-debug-config-ref.html)
[Stack Over Flow - How to debug in VS Code a local AWS Lambda function with API Gateway written in TypeScript?](https://stackoverflow.com/questions/65197581/how-to-debug-in-vs-code-a-local-aws-lambda-function-with-api-gateway-written-in)

[msdn - Visual Studio で JavaScript アプリまたは TypeScript アプリをデバッグする](https://docs.microsoft.com/ja-jp/visualstudio/javascript/debug-nodejs?view=vs-2019#troubleshooting_source_maps)
[AWS - sam local start-api](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-start-api.html)
[3step](https://levelup.gitconnected.com/how-to-use-typescript-for-aws-lambda-in-3-steps-1996243547eb)
[設定なしでデバッグ実行](https://dev.classmethod.jp/articles/step-execution-typescript-by-vscode/)
[esbuild](https://esbuild.github.io/content-types/#direct-eval)