🎄 科学と神々株式会社 アドベントカレンダー 2025
Hybrid License System Day 6: API Gatewayの役割と責務
API Gateway編 (1/5)
📖 はじめに
Day 6では、API Gatewayの役割と責務を学びます。マイクロサービスアーキテクチャにおいてAPI Gatewayがなぜ重要なのか、その設計思想を理解しましょう。
🚪 API Gatewayとは何か?
定義
API Gatewayは、クライアントとバックエンドサービス群の間に位置する単一のエントリーポイントです。
Client Applications
↓
┌────────────────┐
│ API Gateway │ ← 単一のエントリーポイント
│ (Port 3000) │
└────────┬───────┘
↓
┌────┴────┐
│ │
┌───▼──┐ ┌──▼──┐ ┌──────┐
│ Auth │ │Admin│ │Other │
│Service│ │Svc. │ │Svc. │
└──────┘ └─────┘ └──────┘
🎯 API Gatewayの主要な責務
1. リクエストルーティング
クライアントからのリクエストを適切なバックエンドサービスに転送します。
// api-gateway/src/routes/auth.js
const express = require('express');
const router = express.Router();
const axios = require('axios');
const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL || 'http://localhost:3001';
// ライセンスアクティベーションをAuth Serviceにプロキシ
router.post('/license/activate', async (req, res) => {
try {
const response = await axios.post(
`${AUTH_SERVICE_URL}/activate`,
req.body,
{
headers: {
'Content-Type': 'application/json',
'X-Service-Secret': process.env.SERVICE_SECRET
},
timeout: 5000
}
);
res.status(response.status).json(response.data);
} catch (error) {
if (error.response) {
res.status(error.response.status).json(error.response.data);
} else {
res.status(503).json({ error: 'Auth service unavailable' });
}
}
});
// ライセンス検証をAuth Serviceにプロキシ
router.get('/license/validate', async (req, res) => {
try {
const response = await axios.get(
`${AUTH_SERVICE_URL}/validate`,
{
headers: {
'Authorization': req.headers.authorization,
'X-Service-Secret': process.env.SERVICE_SECRET
},
timeout: 5000
}
);
res.status(response.status).json(response.data);
} catch (error) {
if (error.response) {
res.status(error.response.status).json(error.response.data);
} else {
res.status(503).json({ error: 'Auth service unavailable' });
}
}
});
module.exports = router;
2. レート制限(Rate Limiting)
過剰なリクエストからシステムを保護します。
// api-gateway/src/middleware/rateLimit.js
const rateLimit = require('express-rate-limit');
// IPベースのレート制限
const ipLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 100リクエスト/15分
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Too many requests',
retryAfter: req.rateLimit.resetTime
});
}
});
// ユーザーベースのレート制限(認証済みリクエスト用)
const userLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 200, // 認証済みユーザーはより多くのリクエストを許可
keyGenerator: (req) => {
// JWTからユーザーIDを抽出
const token = req.headers.authorization?.split(' ')[1];
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return decoded.userId;
} catch (error) {
return req.ip; // トークンが無効な場合はIPにフォールバック
}
}
return req.ip;
}
});
module.exports = { ipLimiter, userLimiter };
3. CORS設定
クロスオリジンリクエストを適切に処理します。
// api-gateway/src/middleware/cors.js
const cors = require('cors');
const allowedOrigins = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',')
: ['http://localhost:3000', 'http://localhost:3002'];
const corsOptions = {
origin: (origin, callback) => {
// 開発環境ではoriginなしのリクエストを許可(Postmanなど)
if (!origin || allowedOrigins.includes(origin) || allowedOrigins.includes('*')) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Version'],
exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset']
};
module.exports = cors(corsOptions);
4. セキュリティヘッダー
Helmet.jsを使用してセキュリティヘッダーを設定します。
// api-gateway/src/server.js
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000, // 1年
includeSubDomains: true,
preload: true
},
frameguard: {
action: 'deny' // X-Frame-Options: DENY
},
noSniff: true, // X-Content-Type-Options: nosniff
xssFilter: true // X-XSS-Protection: 1; mode=block
}));
5. ヘルスチェック
バックエンドサービスの状態を監視します。
// api-gateway/src/routes/health.js
const express = require('express');
const router = express.Router();
const axios = require('axios');
router.get('/health', async (req, res) => {
const services = {
'api-gateway': { status: 'healthy' },
'auth-service': { status: 'unknown' },
'admin-service': { status: 'unknown' }
};
// Auth Serviceのヘルスチェック
try {
const authResponse = await axios.get(
`${process.env.AUTH_SERVICE_URL}/health`,
{ timeout: 3000 }
);
services['auth-service'] = {
status: authResponse.data.status || 'healthy',
responseTime: authResponse.headers['x-response-time']
};
} catch (error) {
services['auth-service'] = {
status: 'unhealthy',
error: error.message
};
}
// Admin Serviceのヘルスチェック
try {
const adminResponse = await axios.get(
`${process.env.ADMIN_SERVICE_URL}/health`,
{ timeout: 3000 }
);
services['admin-service'] = {
status: adminResponse.data.status || 'healthy',
responseTime: adminResponse.headers['x-response-time']
};
} catch (error) {
services['admin-service'] = {
status: 'unhealthy',
error: error.message
};
}
// 全体のステータス判定
const overallStatus = Object.values(services).every(s => s.status === 'healthy')
? 'healthy'
: 'degraded';
res.status(overallStatus === 'healthy' ? 200 : 503).json({
service: 'api-gateway',
status: overallStatus,
timestamp: new Date().toISOString(),
services
});
});
module.exports = router;
🔄 API Gateway vs リバースプロキシ
リバースプロキシ(Nginx, HAProxy)
- 目的: 負荷分散、SSL終端、静的コンテンツ配信
- 設定: 設定ファイル(nginx.conf など)
- 機能: シンプル、高速、静的ルーティング
API Gateway
- 目的: API管理、セキュリティ、ビジネスロジック
- 設定: プログラマティック(JavaScript/Python など)
- 機能: 複雑、柔軟、動的ルーティング
┌─────────────────────────────────────┐
│ Nginx (Reverse Proxy) │
│ - SSL Termination │
│ - Load Balancing │
│ - Static Content │
└──────────────┬──────────────────────┘
▼
┌─────────────────────────────────────┐
│ API Gateway (Express.js) │
│ - Request Routing │
│ - Rate Limiting │
│ - Authentication │
│ - Request Transformation │
└──────────────┬──────────────────────┘
▼
┌─────┴──────┐
│ │
┌────▼───┐ ┌───▼────┐
│ Auth │ │ Admin │
│Service │ │Service │
└────────┘ └────────┘
📊 API Gatewayのメリット
1. クライアントの簡素化
クライアントは複数のバックエンドサービスを意識する必要がありません。
// クライアント側のコード(簡潔)
const API_BASE = 'http://localhost:3000/api/v1';
// ライセンスアクティベーション
await fetch(`${API_BASE}/license/activate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, clientId })
});
// ユーザー統計取得
await fetch(`${API_BASE}/admin/stats`, {
headers: { 'Authorization': `Bearer ${token}` }
});
// クライアントは Auth Service (3001) や Admin Service (3002) のポートを知らなくてよい
2. バックエンドの柔軟性
バックエンドサービスの構成変更がクライアントに影響しません。
// サービスの移行例
// 古い設定
const AUTH_SERVICE_URL = 'http://localhost:3001';
// 新しい設定(クライアント変更不要)
const AUTH_SERVICE_URL = 'http://auth-service-cluster:8080';
3. 横断的関心事の一元管理
CORS、レート制限、ログなどを1箇所で管理できます。
// すべてのリクエストに共通のミドルウェアを適用
app.use(helmet()); // セキュリティヘッダー
app.use(corsMiddleware); // CORS
app.use(ipLimiter); // レート制限
app.use(requestLogger); // ログ
🚀 実装のポイント
1. タイムアウト設定
バックエンドサービスのタイムアウトを適切に設定します。
const response = await axios.post(url, data, {
timeout: 5000 // 5秒でタイムアウト
});
2. エラーハンドリング
バックエンドサービスのエラーを適切にクライアントに返します。
try {
const response = await axios.post(url, data);
res.status(response.status).json(response.data);
} catch (error) {
if (error.response) {
// バックエンドがエラーレスポンスを返した
res.status(error.response.status).json(error.response.data);
} else if (error.request) {
// バックエンドが応答しなかった
res.status(503).json({ error: 'Service unavailable' });
} else {
// リクエスト設定エラー
res.status(500).json({ error: 'Internal server error' });
}
}
3. ヘルスチェックの活用
定期的にバックエンドサービスの状態を確認します。
// Docker Composeのヘルスチェック設定
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
🎯 次のステップ
Day 7では、ルーティングとプロキシの実装について詳しく学びます。動的ルーティング、リクエストの転送方法、エラーハンドリングを実装しましょう。
🔗 関連リンク
次回予告: Day 7では、Express.jsを使った動的ルーティングとプロキシの実装を詳しく解説します!
Copyright © 2025 Gods & Golem, Inc. All rights reserved.