cdkv2を使って別Stackで作ったlambda layerをlambdaに適用したメモ

## 概要
[前回](https://qiita.com/hibohiboo/items/960f713897cfc3b41501)のLambda群はlayerを使用しておらず、無駄なbundleが発生している。
また、同一stackでlambda layer を作成すると、毎回作り直しになるのも無駄である。
今回は、別StackでLambdaLayerを作成し、SSMにarnを保存して使用する方法をとる。

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/tree/id/7)

## 環境

+ Windows10
+ gitbash
+ aws-cli/2.7.21 Python/3.9.11 Windows/10 exe/AMD64 prompt/off
* cdk v2.35
* node v16.16.0

## ディレクトリ構成

```yaml
- package.json # lambdaで使用するライブラリを定義
- cdk
  - .env         # 環境変数定義
  - package.json # スクリプト定義
  - bin
    - lambda-layer-version-deploy.ts # lambda layerのデプロイ
    - lambdaWithCognito.ts           # lambdaのデプロイ
  - lib
    - lambda-layers-stack.ts
    - LambdaWithCognitoStack.ts
    - process
      - pre.ts
      - setup.ts
```

## ソース
### スクリプト
```diff:cdk/package.json
{
  "scripts": {
    "createLayer": "ts-node lib/process/pre.ts",
+    "create-layer-for-lambda-with-cognito-deploy": "npm run createLayer && npm run copyData && cdk -a \"npx ts-node --prefer-ts-exts bin/lambda-layer-version-deploy.ts\" deploy  -c @aws-cdk/core:newStyleStackSynthesis=true --profile hoge",
    "lambda-with-cognito-deploy": "cdk -a \"npx ts-node --prefer-ts-exts bin/lambdaWithCognito.ts\" deploy  -c @aws-cdk/core:newStyleStackSynthesis=true --profile hoge"  
   }
}
```

### lambda_layerのデプロイファイル作成
* lambda layerにデプロイする node_modulesを作成。
  * `npm install --production` で `dependency`のみをインストール

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

```ts
import * as childProcess from 'child_process';
import * as fs from 'fs-extra';
import * as path from 'path'

const nodeModulesPath = './bundle-node_modules';
export const NODE_LAMBDA_LAYER_DIR = path.resolve(process.cwd(), nodeModulesPath);
export const bundleNpm = () => {
  createNodeModules();
};

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}`)

const createNodeModules = () => {
  copyPackageJson();
  childProcess.execSync(`npm install --production`, {
    cwd: getModulesInstallDirName(),
    env: { ...process.env },
  });
}

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

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

```json:package.json
{
  "dependencies": {
    "aws-jwt-verify": "^3.1.0",
    "aws-sdk": "^2.1189.0",
    "date-fns": "^2.29.1",
    "pg": "^8.7.3"
  },
  "devDependencies": {
    "@types/aws-lambda": "^8.10.101",
    "@types/pg": "^8.6.5",
    "esbuild": "^0.14.53",
    "typescript": "^4.7.4"
  }
}
```

* lambda_layerに入っているモジュールをlabdaのバンドル対象から外す設定を記載しておく
```ts:cdk/constants/lambda-layer.ts
export const externalModules = [
  'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
  'date-fns', // Layrerに入れておきたいモジュール
  'pg',
  'aws-jwt-verify'
]
```

### lambda layerのデプロイ
* SSMのkeyは任意の名前。今回は`/layer_versions_arn`とした。
 
```.env
# 省略
# lambda layerのarn
SSM_PARAM_KEY_LAYER_VERSIONS_ARN=/layer_versions_arn
```


```ts:cdk/bin/lambda-layer-version-deploy.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as dotenv from 'dotenv'
import * as cdk from 'aws-cdk-lib';
import { LambdaLayersStack } from '../lib/lambda-layers-stack';
dotenv.config()
const envList = [
  'SSM_PARAM_KEY_LAYER_VERSIONS_ARN'
] as const
for (const key of envList) {
  if (!process.env[key]) throw new Error(`please add ${key} to .env`)
}
const processEnv = process.env as Record<typeof envList[number], string>
const app = new cdk.App();
const env = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
}

new LambdaLayersStack(app, 'LambdaLayersStack', {
  ssmKey: processEnv.SSM_PARAM_KEY_LAYER_VERSIONS_ARN,
  env,
});
```

```ts:cdk/lib/lambda-layers-stack.ts
import { Aspects, Stack, StackProps, Tag, Tags } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import { NODE_LAMBDA_LAYER_DIR } from './process/setup';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';

interface LambdaLayersStackProps extends StackProps {
  ssmKey: string
}

export class LambdaLayersStack extends Stack {
  constructor(scope: Construct, id: string, props: LambdaLayersStackProps) {
    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]
      }
    );

    // Lambda Layer参照用にarnを保存
    const layerArnParameter = new StringParameter(this, "ssm-layer-version", {
      parameterName: props.ssmKey,
      stringValue: nodeModulesLayer.layerVersionArn,
      description: 'layer version arn for lambda'
    });
    Tags.of(layerArnParameter).add('Name', 'ssm-layer-version');

    Aspects.of(this).add(new Tag('Stack', id));
  }
}
```


### lambdaのデプロイ

```diff:cdk/bin/lambdaWithCognito.ts
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { LambdaWithCognitoStack } from '../lib/LambdaWithCognitoStack'
import * as dotenv from 'dotenv'

// 依存関係: lambda-layer-vesion-deploy
// create-layer-for-lambda-with-cognito-deploy を事前に行い、Lambda LayerのArnをSSMに保存していること

dotenv.config()
const envList = [
  'PROJECT_ID',
  'DOMAIN_PREFIX',
  'CALLBACK_URLS',
  'LOGOUT_URLS',
  'FRONTEND_URLS',
+  'SSM_PARAM_KEY_LAYER_VERSIONS_ARN'
] as const
for (const key of envList) {
  if (!process.env[key]) throw new Error(`please add ${key} to .env`)
}
const processEnv = process.env as Record<typeof envList[number], string>

const app = new cdk.App()
const env = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
}
const projectId = processEnv.PROJECT_ID

new LambdaWithCognitoStack(app, `${projectId}-lambda-with-cognito-stack`, {
  projectId,
  domainPrefix: processEnv.DOMAIN_PREFIX,
  callbackUrls: processEnv.CALLBACK_URLS.split(','),
  logoutUrls: processEnv.LOGOUT_URLS.split(','),
  frontendUrls: processEnv.FRONTEND_URLS.split(','),
+  ssmKeyForLambdaLayerArn: processEnv.SSM_PARAM_KEY_LAYER_VERSIONS_ARN,
  env
})
```

```diff:cdk/lib/LambdaWithCognitoStack.ts
import * as lambda from 'aws-cdk-lib/aws-lambda'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
+ import * as ssm from 'aws-cdk-lib/aws-ssm';
+ import { externalModules } from '../constants/lambda-layer'
// 省略
interface Props extends StackProps {
  projectId: string
  domainPrefix: string
  frontendUrls: string[]
  callbackUrls: string[]
  logoutUrls: string[]
+  ssmKeyForLambdaLayerArn: string
}

export class LambdaWithCognitoStack extends Stack {
  constructor(scope: Construct, id: string, props: Props) {
    // 省略
+    const lambdaLayerArn = ssm.StringParameter.valueForStringParameter(this, props.ssmKeyForLambdaLayerArn);
    const baseFunctionProps = {
      runtime: lambda.Runtime.NODEJS_14_X,
+      layers: [
+        lambda.LayerVersion.fromLayerVersionArn(this, 'node_modules-layer', lambdaLayerArn)
+      ],
+      bundling: {
+        externalModules
+      }
    }
    const handler = new NodejsFunction(
      this,
      `${props.projectId}-hello-lambda`,
      {
        ...baseFunctionProps,
        entry: '../src/handler/api/hello.ts',
        functionName: 'hello',
        description: 'ハロー',
      },
    )

```

## 実行
gitbashで以下のコマンドを実行。

```
npm run create-layer-for-lambda-with-cognito-deploy
npm run lambda-with-cognito-deploy
```

## 確認
layerが設定された。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/12f52069-9923-bcd5-5e82-0f99efd348c5.png)

## 複数Layerの適用
node_modulesが2つになるが、マージされて普通に使用可能。
今回は、検証のみに必要なモジュールがあるため、それだけ別レイヤに分けて試してみた。

[変更コミット](https://github.com/hibohiboo/aws-cdk-v2/commit/84de7adbe6a42fccb1387920fcf9333921547118)




## 参考
[AWS CDK - Lambda への Layer（ARN）のアタッチ](https://qiita.com/takmot/items/3e679cfcf520c0cc565e)