feat(infra): apollo router deployment and supergraph composition

M2: Apollo Router replaces BFF manual aggregation (ADR-037)

- Dockerfile: self-contained with rover + router, auto-compose on startup

- router.yaml: CORS, Router-Authorization header injection, traffic shaping

- supergraph.yaml: 6 subgraphs (iam/core-edu/content/msg/ai/data-ana)

- docker-compose: apollo-router on port 3000, depends on all subgraphs

- ROUTER_AUTH_SECRET added to secrets.example.env

- port-allocation.md: apollo-router registered on port 3000
This commit is contained in:
SpecialX
2026-07-15 01:32:14 +08:00
parent 6af1aa0d82
commit 163bff6666
7 changed files with 266 additions and 13 deletions

View File

@@ -0,0 +1,29 @@
# Apollo Router Dockerfile (v2.1 M2)
#
# 自包含镜像rover组合 supergraph+ apollo-router运行时
# 启动时自动等待子图就绪 → rover compose → 启动 router
FROM ghcr.io/apollographql/router:v1.45.0
USER root
# 安装 curlhealthcheck + rover 下载)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
rm -rf /var/lib/apt/lists/*
# 安装 roverApollo CLI for supergraph composition
RUN curl -sSL https://rover.apollo.dev/nix/v0.30.0 | sh -s -- --install /usr/local/bin
# 接受 ELv2 许可
ENV APOLLO_ELV2_LICENSE=accept
# 复制配置文件
COPY router.yaml /dist/configuration.yaml
COPY supergraph.yaml /dist/supergraph.yaml
COPY entrypoint.sh /dist/entrypoint.sh
RUN chmod +x /dist/entrypoint.sh
EXPOSE 3000 8088
ENTRYPOINT ["/dist/entrypoint.sh"]

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Apollo Router 启动脚本
#
# 1. 等待所有子图 /graphql 端点就绪
# 2. 使用 rover 组合 supergraph SDL
# 3. 启动 apollo-router
set -e
SUBGRAPHS=(
"iam:3002"
"core-edu:3004"
"content:3005"
"msg:3007"
"ai:3008"
"data-ana:3006"
)
echo "[apollo-router] Waiting for subgraphs to be ready..."
wait_for_subgraph() {
local name="$1"
local host="$2"
local port="$3"
local max_retries=60
local retry=0
while [ $retry -lt $max_retries ]; do
if curl -sf -o /dev/null "http://${host}:${port}/healthz" 2>/dev/null; then
echo "[apollo-router] ${name} is ready (http://${host}:${port})"
return 0
fi
retry=$((retry + 1))
echo "[apollo-router] Waiting for ${name} at ${host}:${port} (attempt ${retry}/${max_retries})"
sleep 3
done
echo "[apollo-router] WARNING: ${name} not ready after ${max_retries} attempts, continuing anyway..."
return 0
}
for entry in "${SUBGRAPHS[@]}"; do
name="${entry%%:*}"
hostport="${entry#*:}"
host="${hostport%%:*}"
port="${hostport##*:}"
wait_for_subgraph "$name" "$host" "$port"
done
# 组合 supergraph SDL
echo "[apollo-router] Composing supergraph SDL..."
export APOLLO_ELV2_LICENSE=accept
max_compose_retries=5
compose_retry=0
while [ $compose_retry -lt $max_compose_retries ]; do
compose_retry=$((compose_retry + 1))
if rover supergraph compose --config /dist/supergraph.yaml --output /tmp/supergraph.graphql 2>&1; then
echo "[apollo-router] Supergraph composed successfully"
break
fi
echo "[apollo-router] Compose attempt ${compose_retry}/${max_compose_retries} failed, retrying in 5s..."
sleep 5
done
if [ ! -f /tmp/supergraph.graphql ]; then
echo "[apollo-router] ERROR: Failed to compose supergraph after ${max_compose_retries} attempts"
exit 1
fi
# 启动 router
echo "[apollo-router] Starting Apollo Router on port 3000..."
exec /router --config /dist/configuration.yaml --supergraph /tmp/supergraph.graphql --hot-reload

View File

@@ -0,0 +1,77 @@
# Apollo Router 配置v2.1 M2
#
# Router 作为唯一的外部 GraphQL 入口,替代 3 个 BFF 的手动聚合。
# 前端 → RouterGraphQL→ 各子图(/graphql
# 内部调用(后端 → 后端)仍走 gRPCADR-037
#
# 端口3000NestJS BFF 段空闲端口)
# 镜像ghcr.io/apollographql/router
supergraph:
listen: 0.0.0.0:3000
path: /graphql
introspection: true
# Sandbox 模式:支持开发和调试
homepage:
enabled: true
path: /
# CORS允许前端 portal 访问
sandbox:
enabled: true
cors:
origins:
- "http://localhost:4000"
- "http://localhost:4001"
- "http://localhost:4002"
- "http://localhost:4003"
- "http://teacher-portal:4000"
- "http://student-portal:4001"
- "http://parent-portal:4002"
- "http://admin-portal:4003"
methods:
- GET
- POST
- OPTIONS
headers:
- Authorization
- Content-Type
- X-Request-Id
- X-Expected-Version
- If-Match
credentials: true
# 向所有子图注入 Router-Authorization HeaderADR-036
# 子图的 RouterAuthGuard 校验此 Header拒绝非 Router 的直接 GraphQL 请求
headers:
all:
request:
- add:
name: "router-authorization"
value: "${env.ROUTER_AUTH_SECRET}"
# 流量控制
traffic_shaping:
all:
router:
timeout: 30s
subgraph:
timeout: 10s
global_rate_limit:
capacity: 1000
interval: 1s
# 健康检查
health_check:
listen: 0.0.0.0:8088
# 日志
telemetry:
instrumentation:
spans:
mode: spec_compliant
logging:
level: info
format: json

View File

@@ -0,0 +1,42 @@
# Apollo Federation 2 Supergraph 组合配置
#
# 列出所有子图及其路由 URLrover supergraph compose 使用此文件生成 supergraph SDL。
# 子图 /graphql 端点必须返回 _service { sdl } 以支持内省组合。
#
# 生成命令:
# rover supergraph compose --config supergraph.yaml --output supergraph-schema.graphql
#
# v2.1 M2: apollo-router 替代 3 个 BFF 的手动聚合职责
federation_version: =2.9.0
subgraphs:
iam:
routing_url: http://iam:3002/graphql
schema:
subgraph_url: http://iam:3002/graphql
core-edu:
routing_url: http://core-edu:3004/graphql
schema:
subgraph_url: http://core-edu:3004/graphql
content:
routing_url: http://content:3005/graphql
schema:
subgraph_url: http://content:3005/graphql
msg:
routing_url: http://msg:3007/graphql
schema:
subgraph_url: http://msg:3007/graphql
ai:
routing_url: http://ai:3008/graphql
schema:
subgraph_url: http://ai:3008/graphql
data-ana:
routing_url: http://data-ana:3006/graphql
schema:
subgraph_url: http://data-ana:3006/graphql

View File

@@ -294,6 +294,30 @@ services:
depends_on:
redis:
condition: service_started
apollo-router:
build:
context: ./apollo-router
container_name: edu-apollo-router
profiles: ["p3", "p4", "p5", "p6"]
restart: unless-stopped
environment:
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-dev-router-secret}
APOLLO_ELV2_LICENSE: accept
ports:
- "3000:3000"
- "8088:8088"
depends_on:
- iam
- core-edu
- content
- msg
- ai
- data-ana
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
interval: 15s
timeout: 5s
retries: 5
volumes:
mysql_data:
redis_data:

View File

@@ -38,7 +38,8 @@
## 3. NestJS BFF + 业务服务3000-3099
| 服务 | HTTP 端口 | gRPC 端口 | 说明 | 阶段 |
| ----------- | --------- | --------- | ----------------------------------------------- | ------- |
| ------------- | --------- | --------- | ----------------------------------------------- | ------- |
| apollo-router | 3000 | — | v2.1 GraphQL 聚合层(替代 BFF 手动聚合) | P3 |
| iam | 3002 | 50052 | P2 启用 gRPC serverI1 裁决2026-07-09 修正) | P2 ✅ |
| teacher-bff | 3003 | — | BFF 不暴露 gRPC对下游走 gRPC | P2 ✅ |
| core-edu | 3004 | 50053 | 含原 classes 服务(合并后) | P3 |
@@ -50,7 +51,7 @@
| parent-bff | 3010 | — | BFF 不暴露 gRPC | P4 |
| ~~classes~~ | ~~3001~~ | ~~—~~ | 已合并入 core-eduP3 | P1 历史 |
> **端口空闲**3000、3011-3099 预留扩展。
> **端口空闲**3011-3099 预留扩展。
---

View File

@@ -48,3 +48,10 @@ REDIS_PASSWORD=<replace-with-24-char-redis-password>
# 生成openssl rand -base64 32
# 注意:旋转前需先解密所有已加密字段,旋转后重新加密
ENCRYPTION_KEY=<replace-with-base64-32-byte-aes-key>
# ---------- Apollo Router 信任凭证 ----------
# 用途Router → 子图的共享密钥,子图 RouterAuthGuard 校验此 Header
# 最小长度32 字符
# 生成openssl rand -hex 32
# 注意Router 和所有子图必须使用相同的密钥
ROUTER_AUTH_SECRET=<replace-with-32-char-router-auth-secret>