aws-cdk を使って lambda function の開発をしている際に、良さげに出来た lambda layer 管理の方法をメモしておこうと思ったので記しておきます。
初学者なのでもっと良い方法はあるかもです。。
動機
-
import { hoge } from 'layer'
みたいに lambda layer にしたコードを使いたい - vendor ライブラリも実装者が特に意識せず使いたい(vendor ライブラリも layer にしておく必要があります)
-
package.json
は1つだけで良い
という事を解決し、かつ簡単にその環境を用意したいという背景からです。
動作確認環境
- typescript: 3.8.3
- aws-cdk: 1.26.0
- aws-sam-cli: 0.40.0
まずは簡単な lambda function を用意しておく
以下は、 cdk init --language typescript
で作成した cdk の環境を基に書いていきます。
プロジェクト名: cdk-project
$ tree -I node_modules -L 3
.
├── README.md
├── bin
│ └── cdk-project.ts
├── cdk.json
├── jest.config.js
├── lib
│ └── cdk-project-stack.ts
├── package-lock.json
├── package.json
├── test
│ └── cdk-project.test.ts
└── tsconfig.json
簡単に Hello World
を返す lambda function を作成しておきます。
$ mkdir -p dir lambda/helloWorld
$ vi lambda/helloWorld/index.ts
// lambda/helloWorld/index.ts
import { APIGatewayEvent, Callback, Cotnext } from 'aws-lambda'
exports.handler = async (event: APIGatewayEvent, context: Context, callback: Callback) => {
callback(null, 'Hello World')
}
lambda を作成するために lib/cdk-project-stack.ts
を以下のように修正する。
// lib/cdk-project-stack.ts
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
export class CdkProjectStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const helloWorldLambda = new lambda.Function(this, 'helloWorldLambda', {
code: lambda.Code.fromAsset('lambda/helloWorld'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_12_X
})
}
}
// bin/cdk-project.ts にも書かれているがコード記載省略のため
const app = new cdk.App()
new CdkProjectStack(app, 'HelloWorldCdkApp')
app.synth()
ここで一旦 sam で確認をしてみる。
$ yarn build
$ cdk synth
$ sam local invoke -t cdk.out/HelloWorldCdkApp.template.json
Invoking index.handler (nodejs12.x)
Fetching lambci/lambda:nodejs12.x Docker container image......
(省略)
"Hello World"
Hello World
が返ってくる事を確認できた。
lambda layer を用意する
本題です。
lambda layer を deploy するには以下のルールがあります。
- zip で S3 にアップロードされている
- zip 展開後の構造が
nodejs/node_modules/
となっている
aws-cdk を用いた場合、 指定したディレクトリを zip にして S3 へアップロードはしてくれるのですが、 nodejs/node_modules/
という構造にはこちら側でしておく必要があります。
ここが厄介なのですが、
import { hoge } from 'layer'
みたいに lambda layer にしたコードを使いたい
をしようとして
.
├── lambda
│ └── helloWorld
└── layer
と単純に行うと 「vendor ライブラリは? まとめて lambda layer にするのめんどそう。。」となり
.
├── lambda
│ └── helloWorld
└── layer
└── nodejs
└── nodejs
└── node_modules
とすると、 「え! vendor ライブラリ含める場合 nodejs
以下に package.json
用意して管理する?しかも、 import 文気持ち悪い。。どうしよう」となり、
vendor ライブラリも実装者が特に意識せず使いたい(aws-cdkの場合 vendor ライブラリも layer にしておく必要があります)
package.json
は1つだけで良い
ここら辺上手く出来ないじゃんとなりました。
結論としては以下のような構造とする事で解決しました。(上記前者の方法を使ってるんですけどねw)
- 独自共通化処理を
layer
ディレクトリ以下で管理する - root の dependencies に
layer
ディレクトリをパス指定で追加する - root の dependencies に lambda layer として使いたい vendor ライブラリを追加する
- root の dependencies に指定したライブラリを
layer.out/nodejs/node_modules/
の構造にしてyarn install
する
layer.out/nodejs/node_modules/
膨れるやんって思った方は devDependencies に移すべきものがないか検討してみてください。
1. 独自共通化処理を layer
ディレクトリ以下で管理する
.
├── lambda
│ └── helloWorld
└── layer
└── utils
│ └── index.ts
├── ...
└── index.ts
簡単に数値を加算する関数を layer に追加しておく。
// layer/utils/index.ts
export const sum = (num1: number, num2: number): number => num1 + num2
layer
以下で共通化処理を管理し、 layer/index.ts
でまとめて export します。
// layer/index.ts
export * from './utils'
...
2. root の dependencies に layer
ディレクトリをパス指定で追加する
package.json
を以下のように修正。
{
...
"dependencies": {
"layer": "file:./layer"
}
}
3. root の dependencies に lambda layer として使いたい vendor ライブラリを追加する
vendor コードの例として dayjs
も dependencies に追加しておきます。
{
...
"dependencies": {
"dayjs": "^1.8.22",
"layer": "file:./layer"
}
}
4. root の package.json の dependencies に指定したライブラリを layer.out/nodejs/node_modules/
の構造にして yarn install
する
dependencies に指定したライブラリを指定の構造で install する preprocess script を作成します。
// lib/layerSetup.ts
import * as childProcess from 'child_process'
const LAMBDA_LAYER_DIR_NAME = './layer.out/nodejs/node_modules/'
export const bundleLayer = () => childProcess.execSync(`yarn install --production --modules-folder ${LAMBDA_LAYER_DIR_NAME}`)
// bin/cdk-project.ts
import ...
import { bundleLayer } from '../lib/layerSetup'
// pre-process
bundleLayer()
const app = new cdk.App()
new CdkProjectStack(app, 'HelloWorldCdkApp')
あとは tsc でトランスパイルする前に layer
を upgrade しておけば、 layer 更新した際も反映が楽になるので、 npm scripts
を
{
...
"scripts": {
"build": "yarn upgrade layer && tsc",
...
}
}
としておけば良さそう。
あとは stack に lambda layer を追加してあげて
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
export class CdkProjectStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const layer = new lambda.LayerVersion(this, 'layer', {
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
code: lambda.Code.fromAsset('layer.out'),
})
const helloWorldLambda = new lambda.Function(this, 'helloWorldLambda', {
code: lambda.Code.fromAsset('lambda/helloWorld'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_12_X,
layers: [layer]
})
}
}
$ yarn build
$ cdk synth
とすれば、 preprocess で layer.out/nodejs/node_modules/...
が作成され、 layer.out
を layer として設定してくれる。
sam で lambda layer の動作確認
helloWorldLambda を雑に lambda layer の関数を使うように修正しておく。
import { APIGatewayEvent, Callback, Context } from 'aws-lambda'
import * as dayjs from 'dayjs'
import { sum } from 'layer'
dayjs.locale('ja')
exports.handler = async (event: APIGatewayEvent, context: Context, callback: Callback) => {
console.log('Hello World')
console.log(sum(1, 2)) // 3
callback(null, dayjs('2020-01-01'))
}
$ yarn build
$ cdk synth
$ sam local invoke -t ./cdk.out/HelloWorldCdkApp.template.json
Invoking index.handler (nodejs12.x)
layer27F209B1 is a local Layer in the template
Building image...
Requested to skip pulling images ...
(省略)
... INFO Hello World
... INFO 3
(省略)
"2020-01-01T00:00:00.000Z"
出来た。
ちなみに vendor code を lambda layer にしておかないと
{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'dayjs'\nRequire stack:\n- /var/task/index.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js"}
みたいになる。
まとめ
- dependencies で lambda layer を管理できる体制にする。(dependencies と devDependencies を区別する意識をつけてこう)
- preprocess で dependencies を
nodejs/node_modules
の形式にする。 - npm scripts で synth までのコマンドをあまり意識しなくて良いようにする。
synth する前に cdk.out
や layer.out
なんかを削除しておかないと上手く更新しない場合もあったりするので cdk:synth
みたいな npm scripts 用意して cdk synth
前に clean しておく処理挟むと良さそうです。(ここら辺の aws-cdk の挙動はまだ調べられていない)
以上です。
もっとこうしたら良いよ!っていうのがあったら教えてください!