基础设施代码项目集成模式与脚本使用策略
1. 项目集成概述
在代码库中,项目之间通常存在依赖关系,因此需要考虑何时以及如何将相互依赖的项目的不同版本进行组合。以 ShopSpinner 团队的代码库为例,其包含多个项目,如两个堆栈项目(application_infrastructure - stack 和 shared_network_stack)、两个服务器配置项目(tomcat - server 和 monitor - server)以及一个基础设施项目(application - server - image)。这些项目之间存在着复杂的依赖关系,例如 application_infrastructure - stack 项目在 shared_network_stack 创建的网络结构内创建其基础设施,并使用 application - server - image 项目构建的服务器映像创建服务器集群;而 application - server - image 项目则通过应用 tomcat - server 和 monitor - server 中的服务器配置定义来构建服务器映像。
当对这些基础设施代码项目进行更改时,会创建项目代码的新版本,这些项目版本可以在构建时、交付时或应用时进行集成。
1.1 链接与集成的类比
将计算机程序的元素组合在一起称为链接,这与基础设施项目的集成有相似之处。在构建可执行文件时,库会被静态链接,类似于构建时项目集成;而当可执行文件进行调用时,安装在机器上的库会被动态链接,这有点像应用时项目集成。不过,这种类比并不完美,因为更改程序会影响可执行文件的行为,而更改基础设施通常会影响资源的状态。
2. 构建时项目集成模式
2.1 模式描述
构建时项目集成模式会跨多个项目执行构建活动,包括集成它们之间的依赖关系并设置项目间的代码版本。构建过程通常先对各个组成项目进行单独构建和测试,然后再一起进行构建和测试。该模式的特点是为所有项目生成单个工件,或者生成一组经过版本控制、可一起提升和应用的工件。
例如,在一个示例中,单个构建阶段使用多个服务器配置项目生成一个服务器映像。构建阶段可能包括多个步骤,如构建和测试各个服务器配置模块,但最终输出的服务器映像是由所有组成项目的代码组成的。
2.2 动机
将项目一起构建可以尽早解决依赖问题,快速反馈冲突情况,并在整个交付过程到生产环境中实现代码库的高度一致性。在构建时集成的项目代码在整个交付周期内都是一致的,同一版本的代码会在整个过程的每个阶段应用到生产环境。
2.3 适用性
是否使用此模式主要取决于个人偏好,以及团队管理跨项目构建复杂性的能力。
2.4 后果
在运行时构建和集成多个项目非常复杂,特别是对于大量项目。根据构建的实现方式,可能会导致反馈时间变慢。大规模使用构建时项目集成需要复杂的工具来编排构建,像 Google 和 Facebook 这样的大型组织会有专门的团队来维护内部工具。此外,由于项目是一起构建的,它们之间的边界不如其他模式明显,这可能导致项目之间的耦合更紧密,使得进行小的更改时可能会影响代码库的许多其他部分,增加了更改的时间和风险。
2.5 实现步骤
- 存储项目 :将所有用于构建的项目存储在单个存储库(通常称为单仓库)中,通过集成代码版本控制来简化一起构建的过程。
- 选择构建工具 :大多数软件构建工具,如 Gradle、Make、Maven、MSBuild 和 Rake,可用于编排适度数量项目的构建。但对于大量项目,运行构建和测试可能需要很长时间。
- 并行化处理 :可以通过在不同线程、进程甚至计算网格中并行构建和测试多个项目来加速这一过程,但这需要更多的计算资源。
- 使用有向图优化 :使用有向图将构建和测试限制在代码库中发生更改的部分,这样可以减少提交后构建和测试所需的时间,使其仅比单独构建项目多花费一点时间。
- 使用专业工具 :有一些专门设计用于处理大规模多项目构建的工具,如 Bazel、Buck、Pants 和 Please,这些工具大多受到 Google 和 Facebook 内部工具的启发。
2.6 相关模式
构建时项目集成的替代方案是在交付时或应用时进行项目版本集成。单仓库策略虽然不是一种模式,但支持此模式。此外,在创建服务器映像时应用服务器配置代码的示例以及不可变服务器模式都是构建时集成而非交付时集成的例子。许多项目构建在构建时解决对第三方库的依赖,将它们下载并与可交付成果捆绑在一起,但这些依赖项不会与使用它们的项目一起构建和测试,当这些依赖项来自同一组织内的其他项目时,这是交付时项目集成的一个例子。
2.7 单仓库与构建时项目集成的区别
大多数对单仓库策略的描述包括将存储库中的所有项目一起构建,即构建时项目集成。但单仓库只是一种实现选项,有些团队在单个存储库中管理多个项目但并不一起构建,他们虽然称其代码库为单仓库,但并未使用这里描述的模式。另一方面,从技术上讲,也可以从单独的存储库中检出项目并一起构建,这符合构建时项目集成的模式,但会使源代码版本与构建的关联和跟踪变得复杂,例如在调试生产问题时。
以下是构建时项目集成的 mermaid 流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A([开始]):::startend --> B(存储项目到单仓库):::process
B --> C(选择构建工具):::process
C --> D{项目数量多?}:::process
D -->|是| E(并行化处理):::process
D -->|否| F(正常构建测试):::process
E --> G(使用有向图优化):::process
F --> G
G --> H(使用专业工具):::process
H --> I([结束]):::startend
3. 交付时项目集成模式
3.1 模式描述
对于存在依赖关系的多个项目,交付时项目集成会先对每个项目进行单独构建和测试,然后再将它们组合起来。这种方法比构建时集成更晚地集成代码版本。一旦项目组合并测试完成,它们的代码将一起通过交付周期的其余阶段。
例如,ShopSpinner 的 application - infrastructure - stack 项目使用 application - server - image 项目定义的服务器映像定义虚拟机集群。当对基础设施堆栈代码进行更改时,交付管道会先单独构建和测试堆栈项目,如果新版本通过测试,则进入集成测试阶段,该阶段将堆栈与最后一个通过自身测试的服务器映像进行集成测试,此阶段是两个项目的集成点,项目版本随后一起进入管道的后续阶段。
3.2 动机
在集成项目之前先单独构建和测试项目是一种强制项目之间保持清晰边界和松散耦合的方法。例如,ShopSpinner 团队的一名成员在 application - infrastructure - stack 中实现了一个防火墙规则,该规则打开了 application - server - image 配置文件中定义的 TCP 端口,但在推送代码时,堆栈的测试阶段失败,因为构建代理上没有另一个项目的配置文件。这种失败暴露了两个项目之间的耦合问题,团队成员可以更改代码以使用参数值来设置要打开的端口号,从而使代码更易于维护。
3.3 适用性
交付时集成适用于需要在代码库中的项目之间保持清晰边界,但仍希望一起测试和交付每个项目版本的情况。然而,该模式难以扩展到大量项目。
3.4 后果
交付时集成将解决和协调不同项目不同版本的复杂性转移到交付过程中,这需要复杂的交付实现,如管道。
3.5 实现步骤
- 使用“扇入”管道设计 :交付管道使用“扇入”管道设计来集成不同的项目,将不同项目组合在一起的阶段称为扇入阶段或项目集成阶段。
- 集成项目的方式 :集成阶段如何集成不同项目取决于组合的项目类型。例如,对于使用服务器映像的堆栈项目,堆栈代码将被应用并传递对相关版本映像的引用,基础设施依赖项从代码交付存储库中检索。
-
处理组合项目版本的方法
:
- 捆绑成单个工件 :将所有项目代码捆绑成单个工件,用于后续阶段。例如,当两个不同的堆栈项目集成和测试时,集成阶段可以将两个项目的代码压缩成单个存档,并将其提升到下游管道阶段。在 GitOps 流程中,可以将项目合并到集成阶段分支,然后从该分支合并到下游分支。
- 创建描述符文件 :创建一个包含每个项目版本号的描述符文件,例如:
descriptor - version: 1.9.1
stack - project:
name: application - infrastructure - stack
version: 1.2.34
server - image - project:
name: application - server - image
version: 1.1.1
交付过程将描述符文件视为一个工件,每个应用基础设施代码的阶段从交付存储库中拉取各个项目的工件。
-
标记资源
:用聚合版本号标记相关资源。
3.6 相关模式
构建时项目集成模式在开始时就集成项目,而应用时项目集成模式在每个使用它们的交付阶段集成项目,但不“锁定”版本。
以下是交付时项目集成的 mermaid 流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A([开始]):::startend --> B(单独构建测试项目):::process
B --> C(进入集成测试阶段):::process
C --> D{测试通过?}:::process
D -->|是| E(选择集成方式):::process
D -->|否| B
E --> F{选择处理版本方法}:::process
F -->|捆绑成单个工件| G(提升到下游阶段):::process
F -->|创建描述符文件| H(各阶段拉取工件):::process
F -->|标记资源| I(继续交付):::process
G --> J([结束]):::startend
H --> J
I --> J
4. 应用时项目集成模式
4.1 模式描述
应用时项目集成,也称为解耦交付或解耦管道。此模式将多个项目分别推进交付阶段。当有人更改项目代码时,管道会将更新后的代码应用到该项目交付路径中的每个环境。在这个过程中,该项目代码的版本可能会在每个环境中与上游或下游项目的不同版本进行集成。
以 ShopSpinner 为例,application - infrastructure - stack 项目依赖于 shared - network - stack 项目创建的网络结构。每个项目都有自己的交付阶段,当将 application - infrastructure - stack 代码应用到某个环境时,项目间的集成就会发生。这一操作会创建或更改一个使用共享网络结构(如子网)的服务器集群,而且无论该环境中共享网络堆栈的版本如何,集成都会发生,即每次应用代码时,版本的集成都是单独进行的。
4.2 动机
在应用时集成项目可以最大程度地减少项目之间的耦合。不同的团队可以将其系统的更改推送到生产环境,而无需进行协调,也不会因其他团队项目的更改问题而受阻。
4.3 适用性
这种高度解耦的模式适用于具有自主团队结构的组织,也有助于处理大规模系统,因为在大规模系统中,协调数百或数千名工程师的发布并同步交付是不切实际的。
4.4 后果
此模式将破坏项目间依赖关系的风险转移到应用时操作,并且无法确保整个管道的一致性。如果有人将一个项目的更改通过管道推送的速度比其他项目的更改快,那么该项目在生产环境中集成的版本可能与在测试环境中不同。此外,项目之间的接口需要仔细管理,以确保任何给定依赖关系两侧的不同版本之间具有最大的兼容性,因此这种模式在设计、维护和测试依赖关系及接口方面需要更高的复杂性。
4.5 实现步骤
- 设计解耦管道 :在某些方面,使用应用时集成设计和解耦构建及管道比其他模式更简单,每个管道负责构建、测试和交付单个项目。
- 集成基础设施堆栈 :在应用 application - infrastructure - stack 阶段,需要引用 shared - network - stack 创建的网络结构。相关技术可用于在基础设施堆栈之间共享标识符。
- 明确依赖关系 :由于无法保证在任何给定环境中使用的是另一个项目代码的哪个版本,因此团队需要清楚地识别项目之间的依赖关系,并将其视为合同。例如,shared - network - stack 需以标准化方式公开网络结构的标识符,供其他项目使用。
- 使用测试夹具 :可以使用测试夹具来单独测试每个堆栈。以 ShopSpinner 为例,团队可以在不使用 shared - network - stack 实例的情况下测试 application - infrastructure - stack 项目,通过创建简化的网络设置来减少应用堆栈对网络堆栈实现细节的依赖。
- 实施合同测试 :拥有其他项目依赖的项目团队可以实施合同测试,以证明其代码符合预期。例如,shared - network - stack 可以验证网络结构(子网)是否创建以及其标识符是否以其他项目可使用的机制公开。同时,要确保合同测试有明确的标签,以便在测试失败时,开发人员能意识到可能会破坏其他项目,而不是仅仅更新测试以匹配更改。许多组织发现消费者驱动的合同(CDC)测试很有用,即依赖于提供者项目资源的消费者项目团队编写在提供者项目管道中运行的测试,这有助于提供者团队了解消费者团队的期望。
4.6 相关模式
构建时项目集成模式与应用时项目集成模式处于相反的极端,前者在交付周期开始时一次性集成项目,而不是每次都集成;交付时项目集成模式也一次性集成项目,但在交付周期的某个点进行,而不是在开始时。此外,在服务器供应中使用应用时集成的例子,如在创建新服务器实例时应用服务器配置模块等依赖项,通常采用提升到相关阶段的服务器模块的最新版本。
以下是应用时项目集成的 mermaid 流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A([开始]):::startend --> B(各项目单独交付):::process
B --> C(应用代码到环境):::process
C --> D(集成项目):::process
D --> E{集成成功?}:::process
E -->|是| F(继续交付):::process
E -->|否| G(检查依赖和接口):::process
G --> B
F --> H([结束]):::startend
5. 使用脚本包装基础设施工具
大多数管理基础设施代码的团队会创建自定义脚本来编排和运行他们的基础设施工具。有些团队使用软件构建工具,如 Make、Rake 或 Gradle;另一些团队则使用 Bash、Python 或 PowerShell 编写脚本。在很多情况下,这些辅助代码变得至少和定义基础设施的代码一样复杂,导致团队花费大量时间进行调试和维护。
团队可能在构建时、交付时或应用时运行这些脚本,并且这些脚本通常会处理多个项目阶段。脚本可以处理各种任务,包括:
- 编排基础设施工具的运行。
- 处理项目不同阶段的任务,如构建、交付和应用。
5.1 脚本使用的影响
由于脚本的复杂性增加,团队可能会面临更多的调试和维护工作。为了减少这些问题,可以考虑以下策略:
-
选择合适的工具
:根据项目的规模和需求,选择合适的软件构建工具或脚本语言。例如,对于小型项目,简单的 Bash 脚本可能就足够了;而对于大型项目,使用 Gradle 等专业构建工具可能更合适。
-
模块化设计
:将脚本拆分成多个模块,每个模块负责一个特定的任务。这样可以提高脚本的可维护性和可测试性。
-
自动化测试
:为脚本编写自动化测试,确保脚本在不同环境和输入下都能正常工作。
6. 总结
不同的项目集成模式和脚本使用策略各有优缺点,适用于不同的场景。以下是一个简单的对比表格:
| 集成模式 | 优点 | 缺点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| 构建时项目集成 | 尽早解决依赖问题,代码一致性高 | 构建复杂,可能导致耦合紧密 | 团队有能力管理复杂构建,追求代码一致性 |
| 交付时项目集成 | 强制项目间边界清晰,耦合松散 | 交付过程复杂,难以扩展 | 需要项目边界清晰,一起测试交付版本 |
| 应用时项目集成 | 解耦项目,适合自主团队和大规模系统 | 增加接口管理复杂度,无法保证一致性 | 自主团队结构,大规模系统 |
在实际应用中,需要根据项目的特点、团队的能力和组织的需求来选择合适的集成模式和脚本使用策略。同时,合理的设计和管理可以减少各种模式带来的负面影响,提高项目的开发和交付效率。
希望通过本文的介绍,你能对项目集成模式和脚本使用有更深入的理解,并能在实际工作中做出更合适的选择。
超级会员免费看
946

被折叠的 条评论
为什么被折叠?



