Serverless NestJS

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

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

## NestJSプロジェクト作成
いつものやつです。
ひとまずローカルでHello Worldができる状態にします。

### プロジェクト作成
```console:
$ nest new nest-serverless-test
```

### 動作確認
```console:
$ cd nest-serverless-test
$ npm run start
 ~ 中略 ~
[Nest] 3935   - 2020-02-29 19:19:07   [NestApplication] Nest application successfully started +1ms
```

```console:
$ curl localhost:3000
Hello World!
```

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

### パッケージインストール
```console:
$ 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インスタンスをキャッシュして、起動済みであればキャッシュを利用し起動済みでない場合は新しく起動するという処理になっています。

```typescript: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を作成して設定を行います。

```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を利用することでローカルでもテスト可能になりますので、試しに実行してみます。

```console:
$ 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
```

```console:
$ curl localhost:3000
Hello World!
```

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

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

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

### AWSへのデプロイ
nestをbuildしてからdeployを行う必要があります。

```console:
$ 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
```

```console:
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/
Hello World!
```

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

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


## 環境変数を渡す
この後はもう少し実践的な内容になります。

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

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

```yaml: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に指定したファイルを読み取るように設定します。

### 環境変数の読み取り

#### パッケージインストール
```console:
$ npm i --save @nestjs/config
```

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

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

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

```typescript: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ファイルに設定したキーを指定して取得します。
これで環境変数が取得できます。

```typescript: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です。

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

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

```console: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.
```

```console: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
```