Contents
NestJS マイクロサービスの概要とセットアップ
NestJS は HTTP、WebSocket、マイクロサービスという 3 種類のトランスポート層を統一的に扱えるフレームワークです。@nestjs/microservices パッケージを導入すれば、gRPC・Kafka・NATS といった業界標準プロトコルでの通信が数行の設定だけで実現できます【1】。本節では、パッケージのインストール手順と最小構成アプリへの組み込み方を解説します。
パッケージのインストール
@nestjs/microservices と依存ライブラリをプロジェクトに追加します。
|
1 2 |
npm i @nestjs/microservices reflect-metadata |
基本的なブートストラップコード
以下は main.ts にマイクロサービス用の設定を組み込む最小例です。gRPC 用オプションだけを書きましたが、Transport を変えるだけで Kafka や NATS へも拡張できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { MicroserviceOptions, Transport, } from '@nestjs/microservices'; import { join } from 'path'; async function bootstrap() { const app = await NestFactory.create(AppModule); // gRPC のマイクロサービス設定 const grpcOptions: MicroserviceOptions = { transport: Transport.GRPC, options: { package: 'order', protoPath: join(__dirname, '../proto/order.proto'), }, }; app.connectMicroservice(grpcOptions); await app.startAllMicroservicesAsync(); await app.listen(3000); } bootstrap(); |
このコードだけで Nest アプリはマイクロサービスとして起動し、localhost:50051(デフォルト)で gRPC エンドポイントが公開されます【2】。
アーキテクチャ設計とサービス分割
大規模システムでは「機能ごとに独立したサービス」を意識して設計することが、信頼性・スケーラビリティ向上の鍵となります。本稿で扱う構成は API Gateway → 認証サービス → 注文サービス → 在庫サービス の 4 層モデルです【3】。各コンポーネントを単一責任の Nest アプリとしてデプロイし、Kubernetes 上で Service と Ingress によって外部アクセスを制御します。
コンポーネント概要
- API Gateway: 外部 HTTP/gRPC エントリポイント。認証トークンの検証とルーティングだけを担当
- Auth Service: JWT 発行・検証、ユーザー情報管理
- Order Service: 受注処理、イベント発行(Kafka/NATS)
- Inventory Service: 在庫確保・減算、在庫更新通知
これらはすべて DNS ベースのサービスディスカバリで相互参照できるようにします。Consul と Kubernetes の連携により、Pod が再スケジュールされても名前解決が自動的に追従します【4】。
Kubernetes Service 定義(例)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
apiVersion: v1 kind: Service metadata: name: order-service spec: selector: app: order ports: - protocol: TCP port: 50051 # gRPC ポート targetPort: 50051 |
上記の order-service は同一名前空間内の他サービスから http://order-service:50051 のように呼び出せます。
通信方式の実装例
マイクロサービス間の通信は 同期 (gRPC) と 非同期 (Kafka/NATS) の二本柱で設計します。ここではそれぞれの実装手順とベストプラクティスを示します。
gRPC 実装
proto 定義 (order.proto)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
syntax = "proto3"; package order; service OrderService { rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse); } message CreateOrderRequest { string userId = 1; repeated string productIds = 2; } message CreateOrderResponse { string orderId = 1; bool success = 2; } |
サーバ側実装 (order.service.ts)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Injectable } from '@nestjs/common'; import { GrpcMethod } from '@nestjs/microservices'; import { randomUUID } from 'crypto'; @Injectable() export class OrderService { @GrpcMethod('OrderService', 'CreateOrder') createOrder(data: { userId: string; productIds: string[] }) { const orderId = randomUUID(); return { orderId, success: true }; } } |
クライアント側呼び出し (order.client.ts)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { Injectable, Inject } from '@nestjs/common'; import { ClientGrpc } from '@nestjs/microservices'; import { Observable } from 'rxjs'; import { OrderServiceClient } from './proto/order.pb'; @Injectable() export class OrderClient { private readonly orderSvc: OrderServiceClient; constructor(@Inject('GRPC_CLIENT') client: ClientGrpc) { this.orderSvc = client.getService<OrderServiceClient>('OrderService'); } create(userId: string, productIds: string[]): Observable<any> { return this.orderSvc.createOrder({ userId, productIds }); } } |
mTLS(相互 TLS)設定
- 証明書の用意
bash
# CA の作成
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt
# サーバ証明書
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 -sha256
# クライアント証明書は同様に作成
@grpc/grpc-js
2. **Nest 側のオプション**(のServerCredentials.createSsl を使用)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { readFileSync } from 'fs'; import { ServerCredentials } from '@grpc/grpc-js'; const rootCert = readFileSync('ca.crt'); const keyPair = { private_key: readFileSync('server.key'), cert_chain: readFileSync('server.crt') }; const grpcOptions: MicroserviceOptions = { transport: Transport.GRPC, options: { package: 'order', protoPath: join(__dirname, '../proto/order.proto'), // 正しいオプション名は `credentials` credentials: ServerCredentials.createSsl(rootCert, [keyPair], true), }, }; |
この設定により、クライアントが有効な証明書を提示しない限り接続が拒否されます。相互認証で内部通信の安全性が格段に向上します【5】。
Kafka/NATS を用いたイベント駆動
Kafka モジュール (kafka.module.ts)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; @Module({ imports: [ ClientsModule.register([ { name: 'KAFKA_SERVICE', transport: Transport.KAFKA, options: { client: { brokers: ['kafka-0:9092', 'kafka-1:9092'], }, consumer: { groupId: 'order-consumer' }, }, }, ]), ], exports: [ClientsModule], }) export class KafkaModule {} |
発行側(注文サービス)
|
1 2 3 4 5 6 7 8 9 |
@Injectable() export class OrderPublisher { constructor(@Inject('KAFKA_SERVICE') private readonly client) {} async publishOrderCreated(orderId: string, userId: string) { await this.client.emit('order_created', { orderId, userId }).toPromise(); } } |
購読側(在庫サービス)
|
1 2 3 4 5 |
@MessagePattern('order_created') async handleOrderCreated(@Payload() data: { orderId: string; userId: string }) { await this.inventoryService.decrease(data.productIds); } |
Kafka/NATS のメッセージは Protobuf でシリアライズするとサイズが小さく、serializer: new ProtobufSerializer() を ClientsModule.register に付与すれば自動的に変換できます。
デプロイと運用基盤
ローカル環境の素早い立ち上げと本番 Kubernetes へのデプロイを両方サポートするため、Docker Compose と Helm Chart の二段階アプローチを採ります。
Docker Compose(ローカル開発向け)
以下はすべてのサービスとミドルウェアを網羅した docker-compose.yml です。depends_on を全サービスに記述し、起動順序が明確になるようにしています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
version: '3.8' services: api-gateway: build: ./gateway ports: ["8080:80"] depends_on: - auth - order - inventory auth: build: ./auth environment: - JWT_SECRET=${JWT_SECRET} networks: [backend] depends_on: - consul order: build: ./order env_file: .env networks: [backend] depends_on: - kafka - consul inventory: build: ./inventory networks: [backend] depends_on: - kafka - consul kafka: image: bitnami/kafka:latest environment: KAFKA_BROKER_ID: 1 KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181 ports: ["9092:9092"] networks: [backend] depends_on: - zookeeper zookeeper: image: bitnami/zookeeper:latest environment: ALLOW_ANONYMOUS_LOGIN: "yes" ports: ["2181:2181"] networks: [backend] consul: image: hashicorp/consul:latest command: "agent -dev -client=0.0.0.0" ports: ["8500:8500"] networks: [backend] networks: backend: |
docker compose up -d だけで、API Gateway → 各マイクロサービス → Kafka/Zookeeper → Consul が起動し、ローカルでも本番に近い環境を再現できます。
Helm Chart(本番 Kubernetes)
ディレクトリ構成例
|
1 2 3 4 5 6 7 8 |
helm/ ├─ charts/ │ ├─ api-gateway/ │ ├─ auth-service/ │ ├─ order-service/ │ └─ inventory-service/ └─ values.yaml # 環境横断設定 |
共通 values.yaml の抜粋
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
global: imagePullPolicy: IfNotPresent namespace: prod authService: replicaCount: 3 env: JWT_SECRET: {{ .Values.secrets.jwtSecret }} orderService: replicaCount: 4 grpcPort: 50051 inventoryService: replicaCount: 2 consul: enabled: true serverReplicas: 3 |
Consul と Kubernetes の連携手順
- Consul Helm Chart をインストール(公式チャート)
bash
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install consul hashicorp/consul \
--set server.replicaCount=3 \
--set client.enabled=true - 各マイクロサービスの Deployment に
CONSUL_HTTP_ADDR環境変数を注入
yaml
env:- name: CONSUL_HTTP_ADDR
value: "consul-server.consul.svc.cluster.local:8500"
- name: CONSUL_HTTP_ADDR
- 起動時に Service Registration API を呼び出す Init コンテナ(例)
|
1 2 3 4 5 6 7 8 9 10 |
initContainers: - name: register-consul image: curlimages/curl:7.85.0 command: - sh - -c - | curl -X PUT http://$CONSUL_HTTP_ADDR/v1/agent/service/register \ -d '{"Name":"order-service","Port":50051,"Check":{"GRPC":"localhost:50051","Interval":"10s"}}' |
- Kubernetes の Service 名と Consul の DNS 名前空間を統一
- Consul が提供する
*.service.consulドメインで名前解決できるよう、Pod にdnsPolicy: "ClusterFirstWithHostNet"を設定。
この手順により、Pod の再スケジュールや水平スケーリングが起きても Consul のカタログが自動的に更新され、他サービスは DNS 名だけで最新エンドポイントを取得できます。
信頼性・テスト・モニタリング
マイクロサービスの運用では「障害検知」「リトライ」「回復」の三本柱が重要です。NestJS は公式パッケージ @nestjs/terminus と外部ライブラリ opossum を組み合わせることで、ヘルスチェックとサーキットブレーカーを簡単に実装できます。
ヘルスチェック(Terminus)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Controller, Get } from '@nestjs/common'; import { HealthCheckService, HttpHealthIndicator, HealthCheck, } from '@nestjs/terminus'; @Controller('health') export class HealthController { constructor( private readonly health: HealthCheckService, private readonly http: HttpHealthIndicator, ) {} @Get() @HealthCheck() check() { return this.health.check([ () => this.http.pingCheck('order-service', 'http://order-service:3000/health'), ]); } } |
サーキットブレーカー(opossum)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import CircuitBreaker from 'opossum'; import { HttpService } from '@nestjs/axios'; @Injectable() export class InventoryClient { private readonly breaker: CircuitBreaker<any>; constructor(private readonly http: HttpService) { this.breaker = new CircuitBreaker( async (payload) => this.http.post('http://inventory-service/api/deduct', payload).toPromise(), { timeout: 3000, errorThresholdPercentage: 50, resetTimeout: 10000 }, ); this.breaker.fallback(() => ({ success: false, reason: 'fallback' })); } async deduct(payload: any) { return this.breaker.fire(payload); } } |
ユニットテストと E2E テスト
Nest の TestingModule と Jest を組み合わせれば、DI コンテナをそのままモックできるため高速なテストが可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// order.service.spec.ts describe('OrderService', () => { let service: OrderService; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [OrderService], }).compile(); service = module.get(OrderService); }); it('should create an order and return a UUID', async () => { const result = await service.createOrder({ userId: 'u1', productIds: ['p1'] }); expect(result.orderId).toMatch(/[0-9a-f-]{36}/); expect(result.success).toBe(true); }); }); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// app.e2e-spec.ts describe('Order API (e2e)', () => { let app: INestApplication; beforeAll(async () => { const module = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = module.createNestApplication(); await app.init(); }); it('/order (POST) creates order', async () => { return request(app.getHttpServer()) .post('/order') .send({ userId: 'u1', productIds: ['p2'] }) .expect(201) .expect((res) => { expect(res.body.orderId).toBeDefined(); }); }); }); |
可観測性:Metrics と Logging
Prometheus メトリクス(order.service.ts)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Counter } from 'prom-client'; const orderCreated = new Counter({ name: 'order_created_total', help: 'Total number of created orders', }); @GrpcMethod('OrderService', 'CreateOrder') createOrder(data) { orderCreated.inc(); // ビジネスロジック… } |
Grafana ダッシュボード例
| パネル | メトリクス |
|---|---|
| Panel 1 | order_created_total の時間推移 |
| Panel 2 | gRPC latency(Histogram) |
| Panel 3 | Kafka consumer lag (kafka_consumer_lag) |
Winston + Elastic Stack
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { createLogger, transports, format } from 'winston'; import ElasticsearchTransport from 'winston-elasticsearch'; export const logger = createLogger({ level: 'info', format: format.json(), defaultMeta: { service: 'order-service' }, transports: [ new transports.Console(), new ElasticsearchTransport({ node: process.env.ELASTIC_URL }), ], }); |
構造化ログとメトリクスを同時に取得すれば、Grafana のアラートから Elastic の検索まで一貫した障害対応フローが実現します。
セキュリティベストプラクティス
mTLS と JWT の二層防御
- mTLS: ネットワークレベルで相互認証を行い、通信路の盗聴・改ざんを防止
- JWT: アプリケーションレイヤーでユーザー単位の権限チェックを実装
JWT ガード例
|
1 2 3 4 5 6 |
import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {} |
コントローラに @UseGuards(JwtAuthGuard) を付与すれば、認証済みリクエストのみが処理されます。
機密情報の管理
- ローカル開発:
.envとdotenvパッケージで環境変数をロード - 本番環境: Kubernetes Secret に格納し、Pod の
envFromで注入
|
1 2 3 4 5 6 7 8 |
apiVersion: v1 kind: Secret metadata: name: jwt-secret type: Opaque data: JWT_SECRET: {{ .Values.secrets.jwtSecret | b64enc }} |
コード側では process.env.JWT_SECRET を直接参照し、リポジトリに平文が残らないよう徹底します。
次のステップ
- ローカルで動作確認
bash
docker compose up -d
# すべてのサービスが起動したら curl http://localhost:8080/health を実行 - GitHub リポジトリへクローン(サンプルコード一式を公開)
- Helm Chart を本番クラスターにデプロイ
bash
helm upgrade --install prod ./helm -f values-prod.yaml - Prometheus/Grafana でメトリクス確認、Alertmanager による通知設定
以上の手順を踏めば、NestJS を核にしたマイクロサービス基盤が実務レベルで利用可能になります。継続的インテグレーション・デリバリー(CI/CD)と組み合わせて、コード変更から本番デプロイまで自動化することを推奨します。
参考文献
| 番号 | 出典 |
|---|---|
| 【1】 | NestJS公式マイクロサービスガイド – https://docs.nestjs.com/microservices/basics |
| 【2】 | MicroserviceOptions の型定義と使用例 – https://docs.nestjs.com/microservices/grpc |
| 【3】 | マイクロサービスアーキテクチャのベストプラクティス(Martin Fowler) – https://martinfowler.com/articles/microservices.html |
| 【4】 | Consul と Kubernetes の統合手順 – https://www.consul.io/docs/k8s/installation |
| 【5】 | NestJS での gRPC + mTLS 実装例 – https://docs.nestjs.com/microservices/grpc#secure-grpc-connection |