NestJSのMicroservices機能を使ってマイクロサービスアーキテクチャのサンプルアプリケーションを実装する
はじめに
マイクロサービスアーキテクチャの導入を検討する中で、NestJSのMicroservices機能を技術検討したので、この記事で紹介いたします。
NestJSを使って、Rest APIを実装はたくさん行ってきましたが、Micorosevices機能を動かしてみるのは初めてでした。Rest APIの実装とは実装方法が大きくことなるため、キャッチアップには一定の壁がある印象でした。特にNestJSのMicroservices機能に関する日本語の情報は少ない印象(2024年03月05日)なので、これか開発される方の参考になれば幸いです。
マイクロサービスアーキテクチャとは
マイクロサービスアーキテクチャの説明については世の中にたくさん情報があるので、今回は割愛します。
Microsoft Learnの「マイクロサービスとは」がわかりやすかったのでリンクと一部引用を転載します。
- マイクロサービスは小さく、独立的で、疎結合しています。 小規模な 1 つの開発者チームでサービスを作成および管理できます。
- 各サービスは個別のコードベースであり、小規模な開発チームで管理できます。
- サービスは個別にデプロイできます。 チームは、アプリケーション全体を再構築したり再デプロイしたりすることなく、既存のサービスを更新できます。
- サービスはサービスのデータや外部の状態を保持する役割を担います。 これは、個別のデータ層でデータを保持する従来のモデルと異なる点です。
- サービスは、明確に定義された API を使用して、互いに通信します。 各サービス内部の実装の詳細は、他のサービスに開示されません。
- ポリグロット プログラミングをサポートします。 たとえば、サービスは、同じテクノロジ スタック、ライブラリ、またはフレームワークを共有する必要はありません。
NestJSのMicroservices機能とは
NestJSはマイクロサービスアーキテクチャ構築のための機能をサポートしています。
In addition to traditional (sometimes called monolithic) application architectures, Nest natively supports the microservice architectural style of development. Most of the concepts discussed elsewhere in this documentation, such as dependency injection, decorators, exception filters, pipes, guards and interceptors, apply equally to microservices. Wherever possible, Nest abstracts implementation details so that the same components can run across HTTP-based platforms, WebSockets, and Microservices. This section covers the aspects of Nest that are specific to microservices.****
NestJSのMicroservicesを使ったマイクロサービスアーキテクチャの構築
今回はサンプルアプリケーションとして、1つのAPI Gatewayと2つのマイクロサービスを構築していきたいと思います。
「新規ユーザー登録」、「(ユーザー登録完了)メール送信」、「ユーザー一覧取得」、「メール送信履歴取得」の4つの機能を用意し、新規ユーザー登録からメール送信
、登録済みユーザーリスト取得
、メール送信履歴の取得
の3つのシナリオを想定したアプリケーションを構築します。
上記をマイクロサービスアーキテクチャを用いて構築する場合、アプリケーション構成は下記の通りとなります。
- API Gateway
- バックエンドのインターフェースとして、3つのRest APIを用意します
- 新規ユーザー登録
- ユーザーリスト取得
- メール送信履歴取得
- バックエンドのインターフェースとして、3つのRest APIを用意します
- Microservice A(User)
- Userに関する2つのAPIを用意します
- ユーザーリスト取得
- 新規ユーザー登録
- Userに関する2つのAPIを用意します
- Microservice B(Mail)
- Mailに関する2つのAPIを用意します
- メール送信
- メール送信履歴取得
- Mailに関する2つのAPIを用意します
クライアントからのリクエストを受けるために、API GatewayでRest APIを用意します。そして、ユーザーに関する機能をMicroservice A(User)、メールに関する機能をMicroservice B(Mail)で構築することでマイクロサービスアーキテクチャを用いた設計を表現します。
NestJSのmicroservices実装ガイド
では実際にどのように実装していくのか、ポイントをおさせて解説してきます。
サンプルコードをGitHubへアップしたのでそちらも合わせてご参考にしていただければと思います。
r-knm/nestjs-microservices-sample
@nestjs/microservices
のインストールする
NestJSのMicroservices機能と使う場合、通常のRest API実装をする場合のNestJS関連のパッケージに加え、@nestjs/microservicesのインストールが必要です。
API Gatewayのアプリケーションにマイクロサービス化した通信先のアプリケーションを登録する
ClientModuleクラスのregisterメソッドを使って通信先マイクロサービスアプリケーションを登録します。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ClientsModule, Transport } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{
name: 'USER_SERVICE',
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
},
{
name: 'MAIL_SERVICE',
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3002,
},
},
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
NestJSアプリケーションをマイクロサービスアプリケーションとして起動する
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
},
);
app.listen();
}
bootstrap();
※ mail-service/src/main.ts
でも同様の記述を追加する必要があります
通信パターンを実装する
NestJSのマイクロサービス機能として「Messagesパターン」と「Eventsパターン」の2つの通信パターンが用意されています。
Messagesパターン
(NestJS公式Docより)Request-response
The request-response message style is useful when you need to exchange messages between various external services. With this paradigm, you can be certain that the service has actually received the message (without the need to manually implement a message ACK protocol).
サービス間でリクエストとレスポンスのやり取りをする場合は「Messagesパターン」を採用します。
今回のサンプルアプリケーションだと以下の通信でMessagesパターンを採用しています。
- API GatewayとMicroservice A(User): ユーザーリスト取得
- API GatewayとMicroservice B(Mail): メール送信履歴取得
[リクエスト側]
import { Inject, Injectable } from '@nestjs/common';
import { CreateUserRequestDto } from './create-user.dto';
import { ClientProxy } from '@nestjs/microservices';
import { CreateUserEvent } from './create-user.event';
import { GetUsersRequestDto } from './get-users.dto';
import { GetSendEmailHistoryRequestDto } from './get-send-email-history.dto';
@Injectable()
export class AppService {
constructor(
@Inject('MAIL_SERVICE') private readonly mailServiceClient: ClientProxy,
@Inject('USER_SERVICE') private readonly userServiceClient: ClientProxy,
) {}
createUser(dto: CreateUserRequestDto) {
this.userServiceClient.emit(
'create-user',
new CreateUserEvent(dto.name, dto.email),
);
}
async getUsers(): Promise<GetUsersRequestDto> {
const observable = this.userServiceClient.send<
{
id: number;
name: string;
email: string;
createdAt: Date;
}[]
>({ cmd: 'get-users' }, {});
return { users: await observable.toPromise() };
}
async getEmailHistory(): Promise<GetSendEmailHistoryRequestDto> {
const observable = this.mailServiceClient.send<
{
toUser: {
id: number;
email: string;
name: string;
};
status: 'success' | 'error';
ts: number;
}[]
>({ cmd: 'get-send-email-history' }, {});
return { history: await observable.toPromise() };
}
}
- マイクロサービス名を指定し、ClientProxyクラスをDIします
- Messagesパターンのリクエストは、sendメソッドを用いて実施します
- cmdというプロパティにコマンド名を登録し、レスポンス側のマイクロサービスとハンドリングをします
[レスポンス側]
import { Controller } from '@nestjs/common';
import { AppService } from './app.service';
import { EventPattern, MessagePattern } from '@nestjs/microservices';
import { CreateUserEvent } from './create-user.event';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@EventPattern('create-user')
handleUserCreate(data: CreateUserEvent) {
return this.appService.handleUserCreate(data);
}
@MessagePattern({ cmd: 'get-users' })
async getUsers(): Promise<
{ id: number; name: string; email: string; createdAt: Date }[]
> {
return await this.appService.getUsers();
}
}
- Messagesパターンのリクエスト受け取りには@MessagePatternデコレーターを用います
- cmdプロパティには、リクエスト側で設定したコマンド名を登録します
- あとは、レスポンスする値をリターンしたら処理構築は完了です
※ メール送信履歴取得のMessagesパターンはサンプルリポジトリにて実装コードを確認できます
Eventsパターン
While the request-response method is ideal for exchanging messages between services, it is less suitable when your message style is event-based - when you just want to publish events without waiting for a response. In that case, you do not want the overhead required by request-response for maintaining two channels.
レスポンスを必要としない片一方のサービスからのメッセージ送信をする場合は「Eventパターン」を採用します。
今回のサンプルアプリケーションだと以下の通信でEventsパターンを採用しています。
- API GatewayとMicroservice A(User): 新規ユーザー登録
- Microservice A(User)とMicroservice B(Mail): メール送信
[リクエスト送信側]
import { Inject, Injectable } from '@nestjs/common';
import { CreateUserRequestDto } from './create-user.dto';
import { ClientProxy } from '@nestjs/microservices';
import { CreateUserEvent } from './create-user.event';
import { GetUsersRequestDto } from './get-users.dto';
import { GetSendEmailHistoryRequestDto } from './get-send-email-history.dto';
@Injectable()
export class AppService {
constructor(
@Inject('MAIL_SERVICE') private readonly mailServiceClient: ClientProxy,
@Inject('USER_SERVICE') private readonly userServiceClient: ClientProxy,
) {}
createUser(dto: CreateUserRequestDto) {
this.userServiceClient.emit(
'create-user',
new CreateUserEvent(dto.name, dto.email),
);
}
async getUsers(): Promise<GetUsersRequestDto> {
const observable = this.userServiceClient.send<
{
id: number;
name: string;
email: string;
createdAt: Date;
}[]
>({ cmd: 'get-users' }, {});
return { users: await observable.toPromise() };
}
async getEmailHistory(): Promise<GetSendEmailHistoryRequestDto> {
const observable = this.mailServiceClient.send<
{
toUser: {
id: number;
email: string;
name: string;
};
status: 'success' | 'error';
ts: number;
}[]
>({ cmd: 'get-send-email-history' }, {});
return { history: await observable.toPromise() };
}
}
- マイクロサービス名を指定し、ClientProxyクラスをDIします
- Messagesパターンのリクエストと同様に、Eventsパターンでもsendメソッドを用いて実施します
- cmdというプロパティにコマンド名を登録し、リクエスト受信側のマイクロサービスとハンドリングをします
[リクエスト受信側]
import { Controller } from '@nestjs/common';
import { AppService } from './app.service';
import { EventPattern, MessagePattern } from '@nestjs/microservices';
import { SendEmailEvent } from './send-email.event';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@EventPattern('send-email')
handleSendEmail(data: SendEmailEvent) {
return this.appService.sendEmail(data);
}
@MessagePattern({ cmd: 'get-send-email-history' })
async getSendEmailHistory(): Promise<
{
toUser: {
id: number;
email: string;
name: string;
};
status: 'success' | 'error';
ts: number;
}[]
> {
return await this.appService.getSendEmailHistory();
}
}
- Eventsパターンのリクエスト受け取りには@EventPatternデコレーターを用います
- デコレーターの第一引数には、リクエスト送信側で設定したコマンド名を登録します
- あとは、レスポンスする値をリターンしたら処理構築は完了です
※ メール送信のMessagesパターンはサンプルコードのリポジトリにて実装コードを確認できます
NestJSを使ったMicroservicesの実装コード解説は以上になります。
あとはサンプルコードをGitHubへアップしたのでそちらも合わせてご参考にしていただければと思います。
r-knm/nestjs-microservices-sample
いくつかの疑問ポイント解消のための調査
技術検討をしながら疑問に感じたポイントを解消するため、追加調査した内容を記載します。
マイクロサービスへの通信で(HTTPではなく)TCPやgRPCを採用するのはなぜ?
主なメリットとして下記が挙げられる
-
パフォーマンスと効率性
- HTTP/HTTPS経由のRest APIと比較して、gRPCやTCPを用いることで高速で効率的な通信が可能になる。
-
ストリーミングサポート
- gRPCなどのプロトコルでは、Request-Responseだけでなく、双方向ストリーミング通信をサポートしているため、リアルタイム通信やイベント駆動型処理の実現がしやすい
-
エコシステム
- gRPCなどのプロトコルは、サーバー間通信を実現するための機能が豊富に提供されているためエコシステムの恩恵を受けることができる
NestJSを使ったマイクロサービスアーキテクチャ構築ではどの技術を採用する?
(独断と偏見を多分に含みますが、)今回の技術検討を経て、実際にプロダクト開発をする場合は下記の構成を検討したいと考えています。
- API GatewayはGraphQLを採用し、BFFパターンを構築する
- Microserviceとのサーバー間通信では、gRPCを採用しエコシステムの恩恵を受ける
そして、これらはNestJSですべて実装可能となります。
おわりに
今回の技術検討では、NestJSのMicroservicesの検討を行いました。
NestJSはバックエンドのフルスタックフレームワークとして、Rest APIやGraphQLをはじめMicroservicesのためのサーバー間通信を高いレベルでサポートしていることがわかりました。また、NestJSを使うことで、API GatewayとMicroservicesを含むマイクロサービスアーキテクチャにおけるアプリケーションを同一フレームワークで構築できるため、初期導入において大きなメリットを受けることができると感じました。
Discussion