跳转至

基础设施部署

适用于所有环境。完成 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_PASSWORDREDIS_PASSWORDOTLP_TS_PASSWORDJWT_SECRETSECRET_ENCRYPTION_KEY 的值同步到 CNB 密钥仓库的 tfrsmanager_env.{env}.yml 中,确保应用服务部署时能连接正确的数据库和 Redis 与中央 TS。Infisical 服务端的 4 个变量(INFISICAL_ENCRYPTION_KEY 等)不需要放入 CNB,它们只存在于服务器上的 .env.infra 中。

中央 TS 密码一致性硬约束OTLP_TS_PASSWORD 必须在 .env.infra 与 CNB tfrsmanager_env.{env}.yml 中保持完全一致。不一致时 user-service 启动会 Fatal(计费链路 of record 不可降级)。

两层配置的关系

配置文件 存放位置 包含内容 管理方式
.env.infra 服务器 /opt/tfrs/ 基础设施凭证(DB密码、Infisical主密钥等) 首次部署手动创建,长期不变
.env.prod CNB 每次部署生成 应用服务配置(业务变量、镜像标签等) CI/CD 自动生成

两者共享 DB_PASSWORDREDIS_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-serviceadmin-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 创建管理员账号

  1. 浏览器访问 https://secret-{env}.turingfocus.cn
  2. 注册管理员账号

5.2 禁用公开注册(安全必须)

⚠️ 关键安全配置: 必须在创建管理员账号后立即执行,防止未授权用户注册。

  1. 进入 Server Console(左侧导航栏或 /admin
  2. GeneralAllow user signups → 选择 Disabled
  3. 点击 Save

禁用后,新用户只能通过管理员邀请加入。所有环境(staging / beta / prod)均须执行此步骤。

5.3 创建项目和 Machine Identity

  1. 创建项目(如 tfrs-{env}
  2. 进入 Organization → Access ControlMachine Identities tab
  3. 点击 Create Organization Machine Identity
  4. 名称:cvm-{env}-tfrs-manager
  5. Role:Member
  6. 进入 Machine Identity 详情页 → Universal AuthAdd Client Secret
  7. 记录 ClientIDClientSecret(Client Secret 仅显示一次)
  8. 在 Machine Identity 详情页底部 Projects 区域 → Add to Project
  9. 选择项目 tfrs-{env}
  10. Role:Admin

5.4 创建必要的文件夹结构

在 Infisical 项目的对应环境(Staging/Production)下,创建以下文件夹:

  1. 进入项目 → 选择环境 tab(如 Staging)
  2. 点击 Add Secret 旁的下拉箭头 → Add Folder
  3. 依次创建以下文件夹并 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-131 provisionPhaseBBilling)读出物化为客户集群 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 Slug Equal {env} AND Secret Path Glob 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 按环境分独立实例(不是单实例多环境,实测踩坑): - prodhttps://secret.turingfocus.cn - staginghttps://secret-staging.turingfocus.cn - betahttps://secret-beta.turingfocus.cn

INFISICAL_INTERNAL_URLINFISICAL_PROJECT_IDINFISICAL_PROJECT_SLUGINFISICAL_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。

# 正确生成方式
openssl rand -base64 24 | head -c 32

Docker + Firewalld 冲突

firewalld 启动后会清除 Docker 的 iptables 规则,导致 docker network create 失败。必须在 firewalld 启动后重启 Docker

systemctl restart docker

完成后进入下一步:后端服务部署