Files
easyai-ai-gateway/docs/runbooks/production-ci-cd.md
T
chengcheng 91ea0e6a2d
ci / verify (pull_request) Failing after 4m18s
ci: harden production quality gates
2026-07-17 14:23:49 +08:00

149 lines
8.5 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.
# 生产 CI/CD 运行手册
## 信任边界与固定资源
- 源码仓:`https://git.51easyai.com/BCAI/easyai-ai-gateway`
- CI Runner`easyai-gateway-ci-v2-runner.service`
- CI 标签:`easyai-gateway-ci-unprivileged-v2`
- Runner 状态:Docker named volume `easyai-gateway-ci-v2-data` 内的 `/data/.runner`
- 只读工具链:`/opt/easyai-gateway-ci`
- Runner 配置:`/etc/easyai-gateway-ci-v2/config.yaml`
- Runner 运行参数:`/etc/easyai-gateway-ci-v2/runner.env`
- 生产入口:`https://ai.51easyai.com`
- 部署目录:`/root/easyai-ai-gateway-deploy`
源码仓只执行质量 CIPR context 是 `ci / verify (pull_request)``main` context 是 `ci / verify (push)`,版本 Tag 使用独立 workflow/context `release-ci / verify-tag (push)`。三者都执行 Go 格式/vet/test/govulncheck、前端 lint/test/build、依赖审计、Compose 校验、Trivy 仓库扫描和 CI 脚本自测。Job 没有 Docker socket、`sudo` 或部署目录权限,也不构建发布镜像。
生产 CD 由部署仓的 root-owned dispatcher 执行。它在相同源码 SHA 上同时等待 `ci / verify (push)` 与本次新产生的 `release-ci / verify-tag (push)` 成功,然后只运行部署仓中固定的 BuildKit 构建、Trivy 镜像扫描、digest 发布、备份、迁移、健康检查和回滚命令;不得执行 Tag checkout 中的 `pnpm`、测试、shell 脚本或应用程序。
provision 安装 checksum-pinned 的官方静态 ShellCheck `0.11.0` 和 Docker Compose `5.3.1`,不会依赖宿主是否预装这些命令。所有下载都在使用前校验固定 SHA-256,并在精确 Node Job 镜像内实际执行版本检查。
## 首次安装或修复 CI Runner
前置条件:Linux x86_64、可用 Docker Engine、root 权限,以及 Docker 文件系统至少 8 GiB 可用空间。脚本只压缩本项目专属 `easyai-gateway-ci` Buildx 缓存,不运行全局 `docker system prune`,因为该服务器与其他服务共享 Docker 状态。
从 Gitea 仓库设置的 Actions Runner 页面取得一次性仓库注册 Token,在服务器可信的源码 checkout 中执行:
```bash
RUNNER_REGISTRATION_TOKEN='<一次性 Token>' ./scripts/provision-ci-runner.sh
```
已有有效 named volume 时可直接重跑,不再需要 Token:
```bash
./scripts/provision-ci-runner.sh
```
注册 Token 通过交互 stdin 送给短命注册容器,不出现在宿主或容器的命令行参数中;provision 随后立即从环境中清除它,长期 systemd unit 也不包含 Token。不要输出或复制 `/data/.runner` 内容。脚本会停用旧 host Runner,并确保旧 Runner 用户和 v2 遗留用户不属于 `docker`/`sudo` 组。
默认磁盘门禁可用环境变量提高,但生产环境不应降低:
```bash
CI_RUNNER_MIN_FREE_GIB=8 \
CI_RUNNER_MIN_POST_INSTALL_FREE_GIB=4 \
RUNNER_REGISTRATION_TOKEN='<一次性 Token>' \
./scripts/provision-ci-runner.sh
```
## AppArmor 分支
Ubuntu 22.04 默认没有 `kernel.apparmor_restrict_unprivileged_userns` 限制,当前服务器没有 `rootlesskit` profile 是受支持路径。provision 会实测 Docker 是否接受该 profile;不存在时写入:
```text
RUNNER_SECURITY_OPTIONS=""
```
如果新系统的 `/proc/sys/kernel/apparmor_restrict_unprivileged_userns``1`,则必须先由宿主发行版或 Docker 安装包提供并加载 `rootlesskit` profileprovision 只探测和使用,不会自行复制一份 profile。缺失时 provision 会停止;不得手工关闭该 sysctl,也不得使用 `apparmor=unconfined` 绕过。
参考:[Docker Rootless Docker-in-Docker](https://docs.docker.com/engine/security/rootless/tips/#rootless-docker-in-docker)、[Ubuntu 安全特性版本矩阵](https://documentation.ubuntu.com/security/security-features/security-features-overview/)。
## Runner 真实验证
安装脚本会先启动一个无 Token、无持久 Runner 状态挂载、无宿主 bind mount 的短命 rootless DinD probe;随后启动长期服务,确认 Runner 已向 Gitea declare、通过内层 `docker info` 验证 `name=rootless`,再从内层 daemon 拉取固定 digest 的 Job 镜像,并在该镜像中实际运行全部工具。人工复核:
```bash
systemctl is-active easyai-gateway-ci-v2-runner.service
systemctl is-enabled easyai-gateway-ci-v2-runner.service
systemctl cat easyai-gateway-ci-v2-runner.service
journalctl -u easyai-gateway-ci-v2-runner.service --since '15 minutes ago' --no-pager
docker inspect -f 'user={{.Config.User}} privileged={{.HostConfig.Privileged}} apparmor={{.AppArmorProfile}}' \
easyai-gateway-ci-v2-runner
docker inspect -f '{{range .Mounts}}{{println .Source " -> " .Destination .Mode}}{{end}}' \
easyai-gateway-ci-v2-runner
docker exec easyai-gateway-ci-v2-runner docker info \
--format '{{range .SecurityOptions}}{{println .}}{{end}}'
docker exec easyai-gateway-ci-v2-runner docker image inspect \
'docker.io/library/node:24.16.0-bookworm@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7' \
--format '{{.Id}}'
```
预期:外层 image user 为 `rootless`、外层因 DinD 为 `privileged=true`、内层安全选项含 `name=rootless`RootlessKit 使用独立 PID namespace,内层 `--pid=host` 也不能看到外层 Runner;挂载仅包含专用 `/data`、只读配置和只读工具链,不能出现 `/var/run/docker.sock``/run/docker.sock`
在 Gitea 中分别验证:
1. 同仓 PR 的 `ci / verify (pull_request)` 成功。
2. `main` Push 的 `ci / verify (push)` 成功。
3. 合法 SemVer Tag 新产生独立 `release-ci / verify-tag (push)`;非法或不属于 `main` 历史的 Tag 失败,且 Tag context 不能被旧 `main` context 替代。
4. Fork PR 默认不自动调度,维护者先检查 `.gitea/workflows/**` 和容器选项后再批准。
## Fork PR 残余风险
rootless DinD 不是 VM 沙箱。外层仍按官方要求使用 `--privileged`Gitea runner 也会解析 PR 控制的 `jobs.<job>.container.options`。配置已禁止 Job privileged、宿主/内层 Docker socket 和任意 bind source,但 Runner、rootless Docker、容器运行时或内核漏洞仍可能突破边界;Job 的出站网络和资源消耗也不是强租户隔离。因此 Fork PR 必须维护者批准,CI 不得注入生产 Secret;未来应迁移到独立 CI 主机或虚拟机。
## 发布生产版本
1. 合并到受保护 `main`,确认源码 CI 成功。
2. 在已验证提交上创建完整 SemVer Tag 并推送。
3. 确认同一 SHA 的 `ci / verify (push)` 和本次 `release-ci / verify-tag (push)` 都成功。
4. 由部署仓 root-owned dispatcher 构建、扫描并发布 digest;源码仓工作流不会直接调用生产 helper。
```bash
git switch main
git pull --ff-only
git tag -a v0.1.1 -m 'release: v0.1.1'
git push origin v0.1.1
```
dispatcher 以完整 Git SHA 发布 Registry Tag,并把 Registry 返回的 digest 写入服务器 root-only 发布清单。生产 Compose 使用 `repository@sha256:...`,不是 `latest`、版本号或可变 SHA Tag。
## 发布后验证
```bash
curl -fsS https://ai.51easyai.com/gateway-api/healthz
curl -fsS https://ai.51easyai.com/gateway-api/readyz
ssh root@110.42.51.33 'cd /root/easyai-ai-gateway-deploy && ./gateway-ops.sh ps'
```
同时检查服务器磁盘、API 日志和数据库容器健康状态。发布后的首小时如出现错误率翻倍、P95 延迟增加 50%、数据完整性或安全问题,应立即回滚。
## 应用与数据库回滚
发布失败时固定部署脚本会恢复上一组 API/Web digest 并重复健康检查。人工回滚只选择服务器已有 root-only 发布清单的历史 Git SHA
```bash
sudo /usr/local/sbin/easyai-ai-gateway-release <40 位历史 Git SHA>
```
没有 `/root/easyai-ai-gateway-deploy/.releases/<SHA>.env` 的远端 SHA Tag 不应被盲目信任。数据库备份位于 `/var/backups/easyai-ai-gateway`,只保留最近 5 份。应用回滚不会自动恢复数据库;只有停止写入并再次保留当前副本后,操作员才能选择备份执行 `pg_restore`
## Runner 故障恢复
先在 Gitea 确认没有运行中的 Job,再检查服务与两个 daemon:
```bash
systemctl status easyai-gateway-ci-v2-runner.service --no-pager
docker logs --tail 100 easyai-gateway-ci-v2-runner
docker exec easyai-gateway-ci-v2-runner docker info
df -h /var/lib/docker
```
确认空闲后才执行:
```bash
systemctl restart easyai-gateway-ci-v2-runner.service
journalctl -u easyai-gateway-ci-v2-runner.service -n 100 --no-pager
```
不要删除 `easyai-gateway-ci-v2-data` volume;它包含 Runner 注册状态和内层 daemon 缓存。不得用全局 prune 处理磁盘压力。