Lambda Layer | Bad Practice vs (たぶん) 正しい使い方
こんにちは、Lambda Function て便利ですよね。
API-Gateway などでも使いますが、CDK のカスタムランタイムなど一時的な利用でもよく使っています。
一方、Lambda Layer は使ったことがなかったことから、試しにいじってみたら良くない使い方をしたようで。。
今回は、Layer 初心者である自分が疑問に思って確認したこと、自分がやった良くない使い方、そして Layer の(たぶん)正しい使い方を共有したいと思います。
前提
CDK を利用します
不明な部分はちょっと掘る(深追いはしません)
TypeScript を使用します(CDK テンプレート/Lambda ソース)
1. Lambda Layer で確認したこと
AWS document を確認
Lambda Layer について、AWS ドキュメントでは下記の説明がされています。
つまり、複数の Lambda Function で共通で使用するデータ、バイナリ、ライブラリなどを Layer に保存することで、各 Lambda Function から共通部品として利用できるものと認識しました。
また、Lambda Layer によるメリットも以下のように書かれていました。
デプロイサイズが小さくなる
コア機能と共通部品を分離できる
複数 Lambda Function 間で依存関係を共有する
Lambda コンソールのエディターでコードがみやすくなる
Lambda Layer の定義方法と使い方
CDK を使う場合、以下のように Lambda Layer を定義します。
// Create a Layer
const layer = new lambda.LayerVersion(this, 'Layer', {
description: 'A layer with some utilities',
compatibleArchitectures: [lambda.Architecture.X86_64, lambda.Architecture.ARM_64],
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
code: lambda.Code.fromAsset('./dist/layer'),
});
さらに、Lambda Function 定義時に、作成した Layer を Arn とバージョンで指定します。
// Create a Lambda Function
const lambdaFunction = new nodejs.NodejsFunction(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
entry: path.join(__dirname, '../lambdas/function.ts'),
layers: [lambda.LayerVersion.fromLayerVersionArn(this, 'Layer', 'arn:aws:lambda:us-west-2:XXX:layer:XXX:NN')],
bundling: {
externalModules: ['lambda-layer'],
},
});
なお、Layer 作成時の各種オプション compatibleArchitectures や compatibleRuntimes は Lambda コンソールから確認できるラベルとして使われ、チェック機構などはありません。
そのため、実際のコードとは乖離した値を設定することができるのでお気をつけください。
さらに、Layer 作成時にトランスパイル/コンパイルしてくれるわけではないのも注意です。
Layer のアクセス方法/どこに保存される?
Lambda Layer を作成し、Lambda Function に追加すると、Lambda Function のランタイム上の /opt ディレクトリに保管されます。
さらに、/opt/node_modules などのパスが Lambda Function の PATH に設定済みとなっています。
言語に依存しますので詳細はこちら[AWS-各 Lambda ランタイムのレイヤーパス]
これにより、Layer に展開したライブラリなどを、Lambda Function から容易に import して利用することができます。
2. Lambda Layer を TypeScript 用ライブラリに利用してみる
TypeScript を使ったライブラリとして Lambda Layer を使ってみました。
以下のように、異なる CDK スタックを定義し、Lambda Layer に共有関数を、Lambda Function ではその関数を利用する形にします。
ポイント1: Layer はあらかじめトランスパイルが必要
Lambda Layer は渡されたファイルをそのままアップロードするため、LayerStack の展開時にはトランスパイルされたコードを渡す必要があります。
今回は、cdk deploy の前に esbuild を利用してトランスパイルしています。
// LayerStack の package.json
// deploy 前に トランスパイル
"scripts": {
"build": "npx esbuild --bundle --platform=node --sourcemap ./lambdas/lambda-layer --outdir=./dist/layer/nodejs/node_modules",
"deploy": "npm run build && npx cdk deploy --require-approval never"
},`
また、CDK 定義上は、`./dist/layer` 配下を渡すように設定しました。
// Create a Layer
// dist/layer 配下を渡す
const layer = new lambda.LayerVersion(this, 'Layer', {
description: 'A layer with some utilities',
compatibleArchitectures: [lambda.Architecture.X86_64, lambda.Architecture.ARM_64],
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
code: lambda.Code.fromAsset('./dist/layer'),
});
こうすることで、Lambda Function 上の `/opt/nodejs/node_modules/` 配下には、以下のようなトランスパイル済みのファイルが配置されます。また PATH も通っているため Function から容易にアクセスが可能になりました。
// Lambda Function ランタイムから /opt ディレクトリ配下をスキャンした結果
/opt/nodejs
/opt/nodejs/node_modules
/opt/nodejs/node_modules/lambda-layer.js
/opt/nodejs/node_modules/lambda-layer.js.map
ポイント2: 開発中のライブラリ解決
上記だけで Lambda Function は Layer を取り込み動作するのですが、開発中の困るポイントが一個残されています
例えば Lambda Function のコードをローカル VSCode で開いても、共有ライブラリの解決ができず、以下のような Cannot find module が表示されます。
ここは、共有ライブラリのコードをローカルに保存し PATH を通せば良いのですが、どのような環境にも適用できる手はなかなかありませんでした。
今回は、(テストも含めて) GitHub 上にコンパイル済みのコードを push し、それを開発中に npm install するやり方を試しました。
// LayerStack の package.json
// "npm run package" を走らせることで npm install 可能なパッケージを作成
"scripts": {
"build": "npx esbuild --bundle --platform=node --sourcemap ./lambdas/lambda-layer --outdir=./dist/layer/nodejs/node_modules",
"package": "npx tsc",
"deploy": "npm run build && npm run package && npx cdk deploy --require-approval never"
},
// LayerStack の tsconfig.json
// npm 用パッケージの出力ディレクトリを指定
{
"compilerOptions": {
"outDir": "./dist/package"
},
}
このコードを GitHub 上にpush しておくと、npm install コマンドでライブラリとして利用でき、ローカル開発中でもライブラリ解決が行われました。
# FunctionStack に共有ライブラリとしてインストール
npm i -D git+ssh://git@github.com:<ORGANIZATION>/<REPOSITORY名>
# node_modules には以下のように展開される
node_modules
├── lambda-layer
│ ├── README.md
│ ├── cdk.json
│ ├── dist
│ │ └── package
│ │ ├── db
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ └── s3
ポイント3: Lambda Function 上にライブラリ本文が入らないようにする
ポイント2 の副作用として、このままでは Lambda Function 内にライブラリのコードが含まれてしまいます。
esbuild が依存ライブラリを bundle していることが原因です。
esbuild の external 設定を行うことで、bundle 非対象なライブラリを指定します。
// (再掲) Create a Lambda Function
// externalModules で bundle 非対象なライブラリ名を指定
const lambdaFunction = new nodejs.NodejsFunction(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
entry: path.join(__dirname, '../lambdas/function.ts'),
layers: [lambda.LayerVersion.fromLayerVersionArn(this, 'Layer', 'arn:aws:lambda:us-west-2:XXX:layer:XXX:NN')],
bundling: {
externalModules: ['lambda-layer'],
},
});
これにより、ライブラリのコードは Layer だけを利用する綺麗な形になりました。
結果
今回は以下のような構成で作ることで、Layer 上のライブラリを Lambda Function で利用できるようになりました。
ポイント
Layer に上げる前にトランスパイルする
ローカル(VSCode) で開発中、ライブラリ解決するためにひと手間追加
GitHub 上にトランスパイル済みコードを push
ローカル開発中は、npm install -D でライブラリ解決
Lambda Function にライブラリコードが入らないように、esbuild の external 設定を追加(CDKコードへ)
3. このユースケースをもっと良い構成にするには
Layer と開発環境用パッケージの2種類を用意するというのがどうしてもネックと思いました。
そこで、以下の構成のほうがより開発しやすいだろうと思います(実際に動きました)
こちらの構成に関しては、また機会があれば共有したいと思います
4. Layer の良い使い方
まとめると、「Lambda Layer や Lambda Function の開発時/利用時とで開発環境の差異が生じないもの」もしくは「開発時と利用時で簡単に差異を吸収できるもの」が Layer 利用時の良い使い方となるようです。
とすると以下のような使い方が Lambda Layer に適していると言えます。
静的なデータファイルやバイナリの共有
(例) json, csv
(例) ffmpeg バイナリLambda 拡張機能 (Lambda extension)
Lambda Function とは別環境で、任意のツールを統合する仕組み
(例) Lambda Function のモニタリングツールを常駐させる
(例) Lambda Function の実行前・実行中・実行後に診断情報をキャプチャする
また、Lambda カスタムランタイムの機能も Layer を使っていますが、こちらはローカル環境との差異を吸収できるか不明なため、欄外にしています。
Lambda カスタムランタイム
Lambda 標準ランタイム(NodeJS, Python, Dotnet, etc.) 以外を使いたい場合に利用
(例) bash をランタイムに設定し、bash スクリプトを動かす
CodeArtifact を使ったユースケースも含めて、また触る機会がありましたら共有したいと思います