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:
29
infra/apollo-router/Dockerfile
Normal file
29
infra/apollo-router/Dockerfile
Normal 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
|
||||||
|
|
||||||
|
# 安装 curl(healthcheck + rover 下载)
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends curl ca-certificates && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 安装 rover(Apollo 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"]
|
||||||
73
infra/apollo-router/entrypoint.sh
Normal file
73
infra/apollo-router/entrypoint.sh
Normal 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
|
||||||
77
infra/apollo-router/router.yaml
Normal file
77
infra/apollo-router/router.yaml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Apollo Router 配置(v2.1 M2)
|
||||||
|
#
|
||||||
|
# Router 作为唯一的外部 GraphQL 入口,替代 3 个 BFF 的手动聚合。
|
||||||
|
# 前端 → Router(GraphQL)→ 各子图(/graphql)
|
||||||
|
# 内部调用(后端 → 后端)仍走 gRPC(ADR-037)。
|
||||||
|
#
|
||||||
|
# 端口:3000(NestJS 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 Header(ADR-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
|
||||||
42
infra/apollo-router/supergraph.yaml
Normal file
42
infra/apollo-router/supergraph.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Apollo Federation 2 Supergraph 组合配置
|
||||||
|
#
|
||||||
|
# 列出所有子图及其路由 URL,rover 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
|
||||||
@@ -294,6 +294,30 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
redis:
|
redis:
|
||||||
condition: service_started
|
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:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|||||||
@@ -37,20 +37,21 @@
|
|||||||
|
|
||||||
## 3. NestJS BFF + 业务服务(3000-3099)
|
## 3. NestJS BFF + 业务服务(3000-3099)
|
||||||
|
|
||||||
| 服务 | HTTP 端口 | gRPC 端口 | 说明 | 阶段 |
|
| 服务 | HTTP 端口 | gRPC 端口 | 说明 | 阶段 |
|
||||||
| ----------- | --------- | --------- | ----------------------------------------------- | ------- |
|
| ------------- | --------- | --------- | ----------------------------------------------- | ------- |
|
||||||
| iam | 3002 | 50052 | P2 启用 gRPC server(I1 裁决,2026-07-09 修正) | P2 ✅ |
|
| apollo-router | 3000 | — | v2.1 GraphQL 聚合层(替代 BFF 手动聚合) | P3 |
|
||||||
| teacher-bff | 3003 | — | BFF 不暴露 gRPC,对下游走 gRPC | P2 ✅ |
|
| iam | 3002 | 50052 | P2 启用 gRPC server(I1 裁决,2026-07-09 修正) | P2 ✅ |
|
||||||
| core-edu | 3004 | 50053 | 含原 classes 服务(合并后) | P3 |
|
| teacher-bff | 3003 | — | BFF 不暴露 gRPC,对下游走 gRPC | P2 ✅ |
|
||||||
| content | 3005 | 50054 | Neo4j + ES | P4 |
|
| core-edu | 3004 | 50053 | 含原 classes 服务(合并后) | P3 |
|
||||||
| data-ana | 3006 | 50055 | HTTP 保留作 Gateway 直连降级,gRPC 为 P4 主入口 | P4 |
|
| content | 3005 | 50054 | Neo4j + ES | P4 |
|
||||||
| msg | 3007 | 50056 | 通知中心 | P5 |
|
| data-ana | 3006 | 50055 | HTTP 保留作 Gateway 直连降级,gRPC 为 P4 主入口 | P4 |
|
||||||
| ai | 3008 | 50058 | LLM 网关 | P5 |
|
| msg | 3007 | 50056 | 通知中心 | P5 |
|
||||||
| student-bff | 3009 | — | BFF 不暴露 gRPC | P3 |
|
| ai | 3008 | 50058 | LLM 网关 | P5 |
|
||||||
| parent-bff | 3010 | — | BFF 不暴露 gRPC | P4 |
|
| student-bff | 3009 | — | BFF 不暴露 gRPC | P3 |
|
||||||
| ~~classes~~ | ~~3001~~ | ~~—~~ | 已合并入 core-edu(P3) | P1 历史 |
|
| parent-bff | 3010 | — | BFF 不暴露 gRPC | P4 |
|
||||||
|
| ~~classes~~ | ~~3001~~ | ~~—~~ | 已合并入 core-edu(P3) | P1 历史 |
|
||||||
|
|
||||||
> **端口空闲**:3000、3011-3099 预留扩展。
|
> **端口空闲**:3011-3099 预留扩展。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -48,3 +48,10 @@ REDIS_PASSWORD=<replace-with-24-char-redis-password>
|
|||||||
# 生成:openssl rand -base64 32
|
# 生成:openssl rand -base64 32
|
||||||
# 注意:旋转前需先解密所有已加密字段,旋转后重新加密
|
# 注意:旋转前需先解密所有已加密字段,旋转后重新加密
|
||||||
ENCRYPTION_KEY=<replace-with-base64-32-byte-aes-key>
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user