使用 GitOps 管理多群集环境中的工作负载
开发现代云原生应用程序通常包括跨一组 Kubernetes 群集生成、部署、配置和提升工作负载。 随着 Kubernetes 群集类型的日益多样化以及应用程序和服务的多样化,该过程可能会变得复杂且不可缩放。 企业组织可以通过建立良好定义的结构来组织人员及其活动,并通过使用自动化工具,在这些工作中取得更大的成功。
本文将引导你完成典型的业务方案,概述组织在多群集环境中管理云原生工作负载时经常面对的相关角色和主要挑战。 它还提出了一种体系结构模式,可以简化这一复杂过程,并增强其可观测性和可伸缩性。
本文介绍了开发云原生应用程序的组织。 任何应用程序都需要计算资源来处理工作。 在云原生环境中,此计算资源是 Kubernetes 群集。 组织可能拥有一个群集,或者更为常见的是拥有多个群集。 因此,组织必须决定哪些应用程序应在哪些群集上运行。 换句话说,组织必须跨群集计划应用程序。 此决策或计划的结果是群集在其环境中所需状态的模型。 准备就绪后,组织需要以某种方式将应用程序交付给分配的群集,以便可以将所需状态变为现实,或者换句话说,协调它。
每个应用程序都会经历一个软件开发生命周期,以将它提升到生产环境。 例如,生成应用程序,部署到开发环境,对其进行测试并提升到过渡环境,进行测试,最后交付到生产环境。 对于云原生应用程序,应用程序在其整个生命周期中需要并面向不同的 Kubernetes 群集资源。 此外,应用程序通常需要群集来提供一些平台服务(例如 Prometheus 和 Fluentbit)以及基础结构配置(如网络策略)。
根据应用程序的不同,应用程序部署到的群集类型可能存在很大的差异。 具有不同配置的同一应用程序可以托管在云中的托管群集、本地环境中的连接群集、工厂生产线或军用无人机半连接边缘设备上的一组群集上,以及星际飞船上的气隙群集上。 另一个方面的复杂性是处于早期生命周期阶段(如开发和 QA)的群集通常由开发人员管理,而对实际生产群集的协调可能由组织的客户管理。 在后一种情况下,开发人员可能只负责在不同的环上提升和计划应用程序。
在只有一个应用程序和少量操作的小型组织中,可以使用少量脚本和管道手动处理这些过程的大部分工作。 但对于运营规模更大的企业组织来说,这可能是一个真正的挑战。 这些组织通常会生成数百个应用程序,这些应用程序面向数百种群集类型,并由数千个物理群集提供支持。 在这些情况下,使用脚本手动处理此类操作是不可行的。
在多群集环境中大规模执行此类工作负载管理需要以下功能:
- 分离对计划和协调工作的关注
- 通过一系列环境提升多群集状态
- 复杂、可扩展且可替换的计划程序
- 根据其性质和连接性,为不同的群集类型灵活使用不同的协调程序
- 大规模平台配置管理
在描述方案之前,我们首先澄清涉及到哪些角色、他们承担的职责,以及角色之间如何相互交互。
平台团队负责管理托管由应用程序团队生成的应用程序的群集。
平台团队的主要职责包括:
- 定义过渡环境(开发、QA、UAT、Prod)。
- 定义群集类型及其在环境中的分布。
- 预配新群集。
- 跨群集管理基础结构配置。
- 维护应用程序所使用的平台服务。
- 计划群集上的应用程序和平台服务。
应用程序团队负责其应用程序的软件开发生命周期 (SDLC)。 它们提供 Kubernetes 清单,以描述如何将应用程序部署到不同的目标。 他们负责拥有 CI/CD 管道,以创建容器映像和 Kubernetes 清单,并跨环境阶段提升部署项目。
通常,应用程序团队并不了解他们要部署到的群集。 他们不了解多群集环境的结构、全局配置或由其他团队执行的任务。 应用程序团队主要了解其应用程序推出的成功,这由管道阶段的成功定义。
应用程序团队的主要职责包括:
- 开发、生成、部署、测试、提升、发布和支持其应用程序。
- 维护和参与其应用程序的源和清单存储库。
- 定义和配置应用程序部署目标。
- 与平台团队沟通,从而请求所需计算资源以成功执行 SDLC 操作。
此图显示了平台和应用程序团队角色在执行常规活动时如何相互交互。
整个过程的主要概念是分离关注点。 过程中包括工作负载,例如应用程序和平台服务,还包括运行这些工作负载的平台。 应用程序团队负责工作负载(对象),而平台团队则专注于平台(即位置)。
应用程序团队对其应用程序运行 SDLC 操作,并跨环境推动更改。 他们不知道应用程序会在每个环境中部署在哪个群集上。 相反,应用程序团队使用部署目标的概念进行操作,而该概念只是环境中被命名的抽象概念。 例如,部署目标可以是开发上的集成、QA 上的功能测试和性能测试、早期采用者、Prod 上的外部用户等。
应用程序团队为每个推出环境定义部署目标,并且知道如何配置其应用程序以及如何为每个部署目标生成清单。 此过程自动进行,并且存在于应用程序存储库空间中。 这将为每个部署目标生成清单,并存储在清单存储中,例如 Git 存储库、Helm 存储库或 OCI 存储。
平台团队对应用程序的了解有限,因此并不参与应用程序配置和部署过程。 平台团队负责按群集类型分组的平台群集。 他们通过配置值说明群集类型,例如 DNS 名称、外部服务的终结点等。 平台团队为各种群集类型分配或计划应用程序部署目标。 确定部署目标后,物理群集上的应用程序行为将由部署目标配置值与群集类型配置值共同确定。
平台团队使用单独的平台存储库,其中包含每种群集类型的清单。 这些清单定义了应在每种群集类型上运行的工作负载,以及应当应用的平台配置值。 群集可以使用其首选协调器从平台存储库中提取该信息,然后应用该清单。
群集将其与平台和应用程序存储库的符合性状态报告到部署可观测性中心。 平台和应用程序团队可以查询此信息,以跨群集分析历史工作负载部署。 此信息可用于仪表板、警报和部署管道,以实现渐进式推出。
让我们看一下高级解决方案体系结构,并了解其主要组件。
平台团队在控制平面中模拟多群集环境。 其设计以人为本,易于理解、更新和审查。 控制平面通过抽象概念(例如群集类型、环境、工作负载、计划策略、配置和模板)进行操作。 这些抽象由向群集类型分配部署目标和配置值的自动化过程进行处理,然后将结果保存到平台 GitOps 存储库。 尽管可能存在数千个物理群集,但平台存储库可在更高级别运行,从而将群集分组为群集类型。
对控制平面存储的主要要求是提供可靠且安全的事务处理功能,而不是针对大量数据进行复杂查询。 可以使用各种技术来存储控制平面数据。
此体系结构设计建议使用包含一组管道的 Git 存储库来跨环境存储和提升平台抽象。 此设计有以下几个优势:
- GitOps 原则的所有优势,例如版本控制、更改审批、自动化、基于拉取的协调。
- GitHub 等 Git 存储库可提供现成的分支、安全性和 PR 审查功能。
- 使用 GitHub Actions 工作流或类似的业务流程协调程序轻松实现提升流。
- 无需维护和公开单独的控制平面服务。
控制平面存储库包含两种类型的数据:
- 跨环境获得提升的数据,例如载入的工作负载列表和各种模板。
- 特定于环境的配置,例如包含的环境群集类型、配置值和计划策略。 此数据不会提升,因为它特定于每个环境。
要提升的数据存储在 main
分支中。 特定于环境的数据存储在相应的环境分支中,例如示例 dev
、qa
和 prod
。 将数据从控制平面转换到 GitOps 存储库是提升和计划流两者的结合。 提升流会跨环境在水平方向上移动更改;计划流则执行计划,并为每个环境以垂直方式生成清单。
向 main
分支的提交将启动提升流,从而逐一触发每个环境的计划流。 计划流将从 main
中获取基本清单,应用与此环境分支对应的配置值,并使用生成的清单创建对平台 GitOps 的 PR。 在此环境中成功完成推出后,提升流会继续进行,并在下一个环境中执行相同的过程。 在每个环境中,流会提升 main
分支的同一提交 ID,从而确保来自 main
的内容只有在成功部署到上一个环境后才会进入下一个环境。
向控制平面存储库中的环境分支进行提交会启动此环境的计划流。 例如,你可能已在 QA 环境中配置了 cosmo-db 终结点。 你只想更新平台 GitOps 存储库的 QA 分支,而不涉及任何其他内容。 计划会提取与提升到此环境的最新提交 ID 对应的 main
内容、应用配置,并将生成的清单提升到平台 GitOps 分支。
在平台 GitOps 存储库中,群集类型的每个工作负载分配由包含以下项的文件夹表示:
- 适用于此类型群集上此环境中的此工作负载的专用命名空间。
- 限制工作负载权限的平台策略。
- 合并的平台配置映射了工作负载可以使用的值。
- 协调器资源,指向存储实际工作负载清单或 Helm 图表的工作负载清单存储。 例如,Flux GitRepository 和 Flux Kustomization、ArgoCD 应用程序、Zarf 描述符等。
每种群集类型可以使用不同的协调器(例如 Flux、ArgoCD、Zarf、Rancher Fleet 等),以从工作负载清单存储传递清单。 群集类型定义是指一个协调器,该协调器定义了清单模板的集合。 计划程序会使用这些模板来生成协调器资源,例如 Flux GitRepository 和 Flux Kustomization、ArgoCD 应用程序、Zarf 描述符等。 可将相同的工作负载计划到由不同的协调器管理的群集类型,例如 Flux 和 ArgoCD。 计划程序将为一个群集生成 Flux GitRepository 和 Flux Kustomization,并为另一个群集生成 ArgoCD 应用程序,但两者都指向包含工作负载清单的同一工作负载清单存储。
平台服务是由平台团队维护的工作负载,例如 Prometheus、NGINX、Fluentbit 等。 与任何工作负载一样,它们具有源存储库和清单存储。 源存储库可能包含指向外部 Helm 图表的指针。 CI/CD 管道会通过容器拉取图表并执行必要的安全扫描,然后再将其提交到清单存储(从该存储中协调到群集)。
部署可观测性中心是一个中央存储,可轻松通过复杂查询来查询大量数据。 它包含部署数据,其中包含有关工作负载版本及其跨群集部署状态的历史信息。 群集会在存储中注册自己,并通过 GitOps 存储库更新其符合性状态。 群集仅在 Git 提交级别运行。 高级信息(如应用程序版本、环境和群集类型数据)将从 GitOps 存储库传输到中央存储。 此高级信息会在中央存储中与从群集发送的提交符合性数据相关联。
部署目标上的应用程序行为由配置值决定。 但是,配置值并不完全相同。 这些值由不同角色在应用程序生命周期中的不同阶段提供,并且具有不同的范围。 通常,有应用程序配置和平台配置。
应用程序开发人员提供的应用程序配置从部署目标详细信息中抽象出来。 通常,应用程序开发人员不知道特定于主机的详细信息,例如应用程序将部署到哪些主机,或者有多少主机。 但是,应用程序开发人员知道应用程序在进入生产阶段前所经过的一系列环境和环节。
此外,应用程序可能会在每个环境中多次部署,扮演不同的角色。 例如,同一应用程序既可以用作 dispatcher
,也可以用作 exporter
。 应用程序开发人员可能希望针对各种用例对应用程序进行不同的配置。 例如,如果应用程序在 QA 环境中作为 dispatcher
运行,则应以这种方式进行配置,而不考虑实际主机。 此类型的配置值在开发时提供,即应用程序开发人员针对各种环境/环节和应用程序角色创建部署描述符/清单时。
除了开发时的配置,应用程序通常还需要特定于平台的配置值,例如终结点、标记或机密。 在部署应用程序的每个主机上,这些值都可能各不相同。 由应用程序开发人员创建的部署描述符/清单会引用包含这些值的配置对象,例如配置映射或机密。 应用程序开发人员希望这些配置对象存在于主机上,并可供应用程序使用。 通常,这些对象及其值由平台团队提供。 根据组织的不同,平台团队角色可能由不同的部门/人员提供支持,例如 IT 全球团队、站点 IT 团队、设备所有者等。
应用程序开发人员和平台团队的关注点是完全分开的。 应用程序开发人员专注于应用程序;他们拥有并配置应用程序。 同样,平台团队拥有并配置平台。 关键是平台团队不配置应用程序,他们为应用程序配置环境。 本质上,他们提供环境变量值供应用程序使用。
平台配置通常包括与使用它们的应用程序不相关的通用配置,以及可能对每个应用程序都是唯一的应用程序特定的配置。
尽管平台团队对应用程序及其工作原理的了解可能有限,但他们知道目标主机上需要提供哪些平台配置。 这些信息由应用程序开发人员提供。 他们指定其应用程序所需的配置值、值类型和约束。 定义此协定的其中一种方法是使用 JSON 架构。 例如:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "patch-to-core Platform Config Schema",
"description": "Schema for platform config",
"type": "object",
"properties": {
"ENVIRONMENT": {
"type": "string",
"description": "Environment Name"
},
"TimeWindowShift": {
"type": "integer",
"description": "Time Window Shift"
},
"QueryIntervalSec": {
"type": "integer",
"description": "Query Interval Sec"
},
"module": {
"type": "object",
"description": "module",
"properties": {
"drop-threshold": { "type": "number" }
},
"required": ["drop-threshold"]
}
},
"required": [
"ENVIRONMENT",
"module"
]
}
此方法在开发人员社区中广为人知,因为 Helm 使用 JSON 架构来定义为 Helm 图表提供的可能值。
正式协定还可以实现自动化。 平台团队使用控制平面来提供配置值。 控制平面会分析应在主机上部署的应用程序。 它使用配置架构来建议平台团队应提供哪些值。 控制平面为每个应用程序实例组合配置值,并根据架构对其进行验证,以查看所有值是否都已正确设置。
控制平面可以在多个阶段的不同时间点执行验证。 例如,当平台团队提供配置值时,控制平面就会对其进行验证,检查其类型、格式和基本约束。 当控制平面在配置快照中为应用程序组合所有可用配置值时,就会执行最终验证,也是最重要的验证。 只有在此时,才能检查是否存在所需的配置值,并检查涉及来自不同源的多个值的完整性约束。
控制平面为部署目标上的应用程序实例组合配置值快照。 它可从不同的配置容器中拉取值。 这些容器之间的关系可能代表层次结构或图形。 控制平面按照一些规则来确定应将哪些容器中的哪些配置值注入到应用程序配置快照中。 平台团队负责定义配置容器并制定注入规则。 应用程序开发人员不知道此结构。 他们知道要提供的配置值,而这些值来自何处不是他们所关注的。
实现配置组合的简单灵活方法是标签匹配方法。
在此关系图中,配置容器以不同级别对配置值进行分组,例如站点、线路、环境和区域。 根据组织的不同,这些容器中的值可能由不同的角色提供,例如 IT 全球团队、站点 IT 团队、设备所有者,或者仅平台团队。 每个容器都标有一组标签,用于定义此容器中的值适用的情况。 除了配置容器,还有表示应用程序和要部署应用程序的主机的抽象。 这两个也标有标签。 应用程序标签和主机标签的组合构成了实例的标签集。 此集确定应拉取到应用程序配置快照中的配置容器的值。 此快照将传送到主机,并馈送到应用程序实例。 控制平面循环访问容器,并评估容器的标签是否与实例的标签集匹配。 如果匹配,容器的值将包含在最终快照中;如果不匹配,则会跳过容器。 控制平面可以针对复杂对象和数组配置不同的替代和合并策略。
此方法最大的优点之一是可伸缩性。 配置容器的结构从应用程序实例中抽象出来,而应用程序实例并不真正了解这些值的来源。 这样,平台团队就可以轻松操作配置容器,引入新的级别和配置组,而无需重新配置数百个应用程序实例。
控制平面为每个主机上的每个应用程序实例组合配置快照。 应用程序、主机、基础技术和应用程序部署方式可能多种多样。 此外,同一应用程序在从开发环境到生产环境过程中的部署方式可能完全不同。 控制平面关注的是管理配置,而不是部署。 它应该与基础应用程序/主机技术无关,并针对每种情况生成适合格式的配置快照(例如,Kubernetes 配置映射、属性文件、Symphony 目录或其他格式)。
一个选项是将不同的模板分配给不同的主机类型。 当控制平面为要部署在主机上的应用程序生成配置快照时,会使用这些模板。 应用在开发人员社区中广为人知的标准模板化方法会很有帮助。 例如,可以使用行业内广泛使用的 Go 模板来定义以下模板:
# Standard Kubernetes config map
apiVersion: v1
kind: ConfigMap
metadata:
name: platform-config
namespace: {{ .Namespace}}
data:
{{ toYaml .ConfigData | indent 2}}
# Symphony catalog object
apiVersion: federation.symphony/v1
kind: Catalog
metadata:
name: platform-config
namespace: {{ .Namespace}}
spec:
type: config
name: platform-config
properties:
{{ toYaml .ConfigData | indent 4}}
# JSON file
{{ toJson .ConfigData}}
然后,我们将这些模板分别分配给主机 A、B 和 C。 假设将具有相同配置值的应用程序部署到这三个主机,则控制平面会为每个实例生成三个不同的配置快照:
# Standard Kubernetes config map
apiVersion: v1
kind: ConfigMap
metadata:
name: platform-config
namespace: line1
data:
FACTORY_NAME: Atlantida
LINE_NAME_LOWER: line1
LINE_NAME_UPPER: LINE1
QueryIntervalSec: "911"
# Symphony catalog object
apiVersion: federation.symphony/v1
kind: Catalog
metadata:
name: platform-config
namespace: line1
spec:
type: config
name: platform-config
properties:
FACTORY_NAME: Atlantida
LINE_NAME_LOWER: line1
LINE_NAME_UPPER: LINE1
QueryIntervalSec: "911"
{
"FACTORY_NAME" : "Atlantida",
"LINE_NAME_LOWER" : "line1",
"LINE_NAME_UPPER": "LINE1",
"QueryIntervalSec": "911"
}
控制平面使用配置容器来操作,这些容器在层次结构或图形中对不同级别的配置值进行分组。 这些容器应存储在某个位置。 最明显的方法是使用数据库。 它可以是 etcd、关系数据库、分层数据库或图形数据库,因此提供了最为灵活、最可靠的体验。 数据库能够在每个单个配置容器级别精细跟踪和处理配置值。
除了存储以及能够有效查询和操作配置对象等主要功能外,还应该具有与更改跟踪、审批、提升、回滚、版本比较等相关的功能。 控制平面可以在数据库之上实现所有这些功能,并将所有内容封装在整体托管服务中。
或者,也可以将此功能委托给 Git,以遵循“配置即代码”概念。 例如,Kalypso是一个 Kubernetes 操作器,可将配置容器视为自定义 Kubernetes 资源,这些资源基本上存储在 etcd 数据库中。 尽管控制平面没有规定要这样,但通常的做法是在 Git 存储库中生成配置值,并应用它现时提供的所有优势。 然后,使用 GitOps 操作器将配置值交付给 Kubernetes etcd 存储,控制平面可以在其中使用它们来执行组合。
不需要为整个组织设置一个单一的 Git 存储库来存储配置值。 鉴于“平台团队”的角色、职责及其访问级别多种多样,这样的存储库在规模较大时可能会成为瓶颈。 相反,可以使用 GitOps 操作器引用(如 Flux GitRepository 和 Flux Kustomization)生成存储库层次结构并消除摩擦点:
每当应用程序开发人员在应用程序中引入更改时,都会生成新的应用程序版本。 同样,新的平台配置值也会产生新版本的配置快照。 借助版本控制就可以跟踪更改、显式推出和回滚。
关键点是应用程序配置快照彼此独立进行版本控制。 全局级别或站点级别的单个配置值更改不一定会将所有应用程序配置快照生成新版本;它仅影响注入此值的快照。 一个简单有效的跟踪方法是使用快照内容的哈希作为其版本。 这样,如果快照内容因全局配置中发生了更改而改变,就会出现新版本。 这个新版本可以通过手动或自动方式进行应用。 在任何情况下,这都是一个可跟踪的事件,如果需要,可以回滚。