Node.jsランタイムのLambdaでNodeモジュールを使いたいときにやったこと


目次

はじめに

こんにちは。ピクトリンク事業部開発部サーバサイド開発課のkitajimaです。最近CDKに入門しました。
弊チームでは先日、API Gateway + Lambdaの構成をCDKで構築し、APIを実装する機会がありました。その際Node.jsで書いたLambdaスクリプト単体をアップロードしたところ、"Unable to import module"が発生しました。 この記事ではその際対応したことを紹介させていただこうと思います。
同じようにNode.jsランタイムのLambdaを初めて構築してみたい方の参考になれば幸いです。

環境

  • AWS CDK v2
  • CDK実装 TypeScript
  • Lambdaランタイム Node.js 16.x
  • Lambdaスクリプト実装 TypeScript

状況再現

そのときのインフラ構成の一部を再現したものはこちらです。Constructはaws_lambda.Functionを使用しており、Lambdaに関する他のリソースは作成していませんでした。

const function1 = new aws_lambda.Function(this, 'Function', {
  code: AssetCode.fromAsset('./src'),
  handler: "hello-world.handler",
  runtime: Runtime.NODEJS_16_X,
});

何が求められているか

LambdaではすべてのNode.jsライブラリがパッケージ化されているわけではなく、自前でアップロードする必要があったためでした。
Node.js 内の Lambda コードについて「モジュールをインポートできません」エラーを解決する

これに対処すべく、今回は以下の2つの方法を試してみました。

node_modulesをLambda Layerにアップロードする

やったこと

  1. 必要なnode_modulesをプロジェクトのnodejs/bundle内に用意します。
  2. 1.をLambda Layerにアップロードするために以下のリソースを定義します。
    const lambdaLayer = new LayerVersion(this, 'Layer', {
      code: AssetCode.fromAsset("./bundle"),
    });
    
    const function1 = new aws_lambda.Function(this, 'Function', {
      code: AssetCode.fromAsset( './src'),
      handler: "hello-world.handler",
      runtime: Runtime.NODEJS_16_X,
    });
    function1.addLayers(lambdaLayer);
    
  3. cdk deployを叩きます。

結果

node_modulesがLambda Layerにアップロードされ、Lambda関数からライブラリが利用できるようになります。

モジュールをバンドルする

やったこと

  1. 以下のようにaws_lambda_nodejs.NodejsFunction を使用してLambda関数を構築します。

    const function1 = new NodejsFunction(this, 'function', {
      handler: 'hello-world',
      entry: './src/hello-world.ts',
      runtime: Runtime.NODEJS_16_X,
    });
    
  2. cdk deployを叩きます。

結果

ライブラリとプロダクトコードがバンドルされた1つのJavaScriptファイルが、Lambda関数としてアップロードされます。

なお、aws_lambda_nodejs.NodejsFunctionの場合、esbuildというビルドツールがTypeScriptのトランスパイルも行ってくれます。
※トランスパイル(トランスコンパイル)…別のプログラミング言語に変換すること。今回のケースでは、TypeScriptをJavaScriptへ変換することです。
Node.jsランタイムで動作させるにはJavaScriptへの変換が必要ですが、トランスパイルによって手元で変換せずにデプロイが可能となります。

どちらを採用するか

今回は、以下の理由からモジュールをバンドルするを採用することにしました。

  1. Lambdaのサイズ上限250MBに引っかかった
    node_modulesをLambda Layerにアップロードしたい場合、Lambdaのデプロイパッケージサイズ上限に引っかかる可能性があります。今回はこちらに引っかかってしまい、デプロイ時にエラーが発生しました。
    aws_lambda_nodejs.NodejsFunctionを使用してesbuildによる単一ファイルへのバンドルが実行される際、node_modules全てではなく必要最低限の依存関係のみ含むためにサイズが小さくなったのだと推測しています。
    要検証ですが、1.の問題はnode_modulesの適切なダイエットを実施すれば回避可能かもしれません。

  2. Lambda Layerの場合、必要なnode_modulesをプロジェクトのnodejs/内に用意する必要がありますが、こちらはConstructがよしなに配置してくれるものではありません。デプロイの度に手動でコピーするなり、コピー用のスクリプトを書いたり、何らかの方法で事前に配置する必要があるようです。

こちらの参考記事では、デプロイ時に走るようなセットアップ用のスクリプトを自作しておられました。
AWS CDK を使って node_modules を AWS Lambda Layers にデプロイするサンプル

モジュールをバンドルするを採用したので、スクリプトを用意、管理する手間も発生しませんでした。

最後に

CDKをがっつり触ったのは今取り組んでいる施策が初めてで、リソースをプログラマブルに用意できる感覚は非常に便利に感じました。不慣れなインフラ部分もこれで構築すると楽しいです。
また、API Gateway + Lambdaは代表的なサーバレス構成なので、今回学んだことは今後の業務にも活かされるだろうと思っています。皆様にも参考になれば幸いです。
お読みいただきありがとうございました!