GraphQL Federation実践ガイド - マイクロサービスを統合する最新アーキテクチャ
GraphQL Federationを使用したマイクロサービスアーキテクチャの設計と実装を徹底解説。サブグラフの構築、スキーマ統合、認証・認可、パフォーマンス最適化まで、実践的なコード例と共に紹介します。
APIゲートウェイパターンの詳細な実装方法を解説。認証・認可、レート制限、サーキットブレーカー、ロードバランシングなど、マイクロサービスアーキテクチャに不可欠な機能の実装例を豊富に紹介します。
マイクロサービスアーキテクチャの普及に伴い、API ゲートウェイは現代の Web アプリケーション開発において不可欠な要素となりました。本記事では、API ゲートウェイパターンの基礎から、認証・認可、レート制限、サーキットブレーカーなどの高度な実装まで、実践的なコード例とともに徹底解説します。
マイクロサービスアーキテクチャでは、クライアントが複数のサービスと直接通信することで以下の問題が発生します:
チャートを読み込み中...
この構成には以下の課題があります:
課題 | 影響 | APIゲートウェイによる解決 |
---|---|---|
複数エンドポイント管理 | クライアントの複雑化 | 単一エントリーポイント |
認証の重複実装 | 各サービスで認証処理 | 一元的な認証管理 |
クロスカッティング処理 | 横断的関心事の散在 | 共通処理の集約 |
サービス変更の影響 | クライアント修正が必要 | バックエンドの抽象化 |
// api-gateway.js - 基本的なAPIゲートウェイの実装
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const CircuitBreaker = require('opossum');
class APIGateway {
constructor(config) {
this.app = express();
this.config = config;
this.services = new Map();
this.circuitBreakers = new Map();
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
// リクエストロギング
this.app.use(this.requestLogger());
// CORS設定
this.app.use(this.corsHandler());
// ボディパーサー
this.app.use(express.json());
// セキュリティヘッダー
this.app.use(this.securityHeaders());
}
setupRoutes() {
// ヘルスチェックエンドポイント
this.app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date() });
});
// サービスルーティング
this.config.services.forEach(service => {
this.registerService(service);
});
}
registerService(serviceConfig) {
const {
name,
path,
target,
auth = true,
rateLimit: rateLimitConfig,
circuitBreaker: cbConfig
} = serviceConfig;
// サービス情報を保存
this.services.set(name, serviceConfig);
// ミドルウェアチェーン
const middlewares = [];
// 認証が必要な場合
if (auth) {
middlewares.push(this.authMiddleware());
}
// レート制限
if (rateLimitConfig) {
middlewares.push(this.createRateLimiter(rateLimitConfig));
}
// サーキットブレーカー
if (cbConfig) {
middlewares.push(this.createCircuitBreaker(name, cbConfig));
}
// プロキシ設定
const proxyOptions = {
target,
changeOrigin: true,
pathRewrite: {
[`^${path}`]: ''
},
onProxyReq: this.onProxyRequest.bind(this),
onProxyRes: this.onProxyResponse.bind(this),
onError: this.onProxyError.bind(this)
};
// ルート登録
this.app.use(
path,
...middlewares,
httpProxy.createProxyMiddleware(proxyOptions)
);
}
// リクエストロガー
requestLogger() {
return (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log({
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
});
});
next();
};
}
// CORSハンドラー
corsHandler() {
return (req, res, next) => {
const origin = req.get('origin');
const allowedOrigins = this.config.cors?.allowedOrigins || ['*'];
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin || '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
};
}
// セキュリティヘッダー
securityHeaders() {
return (req, res, next) => {
res.header('X-Content-Type-Options', 'nosniff');
res.header('X-Frame-Options', 'DENY');
res.header('X-XSS-Protection', '1; mode=block');
res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
};
}
start(port = 3000) {
this.app.listen(port, () => {
console.log(`API Gateway listening on port ${port}`);
});
}
}
// auth-middleware.js - 高度な認証・認可システム
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
class AuthenticationManager {
constructor(config) {
this.config = config;
this.jwksClient = jwksClient({
jwksUri: config.jwksUri || `${config.authServer}/.well-known/jwks.json`,
cache: true,
cacheMaxAge: 600000, // 10分
rateLimit: true,
jwksRequestsPerMinute: 10
});
// キャッシュ
this.tokenCache = new Map();
this.permissionCache = new Map();
}
// JWT検証ミドルウェア
authMiddleware() {
return async (req, res, next) => {
try {
const token = this.extractToken(req);
if (!token) {
return res.status(401).json({
error: 'Unauthorized',
message: 'No token provided'
});
}
// キャッシュチェック
const cached = this.tokenCache.get(token);
if (cached && cached.exp > Date.now()) {
req.user = cached.user;
return next();
}
// トークン検証
const decoded = await this.verifyToken(token);
// ユーザー情報を追加
req.user = {
id: decoded.sub,
email: decoded.email,
roles: decoded.roles || [],
permissions: decoded.permissions || [],
token: token
};
// キャッシュに保存
this.tokenCache.set(token, {
user: req.user,
exp: decoded.exp * 1000
});
next();
} catch (error) {
console.error('Authentication error:', error);
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'TokenExpired',
message: 'Token has expired'
});
}
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid token'
});
}
};
}
// 認可ミドルウェア(RBAC)
authorize(requiredPermissions = []) {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'Unauthorized',
message: 'User not authenticated'
});
}
try {
// ユーザーの権限を取得
const userPermissions = await this.getUserPermissions(req.user);
// 必要な権限をすべて持っているかチェック
const hasPermissions = requiredPermissions.every(permission =>
userPermissions.includes(permission)
);
if (!hasPermissions) {
return res.status(403).json({
error: 'Forbidden',
message: 'Insufficient permissions',
required: requiredPermissions,
userPermissions: userPermissions
});
}
req.user.permissions = userPermissions;
next();
} catch (error) {
console.error('Authorization error:', error);
return res.status(500).json({
error: 'InternalServerError',
message: 'Authorization check failed'
});
}
};
}
// トークン抽出
extractToken(req) {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
// クッキーからも取得可能
if (req.cookies && req.cookies.access_token) {
return req.cookies.access_token;
}
return null;
}
// トークン検証
async verifyToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(
token,
this.getKey.bind(this),
{
algorithms: ['RS256'],
issuer: this.config.issuer,
audience: this.config.audience
},
(err, decoded) => {
if (err) reject(err);
else resolve(decoded);
}
);
});
}
// 公開鍵の取得
getKey(header, callback) {
this.jwksClient.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
} else {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
}
});
}
// ユーザー権限の取得
async getUserPermissions(user) {
const cacheKey = `${user.id}:permissions`;
// キャッシュチェック
const cached = this.permissionCache.get(cacheKey);
if (cached && cached.exp > Date.now()) {
return cached.permissions;
}
// ロールベースの権限を展開
const permissions = new Set();
for (const role of user.roles) {
const rolePermissions = await this.getRolePermissions(role);
rolePermissions.forEach(p => permissions.add(p));
}
// ユーザー固有の権限を追加
user.permissions.forEach(p => permissions.add(p));
const permissionList = Array.from(permissions);
// キャッシュに保存(5分間)
this.permissionCache.set(cacheKey, {
permissions: permissionList,
exp: Date.now() + 300000
});
return permissionList;
}
// ロールの権限を取得
async getRolePermissions(role) {
// 実際の実装では、データベースや外部サービスから取得
const rolePermissionMap = {
admin: ['*'],
user: ['read:profile', 'update:profile'],
guest: ['read:public']
};
return rolePermissionMap[role] || [];
}
}
// OAuth2.0統合
class OAuth2Integration {
constructor(config) {
this.config = config;
this.providers = new Map();
// プロバイダー設定
config.providers.forEach(provider => {
this.providers.set(provider.name, provider);
});
}
// OAuth認証フロー開始
initiateAuth(provider) {
return (req, res) => {
const providerConfig = this.providers.get(provider);
if (!providerConfig) {
return res.status(400).json({
error: 'InvalidProvider',
message: `Provider ${provider} not configured`
});
}
const state = this.generateState();
const nonce = this.generateNonce();
// セッションに保存
req.session.oauth = { state, nonce, provider };
const authUrl = new URL(providerConfig.authorizationEndpoint);
authUrl.searchParams.append('client_id', providerConfig.clientId);
authUrl.searchParams.append('redirect_uri', providerConfig.redirectUri);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', providerConfig.scope);
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('nonce', nonce);
res.redirect(authUrl.toString());
};
}
// コールバック処理
async handleCallback(req, res) {
const { code, state } = req.query;
const { oauth } = req.session;
if (!oauth || oauth.state !== state) {
return res.status(400).json({
error: 'InvalidState',
message: 'State mismatch'
});
}
try {
const providerConfig = this.providers.get(oauth.provider);
// アクセストークン取得
const tokens = await this.exchangeCodeForTokens(
code,
providerConfig
);
// ユーザー情報取得
const userInfo = await this.getUserInfo(
tokens.access_token,
providerConfig
);
// アプリケーショントークン生成
const appToken = await this.generateAppToken(userInfo);
res.json({
access_token: appToken,
token_type: 'Bearer',
expires_in: 3600
});
} catch (error) {
console.error('OAuth callback error:', error);
res.status(500).json({
error: 'AuthenticationFailed',
message: 'Failed to complete authentication'
});
}
}
generateState() {
return require('crypto').randomBytes(16).toString('hex');
}
generateNonce() {
return require('crypto').randomBytes(16).toString('hex');
}
}
// 基本的なレート制限
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 最大100リクエスト
message: 'Too many requests'
});
app.use('/api/', limiter);
// 高度なレート制限システム
const Redis = require('ioredis');
class AdvancedRateLimiter {
constructor(redisClient) {
this.redis = redisClient;
this.strategies = new Map();
// デフォルト戦略
this.registerStrategy('fixed-window', this.fixedWindow.bind(this));
this.registerStrategy('sliding-window', this.slidingWindow.bind(this));
this.registerStrategy('token-bucket', this.tokenBucket.bind(this));
this.registerStrategy('leaky-bucket', this.leakyBucket.bind(this));
}
// ミドルウェア生成
middleware(options = {}) {
const {
strategy = 'sliding-window',
keyGenerator = this.defaultKeyGenerator,
points = 100,
duration = 900, // 15分(秒)
blockDuration = 0,
skipSuccessfulRequests = false,
skipFailedRequests = false
} = options;
return async (req, res, next) => {
try {
const key = keyGenerator(req);
const strategyFn = this.strategies.get(strategy);
if (!strategyFn) {
throw new Error(`Unknown strategy: ${strategy}`);
}
const result = await strategyFn(key, {
points,
duration,
blockDuration
});
// レスポンスヘッダーを設定
res.setHeader('X-RateLimit-Limit', points);
res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
res.setHeader('X-RateLimit-Reset', new Date(result.resetTime).toISOString());
if (result.allowed) {
// 成功したリクエストをスキップするオプション
if (skipSuccessfulRequests) {
res.on('finish', async () => {
if (res.statusCode < 400) {
await this.restore(key, 1);
}
});
}
next();
} else {
// リトライヘッダー
if (result.retryAfter) {
res.setHeader('Retry-After', result.retryAfter);
}
res.status(429).json({
error: 'TooManyRequests',
message: 'Rate limit exceeded',
retryAfter: result.retryAfter
});
}
} catch (error) {
console.error('Rate limiter error:', error);
// エラー時は通過させる(fail open)
next();
}
};
}
// Sliding Window実装
async slidingWindow(key, options) {
const now = Date.now();
const windowStart = now - (options.duration * 1000);
const pipe = this.redis.pipeline();
// 古いエントリを削除
pipe.zremrangebyscore(key, '-inf', windowStart);
// 現在のカウントを取得
pipe.zcard(key);
// 新しいエントリを追加
pipe.zadd(key, now, `${now}-${Math.random()}`);
// TTLを設定
pipe.expire(key, options.duration);
const results = await pipe.exec();
const count = results[1][1];
if (count >= options.points) {
// レート制限超過
await this.redis.zrem(key, `${now}-${Math.random()}`);
// 最も古いエントリの時刻を取得
const oldestEntry = await this.redis.zrange(key, 0, 0, 'WITHSCORES');
const resetTime = oldestEntry.length > 0
? parseInt(oldestEntry[1]) + (options.duration * 1000)
: now + (options.duration * 1000);
return {
allowed: false,
remainingPoints: 0,
resetTime,
retryAfter: Math.ceil((resetTime - now) / 1000)
};
}
return {
allowed: true,
remainingPoints: options.points - count - 1,
resetTime: now + (options.duration * 1000)
};
}
// Token Bucket実装
async tokenBucket(key, options) {
const now = Date.now();
const bucketKey = `tb:${key}`;
// Luaスクリプトで原子性を保証
const luaScript = `
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or max_tokens
local last_refill = tonumber(bucket[2]) or now
-- トークンを補充
local time_passed = (now - last_refill) / 1000
local new_tokens = math.min(max_tokens, tokens + (time_passed * refill_rate))
if new_tokens >= cost then
-- トークンを消費
new_tokens = new_tokens - cost
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return {1, new_tokens}
else
return {0, new_tokens}
end
`;
const result = await this.redis.eval(
luaScript,
1,
bucketKey,
options.points, // max tokens
options.points / options.duration, // refill rate
now,
1 // cost
);
const [allowed, remainingTokens] = result;
return {
allowed: allowed === 1,
remainingPoints: Math.floor(remainingTokens),
resetTime: now + (options.duration * 1000)
};
}
// 分散レート制限(複数インスタンス対応)
async distributedRateLimit(key, options) {
const luaScript = `
local key = KEYS[1]
local points = tonumber(ARGV[1])
local duration = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local current = redis.call('GET', key)
if current == false then
current = 0
else
current = tonumber(current)
end
if current < points then
redis.call('INCR', key)
redis.call('EXPIRE', key, duration)
return {1, points - current - 1}
else
local ttl = redis.call('TTL', key)
return {0, ttl}
end
`;
const result = await this.redis.eval(
luaScript,
1,
`rl:${key}`,
options.points,
options.duration,
Date.now()
);
const [allowed, remaining] = result;
return {
allowed: allowed === 1,
remainingPoints: allowed === 1 ? remaining : 0,
resetTime: Date.now() + (options.duration * 1000),
retryAfter: allowed === 1 ? null : remaining
};
}
// デフォルトのキー生成
defaultKeyGenerator(req) {
return req.ip || req.connection.remoteAddress;
}
// ユーザーベースのキー生成
userKeyGenerator(req) {
return req.user ? `user:${req.user.id}` : this.defaultKeyGenerator(req);
}
// APIキーベースのキー生成
apiKeyGenerator(req) {
const apiKey = req.headers['x-api-key'];
return apiKey ? `api:${apiKey}` : this.defaultKeyGenerator(req);
}
// 戦略の登録
registerStrategy(name, implementation) {
this.strategies.set(name, implementation);
}
// ポイントの復元(エラー時など)
async restore(key, points) {
await this.redis.decrby(`rl:${key}`, points);
}
}
// 基本的なレート制限
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 最大100リクエスト
message: 'Too many requests'
});
app.use('/api/', limiter);
// 高度なレート制限システム
const Redis = require('ioredis');
class AdvancedRateLimiter {
constructor(redisClient) {
this.redis = redisClient;
this.strategies = new Map();
// デフォルト戦略
this.registerStrategy('fixed-window', this.fixedWindow.bind(this));
this.registerStrategy('sliding-window', this.slidingWindow.bind(this));
this.registerStrategy('token-bucket', this.tokenBucket.bind(this));
this.registerStrategy('leaky-bucket', this.leakyBucket.bind(this));
}
// ミドルウェア生成
middleware(options = {}) {
const {
strategy = 'sliding-window',
keyGenerator = this.defaultKeyGenerator,
points = 100,
duration = 900, // 15分(秒)
blockDuration = 0,
skipSuccessfulRequests = false,
skipFailedRequests = false
} = options;
return async (req, res, next) => {
try {
const key = keyGenerator(req);
const strategyFn = this.strategies.get(strategy);
if (!strategyFn) {
throw new Error(`Unknown strategy: ${strategy}`);
}
const result = await strategyFn(key, {
points,
duration,
blockDuration
});
// レスポンスヘッダーを設定
res.setHeader('X-RateLimit-Limit', points);
res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
res.setHeader('X-RateLimit-Reset', new Date(result.resetTime).toISOString());
if (result.allowed) {
// 成功したリクエストをスキップするオプション
if (skipSuccessfulRequests) {
res.on('finish', async () => {
if (res.statusCode < 400) {
await this.restore(key, 1);
}
});
}
next();
} else {
// リトライヘッダー
if (result.retryAfter) {
res.setHeader('Retry-After', result.retryAfter);
}
res.status(429).json({
error: 'TooManyRequests',
message: 'Rate limit exceeded',
retryAfter: result.retryAfter
});
}
} catch (error) {
console.error('Rate limiter error:', error);
// エラー時は通過させる(fail open)
next();
}
};
}
// Sliding Window実装
async slidingWindow(key, options) {
const now = Date.now();
const windowStart = now - (options.duration * 1000);
const pipe = this.redis.pipeline();
// 古いエントリを削除
pipe.zremrangebyscore(key, '-inf', windowStart);
// 現在のカウントを取得
pipe.zcard(key);
// 新しいエントリを追加
pipe.zadd(key, now, `${now}-${Math.random()}`);
// TTLを設定
pipe.expire(key, options.duration);
const results = await pipe.exec();
const count = results[1][1];
if (count >= options.points) {
// レート制限超過
await this.redis.zrem(key, `${now}-${Math.random()}`);
// 最も古いエントリの時刻を取得
const oldestEntry = await this.redis.zrange(key, 0, 0, 'WITHSCORES');
const resetTime = oldestEntry.length > 0
? parseInt(oldestEntry[1]) + (options.duration * 1000)
: now + (options.duration * 1000);
return {
allowed: false,
remainingPoints: 0,
resetTime,
retryAfter: Math.ceil((resetTime - now) / 1000)
};
}
return {
allowed: true,
remainingPoints: options.points - count - 1,
resetTime: now + (options.duration * 1000)
};
}
// Token Bucket実装
async tokenBucket(key, options) {
const now = Date.now();
const bucketKey = `tb:${key}`;
// Luaスクリプトで原子性を保証
const luaScript = `
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or max_tokens
local last_refill = tonumber(bucket[2]) or now
-- トークンを補充
local time_passed = (now - last_refill) / 1000
local new_tokens = math.min(max_tokens, tokens + (time_passed * refill_rate))
if new_tokens >= cost then
-- トークンを消費
new_tokens = new_tokens - cost
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return {1, new_tokens}
else
return {0, new_tokens}
end
`;
const result = await this.redis.eval(
luaScript,
1,
bucketKey,
options.points, // max tokens
options.points / options.duration, // refill rate
now,
1 // cost
);
const [allowed, remainingTokens] = result;
return {
allowed: allowed === 1,
remainingPoints: Math.floor(remainingTokens),
resetTime: now + (options.duration * 1000)
};
}
// 分散レート制限(複数インスタンス対応)
async distributedRateLimit(key, options) {
const luaScript = `
local key = KEYS[1]
local points = tonumber(ARGV[1])
local duration = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local current = redis.call('GET', key)
if current == false then
current = 0
else
current = tonumber(current)
end
if current < points then
redis.call('INCR', key)
redis.call('EXPIRE', key, duration)
return {1, points - current - 1}
else
local ttl = redis.call('TTL', key)
return {0, ttl}
end
`;
const result = await this.redis.eval(
luaScript,
1,
`rl:${key}`,
options.points,
options.duration,
Date.now()
);
const [allowed, remaining] = result;
return {
allowed: allowed === 1,
remainingPoints: allowed === 1 ? remaining : 0,
resetTime: Date.now() + (options.duration * 1000),
retryAfter: allowed === 1 ? null : remaining
};
}
// デフォルトのキー生成
defaultKeyGenerator(req) {
return req.ip || req.connection.remoteAddress;
}
// ユーザーベースのキー生成
userKeyGenerator(req) {
return req.user ? `user:${req.user.id}` : this.defaultKeyGenerator(req);
}
// APIキーベースのキー生成
apiKeyGenerator(req) {
const apiKey = req.headers['x-api-key'];
return apiKey ? `api:${apiKey}` : this.defaultKeyGenerator(req);
}
// 戦略の登録
registerStrategy(name, implementation) {
this.strategies.set(name, implementation);
}
// ポイントの復元(エラー時など)
async restore(key, points) {
await this.redis.decrby(`rl:${key}`, points);
}
}
// circuit-breaker.js - 高度なサーキットブレーカー
const EventEmitter = require('events');
class CircuitBreaker extends EventEmitter {
constructor(options = {}) {
super();
this.name = options.name || 'default';
this.timeout = options.timeout || 3000;
this.errorThreshold = options.errorThreshold || 50; // パーセンテージ
this.volumeThreshold = options.volumeThreshold || 10; // 最小リクエスト数
this.resetTimeout = options.resetTimeout || 30000; // 30秒
this.halfOpenRequests = options.halfOpenRequests || 3;
// 状態管理
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.stats = {
requests: 0,
failures: 0,
successes: 0,
timeouts: 0,
lastFailureTime: null,
lastSuccessTime: null
};
// スライディングウィンドウ
this.window = [];
this.windowSize = options.windowSize || 100;
// Half-Open状態のカウンター
this.halfOpenAttempts = 0;
}
async execute(fn, fallback) {
// 状態チェック
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.transitionTo('HALF_OPEN');
} else {
// フォールバック実行
if (fallback) {
return this.executeFallback(fallback);
}
throw new Error(`Circuit breaker is OPEN for ${this.name}`);
}
}
// Half-Open状態での制限
if (this.state === 'HALF_OPEN' && this.halfOpenAttempts >= this.halfOpenRequests) {
if (fallback) {
return this.executeFallback(fallback);
}
throw new Error(`Circuit breaker is testing for ${this.name}`);
}
try {
// タイムアウト付き実行
const result = await this.executeWithTimeout(fn);
this.onSuccess();
return result;
} catch (error) {
this.onFailure(error);
if (fallback) {
return this.executeFallback(fallback);
}
throw error;
}
}
async executeWithTimeout(fn) {
return new Promise(async (resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout after ${this.timeout}ms`));
}, this.timeout);
try {
const result = await fn();
clearTimeout(timer);
resolve(result);
} catch (error) {
clearTimeout(timer);
reject(error);
}
});
}
async executeFallback(fallback) {
try {
this.emit('fallback', { name: this.name });
return await fallback();
} catch (error) {
this.emit('fallback-error', { name: this.name, error });
throw error;
}
}
onSuccess() {
this.stats.requests++;
this.stats.successes++;
this.stats.lastSuccessTime = Date.now();
// ウィンドウに記録
this.recordResult(true);
if (this.state === 'HALF_OPEN') {
this.halfOpenAttempts++;
// 成功が続いたらCLOSEDに戻す
if (this.getRecentSuccessRate() > 0.8) {
this.transitionTo('CLOSED');
}
}
this.emit('success', { name: this.name, stats: this.stats });
}
onFailure(error) {
this.stats.requests++;
this.stats.failures++;
this.stats.lastFailureTime = Date.now();
if (error.message && error.message.includes('Timeout')) {
this.stats.timeouts++;
}
// ウィンドウに記録
this.recordResult(false);
if (this.state === 'HALF_OPEN') {
this.halfOpenAttempts++;
// 失敗したら即座にOPENに戻す
this.transitionTo('OPEN');
} else if (this.state === 'CLOSED') {
// エラー率チェック
if (this.shouldOpen()) {
this.transitionTo('OPEN');
}
}
this.emit('failure', { name: this.name, error, stats: this.stats });
}
recordResult(success) {
this.window.push({
success,
timestamp: Date.now()
});
// ウィンドウサイズを維持
if (this.window.length > this.windowSize) {
this.window.shift();
}
}
shouldOpen() {
if (this.window.length < this.volumeThreshold) {
return false;
}
const errorRate = this.getRecentErrorRate();
return errorRate >= (this.errorThreshold / 100);
}
shouldAttemptReset() {
const now = Date.now();
const timeSinceLastFailure = now - this.stats.lastFailureTime;
return timeSinceLastFailure >= this.resetTimeout;
}
getRecentErrorRate() {
if (this.window.length === 0) return 0;
const failures = this.window.filter(r => !r.success).length;
return failures / this.window.length;
}
getRecentSuccessRate() {
if (this.window.length === 0) return 0;
const successes = this.window.filter(r => r.success).length;
return successes / this.window.length;
}
transitionTo(newState) {
const oldState = this.state;
this.state = newState;
if (newState === 'HALF_OPEN') {
this.halfOpenAttempts = 0;
}
this.emit('state-change', {
name: this.name,
from: oldState,
to: newState
});
}
// 統計情報の取得
getStats() {
return {
name: this.name,
state: this.state,
stats: this.stats,
errorRate: this.getRecentErrorRate(),
successRate: this.getRecentSuccessRate()
};
}
// 手動リセット
reset() {
this.transitionTo('CLOSED');
this.stats = {
requests: 0,
failures: 0,
successes: 0,
timeouts: 0,
lastFailureTime: null,
lastSuccessTime: null
};
this.window = [];
this.halfOpenAttempts = 0;
}
}
// サーキットブレーカー管理
class CircuitBreakerManager {
constructor() {
this.breakers = new Map();
this.defaultOptions = {
timeout: 3000,
errorThreshold: 50,
volumeThreshold: 10,
resetTimeout: 30000
};
}
getBreaker(name, options = {}) {
if (!this.breakers.has(name)) {
const breaker = new CircuitBreaker({
name,
...this.defaultOptions,
...options
});
// イベントリスナーの設定
breaker.on('state-change', (data) => {
console.log(`Circuit breaker ${data.name}: ${data.from} -> ${data.to}`);
});
this.breakers.set(name, breaker);
}
return this.breakers.get(name);
}
// 全ブレーカーの状態を取得
getAllStats() {
const stats = {};
for (const [name, breaker] of this.breakers) {
stats[name] = breaker.getStats();
}
return stats;
}
// ヘルスチェックエンドポイント用
getHealth() {
const stats = this.getAllStats();
const openBreakers = Object.values(stats).filter(s => s.state === 'OPEN');
return {
healthy: openBreakers.length === 0,
totalBreakers: this.breakers.size,
openBreakers: openBreakers.length,
details: stats
};
}
}
チャートを読み込み中...
// complete-gateway.js - 本番環境対応のAPIゲートウェイ
const express = require('express');
const Redis = require('ioredis');
const { createProxyMiddleware } = require('http-proxy-middleware');
class ProductionAPIGateway {
constructor(config) {
this.app = express();
this.config = config;
this.redis = new Redis(config.redis);
// 各種マネージャーの初期化
this.authManager = new AuthenticationManager(config.auth);
this.rateLimiter = new AdvancedRateLimiter(this.redis);
this.circuitBreakerManager = new CircuitBreakerManager();
// メトリクス
this.metrics = {
requests: 0,
errors: 0,
latencies: []
};
this.initialize();
}
initialize() {
// グローバルミドルウェア
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.app.use(this.correlationIdMiddleware());
this.app.use(this.metricsMiddleware());
// ヘルスチェック
this.setupHealthChecks();
// サービスルーティング
this.setupServiceRoutes();
// エラーハンドリング
this.app.use(this.errorHandler());
}
setupServiceRoutes() {
// サービス定義
const services = [
{
name: 'auth',
path: '/api/auth',
target: 'http://auth-service:3001',
public: true,
rateLimit: { points: 5, duration: 60 }
},
{
name: 'users',
path: '/api/users',
target: 'http://user-service:3002',
permissions: ['user:read'],
cache: { ttl: 300 }
},
{
name: 'orders',
path: '/api/orders',
target: 'http://order-service:3003',
permissions: ['order:read'],
circuitBreaker: { errorThreshold: 30 }
},
{
name: 'products',
path: '/api/products',
target: 'http://product-service:3004',
public: true,
cache: { ttl: 600 },
rateLimit: { points: 100, duration: 60 }
}
];
services.forEach(service => this.registerService(service));
}
registerService(service) {
const middlewares = [];
// 公開APIでない場合は認証が必要
if (!service.public) {
middlewares.push(this.authManager.authMiddleware());
}
// 権限チェック
if (service.permissions) {
middlewares.push(this.authManager.authorize(service.permissions));
}
// レート制限
if (service.rateLimit) {
middlewares.push(this.rateLimiter.middleware({
keyGenerator: service.public
? (req) => req.ip
: (req) => req.user?.id || req.ip,
...service.rateLimit
}));
}
// キャッシュ
if (service.cache) {
middlewares.push(this.cacheMiddleware(service.cache));
}
// サーキットブレーカー
const circuitBreaker = this.circuitBreakerManager.getBreaker(
service.name,
service.circuitBreaker
);
// プロキシ設定
const proxyMiddleware = createProxyMiddleware({
target: service.target,
changeOrigin: true,
pathRewrite: { [`^${service.path}`]: '' },
onProxyReq: (proxyReq, req, res) => {
// リクエストヘッダーの追加
proxyReq.setHeader('X-Correlation-ID', req.correlationId);
proxyReq.setHeader('X-Original-IP', req.ip);
if (req.user) {
proxyReq.setHeader('X-User-ID', req.user.id);
proxyReq.setHeader('X-User-Roles', req.user.roles.join(','));
}
},
onProxyRes: (proxyRes, req, res) => {
// レスポンスヘッダーの追加
proxyRes.headers['X-Service-Name'] = service.name;
proxyRes.headers['X-Response-Time'] = Date.now() - req.startTime;
},
onError: (err, req, res) => {
console.error(`Proxy error for ${service.name}:`, err);
res.status(502).json({
error: 'BadGateway',
message: 'Service temporarily unavailable',
service: service.name
});
}
});
// サーキットブレーカーでラップ
const wrappedProxy = async (req, res, next) => {
try {
await circuitBreaker.execute(
() => new Promise((resolve, reject) => {
const originalEnd = res.end;
res.end = function(...args) {
originalEnd.apply(res, args);
if (res.statusCode < 500) {
resolve();
} else {
reject(new Error(`Service error: ${res.statusCode}`));
}
};
proxyMiddleware(req, res, (err) => {
if (err) reject(err);
});
}),
// フォールバック
async () => {
if (service.cache) {
const cached = await this.getCachedResponse(req);
if (cached) {
res.setHeader('X-Cache-Status', 'stale');
return res.json(cached);
}
}
res.status(503).json({
error: 'ServiceUnavailable',
message: 'Service is temporarily unavailable',
service: service.name
});
}
);
} catch (error) {
next(error);
}
};
// ルート登録
this.app.use(service.path, ...middlewares, wrappedProxy);
}
// 相関IDミドルウェア
correlationIdMiddleware() {
return (req, res, next) => {
req.correlationId = req.headers['x-correlation-id'] ||
require('uuid').v4();
req.startTime = Date.now();
res.setHeader('X-Correlation-ID', req.correlationId);
next();
};
}
// メトリクスミドルウェア
metricsMiddleware() {
return (req, res, next) => {
this.metrics.requests++;
res.on('finish', () => {
const duration = Date.now() - req.startTime;
this.metrics.latencies.push(duration);
if (this.metrics.latencies.length > 1000) {
this.metrics.latencies.shift();
}
if (res.statusCode >= 500) {
this.metrics.errors++;
}
});
next();
};
}
// キャッシュミドルウェア
cacheMiddleware(options) {
return async (req, res, next) => {
// GETリクエストのみキャッシュ
if (req.method !== 'GET') {
return next();
}
const cacheKey = this.generateCacheKey(req);
try {
const cached = await this.redis.get(cacheKey);
if (cached) {
const data = JSON.parse(cached);
res.setHeader('X-Cache-Status', 'hit');
return res.json(data);
}
} catch (error) {
console.error('Cache error:', error);
}
// キャッシュミスの場合
res.setHeader('X-Cache-Status', 'miss');
// レスポンスをインターセプト
const originalJson = res.json;
res.json = function(data) {
// 成功レスポンスのみキャッシュ
if (res.statusCode === 200) {
this.redis.setex(
cacheKey,
options.ttl,
JSON.stringify(data)
).catch(err => console.error('Cache write error:', err));
}
return originalJson.call(this, data);
}.bind(this);
next();
};
}
generateCacheKey(req) {
const parts = [
'cache',
req.path,
req.user?.id || 'anonymous',
JSON.stringify(req.query)
];
return parts.join(':');
}
setupHealthChecks() {
this.app.get('/health', (req, res) => {
const health = {
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
services: this.circuitBreakerManager.getHealth(),
metrics: {
requests: this.metrics.requests,
errors: this.metrics.errors,
errorRate: this.metrics.requests > 0
? this.metrics.errors / this.metrics.requests
: 0,
avgLatency: this.metrics.latencies.length > 0
? this.metrics.latencies.reduce((a, b) => a + b) / this.metrics.latencies.length
: 0
}
};
const statusCode = health.services.healthy ? 200 : 503;
res.status(statusCode).json(health);
});
}
errorHandler() {
return (err, req, res, next) => {
console.error('Gateway error:', {
error: err.message,
stack: err.stack,
correlationId: req.correlationId,
path: req.path,
method: req.method
});
res.status(err.status || 500).json({
error: err.name || 'InternalServerError',
message: err.message || 'An unexpected error occurred',
correlationId: req.correlationId
});
};
}
start(port = 3000) {
this.app.listen(port, () => {
console.log(`API Gateway started on port ${port}`);
});
}
}
最適化手法 | 効果 | 実装難易度 | 推奨度 |
---|---|---|---|
レスポンスキャッシュ | レイテンシ90%削減 | 低 | ★★★★★ |
コネクションプーリング | スループット30%向上 | 中 | ★★★★☆ |
HTTP/2対応 | レイテンシ20%削減 | 低 | ★★★★☆ |
圧縮(gzip/brotli) | 帯域幅60%削減 | 低 | ★★★★★ |
リクエストバッチング | API呼び出し80%削減 | 高 | ★★★☆☆ |
API ゲートウェイは、マイクロサービスアーキテクチャにおける中心的なコンポーネントです。適切に実装されたゲートウェイは、セキュリティ、パフォーマンス、可用性のすべてを向上させます。