# 在我机器上能跑，那就把机器一起发出去

关于 CI/CD 与容器的速成课。21 个概念——deploy、works on my machine、continuous integration、shift left、continuous delivery 与 deployment、artifact、pipeline、fail fast、container、Docker、image、Kubernetes、desired state、rolling update、rollback、canary、blue-green、infrastructure as code——每个都配英文名称、一幅图、一张卡片和一个例子。结尾附一张速查表，列出全部概念以及它们如何连在一起。

- **Author:** Fedor Chishchin
- **Level:** intermediate
- **Reading time:** 约 35 分钟阅读
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/zh/learn/cicd-express

---

## 1. deploy 这个难题

写代码只是工作的一半；把它安全地送到用户面前是另一半。在很长一段时间里，后一半都缓慢、靠手工、令人胆战心惊——而理解它为什么如此，正是理解一切修补手段的关键。

### 课程地图：七组共 21 个概念

> 一张地铁线路图：先看整条线路，再一站一站地坐过去。

这门课跟着一个改动，从笔记本一路走到用户手里。先讲**难题**：为什么手工 deploy 吓人。然后是 **CI**（检查每个改动）、**CD**（把它送到用户那里）、**pipeline**（完成这一切的机器）、**容器**（到处都是同一个环境）、**编排**（很多容器跑在很多服务器上），以及**安全 deploy**（怎样发布才能随时撤销）。

_Diagram: 七组概念——各章就按它们来。_

### 每个概念怎么读

> 一本护照：每个人的栏目都一样，所以很容易比较。

下面每个概念的结构都相同：它的**英文名称**（你在文档和工作中会遇到的术语）、一幅生活中的画面、一张**图**，以及一张有若干栏目的**卡片**——问题或「是什么」、思路、例子、代价或陷阱，以及相关概念。整门课只有一个大想法：**交付软件应该是无聊的、自动化的、处处一致的。**

### Deploy——为什么手工做很吓人

> 一位外科医生在午夜里凭记忆徒手做一台精细的手术，又累又困——手一抖，病人就垮了。这就是手工的生产环境 deploy。

所谓 **deploy**，不过就是「让新版本上线」。过去的做法，是一个人按部就班地手工操作：把文件复制到服务器，敲几条命令，然后祈祷。这又慢又容易出错，风险大到团队都很少 deploy——于是每次 release 都变得庞大，从而风险更高。靠手工去做，它就成了一周里最让人紧张的时刻。整个 CI/CD 领域存在的意义，就是把那个时刻变得无聊。

_Diagram: 恶性循环：手工 deploy 吓人 → 团队很少 deploy → release 越来越庞大 → 更吓人。_

#### Deploy (release, rollout, 发布、上线)

- **是什么:** 让应用的新版本对用户上线。
- **例子:** 手工 deploy：SSH 登进服务器，复制文件，重启进程——然后祈祷。
- **陷阱:** 靠手工又慢又有风险——所以很少做，每次 release 都变得庞大。
- **关联:** 由 **Continuous Delivery** 和 **Pipeline** 自动化；由 **Rollback**、**Canary**、**Blue-green** 变得安全。

### Works on my machine——经典陷阱

> 一份食谱在你自家厨房做出来完美无缺，到了别人厨房却失败了——烤箱不同、面粉不同、海拔不同。食谱并不是故事的全部。

在开发者笔记本上跑得好好的代码，到了生产环境常常崩掉，因为两个**环境**不一样——版本不同、设置不同、库不同、操作系统不同。**「在我机器上能跑」**正是这一整类 bug 的笑点，它们都源于这些差异。笔记本和服务器不是同一个地方，所以「在我构建它的地方能跑」对它实际运行的地方什么都保证不了。

_Diagram: 同样的代码，不同的环境：另一个运行时版本、另一些库、另一个操作系统——于是出现一个只有生产环境才有的 bug。_

#### Works on my machine (environment drift, 环境漂移)

- **是什么:** 一个只在另一个环境里出现的 bug，因为两个环境不一样。
- **例子:** 笔记本上是 Node 20，服务器上是 Node 16——用了较新语言特性的代码，只在生产环境里报错。
- **陷阱:** 代码不是故事的全部：版本、设置、库，以及它周围的操作系统，也都有影响。
- **关联:** 由 **Container** 根治——应用自带环境——也靠 **Artifact**——你测试的就是你发布的。

### Boring releases——发布应该是无聊的

> 一部电梯，你一天进出上百次都不会多想一下——因为它是自动化的、经过测试的、安全的。你绝不会去坐一部每趟都得手工小心操作的电梯。

这门课里所有内容的目标，就是让交付软件变得**无聊**：自动化、经过测试、可重复到这种地步——你一天里多次 release 小改动，却毫无波澜。这有点反直觉：deploy 得 _越_ 勤，反而 _越_ 安全——每次 release 都很小（**small batches**），所以一旦出问题，原因一目了然，修起来也很轻。罕见、庞大、手工的 release 才是危险的那种。「无聊」是一次 deploy 所能赢得的最高赞美。

_Diagram: 上：两次庞大又吓人的 release。下：十次小的——哪一次出问题，一眼就知道。_

#### Boring releases (small batches, frequent small releases, 小步频繁发布)

- **问题:** 一次大 release 里有几百个改动，出问题时没人知道是哪一个干的。
- **思路:** 经常发布小改动——自动化地、毫无波澜地。
- **例子:** 不再一个月发一次——而是一天发好几次很小的 release，每次只有几行。
- **代价:** 需要自动化：没人能靠手工 deploy 得这么勤。
- **关联:** 只有靠 **CI**、**Continuous Delivery** 和 **Rollback** 才可能做到。

> **定调的问题:** 「在这里交付一个改动，是一件平静的、自动化的、稀松平常的事——还是一桩缓慢、靠手工、让人提心吊胆、我能不做就不做的大事？」

> 手工 deploy 又慢、又危险、又罕见——这反而让它更危险。目标恰恰相反：让交付软件自动化到、细碎到无聊的地步，一天里做很多次。

## 2. Continuous Integration 持续集成

CI/CD 的前一半，讲的是在问题一冒头的瞬间就抓住它——做法是在每个改动落地时自动 build 并 test 它——这样那份共享的代码库就始终能正常工作。

### Continuous Integration（CI）——每个改动都被 build 和 test

> 一个拼写检查器，你每打一句话它就检查一句，当场标出错误——而不是一个校对员，一个月后等一切纠缠在一起时，才找出上百个错误。

**Continuous integration（CI）** 的意思是：每当有人提出一个改动，一个自动化系统就会 build 这个项目并对它运行 test——在几分钟之内完成，全程不经人手。如果这个改动让 build 坏掉或没通过某个 test，你立刻就会知道，而此时它还很小、还鲜活地留在你脑子里。CI 是一个常开的哨兵，在每一份贡献能惹麻烦之前就先检查它。

_Diagram: 每个 commit 都进 CI 服务器：build 和 test。通过——可以安全合并；失败——作者几分钟内就知道。_

#### Continuous Integration (CI, 持续集成)

- **问题:** 一个有问题的改动，几周后才被发现，那时它已经和其他一切纠缠在一起了。
- **思路:** 每个改动一到，就自动 build 并 test。
- **例子:** 每个 pull request 上，一个 CI 服务（比如 GitHub Actions）运行 build 和 test，并在上面打一个绿色或红色的标记。
- **代价:** 你需要好的自动化 test——CI 只检查你的 test 检查的东西。
- **关联:** 尽早抓住问题（**Shift left**），保持 **Green main branch**；是任何 **Pipeline** 的第一个阶段。

### Shift left——趁问题还小就抓住它

> 发现一块砖砌歪了，趁早改很容易；等在上面又盖了三层楼才发现，那就是一场灾难。早发现很便宜；晚发现很昂贵。

CI 的价值在于时机。一个 bug 在被引入的当下就被抓住，修起来不费吹灰之力——你清清楚楚知道改了什么。同一个 bug 几周后才被发现，埋在上面盖起来的一切之下，可能要花上几天才能理清。这就是 **shift left** 的想法——把发现问题的时间点在时间线上往左移，更靠近改动发生的那一刻。CI 把发现故障的时机从「很久以后，跟一切混在一起」挪到了「就是现在，孤立地」。

_Diagram: bug 发现得越晚，修复的代价越高。CI 把发现的时间点左移——移到 commit 本身。_

#### Shift left (catch it early, 尽早发现)

- **是什么:** 尽可能早地发现问题——靠近改动发生的那一刻。
- **例子:** commit 3 分钟后某个 test 失败了——你知道是哪一行干的。同一个 bug 一个月后在生产环境里出现——要查好几天。
- **陷阱:** 检查必须快：如果 CI 要跑一个小时，人们就不再等它了。
- **关联:** **CI** 的核心价值；与 **Fail fast** 是同一种精神。

### Green main branch——让 main 始终可用

> 一间共用的厨房，每个人走之前都必须收拾干净——这样下一个人随时都能用，而不是一堆要别人先去收拾的烂摊子。

CI 更深一层的目的，是让 **main 分支**——那个共享的、可信赖的版本——始终处于可用状态。一个改动只有在能 build 并通过 test 时才会并入 main，所以 main 永远不会变成一团堵住整个团队的烂摊子。这正是频繁、有把握地交付得以成立的原因：当 main 始终可用，你随时都可以从它发布。

_Diagram: 分支 A 通过了 CI，合并进 main。分支 B 没通过 CI——修好之前一直留在外面。_

#### Green main branch (main always green, main 始终是绿的)

- **问题:** 一个坏掉的 main 会堵住整个团队，让任何 release 都无法进行。
- **思路:** 只有 build 成功并通过 test 的东西才能进入 main。
- **例子:** 仓库里的一条规则：在 CI 检查全绿之前，「Merge」按钮一直是禁用的。
- **代价:** 有时你得等 CI，并在合并前先修好 test。
- **关联:** 由 **CI** 强制执行；让 **Continuous Delivery** 可以随时从 main 发布。

> **CI 的问题:** 「每个改动在并入共享代码之前，是不是都被自动 build 和 test 过了——这样故障能立刻被发现，main 也始终可用？」

> Continuous integration 在每个改动落地的瞬间就 build 并 test 它，趁问题还小就抓住它，让 main 分支始终处于可用状态。

## 3. Continuous Delivery 持续交付

如果说 CI 确保每个改动都是好的，那么 CD 就负责把这个好改动自动送到用户面前——把 release 从一场手工仪式，变成一个按钮，甚至连按钮都不用。

### Continuous Delivery（CD）——自动化的是发布，不只是测试

> 一条工厂流水线，并不止于质检，而是把合格的产品一路送到装货码头、装上卡车——没人用手搬箱子。

**Continuous delivery（CD）** 是 CI 的延伸：一个改动通过所有自动检查之后，系统还会自动为 release 做好准备，并且可以把它推到生产环境。那些手工的、容易出错的 deploy 步骤，变成了每次都以同样方式运行的代码。**CI 证明改动是好的，CD 把这个好改动送到用户手里**——不需要有人在半夜里小心翼翼地往服务器上敲命令。

_Diagram: CI 负责「改动是好的」，CD 负责「已经送到用户面前」。整条路都是自动化的。_

#### Continuous Delivery (CD, 持续交付)

- **问题:** 改动已经证明是好的，但发布仍靠手工——又慢又出错。
- **思路:** 发布步骤就是代码；每个好改动都自动处于随时可发布的状态。
- **例子:** CI 跑绿之后，一个脚本构建软件包、存起来，再用一个按钮把它推到生产环境。
- **代价:** 你要把整条发布路径一次性写成代码，而且得信任你的自动化 test。
- **关联:** 建立在 **CI** 之上；发布的是 **Artifact**；运行在 **Pipeline** 里。

### Continuous Delivery vs Continuous Deployment——有按钮还是没按钮

> 一个封好、写好地址、放在门边等着的包裹，对比一个已经交给快递员的包裹——都已准备好出发，但前一个要等你发话。

这里有个微妙的区别。**Continuous delivery** 指每个好改动都被自动准备到随时 _可_ deploy 的状态，但要由人按下按钮来发布。**Continuous deployment** 则更进一步：每个通过检查的改动都会自动发布到生产环境，完全没有按钮。两者都叫「CD」；区别在于最后一步是否由人批准。

_Diagram: 上——delivery：由人批准最后一步。下——deployment：通过检查，直接上生产环境。_

#### Continuous Delivery vs Continuous Deployment (CD 里的两个 D)

- **是什么:** Delivery——准备好发布，由人按按钮。Deployment——自己发布，没有按钮。
- **例子:** 银行在签字批准后按一下按钮发布（delivery）；一个网络服务每个绿色 commit 都自己上线（deployment）。
- **陷阱:** 怎么选，取决于你有多信任自己的自动检查。
- **关联:** 两者都属于 **Continuous Delivery**；没有按钮时，**Canary** 和 **Rollback** 就更重要了。

### Artifact——你发布的那个东西

> 一个封好、贴了标签的箱子，只打包一次，然后作为一个整体被运输、存放、送达——而不是每到一站都用散件重新组装。

pipeline 会产出一个 **artifact**——你的应用的一个构建好的、带版本号的、可直接运行的软件包。你只构建一次，这同一个 artifact 会依次经过 testing、staging 和生产环境，所以你测试的就恰恰是你发布的。如果每个阶段都重新构建，细微的差异就可能悄悄混进来。**Build once, deploy everywhere**——一个 artifact，构建一次，到处部署——消除了一整类「可是在测试里是通过的」这种意外。

_Diagram: 只构建一次——v1.4.2 版本去 test，去 staging，去生产环境。沿途不重新构建。_

#### Artifact (build once, deploy everywhere, 一次构建、处处部署)

- **是什么:** 应用的一个构建好的、带版本号的软件包，可直接运行。
- **例子:** `app v1.4.2`——同一个软件包依次经过 test、staging 和生产环境。
- **陷阱:** 每个阶段都重新构建，到达生产环境的就不再是你测试过的那个东西。
- **关联:** **Pipeline** 的产出；对容器来说，artifact 就是 **Image**；让 **Rollback** 成为可能。

> **CD 的问题:** 「一个改动一旦证明是好的，它是不是就作为一个经过测试的 artifact 自动流向用户——还是仍然有人在手工构建、手工 deploy？」

> Continuous delivery 把每个好改动的发布，作为一个构建好的 artifact 自动化——无论是由人按下最后的按钮（delivery），还是它自己上线（deployment）。

## 4. pipeline 流水线

CI 和 CD 由一条 pipeline 来运行：这是一条自动化的装配线，一个改动从 commit 到生产环境都沿着它走，路上只要出问题就会停下。

### Pipeline——自动化的装配线

> 一条汽车工厂的流水线：底盘依次经过焊接、喷漆、检验，每个工位做完自己的活才往下传——没有跳过的步骤，也没有人用手搬运。

**pipeline** 是一个改动在通往生产环境的路上所经过的一系列自动化**阶段**——通常是 **build**，然后 **test**，然后 **deploy**。一个 commit 从一头进去，如果一切顺利，另一头出来的就是一次上线的 release，中间没有任何人工步骤。pipeline 就是让 CI/CD 变成现实的那台具体机器：「把整条路自动化」真正落在这里。

_Diagram: Commit → build → test → deploy → 上线的 release。框里的一切都由机器完成，而不是人。_

#### Pipeline (CI/CD pipeline, 流水线)

- **是什么:** 从 commit 到生产环境之间，一串定义好的自动化阶段。
- **例子:** 仓库里的一个配置文件描述了三个阶段：`build` → `test` → `deploy`；CI 服务在每个 commit 上运行它们。
- **陷阱:** pipeline 也是代码：它需要维护，而且一条很慢的 pipeline 会拖慢整个团队。
- **关联:** **CI** 和 **Continuous Delivery** 的家；由 **Fail fast** 守护；产出 **Artifact**。

### Fail fast——红灯停，绿灯行

> 一条装配线上有一根拉绳，谁都可以拉，一出现缺陷就能让一切立刻停下——线上还有一盏红绿灯：绿灯，工作继续往下流；红灯，一切等待，直到问题被清除。

一条好的 pipeline 就是为 **fail fast** 而建的：如果在任何一个阶段 build 坏了或 test 失败了，pipeline 就停下，这个改动不再往前走。pipeline 用**绿色**（通过）和**红色**（失败）来说话。这个简单的信号，用一道毫不含糊的关卡取代了焦虑的人工判断。团队的规矩很简单：**红灯时不在上面继续构建，红灯时不发布**。

_Diagram: test 变红了——流水线停了：deploy 根本不会运行，生产环境毫发无损。绿灯继续，红灯停止。_

#### Fail fast (red / green, 红 / 绿)

- **问题:** 如果流水线不停下，一个有问题的改动可能一路滚到生产环境。
- **思路:** 任何一个阶段变红，pipeline 立刻停下；只有绿色才往下走。
- **例子:** 两个 test 失败了——`deploy` 阶段连启动都不会；团队先去修。
- **代价:** 不稳定的 test 会无缘无故让流水线停下——必须把它们修好。
- **关联:** **Pipeline** 的安全属性；与 **Shift left** 是同一种精神。

> **pipeline 的问题:** 「每个改动是不是都走同一条自动化的 build-test-deploy 流水线，任何阶段一变红就立刻戛然而止——这样没有任何坏东西能到达生产环境？」

> pipeline 是从 commit 到生产环境的自动化装配线——build、test、deploy——它 fail fast，遇红就停，所以没有哪个有问题的改动能溜过去。

## 5. 容器

故事的另一半直接解决「在我机器上能跑」：把应用连同它需要的一切打包成一个单元，让它在任何地方都跑得一模一样。这个单元就是容器。

### Container——应用连同它的环境

> 宇航员的航天服自带空气、温度和气压——所以无论在空间站里还是在太空中，人都能一样地工作，因为他把自己的环境带在身上。

**container** 把你的应用和它运行所需的一切——代码、库、设置、正确的版本——打包成一个自成一体的单元。因为它自带环境，所以无论你把它放在哪里，都跑得一样：你的笔记本、测试服务器、生产环境。容器不依赖宿主机上装了什么，所以造成「在我机器上能跑」的那些差异就这样被填上了。

_Diagram: 容器里面：代码、库、设置和运行时。这就是它在笔记本上和生产环境里跑得一样的原因。_

#### Container

- **是什么:** 应用加上它的整个环境，打包成一个自成一体的单元。
- **例子:** 里面装着你的 API 和 Node 20 的容器，在 macOS 笔记本和 Linux 服务器上跑得一样。
- **陷阱:** 容器修好的是环境，不是你代码里的 bug——你仍然需要 test。
- **关联:** 根治 **Works on my machine**；从 **Image** 启动；大规模由 **Kubernetes** 管理。

### Docker——给它命名的那个集装箱

> 钢制的海运集装箱：任何港口、轮船、卡车处理它的方式都一样，因为这个箱子是标准的，不管里面装的是什么。全世界的货物就靠这一个想法在流动。

这个名字本身就是比喻。在有标准集装箱之前，货物是一件一件笨拙地装上去的；标准箱让任何起重机、任何轮船都能以同样的方式处理任何货物。软件的**容器**对应用做的是同一件事：一个标准单元，任何兼容的系统都能运行它，而不必关心里面是什么。**Docker** 就是让这一切成为主流的工具。把箱子标准化，才让整条物流链——也就是你的 pipeline——顺畅运转。

_Diagram: 就像任何起重机、轮船和卡车都能处理标准箱，任何笔记本、CI runner 和云都能运行 Docker image。_

#### Docker

- **是什么:** 让容器成为主流的工具：它构建并运行容器。
- **例子:** `docker build` 根据 Dockerfile 构建一个 image；`docker run` 从它启动一个容器。
- **陷阱:** 标准化的是箱子，不是里面的东西：里面装的仍然得是一个能正常工作的应用。
- **关联:** 制造 **Container** 和 **Image**；由 **Kubernetes** 运行它们。

### Image vs Container——食谱和菜

> 一份食谱，对比照它做出来的那顿饭：一个是固定的一套说明，另一个是一个具体的实例，你可以做出很多份，份份一样。

有两个词常被混淆。**image** 是打包好的、冻结的蓝图——包含你的应用及其环境的构建产物，存放在仓库里（一个 **registry**）。**container** 是从这个 image 启动的、正在运行的实例。一个 image 可以启动许多一模一样的容器，就像一份食谱能做出许多一模一样的菜。你在 pipeline 里只构建一次 image，然后想跑多少容器就从它跑多少——每一个都是一样的。

_Diagram: Dockerfile → build → image（存在 registry 里）→ run → 你需要多少，就有多少个一模一样的运行中的容器。_

#### Image vs Container (食谱和菜)

- **是什么:** Image——冻结的蓝图（食谱）。Container——它正在运行的实例（菜）。
- **例子:** 一个 image `api:1.4.2`——五个一模一样的运行中的容器。
- **陷阱:** 在运行中的容器里改东西没有意义——应该改 image，然后重新构建。
- **关联:** 对容器来说，image 就是 **Artifact**；**Kubernetes** 让这些副本持续运行。

> **容器的问题:** 「这个应用是不是把它的整个环境装在容器里带着走，所以它在我的笔记本上和生产环境里跑得一模一样——而不是依赖碰巧装了什么？」

> 容器把应用连同它需要的一切打包，所以它在哪里都跑得一样。你只构建一次 image（食谱），然后从它运行一模一样的容器（菜）。

## 6. Orchestration 编排

运行一个容器很容易。要在许多机器上运行几百个——让它们保持健康、随需求伸缩、不停机地更新——就需要一位指挥。这就是编排。

### Orchestration / Kubernetes——容器的指挥

> 一位空中交通管制员在调度满天的飞机——决定哪架降落在哪里，遇到问题就改道，让几百个移动的部件保持安全、协调。

当你有许多容器分散在一大批服务器上时，必须有个东西来决定每个容器跑在哪里、重启挂掉的那些、并均衡负载。这就是 **orchestration**，占主导地位的工具是 **Kubernetes**。你告诉它想要的状态——「把这个服务跑五份」——它就持续不断地努力让现实与之相符：安放容器、盯着它们、修正偏差。

_Diagram: 你说「5 份」——Kubernetes 自己把它们分散到各台服务器（节点）上，盯着它们，并在挂掉时重启。_

#### Kubernetes (K8s, container orchestrator, 容器编排器)

- **问题:** 没人能靠手工盯住许多服务器上的几百个容器。
- **思路:** 编排器决定每个容器跑在哪里，重启挂掉的，并均衡负载。
- **例子:** 你在 YAML 里用 `replicas: 5` 描述一个服务——Kubernetes 就在不同服务器上维持五份副本。
- **代价:** 一个有自己一套语言的复杂系统；对一个小服务来说往往大材小用。
- **关联:** 从 **Image** 运行 **Container**；保存 **Desired state**；执行 **Rolling update**。

### Desired state——自愈与自动伸缩

> 一个你设定了目标温度的恒温器：它不停地感知房间，自己调节，你不用再碰它。你声明目标，它来维持。

编排是**声明式**的：你描述目标，而不是步骤。说一句「始终保持五份健康的副本在运行」，如果有一份崩了，系统会自动启动一个替补——这就是 **self-healing**（自愈）。如果流量激增，它可以增加副本，需求回落时再撤掉——这就是 **auto-scaling**（自动伸缩）。你不再手工照看服务器，而是声明你想要什么。从「做」到「声明」的这个转变，正是它的核心。

_Diagram: 一个无尽的循环：把期望的状态和实际的状态对比，修正偏差。一份副本崩了——就启动一个新的。_

#### Desired state (declarative, self-healing, auto-scaling, 声明式、自愈、自动伸缩)

- **是什么:** 你声明目标（「5 份健康副本」）；系统不断把现实拉回到目标上。
- **例子:** 夜里一份副本崩了——几秒钟内就有新的顶上，没人被叫醒。
- **陷阱:** 系统会精确地维持你所描述的东西：目标写错了，它也会同样固执地维持错的。
- **关联:** **Kubernetes** 的核心；与 **Infrastructure as Code** 是同一种「描述它，而不是手工去做」的想法。

### Rolling update——不停机地发布

> 在行驶的卡车上一个一个地换轮胎，让它永远不用停下——车一直在往前开，每个部件却在底下被换掉。

编排让你可以通过 **rolling update** 不停机地更新：它每次替换几个旧容器为新容器，并在继续之前检查每个新容器是否健康（一次 **health check**），所以服务自始至终都在线。如果新版本表现不对，它可以停下来，并自动 **roll back** 到旧版本。现代服务就是这样在没有维护窗口的情况下发布更新的——在线、逐步、可逆。

_Diagram: 副本一次几个地从 v1 切到 v2，每个都经过检查——服务一路都在线。_

#### Rolling update

- **问题:** 靠停掉服务来更新，意味着用户要经历停机。
- **思路:** 一次替换几份副本，并对每个新副本做 health check。
- **例子:** 四份 v1 副本中，先有两份变成 v2，再轮到另外两份——服务一秒钟都没停过。
- **代价:** 有一段时间两个版本同时在运行——它们必须能够并存。
- **关联:** 由 **Kubernetes** 完成；出问题时——**Rollback**；近亲——**Canary** 和 **Blue-green**。

> **编排的问题:** 「谁在让这些容器保持运行、按需求伸缩它们、不停机地更新它们——是我在手工做，还是我声明了期望状态，让编排器来维持它？」

> 编排（Kubernetes）在许多机器上运行许多容器：你声明期望状态，它就自愈、自动伸缩，并且不停机地推出更新。

## 7. 安全地 deploy

这一整套机制，都是为了让交付既安全又频繁。最后这些习惯，是为了缩小任何一次 release 的爆炸半径——因为最安全的 deploy，是你能撤销的那一种。

### Rollback——永远留一条退路

> 一份带「撤销」的文档：你放心大胆地做一个大改动，因为知道按一下键就能恢复上一个版本，所以可以自由尝试，而不是害怕每一次编辑。

最重要的安全属性是 **rollback**——当一次 release 出了问题时，能立刻回到上一个已知良好的版本。因为 pipeline 发布的是带版本号的 artifact，所以退回去就只是把前一个重新 deploy 一遍。知道自己能在几秒钟内撤销一次 deploy，正是让频繁发布变得安全的原因：一个坏版本只是一个小小的、可逆的波折，而不是一场危机。**永远不要发布你收不回来的东西。**

_Diagram: v43 坏了——重新 deploy 上一个好的 artifact，v42。几秒钟，而不是通宵救火。_

#### Rollback

- **问题:** 一个坏的 release 已经在生产环境里了，而修复要花好几个小时。
- **思路:** 立刻把上一个好版本放回去，之后再从容地修。
- **例子:** v43 在报错——一条命令就把 artifact v42 放回生产环境。
- **代价:** 需要带版本号的 artifact；数据库的改动比代码更难回滚。
- **关联:** 靠 **Artifact** 才行得通；内置于 **Rolling update**、**Canary** 和 **Blue-green** 之中。

### Canary release——先给少数用户

> 在把新菜端给整场宴席之前，先让几位愿意尝鲜的客人试一口——如果味道不对，只有几个人注意到，你什么也没损失。

**canary** deploy 会先把新版本发给一小部分用户。如果他们那边的指标保持健康，你就把它推给所有人；如果不健康，你把它撤回，几乎没影响到任何人。它把一次 release 从「全赢或全输」的赌博，变成一个受控的、可逆的步骤，并让一次失误的**爆炸半径**保持很小。

_Diagram: 路由器把 5% 的流量发给新版本。指标正常——推给所有人；不正常——撤回。_

#### Canary release

- **问题:** 新版本里的一个 bug 会一下子打到所有用户。
- **思路:** 先把新版本交给一小部分流量，并观察指标。
- **例子:** 5% 的用户用上 v2；错误并不比 v1 多——比例逐步升到 100%。
- **代价:** 需要能按百分比拆分流量，并且要有好的指标来判断。
- **关联:** 不好就 **Rollback**；邻居——**Blue-green** 和 **Rolling update**。

### Blue-green deployment——两套环境和一个开关

> 剧院里两个一模一样的舞台：演出在一个上面进行，同时另一个上面布置新的一场。到了合适的时机，观众只需转向第二个——而第一个仍然备好，万一要退回去。

**Blue-green** 保持两套生产环境，并把流量一次性地从旧的（**blue**）切到新的（**green**），需要时还能瞬间切回来。新版本在旧版本仍然服务用户的时候就部署好并检查完毕；然后一个开关——所有人就都在新版本上了。和 canary 一样，它把一次 release 变成一个受控的、可逆的步骤。

_Diagram: 路由器一次开关就把全部流量从 blue（v1）挪到 green（v2）；blue 保持待命，以备 rollback。_

#### Blue-green deployment

- **问题:** 在线切换到新版本有风险，而且难以撤销。
- **思路:** 两套完整的环境；流量一次性从旧的翻到新的。
- **例子:** v2 在 green 里启动并检查完毕；路由器把 100% 的流量发给它；出问题——切回 blue。
- **代价:** 你要维持两套完整的环境——release 期间资源翻倍。
- **关联:** 瞬间 **Rollback**；与 **Canary** 不同，它一次性切换所有人。

### Infrastructure as Code（IaC）——用文件描述你的服务器

> 一张蓝图，你能照着它把整栋房子原样重建出来；对比一栋凭记忆盖的房子，一旦烧毁，没人能把它复原。

同样的纪律也延伸到服务器本身。**Infrastructure as code（IaC）** 的意思是：用文件来定义你的服务器、网络和配置，而不是在控制台里手工点来点去。这样，你的整个环境就是带版本的、可审查的、可复现的——你能把它原样重建，改动也和代码一样走同一条 pipeline。它堵上了最后一个缺口：免得「有人手工改了个设置然后忘了」，把别处所有的严谨都一笔勾销。

_Diagram: 基础设施文件 → 在 git 里评审 → pipeline 把它们应用到 staging 和生产环境。不在控制台里手工点击。_

#### Infrastructure as Code (IaC, 基础设施即代码)

- **问题:** 有人手工改了服务器的设置然后忘了——环境没法复现。
- **思路:** 用文件描述服务器、网络和设置，并通过同一条 pipeline 来改动它们。
- **例子:** Terraform 文件描述一台服务器和一个数据库；改动要经过评审，再由 pipeline 应用。
- **代价:** 多学一门语言，再加一点纪律：不在控制台里手工做「快速修复」。
- **关联:** 经由 **Pipeline** 流转；与 **Desired state** 是同一种声明式的想法。

#### 在你依赖一套 deploy 流程之前

- **是不是自动化的**——从 commit 到生产环境全程走 pipeline，而不是靠手工？
- **CI 是不是** build 并 test 每个改动，让 main 始终可用？
- **你发布的是不是同一个 artifact** 或容器，从 test 到生产环境完全一致？
- **你能不能立刻 rollback** 到上一个好版本？
- **有风险的 release 是不是逐步推出**——canary 或 blue-green，而不是一下子全上？
- **基础设施本身是不是代码**——带版本、可复现？

> 安全的交付就是可逆的交付：把 pipeline 自动化，发布一个经过测试的 artifact，留一个一键 rollback，把有风险的改动逐步推出，并把基础设施定义成代码。

## 8. 一切如何连在一起

二十一个概念其实是一条线。一个改动通过 CI，变成一个 artifact，由 CD 交付，由编排器运行，而安全发布的各种技巧则在出问题时接住你。在这一切之下，是被写成代码的基础设施。

### 一张图：从 commit 到用户

> 一个包裹的路线：只打包一次，在仓库里检查，开车送出，交到手上——而且每一步都可以退回给寄件人。

从左到右读这张图：**Commit** → **CI**（build 和 test；红灯就停）→ **Image**（artifact，只构建一次）→ **CD**（release）→ **Kubernetes**（运行它并维持 desired state）。怎样安全地发布，由 **Rolling update**、**Canary**、**Blue-green** 和 **Rollback** 来决定。而 **Infrastructure as Code** 则是把同样的规则用到服务器本身：它们也要走 pipeline。

_Diagram: 上面是 pipeline，中间是安全发布的各种技巧，基础设施即代码是走同一条路的地基。_

#### 你交付得好的迹象

- 发布是**无聊的**——小、自动化、经常做，而不是一件罕见又吓人的事。
- 任何东西合并之前，**CI 都是绿的**，而且 **main 始终可用**。
- 你一路发布到生产环境的，是你测试过的**同一个 artifact/容器**。
- 一个坏的 release 只是一次快速的 **rollback**，而不是一场危机。
- 有风险的改动通过 **canary 或 blue-green** 推出，而你的基础设施是**代码**。

## 速查表：全部 21 个概念

### deploy 这个难题

- **Deploy** (release): 让新版本上线。靠手工——又慢、又吓人、又罕见。
- **Works on my machine** (environment drift): 由环境不同引起的 bug：同样的代码，不同的版本和设置。
- **Boring releases** (small batches): 小而频繁的 release，比罕见而庞大的更安全。

### Continuous Integration

- **Continuous Integration** (CI): 每个改动在几分钟内被自动 build 并 test。
- **Shift left** (catch it early): bug 发现得越早，修复得越便宜。
- **Green main branch** (main always works): 只有通过 CI 的东西才能进入 main。

### Continuous Delivery

- **Continuous Delivery** (CD): 好改动的发布被自动化：deploy 的步骤就是代码。
- **Delivery vs Deployment** (有按钮还是没按钮): Delivery——由人按按钮；deployment——直接上生产环境。
- **Artifact** (build once, deploy everywhere): 一个构建好的软件包流经每个环境——你测试的就是你发布的。

### Pipeline

- **Pipeline** (CI/CD pipeline): 自动化的流水线：build → test → deploy。
- **Fail fast** (red / green): 一个阶段变红，流水线就停；只有绿色才往下走。

### 容器

- **Container** (应用 + 环境): 应用连同它的环境——在哪里都跑得一样。
- **Docker** (工具): 让容器成为标准「箱子」的那个工具。
- **Image vs Container** (食谱和菜): image 是冻结的蓝图；container 是它正在运行的实例。

### Orchestration

- **Kubernetes** (编排器): 把容器分散到各台服务器上，盯着它们，重启它们。
- **Desired state** (self-healing, auto-scaling): 你声明目标——系统让现实一直停在那里。
- **Rolling update** (zero downtime): 一次替换几份副本——不停机。

### 安全的 deploy

- **Rollback** (撤销): 立刻放回上一个好版本。
- **Canary release** (先给少数用户): 新版本先发给一小部分用户。
- **Blue-green deployment** (两套环境): 流量一次性从旧环境翻到新环境——也能翻回来。
- **Infrastructure as Code** (IaC): 服务器和网络用文件描述，并通过 pipeline 改动。

> 交付应该是无聊的：把路径自动化，发布同一个经过测试的 artifact，并且永远留一条退路。

速成课结束 · 8 章 · 21 个概念

接下来是实践：找一个小项目，写一个 Dockerfile 让它在容器里跑起来，再搭一条 pipeline，在每次改动时 build 并 test 它。然后 deploy 它，弄坏它，再 rollback。当一次 release 不再让你害怕，因为你知道流水线会把问题拦住、你也能把它撤销时，整套哲学就豁然开朗了。但要把一个想法放在其他所有想法之上：交付应该是无聊的。把路径自动化，让每个改动都以同样的方式被 build、被 test、可被 deploy，再把应用打包，让它在任何地方都跑得一模一样——于是「在我机器上能跑」终于变成了「在哪里都能跑」。
