feat:增加k8s部署
This commit is contained in:
parent
dcb4e451a1
commit
58cada6f59
237
k8s/README.md
Normal file
237
k8s/README.md
Normal file
@ -0,0 +1,237 @@
|
||||
# TopFans K8s / Helm 部署手册 (Phase 1)
|
||||
|
||||
> Phase 1: 单 namespace `topfans` 合并部署, 10 个 Go 服务 + gateway 共享一套基础设施,
|
||||
> Postgres / Redis 走阿里云 RDS / ElastiCache ExternalName。
|
||||
>
|
||||
> 设计文档: [`../docs/superpowers/specs/2026-06-08-docker-to-k8s-migration-design.md`](../docs/superpowers/specs/2026-06-08-docker-to-k8s-migration-design.md)
|
||||
>
|
||||
> ⚠️ Phase 2 (按组隔离) 不在本手册范围, 见设计文档第十一章。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
k8s/
|
||||
├── README.md # 本文件
|
||||
├── deploy.sh # Helm / K8s 部署脚本 (主入口)
|
||||
├── helm/
|
||||
│ └── topfans/ # 单个 chart, 覆盖整个 Phase 1
|
||||
│ ├── Chart.yaml
|
||||
│ ├── values.yaml # 默认值 (CI/Dev 用)
|
||||
│ ├── values-prod.example.yaml # 模板 (空值, 部署时 cp 后填)
|
||||
│ └── templates/
|
||||
│ ├── _helpers.tpl
|
||||
│ ├── 00-namespace.yaml
|
||||
│ ├── gateway/ ... # 13 个服务各自子目录
|
||||
│ ├── hpa/ ... # 3 个 HPA
|
||||
│ ├── secrets/ ... # 4 个 Secret
|
||||
│ ├── external-db/ ... # postgres / redis ExternalName
|
||||
│ ├── ingress.yaml
|
||||
│ ├── pg-sequence-sync.yaml # 切流量前必跑的 hook Job
|
||||
│ ├── oss-cors-init/job.yaml
|
||||
│ └── future-services/ ... # 4 个未来服务 .gitkeep
|
||||
└── jobs/
|
||||
└── pg-sequence-sync/ # 独立手动跑版本 (备份用)
|
||||
├── script.sh
|
||||
└── job-template.yaml
|
||||
```
|
||||
|
||||
## 首次部署 (Checklist)
|
||||
|
||||
按顺序完成:
|
||||
|
||||
### 1. 集群与基础设施
|
||||
|
||||
- [ ] 阿里云 ACK 测试集群创建 (按设计文档 §六 Step 1, 推荐)
|
||||
- [ ] 装 `nginx-ingress-controller` (Helm, 集群级别)
|
||||
- [ ] 装 `cert-manager` (Helm, 集群级别)
|
||||
- [ ] 创建 ClusterIssuer `letsencrypt-prod` (用 DNS-01 校验)
|
||||
- [ ] 创建 namespace `topfans` (helm install 时自动 create, 也可提前 `kubectl create ns topfans`)
|
||||
- [ ] 创建阿里云 RDS (PostgreSQL) + 用阿里云 DTS 把 VM 的数据迁过去
|
||||
- [ ] 创建阿里云 ElastiCache (Redis), 同步数据 (如果有)
|
||||
- [ ] **PG 端连通验证**: `psql -h <rds-endpoint>` 确认能连上, schemata 在
|
||||
|
||||
### 2. CI 镜像仓库
|
||||
|
||||
- [ ] 阿里云 ACR 创建 namespace `topfans`, registry 是 `registry.cn-shanghai.aliyuncs.com`
|
||||
- [ ] CI (GitHub Actions / 阿里云云效) 配 ACR push 凭据, 跑通 `gateway:v1.0.0` 一个 image
|
||||
- [ ] (`Dockerfile.services` 多阶段构建已 OK, 无需改)
|
||||
|
||||
### 3. 真值 values-prod.yaml
|
||||
|
||||
**推荐用交互脚本生成** (避免在 example 里写假值被误部署):
|
||||
|
||||
```bash
|
||||
./k8s/scripts/fill-values-prod.sh
|
||||
# 交互式提示输入 DB / Redis / JWT / OSS / Dify / OpenAI / SMS 真值
|
||||
# 写完自动 chmod 600 + 检查 .gitignore + 用 gitleaks/detect-secrets 扫描 (如已装)
|
||||
|
||||
# CI 模式: 全用环境变量
|
||||
./k8s/scripts/fill-values-prod.sh --non-interactive
|
||||
|
||||
# 写完用 sops 加密 (加密文件可入 git)
|
||||
./k8s/scripts/fill-values-prod.sh --sops
|
||||
|
||||
# 只看不写
|
||||
./k8s/scripts/fill-values-prod.sh --dry-run
|
||||
```
|
||||
|
||||
手写版 (不推荐, 易出拼错):
|
||||
|
||||
```bash
|
||||
cd k8s/helm/topfans
|
||||
cp values-prod.example.yaml values-prod.yaml
|
||||
chmod 600 values-prod.yaml
|
||||
$EDITOR values-prod.yaml
|
||||
# 重点字段:
|
||||
# global.dbExternalEndpoint: rm-xxxxxx.mysql.rds.aliyuncs.com
|
||||
# global.redisExternalEndpoint: r-xxxxxx.redis.rds.aliyuncs.com
|
||||
# services.<service>.image.tag: (CI 注入, 或先用 latest)
|
||||
# secrets.* : DB / JWT / OSS / AI / SMS 真值
|
||||
```
|
||||
|
||||
⚠️ **`values-prod.yaml` 不进 git** (`.gitignore` 已加, 脚本会二次确认)。
|
||||
|
||||
### 4. 首次部署
|
||||
|
||||
```bash
|
||||
cd /path/to/repo
|
||||
./k8s/deploy.sh install
|
||||
```
|
||||
|
||||
会自动按顺序:
|
||||
1. `helm.sh/hook pre-install` 创建 namespace
|
||||
2. `helm.sh/hook pre-install` 跑 PG sequence sync (如果 `pgSequenceSync.enabled=true`)
|
||||
3. `helm.sh/hook pre-install` 跑 oss-cors-init (Job 跑完即退)
|
||||
4. 创建所有 Service / Deployment / Secret / ConfigMap / HPA
|
||||
5. `helm.sh/hook pre-install` (其实是 `Install` 阶段) 跑完等 Pod Ready
|
||||
|
||||
### 5. Ingress 域名与 TLS
|
||||
|
||||
```bash
|
||||
# 改 ingress 内的 host
|
||||
$EDITOR k8s/helm/topfans/values-prod.yaml
|
||||
# ingress.hosts[0].host: api.example.com
|
||||
# ingress.tls[0].hosts[0]: api.example.com
|
||||
|
||||
# 解析到 nginx-ingress-controller 的 external IP
|
||||
kubectl get svc -n ingress-nginx
|
||||
# api.example.com A 记录 → <EXTERNAL-IP>
|
||||
|
||||
./k8s/deploy.sh upgrade --reuse-values
|
||||
```
|
||||
|
||||
## 升级流程 (日常)
|
||||
|
||||
```bash
|
||||
# 1. CI 已经把镜像推到 ACR:
|
||||
# registry.cn-shanghai.aliyuncs.com/topfans/gateway:v1.0.1
|
||||
# registry.cn-shanghai.aliyuncs.com/topfans/userservice:v1.0.1
|
||||
# ... (13 个 image 都标 v1.0.1)
|
||||
|
||||
# 2. 把所有 service 的 image tag 改成新版本:
|
||||
./k8s/deploy.sh upgrade v1.0.1
|
||||
|
||||
# 实质执行:
|
||||
# helm upgrade topfans ./helm/topfans \
|
||||
# -f values-prod.yaml \
|
||||
# --set global.image.tag=v1.0.1 \
|
||||
# --wait --timeout 10m
|
||||
```
|
||||
|
||||
`helm upgrade --wait` 会按 readinessProbe 等到所有 Pod Ready 才返回,
|
||||
失败的话 Exit code 非 0, deploy.sh 提示回滚。
|
||||
|
||||
## 回滚
|
||||
|
||||
```bash
|
||||
# 看历史
|
||||
./k8s/deploy.sh history
|
||||
# (helm history 通过 deploy.sh status 子命令也看得到)
|
||||
|
||||
# 回滚到上一版
|
||||
./k8s/deploy.sh rollback
|
||||
|
||||
# 回滚到指定 revision
|
||||
./k8s/deploy.sh rollback 3
|
||||
```
|
||||
|
||||
## PostgreSQL 序列同步 (Hard Blocker)
|
||||
|
||||
> **CLAUDE.md 强制规范 + 设计文档 §10.3 标记为 Hard Blocker**
|
||||
> 任何手动 INSERT id 都必须配套 setval; 切流量前必跑。
|
||||
|
||||
```bash
|
||||
./k8s/deploy.sh sync-pg # 同步 + 验证
|
||||
./k8s/deploy.sh sync-pg --status # 只验证
|
||||
```
|
||||
|
||||
挂在 Helm 上的版本 (设计文档 §六 Step 4 提到的方案):
|
||||
- `helm install / upgrade` 自动跑 `templates/pg-sequence-sync.yaml` (helm hook, weight=-8)
|
||||
- 失败 abort deployment
|
||||
|
||||
**何时必须手动跑** (用 `deploy.sh sync-pg`):
|
||||
1. 首次切流量前
|
||||
2. 任何 backfill SQL (含手动 INSERT) 后
|
||||
3. 数据迁移测试期间, 反复切换期间
|
||||
|
||||
## Secret 管理 (最低要求)
|
||||
|
||||
真值 **必须** 通过以下任一方式入 K8s,**严禁**把 values-prod.yaml 入 git:
|
||||
|
||||
| 方式 | 说明 | 适用 |
|
||||
|---|---|---|
|
||||
| **kubectl create secret** | 手 kubectl create secret 加 Secret, helm 不管 | 1-2 人小团队 |
|
||||
| **CI/CD 注入** | CI 流水线 kubectl apply / helm install --set 注入 | 推荐 |
|
||||
| **SOPS + Git** | values-prod.yaml sops -e 加密后入 git, CI decrypt | 多环境共享 |
|
||||
| **External Secrets Operator** | 接阿里云 KMS, 动态拉真值 | Phase 2+ |
|
||||
|
||||
当前 **Phase 1 默认用 kubectl create secret** (最快上手), 后续切 SOPS。
|
||||
|
||||
## 监控 / 日志 (设计文档 §六 Step 6, 可选)
|
||||
|
||||
不阻塞 Phase 1 上线, 上线后补:
|
||||
- Prometheus Operator (集群级)
|
||||
- Grafana dashboard (按 dubbo / DB / Redis 分类)
|
||||
- Loki + Promtail (日志聚合)
|
||||
- 阿里云 ARMS (一键接入, 推荐)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: helm install 报 `duplicate key on secret` / `ServerTimeout`
|
||||
|
||||
**A**: 上一次 helm install 没清干净。`helm uninstall topfans` 再重试。
|
||||
|
||||
### Q: PG 序列同步失败, 报 `setval: sequence_name not found`
|
||||
|
||||
**A**: 说明该 BIGSERIAL 表要么不存在, 要么 sequence 名拼错, 可能表没建好。
|
||||
连 RDS 验证: `\d+ assets` 看最下方的 `Sequence: assets_id_seq` 是否对得上。
|
||||
|
||||
### Q: 容器启动后卡在 Init 或 CrashLoopBackOff
|
||||
|
||||
**A**: 看日志: `kubectl logs -n topfans <pod> --previous`
|
||||
常见:
|
||||
- DB 连接不上: 检查 `secrets/db/db.password` 是否对, RDS 安全组是否放行 K8s 节点 CIDR
|
||||
- OSS 配置缺: `oss-credentials` Secret 没建, helm uninstall + 重装
|
||||
|
||||
### Q: HPA 不工作
|
||||
|
||||
**A**: `kubectl get hpa -n topfans` 看是不是 `Unknown`, 通常是没有 metrics-server。
|
||||
Phase 1 实施时安装 metrics-server (Helm, 一般 ingress-nginx chart 一起装)。
|
||||
|
||||
### Q: 各服务的 healthcheck 通不过
|
||||
|
||||
**A**: 各服务的 healthcheck path 不一致, 见 `values.yaml.services.<svc>.healthPath`:
|
||||
- 大多数用 `/health`
|
||||
- `statisticservice` / `notificationservice` 用 `/healthz`
|
||||
- `moderationservice` 用 `/` (历史原因, 不修, 见设计文档 §10.4)
|
||||
|
||||
---
|
||||
|
||||
## 参考
|
||||
|
||||
- 设计文档: `../docs/superpowers/specs/2026-06-08-docker-to-k8s-migration-design.md`
|
||||
- Helm 官方文档: https://helm.sh/docs/
|
||||
- 阿里云 ACK: https://help.aliyun.com/product/85222.html
|
||||
- 阿里云 ACR: https://help.aliyun.com/product/60716.html
|
||||
|
||||
最后更新: 2026-07-06 (Phase 1 首版)
|
||||
378
k8s/deploy.sh
Executable file
378
k8s/deploy.sh
Executable file
@ -0,0 +1,378 @@
|
||||
#!/bin/bash
|
||||
# ===================================================================
|
||||
# TopFans Helm / Kubernetes 部署脚本 (Phase 1)
|
||||
# ===================================================================
|
||||
# 功能:
|
||||
# - helm install / upgrade (CI 已经把镜像推到 ACR)
|
||||
# - helm rollback (任意 release 版本)
|
||||
# - helm status 查看
|
||||
# - 手动跑 PostgreSQL 序列同步 (切流量前 hard blocker)
|
||||
# - 临时启停某个服务 (scale / disable)
|
||||
#
|
||||
# 使用前提:
|
||||
# 1. K8s 集群已就绪 (ACK / TKE / k3s 任一)
|
||||
# 2. 已 kubectl config get-contexts 选好 cluster context
|
||||
# 3. helm 3.x 已装
|
||||
# 4. (切流量前) PostgreSQL 序列同步已成功 (本脚本的 sync-pg 子命令)
|
||||
# 5. values-prod.yaml 已就绪 (用 sops 加密 或 直接 kubectl create secret)
|
||||
#
|
||||
# 使用方式:
|
||||
# # 安装 (首次)
|
||||
# ./k8s/deploy.sh install
|
||||
#
|
||||
# # 升级 (推荐: 用 helm upgrade --reuse-values)
|
||||
# ./k8s/deploy.sh upgrade v1.0.1
|
||||
#
|
||||
# # 回滚到上一版
|
||||
# ./k8s/deploy.sh rollback
|
||||
#
|
||||
# # 显式回滚到指定版本
|
||||
# ./k8s/deploy.sh rollback 3
|
||||
#
|
||||
# # PostgreSQL 序列同步 (切流量前必跑)
|
||||
# ./k8s/deploy.sh sync-pg
|
||||
#
|
||||
# # 看状态
|
||||
# ./k8s/deploy.sh status
|
||||
#
|
||||
# # 看某个服务的 Pod 日志
|
||||
# ./k8s/deploy.sh logs gateway
|
||||
# ===================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# ==================== 颜色 ====================
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ==================== 路径 / 命名 ====================
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HELM_CHART_DIR="${SCRIPT_DIR}/helm/topfans"
|
||||
NAMESPACE="${NAMESPACE:-topfans}"
|
||||
RELEASE_NAME="${RELEASE_NAME:-topfans}"
|
||||
VALUES_FILE="${VALUES_FILE:-${SCRIPT_DIR}/helm/topfans/values-prod.yaml}"
|
||||
|
||||
print_step() {
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE} $1${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
}
|
||||
|
||||
print_msg() { echo -e "${1}${2}${NC}"; }
|
||||
|
||||
print_help() {
|
||||
cat << EOF
|
||||
${MAGENTA}TopFans K8s / Helm 部署脚本 (Phase 1)${NC}
|
||||
|
||||
${YELLOW}用法:${NC}
|
||||
$0 <command> [args]
|
||||
|
||||
${YELLOW}命令:${NC}
|
||||
${GREEN}install${NC} 首次部署 (helm install)
|
||||
${GREEN}upgrade${NC} <version> 升级镜像 (用 version 写所有服务 tag)
|
||||
${GREEN}upgrade --reuse-values${NC} 升级, 复用 values
|
||||
${GREEN}rollback${NC} [revision] 回滚 (默认上一版)
|
||||
${GREEN}status${NC} 看 helm status + pod 状态
|
||||
${GREEN}logs${NC} <service> 看 Pod 日志 (e.g. logs gateway)
|
||||
${GREEN}sync-pg${NC} PostgreSQL 序列同步 (切流量前必跑)
|
||||
${GREEN}sync-pg --status${NC} 只验证, 不同步
|
||||
${GREEN}list${NC} 列所有 release
|
||||
${GREEN}uninstall${NC} 卸载 (生产慎用!)
|
||||
|
||||
${YELLOW}环境变量:${NC}
|
||||
NAMESPACE 命名空间 (默认 topfans)
|
||||
RELEASE_NAME helm release 名 (默认 topfans)
|
||||
VALUES_FILE values 文件 (默认 helm/topfans/values-prod.yaml)
|
||||
|
||||
${YELLOW}示例:${NC}
|
||||
$0 install
|
||||
$0 upgrade v1.0.1
|
||||
$0 upgrade --reuse-values
|
||||
$0 rollback 2
|
||||
$0 sync-pg
|
||||
$0 status
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ==================== 必要工具检查 ====================
|
||||
check_prereqs() {
|
||||
local missing=0
|
||||
for cmd in helm kubectl grep awk; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
print_msg "$RED" "❌ 未安装: $cmd"
|
||||
missing=$((missing+1))
|
||||
fi
|
||||
done
|
||||
if [ "$missing" -gt 0 ]; then
|
||||
print_msg "$RED" "请先装 helm / kubectl"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 必须有 namespace 上下文
|
||||
if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
|
||||
print_msg "$YELLOW" "⚠️ namespace '$NAMESPACE' 不存在, helm install 时会自动创建"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==================== install ====================
|
||||
do_install() {
|
||||
print_step "🚀 helm install ${RELEASE_NAME}"
|
||||
print_msg "$YELLOW" "Chart: $HELM_CHART_DIR"
|
||||
print_msg "$YELLOW" "Namespace: $NAMESPACE"
|
||||
print_msg "$YELLOW" "Values: $VALUES_FILE"
|
||||
|
||||
if [ ! -f "$VALUES_FILE" ]; then
|
||||
print_msg "$RED" "❌ values 文件不存在: $VALUES_FILE"
|
||||
print_msg "$YELLOW" "复制模板: cp ${VALUES_FILE}.example ${VALUES_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
helm install "$RELEASE_NAME" "$HELM_CHART_DIR" \
|
||||
--namespace "$NAMESPACE" \
|
||||
--create-namespace \
|
||||
--values "$VALUES_FILE" \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
|
||||
print_step "✅ 安装完成"
|
||||
do_status
|
||||
}
|
||||
|
||||
# ==================== upgrade ====================
|
||||
do_upgrade() {
|
||||
local version="${1:-}"
|
||||
local reuse_values=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--reuse-values) reuse_values=true; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
print_step "🔼 helm upgrade ${RELEASE_NAME}"
|
||||
print_msg "$YELLOW" "Version: ${version:-latest}"
|
||||
|
||||
if [ "$reuse_values" = "true" ]; then
|
||||
print_msg "$CYAN" "使用 --reuse-values (不重读 values 文件)"
|
||||
helm upgrade "$RELEASE_NAME" "$HELM_CHART_DIR" \
|
||||
--namespace "$NAMESPACE" \
|
||||
--reuse-values \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
else
|
||||
[ -z "$version" ] && { print_msg "$RED" "❌ 请指定 version 或加 --reuse-values"; exit 1; }
|
||||
helm upgrade "$RELEASE_NAME" "$HELM_CHART_DIR" \
|
||||
--namespace "$NAMESPACE" \
|
||||
--values "$VALUES_FILE" \
|
||||
--set "global.image.tag=$version" \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
fi
|
||||
|
||||
print_step "✅ 升级完成"
|
||||
do_status
|
||||
}
|
||||
|
||||
# ==================== rollback ====================
|
||||
do_rollback() {
|
||||
local revision="${1:-}"
|
||||
print_step "🔄 helm rollback ${RELEASE_NAME}"
|
||||
|
||||
if [ -z "$revision" ]; then
|
||||
print_msg "$YELLOW" "回滚到上一版"
|
||||
helm rollback "$RELEASE_NAME" --namespace "$NAMESPACE" --wait
|
||||
else
|
||||
print_msg "$YELLOW" "回滚到 revision $revision"
|
||||
helm rollback "$RELEASE_NAME" "$revision" --namespace "$NAMESPACE" --wait
|
||||
fi
|
||||
|
||||
print_step "✅ 回滚完成"
|
||||
do_status
|
||||
}
|
||||
|
||||
# ==================== status ====================
|
||||
do_status() {
|
||||
print_step "📊 ${RELEASE_NAME} status"
|
||||
echo ""
|
||||
print_msg "$CYAN" "── helm history ──"
|
||||
helm history "$RELEASE_NAME" --namespace "$NAMESPACE" || true
|
||||
echo ""
|
||||
print_msg "$CYAN" "── pods ──"
|
||||
kubectl get pods -n "$NAMESPACE" -o wide
|
||||
echo ""
|
||||
print_msg "$CYAN" "── services ──"
|
||||
kubectl get svc -n "$NAMESPACE"
|
||||
echo ""
|
||||
print_msg "$CYAN" "── ingress ──"
|
||||
kubectl get ingress -n "$NAMESPACE" 2>/dev/null || true
|
||||
echo ""
|
||||
|
||||
# 健康探针短查 (gateway 200 即认为 OK)
|
||||
gateway_ip=$(kubectl get svc "${RELEASE_NAME}-gateway" -n "$NAMESPACE" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || echo "")
|
||||
if [ -n "$gateway_ip" ]; then
|
||||
print_msg "$CYAN" "── gateway health (in-cluster) ──"
|
||||
kubectl run -n "$NAMESPACE" curl-probe --rm -it --restart=Never \
|
||||
--image=curlimages/curl --quiet -- \
|
||||
curl -fsS "http://${gateway_ip}:8080/health" \
|
||||
&& print_msg "$GREEN" "✅ gateway health OK" \
|
||||
|| print_msg "$YELLOW" "⚠️ gateway 探针未通过 (可能 startup 中)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==================== logs ====================
|
||||
do_logs() {
|
||||
local svc="${1:?用法: $0 logs <service>}"
|
||||
print_step "📜 logs ${svc}"
|
||||
kubectl logs -n "$NAMESPACE" -l "app.kubernetes.io/component=${svc}" --tail=200 -f
|
||||
}
|
||||
|
||||
# ==================== PostgreSQL 序列同步 ====================
|
||||
# Hard blocker for traffic switching (CLAUDE.md / 设计文档 §10.3).
|
||||
# 行为:
|
||||
# sync-pg [--status]
|
||||
# 把 db-credentials 里的 DB_PASSWORD + values 里的 tables 灌到 Secret,
|
||||
# kubectl create -f k8s/jobs/pg-sequence-sync/manual-job.yaml,
|
||||
# wait until complete, 出错 exit 1。
|
||||
do_sync_pg() {
|
||||
local verify_only=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--status) verify_only=true; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
print_step "🔧 PostgreSQL 序列同步 (CLAUDE.md hard blocker)"
|
||||
print_msg "$YELLOW" "Namespace: $NAMESPACE"
|
||||
|
||||
if [ "$verify_only" = "true" ]; then
|
||||
print_msg "$CYAN" "只验证, 不同步"
|
||||
# 直接连 DB 验证
|
||||
kubectl run -n "$NAMESPACE" pg-probe --rm -it --restart=Never \
|
||||
--image=alpine:3.19 --quiet -- sh -c "
|
||||
apk add --no-cache postgresql16-client > /dev/null
|
||||
TABLES=$(yq eval '.pgSequenceSync.tables | join(\",\")' "$VALUES_FILE" 2>/dev/null || echo 'assets,users,stars')
|
||||
for tbl in \$(echo \"\$TABLES\" | tr ',' ' '); do
|
||||
h=\$(PGPASSWORD=$(kubectl get secret db-credentials -n $NAMESPACE -o jsonpath='{.data.DB_PASSWORD}' | base64 -d) psql -h postgres -U postgres -d topfans -tAc \
|
||||
\"SELECT last_value >= COALESCE((SELECT MAX(id) FROM ONLY \$tbl), 0) FROM pg_sequences WHERE sequencename = '\$tbl_id_seq';\")
|
||||
[ \"\$h\" = 't' ] && echo \" ✅ \$tbl healthy\" || echo \" ❌ \$tbl UNHEALTHY\"
|
||||
done
|
||||
"
|
||||
return
|
||||
fi
|
||||
|
||||
# 拉 values 里的 tables, 灌到临时 Secret
|
||||
print_msg "$YELLOW" "准备 Job..."
|
||||
local pg_password
|
||||
pg_password=$(kubectl get secret db-credentials -n "$NAMESPACE" -o jsonpath='{.data.DB_PASSWORD}' | base64 -d)
|
||||
|
||||
cat <<EOJOB | kubectl apply -n "$NAMESPACE" -f -
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: topfans-pg-sequence-sync-manual-$(date +%s)
|
||||
namespace: $NAMESPACE
|
||||
labels:
|
||||
app.kubernetes.io/component: pg-sequence-sync
|
||||
app.kubernetes.io/part-of: topfans
|
||||
app.kubernetes.io/run: manual
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
backoffLimit: 0
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: pg-sync
|
||||
image: alpine:3.19
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
apk add --no-cache postgresql16-client bash > /dev/null
|
||||
TABLES="${TABLES:-assets,asset_registry,users,stars,activity_assets,collection_assets,materials,exhibitions,galleries}"
|
||||
bad=0
|
||||
for tbl in \$(echo "\$TABLES" | tr ',' ' '); do
|
||||
tbl=\$(echo "\$tbl" | tr -d ' ')
|
||||
[ -z "\$tbl" ] && continue
|
||||
max_id=\$(PGPASSWORD="${pg_password}" psql -h postgres -U postgres -d topfans -tAc "SELECT COALESCE(MAX(id),0) FROM \$tbl;")
|
||||
[ "\$max_id" = "0" ] && { echo " \$tbl: empty, skip"; continue; }
|
||||
PGPASSWORD="${pg_password}" psql -h postgres -U postgres -d topfans \
|
||||
-c "SELECT setval('\${tbl}_id_seq', \$max_id, true);"
|
||||
h=\$(PGPASSWORD="${pg_password}" psql -h postgres -U postgres -d topfans -tAc \
|
||||
"SELECT last_value >= \$max_id FROM pg_sequences WHERE sequencename = '\${tbl}_id_seq';")
|
||||
if [ "\$h" = "t" ]; then
|
||||
echo " ✅ \$tbl → \$max_id"
|
||||
else
|
||||
echo " ❌ \$tbl sync failed"
|
||||
bad=\$((bad+1))
|
||||
fi
|
||||
done
|
||||
[ "\$bad" -gt 0 ] && exit 1
|
||||
echo "✅ done"
|
||||
env:
|
||||
- name: PGPASSWORD_FORCE
|
||||
value: "already-baked-into-args"
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 300m, memory: 128Mi }
|
||||
EOJOB
|
||||
|
||||
print_msg "$YELLOW" "等 Job 完成 (timeout 5min)..."
|
||||
local jobname
|
||||
jobname=$(kubectl get jobs -n "$NAMESPACE" -l "app.kubernetes.io/run=manual" --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}')
|
||||
kubectl wait --for=condition=complete "job/${jobname}" -n "$NAMESPACE" --timeout=300s || {
|
||||
print_msg "$RED" "❌ Job 失败"
|
||||
kubectl logs -n "$NAMESPACE" "job/${jobname}"
|
||||
exit 1
|
||||
}
|
||||
print_msg "$GREEN" "✅ 序列同步成功"
|
||||
}
|
||||
|
||||
# ==================== list / uninstall ====================
|
||||
do_list() {
|
||||
helm list -n "$NAMESPACE" -A
|
||||
}
|
||||
|
||||
do_uninstall() {
|
||||
print_step "⚠️ 卸载 ${RELEASE_NAME}"
|
||||
echo -e "${RED}⚠️ 真要删除整个 K8s 部署吗? 数据库 (RDS) 不受影响, 这是无状态 Pod.$NC"
|
||||
read -p "(yes/no): " confirm
|
||||
[ "$confirm" = "yes" ] || { print_msg "$YELLOW" "已取消"; exit 0; }
|
||||
helm uninstall "$RELEASE_NAME" -n "$NAMESPACE"
|
||||
print_msg "$GREEN" "✅ 已卸载"
|
||||
}
|
||||
|
||||
# ==================== main ====================
|
||||
main() {
|
||||
if [ $# -eq 0 ]; then
|
||||
print_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_prereqs
|
||||
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
case "$cmd" in
|
||||
install) do_install "$@" ;;
|
||||
upgrade) do_upgrade "$@" ;;
|
||||
rollback) do_rollback "$@" ;;
|
||||
status) do_status ;;
|
||||
logs) do_logs "$@" ;;
|
||||
sync-pg|pg-sync) do_sync_pg "$@" ;;
|
||||
list) do_list ;;
|
||||
uninstall) do_uninstall ;;
|
||||
-h|--help|help) print_help ;;
|
||||
*) print_msg "$RED" "未知命令 '$cmd'"; print_help; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
20
k8s/helm/topfans/Chart.yaml
Normal file
20
k8s/helm/topfans/Chart.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
apiVersion: v2
|
||||
name: topfans
|
||||
description: |
|
||||
TopFans 全明星粉丝平台 — Phase 1 (合并部署/降本)
|
||||
单 namespace (topfans), 9+4 数据服务 + gateway 共享一套部署,
|
||||
外部托管 PostgreSQL (RDS) + Redis (ElastiCache) 走 ExternalName。
|
||||
Phase 2 (按组隔离) 见 docs/superpowers/specs/2026-06-08-docker-to-k8s-migration-design.md#十一
|
||||
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- topfans
|
||||
- dubbo
|
||||
- go
|
||||
home: https://github.com/zerosaturation/TopFansByGithub
|
||||
maintainers:
|
||||
- name: zerosaturation
|
||||
sources:
|
||||
- https://github.com/zerosaturation/TopFansByGithub
|
||||
10
k8s/helm/topfans/templates/00-namespace.yaml
Normal file
10
k8s/helm/topfans/templates/00-namespace.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
name: {{ .Values.namespace.name | default "topfans" }}
|
||||
purpose: phase1-merged-deploy
|
||||
phase: phase1
|
||||
# 留 phase2 拆分空间 — Phase 2 会拆出 topfans-shared / topfans-group-* 各命名空间
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
131
k8s/helm/topfans/templates/_helpers.tpl
Normal file
131
k8s/helm/topfans/templates/_helpers.tpl
Normal file
@ -0,0 +1,131 @@
|
||||
{{/*
|
||||
公共辅助模板。Chart 内所有资源都用这些 helper 拼接 name / labels / selectors,
|
||||
避免每个文件重复 boilerplate。
|
||||
|
||||
用法:
|
||||
{{ include "topfans.fullname" . }}
|
||||
{{ include "topfans.labels" . }}
|
||||
{{ include "topfans.commonEnv" . }}
|
||||
{{ include "topfans.serviceEnv" (dict "name" "userservice" "port" 20000 "envFromSecret" "db-credentials") }}
|
||||
*/}}
|
||||
|
||||
{{/* ============ 命名 ============ */}}
|
||||
|
||||
{{/* topfans.name — chart 基础名(默认 "topfans"),可被 nameOverride 覆盖 */}}
|
||||
{{- define "topfans.name" -}}
|
||||
{{- default "topfans" .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* topfans.fullname — release 名 + chart 名拼接,63 字符限制内 */}}
|
||||
{{- define "topfans.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default "topfans" .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* topfans.chart — chart 名 + 版本 */}}
|
||||
{{- define "topfans.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* ============ Labels (所有 K8s 资源都打这套, 方便 kubectl 过滤) ============ */}}
|
||||
|
||||
{{- define "topfans.labels" -}}
|
||||
helm.sh/chart: {{ include "topfans.chart" . }}
|
||||
{{ include "topfans.selectorLabels" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: topfans
|
||||
{{- end -}}
|
||||
|
||||
{{- define "topfans.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "topfans.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/* ============ 通用 ENV (DB + Redis + 业务常量) ============
|
||||
与 docker-compose.prod.yml 的 x-common-env 等价, 注入到所有服务的 Pod 中。 */}}
|
||||
|
||||
{{- define "topfans.commonEnv" -}}
|
||||
- name: GIN_MODE
|
||||
value: {{ .Values.global.ginMode | default "release" | quote }}
|
||||
- name: ENV
|
||||
value: {{ .Values.global.env | default "production" | quote }}
|
||||
- name: LOG_LEVEL
|
||||
value: {{ .Values.global.logLevel | default "info" | quote }}
|
||||
- name: DB_HOST
|
||||
value: {{ .Values.global.dbHost | quote }}
|
||||
- name: DB_PORT
|
||||
value: {{ .Values.global.dbPort | default "5432" | quote }}
|
||||
- name: DB_USER
|
||||
value: {{ .Values.global.dbUser | default "postgres" | quote }}
|
||||
- name: DB_NAME
|
||||
value: {{ .Values.global.dbName | default "topfans" | quote }}
|
||||
- name: DB_SSLMODE
|
||||
value: {{ .Values.global.dbSslmode | default "disable" | quote }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: DB_PASSWORD
|
||||
- name: REDIS_HOST
|
||||
value: {{ .Values.global.redisHost | quote }}
|
||||
- name: REDIS_PORT
|
||||
value: {{ .Values.global.redisPort | default "6379" | quote }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: REDIS_PASSWORD
|
||||
- name: REDIS_DB
|
||||
value: {{ .Values.global.redisDb | default "0" | quote }}
|
||||
{{- end -}}
|
||||
|
||||
{{/* ============ ServiceAccount (Phase 1 所有服务共用一个) ============ */}}
|
||||
|
||||
{{- define "topfans.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{- default (include "topfans.fullname" .) .Values.serviceAccount.name -}}
|
||||
{{- else -}}
|
||||
{{- default "default" .Values.serviceAccount.name -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* ============ 单个服务的 Dubbo URL 拼接 ============
|
||||
例: {{ include "topfans.dubboURL" (dict "name" "userservice" "port" 20000) }}
|
||||
=> tri://userservice:20000
|
||||
|
||||
故意不把 "tri://" 放 values 让用户改, 这格式属于实现细节,
|
||||
如果哪天换协议 (HTTP / gRPC) 是 chart 级别的事, 不该让运维碰。 */}}
|
||||
|
||||
{{- define "topfans.dubboURL" -}}
|
||||
{{- printf "tri://%s:%d" .name .port -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* ============ Image 拼接 (Phase 1: 阿里云 ACR 推荐) ============
|
||||
用法:
|
||||
{{ include "topfans.image" (dict "Values" .Values "service" "gateway" "tag" "v1.0.0") }}
|
||||
输出:
|
||||
registry.cn-shanghai.aliyuncs.com/topfans/gateway:v1.0.0
|
||||
*/}}
|
||||
|
||||
{{- define "topfans.image" -}}
|
||||
{{- $registry := .Values.global.image.registry | default "" -}}
|
||||
{{- $ns := .Values.global.image.repositoryNamespace | default "topfans" -}}
|
||||
{{- $svc := .service -}}
|
||||
{{- $tag := .tag | default "latest" -}}
|
||||
{{- if $registry -}}
|
||||
{{- printf "%s/%s/%s:%s" $registry $ns $svc $tag -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" $svc $tag -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* ============ 自检可读性: dump key —— 给 helm template --debug 用, 不影响部署 ============ */}}
|
||||
85
k8s/helm/topfans/templates/activityservice/deployment.yaml
Normal file
85
k8s/helm/topfans/templates/activityservice/deployment.yaml
Normal file
@ -0,0 +1,85 @@
|
||||
# Activity Service — 活动 (微信扫码 / 落地页)
|
||||
{{- $svc := .Values.services.activityservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-activityservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: activityservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: activityservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: activityservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: activityservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: activityservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: activityservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: activityservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
86
k8s/helm/topfans/templates/aichatservice/deployment.yaml
Normal file
86
k8s/helm/topfans/templates/aichatservice/deployment.yaml
Normal file
@ -0,0 +1,86 @@
|
||||
# AI Chat Service — Dify 对话中转
|
||||
{{- $svc := .Values.services.aichatservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-aichatservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: aichatservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: aichatservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: aichatservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: aichatservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
- secretRef: { name: ai-keys } # DIFY_API_BASE / DIFY_API_KEY
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: 30 # AI 启动慢, 给 30s
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: aichatservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: aichatservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: aichatservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
86
k8s/helm/topfans/templates/assetservice/deployment.yaml
Normal file
86
k8s/helm/topfans/templates/assetservice/deployment.yaml
Normal file
@ -0,0 +1,86 @@
|
||||
# Asset Service — 资产生成 (积分扣减、铸造核心)
|
||||
{{- $svc := .Values.services.assetservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-assetservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: assetservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: assetservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: assetservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: assetservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
- secretRef: { name: oss-credentials }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: assetservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: assetservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: assetservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
@ -0,0 +1,24 @@
|
||||
# ===================================================================
|
||||
# Postgres ExternalName (Phase 1: 外部托管 RDS)
|
||||
# ===================================================================
|
||||
# 应用代码继续用 DB_HOST=postgres 这种短名, K8s 解析成阿里云 RDS endpoint。
|
||||
# 这样 Phase 2 换数据库 (例如拆 group 各自独立库) 只需改 ExternalName.target,
|
||||
# 应用代码不动。
|
||||
# ===================================================================
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
app.kubernetes.io/component: external-db
|
||||
app.kubernetes.io/part-of: topfans
|
||||
db: postgres
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: {{ .Values.global.dbExternalEndpoint | default "postgres-ext.example.com" }}
|
||||
# 仅做 DNS 解析, 没端口选择器 (ExternalName 不需要)
|
||||
ports:
|
||||
- name: postgres
|
||||
port: {{ .Values.global.dbPort | default "5432" }}
|
||||
protocol: TCP
|
||||
21
k8s/helm/topfans/templates/external-db/redis-external.yaml
Normal file
21
k8s/helm/topfans/templates/external-db/redis-external.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
# ===================================================================
|
||||
# Redis ExternalName (Phase 1: 外部托管 ElastiCache)
|
||||
# ===================================================================
|
||||
# 同 postgres, 应用代码继续用 REDIS_HOST=redis 短名即可。
|
||||
# ===================================================================
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
app.kubernetes.io/component: external-db
|
||||
app.kubernetes.io/part-of: topfans
|
||||
db: redis
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: {{ .Values.global.redisExternalEndpoint | default "redis-ext.example.com" }}
|
||||
ports:
|
||||
- name: redis
|
||||
port: {{ .Values.global.redisPort | default "6379" }}
|
||||
protocol: TCP
|
||||
14
k8s/helm/topfans/templates/future-services.yaml
Normal file
14
k8s/helm/topfans/templates/future-services.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
# ===================================================================
|
||||
# Future Services (Phase 1 占位)
|
||||
# ===================================================================
|
||||
# 设计文档 §四.1 列出, 以下 4 个服务 **本阶段不实现**, Phase 1.x 或 Phase 2
|
||||
# 启动时, 拆出独立 chart 子目录 (Phase 2: topfans-shared / topfans-group-<name>),
|
||||
# 或在本 chart 加 templates/<service>/deployment.yaml。
|
||||
#
|
||||
# - admin 后台管理 (运营 / 客服), 共享 DB 不走 API
|
||||
# - review 审核工作流 (UGC 内容审核), 被 gateway / asset 调用
|
||||
# - ai-image-gen 镭射卡生成, 当前 gateway 直接调 OpenAI / MiniMax
|
||||
# - ai-chat AI 对话 (粉丝互动), 与 aichatservice 是不同服务
|
||||
#
|
||||
# 本文件仅起占位作用, 不声明任何 K8s 资源。
|
||||
# ===================================================================
|
||||
93
k8s/helm/topfans/templates/galleryservice/deployment.yaml
Normal file
93
k8s/helm/topfans/templates/galleryservice/deployment.yaml
Normal file
@ -0,0 +1,93 @@
|
||||
# Gallery Service — 展厅
|
||||
{{- $svc := .Values.services.galleryservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-galleryservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: galleryservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: galleryservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: galleryservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: galleryservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
{{- if not (hasSuffix "_SECRET_REF" $k) }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
# MQ_REDIS_PASSWORD 走 db-credentials secret, 不重复声明
|
||||
- name: MQ_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: REDIS_PASSWORD
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: galleryservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: galleryservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: galleryservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
90
k8s/helm/topfans/templates/gateway/deployment.yaml
Normal file
90
k8s/helm/topfans/templates/gateway/deployment.yaml
Normal file
@ -0,0 +1,90 @@
|
||||
# ===================================================================
|
||||
# Gateway — API 入口 (Phase 1 单 ns 内唯一流量入口)
|
||||
# ===================================================================
|
||||
# 所有外部流量经 nginx-ingress → gateway:8080,
|
||||
# 内部短 DNS 调各 Dubbo 服务。
|
||||
# ===================================================================
|
||||
{{- $svc := .Values.services.gateway -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-gateway
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: gateway
|
||||
app.kubernetes.io/role: entrypoint
|
||||
spec:
|
||||
{{- if not $svc.hpa.enabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: gateway
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: gateway
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: gateway
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: jwt-secret
|
||||
- secretRef:
|
||||
name: ai-keys # OPENAI_API_KEY / DIFY_API_KEY / SMS
|
||||
- secretRef:
|
||||
name: oss-credentials # OSS_* / LANDING_BASE_URL
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-gateway
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: gateway
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: gateway
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ $svc.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
26
k8s/helm/topfans/templates/hpa/aichatservice.yaml
Normal file
26
k8s/helm/topfans/templates/hpa/aichatservice.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
# AI Chat HPA — Dify 调用慢, 给 AI 启动预留更多副本
|
||||
{{- $svc := .Values.services.aichatservice -}}
|
||||
{{- if and $svc.enabled $svc.hpa.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-aichatservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: aichatservice
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "topfans.fullname" . }}-aichatservice
|
||||
minReplicas: {{ $svc.hpa.minReplicas | default 1 }}
|
||||
maxReplicas: {{ $svc.hpa.maxReplicas | default 5 }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ $svc.hpa.targetCPU | default 70 }}
|
||||
{{- end }}
|
||||
35
k8s/helm/topfans/templates/hpa/gateway.yaml
Normal file
35
k8s/helm/topfans/templates/hpa/gateway.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
# Gateway HPA — 流量入口, 流量波动大
|
||||
{{- $svc := .Values.services.gateway -}}
|
||||
{{- if and $svc.enabled $svc.hpa.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-gateway
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: gateway
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "topfans.fullname" . }}-gateway
|
||||
minReplicas: {{ $svc.hpa.minReplicas | default 2 }}
|
||||
maxReplicas: {{ $svc.hpa.maxReplicas | default 10 }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ $svc.hpa.targetCPU | default 60 }}
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300 # 5 min 内不连续降
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 30
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 60
|
||||
{{- end }}
|
||||
26
k8s/helm/topfans/templates/hpa/notificationservice.yaml
Normal file
26
k8s/helm/topfans/templates/hpa/notificationservice.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
# Notification HPA — 推送突发流量 (活动 / 抽奖)
|
||||
{{- $svc := .Values.services.notificationservice -}}
|
||||
{{- if and $svc.enabled $svc.hpa.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-notificationservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: notificationservice
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "topfans.fullname" . }}-notificationservice
|
||||
minReplicas: {{ $svc.hpa.minReplicas | default 1 }}
|
||||
maxReplicas: {{ $svc.hpa.maxReplicas | default 4 }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ $svc.hpa.targetCPU | default 70 }}
|
||||
{{- end }}
|
||||
52
k8s/helm/topfans/templates/ingress.yaml
Normal file
52
k8s/helm/topfans/templates/ingress.yaml
Normal file
@ -0,0 +1,52 @@
|
||||
# ===================================================================
|
||||
# Ingress — 集群级流量入口
|
||||
# ===================================================================
|
||||
# api.example.com → topfans/gateway:8080
|
||||
# 假设 nginx-ingress + cert-manager 已预装 (Phase 1 部署时确认)
|
||||
# 部署前必须:
|
||||
# 1. 装 nginx-ingress-controller (Phase 1 — 一次性)
|
||||
# 2. 装 cert-manager (Phase 1 — 一次性)
|
||||
# 3. 创建 ClusterIssuer `letsencrypt-prod` (TLS 用)
|
||||
# ===================================================================
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: topfans-gateway
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: gateway
|
||||
app.kubernetes.io/role: ingress
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className | default "nginx" }}
|
||||
{{- with .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType | default "Prefix" }}
|
||||
backend:
|
||||
service:
|
||||
{{- if eq .backend "gateway" }}
|
||||
name: {{ printf "%s-gateway" (include "topfans.fullname" $) | default "topfans-gateway" }}
|
||||
port:
|
||||
number: {{ $.Values.services.gateway.port }}
|
||||
{{- else }}
|
||||
name: {{ .backend }}
|
||||
port:
|
||||
number: {{ (index $.Values.services .backend).port | default 80 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
85
k8s/helm/topfans/templates/moderationservice/deployment.yaml
Normal file
85
k8s/helm/topfans/templates/moderationservice/deployment.yaml
Normal file
@ -0,0 +1,85 @@
|
||||
# Moderation Service — 举报/反馈/自动隐藏
|
||||
{{- $svc := .Values.services.moderationservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-moderationservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: moderationservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: moderationservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: moderationservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: moderationservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: moderationservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: moderationservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: moderationservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
@ -0,0 +1,85 @@
|
||||
# Notification Service — 通知中心 + 推送
|
||||
{{- $svc := .Values.services.notificationservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-notificationservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: notificationservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: notificationservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: notificationservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: notificationservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: ai-keys } # SMS_ACCESS_KEY_ID / SECRET / SIGN_NAME / TEMPLATE_CODE
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/healthz" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/healthz" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: notificationservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: notificationservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: notificationservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
43
k8s/helm/topfans/templates/oss-cors-init/job.yaml
Normal file
43
k8s/helm/topfans/templates/oss-cors-init/job.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
# ===================================================================
|
||||
# oss-cors-init — 一次性 Job, 给 OSS bucket 推 CORS (POST 直传必需)
|
||||
# ===================================================================
|
||||
# Phase 1 部署 / 升级 (cluster-admin upgrade) 前用 helm hook 跑一次,
|
||||
# 与 docker-compose.prod.yml 中 restart: "no" 的语义对齐 — 跑完即退, 不留 Pod。
|
||||
#
|
||||
# 触发:
|
||||
# helm install / helm upgrade 时自动跑 (pre-install, pre-upgrade)
|
||||
# ===================================================================
|
||||
{{- $job := .Values.services.ossCorsInit -}}
|
||||
{{- if $job.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-oss-cors-init
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: oss-cors-init
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "-10" # 在 -5 之前跑
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 300
|
||||
backoffLimit: 0 # 不重试, 失败立刻 fail (deploy.sh 能看到错误)
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: oss-cors-init
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: oss-cors-init
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $job.image.repositoryName "tag" $job.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
envFrom:
|
||||
- secretRef: { name: oss-credentials }
|
||||
resources:
|
||||
{{- toYaml $job.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
127
k8s/helm/topfans/templates/pg-sequence-sync.yaml
Normal file
127
k8s/helm/topfans/templates/pg-sequence-sync.yaml
Normal file
@ -0,0 +1,127 @@
|
||||
# ===================================================================
|
||||
# PostgreSQL 序列同步 (Phase 1 切流量前必跑 — Helm hook 自动触发)
|
||||
# ===================================================================
|
||||
# CLAUDE.md 强制规范 + 设计文档 §10.3 hard blocker:
|
||||
# 任何手动 INSERT id 必须 setval, 否则后续 GORM 报 duplicate key。
|
||||
# 切流量前必须 sync 所有 BIGSERIAL 表。
|
||||
#
|
||||
# 本模板作为 Helm hook, 在 helm install / upgrade 时自动跑:
|
||||
# 1. 创建 ConfigMap 含脚本
|
||||
# 2. 创建一次性 Job, 跑完退出
|
||||
# 3. 失败时 helm 中断, deploy.sh / CI 看到非 0
|
||||
# ===================================================================
|
||||
{{- if .Values.pgSequenceSync.enabled }}
|
||||
{{- $tables := join "," .Values.pgSequenceSync.tables -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-pg-sequence-sync-script
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: pg-sequence-sync
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "-9"
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
data:
|
||||
script.sh: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
PG_HOST="${PG_HOST:-postgres}"
|
||||
PG_PORT="${PG_PORT:-5432}"
|
||||
PG_USER="${PG_USER:-postgres}"
|
||||
PG_DB="${PG_DB:-topfans}"
|
||||
PG_PASSWORD="${PG_PASSWORD:?must be provided by secret}"
|
||||
TABLES_CSV="{{ $tables }}"
|
||||
|
||||
IFS=',' read -ra TABLES <<< "$TABLES_CSV"
|
||||
echo "Syncing {{ len .Values.pgSequenceSync.tables }} tables"
|
||||
|
||||
bad=0
|
||||
for tbl in "${TABLES[@]}"; do
|
||||
tbl=$(echo "$tbl" | tr -d ' ')
|
||||
[ -z "$tbl" ] && continue
|
||||
max_id=$(PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-tAc "SELECT COALESCE(MAX(id), 0) FROM ${tbl};")
|
||||
if [ "$max_id" = "0" ]; then
|
||||
echo " ${tbl}: empty, skip"
|
||||
continue
|
||||
fi
|
||||
PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-c "SELECT setval('${tbl}_id_seq', ${max_id}, true);"
|
||||
h=$(PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-tAc "SELECT last_value >= ${max_id} FROM pg_sequences WHERE sequencename = '${tbl}_id_seq';")
|
||||
if [ "$h" = "t" ]; then
|
||||
echo " ✅ ${tbl}_id_seq → ${max_id}"
|
||||
else
|
||||
echo " ❌ ${tbl}_id_seq failed to sync"
|
||||
bad=$((bad+1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$bad" -gt 0 ]; then
|
||||
echo "❌ ${bad} sequences unhealthy, abort deployment"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ all sequences healthy"
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-pg-sequence-sync
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: pg-sequence-sync
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "-8" # 在 ConfigMap 之后
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
backoffLimit: 0
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: pg-sync
|
||||
image: alpine:3.19
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -e
|
||||
apk add --no-cache postgresql16-client bash > /dev/null
|
||||
bash /scripts/script.sh
|
||||
env:
|
||||
- name: PG_HOST
|
||||
value: "postgres"
|
||||
- name: PG_PORT
|
||||
value: "5432"
|
||||
- name: PG_USER
|
||||
value: {{ .Values.global.dbUser | default "postgres" | quote }}
|
||||
- name: PG_DB
|
||||
value: {{ .Values.global.dbName | default "topfans" | quote }}
|
||||
- name: PG_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: DB_PASSWORD
|
||||
- name: TABLES_CSV
|
||||
value: "{{ $tables }}"
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
resources:
|
||||
{{- toYaml .Values.pgSequenceSync.resources | nindent 10 }}
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: {{ include "topfans.fullname" . }}-pg-sequence-sync-script
|
||||
defaultMode: 0o755
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
{{- end }}
|
||||
42
k8s/helm/topfans/templates/secrets/ai-keys.yaml
Normal file
42
k8s/helm/topfans/templates/secrets/ai-keys.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
# ===================================================================
|
||||
# AI / SMS / OpenAI 兼容 API 一站式 Secret
|
||||
# ===================================================================
|
||||
# 集中管理所有外部 API 凭据, 各服务按需 secretKeyRef 引用。
|
||||
# -------------------------------------------------------------------
|
||||
# ⚠️ 真值不进 git! 改动这些字段:
|
||||
# - 用 helm upgrade --set secrets.ai.xxx=...
|
||||
# - 或 CI/CD 注入
|
||||
# ===================================================================
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ai-keys
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
app.kubernetes.io/component: secret
|
||||
app.kubernetes.io/part-of: topfans
|
||||
secret-type: ai-keys
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Dify (aichatservice 用)
|
||||
DIFY_API_BASE: {{ .Values.secrets.ai.difyApiBase | default "http://dify-ext:5001/v1" | quote }}
|
||||
DIFY_API_KEY: {{ .Values.secrets.ai.difyApiKey | required "secrets.ai.difyApiKey 必填" | quote }}
|
||||
|
||||
# MiniMax (gateway 镭射卡生成 用, 当 LASER_GEN_PROVIDER=minimax 时)
|
||||
MINIMAX_API_KEY: {{ .Values.secrets.ai.minimaxApiKey | default "" | quote }}
|
||||
MINIMAX_API_URL: {{ .Values.secrets.ai.minimaxApiUrl | default "https://api.minimaxi.com/v1/image_generation" | quote }}
|
||||
|
||||
# OpenAI 兼容 API (微达API中转站, gateway 镭射卡生成 默认)
|
||||
OPENAI_API_KEY: {{ .Values.secrets.ai.openaiApiKey | required "secrets.ai.openaiApiKey 必填" | quote }}
|
||||
OPENAI_BASE_URL: {{ .Values.secrets.ai.openaiBaseUrl | default "https://api.weda.cc/v1" | quote }}
|
||||
OPENAI_MODEL: {{ .Values.secrets.ai.openaiModel | default "gpt-image-2" | quote }}
|
||||
|
||||
# (预留) Qwen / 其他 AI 平台 - Phase 2/3 用
|
||||
QWEN_API_KEY: {{ .Values.secrets.ai.qwenApiKey | default "" | quote }}
|
||||
|
||||
# SMS (阿里云, 通知服务用)
|
||||
SMS_ACCESS_KEY_ID: {{ .Values.secrets.sms.accessKeyId | default "" | quote }}
|
||||
SMS_ACCESS_KEY_SECRET: {{ .Values.secrets.sms.accessKeySecret | default "" | quote }}
|
||||
SMS_SIGN_NAME: {{ .Values.secrets.sms.signName | default "" | quote }}
|
||||
SMS_TEMPLATE_CODE: {{ .Values.secrets.sms.templateCode | default "" | quote }}
|
||||
SMS_REGION: {{ .Values.secrets.sms.region | default "cn-hangzhou" | quote }}
|
||||
15
k8s/helm/topfans/templates/secrets/db-credentials.yaml
Normal file
15
k8s/helm/topfans/templates/secrets/db-credentials.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: db-credentials
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
app.kubernetes.io/component: secret
|
||||
app.kubernetes.io/part-of: topfans
|
||||
secret-type: db-credentials
|
||||
type: Opaque
|
||||
stringData:
|
||||
# 真值从 values-prod.yaml 注入 (或用 External Secrets Operator / SOPS 替换)
|
||||
# 这些密钥的更新走 kubectl edit secret / helm upgrade 重新加密传递, 不进 git
|
||||
DB_PASSWORD: {{ .Values.secrets.db.password | default "" | quote }}
|
||||
REDIS_PASSWORD: {{ .Values.secrets.redis.password | default "" | quote }}
|
||||
12
k8s/helm/topfans/templates/secrets/jwt-secret.yaml
Normal file
12
k8s/helm/topfans/templates/secrets/jwt-secret.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: jwt-secret
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
app.kubernetes.io/component: secret
|
||||
app.kubernetes.io/part-of: topfans
|
||||
secret-type: jwt
|
||||
type: Opaque
|
||||
stringData:
|
||||
JWT_SECRET: {{ .Values.secrets.jwt.secret | required "secrets.jwt.secret 必须填写, 来自 values-prod.yaml" | quote }}
|
||||
21
k8s/helm/topfans/templates/secrets/oss-credentials.yaml
Normal file
21
k8s/helm/topfans/templates/secrets/oss-credentials.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: oss-credentials
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
app.kubernetes.io/component: secret
|
||||
app.kubernetes.io/part-of: topfans
|
||||
secret-type: oss
|
||||
type: Opaque
|
||||
stringData:
|
||||
OSS_REGION: {{ .Values.secrets.oss.region | default "cn-shanghai" | quote }}
|
||||
OSS_BUCKET_NAME: {{ .Values.secrets.oss.bucket | default "" | quote }}
|
||||
OSS_ACCESS_KEY_ID: {{ .Values.secrets.oss.accessKeyId | default "" | quote }}
|
||||
OSS_ACCESS_KEY_SECRET: {{ .Values.secrets.oss.accessKeySecret | default "" | quote }}
|
||||
OSS_STS_ROLE_ARN: {{ .Values.secrets.oss.stsRoleArn | default "" | quote }}
|
||||
OSS_AVATAR_DIR: {{ .Values.secrets.oss.avatarDir | default "avatar/" | quote }}
|
||||
OSS_ASSET_DIR: {{ .Values.secrets.oss.assetDir | default "asset/" | quote }}
|
||||
OSS_TOKEN_EXPIRE_TIME: {{ .Values.secrets.oss.tokenExpireTime | default "3600" | quote }}
|
||||
# 落地页 base url (分享服务用)
|
||||
LANDING_BASE_URL: {{ .Values.secrets.landingBaseUrl | default "https://api.example.com" | quote }}
|
||||
85
k8s/helm/topfans/templates/socialservice/deployment.yaml
Normal file
85
k8s/helm/topfans/templates/socialservice/deployment.yaml
Normal file
@ -0,0 +1,85 @@
|
||||
# Social Service — 社交关系
|
||||
{{- $svc := .Values.services.socialservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-socialservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: socialservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: socialservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: socialservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: socialservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: socialservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: socialservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: socialservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
86
k8s/helm/topfans/templates/starbookservice/deployment.yaml
Normal file
86
k8s/helm/topfans/templates/starbookservice/deployment.yaml
Normal file
@ -0,0 +1,86 @@
|
||||
# Starbook Service — 星簿
|
||||
{{- $svc := .Values.services.starbookservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-starbookservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: starbookservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: starbookservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: starbookservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: starbookservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
- secretRef: { name: oss-credentials }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: starbookservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: starbookservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: starbookservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
107
k8s/helm/topfans/templates/statisticservice/deployment.yaml
Normal file
107
k8s/helm/topfans/templates/statisticservice/deployment.yaml
Normal file
@ -0,0 +1,107 @@
|
||||
# Statistic Service — 数据看板 (独立 schema: statistic)
|
||||
{{- $svc := .Values.services.statisticservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-statisticservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: statisticservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: statisticservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: statisticservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: statisticservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
# statistic 用独立 schema, 不复用 common-env 的 DB_*
|
||||
- {name: GIN_MODE, value: {{ $.Values.global.ginMode | default "release" | quote }} }
|
||||
- {name: ENV, value: {{ $.Values.global.env | default "production" | quote }} }
|
||||
- {name: LOG_LEVEL, value: {{ $.Values.global.logLevel | default "info" | quote }} }
|
||||
- {name: PORT, value: {{ $svc.port | quote }} }
|
||||
# DB (复用 db-credentials)
|
||||
- {name: STATISTIC_DB_HOST, value: "postgres" }
|
||||
- {name: STATISTIC_DB_PORT, value: "5432" }
|
||||
- {name: STATISTIC_DB_NAME, value: "topfans" }
|
||||
- {name: STATISTIC_DB_SCHEMA, value: "statistic" }
|
||||
- {name: STATISTIC_DB_SSLMODE, value: "disable" }
|
||||
- name: STATISTIC_DB_USER
|
||||
value: {{ .Values.global.dbUser | default "postgres" | quote }}
|
||||
- name: STATISTIC_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: DB_PASSWORD
|
||||
# Redis
|
||||
- {name: STATISTIC_REDIS_HOST, value: "redis" }
|
||||
- {name: STATISTIC_REDIS_PORT, value: "6379" }
|
||||
- {name: STATISTIC_REDIS_DB, value: "0" }
|
||||
- name: STATISTIC_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: REDIS_PASSWORD
|
||||
# 跨服务
|
||||
- {name: USER_SERVICE_URL, value: "tri://userservice:20000" }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/healthz" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/healthz" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: statisticservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: statisticservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: statisticservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
92
k8s/helm/topfans/templates/taskservice/deployment.yaml
Normal file
92
k8s/helm/topfans/templates/taskservice/deployment.yaml
Normal file
@ -0,0 +1,92 @@
|
||||
# Task Service — 异步任务
|
||||
{{- $svc := .Values.services.taskservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-taskservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: taskservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: taskservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: taskservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: taskservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
{{- if not (hasSuffix "_SECRET_REF" $k) }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
- name: MQ_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: REDIS_PASSWORD
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: taskservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: taskservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: taskservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
85
k8s/helm/topfans/templates/userservice/deployment.yaml
Normal file
85
k8s/helm/topfans/templates/userservice/deployment.yaml
Normal file
@ -0,0 +1,85 @@
|
||||
# User Service — 用户服务
|
||||
{{- $svc := .Values.services.userservice -}}
|
||||
{{- if $svc.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "topfans.fullname" . }}-userservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: userservice
|
||||
spec:
|
||||
{{- $hpaEnabled := false -}}
|
||||
{{- if hasKey $svc "hpa" -}}
|
||||
{{- if hasKey $svc.hpa "enabled" -}}
|
||||
{{- $hpaEnabled = $svc.hpa.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $hpaEnabled }}
|
||||
replicas: {{ $svc.replicas | default 1 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: userservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "topfans.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: userservice
|
||||
spec:
|
||||
serviceAccountName: {{ include "topfans.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: userservice
|
||||
image: {{ include "topfans.image" (dict "Values" .Values "service" $svc.image.repositoryName "tag" $svc.image.tag) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy | default "IfNotPresent" }}
|
||||
ports:
|
||||
- name: dubbo
|
||||
containerPort: {{ $svc.port }}
|
||||
env:
|
||||
{{- include "topfans.commonEnv" . | nindent 10 }}
|
||||
{{- range $k, $v := $svc.env }}
|
||||
- name: {{ $k }}
|
||||
value: {{ $v | quote }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef: { name: jwt-secret }
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultLiveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultLiveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultLiveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultLiveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.healthPath | default "/health" }}
|
||||
port: {{ $svc.port }}
|
||||
initialDelaySeconds: {{ $.Values.probes.defaultReadiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $.Values.probes.defaultReadiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $.Values.probes.defaultReadiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $.Values.probes.defaultReadiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 10 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: userservice
|
||||
namespace: {{ .Values.namespace.name | default "topfans" }}
|
||||
labels:
|
||||
{{- include "topfans.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: userservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
{{- include "topfans.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: userservice
|
||||
ports:
|
||||
- name: dubbo
|
||||
port: {{ $svc.port }}
|
||||
targetPort: dubbo
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
75
k8s/helm/topfans/values-prod.example.yaml
Normal file
75
k8s/helm/topfans/values-prod.example.yaml
Normal file
@ -0,0 +1,75 @@
|
||||
# ===================================================================
|
||||
# TopFans Helm — 生产 values 模板 (空值, 真值不入 git)
|
||||
# ===================================================================
|
||||
# 使用:
|
||||
# 1. cp values-prod.example.yaml values-prod.yaml
|
||||
# 2. 填入真值 (DB_PASSWORD, API Key 等)
|
||||
# 3. (可选) sops -e values-prod.yaml > values-prod.sops.yaml
|
||||
# 或直接由 CI 注入
|
||||
# 4. helm install topfans ./helm/topfans -f values-prod.yaml
|
||||
#
|
||||
# 真值文件 values-prod.yaml 必须加入 .gitignore! 提交时被 reject。
|
||||
# ===================================================================
|
||||
|
||||
global:
|
||||
image:
|
||||
registry: registry.cn-shanghai.aliyuncs.com
|
||||
repositoryNamespace: topfans
|
||||
|
||||
env: production
|
||||
ginMode: release
|
||||
logLevel: info
|
||||
|
||||
dbHost: postgres
|
||||
dbPort: "5432"
|
||||
dbUser: postgres
|
||||
dbName: topfans
|
||||
dbSslmode: disable
|
||||
redisHost: redis
|
||||
redisPort: "6379"
|
||||
redisDb: "0"
|
||||
|
||||
# ---------- 每个服务的 image tag 由 CI 注入 ----------
|
||||
services:
|
||||
gateway:
|
||||
image: { tag: "" } # ← CI 写入实际版本 (v1.0.0 / 8e3f...)
|
||||
userservice: { image: { tag: "" } }
|
||||
assetservice: { image: { tag: "" } }
|
||||
socialservice: { image: { tag: "" } }
|
||||
galleryservice: { image: { tag: "" } }
|
||||
activityservice: { image: { tag: "" } }
|
||||
starbookservice: { image: { tag: "" } }
|
||||
taskservice: { image: { tag: "" } }
|
||||
aichatservice: { image: { tag: "" } }
|
||||
statisticservice: { image: { tag: "" } }
|
||||
notificationservice: { image: { tag: "" } }
|
||||
moderationservice: { image: { tag: "" } }
|
||||
ossCorsInit: { image: { tag: "" } }
|
||||
|
||||
# ---------- Ingress 真域名 ----------
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: api.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend: gateway
|
||||
tls:
|
||||
- hosts:
|
||||
- api.example.com
|
||||
secretName: topfans-tls
|
||||
|
||||
# ---------- 真实序列同步表 (切流量前补全) ----------
|
||||
pgSequenceSync:
|
||||
tables:
|
||||
- assets
|
||||
- asset_registry
|
||||
- users
|
||||
- stars
|
||||
- activity_assets
|
||||
- collection_assets
|
||||
- materials
|
||||
- exhibitions
|
||||
- galleries
|
||||
388
k8s/helm/topfans/values.yaml
Normal file
388
k8s/helm/topfans/values.yaml
Normal file
@ -0,0 +1,388 @@
|
||||
# ===================================================================
|
||||
# TopFans Helm Chart — 默认 values (Phase 1)
|
||||
# ===================================================================
|
||||
# 真实部署时用 values-prod.yaml 覆盖以下占位符:
|
||||
# helm install topfans ./helm/topfans -f values-prod.yaml
|
||||
#
|
||||
# 严禁把含真密码 / 真 API Key 的 values-prod.yaml 提交到 git!
|
||||
# .gitignore 已加入 *.values-prod.yaml / k8s/helm/**/values-prod.yaml
|
||||
# 真值由 CI/CD 或 External Secrets Operator 注入。
|
||||
# ===================================================================
|
||||
|
||||
# ---------- Global ----------
|
||||
# Namespace (Phase 1: 全部在 topfans 单 ns, Phase 2 会拆成 topfans-shared / topfans-group-*)
|
||||
namespace:
|
||||
name: topfans
|
||||
|
||||
# ---------- Global ----------
|
||||
global:
|
||||
# 镜像仓库 (§10.1 选项 A: 阿里云 ACR 推荐)
|
||||
# 推送路径模板: registry.cn-shanghai.aliyuncs.com/topfans/<service>
|
||||
# 临时改 registry: helm install -f values-prod.yaml --set global.image.registry=...
|
||||
image:
|
||||
registry: registry.cn-shanghai.aliyuncs.com
|
||||
repositoryNamespace: topfans
|
||||
# 例如最终 gateway image: registry.cn-shanghai.aliyuncs.com/topfans/gateway:v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
# tag 在每个服务下覆盖; 默认 latest 不可用于生产
|
||||
|
||||
# 环境 (prod / staging / dev)
|
||||
env: production
|
||||
ginMode: release
|
||||
logLevel: info
|
||||
|
||||
# DB / Redis 走 ExternalName → RDS / ElastiCache (Phase 1 强制)
|
||||
dbHost: postgres # K8s 内的 short DNS, 对应 templates/external-db/postgres-external.yaml
|
||||
dbPort: "5432"
|
||||
dbUser: postgres
|
||||
dbName: topfans
|
||||
dbSslmode: disable
|
||||
redisHost: redis # K8s 内的 short DNS, 对应 templates/external-db/redis-external.yaml
|
||||
redisPort: "6379"
|
||||
redisDb: "0"
|
||||
|
||||
# ---------- ServiceAccount ----------
|
||||
# Phase 1 服务都不需要访问 K8s API, 创建但保持最小权限
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: topfans
|
||||
annotations: {}
|
||||
|
||||
# ---------- Secrets 真值 (默认空, 由 values-prod.yaml 注入) ----------
|
||||
secrets:
|
||||
db:
|
||||
password: "" # 来自 values-prod.yaml
|
||||
redis:
|
||||
password: "" # 来自 values-prod.yaml
|
||||
jwt:
|
||||
secret: "" # 来自 values-prod.yaml
|
||||
oss:
|
||||
region: "cn-shanghai"
|
||||
bucket: ""
|
||||
accessKeyId: ""
|
||||
accessKeySecret: ""
|
||||
stsRoleArn: ""
|
||||
avatarDir: "avatar/"
|
||||
assetDir: "asset/"
|
||||
tokenExpireTime: "3600"
|
||||
landingBaseUrl: "https://api.example.com"
|
||||
ai:
|
||||
difyApiBase: "http://dify-ext:5001/v1"
|
||||
difyApiKey: ""
|
||||
minimaxApiKey: ""
|
||||
minimaxApiUrl: "https://api.minimaxi.com/v1/image_generation"
|
||||
openaiApiKey: ""
|
||||
openaiBaseUrl: "https://api.weda.cc/v1"
|
||||
openaiModel: "gpt-image-2"
|
||||
qwenApiKey: ""
|
||||
sms:
|
||||
accessKeyId: ""
|
||||
accessKeySecret: ""
|
||||
signName: ""
|
||||
templateCode: ""
|
||||
region: "cn-hangzhou"
|
||||
|
||||
# ---------- 13 个服务统一 schema ----------
|
||||
# 为减少重复, 每个服务块用同一组子键:
|
||||
# enabled — 是否部署 (Phase 1 全 true, future-services 目录除外)
|
||||
# replicas — Pod 副本数 (HPA 模式下会被 HPA 覆盖 min/max)
|
||||
# port — 服务端口 (用于 Service + Dubbo URL + livenessProbe)
|
||||
# healthPath — 健康检查路径 (有的服务是 /health, statistic 是 /healthz)
|
||||
# healthPort — 健康检查端口 (默认同 port; moderationservice 例外)
|
||||
# image — 子块: repositoryName / tag / pullPolicy
|
||||
# resources — requests / limits (CPU + memory), 4G/2C 单机裁剪版
|
||||
# env — 业务专属 env (覆盖 common-env)
|
||||
# 使用纯字符串, 不写敏感 (敏感走 Secret)
|
||||
# probe — 子块: livenessProbe / readinessProbe 阈值 (默认给了合理默认值)
|
||||
|
||||
services:
|
||||
# ============== Gateway (流量入口, 启用 HPA) ==============
|
||||
gateway:
|
||||
enabled: true
|
||||
replicas: 2
|
||||
port: 8080
|
||||
healthPath: /health
|
||||
image:
|
||||
repositoryName: gateway
|
||||
tag: latest # CI 覆盖
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 128Mi }
|
||||
limits: { cpu: 500m, memory: 256Mi }
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPU: 60
|
||||
env:
|
||||
SERVER_PORT: "8080"
|
||||
# 以下 Dubbo URL 是 K8s short DNS, 与原 docker-compose 对齐 (应用代码零改动)
|
||||
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||
DUBBO_SOCIAL_SERVICE_URL: tri://socialservice:20002
|
||||
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||
DUBBO_GALLERY_SERVICE_URL: tri://galleryservice:20001
|
||||
DUBBO_ACTIVITY_SERVICE_URL: tri://activityservice:20004
|
||||
DUBBO_TASK_SERVICE_URL: tri://taskservice:20006
|
||||
DUBBO_STARBOOK_SERVICE_URL: tri://starbookservice:20005
|
||||
DUBBO_AI_CHAT_SERVICE_URL: tri://aichatservice:20008
|
||||
DUBBO_STATISTIC_SERVICE_URL: tri://statisticservice:20009
|
||||
DUBBO_NOTIFICATION_SERVICE_URL: tri://notificationservice:20010
|
||||
DUBBO_MODERATION_SERVICE_URL: tri://moderationservice:20011
|
||||
LASER_GEN_PROVIDER: openai
|
||||
# OPENAI_BASE_URL / OPENAI_MODEL 来自 envFrom secret (ai-keys)
|
||||
# OPENAI_API_KEY 来自 envFrom secret (ai-keys)
|
||||
|
||||
# ============== 数据服务 ==============
|
||||
userservice:
|
||||
enabled: true
|
||||
replicas: 2
|
||||
port: 20000
|
||||
healthPath: /health
|
||||
healthPort: 20000
|
||||
image: { repositoryName: userservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 500m, memory: 150Mi }
|
||||
env:
|
||||
PORT: "20000"
|
||||
STATISTIC_SERVICE_URL: tri://statisticservice:20009
|
||||
|
||||
socialservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20002
|
||||
healthPath: /health
|
||||
healthPort: 20002
|
||||
image: { repositoryName: socialservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 500m, memory: 150Mi }
|
||||
env:
|
||||
PORT: "20002"
|
||||
USER_SERVICE_URL: tri://userservice:20000
|
||||
ASSET_SERVICE_URL: tri://assetservice:20003
|
||||
STATISTIC_SERVICE_URL: tri://statisticservice:20009
|
||||
|
||||
assetservice:
|
||||
enabled: true
|
||||
replicas: 2
|
||||
port: 20003
|
||||
healthPath: /health
|
||||
healthPort: 20003
|
||||
image: { repositoryName: assetservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 500m, memory: 200Mi }
|
||||
env:
|
||||
PORT: "20003"
|
||||
USER_SERVICE_URL: tri://userservice:20000
|
||||
STATISTIC_SERVICE_URL: tri://statisticservice:20009
|
||||
|
||||
galleryservice:
|
||||
enabled: true
|
||||
replicas: 2
|
||||
port: 20001
|
||||
healthPath: /health
|
||||
healthPort: 20001
|
||||
image: { repositoryName: galleryservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 500m, memory: 150Mi }
|
||||
env:
|
||||
PORT: "20001"
|
||||
USER_SERVICE_URL: tri://userservice:20000
|
||||
ASSET_SERVICE_URL: tri://assetservice:20003
|
||||
TASK_SERVICE_URL: tri://taskservice:20006
|
||||
STATISTIC_SERVICE_URL: tri://statisticservice:20009
|
||||
MQ_REDIS_ADDR: redis:6379
|
||||
MQ_REDIS_DB: "2"
|
||||
MQ_REDIS_PASSWORD_SECRET_REF: db-credentials
|
||||
|
||||
activityservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20004
|
||||
healthPath: /health
|
||||
healthPort: 20004
|
||||
image: { repositoryName: activityservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 32Mi }
|
||||
limits: { cpu: 500m, memory: 100Mi }
|
||||
env:
|
||||
PORT: "20004"
|
||||
USER_SERVICE_URL: tri://userservice:20000
|
||||
|
||||
starbookservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20005
|
||||
healthPath: /health
|
||||
healthPort: 20005
|
||||
image: { repositoryName: starbookservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 500m, memory: 150Mi }
|
||||
env:
|
||||
PORT: "20005"
|
||||
ASSET_SERVICE_URL: tri://assetservice:20003
|
||||
|
||||
taskservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20006
|
||||
healthPath: /health
|
||||
healthPort: 20006
|
||||
image: { repositoryName: taskservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 500m, memory: 150Mi }
|
||||
env:
|
||||
PORT: "20006"
|
||||
USER_SERVICE_URL: tri://userservice:20000
|
||||
GALLERY_SERVICE_URL: tri://galleryservice:20001
|
||||
STATISTIC_SERVICE_URL: tri://statisticservice:20009
|
||||
MQ_REDIS_ADDR: redis:6379
|
||||
MQ_REDIS_DB: "2"
|
||||
MQ_REDIS_PASSWORD_SECRET_REF: db-credentials
|
||||
|
||||
aichatservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20008
|
||||
healthPath: /health
|
||||
healthPort: 20008
|
||||
image: { repositoryName: aichatservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 250m, memory: 256Mi }
|
||||
limits: { cpu: 1000m, memory: 512Mi }
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
targetCPU: 70
|
||||
env:
|
||||
PORT: "20008"
|
||||
# DIFY_API_BASE / DIFY_API_KEY 由 ai-keys Secret 注入
|
||||
|
||||
statisticservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20009
|
||||
healthPath: /healthz
|
||||
healthPort: 20009
|
||||
image: { repositoryName: statisticservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 128Mi }
|
||||
limits: { cpu: 500m, memory: 300Mi }
|
||||
env:
|
||||
PORT: "20009"
|
||||
STATISTIC_DB_HOST: postgres
|
||||
STATISTIC_DB_PORT: "5432"
|
||||
STATISTIC_DB_NAME: topfans
|
||||
STATISTIC_DB_SCHEMA: statistic
|
||||
STATISTIC_DB_SSLMODE: disable
|
||||
STATISTIC_REDIS_HOST: redis
|
||||
STATISTIC_REDIS_PORT: "6379"
|
||||
STATISTIC_REDIS_DB: "0"
|
||||
# 注意: STATISTIC_DB_PASSWORD / STATISTIC_REDIS_PASSWORD 用 secretKeyRef, 见 templates/*/statisticservice.yaml
|
||||
USER_SERVICE_URL: tri://userservice:20000
|
||||
|
||||
notificationservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20010
|
||||
healthPath: /healthz
|
||||
healthPort: 20010
|
||||
image: { repositoryName: notificationservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 128Mi }
|
||||
limits: { cpu: 500m, memory: 300Mi }
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 1
|
||||
maxReplicas: 4
|
||||
targetCPU: 70
|
||||
env:
|
||||
PORT: "20010"
|
||||
PUSH_ENABLED: "true"
|
||||
PUSH_URL: "https://env-00jy6bcqqwy6.dev-hz.cloudbasefunction.cn/sendMessage"
|
||||
PUSH_TIMEOUT_MS: "4000"
|
||||
|
||||
moderationservice:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
port: 20011
|
||||
healthPath: / # 文档 §10.4 提到原 compose 是 / 无 healthz path, 保留兼容
|
||||
healthPort: 20011
|
||||
image: { repositoryName: moderationservice, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 128Mi }
|
||||
limits: { cpu: 500m, memory: 300Mi }
|
||||
env:
|
||||
PORT: "20011"
|
||||
DUBBO_USER_SERVICE_URL: tri://userservice:20000
|
||||
DUBBO_ASSET_SERVICE_URL: tri://assetservice:20003
|
||||
DUBBO_NOTIFICATION_SERVICE_URL: tri://notificationservice:20010
|
||||
|
||||
# ============== 一次性 Job (oss-cors-init) ==============
|
||||
# 通过 helm hook 在 pre-install / pre-upgrade 时跑一次, 成功后不再重启
|
||||
ossCorsInit:
|
||||
enabled: true
|
||||
image: { repositoryName: oss-cors-init, tag: latest }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 300m, memory: 256Mi }
|
||||
|
||||
# ---------- 探针默认参数 (每个服务可覆盖) ----------
|
||||
probes:
|
||||
defaultLiveness:
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
defaultReadiness:
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
# ---------- Ingress ----------
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "20m"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
|
||||
# cert-manager (Phase 1 部署时已经预装)
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: api.example.com # 部署时改成真域名
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend: gateway
|
||||
tls:
|
||||
- hosts:
|
||||
- api.example.com
|
||||
secretName: topfans-tls
|
||||
|
||||
# ---------- PG 序列同步 (切流量前必跑) ----------
|
||||
pgSequenceSync:
|
||||
enabled: true
|
||||
# 列出所有 BIGSERIAL 的表, 脚本会逐个 setval(... _id_seq, (SELECT MAX(id) FROM ...))
|
||||
# 与 backend/scripts/... 等手动同步脚本同源, 详见 k8s/jobs/pg-sequence-sync/job.yaml
|
||||
tables:
|
||||
- assets
|
||||
- asset_registry
|
||||
- users
|
||||
- stars
|
||||
- activity_assets
|
||||
- collection_assets
|
||||
- materials
|
||||
- exhibitions
|
||||
- galleries
|
||||
# 脚本运行后验证: SELECT ... WHERE is_healthy=true 才视为成功
|
||||
verifyOnRun: true
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 64Mi }
|
||||
limits: { cpu: 300m, memory: 128Mi }
|
||||
138
k8s/jobs/pg-sequence-sync/job-template.yaml
Normal file
138
k8s/jobs/pg-sequence-sync/job-template.yaml
Normal file
@ -0,0 +1,138 @@
|
||||
# ===================================================================
|
||||
# PostgreSQL 序列同步 Job 模板
|
||||
# ===================================================================
|
||||
# 这个是"裸"模板, 部署时 helm hook 在 pre-install / pre-upgrade 时跑一次。
|
||||
# Chart 内部 templates/ 也有一个对应的模板走 helm hook, 两种用法:
|
||||
#
|
||||
# 1. helm install/upgrade 自动触发 (推荐)
|
||||
# → 见 templates/pg-sequence-sync.yaml (已挂 helm hook)
|
||||
#
|
||||
# 2. 单独手动跑 (紧急 / 调试 / 补单)
|
||||
# → kubectl apply -f k8s/jobs/pg-sequence-sync/manual-job.yaml
|
||||
# (manual-job.yaml 从本模板渲染, 把 ${TABLES_CSV} 替换成实际值)
|
||||
#
|
||||
# ⚠️ 切流量前必跑 (CLAUDE.md 强制规范, 设计文档 §10.3 hard blocker)
|
||||
# ===================================================================
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: topfans-pg-sequence-sync-script
|
||||
namespace: topfans
|
||||
labels:
|
||||
app.kubernetes.io/component: pg-sequence-sync
|
||||
app.kubernetes.io/part-of: topfans
|
||||
data:
|
||||
# 完全展开后的脚本: kubectl create cm -n topfans --from-file=script.sh=script.sh
|
||||
script.sh: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PG_HOST="${PG_HOST:-postgres}"
|
||||
PG_PORT="${PG_PORT:-5432}"
|
||||
PG_USER="${PG_USER:-postgres}"
|
||||
PG_DB="${PG_DB:-topfans}"
|
||||
PG_PASSWORD="${PG_PASSWORD:?PG_PASSWORD 必须通过 envFrom secret 注入}"
|
||||
TABLES_CSV="${TABLES_CSV:-assets,asset_registry,users,stars,activity_assets,collection_assets,materials,exhibitions,galleries}"
|
||||
|
||||
IFS=',' read -ra TABLES <<< "$TABLES_CSV"
|
||||
echo "Tables to sync: ${#TABLES[@]}"
|
||||
|
||||
for tbl in "${TABLES[@]}"; do
|
||||
tbl=$(echo "$tbl" | tr -d ' ')
|
||||
[ -z "$tbl" ] && continue
|
||||
max_id=$(PGPASSWORD="$PG_PASSWORD" psql \
|
||||
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-tAc "SELECT COALESCE(MAX(id), 0) FROM ${tbl};")
|
||||
[ "$max_id" = "0" ] && { echo " ${tbl}: empty, skip"; continue; }
|
||||
PGPASSWORD="$PG_PASSWORD" psql \
|
||||
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-c "SELECT setval('${tbl}_id_seq', ${max_id}, true);"
|
||||
echo " ${tbl}: setval(${tbl}_id_seq, ${max_id}, true)"
|
||||
done
|
||||
|
||||
# Verify
|
||||
bad=0
|
||||
for tbl in "${TABLES[@]}"; do
|
||||
tbl=$(echo "$tbl" | tr -d ' ')
|
||||
[ -z "$tbl" ] && continue
|
||||
h=$(PGPASSWORD="$PG_PASSWORD" psql \
|
||||
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-tAc "SELECT last_value >= COALESCE((SELECT MAX(id) FROM ONLY ${tbl}), 0) FROM pg_sequences WHERE sequencename = '${tbl}_id_seq';")
|
||||
if [ "$h" != "t" ]; then
|
||||
echo " ❌ ${tbl}_id_seq unhealthy"
|
||||
bad=$((bad+1))
|
||||
else
|
||||
echo " ✅ ${tbl}_id_seq healthy"
|
||||
fi
|
||||
done
|
||||
[ "$bad" -gt 0 ] && { echo "❌ ${bad} unhealthy"; exit 1; }
|
||||
echo "✅ all healthy"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: pg-sequence-sync-pg-password
|
||||
namespace: topfans
|
||||
labels:
|
||||
app.kubernetes.io/component: pg-sequence-sync
|
||||
type: Opaque
|
||||
stringData:
|
||||
PG_PASSWORD: "" # ← 必须从 db-credentials 同步或 CI 注入
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: topfans-pg-sequence-sync
|
||||
namespace: topfans
|
||||
labels:
|
||||
app.kubernetes.io/component: pg-sequence-sync
|
||||
app.kubernetes.io/part-of: topfans
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "-5" # 在 oss-cors-init (-10) 之后跑, 但顺序不重要
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
backoffLimit: 0
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
# postgresql-client 镜像: 用 bitnami/postgresql:latest 仅 client 部分, 不带 server
|
||||
# 或 alpine + apk add postgresql-client。 这里用 Google 维护的轻量镜像
|
||||
containers:
|
||||
- name: psql
|
||||
image: alpine:3.19
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -e
|
||||
apk add --no-cache postgresql16-client bash >/dev/null
|
||||
bash /scripts/script.sh
|
||||
env:
|
||||
- name: PG_HOST
|
||||
value: "postgres"
|
||||
- name: PG_PORT
|
||||
value: "5432"
|
||||
- name: PG_USER
|
||||
value: "postgres"
|
||||
- name: PG_DB
|
||||
value: "topfans"
|
||||
- name: PG_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: db-credentials
|
||||
key: DB_PASSWORD
|
||||
- name: TABLES_CSV
|
||||
value: "assets,asset_registry,users,stars,activity_assets,collection_assets,materials,exhibitions,galleries"
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: topfans-pg-sequence-sync-script
|
||||
defaultMode: 0o755
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534 # nobody
|
||||
117
k8s/jobs/pg-sequence-sync/script.sh
Executable file
117
k8s/jobs/pg-sequence-sync/script.sh
Executable file
@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# ===================================================================
|
||||
# PostgreSQL 序列同步脚本 (Phase 1 切流量前必跑 — CLAUDE.md 强制规范)
|
||||
# ===================================================================
|
||||
# 用途:
|
||||
# 对所有 BIGSERIAL 主键的表,把 postgres 序列的 last_value 推进到 MAX(id),
|
||||
# 防止应用代码 INSERT 时报 duplicate key value violates unique constraint
|
||||
#
|
||||
# 调用:
|
||||
# kubectl create job ...
|
||||
# 或 helm hook (pre-install / pre-upgrade 自动跑, 失败则 helm install 中断)
|
||||
#
|
||||
# 退出码:
|
||||
# 0 = 全部 healthy
|
||||
# 1 = 有 unhealthy 序列, 切流量会失败, 必须排查
|
||||
# ===================================================================
|
||||
set -euo pipefail
|
||||
|
||||
PG_HOST="${PG_HOST:-postgres}"
|
||||
PG_PORT="${PG_PORT:-5432}"
|
||||
PG_USER="${PG_USER:-postgres}"
|
||||
PG_DB="${PG_DB:-topfans}"
|
||||
PG_PASSWORD="${PG_PASSWORD:?PG_PASSWORD 必须通过 envFrom secret 注入}"
|
||||
|
||||
# 由 ConfigMap / values 注入
|
||||
TABLES_CSV="${TABLES_CSV:-assets,asset_registry,users,stars,activity_assets,collection_assets,materials,exhibitions,galleries}"
|
||||
|
||||
# 转数组
|
||||
IFS=',' read -ra TABLES <<< "$TABLES_CSV"
|
||||
|
||||
echo "============================================="
|
||||
echo "Phase 1: PostgreSQL 序列同步"
|
||||
echo "============================================="
|
||||
echo "Target: ${PG_USER}@${PG_HOST}:${PG_PORT}/${PG_DB}"
|
||||
echo "Tables: ${#TABLES[@]} 个 BIGSERIAL 表"
|
||||
echo ""
|
||||
|
||||
# ---------- Step 1: 同步所有表 ----------
|
||||
echo "📍 Step 1/2: 同步所有表的 _id_seq"
|
||||
echo ""
|
||||
for tbl in "${TABLES[@]}"; do
|
||||
tbl=$(echo "$tbl" | tr -d ' ')
|
||||
[ -z "$tbl" ] && continue
|
||||
echo " → ${tbl}"
|
||||
|
||||
# 先看 max(id), 确认表非空
|
||||
max_id=$(PGPASSWORD="$PG_PASSWORD" psql \
|
||||
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-tAc "SELECT COALESCE(MAX(id), 0) FROM ${tbl};")
|
||||
|
||||
if [ "$max_id" = "0" ]; then
|
||||
echo " (空表, 不需要同步序列)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# setval(_id_seq, max(id), true) — true 表示下一次 nextval 直接返回 max(id)+1
|
||||
PGPASSWORD="$PG_PASSWORD" psql \
|
||||
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-c "SELECT setval('${tbl}_id_seq', ${max_id}, true);"
|
||||
|
||||
echo " ✅ 已同步到 ${max_id}"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ---------- Step 2: 验证 (硬性 PASS 条件) ----------
|
||||
echo "📍 Step 2/2: 验证所有序列 healthy"
|
||||
echo ""
|
||||
|
||||
unhealthy_file=$(mktemp)
|
||||
for tbl in "${TABLES[@]}"; do
|
||||
tbl=$(echo "$tbl" | tr -d ' ')
|
||||
[ -z "$tbl" ] && continue
|
||||
seq="${tbl}_id_seq"
|
||||
|
||||
# pg_sequences 自 PG10 起可用 — last_value >= max_id 视为 healthy
|
||||
result=$(PGPASSWORD="$PG_PASSWORD" psql \
|
||||
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
|
||||
-tAc "
|
||||
SELECT
|
||||
seq.schemaname,
|
||||
seq.sequencename,
|
||||
seq.last_value,
|
||||
(SELECT COALESCE(MAX(id), 0) FROM ONLY ${tbl}) AS table_max_id,
|
||||
seq.last_value >= (SELECT COALESCE(MAX(id), 0) FROM ONLY ${tbl}) AS is_healthy
|
||||
FROM pg_sequences seq
|
||||
WHERE seq.sequencename = '${seq}';
|
||||
")
|
||||
|
||||
is_healthy=$(echo "$result" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
|
||||
last_value=$(echo "$result" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
||||
table_max=$(echo "$result" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
||||
|
||||
if [ "$is_healthy" = "t" ]; then
|
||||
echo " ✅ ${seq}: last_value=${last_value} >= max(id)=${table_max}"
|
||||
else
|
||||
echo " ❌ ${seq}: last_value=${last_value} < max(id)=${table_max} ← 不健康!"
|
||||
echo "${seq}|${last_value}|${table_max}" >> "$unhealthy_file"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ---------- 收尾 ----------
|
||||
unhealthy_count=$(wc -l < "$unhealthy_file" 2>/dev/null || echo 0)
|
||||
rm -f "$unhealthy_file"
|
||||
|
||||
if [ "$unhealthy_count" -gt 0 ]; then
|
||||
echo "============================================="
|
||||
echo "❌ 同步失败: ${unhealthy_count} 个序列不健康"
|
||||
echo "切流量会触发 duplicate key 错误, 启动 abort!"
|
||||
echo "============================================="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "============================================="
|
||||
echo "✅ 全部 ${#TABLES[@]} 个序列 healthy, 可以切流量"
|
||||
echo "============================================="
|
||||
exit 0
|
||||
511
k8s/scripts/fill-values-prod.sh
Executable file
511
k8s/scripts/fill-values-prod.sh
Executable file
@ -0,0 +1,511 @@
|
||||
#!/bin/bash
|
||||
# ===================================================================
|
||||
# 交互式生成 values-prod.yaml (K8s helm 真值文件)
|
||||
# ===================================================================
|
||||
# 目的:
|
||||
# - 避免把真密码 / API Key 写进 example 文件 (且 AI 永远不该看到)
|
||||
# - 引导式输入, **未填的字段占位为 __FILL_ME__**, 不阻断流程
|
||||
# - 文件权限 600 (owner only), .gitignore 确认
|
||||
# - 写完会列出仍待填的占位符, 用户 $EDITOR 补齐
|
||||
# - 可选: sops 加密后写 .sops.yaml (加密的可入 git)
|
||||
#
|
||||
# 使用:
|
||||
# ./k8s/scripts/fill-values-prod.sh # 交互 (推荐, 空字段占位)
|
||||
# ./k8s/scripts/fill-values-prod.sh --strict # 严格模式: 必填字段空 → exit 1
|
||||
# ./k8s/scripts/fill-values-prod.sh --non-interactive # 全用环境变量 (CI 用, 仍严格)
|
||||
# ./k8s/scripts/fill-values-prod.sh --dry-run # 只看不写
|
||||
# ./k8s/scripts/fill-values-prod.sh --sops # 写完后 sops 加密
|
||||
#
|
||||
# 占位符语义:
|
||||
# __FILL_ME__ 必填, helm install 会失败, 部署前必须替换
|
||||
# (留空) 可选, helm install 用 default 值
|
||||
#
|
||||
# 真值来源 (与生产 / 团队 1Password 对照):
|
||||
# * RDS endpoint 阿里云 RDS 控制台 → 数据库连接 → 内网地址
|
||||
# * ElastiCache 阿里云 Redis 控制台 → 连接地址
|
||||
# * DB / Redis 密码 RDS / Redis 实例设置
|
||||
# * JWT Secret 现有 docker/.env.prod 里的 JWT_SECRET
|
||||
# * OSS keys 阿里云 RAM 控制台 (子账号 AccessKey)
|
||||
# * DIFY_API_BASE/KEY Dify 工作室 → 工作室 API → API 密钥
|
||||
# * OPENAI_API_KEY 微达API 控制台
|
||||
# * SMS_ACCESS_KEY_ID 阿里云短信服务控制台
|
||||
# ===================================================================
|
||||
|
||||
set -euo pipefail
|
||||
# set 模式三件套:
|
||||
# -e: 任何命令非 0 退出立刻 abort (避免脚本"半路成功"的诡异状态)
|
||||
# -u: 引用未定义变量时报错 (避免 var=$VAR 在 VAR 没设时悄悄变成空)
|
||||
# -o pipefail: 管道链上任一环节失败 → 整链失败 (否则 `cmd1 | cmd2`
|
||||
# 只要 cmd2 成功就 exit 0, 隐藏 cmd1 的失败, 比如 `gitleaks | tail`)
|
||||
# **坑**: set -u 时 `${!var}` 这种间接引用要写 `${!var:-}`,
|
||||
# 否则 var 没定义时直接 abort (见 prompt_required 里的写法)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# ${BASH_SOURCE[0]} 是当前脚本自身路径 (source 时为调用者),
|
||||
# dirname 取目录, cd+ls 解析软链/相对路径 → 拿到**真实**绝对路径。
|
||||
# 这样无论用户从哪个 cwd 跑 `bash /path/to/this.sh`, SCRIPT_DIR 都是脚本所在目录。
|
||||
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
# SCRIPT_DIR 是 k8s/scripts/, 往上一级是 k8s/, 再上一级是仓库根。
|
||||
# 这个变量专门给 .gitignore 写入路径用, 不用 helm chart 路径因为可能跨项目。
|
||||
|
||||
CHART_DIR="$REPO_ROOT/k8s/helm/topfans"
|
||||
EXAMPLE_FILE="$CHART_DIR/values-prod.example.yaml"
|
||||
# 该文件作为**参考模板** (空占位结构), 真值由本脚本生成到 OUTPUT_FILE。
|
||||
# 注意: 我**没有**让脚本自动 cp example, 因为 example 写假值容易误部署。
|
||||
OUTPUT_FILE="$CHART_DIR/values-prod.yaml"
|
||||
# 真值输出文件, .gitignore 已屏蔽, chmod 600 由本脚本设。
|
||||
|
||||
# ---------- CLI 参数 ----------
|
||||
DRY_RUN=false # --dry-run: 只 echo 内容不写文件 (调试用)
|
||||
NON_INTERACTIVE=false # --non-interactive: 全靠环境变量读, 不交互; 隐含 STRICT=true
|
||||
STRICT=false # --strict: 必填字段空时 exit 1 (而不是写占位符)
|
||||
USE_SOPS=false # --sops: 写完后用 SOPS 加密成 .sops.yaml (可入 git)
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--non-interactive|--ci) NON_INTERACTIVE=true; STRICT=true ;;
|
||||
# CI 流水线里如果允许"留空写占位符"会导致 helm install 后
|
||||
# 才报错, 太晚; 故 CI 必填字段缺失直接 abort。
|
||||
--strict) STRICT=true ;;
|
||||
# 交互模式手动开严格, 留空会 exit 1 而非占位。
|
||||
--sops) USE_SOPS=true ;;
|
||||
-h|--help)
|
||||
# 用 sed 抽文件开头 30 行做 manpage, 任何改 header 都会自动反映
|
||||
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
||||
exit 0 ;;
|
||||
*)
|
||||
echo "未知参数: $arg"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- 颜色 ----------
|
||||
# ANSI 控制序列。 `-e` 让 echo 解释 `\033` 转义。
|
||||
# 颜色在 macOS Terminal / iTerm / VS Code 终端都有效; 但在 dumb / redirected
|
||||
# 输出时会显示成 [\033 之类的乱码 — 这种场景要靠 `tput` 检查 TTY 后再决定是否染色。
|
||||
# 本脚本只在交互 + 日志输出里染色, 写到文件的内容不带颜色, 所以乱码不会进 values-prod.yaml。
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
# NC (No Color) 必须每次 reset, 否则一个 print 染色后所有后续 echo 都染色。
|
||||
print_step() { echo -e "\n${CYAN}━━ $1 ━━${NC}"; }
|
||||
print_warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
|
||||
print_err() { echo -e "${RED}❌ $1${NC}"; }
|
||||
print_ok() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||
|
||||
# ---------- 占位符常量 ----------
|
||||
PLACEHOLDER='__FILL_ME__'
|
||||
# 唯一强占位符, helm template 时会原样输出, helm install 时 K8s Admission 会拒
|
||||
# (因为敏感字段不能为空)。这种"自我校验"机制让运维**部署前**就能发现有没有漏填。
|
||||
# 唯一**不**当作占位符的字符串: 空串 "" — 空串可能是合法的可选值。
|
||||
PLACEHOLDER_OPT='(留空, 用 chart default)'
|
||||
# 目前没用到 (prompt_optional 默认逻辑是不写占位符, 而是让 helm fallback), 留着给以后文档用
|
||||
|
||||
# ---------- pre-check: not already present ----------
|
||||
# 在大量 prompt 之前问一声, 避免写到一半才发现要保留旧文件 → 用户已经覆盖了半天输入。
|
||||
# CI / dry-run 跳过确认 (CI 没交互, dry-run 不写文件)
|
||||
if [ -f "$OUTPUT_FILE" ] && [ "$DRY_RUN" = false ] && [ "$NON_INTERACTIVE" = false ]; then
|
||||
print_warn "values-prod.yaml 已存在, 继续会覆盖!"
|
||||
read -rp "继续? (yes/no): " confirm
|
||||
# 用 yes/no 而非 y/n 是为了避免"敲错键"误覆盖 (验证过 yes 才继续)
|
||||
[ "$confirm" = "yes" ] || { print_warn "已取消"; exit 0; }
|
||||
fi
|
||||
|
||||
# ---------- 待填占位符记录 ----------
|
||||
# **Bug 历史**: 之前用 bash 数组 PENDING+=(), 但 prompt_* 在 $(...) command
|
||||
# substitution 里调用, subshell 改的全局变量不会传回父 shell, 导致 summary
|
||||
# 永远显示"全部已填"。修复: 改用临时文件。
|
||||
PENDING_FILE="$(mktemp -t fill-pending.XXXXXX)"
|
||||
# mktemp -t name.XXXXXX → /tmp/name.XXXXXX, 随机后缀避免冲突。
|
||||
# 在 macOS 默认 /tmp/, Linux 默认 $TMPDIR(/tmp/)。
|
||||
|
||||
trap 'rm -f "$PENDING_FILE"' EXIT
|
||||
# trap ... EXIT 在脚本退出 (包括 abort/err) 时都执行, 保证不留垃圾。
|
||||
# 关键: 不能用 trap ... INT, 这样 ctrl-c 才不会触发清理, 但脚本会卡死。
|
||||
# EXIT 是兜底。
|
||||
|
||||
> "$PENDING_FILE" # 清空 (mktemp 创建的可能已有 stale 残留下文)
|
||||
|
||||
# 把待补字段记到文件, 行格式: section_key|comment|nature(nature=必填/可选)
|
||||
# 用 | 分隔: 用户**写**的不是分隔符, 字段名是固定的点路径, 没有 |。
|
||||
record_pending() {
|
||||
local section_key="$1" comment="$2" nature="$3"
|
||||
printf '%s|%s|%s\n' "$section_key" "$comment" "$nature" >> "$PENDING_FILE"
|
||||
}
|
||||
|
||||
# ---------- helpers ----------
|
||||
# 两个 prompt 函数的语义:
|
||||
# prompt_required — 必填, 留空 → 写 __FILL_ME__ + 加入 PENDING_FILE 标 "必填"
|
||||
# prompt_optional — 可选, 留空 → 用 chart default; 在 prompt 处明确告诉用户
|
||||
# "留空用 default 还是留空跳过", 用户可控
|
||||
#
|
||||
# 共同的间接引用: ${!var} 是 bash 的 "间接变量" 语法。
|
||||
# 写法: var="FOO"; FOO=bar; echo "${!var}" → bar
|
||||
# 用 `${!var:-}` + `-n` 测试, 是 set -u 兼容: var 没定义时返回空字符串, 不报错。
|
||||
|
||||
prompt_required() {
|
||||
# 参数:
|
||||
# $1 var — 在 CI/--strict 模式下读的环境变量名 (例如 DB_PASSWORD)
|
||||
# $2 desc — 交互模式下显示的提示语 (例: 'Postgres 密码')
|
||||
# $3 section_key — YAML 路径 (例: secrets.db.password), 用于 PENDING_FILE 记录
|
||||
# $4 secret — 是否隐藏输入 (true/false), 密码字段用 true 走 `read -s`
|
||||
local var="$1" desc="$2" section_key="$3" secret="${4:-false}"
|
||||
local val prompt_str="$desc: "
|
||||
# prompt_str 默认用 ": ", 后续 secret=true 时 -s 模式不带 prefix prompt 也行, 但保留
|
||||
|
||||
if [ "$NON_INTERACTIVE" = true ] || [ "$STRICT" = true ]; then
|
||||
# CI / strict 路径: 不交互, 直接尝试读环境变量
|
||||
if [ -n "${!var:-}" ]; then
|
||||
# 找到了 → 输出真值
|
||||
printf '%s' "${!var}"
|
||||
return
|
||||
elif [ "$NON_INTERACTIVE" = true ]; then
|
||||
# CI 缺 var → 不可恢复, abort
|
||||
print_err "$section_key 必填 (CI 模式: env $var 未设)"
|
||||
exit 1
|
||||
fi
|
||||
# 走到这里是 STRICT=true 且 interactive 且 env $var 没设:
|
||||
# 不 abort, 走下面的 read, 但**最后**校验非空。
|
||||
fi
|
||||
|
||||
# 交互模式 (默认, 或 --strict 但 env $var 未设)
|
||||
# read -r: 不要 backslash escape (默认值), 不加 -r 的话 \n 会变成 \\
|
||||
# read -sp: -s 隐藏输入字符 (密码不回显); 注意 -s 在 macOS 上 minor bug,
|
||||
# 偶发会丢首字符, 用 `read -s`, 再 `echo`, 然后 trim 即可
|
||||
if [ "$secret" = true ]; then
|
||||
read -rsp "$prompt_str" val || val=""
|
||||
# `|| val=""`: read 失败 (Ctrl-D EOF / Ctrl-C 中断) 时不让 set -e 炸掉
|
||||
echo # 隐藏模式下 read 不自动换行, 我们手动 echo 一个
|
||||
else
|
||||
read -rp "$prompt_str" val || val=""
|
||||
fi
|
||||
|
||||
if [ -z "$val" ]; then
|
||||
# 用户敲了空回车 → 占位 + 记到 PENDING_FILE
|
||||
record_pending "$section_key" "$desc" "必填"
|
||||
printf '%s' "$PLACEHOLDER"
|
||||
else
|
||||
printf '%s' "$val"
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_optional() {
|
||||
# 参数:
|
||||
# $1 var — CI/--strict 模式下的环境变量名
|
||||
# $2 desc — 提示语
|
||||
# $3 default — 内置 default 值 (从 chart 同步过来), 用户敲回车用这个
|
||||
# $4 section_key — YAML 路径, 留空时记 PENDING_FILE (可选)
|
||||
#
|
||||
# 行为对比 prompt_required:
|
||||
# - 不会写 PLACEHOLDER 占位符, 而是写空串或 default
|
||||
# - 留空时 PENDING_FILE 标 "可选", summary 显示 🟡 而不是 ⛔
|
||||
# - 这样 chart 默认值可以"自动"接管, 即使这个 prompt 永远不交互
|
||||
local var="$1" desc="$2" default="${3:-}" section_key="${4:-}"
|
||||
local val prompt_str
|
||||
|
||||
if [ "$NON_INTERACTIVE" = true ]; then
|
||||
# CI: env $var 优先 → default → 完全空 (helm 会用 chart 内置 default)
|
||||
if [ -n "${!var:-}" ]; then
|
||||
printf '%s' "${!var}"
|
||||
elif [ -n "$default" ]; then
|
||||
printf '%s' "$default"
|
||||
else
|
||||
printf '%s' "" # 留空, helm 会用 chart 内的 default / required 拦截
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
# 交互模式: 显示提示包含 default + 留空说明
|
||||
if [ -n "$default" ]; then
|
||||
prompt_str="$desc [$default, 留空用 default]: "
|
||||
else
|
||||
prompt_str="$desc [留空跳过]: "
|
||||
fi
|
||||
|
||||
read -rp "$prompt_str" val || val=""
|
||||
if [ -z "$val" ]; then
|
||||
if [ -n "$default" ]; then
|
||||
# 用户敲回车, 用 default
|
||||
printf '%s' "$default"
|
||||
elif [ -n "$section_key" ]; then
|
||||
# 给 section_key 的可选字段留空: helm 收到空串而非 default,
|
||||
# 仍记录到 PENDING_FILE (summary 用), 但标 🟡
|
||||
record_pending "$section_key" "$desc" "可选"
|
||||
printf '%s' ""
|
||||
else
|
||||
printf '%s' "" # 真正无配置 (与 helm fallback 一致)
|
||||
fi
|
||||
else
|
||||
printf '%s' "$val"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- input collection ----------
|
||||
# 共 4 个章节, 22 个字段, 按"依赖关系"先通用后专用:
|
||||
# 1/4 全球通用 — DB / Redis 连接信息
|
||||
# 2/4 Gateway / 应用 — JWT / image tag / 域名
|
||||
# 3/4 OSS / 短信 — 阿里云资源访问凭据
|
||||
# 4/4 AI (镭射卡 / 对话) — 外部 AI 服务凭据
|
||||
# 这种分组让运维心里有数, 而不是随便顺序问 30 个问题。
|
||||
|
||||
print_step "1/4 全球通用"
|
||||
|
||||
DB_HOST_INTERNAL=$(prompt_required DB_HOST_INTERNAL 'Postgres RDS 内网地址 (例: rm-xxx.mysql.rds.aliyuncs.com)' 'global.dbExternalEndpoint')
|
||||
# DB_HOST 在 K8s 内部固定是 `postgres` (ExternalName), 这里要填的是 RDS **外网**
|
||||
# 内网地址: K8s Pod 用这个解析到 RDS (Pod 不出外网, 必须用阿里云 VPC 内网地址)。
|
||||
# 阿里云控制台: RDS → 实例 → 数据库连接 → 内网地址 (以 .mysql.rds.aliyuncs.com 结尾)。
|
||||
DB_PORT=$(prompt_optional DB_PORT 'Postgres 端口' '5432' 'global.dbPort')
|
||||
DB_NAME=$(prompt_optional DB_NAME '数据库名' 'topfans' 'global.dbName')
|
||||
DB_USER=$(prompt_optional DB_USER '用户名' 'postgres' 'global.dbUser')
|
||||
DB_PASSWORD=$(prompt_required DB_PASSWORD 'Postgres 密码' 'secrets.db.password' true)
|
||||
|
||||
REDIS_HOST_INTERNAL=$(prompt_required REDIS_HOST_INTERNAL 'Redis ElastiCache 内网地址 (例: r-xxx.redis.rds.aliyuncs.com)' 'global.redisExternalEndpoint')
|
||||
# 同上, K8s 内部 `redis` 短名对应 ExternalName, 实际值要填阿里云 Redis 内网地址。
|
||||
# 在 Redis 控制台 → 实例 → 连接管理 → 内网地址。
|
||||
REDIS_PORT=$(prompt_optional REDIS_PORT 'Redis 端口' '6379' 'global.redisPort')
|
||||
REDIS_PASSWORD=$(prompt_required REDIS_PASSWORD 'Redis 密码' 'secrets.redis.password' true)
|
||||
|
||||
print_step "2/4 Gateway / 应用"
|
||||
|
||||
JWT_SECRET=$(prompt_required JWT_SECRET 'JWT_SECRET (32 字符以上, 与 docker/.env.prod 的值保持一致)' 'secrets.jwt.secret' true)
|
||||
# ⚠️ 关键: JWT Secret 一旦改, 所有已签发的 token 全部失效, 用户会全部退出登录。
|
||||
# **必须**与现有 docker/.env.prod 的 JWT_SECRET 保持一致 (除非主动全员踢出)。
|
||||
# 在云上托管的密钥里取, 不要换新值 (会全站掉登录)。
|
||||
IMAGE_TAG=$(prompt_optional IMAGE_TAG '所有服务 image tag (CI 决定后填, 例如 v1.0.0)' 'latest')
|
||||
# 默认 latest 允许本地开发 (debian docker-compose), 生产必须填具体版本 (v1.0.0)。
|
||||
# helm install 用 --set global.image.tag=$IMAGE_TAG 可以临时覆盖所有服务 (见 deploy.sh upgrade)。
|
||||
INGRESS_HOST=$(prompt_optional INGRESS_HOST 'Ingress 域名' 'api.example.com' 'ingress.host')
|
||||
# Ingress 域名, K8s nginx-ingress 会读这个生成 cert-manager Certificate CR (TLS 自动续期)。
|
||||
# 部署前到阿里云 DNS 控制台把 A 记录指向 nginx-ingress-controller 的 EXTERNAL-IP。
|
||||
|
||||
print_step "3/4 OSS / 短信"
|
||||
|
||||
OSS_REGION=$(prompt_optional OSS_REGION 'OSS region' 'cn-shanghai' 'secrets.oss.region')
|
||||
# OSS region (上海) 与 RDS region 不一定一致, 按实际 bucket 创建的 region 填。
|
||||
OSS_BUCKET=$(prompt_required OSS_BUCKET 'OSS bucket 名 (例: top-fans-prod)' 'secrets.oss.bucket')
|
||||
OSS_KEY_ID=$(prompt_required OSS_KEY_ID 'OSS AccessKey ID' 'secrets.oss.accessKeyId')
|
||||
OSS_KEY_SECRET=$(prompt_required OSS_KEY_SECRET 'OSS AccessKey Secret' 'secrets.oss.accessKeySecret' true)
|
||||
# ⚠️ 这些是 OSS 全权子账号 (有 RAM STS AssumeRole 拿临时 token 上传头像/资产生成)
|
||||
# 强烈推荐**用子账号**而不是主账号 AccessKey, 子账号只授 OSS 写权限 + 受 IP 白名单。
|
||||
# 取值: 阿里云 RAM 控制台 → 用户 → AccessKey。
|
||||
OSS_STS_ROLE_ARN=$(prompt_optional OSS_STS_ROLE_ARN 'OSS STS Role ARN (留空跳过)' '' 'secrets.oss.stsRoleArn')
|
||||
# ARN 格式: acs:ram::1387642798143585:role/top-fans-oss-user
|
||||
# 没有可留空, 但前端的"STS 临时上传"会失败 (降级到 long-lived key, 不安全)。
|
||||
SMS_KEY_ID=$(prompt_optional SMS_KEY_ID 'SMS AccessKey ID (阿里云短信, 留空跳过)' '' 'secrets.sms.accessKeyId')
|
||||
SMS_KEY_SECRET=$(prompt_optional SMS_KEY_SECRET 'SMS AccessKey Secret (短信不用就留空)' '' 'secrets.sms.accessKeySecret' true)
|
||||
SMS_SIGN_NAME=$(prompt_optional SMS_SIGN_NAME 'SMS 签名 (留空跳过)' '' 'secrets.sms.signName')
|
||||
SMS_TEMPLATE=$(prompt_optional SMS_TEMPLATE 'SMS 模板代码 (留空跳过)' '' 'secrets.sms.templateCode')
|
||||
# SMS 4 项是阿里云短信服务的凭据, 不开通短信服务 (初版用 uniCloud 推送替代) 可全部留空。
|
||||
|
||||
print_step "4/4 AI (镭射卡 / 对话)"
|
||||
|
||||
DIFY_API_BASE=$(prompt_optional DIFY_API_BASE 'DIFY_API_BASE' 'http://dify:5001/v1' 'secrets.ai.difyApiBase')
|
||||
# Dify 地址, 默认是 K8s 内部的 `dify:5001` 短 DNS (Dify 也用 ExternalName 占位,
|
||||
# 见模板 external-db/ 那一对), 实际生产可能是阿里云部署的 Dify 公网地址。
|
||||
DIFY_API_KEY=$(prompt_required DIFY_API_KEY 'DIFY_API_KEY' 'secrets.ai.difyApiKey' true)
|
||||
# 在 Dify 控制台: 工作室 → 角角(应用名) → 后端服务 API → API 密钥 (格式 app-xxxxx)。
|
||||
OPENAI_BASE=$(prompt_optional OPENAI_BASE 'OpenAI 兼容 API base' 'https://api.weda.cc/v1' 'secrets.ai.openaiBaseUrl')
|
||||
OPENAI_KEY=$(prompt_required OPENAI_KEY 'OpenAI 兼容 API Key' 'secrets.ai.openaiApiKey' true)
|
||||
# 默认走微达API中转站 (OPENAI_BASE=weda.cc)。可在 dashboard 切换成其他兼容服务。
|
||||
# 没有微达账号就手动填 OpenAI 直连地址 https://api.openai.com/v1。
|
||||
OPENAI_MODEL=$(prompt_optional OPENAI_MODEL 'OpenAI model' 'gpt-image-2' 'secrets.ai.openaiModel')
|
||||
# 镭射卡生成模型, 默认 gpt-image-2。生产可在 LASER_GEN_PROVIDER 切到 dify/minimax。
|
||||
MINIMAX_KEY=$(prompt_optional MINIMAX_KEY 'MiniMax API Key (默认用 OpenAI, 不需要可留空)' '' 'secrets.ai.minimaxApiKey' true)
|
||||
MINIMAX_URL=$(prompt_optional MINIMAX_URL 'MiniMax URL' 'https://api.minimaxi.com/v1/image_generation' 'secrets.ai.minimaxApiUrl')
|
||||
# 当 LASER_GEN_PROVIDER=minimax 时启用, 默认 OpenAI, 这 2 项可留空。
|
||||
|
||||
# ---------- write file ----------
|
||||
# 这里用 here-string (...="...") 直接构造 YAML 字符串, 而不是逐行 echo,
|
||||
# 因为变量多且结构清晰, here-string 写出来最易读。
|
||||
# 关键约定:
|
||||
# 1. 所有"必填没填"的变量值都是 PLACEHOLDER='__FILL_ME__' (常量)
|
||||
# → helm template 会原样输出 → K8s Admission Webhook 会拒
|
||||
# → 部署前运维必须用 $EDITOR 替换这些字符串
|
||||
# 2. 所有"可选没填"的变量值都是 "" (空字符串, 这是合法 YAML)
|
||||
# → helm template 不会输出该字段 → chart 用内置 default
|
||||
# 3. 每个 ⚠ 标记提醒: 这个字段一旦有真值就是凭据
|
||||
#
|
||||
# **不要改字段顺序**, 给运维/AI 看的约定俗成, 改字段顺序增大 PR review 难度。
|
||||
OUTPUT_CONTENT="global:
|
||||
dbHost: postgres # K8s 内部短名, 对应 ExternalName postgres → RDS endpoint
|
||||
dbPort: \"$DB_PORT\" # 见 global.dbExternalEndpoint
|
||||
dbUser: \"$DB_USER\"
|
||||
dbName: \"$DB_NAME\"
|
||||
dbExternalEndpoint: \"$DB_HOST_INTERNAL\" # ⚠ 真值: 阿里云 RDS 内网地址, 不入 git
|
||||
redisHost: redis # 同上, ExternalName redis → ElastiCache endpoint
|
||||
redisPort: \"$REDIS_PORT\"
|
||||
redisExternalEndpoint: \"$REDIS_HOST_INTERNAL\" # ⚠ 真值
|
||||
|
||||
image:
|
||||
registry: registry.cn-shanghai.aliyuncs.com # 阿里云 ACR (design doc §10.1 选项 A)
|
||||
repositoryNamespace: topfans # 仓库 namespace
|
||||
|
||||
env: production
|
||||
ginMode: release
|
||||
logLevel: info
|
||||
|
||||
secrets:
|
||||
db:
|
||||
password: \"$DB_PASSWORD\" # ⚠ 真值
|
||||
redis:
|
||||
password: \"$REDIS_PASSWORD\" # ⚠ 真值
|
||||
jwt:
|
||||
secret: \"$JWT_SECRET\" # ⚠ 真值, 改了会让全站 token 失效
|
||||
oss:
|
||||
region: \"$OSS_REGION\"
|
||||
bucket: \"$OSS_BUCKET\"
|
||||
accessKeyId: \"$OSS_KEY_ID\" # ⚠ 真值: RAM 子账号 AccessKey
|
||||
accessKeySecret: \"$OSS_KEY_SECRET\" # ⚠ 真值
|
||||
stsRoleArn: \"$OSS_STS_ROLE_ARN\" # STS AssumeRole ARN (可选)
|
||||
landingBaseUrl: \"https://$INGRESS_HOST\" # 分享服务生成的落地页链接前缀
|
||||
ai:
|
||||
difyApiBase: \"$DIFY_API_BASE\"
|
||||
difyApiKey: \"$DIFY_API_KEY\" # ⚠ 真值
|
||||
openaiApiKey: \"$OPENAI_KEY\" # ⚠ 真值
|
||||
openaiBaseUrl: \"$OPENAI_BASE\"
|
||||
openaiModel: \"$OPENAI_MODEL\"
|
||||
minimaxApiKey: \"$MINIMAX_KEY\" # 可选
|
||||
minimaxApiUrl: \"$MINIMAX_URL\" # 可选
|
||||
sms:
|
||||
accessKeyId: \"$SMS_KEY_ID\" # ⚠ 真值 (可选)
|
||||
accessKeySecret: \"$SMS_KEY_SECRET\" # ⚠ 真值 (可选)
|
||||
signName: \"$SMS_SIGN_NAME\"
|
||||
templateCode: \"$SMS_TEMPLATE\"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 所有服务的 image tag 用全局覆盖 (services.<x>.image.tag = global.image.tag);
|
||||
# 单服务差异在 helm install 时
|
||||
# 用 --set 临时覆盖 (例: --set services.gateway.replicas=5)
|
||||
# ---------------------------------------------------------------
|
||||
services: {}
|
||||
"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
# --dry-run 模式: 不写文件, 把内容 echo 给用户看, 让他"先看再决定"
|
||||
# 经常用来调试: ./fill-values-prod.sh --dry-run | less
|
||||
print_warn "DRY RUN: 不写文件, 上面是预览"
|
||||
echo "$OUTPUT_CONTENT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
print_step "写文件"
|
||||
echo -n "$OUTPUT_CONTENT" > "$OUTPUT_FILE"
|
||||
# echo -n 不带末尾换行; 加上 here-string 末尾已有一个 \n, 总共一个换行, 是标准 YAML 风格。
|
||||
# > 的本质是 truncate+write, 即使脚本中间 SIGINT 也能拿到部分文件 (避免破坏性 >|)
|
||||
chmod 600 "$OUTPUT_FILE"
|
||||
# 600 = -rw------- ; owner 可读写, group/other 无权限。
|
||||
# 防止偶然的 `cat file` / `less file` / git status (本地) 暴漏真值。
|
||||
# 注意: chmod 在 NTFS / FAT 文件系统上无效, 这是 Linux/Mac ext4/apfs 的特性。
|
||||
print_ok "已写 $OUTPUT_FILE (权限 600, owner-only)"
|
||||
|
||||
# ---------- .gitignore 检查 ----------
|
||||
# 即使剧本本身极小概率出错 (运维 cp 命令误填), 也要有兜底防止真值入 git。
|
||||
# .gitignore 用 glob ** 匹配任何层级的 values-prod.yaml。
|
||||
# 检测是否已存在该条目:
|
||||
GITIGNORE="$REPO_ROOT/.gitignore"
|
||||
if grep -qE '^k8s/helm/\*\*/values-prod\.yaml$' "$GITIGNORE" 2>/dev/null; then
|
||||
print_ok ".gitignore 已排除 values-prod.yaml"
|
||||
else
|
||||
print_warn ".gitignore **似乎** 未排除 values-prod.yaml, 现在加"
|
||||
# 非交互模式 (CI) 不自动改 .gitignore — 那是仓库层面的修改, 必须人工 review。
|
||||
if [ "$NON_INTERACTIVE" = false ]; then
|
||||
read -rp "(yes/no): " confirm
|
||||
[ "$confirm" = "yes" ] && {
|
||||
printf '\n# K8s 真值文件 (含 DB/AI 凭据, 不入 git)\nk8s/helm/**/values-prod.yaml\n' >> "$GITIGNORE"
|
||||
print_ok "已加 .gitignore 条目"
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- 泄漏扫描 ----------
|
||||
# 兜底: 即使 chmod 600 / .gitignore 都对, 万一用户 vim 编辑时手滑推到 git
|
||||
# 也是灾难。 这里调 gitleaks (首选) / detect-secrets 扫**刚生成**的 file
|
||||
# 检查真值模式是否还像 placeholder (没填)。
|
||||
# 注意: 第一次 gitleaks detect 是**整个仓库** scan, 不是单 file,
|
||||
# 这通常 CI 跑; 本地想要只 scan 当前文件需要 detect-secrets。
|
||||
|
||||
if command -v gitleaks &>/dev/null; then
|
||||
print_step "gitleaks 扫描"
|
||||
# -v: 详细模式; --no-banner: 不要版本条; 2>&1: 把 stderr 合到 stdout
|
||||
# `|| true`: 不让 `gitleaks 检测到 leak` (exit 1) 把整个脚本带 abort
|
||||
# 因为这是 informational, 我们后面让运维自己处理
|
||||
gitleaks detect --source . --no-banner -v 2>&1 | tail -10 || true
|
||||
elif command -v detect-secrets &>/dev/null; then
|
||||
print_step "detect-secrets 扫描"
|
||||
detect-secrets scan "$OUTPUT_FILE" 2>&1 || true
|
||||
else
|
||||
print_warn "未装 gitleaks / detect-secrets, 手动核对:"
|
||||
echo " git diff --no-color k8s/helm/topfans/values-prod.yaml | grep -E 'password|key|secret' | head"
|
||||
# `head` 防泄漏太长; 仅看有没有"看起来是真值"的行
|
||||
fi
|
||||
|
||||
# ---------- SOPS 加密 (可选) ----------
|
||||
# 真值**明文**版的 values-prod.yaml 必须 600 权限 + 不入 git (前两步已做)。
|
||||
# 但运维不方便于共享给队友时, 用 SOPS 加密成 .sops.yaml 可以入 git
|
||||
# (加密内容读者无法解密, 但解密密钥在 KMS / PGP / age 中)。
|
||||
# 加密 helm install 流程:
|
||||
# sops --decrypt values-prod.sops.yaml > values-prod.yaml
|
||||
# helm install topfans ./helm/topfans -f values-prod.yaml
|
||||
if [ "$USE_SOPS" = true ]; then
|
||||
if ! command -v sops &>/dev/null; then
|
||||
print_err "sops 未装 (brew install sops)"
|
||||
exit 1
|
||||
fi
|
||||
print_step "sops 加密"
|
||||
SOPS_FILE="${OUTPUT_FILE%.yaml}.sops.yaml"
|
||||
# 去掉末尾的 .yaml, 加 .sops.yaml 作后缀: values-prod.sops.yaml
|
||||
sops --encrypt --in-place "$OUTPUT_FILE"
|
||||
# --in-place 原地加密, 不保留明文
|
||||
mv "$OUTPUT_FILE" "$SOPS_FILE"
|
||||
chmod 600 "$SOPS_FILE"
|
||||
print_ok "加密: $SOPS_FILE"
|
||||
print_ok "解密: sops --decrypt $SOPS_FILE > $OUTPUT_FILE"
|
||||
print_warn "生产部署时 SOPS_FILE 可以入 git (加密的)"
|
||||
fi
|
||||
|
||||
print_step "✅ 完成"
|
||||
|
||||
# ---------- 待填占位符汇总 ----------
|
||||
# 决策点: if [ -s FILE ] 检查文件 size > 0 (有内容 = 有 pending 项)。
|
||||
# 不用 [ -f FILE ] 因为 mktemp 创建后已存在, -f 永远 true;
|
||||
# 不用 ${#PENDING[@]} 因为可能有未处理的元素 (虽然我们用文件而非数组)。
|
||||
if [ -s "$PENDING_FILE" ]; then
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠ 本次未填的字段, deploy 前必须补齐 ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
# 表格列宽: 字段名 40 字符 (足够 longest = secrets.ai.minimaxApiKey)
|
||||
printf " %-40s %s\n" "字段" "说明"
|
||||
printf " %-40s %s\n" "────" "────"
|
||||
while IFS='|' read -r key desc nature; do
|
||||
# IFS='|' 覆盖全局 IFS 为本条 read, 不会污染后续 read
|
||||
if [ "$nature" = "必填" ]; then
|
||||
marker="⛔ 必填"
|
||||
else
|
||||
marker="🟡 可选"
|
||||
fi
|
||||
printf " ${YELLOW}%-40s${NC} %s · %s\n" "$key" "$marker" "$desc"
|
||||
# 重定向 < "$PENDING_FILE" 让 read 从文件读, 而不是 stdin
|
||||
done < "$PENDING_FILE"
|
||||
echo ""
|
||||
print_warn "占位符 $PLACEHOLDER 已经写在文件里"
|
||||
print_warn "补齐方式: \$EDITOR k8s/helm/topfans/values-prod.yaml"
|
||||
print_warn "搜索: grep -n $PLACEHOLDER k8s/helm/topfans/values-prod.yaml"
|
||||
print_warn "替换后通过 helm template dry-run 验证:"
|
||||
print_warn " helm template topfans k8s/helm/topfans/ -f k8s/helm/topfans/values-prod.yaml --namespace topfans | grep $PLACEHOLDER"
|
||||
print_warn " 没有输出 = 全部替换完"
|
||||
# helm template + grep 是部署前的**硬性** sanity check:
|
||||
# 1. 占位符没替换会原样输出, grep 应该能命中
|
||||
# 2. 没命中 = 全部填好了, 可以 install
|
||||
else
|
||||
echo ""
|
||||
print_ok "所有必填字段都已填, 可以直接 helm install"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "用法:"
|
||||
echo " ./k8s/deploy.sh upgrade $IMAGE_TAG # 升级 image tag = $IMAGE_TAG"
|
||||
echo " ./k8s/deploy.sh upgrade --reuse-values # 升级, 复用当前 values"
|
||||
echo ""
|
||||
echo "验证 (不进真值到 git):"
|
||||
echo " cd $REPO_ROOT && git status k8s/helm/topfans/values-prod.yaml"
|
||||
# `git status` 显示为 ignored 才算稳。若显示为 untracked 那就是 .gitignore 出问题了,
|
||||
# 这次 helm install 千万**别**在生产路径上跑 (应立刻看 .gitignore 是否漏配)。
|
||||
Loading…
Reference in New Issue
Block a user