19
13

More than 3 years have passed since last update.

AWS CDK + Typescript 環境で lambda layer を上手く管理する

Posted at

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)

  1. 独自共通化処理を layer ディレクトリ以下で管理する
  2. root の dependencies に layer ディレクトリをパス指定で追加する
  3. root の dependencies に lambda layer として使いたい vendor ライブラリを追加する
  4. 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.outlayer.out なんかを削除しておかないと上手く更新しない場合もあったりするので cdk:synth みたいな npm scripts 用意して cdk synth 前に clean しておく処理挟むと良さそうです。(ここら辺の aws-cdk の挙動はまだ調べられていない)

以上です。
もっとこうしたら良いよ!っていうのがあったら教えてください!

19
13
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
19
13