diff --git a/contrib/roncarve/.env.example b/contrib/roncarve/.env.example index 4bdfab6..0176c61 100644 --- a/contrib/roncarve/.env.example +++ b/contrib/roncarve/.env.example @@ -1,2 +1,4 @@ -# Copy this file to .env and replace the placeholder with the SMTP password. +# Copy this file to .env and replace all placeholders. GITEA__mailer__PASSWD=replace-with-tencent-ses-smtp-password +GITA_DB_PASSWORD=replace-with-random-database-password +MARIADB_ROOT_PASSWORD=replace-with-random-root-password diff --git a/contrib/roncarve/DEVOPS-RKE2-TEST.md b/contrib/roncarve/DEVOPS-RKE2-TEST.md new file mode 100644 index 0000000..907eca4 --- /dev/null +++ b/contrib/roncarve/DEVOPS-RKE2-TEST.md @@ -0,0 +1,570 @@ +# Gita Actions 与 RKE2 测试环境 DevOps 方案 + +> 文档日期:2026-08-17 +> +> 目标:代码通过合并请求进入 `master` 分支后,自动完成测试、镜像构建、镜像推送和 RKE2 测试环境发布。 + +## 1. 结论 + +推荐采用以下流水线: + +```text +功能分支 -> 合并请求检查 -> 合并到 master -> Gita Actions + -> 构建并推送不可变镜像 -> Helm 原子升级 RKE2 + -> 健康检查/冒烟测试 -> 成功或自动回滚 +``` + +触发器应使用 `push` 到 `master`,同时通过分支保护禁止直接推送 `master`。这样“`master` 收到 push”在管理流程上就等同于“合并请求已合并”。 + +第一阶段采用 Gita Actions 直接执行 Helm 发布;后续若引入 Argo CD,可保留构建阶段,仅将部署阶段替换为更新环境仓库,由 Argo CD 拉取并同步。 + +### 1.1 2026-08-16 至 2026-08-17 实施结果 + +| 项目 | 已实施配置 | +| --- | --- | +| 触发规则 | `.gitea/workflows/deploy-test.yaml` 只在 `master` push(由合并产生)或人工调度时执行 | +| 目标仓库提交 | `AItrackwalker/AItrackwalker` 的 `develop` 分支;`d4b27d8` 增加流水线,`a924528` 修正失败镜像保留,`d9b95d7` 改为从 Gita 内部检出,`be11e4e` 启用完整构建缓存清理,`fef3e00` 增加发布后验证失败回滚和回滚完成前镜像保护,`4af2d6d` 精确保留当前及上一部署版本,`64705fb` 在推送后释放 Runner 本地镜像并在部署前清空构建缓存,`a69fc7b` 修复前端在 RKE2 中错误使用 Docker DNS,`c64f3cf` 修复滚动发布后旧页面动态模块失效的问题 | +| 分支保护 | `master` 禁止直接 push、禁止 force push、至少 1 人批准;拒绝评审、分支落后或批准后代码变化都会阻止合并 | +| Runner | Gita Runner `v0.6.1`,仓库级注册,名称 `rke2-test-runner`,标签 `trackwalker-build`、`rke2-test-deploy`;Runner 官方镜像固定到 SHA-256 digest,定制任务镜像使用日期版本标签且基础镜像固定 digest | +| Runner 隔离 | Rootless Docker-in-Docker,不挂载宿主机 Docker Socket;经批准启用 `privileged`,限制 4 CPU、6 GiB、并发 1 | +| Registry | 7 个业务镜像推送到 `git.roncarve.com/aitrackwalker/*`,使用 `master-<12位SHA>` 不可变标签;测试节点将该域名解析到 Docker 网关 `172.19.0.1`,避免同机镜像拉取绕行公网 | +| 镜像保留 | 部署前读取当前集群镜像标签;成功后逐个镜像包精确保留新标签和上一部署标签,其余版本删除;镜像推送后移除 Runner 本地标签,部署前及任务结束时清理构建缓存和悬空镜像 | +| Kubernetes 凭据 | ServiceAccount `gita-deployer`,仅授权 `trackwalker` 命名空间;已确认不可读取 Node;同机 Runner 通过 Docker 网关 `172.19.0.1:6443` 连接,并使用 `tls-server-name: 62.234.49.39` 校验证书 | +| 拉取凭据 | `trackwalker/gita-registry` Secret,并挂载到该命名空间默认 ServiceAccount;使用独立 `read:package` 令牌 | +| Gita 凭据 | 专用账号 `trackwalker-ci`;Actions 写令牌为 `write:package,read:repository`,与集群只读拉取令牌分离;仓库 Secrets 为 `REGISTRY_USERNAME`、`REGISTRY_PASSWORD`、`RKE2_TEST_KUBECONFIG_B64` | +| 验证记录 | `#1` 发现外部 checkout 超时;`#2` 验证内部检出并发现任务镜像缺少 Helm;`#3` 验证定制工具镜像并发现 Registry DNS 指向错误;`#4` 完成 7 镜像构建与推送并发现同机公网 RKE2 API 不可达;`#5` 完成 Helm revision 5 和全部 rollout,发现网关上游端口错误及发布后失败镜像不应提前删除;`#6` 完成首轮 7 镜像冷构建和 Helm revision 6;`#7` 完成前端 Kubernetes DNS 修复后的 7 镜像构建、Helm revision 7、全部 rollout、HTTPS 与认证配置接口验证;`#8` 完成旧动态模块恢复修复、Helm revision 8、全部 rollout、缓存策略和模块加载验证,状态为成功 | + +### 1.2 最终验收快照 + +2026-08-17 已在当前测试机完成实际 Runner 构建部署验收: + +| 验收项 | 结果 | +| --- | --- | +| Actions | Run `#8`,提交 `c64f3cf8f35b`,`workflow_dispatch`,状态成功 | +| Helm | `trackwalker` revision 8 为 `deployed`,revision 7 为 `superseded` | +| 工作负载 | 8 个 Deployment、5 个 StatefulSet 全部 Ready;7 个业务 Deployment 使用 `master-c64f3cf8f35b` | +| HTTPS | `https://aitrackwalker.roncarve.com/` 返回 HTTP 200 | +| 认证接口 | `GET /api/v1/auth/config` 返回 HTTP 200,认证模式为 `LOCAL`,要求登录;前端日志不再出现 `backend could not be resolved` | +| 动态模块 | 已失效的 `CommandCenterView-B7Qoj-nd.js` 返回 HTTP 404 而不是首页 HTML;当前模块返回 HTTP 200;入口页与 Service Worker 禁止缓存,带哈希资源使用 immutable 缓存,前端监听 `vite:preloadError` 并单次自动刷新 | +| 镜像版本 | 7 个业务镜像包均且仅保留 `master-c64f3cf8f35b` 和 `master-a69fc7bdbb79` | +| 回滚镜像 | revision 7 使用的 `master-a69fc7bdbb79` 完整保留,可作为上一版本回滚目标 | +| 权限边界 | `gita-deployer` 可以更新 `trackwalker` Deployment,但不能读取 Node | +| Runner 清理 | Build Cache 为 0,本地仅保留固定任务镜像及其基础镜像;未挂载宿主 Docker Socket | +| 磁盘 | `/dev/vda1` 为 80 GiB;Run `#8` 构建期间最低观测剩余约 7.4 GiB,镜像与构建缓存清理完成后剩余约 12 GiB、使用率 86% | +| 临时凭据 | `codex-*` 临时访问令牌数量为 0 | +| 最新流水线 | `develop` 已推送 `c64f3cf`;镜像推送后删除 Runner 本地标签,进入部署前执行完整 BuildKit 缓存清理,并使用容器系统 DNS 解析后端服务 | + +实现文件: + +- Gita 服务与 Runner:`contrib/roncarve/compose.production.yaml` +- Caddy Registry 网络别名:`contrib/roncarve/gateway/docker-compose.yml` +- 测试环境 HTTPS 到 Traefik 的网关:`contrib/roncarve/gateway/nginx.conf` +- Runner 参数:`contrib/roncarve/runner/config.yaml` +- RKE2 最小权限:`contrib/roncarve/rke2/trackwalker-ci-rbac.yaml` +- 目标仓库工作流:`.gitea/workflows/deploy-test.yaml` +- 目标仓库测试环境值:`.gitea/deploy/test-values.yaml` +- 目标仓库镜像清理:`.gitea/scripts/cleanup-container-versions.sh` + +工作流不依赖 GitHub 上的 `actions/checkout`。源码检出使用 Gita 自动生成的短期任务令牌和内部服务地址,避免外部 GitHub 网络波动阻断发布;令牌不会输出到日志或写入仓库。 + +### 1.3 前端动态模块发布兼容 + +前端采用带内容哈希的动态模块。滚动发布后,已打开的旧页面可能继续请求已被新镜像替换的旧文件名。当前实现同时使用以下保护: + +- Nginx 对 `/assets/` 仅返回真实静态文件,缺失文件返回 HTTP 404,不回退到 `index.html`。 +- `index.html`、SPA 导航响应和 `service-worker.js` 使用 `no-cache, no-store, must-revalidate`;带哈希的 `/assets/` 使用一年 `immutable` 缓存。 +- 前端监听 Vite 的 `vite:preloadError`,动态模块失效时按会话最多自动刷新一次,避免无限刷新。 +- Service Worker 仅缓存成功响应,只有导航请求可使用 HTML 离线回退;JS、CSS 等非导航资源缺失时返回网络错误。 + +该机制从提交 `c64f3cf` 起生效。发布前已经打开且仍在执行旧 JavaScript 的页面需要首次手动强制刷新一次,后续发布由新代码自动恢复。 + +## 2. 当前测试环境现状 + +本节汇总 2026-08-16 初始检查与 2026-08-17 最终验收结果,未在文档中记录任何密码、令牌或私钥。 + +| 项目 | 当前状态 | +| --- | --- | +| 测试机 | `62.234.49.39` / `VM-0-3-tencentos` | +| 操作系统/架构 | TencentOS Server 4 / x86_64 | +| 节点资源 | 8 CPU、15 GiB 内存、无 Swap | +| 磁盘 | `/dev/vda1` 和 XFS 已在线扩容到 80 GiB;Run `#8` 完整构建发布及镜像清理后剩余约 12 GiB、使用率 86% | +| RKE2 | `v1.36.1+rke2r1`,服务为 active | +| 集群拓扑 | 单节点;角色为 control-plane、etcd,节点 Ready | +| 容器运行时 | containerd `2.2.3-k3s1` | +| 网络 | Canal;Pod CIDR `10.42.0.0/16`;Service CIDR `10.43.0.0/16` | +| Ingress | Traefik 3.6.16,默认 IngressClass | +| 测试域名 | `aitrackwalker.roncarve.com`;外部 TLS 由 Caddy 终止,再转发到 Traefik NodePort `30080` | +| 存储 | local-path;所有 PV 为单节点本地 RWO,回收策略为 Delete | +| 应用命名空间 | `trackwalker` | +| 应用工作负载 | 8 个 Deployment、5 个 StatefulSet,检查时全部 Ready | +| 数据服务 | PostgreSQL、Redis、Kafka、MinIO、Prometheus | +| Helm | Release `trackwalker` revision 8 已部署,revision 7 可用于上一版本回滚 | +| 部署脚本 | `/opt/trackwalker/build-context/deploy/scripts/deploy.sh` | +| Helm Chart | `/opt/trackwalker/build-context/deploy/charts/trackwalker` | +| 当前应用版本 | `0.1.0-dev` | +| 当前镜像地址 | 7 个业务 Deployment 已切换到 `git.roncarve.com/aitrackwalker/*:master-c64f3cf8f35b`;第三方 `media-gateway` 仍使用既有外部镜像地址 | +| RKE2 镜像配置 | embedded registry 已开启;没有 `/etc/rancher/rke2/registries.yaml`;`/etc/hosts` 将 `git.roncarve.com` 映射到本机 Caddy 所在 Docker 网关 `172.19.0.1`,TLS 仍校验正式域名证书 | +| Kubernetes 拉取凭据 | 已创建 `trackwalker/gita-registry`,并配置为默认 ServiceAccount 的 imagePullSecret | +| etcd 备份 | 本地约每 12 小时生成一次快照,当前可见最近 5 份 | + +测试节点同时运行 Gita、MariaDB、Caddy、Web 站点等 Docker 容器。第一阶段按公司确认在该节点构建,通过 4 CPU、6 GiB、并发 1、独立 Rootless Docker 数据目录和构建后清理限制资源占用;后续仍建议将 Runner 迁移到独立构建机。 + +## 3. 必须先处理的问题 + +### 3.1 构建 Runner 位置 + +长期推荐新增一台独立构建机运行 Gita Runner。第一阶段已按确认部署在当前测试机: + +- 安装 Docker/BuildKit、Git、kubectl 和 Helm。 +- Runner 仅注册到指定组织或仓库,标签使用 `trackwalker-build`。 +- 限制并发为 1,设置 CPU、内存和磁盘清理策略。 +- 构建机只需要访问 Gita、镜像仓库和 RKE2 API `62.234.49.39:6443`。 + +如果第一阶段只能使用当前测试机,应至少: + +- 将 Runner 容器限制为不超过 4 CPU、6 GiB 内存。 +- Actions 并发限制为 1。 +- Runner Docker 数据保存在 `/opt/gita/runner/docker`,与 RKE2 containerd 数据隔离。 +- 定期清理构建缓存,但不得清理 RKE2/containerd 正在使用的镜像。 +- Rootless Docker-in-Docker 需要 `privileged: true`;该权限已经明确批准,且未挂载 `/var/run/docker.sock`。 + +### 3.2 镜像仓库 + +将应用镜像从占位地址 `registry.example.com` 迁移到 Gita OCI 镜像仓库,例如: + +```text +git.roncarve.com/aitrackwalker/frontend +git.roncarve.com/aitrackwalker/backend +git.roncarve.com/aitrackwalker/uav-access-service +git.roncarve.com/aitrackwalker/vision-inference +git.roncarve.com/aitrackwalker/pointcloud-analysis +git.roncarve.com/aitrackwalker/artifact-installer +git.roncarve.com/aitrackwalker/edge-agent +``` + +当前 Registry 与 RKE2 位于同一测试节点。节点 `/etc/hosts` 必须包含: + +```text +172.19.0.1 git.roncarve.com +``` + +`172.19.0.1` 是 `gateway_gateway` Docker 网络的网关,访问 443 端口会进入 Caddy,并继续使用 `git.roncarve.com` 的正式 TLS 证书。若以后重建 Docker 网络并改变网段,应同步更新该映射;可用 `docker network inspect gateway_gateway` 和 `curl https://git.roncarve.com/v2/` 验证,未登录时返回 HTTP 401 即表示 Registry 路由正常。 + +每次构建使用提交 SHA 作为不可变标签,例如: + +```text +git.roncarve.com/aitrackwalker/backend:master-a1b2c3d4e5f6 +``` + +不要仅依赖 `latest` 或 `master` 可变标签。可额外推送 `test-latest` 供人工查看,但 Helm 发布必须使用 SHA 标签或镜像 digest。 + +为 RKE2 创建专用只读镜像账号,并在 `trackwalker` 命名空间创建 `docker-registry` 类型 Secret。Helm Chart 应显式引用该 imagePullSecret;不要把镜像密码写入 values 文件或工作流。 + +### 3.3 分支保护 + +在目标仓库对 `master` 设置: + +- 禁止普通成员直接 push。 +- 只允许通过合并请求进入 `master`。 +- 至少 1 人批准。 +- 必须通过单元测试、静态检查和镜像构建预检。 +- 合并前必须解决冲突和未处理讨论。 +- 禁止 force push 和删除分支。 + +工作流本身无法可靠区分“合并产生的 push”和“直接 push”。强制合并请求流程应由分支保护保证。 + +## 4. Gita 与 Runner 配置 + +### 4.1 启用 Actions + +在 Gita Docker 环境变量中加入: + +```yaml +GITEA__actions__ENABLED: "true" +``` + +已重新创建 Gita 容器并完成 Runner 仓库级注册。注册令牌只保存在服务器只读挂载文件中,不写入业务仓库。 + +### 4.2 Runner 标签与职责 + +当前一台 Runner 同时声明两个标签: + +| Runner 标签 | 职责 | 权限 | +| --- | --- | --- | +| `trackwalker-build` | 测试、构建、推送镜像 | Docker 和镜像仓库写权限;无 Kubernetes 管理权限 | +| `rke2-test-deploy` | Helm 发布、验证和回滚 | 仅允许操作 `trackwalker` 命名空间 | + +当前工作流使用 `trackwalker-build` 运行单一串行任务,部署阶段使用命名空间最小权限 kubeconfig;`rke2-test-deploy` 标签为后续拆分构建和部署任务预留。 + +### 4.3 Gita Secrets + +已在 `AItrackwalker/AItrackwalker` 仓库级配置以下 Secrets: + +| Secret | 用途 | +| --- | --- | +| `REGISTRY_USERNAME` | 镜像仓库机器人账号 | +| `REGISTRY_PASSWORD` | 镜像仓库机器人密码或令牌 | +| `RKE2_TEST_KUBECONFIG_B64` | 仅限 `trackwalker` 命名空间的 kubeconfig,Base64 编码 | + +未保存 root SSH 私钥、RKE2 管理员 kubeconfig `/etc/rancher/rke2/rke2.yaml` 或平台管理员密码。 + +## 5. RKE2 最小权限 + +创建 CI 专用 ServiceAccount,并只授予测试命名空间发布所需权限。以下为基础示例,后续应根据 Helm 实际管理的资源进一步收紧: + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gita-deployer + namespace: trackwalker +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: gita-deployer + namespace: trackwalker +rules: + - apiGroups: [""] + resources: ["configmaps", "secrets", "services", "serviceaccounts", "pods", "pods/log", "persistentvolumeclaims"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: gita-deployer + namespace: trackwalker +subjects: + - kind: ServiceAccount + name: gita-deployer + namespace: trackwalker +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: gita-deployer +``` + +Helm 需要管理 Release Secret,因此 Role 中包含 Secret 写权限。应用业务 Secret 应继续使用 SOPS 或其他密钥管理方式,不由普通构建步骤读取。 + +## 6. 流水线设计 + +### 6.1 合并请求阶段 + +对所有指向 `master` 的合并请求执行: + +1. 格式化、Lint 和单元测试。 +2. 前后端构建检查。 +3. Helm `lint` 和 `template`。 +4. Kubernetes 清单校验。 +5. 镜像构建预检,但不推送测试环境标签、不部署。 +6. 可选生成 SBOM 和漏洞扫描报告。 + +### 6.2 合并后阶段 + +`master` 收到 push 后执行: + +1. 再次检出合并后的完整代码。 +2. 执行必要的快速回归测试。 +3. 构建受影响的应用镜像。 +4. 推送 `master-<12位SHA>` 标签、记录 digest,并立即删除 Runner 本地标签。 +5. 所有镜像推送完成后清空 BuildKit 构建缓存,避免部署阶段磁盘空间不足。 +6. 生成一次性 CI values 文件,不修改服务器上的源码目录。 +7. 执行 `helm upgrade --install --atomic --wait --timeout 20m`。 +8. 检查 Deployment/StatefulSet rollout。 +9. 对 `https://aitrackwalker.roncarve.com` 执行冒烟测试。 +10. rollout 或冒烟测试失败时回滚到升级前 Helm revision;只有回滚成功后才删除失败标签。 +11. 成功后精确保留当前和上一部署标签,并保存构建日志、镜像 digest、Helm revision 和发布结果。 + +`--atomic` 会在 Helm 升级失败时回滚本次 Release,但数据库结构变更仍需遵守向前/向后兼容原则,不能依赖 Helm 自动还原数据。 + +## 7. 工作流示例 + +目标仓库已创建 `.gitea/workflows/deploy-test.yaml`。实际版本按 Runner 并发 1 串行构建 7 个镜像,使用 `.gitea/deploy/test-values.yaml` 覆盖业务镜像仓库,并在部署成功后调用 `.gitea/scripts/cleanup-container-versions.sh` 执行精确保留策略。 + +以下内容用于说明流水线结构;运维时应以目标仓库中的实际文件为准: + +```yaml +name: deploy-test-on-master + +on: + push: + branches: + - master + workflow_dispatch: + +concurrency: + group: trackwalker-test + cancel-in-progress: false + +env: + REGISTRY: git.roncarve.com + IMAGE_PREFIX: git.roncarve.com/aitrackwalker + NAMESPACE: trackwalker + RELEASE_NAME: trackwalker + +jobs: + test: + runs-on: trackwalker-build + steps: + - name: Checkout merged source from Gita + env: + GITA_JOB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git init . + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git -c http.extraHeader="Authorization: token ${GITA_JOB_TOKEN}" \ + fetch --depth=1 origin "${GITHUB_SHA}" + git checkout --detach FETCH_HEAD + + - name: Run project checks + run: ./scripts/ci-test.sh + + - name: Validate Helm chart + run: | + helm lint deploy/charts/trackwalker + helm template trackwalker deploy/charts/trackwalker \ + --namespace trackwalker \ + --values deploy/charts/trackwalker/values.yaml \ + --values deploy/charts/trackwalker/values-single.yaml \ + >/dev/null + + build: + runs-on: trackwalker-build + needs: test + strategy: + fail-fast: true + matrix: + include: + - image: frontend + context: frontend + - image: backend + context: platform/backend + - image: uav-access-service + context: uav-access-service + - image: vision-inference + context: ai-services/vision-inference + - image: pointcloud-analysis + context: ai-services/pointcloud-analysis + - image: artifact-installer + context: ai-services/artifact-installer + - image: edge-agent + context: edge-agent + steps: + - name: Checkout merged source from Gita + env: + GITA_JOB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git init . + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git -c http.extraHeader="Authorization: token ${GITA_JOB_TOKEN}" \ + fetch --depth=1 origin "${GITHUB_SHA}" + git checkout --detach FETCH_HEAD + + - name: Login to registry + run: | + printf '%s' '${{ secrets.REGISTRY_PASSWORD }}' | \ + docker login "${REGISTRY}" \ + --username '${{ secrets.REGISTRY_USERNAME }}' \ + --password-stdin + + - name: Build and push image + shell: bash + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -Eeuo pipefail + short_sha="${COMMIT_SHA:0:12}" + image="${IMAGE_PREFIX}/${{ matrix.image }}:master-${short_sha}" + docker build --pull --tag "${image}" "${{ matrix.context }}" + docker push "${image}" + + deploy: + runs-on: rke2-test-deploy + needs: build + environment: test + steps: + - name: Checkout merged source from Gita + env: + GITA_JOB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git init . + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git -c http.extraHeader="Authorization: token ${GITA_JOB_TOKEN}" \ + fetch --depth=1 origin "${GITHUB_SHA}" + git checkout --detach FETCH_HEAD + + - name: Configure Kubernetes access + shell: bash + run: | + set -Eeuo pipefail + install -d -m 0700 "${RUNNER_TEMP}/kube" + printf '%s' '${{ secrets.RKE2_TEST_KUBECONFIG_B64 }}' | \ + base64 -d >"${RUNNER_TEMP}/kube/config" + chmod 0600 "${RUNNER_TEMP}/kube/config" + echo "KUBECONFIG=${RUNNER_TEMP}/kube/config" >>"${GITHUB_ENV}" + + - name: Deploy with Helm + shell: bash + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -Eeuo pipefail + short_sha="${COMMIT_SHA:0:12}" + tag="master-${short_sha}" + helm upgrade --install "${RELEASE_NAME}" deploy/charts/trackwalker \ + --namespace "${NAMESPACE}" \ + --values deploy/charts/trackwalker/values.yaml \ + --values deploy/charts/trackwalker/values-single.yaml \ + --set global.imageRegistry="${REGISTRY}" \ + --set images.frontend.tag="${tag}" \ + --set images.backend.tag="${tag}" \ + --set images.uavAccess.tag="${tag}" \ + --set images.visionCpu.tag="${tag}" \ + --set images.pointcloudCpu.tag="${tag}" \ + --set images.artifactInstaller.tag="${tag}" \ + --set images.edgeAgent.tag="${tag}" \ + --atomic \ + --wait \ + --timeout 20m + + - name: Verify rollout + shell: bash + run: | + set -Eeuo pipefail + for deployment in \ + frontend backend uav-access-service vision-inference \ + pointcloud-analysis artifact-installer edge-agent; do + kubectl --namespace "${NAMESPACE}" rollout status \ + "deployment/${deployment}" --timeout=180s + done + + - name: Smoke test + run: | + curl --fail --show-error --silent \ + --retry 5 --retry-delay 5 \ + https://aitrackwalker.roncarve.com/ >/dev/null +``` + +示例中的 `./scripts/ci-test.sh` 需要由 TrackWalker 项目统一封装各语言测试命令。实际镜像构建路径应以目标仓库结构为准。 + +## 8. 复用现有部署资产 + +现有 `deploy.sh` 已包含以下能力: + +- 部署锁,避免并行发布。 +- 配置和版本校验。 +- SOPS 解密入口。 +- Helm `--atomic --wait --timeout 20m`。 +- 发布后验证。 +- `--resume-from application` 从应用阶段继续。 + +第一阶段可以在 CI 中复用该脚本: + +```bash +deploy/scripts/deploy.sh \ + --profile single \ + --inventory deploy/inventories/single/hosts.yml \ + --config deploy/config/site.yml \ + --secrets "${RUNNER_TEMP}/secrets.sops.yaml" \ + --bundle-root . \ + --allow-development-bundle \ + --resume-from application +``` + +但长期建议增加专门的 `scripts/ci-deploy-test.sh`,只执行应用升级和验证,避免每次提交都进入操作系统、RKE2 安装和离线包校验流程。 + +## 9. 回滚策略 + +正常回滚: + +```bash +helm --namespace trackwalker history trackwalker +helm --namespace trackwalker rollback trackwalker --wait --timeout 20m +``` + +紧急镜像回滚: + +```bash +kubectl --namespace trackwalker rollout undo deployment/backend +kubectl --namespace trackwalker rollout status deployment/backend --timeout=180s +``` + +要求: + +- 每个业务镜像仅保留最近 2 个成功构建版本:正在使用版本和上一次使用版本。 +- 清理操作只在 Helm 部署、rollout 和冒烟测试全部成功后执行;发布后验证失败时先回滚,只有回滚成功才删除失败标签,回滚失败则保留镜像供人工恢复。 +- 每次发布记录 Git SHA、镜像 digest、Helm revision、执行人和时间。 +- 数据库迁移采用 expand/contract,两次发布完成破坏性变更。 +- 发布前检查 etcd 快照状态;业务数据还需 PostgreSQL、MinIO 等独立备份,不能只依赖 etcd。 + +## 10. 当前环境风险和整改优先级 + +### P0:自动部署前完成 + +1. 已完成系统盘分区和 XFS 在线扩容,容量为 80 GiB。 +2. 已完成业务镜像仓库覆盖;第三方基础组件仍按原配置使用 `registry.example.com`。 +3. 已创建 Gita 镜像仓库机器人账号和 RKE2 imagePullSecret。 +4. 已启用 Gita Actions 并注册仓库级受控 Runner。 +5. 已开启 `master` 分支保护,禁止直接 push 和 force push。 +6. 已创建命名空间级 RBAC,并验证凭据不能读取 Node。 +7. 待独立处理 `/opt/trackwalker/deployment-secrets.yaml` 的明文审计和 SOPS 迁移;本次流水线未复制该文件,也未扩大 Secret 读取范围。 + +### P1:首轮流水线完成后 + +1. 已由 Caddy 为 `aitrackwalker.roncarve.com` 提供 TLS;后续增加证书到期监控。 +2. 增加镜像 SBOM、漏洞扫描和 digest 锁定。 +3. 将 Helm、kubectl 和 Runner 镜像版本固定,禁止使用 `latest`。 +4. Actions 日志和构建缓存策略已启用;继续观察磁盘水位并设置容量告警。 +5. 对 PostgreSQL、MinIO 和 PVC 建立独立备份与恢复演练。 +6. 为 npm、Maven 和 PyPI 建立依赖代理缓存;冷构建时 `pointcloud-analysis` 需要下载约 448 MiB 的 Open3D wheel,是当前最明显的外部网络耗时点。 + +### P2:后续演进 + +1. 增加 RKE2 worker 节点,将业务工作负载迁出 control-plane。 +2. 将 Gita、构建 Runner 与 RKE2 节点分离。 +3. 引入 Argo CD,改为 GitOps 部署。 +4. 引入 Harbor 作为统一镜像仓库,并配置签名、扫描和保留策略。 +5. 引入 Prometheus Alertmanager、Loki/Grafana 和发布事件关联。 + +## 11. 验收标准 + +- 合并请求未合并时不会部署测试环境。 +- 合并进入 `master` 后 1 分钟内创建流水线。 +- 每次部署使用唯一 SHA 标签或 digest。 +- 任一步测试或镜像构建失败时不执行部署。 +- Helm 升级失败时 Release 自动回滚。 +- 成功后所有 Deployment 和 StatefulSet Ready,冒烟测试通过。 +- Runner 日志中不出现密码、Token、kubeconfig 内容。 +- `master` 不能被普通成员直接 push。 +- 可以根据 Git SHA 查到对应镜像和 Helm revision。 +- 可以在 10 分钟内回滚到上一个可用版本。 + +## 12. 推荐实施顺序 + +1. 已扩容测试机磁盘。 +2. 已启用 Gita Actions。 +3. 已部署并注册构建 Runner。 +4. 已配置 Gita OCI 镜像仓库和机器人账号。 +5. 已配置 RKE2 imagePullSecret 与 CI RBAC。 +6. 已修改 Helm Chart,支持业务镜像单独指定私有仓库。 +7. 已增加 `master` 合并后构建部署工作流并提交到 `develop`。 +8. 待项目团队补充合并请求阶段的语言级测试与 Lint 工作流。 +9. 已完成首次完整冷构建部署验证;待安排维护窗口执行一次失败回滚和一次人工回滚演练。 +10. 稳定运行后再评估 Argo CD 和 Harbor。 + +## 13. 参考资料 + +- [Gitea Actions](https://docs.gitea.com/usage/actions/overview) +- [Gitea Runner](https://docs.gitea.com/usage/actions/act-runner) +- [RKE2 私有镜像仓库配置](https://docs.rke2.io/install/private_registry) +- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) +- [Helm Upgrade](https://helm.sh/docs/helm/helm_upgrade/) diff --git a/contrib/roncarve/README.md b/contrib/roncarve/README.md index fd7c69f..2e81524 100644 --- a/contrib/roncarve/README.md +++ b/contrib/roncarve/README.md @@ -1,6 +1,6 @@ # Gita 本地 Docker -复制 `.env.example` 为 `.env`,并填写腾讯云邮件推送 SMTP 密码: +复制 `.env.example` 为 `.env`。本地模式填写腾讯云邮件推送 SMTP 密码;生产模式还需填写 Gita 数据库密码和 MariaDB root 密码: ```powershell Copy-Item contrib/roncarve/.env.example contrib/roncarve/.env @@ -24,4 +24,12 @@ docker compose -f contrib/roncarve/compose.local.yaml up -d --build docker compose -f contrib/roncarve/compose.local.yaml down ``` -生产环境使用 `compose.production.yaml`,通过外部 `gateway_gateway` 网络接入现有 Caddy,数据目录为 `/opt/gita/data`。 +生产环境使用 `compose.production.yaml`,通过外部 `gateway_gateway` 网络接入现有 Caddy。Gita 数据目录为 `/opt/gita/data`,MariaDB 数据目录为 `/opt/gita/mariadb`,Runner 数据目录为 `/opt/gita/runner`。 + +`gateway/docker-compose.yml` 记录 Caddy/Nginx 网关容器结构,`gateway/nginx.conf` 将测试环境 HTTPS 请求转发到 RKE2 Traefik NodePort `30080`。同机 RKE2 拉取 Gita Registry 时,测试节点 `/etc/hosts` 使用 `172.19.0.1 git.roncarve.com`,避免镜像流量绕行公网。 + +Gita Actions、镜像保留策略和 RKE2 测试环境发布配置见 [DEVOPS-RKE2-TEST.md](DEVOPS-RKE2-TEST.md)。 + +Runner 任务镜像由 `runner/job-image.Dockerfile` 构建,基础镜像固定 digest,只增加 Helm 和 kubectl 两个部署工具。构建上下文中的二进制文件应分别来自固定版本的 Helm 镜像和当前 RKE2 安装目录。 + +2026-08-17 已在测试机完成 7 镜像构建和 Helm revision 8 发布,全部工作负载 Ready,HTTPS 冒烟测试、认证配置接口和动态模块加载均验证通过。旧动态模块现在返回 404 而不是首页 HTML,入口页禁止缓存,当前前端可在后续发布出现过期模块时单次自动刷新。镜像仓库按服务仅保留当前与上一构建两个标签;Runner 在每个镜像推送后释放本地标签,并在部署前清空 BuildKit 缓存。 diff --git a/contrib/roncarve/compose.production.yaml b/contrib/roncarve/compose.production.yaml index 2e5261e..56899b5 100644 --- a/contrib/roncarve/compose.production.yaml +++ b/contrib/roncarve/compose.production.yaml @@ -1,4 +1,29 @@ services: + mariadb: + image: mariadb:11.4 + container_name: gita-mariadb + hostname: gita-mariadb + restart: unless-stopped + env_file: + - .env + environment: + MARIADB_DATABASE: gita + MARIADB_USER: gita + MARIADB_PASSWORD: ${GITA_DB_PASSWORD} + MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_bin + volumes: + - /opt/gita/mariadb:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 12 + networks: + gateway: + aliases: + - mariadb + gita: build: context: ../.. @@ -9,10 +34,13 @@ services: NPM_REGISTRY: https://registry.npmmirror.com image: roncarve/gita:1.27.2-production container_name: gitea - hostname: git.roncarve.com + hostname: gitea restart: unless-stopped env_file: - .env + depends_on: + mariadb: + condition: service_healthy ports: - "2222:22" environment: @@ -21,8 +49,13 @@ services: GITEA_WORK_DIR: /data/gita GITEA_CUSTOM: /data/gita GITEA____APP_NAME: Gita 代码协作平台 - GITEA__database__DB_TYPE: sqlite3 - GITEA__database__PATH: /data/gita/gita.db + GITEA__database__DB_TYPE: mysql + GITEA__database__HOST: mariadb:3306 + GITEA__database__NAME: gita + GITEA__database__USER: gita + GITEA__database__PASSWD: ${GITA_DB_PASSWORD} + GITEA__database__CHARSET: utf8mb4 + GITEA__actions__ENABLED: "true" GITEA__server__PROTOCOL: http GITEA__server__HTTP_ADDR: 0.0.0.0 GITEA__server__HTTP_PORT: "3000" @@ -56,6 +89,30 @@ services: aliases: - gitea + runner: + image: docker.io/gitea/act_runner@sha256:6b8f7c4297c0a5c4c181e4737665d4af69288cdc380e2887105a05a2b78930df + container_name: gita-runner + hostname: gita-rke2-test-runner + restart: unless-stopped + privileged: true + cpus: 4 + mem_limit: 6g + depends_on: + - gita + environment: + CONFIG_FILE: /config.yaml + GITEA_INSTANCE_URL: http://gitea:3000/ + GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /run/secrets/runner-registration-token + GITEA_RUNNER_NAME: rke2-test-runner + DOCKER_HOST: unix:///var/run/user/1000/docker.sock + volumes: + - /opt/gita/runner/config.yaml:/config.yaml:ro + - /opt/gita/runner/registration-token:/run/secrets/runner-registration-token:ro + - /opt/gita/runner/data:/data + - /opt/gita/runner/docker:/home/rootless/.local/share/docker + networks: + - gateway + networks: gateway: external: true diff --git a/contrib/roncarve/gateway/Caddyfile b/contrib/roncarve/gateway/Caddyfile new file mode 100644 index 0000000..a9ac172 --- /dev/null +++ b/contrib/roncarve/gateway/Caddyfile @@ -0,0 +1,59 @@ +{ + email admin@roncarve.com +} + +rancher.aitrackwalker.roncarve.com { + encode zstd gzip + reverse_proxy https://host.docker.internal:30443 { + transport http { + tls_insecure_skip_verify + } + } +} + +aitrackwalker.roncarve.com { + encode zstd gzip + reverse_proxy nginx:8080 +} + +http://62.234.49.39 { + encode zstd gzip + reverse_proxy nginx:8080 { + header_up Host aitrackwalker.roncarve.com + } +} + +http://62.234.49.39:8080 { + encode zstd gzip + reverse_proxy nginx:8080 { + header_up Host aitrackwalker.roncarve.com + } +} + +www.roncarve.com { + encode zstd gzip + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + Referrer-Policy "strict-origin-when-cross-origin" + } + reverse_proxy roncarve-web:80 +} + +roncarve.com { + encode zstd gzip + + handle /WW_verify_kMPlH8gCU5u7a4LQ.txt { + root * /srv/wecom-verify + file_server + } + + handle { + redir https://www.roncarve.com{uri} permanent + } +} + +git.roncarve.com { + encode zstd gzip + reverse_proxy gitea:3000 +} diff --git a/contrib/roncarve/gateway/docker-compose.yml b/contrib/roncarve/gateway/docker-compose.yml new file mode 100644 index 0000000..521d1a4 --- /dev/null +++ b/contrib/roncarve/gateway/docker-compose.yml @@ -0,0 +1,48 @@ +services: + caddy: + image: caddy:2.10-alpine + container_name: trackwalker-caddy + restart: unless-stopped + ports: + - "80:80" + - "8080:8080" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ./wecom-verify:/srv/wecom-verify:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - nginx + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + gateway: + aliases: + - git.roncarve.com + + nginx: + image: nginx:1.27-alpine + container_name: trackwalker-nginx + restart: unless-stopped + expose: + - "8080" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + networks: + gateway: + aliases: + - nginx + app: + +volumes: + caddy_data: + caddy_config: + +networks: + gateway: + app: + name: trackwalker-app diff --git a/contrib/roncarve/gateway/nginx.conf b/contrib/roncarve/gateway/nginx.conf new file mode 100644 index 0000000..0d1ac1b --- /dev/null +++ b/contrib/roncarve/gateway/nginx.conf @@ -0,0 +1,39 @@ +events {} + +http { + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream trackwalker_app { + server host.docker.internal:30080; + } + + server { + listen 8080; + server_name _; + client_max_body_size 0; + + location = /gateway-health { + access_log off; + default_type text/plain; + return 200 "gateway ok\n"; + } + + location / { + proxy_pass http://trackwalker_app; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_request_buffering off; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + } +} diff --git a/contrib/roncarve/rke2/trackwalker-ci-rbac.yaml b/contrib/roncarve/rke2/trackwalker-ci-rbac.yaml new file mode 100644 index 0000000..59262da --- /dev/null +++ b/contrib/roncarve/rke2/trackwalker-ci-rbac.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gita-deployer + namespace: trackwalker +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: gita-deployer + namespace: trackwalker +rules: + - apiGroups: [""] + resources: ["configmaps", "secrets", "services", "serviceaccounts"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["pods", "pods/log"] + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: gita-deployer + namespace: trackwalker +subjects: + - kind: ServiceAccount + name: gita-deployer + namespace: trackwalker +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: gita-deployer +--- +apiVersion: v1 +kind: Secret +metadata: + name: gita-deployer-token + namespace: trackwalker + annotations: + kubernetes.io/service-account.name: gita-deployer +type: kubernetes.io/service-account-token diff --git a/contrib/roncarve/runner/config.yaml b/contrib/roncarve/runner/config.yaml new file mode 100644 index 0000000..2cb143f --- /dev/null +++ b/contrib/roncarve/runner/config.yaml @@ -0,0 +1,36 @@ +log: + level: info + +runner: + file: /data/.runner + capacity: 1 + timeout: 2h + shutdown_timeout: 5m + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + fetch_interval_max: 5s + labels: + - "trackwalker-build:docker://roncarve/trackwalker-runner-tools:2026.08.17" + - "rke2-test-deploy:docker://roncarve/trackwalker-runner-tools:2026.08.17" + +cache: + enabled: false + +container: + network: bridge + privileged: false + options: "--cpus=3 --memory=4g --pids-limit=1024" + valid_volumes: [] + docker_host: "" + force_pull: false + force_rebuild: false + require_docker: true + docker_timeout: 30s + bind_workdir: false + +host: + workdir_parent: /data/host-workspace + +metrics: + enabled: false diff --git a/contrib/roncarve/runner/job-image.Dockerfile b/contrib/roncarve/runner/job-image.Dockerfile new file mode 100644 index 0000000..f63323f --- /dev/null +++ b/contrib/roncarve/runner/job-image.Dockerfile @@ -0,0 +1,4 @@ +FROM docker.gitea.com/runner-images:ubuntu-24.04@sha256:e77e2b1ebba51adb1c59d8eb185bc54e397b7e22442756aa7ea0e7b841fd2906 + +COPY --chmod=0755 helm /usr/local/bin/helm +COPY --chmod=0755 kubectl /usr/local/bin/kubectl