ブログ記事

APIゲートウェイパターン完全ガイド2025 - マイクロサービスの要となる設計

APIゲートウェイパターンの詳細な実装方法を解説。認証・認可、レート制限、サーキットブレーカー、ロードバランシングなど、マイクロサービスアーキテクチャに不可欠な機能の実装例を豊富に紹介します。

18分で読めます
R
Rina
Daily Hack 編集長
Web開発
API マイクロサービス ゲートウェイ アーキテクチャ セキュリティ
APIゲートウェイパターン完全ガイド2025 - マイクロサービスの要となる設計のヒーロー画像

マイクロサービスアーキテクチャの普及に伴い、API ゲートウェイは現代の Web アプリケーション開発において不可欠な要素となりました。本記事では、API ゲートウェイパターンの基礎から、認証・認可、レート制限、サーキットブレーカーなどの高度な実装まで、実践的なコード例とともに徹底解説します。

この記事で学べること

  • API ゲートウェイの役割と責務の明確化
  • 認証・認可の実装パターンとベストプラクティス
  • レート制限とスロットリングの詳細な実装
  • サーキットブレーカーによる障害対策
  • マイクロサービス環境での実践的な活用方法

APIゲートウェイの必要性

マイクロサービスアーキテクチャでは、クライアントが複数のサービスと直接通信することで以下の問題が発生します:

APIゲートウェイなしの問題点

チャートを読み込み中...

この構成には以下の課題があります:

APIゲートウェイが解決する課題
課題 影響 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}`);
    });
  }
}

認証・認可の実装

JWT認証とRBACの統合

// 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
    };
  }
}

マイクロサービスでの実践例

完全なAPIゲートウェイ実装

マイクロサービスアーキテクチャ

チャートを読み込み中...

// 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}`);
    });
  }
}

パフォーマンス最適化

APIゲートウェイの最適化手法と効果
最適化手法 効果 実装難易度 推奨度
レスポンスキャッシュ レイテンシ90%削減 ★★★★★
コネクションプーリング スループット30%向上 ★★★★☆
HTTP/2対応 レイテンシ20%削減 ★★★★☆
圧縮(gzip/brotli) 帯域幅60%削減 ★★★★★
リクエストバッチング API呼び出し80%削減 ★★★☆☆

まとめ

API ゲートウェイは、マイクロサービスアーキテクチャにおける中心的なコンポーネントです。適切に実装されたゲートウェイは、セキュリティ、パフォーマンス、可用性のすべてを向上させます。

実装時のベストプラクティス

  1. 段階的な機能追加 - 最小限の機能から始めて徐々に拡張
  2. 適切な監視 - すべての重要メトリクスを追跡
  3. 障害対策 - サーキットブレーカーとフォールバックは必須
  4. キャッシュ戦略 - 適切な TTL とキャッシュキー設計
  5. セキュリティ - 認証・認可・レート制限の三位一体
Rinaのプロフィール画像

Rina

Daily Hack 編集長

フルスタックエンジニアとして10年以上の経験を持つ。 大手IT企業やスタートアップでの開発経験を活かし、 実践的で即効性のある技術情報を日々発信中。 特にWeb開発、クラウド技術、AI活用に精通。

この記事は役に立ちましたか?

あなたのフィードバックが記事の改善に役立ちます

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。