0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hybrid License System Day 6: API Gatewayの役割と責務

Last updated at Posted at 2025-12-05

🎄 科学と神々株式会社 アドベントカレンダー 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.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?