后端服务部署¶
⚠️ 已退役(TFRM-254 / TFRM-243):本文档描述的 compose / SSH 部署方式已停用。app 部署已收敛为 Flux 拉模式 GitOps——CI 只造镜像,部署改由 gitops 仓
turingfocus/gitopsapps/overlays/<env>钉 tag → Flux reconcile。权威部署文档见 gitops 仓docs/plans/gitops-cd-flux.md。以下内容仅作历史参考。适用于所有环境。完成 Nginx 与网络配置 后执行。
部署架构¶
镜像构建/推送统一走 CNB registry,各环境部署方式不同:
beta: CNB Pipeline → 构建镜像 → SSH Deploy → 服务器 docker compose up(tfrs-net)
staging: CNB Pipeline → 构建镜像 → SSH Deploy → kubectl set image + rollout status(k3s ns tfrs-mgmt)
- beta:三个项目共享同一台服务器和 Docker 网络
tfrs-net,scripts/deploy.sh+docker-compose.cloud.yml。 - staging:管理平面已于 2026-07-06 迁移到专属单节点 k3s(ns
tfrs-mgmt,TFRM-251);tfrsmanager 后端三服务为 k8s Deployment,scripts/deploy-staging-k3s.sh经 kubectl 零停机滚动更新镜像。
环境变量体系¶
环境变量分为两类,存放在 CNB 密钥仓库 turingfocus/build_env 中:
1. 部署凭证(按环境独立,每个环境一个文件)¶
| 文件 | 用途 |
|---|---|
tfrsmanager_deploy_ssh.yml |
Beta 服务器 SSH + Docker 凭证 |
tfrsmanager_deploy_ssh_staging.yml |
Staging 服务器 SSH + Docker 凭证 |
模板:
# 服务器 SSH 登录
PROD_SSH_HOST: "{服务器公网IP}"
PROD_SSH_PORT: "22"
PROD_SSH_USER: "deploy"
PROD_SSH_KEY: |
-----BEGIN RSA PRIVATE KEY-----
{deploy 用户的 SSH 私钥}
-----END RSA PRIVATE KEY-----
# Docker 镜像仓库凭证(CNB Registry)
PROD_DOCKER_USER: "{CNB 用户名}"
PROD_DOCKER_PASSWORD: "{CNB Token}"
注意: SSH 私钥需要在目标服务器上生成并添加到
deploy用户的authorized_keys。
2. 业务环境变量(按环境独立,区分沙箱/生产配置)¶
| 文件 | 用途 |
|---|---|
tfrsmanager_env.prod.yml |
Beta 后端业务变量 |
tfrsmanager_env.staging.yml |
Staging 后端业务变量 |
tfrs_user_portal.yaml |
Beta UserPortal 变量 |
tfrs_user_portal_staging.yaml |
Staging UserPortal 变量 |
tfrs_admin_portal.yaml |
Beta AdminPortal 变量 |
tfrs_admin_portal_staging.yaml |
Staging AdminPortal 变量 |
Staging 环境变量配置指南¶
TFRSManager 后端 (tfrsmanager_env.staging.yml)¶
以下变量需要用户手动配置。标记 ⚠️ 沙箱 的项目必须使用沙箱/测试环境配置,不得使用生产值。
# ===== 基础服务 =====
GIN_MODE: "release"
DB_HOST: "postgres" # Docker 容器名
DB_PORT: "5432"
DB_USER: "tfrs"
DB_PASSWORD: "{自行设置}"
DB_NAME: "tfrs_staging"
DB_DRIVER: "postgres"
DB_SSL_MODE: "disable"
REDIS_ADDR: "redis:6379" # Docker 容器名
REDIS_PASSWORD: "{自行设置}"
REDIS_DB: "0"
REDIS_ENABLED: "true"
# ===== 认证 =====
JWT_SECRET: "{自行生成,openssl rand -hex 32}"
# ===== 日志 =====
LOG_LEVEL: "info"
LOG_ENVIRONMENT: "staging"
LOG_DIR: "/app/logs"
LOG_MAX_SIZE: "100"
LOG_MAX_BACKUPS: "5"
LOG_MAX_AGE: "30"
LOG_COMPRESS: "true"
# ===== ⚠️ 沙箱:微信支付 =====
WECHAT_APPID: "{沙箱 AppID}"
WECHAT_MCHID: "{沙箱商户号}"
WECHAT_MCH_PRIVATE_KEY: "{沙箱私钥}"
WECHAT_MCH_SERIAL_NO: "{沙箱证书序列号}"
WECHAT_APIV3_KEY: "{沙箱 APIv3 密钥}"
WECHAT_NOTIFY_URL: "https://user-staging.turingfocus.cn/api/v1/payment/wechat/notify"
WECHAT_IS_SANDBOX: "true"
# ===== ⚠️ 沙箱:支付宝 =====
ALIPAY_APPID: "{沙箱 AppID}"
ALIPAY_APP_PRIVATE_KEY: "{沙箱应用私钥}"
ALIPAY_ALIPAY_PUBLIC_KEY: "{沙箱支付宝公钥}"
ALIPAY_SIGN_TYPE: "RSA2"
ALIPAY_NOTIFY_URL: "https://user-staging.turingfocus.cn/api/v1/payment/alipay/notify"
ALIPAY_IS_SANDBOX: "true"
# ===== 短信(可共用生产配置或使用测试模板) =====
SMS_PROVIDER: "tencent"
SMS_TENCENT_SECRET_ID: "{腾讯云 SecretId}"
SMS_TENCENT_SECRET_KEY: "{腾讯云 SecretKey}"
SMS_TENCENT_SDK_APP_ID: "{SDK AppID}"
SMS_TENCENT_SIGN_NAME: "{签名}"
SMS_CODE_TTL_MINUTES: "5"
# ===== 对象存储(可共用或独立 Bucket) =====
COS_BUCKET_NAME: "{Staging Bucket}"
COS_REGION: "ap-beijing"
COS_SECRET_ID: "{SecretId}"
COS_SECRET_KEY: "{SecretKey}"
COS_BASE_URL: "https://{bucket}.cos.{region}.myqcloud.com"
COS_MAX_AVATAR_SIZE: "2097152"
# ===== K8s 集群 =====
K8S_CONFIG_DIR: "/app/data/kubeconfigs"
# ===== Infisical 密钥管理 =====
# ⚠️ Infisical 按环境分独立实例:prod=secret.turingfocus.cn / staging=secret-staging.turingfocus.cn。
# CLIENT_ID/SECRET 用 03 §5.3 的 cvm-staging-tfrs-manager(项目 Admin)。
# PROJECT_ID 与 PROJECT_SLUG 必须是同一项目(TFRM-131 不变量 B;启动日志「Infisical Phase B 三元组」核对)。
INFISICAL_CLIENT_ID: "{Staging cvm-staging-tfrs-manager ID}"
INFISICAL_CLIENT_SECRET: "{Staging cvm-staging-tfrs-manager Secret}"
INFISICAL_PROJECT_ID: "{项目 Settings 页 Project ID}"
INFISICAL_PROJECT_SLUG: "{项目 Settings 页 Project slug}" # TFRM-131:注入 spec.billing.infisical.projectSlug 用 slug 非 ID
INFISICAL_ENVIRONMENT: "staging" # 环境 slug(非显示名)
INFISICAL_SITE_URL: "https://secret-staging.turingfocus.cn"
# ===== 加密 =====
SECRET_ENCRYPTION_KEY: "{自行生成,openssl rand -hex 32}"
SECRET_CACHE_TTL: "3600"
# ===== 可观测性 =====
OTEL_SERVICE_NAME: "tfrsmanager-staging"
OTEL_SERVICE_VERSION: "1.0.0"
OTEL_ENVIRONMENT: "staging"
JAEGER_ENDPOINT: "http://jaeger:4318/v1/traces"
OTEL_SAMPLING_RATIO: "1.0"
# ===== 飞书应用 =====
FEISHU_CALLBACK_BASE_URL: "https://user-staging.turingfocus.cn"
FEISHU_OAUTH_REDIRECT_URL: "https://user-staging.turingfocus.cn/auth/feishu/callback"
FEISHU_ISV_ENABLED: "false"
FEISHU_ISV_APP_ID: ""
FEISHU_ISV_APP_SECRET: ""
# ===== 消费调度器 =====
CONSUMPTION_SCHEDULE_INTERVAL: "5m"
CONSUMPTION_PULL_WINDOW_MINUTES: "6"
# ===== 中央 TimescaleDB(TFRM-120 / TFRM-114 计费 of record)=====
# 注意:OTLP_TS_HOST / OTLP_TS_PORT 不放入 Infisical / CNB 密钥仓库。
# 容器互通的 DNS(timescaledb:5432)由 docker-compose.cloud.yml 的
# user-service.environment 段硬编码注入,且会覆盖 env_file。
# 在 Infisical 里再配 host/port 不会生效,反而易让人误以为要填 127.0.0.1:5433。
OTLP_TS_USER: "tfrs"
OTLP_TS_PASSWORD: "{与 .env.infra 完全一致;不一致 user-service 启动 Fatal}"
OTLP_TS_DBNAME: "tfrs_billing"
OTLP_TS_SSLMODE: "disable" # 同 VM docker network 内不需 TLS(详见 docs/specs/otlp-receiver-contract.md §9)
OTLP_TS_AUTO_MIGRATE: "true" # otlp-receiver 启动时跑嵌入式 schema migration(幂等)
OTLP_SINK_TYPE: "timescale" # 切 sink 到中央 TS;缺省 logging 仅适用本地开发
# ===== 中央 TS 远端冷归档(TFRM-123 / 财务审计 2 年 of-record)=====
# scripts/timescale-backup.sh::do_upload 用 aws s3 cp 推全量到远端 bucket。
# 留空 BACKUP_REMOTE_BUCKET → 跳过上传(向后兼容);prod 必填以兑现合规。
# 凭证与 COS_* 同源(同腾讯云账号,建议 bucket 级最小权限子账号)。
# 端点区域复用上方 COS_REGION,不新增区域变量。
# 宿主机依赖:deploy VM 需装 awscli(bootstrap-env / VM 初始化一次性)。
# 远端 ≥ 2 年保留由 bucket 侧 lifecycle rule(prefix=timescale/full/,≥740 天)兑现,
# 运营在腾讯云 COS 控制台一次性配置,脚本不删远端(详见 docs/timescaledb-ops.md §远端冷归档 SOP)。
BACKUP_REMOTE_BUCKET: "{远端 bucket 全名,含 appid,如 tfrs-ts-archive-1300000000}"
BACKUP_REMOTE_SECRET_ID: "{腾讯云 SecretId,与 COS 同源}"
BACKUP_REMOTE_SECRET_KEY: "{腾讯云 SecretKey,与 COS 同源}"
# ===== 集群初始化 =====
CLUSTER_INIT_OPERATOR_IMAGE: "{Operator 镜像地址}"
CLUSTER_INIT_OPERATOR_NAMESPACE: "tfrs-system"
CLUSTER_INIT_DEFAULT_CLUSTER_TYPE: "standard"
CLUSTER_INIT_INFISICAL_REGISTRY_PATH: "/docker-registry"
# ===== Phase B 计费注入(TFRM-131;billingEnabled 集群纳管时生效)=====
# OTLP_ENDPOINT 必填才能开 Phase B;其余有代码默认值,按需覆盖。
# ESO 专用 MI 凭据不在此 — 由 03 §5.6 写入 Infisical /otlp-eso-auth/,Manager 运行时读取。
CLUSTER_INIT_OTLP_ENDPOINT: "https://metrics-staging.turingfocus.cn" # 该 env Manager OTLP receiver 公网 host(复用 metrics-* 域名)
CLUSTER_INIT_OTLP_ESO_AUTH_PATH: "/otlp-eso-auth/" # 专用 ESO MI 凭据固定路径(对应 03 §5.6)
CLUSTER_INIT_ESO_VERSION: "0.10.7" # ESO chart 版本(默认值,一般不改)。注意无 v 前缀——ghcr.io OCI tag 格式(TFRM-133)
CLUSTER_INIT_ESO_NAMESPACE: "external-secrets-system" # ESO 自身安装 ns(产品自有,独立关注点)
CLUSTER_INIT_OTLP_AUTH_SECRET_NAME: "tfrs-otlp-infisical-auth-source" # 物化到客户集群的 ESO authSecret 名(与 Operator ESO ExternalSecret 约定)
# ===== Helm/Operator =====
OPERATOR_HELM_REPO_URL: "{Helm Repo URL}"
OPERATOR_CHART_VERSION: "{Chart 版本}"
OPERATOR_IMAGE_TAG: "{Operator 镜像 Tag}"
HELM_STORAGE_DRIVER: "secret"
前端项目 (tfrs_user_portal_staging.yaml / tfrs_admin_portal_staging.yaml)¶
# UserPortal Staging
NEXT_PUBLIC_API_URL: "https://user-staging.turingfocus.cn"
NEXT_PUBLIC_ENV: "staging"
# 其他前端特定变量...
# AdminPortal Staging
NEXT_PUBLIC_API_URL: "https://admin-staging.turingfocus.cn"
NEXT_PUBLIC_ENV: "staging"
# 其他前端特定变量...
CNB 流水线配置¶
每个项目的 .cnb.yml 中添加 staging 部署事件(触发名不变):
# 手动触发 Staging 部署
$:
web_trigger_deploy_staging:
- <<: *deploy-staging-pipeline
api_trigger_deploy_staging:
- <<: *deploy-staging-pipeline
⚠️ TFRSManager 后端 staging = k3s(TFRM-251):
.deploy-staging-pipeline不再走 docker-compose,而是经 SSH 在 staging 机器(k3s API 绑 localhost)执行kubectl -n tfrs-mgmt set image deploy/<svc> <svc>=<新镜像>+kubectl rollout status(零停机,见scripts/deploy-staging-k3s.sh)。env 由 cluster secret/configmap bootstrap,不再每次部署注入.env——tfrsmanager_env.staging.yml用于 bootstrap cluster secret(env 变更 → 重建 secret +kubectl rollout restart),故已从 staging pipeline 的 imports 移除。前置:staging 部署用户须能运行kubectl并读取KUBECONFIG(默认/etc/rancher/k3s/k3s.yaml:root / sudo / 已复制 kubeconfig; 脚本 preflight 会 fail-loud 兜底)。
下表 imports 密钥映射对 beta 及前端项目仍适用:
| 配置项 | Beta (Prod) | Staging |
|---|---|---|
| SSH 凭证 | tfrsmanager_deploy_ssh.yml |
tfrsmanager_deploy_ssh_staging.yml |
| 后端业务变量 | tfrsmanager_env.prod.yml |
tfrsmanager_env.staging.yml(k3s:bootstrap cluster secret,非 per-deploy) |
| UserPortal 变量 | tfrs_user_portal.yaml |
tfrs_user_portal_staging.yaml |
| AdminPortal 变量 | tfrs_admin_portal.yaml |
tfrs_admin_portal_staging.yaml |
部署步骤¶
前置准备(首次部署)¶
-
生成 deploy 用户 SSH 密钥(在 staging 服务器上):
-
在 CNB 密钥仓库创建配置文件(参照上述模板填充)
-
各项目
.cnb.yml添加 staging 流水线
触发部署¶
# 通过 CNB MCP 触发(API 方式)
cnb_startBuild(repo: "turingfocus/k8s/tfrsmanager", branch: "main", event: "api_trigger_deploy_staging")
cnb_startBuild(repo: "turingfocus/ui/TFRobotFrontPortal", branch: "main", event: "api_trigger_deploy_staging")
cnb_startBuild(repo: "turingfocus/ui/TFRobotAdminPortal", branch: "main", event: "api_trigger_deploy_staging")
或通过 CNB Web 界面手动触发 web_trigger_deploy_staging。
数据迁移脚本部署顺序约束¶
scripts/migrations/ 下脚本不会随容器自动执行(参见 scripts/migrations/README.md),DBA 需手动按序执行。涉及 GORM not null 标签变更的 DDL 脚本必须遵循以下窗口约束:
- 两个窗口不可合并:backfill 完成后人工
SELECT count(*) FROM <table> WHERE <col> IS NULL确认残留为 0,才推 DDL 脚本 - DDL 必须早于代码上线:避免「新代码已上线但旧 schema 仍允许 NULL」期间产生脏数据
- 新环境从零部署不依赖本顺序:GORM AutoMigrate 会按代码侧的 tag 直接建出 NOT NULL 列
案例:TFRM-38(organizations.owner_user_id NOT NULL)¶
# 窗口 1
psql -h <staging-host> -U tfrs -d tfrs_manager -f scripts/migrations/002_backfill_enterprise_owner_user_id.sql
# 验证:SELECT COUNT(*) FROM organizations WHERE owner_user_id IS NULL AND deleted_at IS NULL; -- 期望 0
# 窗口 2(与代码同窗口)
psql -h <staging-host> -U tfrs -d tfrs_manager -f scripts/migrations/003_alter_organizations_owner_user_id_not_null.sql
# 立即触发 staging 部署,让带 GORM not null tag 的代码上线
如发现窗口 1 输出 RAISE EXCEPTION(仍有残留),停止操作并人工审查空壳组织清单,不可强制推进。