NestJS で作成したRESTfull APIをAWS Lambda + API Gatewayで構成する
はじめに
株式会社SHIFT DAAE(ダーエ)部で開発エンジニアをしている Shinagawaです。
今回は、NestJS で作成したRESTfull APIをAWS Lambda + API Gatewayで動作させてみました。(※1)
また、AWS CDKで構築してみました。
環境
この検証は以下の環境で実施しています。
Node.js: 16
AWS CDK: 2.13.0
NestJS: 8.0
TypeScript: 3.9
NestJSについて
NestJS - A progressive Node.js framework
公式サイトでは特徴が次のように紹介されています。
モジュラーアーキテクチャによる柔軟性の提供
あらゆる形式のサーバーサイドアプリケーションに対応
最新のJSの機能を利用し、デザインパターンと成熟した技術をnode.jsで実現
利用した感想としては、 デコレータを利用してルーティングやリクエストパラメータを定義できるため、 SpringBootやFastAPIの様なデコレータを利用したフレームワークの利用経験があれば親しみやすいなと思いました。
構成
次のようなポイントで構築します。
API GatewayをAPIのエンドポイントとする
API Gatewayのベースパス/v1/{proxy+} へのリクエストを Lambdaにプロキシ(※2)するよう設定
LambdaでNestJSアプリケーションを起動し、リクエストを処理
API GatewayでCORS対応を実施
アプリケーション作成
プロジェクト初期化
Nest CLIを利用してプロジェクトを初期化します。
$ npm i -g @nestjs/cli
$ nest new test-app
インストールの際に使用するツールのオプションを聞かれるので目的に合わせて適当に入力します。
プロジェクト初期化が終わると以下のようにファイルが生成されます。
npm run startで開発サーバを開始することができます。
.
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts
この状態ではLambdaで動作させることが出来ず、handler関数を設定する必要があります。
アプリケーション
後の動作確認のために適当にエンドポイントを実装しておきます。
// src/app.controller.ts
import { Controller, Get, Post, Patch } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('foo')
foo() {
return {
message: 'foo',
};
}
@Post('foo/bar')
bar() {
return {
message: 'bar',
};
}
@Patch('foo/bar/baz')
baz() {
return {
message: 'baz',
};
}
}
handlerの実装
handler関数を実装します。
まず、aws-serverless-expressをインストールします。
npm install aws-serverless-express
main.tsを次のように実装します。
// src/main.ts
import { Server } from 'http';
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import * as express from 'express';
const binaryMimeTypes: string[] = [];
let cachedServer: Server;
async function bootstrapServer(): Promise<Server> {
if (!cachedServer) {
const expressApp = express();
const nestApp = await NestFactory.create(
AppModule,
new ExpressAdapter(expressApp),
);
nestApp.setGlobalPrefix('v1'); // API のベースパス。詳細後述
nestApp.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '任意のオリジン')
res.header('Access-Control-Allow-Headers', '任意のHTTPヘッダ')
res.header(
'Access-Control-Allow-Methods',
'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'
)
next()
});
await nestApp.init();
cachedServer = createServer(expressApp, undefined, binaryMimeTypes);
}
return cachedServer;
}
// Lambdaに渡されるhandler
export const handler = async (event: any, context) => {
cachedServer = await bootstrapServer();
return proxy(cachedServer, event, context, 'PROMISE').promise;
};
これでアプリケーションとしての実装は完了です。
AWSリソース構築・デプロイ
CDKプロジェクト初期化
こちらを参考にしてTypeScriptでCDKプロジェクトを初期化します。
CDK実装
Stackを次のように修正します。
// lib/*-stack.ts
import {
Stack,
StackProps,
Duration,
aws_lambda as lambda,
aws_apigateway as apigw,
} from "aws-cdk-lib";
import { Construct } from "constructs";
export class NestAppStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
// Lambda layer
const lambdaLayer = new lambda.LayerVersion(this, `NestApplambdaLayer`, {
code: lambda.Code.fromAsset("src/node_modules"),
compatibleRuntimes: [
lambda.Runtime.NODEJS_16_X,
],
});
// Lambda
const appLambda = new lambda.Function(this, `NestApplambda`, {
runtime: lambda.Runtime.NODEJS_16_X,
code: lambda.Code.fromAsset("src/dist"),
handler: "main.handler",
layers: [lambdaLayer],
environment: {
NODE_PATH: "$NODE_PATH:/opt",
AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1",
},
timeout: Duration.seconds(30),
});
// API Gateway
const restApi = new apigw.RestApi(this, `NestAppApiGateway`, {
restApiName: `NestAppApiGw`,
deployOptions: {
stageName: "v1",
},
// CORS設定
defaultCorsPreflightOptions: {
// warn: 要件に合わせ適切なパラメータに絞る
allowOrigins: apigw.Cors.ALL_ORIGINS,
allowMethods: apigw.Cors.ALL_METHODS,
allowHeaders: apigw.Cors.DEFAULT_HEADERS,
statusCode: 200,
},
});
restApi.root.addProxy({
defaultIntegration: new apigw.LambdaIntegration(appLambda),
anyMethod: true
});
}
}
余談ですが、API Gatewayのベースパスを空で作成することは不可能でした。
API Gatewayのステージング変数は必須となるようです。
NestJSアプリケーション配置
CDKプロジェクトのルートにsrcディレクトリを作成し、先ほど作成したNestJSアプリケーションを移動します。
移動後ビルドします。
cd src
npm run build
デプロイ
次のコマンドでデプロイを実施します。
cdk deploy
CDKのデプロイ完了後、API Gatewayのエンドポイントが払い出されるので、期待通りのレスポンスとなれば成功です。
Tips
その他、開発の中で触れたtipsについて触れていきます。
アップロード上限をAPI Gatewayに合わせる
開発の際に、API Gatewayにはリクエストが通るのに、Lambdaに弾かれるということがありました。
API Gatewayのリクエストサイズの上限は10MBであり、 これに合わせてNestJSのリクエストサイズの上限を引き上げることで対応できました。
具体的には、bodyParserを利用して次のように変更します。
// src/main.ts
import { NestFactory } from '@nestjs/core';
import * as bodyParser from 'body-parser';
function bootstrap() {
const nestApp = await NestFactory.create(AppModule);
// 略
nestApp.use(bodyParser.json({limit: '10mb'}));
// 略
}
webpackによるバンドル
コールドスタートなアーキテクチャでは、利用するファイルサイズが大きくなるほどレイテンシが発生する場合があります。
またLambdaは、ソースコードを圧縮した状態で50MB、展開した状態で250MB以下に抑えなければならない制限もあります。
NestJSでは、Nest CLIに付属しているwebapckのオプション(nest build --webpack)を利用することでアプリケーションをbundleすることができます。
またNestJSのドキュメントではバンドルした場合のパフォーマンス比較について説明されています。
https://docs.nestjs.com/faq/serverless
NestJS で @nestjs/platform-express を利用した場合
となるようです。
参考にしたサイト
※1: 本記事の実装やサンプルコードは本来行うべきセキュリティ対策やエラーハンドリングは省いているのでご承知おきください。
※2: API Gatewayの機能であるAPI GatewayのHTTPプロキシ統合機能の利用
\もっと身近にもっとリアルに!DAAE公式Twitter/
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/