- JavaScript 100%
- apps/ 改成 kustomize,镜像 tag 统一在 kustomization.yaml 的 images 段, CI 用 hack/set-image-tag.mjs 改一行就是一次发布 - 去掉 imagePullPolicy: Always——tag 不可变之后 Argo CD 自己能发现更新 - apps/easytier.yaml:EasyTier 公共共享节点,klipper 直接绑宿主机 11010 - player-api 补 INVOICE_PROVIDER / NATS_URL / EASYTIER_PEERS,admin-api 补 NATS_URL - runner:任务容器连边车 dind,并把 Forgejo 的公网名指到控制 IP(hairpin) |
||
|---|---|---|
| apps | ||
| argocd | ||
| bootstrap | ||
| hack | ||
| infra | ||
| vendor | ||
| .gitignore | ||
| README.md | ||
部署
测试环境的 Kubernetes 与基础设施。清单都在这个目录下,
可以直接 kubectl apply / helm -f,也可以后续交给 Argo CD 托管。
集群本身怎么装的见 ../../staging-env.md。
现状
单节点 k3s v1.36.4,机器 96 核 / 123 GiB / 900 GB 可用。
| 组件 | 版本 | 位置 | 入口 |
|---|---|---|---|
| Traefik | 3.7.8 | kube-system |
k3s 自带 |
| cert-manager | chart jetstack | cert-manager |
— |
| CloudNativePG operator | chart 0.29.0 | cnpg-system |
— |
| PostgreSQL 18.1 | CNPG Cluster,3 实例 | dainiwan |
pg-rw / pg-ro |
| Valkey 9.0 | StatefulSet | dainiwan |
valkey:6379 |
| NATS + JetStream | chart nats | dainiwan |
nats:4222 |
| Argo CD | chart argo-cd | argocd |
argocd.staging… |
| Forgejo 15.0.7 | chart 17.1.5(OCI) | forgejo |
forgejo.staging… |
| Forgejo Runner 9.0.1 | Deployment + dind | forgejo |
— |
| GlitchTip 6.0.3 | 普通清单 | glitchtip |
glitchtip.staging… |
GitOps
集群由 Argo CD 从 Forgejo 上的这个仓库同步,不要再手工 apply。
https://forgejo.staging.dnw.434512.xyz/dainiwan/deploy
改配置的正确姿势:改这个仓库 → push → Argo CD 自动跟随(实测约 100 秒内生效)。
结构
argocd/root.yaml 是 app-of-apps 的根,手工 apply 一次即可;
之后它管着 argocd/apps/ 下的所有 Application。加一个组件 =
往 argocd/apps/ 丢一个 yaml 再 push。
kubectl apply -f argocd/root.yaml # 只需要做一次
kubectl -n argocd get application # 之后看这里
Helm 类组件用的是多源 Application:chart 从上游拉,values 从本仓库取
($values/infra/<组件>/values.yaml),这样 values 有版本历史,chart 不用 vendor。
两个不归 Argo CD 管的东西
Argo CD 自己。 让它管自己,升级时会「同步到一半把自己重启」,救起来很麻烦。
它由 deploy/argocd/values.yaml + helm 手工维护。
所有 Secret。 口令、令牌、证书都只在集群里,仓库里一个都没有。
新建环境时按下面「装机顺序」里的 kubectl create secret 重新生成。
以后要进 GitOps 的话用 Sealed Secrets 或 External Secrets,别直接提交。
helm 的历史记录还在
cert-manager / cnpg / nats / forgejo 这四个当初是 helm 装的,现在归 Argo CD 管了,
但 helm list -A 里还能看到旧的 release 记录。它们已经不是事实来源。
留着是为了保底:Argo CD 出问题时还能 helm upgrade --install 直接救。
代价是有人可能误以为该用 helm 改配置,然后和 Argo CD 的 selfHeal 打架。
想彻底清干净:
for r in cert-manager:cert-manager cnpg:cnpg-system nats:dainiwan forgejo:forgejo; do
kubectl -n ${r#*:} delete secret -l "owner=helm,name=${r%%:*}"
done
装机顺序
依赖关系决定顺序,跳步会失败。
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml # 服务器上;/etc/profile.d/k3s.sh 已经写好
# 0. 命名空间
kubectl apply -f deploy/bootstrap/namespaces.yaml
# 1. helm 仓库
helm repo add jetstack https://charts.jetstack.io
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo add nats https://nats-io.github.io/k8s/helm/charts
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
# 2. cert-manager
helm upgrade --install cert-manager jetstack/cert-manager -n cert-manager --create-namespace \
-f deploy/infra/cert-manager/values.yaml --wait
kubectl apply -f deploy/infra/cert-manager/clusterissuer.yaml
# 3. CNPG operator(chart 要从本机传,见下面「GitHub 下载」)
helm upgrade --install cnpg /root/charts/cloudnative-pg-0.29.0.tgz -n cnpg-system --create-namespace \
-f deploy/infra/cnpg/values.yaml --wait
# 4. 数据库口令(随机生成,只落在 Secret 里)
kubectl -n dainiwan create secret generic pg-app-credentials \
--from-literal=username=dainiwan \
--from-literal=password="$(openssl rand -base64 24 | tr -d /=+ | cut -c1-28)"
for app in forgejo glitchtip; do
kubectl -n dainiwan create secret generic ${app}-db --type=kubernetes.io/basic-auth \
--from-literal=username=$app \
--from-literal=password="$(openssl rand -base64 24 | tr -d /=+ | cut -c1-28)"
done
# 5. Postgres 集群与附加库
kubectl apply -f deploy/infra/postgres/cluster.yaml
kubectl apply -f deploy/infra/postgres/databases.yaml
kubectl -n dainiwan wait --for=condition=Ready cluster/pg --timeout=10m
# 6. Valkey 与 NATS
kubectl apply -f deploy/infra/valkey/valkey.yaml
helm upgrade --install nats nats/nats -n dainiwan -f deploy/infra/nats/values.yaml --wait
# 7. Argo CD
helm upgrade --install argocd argo/argo-cd -n argocd --create-namespace \
-f deploy/argocd/values.yaml --wait
# 8. Forgejo(chart 是 OCI 制品,不在传统 helm 仓库里)
kubectl -n forgejo create secret generic forgejo-admin \
--from-literal=username=dnwadmin \
--from-literal=password="$(openssl rand -base64 24 | tr -d /=+ | cut -c1-24)" \
--from-literal=email=support@joelclub.com
kubectl -n dainiwan get secret forgejo-db -o yaml \
| sed 's/namespace: dainiwan/namespace: forgejo/' | kubectl apply -f -
helm upgrade --install forgejo oci://code.forgejo.org/forgejo-helm/forgejo --version 17.1.5 \
-n forgejo -f deploy/infra/forgejo/values.yaml
# 9. Forgejo Runner
POD=$(kubectl -n forgejo get pod -l app.kubernetes.io/name=forgejo -o name | head -1)
TOKEN=$(kubectl -n forgejo exec $POD -c forgejo -- forgejo forgejo-cli actions generate-runner-token | tail -1 | tr -d '\r\n')
kubectl -n forgejo create secret generic forgejo-runner-token --from-literal=token="$TOKEN"
kubectl apply -f deploy/infra/forgejo/runner.yaml
# 10. GlitchTip
PW=$(kubectl -n dainiwan get secret glitchtip-db -o jsonpath='{.data.password}' | base64 -d)
kubectl -n glitchtip create secret generic glitchtip-secrets \
--from-literal=SECRET_KEY="$(openssl rand -base64 48 | tr -d /=+ | cut -c1-50)" \
--from-literal=DATABASE_URL="postgres://glitchtip:${PW}@pg-rw.dainiwan.svc.cluster.local:5432/glitchtip" \
--from-literal=REDIS_URL="redis://valkey.dainiwan.svc.cluster.local:6379/3"
kubectl apply -f deploy/infra/glitchtip/env.yaml -f deploy/infra/glitchtip/glitchtip.yaml
装的过程中踩到的坑
helm 找不到集群。 k3s 的 kubectl 会自动读 /etc/rancher/k3s/k3s.yaml,helm 不会。
已经写了 /etc/profile.d/k3s.sh 导出 KUBECONFIG,非交互式 ssh 里要自己 export。
GitHub release 资产下不动。 CNPG 的 chart 托管在 GitHub releases 上,
服务器上连拉三次都是 unexpected EOF(普通 GitHub 页面是通的,大文件不行)。
绕法是本机下好再传:
curl -sLO https://github.com/cloudnative-pg/charts/releases/download/cloudnative-pg-v0.29.0/cloudnative-pg-0.29.0.tgz
scp cloudnative-pg-0.29.0.tgz root@110.42.57.249:/root/charts/
镜像和普通 chart 仓库都没问题——只有 GitHub 的 release 资产要这么绕。
CNPG 的角色不能用独立的 DatabaseRole CR。 operator 只给 Cluster spec 里引用到的
Secret 授权,独立 CR 引用的 Secret 会因为 RBAC 读不到而一直
secrets "forgejo-db" is forbidden。角色要写在 Cluster 的 managed.roles 里。
Forgejo 的 chart 不在 helm 仓库里。 code.forgejo.org/api/packages/forgejo-helm/helm
的 index 返回 200 但里面是空的,helm search 找不到任何 chart。
它是 OCI 制品:oci://code.forgejo.org/forgejo-helm/forgejo。
Forgejo 的 additionalConfigFromEnvs 必须放在 gitea: 段下面。
放顶层不报错也不生效,表现是数据库口令为空、init 容器反复
password authentication failed。
GlitchTip 的 Helm chart 拉不到。 官方 chart 在 GitLab 包仓库,匿名访问 401。 改成普通清单部署,顺便复用集群里的 Postgres 和 Valkey,少维护一套库。
访问方式
*.staging.dnw.434512.xyz 已经配了泛解析,指向 114.66.55.190。
三个后台都是 Let's Encrypt 正式证书(cert-manager HTTP-01 自动签发续期)。
export KUBECONFIG=~/.kube/dainiwan-staging.yaml # 开发机上
kubectl get pods -A
| 服务 | 地址 | 账号 |
|---|---|---|
| Forgejo | https://forgejo.staging.dnw.434512.xyz | dnwadmin |
| Argo CD | https://argocd.staging.dnw.434512.xyz | admin |
| GlitchTip | https://glitchtip.staging.dnw.434512.xyz | 首次进去创建 |
口令都在集群的 Secret 里,不写进仓库:
kubectl -n forgejo get secret forgejo-admin -o jsonpath='{.data.password}' | base64 -d
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d
kubectl -n dainiwan get secret pg-app-credentials -o jsonpath='{.data.password}' | base64 -d
Argo CD 的初始口令用完记得删掉那个 Secret。
集群内连接串
业务服务部署进来之后用这些地址:
DATABASE_URL=postgres://dainiwan:<pw>@pg-rw.dainiwan.svc.cluster.local:5432/dainiwan
VALKEY_ADDR=valkey.dainiwan.svc.cluster.local:6379
NATS_URL=nats://nats.dainiwan.svc.cluster.local:4222
pg-rw 永远指向当前主库,主从切换时不用改配置;只读查询可以走 pg-ro。
业务服务
四个应用都在 apps/ 下,由 Argo CD 的 business Application 管。
| 服务 | 镜像 | 入口 |
|---|---|---|
| admin-api | dainiwan/admin-api:dev |
admin.staging…/api |
| player-api | dainiwan/player-api:dev |
api.staging… |
| website | dainiwan/website:dev |
www.staging… |
| admin-web | dainiwan/admin-web:dev |
admin.staging… |
管理站点的 /api 由 Traefik 直接转给 admin-api,和静态站点同源,
前端不需要跨域配置,nginx 里也不用再代理一层。
构建镜像
镜像目前在服务器上手工构建(Forgejo Actions 的流水线还没写):
ssh root@110.42.57.249
cd /root/src/backend && git pull # 或 /root/src/frontend
REG=forgejo.staging.dnw.434512.xyz/dainiwan
docker build -f services/player-api/Dockerfile -t $REG/player-api:dev . && docker push $REG/player-api:dev
官网要在构建期连 API(预渲染活动详情页),多两个参数:
cd /root/src/frontend
docker build --add-host api.staging.dnw.434512.xyz:110.42.57.249 \
--build-arg NUXT_PUBLIC_API_BASE=https://api.staging.dnw.434512.xyz/api/v1 \
-f app-website/Dockerfile -t $REG/website:dev .
推完镜像要 kubectl -n dainiwan rollout restart deploy/<名字>:
tag 是可变的 :dev,Argo CD 看不出变化。清单里已经设了
imagePullPolicy: Always,不然节点会一直复用本地旧镜像。
正式发版应该用不可变 tag(git sha),那时 Argo CD 能自己发现更新。
构建环境的三个坑
Go 模块代理。 proxy.golang.org 在这台机器上不通,Dockerfile 里默认
GOPROXY=https://goproxy.cn,direct,海外构建用 --build-arg 覆盖。
基础镜像。 Docker 的 registry-mirrors 只对 docker.io 生效,而且不支持
带路径+鉴权的 Harbor 代理项目。做法是先从代理项目拉下来再打上规范 tag:
M=registry-mirror.rainyun.cn/proxy-docker.io/library
docker pull $M/golang:1.27-alpine && docker tag $M/golang:1.27-alpine golang:1.27-alpine
docker pull registry-mirror.rainyun.cn/proxy-gcr.io/distroless/static-debian12:nonroot \
&& docker tag registry-mirror.rainyun.cn/proxy-gcr.io/distroless/static-debian12:nonroot \
gcr.io/distroless/static-debian12:nonroot
前端镜像不能用 Alpine。 pnpm 12 会按 devEngines.runtime 自己下一个 Node,
那是 glibc 构建的,在 musl 上跑不起来(缺 libatomic.so.1)。用 bookworm-slim。
另外 Node 26 起不再自带 corepack,装 pnpm 要用 npm i -g pnpm@<版本>。
数据库迁移
apps/migrate-job.yaml 是 Argo CD 的 PreSync 钩子,每次同步先跑迁移再滚服务。
注意钩子只在真的发生同步时执行——首次引导或应用已经 Synced 时不会触发,
那种情况下手工跑一次:
sed '/argocd.argoproj.io\/hook/d; s/name: db-migrate/name: db-migrate-once/' \
apps/migrate-job.yaml | kubectl apply -f -
演示数据用同一个 Job 换成 command: ["/seed"]。seed 自带「非 development 不灌」
的保护,测试环境要临时把 APP_ENV 覆盖成 development。
宿主机访问自建域名
这台机器到不了自己的业务 IP 114.66.55.190(雨云的 hairpin 限制),
所以 /etc/hosts 里把三个自建域名指到了控制 IP 110.42.57.249。
docker build 里同理,用 --add-host。
还没做
- 单机模拟多节点,用于故障转移演练。现在三个 Postgres 实例都在同一台机器上,
能演练「主库 Pod 挂掉自动切主」,演练不了节点级故障。
Postgres 的
affinity.enablePodAntiAffinity现在是关的,多节点之后要打开。 - 业务服务本身(admin-api / player-api / 官网 / 管理站点)还没打镜像、没进集群。
- Forgejo Actions 流水线:镜像还是手工构建推送的,没有 CI。
- 不可变镜像 tag:现在是
:dev,改成 git sha 之后 Argo CD 才能自动发现新版本。 - 备份:CNPG 支持 PITR 到对象存储,还没配。
- Secret 管理:现在全靠手工
kubectl create secret,没有版本、没法审计。 上生产前要换成 Sealed Secrets 或 External Secrets。