小Cの已经记不起来的博客

Git 工作流中的 蓝绿部署 最佳实践

我一开始也以为,蓝绿部署是不是就在 Git 里搞两个分支,一个叫 blue,一个叫 green,然后切分支上线。结果真上手之后发现,这样特别容易把自己玩死。尤其是团队里有人随手合并、有人忘了打 tag、有人拿开发环境分支去发生产,那场面基本就是事故现场。

下面是我后来比较推荐的一种做法:Git 负责版本和构建入口,蓝绿负责运行期切换,二者别搅在一起。

先说清楚蓝绿部署到底切的是啥

蓝绿部署的“蓝”和“绿”,不是 Git 分支名,而是两套正在运行的环境,或者至少是两个可切换的服务实例。你可以理解为:

blue:  运行版本 v1.2.0
green: 运行版本 v1.3.0
网关:默认指向 blue

发布时先把 v1.3.0 部署到 green,等健康检查过了,再把网关切到 green。老版本 blue 先别急着销毁,等确认没问题了再停。

所以真正的核心是:切流量,而不是切代码分支

如果只在 Git 里建一个 bluegreen 分支,然后手动 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: 运行期环境

比如一次完整流程大概长这样:

  1. 开发合入 main
  2. 准备发版时打 tag:v1.3.0
  3. CI 根据 tag 构建镜像:registry.example.com/app:1.3.0-abc1234
  4. 部署到当前空闲环境,比如 green
  5. 执行健康检查
  6. 切换流量到 green
  7. 观察一段时间,成功则保留 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: bluecolor: 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 历史发呆,也不用半夜盯着线上日志猜到底跑的是哪个分支。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放