aws-cdk v2でlambda layers を扱ったメモ

## 概要
aws-cdkでもlambda layersが扱えることを確認する。
コードの共通化を試す。
[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/tree/e32dc9658f313e651cf69fe82dd219891beb0efb)

## node_modulesのLayer化
必要なことは以下。

* node_modulesフォルダをLayerのフォルダに指定する
  * 実行する上では`devDependency`はいらないので、`dependency`のみを含んだnode_modulesにする
* ビルド時にnode_modulesのファイルはバンドルしないようにする


<details><div>

`dev`(ローカルで確認)や`deploy`の前に、Layerのフォルダを作成するスクリプト`pre.ts`を実行するようにする。npmのルールで、`preスクリプト名`で定義したスクリプトは、目的のスクリプトの実行前に行うようになるのでそれを利用する。

```json:cdk/package.json
{
  "scripts": {
    "createLayer": "ts-node lib/process/pre.ts",
    "predev": "npm run createLayer",
    "dev": "sam-beta-cdk local start-api",
    "predeploy": "npm run createLayer",
    "deploy": "cdk deploy -c @aws-cdk/core:newStyleStackSynthesis=true --profile produser"
  }
}
```


```ts:cdk/lib/process/pre.ts
import { bundleNpm } from "./setup";

// 実行
bundleNpm();
```



```ts:cdk/lib/process/setup.ts
import * as childProcess from 'child_process';
import * as fs from 'fs-extra';
import * as path from 'path'

const nodeModulesPath = './bundle-node_modules';
const commonModulesPath = './bundle-common';
export const NODE_LAMBDA_LAYER_DIR = path.resolve(process.cwd(), nodeModulesPath);
export const COMMON_LAMBDA_LAYER_DIR = path.resolve(process.cwd(), commonModulesPath);

const NODE_LAMBDA_LAYER_RUNTIME_DIR_NAME = `nodejs`;
const runtimeDirName = path.resolve(process.cwd(), `${nodeModulesPath}/${NODE_LAMBDA_LAYER_RUNTIME_DIR_NAME}`);
const distFilePath = (file: string) => path.resolve(process.cwd(), `${nodeModulesPath}/${NODE_LAMBDA_LAYER_RUNTIME_DIR_NAME}/${file}`)
const srcFilePath = (file: string) => path.resolve(`${process.cwd()}/../${file}`)

export const bundleNpm = () => {
  createNodeModules();
};


const createNodeModules = () => {
  // create bundle directory
  copyPackageJson();

  // install package.json (production)
  childProcess.execSync(`npm install --production`, {
    cwd: getModulesInstallDirName(),
    env: { ...process.env },
  });
}


const copyPackageJson = () => {
  fs.mkdirsSync(runtimeDirName);
  ['package.json']
    .map(file => fs.copyFileSync(srcFilePath(file), distFilePath(file)));
};

```

externalModulesでnode_modulesで管理するスクリプトをバンドルに含めないようにする。今回の場合だと、`date-fns`

```ts:cdk/lib/cdk-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import { LambdaIntegration, RestApi } from 'aws-cdk-lib/aws-apigateway';
import { COMMON_LAMBDA_LAYER_DIR, NODE_LAMBDA_LAYER_DIR } from './process/setup';

export class CdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const nodeModulesLayer = new lambda.LayerVersion(this, 'NodeModulesLayer',
      {
        code: lambda.AssetCode.fromAsset(NODE_LAMBDA_LAYER_DIR),
        compatibleRuntimes: [lambda.Runtime.NODEJS_14_X]
      }
    );

    const helloLambda = new NodejsFunction(this, 'helloLambda', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: `../src/handler/api/hello.ts`,
      layers: [nodeModulesLayer],
      bundling: {
        externalModules: [
          'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
          'date-fns', // Layrerに入れておきたいモジュール
        ],

      }
    });
    const api = new RestApi(this, 'ServerlessRestApi', { cloudWatchRole: false });
    api.root.addResource('hello').addMethod('GET', new LambdaIntegration(helloLambda));
  }
}
```


</div></details>


## ユーティリティのLayer化
[/opt/nodejs/](https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-import-module-error-nodejs/)配下にレイヤーのパスが通ることを利用する。
トランスパイルが通るよう、tsconfigとesbuildの設定を行う。

```diff:tsconfig.json
{
  "compilerOptions": {
    // 省略
    "baseUrl": ".",
    "paths": {
+      "/opt/nodejs/*": ["src/common/*"],
      "@/*" : ["./src/*"]
    }
  }
}
```

```diff:cdk/lib/cdk-stack.ts
// 省略
    const helloLambda = new NodejsFunction(this, 'helloLambda', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: `../src/handler/api/hello.ts`,
      layers: [nodeModulesLayer, commonModulesLayers],
      bundling: {
        externalModules: [
          'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
          'date-fns', // Layrerに入れておきたいモジュール
+          '/opt/nodejs/*'
        ],

      }
    });
// 省略
```

このときのフォルダ構造は以下となっている

```
- tsconfig
- src
  - common
    - utils
      - index.ts
    - index.ts
  - handler
    - api
      - hello.ts
```

利用の仕方は以下。

```diff:src/handler/api/hello.ts
import { APIGatewayProxyHandler } from 'aws-lambda';
import { format } from 'date-fns';
- import { formatDate } from '@/common/index';
+ import { formatDate } from '/opt/nodejs/index';

export const handler: APIGatewayProxyHandler = async (event) => {
  const datetime = formatDate(new Date(event.requestContext.requestTimeEpoch))
  return {
    'statusCode': 200,
    'body': JSON.stringify({
      message: `hello world. ${datetime} = ${event.requestContext.requestTimeEpoch}. now: ${format(new Date(), 'yyyy-MM-dd HH:mm:ss.SSS')}`,
    })
  };
};
```



## 参考

[AWS CDK + Typescript 環境で lambda layer を上手く管理する](https://qiita.com/hey3/items/b4032841b01e96b75e3e)
[How to Split TypeScript-Based Lambda Functions into Lambda Layers](https://www.qloudx.com/how-to-split-typescript-based-lambda-functions-into-lambda-layers/)