Isolate cloud model access before enabling product RAG workflows
Some checks failed
verify / verify (push) Has been cancelled
Some checks failed
verify / verify (push) Has been cancelled
The API and ingestion tools now use a fixed internal model gateway while governed profiles, embedding cache assignments, traceable citations, and stable API errors establish the boundaries required by later workflows. Constraint: The current Alibaba Cloud workspace rejects all three live model calls with authentication failures Rejected: Give the API or seed tools the Bailian key and direct egress | combines database access, cloud credentials, and public network access Rejected: Mix offline and Bailian vectors in one demo namespace | makes profile activation and retrieval ambiguous Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep Bailian credentials and egress exclusive to model-gateway and create a new immutable profile hash for any embedding identity change Tested: make verify; 121 backend tests; 14 frontend tests; fresh and populated Alembic upgrade-downgrade-upgrade; two idempotent offline seeds; Docker health and HTTP retrieval; isolated provider smoke Not-tested: Successful live Bailian responses because the supplied workspace credential currently fails authentication
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
| 项目 | 设计值 |
|
||||
|---|---|
|
||||
| 部署方式 | Docker Compose 单机 |
|
||||
| 长期服务 | `web`、`api`、`worker`、`db` |
|
||||
| 长期服务 | 当前 `web`、`gateway`、`api`、`model-gateway`、`db`;后续加入 `worker` |
|
||||
| 一次性服务 | `migrate` |
|
||||
| 对外端口 | 仅 Nginx/Web |
|
||||
| 密钥 | Docker Secret / Secret Manager |
|
||||
@@ -17,7 +17,7 @@
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
这条命令应完成数据库健康等待、迁移、Web/API/Worker 启动。它不等于“所有数据和模型凭证自动生成”,首次部署仍必须完成密钥轮换、Secret 创建和三模型能力探测。
|
||||
这条命令当前完成数据库健康等待、一次性迁移、`model-gateway`、API、入口 Gateway 和 Web 启动;Worker 落地后也遵循同一依赖链。它不等于“所有数据和模型凭证自动生成”,首次部署仍必须完成密钥轮换、Secret 创建和三模型能力探测。
|
||||
|
||||
架构定位是适合毕设、演示和单机内部使用的生产式单节点,不宣称跨主机高可用。互联网生产环境应替换托管数据库、对象存储和集中密钥管理。
|
||||
|
||||
@@ -29,8 +29,9 @@ docker compose up -d --build
|
||||
|---|---|---|---|
|
||||
| `db` | 固定版本 pgvector 镜像 | 元数据、向量、任务、会话、评测 | 仅内部网络 |
|
||||
| `migrate` | 与后端同镜像 | 运行 Alembic,一次成功退出 | 无 |
|
||||
| `api` | `backend/Dockerfile` | FastAPI、检索、问答、管理 API | 仅 internal data 网络 |
|
||||
| `api` | `backend/Dockerfile` | FastAPI、检索、问答、管理 API | 仅 internal data/model 网络 |
|
||||
| `gateway` | 与后端同镜像 | 固定上游、请求边界、脱敏错误和流式转发 | 仅 internal ingress/data 网络 |
|
||||
| `model-gateway` | 与后端同镜像 | 唯一读取百炼 Key,代理 Embedding/Rerank/Chat | 仅 internal model + egress 网络,无宿主机端口 |
|
||||
| `worker` | 同一后端镜像 | 解析、向量化、评测后台任务 | 无 |
|
||||
| `web` | `frontend/Dockerfile` | React 静态资源、Nginx 同源入口和 SSE 代理 | `127.0.0.1:8000`;生产部署 HTTPS |
|
||||
| `ocr-worker` | 可选 profile | PaddleOCR 重型任务 | 无 |
|
||||
@@ -72,6 +73,11 @@ x-rag-config: &rag-config
|
||||
MODEL_MAX_RETRIES: "${MODEL_MAX_RETRIES:-3}"
|
||||
MODEL_MAX_CONCURRENCY: "${MODEL_MAX_CONCURRENCY:-4}"
|
||||
|
||||
x-model-client-config: &model-client-config
|
||||
MODEL_GATEWAY_BASE_URL: http://model-gateway:8000
|
||||
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_api_token
|
||||
MODEL_GATEWAY_CALLER: api
|
||||
|
||||
services:
|
||||
db:
|
||||
image: pgvector/pgvector:0.8.2-pg17
|
||||
@@ -115,15 +121,35 @@ services:
|
||||
networks: [data]
|
||||
restart: "no"
|
||||
|
||||
model-gateway:
|
||||
build: ./backend
|
||||
command: ["uvicorn", "app.model_gateway:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
environment:
|
||||
<<: *rag-config
|
||||
MODEL_GATEWAY_ALLOWED_TOKEN_FILES: >-
|
||||
api=/run/secrets/model_gateway_api_token,worker=/run/secrets/model_gateway_worker_token
|
||||
secrets:
|
||||
- bailian_api_key
|
||||
- model_gateway_api_token
|
||||
- model_gateway_worker_token
|
||||
networks: [model, egress]
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)"]
|
||||
restart: unless-stopped
|
||||
|
||||
api:
|
||||
build: ./backend
|
||||
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
environment: *runtime-config
|
||||
model-gateway:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
<<: [*runtime-config, *model-client-config]
|
||||
secrets:
|
||||
- postgres_app_password
|
||||
- model_gateway_api_token
|
||||
volumes:
|
||||
- uploads:/data/uploads
|
||||
healthcheck:
|
||||
@@ -132,7 +158,7 @@ services:
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
networks: [data]
|
||||
networks: [data, model]
|
||||
restart: unless-stopped
|
||||
|
||||
gateway:
|
||||
@@ -151,24 +177,29 @@ services:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
<<: [*runtime-config, *rag-config]
|
||||
<<: [*runtime-config, *model-client-config]
|
||||
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token
|
||||
MODEL_GATEWAY_CALLER: worker
|
||||
WORKER_CAPABILITIES: ${WORKER_CAPABILITIES:-document_parse,embedding,rerank,evaluation}
|
||||
secrets:
|
||||
- postgres_app_password
|
||||
- bailian_api_key
|
||||
- model_gateway_worker_token
|
||||
volumes:
|
||||
- uploads:/data/uploads
|
||||
networks: [data, egress]
|
||||
networks: [data, model]
|
||||
restart: unless-stopped
|
||||
|
||||
provider-smoke:
|
||||
build: ./backend
|
||||
command: ["python", "-m", "app.tools.provider_smoke"]
|
||||
profiles: ["tools"]
|
||||
environment: *rag-config
|
||||
depends_on:
|
||||
model-gateway:
|
||||
condition: service_healthy
|
||||
environment: *model-client-config
|
||||
secrets:
|
||||
- bailian_api_key
|
||||
networks: [egress]
|
||||
- model_gateway_api_token
|
||||
networks: [model]
|
||||
restart: "no"
|
||||
|
||||
seed-demo:
|
||||
@@ -179,11 +210,13 @@ services:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
<<: [*runtime-config, *rag-config]
|
||||
<<: [*runtime-config, *model-client-config]
|
||||
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token
|
||||
MODEL_GATEWAY_CALLER: worker
|
||||
secrets:
|
||||
- postgres_app_password
|
||||
- bailian_api_key
|
||||
networks: [data, egress]
|
||||
- model_gateway_worker_token
|
||||
networks: [data, model]
|
||||
restart: "no"
|
||||
|
||||
web:
|
||||
@@ -209,6 +242,9 @@ networks:
|
||||
data:
|
||||
driver: bridge
|
||||
internal: true
|
||||
model:
|
||||
driver: bridge
|
||||
internal: true
|
||||
egress:
|
||||
driver: bridge
|
||||
|
||||
@@ -219,13 +255,17 @@ secrets:
|
||||
file: ./secrets/postgres_migrator_password
|
||||
postgres_app_password:
|
||||
file: ./secrets/postgres_app_password
|
||||
model_gateway_api_token:
|
||||
file: ./secrets/model_gateway_api_token
|
||||
model_gateway_worker_token:
|
||||
file: ./secrets/model_gateway_worker_token
|
||||
bailian_api_key:
|
||||
file: ./secrets/bailian_api_key
|
||||
```
|
||||
|
||||
最终实现固定镜像 digest,示意版本只说明可行结构。`migrate` 只有一个实例执行;API 和 Worker 不在启动时并发自动迁移。首次初始化脚本只在 `db` 容器内使用 bootstrap 凭据创建 `vector` 扩展、无超级用户权限的 migrator/app 登录角色和专属 schema;migrator 拥有 schema/DDL,app 仅获运行期所需的 DML/sequence 权限及默认权限,API/Worker 永远不挂载 bootstrap 或 migrator Secret。最后一个初始化脚本只有在全部 SQL 事务和授权成功后,才在 `PGDATA` 内用同文件系统 rename 原子写入 `.rag-bootstrap-complete`;healthcheck 同时检查 `pg_isready` 与该哨兵,防止临时初始化 server 提前放行迁移。备份再使用独立只读角色。Compose 的 `.env` 仅做变量插值,不会自动注入容器,因此可配置的非敏感项和 `*_FILE` 路径通过 YAML anchor 在 `environment` 中完整声明。后端从各自的 `POSTGRES_PASSWORD_FILE` 读取密码后在内存中组装 DSN,不把明文密码放进 `.env` 或 `DATABASE_URL`。
|
||||
实际实现固定数据库镜像 digest。`migrate` 只有一个实例执行;成功应用到 head 后退出码为 0,随后在 `docker compose ps -a` 显示 `Exited (0)` 是一次性任务的正常终态,不是服务暂停。API 和 Worker 不在启动时并发自动迁移。首次初始化脚本只在 `db` 容器内使用 bootstrap 凭据创建 `vector` 扩展、无超级用户权限的 migrator/app 登录角色和专属 schema;migrator 拥有 schema/DDL,app 仅获运行期所需的 DML/sequence 权限及默认权限,API/Worker 永远不挂载 bootstrap 或 migrator Secret。最后一个初始化脚本只有在全部 SQL 事务和授权成功后,才在 `PGDATA` 内用同文件系统 rename 原子写入 `.rag-bootstrap-complete`;healthcheck 同时检查 `pg_isready` 与该哨兵,防止临时初始化 server 提前放行迁移。Compose 的 `.env` 仅做变量插值,不会自动注入容器;后端从 `*_FILE` 读取 Secret,不把明文密码放进 `.env` 或 `DATABASE_URL`。
|
||||
|
||||
网络分为四层:`web` 位于普通 `edge` 和 internal `ingress`;`gateway` 位于 `ingress + data`;API、迁移、Worker 和数据库位于 internal `data`;只有明确需要云调用的一次性工具或未来 Worker 才能加入命名的 `egress`。因此浏览器入口不能横向连接数据库,数据库感知 API 也没有公网默认出口。只有无 Secret 的 Web 容器发布回环端口;gateway 固定上游并继续执行请求边界和脱敏错误契约。详细理由和被否决方案见 [ADR-0004](adr/0004-secretless-web-ingress.md)。必须注意:普通 `edge` bridge 为 Web 提供宿主机端口发布时,也给 Web 留有技术上的默认公网出口;当前接受这一点的前提是 Web 无 Secret、无数据网络、无挂载且代理上游固定。若部署基线要求入口容器也完全禁网,必须在宿主机防火墙/出口代理层阻断,不能把“未加入名为 egress 的网络”误写成“没有外网出口”。普通 bridge 本身也不构成域名白名单,正式联网 Worker 仍需主机防火墙或出口代理只放行百炼和对象存储域名。
|
||||
网络分为五层:`web` 位于普通 `edge` 和 internal `ingress`;入口 `gateway` 位于 `ingress + data`;API 位于 internal `data + model`;数据库/迁移位于 internal `data`;只有 `model-gateway` 位于 internal `model + egress`。`provider-smoke` 只持 API 内部 token,真实 `seed-demo` 只持数据库 Secret 和 Worker 内部 token;二者都不挂载百炼 Key、不加入 `egress`。未来 Worker 沿用相同边界。`model-gateway` 不连接数据库、不挂载上传卷、不发布端口。入口隔离见 [ADR-0004](adr/0004-secretless-web-ingress.md),模型出口隔离见 [ADR-0005](adr/0005-isolate-model-egress.md)。必须注意:普通 `edge` bridge 为 Web 提供宿主机端口发布时,也给 Web 留有技术上的默认公网出口;生产仍需主机防火墙或出口代理只允许 `model-gateway` 到百炼域名。
|
||||
|
||||
### 2.3 后台任务为何不用 Redis
|
||||
|
||||
@@ -289,7 +329,9 @@ def read_secret(file_env: str, value_env: str) -> str:
|
||||
return os.environ[value_env]
|
||||
```
|
||||
|
||||
生产只允许文件/Secret Manager;本地值环境变量仅作为临时兼容。应用启动后不打印 Settings 的 Secret 字段,异常对象不得包含请求头。
|
||||
生产只允许文件/Secret Manager;本地值环境变量仅作为临时兼容。`bailian_api_key` 只挂载到 `model-gateway`。API 与 Worker 分别挂载 `model_gateway_api_token`、`model_gateway_worker_token`;调用同时使用 Bearer token 和 `X-RAG-Caller`,服务端以常量时间比较并拒绝 token/身份不匹配。应用启动后不打印 Settings 的 Secret 字段,异常对象不得包含请求头。
|
||||
|
||||
Secret 或模型端点配置在运行中发生变化时,`model-gateway` 会原子清空旧 provider 与内部 token,并进入 `restart_required`;旧、新 token 都不能在旧进程继续调用。Docker 的 `restart: unless-stopped` 不会仅因 unhealthy 自动重启,因此轮换操作必须协调执行 `docker compose restart model-gateway api`(Worker 落地后同时重启 Worker),待 readiness 恢复后再运行 provider smoke。禁止只替换文件而让旧进程长期携带旧凭据。
|
||||
|
||||
### 3.3 Git 防泄漏
|
||||
|
||||
@@ -326,6 +368,10 @@ EMBEDDING_MODEL=text-embedding-v4
|
||||
EMBEDDING_DIMENSION=1024
|
||||
RERANK_MODEL=qwen3-rerank
|
||||
LLM_MODEL=deepseek-v4-flash
|
||||
|
||||
MODEL_GATEWAY_BASE_URL=http://model-gateway:8000
|
||||
MODEL_GATEWAY_TOKEN_FILE=/run/secrets/model_gateway_api_token
|
||||
MODEL_GATEWAY_CALLER=api
|
||||
```
|
||||
|
||||
启动时 fail fast:
|
||||
@@ -336,6 +382,8 @@ LLM_MODEL=deepseek-v4-flash
|
||||
- Embedding 维度为 schema 支持的 1024;
|
||||
- 生产没有默认密码和通配 CORS;
|
||||
- Secret 文件存在、非空且权限合理;
|
||||
- 内部模型地址必须精确为 `http://model-gateway:8000`,不能通过配置改成公网或用户控制的 URL;
|
||||
- API/Worker 内部 token 必须不同,caller 必须与 token 身份一致;
|
||||
- `APP_ENV=production` 时关闭开发文档或增加管理员保护。
|
||||
|
||||
### 4.2 地域与计费方案
|
||||
@@ -373,13 +421,15 @@ LLM_MODEL=deepseek-v4-flash
|
||||
|
||||
`GET {OPENAI_BASE}/models` 可作为辅助清单,但不能证明 Rerank 路径和三个模型权限都正常,最小实际调用才是最终验证。
|
||||
|
||||
截至 2026-07-13,最近一次真实探测已到达百炼端点,但 Embedding、Rerank、Chat 三项均返回 401。改由内部 `model-gateway` 路由后必须重新运行同一探测;已有 401 证明不了模型授权可用。必须核对 Key 所属工作空间、北京地域、端点和计费/模型权限后重新验收。401 不进入自动重试风暴,也不能把 Stage 1 标记完成。
|
||||
|
||||
## 6. 网络与接口安全
|
||||
|
||||
### 6.1 网络边界
|
||||
|
||||
- 只发布 Nginx 端口;数据库、API、Worker 不直接暴露公网;
|
||||
- Nginx 到 API 使用内部网络;
|
||||
- `data` 网络启用 Docker internal 隔离,Web 不加入;API/Worker 通过独立 `egress` 网络访问云服务;
|
||||
- `data` 和 `model` 网络启用 Docker internal 隔离,Web 不加入;API/Worker 只经 `model-gateway` 访问云服务,自身不加入 `egress`;
|
||||
- 生产入口启用 TLS、HSTS 和安全响应头;
|
||||
- 出站只允许百炼/OSS 等必要域名;
|
||||
- 数据库账号按迁移、应用、备份职责分权;
|
||||
@@ -442,11 +492,12 @@ LLM_MODEL=deepseek-v4-flash
|
||||
### 8.1 健康端点
|
||||
|
||||
- `/health/live`:进程事件循环可用,不检查外部模型;
|
||||
- `/health/ready`:数据库、迁移版本、存储路径和配置可用;
|
||||
- API `/health/ready`:当前验证数据库可用;后续再纳入迁移版本和存储路径对账;
|
||||
- `model-gateway /health/ready`:只验证本地 Key/token/端点配置可加载且未漂移,不产生计费云调用;
|
||||
- `/admin/providers/health`:按需调用三个模型,结果短期缓存;
|
||||
- `/admin/index/health`:`documents.active_version_id`、searchable 切片投影、非空向量、文本哈希和 embedding profile 一致性。
|
||||
|
||||
百炼短时波动不应让编排器不断重启健康 API 容器。
|
||||
百炼短时波动不应让编排器不断重启健康 API 容器。真实三能力是否可用只能由显式 provider smoke 证明,不能从 `model-gateway` readiness 推断。
|
||||
|
||||
### 8.2 日志脱敏
|
||||
|
||||
|
||||
Reference in New Issue
Block a user