18
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LIFULLAdvent Calendar 2022

Day 4

AWS CDKのILocalBundlingでLambda FunctionとLayerを構築する

Last updated at Posted at 2022-12-03

概要

AWS CDKを使ってLambda FunctionとLayerを構築する。
やりたいこととしては、

  • FunctionのソースはTypeScriptで書く
  • Functionにはnpmモジュールを含めない
  • Layerはnpmモジュールのみ含める
  • 開発中は node_modules を参照/解決できるようにする
  • 事前準備のためのスクリプトを用意したくない
  • 設定ファイルでパスをゴニョゴニョしたくない
  • AWS CDK標準の仕組みを使う(FunctionとLayerへのバンドリングの話)

やってないこと/考慮してないことは、

  • Layerにユーザー定義のモジュールを含める
  • sam local invoke で動作すること
  • bash の動かない環境でのローカルバンドリング

ファイル構成

最終的にこのようなファイル構成になった(主要なファイルのみ)。
project/package.jsondevDependencies には型定義やら開発時に必要なものが色々入っている。後述の NodejsFunction がローカルでbundling出来るように esbuild をインストールしておくこと。
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. Layerのバンドル処理
    1. AWS CDKの処理内でバンドル処理用のシェルスクリプトを呼び出す
    2. project/src/lambda/package.json を使って npm ci を実行する
    3. ローカルでバンドル処理できない状態であれば、Dockerでのバンドル処理を試みる
      1. Dockerイメージがなければ pull
      2. コンテナを起動し、その内部で npm ci を実行する
    4. バンドル結果は project/cdk.out 以下に出力される
  2. Functionのバンドル処理
    1. Layerに含めたモジュールは明示的に除外する(package.json の dependencies)
    2. TypeScriptからJavaScriptにトランスパイル
    3. バンドル結果は project/cdk.out 以下に出力される
  3. FunctionからLayerを参照する

実装

Layer

まずLayerを作る。
全体はこんな感じ。

project/lib/stack.ts
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,
});

ローカルでのバンドル処理

local に指定した LocalBundlerILocalBundling インターフェイスの tryBundle メソッドを実装したクラス。
このクラスでローカルでのバンドル処理を行う。

project/lib/stack.ts
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がよいようにしてくれる。

LocalBundler#tryBundle
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 を行っている。というかそれしかやってない。

bundle.sh
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でのバンドル処理にフォールバックする。
具体的には tryBundlefalse を返した時にフォールバックする。
/asset-output/ 以下に出力する必要があるので、ローカルでのバンドル処理と同様に /asset-output/nodejs 以下に出力する。
lambda.Code.fromAsset('src/lambda') で指定したパスが /asset-input にマウントされ、カレントディレクトリになっているのだと思う。

project/lib/stack.ts
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',

Function

lambda.Function を使うとTypeScriptのままデプロイされてしまうので言語に特化したモジュールを使う。
TypeScriptの場合 aws-cdk-lib/aws-lambda-nodejsNodejsFunction を使う。
バンドル処理以外の使い方は lambda.Function と同じ。

project/lib/stack.ts
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],
});

bundling にはesbuildのオプションを指定する。
externalModules には配列でバンドル対象外のモジュールを指定する。
ここではLayerに含めたモジュールを除外している。

project/lib/stack.ts
bundling: {
  externalModules: external,
},

除外するモジュールの一覧は src/lambda/package.jsondependencies から取得した。
package.json を import したかったので tsconfig.json"resolveJsonModule": true を追加してある。

project/lib/stack.ts
import * as manifest from '../../src/lambda/package.json';
  :
const external = Object.keys(manifest.dependencies);

先ほど作ったLayerを指定する。
fn.addLayers(layer) でもいい。

project/lib/stack.ts
layers: [layer],

実行

実行してみる。
bundle.shset -x しているので色々でている。
Dockerでのバンドル処理は初回pullが走ったり、2回目以降もそれなりに時間がかかる。
環境的に問題がなければローカルでのバンドル処理が手っ取り早いと思う。

$ 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
18
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?