速成课 · No. 20
有两个想法改变了软件从开发者笔记本走到真实用户手中的方式。第一,把从代码到生产环境的整条路自动化,让发布变得无聊而不是吓人——这就是 CI/CD。第二,把应用连同它需要的一切一起打包,让它在任何地方都跑得一模一样——这就是容器。两者结合,把「在我机器上能跑」从借口变成了正经的计划。
只讲精髓 · 每个想法一个画面 · 掌握术语
写代码只是工作的一半;把它安全地送到用户面前是另一半。在很长一段时间里,后一半都缓慢、靠手工、令人胆战心惊——而理解它为什么如此,正是理解一切修补手段的关键。
手工 deploy 又慢又吓人
一位外科医生在午夜里凭记忆徒手做一台精细的手术,又累又困——手一抖,病人就垮了。这就是手工的生产环境 deploy。
过去交付软件的方式,是一个人按部就班地手工操作:把文件复制到服务器,敲几条命令,然后祈祷。这又慢又容易出错,风险大到团队都很少 deploy——于是每次 release 都变得庞大,从而风险更高。所谓 deploy,不过就是「让新版本上线」,可一旦手工去做,它就成了一周里最让人紧张的时刻。整个 CI/CD 领域存在的意义,就是把那个时刻变得无聊。
「在我机器上能跑」这个陷阱
一份食谱在你自家厨房做出来完美无缺,到了别人厨房却失败了——烤箱不同、面粉不同、海拔不同。食谱并不是故事的全部。
在开发者笔记本上跑得好好的代码,到了生产环境常常崩掉,因为两个环境不一样——版本不同、设置不同、库不同、操作系统不同。**「在我机器上能跑」**正是这一整类 bug 的笑点,它们都源于这些差异。更深层的问题在于:笔记本和服务器不是同一个地方,所以「在我构建它的地方能跑」对它实际运行的地方什么都保证不了。
release 本该是无聊的
一部电梯,你一天进出上百次都不会多想一下——因为它是自动化的、经过测试的、安全的。你绝不会去坐一部每趟都得手工小心操作的电梯。
这门课里所有内容的目标,就是让交付软件变得无聊:自动化、经过测试、可重复到这种地步——你一天里多次 release 小改动,却毫无波澜。这有点反直觉:deploy 得 越 勤,反而 越 安全——每次 release 都很小,所以一旦出问题,原因一目了然,修起来也很轻。罕见、庞大、手工的 release 才是危险的那种。「无聊」是一次 deploy 所能赢得的最高赞美。
手工 deploy 又慢、又危险、又罕见——这反而让它更危险。目标恰恰相反:让交付软件自动化到、细碎到无聊的地步,一天里做很多次。
CI/CD 的前一半,讲的是在问题一冒头的瞬间就抓住它——做法是在每个改动落地时自动 build 并 test 它——这样那份共享的代码库就始终能正常工作。
每个改动都被自动 build 和 test
一个拼写检查器,你每打一句话它就检查一句,当场标出错误——而不是一个校对员,一个月后等一切纠缠在一起时,才找出上百个错误。
continuous integration(CI)的意思是:每当有人提出一个改动,一个自动化系统就会 build 这个项目并对它运行 test——在几分钟之内完成,全程不经人手。如果这个改动让 build 坏掉或没通过某个 test,你立刻就会知道,而此时它还很小、还鲜活地留在你脑子里。CI 是一个常开的哨兵,在每一份贡献能惹麻烦之前就先检查它。
趁问题还小就抓住它
发现一块砖砌歪了,趁早改很容易;等在上面又盖了三层楼才发现,那就是一场灾难。早发现很便宜;晚发现很昂贵。
CI 的价值在于时机。一个 bug 在被引入的当下就被抓住,修起来不费吹灰之力——你清清楚楚知道改了什么。同一个 bug 几周后才被发现,埋在上面盖起来的一切之下,可能要花上几天才能理清。通过对每个改动立刻 test,CI 让问题始终微小且可追溯。它把发现故障的时机从「很久以后,跟一切混在一起」挪到了「就是现在,孤立地」。
让 main branch 始终能正常工作
一间共享厨房,每个人离开前都必须收拾干净——于是它对下一个人来说始终可用,绝不会变成一堆别人得先去清理的烂摊子。
CI 更深的目的,是让 main branch——那个共享的、被信任的版本——始终处于能正常工作的状态。一个改动只有在能 build 且通过 test 的前提下才会并入 main,所以 main 永远不会变成一团乱、把整个团队卡住。这正是频繁而有底气地交付软件成为可能的原因:当 main 始终能工作,你随时都能从它上面 release,而不必先去理清别人那半成品的烂摊子。
continuous integration 在每个改动落地的瞬间就 build 并 test 它,趁问题还小就抓住它,并让 main branch 始终处于能工作的状态。
如果说 CI 确保每个改动都是好的,那么 CD 就把这个好改动自动送到用户手中——让 release 从一场手工仪式变成一个按钮,或者干脆连按钮都不要。
把 release 自动化,而不只是把 test 自动化
一条工厂流水线,不在质检处停下,而是把合格的产品一路送到装货台、装上卡车——没人去手抱箱子。
continuous delivery(CD)是对 CI 的延伸:在一个改动通过了所有自动检查之后,系统还会自动把它准备好以供 release,并能把它推上生产环境。那些手工的、容易出错的 deploy 步骤,变成了每次都以同样方式运行的代码。CI 证明这个改动是好的;CD 则把这个好改动送到用户手中,而不必有人在午夜里小心翼翼地往服务器里敲命令。
delivery 与 deployment 的区别
一个已封好、写好地址、就摆在门边的包裹,对比一个已经交到快递员手上的包裹——两者都已就绪,但前者还在等你发话。
这里有个微妙的分野。continuous delivery 的意思是,每个好的改动都被自动准备到 可以 deploy 的状态,但由人按下按钮才 release 它。continuous deployment 走得更远:每个通过检查的改动都自动 release 到生产环境,根本没有按钮。两者都叫「CD」;区别只在于最后那一步是否要人来批准。团队的选择,取决于他们对自己的自动检查有多信任。
artifact 才是你真正交付出去的东西
一个封好、贴好标签、只打包一次的箱子,此后作为一个整体被运输、存放、送达——而不是在每一站都从零散部件重新组装。
pipeline 会产出一个 artifact——你应用的一个单一的、构建好的、带版本的包,随时可以运行。你构建它一次,然后正是这一个完全相同的 artifact 穿过 test、staging 和生产环境,于是你测过的,正是你交付的。这对可靠性至关重要:如果你在每个阶段都重新构建,微妙的差异就可能悄悄混进来。一个 artifact,构建一次,处处 deploy,消除了一整类「可它在 test 里明明过了」的意外。
continuous delivery 把每个好改动的 release 自动化为单一的、构建好的 artifact——无论是由人按下最后那个按钮(delivery),还是它自己发出去(deployment)。
CI 和 CD 由一条 pipeline 来运行:一条自动化的装配线,改动从 commit 一路走到生产环境,途中一旦出岔子就停下。
pipeline 是一条自动化的装配线
一条汽车工厂流水线:底盘按顺序经过焊接、喷漆、检验,每个工位都先干完自己的活再往下传——不跳过任何一步,也没有手工搬运。
pipeline 是一个改动在通往生产环境的路上要依次经过的那一连串自动化阶段——通常是 build,然后 test,再然后 deploy。一个 commit 从这头进去,如果一切顺利,一次上线的 release 就从那头出来,中间没有任何人手的步骤。pipeline 是让 CI/CD 落到实处的那台具体的机器:「把整条路自动化」真正栖身之处,就在这里,以一个个定义清楚、可重复的阶段呈现。
fail fast:出了问题就把流水线停下
一条装配线,上面有一根绳子,任何人都能一拉就在缺陷出现的瞬间叫停一切——这远好过让有瑕疵的产品一路滚到装货环节。
一条好的 pipeline 被设计成会 fail fast:如果在任何阶段 build 坏了或某个 test 没通过,pipeline 就停下,这个改动不再往前走一步。一个坏掉的改动永远到不了生产环境,因为它一被抓住,流水线就停了。这是整套设置的安全特性——每个改动都必须穿过每一道关卡,而单单一盏红灯就能把 release 彻底叫停。任何有瑕疵的东西都溜不到用户那里。
green 就走,red 就停
流水线上的一盏红绿灯:绿灯,活儿就往下流;红灯,一切都等着,直到问题被排除。没有什么需要拿捏的判断,只有一个清晰的信号。
pipeline 用 green(通过)和 red(失败)说话。pipeline 绿了,意味着每一项检查都通过、这个改动可以安全往下走;红了,意味着有东西坏了、必须先修好才能让任何东西动一步。这个简单的信号,用一道毫不含糊的关卡,取代了那种焦虑的手工拿捏。团队的规矩于是变得简单:绝不在 red 上接着构建,绝不在 red 上 release——让 pipeline 保持 green,交付软件就默认安全。
pipeline 是那条从 commit 到生产环境的自动化装配线——build、test、deploy——它会 fail fast,一遇 red 就停,于是没有任何坏掉的改动能溜过去。
故事的另一半,直接解决「在我机器上能跑」:把应用连同它需要的一切打包成一个单元,让它在任何地方都跑得一模一样。那个单元,就是容器。
容器把应用连同它需要的一切一起打包
一套宇航服,自带空气、温度和气压——于是无论在空间站里还是在开放太空中,人都以同样的方式工作,把自己的环境随身带着走。
一个 容器 把你的应用,连同它运行所需的一切——代码、库、设置、对的版本——捆成一个自包含的单元。因为它随身带着自己的环境,所以你把它放到哪里,它都跑得一样:你的笔记本、一台 test 服务器、生产环境。容器不依赖宿主机上装了什么,于是那些导致「在我机器上能跑」的差异,就这么消失了。
给它起名的那种海运集装箱
钢制海运集装箱:任何港口、任何船、任何卡车,都以同样的方式处理它,因为这个箱子是标准的,不管里面装的是什么。全世界的货物,都建立在这一个想法之上。
这个名字本身就是那个比喻。在标准海运集装箱出现之前,货物是一件一件别别扭扭地装上去的;而那个标准箱子,让任何吊车、任何船都能以完全相同的方式处理任何货物。软件的 容器 对应用做了同样的事:一个标准单元,任何兼容的系统都能运行它,而不必在乎里面是什么。Docker 正是把这件事推向主流的工具。把箱子标准化,正是让整条物流链——也就是你的 pipeline——顺畅运转的关键。
image 与容器:食谱和菜
一份食谱,对比照着它做出来的那道菜:一个是固定的指令集合,另一个是你能照着做出许多份的某个具体实例,而且份份相同。
有两个词常被搞混。一个 image 是那份打包好、冻结住的蓝图——那个构建好的 artifact,装着你的应用和它的环境,搁在存储里。一个 容器 则是从那个 image 启动起来的、正在运行的实例。一个 image 能启动许多个一模一样的容器,就像一份食谱能做出许多份一模一样的菜。你在 pipeline 里把 image 构建一次,然后按需从它启动任意多个容器——而每一个都一模一样。
容器把应用连同它需要的一切一起打包,于是它在任何地方都跑得一样。你把一个 image 构建一次(食谱),再从它启动一模一样的容器(菜)。
跑一个容器很容易。跨许多机器跑成百上千个容器——让它们保持健康、给它们扩容、不停机地更新它们——就需要一位指挥。那就是 orchestration。
orchestration 跨许多机器跑许多容器
一位空中交通管制员,管理着满天的飞机——决定哪架在哪里降落,绕开各种问题改道,让成百上千个移动的部件既安全又协调。
一旦你有许多容器铺在一队服务器上,就总得有什么东西来决定每一个在哪里跑、把死掉的那些重启起来、并均衡负载。那就是 orchestration,而占主导地位的工具是 Kubernetes。你告诉它你想要的状态——「跑这个服务的五份副本」——它就持续地努力让现实与之相符:摆放容器、盯着它们、纠正偏离。它就是协调整支容器乐队的那位指挥。
self-healing 与扩容是内置的
一个你设到目标温度的恒温器:它不停地感知房间并自行调节,无需你再碰它一下。你声明目标;它来维持。
orchestration 是 声明式 的:你描述目标,而不是步骤。说一句「始终保持五份健康的副本在跑」,一旦有一个崩了,系统就自动启动一个替补——self-healing。如果流量飙升,它能加更多副本,等需求回落时再撤掉——auto-scaling。你不再手工伺候服务器,而是声明你想要什么,让编排器把系统稳稳维持在那里。从「亲手做」到「声明」的这个转变,正是它的核心。
rolling update 不停机地交付更新
给一辆行进中的卡车一个一个地换轮胎,让它根本不必停下——车一边滚着,每个部件一边在它底下被换掉。
orchestration 通过 rolling update 让你零停机地更新:它把旧容器一次几个地替换成新的,每个都先确认健康再继续,于是服务全程在线。如果新版本表现失常,它能停下并自动 rollback 回旧版本。现代服务正是这样不需要维护窗口就持续交付更新的——这个替换是在线进行的、渐进的、可逆的。
orchestration(Kubernetes)跨许多机器跑许多容器:你声明想要的状态,它就 self-heal、auto-scale,并不停机地推出更新。
所有这些机器存在的意义,就是让交付软件既安全又频繁。最后这些习惯,讲的是缩小任何一次 release 的波及范围——因为最安全的 deploy,是你能撤销的那一种。
永远留一条退路:rollback
一份带撤销功能的文档:你大胆地改,因为知道一个按键就能恢复上一个版本,于是你放开手脚去试,而不是怕每一次编辑。
最重要的安全特性是 rollback——在一次 release 出岔子时,能立刻回到上一个已知良好的版本的能力。因为 pipeline 交付的是带版本的 artifact,回退不过就是重新 deploy 上一个。知道你能在几秒内撤销一次 deploy,正是频繁 release 之所以安全的原因:一个坏版本只是一个小小的、可逆的闪失,而不是一场危机。绝不要交付你收不回来的东西。
缩小波及范围:canary 与 blue-green
在端给整桌宴席之前,先让几位愿意的客人尝尝一道新菜——要是味道不对,只有几个人察觉,而你什么也没损失。
有两种模式能缩小一次 release 的风险。canary 这种 deploy 先把新版本送给一小撮用户;如果他们那边的指标保持健康,你就把它推给所有人,如果不健康,你就把它撤回,而几乎没影响到任何人。blue-green 则保留两套生产环境,把流量从旧的(blue)一下子切到新的(green),需要时能瞬间切回去。两者都把一次 release 从一场孤注一掷的赌博,变成了一个受控的、可逆的步骤。
把你的基础设施描述成代码
一张能照着把整栋房子分毫不差重建起来的蓝图,对比一栋凭记忆盖起来、一旦烧毁就没人能复刻的房子。
同样的纪律也延伸到服务器本身。infrastructure as code(IaC)的意思是,把你的服务器、网络和配置定义在文件里,而不是手工点着控制台一路点过去。如今你的整个环境都带版本、可评审、可复现——你能把它分毫不差地重建,而且改动会走和代码一样的那条 pipeline。它堵上了最后那道缝隙:在别处一切都严丝合缝时,「有人手工改了个设置又忘了」本可以把这一切严谨全部毁掉。
- 它是不是端到端自动化的——从 commit 到生产环境走一条 pipeline,而不是靠手工? - CI 是不是 会 build 并 test 每个改动,让 main 始终能工作? - 你交付出去的是不是一个单一的 artifact 或容器,从 test 到生产环境都一模一样? - 你能不能立刻 rollback 回上一个良好版本? - 有风险的 release 是不是渐 进地发出去——用 canary 或 blue-green,而不是一次性全发? - 基础设施本身是不是代码——带版本、可复现?
- deploy / release——让一个新版本对用户上线。 - CI(continuous integration)—— 自动 build 并 test 每个改动。 - CD(continuous delivery / deployment)——把 release 自动化; 有按钮,或没按钮。 - pipeline / build / artifact / fail fast——那条自动化的流水线、 它的产出,以及它的安全机制。 - container / image / Docker——带着环境的应用、那份蓝图、那个工具。 - orchestration / Kubernetes / self-healing / rolling update——大规模、安全地运行 容器。 - rollback / canary / blue-green / infrastructure as code——一次安全 deploy 的那些模式。
- 交付软件是无聊的——细碎、自动化、做得很勤,而不是一桩罕见而吓人的大事。 - 在任何东西合并之前 CI 都是 green 的,而且 main 始终能工作。 - 你一路交付到生产环境的,是你测过的同一个 artifact/容器。 - 一个坏 release 只是一次快速的 rollback,而不是一场危机。 - 有风险的改动用 canary 或 blue-green 发出去,而你的基础设施是代码。
安全的交付,就是可逆的交付:把 pipeline 自动化,只交付一个测过的 artifact,留一个一键 rollback,把有风险的改动渐进地 release,并把基础设施定义为代码。