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

8.5 KiB
Raw Blame History

生产 CI/CD 运行手册

信任边界与固定资源

  • 源码仓:https://git.51easyai.com/BCAI/easyai-ai-gateway
  • CI Runnereasyai-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 中执行:

RUNNER_REGISTRATION_TOKEN='<一次性 Token>' ./scripts/provision-ci-runner.sh

已有有效 named volume 时可直接重跑,不再需要 Token

./scripts/provision-ci-runner.sh

注册 Token 通过交互 stdin 送给短命注册容器不出现在宿主或容器的命令行参数中provision 随后立即从环境中清除它,长期 systemd unit 也不包含 Token。不要输出或复制 /data/.runner 内容。脚本会停用旧 host Runner并确保旧 Runner 用户和 v2 遗留用户不属于 docker/sudo 组。

默认磁盘门禁可用环境变量提高,但生产环境不应降低:

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不存在时写入

RUNNER_SECURITY_OPTIONS=""

如果新系统的 /proc/sys/kernel/apparmor_restrict_unprivileged_userns1,则必须先由宿主发行版或 Docker 安装包提供并加载 rootlesskit profileprovision 只探测和使用,不会自行复制一份 profile。缺失时 provision 会停止;不得手工关闭该 sysctl也不得使用 apparmor=unconfined 绕过。

参考:Docker Rootless Docker-in-DockerUbuntu 安全特性版本矩阵

Runner 真实验证

安装脚本会先启动一个无 Token、无持久 Runner 状态挂载、无宿主 bind mount 的短命 rootless DinD probe随后启动长期服务确认 Runner 已向 Gitea declare、通过内层 docker info 验证 name=rootless,再从内层 daemon 拉取固定 digest 的 Job 镜像,并在该镜像中实际运行全部工具。人工复核:

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=rootlessRootlessKit 使用独立 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 沙箱。外层仍按官方要求使用 --privilegedGitea 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。
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。

发布后验证

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

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

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

确认空闲后才执行:

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 处理磁盘压力。