Skip to content
概述
单环境流水线解决的是“从源码到可运行实例”这一条链路。当交付链路需要经过开发验证、预发布验收再到最终上线,直接在原有流水线上追加阶段会引入耦合:构建、测试、部署被绑在同一流程里,一旦预发布阶段需要人工确认,整个流水线就停在原地,后续提交的执行也跟着排队。
多环境流水线的思路是把环境抽象为独立部署目标,让推进链路通过依赖关系串联,而不是把多个环境写死在同一个 job 里。
多环境流水线结构
典型的划分方式是 dev、staging、production 三层。每个环境对应一个部署 job,job 之间通过 needs 关键字定义严格的顺序关系:dev 完成后才进入 staging,staging 通过后才进入 production。
yaml
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }}
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- id: upload
uses: actions/upload-artifact@v4
with:
name: app-build
path: dist/
deploy-dev:
needs: build
uses: ./.github/workflows/deploy.yml
with:
environment: dev
artifact-name: app-build
deploy-staging:
needs: deploy-dev
uses: ./.github/workflows/deploy.yml
with:
environment: staging
artifact-name: app-build
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/deploy.yml
with:
environment: production
artifact-name: app-build这里 needs 构建的是 job 之间的依赖图,不是流水线的“阶段”概念。GitHub Actions 按 DAG 解析执行顺序,上游 job 成功退出后下游才会被调度。
把部署拆成独立 job 的直接好处是每个 job 可以绑定不同的 environment。GitHub Actions 中,environment 是一组针对部署目标的配置集合,包含保护规则、密钥和变量。
环境门禁
environment 的保护规则是实现门禁的核心机制。在仓库的 Settings → Environments 中可以为每个环境配置以下约束。
必需审阅者
指定一个或多个人员或团队。部署 job 在执行前进入 waiting 状态,直到所有审阅者批准。任意一人拒绝则部署取消。这对应人工审批节点。
等待计时器
设置一个倒计时(上限 4320 分钟),部署 job 被触发后至少等待指定时长才开始执行。用途是作为“冷静窗口”,给监控告警留出反应时间,或者在非工作时间自动延迟到次日。
部署分支限制
限定只有特定分支才能触发该环境的部署。例如 production 环境只允许 main 分支,防止特性分支意外部署。
yaml
deploy-production:
needs: deploy-staging
environment:
name: production
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: app-build
- run: ./deploy.sh production当这个 job 被调度时,Actions 先检查 production 环境的保护规则。有 required reviewers 时,job 状态显示黄色等待图标,审阅者收到通知。审批通过后 job 进入 queued 再到 in_progress。如果设置了 wait timer,审批通过后还要再等计时器归零。
有一个容易被忽略的行为:部署 job 在等待审批或计时期间,同一环境又触发了一次新的部署,Actions 会取消前一个排队中的 job,以最新的为准。审批流需要和团队流程对齐——审批人应当只批准最新的部署请求。
制品晋升与环境配置注入
制品走完 dev 验证后,同一个构建产物继续部署到 staging,而不是重新构建。这是制品晋升的核心约束:交付到上线的是在 dev 和 staging 都验证过的同一个二进制包,消除“重建引入差异”的风险。
制品晋级与版本追溯
GitHub Actions 中,actions/upload-artifact 上传的产物在整个 workflow run 内可用,跨 job 通过 actions/download-artifact 获取。上一节示例中,build job 上传产物后,deploy-dev、deploy-staging、deploy-production 三个 job 下载的都是同一份 app-build。
如果部署分散在多个 workflow 文件中(例如用 workflow_call 触发),可以借助 gh run download CLI 或 dawidd6/action-download-artifact 跨 workflow run 拉取制品。跨 run 下载时需要注意 artifact 的保留期限(默认 90 天,可调范围 1–400 天),避免 staging 阶段因为制品过期而失败。
版本追溯通常依赖两项信息:Git commit SHA 和制品名称。把 ${{ github.sha }} 作为制品文件名的一部分,或者在部署时将其写入运行时环境的 /version 端点,可以建立“部署实例 → 制品 → commit → PR”的追溯链。
环境特定配置与密钥注入
同一个制品部署到不同环境,差异在于配置:数据库连接串、外部 API 地址、日志级别、功能开关等。把这些差异打进镜像会迫使每个环境维护一个镜像版本,制品晋升也就失去了意义。正确做法是运行时注入。
GitHub Actions 的 environment 支持独立的 Variables 和 Secrets 作用域。在 Settings → Environments 中为每个环境添加变量和密钥后,workflow 中通过 vars.<name> 和 secrets.<name> 读取。
yaml
deploy-production:
environment: production
steps:
- run: |
cat > .env <<EOF
DATABASE_URL=${{ secrets.DATABASE_URL }}
LOG_LEVEL=${{ vars.LOG_LEVEL }}
EOF
- run: ./deploy.sh production运行时会生成内容如下的 .env 文件,密钥值不在日志中暴露:
DATABASE_URL=***
LOG_LEVEL=info如果部署目标是 Kubernetes,配置的注入方式更结构化:Variables 通常写入 ConfigMap,Secrets 写入 K8s Secret。以 Secret 为例,先在集群中创建:
bash
kubectl create secret generic app-secrets \
--from-literal=DATABASE_URL=postgres://... \
--from-literal=API_KEY=sk-...然后在 Pod 定义中通过 envFrom 注入为环境变量:
yaml
spec:
containers:
- name: app
image: my-app:latest
envFrom:
- secretRef:
name: app-secrets容器启动后,DATABASE_URL 和 API_KEY 作为环境变量可用,不需要修改镜像。Secret 以 base64 编码存储在 etcd 中,配合 RBAC 和 encryption at rest 控制访问。
蓝绿部署
蓝绿部署的思路是把风险绑定到“待激活环境”上:同时维护两套对等的环境(blue 和 green),只有一套承载实时流量。发布新版本时,先把新版本部署到空闲环境并完成验证,然后通过负载均衡器切换流量指向。
这不是简单的“起一个新实例再停旧实例”。关键在于流量切换的原子性和切换后旧环境的保留时长。
实现方式
以 Nginx 为例,配置两个 upstream 块:
nginx
upstream blue {
server 10.0.1.10:3000;
}
upstream green {
server 10.0.2.10:3000;
}
server {
listen 80;
location / {
proxy_pass http://blue;
}
}假设当前流量指向 blue。新版本部署流程为:
- 向 green 环境部署新版本,此时 green 不承载流量
- 对 green 执行冒烟测试:健康检查、关键 API 是否正常
- 修改
proxy_pass http://blue;为proxy_pass http://green;并 reload Nginx(nginx -s reload)
reload 动作的执行窗口很短(通常几十毫秒),新连接会被 Nginx worker 在优雅关闭过程中处理完毕。这实现了“零停机”效果,但不是真正的事务性切换——正在处理的请求不会中断,长连接可能仍连接到旧环境。
在流水线中,切换动作可以编排为独立 job:
yaml
jobs:
deploy-green:
environment: production
steps:
- run: ./deploy-to-green.sh
- run: ./smoke-test.sh green
switch-traffic:
needs: deploy-green
environment: production
steps:
- run: ssh lb-01 'sed -i "s/blue/green/" /etc/nginx/sites-enabled/app && nginx -s reload'回滚的方式也很直接:旧环境(blue)仍然在运行,只是没有流量。发现问题后把 proxy_pass 切回 blue 即可,回滚耗时等同于一次 reload。这个机制成立的前提是数据库 schema 的变更是向后兼容的——如果新版本修改了数据库结构,回滚到旧代码时可能写入失败。涉及破坏性 schema 变更时不能直接回滚,需要额外的兼容层或回滚脚本。
金丝雀发布
蓝绿部署是二态的:要么全量走新版本,要么全量回旧版本。金丝雀发布引入“部分流量”的中间状态——新版本只接收 5% 或 10% 的请求,与旧版本并存运行一段时间,通过监控指标判断新版本是否健康,再逐步增加流量比例直到 100%。
按比例放量
实现金丝雀需要负载均衡器支持流量权重分配。以 Kubernetes Ingress Nginx 的 canary 注解为例:
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
backend:
service:
name: app-v2
port:
number: 3000该配置按权重 10% 把流量引到 app-v2 Service,其余 90% 仍走当前的稳定 Service。调整 canary-weight 的值即可控制放量比例。
放量过程通常分为几个台阶:5% → 10% → 25% → 50% → 100%,每一步之间留出 5 到 15 分钟的观察窗口。观察期内重点监控的指标包括:
- 错误率(5xx 占比是否超出基线)
- 响应延迟(p95/p99 是否恶化)
- 业务指标(订单成功率、支付回调率等)
自动回滚
自动回滚的触发条件是部署脚本的一部分,不是流水线原生支持的功能。典型的做法是在每一步放量后调用监控 API 查询错误预算,如果超出阈值则执行回滚并中止流水线。
yaml
jobs:
canary-10pct:
environment: production
steps:
- run: |
kubectl annotate ingress app-canary \
nginx.ingress.kubernetes.io/canary-weight="10" --overwrite
- run: sleep 600 # 10 分钟观察窗口
- run: |
ERROR_RATE=$(curl -s "https://monitoring/api/error-rate?service=app&version=v2")
if [ "$ERROR_RATE" -gt 1.0 ]; then
kubectl annotate ingress app-canary \
nginx.ingress.kubernetes.io/canary-weight="0" --overwrite
exit 1
fi
canary-25pct:
needs: canary-10pct
environment: production这种编排方式把观察、决策和回滚动作都放在脚本内,流水线本身只是一个顺序执行器和状态记录器。实际运行中,流量切换 API、监控查询和回滚动作之间存在网络延迟和时序窗口——流量刚切到 10% 就收到错误告警时,回滚脚本和流水线失败标记之间可能有十几秒的竞态。
发布策略对比
蓝绿部署和金丝雀发布不是“哪个更好”的问题,而是适用条件不同。
蓝绿部署的优势在于切换和回滚都快。代价是需要两倍的基础设施资源(至少维护两套完整实例),且数据库 schema 变更要向后兼容。它适合变更频率不极端、但对回滚速度要求高的场景,例如支付服务、用户认证这类核心组件的版本升级。
金丝雀发布适合需要控制发布风险的场景:新版本存在不确定性,但不想一次影响所有用户。代价是发布过程拉长——从 5% 到 100% 可能需要一小时以上,且需要足够的可观测性来支撑逐步放量的判断。没有良好的监控和告警,金丝雀的“渐进验证”只是在拉长故障暴露时间。
还有一个容易被忽略的维度:如果服务的调用方是外部客户端(SDK、API 调用方),金丝雀的流量分配很难保证同一个客户端始终打到同一个版本。这种情况下 session affinity(基于 cookie 或 header 的粘性路由)是必需的,否则客户端会因为版本差异出现不可预期的行为。
在流水线中接入发布策略
无论是蓝绿还是金丝雀,在流水线中的接入方式都可以抽象为“部署 + 验证 + 路由操作”的组合。流水线不直接感知部署策略,而是调用平台 API 执行对应动作。
组织方式上,常见的做法是把部署和路由操作封装为可复用的 Action 或脚本,主 workflow 只负责编排调用时机:
build → deploy-dev → deploy-staging → [approval gate] → deploy-production → switch-traffic蓝绿的 switch-traffic 是一个单次、原子的 API 调用;金丝雀的流量调整则是一个循环结构——在 GitHub Actions 中,循环是隐式的,通过多个连续的 job 节点实现放量台阶,每个台阶都带监控检查和失败回滚。
切换动作本身依赖平台。Kubernetes 上可以通过修改 Service selector 或 Ingress 注解完成;AWS 环境可能通过 Route53 加权记录或 ALB 的目标组权重实现;自建 Nginx 则是配置变更加 reload。流水线不需要关心具体实现,只需调用对应脚本。
注意点
GitHub Actions 的 environment 保护规则中,wait timer 和 required reviewers 是并列关系,不是串行。有审阅者时 timer 仍然生效,审批通过后还要等计时器归零。如果两者的组合延迟超出了预期部署窗口,部署会被推迟到下一轮。
蓝绿部署依赖“旧环境保持运行”来实现快速回滚,切换后旧环境仍占据资源。如果流水线没有清理旧环境的步骤,两套环境会一直并存,下一轮部署时可能需要额外判断哪个环境是空闲的。
金丝雀发布中的监控阈值需要基于稳定版本的基线数据,而不是绝对值。新版本在低流量下的错误率可能看起来正常,但放大到全量后问题才暴露。放量到 100% 后的前几分钟仍然需要保持观察,此时已经没有旧版本可以兜底,发现异常需要重新部署上一版本。
数据库迁移与部署策略的配合是绕不开的问题。任何涉及 schema 变更的发布都需要先运行迁移(确保向前兼容),再部署新代码,最后清理旧 schema。在这个约束下,蓝绿的“瞬间切换”和 schema 变更的分步执行天然存在时序矛盾,需要拆分为多个部署周期。
参考链接
- [1] https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
- [3] https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts
- [4] https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
- [5] https://kubernetes.io/docs/concepts/configuration/secret/
