- Published on
APIゲートウェイパターンとBFF設計:Kong・Envoy・GraphQL Federationの実践実装
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- APIゲートウェイパターンの理解
- BFF(Backend for Frontend)パターン
- ゲートウェイツール比較
- Kong Gateway本番構成
- Envoy Proxy構成
- GraphQL Federation
- 認証と認可の統合
- レートリミッティングとサーキットブレーカー
- オブザーバビリティの統合
- トラブルシューティング
- 本番チェックリスト
- アンチパターンと失敗事例
- 参考資料

はじめに
マイクロサービスアーキテクチャが産業全般に普及する中、サービス間通信の複雑さをどのように管理するかは、すべてのエンジニアリング組織の中核的な課題となっています。グローバルAPI管理市場は2026年時点で約51億ドル規模に成長し、CAGR 32.3%の爆発的な拡大を見せています。2025年時点で組織の31%が複数のAPIゲートウェイを運用しており、そのうち11%は3つ以上のゲートウェイを並行運用しています。
APIゲートウェイパターンは、クライアントとバックエンドサービスの間に単一のエントリーポイント(Single Entry Point)を配置し、ルーティング、認証、レートリミッティング、プロトコル変換などの横断的関心事(Cross-Cutting Concerns)を集約するアーキテクチャパターンです。ここにBFF(Backend for Frontend)パターンを組み合わせることで、Web、モバイル、IoTなど各フロントエンドタイプに最適化された専用バックエンドを提供できます。
本記事では、APIゲートウェイパターンとBFFパターンのコア原理を検討し、Kong Gateway、Envoy Proxy、GraphQL Federationという3つの主要ツールの実践的な構成方法を扱います。認証・認可の統合、レートリミッティング、サーキットブレーカー、オブザーバビリティ(Observability)の設定まで、本番運用に必要なすべての内容を実践コードとともに提供します。
APIゲートウェイパターンの理解
ゲートウェイの役割
APIゲートウェイは、マイクロサービスアーキテクチャにおけるすべてのクライアントリクエストの単一エントリーポイントの役割を果たします。内部サービストポロジーの複雑さを抽象化し、クライアントにクリーンで一貫したインターフェースを提供します。主な責務は以下の通りです。
- リクエストルーティング: URLパス、ヘッダー、メソッドに基づいて適切なバックエンドサービスにトラフィックを転送
- プロトコル変換: REST-to-gRPC、HTTP-to-WebSocketなどプロトコル間の変換
- レスポンス集約: 複数のサービスのレスポンスを1つにまとめてクライアントに返却(API Composition)
- 横断的関心事の処理: 認証、認可、レートリミッティング、ロギング、キャッシング、CORSなどを一元管理
- サービスディスカバリ: 動的にサービスインスタンスを探索しロードバランシングを実行
ゲートウェイトポロジーアーキテクチャ
API 게이트웨이 토폴로지
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Web App │ │Mobile App│ │ Partner │ │IoT Device│
│ (React) │ │ (Swift) │ │ (B2B) │ │ (MQTT) │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
└──────┬──────┴──────┬──────┘ │
│ │ │
┌──────▼──────┐ ┌───▼──────────┐ ┌──────▼──────┐
│ Edge LB │ │ Edge LB │ │ Edge LB │
│ (L4/L7) │ │ (L4/L7) │ │ (L4/L7) │
└──────┬──────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
┌──────▼───────────────▼──────────────────▼──────┐
│ API Gateway Cluster │
│ ┌──────────────────────────────────────────┐ │
│ │ Auth │ Rate Limit │ Logging │ Transform │ │
│ └──────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ Route Matching & Dispatch │ │
│ └─────────────────────────────────────────┘ │
└───────┬──────────┬──────────┬──────────┬───────┘
│ │ │ │
┌──────▼───┐┌────▼────┐┌────▼────┐┌───▼──────┐
│ User Svc ││Order Svc││Pay Svc ││Notify Svc│
│ (gRPC) ││ (REST) ││ (REST) ││(WebSocket│
└──────────┘└─────────┘└─────────┘└──────────┘
Edgeゲートウェイ vs Internalゲートウェイ
実務では、ゲートウェイを階層的に配置します。Edgeゲートウェイは外部トラフィックのエントリーポイントとしてDDoS防御、TLS終端、積極的なレートリミッティングを実行します。Internalゲートウェイはサービス間通信を管理し、サービスディスカバリ、mTLS、きめ細かなアクセス制御を担当します。
외부 트래픽
│
┌───▼────────────────────────┐
│ Edge Gateway (Kong) │ ← TLS 종료, WAF, 레이트 리미팅
│ - DDoS 방어 │
│ - JWT 검증 │
│ - 글로벌 레이트 리미팅 │
└───────────┬────────────────┘
│
┌───────────▼────────────────┐
│ Internal Gateway (Envoy) │ ← mTLS, 서비스 라우팅
│ - mTLS 상호 인증 │
│ - 서비스 디스커버리 │
│ - 세밀한 RBAC │
└──┬────────┬────────┬───────┘
│ │ │
┌──▼──┐ ┌──▼──┐ ┌──▼──┐
│Svc A│ │Svc B│ │Svc C│
└─────┘ └─────┘ └─────┘
BFF(Backend for Frontend)パターン
BFFパターンが必要な理由
単一のAPIゲートウェイですべてのクライアントにサービスを提供していると、必然的に「一つのサイズがすべてに合わない」問題に直面します。Webアプリケーションは豊富なデータを一度に取得したいのに対し、モバイルアプリは帯域幅節約のため最小限のフィールドのみ必要です。IoTデバイスはバイナリプロトコルを、サードパーティパートナーは安定したREST APIを期待します。
BFFパターンはSam Newmanが提案したアーキテクチャパターンで、各フロントエンドタイプに合った専用バックエンドを配置してこの問題を解決します。BFFは特定のユーザー体験に密接に結合され、そのUIを担当するチームがBFFも合わせて管理します。
BFFアーキテクチャ
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Web App │ │Mobile App│ │ Admin App│
│ (React) │ │ (Flutter)│ │ (Vue.js) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
│ Web BFF │ │Mobile BFF│ │Admin BFF │
│(Node.js) │ │ (Go) │ │(Node.js) │
│ │ │ │ │ │
│- 풍부한 │ │- 경량 │ │- 대시보드│
│ 데이터 │ │ 페이로드│ │ 집계 │
│- SSR 지원│ │- 오프라인│ │- 벌크 │
│- SEO │ │ 캐싱 │ │ 연산 │
└──┬───┬───┘ └──┬───┬───┘ └──┬───┬───┘
│ │ │ │ │ │
│ └────────┬───┘ └────────┬───┘ │
│ │ │ │
┌──▼──┐ ┌───▼──┐ ┌───▼──┐ │
│User │ │Order │ │Admin │◄──┘
│ Svc │ │ Svc │ │ Svc │
└─────┘ └──────┘ └──────┘
BFF実装例:Node.js Web BFF
// web-bff/src/server.ts
import express from 'express'
import axios from 'axios'
const app = express()
// Web BFF: 대시보드를 위한 데이터 집계 엔드포인트
app.get('/api/dashboard', async (req, res) => {
const userId = req.headers['x-user-id'] as string
try {
// 여러 마이크로서비스에서 병렬로 데이터 수집
const [userProfile, recentOrders, notifications, analytics] = await Promise.all([
axios.get(`${process.env.USER_SERVICE_URL}/users/${userId}`),
axios.get(`${process.env.ORDER_SERVICE_URL}/orders?userId=${userId}&limit=10`),
axios.get(`${process.env.NOTIFICATION_SERVICE_URL}/notifications/${userId}?unread=true`),
axios.get(`${process.env.ANALYTICS_SERVICE_URL}/users/${userId}/summary`),
])
// Web 프론트엔드에 최적화된 응답 구조
res.json({
user: {
name: userProfile.data.name,
email: userProfile.data.email,
avatar: userProfile.data.avatarUrl,
memberSince: userProfile.data.createdAt,
},
orders: {
recent: recentOrders.data.items.map((order: any) => ({
id: order.id,
status: order.status,
total: order.totalAmount,
date: order.createdAt,
itemCount: order.items.length,
// 웹에서만 필요한 상세 정보 포함
trackingUrl: order.trackingUrl,
invoice: order.invoiceUrl,
})),
totalCount: recentOrders.data.totalCount,
},
notifications: {
unreadCount: notifications.data.count,
items: notifications.data.items.slice(0, 5),
},
analytics: {
totalSpent: analytics.data.totalSpent,
orderFrequency: analytics.data.orderFrequency,
favoriteCategories: analytics.data.topCategories,
},
})
} catch (error) {
// 부분 실패 시 가용한 데이터만 반환 (Graceful Degradation)
res.status(207).json({
partial: true,
error: 'Some services are unavailable',
available: {},
})
}
})
// Mobile BFF와의 차이점: 웹은 SSR을 위한 전체 데이터를 반환
// Mobile BFF는 동일 엔드포인트에서 경량 필드만 반환
app.listen(3001, () => {
console.log('Web BFF running on port 3001')
})
BFFコア設計原則
- 1つのBFFは1つのフロントエンドに対応: Web BFFがモバイルの要件を処理し始めると「多目的BFF」となり、元の問題に逆戻りします。
- BFFはビジネスロジックを持たない: データ集約、変換、フォーマッティングのみ実行し、ビジネスルールは必ず下位サービスに配置します。
- BFFチームとフロントエンドチームは同一チーム: フロントエンド開発者が自分のBFFを直接管理する時が最も効率的です。
- セキュリティ境界としてのBFF: 特にSPA環境でOIDC/OAuth 2.0フローのトークンネゴシエーションをBFFがConfidential Clientとして処理し、パブリッククライアントのセキュリティリスクを排除します。
ゲートウェイツール比較
主要なAPIゲートウェイソリューションを本番運用の観点から比較します。
| 項目 | Kong Gateway | Envoy Proxy | AWS API Gateway | GraphQL Federation |
|---|---|---|---|---|
| 基盤技術 | NGINX + Lua | C++ (自社開発) | AWS 관리형 | Apollo Router (Rust) |
| デプロイモデル | セルフホスティング / Konnect | セルフホスティング / サイドカー | フルマネージド | セルフホスティング / GraphOS |
| プロトコル | HTTP, gRPC, WebSocket | HTTP/1.1, HTTP/2, gRPC, TCP | REST, WebSocket, HTTP | GraphQL |
| 性能 | ~50,000 TPS/노드 | ~100,000+ TPS/노드 | AWS 내부 최적화 | サブグラフ数に比例 |
| 拡張性 | 플러그인 (Lua, Go) | 필터 (C++, Wasm, Lua) | Lambda 연동 | 서브그래프 서비스 |
| K8s統合 | KIC (Gateway API 준수) | Envoy Gateway / Istio | EKS 통합 | Helm Chart |
| オブザーバビリティ | Prometheus, Datadog | OpenTelemetry 네이티브 | CloudWatch | Apollo Studio |
| 認証 | 플러그인 (JWT, OAuth) | 필터 (JWT, ext_authz) | Cognito, Lambda Auth | 서브그래프 위임 |
| レートリミッティング | 플러그인 (ローカル/グローバル) | 필터 (ローカル/グローバル) | スロットリング内蔵 | カスタム実装が必要 |
| ライセンス | Apache 2.0 / Enterprise | Apache 2.0 | 従量制 | Elastic License |
| 学習曲線 | 中程度 | 高い | 低い | 高い |
| 適合シナリオ | 汎用API管理 | サービスメッシュ / L7制御 | AWS 네이티브 | マルチチームグラフ統合 |
選択ガイド
- Kong: プラグインエコシステムが豊富でGUI管理ツールが必要な場合。60以上の公式プラグインを提供しています。ただし、OIDC、高度な分析などのコア機能がEnterpriseライセンスに含まれる点に注意が必要です。
- Envoy: 高性能L7プロキシが必要な場合やサービスメッシュ(Istio、Consul)との統合が必要な場合。xDS APIによる動的設定が強みですが、設定の複雑さが高いです。
- AWS API Gateway: AWSネイティブ環境で管理負担を最小化したい場合。Lambda統合が強力ですが、AWSロックインとコールドスタート遅延が欠点です。
- GraphQL Federation: 多数のチームが独立してグラフを提供し、フロントエンドが柔軟なクエリを必要とする場合。スキーマ設計の初期投資は大きいですが、長期的にフロントエンドの生産性が最大化されます。
Kong Gateway本番構成
Kongのインストールと基本構成(Kubernetes)
Kong Ingress Controller(KIC)はKubernetes Gateway APIをファーストクラスシチズンとしてサポートし、Gateway APIコア適合性テストを100%パスした最初のゲートウェイです。
# kong-values.yaml - Helm Chart 설정
# helm install kong kong/ingress -f kong-values.yaml -n kong-system
gateway:
image:
repository: kong/kong-gateway
tag: '3.9'
env:
database: 'off' # DB-less 모드 (선언적 설정)
proxy_access_log: /dev/stdout
admin_access_log: /dev/stdout
proxy_error_log: /dev/stderr
admin_error_log: /dev/stderr
plugins: bundled,oidc,prometheus
proxy:
type: LoadBalancer
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
tls:
enabled: true
admin:
enabled: true
type: ClusterIP # 클러스터 내부에서만 접근
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
controller:
image:
repository: kong/kubernetes-ingress-controller
tag: '3.4'
ingressClass: kong
resources:
requests:
cpu: 250m
memory: 256Mi
Kongサービスとルーティング構成
# kong-gateway-api.yaml
# Kubernetes Gateway API를 사용한 선언적 라우팅 구성
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: kong-gateway
namespace: kong-system
annotations:
konghq.com/gateway-operator: 'true'
spec:
gatewayClassName: kong
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: api-tls-cert
kind: Secret
---
# HTTPRoute로 서비스별 라우팅 정의
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: user-service-route
namespace: default
annotations:
konghq.com/plugins: rate-limiting-global,jwt-auth,cors
spec:
parentRefs:
- name: kong-gateway
namespace: kong-system
hostnames:
- 'api.example.com'
rules:
- matches:
- path:
type: PathPrefix
value: /api/v1/users
backendRefs:
- name: user-service
port: 8080
weight: 100
- matches:
- path:
type: PathPrefix
value: /api/v1/orders
backendRefs:
- name: order-service
port: 8080
weight: 90
- name: order-service-canary
port: 8080
weight: 10 # 카나리 배포: 10% 트래픽
---
# Kong 플러그인: JWT 인증
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: jwt-auth
namespace: default
config:
key_claim_name: iss
claims_to_verify:
- exp
header_names:
- Authorization
plugin: jwt
---
# Kong 플러그인: 레이트 리미팅
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: rate-limiting-global
namespace: default
config:
minute: 100
hour: 5000
policy: redis
redis:
host: redis-master.redis-system.svc.cluster.local
port: 6379
database: 0
timeout: 2000
fault_tolerant: true # Redis 장애 시에도 요청 통과
hide_client_headers: false
plugin: rate-limiting
---
# Kong 플러그인: CORS
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: cors
namespace: default
config:
origins:
- 'https://app.example.com'
- 'https://admin.example.com'
methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
headers:
- Authorization
- Content-Type
- X-Request-ID
exposed_headers:
- X-RateLimit-Remaining
max_age: 3600
credentials: true
plugin: cors
Envoy Proxy構成
Envoy基本構成
Envoyは高性能なC++ベースのプロキシで、L7層でHTTP/2、gRPCをネイティブにサポートします。xDS APIによる動的設定更新がコアの強みです。
# envoy-config.yaml
# Envoy 프록시 정적 설정 (프로덕션에서는 xDS 동적 설정 사용 권장)
admin:
address:
socket_address:
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: api_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: AUTO
# 액세스 로그 설정
access_log:
- name: envoy.access_loggers.file
typed_config:
'@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /dev/stdout
log_format:
json_format:
timestamp: '%START_TIME%'
method: '%REQ(:METHOD)%'
path: '%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%'
protocol: '%PROTOCOL%'
response_code: '%RESPONSE_CODE%'
duration_ms: '%DURATION%'
upstream_host: '%UPSTREAM_HOST%'
request_id: '%REQ(X-REQUEST-ID)%'
# HTTP 필터 체인
http_filters:
# JWT 인증 필터
- name: envoy.filters.http.jwt_authn
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
auth0_provider:
issuer: 'https://your-tenant.auth0.com/'
audiences:
- 'https://api.example.com'
remote_jwks:
http_uri:
uri: 'https://your-tenant.auth0.com/.well-known/jwks.json'
cluster: auth0_jwks
timeout: 5s
cache_duration: 600s
forward: true
payload_in_metadata: jwt_payload
rules:
- match:
prefix: /api/
requires:
provider_name: auth0_provider
- match:
prefix: /health
# 레이트 리미팅 필터
- name: envoy.filters.http.local_ratelimit
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: http_local_rate_limiter
token_bucket:
max_tokens: 1000
tokens_per_fill: 100
fill_interval: 1s
filter_enabled:
runtime_key: local_rate_limit_enabled
default_value:
numerator: 100
denominator: HUNDRED
filter_enforced:
runtime_key: local_rate_limit_enforced
default_value:
numerator: 100
denominator: HUNDRED
response_headers_to_add:
- append_action: OVERWRITE_IF_EXISTS_OR_ADD
header:
key: x-local-rate-limit
value: 'true'
# 라우터 필터 (반드시 마지막)
- name: envoy.filters.http.router
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
# 라우트 설정
route_config:
name: local_route
virtual_hosts:
- name: api_service
domains: ['api.example.com']
routes:
- match:
prefix: '/api/v1/users'
route:
cluster: user_service
timeout: 10s
retry_policy:
retry_on: '5xx,connect-failure,reset'
num_retries: 3
per_try_timeout: 3s
retry_back_off:
base_interval: 0.1s
max_interval: 1s
- match:
prefix: '/api/v1/orders'
route:
cluster: order_service
timeout: 15s
retry_policy:
retry_on: '5xx,connect-failure'
num_retries: 2
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
'@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/server.crt
private_key:
filename: /etc/envoy/certs/server.key
clusters:
- name: user_service
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: user_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: user-service.default.svc.cluster.local
port_value: 8080
# 서킷 브레이커 설정
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 1024
max_pending_requests: 1024
max_requests: 1024
max_retries: 3
# 헬스 체크
health_checks:
- timeout: 5s
interval: 10s
unhealthy_threshold: 3
healthy_threshold: 2
http_health_check:
path: /health
# Outlier Detection (이상 탐지)
outlier_detection:
consecutive_5xx: 5
interval: 10s
base_ejection_time: 30s
max_ejection_percent: 50
- name: order_service
type: STRICT_DNS
lb_policy: LEAST_REQUEST
load_assignment:
cluster_name: order_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: order-service.default.svc.cluster.local
port_value: 8080
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 512
max_pending_requests: 512
- name: auth0_jwks
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: auth0_jwks
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: your-tenant.auth0.com
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
'@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: your-tenant.auth0.com
GraphQL Federation
Apollo Federation v2の概要
GraphQL Federationは、複数のチームが独立して管理するサブグラフ(Subgraph)を1つの統合されたスーパーグラフ(Supergraph)に合成するアーキテクチャパターンです。Apollo Federation v2は改善された共有オーナーシップモデル、向上したタイプマージ、よりクリーンな構文を提供します。
┌──────────────────────────────────────────────────┐
│ 클라이언트 │
│ (Web / Mobile App) │
└────────────────────┬─────────────────────────────┘
│ GraphQL 쿼리
┌────────────────────▼─────────────────────────────┐
│ Apollo Router │
│ (슈퍼그래프 실행 엔진) │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Query Plan (쿼리 실행 계획) │ │
│ │ 1. users 서브그래프에서 User 조회 │ │
│ │ 2. orders 서브그래프에서 Order 조회 │ │
│ │ 3. reviews 서브그래프에서 Review 조회 │ │
│ │ 4. 결과 병합 후 클라이언트에 반환 │ │
│ └────────────────────────────────────────────┘ │
└───────┬───────────────┬───────────────┬──────────┘
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Users │ │ Orders │ │ Reviews │
│ Subgraph │ │ Subgraph │ │ Subgraph │
│ (Team A) │ │ (Team B) │ │ (Team C) │
│ │ │ │ │ │
│ - User │ │ - Order │ │ - Review │
│ - Profile │ │ - LineItem │ │ - Rating │
│ │ │ - extends │ │ - extends │
│ │ │ User │ │ User │
└──────────────┘ └─────────────┘ └─────────────┘
Federationサブグラフの実装
// subgraphs/users/src/index.ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { buildSubgraphSchema } from '@apollo/subgraph'
import { gql } from 'graphql-tag'
const typeDefs = gql`
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.11"
import: ["@key", "@shareable", "@external", "@provides"]
)
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): UserConnection!
me: User
}
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
avatarUrl: String
role: UserRole!
createdAt: String!
updatedAt: String!
}
type UserConnection {
nodes: [User!]!
totalCount: Int!
pageInfo: PageInfo!
}
type PageInfo @shareable {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
enum UserRole {
ADMIN
USER
MODERATOR
}
`
const resolvers = {
Query: {
user: async (_: any, { id }: { id: string }, context: any) => {
return context.dataSources.usersAPI.getUser(id)
},
users: async (_: any, args: any, context: any) => {
return context.dataSources.usersAPI.getUsers(args)
},
me: async (_: any, __: any, context: any) => {
if (!context.userId) throw new Error('Not authenticated')
return context.dataSources.usersAPI.getUser(context.userId)
},
},
User: {
__resolveReference: async (ref: { id: string }, context: any) => {
// Federation이 다른 서브그래프에서 User를 참조할 때 호출
return context.dataSources.usersAPI.getUser(ref.id)
},
},
}
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
})
const { url } = await startStandaloneServer(server, {
listen: { port: 4001 },
context: async ({ req }) => ({
userId: req.headers['x-user-id'],
dataSources: {
usersAPI: new UsersDataSource(),
},
}),
})
console.log(`Users subgraph ready at ${url}`)
// subgraphs/orders/src/index.ts
import { ApolloServer } from '@apollo/server'
import { buildSubgraphSchema } from '@apollo/subgraph'
import { gql } from 'graphql-tag'
const typeDefs = gql`
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.11"
import: ["@key", "@external", "@requires"]
)
type Query {
order(id: ID!): Order
ordersByUser(userId: ID!, status: OrderStatus): [Order!]!
}
# User 타입을 확장하여 orders 필드 추가
type User @key(fields: "id") {
id: ID! @external
orders(status: OrderStatus, limit: Int = 10): [Order!]!
totalSpent: Float! @requires(fields: "id")
}
type Order @key(fields: "id") {
id: ID!
userId: ID!
user: User!
items: [OrderItem!]!
status: OrderStatus!
totalAmount: Float!
currency: String!
createdAt: String!
shippingAddress: Address
}
type OrderItem {
productId: ID!
productName: String!
quantity: Int!
unitPrice: Float!
}
type Address {
street: String!
city: String!
country: String!
zipCode: String!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
`
const resolvers = {
Query: {
order: async (_: any, { id }: { id: string }, ctx: any) => {
return ctx.dataSources.ordersAPI.getOrder(id)
},
ordersByUser: async (_: any, args: any, ctx: any) => {
return ctx.dataSources.ordersAPI.getOrdersByUser(args.userId, args.status)
},
},
User: {
orders: async (user: { id: string }, args: any, ctx: any) => {
return ctx.dataSources.ordersAPI.getOrdersByUser(user.id, args.status)
},
totalSpent: async (user: { id: string }, _: any, ctx: any) => {
return ctx.dataSources.ordersAPI.getTotalSpent(user.id)
},
},
Order: {
user: (order: { userId: string }) => ({ __typename: 'User', id: order.userId }),
},
}
スーパーグラフ構成
# supergraph-config.yaml
# rover supergraph compose --config supergraph-config.yaml > supergraph.graphql
federation_version: =2.11.2
subgraphs:
users:
routing_url: http://users-subgraph:4001/graphql
schema:
subgraph_url: http://users-subgraph:4001/graphql
orders:
routing_url: http://orders-subgraph:4002/graphql
schema:
subgraph_url: http://orders-subgraph:4002/graphql
reviews:
routing_url: http://reviews-subgraph:4003/graphql
schema:
subgraph_url: http://reviews-subgraph:4003/graphql
# router-config.yaml (Apollo Router 설정)
supergraph:
listen: 0.0.0.0:4000
introspection: false # 프로덕션에서는 비활성화
# 헤더 전파
headers:
all:
request:
- propagate:
named: 'Authorization'
- propagate:
named: 'X-Request-ID'
- propagate:
named: 'X-User-ID'
# 서브그래프별 설정
traffic_shaping:
all:
timeout: 10s
subgraphs:
orders:
timeout: 15s # 주문 서비스는 더 긴 타임아웃
# 쿼리 깊이 제한 (DoS 방지)
limits:
max_depth: 15
max_height: 200
max_aliases: 30
# 텔레메트리
telemetry:
exporters:
tracing:
otlp:
enabled: true
endpoint: http://otel-collector:4317
protocol: grpc
metrics:
prometheus:
enabled: true
listen: 0.0.0.0:9090
path: /metrics
# 응답 캐싱
preview_entity_cache:
enabled: true
subgraph:
all:
enabled: true
ttl: 60s
認証と認可の統合
ゲートウェイ層JWT検証ミドルウェア
ゲートウェイでJWTを検証し、検証済みのクレームをヘッダーに変換して下位サービスに転送するのが標準パターンです。RS256またはES256アルゴリズムで署名されたトークンを使用し、IdPの公開鍵で署名を検証します。
// gateway/src/middleware/auth.ts
import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'
import { Request, Response, NextFunction } from 'express'
// JWKS 클라이언트 (키 캐싱 포함)
const client = jwksClient({
jwksUri: `${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 600000, // 10분
rateLimit: true,
jwksRequestsPerMinute: 10,
})
function getSigningKey(header: jwt.JwtHeader): Promise<string> {
return new Promise((resolve, reject) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) return reject(err)
const signingKey = key?.getPublicKey()
if (!signingKey) return reject(new Error('No signing key found'))
resolve(signingKey)
})
})
}
// 경로별 인증 정책
const AUTH_POLICIES: Record<string, { required: boolean; scopes?: string[] }> = {
'/api/v1/users/me': { required: true, scopes: ['read:profile'] },
'/api/v1/orders': { required: true, scopes: ['read:orders'] },
'/api/v1/admin': { required: true, scopes: ['admin:all'] },
'/health': { required: false },
'/api/v1/products': { required: false }, // 공개 API
}
export async function authMiddleware(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
// 경로별 정책 조회
const policy = Object.entries(AUTH_POLICIES).find(([path]) => req.path.startsWith(path))
if (policy && !policy[1].required) {
return next()
}
const authHeader = req.headers.authorization
if (!authHeader?.startsWith('Bearer ')) {
res.status(401).json({
error: 'unauthorized',
message: 'Missing or invalid Authorization header',
})
return
}
const token = authHeader.substring(7)
try {
// JWT 디코딩 (검증 전 헤더 확인)
const decoded = jwt.decode(token, { complete: true })
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid token format')
}
// 알고리즘 화이트리스트 검증
if (!['RS256', 'ES256'].includes(decoded.header.alg)) {
throw new Error(`Unsupported algorithm: ${decoded.header.alg}`)
}
// JWKS에서 공개 키를 가져와 서명 검증
const signingKey = await getSigningKey(decoded.header)
const payload = jwt.verify(token, signingKey, {
algorithms: ['RS256', 'ES256'],
audience: process.env.API_AUDIENCE,
issuer: `${process.env.AUTH0_DOMAIN}/`,
clockTolerance: 30, // 30초 시계 오차 허용
}) as jwt.JwtPayload
// 스코프 검증
if (policy?.[1]?.scopes) {
const tokenScopes = (payload.scope || '').split(' ')
const requiredScopes = policy[1].scopes
const hasAllScopes = requiredScopes.every((s) => tokenScopes.includes(s))
if (!hasAllScopes) {
res.status(403).json({
error: 'insufficient_scope',
message: `Required scopes: ${requiredScopes.join(', ')}`,
})
return
}
}
// 검증된 클레임을 헤더로 전파 (하위 서비스에서 사용)
req.headers['x-user-id'] = payload.sub || ''
req.headers['x-user-email'] = payload.email || ''
req.headers['x-user-roles'] = JSON.stringify(payload['https://api.example.com/roles'] || [])
req.headers['x-auth-time'] = String(payload.iat || 0)
// 원본 Authorization 헤더는 제거 (하위 서비스에 토큰 노출 방지)
delete req.headers.authorization
next()
} catch (error: any) {
const errorMap: Record<string, { status: number; message: string }> = {
TokenExpiredError: { status: 401, message: 'Token has expired' },
JsonWebTokenError: { status: 401, message: 'Invalid token' },
NotBeforeError: { status: 401, message: 'Token not yet valid' },
}
const mapped = errorMap[error.name] || { status: 401, message: 'Authentication failed' }
res.status(mapped.status).json({ error: error.name, message: mapped.message })
}
}
レートリミッティングとサーキットブレーカー
多段階レートリミッティング戦略
効果的なレートリミッティングは単一層ではなく多段階で構成されます。EdgeではIP基準の積極的な制限でDDoSを防御し、ゲートウェイではユーザー/APIキーベースのきめ細かな制限を適用します。
// gateway/src/middleware/rateLimiter.ts
import Redis from 'ioredis'
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 100, 3000),
enableReadyCheck: true,
})
interface RateLimitConfig {
windowMs: number // 시간 창 (밀리초)
maxRequests: number // 최대 요청 수
keyPrefix: string // Redis 키 프리픽스
}
// 슬라이딩 윈도우 레이트 리미터 (Redis Sorted Set 활용)
async function slidingWindowRateLimit(
identifier: string,
config: RateLimitConfig
): Promise<{ allowed: boolean; remaining: number; retryAfter?: number }> {
const key = `${config.keyPrefix}:${identifier}`
const now = Date.now()
const windowStart = now - config.windowMs
// Redis 트랜잭션으로 원자적 처리
const pipeline = redis.pipeline()
pipeline.zremrangebyscore(key, 0, windowStart) // 만료된 항목 제거
pipeline.zadd(key, now.toString(), `${now}:${Math.random()}`) // 현재 요청 추가
pipeline.zcard(key) // 현재 윈도우 내 요청 수
pipeline.expire(key, Math.ceil(config.windowMs / 1000)) // TTL 설정
const results = await pipeline.exec()
if (!results) throw new Error('Redis pipeline failed')
const currentCount = results[2]?.[1] as number
const allowed = currentCount <= config.maxRequests
const remaining = Math.max(0, config.maxRequests - currentCount)
if (!allowed) {
// 가장 오래된 요청이 만료되는 시점 계산
const oldestInWindow = await redis.zrange(key, 0, 0, 'WITHSCORES')
const retryAfter =
oldestInWindow.length >= 2
? Math.ceil((parseInt(oldestInWindow[1]) + config.windowMs - now) / 1000)
: Math.ceil(config.windowMs / 1000)
return { allowed: false, remaining: 0, retryAfter }
}
return { allowed: true, remaining }
}
// 티어별 레이트 리미팅 정책
const RATE_LIMIT_TIERS: Record<string, RateLimitConfig> = {
free: { windowMs: 60_000, maxRequests: 60, keyPrefix: 'rl:free' },
pro: { windowMs: 60_000, maxRequests: 600, keyPrefix: 'rl:pro' },
enterprise: { windowMs: 60_000, maxRequests: 6000, keyPrefix: 'rl:enterprise' },
// IP 기반 글로벌 제한 (DDoS 방어)
global_ip: { windowMs: 1_000, maxRequests: 50, keyPrefix: 'rl:ip' },
}
export async function rateLimitMiddleware(req: any, res: any, next: any) {
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip
const userId = req.headers['x-user-id']
const tier = req.headers['x-user-tier'] || 'free'
try {
// 1단계: IP 기반 글로벌 레이트 리미팅
const ipResult = await slidingWindowRateLimit(clientIp, RATE_LIMIT_TIERS.global_ip)
if (!ipResult.allowed) {
res.set('Retry-After', String(ipResult.retryAfter))
return res.status(429).json({ error: 'Too many requests (IP limit)' })
}
// 2단계: 사용자/티어 기반 레이트 리미팅
if (userId) {
const config = RATE_LIMIT_TIERS[tier] || RATE_LIMIT_TIERS.free
const userResult = await slidingWindowRateLimit(userId, config)
res.set('X-RateLimit-Limit', String(config.maxRequests))
res.set('X-RateLimit-Remaining', String(userResult.remaining))
if (!userResult.allowed) {
res.set('Retry-After', String(userResult.retryAfter))
return res.status(429).json({ error: 'Rate limit exceeded', tier })
}
}
next()
} catch (error) {
// Redis 장애 시 요청을 통과시킴 (fault-tolerant)
console.error('Rate limiter error:', error)
next()
}
}
サーキットブレーカーパターン
サーキットブレーカーは失敗率が閾値を超えると回路を開いて高速な失敗レスポンスを返し、クールダウン後にHalf-Open状態で段階的に復旧を試みます。
// gateway/src/middleware/circuitBreaker.ts
enum CircuitState {
CLOSED = 'CLOSED', // 정상 - 요청 통과
OPEN = 'OPEN', // 차단 - 즉시 실패 반환
HALF_OPEN = 'HALF_OPEN', // 테스트 - 제한적 요청 허용
}
interface CircuitBreakerConfig {
failureThreshold: number // 실패 임계치 (예: 5)
successThreshold: number // 복구 판정 성공 횟수 (예: 3)
timeout: number // Open 상태 유지 시간 (ms)
monitorWindow: number // 모니터링 윈도우 (ms)
halfOpenMaxRequests: number // Half-Open에서 허용할 최대 요청
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED
private failureCount = 0
private successCount = 0
private lastFailureTime = 0
private halfOpenRequests = 0
constructor(
private name: string,
private config: CircuitBreakerConfig
) {}
async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime >= this.config.timeout) {
this.transitionTo(CircuitState.HALF_OPEN)
} else {
console.warn(`[CircuitBreaker:${this.name}] OPEN - rejecting request`)
if (fallback) return fallback()
throw new Error(`Service ${this.name} is unavailable (circuit open)`)
}
}
if (this.state === CircuitState.HALF_OPEN) {
if (this.halfOpenRequests >= this.config.halfOpenMaxRequests) {
if (fallback) return fallback()
throw new Error(`Service ${this.name} is testing recovery`)
}
this.halfOpenRequests++
}
try {
const result = await fn()
this.onSuccess()
return result
} catch (error) {
this.onFailure()
if (fallback && this.state === CircuitState.OPEN) return fallback()
throw error
}
}
private onSuccess() {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++
if (this.successCount >= this.config.successThreshold) {
this.transitionTo(CircuitState.CLOSED)
}
}
this.failureCount = 0
}
private onFailure() {
this.failureCount++
this.lastFailureTime = Date.now()
if (this.failureCount >= this.config.failureThreshold) {
this.transitionTo(CircuitState.OPEN)
}
}
private transitionTo(newState: CircuitState) {
console.log(`[CircuitBreaker:${this.name}] ${this.state} -> ${newState}`)
this.state = newState
if (newState === CircuitState.CLOSED) {
this.failureCount = 0
this.successCount = 0
}
if (newState === CircuitState.HALF_OPEN) {
this.halfOpenRequests = 0
this.successCount = 0
}
}
getStatus() {
return {
name: this.name,
state: this.state,
failureCount: this.failureCount,
lastFailureTime: this.lastFailureTime ? new Date(this.lastFailureTime).toISOString() : null,
}
}
}
// 서비스별 서킷 브레이커 인스턴스
export const circuitBreakers = {
userService: new CircuitBreaker('user-service', {
failureThreshold: 5,
successThreshold: 3,
timeout: 30_000, // 30초 후 Half-Open 전환
monitorWindow: 60_000, // 1분 모니터링 윈도우
halfOpenMaxRequests: 3,
}),
orderService: new CircuitBreaker('order-service', {
failureThreshold: 3,
successThreshold: 2,
timeout: 60_000, // 결제 관련은 보수적으로 60초
monitorWindow: 60_000,
halfOpenMaxRequests: 1,
}),
}
オブザーバビリティの統合
ゲートウェイはすべてのトラフィックが通過するポイントであるため、オブザーバビリティの中核ハブとなります。ログ、メトリクス、トレースの3つの柱(Three Pillars)をすべてゲートウェイ層で収集する必要があります。
OpenTelemetry統合設定
# otel-collector-config.yaml
# OpenTelemetry Collector 설정 - 게이트웨이 트래픽 수집
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1024
# 게이트웨이 메타데이터 추가
attributes:
actions:
- key: deployment.environment
value: production
action: upsert
- key: service.layer
value: gateway
action: upsert
# 샘플링 (전체의 10%만 상세 트레이스)
tail_sampling:
decision_wait: 10s
policies:
- name: error-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: latency-policy
type: latency
latency:
threshold_ms: 1000
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 10
exporters:
otlphttp/jaeger:
endpoint: http://jaeger:4318
prometheus:
endpoint: 0.0.0.0:8889
loki:
endpoint: http://loki:3100/loki/api/v1/push
labels:
resource:
service.name: 'service_name'
service.layer: 'layer'
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, attributes, tail_sampling]
exporters: [otlphttp/jaeger]
metrics:
receivers: [otlp]
processors: [batch, attributes]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [batch, attributes]
exporters: [loki]
コアメトリクスダッシュボード項目
ゲートウェイで必ずモニタリングすべきメトリクスは以下の通りです。
- リクエストレート(Request Rate): 毎秒リクエスト数。異常な急増はDDoSまたはクライアントバグを意味します。
- エラーレート(Error Rate): 4xx、5xxレスポンスの割合。サービスごとに分離して追跡します。
- レイテンシ(Latency): p50、p95、p99パーセンタイル。p99がSLAを超過するとアラート発生。
- サーキットブレーカー状態: 各サービスのサーキット状態をリアルタイムで表示します。
- レートリミッティングヒット率: 制限に引っかかるリクエストの割合。正常なユーザーが頻繁に制限される場合、閾値の調整が必要です。
- アップストリーム接続プール: アクティブ接続、待機リクエスト、タイムアウト数を追跡します。
トラブルシューティング
障害シナリオ1:ゲートウェイメモリリーク
症状: ゲートウェイPodのメモリ使用量が持続的に増加してOOM Killが発生。
原因: 大容量のリクエスト/レスポンスボディをバッファリングしながらメモリを解放できない場合。特にファイルアップロードAPIや大容量JSONレスポンスをプロキシする際に発生。
解決策:
# Kong: 요청/응답 본문 버퍼링 제한
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: request-size-limiting
config:
allowed_payload_size: 10 # MB
size_unit: megabytes
require_content_length: true
plugin: request-size-limiting
# Envoy: 버퍼 제한 설정
http_filters:
- name: envoy.filters.http.buffer
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer
max_request_bytes: 10485760 # 10MB
障害シナリオ2:カスケード障害
症状: 1つのバックエンドサービスの障害がゲートウェイを通じてシステム全体に伝播。
原因: サーキットブレーカー未設定またはタイムアウトが長すぎる場合。障害サービスへのリクエストがゲートウェイの接続プールを枯渇させる。
復旧手順:
- 障害サービスに対するサーキットブレーカーを直ちに手動で開放
- 該当サービスルートに静的フォールバックレスポンスを設定
- 接続プールの状態を確認し、必要に応じてゲートウェイPodのローリング再起動
- 障害サービス復旧後、トラフィックを段階的に復元(カナリア)
障害シナリオ3:TLS証明書の期限切れ
症状: 突然のAPI全体503エラー。クライアントでSSL/TLSハンドシェイク失敗ログを確認。
予防: cert-managerを活用した自動証明書更新と、期限切れ30日前のアラート設定。
# 인증서 만료일 확인 스크립트
#!/bin/bash
DOMAIN="api.example.com"
EXPIRY=$(echo | openssl s_client -servername $DOMAIN -connect $DOMAIN:443 2>/dev/null \
| openssl x509 -noout -dates 2>/dev/null \
| grep notAfter | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
echo "Certificate expires: $EXPIRY ($DAYS_LEFT days remaining)"
if [ "$DAYS_LEFT" -lt 30 ]; then
echo "WARNING: Certificate expiring soon!"
# 슬랙/PagerDuty 알림 전송
fi
障害シナリオ4:GraphQL Federationサブグラフスキーマの衝突
症状: rover supergraph composeが失敗。2つのサブグラフが同じタイプの同じフィールドを異なる戻り値型で定義。
解決策: Federation v2の@shareableディレクティブを使用して共有タイプフィールドを明示的に宣言し、@overrideディレクティブでフィールドのオーナーシップを移行します。CI/CDパイプラインでrover subgraph checkを実行して互換性を事前検証する必要があります。
本番チェックリスト
デプロイ前の検証
- ゲートウェイクラスタを最低3ノード以上でHA構成
- TLS 1.3の強制適用と弱い暗号スイートの無効化
- ヘルスチェックエンドポイント(
/health、/ready)の構成 - リクエスト/レスポンスサイズ制限の設定(デフォルト10MB以下)
- タイムアウト設定:接続5秒、読み取り30秒、書き込み30秒
- DNS TTLの適切な設定(長すぎると障害復旧が遅延)
セキュリティ
- すべての外部通信にTLS適用、内部通信にmTLS適用
- JWT検証はゲートウェイで実行、トークンは下位サービスに転送しない
- CORS許可ドメインのホワイトリスト適用
- レートリミッティング:IPベース(DDoS)、ユーザーベース(公平使用)の二重適用
- 機密ヘッダー(Authorization、Cookie)のロギングでのマスキング
- Admin APIを内部ネットワークからのみアクセス可能に制限
オブザーバビリティ
- 分散トレーシング:すべてのリクエストにX-Request-IDを付与して伝播
- メトリクス:RED(Rate、Errors、Duration)メトリクスの収集とダッシュボード構築
- アラート:エラー率5%超過、p99レイテンシSLA超過、サーキットブレーカーOpen時に即時アラート
- ロギング:構造化JSONログ、リクエスト/レスポンスボディはデバッグモードでのみロギング
運用
- カナリアデプロイ:ゲートウェイ設定変更時に5-10%のトラフィックで検証後、全体適用
- 設定バージョン管理:すべてのゲートウェイ設定をGitで管理(GitOps)
- ロールバック手順:30秒以内に以前の設定にロールバック可能な手順を確保
- 負荷テスト:予想ピークトラフィックの2倍以上で定期的に負荷テストを実施
- カオスエンジニアリング:ゲートウェイノード1台の障害時の自動復旧を検証
アンチパターンと失敗事例
アンチパターン1:God Gateway(神ゲートウェイ)
すべてのビジネスロジックをゲートウェイに実装するパターン。リクエスト検証、データ変換、ビジネスルール適用、レスポンス集約をすべてゲートウェイが処理すると、ゲートウェイがモノリス化します。ゲートウェイはルーティング、認証、レートリミッティングなどの横断的関心事のみ処理すべきです。
アンチパターン2:共有BFF
1つのBFFがWebとモバイルの両方にサービスを提供し始めると、双方の要件が衝突して複雑さが指数関数的に増加します。「このフィールドはモバイルでのみ必要」という条件文がBFF全体に広がり、BFFがなかった時よりも悪い状況になります。各フロントエンドタイプごとに専用のBFFを維持してください。
アンチパターン3:ゲートウェイでのデータ変換の濫用
ゲートウェイで過度なレスポンス変換(フィールド名変更、データ構造の再配置、ビジネスロジックに基づくフィールドフィルタリング)を実行すると、ゲートウェイのCPU使用量が急増しデバッグが困難になります。データ変換はBFFまたはサービス自体で実行すべきです。
アンチパターン4:レートリミッティングなしのデプロイ
「我々のサービスはトラフィックが少ないのでレートリミッティングは不要」というのは最もよくある間違いです。予期しないクローラー、不正に実装されたクライアントの無限リトライ、APIキー漏洩による不正使用はトラフィック規模に関係なく発生します。本番デプロイ時のレートリミッティングは選択ではなく必須です。
アンチパターン5:サーキットブレーカー未適用の同期呼び出しチェーン
ゲートウェイがサービスAを呼び出し、サービスAがサービスBを呼び出す同期チェーンでサーキットブレーカーがない場合、サービスBの障害がサービスAとゲートウェイを連鎖的にダウンさせます(カスケード障害)。すべての外部呼び出しポイントにサーキットブレーカーを適用し、可能であれば非同期通信に切り替えてください。
失敗事例:GraphQL Federation N+1クエリ問題
Federation環境でRouterがサブグラフAからユーザーリストを取得した後、各ユーザーの注文をサブグラフBから個別に照会するとN+1問題が発生します。Apollo RouterのQuery Planは可能な限りバッチリクエストで最適化しますが、サブグラフがバッチ照会をサポートしない場合、性能が急激に低下します。サブグラフの__resolveReferenceがバッチをサポートするようDataLoaderパターンを必ず適用する必要があります。
参考資料
- Microservices Pattern: API Gateway / Backends for Frontends - Chris Richardsonのマイクロサービスパターンリファレンス
- Sam Newman - Backends For Frontends - BFFパターンの原著者による説明
- Kong Gateway Kubernetes Documentation - Kong公式Kubernetesインストールガイド
- Envoy Proxy Documentation - Envoyプロキシ公式ドキュメント
- Introduction to Apollo Federation - Apollo GraphQL Federation公式ドキュメント
- The Backend for Frontend Pattern (BFF) - Auth0 - Auth0のBFFパターンとセキュリティガイド
- Backends for Frontends Pattern - Azure Architecture Center - Microsoft AzureのBFFパターンリファレンス
- Kubernetes Gateway API Implementations - Kubernetes Gateway API実装一覧
- API Gateway Patterns for Microservices - Kong vs NGINX vs Envoy - ゲートウェイ比較分析