daikiojm’s diary

ブログを書くときがやってきたどん!

Nest.jsでgRPCサービスのハンドリング

Nest.jsでは microservices パッケージの1機能として、gRPCによる通信をサポートしている。

Nest.jsでgRPCのサーバ実装をされてる方がいて、この記事がすごく参考になった。 https://qiita.com/jnst/items/27b6a0cd3813b34f98e4

microservices パッケージにはクライアント実装のラッパーも含まれてるようなので、今回はこれを試してみる。
Nest.js公式の sampleの実装 をみるとなんとなく雰囲気はわかる。

サーバ

上記で挙げたQiitaの記事のサンプルリポジトリGitHubに公開されていたので、こちらを使わせてもらった。
以下の .proto に定義されたサービスをハンドリングする前提で進める。

https://github.com/jnst/x-nestjs-grpc/blob/master/protos/rpc/rpc.proto

プロジェクトの雛形作成

$ node -v
v10.14.1
$ yarn global add @nestjs/cli

必要なパッケージをインストール

$ yarn add @nestjs/microservices @grpc/proto-loader grpc protobufjs

今回はNest CLIでプロジェクトを作成し、rest moduleというモジュールを定義した。

$ nest new x-nestjs-grpc-client
$ nest g mo rest

main.ts でhttp待受ポートを変更

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // Serverに使うプロジェクトが3000を使っているのでずらした
  await app.listen(3001);
}
bootstrap();

ClientOption

@Client Decoratorに渡すconfigは以下の通り。

import { ClientOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';

const protoDir = join(__dirname, '..', 'protos');
export const grpcClientOptions: ClientOptions = {
  transport: Transport.GRPC,
  options: {
    url: '0.0.0.0:5000',
    package: 'rpc',
    protoPath: '/rpc/rpc.proto',
    loader: {
      keepCase: true,
      longs: Number,
      enums: String,
      defaults: false,
      arrays: true,
      objects: true,
      includeDirs: [protoDir],
    },
  },
};

options.loader 以下のオブジェクトは protoLoader.loadSyncOptions オブジェクト内容として扱われる。

gRPCサーバの機能をREST APIとして提供するコントローラー

今回のサンプルでは、Nest.jsで作成したWebサーバから別のgRPCサーバを呼び出して、REST APIとしてその機能を提供するというユースケースを想定している。

以下は、ControllerからgRPCをハンドリングするサービスの機能を呼び出している部分。 @Client DecoratorでgRPCクライアントを取得して、ライフサイクルフック onModuleInit のタイミングで、実際に使用するサービスを動的に取得している。
(このライフサイクルフックを使わずにConstructor内でサービスの取得を行おうとしたところエラーが発生したので、おとなしく使ったほうが良さそう)

import { Controller, Get, Query, OnModuleInit } from '@nestjs/common';
import { Client, ClientGrpc } from '@nestjs/microservices';
import {
  RpcService,
  Empty,
  GetChampionResponse,
  GetChampionRequest,
  ListChampionsResponse,
  GetBattleFieldResponse,
} from '../../types';
import { grpcClientOptions } from '../grpc-client.options';

@Controller('rest')
export class RestController implements OnModuleInit {
  @Client(grpcClientOptions) private readonly client: ClientGrpc;
  private rpcService: RpcService

  onModuleInit(): void {
    this.rpcService = this.client.getService<RpcService>('Rpc');
  }

  @Get('champion')
  async getChampion(
    @Query() request: GetChampionRequest,
  ): Promise<GetChampionResponse> {
    return this.rpcService.getChampion(request);
  }

  @Get('champions')
  async getChampions(@Query() request: Empty): Promise<ListChampionsResponse> {
    return this.rpcService.listChampions(request);
  }

  @Get('battle_field')
  async getBattleField(
    @Query() request: Empty,
  ): Promise<GetBattleFieldResponse> {
    return this.rpcService.getBattleField(request);
  }
}

実際の利用場面では、gRPCサーバから取得した情報をREST サーバー側で加工してからレスポンスするパターンなどが想定される。
その場合、今回のように直接コントローラーでgRPC Clientを扱うのではなくService層で扱うことになるかと思う。

動作確認

次のように、gRPCサーバーの内容をREST APIとして呼び出せるようになっていることがわかる。

$ curl -s 'localhost:3001/rest/champion?champion_id=1' | jq
{
  "champion": {
    "champion_id": 1,
    "type": 3,
    "name": "Akali",
    "message": "If you look dangerous, you better be dangerous."
  }
}
$ curl -s 'localhost:3001/rest/champions' | jq
{
  "champions": [
    {
      "champion_id": 1,
      "type": "ASSASSIN",
      "name": "Akali",
      "message": "If you look dangerous, you better be dangerous."
    },
    {
      "champion_id": 2,
      "type": "MAGE",
      "name": "Kennen",
      "message": "The Heart of the Tempest beats eternal...and those beaten remember eternally."
    },
    {
      "champion_id": 3,
      "type": "FIGHTER",
      "name": "Tryndamere",
      "message": "Rage is my weapon."
    }
  ]
}
$ curl -s 'localhost:3001/rest/battle_field' | jq
{
  "battle_field": {
    "battle_field_id": 2,
    "name": "The Twisted Treeline",
    "description": ""
  }
}

終わりに

Nest.jsの microservices パッケージ自体、まだ発展途上な感じられる(インターフェースは定義されいるが実装がされていないなど)が、Nest.jsライクな操作(Decoratorでサービスのメソッドを定義するなど)でgRPCのマイクロサービスを構築できるのはかなりのメリットだと思うので、今後も使っていきたい。Server, Clientともにnodeの grpc パッケージのインターフェースを事前に触っていると取っつきやすいように感じる。

リポジトリはこちら
https://github.com/daikiojm/x-nestjs-grpc-client