...
Back

上线一件无法撤销的事

大多数部署是可回滚的,所以我们的习惯默认了可回滚。对那些不可回滚的操作,安全必须前移 —— 前移到让不可逆那一步无法被意外触发的门禁里。

上线一件无法撤销的事

上线一件无法撤销的事 🚦

我们部署的几乎一切都能回滚。就这一个事实塑造了我们所有的习惯:小步发布、盯着曲线、看着不对就回滚。这是个好体系,而它完全建立在可逆性之上。

然后你撞上一个不具备这个性质的操作 —— 一条真实支付链路、一次破坏性迁移、一条写进永久位置的记录、一笔真金白银的首笔交易 —— 于是所有让你快起来的习惯都变成了负债。不是因为习惯坏,而是因为它们假设了一个不再存在的撤销键。


把安全前移

对可逆的工作,安全在动作之后:部署、观察、回滚。对不可逆的工作,"之后"什么都没有,所以全部安全都得放在之前。落到实处是三件事。

让不可逆那一步需要一次显式的、带外的批准。 不是代码评审 —— 而是在执行那一刻的一次单独的肯定动作。我们的做法是一个必须显式传入的构建替换变量;没有它,流水线拒绝进入不可逆阶段。重要的性质不是具体机制,而是:默认路径无法抵达那个不可逆动作。 必须有人有意地说"是",而且这个"是"不能通过从历史里重跑一条命令而发生。

动手之前先验证目标。 最有价值的单项检查,是确认你即将写入的东西就是你以为的那个:正确的环境、正确的账户、正确的标识 —— 而且是从活的系统里读回来的,不是从你正在看的那个配置文件里读的。配置描述的是意图,读回来的才是现实,而昂贵的错误就住在两者的缝隙里。

写完之后读回来。 写入,然后从权威来源读取该值,和你的意图比对。一次报告成功的写入,不是"正确的值现在已存储"的证据 —— 它只是"请求被接受了"的证据。对任何你无法重来的事情,这个区别就是全部。


门禁不能在情急之下被绕过

门禁存在的意义,恰恰是为了那个"有人在压力之下、并且确信自己是对的"的时刻。如果它能靠加一个参数跳过,那它就会恰好在那时被跳过。

对我们管用的:

  • 门禁失败即关闭。 缺少批准意味着停止,不是警告。一个未设置时默认为真的批准不叫门禁。
  • 批准要点名它在批准什么。 "是,部署"是弱的;"是,在这个环境里执行这个具体的不可逆操作"才是一句有人真能为之负责的话。
  • 门禁要有测试,而且测试要经过变异验证。 故意把门禁弄坏,确认测试会失败。一个对着坏掉的门禁仍然通过的门禁测试,比没有测试更糟,因为它在制造信心。
  • 任何人不得凭自己的判断绕过它,包括写它的人。如果它错了,就修门禁 —— 而不是这一次先绕过去、承诺以后再修。

熔断开关是一个阶段,不是一种设计

在不可逆上线之前,熔断开关是正确的:一个能关掉新路径的标志、一个暴露上限、一条对真实环境的硬编码拒绝。它们让你可以先发布代码、再发布行为。

它们同时从构造上就是临时的,而纪律体现在移除它们。上线之后被留在原地的熔断开关不是安全特性 —— 它是一个没人测试的未走过的分支,并逐渐变成一种意外弄坏系统的方式。

所以移除它们本身是一次独立的改动,要像添加它们时一样认真评审,并把同一个问题反过来问一遍:如果这个现在永远开着,什么会坏? 我们的那次是一个什么别的都不做的专门提交 —— 这种形状才让评审的人真的能检查它。


超出「上线」的那部分

这一切听起来专属于高风险操作,但底下那个习惯很小,而且到处都值得有:

在任何动作之前,先知道它是否可逆。如果不可逆,把这件事说出来。

不可逆操作造成的损害,大多不是来自"草率地执行了它们",而是来自"没注意到它们是不可逆的" —— 把一次永久写入当成一次部署来对待,因为它看起来像部署,因为工具让它看起来像部署。

门禁、读回、显式批准:这些全都是让"类别"在关键时刻变得可见的机制。机制是可以商量的。注意到,不可以。