Infra as Code:工程银弹还是乌托邦幻想

问题的提出

近些年来,在 AWS 内部越来越流行 serverless 风格的架构设计。当服务的基础设施足够复杂,AWS CloudFormation 在实践中会暴露出各种各样的问题:多条 pipeline 之间存在依赖,某些 stack 的更新必须依赖另一些 stack 的新版本或者旧版本,复杂依赖可能会必然导致冲突,部分资源更新成功后又因为后续步骤失败而进入中间态,回滚过程本身也可能失败(UPDATE_ROLLBACK_FAILED),最终整个系统停留在一个既不完全是旧状态、也不完全是新状态的位置,手工修复起来极其复杂耗时,令人痛不欲生。这类糟糕的用户体验很容易让人产生某种“我寻思”:CloudFormation 是否有可能像数据库那样,提供完整的 ACID 语义呢?如果我们能要求这些 control plane 操作要么全部成功,要么完全不发生,那么这些复杂依赖、更新失败、回滚失败的痛点似乎都可以得到极大缓解。毕竟从形式上看,control plane API 只是负责管理用户资源,它们本质上就是在操作一组数据库对象,完全可以被事务化。

经典思路:2PC

顺着这个方向继续思考,自然而然的第一步往往是类比两阶段提交(2 Phase Commit):既然一个 stack 更新会涉及多个资源、多个服务和多个 control plane ,那么似乎可以让各参与方先进入 prepare 状态,确认自己能够完成目标变更;等所有参与方都准备就绪之后,再统一 commit;如果中途某个环节失败,则统一 abort。从这个角度看,CloudFormation 缺少的似乎只是更强的分布式 ACID 事务支持而已。对于这个想法,现实层面的第一反应就是:prepare 很可能意味着资源预留,名字要先占住,容量要先保留,IP 地址要先锁住不能再分配给别人,这会影响其他用户的请求,降低系统整体吞吐和公平性。但这个 concern 其实只是工程设计上的 trade-off,而不是实现上的不可行。数据库里的锁同样会阻塞并发访问,长事务(例如全表扫描)也会阻塞其他写事务,因此仅仅指出 prepare 会锁定资源,并不足以说明基础设施系统不能被事务化。它只是引入了不太现实的代价,但尚未触及能力边界本身。

分布式事务需要怎样的承诺?

继续往下分析,问题开始从“能不能做 prepare”转向“prepare 究竟要提供什么强度的保证”。如果 prepare 只是做一次严格的预检查,只是验证当前条件下某项操作看起来可执行,那么这件事通常并不困难,也确实有工程价值。许多资源都可以做相当严谨的 validation。名称是否冲突、配置是否合法、权限是否足够、依赖是否存在、容量在当前时刻是否看起来可用,这些都可以在 commit 之前检查出来。但这样的 prepare 更接近 preflight check,而不是数据库事务里的 prepare。它所表达的是“当前看来可以执行”,并不保证“后续 commit 一定成功”。而真正强事务语义下的 prepare 隐含着一种承诺:参与方进入 prepare 后,只要最终收到 commit,就应当能够完成操作,而不会因为外部条件变化而失败。这里,才是我们一头撞上的本质困难之墙。

在基础设施系统中,很多操作的成功并不只依赖数据库对象的更新。以 target group 这类资源为例,prepare 可以做很多事:校验 target 是否存在,规则是否冲突,配置是否合法,当前健康状态是否正常,这些都可以预先检查,甚至可以做得非常严格。但 commit 是否最终成功,还必须依赖另一类性质完全不同的约束:target 在 commit 时是否仍然 healthy,底层实例是否继续运行,网络路径是否仍然可用, data plane 中的连接和流量是否还有容量,外部请求在整个过程中是否引发新的状态变化。这些条件具有共同特征——它们会持续变化,它们并不完全由 control plane 掌握,它们也无法被真正冻结。到这里,prepare 的问题就不再是“能否提前做检查”,而是“哪些决定成功与否的状态,能够被 control plane 承诺下来”。如果成功依赖的是未来一段时间中的运行时状态,那么 prepare 只能验证当前状态,而无法承诺未来状态。这正是事务性保证开始失效的地方,它无法完整掌控物理世界——你永远无法回答:机器挂了还怎么再继续 commit 先前承诺能够成功的操作。

从这个角度看,CloudFormation 之所以无法支持真正的 ACID,是因为基础设施系统的关键正确性条件,部分依赖于不可冻结、不可完全事务化的运行时状态。机器可能在任意时刻失效,健康状态可能在 control plane 之外变化,网络和连接状态持续波动,真实流量并不会因为系统正在执行 prepare 而暂停,某些外部副作用一旦发生就无法被完全撤销。这里的关键并不在于“操作会失败”,因为数据库系统同样运行在一个有并发、竞争和宕机的世界中。差异在于,数据库的关键状态主要位于系统内部,能够被锁定、快照、回滚和持久化;而基础设施系统的关键状态部分外化到了运行时环境中,正确性所依赖的条件并不都在 control plane 内部。

进一步思考,CloudFormation 跨越大量 AWS 服务,每个服务拥有独立的 control plane 和特有的资源类型,这不仅使得事务协调在工程上更加复杂,更关键的是,不同服务之间的语义并不一致:某些纯资源管理型的服务,在理论上可以支持事务语义,例如支持较强的 prepare 或状态预留;而另一些依赖运行时行为的服务,则无法提供类似保证。这就带来一个更本质的限制:事务语义无法在系统中局部成立。一旦某个环节依赖不可冻结的运行时状态,无法提供可靠的 commit 保证,这种不确定性就会沿着依赖关系传播,使整个操作无法维持 ACID。换句话说,即便部分 control plane 可以被事务化,非事务性的部分也会“传染”到整个系统,使 CloudFormation 在整体上无法提供统一的事务语义。因此,AWS的多 control plane 不仅仅是增加了协调复杂度,它还传播了这种资源管理语义上的不一致,使得系统更难在全局层面建立 ACID 保证。

IaC 的抽象边界

我们知道,CloudFormation 是 Infrastructure as Code 在 AWS 体系中的一种具体且完善的实现。既然我们已经确信:基础设施系统本身的正确性依赖于不可冻结的运行时状态,无法被完整事务化以提供可靠的全局执行保证,那么这种限制不仅仅是 CloudFormation 本身的局限,而是体现在 IaC 这一抽象本身。当我们用 IaC 去描述和驱动系统时,它所能够提供的正确性保证,受限于 control plane 所能覆盖的状态范围。一旦系统的正确性依赖于 control plane 之外的动态运行时行为,这种抽象就不可避免地开始失效。

IaC 最初的设计目标其实很清晰,它非常擅长描述结构性的事物:网络拓扑、权限管理、静态依赖、资源部署、运行时容量,这些都属于 control plane 意义上的结构状态,适合被声明式配置描述,适合放进版本控制,也适合做自动化执行。在传统虚拟机和容器时代,应用代码和基础设施分界明显,IaC 在这些方面非常成功,也确实显著改善了基础设施的可维护性和可追踪性。在这个背景下,回过头再来看几乎完全建立在 IaC 之上的现代 serverless 系统,会发现它们逐渐将原本属于系统行为的部分,也纳入了基础设施描述之中,从而开始逼近甚至超出这一抽象的边界:资源定义、事件驱动、权限关系、执行状态机、容错和重试,全部通过基础设施描述来表达,但这并不天然属于 IaC 擅长的抽象层。当系统的行为通过 infrastructure 被间接编码时,IaC 的抽象边界开始与系统行为边界发生重叠。此时,IaC 不再只是描述系统的静态结构,而开始承载系统的行为本身,我们遭遇的“stack 更新失败地狱”不能简单归结于工程实现上的不可靠,而是抽象边界被突破后所带来的系统性后果。

这也是复杂 serverless 系统在实践中经常变得难以控制的原因。Lambda、Step Functions、EventBridge、SNS/SQS、IAM、API Gateway 这些资源搅拌在一起,表面上仍然是基础设施定义,实际上却已经在表达一个分布式系统的行为模型。此时应用部署与基础设施部署几乎重合,应用复杂度会直接映射成 infrastructure 复杂度,而 IaC 工具被迫承担一种并不适合它的职责,开始扮演一种隐式 orchestration engine 的角色。这里遇到的困难并不只是 stack 太多、依赖太复杂或更新顺序太难推理,而是系统的行为本身已经被编码进资源依赖关系之中。换一种工具,例如 Terraform、CDK 或其他框架,可以改变语法和局部抽象,但通常无法消除底层资源模型所带来的这部分复杂度。

Kubernetes:建模动态世界

经过了上面的讨论,就更容易理解 Kubernetes 的设计哲学。Kubernetes 看起来也放弃了更强的事务语义,它允许中间状态不一致,允许操作失败,允许系统在一段时间内偏离目标状态。但它采用的是一种更适合动态世界的建模方式。它并不要求每一步操作都一次成功,也不试图保证一个本质上无法冻结的系统在所有时刻都完全正确。它把目标状态声明出来,通过 reconciliation loop 持续比较 actual state 与 desired state,然后不断修正偏差。这里真正重要的,并不是使用了 Raft 或拥有一个强一致的 control plane 状态存储,尽管这些机制当然不可或缺;但更关键的思想是,系统接受了偏离会持续发生这一现实,并把不断修正偏离作为首要设计目标。

从这里看,transaction 与 reconciliation 的差别,并不只是实现手法的不同,而是问题建模方式的不同,后者承认在这样的系统中,ACID 事务语义本身就不再是合适的问题建模方式。事务模型希望防止不一致的出现,希望每一次操作都在严格边界内完成成功或回滚。reconciliation 模型承认不一致会持续出现,并把系统设计成一个自我修正的过程。对于一个运行时状态持续变化、物理健康和流量无法冻结、外部副作用难以彻底收回的系统来说,后者通常更接近现实。它看起来能力更弱,因为它不承诺所有时刻的一致正确性;但它在工程上更可靠,因为它把失败和偏离视为设计内的常态。

总结

回到最初的问题:为什么 CloudFormation 不能像数据库那样支持 ACID ?一个更简洁的答案是:CloudFormation 所面对的并不是一个纯粹的资源状态存储问题,它所管理对象的正确性部分依赖于 control plane 之外持续变化的运行时世界,而这部分状态无法被冻结、无法完整 snapshot,也无法用事务边界彻底包裹,复杂部署中的失败状态因而也就无法被彻底消除。

因此,IaC 这类工具在结构性问题上非常有效,但在行为性问题上天然带有边界。复杂的 serverless 系统之所以容易变得难以控制,本质上是因为应用逻辑、异步行为和基础设施资源之间耦合得过于紧密,使得基础设施定义开始承载系统行为本身。在这个前提下,工程实践的重点就不再只是追求更强的 apply 成功保证,而会逐渐转向限制 IaC 的职责边界,将行为复杂度从资源图中剥离出来,用更适合动态系统的方法处理运行时不确定性。

从这个角度看,Infrastructure as Code 更适合被理解为一种在特定问题边界内有效的工程方法。它擅长描述系统的结构与静态拓扑,使 control plane 上的状态更清晰、更可追踪,但难以单独承担对动态系统行为的建模与控制。工业界逐渐演化出的分层、平台化和混合架构,本质上都是在将结构与行为分离,将 control plane 与运行时解耦,从而避免将两者压缩到同一抽象中。

可以用一句话作为收尾:

Transaction 试图通过“冻结时间”来保证确定性,而基础设施系统运行在一个始终在变化的世界里,这就是前者失效的根源。

comments powered by Disqus
Published:
2026-03-16
分类:
Tag: