AWS CDKでLambda Layerを設定しLambdaから読み出してローカル実行を行ったメモ


概要

前回にLambdaの実行を確認できた。
生成物を確認し、必要とするモジュールもバンドルされていることも確認できた。
Lambda間で共通のモジュールを使う場合、すべてのLambdaにバンドルするのは無駄である。(容量も重くなるしバンドル時間もかかる)
AWSにはLambda Layerという仕組みがあるのでそれを使って解決する。

AWS CDK を使って node_modules を AWS Lambda Layers にデプロイするサンプルを見れば実現できる。
ただし、Windowsで動かすときに少しハマったので備忘録を残す。

ソースコード

やること

SAMを使ったローカル実行

やらないこと

デプロイ

ソースコード修正

ビルド設定

  • Lambda Layerにいれる成果物をバンドルのときに作成できるように pre-prrocess追加
bin/sample-index.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { SampleStack } from '../lib/sample-stack';
import { bundleNpm } from '../lib/process/setup';


+ // pre-process
+ bundleNpm();

// create app
const app = new cdk.App();
new SampleStack(app, 'SampleStack2021');

Stackの記述修正

  • stackにLamba Layerの作成を記述する
lib/sample-stack.ts


+ import { NODE_LAMBDA_LAYER_DIR } from './process/setup';

export class SampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);


+    const nodeModulesLayer = new lambda.LayerVersion(this, 'NodeModulesLayer',
+      {
+        code: lambda.AssetCode.fromAsset(NODE_LAMBDA_LAYER_DIR),
+        compatibleRuntimes: [lambda.Runtime.NODEJS_14_X]
+      }
+    );

    new NodejsFunction(this, 'test', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/test.ts',
      functionName: 'kotatest',
      bundling: {
        externalModules: [
          'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
          'date-fns', // Layrerに入れておきたいモジュール
        ],
        define: { // Replace strings during build time
          'process.env.API_KEY': JSON.stringify(JSON.stringify('"xxx-xxx"')), 
        },
      },
+      layers: [nodeModulesLayer],
    });

  }
}

全文
lib/sample-stack.ts
import * as cdk from '@aws-cdk/core';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
import * as lambda from '@aws-cdk/aws-lambda';
import { NODE_LAMBDA_LAYER_DIR } from './process/setup';
export class SampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new NodejsFunction(this, 'hello', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/hello.ts',
      functionName: 'kotahello',
      handler: 'lambdaHandler'
    });

    const nodeModulesLayer = new lambda.LayerVersion(this, 'NodeModulesLayer',
      {
        code: lambda.AssetCode.fromAsset(NODE_LAMBDA_LAYER_DIR),
        compatibleRuntimes: [lambda.Runtime.NODEJS_14_X]
      }
    );

    new NodejsFunction(this, 'test', {
      runtime: lambda.Runtime.NODEJS_14_X,
      entry: 'src/lambda/handlers/test.ts',
      functionName: 'kotatest',
      bundling: {
        externalModules: [
          'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
          'date-fns', // Layrerに入れておきたいモジュール
        ],
        define: { // Replace strings during build time
          'process.env.API_KEY': JSON.stringify(JSON.stringify('"xxx-xxx"')), // バグってそう.二重でstringifyしないとInvalid define valueのエラー
        },
      },
      layers: [nodeModulesLayer],
    });

  }
}

Layerの成果物作成

  • pre-prrocessで呼び出される
    • Lambda Layerにアップロードするディレクトリの作成
    • package.json, package-lock.jsonのコピー
    • npm installでnode_modulesディレクトリを作成
  • 参考npm --prefix ${getModulesInstallDirName()} install --productionだとnpm installされずにシンボリックリンクが作成されてしまった
    • 以下のようにcwdを使ってディレクトリを指定するよう修正して対応
lib/process/setup.ts
  // install package.json (production)
  childProcess.execSync(`npm  install --production`, {
    cwd: getModulesInstallDirName(),
    // bundle時にパイプで出力するtemplate.yamlに、余分な文字列が含まれてしまわないように出力はオフ
    stdio: ['ignore', 'ignore', 'ignore'],
    env: { ...process.env },
    shell: 'bash'
  });

全文
lib/process/setup.ts
#!/usr/bin/env node
import * as childProcess from 'child_process';
import * as fs from 'fs-extra';
import * as path from 'path'

const bundlePath = './bundle';
export const NODE_LAMBDA_LAYER_DIR = path.resolve(process.cwd(), bundlePath);
const NODE_LAMBDA_LAYER_RUNTIME_DIR_NAME = `nodejs`;
const runtimeDirName = path.resolve(process.cwd(), `${bundlePath}/${NODE_LAMBDA_LAYER_RUNTIME_DIR_NAME}`);
const distFilePath = (file: string) => path.resolve(process.cwd(), `${bundlePath}/${NODE_LAMBDA_LAYER_RUNTIME_DIR_NAME}/${file}`)
const srcFilePath = (file: string) => path.resolve(`${process.cwd()}/${file}`)

export const bundleNpm = () => {
  // create bundle directory
  copyPackageJson();

  // install package.json (production)
  childProcess.execSync(`npm  install --production`, {
    cwd: getModulesInstallDirName(),
    // bundle時にパイプで出力するtemplate.yamlに、余分な文字列が含まれてしまわないように出力はオフ
    stdio: ['ignore', 'ignore', 'ignore'],
    env: { ...process.env },
    shell: 'bash'
  });
};

const copyPackageJson = () => {

  // copy package.json and package.lock.json
  fs.mkdirsSync(getModulesInstallDirName());
  ['package.json', 'package-lock.json']
    .map(file => fs.copyFileSync(srcFilePath(file), distFilePath(file)));

};

const getModulesInstallDirName = (): string => {
  return runtimeDirName;
};

実践

  • 動いた
  • Build imageが大分長くなった気がする。。。
    • 2回目からはそこまででもないかも

参考

AWS CDK を使って node_modules を AWS Lambda Layers にデプロイするサンプル
aws lambda layer