こんちは、最近コストコに初めて行ったんですが、どれも量が多すぎて結局賞味期限が長いクッキーぐらいしか買えませんでした。
そんな中表題の通り、Nest.jsのアプリケーションをAWS Lambda上で動作させるようにしました。
なんのために?
Nest.jsのアプリケーションをAWSにデプロイする際には、ECS(Fargate)やEC2など様々なパターンが考えられます。コストをできるだけ抑えたいのであれば、ServerlessであるLambdaが候補に上がってくることが多いと思います、ですが、アプリケーションコードをLambda専用で記述する必要が出てきてしまいます。
アプリケーションへのリクエストが次第に増えていくと、Lambdaに比べてEC2やECSなどがコストパフォーマンスに優れるようになるのですが、そのときにアプリケーションのコードをLambdaから分離させるのはそこそこ大変だと思っています。
そこで、アプリケーションをLambdaに(ほぼ)依存させずに通常のWebサーバーとして開発し、EC2やECSに移行が容易にできるようにすることで、この問題を解決できると思っています。
サーバーサイドTypeScriptでこのようなことを可能にするライブラリはすでにいくつかあり、例えばExpressのアプリケーションをCodeGenieApp/serverless-expressというツールでwrappingすることでLambda専用にすることができます。また、Pythonでも同様のものは存在しています。(初学者に優しいスモールスタートで始めるPythonアプリケーションを考えてみた)
私が最近作ってるアプリケーションではNest.jsを利用しているので、これをLambdaで動作できるようにしてみました。
1.Nest.jsのアプリケーションの修正
以下の場合にはLambdaで動作させることができません
- Nest.jsをExpress以外のFW(例えばFastify)上で動作させている場合
- Lambdaデプロイ時のパッケージサイズが250MBを超える場合
基本的にはNest.js公式ドキュメントのFAQ>Serverlessに記載されている対応内容に沿っていきます。
まずは、CodeGenieApp/serverless-expressをインストールします。以前はライブラリ名が@vendia/serverless-express
でしたが最近変わったみたいです。
また、@nestjs/platform-express がインストールされていない場合はこれも必要になるためインストールしましょう
また、公式ドキュメントでaws-lambda
はdependencies
に保存していましたが、後述のパッケージサイズの問題からdevDependencies
が好ましいです。
また、webpackでビルドすることでコールドスタートの最適化が図れますが、トラブルが起こる可能性が高いので、一旦はwebpackを利用せずにデプロイできるところまで進みましょう
npm i @codegenie/serverless-express @nestjs/platform-express
npm i -D aws-lambda @types/aws-lambda
次に、Lambda関数が実行する基点となるファイル(ex. serverless.ts
)を作成します。
中身はNest.jsのentryFile
(通常はmain.ts
)を以下の通り修正する必要があります。
+import * as serverlessExpress from '@codegenie/serverless-express';
+let server: Handler;
async function bootstrap(): Promise<Handler> {
const app = await NestFactory.create(AppModule);
await app.init();
- await app.listen(3000);
+ const expressApp = app.getHttpAdapter().getInstance();
+ // @ts-ignore
+ return serverlessExpress({ app: expressApp });
}
+export const handler: Handler = async (
+ event: any,
+ context: Context,
+ callback: Callback,
+) => {
+ server = server ?? (await bootstrap());
+ return server(event, context, callback);
+};
ちなみに、もしserverlessExpressを公式ドキュメントの手順通りに設定すると実行時にエラーが出てしまいます。CommonJS周りのエラーっぽいのでimport * as serverlessExpress
として、servrlessExpressの初期化は型エラーが出たので@ts-ignore
させてます(多分型定義か何かがおかしいのでしょう)
{
"errorType": "Runtime.UnhandledPromiseRejection",
"errorMessage": "TypeError: (0 , serverless_express_1.default) is not a function",
"reason": {
"errorType": "TypeError",
"errorMessage": "(0 , serverless_express_1.default) is not a function",
"stack": [
"TypeError: (0 , serverless_express_1.default) is not a function",
" at bootstrap (/var/task/dist/src/entrypoint/serverless.js:19:45)"
]
},
"promise": {},
"stack": [
"Runtime.UnhandledPromiseRejection: TypeError: (0 , serverless_express_1.default) is not a function",
" at process.<anonymous> (file:///var/runtime/index.mjs:1276:17)",
" at process.emit (node:events:517:28)",
" at emit (node:internal/process/promises:149:20)",
" at processPromiseRejections (node:internal/process/promises:283:27)",
" at process.processTicksAndRejections (node:internal/process/task_queues:96:32)"
]
}
2.Nest.jsのビルドとLambda関数のデプロイ
Lambdaでは、TypeScriptは実行できないのでNest.jsのアプリケーションのコードはJavascriptにコンパイルする必要があります。
私は今回はCDKでデプロイを考えているので、NodejsFunctionコンストラクタを利用しCDK実行時にビルドするパターンか、あらかじめ手動でビルドするパターンのいずれかが可能です。
NodejsFunction
を利用すると裏側でesbuild
が実行されているので高速にビルドできるのですが、Nest.jsが動作するためには追加の設定が必要そうだったので今回はNest.jsが提供するbuildコマンドを使って手動でビルドすることにしました
npm run nest build
実行後、dist
ディレクトリにコンパイルされたコードが出力されます。
あとはこのdist
ディレクトリとnode_modules
ディレクトリの2つをまとめて1つのzipファイルにすることでデプロイが可能です。
Lambda関数のハンドラーには、上記で作成したファイルのhandler
関数を指定します(ex. dist/src/entrypoint/serverless.handler
)
私の環境ではCDKを利用しているので、以下のように記述してLambdaをデプロイしました。
const mainapp = new lambda.Function(this, 'ftft-mainapp', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'dist/src/entrypoint/serverless.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../../backend/lambda-package')),
environment: {
NO_COLOR: "true"
}
});
ちなみに、NO_COLOR
を設定しないと出力されるログがCloudWatcLogs上で見にくくなってしまいます
[32m[Nest] 8 - [39m01/08/2024, 7:31:58 AM [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mConfigHostModule dependencies initialized[39m[38;5;3m +1ms[39m
デプロイパッケージのサイズについて
Lambdaにデプロイできるzipファイルは最大で250MBです。何も考えずにnode_modules
をまるごと含めると簡単にこれを超えてしまいます。
Lambda の .zip デプロイパッケージの最大サイズは 250 MB (解凍) です。この制限は、Lambda レイヤーを含む、更新するすべてのファイルの合計サイズに適用されることに注意してください。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package.html#python-package-create-update
そのため、私の環境ではいくつか工夫をする必要がありました。
devDependencies
のパッケージの削除
nest build
後にnode_modulesからdevDependencies
に記載された不要なパッケージを削除し、デプロイパッケージのサイズを削減することをお勧めします。以下のコマンドでできるみたいです
npm prune --production
yarn --production
pnpm prune --prod
実行時に不要なパッケージをdevDependencies
に移動
また、上記で述べたように、aws-lambda
などの実行に問題がないパッケージは全てdevDependencies
に保存することでも削減が可能です。私の環境ではtypescript
や
Lambda の .zip デプロイパッケージの最大サイズは 250 MB (解凍) です。この制限は、Lambda レイヤーを含む、更新するすべてのファイルの合計サイズに適用されることに注意してください。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package.html#python-package-create-update
パッケージマネージャーにはpnpm
は利用できなさそう
パッケージマネージャーのpnpm
では、他のものに比べてnode_modulesのサイズが小さくなるとのことなので利用してみましたが、Lambda関数の実行中にnode_modulesのパッケージのimportがうまくいかずエラーが発生してしまいます。
以下でエラーが出てるtslib
は@nestjs/core
の依存するパッケージなので、おそらく外部パッケージがさらに依存するパッケージのimportがpnpmだとうまくいかないのではないかと考えてpnpm i
をpnpm i --shamefully-hoist
に変えてみましたが特に挙動は変化しませんでした。
{
"errorType": "Runtime.ImportModuleError",
"errorMessage": "Error: Cannot find module 'tslib'\nRequire stack:\n- /var/task/node_modules/@nestjs/core/index.js\n- /var/task/dist/src/entrypoint/serverless.js\n- /var/runtime/index.mjs",
"stack": [
"Runtime.ImportModuleError: Error: Cannot find module 'tslib'",
"Require stack:",
"- /var/task/node_modules/@nestjs/core/index.js",
"- /var/task/dist/src/entrypoint/serverless.js",
"- /var/runtime/index.mjs",
" at _loadUserApp (file:///var/runtime/index.mjs:1087:17)",
" at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
" at async start (file:///var/runtime/index.mjs:1282:23)",
" at async file:///var/runtime/index.mjs:1288:1"
]
}
デプロイスクリプト
毎回手動でビルドやパッケージのインストールをするのが面倒だったのでビルド用のスクリプトを作って運用してます
#!/bin/bash -eu
rm -rf node_modules
yarn
yarn run build
yarn --production
rm -rf lambda-package
mkdir lambda-package
cp -R dist lambda-package
cp -R node_modules lambda-package
yarn run start:prod
まとめ
- Lambda関数のデプロイパッケージのサイズに上限があるので、パッケージのサイズに気を使いましょう
- パッケージインストール時にpnpmは避けたほうが良いかもしれません