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.loadSync
の Options
オブジェクト内容として扱われる。
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
パッケージのインターフェースを事前に触っていると取っつきやすいように感じる。