使用AWS CDK的ILocalBundling来构建Lambda函数和Layer
总结
用AWS CDK构建Lambda函数和Layer。
我想要实现的是,
-
- FunctionのソースはTypeScriptで書く
-
- Functionにはnpmモジュールを含めない
-
- Layerはnpmモジュールのみ含める
-
- 開発中は node_modules を参照/解決できるようにする
-
- 事前準備のためのスクリプトを用意したくない
-
- 設定ファイルでパスをゴニョゴニョしたくない
- AWS CDK標準の仕組みを使う(FunctionとLayerへのバンドリングの話)
在中国,只需要一种选择进行同义转述,原文为:没有做的事情 / 没有考虑的事情是什么。
-
- Layerにユーザー定義のモジュールを含める
sam local invoke で動作すること
bash の動かない環境でのローカルバンドリング
文件结构
最終的檔案架構如下所示(僅顯示主要檔案)。
在 project/package.json 的 devDependencies 中包含了各種型別定義和開發時所需的內容。請先安裝 esbuild,以便於後述的 NodejsFunction 在本地執行 bundling。
project/src/lambda/package.json 僅包含需要包含在 Lambda Layer 中的模組。
project/
├── bin/
│ └── app.ts
├── lib/
│ └── stack.ts
├── src/
│ └── lambda/
│ ├── handlers/
│ │ └── func1/
│ │ └── main.ts // ← ./libなどあれば、それも含めてLambda Functionにバンドルされる
│ ├── package-lock.json
│ └── package.json // ← こいつの dependencies をLambda Layerに含める
├── test/
│ └── app.test.ts
├── cdk.json
├── package-lock.json
├── package.json // ← AWS CDKとか型定義とか開発時に必要なもの
└── tsconfig.json
部署过程的步骤
大致上将部署处理总结如下。
-
- 在AWS CDK的处理过程中调用用于捆绑处理的shell脚本
使用project/src/lambda/package.json 执行npm ci命令
如果无法在本地进行捆绑处理,则尝试使用Docker进行捆绑处理
如果没有Docker镜像,就拉取并执行npm ci命令
捆绑结果将输出到project/cdk.out目录下
函数的捆绑处理
显式排除包含在Layer中的模块(package.json的依赖项)
将TypeScript转译为JavaScript
捆绑结果将输出到project/cdk.out目录下
从函数中引用Layer
执行。
层级 jí)
分层
层次 cì)
纵深
首先创建一个图层。
整体情感如下。
import * as lambda from "aws-cdk-lib/aws-lambda";
:
const layer = new lambda.LayerVersion(this, 'Layer', {
layerVersionName: layerName,
code: lambda.Code.fromAsset('src/lambda', {
bundling: {
local: new LocalBundler(path.resolve(__dirname, '../src/lambda')),
image: lambda.Runtime.NODEJS_16_X.bundlingImage,
command: [
'bash',
'-c',
[
'mkdir -p /asset-output/nodejs',
'cp package.json package-lock.json /asset-output/nodejs/',
'npm ci --omit=dev --prefix /asset-output/nodejs',
].join(' && '),
],
user: 'root',
},
}),
compatibleRuntimes: [lambda.Runtime.NODEJS_16_X],
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
本地的捆绑处理
本地指定的LocalBundler是一个实现了ILocalBundling接口中的tryBundle方法的类。这个类用于在本地进行捆绑处理。
local: new LocalBundler(absolutePath),
尝试实现tryBundle,并执行bundle.sh。如果失败,则返回false,回退到Docker中。
参数outputDir将传递一个类似于project/cdk.out/asset.xxx的路径。
期望Lambda Layer的结构类似于node_modules/nodejs,因此将输出结果放在project/cdk.out/asset.xxx/nodejs下。
一旦输出完成,剩下的事情就交给AWS CDK处理。
import { BundlingOptions, Duration, ILocalBundling } from 'aws-cdk-lib';
:
export class LocalBundler implements ILocalBundling {
:
tryBundle(outputDir: string, options: BundlingOptions): boolean {
try {
const result = execFileSync(path.resolve(__dirname, 'bundle.sh'), {
env: {
PATH: process.env.PATH,
BUNDLER_OUTPUT_DIR: outputDir,
BUNDLER_ROOT_DIR: path.resolve(__dirname, '../src/lambda'),
},
timeout: Duration.seconds(60).toMilliseconds(),
encoding: 'utf-8',
});
return true;
} catch (e: any) {
console.error(e);
return false;
}
}
}
bundle.sh在开发过程中被拆分成了一个单独的文件,以便进行单元测试确认。
在这里执行了npm ci,只做了这一件事。
output_dir="$BUNDLER_OUTPUT_DIR"
if [ -z "$output_dir" ]; then
echo '$BUNDLER_OUTPUT_DIR is empty' 1>&2
exit 1
fi
output_dir="$output_dir/nodejs"
mkdir -p "$output_dir"
root_dir="$BUNDLER_ROOT_DIR"
if [ -z "$root_dir" ]; then
echo '$BUNDLER_ROOT_DIR is empty' 1>&2
exit 1
elif [ ! -d "$root_dir" ]; then
echo '$BUNDLER_ROOT_DIR is not found' 1>&2
exit 1
fi
cd "$root_dir"
cp package.json package-lock.json "$output_dir"
npm ci --omit=dev --prefix "$output_dir"
使用Docker的捆绑处理
如果本地无法进行捆绑处理,则回退到使用Docker进行捆绑处理。
具体来说,当tryBundle返回false时,将会回退。
由于image是必需的,只能通过指定来解决。
由于需要在/asset-output/目录下输出,所以要像在本地捆绑处理时一样,在/asset-output/nodejs目录下进行输出。
我认为lambda.Code.fromAsset(‘src/lambda’)指定的路径被挂载到/asset-input,并成为当前目录。
image: lambda.Runtime.NODEJS_16_X.bundlingImage,
command: [
'bash',
'-c',
[
'mkdir -p /asset-output/nodejs',
'cp package.json package-lock.json /asset-output/nodejs/',
'npm ci --omit=dev --prefix /asset-output/nodejs',
].join(' && '),
],
user: 'root',
功能
使用lambda.Function会导致以TypeScript的形式进行部署,因此我们需要使用针对特定语言的模块。
对于TypeScript,可以使用aws-cdk-lib/aws-lambda-nodejs中的NodejsFunction。
除了捆绑处理之外,其余用法与lambda.Function相同。
const fn = new NodejsFunction(this, 'Function', {
entry: `src/lambda/handlers/func1/main.ts`,
handler: 'handler',
bundling: {
externalModules: external,
},
runtime: lambda.Runtime.NODEJS_16_X,
layers: [layer],
});
在bundle的时候,需要指定esbuild的选项。
在externalModules中,用数组指定要排除在bundle之外的模块。
这里我们通过external将Layer中包含的模块排除掉。
排除的模块列表是从src/lambda/package.json的dependencies中获取的。
bundling: {
externalModules: external,
},
可以指定之前创建的图层。
也可以使用 fn.addLayers(layer)。
layers: [layer],
执行
我建議先在本地執行捆綁處理,試試看。
由於在 bundle.sh 文件中設置了 set -x,所以會顯示各種信息。
使用 Docker 進行捆綁處理可能需要第一次拉取,而且第二次及以後也需要一定的時間。
如果環境沒有問題,我認為在本地進行捆綁處理是最快的方式。
$ cdk synth
Bundling asset xxx/Layer/Code/Stage...
+ output_dir=/xxx/project/cdk.out/asset.xxx
+ '[' -z /xxx/cdk.out/asset.xxx ']'
+ output_dir=/xxx/cdk.out/asset.xxx/nodejs
+ mkdir -p /xxx/cdk.out/asset.xxx/nodejs
+ root_dir=/xxx/src/lambda
+ '[' -z /xxx/src/lambda ']'
+ '[' '!' -d /xxx/src/lambda ']'
+ cd /xxx/src/lambda
+ cp package.json package-lock.json /xxx/cdk.out/asset.xxx/nodejs
+ npm ci --omit=dev --prefix /xxx/cdk.out/asset.xxx/nodejs
Bundling asset xxx/Function/Code/Stage...
cdk.out/bundling-temp-xxx/index.js 1.0kb
⚡ Done in 5ms
Successfully synthesized to /xxx/cdk.out