AWS CDKでLambda Layerを設定しLambdaから読み出してローカル実行を行ったメモ

## 概要
[前回](https://qiita.com/hibohiboo/items/e8eeced37c40d0792df5)にLambdaの実行を確認できた。
生成物を確認し、必要とするモジュールもバンドルされていることも確認できた。
Lambda間で共通のモジュールを使う場合、すべてのLambdaにバンドルするのは無駄である。（容量も重くなるしバンドル時間もかかる）
AWSにはLambda Layerという仕組みがあるのでそれを使って解決する。

[AWS CDK を使って node_modules を AWS Lambda Layers にデプロイするサンプル][*1]を見れば実現できる。
ただし、Windowsで動かすときに少しハマったので備忘録を残す。

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


## やること
SAMを使ったローカル実行

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

## ソースコード修正

### ビルド設定

* Lambda Layerにいれる成果物をバンドルのときに作成できるように pre-prrocess追加

```diff: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';


+ // pre-process
+ bundleNpm();

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

```


### Stackの記述修正
* stackにLamba Layerの作成を記述する

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


+ import { NODE_LAMBDA_LAYER_DIR } from './process/setup';

export class SampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.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]
+      }
+    );

    new NodejsFunction(this, 'test', {
      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"')), 
        },
      },
+      layers: [nodeModulesLayer],
    });

  }
}

```



<details><summary>全文</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';
import { NODE_LAMBDA_LAYER_DIR } from './process/setup';
export class SampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new NodejsFunction(this, 'hello', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/hello.ts',
      functionName: 'kotahello',
      handler: 'lambdaHandler'
    });

    const nodeModulesLayer = new lambda.LayerVersion(this, 'NodeModulesLayer',
      {
        code: lambda.AssetCode.fromAsset(NODE_LAMBDA_LAYER_DIR),
        compatibleRuntimes: [lambda.Runtime.NODEJS_14_X]
      }
    );

    new NodejsFunction(this, 'test', {
      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"')), // バグってそう.二重でstringifyしないとInvalid define valueのエラー
        },
      },
      layers: [nodeModulesLayer],
    });

  }
}
```

</div></details>

### Layerの成果物作成
* pre-prrocessで呼び出される
  * Lambda Layerにアップロードするディレクトリの作成
  * package.json, package-lock.jsonのコピー
  * `npm install`でnode_modulesディレクトリを作成
* [参考][*1]の`npm --prefix ${getModulesInstallDirName()} install --production`だとnpm installされずにシンボリックリンクが作成されてしまった
  * 以下のようにcwdを使ってディレクトリを指定するよう修正して対応


```ts:lib/process/setup.ts
  // install package.json (production)
  childProcess.execSync(`npm  install --production`, {
    cwd: getModulesInstallDirName(),
    // bundle時にパイプで出力するtemplate.yamlに、余分な文字列が含まれてしまわないように出力はオフ
    stdio: ['ignore', 'ignore', 'ignore'],
    env: { ...process.env },
    shell: 'bash'
  });
```

<details><summary>全文</summary><div>


```ts:lib/process/setup.ts
#!/usr/bin/env node
import * as childProcess from 'child_process';
import * as fs from 'fs-extra';
import * as path from 'path'

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

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

  // install package.json (production)
  childProcess.execSync(`npm  install --production`, {
    cwd: getModulesInstallDirName(),
    // bundle時にパイプで出力するtemplate.yamlに、余分な文字列が含まれてしまわないように出力はオフ
    stdio: ['ignore', 'ignore', 'ignore'],
    env: { ...process.env },
    shell: 'bash'
  });
};

const copyPackageJson = () => {

  // copy package.json and package.lock.json
  fs.mkdirsSync(getModulesInstallDirName());
  ['package.json', 'package-lock.json']
    .map(file => fs.copyFileSync(srcFilePath(file), distFilePath(file)));

};

const getModulesInstallDirName = (): string => {
  return runtimeDirName;
};
```

</div></details>

## 実践

* 動いた
* Build imageが大分長くなった気がする。。。
  * 2回目からはそこまででもないかも

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/e9636fc4-6f62-349b-18d1-5d45d3fd4c06.png)


## 参考
[AWS CDK を使って node_modules を AWS Lambda Layers にデプロイするサンプル][*1]
[aws lambda layer](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html)

[*1]:https://dev.classmethod.jp/articles/aws-cdk-node-modules-lambda-layer/)