Prisma + zod を使用したLambdaのファイルサイズ肥大を抑えるために Lambda Layersを利用したメモ

## 概要

[前回](https://qiita.com/hibohiboo/items/ed08b21799dd92bedc3e)に作成したファイルだと、バンドル時にハンドラファイルにスキーマファイルを含めてしまい、サイズが肥大化する問題があった。

今回、Lambda Layersを使用し、スキーマファイルを外だしできるようにする。
[/opt/nodejs/ にレイヤーのパスが通る][*1]ため、そこにスキーマファイルを入れるように設定した。

[ソースコード](https://github.com/hibohiboo/kartagraph-monorepo/tree/e48a851f4eef7dfeb5b7410b8b8432505d816af0/)



## ソースの変更
### Lambdaの定義ファイル
tsconfigを修正し、ソースを直接読みにいっていた部分を、デプロイ後のパスである`/opt/nodejs/`以下のフォルダを見に行くようにする

```diff:backend/tsconfig.json
    "paths": {
      "@kartagraph-backend/*": ["./src/*"],
      "@kartagraph-types/*": ["../types/src/*"],
-      "@kartagraph-prisma-zod": ["../prisma/generated/zod/index.ts"],
+      "/opt/nodejs/*": ["../prisma/generated/zod/*"],
    }
```

```diff:backend/src/domain/scenario/scenarioRepository.ts
import { execQuery } from '@kartagraph-backend/utils/repository';
- import { scenario_listSchema } from '@kartagraph-prisma-zod';
+ import { scenario_listSchema } from '/opt/nodejs/index';
import { ScenarioListItem } from '@kartagraph-types/index';

export const getScenarioList = async (): Promise<ScenarioListItem> => {
```

### スキーマファイルを個別でバンドルする

```diff:prisma/package.json
{
  "name": "@kartagraph/prisma-zod",
  "scripts": {
+    "bundle": "esbuild --bundle generated/zod/index.ts --outfile=dist/index.js --target=node20 --format=cjs",
    "generate": "prisma generate",
    "pull": "prisma db pull"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@prisma/client": "^5.10.2",
    "zod": "^3.22.4"
  },
  "devDependencies": {
+    "esbuild": "^0.20.1",
    "prisma": "^5.10.2",
    "zod-prisma-types": "^3.1.6"
  }
}
```

### バンドルしたファイルを含めたLayerを作成

```diff:infrastracture/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 nodeModulesPath = './bundle-node_modules';
export const NODE_LAMBDA_LAYER_DIR = path.resolve(process.cwd(), nodeModulesPath);
+ const zodSchemaPath = '../prisma/dist';
+ const commonModulesPath = './bundle-common';
+ 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()}/../backend/${file}`);

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

const createNodeModules = () => {
  copyPackageJson();

  childProcess.execSync(`npm install --omit=dev`, {
    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;
};
+ const copyCommonModules = () => {
+   const dist = path.resolve(process.cwd(), `${commonModulesPath}/${NODE_LAMBDA_LAYER_RUNTIME_DIR_NAME}`);
+   const src = path.resolve(`${process.cwd()}/${zodSchemaPath}`);
+   fs.mkdirsSync(`${commonModulesPath}`);
+   fs.copySync(src, dist);
+ };
```

```diff:infrastracture/bin/lambda-layer-version-deploy.ts
// 省略

bundleNpm();

new LambdaLayersStack(app, `${processEnv.PROJECT_ID}-LambdaLayersStack`, {
  ssmKey: `${processEnv.SSM_PARAM_KEY_LAYER_VERSIONS_ARN}-${processEnv.PROJECT_ID}`,
+  commonSsmKey: `${processEnv.SSM_PARAM_KEY_LAYER_VERSIONS_ARN}-${processEnv.PROJECT_ID}-common`,
  env,
});

```

```diff:infrastracture/lib/lambda-layer-stack.ts
// 省略

interface LambdaLayersStackProps extends StackProps {
  ssmKey: string;
+  commonSsmKey: 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: [RUNTIME_VERSION],
    });
    // 省略

+    const commonModulesLayers = new lambda.LayerVersion(this, 'CommonModulesLayer', {
+      code: lambda.AssetCode.fromAsset(COMMON_LAMBDA_LAYER_DIR),
+      compatibleRuntimes: [RUNTIME_VERSION],
+    });
+    const commonArnParameter = new StringParameter(this, 'ssm-layer-version-common', {
+      parameterName: props.commonSsmKey,
+      stringValue: commonModulesLayers.layerVersionArn,
+      description: 'layer version arn for common lambda',
+    });

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

```diff:infrastracture/package.json
{
  // 省略
  "scripts": {
    // 省略
+    "predeploy-layer": "cd ../prisma && npm run bundle",
    "deploy-layer": "cdk -a \"npx ts-node --prefer-ts-exts bin/lambda-layer-version-deploy.ts\" deploy --all",
    "deploy-rest": "cdk -a \"npx ts-node --prefer-ts-exts bin/rest.ts\" deploy --all"
  },
  // 省略
}
```

### スキーマファイル入りのレイヤーを使用するようにする

```diff:infrastracture/lib/rest-stack.ts
// 省略

const bundling = {
  externalModules: [
                     '@neondatabase/serverless'
                   , '@aws-sdk/client-s3'
+                   , '/opt/nodejs/*'
                   ],
};

interface Props extends core.StackProps {
  projectId: string;
  ssmLambdaLayerKey: string;
+  ssmLambdaCommonLayerKey: string;
  ssmAPIGWUrlKey: string;
  apiVersion: string;
  neonEndpoint: string;
  cloundFrontDomain: string;
  mediaBucketName: string;
}

// 省略
export class KartaGraphRESTAPIStack extends core.Stack {
  private apiRoot: apigateway.Resource;
  private resourceMap = new Map<string, apigateway.Resource>();

  constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props);
    // 省略

    const defaultLambdaProps = this.createLambdaProps({
      ssmKeyForLambdaLayerArn: props.ssmLambdaLayerKey,
+      ssmKeyForLambdaCommonLayerArn: props.ssmLambdaCommonLayerKey,
      environment: { ...commonEnv },
      timeoutSec: 5, // 外部エンドポイントを経由してJSONを処理するため3秒では足りない
    });

    // 省略
  }

  // 省略

  private createLambdaProps(props: {
    ssmKeyForLambdaLayerArn: string;
+    ssmKeyForLambdaCommonLayerArn: string;
    environment?: Record<string, string>;
    initialPolicy?: iam.PolicyStatement[];
    timeoutSec?: number;
  }) {
    const lambdaLayerArn = StringParameter.valueForStringParameter(this, props.ssmKeyForLambdaLayerArn);
+    const commonLambdaLayerArn = StringParameter.valueForStringParameter(this, props.ssmKeyForLambdaCommonLayerArn);

    const layers = [
      LayerVersion.fromLayerVersionArn(this, 'node_modules-layer', lambdaLayerArn),
+      LayerVersion.fromLayerVersionArn(this, 'common-layer', commonLambdaLayerArn),
    ];

    // 同じStack上でLayerVersionを作っていない場合、cdk synthで sam local 実行用のoutputを作るときにレイヤーを使うとエラーになる。
    const layerSettings = !!process.env['CDK_SYNTH'] ? {} : { bundling, layers };

    return {
      runtime: RUNTIME_VERSION,
      ...layerSettings,
      environment: props.environment,
      initialPolicy: props.initialPolicy,
      timeout: props.timeoutSec ? core.Duration.seconds(props.timeoutSec) : undefined,
    };
  }
  // 省略
}
```


## 参考
[cdkv2を使って別Stackで作ったlambda layerをlambdaに適用したメモ](https://qiita.com/hibohiboo/items/c83a932f28f8d7b49e24)
[aws-cdk v2でlambda layers を扱ったメモ][*1]
[dbマイグレーションの型付けのファイル容量を減らす](https://github.com/hibohiboo/kartagraph-monorepo/issues/27)
[Deploying Prisma with AWS Lambda Layers in CDK](https://medium.com/ama-tech-blog/deploying-prisma-with-aws-lambda-layers-in-cdk-10608f5598c5)

[*1]:https://qiita.com/hibohiboo/items/b33c530a5a3e8a9c7411