概要
aws-cdkでもlambda layersが扱えることを確認する。
コードの共通化を試す。
ソースコード
node_modulesのLayer化
必要なことは以下。
- node_modulesフォルダをLayerのフォルダに指定する
- 実行する上では
devDependency
はいらないので、dependency
のみを含んだnode_modulesにする
- 実行する上では
- ビルド時にnode_modulesのファイルはバンドルしないようにする
dev
(ローカルで確認)やdeploy
の前に、Layerのフォルダを作成するスクリプトpre.ts
を実行するようにする。npmのルールで、preスクリプト名
で定義したスクリプトは、目的のスクリプトの実行前に行うようになるのでそれを利用する。
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"
}
}
cdk/lib/process/pre.ts
import { bundleNpm } from "./setup";
// 実行
bundleNpm();
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
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));
}
}
ユーティリティのLayer化
/opt/nodejs/配下にレイヤーのパスが通ることを利用する。
トランスパイルが通るよう、tsconfigとesbuildの設定を行う。
tsconfig.json
{
"compilerOptions": {
// 省略
"baseUrl": ".",
"paths": {
+ "/opt/nodejs/*": ["src/common/*"],
"@/*" : ["./src/*"]
}
}
}
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
利用の仕方は以下。
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 を上手く管理する
How to Split TypeScript-Based Lambda Functions into Lambda Layers