From 163bff6666a72df48f29218f041f1641f8565ee7 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:32:14 +0800 Subject: [PATCH] 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 --- infra/apollo-router/Dockerfile | 29 +++++++++++ infra/apollo-router/entrypoint.sh | 73 +++++++++++++++++++++++++++ infra/apollo-router/router.yaml | 77 +++++++++++++++++++++++++++++ infra/apollo-router/supergraph.yaml | 42 ++++++++++++++++ infra/docker-compose.yml | 24 +++++++++ infra/port-allocation.md | 27 +++++----- infra/security/secrets.example.env | 7 +++ 7 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 infra/apollo-router/Dockerfile create mode 100644 infra/apollo-router/entrypoint.sh create mode 100644 infra/apollo-router/router.yaml create mode 100644 infra/apollo-router/supergraph.yaml diff --git a/infra/apollo-router/Dockerfile b/infra/apollo-router/Dockerfile new file mode 100644 index 0000000..ab10041 --- /dev/null +++ b/infra/apollo-router/Dockerfile @@ -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"] diff --git a/infra/apollo-router/entrypoint.sh b/infra/apollo-router/entrypoint.sh new file mode 100644 index 0000000..fcfaad2 --- /dev/null +++ b/infra/apollo-router/entrypoint.sh @@ -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 diff --git a/infra/apollo-router/router.yaml b/infra/apollo-router/router.yaml new file mode 100644 index 0000000..9526b56 --- /dev/null +++ b/infra/apollo-router/router.yaml @@ -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 diff --git a/infra/apollo-router/supergraph.yaml b/infra/apollo-router/supergraph.yaml new file mode 100644 index 0000000..6c829f3 --- /dev/null +++ b/infra/apollo-router/supergraph.yaml @@ -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 diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index a0c196b..c4443d6 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -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: diff --git a/infra/port-allocation.md b/infra/port-allocation.md index b818ea1..77b80fb 100644 --- a/infra/port-allocation.md +++ b/infra/port-allocation.md @@ -37,20 +37,21 @@ ## 3. NestJS BFF + 业务服务(3000-3099) -| 服务 | HTTP 端口 | gRPC 端口 | 说明 | 阶段 | -| ----------- | --------- | --------- | ----------------------------------------------- | ------- | -| iam | 3002 | 50052 | P2 启用 gRPC server(I1 裁决,2026-07-09 修正) | P2 ✅ | -| teacher-bff | 3003 | — | BFF 不暴露 gRPC,对下游走 gRPC | P2 ✅ | -| core-edu | 3004 | 50053 | 含原 classes 服务(合并后) | P3 | -| content | 3005 | 50054 | Neo4j + ES | P4 | -| data-ana | 3006 | 50055 | HTTP 保留作 Gateway 直连降级,gRPC 为 P4 主入口 | P4 | -| msg | 3007 | 50056 | 通知中心 | P5 | -| ai | 3008 | 50058 | LLM 网关 | P5 | -| student-bff | 3009 | — | BFF 不暴露 gRPC | P3 | -| parent-bff | 3010 | — | BFF 不暴露 gRPC | P4 | -| ~~classes~~ | ~~3001~~ | ~~—~~ | 已合并入 core-edu(P3) | P1 历史 | +| 服务 | HTTP 端口 | gRPC 端口 | 说明 | 阶段 | +| ------------- | --------- | --------- | ----------------------------------------------- | ------- | +| apollo-router | 3000 | — | v2.1 GraphQL 聚合层(替代 BFF 手动聚合) | P3 | +| iam | 3002 | 50052 | P2 启用 gRPC server(I1 裁决,2026-07-09 修正) | P2 ✅ | +| teacher-bff | 3003 | — | BFF 不暴露 gRPC,对下游走 gRPC | P2 ✅ | +| core-edu | 3004 | 50053 | 含原 classes 服务(合并后) | P3 | +| content | 3005 | 50054 | Neo4j + ES | P4 | +| data-ana | 3006 | 50055 | HTTP 保留作 Gateway 直连降级,gRPC 为 P4 主入口 | P4 | +| msg | 3007 | 50056 | 通知中心 | P5 | +| ai | 3008 | 50058 | LLM 网关 | P5 | +| student-bff | 3009 | — | BFF 不暴露 gRPC | P3 | +| parent-bff | 3010 | — | BFF 不暴露 gRPC | P4 | +| ~~classes~~ | ~~3001~~ | ~~—~~ | 已合并入 core-edu(P3) | P1 历史 | -> **端口空闲**:3000、3011-3099 预留扩展。 +> **端口空闲**:3011-3099 预留扩展。 --- diff --git a/infra/security/secrets.example.env b/infra/security/secrets.example.env index ddd4d92..8e7d2db 100644 --- a/infra/security/secrets.example.env +++ b/infra/security/secrets.example.env @@ -48,3 +48,10 @@ REDIS_PASSWORD= # 生成:openssl rand -base64 32 # 注意:旋转前需先解密所有已加密字段,旋转后重新加密 ENCRYPTION_KEY= + +# ---------- Apollo Router 信任凭证 ---------- +# 用途:Router → 子图的共享密钥,子图 RouterAuthGuard 校验此 Header +# 最小长度:32 字符 +# 生成:openssl rand -hex 32 +# 注意:Router 和所有子图必须使用相同的密钥 +ROUTER_AUTH_SECRET=