見出し画像

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

公式サイトでは特徴が次のように紹介されています。

  1. モジュラーアーキテクチャによる柔軟性の提供

  2. あらゆる形式のサーバーサイドアプリケーションに対応

  3. 最新の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/


執筆者プロフィール:shinagawa
SHIFT DAAE部の開発エンジニア。.bashrcで1日遊べます。
AWSもなんもわからん。

お問合せはお気軽に
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/