Amazon Web Services ブログ
AWS Lambda の Node.js 依存関係を最適化
この記事は、Optimizing Node.js dependencies in AWS Lambda を翻訳したものです。
Node.js モジュール解決の理解
コード内でリソースを require または import すると、Node.js はファイル名、ディレクトリ名、または node_modules ディレクトリ内でそのリソースを解決しようとします。リソースが見つかると、ストレージから読み込まれ、解析されて実行されます。
そのファイルまたは依存モジュールにさらに他の require または import ステートメントが含まれている場合、プロセスが繰り返され、ストレージの読み取りが発生します。関数にインポートされる依存関係とファイルが多いほど、初期化にかかる時間は長くなります。
これはインポートされたコードと実際に使用されるコードに関してのみ影響します。つまりインポートまたは使用されていないファイルをプロジェクトに含めても、起動時のパフォーマンスへの影響は最小限です。
また、何がインポートされているかを評価する必要があります。esbuild、Rollup、WebPack などの JavaScript バンドラーは tree shaking で実行されないコードを削除しますが、ワイルドカード、グローバルまたはトップレベルのインポートによる依存関係のインポートにより、バンドルサイズが大きくなる可能性があります。
ライブラリでサポートされている場合は、パスを指定したインポートを使用してください。
ワイルドカードを使ったインポートは避けてください。
トップレベルのインポートも避けましょう。
AWS SDK for JavaScript V3
AWS Lambda の Node.js ランタイムが使用する AWS SDK のバージョンを管理するには、自分で AWS SDK を用意する必要があります。その際には AWS SDK for JavaScript V3 の使用を検討してください。AWS SDK V3は、サービスごとに個別のパッケージを備えたモジュラーアーキテクチャを採用しています。
これには、インストールの高速化や展開サイズの縮小など、多くの利点があります。また、ファーストクラスの TypeScript サポートや新しいミドルウェアスタックなど、リクエストの多かった機能も多数含まれています。サービスごとに個別のパッケージがあり、トップレベルのインポートはできないため、起動時のパフォーマンスがさらに向上します。
ランタイムに依存しない AWS SDK を利用することで、ビルドプロセス中に軽量化してバンドルすることができ、コールドスタート時間の削減につながります。
Node.js Lambda 関数のバンドルと軽量化
esbuild を使用して Lambda 関数をバンドルして圧縮できます。これは入手可能な最速の JavaScript バンドラーの1つで、多くの場合、WebPack や Parcel のような代替バンドラーよりも10〜100倍高速です。
esbuild を使用するには:
1. npm または yarn を使用して esbuild を開発時の依存関係に追加します。
- npm:
npm i esbuild --save-dev
- yarn:
yarn add esbuild --dev
2. package.json ファイルの scripts セクションにビルドコマンドを記述します。
このスクリプトは最初に dist ディレクトリを削除し、次に下記のコマンドライン引数を使用して esbuild を実行します。
./src/*
まず、アプリケーションのエントリポイントを指定します。esbuild は、指定されたエントリポイントごとに、使用する依存関係のみを含む1つのバンドル(バンドルオプションが有効な場合)を作成します。--entry-names=[dir]/[name]/index
は、esbuild がエントリポイントと同じディレクトリ内のエントリポイントと同じ名前のディレクトリにバンドルを作成するように指定します。このバンドルの名前はindex.js
になります。--bundle
は、すべての依存関係とソースコードを 1 つのファイルにバンドルすることを示します。--minify
はコードを軽量化するために使用されます。--sourcemap
は、軽量化されたコードのデバッグに必要なソースマップファイルの作成に使用されます。軽量化されたコードは元のソースコードとは異なるため、ソースマップを使用すると JavaScript デバッガーは軽量化されたコードを元のコードにマップできます。ソースマップを生成するとデバッグに役立ちますが、サイズが大きくなってしまいます。ソースマップを適用するには有効化する必要があることに注意してください。Lambda 関数でソースマップを有効化するには、NODE_OPTIONS
環境変数に--enable-source-maps
を指定します。--platform=node
と--target=node16.14
は、ターゲットとする ECMAScript のバージョンを指定するために使用されます。バンドラーを使用することで、新しい JavaScript の機能や構文を以前の標準に合わせてコンパイルできることがあります。AWS Lambda は Node.js 16 をサポートするようになったので、ここではターゲットをnode16.14
に設定します。参考までに、 https://node.green/ を参照して Node.js のバージョンと ECMAScript の機能を確認してください。--outdir=dist
は、すべてのファイルがdist
ディレクトリに出力されることを示します。
ビルド
yarn build
または npm run build
でビルドスクリプトを実行します。
パッケージ化とデプロイ
Lambda 関数をパッケージ化するには、dist ディレクトリに移動し、それぞれのディレクトリの内容を圧縮します。index.js と index.js.map のみを含む、関数ごとに 1 つの zip ファイルを作成する必要があることに注意してください。こちらのサンプルプロジェクトをクローンすることもできます。
もしすでに AWS CDK を使用している場合は、 NodeJsFunction コンストラクトの使用を検討してください。このコンストラクトはバンドル手順を抽象化し、内部的に esbuild を使用してコードをバンドルします。
サンプルプロジェクトのビルドとデプロイ
すべてのソースをバンドルしたら、node_modules と元のソースファイルを圧縮するよりもファイルサイズが小さいことに気付くかもしれません。パッケージは100倍以上小さい場合があります。また、初期化も速くなります。
- こちらのサンプルプロジェクトをクローンし、次のコマンドを実行することで、依存関係をインストールし、プロジェクトをビルドしてアプリケーションをパッケージ化します。
これにより、dist ディレクトリとプロジェクトルートに zip アーティファクトが生成されます。dist/ddbHandler.zip と unoptimized.zip のサイズの違いを比較すると、バンドルされていないアーティファクトは 10 倍以上もサイズが大きくなっています。展開すると、依存関係を含むコードのサイズは19Mbを超えますが、バンドル・軽量化されたものでは2.1Mbです。
この ddBHandler の例では、依存関係にある AWS SDK DynamoDB モジュールに複数のファイルとリソースが含まれているため、影響が大きくなっています。
- アプリケーションをデプロイするには、以下を実行します。
結果の測定と比較
デプロイ後はコールドスタートのパフォーマンスも大幅に改善することが分かります。Artillery を使用して Lambda 関数の負荷テストを行うことができます。下記のコマンドでは、デプロイ時に出力される URL で対象を置き換えてください。
バンドルされていない Lambda 関数の負荷テスト
バンドルされた Lambda 関数の負荷テスト
結果を CloudWatch Insights で確認するには、2 つの関数のロググループを選択し、次のクエリを実行します。
下記の結果を見ると、DDBV3x86 のコールドスタート呼び出しは 551 ミリ秒(p90)で実行されますが、ddbvzTopLevelX86Unbundled では 945 ミリ秒(p90)で実行されます。つまり軽量化してバンドルした AWS SDK V3 バージョンでは、コールドスタートが約 1.7 倍高速になり、ウォームスタート時のパフォーマンスも高速になることが分かりました。
まとめ
この記事では、コードをバンドルして軽量化することで、Node.js のコールドスタートのパフォーマンスを最大 70% 向上させる方法を学びました。また、JavaScript 用 AWS SDK の別バージョンを提供する方法と、依存関係やそのインポート方法が Node.js Lambda 関数のパフォーマンスに影響を与えることも学びました。最高のパフォーマンスを実現するには、AWS SDK for JavaScript V3 を使用し、コードをバンドルして軽量化し、トップレベルのインポートは避けてください。
その他のサーバーレス学習リソースについては、サーバーレスを始めようや Serverless Land をご覧ください。
この記事はシニアパートナーソリューションアーキテクトのリチャード・デイヴィッドソンが執筆し、パブリックセクターソリューションアーキテクトの松井佑馬が翻訳しました。