Git 工作流中的 蓝绿部署 最佳实践
我一开始也以为,蓝绿部署是不是就在 Git 里搞两个分支,一个叫 blue,一个叫 green,然后切分支上线。结果真上手之后发现,这样特别容易把自己玩死。尤其是团队里有人随手合并、有人忘了打 tag、有人拿开发环境分支去发生产,那场面基本就是事故现场。
下面是我后来比较推荐的一种做法:Git 负责版本和构建入口,蓝绿负责运行期切换,二者别搅在一起。
先说清楚蓝绿部署到底切的是啥
蓝绿部署的“蓝”和“绿”,不是 Git 分支名,而是两套正在运行的环境,或者至少是两个可切换的服务实例。你可以理解为:
blue: 运行版本 v1.2.0
green: 运行版本 v1.3.0
网关:默认指向 blue
发布时先把 v1.3.0 部署到 green,等健康检查过了,再把网关切到 green。老版本 blue 先别急着销毁,等确认没问题了再停。
所以真正的核心是:切流量,而不是切代码分支。
如果只在 Git 里建一个 blue、green 分支,然后手动 git checkout,那部署过程非常不可控。因为代码分支只表示源码状态,不表示线上到底跑的是哪个产物、哪个镜像、哪个配置、哪个依赖版本。
Git 工作流里最好别直接发分支
很多团队习惯这样:
develop -> release/1.3.0 -> main
然后 CI/CD 配置成:
deploy:
only:
- main
看起来挺正常,但真做蓝绿部署时会有个问题:同一个 main 可能被重新构建很多次,线上到底跑的是哪一次构建?如果没有唯一标识,回滚的时候很痛苦。
我后来比较喜欢把“发布入口”和“可部署产物”分开。
发布入口可以用 tag,比如:
git tag -a v1.3.0 -m "release 1.3.0"
git push origin v1.3.0
CI 监听 tag:
on:
push:
tags:
- "v*.*.*"
这样每次发布都有明确起点。
Git tag 的优势是它不会被日常开发随手推乱,至少比直接发 main 分支更克制。而且 tag 很适合拿来做镜像标签的一部分。
推荐:主干开发,tag 发布,镜像产物,蓝绿切换
我个人的习惯是:
main: 始终可发布,但不代表已经发布
tag: 真正触发生产发布
image: 构建出来的不可变产物
blue/green: 运行期环境
比如一次完整流程大概长这样:
- 开发合入
main - 准备发版时打 tag:
v1.3.0 - CI 根据 tag 构建镜像:
registry.example.com/app:1.3.0-abc1234 - 部署到当前空闲环境,比如
green - 执行健康检查
- 切换流量到
green - 观察一段时间,成功则保留
blue做回滚,失败则切回blue
这里最关键的是镜像 tag。别只用 latest。
docker build -t registry.example.com/app:latest .
docker push registry.example.com/app:latest
这玩意儿真的很容易翻车。你以为线上是最新镜像,结果缓存、节点、部署顺序一混乱,就不知道谁是谁了。
我建议至少带完整版本号和 commit hash:
VERSION=1.3.0
COMMIT=$(git rev-parse --short HEAD)
IMAGE="registry.example.com/app:${VERSION}-${COMMIT}"
docker build -t "$IMAGE" .
docker push "$IMAGE"
这样线上到底跑的什么,基本一眼就能查出来。
CI 里可以这么写
用 GitLab CI 举例:
build:
stage: build
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
script:
- IMAGE="registry.example.com/app:${CI_COMMIT_TAG#v}-${CI_COMMIT_SHORT_SHA}"
- docker build -t "$IMAGE" .
- docker push "$IMAGE"
- echo "$IMAGE" > deployed-image.txt
artifacts:
paths:
- deployed-image.txt
部署阶段不重新 build,只读取镜像地址:
deploy-green:
stage: deploy
script:
- IMAGE=$(cat deployed-image.txt)
- kubectl set image deployment/app-green app="$IMAGE"
- kubectl rollout status deployment/app-green
切流量阶段单独控制:
promote-green:
stage: promote
when: manual
script:
- aws elbv2 modify-listener --listener-arn "$LISTENER_ARN" --default-actions file://green.json
我比较喜欢把 promote 做成手动步骤,不是因为它更高级,而是因为自动切流量有时候很吓人。尤其刚上线那几十秒,日志、错误率、延迟、订单状态,这些还是需要人看一眼。
别把蓝绿配置写死在代码里
蓝绿切换最好由配置中心、网关、service mesh、负载均衡器来做,而不是让应用重启。
比如 Kubernetes 里面可以有两个 deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-blue
spec:
template:
metadata:
labels:
color: blue
另一个:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-green
spec:
template:
metadata:
labels:
color: green
Service 用 selector 控制指向谁:
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
color: green
切换时只改 color: blue 或 color: green,不用动业务代码。
不过这里有个坑:如果两个 deployment 都带着同一个 label 进入 service selector,流量会同时打到蓝和绿。这个听起来很傻,但我见过有人真这么干过,结果线上两个版本一起跑,日志看得人头皮发麻。
更稳一点可以加一个单独 label:
labels:
app: app
color: green
active: "true"
service 选:
selector:
app: app
active: "true"
切换时把 old 的 active 去掉,new 的加上。这个比直接改 color 更不容易误伤。
Git 标签、镜像、环境、流量最好都有对应记录
蓝绿部署最怕的是“我不知道线上现在到底是谁”。
所以每个环境至少要有这几个东西能对应起来:
环境: green
版本: v1.3.0
commit: 8d21f3a
镜像: registry.example.com/app:1.3.0-8d21f3a
发布时间: 2026-09-11 15:20
流量状态: active
如果这些只存在某个人脑子里,迟早出事。
我一般会在发布说明或者部署变量里带上这些信息。比如镜像 label:
docker build \
--label git.commit="$COMMIT" \
--label git.tag="$VERSION" \
--label release.color=green \
-t "$IMAGE" .
或者 K8s annotation:
metadata:
annotations:
release.git.commit: "8d21f3a"
release.git.tag: "v1.3.0"
release.color: "green"
查的时候不用猜:
kubectl get deployment app-green -o jsonpath='{.spec.template.metadata.annotations}'
回滚不要只靠 Git revert
Git 回滚和线上回滚不是一回事。
如果你已经切到 green 了,发现新版本有问题,第一动作通常是:
切换流量回 blue
不是马上 git revert。
因为业务恢复优先。先把用户稳住,再慢慢处理代码问题。
回滚代码时可以这样:
git revert <bad-commit>
或者重新发一个老 tag:
git checkout v1.2.0
git tag v1.2.1
git push origin v1.2.1
但如果你的版本语义比较严格,不建议随便给旧代码打新 tag。除非你明确这是 hotfix 分支出来的版本。否则 v1.2.1 里面到底有没有新修复,别人会搞不清楚。
我后来更喜欢这样:
v1.3.0 有 bug
先流量切回 blue,也就是 v1.2.0
修复后打 v1.3.1
简单直接,至少语义清楚。
一个比较稳的发布节奏
我自己的习惯是每次发布前先把蓝绿都检查一遍:
git fetch --all --tags
git tag --sort=-creatordate | head
确认当前线上版本:
kubectl get deployment app-blue app-green -o wide
确认当前流量:
kubectl get service app -o wide
如果 green 是 active,就先看 green 对应哪个 tag。
然后发布新版本到 blue。这里不是固定蓝永远老,绿永远新。谁空闲谁部署。这个规则一开始可能有点反直觉,但比固定某个颜色“永远是稳定版”更省资源,也更灵活。
部署完先别切:
kubectl rollout status deployment/app-blue
再做健康检查:
curl -fsS https://staging-api.example.com/healthz
或者直接在蓝环境挂一个测试流量:
X-Canary: 5%
X-Color: blue
等一小会儿,没问题再切:
kubectl patch service app -p '{"spec":{"selector":{"color":"blue"}}}'
切完盯日志:
kubectl logs -l color=blue --tail=100
如果错误率正常,再把 green 保留一段时间当回滚环境。如果没问题,可以缩掉 green。
真正省心的做法:环境名不写死在代码里
很多蓝绿部署的问题,其实是代码和配置耦合太深。比如应用一启动就读一个写死的环境变量:
DEPLOY_COLOR=green
然后某个中间件根据这个值决定行为,后面就越来越乱。
我建议至少区分这三类配置:
APP_ENV=prod
DEPLOY_COLOR=green
RELEASE_ID=v1.3.0-8d21f3a
DEPLOY_COLOR 只是运行期身份,不应该决定业务逻辑。业务开关尽量走配置中心,别走分支名,也别走环境变量硬编码。
比如:
FEATURE_X=true
这个开关可以放在配置中心,而不是因为 green 环境就必须打开。否则以后排查问题时,你会分不清到底是蓝绿导致的,还是配置导致的。
几个我踩过的坑
一个是 CI 并发构建。同一个 tag 触发两次构建,后一次覆盖前一次镜像 tag,结果线上镜像和提交记录对不上。解决办法很简单,tag 发布必须幂等,或者镜像 tag 加 commit hash。
v1.3.0-8d21f3a
比单纯:
v1.3.0
稳很多。
一个是数据库 migration。蓝绿部署时,老版本和新版本可能同时存在。如果新 migration 破坏了老版本依赖,切回去就完了。
所以数据库变更最好保持向后兼容:
先发兼容代码
再发新 schema
最后再切流量
不要一个发布里把不兼容 migration 和代码一起硬推上去。
还有一个是灰度和蓝绿混用。很多人嘴上说的是蓝绿,实际做的是滚动发布或者金丝雀。这倒不丢人,反正都是发布策略。但你得知道自己在做什么。蓝绿强调完整环境切换,金丝雀强调小流量观察,滚动强调逐步替换实例。如果三者混在一起,排查问题时会很痛苦。
最后一点个人体会
蓝绿部署在 Git 工作流里最好不要搞得太复杂。Git 只负责记录“这次发布是什么版本”,真正决定流量的是部署产物和网关。
如果非要给 Git 流程一个简单规则,我会选:
main 保持可发布
tag 表示正式版本
镜像 tag 带 commit
blue/green 由部署系统管理
回滚先切流量,再处理代码
这样至少不用每次发布都打开 Git 历史发呆,也不用半夜盯着线上日志猜到底跑的是哪个分支。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。