Skip to content

02 · 部署与 CI/CD:多环境与发布策略 ​

D01 解决了"把服务打包成镜像",本篇解决更难的一半:怎么把镜像可靠地、可回滚地发布到线上。前端发版是"换静态文件 + 清缓存",后端发版是"换运行中的服务",稍有差池就是线上事故。本篇用前端对照讲清多环境、镜像流转、CI/CD 与四种发布策略。

速览卡 ​

项内容
一句话定位从"本地能跑"到"线上稳定":多环境、CI/CD、镜像推送、蓝绿/金丝雀/回滚的完整链路
核心内容一次发布的完整链路、多环境配置、镜像标签策略、GitHub Actions 最小流水线、四种发布策略、回滚与数据库迁移
读完能回答CI 和 CD 到底谁负责什么?发布出问题怎么在 5 分钟内回滚?为什么不能只做"推到服务器重启"?
相关板块D01 · Docker 与容器化 | C17 · 部署与 CI/CD | A16 · 前端 CI/CD

一、一次发布到底要过几道关 ​

前端发版对照(你已经熟悉的部分):

前端发版后端发版
构建产物是一堆静态文件构建产物是一个在跑的进程
换文件 + 清 CDN 缓存即可旧进程要平滑退出、新进程要接住流量
出问题回滚 = 换回旧文件出问题回滚 = 切回旧版本(有状态时要格外小心)
几乎没有"连接中"窗口存在断连窗口,健康检查不到位就报 502

一条完整发布链路,每一环都可能成为事故点:

text
代码提交 → CI 跑测试 → 构建镜像 → 推送到镜像仓库
  → CD 拉取新镜像 → 健康检查通过 → 切流量 → 监控确认

前端最容易漏掉的认知:后端发版不是"上传代码重启",而是一个有状态的切换过程。数据库连接、内存缓存、正在处理的请求,都可能在切换瞬间出错。


二、多环境:一套代码,多套配置 ​

2.1 环境从哪来 ​

环境谁在用数据典型配置来源
dev自己开发本地库/内存.env 本地文件
test/stagingCI 跑测试、发布前验收独立测试库CI 平台的环境变量
prod真实用户生产库平台环境变量 + 密钥服务

铁律:代码只有一份,环境差异全部来自配置。配置里最容易踩的坑是把密钥写进镜像(镜像会被拉取分享,见 D01 第六节)。

2.2 配置来源优先级 ​

按"越外层越优先"的原则读取:

text
命令行参数 > 环境变量 > .env 文件 > 代码默认值

这样本地用 .env,线上用平台注入的环境变量,同一份代码不用改。FastAPI 常用 pydantic-settings 做这件事:

python
# config.py:配置来源优先级由 pydantic-settings 处理
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # 环境变量优先,其次读 .env 文件
    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

    app_name: str = "todo-api"
    database_url: str = "postgres://user:pass@localhost:5432/app"
    # Secret 不从文件读默认值,线上必须由环境变量注入
    api_key: str = ""

settings = Settings()

环境变量里也没有默认值、api_key 为空时,应用应该在启动时直接报错,而不是带病启动。


三、镜像仓库:构建产物怎么流转 ​

3.1 为什么需要镜像仓库 ​

镜像在 CI 机器上构建,在另一台服务器上运行——中间需要一个"中转站",这就是镜像仓库(Registry),对应前端的 npm registry / 应用商店。

Registry谁用特点
Docker Hub公共镜像公开免费,私有仓库收费/限流
GitHub Container RegistryGitHub 用户跟仓库权限走,配 Actions 方便
云厂商镜像服务 / Harbor团队/生产内网拉取快、可做镜像扫描

3.2 标签(Tag)策略:唯一且不可变 ​

前端最忌"覆盖式发版"(旧版本被新版本覆盖,回滚无门);镜像也一样。禁止复用同一个 tag 反复覆盖,要用唯一且不可变的 tag:

bash
# 反例:latest 反复覆盖,出问题不知道线上是哪一版
docker push my-api:latest

# 正例:每次构建生成唯一 tag,历史版本永远可回滚
docker tag my-api my-api:2026-10-13-42
docker push my-api:2026-10-13-42

latest 只适合本地测试。线上部署必须钉住唯一 tag,否则"回滚到上一版"无从谈起。


四、CI:每次提交的质量闸门 ​

4.1 CI 负责什么 ​

CI(持续集成)回答"这次改动能不能合"。前端对应物是你熟悉的 lint + test + build,只是对象换成后端:

阶段干什么前端对照
跑测试pytest / 单元测试vitest / jest
静态检查ruff / mypyeslint / tsc --noEmit
构建镜像docker buildnpm run build
推送镜像docker push(仅主干分支)上传 dist 到 CDN

4.2 一个最小可用的 GitHub Actions 流水线 ​

yaml
# .github/workflows/deploy.yml
name: build-and-deploy

on:
  push:
    branches: [main]   # 只在主干触发完整流水线

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -r requirements-dev.txt
      - run: pytest                # 测试不过,下面全部不执行
      - run: ruff check .

  build-and-push:
    needs: test                   # 闸门:test 失败就不发布
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha                   # 唯一 tag:git 提交哈希
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max  # 复用 D01 的层缓存,加快构建

闸门原则:needs: test 让测试成为硬门槛;只用 type=sha 生成唯一 tag,保证镜像与代码一一对应。


五、CD:从镜像到线上 ​

5.1 CD 负责什么 ​

CD(持续部署)回答"镜像怎么到线上、怎么切流量、出问题怎么回"。这一步决定了发布是"稳"还是"赌"。

5.2 四种发布策略 ​

策略做法前端类比适合
滚动更新(Rolling)一批一批替换旧容器,保持服务不中断灰度切 CDN 节点无状态服务默认选择
蓝绿发布(Blue/Green)新旧两套并存,一键整体切流量整站切换 A/B 版本需要整体回滚、版本差异大
金丝雀发布(Canary)先放 5% 流量,观察后逐步放量灰度到小部分用户高风险改动、想先看线上效果
重建发布(Recreate)停旧起新,有断连窗口停机维护有状态且不能双跑的服务
yaml
# docker compose 滚动发布的最小示意
services:
  api:
    image: ghcr.io/you/todo-api:${IMAGE_TAG}   # tag 来自 CI,部署时注入
    ports:
      - "8000:8000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 3
    deploy:
      replicas: 3
      update_config:
        parallelism: 1      # 一次只换一个,观察健康后再换下一个
        order: start-first  # 先起新的、再停旧的

5.3 健康检查是发布的地基 ​

没有健康检查,编排系统就不知道"新容器到底活没活",常见后果是把流量切给一个还没就绪的新实例。健康检查要区分两种:

类型语义反例
存活(liveness)进程没死只检查 / 是否返回 200
就绪(readiness)可以接流量了数据库连接建立好、依赖初始化完成才返回 200

一个常见误区:健康检查路径只查"进程活着",不查"依赖可用"。数据库挂了还返回 200,编排系统就会把流量送进一个"活着但不可用"的实例。

5.4 回滚:发布流程里必须有的逃生舱 ​

回滚方式怎么做代价
镜像回滚把部署 tag 指回上一个唯一 tag,重新发布秒级,但会重放一次发布(数据库迁移要小心)
蓝绿切回流量直接切回旧环境秒级,旧环境还在就不用重发

回滚的四问清单:

  • 上一个可用版本是什么?(唯一 tag 找得到吗)
  • 数据库迁移是向后兼容的吗?(回滚后新代码访问旧表会不会炸)
  • 回滚后旧版本需要的密钥/配置还在吗?
  • 回滚本身有演练过吗?

没有回滚能力的发布不是发布,是赌博。 每次部署记版本号,出问题 5 分钟内回滚。

5.5 数据库迁移:最容易忽略的"回滚陷阱" ​

前端发版不用管数据库;后端发版最危险的就在这。应用代码可以回滚,数据库 schema 迁移不一定能回滚。

场景风险建议
只增字段/表新旧代码都能跑安全,可放心发布
删字段/改列类型回滚后旧代码访问不存在的列先加字段 → 双写 → 再删,或做"向前兼容"迁移
数据清洗/重建索引时间长、失败难恢复单独脚本、备份、灰度执行

核心原则:让每次发布尽量"可回滚"——迁移保持向前兼容(新增不改删),应用代码先发、schema 破坏性变更后置。


六、发布检查单与演进路线 ​

6.1 上线前检查单 ​

  • [ ] 镜像 tag 唯一且不可变,能精确指回上一版
  • [ ] 密钥不写入镜像/仓库,走环境变量或密钥服务
  • [ ] 健康检查同时覆盖存活与就绪
  • [ ] 有数据库迁移方案,且迁移向前兼容
  • [ ] 回滚有演练,5 分钟内能切回
  • [ ] 日志打到 stdout,线上有日志/指标可看(见 C08)

6.2 演进路线 ​

text
第一步:单机 docker compose + 手动发布(学会镜像与回滚)
第二步:接 CI 自动构建推送 + 服务器拉新镜像(学会流水线)
第三步:蓝绿/金丝雀 + 健康检查(学会发布策略)
第四步:云容器平台 / 编排系统(D03~D08 的领域,按需深入)

自检清单 ​

  • [ ] 能画出"提交 → 测试 → 构建 → 推送 → 部署 → 切流量"的完整链路
  • [ ] 能用 pydantic-settings / 等价工具实现"环境变量优先于 .env"
  • [ ] 能给镜像打唯一 tag,并解释为什么不能覆盖 latest
  • [ ] 能写一个"测试不过就不发布"的 GitHub Actions 流水线
  • [ ] 能说清滚动/蓝绿/金丝雀/重建四种策略各自适合什么
  • [ ] 知道健康检查要分存活与就绪,并写对就绪检查
  • [ ] 知道数据库迁移为什么会影响回滚,并给出向前兼容方案

常见误区 ​

误区事实
"发布 = 上传代码 + 重启"是"有状态的切换过程",要做健康检查与平滑过渡
"每次都用 latest 打镜像"覆盖式 tag 让回滚无从谈起,必须唯一且不可变
"CI 跑完测试就够了"CI 只解决"能不能合","怎么上线、怎么回"是 CD 的事
"健康检查 = 进程能 ping 通"要区分存活与就绪,就绪要查依赖是否可用
"回滚就是重新部署旧版本"有数据库迁移时,旧代码可能访问不到新 schema,要先想迁移兼容性
"蓝绿成本高所以不用"关键服务值得;成本换来的是秒级整体回滚

动手练习 ​

练习 A:给镜像打唯一 tag ​

把 D01 构建的镜像打上 git rev-parse --short HEAD 生成的 tag,推到 GitHub Container Registry(ghcr.io),确认能 docker pull 回来。

练习 B:接一条完整流水线 ​

用上文 GitHub Actions 示例,给 FastAPI + pytest 项目配好 test 和 build-and-push 两个 job,故意留一个失败用例,确认 CI 会阻断发布。

练习 C:演练一次回滚 ​

在服务器上部署 v1,改一行代码发 v2,再把 tag 指回 v1 完成回滚,记录整个过程花了多久;如果超过 5 分钟,找出瓶颈。


相关文档 ​

本区 ​

其他域 ​