Lambda Layers を含む AWS CDK ライブラリをパッケージ化する
要約
- CDK で Lambde + Lambda Layers の Construct ライブラリを作る
- 別の CDK App から
npm install
して使う - npm の
postinstall
を活用して Layers のアセットを生成する
CDK の記述言語は Typescript 前提で扱います。 (2020/03 現在 cdk init lib
が Typescript しか対応していないため)
背景
CDK + SAM を使った Lambda の開発環境
CDK を使うと簡単に Lambda および周辺環境のデプロイをすることができます。さらに、AWS SAM CLI を使うとローカルで Lambda 環境を立ち上げてテスト実行を行うことができとてもはかどります。 docs.aws.amazon.com
Lambda + Lambda Layers
Lambda で外部パッケージを使う方法の一つとして Layers があります。 AWS CDK を使う場合においても、以下リンクのように Layers のアセットを用意する方法が紹介されています。
dev.classmethod.jp 記事の内容について今回関係するところだけ簡単にまとめると、
- Layers 以下の
npm install
を行う function を作成しておく - CDK App 実行時に function を呼び出す
cdk synth
やcdk deploy
時に Layers 以下nodejs/node_modules
が生成される
というものです。いくつか課題があることが言及されているものの、多くの場合で有効に機能します。
インフラのライブラリ化
CDK の特徴として、作成した Construct / Stack をライブラリ、パッケージとして扱えることが挙げられます。具体的な言い方をするならば、 CDK で作成した Lambda ないしインフラ一式は、他の CDK App から npm install
, import ...
して参照、カスタマイズして使い回せるということです。これについても参考になる記事があります。
dev.classmethod.jp
やること詳細
今回は Layers を含む Lambda を作成し、それが持ち運び可能なライブラリとしてうまく機能するかを試してみます。 まとめると実現したいことはこのようになります。
- CDK で Lambda 単体の開発, テスト環境の作成
- Layers を含む Lambda として整備
- 作成した Lambda を別の CDK App からライブラリとして使えるようパッケージとして整備
一つづつ具体的なコードで確認していきます。
CDK で Lambda 単体の開発環境を作成
cdk init lib --language typescript
でひな形を生成します。 この時点で CDK ライブラリとしての一式は揃っています。今回はここに、
- Lambda 関数本体
lambda/index.js
を追加 - Lambda をテスト実行するためのランナーとして
bin/app.ts
を追加
します。bin/app.ts
は cdk init app
で初期化したときに生成されるのと基本的に同じものです。 lib
としては不要なのですが、Lambda 関数のテストにはあったほうが便利なので作成しています。ツリーは以下のようになりました。
├── bin │ └── app.ts ├── jest.config.js ├── lambda │ └── index.js ├── lib │ └── index.ts ├── package.json ├── package-lock.json ├── README.md ├── test └── tsconfig.json
Lambda 本体はこれです。
'use strict'; exports.handler = async function(event) { console.log('Hello.'); return 'Hello !!'; }
app はこんなです。
#!/usr/bin/env node import 'source-map-support/register'; import * as cdk from '@aws-cdk/core'; import { CdkExampleLambdaWithLayer } from '../lib/index'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'app', { stackName: 'lambda-with-layer'} ); new CdkExampleLambdaWithLayer(stack, 'LambdaWithLayer');
前述の SAM CLI を使う方法でローカルテストができることを確認します。
$ cdk synth --app ./bin/app.js --no-staging > template.yaml $ grep Lambda::Function template.yaml -B1 LambdaWithLayermyHandlerDA1D7776: Type: AWS::Lambda::Function $ sam local invoke LambdaWithLayermyHandlerDA1D7776 Invoking index.handler (nodejs12.x) ... START RequestId: 27e66fad-f3be-13c4-246f-85de8eca3863 Version: $LATEST ... END RequestId: 27e66fad-f3be-13c4-246f-85de8eca3863 REPORT RequestId: 27e66fad-f3be-13c4-246f-85de8eca3863 Init Duration: 293.40 ms Duration: 7.15 ms Billed Duration: 100 msMemory Size: 128 MB Max Memory Used: 38 MB "Hello !!"
正常に実行できました。
Layers を含む Lambda として整備
外部モジュールを Layers に含め本体 Lambda から読むこむようにしてみます。 Layers の導入は冒頭で貼ったクラスメソッドさんの方法と同じように、プリプロセスを挟む方法をとります。
layer/nodejs
ディレクトリ以下に、Lambda Layers 用のpackage*.json
を置く- Layers 用の npm install を行うスクリプトを追加し、
./bin/app.ts
から呼ぶ - Lambda から require
Layers 用の package.json
を準備します。
$ mkdir -p layers/nodejs $ cd layers/nodejs $ npm init # name 等は適当に $ npm install uuid
npm install 用 function を用意します。 lib/js/setup_layer.js
にしました。
const childProcess = require('child_process'); const path = require('path'); module.exports.setupLayer = () => { const layerDir = path.join(__dirname, '..', '..', 'layer', 'nodejs'); childProcess.execSync(`npm install ${layerDir} --prefix ${layerDir} --production`, { shell: 'bash' }); }
cdk synth
などの際に呼びだされるように、./bin/app.ts
に書いておきます。
import { CdkExampleLambdaWithLayer } from '../lib/index'; +// @ts-ignore +import { setupLayer } from '../lib/js/setup_layer'; +setupLayer(); const app = new cdk.App();
Construct に Layers を追加します。
diff --git a/lib/index.ts b/lib/index.ts @@ ... constructor(scope: cdk.Construct, id: string, props: CdkExampleLambdaWithLayerProps = {}) { super(scope, id); + const layer = new lambda.LayerVersion(this, 'layer', { + code: lambda.Code.fromAsset(path.join(__dirname, '..', 'layer')), + compatibleRuntimes: [lambda.Runtime.NODEJS_12_X], + }); new lambda.Function(this, 'myHandler', { runtime: lambda.Runtime.NODEJS_12_X, code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambda')), handler: 'index.handler', environment: { MY_MESSAGE: props.myMessage || 'Hello' }, + layers: [layer], }); }
Lambda 本体から外部モジュールを参照します。
diff --git a/lambda/index.js b/lambda/index.js @@ ... 'use strict'; +const uuid = require('uuid').v4; exports.handler = async function(event) { - console.log('Hello.'); + console.log(`Hello. ${uuid()}`); - return 'Hello !!'; + return `Hello, ${uuid()}`; }
動作確認します。
$ cdk synth --app ./bin/app.js --no-staging > template.yaml $ sam local invoke LambdaWithLayermyHandlerDA1D7776 ... "Hello, 4c5c267e-7eb9-409e-9d3e-4385e2d708f3"
ここまではよさそうです。
最後に、他から install されたときに Layers がちゃんと生成されるよう、package.json
に postinstall
を追記しておきます。実行するのは、App から呼び出しているものと同じ、npm install 用 function です。
diff --git a/bin/postinst.js b/bin/postinst.js new file mode 100755 @@ ... +#!/usr/bin/env node +const setupLayer = require('../lib/js/setup_layer').setupLayer; + +setupLayer(); diff --git a/package.json b/package.json @@ ... "scripts": { "build": "tsc", "watch": "tsc -w", - "test": "jest" + "test": "jest", + "postinstall": "bin/postinst.js" },
npm パッケージとして公開しておきます。こちらにもあるように、基本的には npm publish
すれば OK です。もしくは、npm は git レポジトリでもパッケージとして扱えるので、github から直接参照することも可能1ではあります。
別の CDK App から使う
別の CDK App を用意し、先程作成したパッケージを npm install
します。
$ npm install @<myusername>/cdk-example-lambda-with-layer
node_modules 以下の自作パッケージディレクトリに、 postinstall によって Layers の外部モジュールが配置されていることが確認できます。
$ ls node_modules/@<myusername>/cdk-example-lambda-with-layer/layer/nodejs/node_modules/ uuid/
スタックからは以下のように利用できます。
diff --git a/lib/cdk-example-import-lambda-stack.ts b/lib/cdk-example-import-lambda-stack.ts @@ ... import * as cdk from '@aws-cdk/core'; +import * as myfunc from '@<myusername>/cdk-example-lambda-with-layer'; export class CdkExampleImportLambdaStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here + new myfunc.CdkExampleLambdaWithLayer(this, 'myFunc', { myMessage: 'Imported!'}); } }
$ cdk deploy
でデプロイし、Lambda Layer確認します。
無事に Layers で含めた外部モジュールを呼び出せていることを確認できました。
おわりに
CDK で Layers を含む Lambda を作成できること、および作成したものを他のアプリケーションから使えることを確認しました。パッケージ管理ができるようになるとインフラのモジュール化が実用的になってくるので、IaC の世代が一歩進む感覚があります。
不便に感じた部分としては、ライブラリ側と呼び出し側で CDK バージョンを揃える必要がある点です。CDK 自体のバージョンアップは非常に頻繁に行われているので、ライブラリとして作るなら常に最新版に対応して公開していく仕組みを自動化するなどが必要かと思いました。引き続き CDK は手探りを続けていきたいと思います。
-
cdk init
生成したときの .gitignore, .npmignore のままでは git レポを install しようとしてもパッケージにならないので調整が要ります↩