基础设施部署¶
适用于所有环境。完成 Nginx 与网络配置 后执行。
基础设施清单¶
所有基础设施组件定义在 docker-compose.cloud.yml 中,通过共享的 tfrs-net Docker 网络互联。
| 组件 | 镜像 | 端口 | 用途 |
|---|---|---|---|
| PostgreSQL | postgres:16.1 | 127.0.0.1:5432 | 业务数据库 |
| Redis | redis:7-alpine | 127.0.0.1:6379 | 缓存、会话 |
| TimescaleDB | timescale/timescaledb:2.17.2-pg16 | 127.0.0.1:5433 | 计费事件 of record(OTLP/TS hypertable,TFRM-120) |
| Infisical | infisical/infisical:latest | 127.0.0.1:8443 | 运行时密钥管理 |
| Infisical PostgreSQL | postgres:16-alpine | 内部 | Infisical 专用数据库 |
| Infisical Redis | redis:7-alpine | 内部 | Infisical 专用缓存 |
全部使用官方镜像,无需自定义构建。
1. 生成凭证¶
首次部署时,在服务器上生成所有随机凭证:
cat > /opt/tfrs/.env.infra << EOF
# ===== 业务数据库 =====
DB_USER=tfrs
DB_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
DB_NAME=tfrs_manager
# ===== Redis =====
REDIS_PASSWORD=$(openssl rand -base64 18 | tr -d '/+=')
# ===== 中央 TimescaleDB(TFRM-120 / TFRM-114 计费 of record) =====
OTLP_TS_USER=tfrs
OTLP_TS_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
OTLP_TS_DBNAME=tfrs_billing
# ===== Infisical =====
INFISICAL_ENCRYPTION_KEY=$(openssl rand -hex 16)
INFISICAL_AUTH_SECRET=$(openssl rand -base64 32)
INFISICAL_SITE_URL=https://secret-{env}.turingfocus.cn
INFISICAL_PG_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
INFISICAL_REDIS_PASSWORD=$(openssl rand -base64 18 | tr -d '/+=')
EOF
chown deploy:deploy /opt/tfrs/.env.infra
cat /opt/tfrs/.env.infra
⚠️ 关键备份:
INFISICAL_ENCRYPTION_KEY是加密所有密钥的主密钥,丢失后 Infisical 中存储的密钥将无法解密。必须离线备份。同步到 CNB: 将
DB_PASSWORD、REDIS_PASSWORD、OTLP_TS_PASSWORD、JWT_SECRET、SECRET_ENCRYPTION_KEY的值同步到 CNB 密钥仓库的tfrsmanager_env.{env}.yml中,确保应用服务部署时能连接正确的数据库和 Redis 与中央 TS。Infisical 服务端的 4 个变量(INFISICAL_ENCRYPTION_KEY 等)不需要放入 CNB,它们只存在于服务器上的.env.infra中。中央 TS 密码一致性硬约束:
OTLP_TS_PASSWORD必须在.env.infra与 CNBtfrsmanager_env.{env}.yml中保持完全一致。不一致时 user-service 启动会 Fatal(计费链路 of record 不可降级)。
两层配置的关系¶
| 配置文件 | 存放位置 | 包含内容 | 管理方式 |
|---|---|---|---|
.env.infra |
服务器 /opt/tfrs/ |
基础设施凭证(DB密码、Infisical主密钥等) | 首次部署手动创建,长期不变 |
.env.prod |
CNB 每次部署生成 | 应用服务配置(业务变量、镜像标签等) | CI/CD 自动生成 |
两者共享 DB_PASSWORD、REDIS_PASSWORD 的值(必须一致),但 Infisical 服务端变量只在 .env.infra 中。
2. 启动基础设施¶
使用 docker-compose.cloud.yml 只启动基础设施层(不含应用服务):
cd /opt/tfrs
docker compose -f docker-compose.cloud.yml --env-file .env.infra up -d \
postgres redis timescaledb \
infisical infisical-postgres infisical-redis
TFRM-120:中央 TimescaleDB 首次启动 -
timescaledb服务定义见docker-compose.cloud.yml,端口127.0.0.1:5433、数据卷tfrs_tsdata+tfrs_ts_wal_archive- Schema 由otlp-receiver启动时自动跑(OTLP_TS_AUTO_MIGRATE=true),无需手动 migrate - 备份策略详见 中央 TimescaleDB 运维手册注意: deploy.sh(CI/CD 触发)只会重启应用服务(
user-service、admin-service),不会动基础设施容器。基础设施一旦启动就长期运行。
3. 验证¶
# 检查所有容器健康状态
docker ps --format "table {{.Names}}\t{{.Status}}"
# 预期输出:全部 healthy
# tfrs_postgres Up X minutes (healthy)
# tfrs_redis Up X minutes (healthy)
# tfrs_timescaledb Up X minutes (healthy)
# tfrs_infisical Up X minutes
# tfrs_infisical_pg Up X minutes (healthy)
# tfrs_infisical_redis Up X minutes (healthy)
# 验证数据库连接
docker exec tfrs_postgres pg_isready -U tfrs -d tfrs_manager
# 验证 Redis
docker exec tfrs_redis redis-cli -a ${REDIS_PASSWORD} ping
# 验证中央 TimescaleDB
docker exec tfrs_timescaledb pg_isready -U tfrs -d tfrs_billing
# 验证 Infisical
curl -s http://127.0.0.1:8443/api/status
4. Docker 镜像加速(国内服务器)¶
如果服务器位于国内,需要配置镜像加速器,否则拉取 Docker Hub 镜像会超时:
cat > /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
},
"registry-mirrors": [
"https://mirror.ccs.tencentyun.com"
]
}
EOF
systemctl restart docker
此配置应在 服务器初始化 阶段完成。腾讯云服务器使用腾讯云加速器,阿里云使用阿里云加速器。
5. Infisical 首次初始化¶
基础设施容器全部运行后,需要初始化 Infisical:
5.1 创建管理员账号¶
- 浏览器访问
https://secret-{env}.turingfocus.cn - 注册管理员账号
5.2 禁用公开注册(安全必须)¶
⚠️ 关键安全配置: 必须在创建管理员账号后立即执行,防止未授权用户注册。
- 进入 Server Console(左侧导航栏或
/admin) - General → Allow user signups → 选择 Disabled
- 点击 Save
禁用后,新用户只能通过管理员邀请加入。所有环境(staging / beta / prod)均须执行此步骤。
5.3 创建项目和 Machine Identity¶
- 创建项目(如
tfrs-{env}) - 进入 Organization → Access Control → Machine Identities tab
- 点击 Create Organization Machine Identity
- 名称:
cvm-{env}-tfrs-manager - Role:Member
- 进入 Machine Identity 详情页 → Universal Auth → Add Client Secret
- 记录
ClientID和ClientSecret(Client Secret 仅显示一次) - 在 Machine Identity 详情页底部 Projects 区域 → Add to Project
- 选择项目
tfrs-{env} - Role:Admin
5.4 创建必要的文件夹结构¶
在 Infisical 项目的对应环境(Staging/Production)下,创建以下文件夹:
- 进入项目 → 选择环境 tab(如 Staging)
- 点击 Add Secret 旁的下拉箭头 → Add Folder
- 依次创建以下文件夹并 Save Changes:
| 文件夹 | 用途 |
|---|---|
kubeconfig |
存储各集群的 kubeconfig 文件 |
registry |
存储镜像仓库认证凭证 |
otlp |
存储 OTLP 计费链路鉴权 token(TFRM-119 实现) |
otlp-eso-auth |
Phase B 计费:ESO 用专用 scoped Machine Identity 凭据(TFRM-131,§5.6 写入) |
⚠️ 必须创建: 如果缺少
/kubeconfig文件夹,集群纳管时上传 kubeconfig 会返回 HTTP 500(Folder with path '/kubeconfig' not found)。
/otlp文件夹由 TFRM-119 集群初始化时使用:每个新纳管集群在/otlp/{cluster_id}/下写入OTLP_TOKEN(PRIMARY_TOKEN),ns 共享 StatefulSet OTel Collector 用于鉴权写中央 TS。
/otlp-eso-auth文件夹由 §5.6 写入「专用 ESO Machine Identity」的CLIENT_ID/CLIENT_SECRET/EXPIRES_AT,供 Phase B 集群纳管时(TFRM-131provisionPhaseBBilling)读出物化为客户集群 K8s Secret,ESO 据此回读/otlp/{cluster_id}/PRIMARY_TOKEN。新环境 bootstrap 必须在此阶段一并规划好,否则 Phase B 计费上行链路缺失(TFRO-52 根因)。⚠️ 待提交工作流: 部分 Infisical 项目启用「pending change → Save Changes」工作流,建/写文件夹与 secret 后必须点 Save Changes,否则离开页面即丢(实测踩坑)。
5.5 写入运行时密钥¶
进入对应文件夹,通过 Add Secret 创建密钥:
| 路径 | 密钥名 | 值来源 | 说明 |
|---|---|---|---|
/registry/ |
CNB_REGISTRY_USERNAME |
固定值 cnb |
CNB 镜像仓库用户名 |
/registry/ |
CNB_REGISTRY_PASSWORD |
CNB 平台 → 个人设置 → Token | CNB 镜像仓库密码 |
操作步骤: 在 Secrets 页面点击对应文件夹进入 → Add Secret → 填写 Key 和 Value → Create Secret。
⚠️ 缺少这些密钥的影响: 集群初始化时 Operator 部署会因无法拉取镜像而失败(
ImagePullBackOff)。
5.6 Phase B 计费:ESO 专用 Machine Identity + /otlp-eso-auth/(TFRM-131 · 新环境必做)¶
为什么 bootstrap 阶段就要做完: Manager 纳管
billingEnabled集群时(provisionPhaseBBilling)会 rotate OTLP token 写/otlp/{id}/PRIMARY_TOKEN,并从 固定路径/otlp-eso-auth/读一份专用 scoped Machine Identity 凭据物化为客户集群 K8s Secret(tfrs-otlp-infisical-auth-source),ESO 用它回读/otlp/。该 MI 不复用 §5.3 的cvm-{env}-tfrs-manager(信任边界隔离 + 最小权限)。若 bootstrap 不预置,Phase B 纳管会 fail-loud 终止(TFRO-52 即此缺口)。
1. 创建专用 Machine Identity(Organization → Access Control → Machine Identities → Create)
- 名称:
tfrs-otlp-eso-{env} - 组织 Role:Member
- 详情页 → Universal Auth(默认即有)→ 记录 Client ID
- Add Client Secret:描述
eso-{env}-otlp-readonly,TTL 建议31536000(1 年),记录 Client Secret(仅显示一次) 与 Expires 日期
2. 加入项目并配最小权限(详情页 Projects → Add to Project)
- 项目
tfrs-{env},项目 Role 选 No Access(基线零权限) - 进入「项目 → Access Control → Machine Identities → 该 MI → Project Additional Privileges → Add Additional Privileges」
- Privilege Name:
otlp-readonly-{env},Duration:Permanent - Add Policies → 勾选 Secrets → 规则:Effect=Allow,Actions=Read Value + Describe Secret(不勾 Modify/Remove/Create)
- Add Condition ×2:
Environment SlugEqual{env}ANDSecret PathGlob Match/otlp/** - Save
3. 写入 /otlp-eso-auth/(对应环境 tab,§5.4 已建文件夹)
| 路径 | 密钥名 | 值来源 |
|---|---|---|
/otlp-eso-auth/ |
CLIENT_ID |
上一步 MI 的 Client ID |
/otlp-eso-auth/ |
CLIENT_SECRET |
上一步 MI 的 Client Secret |
/otlp-eso-auth/ |
EXPIRES_AT |
上一步 client secret 到期日期(如 2027-05-19,供 Operator 过期监控) |
Manager 自身 MI
cvm-{env}-tfrs-manager(项目 Admin)可读/otlp-eso-auth/,故 Manager 能在纳管时取出该凭据。写完点 Save Changes。
断点确认(追加到 §5.3 断点): 同时贴回 tfrs-otlp-eso-{env} 的 Client ID / Client Secret / Expires,以及项目 Slug(项目 Settings 页 Project slug,TFRM-131 不变量 A:注入 CR 用 slug,非 ID)。
5.7 更新 TFRSManager 环境变量¶
将以下值填入 CNB 密钥仓库的 tfrsmanager_env.{env}.yml:
INFISICAL_CLIENT_ID: "{§5.3 cvm-{env}-tfrs-manager 的 ClientID}"
INFISICAL_CLIENT_SECRET: "{§5.3 的 ClientSecret}"
INFISICAL_PROJECT_ID: "{项目 ID,项目 Settings 页 Project ID}"
INFISICAL_PROJECT_SLUG: "{项目 Slug,项目 Settings 页 Project slug}" # TFRM-131:注入 spec.billing 用 slug
INFISICAL_ENVIRONMENT: "{env}" # 环境 slug:staging/prod(非显示名)
INFISICAL_INTERNAL_URL: "{该 env 的 Infisical 实例 host,见下方 ⚠️}"
⚠️ Infisical 按环境分独立实例(不是单实例多环境,实测踩坑): -
prod→https://secret.turingfocus.cn-staging→https://secret-staging.turingfocus.cn-beta→https://secret-beta.turingfocus.cn
INFISICAL_INTERNAL_URL、INFISICAL_PROJECT_ID、INFISICAL_PROJECT_SLUG、INFISICAL_ENVIRONMENT必须指向同一实例的同一项目(TFRM-131 不变量 B;Manager 启动日志「Infisical Phase B 三元组」会打印供核对)。校对坐标前先确认是哪个实例的 hostname,别默认secret.turingfocus.cn。Phase B 相关
CLUSTER_INIT_OTLP_ENDPOINT/CLUSTER_INIT_OTLP_ESO_AUTH_PATH等后端变量见 04-backend-deploy.md。
Infisical 职责说明¶
当前管理的密钥(仅 2 类)¶
| 密钥 | 路径 | 使用场景 |
|---|---|---|
CNB_REGISTRY_USERNAME |
/registry/ |
集群初始化时创建 K8s ImagePullSecret |
CNB_REGISTRY_PASSWORD |
/registry/ |
同上 + Operator 升级 |
PRIMARY_TOKEN |
/otlp/{cluster_id}/ |
TFRM-119:纳管时 rotate 写入,ESO 回读供 Collector 鉴权 |
CLIENT_ID/CLIENT_SECRET/EXPIRES_AT |
/otlp-eso-auth/ |
TFRM-131:专用 ESO MI 凭据,纳管时物化为客户集群 K8s Secret |
可选功能¶
- Kubeconfig 存储(
/kubeconfig/):加密存储集群 kubeconfig,有本地文件 fallback
为什么这 3 个密钥不放 CNB?¶
这些密钥在 运行时 通过 API 动态获取(集群初始化时),不是部署时注入环境变量。CNB 是 CI/CD 工具,不提供运行时 API。
故障影响¶
Infisical 不可用时: - 不影响:已运行的数字员工、用户登录、支付、日常 API - 影响:新集群初始化、Operator 升级 - 降级:kubeconfig 存储回退到本地文件
运维¶
备份¶
# 导出 Infisical 数据库
docker exec tfrs_infisical_pg pg_dump -U infisical infisical > infisical_backup.sql
# 导出业务数据库
docker exec tfrs_postgres pg_dump -U tfrs tfrs_manager > tfrs_backup.sql
升级¶
# 升级单个组件
docker compose -f docker-compose.cloud.yml pull {service}
docker compose -f docker-compose.cloud.yml up -d {service}
常见问题(Staging 部署实录)¶
Infisical OOM¶
Infisical 首次启动需要运行 migration,内存需求较高。docker-compose.cloud.yml 中内存限制不得低于 1.5G,否则会被 OOM Killer 杀死(退出码 137)。
# 确认是否 OOM
docker inspect tfrs_infisical --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
# 输出 137 true 即为 OOM
Infisical Migration Lock¶
如果 Infisical 在 migration 过程中被 OOM 杀死,会残留 migration lock,后续启动报错 Migration table is already locked。
# 手动解锁
docker exec tfrs_infisical_pg psql -U infisical -d infisical \
-c "UPDATE infisical_migrations_lock SET is_locked = 0;"
docker restart tfrs_infisical
日志 Volume 权限¶
应用服务容器以 UID 65532(nonroot)运行,Docker Volume 默认 owner 是 root,导致 permission denied 写日志失败。
# 修复权限
chown -R 65532:65532 /var/lib/docker/volumes/tfrs_tfrs_user_logs/_data/
chown -R 65532:65532 /var/lib/docker/volumes/tfrs_tfrs_admin_logs/_data/
SECRET_ENCRYPTION_KEY 长度¶
SECRET_ENCRYPTION_KEY 必须是正好 32 个字符的字符串(不是 64 位 hex)。代码直接用 []byte(keyStr) 不做 hex decode。
Docker + Firewalld 冲突¶
firewalld 启动后会清除 Docker 的 iptables 规则,导致 docker network create 失败。必须在 firewalld 启动后重启 Docker:
完成后进入下一步:后端服务部署。