使用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

部署过程的步骤

大致上将部署处理总结如下。

    1. 在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
广告
将在 10 秒后关闭
bannerAds