出事时,没人知道生产环境上跑的是什么
目标
配置固定环境、按分支划分的评审环境、关闭环境的作业以及手动生产部署,并用 gitlab-ci-local 实际运行以提交 SHA 留存的部署记录、从该记录中选择的回滚,以及部署冻结规则。
为什么重要
部署必须可以回退才安全。什么时候发往哪里了,要通过 environment 留下来,发出去的东西要用提交 SHA 来识别,出事故时才能在几分钟内回到“当时的那个”。评审环境虽然方便,但如果没有与之成对的关闭机制,就会堆积起来,变成成本和暴露面,而生产部署的审批,仅凭 allow_failure 这一个值,既可能成为门禁,也可能沦为摆设。像冻结期这样“现在不发布”的约定,也必须刻进配置里才能得到遵守。
步骤
- 把
/root/glci-deploy创建为 Git 仓库(.gitignore 中写入.gitlab-ci-local/),在.gitlab-ci.yml中放入 stages[deploy, cleanup]、全局变量DEPLOY_LOG: /root/glci-deploy-history/deploys.log,以及只在 main 上创建的作业deploy-staging(deploy)。environment 的名称是staging,URL 是https://staging.example.com,脚本是echo "env=$CI_ENVIRONMENT_NAME url=$CI_ENVIRONMENT_URL"。提交到 main 并运行。 - 添加只在非 main 分支上创建的作业
review-app(deploy)。environment 名称是review/$CI_COMMIT_REF_SLUG,URL 是https://$CI_COMMIT_REF_SLUG.review.example.com,脚本与第 1 步相同的 echo。提交。评分器会在副本中切换到Feature/Login-Page分支,检查名称是否为review/feature-login-page、URL 是否为https://feature-login-page.review.example.com。 - 给 review-app 的 environment 加上
on_stop: stop-review和auto_stop_in: 1 day,并创建作业stop-review(cleanup)。使用相同的环境名称,action: stop,在非 main 分支上when: manual,脚本是echo "stopping $CI_ENVIRONMENT_NAME"。提交。在 feature 分支上,stop-review 必须在列表中为 manual,并且用--manual stop-review运行时,必须打印stopping review/<슬러그>(占位符为 slug)。 - 添加在 main 上以
when: manual、allow_failure: false创建的作业deploy-prod(deploy)。environment 是production,URL 是https://www.example.com,脚本是echo "env=$CI_ENVIRONMENT_NAME"。提交。直接运行时 deploy-prod 不会运行,而给出--manual deploy-prod时必须运行。 - 创建
/root/glci-deploy/scripts/deploy.sh <환경>(占位符为环境)。如果ROLLBACK_TO为空,就把 tag 定为$CI_COMMIT_SHORT_SHA,并向$DEPLOY_LOG追加一行<환경> registry.example.com/app:<태그> release(占位符依次为环境与 tag),然后输出deployed ...。在 deploy-staging 和 deploy-prod 的脚本末尾加上bash scripts/deploy.sh staging和bash scripts/deploy.sh production,并提交。评分器会在副本中依次部署两个提交,检查记录中是否每个提交都留下了不同的 tag。 - 让 deploy.sh 在收到
ROLLBACK_TO时,不创建新 tag,而是重新发布那个 tag。如果该环境的历史记录中没有以registry.example.com/app:<ROLLBACK_TO>发布的记录,就输出이력에 없는 태그입니다(韩文,意为“该标签不在历史记录中”)并失败。记录行的最后一个词是rollback。评分器会在副本中部署两次后执行回退到第一个 tag 的运行,以及试图回退到不存在的 tag 的运行。 - 在 deploy-prod 的 rules 最前面,放入
if: $CI_DEPLOY_FREEZE时when: never。提交。用--variable CI_DEPLOY_FREEZE=1查看列表,必须没有 deploy-prod,没有这个变量时,它必须以 manual 存在。无论是否冻结,deploy-staging 都必须存在。
参考
- 这台 VM 上没有 GitLab 服务器和 Runner,由 gitlab-ci-local 4.75.1 按与 GitLab 相同的规则解析 .gitlab-ci.yml,并用 shell 执行作业。写了
image:就会试图用 Docker 运行,所以不要使用。受保护变量、掩码、CI_JOB_TOKEN、Runner tag、合并请求流水线的生成是服务器功能,在这里不会重现。 - 运行:在仓库根目录用
gitlab-ci-local --shell-isolation --no-artifacts-to-source(每个作业使用单独的工作目录,不把产物写回仓库),作业列表:gitlab-ci-local --list-csv-all,解析后的配置:gitlab-ci-local --preview。gitlab-ci-local 只把 Git 跟踪的文件传给作业,所以创建文件后请git add。评分器会把仓库复制一份,提交所有文件后,再用同一个工具重新运行。 - gitlab-ci-local 的差异(实测):它不会用 resource_group 让同一环境的部署排队。manual 作业必须用 --manual 指定才会运行,并且不会阻挡后面的 stage。环境页面、部署历史、受保护环境审批、冻结期设置是服务器功能。
- 部署记录
/root/glci-deploy-history/deploys.log位于仓库之外。评分器运行副本时,会用--variable DEPLOY_LOG=<임시 경로>(占位符为临时路径)另行写入。 - Environments · Predefined CI/CD variables(CI_COMMIT_REF_SLUG、CI_DEPLOY_FREEZE) · Resource groups · CI/CD YAML syntax reference
给部署作业贴上标签
把 /root/glci-deploy 创建为 Git 仓库(.gitignore 中写入 .gitlab-ci-local/),在 .gitlab-ci.yml 中放入 stages [deploy, cleanup]、全局变量 DEPLOY_LOG: /root/glci-deploy-history/deploys.log,以及只在 main 上创建的作业 deploy-staging(deploy)。environment 的名称是 staging,URL 是 https://staging.example.com,脚本是 echo "env=$CI_ENVIRONMENT_NAME url=$CI_ENVIRONMENT_URL"。提交到 main 并运行。
带有 environment 的作业,在 GitLab 中会被记录为部署,并在环境页面上留下什么时候发布了什么。在作业内部,可以通过 CI_ENVIRONMENT_NAME 和 CI_ENVIRONMENT_URL 知道自己要去哪里。
每个分支生成的评审环境
添加只在非 main 分支上创建的作业 review-app(deploy)。environment 名称是 review/$CI_COMMIT_REF_SLUG,URL 是 https://$CI_COMMIT_REF_SLUG.review.example.com,脚本与第 1 步相同的 echo。提交。评分器会在副本中切换到 Feature/Login-Page 分支,检查名称是否为 review/feature-login-page、URL 是否为 https://feature-login-page.review.example.com。
分支名称中会混有大写字母、斜杠这类不能用于主机名的字符。CI_COMMIT_REF_SLUG 是转换为小写、并把不允许的字符替换为连字符后的值,所以可以原样用于 URL 和环境名称。
评审环境要与关闭它的作业成对
给 review-app 的 environment 加上 on_stop: stop-review 和 auto_stop_in: 1 day,并创建作业 stop-review(cleanup)。使用相同的环境名称,action: stop,在非 main 分支上 when: manual,脚本是 echo "stopping $CI_ENVIRONMENT_NAME"。提交。在 feature 分支上,stop-review 必须在列表中为 manual,并且用 --manual stop-review 运行时,必须打印 stopping review/<슬러그>(占位符为 slug)。
评审环境会随分支数量而堆积。on_stop 是“关闭这个环境的作业是那个”这样的连接,删除或合并分支时,GitLab 会调用那个作业。auto_stop_in 会让被遗忘的环境在一段时间之后自动关闭。
生产部署由人来点击
添加在 main 上以 when: manual、allow_failure: false 创建的作业 deploy-prod(deploy)。environment 是 production,URL 是 https://www.example.com,脚本是 echo "env=$CI_ENVIRONMENT_NAME"。提交。直接运行时 deploy-prod 不会运行,而给出 --manual deploy-prod 时必须运行。
在 GitLab 中,allow_failure: false 的手动作业,会在被点击之前让流水线保持“已阻塞”。如果是 true,即使不点击,流水线也会以成功结束,无法成为审批门禁。gitlab-ci-local 不会重现这种阻塞,所以通过列表的 allowFailure 列来确认。
把发布了什么以提交 SHA 留存
创建 /root/glci-deploy/scripts/deploy.sh <환경>(占位符为环境)。如果 ROLLBACK_TO 为空,就把 tag 定为 $CI_COMMIT_SHORT_SHA,并向 $DEPLOY_LOG 追加一行 <환경> registry.example.com/app:<태그> release(占位符依次为环境与 tag),然后输出 deployed ...。在 deploy-staging 和 deploy-prod 的脚本末尾加上 bash scripts/deploy.sh staging 和 bash scripts/deploy.sh production,并提交。评分器会在副本中依次部署两个提交,检查记录中是否每个提交都留下了不同的 tag。
如果用 latest 这样会移动的 tag 来部署,出事故时没有人知道“当时发布的是什么”。提交 SHA 是把源码、构建、部署用一行连起来的标识符。部署记录路径要用变量接收,使其可以在运行时更换。
回退就是重新发布历史中已有的 tag
让 deploy.sh 在收到 ROLLBACK_TO 时,不创建新 tag,而是重新发布那个 tag。如果该环境的历史记录中没有以 registry.example.com/app:<ROLLBACK_TO> 发布的记录,就输出 이력에 없는 태그입니다(韩文,意为“该标签不在历史记录中”)并失败。记录行的最后一个词是 rollback。评分器会在副本中部署两次后执行回退到第一个 tag 的运行,以及试图回退到不存在的 tag 的运行。
回退不是“用旧源码重新构建”,而是“把已经验证过并发布过的产物重新发布”。必须拦截历史中没有的 tag,才能避免因一个笔误而把未经验证的镜像发到生产。运行时用 --variable ROLLBACK_TO=<태그>(占位符为 tag)传入。
部署冻结期间不创建生产部署
在 deploy-prod 的 rules 最前面,放入 if: $CI_DEPLOY_FREEZE 时 when: never。提交。用 --variable CI_DEPLOY_FREEZE=1 查看列表,必须没有 deploy-prod,没有这个变量时,它必须以 manual 存在。无论是否冻结,deploy-staging 都必须存在。
如果在项目中设置了冻结期,GitLab 会在那段时间内填充 CI_DEPLOY_FREEZE。规则只使用第一个匹配项,所以冻结规则必须放在 main 规则之前。这是一个不把冻结交给人的记忆,而是让流水线来拒绝的装置。