晚上八点,小林推开实验室的门,发现老周正对着一杯凉了的咖啡发呆。
「周哥,看什么呢?」
「看你昨天的部署。」老周指了指屏幕,「你推一次代码,直接部署到线上,用户立刻就能看到。」
「对啊,这就是 CD。」
「如果代码有 bug 呢?」
「那就……回滚?」
「回滚需要时间。从发现问题,到决定回滚,到执行回滚,再到验证恢复——至少十几分钟。这十几分钟,用户看到的是坏页面。」
「那怎么办?」
「加一层缓冲。」
老周在白板上写:
dev → staging → prod
「开发环境随便折腾,测试环境模拟生产,生产环境只接受验证过的代码。」

一、一台机器跑三个环境
| 环境 |
端口 |
用途 |
谁触发 |
特点 |
| dev |
8001 |
开发自测 |
自动部署 |
随便改、随时挂 |
| staging |
8002 |
预发布验证 |
自动部署 |
模拟生产、数据脱敏 |
| prod |
8000 |
线上 |
手动触发 |
稳定,只接受验证过的代码 |
「教学环境用不同端口区分。生产环境会用不同域名或不同机器——但思路一样:隔离 + 渐进。」
「那 staging 自动跑,我做什么?」
「看一眼。」老周说。
「原稿里 staging 也要手动点——但 manual 按钮挡不住手滑。GitLab 页面上几个播放按钮长得一样、挨得又近,手一滑就点错了。」
确实,经历过的人都知道,界面上那一排 manual job 的播放按钮几乎没有任何视觉区分度,靠「小心点」来防误触,本质上是在赌每一次手都不抖。
「staging 自动跑完,你只要养成『看一眼』的习惯——看监控、看日志有没有异常。比『点一下』更可靠。」
「真正要拦的是 prod——它必须手动,而且要配 Protected environment。」

二、secrets 四原则
GitLab 项目 Settings → CI/CD → Variables,同一个 Key 可以按环境设不同的值:
| Key |
dev |
staging |
prod |
| DB_HOST |
10.0.0.102 |
10.0.0.102 |
10.0.0.201 |
| LOG_LEVEL |
debug |
info |
warn |
注意一个前提:环境作用域的变量只在 job 声明了 environment: name: xxx 时才生效。
那个 environment: 字段不只是「记录部署历史」,它还决定了这个 job 能拿到哪套变量。
secrets 管理四条底线:
- Masked —— 日志不显示
- Protected —— 只允许保护分支访问
- File 类型 —— SSH 私钥这类,存成临时文件
- 不硬编码 —— 代码里不能出现密码
「那如果 secrets 泄露了呢?」
「立刻轮换。改密码、换密钥、重新部署。然后查审计日志——看谁改了变量、谁部署了环境。」
三、保护分支:连你自己也不能直接推
GitLab Settings → Repository → Protected branches:
- Branch:main
- Allowed to merge:Maintainers
- Allowed to push:No one ← 关键
- Allowed to force push:关闭
小林试了试,直接往 main 推:
remote: GitLab: You are not allowed to push code to protected branches on this project.
「被拦住了。」
「包括你。这防的是『手滑推错』。所有改动走 MR,CI 自动检查。」
保护分支的价值不在于「防坏人」,而在于「防自己」。别说新手,老手在赶进度、深夜上线的时候也完全可能把 commit 直接推到 main 上。把路堵死,就不需要每次靠意志力了。
四、保护环境
GitLab Settings → CI/CD → Protected environments:
- Environment:production
- Allowed to deploy:Maintainers
「这样只有 Maintainer 能部署生产。普通开发者只能部署 dev 和 staging。」
这里有个容易被绕过的点:教学环境里你往往既是 Owner 又是操作者——Protected environment 对你形同虚设。
要真的验证这道防线,得用第二个账号(Developer 角色)试着点一次 prod,看到 insufficient permissions 才算数。
防线不是配了就有的,是要试过的。
五、多环境流水线
stages:
- lint
- test
- build
- deploy-dev
- deploy-staging
- deploy-prod
deploy-dev:
stage: deploy-dev
needs: [build-image]
environment:
name: dev
url: http://10.0.0.102:8001
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
deploy-staging:
stage: deploy-staging
needs: [deploy-dev]
environment:
name: staging
url: http://10.0.0.102:8002
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
deploy-prod:
stage: deploy-prod
needs: [deploy-staging]
environment:
name: production
url: http://10.0.0.102:8000
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual # ← 只有 prod 手动
「needs 在这里还有一层隐藏作用——」老周说。
「staging 没跑完,prod 的播放按钮是点不动的(job 处于 blocked 状态)。」
「这道防线不依赖任何人的自觉。」

六、「差点发到生产」,为什么没发生
老周说:「现在模拟一个危险操作:你直接去点 prod 的播放按钮——但先想想,它点得动吗?」
小林点了。页面弹出提示:这个任务处于 blocked 状态。
「为什么?」
「因为 deploy-prod 写了 needs: [deploy-staging]。staging 这次确实跑过了——所以现在它点得动。但如果你是在 staging 跑完之前点的,GitLab 根本不会让你点。」
「也就是说……」
「needs 本身就是一道防线,而且它不依赖任何人的自觉。」

「所以真正的教训是什么?」老周问。
「不是……小心别点错?」
「人总会点错。」老周说。
「靠『小心』防事故,迟早出事。」
「真正要做的是『让点错也无所谓』——配齐 needs、Protected environment、自动回滚。这才是可靠的工程实践。」
这段话值得反复读。工程上的安全从来不来自「别出错」的自我警告,而来自「出错了也不会怎样」的系统设计。把防线交给配置,而不是交给一时清醒。
七、没有结束
小林收拾东西走到门口,回头问:
「周哥,今天差点把测试发到生产。吓死我了。」
「好事。」老周说,「吓一次,记一辈子。以后你点 prod 之前,会多看两眼。」
「但你说防线不靠害怕……」
「害怕让你记住,防线让你安全。」
「明天讲监控——你部署完了,怎么知道它真的正常?」
下一篇:《监控接入》——看得见一台,不算监控。
这次聊的多环境隔离只是 CI/CD 体系里的一环,后续连载会继续沿着部署流水线往下走。如果你也在折腾 GitLab 多环境配置或者遇到过「差点误发生产」的场景,可以到云栈社区翻翻相关讨论,有不少同行踩坑后的复盘记录。