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 synthcdk 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.tscdk 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.jsonpostinstall を追記しておきます。実行するのは、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確認します。

f:id:Zarari:20200428003902p:plain
Lambda 実行確認

無事に Layers で含めた外部モジュールを呼び出せていることを確認できました。

おわりに

CDK で Layers を含む Lambda を作成できること、および作成したものを他のアプリケーションから使えることを確認しました。パッケージ管理ができるようになるとインフラのモジュール化が実用的になってくるので、IaC の世代が一歩進む感覚があります。

不便に感じた部分としては、ライブラリ側と呼び出し側で CDK バージョンを揃える必要がある点です。CDK 自体のバージョンアップは非常に頻繁に行われているので、ライブラリとして作るなら常に最新版に対応して公開していく仕組みを自動化するなどが必要かと思いました。引き続き CDK は手探りを続けていきたいと思います。



  1. cdk init 生成したときの .gitignore, .npmignore のままでは git レポを install しようとしてもパッケージにならないので調整が要ります