マイクロサービスアーキテクチャ入門 — モノリスからの移行戦略と実装パターン
「マイクロサービスに移行したい」と考えていませんか?しかし、闇雲に分割すると逆に複雑性が増し、開発速度が落ちることもあります。この記事では、マイクロサービスアーキテクチャの本質を理解し、適切な移行戦略を立てるための実践的な知識を提供します。
マイクロサービスとは何か?
マイクロサービスは、小さく独立したサービスの集合でシステムを構成するアーキテクチャパターンです。
モノリスとの比較
モノリス(従来型):
┌─────────────────────────────────┐
│ Single Application │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ User │ │Order │ │ Pay │ │
│ │ Mgmt │ │ Mgmt │ │ ment │ │
│ └──────┘ └──────┘ └──────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ Database │ │
│ └─────────────┘ │
└─────────────────────────────────┘
マイクロサービス:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ User │ │ Order │ │ Payment │
│ Service │ │ Service │ │ Service │
│ ┌─────┐ │ │ ┌─────┐ │ │ ┌─────┐ │
│ │ DB │ │ │ │ DB │ │ │ │ DB │ │
│ └─────┘ │ │ └─────┘ │ │ └─────┘ │
└───────────┘ └───────────┘ └───────────┘
↕ ↕ ↕
API Gateway / Service Mesh
マイクロサービスの特徴
1. 独立したデプロイ
// User Serviceの更新
// 他のサービスに影響なし
cd user-service
npm run build
docker build -t user-service:v2.0 .
kubectl apply -f deployment.yaml
2. 技術スタックの自由
// User Service - Node.js + PostgreSQL
// user-service/server.ts
import express from 'express';
import { Pool } from 'pg';
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
app.get('/users/:id', async (req, res) => {
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(rows[0]);
});
app.listen(3001);
# Order Service - Python + MongoDB
# order-service/server.py
from fastapi import FastAPI
from motor.motor_asyncio import AsyncIOMotorClient
app = FastAPI()
client = AsyncIOMotorClient(os.environ['MONGO_URL'])
db = client.orders
@app.get("/orders/{order_id}")
async def get_order(order_id: str):
order = await db.orders.find_one({"_id": order_id})
return order
3. 独立したスケーリング
# Kubernetes HPA (Horizontal Pod Autoscaler)
# order-service/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
注文サービスだけを10台にスケールアウトできます。
メリットとデメリット
メリット
1. 障害の局所化
// Payment Serviceがダウンしても
// User ServiceとOrder Serviceは稼働し続ける
// Circuit Breaker パターン
import CircuitBreaker from 'opossum';
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000
};
const breaker = new CircuitBreaker(async (orderId) => {
const response = await fetch(`http://payment-service/charge/${orderId}`);
return response.json();
}, options);
breaker.fallback(() => ({ status: 'pending', message: 'Payment service temporarily unavailable' }));
app.post('/orders/:id/pay', async (req, res) => {
try {
const result = await breaker.fire(req.params.id);
res.json(result);
} catch (error) {
res.status(503).json({ error: 'Service unavailable' });
}
});
2. チームの独立性
Team A → User Service
Team B → Order Service
Team C → Payment Service
各チームが独立してリリース可能
3. 技術負債の局所化
// 古いUser Serviceを段階的にリプレース
// 他のサービスに影響なし
// Old User Service (Express + MySQL)
// → New User Service (Fastify + PostgreSQL)
// API互換性を保てば段階的移行可能
デメリット
1. 分散システムの複雑さ
// トランザクション管理が困難
// モノリスなら1つのDB Transaction
// マイクロサービスでは Saga パターンが必要
class OrderSaga {
async createOrder(userId: string, items: CartItem[]) {
let orderId: string;
let paymentId: string;
try {
// Step 1: Create order
orderId = await orderService.createOrder(userId, items);
// Step 2: Reserve inventory
await inventoryService.reserve(orderId, items);
// Step 3: Process payment
paymentId = await paymentService.charge(userId, orderId);
// Step 4: Confirm order
await orderService.confirm(orderId);
return { orderId, paymentId };
} catch (error) {
// Compensating transactions (ロールバック)
if (paymentId) await paymentService.refund(paymentId);
if (orderId) await inventoryService.release(orderId);
if (orderId) await orderService.cancel(orderId);
throw error;
}
}
}
2. ネットワークのオーバーヘッド
// モノリス: 関数呼び出し (μs)
const user = getUserById(123);
// マイクロサービス: HTTPリクエスト (ms)
const user = await fetch('http://user-service/users/123').then(r => r.json());
3. デバッグの難しさ
// 分散トレーシングが必須
import { trace, context } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter()));
provider.register();
app.get('/orders/:id', async (req, res) => {
const span = trace.getTracer('order-service').startSpan('get-order');
const order = await orderService.findById(req.params.id);
const user = await userService.findById(order.userId); // 別サービス呼び出し
span.end();
res.json({ order, user });
});
4. データの一貫性
// 結果整合性(Eventual Consistency)を受け入れる必要がある
// ユーザーサービスで削除したユーザーが
// 一時的に注文サービスに残る可能性がある
// イベント駆動で整合性を保つ
import { EventEmitter } from 'events';
const eventBus = new EventEmitter();
// User Service
userService.deleteUser(userId);
eventBus.emit('user.deleted', { userId });
// Order Service
eventBus.on('user.deleted', async ({ userId }) => {
await orderService.anonymizeUserOrders(userId);
});
マイクロサービスに向いているケース
向いている
-
大規模チーム(10人以上)
- チームが独立して開発できる
-
部分的に負荷が高い
- 特定機能だけスケールさせたい
-
技術刷新が必要な部分がある
- 段階的にリプレースしたい
-
ドメインが明確に分離可能
- ユーザー管理、注文処理、決済など
向いていない
-
小規模チーム(5人以下)
- 運用コストが高すぎる
-
ドメインが複雑に絡み合っている
- サービス分割が困難
-
リアルタイム性が最重要
- ネットワークレイテンシが許容できない
モノリスからの移行戦略
ストラングラーパターン(段階的移行)
// Phase 1: モノリスの外側に新サービスを配置
//
// ┌──────────────┐
// │ API Gateway │
// └──────────────┘
// ↓ ↓
// ┌────┐ ┌──────────┐
// │User│ │ Monolith │
// │Svc │ └──────────┘
// └────┘
// API Gateway設定
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
// 新サービスへルーティング
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true
}));
// 既存モノリスへルーティング
app.use('/api', createProxyMiddleware({
target: 'http://monolith:3000',
changeOrigin: true
}));
app.listen(80);
// Phase 2: 機能を1つずつ抽出
// User Service抽出完了 → Order Service抽出開始
// Phase 3: モノリス縮小
// すべての機能を抽出したらモノリス廃止
データベース分離戦略
1. 読み取り専用レプリカの作成
// Step 1: モノリスDBからレプリカ作成
// Step 2: 新サービスはレプリカから読み取り
const userServiceDB = new Pool({
host: 'user-db-replica',
database: 'users'
});
// Step 3: 徐々に書き込みも新サービスへ
2. データ同期期間
// 両方のDBに書き込み(一時的)
async function createUser(userData: UserData) {
// 新DB(メイン)
const user = await newUserDB.users.create(userData);
// 旧DB(同期)
await oldMonolithDB.users.create(userData);
return user;
}
// 一定期間後、旧DBへの書き込みを停止
3. データ分割
-- モノリスDB
CREATE TABLE users (...);
CREATE TABLE orders (...);
CREATE TABLE payments (...);
-- 分割後
-- User Service DB
CREATE TABLE users (...);
-- Order Service DB
CREATE TABLE orders (...);
-- Payment Service DB
CREATE TABLE payments (...);
サービス間通信パターン
1. 同期通信(REST API)
// Order Service → User Serviceを呼び出し
import axios from 'axios';
async function createOrder(userId: string, items: CartItem[]) {
// User Serviceに問い合わせ
const { data: user } = await axios.get(`http://user-service/users/${userId}`);
if (!user) {
throw new Error('User not found');
}
const order = await db.orders.create({
userId,
items,
userEmail: user.email
});
return order;
}
メリット: シンプル、理解しやすい デメリット: User Serviceがダウンすると注文作成不可
2. 非同期通信(Message Queue)
// RabbitMQを使用
import amqp from 'amqplib';
const connection = await amqp.connect('amqp://rabbitmq');
const channel = await connection.createChannel();
// Order Service: メッセージ送信
async function createOrder(userId: string, items: CartItem[]) {
const order = await db.orders.create({ userId, items, status: 'pending' });
// メッセージキューに送信
await channel.sendToQueue('order.created', Buffer.from(JSON.stringify({
orderId: order.id,
userId,
items
})));
return order;
}
// Payment Service: メッセージ受信
channel.consume('order.created', async (msg) => {
if (msg) {
const { orderId, userId } = JSON.parse(msg.content.toString());
try {
await processPayment(orderId, userId);
channel.ack(msg);
} catch (error) {
channel.nack(msg);
}
}
});
メリット: サービスが疎結合、一時的なダウンに強い デメリット: デバッグが難しい、結果整合性
3. イベント駆動(Event Bus)
// Kafka使用
import { Kafka } from 'kafkajs';
const kafka = new Kafka({ brokers: ['kafka:9092'] });
const producer = kafka.producer();
const consumer = kafka.consumer({ groupId: 'payment-service' });
// Order Service: イベント発行
await producer.send({
topic: 'orders',
messages: [
{
key: orderId,
value: JSON.stringify({
event: 'OrderCreated',
orderId,
userId,
items,
timestamp: new Date()
})
}
]
});
// Payment Service: イベント購読
await consumer.subscribe({ topic: 'orders' });
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
const event = JSON.parse(message.value.toString());
if (event.event === 'OrderCreated') {
await handleOrderCreated(event);
}
}
});
// Inventory Service: 同じイベントを購読
// Email Service: 同じイベントを購読
// 1つのイベントで複数のサービスが反応
サービスの境界設計
ドメイン駆動設計(DDD)
// 悪い分割: 技術層で分割
UserController Service
OrderController Service
PaymentController Service
// 良い分割: ドメインで分割
┌─────────────────┐
│ User Context │
│ - Authentication│
│ - Profile │
│ - Preferences │
└─────────────────┘
┌─────────────────┐
│ Order Context │
│ - Cart │
│ - Checkout │
│ - Fulfillment │
└─────────────────┘
┌─────────────────┐
│ Payment Context │
│ - Billing │
│ - Transactions │
└─────────────────┘
サービス分割の実例
// ECサイトのサービス分割例
// 1. User Service
interface UserService {
createUser(data: CreateUserDto): Promise<User>;
authenticate(email: string, password: string): Promise<AuthToken>;
updateProfile(userId: string, data: UpdateProfileDto): Promise<User>;
}
// 2. Product Service
interface ProductService {
listProducts(filters: ProductFilters): Promise<Product[]>;
getProduct(id: string): Promise<Product>;
updateInventory(id: string, quantity: number): Promise<void>;
}
// 3. Order Service
interface OrderService {
createOrder(userId: string, items: CartItem[]): Promise<Order>;
getOrderStatus(orderId: string): Promise<OrderStatus>;
cancelOrder(orderId: string): Promise<void>;
}
// 4. Payment Service
interface PaymentService {
processPayment(orderId: string, method: PaymentMethod): Promise<PaymentResult>;
refund(paymentId: string): Promise<RefundResult>;
}
// 5. Notification Service
interface NotificationService {
sendEmail(to: string, template: EmailTemplate): Promise<void>;
sendSMS(to: string, message: string): Promise<void>;
}
API Gatewayパターン
// Kong, AWS API Gateway, または自作
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import jwt from 'jsonwebtoken';
const app = express();
// 認証ミドルウェア
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Rate limiting
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
// ルーティング
app.use('/api/users', limiter, createProxyMiddleware({
target: 'http://user-service:3001'
}));
app.use('/api/orders', authMiddleware, createProxyMiddleware({
target: 'http://order-service:3002'
}));
app.use('/api/products', limiter, createProxyMiddleware({
target: 'http://product-service:3003'
}));
app.listen(80);
Service Meshパターン
# Istio使用例
# サービス間通信を自動管理
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- match:
- headers:
version:
exact: v2
route:
- destination:
host: order-service
subset: v2
- route:
- destination:
host: order-service
subset: v1
weight: 90
- destination:
host: order-service
subset: v2
weight: 10 # Canary Deployment
監視とトラブルシューティング
分散トレーシング
// OpenTelemetry + Jaeger
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'order-service',
}),
});
const exporter = new JaegerExporter({
endpoint: 'http://jaeger:14268/api/traces',
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
// 使用
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
app.post('/orders', async (req, res) => {
const span = tracer.startSpan('create-order');
try {
const userSpan = tracer.startSpan('fetch-user', { parent: span });
const user = await userService.getUser(req.body.userId);
userSpan.end();
const paymentSpan = tracer.startSpan('process-payment', { parent: span });
const payment = await paymentService.charge(req.body.amount);
paymentSpan.end();
res.json({ success: true });
} finally {
span.end();
}
});
ログ集約
// ELK Stack (Elasticsearch + Logstash + Kibana)
import winston from 'winston';
import { ElasticsearchTransport } from 'winston-elasticsearch';
const logger = winston.createLogger({
transports: [
new ElasticsearchTransport({
level: 'info',
clientOpts: { node: 'http://elasticsearch:9200' },
index: 'logs'
})
]
});
logger.info('Order created', {
orderId: '123',
userId: '456',
service: 'order-service'
});
実装のベストプラクティス
1. サービスは小さく保つ
// 悪い例: 1つのサービスが多すぎる責務を持つ
class UserService {
createUser() {}
authenticate() {}
processOrder() {} // ❌ Orderの責務
sendEmail() {} // ❌ Notificationの責務
}
// 良い例: 単一責任
class UserService {
createUser() {}
authenticate() {}
updateProfile() {}
}
class OrderService {
createOrder() {}
getOrderStatus() {}
}
class NotificationService {
sendEmail() {}
sendSMS() {}
}
2. API契約を明確にする
// OpenAPI仕様書
openapi: 3.0.0
info:
title: Order Service API
version: 1.0.0
paths:
/orders:
post:
summary: Create a new order
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
3. Backward Compatibility
// バージョニング戦略
// Option 1: URLバージョニング
app.use('/v1/orders', ordersV1Router);
app.use('/v2/orders', ordersV2Router);
// Option 2: Headerバージョニング
app.use('/orders', (req, res, next) => {
const version = req.headers['api-version'] || 'v1';
if (version === 'v1') {
ordersV1Router(req, res, next);
} else {
ordersV2Router(req, res, next);
}
});
まとめ
マイクロサービスアーキテクチャは銀の弾丸ではありません。
移行の判断基準
- チームサイズ: 10人以上
- ドメインの分離可能性: 高
- スケールニーズ: 部分的に高負荷
- 既存システム: 複雑化・肥大化
移行手順
- ドメイン分析(DDD)
- ストラングラーパターンで段階的移行
- API Gateway導入
- 監視・トレーシング整備
- サービス分割開始
成功のカギ
- 明確なサービス境界
- 適切な通信パターン選択
- 充実した監視体制
- チームの自律性確保
マイクロサービス開発に役立つツールを探しているなら、DevToolBoxもチェックしてみてください。APIスキーマのバリデーションやJSON変換など、開発効率を上げるツールが揃っています。