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);
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
パッケージのインターフェースを事前に触っていると取っつきやすいように感じる。
リポジトリはこちら
https://github.com/daikiojm/x-nestjs-grpc-client