2022年云原生12因子应用 - xenitab


十二要素应用是一种构建软件即服务应用的方法论,最早是由与Heroku有关的开发者制定的。
这个方法论的首次提出已经过去十年了。尽管有人批评说它只适用于Heroku和类似的网络应用程序服务,但它仍然是软件即服务开发的一个相关标准。
它的一些原则已被纳入Docker,然后纳入OCI,有效地使它们成为容器领域的法律。
这篇博文逐个探讨了这12个因素,并试图评估它们是否仍然相关,或者是否需要更新。

对以Heroku为中心的批评:
Heroku(以及Google App Engine和类似的服务)提供了一个单一的打包模型,我们今天可以称之为 "无IaC":你提供一个HTTP服务器,Heroku为你运行它(或者在Google App Engine的情况下是一个war文件)。
任何非实质性的软件即服务产品都需要将许多应用程序组成一个服务,每个应用程序都有独特的作用:认证、缓存、API、提供静态文件等,并有一些基础设施即代码来描述这些应用程序是如何暴露和互连。
我们最终有了一个 "app应用 "级别和一个 "service服务 "级别,我们在考虑原文时必须谨慎,因为它只谈了应用级别,但它的一些关注点现在却在服务级别。

因素一:代码库
代码库是指任何一个单一的存储库。
代码库在所有部署中都是相同的,尽管不同的版本可能在每个部署中都是活跃的。

这个因素首先是一个论点,即尽可能多地将你的服务表达为可以放在版本控制下的代码。
这在十年前可能已经是过分的;然而,版本控制的最新化身是GitOps,即你的基础设施通过阅读你的版本控制系统来维护自己,并在必要时自动应用更改--这与最初的Heroku模式非常相似。

在代码库和应用程序之间总是存在着一对一的关联。如果有多个代码库,它就不是一个app应用程序,而是一个分布式系统。分布式系统中的每个组件都是一个应用程序app,每个组件都可以单独遵守十二个因素。

这一部分因素在应用层面上仍然是相关的,但现代公共云供应商的基础设施即代码工具和Kubernetes等平台允许我们将一组应用及其支持资源(如秘密)描述为一个包或服务。
一些组织将基础设施即代码和应用程序代码分离到单独的存储库中,而另一些组织则将应用程序和其支持的IaC放在一起;这两种情况都不能无条件地说是最佳实践。
同样,单页应用程序通常被部署到内容分发网络,而其后端可能被部署到公共云供应商或Kubernetes集群。这些是否应该保存在同一个资源库或不同的资源库中,取决于它们的紧密耦合程度。

2022年的开发者会仔细考虑存储库和工件之间的关系。

  • 分支策略
  • 持续集成的完整性和运行时间
  • 持续交付管道
  • 基础设施即代码的可维护性
  • 配置管理
  • 自动化部署

期望随着你的应用程序的发展,重新组织你的来源。

因素二:依赖性
12个因素的应用程序从不依赖于系统范围内包的隐性存在。它通过依赖性声明清单,完整而准确地声明所有的依赖性。[......]12个因素的应用程序也不依赖于任何系统工具的隐性存在。

在容器和功能即服务的世界里,这个因素已经被提升为事实。这些执行环境几乎不提供隐含的依赖性。

现代应用程序往往有一个以上的依赖声明清单,即其项目清单(如go.mod或package.json)和一个Docker文件。
这个因素的后果是,你应该使用Docker基础镜像,这些镜像应该是强迫明确安装支持性的库和工具。

这个因素的最新解释是,升级依赖版本应该始终是一个有意识的行为。
这稍微改变了对原始因素 "确切 "的解释。
各种生态系统和工具链(maven、npm、cargo等)在解决依赖性方面的工作方式不同。有些是在开发者进行构建时解决依赖关系,有些则需要明确的 "升级 "操作来改变构建中的内容。
因此,有一个更新依赖关系的规范化工作流程是至关重要的。例如,当使用Node.js和npm时,开发者通常应该使用npm ci,只有在打算更新依赖关系时才使用传统的npm install(或npm update)。

它在执行过程中使用了一个依赖性隔离工具,以确保没有隐含的依赖性从周围的系统中 "漏进来"。完整和明确的依赖性规范统一应用于生产和开发。

Docker引入的创新之一是,这个因素在构建时就已经被强制执行了,这使得它很容易确保开发和生产的统一性。用构建的容器运行你的自动化测试,执行环境的差异空间很小。

因素三:配置
一个应用程序的配置是所有可能在不同部署(暂存、生产、开发环境等)之间变化的东西。十二个因素的应用程序在环境变量中存储配置。环境变量很容易在部署之间改变,而不需要改变任何代码;与配置文件不同,它们很少有机会被意外地检查到代码库中;而且与自定义配置文件不同,[...]它们是一种语言和操作系统无关的标准。[......]检验一个应用程序是否将所有配置正确地从代码中剔除的试金石是,代码库是否可以在任何时候开放源代码,而不损害任何证书。

这个因素仍然与书面上的内容有很大关系,但也有一些细微的差别需要考虑。

像Terraform这样的基础设施即代码的工具允许我们创建文件和数据库条目。Kubernetes允许我们创建ConfigMaps,它将作为文件提供给容器。在这两种情况下,源配置可以被置于版本控制之下,任何手动编辑都会在下一次部署时被覆盖,而且这种机制很容易被开发人员在本地模仿。因此,它们通过不同的手段达到了与使用环境变量相同的结果。

另外,虽然环境变量是操作友好的,但在编写测试时却有问题,因为它们是全局状态。
在默认为并行测试执行的生态系统中(例如Rust),环境变量不能被使用。
因此,虽然环境变量仍然是接受简单配置值的首选方式,但一个12因素的应用程序应该尽早将它们转换为内部状态。

下面这个因素现在在一个方面已经过时了:
应尽可能使用密钥管理系统(如Hashicorp Vault或Azure Key Vault)存储秘密(密码、私钥等)。
特别是在我们可以依靠基础设施来验证调用进程的情况下(例如通过Kubernetes服务账户),对密钥的访问将不直接需要凭证。密钥的存在处于版本控制之下,但实际的密钥内容并不重要。

此外,对于像Kubernetes这样的平台,服务发现意味着有些方面根本不需要配置。此外,某些形式的配置可以更好地作为IaC控制的资源之间的引用来管理,这就把它们从直接的配置管理考虑中移除。

因素四:后援Backing服务
后援服务是应用程序通过网络消耗的任何服务,作为其正常运行的一部分。12个因素的应用程序的代码没有区分本地和第三方服务。对应用程序来说,两者都是附加资源,通过URL或存储在配置中的其他定位器/凭证进行访问。[......]资源可以随意附加到部署中,也可以从部署中分离出来。

这个因素仍然与书面的内容有关。它目前的迭代有时被称为 "API优先",它可以被描述为这样一个原则:你创建的所有服务都应该能够作为一个支持服务。更广泛地说,随着 "零信任 "的出现和云服务的激增,这个因素的最终逻辑结果是任何服务都可以与地球上的任何其他服务进行交互。

甚至你的协调平台本身也是一个支持服务,而不仅仅是在它里面运行的服务。一个服务可以利用Kubernetes控制平面来运行一个一次性的工作,或者配置云资源来服务一个新客户。

原文中大量关注了关系型数据库。值得指出的是,你可以创建一个服务,在不违反12个因素的情况下,可扩展地服务于长效状态:只要有一个程序可以要求或协商访问状态的特定分片(例如,存储在Azure存储账户中的备份),实际过程可以保持无状态。当代人在这个问题上的想法仍然受到云时代之前的软件的影响(如MySQL、Elasticsearch、RabbitMQ)。对于2022年可能出现的情况,我们可以看看Loki的例子。

因素五:构建、发布、运行
十二个因素的应用在构建、发布和运行阶段之间采用严格的分离。构建阶段是一种转换,它将代码 repo 转换成被称为构建的可执行包。

这个因素或多或少是2022年开发软件即服务的先决条件,但我们需要用自动化这些阶段的要求来补充这个因素。CI/CD软件即服务提供者的成熟,如GitHub、ACR Tasks和Circle CI,意味着现在相对容易实现这一过程的自动化。

通常情况下,构建阶段会将容器镜像推送到一些容器注册处,或者将功能即服务的压缩文件上传到云存储。

发布阶段采用构建阶段产生的构建,并将其与部署的当前配置相结合。

今天,通常的做法是由一个管道来推送发布到运行环境。这在应用程序生命周期的早期非常有用,因为发布过程通常是随着应用程序的发展而发展。为了在构建和发布之间实现更强的分离,你可能要考虑采用GitOps。在Kubernetes领域,你会使用Flux这样的服务。

因素六:流程
12个因素的流程是无状态的,没有任何共享。任何需要持久化的数据都必须存储在有状态的备份服务中。

这个因素仍然与书面有关。在REST API的世界里,这实际上意味着我们在HTTP请求之间不应该在内存中保留任何域的状态--它应该总是被移交给一个缓存服务。这是软件即服务中扩展的主要推动因素。

遵守这一规则也是避免内存泄露的好方法,内存泄露往往困扰着诸如Java、Node、Python和Ruby等垃圾收集的生态系统。在你把缓存外包给Redis之后,你仍然会有泄漏,但它会更容易测量,而且对正确架构缓存的激励也更强。

因素七:端口绑定
十二个因素的应用程序是完全独立的,不依赖于运行时将网络服务器注入执行环境来创建一个面向网络的服务。网络应用通过绑定到一个端口,并监听从该端口进来的请求,将HTTP作为一种服务输出。

这个因素现在是容器化场景的标准。端口绑定的广泛采用使得整个支持代理的生态系统(例如Envoy、Traefik、Toxiproxy)成为可能,这(具有讽刺意味)意味着今天的典型应用通常不是独立的,而是依赖于其他容器化应用来执行,例如认证和追踪。这是一个改进的关注点分离,因此,在2022年,我们在服务层面上考虑这个因素。

端口绑定的方法意味着一个应用可以成为另一个应用的支持服务,方法是在消费应用的配置中提供支持应用的URL作为资源处理。

原文的重点是网络协议,如HTTP和XMPP。为了成为2022年的支持服务,应用程序还应该遵守某种API合同,定义支持服务的预期作用。

许多开发者隐含地认为,使用HTTP等高级协议会产生延迟。在本地网络上的REST调用的开销(例如在云提供商内部)通常是2-4毫秒,所以你需要得到大量的不可并行的请求,然后这个开销才会比RDBMS操作和对消费者的延迟明显。

因素八:并发性
在12个因素的应用程序中,进程是第一等公民。十二要素应用程序中的进程从unix进程模型中获得了运行服务守护程序的有力线索。[......]这并不排除个别进程处理自己的内部复用。但是,一个单独的虚拟机只能增长这么大(垂直规模),所以[应用程序]也必须能够跨越多个物理机上运行的多个进程。

这个因素现在已经或多或少地写入了法律。函数即服务平台通常提供透明的横向扩展。在Kubernetes部署中,你只需给出你期望的pod数量。

因此,横向可扩展性的机制在2022年得到解决。任何成功的应用程序的核心挑战是在多个进程和支持服务之间分配工作,以使其真正实现有意义的横向扩展。典型的由RDBMS支持的网络应用程序的可扩展性通常完全受制于其支持的数据库,迫使RDBMS的垂直扩展--一个分配额外CPU的问题。这通常是昂贵的,而且往往只能产生边际改善。

简而言之,水平可扩展性比垂直可扩展性要好得多,但这完全是软件架构的结果。因此,尽早确定那些真正需要大幅扩展的应用程序是至关重要的,这样就可以对它们进行适当的架构设计。

尽管无服务器模式在软件即服务领域占据主导地位,但仍有垂直扩展时代的遗留问题,而Twelve Factor App试图打破这一局面。例如,Java、Node、Python和Ruby的虚拟机维护着大量的反射元数据,而且非常不愿意去分配内存,导致扩展时效率严重低下。以Go和Rust为首的新一代生态系统在这方面更加节俭。(banq:Java虚拟线程Loom)

因素九:可支配性
十二要素应用程序的进程是一次性的,这意味着它们可以在一瞬间启动或停止。[......]进程应努力减少启动时间。[...] 进程在收到SIGTERM信号时优雅地关闭,[...]允许任何当前请求完成。[......] 十二个因素的应用程序被设计用来处理意外的、非优雅的终止。

这个因素仍然与理论理想有关,并且在今天仍然几乎与十年前一样难以捉摸。例如,Node.js中包含的HTTP服务器默认不执行优雅的关闭(参见nodejs问题2642)。

在现有的代码基础上实现这一因素比听起来要难得多。这意味着要对应用中涉及的所有(通常是隐含的)状态机进行映射,并对它们进行协调,以便不会出现未定义的转换。例如,在我们启动HTTP监听器之前,数据库连接池必须准备好,并且在HTTP监听器关闭和所有飞行中的HTTP请求完成之前不得关闭。工作者必须 "交还 "正在进行的工作项目,这样替换的工作者就不必等待超时来处理这些工作项目。
这种困难在分布式系统中更加复杂,因为一个概念上的 "事务 "可能跨越多个应用程序或支持服务(例如,写入文件存储和发送邮件),需要两阶段的提交语义。(banq:需要分布式第一定理:CAP定理

然而,这个因素是我们在大型云服务中认为理所当然的永远在线体验的关键。一个有工作的优雅关闭和全面健康检查的服务可以随时更新,并建立起开发者和运营者的信心。这可以显著提高生产力,特别是在与自动化测试相结合时。

因素X:开发/生产的平等性
历史上,开发[......]和生产[......]之间有很大的差距,时间差距、人员差距[和]工具差距。[十二个因素的应用是通过保持开发和生产之间的差距来实现持续部署。

这个因素现在被俗称为DevOps,并且仍然像以前一样有意义,但在软件即服务的开发中仍然没有完全建立起来:许多生产环境很难挤到开发人员的笔记本电脑上。一般来说,公共云供应商在支持其服务的开发用例方面投入的精力太少。在这方面,Kubernetes走得最远。

Docker引入了一个边界地带,在那里可以只用Docker Engine为开发环境开发一个容器,并且仍然有理由相信它能在Kubernetes等环境中正常执行。尽管如此,仍然需要对后端服务进行一些配置,而且维护两套不同的仪器也会浪费时间。例如,应用程序通常有一个Docker Compose文件来让开发人员开始工作,还有一个Kubernetes清单用于测试/开发。当这些文件不同步时,在部署时就会出现令人讨厌的意外。

12个因素的开发者抵制在开发和生产之间使用不同的支持服务的冲动

许多全栈开发设置包括 "开发 "服务器,其作用是在应用的源代码改变时自动重新加载。同样,像Docker Desktop和Tilt这样的工具提供的功能和环境与Azure容器实例或Kubernetes等有细微的不同。所有这些都会给开发者的选择带来色彩,并有可能引入一些直到后期才会发现的问题。

2022年的开发者在选择工具时,会同时考虑开发者的经验、持续集成/交付/部署和可操作性。

因素十一:日志
日志提供了对运行中的应用程序行为的可见性。[......]一个十二要素的应用程序从不关心其输出流的路由或存储。它不应该试图写到或管理日志文件。相反,每个运行中的进程都将其事件流写入stdout,不加缓冲。[......]目的地对应用程序来说是不可见的,也是不可配置的,而是完全由执行环境管理。

有趣的是,这个因素实际上并没有建议使用日志记录。

因此,这个因素应该被更新,规定一个应用程序应该是 "可观察的",这意味着它应该自愿提供关于其性能和行为的信息。
我们通常将其分解为日志、指标、追踪和审计跟踪。这些的相对优势在不同的应用程序之间有很大的不同,但所有的应用程序都应该有一个可观察性策略。
通常,需求会随着应用程序的发展而变化:在生命周期的早期,日志可能占主导地位,但随着使用量的增加,重点会转移到度量。一个服务中的应用被视为一个单元,通常提供不同的可观察性需求。

 因素十二:管理进程
一次性的管理进程应该在一个与应用程序的常规长期运行进程相同的环境中运行。它们针对一个版本运行,使用与针对该版本运行的任何进程相同的代码库和配置。

这个因素捕捉到了Rails和Drupal生态系统中常见的做法,还有其他一些。这些都是基于解释型语言,在这些语言中,让脚本或交互式访问应用程序的内部是相对容易的:主要原因是确保数据库访问使用运行时使用的相同模型。在编译语言中,这需要进行模块化(例如,使数据模型成为一个单独的库),这将使开发变得复杂。

然而,该因素有两个基本原则,即使在今天也是如此。

  • 首先,用于管理应用程序的工具应该和你的应用程序一样,以相同的纪律进行版本管理和发布。
  • 第二,操作和升级一个应用程序是其正常使用的一部分,增加内部管理用的端点并不奇怪。

换句话说,至少因素I-IV应该适用于应用程序的工具,就像它适用于应用程序本身一样。

总结
多年来已经提出了各种其他因素,例如超越十二因素应用程序和十二因素应用程序中的七个缺失因素。原始方法的吸引力源于其因素的普遍性。这些贡献具有相关性,但通常仅适用于所有可以(应该)努力达到最初十二个因素的应用程序的一小部分。但是,显然缺少两个方面:安全性和自动化测试。

WhiteHat Security 的优秀人员编写了一个很好的分析,称为安全性和十二因素应用程序,它从安全角度分析每个因素,并为保护您的应用程序提供建议。他们的主要观点是,安全性不是一个额外的因素,而是需要渗透到所有十二个因素中。安全性在原始方法中的代表性不足的指控是值得的,2022 年的开发人员不再有将安全性视为事后考虑的奢侈。

最后,如果没有广泛的自动化测试,就会失去遵循该方法的大部分好处。如果应用程序在部署后立即中断,那么版本控制卫生和水平可扩展性就无关紧要了。十年前,仍然有关于编写程序测试是否值得的讨论。该讨论现已结束,到 2022 年,讨论将是关于特定应用程序或服务的自动化测试策略应该是什么样的。有眼光的开发商认为:

  • 何时在应用程序和/或服务级别应用单元、组件、集成和端到端测试
  • 何时使用支持服务的内存实现
  • 需要什么样的长期测试环境
  • 包含多少自动化静态分析

尽管有这些补充,十二因素应用程序方法在今天仍然非常重要。所有生产软件即服务产品的开发人员都可以从坚持其因素中受益。