Files
geo/deploy/k3s/README.md
T
root 316e2a34aa
Deployment Config CI / Deployment Config (push) Successful in 14s
Frontend CI / Frontend (push) Successful in 1m57s
Backend CI / Backend (push) Successful in 41m0s
Revert "feat(workflows): update registry host to 192.168.100.49 across multiple CI/CD files"
This reverts commit 855e061244.
2026-06-02 23:25:19 +08:00

233 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# geo-rankly k3s deployment
这套清单用于把当前 Docker Compose 拓扑搬到单节点或小集群 k3s 中。默认包含:
- infra: `postgres`, `monitoring-postgres`, `pgbouncer`, `monitoring-pgbouncer`, `rabbitmq`, `redis`, `qdrant`, `minio`
- jobs: `migrate`, `minio-init`
- apps: `tenant-api`, `browser-fetch`, `worker-generate`, `kol-assist-worker`, `scheduler`, `ops-api`, `frontend`, `ops-web`
- ingress: `saas.shengxintui.com` -> `frontend`, `ops.shengxintui.com` -> `ops-web`, `api.shengxintui.com` -> `tenant-api`
对象存储部署跟随 `deploy/config.yaml` / `deploy/config.yml` 里的 `object_storage.provider`,并允许 `deploy/config.local.yaml` / `deploy/config.local.yml` 覆盖:
- `provider: minio``provider: mino`:部署 MinIO 和 `minio-init`
- `provider: aliyun` / `aliyun_oss` / `aliyun-oss` / `oss`:使用 `deploy/k3s-aliyun-oss` overlay,不部署 MinIO 和 `minio-init`
- `provider: r2` / `s3` / `custom_s3`:复用 S3 兼容客户端,不部署自建 MinIOCloudflare R2 建议配置 `region: auto``force_path_style: true`
## 1. 准备镜像
在线环境建议推到镜像仓库后修改 `kustomization.yaml` 里的 `images`
```bash
kubectl kustomize deploy/k3s
```
离线或内网 k3s 可以先用现有打包脚本生成镜像包,再导入到 k3s containerd
```bash
bash deploy/scripts/package.sh latest linux/amd64
tar xzf geo-rankly-deploy-*-amd64.tar.gz
cd geo-rankly-deploy-*-amd64
sudo k3s ctr images import images.tar
```
如果你的服务器是 ARM,把第二个参数换成 `linux/arm64`
## 2. 修改密钥
上线前先改 `deploy/k3s/secrets.yaml`
- `JWT_SECRET`
- `OPS_JWT_SECRET`
- `OPS_DEFAULT_ADMIN_PASSWORD`
- `SCHEDULER_INTERNAL_METRICS_TOKEN`
- `BROWSER_FETCH_TOKEN`
- `POSTGRES_PASSWORD` / `RABBITMQ_DEFAULT_PASS` / `MINIO_ROOT_*`
- `config.local.yaml` 里的 `auth.password_cipher.private_key_pem`
- `ops-config.local.yaml` 里的 `auth.password_cipher.private_key_pem`
- `config/config.yaml` 里的 LLM / SiliconFlow API key
- `config.local.yaml` 里对应的数据库、RabbitMQ、MinIO、JWT 配置
注意:应用运行时通过 `pgbouncer` / `monitoring-pgbouncer` 访问数据库;`migrate` 任务仍然直连 `postgres` / `monitoring-postgres`。数据库密码统一来自 `POSTGRES_PASSWORD` secret,应用配置和 PgBouncer 都会跟随这个 secret。
登录密码加密使用 RSA-OAEP-SHA256。多 Pod 部署时,同一类服务的所有副本必须读取同一把私钥和同一个 `key_id`,否则某个 Pod 下发的公钥对应密文可能被另一个 Pod 解不开。建议租户端和 ops 端分别生成不同私钥:
```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out login-password-rsa.pem
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ops-login-password-rsa.pem
```
然后把完整 PEM 分别粘贴到 `deploy/k3s/secrets.yaml``config.local.yaml``ops-config.local.yaml`
## 3. 部署
```bash
kubectl apply -k deploy/k3s
kubectl -n geo-rankly get pods -w
```
如果使用阿里云 OSS
```bash
kubectl apply -k deploy/k3s-aliyun-oss
kubectl -n geo-rankly get pods -w
```
离线包里的 `bash deploy.sh k3s` 会自动读取根目录 `config.yaml/config.yml``config.local.yaml/config.local.yml`,并选择 `k3s``k3s-aliyun-oss`
查看一次性任务:
```bash
kubectl -n geo-rankly logs job/migrate
kubectl -n geo-rankly logs job/minio-init
```
部署包含 `CREATE INDEX CONCURRENTLY` 的迁移前,先在 staging 用与生产完全一致的 migrate 镜像 tag 跑一次完整迁移,确认 golang-migrate 版本和驱动行为一致;发布清单里不要长期沿用 `geo-rankly/migrate:latest`,应在发版时把 `deploy/k3s/kustomization.yaml``images[].newTag` 固定到本次构建 tag。
迁移完成后,建议检查并发创建的索引是否有效,避免 `CREATE INDEX CONCURRENTLY` 被中断后留下 `INVALID` 索引:
```bash
kubectl -n geo-rankly exec statefulset/postgres -- sh -lc \
'PGPASSWORD="$POSTGRES_PASSWORD" psql -U geo -d geo -c "SELECT indexrelid::regclass AS index_name, indisvalid FROM pg_index WHERE indexrelid::regclass::text LIKE '\''uq_desktop_tasks_publish_dedup%'\'' OR indexrelid::regclass::text LIKE '\''idx_publish_records_article_account_active%'\'' OR indexrelid::regclass::text LIKE '\''idx_desktop_tasks_publish_record_payload%'\'';"'
```
所有结果的 `indisvalid` 都应为 `t`。若出现 `f`,先 `DROP INDEX CONCURRENTLY IF EXISTS <index_name>`,再重新执行迁移。
升级包含 SMZDM(什么值得买)封面必填逻辑时,建议先确认没有已排队或执行中的 SMZDM 发布任务缺少封面,否则这些旧任务会在新版桌面端回写 `publish_cover_required` 失败:
```bash
kubectl -n geo-rankly exec statefulset/postgres -- sh -lc \
'PGPASSWORD="$POSTGRES_PASSWORD" psql -U geo -d geo -c "SELECT dt.desktop_id, dt.status, dt.created_at FROM desktop_tasks dt JOIN publish_records pr ON pr.id = CASE WHEN (dt.payload->>'\''publish_record_id'\'') ~ '\''^[0-9]+$'\'' THEN (dt.payload->>'\''publish_record_id'\'')::bigint ELSE NULL END JOIN publish_batches pb ON pb.id = pr.publish_batch_id WHERE dt.kind = '\''publish'\'' AND dt.platform_id = '\''smzdm'\'' AND dt.status IN ('\''queued'\'', '\''in_progress'\'') AND dt.payload ? '\''publish_record_id'\'' AND COALESCE(NULLIF(pb.cover_asset_url, '\'''\''), NULLIF(dt.payload#>> '\''{content_ref,cover_asset_url}'\'', '\'''\'')) IS NULL ORDER BY dt.created_at DESC;"'
```
确认这些旧任务可废弃后,可先中止仍在排队的任务,避免升级后集中失败;`in_progress` 任务需等待租约结束或由客户端回写:
```bash
kubectl -n geo-rankly exec statefulset/postgres -- sh -lc \
'PGPASSWORD="$POSTGRES_PASSWORD" psql -U geo -d geo -c "WITH stale AS (SELECT dt.desktop_id FROM desktop_tasks dt JOIN publish_records pr ON pr.id = CASE WHEN (dt.payload->>'\''publish_record_id'\'') ~ '\''^[0-9]+$'\'' THEN (dt.payload->>'\''publish_record_id'\'')::bigint ELSE NULL END JOIN publish_batches pb ON pb.id = pr.publish_batch_id WHERE dt.kind = '\''publish'\'' AND dt.platform_id = '\''smzdm'\'' AND dt.status = '\''queued'\'' AND dt.payload ? '\''publish_record_id'\'' AND COALESCE(NULLIF(pb.cover_asset_url, '\'''\''), NULLIF(dt.payload#>> '\''{content_ref,cover_asset_url}'\'', '\'''\'')) IS NULL) UPDATE desktop_tasks dt SET status = '\''aborted'\'', error = jsonb_build_object('\''reason'\'', '\''smzdm_cover_required_upgrade_abort'\'', '\''source'\'', '\''ops_upgrade'\''), active_attempt_id = NULL, lease_token_hash = NULL, lease_expires_at = NULL, updated_at = NOW() FROM stale WHERE dt.desktop_id = stale.desktop_id RETURNING dt.desktop_id;"'
```
如果是升级后需要重新跑迁移:
```bash
kubectl -n geo-rankly delete job migrate --ignore-not-found
kubectl apply -k deploy/k3s
```
### 3.1 可选安装 Kite dashboard
Kite dashboard 用于管理 k3s/Kubernetes 资源,默认安装到 `kube-system`,并通过 NodePort 暴露:
```bash
kubectl apply -f deploy/k3s/addons/kite.yaml
```
访问:
- Kite dashboard: `http://<node-ip>:30082`
- NAS 示例:`http://192.168.100.19:30082`
注意:Kite 官方清单绑定了 `cluster-admin` 权限,只建议在可信内网使用,不要直接暴露到公网。
## 4. 访问
k3s 默认同时支持两种访问方式。
### 4.1 直接用节点 IP + 端口
`frontend``ops-web``tenant-api` 已固定为 NodePort
- tenant admin: `http://<node-ip>:30080`
- ops admin: `http://<node-ip>:30081`
- tenant api: `http://<node-ip>:30083`
这些 NodePort Service 使用 `externalTrafficPolicy: Local`,用于保留客户端源 IP,后端日志和登录审计会读取规范化后的 `X-Forwarded-For` / `X-Real-IP`
如果通过 k3s 内置 Traefik Ingress 对外提供服务,也需要把 Traefik Service 调整为保留源地址:
```bash
kubectl -n kube-system patch svc traefik -p '{"spec":{"externalTrafficPolicy":"Local"}}'
```
临时用 frpc/frps 做 TCP 隧道时,用户源 IP 通常不会穿透到 k3s;这类链路需要 proxy protocol 或 HTTP 反代写入可信 `X-Forwarded-For`,否则日志只能记录隧道出口/NAS 本地转发地址。
例如 NAS IP 是 `192.168.100.19`
- tenant admin: `http://192.168.100.19:30080`
- ops admin: `http://192.168.100.19:30081`
- tenant api: `http://192.168.100.19:30083`
### 4.2 用域名 Ingress + HTTPS
k3s 默认 Traefik 会接管 Ingress。先把域名指到 k3s 节点公网 IP。如果 `www.shengxintui.com` 已经解析到了这个节点,可以把 `saas``ops``api` 做 CNAME 指向 `www.shengxintui.com`
```text
saas.shengxintui.com CNAME www.shengxintui.com
ops.shengxintui.com CNAME www.shengxintui.com
api.shengxintui.com CNAME www.shengxintui.com
```
也可以直接添加 A 记录:
```text
saas.shengxintui.com A <node-ip>
ops.shengxintui.com A <node-ip>
api.shengxintui.com A <node-ip>
```
HTTPS 使用 cert-manager + Let's Encrypt 自动签发和续期证书。签发前需要保证公网可以访问 k3s 节点的 80/443 端口;证书签发成功后,cert-manager 会在过期前自动续期。
先安装 cert-manager
```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.2/cert-manager.yaml
kubectl -n cert-manager get pods -w
```
确认 cert-manager Pod 正常后,安装 Let's Encrypt ClusterIssuer
```bash
kubectl apply -f deploy/k3s/addons/letsencrypt-clusterissuer.yaml
```
然后部署或更新业务清单:
```bash
kubectl apply -k deploy/k3s
```
访问:
- tenant admin: `https://saas.shengxintui.com`
- ops admin: `https://ops.shengxintui.com`
- tenant api: `https://api.shengxintui.com`
检查证书状态:
```bash
kubectl -n geo-rankly get certificate,challenge,order
kubectl -n geo-rankly describe certificate shengxintui-com-tls
```
临时调试时也可用端口转发:
```bash
kubectl -n geo-rankly port-forward svc/frontend 8088:80
kubectl -n geo-rankly port-forward svc/ops-web 8089:80
kubectl -n geo-rankly port-forward svc/rabbitmq 15672:15672
kubectl -n geo-rankly port-forward svc/minio 9001:9001
```
## 5. 常用排查
```bash
kubectl -n geo-rankly get all
kubectl -n geo-rankly describe pod <pod-name>
kubectl -n geo-rankly logs deploy/tenant-api -f
kubectl -n geo-rankly logs deploy/browser-fetch -f
kubectl -n geo-rankly logs deploy/scheduler -f
kubectl -n geo-rankly logs deploy/worker-generate -f
kubectl -n geo-rankly logs deploy/ops-api -f
```
如果应用 Pod 先于 `migrate` 启动失败,通常会自动重启;等 `migrate` 完成后再看 `kubectl -n geo-rankly get pods` 即可。