在 AWS CDK + Typescript 环境中有效地管理 Lambda Layer
使用AWS CDK开发lambda函数时,我想记录下一种管理lambda层的好方法,以备忘。因为我是初学者,可能还有更好的方法。
动机
import { hoge } from ‘layer’ みたいに lambda layer にしたコードを使いたい
vendor ライブラリも実装者が特に意識せず使いたい(vendor ライブラリも layer にしておく必要があります)
package.json は1つだけで良い
出于解决这个问题,同时也希望简单地为这个环境做准备的背景。
确认操作环境
-
- typescript: 3.8.3
-
- aws-cdk: 1.26.0
- aws-sam-cli: 0.40.0
首先准备一个简单的 lambda 函数。
以下是基于使用 cdk init –language typescript 创建的 cdk 环境的项目。
项目名称:cdk-project
$ tree -I node_modules -L 3
.
├── README.md
├── bin
│ └── cdk-project.ts
├── cdk.json
├── jest.config.js
├── lib
│ └── cdk-project-stack.ts
├── package-lock.json
├── package.json
├── test
│ └── cdk-project.test.ts
└── tsconfig.json
我会创建一个简单的匿名函数来返回“Hello World”。
$ mkdir -p dir lambda/helloWorld
$ vi lambda/helloWorld/index.ts
// lambda/helloWorld/index.ts
import { APIGatewayEvent, Callback, Cotnext } from 'aws-lambda'
exports.handler = async (event: APIGatewayEvent, context: Context, callback: Callback) => {
callback(null, 'Hello World')
}
为了创建 lambda,需要按照以下方式修改 lib/cdk-project-stack.ts。
// lib/cdk-project-stack.ts
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
export class CdkProjectStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const helloWorldLambda = new lambda.Function(this, 'helloWorldLambda', {
code: lambda.Code.fromAsset('lambda/helloWorld'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_12_X
})
}
}
// bin/cdk-project.ts にも書かれているがコード記載省略のため
const app = new cdk.App()
new CdkProjectStack(app, 'HelloWorldCdkApp')
app.synth()
我暂时在这里用 sam 确认一下。
$ yarn build
$ cdk synth
$ sam local invoke -t cdk.out/HelloWorldCdkApp.template.json
Invoking index.handler (nodejs12.x)
Fetching lambci/lambda:nodejs12.x Docker container image......
(省略)
"Hello World"
确认已经收到“Hello World”。
准备lambda层
在部署 lambda 层时,必须遵循以下规则。
-
- zip で S3 にアップロードされている
- zip 展開後の構造が nodejs/node_modules/ となっている
如果使用 aws-cdk,它可以将指定的目录压缩成 zip 并上传到 S3,但是需要我们自己处理 nodejs/node_modules/ 这样的结构。这是个棘手的问题。
我想使用转化为 lambda layer 的代码,就像import { hoge } from ‘layer’一样。
打算做某事
.
├── lambda
│ └── helloWorld
└── layer
如果简单地进行,那么就会变成:“供应商库呢?合并成Lambda层好麻烦。。。”
.
├── lambda
│ └── helloWorld
└── layer
└── nodejs
└── nodejs
└── node_modules
如果这样的话,“哎呀!如果包括供应商的库,我需要在nodejs中准备一个package.json来管理?而且,import语句看起来不太舒服。。怎么办呢?”
供应商库可以无需特别意识到使用(对于aws-cdk 特别需要将供应商库保留为层)
只需要一个 package.json 文件即可
在这一带做得不好,变得很烦恼了。
通过采用以下结构,问题得到了解决。(我是用上述第一种方法)
-
- 在layer文件夹以下管理独自共通化处理
-
- 在root的dependencies中添加layer文件夹的路径
-
- 在root的dependencies中添加要用作lambda层的vendor库
- 在root的dependencies中以layer.out/nodejs/node_modules/的结构添加指定的库,并进行yarn安装。
如果你认为 layer.out/nodejs/node_modules/ 这个文件夹有膨胀的情况,建议你考虑将其中应该移至 devDependencies 的内容移动过去。
在layer文件夹中管理单独的共同处理。
.
├── lambda
│ └── helloWorld
└── layer
└── utils
│ └── index.ts
├── ...
└── index.ts
在图层中添加一个简单的函数,用来对数值进行加法运算。
// layer/utils/index.ts
export const sum = (num1: number, num2: number): number => num1 + num2
在 layer 层下管理通用处理并且在 layer/index.ts 文件中进行集中导出。
// layer/index.ts
export * from './utils'
...
在root的依赖项中使用路径指定方式添加layer目录。
修改 package.json 如下所示。
{
...
"dependencies": {
"layer": "file:./layer"
}
}
在root的dependencies中添加想要作为Lambda层使用的供应商库。
为了做一个供应商代码的例子,我们还可以在依赖中添加dayjs。
{
...
"dependencies": {
"dayjs": "^1.8.22",
"layer": "file:./layer"
}
}
4. 将根目录的 package.json 文件中指定的依赖库以 layer.out/nodejs/node_modules/ 结构的方式进行 yarn install 安装。
创建一个预处理脚本,用于按照指定的结构来安装在dependencies中指定的库。
// lib/layerSetup.ts
import * as childProcess from 'child_process'
const LAMBDA_LAYER_DIR_NAME = './layer.out/nodejs/node_modules/'
export const bundleLayer = () => childProcess.execSync(`yarn install --production --modules-folder ${LAMBDA_LAYER_DIR_NAME}`)
// bin/cdk-project.ts
import ...
import { bundleLayer } from '../lib/layerSetup'
// pre-process
bundleLayer()
const app = new cdk.App()
new CdkProjectStack(app, 'HelloWorldCdkApp')
只需要在使用tsc进行转译之前升级layer,这样当更新layer时也可以轻松地进行反映,因此可以npm脚本。
{
...
"scripts": {
"build": "yarn upgrade layer && tsc",
...
}
}
作为一个选项,看起来不错。
最后将 Lambda 层添加到堆栈中。
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
export class CdkProjectStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const layer = new lambda.LayerVersion(this, 'layer', {
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
code: lambda.Code.fromAsset('layer.out'),
})
const helloWorldLambda = new lambda.Function(this, 'helloWorldLambda', {
code: lambda.Code.fromAsset('lambda/helloWorld'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_12_X,
layers: [layer]
})
}
}
$ yarn build
$ cdk synth
如果这样做,就会在预处理中创建 layer.out/nodejs/node_modules/…,然后将 layer.out 设置为 layer。
用Sam进行Lambda Layer的操作验证
将 helloWorldLambda 更改为使用 lambda layer 的函数。
import { APIGatewayEvent, Callback, Context } from 'aws-lambda'
import * as dayjs from 'dayjs'
import { sum } from 'layer'
dayjs.locale('ja')
exports.handler = async (event: APIGatewayEvent, context: Context, callback: Callback) => {
console.log('Hello World')
console.log(sum(1, 2)) // 3
callback(null, dayjs('2020-01-01'))
}
$ yarn build
$ cdk synth
$ sam local invoke -t ./cdk.out/HelloWorldCdkApp.template.json
Invoking index.handler (nodejs12.x)
layer27F209B1 is a local Layer in the template
Building image...
Requested to skip pulling images ...
(省略)
... INFO Hello World
... INFO 3
(省略)
"2020-01-01T00:00:00.000Z"
出来了。
顺便提一句,在不将供应商代码设为 Lambda 层的情况下
{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'dayjs'\nRequire stack:\n- /var/task/index.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js"}
变得像那样。
总结
-
- dependencies で lambda layer を管理できる体制にする。(dependencies と devDependencies を区別する意識をつけてこう)
-
- preprocess で dependencies を nodejs/node_modules の形式にする。
- npm scripts で synth までのコマンドをあまり意識しなくて良いようにする。
在进行 synth 之前,如果不删除 cdk.out 和 layer.out 等文件,有时候更新可能不会成功,所以最好在 cdk:synth 这样的 npm 脚本中添加清理操作,以确保在 cdk synth 之前执行清理处理。(我还没有研究 aws-cdk 的这方面行为)
如果有更好的建议,请告诉我!