Serverless NestJS


NestJSを利用したLambda&API GatewayのServerlessをやってみます。

注意点としては、NestJSはExpress.jsと比較すると重いフレームワーク(フルスタック)なので、起動に時間がかかってしまい、あまりサーバレスに向いているとは言えないです。
が、個人で利用する場合や社内系のアプリケーションの場合などは、サーバレスだと安くて試しやすいと思うので、是非使ってみてください。

NestJSプロジェクト作成

いつものやつです。
ひとまずローカルでHello Worldができる状態にします。

プロジェクト作成

$ nest new nest-serverless-test

動作確認

$ cd nest-serverless-test
$ npm run start
 ~ 中略 ~
[Nest] 3935   - 2020-02-29 19:19:07   [NestApplication] Nest application successfully started +1ms
$ curl localhost:3000
Hello World!

Serverlessの設定

ここからServerlessの設定を行っていきます。

パッケージインストール

$ npm install -g serverless
$ npm install i aws-serverless-express
$ npm install i aws-lambda
$ npm install i serverless-offline
$ npm install -D serverless-layers

Lambdaのhandler作成 

Lambdaなので、いつサーバが切り替わるのかわからないことから、expressインスタンスをキャッシュして、起動済みであればキャッシュを利用し起動済みでない場合は新しく起動するという処理になっています。

src/handler.ts
import { Context, Handler } from 'aws-lambda';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Server } from 'http';
import { ExpressAdapter } from '@nestjs/platform-express';
import { createServer, proxy } from 'aws-serverless-express';
import * as express from 'express';

let cachedServer: Server;

async function bootstrapServer(): Promise<Server> {
  const expressApp = express();
  const adapter = new ExpressAdapter(expressApp);  
  const nestApp = await NestFactory.create(AppModule, adapter);
  await nestApp.init();
  return createServer(expressApp);
}

export const handler: Handler = async (event: any, context: Context) => {
  if (!cachedServer) {
    cachedServer = await bootstrapServer();
  }
  return proxy(cachedServer, event, context, 'PROMISE').promise;
};

Serverlessの設定ファイル作成

ルートディレクトリにserverless.yamlを作成して設定を行います。

serverless.yaml
service: nestjs-serverless-test

plugins:
  - serverless-layers
  - serverless-offline

custom:
  defaultStage: dev
  profiles:
    dev: default
    prod: prod
  serverless-layers:
    layersDeploymentBucket: nestjs-serverless-test-bucket

provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  stage: ${opt.stage, 'dev'}
  profile: ${self:custom.profiles.${self:provider.stage}}
  environment:
    SERVERLESS_STAGE: ${self:provider.stage}

package:
  individually: true
  include:
    - dist/**
  exclude:
    - '**'

functions:
  index:
    handler: dist/handler.handler
    events:
      - http:
          path: '/'
          method: any
      - http:
          path: '{proxy+}'
          method: any

ローカルテスト

serverless-offlineを利用することでローカルでもテスト可能になりますので、試しに実行してみます。

$ npm run build
$ sls offline
Serverless: Starting Offline: dev/ap-northeast-1.

Serverless: Routes for index:
Serverless: ANY /
Serverless: ANY /{proxy*}
Serverless: POST /{apiVersion}/functions/nestjs-serverless-test-dev-index/invocations
$ curl localhost:3000
Hello World!

これで動作確認ができました。
本番にデプロイしていきます。

デプロイ

S3のBucket作成

serverless-layersではs3バケットを利用するので、バケットを作成しておきます。
serverless.yamlのserverless-layers->layersDeploymentBucketで指定したバケット名を指定します。

$ aws s3 mb s3://nestjs-serverless-test-bucket
make_bucket: nestjs-serverless-test-bucket

AWSへのデプロイ

nestをbuildしてからdeployを行う必要があります。

$ nest build 
$ sls deploy
 ~ 中略 ~
Serverless: Stack update finished...
Service Information
service: nestjs-serverless-test
stage: dev
region: ap-northeast-1
stack: nestjs-serverless-test-dev
resources: 12
api keys:
  None
endpoints:
  ANY - https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/
  ANY - https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/{proxy+}
functions:
  index: nestjs-serverless-test-dev-index
layers:
  None
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/
Hello World!

endpointsに記載されているURLにアクセスできれば無事デプロイ完了です。

ちなみに本番環境へのリリースはsls deploy --stage prodです。

環境変数を渡す

この後はもう少し実践的な内容になります。

環境変数を設定してデプロイする方法です。

Serverlessの設定ファイル作成

今回は環境変数を設定ファイルに書き出しておいて、それを読み取りLambdaの環境変数に渡してあげるという方式でいきます。

serverless.yaml
service: nestjs-serverless-test

plugins:
 ~ 同じなので省略 ~

custom:
  defaultStage: dev
  profiles:
    dev: default
    prod: prod
  # ここを追加する
  otherfile:
    environment:
      dev: ${file(./conf/dev/env.yml)}
      prod: ${file(./conf/prod/env.yml)}
  serverless-layers:
    layersDeploymentBucket: nestjs-serverless-test-bucket

provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  stage: ${opt.stage, 'dev'}
  profile: ${self:custom.profiles.${self:provider.stage}}
  environment:
    SERVERLESS_STAGE: ${self:provider.stage}
    # ここを追加する
    TEST_ID: ${self:custom.otherfile.environment.${self:provider.stage}.TEST_ID}

package:
 ~ 同じなので省略 ~

functions:
 ~ 同じなので省略 ~

追加したのは、custom配下にotherfileを設定しました。
ここにprofileごとのファイルを指定しました。

また、provider->environmentに環境変数に追加したいものを指定して、otherfileに指定したファイルを読み取るように設定します。

環境変数の読み取り

パッケージインストール

$ npm i --save @nestjs/config

環境変数をNestアプリケーションに登録する

src/config/configuration.ts
export default () => ({
  test_id: process.env.TEST_ID
});

moduleでconfiguration読み込み

ConfigModule.forRootにloadプロパティを設定し、先ほど作成したconfiguration.tsファイルを読み込みます。

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

環境変数を利用する

controllerのconstructorにConfigServiceを指定してDIさせます。
あとは、利用したいときにconfigServiceのgetメソッドを利用してconfiguration.tsファイルに設定したキーを指定して取得します。
これで環境変数が取得できます。

src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ConfigService } from '@nestjs/config';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService,
              private readonly configService: ConfigService) {}

  @Get()
  getHello(): string {
    const testId = this.configService.get<string>('test_id');
    return this.appService.getHello() + ', TEST ID:' + testId;
  }
}

あとはデプロイして実行してみてTEST IDが返却されたらOKです。

$ nest build 
$ sls deploy
 ~ 中略 ~
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/
Hello World!, TEST ID:dev_test

環境削除

念のためAWSに作成した環境の削除も記載しておきます。

APIGateway+Lambda削除
$ sls remove
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack removal progress...
.....................
Serverless: Stack removal finished...
Serverless: [LayersPlugin]: Removing layer version: 2
Serverless: [LayersPlugin]: Removing layer version: 1
Serverless: [LayersPlugin]: Layers removal finished.
S3バケットの削除
$ aws s3 rb s3://nestjs-serverless-test-bucket --force
delete: s3://nestjs-serverless-test-bucket/serverless/nestjs-serverless-test/dev/layers/nestjs-serverless-test-dev.zip
delete: s3://nestjs-serverless-test-bucket/serverless/nestjs-serverless-test/dev/layers/package.json
remove_bucket: nestjs-serverless-test-bucket