🧩 マイクロサービスアーキテクチャ
📖 定義
マイクロサービスアーキテクチャ(MSA、Microservices Architecture)は、1つの大きなアプリケーションを複数の小さく独立したサービスに分割して開発・デプロイするアーキテクチャパターンです。各サービスは特定のビジネス機能を担当し、独立してデプロイ・スケールできます。モノリシック(Monolithic)アーキテクチャとは異なり、サービス間の疎結合により柔軟性と拡張性を提供します。
🎯 比喩で理解する
大企業 vs スタートアップ連合
モノリシック = 大企業
├─ すべての部門が1つの建物に
├─ 中央集権的管理
├─ 1つの部門の問題 → 全体に影響
├─ 変更が困難
└─ 意思決定が遅い
マイクロサービス = スタートアップ連合
├─ 各チームが独立したオフィス
├─ 自律的な意思決定
├─ 1つのチームの問題 → 他のチームは正常動作
├─ 素早い変更
└─ 柔軟な拡張
レゴ vs 粘土
モノリシック = 粘土の塊
┌──────────────────────────────┐
│ ユーザー │ 商品 │ 注文 │
│ 管理 │ 管理 │ 管理 │
│ すべて1つに │
└──────────────────────────────┘
- 全体を作り直す必要がある
- 1つの部分を修正 → 全体に影響
- 拡張が困難
マイクロサービス = レゴブロック
┌─────┐ ┌─────┐ ┌─────┐
│ユーザー│ │ 商品│ │ 注文│
│サービス│ │サービス│ │サービス│
└─────┘ └─────┘ └─────┘
- ブロック交換が簡単
- 独立して修正
- 必要な部分のみ拡張
⚙️ 動作原理
1. モノリシック vs マイクロサービス
========== モノリシック ==========
┌─────────────────────── ──────────────┐
│ 1つのアプリケーション │
│ │
│ ┌─────────────────────────────┐ │
│ │ ユーザー管理モジュール │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ 商品管理モジュール │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ 注文管理モジュール │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ 決済モジュール │ │
│ └─────────────────────────────┘ │
│ │
│ 1つのデータベース │
│ 1つのコードベース │
│ 1つのデプロイ単位 │
└─────────────────────────────────────┘
メリット:
✅ 開発初期が速い
✅ テストがシンプル
✅ デプロイがシンプル(1つのみ)
✅ デバッグが簡単
デメリット:
❌ 規模が大きくなると複雑
❌ デプロイ時に全体停止
❌ 部分的な拡張不可
❌ 技術スタック変更が困難
========== マイクロサービス ==========
┌──────────┐ ┌──────────┐ ┌──────────┐
│ユーザー │ │商品 │ │注文 │
│サービス │ │サービス │ │サービス │
│ │ │ │ │ │
│Node.js │ │Java │ │Go │
│MongoDB │ │MySQL │ │PostgreSQL│
└──────────┘ └──────────┘ └──────────┘
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│決済 │ │通知 │ │レビュー │
│サービス │ │サービス │ │サービス │
│ │ │ │ │ │
│Python │ │Node.js │ │Ruby │
│Redis │ │Kafka │ │Cassandra │
└──────────┘ └──────────┘ └──────────┘
メリット:
✅ 独立したデプロイ
✅ 技術スタックの自由
✅ 部分的な拡張可能
✅ チームの独立性
✅ 障害の分離
デメリット:
❌ 初期の複雑度が高い
❌ ネットワーク通信のオーバーヘッド
❌ 分散トランザクションが困難
❌ テストが複雑
❌ 運用コスト増加
2. サービス間通信
========== 同期通信 (HTTP/REST) ==========
注文サービス → 商品サービス
"商品123の在庫はありますか?"
↓
"はい、5個あります"
↓
注文サービス → 決済サービス
"10,000円の決済をお願いします"
↓
"決済完了"
↓
注文完了
メリット: シンプル、直感的
デメリット: 1つのサービス障害時に全体が失敗
========== 非同期通信 (メッセージキュー) ==========
注文サービス → Message Queue
"注文が作成されました" メッセージを発行
↓
┌──────────────────┐
│ Message Queue │
│ (RabbitMQ, │
│ Kafka など) │
└──────────────────┘
↓
┌────┴────┬────────┐
↓ ↓ ↓
決済 通知 在庫
サービス サービス サービス
それぞれ独立して処理
メリット: 疎結合、障害の分離
デメリット: 複雑度の増加、デバッグが困難
========== API Gateway ==========
クライアント (モバイル/Web)
↓
┌──────────────────┐
│ API Gateway │
│ - ルーティング │
│ - 認証 │
│ - ロードバランシング│
│ - ロギング │
└──────────────────┘
┌────┴────┬────────┐
↓ ↓ ↓
サービスA サービスB サービスC
役割:
- 単一のエントリーポイント
- クライアントの簡素化
- 共通機能の処理
3. データ管理
========== モノリシック: 共有データベース ==========
┌─────────────────────────────────────┐
│ アプリケーション │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │モジュールA│ │モジュールB│ │モジュールC│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
└─────┼────────┼───────┼──────────────┘
└────────┼───────┘
↓
┌──────────────────┐
│ 1つのデータベース │
│ ┌────┬────┬────┐│
│ │TA│TB│TC││
│ └────┴────┴────┘│
└──────────────────┘
メリット: JOIN が簡単、一貫性を保証
デメリット: 結合度が高い、拡張が困難
========== マイクロサービス: DB per Service ==========
┌──────────┐ ┌──────────┐ ┌──────────┐
│サービス A │ │サービス B │ │サービス C │
└────┬─────┘ └────┬─────┘ └────┬─────┘
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ DB A │ │ DB B │ │ DB C │
│ (MySQL) │ │(MongoDB)│ │(PostgreSQL)│
└─────────┘ └─────────┘ └─────────┘
メリット: 独立性、技術選択の自由
デメリット: JOIN 不可、一貫性が困難
========== データの一貫性 ==========
// Saga パターン
1. 注文サービス: 注文を作成
2. 決済サービス: 決済を処理
3. 在庫サービス: 在庫を減らす
4. 配送サービス: 配送を開始
もし3段階で失敗したら?
→ 補償トランザクション (Compensating Transaction)
4. 在庫減少失敗
3. 決済キャンセル ← 補償
2. 注文キャンセル ← 補償
💡 実際の例
モノリシックの例 (Express.js)
// ========== モノリシックアプリケーション ==========
// server.js - 1つのファイルにすべての機能
const express = require('express');
const app = express();
app.use(express.json());
// 1つのデータベース
const db = require('./database');
// ========== ユー ザー管理 ==========
app.post('/api/users', async (req, res) => {
const { username, email, password } = req.body;
const user = await db.users.create({ username, email, password });
res.json(user);
});
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user);
});
// ========== 商品管理 ==========
app.post('/api/products', async (req, res) => {
const { name, price, stock } = req.body;
const product = await db.products.create({ name, price, stock });
res.json(product);
});
app.get('/api/products', async (req, res) => {
const products = await db.products.findAll();
res.json(products);
});
// ========== 注文管理 ==========
app.post('/api/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
// トランザクションで一貫性を保証
const transaction = await db.sequelize.transaction();
try {
// 1. 在庫確認
const product = await db.products.findById(productId, { transaction });
if (product.stock < quantity) {
throw new Error('在庫不足');
}
// 2. 在庫を減らす
await product.update(
{ stock: product.stock - quantity },
{ transaction }
);
// 3. 注文作成
const order = await db.orders.create(
{ userId, productId, quantity, total: product.price * quantity },
{ transaction }
);
// 4. 決済処理
await processPayment(order.total);
await transaction.commit();
res.json(order);
} catch (error) {
await transaction.rollback();
res.status(400).json({ error: error.message });
}
});
// ========== 決済処理 ==========
app.post('/api/payments', async (req, res) => {
const { orderId, amount } = req.body;
const payment = await db.payments.create({ orderId, amount });
res.json(payment);
});
// 1つのサーバーで実行
app.listen(3000, () => {
console.log('モノリシックサーバー実行中: http://localhost:3000');
});
/*
メリット:
- コードが1か所に
- 開発が速い
- デバッグが簡単
- トランザクションがシンプル
デメリット:
- 規模が大きくなると複雑
- デプロイ時に全体再起動
- 部分的な拡張不可
- 1つの機能障害 → 全体に影響
*/
マイクロサービスの例
// ========== 1. ユーザーサービス (user-service.js) ==========
// ポート: 3001
const express = require('express');
const app = express();
const mongoose = require('mongoose');
app.use(express.json());
// 独立したデータベース
mongoose.connect('mongodb://localhost/users-db');
const User = mongoose.model('User', {
username: String,
email: String,
password: String
});
// ユーザー作成
app.post('/users', async (req, res) => {
const { username, email, password } = req.body;
try {
const user = new User({ username, email, password });
await user.save();
// イベント発行(他のサービスに通知)
await publishEvent('user.created', { userId: user._id, email });
res.json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// ユーザー取得
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
app.listen(3001, () => {
console.log('ユーザーサービス: http://localhost:3001');
});
// ========== 2. 商品サービス (product-service.js) ==========
// ポート: 3002
const express = require('express');
const app = express();
const { Pool } = require('pg');
app.use(express.json());
// PostgreSQL を使用(異なるDB!)
const pool = new Pool({
host: 'localhost',
database: 'products-db',
port: 5432
});
// 商品リスト
app.get('/products', async (req, res) => {
const result = await pool.query('SELECT * FROM products');
res.json(result.rows);
});
// 商品詳細
app.get('/products/:id', async (req, res) => {
const result = await pool.query(
'SELECT * FROM products WHERE id = $1',
[req.params.id]
);
res.json(result.rows[0]);
});
// 在庫確認
app.get('/products/:id/stock', async (req, res) => {
const result = await pool.query(
'SELECT stock FROM products WHERE id = $1',
[req.params.id]
);
res.json({ stock: result.rows[0].stock });
});
// 在庫減少
app.post('/products/:id/decrease-stock', async (req, res) => {
const { quantity } = req.body;
const client = await pool.connect();
try {
await client.query('BEGIN');
// 現在の在庫確認
const result = await client.query(
'SELECT stock FROM products WHERE id = $1 FOR UPDATE',
[req.params.id]
);
const currentStock = result.rows[0].stock;
if (currentStock < quantity) {
throw new Error('在庫不足');
}
// 在庫減少
await client.query(
'UPDATE products SET stock = stock - $1 WHERE id = $2',
[quantity, req.params.id]
);
await client.query('COMMIT');
res.json({ success: true });
} catch (error) {
await client.query('ROLLBACK');
res.status(400).json({ error: error.message });
} finally {
client.release();
}
});
app.listen(3002, () => {
console.log('商品サービス: http://localhost:3002');
});
// ========== 3. 注文サービス (order-service.js) ==========
// ポート: 3003
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const orders = []; // 実際にはデータベースを使用
// 注文作成
app.post('/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
try {
// 1. ユーザー確認(ユーザーサービスを呼び出し)
const userResponse = await axios.get(
`http://localhost:3001/users/${userId}`
);
const user = userResponse.data;
if (!user) {
return res.status(404).json({ error: 'ユーザーが 見つかりません' });
}
// 2. 商品情報取得(商品サービスを呼び出し)
const productResponse = await axios.get(
`http://localhost:3002/products/${productId}`
);
const product = productResponse.data;
// 3. 在庫確認
const stockResponse = await axios.get(
`http://localhost:3002/products/${productId}/stock`
);
const { stock } = stockResponse.data;
if (stock < quantity) {
return res.status(400).json({ error: '在庫が不足しています' });
}
// 4. 在庫減少
await axios.post(
`http://localhost:3002/products/${productId}/decrease-stock`,
{ quantity }
);
// 5. 決済処理(決済サービスを呼び出し)
const total = product.price * quantity;
const paymentResponse = await axios.post(
'http://localhost:3004/payments',
{ userId, amount: total }
);
// 6. 注文作成
const order = {
id: orders.length + 1,
userId,
productId,
quantity,
total,
status: 'completed',
createdAt: new Date()
};
orders.push(order);
// 7. イベント発行
await publishEvent('order.created', order);
res.json(order);
} catch (error) {
// Saga パターン: 補償トランザクション
console.error('注文失敗:', error.message);
// 在庫復旧
try {
await axios.post(
`http://localhost:3002/products/${productId}/increase-stock`,
{ quantity }
);
} catch (rollbackError) {
console.error('在庫復旧失敗:', rollbackError.message);
}
res.status(500).json({ error: '注文処理失敗' });
}
});
// 注文取 得
app.get('/orders/:id', (req, res) => {
const order = orders.find(o => o.id === parseInt(req.params.id));
res.json(order);
});
app.listen(3003, () => {
console.log('注文サービス: http://localhost:3003');
});
// ========== 4. 決済サービス (payment-service.js) ==========
// ポート: 3004
const express = require('express');
const app = express();
app.use(express.json());
const payments = [];
app.post('/payments', async (req, res) => {
const { userId, amount } = req.body;
// 外部決済APIを呼び出し(例: Stripe, Toss Payments)
try {
// 実際の決済処理
const payment = {
id: payments.length + 1,
userId,
amount,
status: 'success',
createdAt: new Date()
};
payments.push(payment);
// イベント発行
await publishEvent('payment.completed', payment);
res.json(payment);
} catch (error) {
res.status(400).json({ error: '決済失敗' });
}
});
app.listen(3004, () => {
console.log('決済サービス: http://localhost:3004');
});
// ========== 5. API Gateway (gateway.js) ==========
// ポート: 3000
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 認証ミドルウェア
function authenticate(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: '認証が必要で す' });
}
// JWT検証など
next();
}
// ロギングミドルウェア
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// ユーザーサービスプロキシ
app.use('/api/users', authenticate, createProxyMiddleware({
target: 'http://localhost:3001',
pathRewrite: { '^/api/users': '/users' },
changeOrigin: true
}));
// 商品サービスプロキシ
app.use('/api/products', createProxyMiddleware({
target: 'http://localhost:3002',
pathRewrite: { '^/api/products': '/products' },
changeOrigin: true
}));
// 注文サービスプロキシ
app.use('/api/orders', authenticate, createProxyMiddleware({
target: 'http://localhost:3003',
pathRewrite: { '^/api/orders': '/orders' },
changeOrigin: true
}));
// 決済サービスプロキシ
app.use('/api/payments', authenticate, createProxyMiddleware({
target: 'http://localhost:3004',
pathRewrite: { '^/api/payments': '/payments' },
changeOrigin: true
}));
app.listen(3000, () => {
console.log('API Gateway: http://localhost:3000');
});
// ========== 6. イベントバス (event-bus.js) ==========
const amqp = require('amqplib');
let connection, channel;
// RabbitMQ 接続
async function connect() {
connection = await amqp.connect('amqp://localhost');
channel = await connection.createChannel();
}
// イベント発行
async function publishEvent(eventType, data) {
await channel.assertQueue(eventType);
channel.sendToQueue(
eventType,
Buffer.from(JSON.stringify(data))
);
console.log(`イベント発行: ${eventType}`, data);
}
// イベント購読
async function subscribeEvent(eventType, callback) {
await channel.assertQueue(eventType);
channel.consume(eventType, (msg) => {
const data = JSON.parse(msg.content.toString());
console.log(`イベント受信: ${eventType}`, data);
callback(data);
channel.ack(msg);
});
}
connect();
module.exports = { publishEvent, subscribeEvent };
Docker Compose でマイクロサービスを実行
# docker-compose.yml
version: '3.8'
services:
# API Gateway
gateway:
build: ./gateway
ports:
- "3000:3000"
depends_on:
- user-service
- product-service
- order-service
- payment-service
# ユーザーサービス
user-service:
build: ./user-service
ports:
- "3001:3001"
environment:
- MONGO_URL=mongodb://mongo:27017/users
depends_on:
- mongo
- rabbitmq
# 商品サービス
product-service:
build: ./product-service
ports:
- "3002:3002"
environment:
- POSTGRES_URL=postgres://postgres:password@postgres:5432/products
depends_on:
- postgres
- rabbitmq
# 注文サービス
order-service:
build: ./order-service
ports:
- "3003:3003"
environment:
- MYSQL_URL=mysql://root:password@mysql:3306/orders
depends_on:
- mysql
- rabbitmq
# 決済サービス
payment-service:
build: ./payment-service
ports:
- "3004:3004"
depends_on:
- rabbitmq
# データベース
mongo:
image: mongo:6
volumes:
- mongo-data:/data/db
postgres:
image: postgres:15
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=products
volumes:
- postgres-data:/var/lib/postgresql/data
mysql:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=orders
volumes:
- mysql-data:/var/lib/mysql
# メッセージキュー
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672" # 管理UI
volumes:
mongo-data:
postgres-data:
mysql-data:
Service Mesh (Istio の例)
# istio-config.yaml
# サービスメッシュ - サービス間通信の管理
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
# トラフィック分散(カナリアデプロイ)
- match:
- headers:
user-type:
exact: beta
route:
- destination:
host: order-service
subset: v2 # 新しいバージョン
weight: 20
- destination:
host: order-service
subset: v1 # 既存のバージョン
weight: 80
# リトライポリシー
- route:
- destination:
host: order-service
retries:
attempts: 3
perTryTimeout: 2s
# タイムアウト
timeout: 10s
---
# Circuit Breaker
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 2
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
🤔 よくある質問
Q1. いつマイクロサービスを使用すべきですか?
A:
✅ マイクロサービスが適している場合:
1. 規模の大きいアプリケーション
- チーム: 10人以上
- コード: 10万行以上
- ユーザー: 数十万人以上
2. 迅速なデプロイが必要
- 1日に複数回デプロイ
- 独立した機能のリリース
- A/Bテストを頻繁に実施
3. 多様な技術スタックが必要
- サービスごとに最適な技術を選択
- レガシーシステムとの統合
4. 独立した拡張が必要
- 特定の機能のみトラフィックが多い
- サービスごとにリソース要件が異なる
5. チームの独立性が重要
- 複数のチームが同時開発
- チーム間の依存性を最小化
例:
- Netflix: 数百のマイクロサービス
- Amazon: 2-pizza team (チームごとにサービスを担当)
- Uber: 地域別、機能別にサービスを分離
❌ モノリシックが適している場合:
1. 小規模なアプリケーション
- チーム: 5人以下
- 機能: 明確でシンプル
- トラフィック: 少ない
2. 初期スタートアップ
- 迅速なMVP開発が必要
- 要件が頻繁に変更
- リソースが限定的
3. シンプルなCRUD
- 複雑なビジネスロジックがない
- サービス間の境界が不明確
4. 運用経験不足
- DevOps チームがない
- 分散システムの経験がない
例:
- ブログ、ポートフォリオ
- 小規模電子商取引
- 社内ツール
📊 意思決定チェックリスト:
□ チーム規模10人以上?
□ コードベース10万行以上?
□ 独立したデプロイが頻繁に必要?
□ 部分的な拡張が必要?
□ DevOps チームがある?
□ 分散システムの経験がある?
3つ以上チェック → マイクロサービスを検討
2つ以下 → モノリシックを維持
Q2. マイクロサービスの最大の課題は?
A:
// ========== 1. 分散トランザクション ==========
// モノリシック: シンプルなトランザクション
await db.transaction(async (t) => {
await createOrder(data, t);
await decreaseStock(productId, t);
await processPayment(amount, t);
// 1つでも失敗すると全体がロールバック
});
// マイクロサービス: 複雑な Saga パターン
async function createOrderSaga(data) {
try {
// ステップ1
const order = await orderService.create(data);
// ステップ2
await productService.decreaseStock(data.productId);
// ステップ3
await paymentService.process(order.total);
return order;
} catch (error) {
// 補償トランザクション(逆順で)
await paymentService.refund(order.total);
await productService.increaseStock(data.productId);
await orderService.cancel(order.id);
throw error;
}
}
// ========== 2. データの一貫性 ==========
// 問題: 複数のサービスに分散したデータ
// ユーザーサービス: userId, name
// 注文サービス: userId, orders
// 決済サービス: userId, payments
// 解決策1: イベントソーシング
eventBus.on('user.updated', async (event) => {
// ユーザー情報が変更されたら他のサービスも更新
await orderService.updateUserInfo(event.userId, event.name);
await paymentService.updateUserInfo(event.userId, event.name);
});
// 解決策2: CQRS (Command Query Responsibility Segregation)
// 書き込みと読み取りを分離
// 書き込み: 各サービス独立
// 読み取り: 統合されたビュー(Read Model)
// ========== 3. ネットワーク遅延 ==========
// モノリシック: 関数呼び出し(速い)
const user = getUser(userId); // 1ms
// マイクロサービス: HTTPリクエスト(遅い)
const user = await axios.get(`http://user-service/users/${userId}`); // 50ms
// 解決策: キャッシング
const redis = require('redis');
const cache = redis.createClient();
async function getUser(userId) {
// 1. キャッシュ確認
const cached = await cache.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
// 2. サービス呼び出し
const response = await axios.get(`http://user-service/users/${userId}`);
const user = response.data;
// 3. キャッシュに保存
await cache.setex(`user:${userId}`, 3600, JSON.stringify(user));
return user;
}
// ========== 4. サービス障害処理 ==========
// Circuit Breaker パターン
const CircuitBreaker = require('opossum');
const options = {
timeout: 3000, // 3秒タイムアウト
errorThresholdPercentage: 50, // 50%失敗時
resetTimeout: 30000 // 30秒後に再試行
};
const breaker = new CircuitBreaker(async (userId) => {
return await axios.get(`http://user-service/users/${userId}`);
}, options);
breaker.fallback(() => ({
id: userId,
name: '不明', // フォールバックデータ
cached: true
}));
// 使用
breaker.fire(userId)
.then(console.log)
.catch(console.error);
// ========== 5. 監視とデバッグ ==========
// 分散トレーシング
// Jaeger、Zipkin を使用
const tracer = require('jaeger-client').initTracer(config);
app.use((req, res, next) => {
const span = tracer.startSpan('http_request');
span.setTag('http.method', req.method);
span.setTag('http.url', req.url);
req.span = span;
next();
});
// サービス間呼び出し時にトレースIDを渡す
await axios.get('http://order-service/orders', {
headers: {
'x-trace-id': req.span.context().toTraceId()
}
});
Q3. API Gateway の役割は?
A:
// ========== API Gateway の主な機能 ==========
const express = require('express');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// ========== 1. ルーティング ==========
// クライアントは1つのエンドポイントだけを知っていればいい
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true
}));
app.use('/api/products', createProxyMiddleware({
target: 'http://product-service:3002',
changeOrigin: true
}));
// ========== 2. 認証と認可 ==========
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'トークンが必要です' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: '無効なトークン' });
}
}
app.use('/api/orders', authenticate, createProxyMiddleware({
target: 'http://order-service:3003'
}));
// ========== 3. Rate Limiting ==========
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100 // 最大100リクエスト
});
app.use('/api/', limiter);
// ========== 4. ロードバランシング ==========
const productServiceInstances = [
'http://product-service-1:3002',
'http://product-service-2:3002',
'http://product-service-3:3002'
];
let currentIndex = 0;
app.use('/api/products', createProxyMiddleware({
target: productServiceInstances[currentIndex],
router: () => {
// ラウンドロビン
const target = productServiceInstances[currentIndex];
currentIndex = (currentIndex + 1) % productServiceInstances.length;
return target;
}
}));
// ========== 5. リクエスト/レスポンス変換 ==========
app.use('/api/legacy', createProxyMiddleware({
target: 'http://legacy-service:8080',
onProxyReq: (proxyReq, req) => {
// リクエスト変換
proxyReq.setHeader('X-API-Version', '2.0');
},
onProxyRes: (proxyRes, req, res) => {
// レスポンス変換
proxyRes.headers['X-Custom-Header'] = 'Gateway';
}
}));
// ========== 6. キャッシング ==========
const redis = require('redis');
const cache = redis.createClient();
app.get('/api/products/:id', async (req, res) => {
const cacheKey = `product:${req.params.id}`;
// キャッシュ確認
const cached = await cache.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// サービス呼び出し
const response = await axios.get(
`http://product-service:3002/products/${req.params.id}`
);
// キャッシュに保存
await cache.setex(cacheKey, 3600, JSON.stringify(response.data));
res.json(response.data);
});
// ========== 7. ロギングと監視 ==========
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
user: req.user?.id
});
});
next();
});
// ========== 8. エラー処理 ==========
app.use((err, req, res, next) => {
console.error('Gateway Error:', err);
if (err.code === 'ECONNREFUSED') {
return res.status(503).json({
error: 'サービスを利用できません'
});
}
res.status(500).json({
error: 'サーバーエラーが発生しました'
});
});
// ========== 9. サービスディスカバリー ==========
const consul = require('consul')();
async function getServiceUrl(serviceName) {
const result = await consul.health.service({
service: serviceName,
passing: true // ヘルスチェック通過したインスタンスのみ
});
if (result.length === 0) {
throw new Error(`${serviceName} サービスが見つかりません`);
}
// ランダムに選択
const instance = result[Math.floor(Math.random() * result.length)];
return `http://${instance.Service.Address}:${instance.Service.Port}`;
}
app.listen(3000);