02 · 部署与 CI/CD:多环境与发布策略
D01 解决了"把服务打包成镜像",本篇解决更难的一半:怎么把镜像可靠地、可回滚地发布到线上。前端发版是"换静态文件 + 清缓存",后端发版是"换运行中的服务",稍有差池就是线上事故。本篇用前端对照讲清多环境、镜像流转、CI/CD 与四种发布策略。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 从"本地能跑"到"线上稳定":多环境、CI/CD、镜像推送、蓝绿/金丝雀/回滚的完整链路 |
| 核心内容 | 一次发布的完整链路、多环境配置、镜像标签策略、GitHub Actions 最小流水线、四种发布策略、回滚与数据库迁移 |
| 读完能回答 | CI 和 CD 到底谁负责什么?发布出问题怎么在 5 分钟内回滚?为什么不能只做"推到服务器重启"? |
| 相关板块 | D01 · Docker 与容器化 | C17 · 部署与 CI/CD | A16 · 前端 CI/CD |
一、一次发布到底要过几道关
前端发版对照(你已经熟悉的部分):
| 前端发版 | 后端发版 |
|---|---|
| 构建产物是一堆静态文件 | 构建产物是一个在跑的进程 |
| 换文件 + 清 CDN 缓存即可 | 旧进程要平滑退出、新进程要接住流量 |
| 出问题回滚 = 换回旧文件 | 出问题回滚 = 切回旧版本(有状态时要格外小心) |
| 几乎没有"连接中"窗口 | 存在断连窗口,健康检查不到位就报 502 |
一条完整发布链路,每一环都可能成为事故点:
代码提交 → CI 跑测试 → 构建镜像 → 推送到镜像仓库
→ CD 拉取新镜像 → 健康检查通过 → 切流量 → 监控确认前端最容易漏掉的认知:后端发版不是"上传代码重启",而是一个有状态的切换过程。数据库连接、内存缓存、正在处理的请求,都可能在切换瞬间出错。
二、多环境:一套代码,多套配置
2.1 环境从哪来
| 环境 | 谁在用 | 数据 | 典型配置来源 |
|---|---|---|---|
| dev | 自己开发 | 本地库/内存 | .env 本地文件 |
| test/staging | CI 跑测试、发布前验收 | 独立测试库 | CI 平台的环境变量 |
| prod | 真实用户 | 生产库 | 平台环境变量 + 密钥服务 |
铁律:代码只有一份,环境差异全部来自配置。配置里最容易踩的坑是把密钥写进镜像(镜像会被拉取分享,见 D01 第六节)。
2.2 配置来源优先级
按"越外层越优先"的原则读取:
命令行参数 > 环境变量 > .env 文件 > 代码默认值这样本地用 .env,线上用平台注入的环境变量,同一份代码不用改。FastAPI 常用 pydantic-settings 做这件事:
# 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 Registry | GitHub 用户 | 跟仓库权限走,配 Actions 方便 |
| 云厂商镜像服务 / Harbor | 团队/生产 | 内网拉取快、可做镜像扫描 |
3.2 标签(Tag)策略:唯一且不可变
前端最忌"覆盖式发版"(旧版本被新版本覆盖,回滚无门);镜像也一样。禁止复用同一个 tag 反复覆盖,要用唯一且不可变的 tag:
# 反例: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 / mypy | eslint / tsc --noEmit |
| 构建镜像 | docker build | npm run build |
| 推送镜像 | docker push(仅主干分支) | 上传 dist 到 CDN |
4.2 一个最小可用的 GitHub Actions 流水线
# .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) | 停旧起新,有断连窗口 | 停机维护 | 有状态且不能双跑的服务 |
# 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 演进路线
第一步:单机 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 分钟,找出瓶颈。
相关文档
本区
- 01 · Docker 与容器化 —— 镜像构建、Compose 编排(本篇的前置)
- D 域 README —— 运维域索引
其他域
- C17 · 部署与 CI/CD —— 后端视角的部署入门(一篇带过)
- C12 · 错误处理日志配置 —— 多环境与密钥管理
- C08 · 可观测性 —— 上线后怎么看日志/指标
- A16 · CI 与 CD 与部署运维 —— 前端发布流水线对照