AWS CDKでLambdaのTypescriptをトランスパイルしてローカル実行したメモ

## 概要
AWS CDKでのビルドに苦労したので備忘録。

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

## やること
* AWS CDKを使ったトランスパイル
* SAMを使ったローカル実行
* 設定によってどんなファイルが出るかの確認

## やらないこと
* デプロイ

## 環境
* Windows Home
* docker desktop 3.2.1
* wsl2 ubuntu20
* SAM CLI, version 1.20.0
* aws-cli/2.1.28 Python/3.8.8 Windows/10 exe/AMD64 prompt/off


## 前提知識

* [CDK Workshop][*1]にざっくり目を通していること
* [cdk コマンドの機能を 実際に叩いて理解する][*2]の表をざっくり見ていること
* [AWS CDK Toolkit (cdk command)][*3]をみてaws cdkをインストールできること
* [Windows での AWS SAM CLI のインストール][*4]をできること
  * dockerはローカル実行をするなら必須

## トランスパイル

* `"esbuild": "^0.9.0"`をnpm installすれば、Dockerがなくてもトランスパイル可能

```json:package.json
    "bundle": "cdk synth && cdk synth > cdk.out/template.yaml",
```

* `cdk synth`でデプロイする予定の成果物をローカルに出力できる。
  * template.yamlはローカル実行用
  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/df056873-4d4e-eca0-cca2-0552a50c19ab.png)



## ローカル実行

* cdkで出力したtemplage.yamlを使って、`sam`コマンドでローカル実行
  * `helloHOGEHOGE`はteamplateファイル内から取得すること.`Type: AWS::Lambda::Function`の上。([*][*5])
  * template.yamlは改行コードlfの必要がある。gitbashならlfだが、powershellなどだと` > template.yaml`でcrlfになるので注意
  * gitbashだと`sam`にパスが通っていないので、`sam.cmd`を利用している

```json:package.json
    "localinvoke": "cd cdk.out && sam.cmd local invoke hello26396490--no-event",
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/b945208a-3681-1d3f-1899-ce318a6db7f9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/dd2aeeb9-bb62-d129-3678-da61806ad6c7.png)



## ビルド成果物検討

* トランスパイルされたjsファイルを見ていく。


<details><summary>Stackの定義は長いので折り畳み</summary><div>

```ts:lib/sample-stack.ts

import * as cdk from '@aws-cdk/core';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
import * as lambda from '@aws-cdk/aws-lambda';

export class SampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new NodejsFunction(this, 'test', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/test.ts',
      functionName: 'kotatest'
    });
    new NodejsFunction(this, 'test2', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/test.ts',
      functionName: 'kotatest',
      bundling: {
        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(JSON.stringify('"xxx-xxx"')), // バグってそう
        },
      },
    });
    new NodejsFunction(this, 'hello', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/hello.ts',
      functionName: 'kotahello',
      handler: 'lambdaHandler'
    });
  }
}

```

</div></details>



### シンプルなやつ

* とても素直

```js
var __defProp = Object.defineProperty;
var __markAsModule = (target) => __defProp(target, "__esModule", {value: true});
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, {get: all[name], enumerable: true});
};

// hello.ts
__markAsModule(exports);
__export(exports, {
  lambdaHandler: () => lambdaHandler
});
var lambdaHandler = async (event, context) => {
  console.log(event);
  console.log(context);
  let response = null;
  try {
    response = {
      statusCode: 200,
      body: JSON.stringify({
        message: "hello world"
      })
    };
  } catch (err) {
    console.log(err);
    return err;
  }
  return response;
};
```




### モジュールを利用しているもの

* date-fnsを使ったjsは以下の感じ。date-fnsが埋め込まれて長い。

```js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __markAsModule = (target) => __defProp(target, "__esModule", {value: true});
var __commonJS = (callback, module2) => () => {
// 省略
var __toModule = (module2) => {
  return __exportStar(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? {get: () => module2.default, enumerable: true} : {value: module2, enumerable: true})), module2);
};

// ../../../node_modules/date-fns/_lib/requiredArgs/index.js
// 省略

// ../utilities/dateUtility.ts
var import_format = __toModule(require_format());
var formatISO = (d) => (0, import_format.default)(d, "yyyy-MM-dd'T'HH:mm:ss");

// test.ts
var handler = async (event) => {
  const test = `Hello from Lambda! TS!${formatISO(new Date())}, key: ${JSON.stringify(process.env.API_KEY)}`;
  const response = {
    statusCode: 200,
    body: JSON.stringify(test)
  };
  return response;
};

```

### externalにモジュールを指定した場合

```diff:lib/sample-stack.ts
    new NodejsFunction(this, 'test2', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/test.ts',
      functionName: 'kotatest',
+      bundling: {
+        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(JSON.stringify('{"value":"xxx-xxx"}')), // バグってそう
        },
      },
    });
```


* requireに置き換わってすっきり

```js
// 省略
// ../utilities/dateUtility.ts
var import_date_fns = __toModule(require("date-fns"));
var formatISO = (d) => (0, import_date_fns.format)(d, "yyyy-MM-dd'T'HH:mm:ss");

// test.ts
var handler = async (event) => {
  const test = `Hello from Lambda! TS!${formatISO(new Date())}, key: ${JSON.stringify(define_process_env_API_KEY_default)}`;
  const response = {
    statusCode: 200,
    body: JSON.stringify(test)
  };
  return response;
};
exports.handler = handler;
```



### ビルド時の文字置換を設定したとき

* 2回JSON.stringifyをする必要があり、バグっていそう

```diff:lib/sample-stack.ts
    new NodejsFunction(this, 'test2', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/test.ts',
      functionName: 'kotatest',
      bundling: {
        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(JSON.stringify('"xxx-xxx"')), // バグってそう
+        },
      },
    });
```


* JSON.stringfyを2回しているが、1回だけだと `> error: Invalid define value (must be valid JSON syntax or a single identifier): {value:xxx-xxx}`のエラーがでる

<details><summary>~~エスケープ別解~~ 2021/04/13 1.97.0で使用不可</summary><div>

* 以下でもエスケープできそう [*][*6] 

```
  JSON.stringify(`\\"${'xxx-xxx'}\\"`)
```


トランスパイル前のソースは以下

```ts
  const test: string = `Hello from Lambda! TS!${dateUtility.formatISO(new Date())}, key: ${process.env.API_KEY}`
  const response = {
```

トランスパイル後のソースは以下

```js

// test.ts
var handler = async (event) => {
  const test = `Hello from Lambda! TS!${formatISO(new Date())}, key: ${"xxx-xxx"}`;

```

**※2021/04/13 1.97.0 でできなくなっている**

```
Done in 5ms
Bundling asset SampleStack2021/test/Code/Stage...
 > error: Invalid define value (must be valid JSON syntax or a single identifier): \"xxx-xxx\"

1 error

D:\develop\develop\tutorial\lesson\aws\typescript\projects\cdk_sample\cdk\node_modules\@aws-cdk\core\lib\asset-staging.ts:472
  for (const key of Object.keys(object).sort()) {
            ^
````


</div></details>

## 参考

[CDK Workshop][*1]
[aws cdk](https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/home.html)
[aws cdk / esbuild config](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda-nodejs)
[aws cdk / lambda nodejs ts](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda-nodejs.NodejsFunction.html)

[cdk コマンドの機能を 実際に叩いて理解する 【 AWS CDK Command Line Interface 】][*2]
[AWS CDK で AWS SAM のテンプレートを出力してデプロイする](https://qiita.com/izanari/items/05f601f12ff5b0f32fd7)
[AWS CDK Toolkit (cdk command)][*3]
[SAM Hello World](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html)
[CDKで作ったLambda@Edgeのコードに環境変数を渡せるようにする][*6]
[Cloud Formationでしっておきたかったこと](https://qiita.com/sisisin/items/d9132a2b67106123d35c)


[*6]:https://sunday-morning.app/posts/2020-06-21-cdk-lambda-edge-environment-variables
[*1]:https://cdkworkshop.com/20-typescript/20-create-project/100-cdk-init.html
[*2]:https://dev.classmethod.jp/articles/aws-cdk-command-line-interface/
[*3]:https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/cli.html
[*4]:https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install-windows.html
[*5]:https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/sam.html