Kubernetes-平台工程-全-
Kubernetes 平台工程(全)
原文:Platform Engineering on Kubernetes
译者:飞龙
前置材料
前言
云原生领域已经成熟到我们可以开始构建实用解决方案的程度。大量项目涌现出来,每个项目都专注于解决更大愿景的一部分。我们现在发现自己正在努力将这些不同的项目拼凑成一个端到端的产品。我们如何管理这些工具列表带来的固有复杂性,并构建一个完整的解决方案?
Mauricio Salatino 的《Kubernetes 平台工程》提供了全面的答案,以平台工程的形式回答了这个问题。平台工程的学科定位是通过高效且可靠的软件交付到生产环境,使云原生开发对应用开发者变得可访问。我认为平台工程是至关重要的现代学科,它将驯服复杂性并实现很久以前当 Kubernetes 首次将云原生技术带给大众时所做出的诱人承诺。
本书提供了必要的见解,说明了现代平台如何被构建以有效地整合生态系统中最有用的云原生技术,并为你的平台的应用开发者客户解决实际问题。它通过实际操作练习和示例,有效地提供了实用的指导,以培养构建有意义平台解决方案的实际技能。这些页面中的宝贵信息将使平台团队能够构建一个自助开发者平台作为他们的产品,使开发者能够以前所未有的速度和可靠性将他们的应用程序交付到生产环境中。
在我作为 Cloud Native Computing Foundation 两个不同项目的共同创建者、维护者和指导委员会成员的时间里,我亲自遇到了云原生生态系统中的许多令人惊叹的个体。从我的经验来看,毛里西奥在撰写这本有益的书籍方面处于独特的位置,这本书将引导你将这些项目整合成一个完整的平台,因为他自己一直通过在多次场合将人、社区和技术结合在一起,在生态系统中一直是一个整合者。毛里西奥展现了一种不可思议的能力,能够识别项目愿景中的共同利益,将合适的人聚集在一起,并找到统一努力的共同基础。他一直致力于他罕见的才能,通过我们的协作和协同作用而不是竞争或重复,找到让我们共同变得更好的路径。
就像毛里西奥将人和技术结合在一起一样,他也将许多项目整合成了这些页面中的一个有价值的整体。我期待这本书中学到的经验将成为你在实现平台工程愿景过程中最值得的步骤之一。请享受这段旅程!
——Jared Watts
Upbound 创始工程师
前言
我在两年多前开始写这本书。在为云原生社区工作超过四年之后,我学到了许多我想与团队分享的经验教训,以加快他们采用 Kubernetes 的旅程。因为我为几个开源项目做出了贡献(其中大多数包含在这本书中),为书中的想法创建目录表并不困难。另一方面,写一本关于永远在变化中的生态系统的书是具有挑战性的。但正如你阅读本书时会发现的那样,平台工程就是管理不断发展的项目和不同团队的需求的复杂性,这些团队需要正确的工具来完成他们的工作。
这本书让我有机会结识并和来自不同背景和社区的行业中最优秀的人一起工作,他们与我有着共同的激情:开源、云原生和知识分享。我环游世界,在云原生领域的会议上发表演讲,始终从社区成员、开发者和努力跟上每天创建的大量开源项目的团队那里收集反馈。我希望这本书能帮助你和你的团队评估、集成和构建在 Kubernetes 之上的平台。
致谢
我要特别感谢所有为本书提供的示例做出贡献的人(包括github.com/salaboy/from-monolith-to-k8s/上的原始仓库和github.com/salaboy/platforms-on-k8s/上的新仓库)。这本书是为并由提到的项目社区所写。
特别感谢我的兄弟 Ezequiel Salatino (salatino.me/),他设计和构建了前端应用程序,让读者能够体验一个网站而不是一堆 REST 端点。我将永远感激 Matheus Cruz 和 Asare Nkansah,他们帮助我构建了示例的大块内容,而没有期待任何回报。最后,感谢我的朋友 Thomas Vitale,他分享了对多个版本草稿的详细评论;你所有的评论使本书的内容更加准确和专注。
没有曼宁团队提供的所有支持,我无法完成这本书。我要感谢开发编辑 Ian Hough,他为稿件投入了无数小时。采购编辑 Michael Stephens 自始至终坚信这本书的想法,Raphael Villela 作为技术编辑提供了所有技术建议,Werner Dijkerman 作为技术校对员,他的评论和确保所有代码都能良好运行。
致所有审稿人:Alain Lompo、Alexander Schwartz、Andres Sacco、Carlos Panato、Clifford Thurber、Conor Redmond、Ernesto Cárdenas Cangahuala、Evan Anderson、Giuseppe Catalano、Gregory A. Lussier、Harinath Mallepally、John Guthrie、Jonathan Blair、Kent Spillner、Lucian Torje、Michael Bright、Mladen Knezic、Philippe Van Bergen、Prashant Dwivedi、Richard Meinsen、Roman Levchenko、Roman Zhuzha、Sachin Rastogi、Simeon Leyzerzon、Simone Sguazza、Stanley Anozie、Theo Despoudis、Vidhya Vinay、Vivek Krishnan、Werner Dijkerman、WIlliam Jamir、Zoheb Ainapore,你们的建议帮助使这本书变得更好。
项目特定感谢:
-
Argo Project (
argoproj.github.io/)—我想感谢 Codefresh 的 Dan Garfield 对本书的持续支持以及他对 OpenGitOps (opengitops.dev/) 初始化的贡献。 -
Crossplane (
crossplane.io)—我想感谢 Jared Watts 不断愿意帮助并推动事物向前发展。同时,我想感谢 Viktor Farcic 和 Stefan Schimanski 对 Crossplane 社区的持续支持。Crossplane 社区教会了我许多宝贵的经验,塑造了我的职业生涯。 -
Dagger (
dagger.io)—我想感谢 Marcos Nils 和 Julian Cruciani 在 Dagger 示例方面的帮助以及他们愿意在为开发者节省时间时改进事物的意愿。 -
Dapr (
dapr.io)—对 Yaron Schneider 和 Mark Fussel 表示衷心的感谢和赞赏,他们不断支持本书的出版,以及对整个 Diagrid (diagrid.io) 团队,他们正在 Dapr 之上构建令人惊叹的产品。 -
Keptn (
keptn.sh)—非常感谢 Giovanni Liva 和 Andreas Grabner 的快速响应以及他们在 Keptn 和 OpenFeature 社区中做出的令人惊叹的工作。 -
Knative (
knative.dev)—整个 Knative 社区都很棒,但特别感谢 Lance Ball,他领导了 Knative Functions 工作组,构建了令人惊叹的东西。 -
Kratix (
kratix.io)—特别感谢 Abby Bangser 分享她的平台见解并审阅本书的关键章节。你所有的评论和意见使这本书的价值大大提升。 -
OpenFeature (
openfeature.dev)—我想感谢 James Milligan 在使 OpenFeature 和flagd示例工作方面的帮助。 -
Tekton (
tekton.dev)—非常感谢 Andrea Fritolli 在 Tekton 社区中的出色工作,以及他总是及时回复我的 Slack 消息。 -
Vcluster (
vcluster.com)—Ishan Khare 和 Fabian Kramm 对我在这本书中所做的工作至关重要。他们让事情运转起来的意愿已经超越了极限。对创建和维护vcluster、Devspace (www.devspace.sh/)和 DevPod (devpod.sh/)项目表示衷心的感谢。
关于这本书
Kubernetes 上的平台工程是为了帮助正在经历 Kubernetes 采用之旅的团队而编写的。本书采用以开发者为中心的方法来涵盖构建、打包和部署云原生应用到 Kubernetes 集群,但并不止于此。一旦你和你团队了解了如何为你的应用使用 Kubernetes,你将面临与管理和扩展 Kubernetes、多租户和多集群设置相关的新挑战。
在 Kubernetes 之上的平台需要集成广泛的各种工具,以使专业团队能够在执行日常任务的同时,防止他们学习所有这些工具的工作原理。平台团队负责学习、整理和集成工具,以使开发团队、数据科学家、运维团队、测试团队、产品团队以及参与你组织软件交付流程的每个人都更容易生活。
大部分内容都集中在 Kubernetes 上,并构建为对用于特定功能的技术栈无关。如果你刚开始使用 Kubernetes,或者你是一名云原生实践者,这本书可以帮助你了解如何将多个项目结合起来构建团队特定的体验,并减少你在日常工作中涉及的认知负荷,无论你和你团队使用的编程语言是什么。
本书是如何组织的:路线图
本书分为九章,并使用“行走骨架”的概念构建一个平台,以支持团队构建会议应用。本书的流程如下:
第一章介绍了平台是什么,为什么你需要一个,以及我们将在这本书中涵盖的平台与云提供商提供的平台相比如何。本章介绍了会议应用的商业用例,后续章节将进一步探讨。
第二章评估了在 Kubernetes 上构建云原生和分布式应用的挑战。本章鼓励读者部署会议应用,并通过更改其配置和测试不同场景来探索其设计。通过分析团队在部署和运行 Kubernetes 上的应用时将面临的挑战,并提供一个使用行走骨架进行实验的游乐场,本书旨在使有足够经验的读者能够应对更大的挑战。
第三章专注于构建、打包和分发工件所需的所有额外步骤,以便在不同的云服务提供商中运行我们的应用程序。本章介绍了服务管道的概念,并探讨了两个不同但互补的项目:Tekton 和 Dagger。
当我们的工件准备就绪,可以部署时,第四章围绕环境管道的概念展开。通过定义我们的环境管道并采用 GitOps 方法,团队可以使用声明式方法管理多个环境的配置。本章探讨了 Argo CD 作为配置和管理环境的工具。
应用程序不能独立工作。大多数应用程序需要基础设施组件,例如数据库、消息代理和身份提供者等,才能正常运行。第五章介绍了使用名为 Crossplane 的项目在云服务提供商之间部署应用程序基础设施组件的 Kubernetes 原生方法。
一旦我们处理好了构建、打包和部署我们的应用程序以及应用程序运行所需的其它组件,第六章建议读者在 Kubernetes 之上构建一个平台,使用到目前为止所学的一切,但仅关注一个简单的用例:创建开发环境。
平台不仅仅是创建环境、管理集群和部署应用程序。平台应该为团队提供定制的工作流程以提高生产力。第七章专注于通过应用级 API 使开发团队更高效,平台团队可以决定如何将这些 API 连接到可用资源。本章评估了 Dapr 和 OpenFeature 等工具,以使团队拥有更多资源,并为他们提供一个运行应用程序的地方。
虽然提高开发者的效率可以缩短软件交付时间,但如果新版本被阻塞且没有在客户面前部署,所有努力都将白费。第八章专注于展示可以在完全承诺之前对新版本进行实验的技术,即更精确地说,是发布策略。本章评估了 Knative Serving 和 Argo Rollouts,以实现不同的发布策略,让团队可以以受控的方式实验新功能。
由于平台是软件,我们需要衡量我们在演进它们时的有效性。第九章评估了两种方法来利用我们构建平台时使用的工具,并计算关键指标,这些指标允许平台工程团队评估他们的平台项目。本章探讨了 CloudEvents、CDEvents 和 Keptn 生命周期工具包作为收集事件、存储它们并聚合它们以计算有意义的指标的选择。
到本书结束时,读者将获得一个清晰的图景和实际操作经验,了解如何在 Kubernetes 之上构建平台,平台工程团队的重点是什么,以及为什么学习和跟上云原生领域的发展对于成功如此重要。
关于代码
本书包含许多源代码示例,既有编号列表,也有与普通文本并列。在这两种情况下,源代码都使用固定宽度字体(如本例所示)格式化,以将其与普通文本区分开来。有时代码也会加粗,以突出显示与章节中先前步骤相比已更改的代码,例如当新功能添加到现有代码行时。
在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新调整了缩进,以适应书中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中移除。许多列表旁边都有代码注释,突出显示重要概念。
您可以从本书的在线版本(liveBook)中获取可执行的代码片段,链接为livebook.manning.com/book/platform-engineering-on-kubernetes。书中示例的完整代码可以在 Manning 网站上下载,链接为www.manning.com/books/platform-engineering-on-kubernetes。
每一章都链接到逐步教程,鼓励读者在自己的环境中动手操作工具和项目。您可以在以下 GitHub 仓库中找到所有源代码和逐步教程:github.com/salaboy/platforms-on-k8s/。
liveBook 讨论论坛
购买《Kubernetes 平台工程》包括对 liveBook(Manning 的在线阅读平台)的免费访问。使用 liveBook 的独特讨论功能,您可以在全局或特定章节或段落中附加评论。为自己做笔记、提问和回答技术问题,以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问livebook.manning.com/book/platform-engineering-on-kubernetes/discussion。您还可以在livebook.manning.com/discussion了解更多关于 Manning 论坛和行为准则的信息。
Manning 对我们读者的承诺是提供一个场所,在那里个人读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要这本书还在印刷中,论坛和先前讨论的存档将可通过出版社的网站访问。
关于作者

Mauricio Salatino 在 Diagrid(diagrid.io)担任开源软件工程师。他目前是 Dapr OSS 贡献者和 Knative 指导委员会成员。在 Diagrid 工作之前,Mauricio 在红帽和 VMware 等公司为云原生开发者构建工具,已经过去了 10 年。当他不为开发者编写工具或为云原生领域的开源项目做出贡献时,他通过他的博客salaboy.com和/或 LearnK8s(learnk8s.io)教授 Kubernetes 和云原生知识。
关于封面插图
《Kubernetes 上的平台工程》封面上的图像是“来自 Argentiera 和 Milo 群岛的女性”,或称为“来自 Argentiera 和 Milo 群岛的女性”,取自 Jacques Grasset de Saint-Sauveur 的收藏,1788 年出版。每一幅插图都是手工精心绘制和着色的。
在那些日子里,仅凭人们的服饰就能轻易地识别出他们居住的地方以及他们的职业或社会地位。Manning 通过基于几个世纪前丰富多样的地域文化的书封面,庆祝计算机行业的创新精神和主动性,这些文化通过如这一系列图片的图片得以重现。
1 (在 Kubernetes 之上的)平台兴起
本章涵盖
-
理解平台及其必要性
-
在 Kubernetes 之上构建平台
-
介绍一个“行走骨架”应用程序
平台工程在技术行业中不是一个新术语。但在云原生空间和 Kubernetes 的背景下,它相当新颖。当我 2020 年开始写这本书时,我们并没有在云原生社区中使用这个术语。然而,到撰写这本书的时候(2023 年),平台工程已经成为云原生和 Kubernetes 社区中的新热点。本书旨在进行一次探索之旅,了解平台是什么,为什么你会使用 Kubernetes,以及更具体地说,使用核心的 Kubernetes API 来构建平台,并使你的内部团队能够更高效地交付软件。
要了解为什么平台工程成为行业趋势,你首先需要了解云原生和 Kubernetes 生态系统。由于本书假设你已经熟悉 Kubernetes、容器和云原生应用程序,我们将专注于描述你在基于 Kubernetes 和云提供商构建和运行这些应用程序时将面临的挑战。我们将采取以开发者为中心的方法,这意味着大多数涵盖的主题都是以与开发者的日常任务相关的方式处理的,以及云原生空间中的无数工具和框架将如何影响他们。
每个软件开发团队的最高目标是向客户交付新的功能和错误/安全修复。新的功能和更稳定的应用程序直接转化为竞争优势和满意的客户。为了更高效地交付更多软件,开发团队必须能够访问他们完成工作所需的工具。平台和平台工程团队的主要目标是使开发者能够更高效地交付软件。这需要一种不同的技术方法,以及一种文化转变,将我们即将构建的平台视为内部客户。
我们将在整个章节中使用一个简单的应用程序(由多个服务组成)作为示例,以构建一个平台,该平台通过使用云原生空间中的所有开源工具,支持团队构建、发布和管理此应用程序。
1.1 什么是平台,为什么我需要一个?
平台是一组服务,帮助公司让他们的软件在客户面前运行(内部或外部)。平台旨在成为团队的一站式商店,拥有所有他们需要的工具,以保持高效并持续交付业务价值——随着流行度的上升和日益增长的提高开发周期的需求,曾经只提供计算资源的平台已经升级了堆栈,提供了越来越多的服务。
平台并不新鲜,云平台也是如此。像 AWS、Google、Microsoft、Alibaba 和 IBM 这样的云服务提供商已经为我们提供了多年的平台。这些云服务提供商为团队提供了许多工具,让他们可以使用按需付费的模式来构建、运行和监控他们的业务关键型应用程序。从业务敏捷性的角度来看,这些云服务提供商提供的平台从根本上改变了使用其服务的团队对期望的设定。这使得公司和团队能够快速启动并创建可以全球扩展的应用程序,而无需进行大量初始投资。如果没有人使用他们正在构建的应用程序,他们的账单在月底时不会很大。在光谱的另一端,如果你成功了,你的应用程序很受欢迎,你必须为月底的大额账单做好准备。你使用的资源(存储、网络流量、服务等)越多,你支付的金额就越多。另一个需要考虑的方面是,如果你依赖云服务提供商提供的工具,随着整个组织习惯了该云服务提供商的工具、工作流程和服务,你将更难离开他们。在不同提供商之间规划和迁移应用程序将变成一种痛苦的经历。
在接下来的章节中,我们将介绍云平台当前的状况以及本书将讨论哪些类型的平台。最近,正如我们行业经常发生的那样,那些可以用来描述非常具体工具和实践的术语往往被营销团队滥用,变成了流行语。我们必须为本书的其余部分设定上下文,以避免混淆。
1.1.1 云服务和特定领域需求
我们可以将云服务组织成不同的层次,这是我们为了了解行业目前处于何种状态以及它将走向何方所必须做的。以下图表显示了云服务提供商提供的服务的一系列类别,从低级基础设施服务,如按需提供硬件,到高级应用程序服务,开发者可以与机器学习模型交互,而无需担心这些模型运行在哪里。图 1.1 显示了这些层次,从底层的低级计算资源开始,向上层扩展到应用层和行业特定服务。

图 1.1 云服务提供商的服务类别
类别越高,你为服务支付的费用就越高,因为这些服务通常为你处理所有底层层和运营成本。例如,假设你在云服务提供商提供的托管服务中配置了一个新的高可用性 PostgreSQL 数据库。图 1.2 显示了一个关系型数据库,如 PostgreSQL 的示例。

图 1.2 在云中配置 PostgreSQL 数据库实例
在这种情况下,服务成本包括所需数据库软件的成本和管理、数据库运行的操作系统以及运行所需的硬件。因为您可能希望监控并获取数据库在应用承受重负载时的性能指标,云服务提供商也会为服务配置所有可用的监控工具。然后,您需要自己进行计算:是否值得支付云服务提供商为我们做出所有这些决定,或者您能否组建一个拥有足够知识、能够本地运行和操作所有这些软件和硬件的内部团队?有时,金钱可能不是问题;您必须处理公司或行业的政策法规。在这种情况下,您是否可以在云服务提供商处运行工作负载和托管数据?
1.1.2 您作为组织的职责
跟踪所有提供的服务、库、框架和工具是一项全职工作。运营和维护公司运行应用所需的广泛软件和硬件需要您拥有合适的团队,最终,如果您在软件交付实践中不是一个大型的成熟组织,或者您通过管理自己的硬件/软件堆栈没有获得任何竞争优势,采用云服务提供商通常是正确的选择。
每个公司和开发人员的职责仍然是查看可用的服务,并选择他们将使用什么以及如何混合匹配这些服务来构建新功能。在组织中发现云架构师(特定云服务提供商的专家或本地专家)定义如何以及使用哪些服务来构建核心应用是很常见的。与云服务提供商的咨询服务合作以获得特定用例和最佳实践的咨询和指导也是很常见的。
云服务提供商可能会建议工具和工作流程来创建应用程序。然而,每个组织都需要经历一个学习曲线,并成熟其应用这些工具以解决特定挑战的实践。聘请云服务专家始终是一个好主意,因为他们从以往的经验中带来知识,为经验较少的团队节省时间。
在本书中,我们将专注于组织特定的平台,而不是那些可以直接购买的通用云平台,例如云服务提供商所提供的那些。我们还希望关注可以在我们组织硬件上本地运行的平台。这对于不能在公共云上运行的更多受监管行业来说非常重要。这迫使我们拥有更广泛的视角,考虑可以应用于云服务提供商领域之外的工具、标准和工作流程。从多个云服务提供商那里消费服务也越来越受欢迎。这可能是由于为一家收购了或被另一家使用不同提供商的公司工作,最终导致多个提供商必须共存,并且应该有一个共享策略。在其他情况下,在更多受监管的行业中,组织被迫在不同的提供商(包括本地工作负载)上运行工作负载,以确保在整个云服务提供商可能崩溃的情况下保持弹性。
我们将要探讨的平台类型将之前提到的客户行为层扩展到包括公司特定的服务、公司特定的标准和开发体验,这些体验允许组织的开发团队为组织和他们的客户构建复杂系统。图 1.3 展示了无论我们是在消费云服务、第三方服务还是内部服务,组织都必须通过在顶部构建专注于解决特定业务挑战的层来混合和匹配这些服务。

图 1.3 组织特定层
这些额外的层大多数情况下是现有服务、数据和事件源之间的“胶水”,它们结合在一起解决业务面临的特定挑战或为他们的客户提供新功能。依赖于第三方服务提供商提供更多特定于业务的工具是很常见的,例如行业特定的或通用的客户关系管理(CRM)系统,如 Salesforce。
对于客户来说,平台、云服务提供商以及服务运行的位置完全无关紧要。在内部,对于开发团队来说,平台作为开发团队完成工作的推动者。平台不是静态的,它们的主要目标是帮助组织改进并擅长持续向客户提供高质量的软件。
无论您的公司在哪个行业运营,无论您是否选择使用云服务提供商,您的公司用于向客户交付新功能的工具和工作流程的组合都可以描述为您的平台。从技术角度来看,平台全部关于系统集成、最佳实践和可组合服务,我们可以将它们组合起来构建更复杂的系统。本书将探讨使平台成功的标准实践、工具和行为,以及您如何构建云原生平台,无论是在一个或多个云服务提供商上运行还是在本地运行。
我们将使用云服务提供商作为参考,比较它们提供的服务和工具,并学习如何通过使用开源工具,在多云服务和本地环境中实现类似的结果。但在探讨具体工具之前,了解我们可以从云服务提供商那里获得什么样的体验是至关重要的。
1.1.3 与云平台合作
所有云服务提供商的共同特点是它们都采用以 API 为首要的方法来提供服务。这意味着要访问它们提供的任何服务,用户都将有一个 API 可供请求和交互。这些 API 暴露了所有服务功能,例如可以创建哪些资源,使用哪些配置参数,资源在哪里(在世界的哪个地区)运行等。这些 API 的另一个重要方面是它们要求一个团队拥有这些 API 定义;这意味着一个团队将负责确定这些 API 将如何被使用,以及它们将如何演变,并明确定义这些 API 不负责的内容。
可以通过查看它们的 API 来分析每个云服务提供商,因为通常每个提供的服务都会有一个 API。常见的情况是,服务仅在测试阶段通过 API 提供给早期用户进行实验、测试和提供反馈,直到服务正式发布。虽然云服务提供商提供的所有服务的结构、格式和风格通常相似,但云服务提供商之间没有标准来定义这些服务应该如何公开以及它们需要支持哪些功能。
手动针对云服务提供商的服务和 API 构建复杂请求是复杂且容易出错的。云服务提供商通常通过提供 SDK(软件开发工具包)来简化开发者的生活,这些 SDK 消耗了用不同编程语言实现的 API 服务。这意味着开发者可以通过在应用程序中包含一个依赖项(库、云服务提供商 SDK)来程序化地连接和使用云服务提供商的服务。虽然这很方便,但它引入了应用程序代码和云服务提供商之间的一些强烈依赖,有时甚至需要我们发布应用程序代码来升级这些依赖。
与 API 一样,SDKs 没有标准,每个 SDK 都严重依赖于编程语言生态系统中的最佳实践和工具。有些情况下,SDKs/客户端与您所使用的编程语言中流行的框架或工具不兼容。SDKs/客户端可能出错的情况包括与云服务提供商提供的版本不匹配的数据库驱动程序,或者云服务提供商尚未支持的编程语言和生态系统。在这种情况下,直接访问 API 是可能的,但很困难,通常不鼓励这样做,因为您的团队将维护连接到云服务提供商服务的所有代码。
云服务提供商还提供 CLIs(命令行界面),这些界面是操作团队和一些开发者工作流程的工具。CLIs 是您可以从操作系统终端下载、安装和使用的二进制文件。CLIs 直接与云服务提供商的 API 交互,但不需要您知道如何创建新应用程序来与 SDKs 交互的服务。CLIs 对于持续集成和自动化管道特别有用,在这些管道中,可能需要按需创建资源,例如运行我们的集成测试。
图 1.4 展示了应用程序和自动化,例如 CI/CD 管道和集成测试,它们消耗相同的 API,但使用由云服务提供商设计的不同工具来简化这些场景。该图还显示了仪表板组件,通常在云服务提供商内部运行,它提供了对所有创建的服务和资源的可视化访问。

图 1.4 云服务提供商的 SDKs、CLIs 和仪表板客户端
最后,由于提供的服务数量和服务之间的相互连接,云服务提供商提供仪表板和用户界面,以便访问和交互所有提供的服务。这些仪表板还提供报告、计费和其他难以使用 CLIs 或直接通过 API 可视化的功能。通过使用这些仪表板,用户可以访问大多数由服务提供的标准功能,并实时查看云服务提供商内部正在创建的内容。
正如之前提到的,仪表板、CLIs 和 SDKs 需要您的团队了解许多云服务提供商特定的流程、工具和术语。由于每个云服务提供商提供的服务数量,难怪找到能够覆盖多个服务提供商的专家具有挑战性。
由于这是一本以 Kubernetes 为重点的书,我想展示云服务提供商创建 Kubernetes 集群所提供的服务体验,这展示了 Google Cloud Platform 提供的仪表板、CLI 和 API。一些云服务提供商提供的体验比其他提供商更好,但总体而言,您应该能够使用所有主要的服务提供商实现相同的效果。
1.1.4 GCP 仪表板、CLIs 和 API
查看图 1.5 中的 Google Kubernetes Engine 仪表板以创建新的 Kubernetes 集群。一旦你点击创建新集群,就会弹出一个表单,要求你填写一些必填字段,例如集群的名称。

图 1.5 Google Kubernetes Engine 创建表单
云服务提供商在提供合理的默认值方面做得非常出色,以避免在创建所需资源之前让你填写 200 个参数。一旦你填写了所有必填字段,表单就提供了一个快速启动配置过程的方法,只需点击底部的创建按钮即可。在这种情况下,Google Cloud Platform 为你提供了你配置的资源每小时的预估成本,这突出了为技术团队提供功能和提供全面服务之间的差异,全面服务涵盖了技术团队的需求,并阐明了这些决策如何影响整个业务。你可以开始调整参数以查看成本如何变化(通常,它会增加)。

图 1.6 通过仪表板、REST 或使用命令行界面(CLI)工具创建
如图 1.6 所示,在创建按钮旁边,你可以看到 REST 选项。这里的云服务提供商通过构建 REST 请求到它们的 API,帮助你创建可以配置的资源,使用表单即可完成。如果你不想花费数小时查看它们的 API 文档来找到负载的形状和创建请求所需的属性,这将非常方便;参见图 1.7。

图 1.7 使用 REST 请求通过 Kubernetes 集群创建
最后,CLI 命令选项,使用云服务提供商的 CLI,在本例中是gcloud,再次被构建以包含 CLI 命令所需的全部参数,这些参数基于你在表单中配置的内容,如图 1.8 所示。

图 1.8 使用gcloud CLI 通过 Kubernetes 集群创建
注意图 1.8 中的水平滚动;这个命令可以变得极其复杂。Google Cloud Platform 的用户体验团队已经做了出色的工作,通过依靠合理的默认值来简化团队设置所有这些参数的方式。在这些方法之间,预期的行为没有差异,但您需要考虑的是,当您使用云服务提供商的仪表板时,您的账户凭证正在当前会话中使用。如果您在云服务提供商的网络之外构建请求或使用 CLI,您必须在发出请求或执行创建资源(s)的命令之前先与云服务提供商进行身份验证。重要的是要注意,这些交互将因云服务提供商而异。您不能期望 AWS 或 Azure 中的命令、仪表板交互或安全机制如何验证 CLI 或 REST 请求的方式相似。
1.1.5 为什么云服务提供商能工作?
虽然可以争论仪表板、CLI、API 和 SDK 是我们将从云服务提供商那里消费的主要工件,但最大的问题是:我们将如何结合这些工具来交付软件?假设您分析为什么全球的组织信任 AWS、Google Cloud Platform 和 Microsoft Azure。您可能会发现,通过采用 API 优先的方法并提供仪表板、CLI、SDK 和众多服务,这些平台为团队提供了三个主要特性,这些特性定义了当今的平台(图 1.9):
-
APIs (合约): 无论您使用哪种工具,平台都必须公开一组 API,使团队能够消费或配置他们完成工作所需的资源。这些 API 是平台工程团队负责维护和发展的责任。
-
黄金路径到生产环境: 平台将团队将更改推送到生产环境所需的流程编码和自动化,这些生产环境允许真实客户/用户访问。
-
可见性: 在任何时候,通过查看云服务提供商的仪表板,组织可以监控正在使用的资源,每个服务的成本,处理事件,并全面了解组织如何交付软件。

图 1.9 云服务提供商平台的优势
这些关键特性是通过一种具有竞争力的按需付费模式提供的,该模式高度依赖需求(流量),在全球范围内(并非所有服务),允许组织将所有运营和基础设施成本外部化。
当云服务提供商不断向更高层次发展(提供高级服务,而不仅仅是提供硬件和应用基础设施,如数据库),您的团队仍然需要学习和整合这些服务来解决他们的业务挑战。
这就是为什么 Kubernetes 和 CNCF 景观(云原生计算基金会,www.cncf.io/)成为探索如何构建云服务提供商无关的平台的关键领域,这些平台允许我们从众多充满活力的项目中挑选和选择。让我们继续探讨下一个话题。
1.2 基于 Kubernetes 构建的平台
我们简要讨论了平台是什么以及云服务提供商如何推动前进,以定义这些平台可以为负责交付软件的组织和开发团队做什么。但这与 Kubernetes 如何对应?Kubernetes 不是一个平台吗?
Kubernetes 被设计成我们云原生应用的声明式系统。Kubernetes 定义了一系列构建块,使我们能够运行和部署我们的工作负载。如今,每个主要的云服务提供商都提供 Kubernetes 管理的服务,这使我们能够以标准化的方式(容器)打包和部署工作负载到云服务提供商。因为 Kubernetes 自带工具和生态系统(CNCF 景观,landscape.cncf.io/)),你可以创建云无关的工作流程来构建、运行和监控你的应用程序。但学习 Kubernetes 只是起点,因为 Kubernetes 提供的构建块非常底层,旨在组合构建工具和系统,以解决更具体的场景。将 Kubernetes 提供的这些底层构建块结合起来,构建更复杂的工具来解决更具体的问题,是一个自然的进化步骤。
虽然 Kubernetes 为我们提供了 API(Kubernetes API)、CLI(kubectl)和仪表板(Kubernetes 仪表板,kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/)),但 Kubernetes 不是一个平台。Kubernetes 是一个元平台或构建平台的平台,因为它提供了构建具体平台所需的所有构建块,这些平台将解决特定领域的挑战。
图 1.10 展示了 Kubernetes 工具和组件如何映射到我们讨论的平台和云服务提供商。

图 1.10 Kubernetes 为我们提供了 CLI、SDK 和仪表板,它是平台吗?
Kubernetes 可以扩展,这也是为什么这本书将探讨使用 Kubernetes API、工具或内部机制来解决通用挑战(如持续集成、持续交付、云资源配置、监控和可观察性、开发者体验等)的具体项目。
1.2.1 Kubernetes 采用之旅
无论如何,平台团队选择哪些工具,我们都必须抽象出所有复杂性以及我们编写的粘合代码,以便使这些工具协同工作。请记住,应用程序开发团队、测试团队和运维团队等,都有不同的优先级和关注点。作为 Kubernetes 采用之旅的一部分,我们必须意识到,并非所有使用这些工具的团队都是 Kubernetes 的专家。
使用您自定义的扩展来扩展 Kubernetes 是使其适应您组织特定挑战的一种方法。请记住,无论您在您的 Kubernetes 集群中编写或安装哪些工具,运维团队都需要在您的生产环境中运行它们,并保持其大规模运行。您编写的每个新工具或扩展都将需要培训消费者团队了解这些工具的工作原理以及它们是为哪些场景设计的。很容易陷入这样的情况:您选择了 10 个不同的工具需要集成,并且需要编写粘合代码。平台团队通常会评估编写粘合代码、为他们的用例重写更定制化的解决方案或扩展现有工具之间的权衡。我强烈建议您熟悉 CNCF 生态系统中的工具(landscape.cncf.io),以避免走向每个工具都是为您的组织量身定制的方向,这意味着您最终需要长期内部维护所有这些工具。
抽象复杂性是构建平台的关键部分。与您的团队明确说明平台能为他们做什么的清晰合同对于成功的平台工程举措至关重要。这些合同作为 API 公开,团队可以通过编程方式、使用仪表板或通过自动化与之交互。
图 1.11 展示了典型的 Kubernetes 采用之旅,旨在实现平台工程。旅程从采用 Kubernetes 作为运行工作负载的目标平台开始,然后研究和选择工具,通常来自 CNCF 生态系统。当初始工具被选中时,您的平台开始成形,并且需要一些投资来配置这些工具,并使它们为您的团队工作。最后,所有这些配置和选定的工具都可以隐藏在更友好的平台 API 后面,使用户能够专注于他们的工作流程,而不是试图了解构成平台的工具和粘合代码的每个细节。

图 1.11 Kubernetes 平台之旅
在这段旅程中,我们可以将平台定义为如何编码提供给我们开发团队所需的所有工作流程所需的知识。操作知识以及关于实现这些工作流程所使用的工具的决定被封装在作为平台 API 的合同中。这些 API 可以使用 Kubernetes API 提供声明式方法,但这不是必需的。一些平台隐藏了平台使用 Kubernetes 的信息,这也可以减少与平台交互的团队的认知负荷。
尽管我已经尝试从非常高的层面来解释什么是平台,但我更倾向于将所有正式的定义委托给负责定义和更新术语的云原生空间的工作组。我强烈建议您查看 CNCF 关于平台的白皮书中的 App Delivery TAG - 平台工作组(tag-app-delivery.cncf.io/whitepapers/platforms/),该工作组承担了尝试定义平台是什么的工作。
他们目前的定义,在撰写本书时,如下所示:“云原生计算平台是一组集成能力,这些能力是根据平台用户的需要定义和展示的。它是一个横切层,确保为广泛的应用程序和用例获取和集成典型能力和服务的持续体验。一个好的平台为使用和管理其能力和服务提供一致的用户体验,例如网络门户、项目模板和自助服务 API。”
在这本书中,我们将通过查看可用的云原生工具来开始构建示例平台的旅程,看看它们如何提供不同的平台能力。但我们从哪里找到这些工具?这些工具是否可以协同工作?我们如何在不同选择之间进行选择?让我们快速看一下 CNCF 景观。
1.2.2 CNCF 景观拼图
跟踪云服务提供商的服务是一项全职工作,每个云服务提供商都会举办年度会议和较小规模的活动来宣布新事物和亮点。在 Kubernetes 和云原生空间中,您也可以期待同样的情况。CNCF 景观持续扩展和演变。正如您在图 1.12 中可以看到的,景观非常庞大,一开始看起来很难阅读。

图 1.12 CNCF 景观(来源:https://landscape.cncf.io)
与云服务提供商提供的服务相比,一个显著的不同点是 CNCF 中每个项目都必须遵循的公开和社区驱动的成熟度模型,以获得毕业状态。每个项目的成熟度之旅独立于任何云服务提供商,作为个人或组织,您都可以影响项目的发展方向或其达到该方向的速度。
虽然云服务提供商已经定义了云的形状,但现在大多数都参与了 CNCF 项目,推动这些开放倡议的成功。他们正在开发可以在云服务提供商之间使用的工具,消除障碍,并在开放环境中而不是在每一个云服务提供商的门后进行创新。图 1.13 显示了 Kubernetes 如何使云原生创新生态系统在云服务提供商之外繁荣发展。云服务提供商没有停止提供新的、更专业的服务,但在过去五年中,我们看到了云服务提供商和软件供应商之间合作改进的转向,以开发新的工具和开放创新。

图 1.13 Kubernetes 使多云云原生生态系统得以实现
大多数由 CNCF 托管的项目的一个共同特点是它们都使用 Kubernetes,扩展其功能并解决更接近开发团队的挑战。CNCF 已经达到一个阶段,越来越多的工具被创建出来以简化开发工具和工作流程。有趣的是,这些工具并不仅仅关注开发者。它们还使运维团队和系统集成商能够将项目粘合在一起,并定义原生于 Kubernetes 的新开发者体验。开发团队无需担心日常工作流程所需的工具和集成。CNCF 生态系统中参与社区的成熟度提高以及推动简化开发团队与所有这些工具交互的方式,催生了关于平台工程的讨论。下一节将探讨这些讨论,为什么你不能购买一个平台,以及我们将在本书的其余部分如何探索这个庞大的生态系统。
1.3 平台工程
与云服务提供商拥有内部团队定义将提供哪些新服务、这些服务将如何扩展以及需要向客户公开哪些工具和 API 一样,很明显,组织可以从拥有自己的内部平台工程团队中受益。这些团队通过决定最佳解决软件交付问题和加快流程的工具选择,帮助使开发团队能够启用。
一个常见的趋势是拥有一个专门的平台工程团队来定义这些 API 并做出平台级决策。平台团队与开发团队、运维团队和云服务提供商专家合作,实施满足工作流程应用团队需求的技术。除了拥有专门的平台工程团队外,书籍《团队拓扑学》([teamtopologies.com/](https://teamtopologies.com/))所倡导的关键文化变革是将平台本身视为一个内部产品,并将你的开发团队视为客户。这并不新鲜,但它促使平台团队在利用平台工具的同时,关注这些内部开发团队的满意度。
图 1.14 展示了应用开发团队(App Dev Teams)如何专注于使用他们偏好的工具开发新功能,同时平台团队创建黄金路径(到生产),这些团队产生的所有工作都用于验证功能并将这些更改交付给我们的组织客户/最终用户。

图 1.14 平台团队安全地将开发者的工作带到生产中。
平台团队与开发团队之间的关系创造了协同效应,专注于提高整个组织的软件交付实践。通过创建黄金路径,平台不仅关注日常开发任务,还旨在自动化开发团队所做的更改如何到达我们组织的最终客户/消费者。
通过在整个过程中增加可见性,你可以帮助整个组织理解和看到团队如何产生新功能,以及这些功能何时将可供我们的最终用户使用。这可以是非常有价值的商业决策、市场营销以及一般规划。
1.3.1 为什么我不能只是购买一个平台?
不幸的是,你无法购买现成的平台来解决你组织的所有需求。正如我们讨论的,你可以购买一个或多个云服务提供商的服务,但你的内部团队需要弄清楚哪些服务以及如何组合它们来解决特定问题。确定哪些工具和服务符合你组织的需要和合规要求,以及如何将这些决策封装在团队可以通过自助方式使用的接口后面,通常是一件你无法购买的事情。
有一些工具是针对这种情况设计的,旨在通过实施一系列开箱即用的流程或提供一套他们支持的非常具有观点的工具来减少平台团队的工作量。这类工具中,同时大量使用和扩展 Kubernetes 的包括 Red Hat OpenShift (www.redhat.com/en/technologies/cloud-computing/openshift) 和 VMware Tanzu (tanzu.vmware.com/tanzu)。这些工具对首席技术官(CTO)和架构师来说非常吸引人,因为它们涵盖了他们需要解决方案的大部分主题,如 CI/CD、运维、开发者工具和框架。根据我的经验,虽然这些工具有助于许多场景,但平台团队在选择工具时需要灵活性,以适应他们现有的实践。最终,如果你购买了这些工具,你的团队也需要花时间学习它们,这就是为什么像 Red Hat OpenShift 和 VMware Tanzu 这样的工具会附带咨询服务,这也是需要考虑在内的额外成本。对于中等和大型组织,采用和适应这些具有观点的现成工具可能需要改变已经为团队所熟知的明确的工作流程和实践。对于较小且不够成熟的组织,这些工具可以通过减少团队在启动新项目时面临的选择数量来节省大量时间,但这些工具和服务的成本可能对于一个年轻组织来说太高。
图 1.15 展示了平台团队选择不同工具时旅程的变化。这些 Kubernetes 发行版(OpenShift、Tanzu 等)可以限制平台团队可以做出的选择数量,但它们也可以节省时间,并附带培训和服务等支持,你的团队可以依赖这些服务。

图 1.15 在 Kubernetes 发行版上构建平台
无论你是否已经是这些工具的客户,你仍然需要负责在这些工具之上构建平台。如果你有 Red Hat OpenShift 或 VMware Tanzu 可供你的团队使用,我强烈建议你熟悉他们支持的工具及其设计选择和决策。与你的工具保持一致并咨询其架构师可能会帮助你找到在工具之上构建层的捷径。
注意事项: 注意,这些工具可以被视为 Kubernetes 发行版。与 Linux 发行版相同的发行版意味着我预计会有越来越多的发行版出现,解决不同的挑战和用例。例如,用于物联网和边缘情况的 K0s 和 MicroK8s 等工具。虽然你可以采用这些发行版中的任何一个,但请确保它们与你的组织目标一致。
因为我想让这本书尽可能实用,所以我们将查看一个简单的应用程序,我们将在下一节中使用它来继续我们的旅程。我们不会为通用用例构建一个通用的平台。我们将构建一个示例平台,展示上一节中涵盖的概念。拥有一个你可以运行、实验和更改的具体示例应该有助于你将讨论的主题映射到你的日常挑战中。下一节中介绍的应用程序突出了你在大多数商业领域中创建、构建和维护分布式应用程序时将面临的挑战。因此,我们将构建的示例平台应该映射到你业务领域中的挑战。
1.4 需要一个行走骨架
在 Kubernetes 生态系统中,通常需要集成至少 10 个或更多的项目或框架来交付一个简单的 PoC(概念验证)。这项工作可能包括将项目构建到可以在 Kubernetes 内部运行的容器中,以及将流量路由到每个服务提供的 REST 端点。如果你想尝试新项目以查看它们是否适合你的生态系统,可以构建一个 PoC 来验证你对这个新项目/框架的工作原理以及它将如何为你和你的团队节省时间的理解。
对于这本书,我创建了一个简单的“行走骨架”。这个云原生应用不仅仅是一个简单的 PoC(Proof of Concept),它还允许你探索如何应用不同的架构模式。它还让你测试如何将不同的工具和框架集成,而无需为实验改变你的项目。我更喜欢使用“行走骨架”这个术语,而不是“概念验证”或“演示应用”,因为“行走骨架”这个术语更接近于本节中引入的应用程序的意图。
这个行走骨架的主要目的是突出从架构角度解决非常具体的挑战,你的应用程序将需要的需求,以及交付实践的角度。你应该能够将样本云原生应用程序中解决这些挑战的方式映射到你的特定领域。挑战可能不会总是相同,但我希望突出每个建议解决方案背后的原则以及引导你决策的方法。
使用这个行走骨架,你还可以确定你需要的最小可行产品,并快速将其部署到生产环境中,在那里你可以对其进行改进。通过将行走骨架带入生产环境,你可以获得关于你需要为其他服务以及从基础设施角度所需的有价值见解。它还可以帮助你的团队了解与这些项目合作需要什么,以及事情可能出错的地方和方式。
构建步行骨架所使用的技术栈并不重要。更重要的是理解各个部分是如何组合在一起的,以及哪些工具和实践可以使得每个服务(或一组服务)背后的团队能够安全且高效地演进。
1.4.1 构建会议应用
在本书的整个过程中,你将使用一个会议应用。这个会议应用可以部署在不同的环境中,以服务于不同的活动。此应用依赖于容器、Kubernetes 以及将在任何主要云提供商和本地 Kubernetes 安装上工作的工具。
图 1.16 展示了应用主页面看起来是什么样子。

图 1.16 会议应用主页
会议应用允许用户管理会议活动,并提供了一个基本着陆页、一个议程页,其中将列出所有批准的演讲,以及一个提案征集表单,潜在演讲者可以在此提交他们的演讲提案。应用还允许会议组织者执行管理任务,例如审查提交的提案以及批准或拒绝它们(见图 1.17)。

图 1.17 会议应用后台页面
此应用由一组具有不同职责的服务组成。图 1.18 展示了你所控制的应用的主要组件——换句话说,就是你和你团队将更改和交付的服务。

图 1.18 会议应用服务。最终用户与前端交互,前端将请求路由到所有后端服务。
团队创建了这些服务来实现一个具有展示业务价值的功能的基本步行骨架。以下是每个服务的简要描述:
-
前端: 该服务是用户访问应用的主要入口点。因此,该服务托管了一个 NextJS 应用(HTML、JavaScript 和 CSS 文件),客户端浏览器将下载这些文件。客户端应用与后端服务交互,该服务接受来自浏览器的请求并将每个请求路由到一个或多个后端服务。
-
议程服务: 该服务负责列出所有获得会议批准的演讲。在会议期间,该服务需要高度可用,因为与会者将在一天中多次访问此服务以在会议之间移动。
-
提案征集 (C4P): 该服务包含处理会议组织期间提案征集用例(简称 C4P)的逻辑。此功能允许潜在演讲者提交演讲提案,会议组织者将对其进行审查并决定哪些提案包含在会议议程中。
-
通知服务: 该服务使会议组织者能够向与会者和演讲者发送通知。
图 1.19 显示了团队选择的提案征集流程,用于构建行走骨架并验证他们对会议应用程序工作方式的假设。通过端到端实现此用例,团队可以验证其选择的技术堆栈和架构假设。

图 1.19 提案征集用例
在实现提案征集用例的基本功能之后,团队可以决定接下来实现哪个用例。会议组织者是否需要管理赞助商?演讲者是否需要一个专门的个人资料页面?添加新功能或服务应该是直接的,因为基础构建块已经就位。
当查看这些用例的实现方式时,你还需要考虑当新的用例将被实施或需要引入变更时,如何跨团队进行协调。为了提高协作效率,你需要有可见性,并且需要了解应用程序是如何工作的。
你还需要考虑这个云原生应用程序的运营方面。你可以想象,在一段时间内,应用程序将打开提案征集请求,供潜在演讲者提交提案,然后接近会议日期时,打开参会者注册页面等。
在整本书中,我将鼓励你通过添加新服务和实现新用例进行实验。在第二章中,当你将应用程序部署到 Kubernetes 集群时,你将检查这些服务的配置方式,不同服务之间的数据流,以及如何扩展服务。
通过使用一个虚构的应用程序进行实验,你可以自由地更改每个服务的内部结构,使用不同的工具,比较结果,甚至可以并行尝试每个服务的不同版本。每个服务都提供了部署这些服务到你的环境所需的所有资源。在第三章和第四章中,我们将更深入地探讨每个服务,了解如何构建和部署每个服务,以便团队可以更改当前的行为并创建和部署新的版本。
在部署这个云原生会议应用程序之前,重要的是要提到与将这些功能捆绑在一个单体应用程序中的主要区别。
但如果会议应用程序是使用单体方法创建的呢?让我们简要讨论一下主要会有哪些区别。
1.4.2 单体与分布式服务集之间的差异
理解单一单体应用程序与完全分布式服务集之间的区别对于理解为什么增加的复杂性是值得努力的关键。如果你仍在使用你想要拆分以采用分布式方法的单体应用程序,本节将突出你将遇到的主要区别。
图 1.20 显示了实现之前讨论过的相同用例的单体应用,但在这种情况下,不同团队在开发不同功能时将共享相同的代码库。在开发单体应用时,没有明确要求内部服务之间有强接口。将不同功能的逻辑分离到封装良好的模块中是可选的。接口的缺乏和功能重叠迫使对应用程序进行更改的团队拥有复杂的协调策略,以确保功能不会冲突,并且更改可以合并到代码库中。

图 1.20 在单体应用中,实现不同用例的所有逻辑都捆绑在一起。这促使不同团队在相同的代码库上工作,并需要他们拥有复杂的协调实践来避免冲突的更改。
从功能上讲,它们是相同的,你可以做同样数量的用例,但单体应用已经显示出一些你可能已经在单体应用中遇到的缺点。以下要点突出了我们将在这本书中使用的云原生应用的优点以及与平行宇宙中的替代单体实现相关的某些缺点:
-
现在服务可以独立演进,团队被赋予了加快速度的权力,并且在代码库级别上没有瓶颈: 在单体应用中,不同团队共同工作有一个单一源代码仓库,项目有一个单一的持续集成管道,这很慢,团队使用特性分支,这会导致复杂合并时出现问题。
-
现在应用程序可以根据不同场景进行不同的扩展: 从可扩展性的角度来看,每个服务可以根据其经历的负载级别进行扩展。在单体应用中,如果只需要扩展单个功能,运维团队只能创建整个应用程序的新实例。对如何扩展不同功能进行精细控制可以成为您用例的一个显著差异化因素,但您必须做好尽职调查。
-
云原生版本要复杂得多,因为它是一个分布式系统: 它更好地利用了云基础设施的灵活性和特性,允许运维团队使用工具来管理这种复杂性以及日常运营。在构建单体应用时,创建内部机制来操作大型应用要普遍得多。在云原生环境中,云提供商和开源项目提供了大量工具,可用于操作和监控云原生应用。
-
欢迎多语言: 每个服务都可以使用不同的编程语言或不同的框架来构建。在单体应用中,应用开发者被束缚在旧版本的库中,因为更改或升级库通常涉及大量的重构,整个应用程序都需要经过测试以确保应用程序不会崩溃。在云原生方法中,服务没有被强制使用单一的技术栈。这允许团队在选择工具时更加自主,在某些情况下可以加快交付时间。
-
单体应用的全有或全无: 如果单体应用程序崩溃,整个应用程序都会崩溃,用户将无法访问任何内容。在云原生版本中,即使服务中断,用户仍然可以访问应用程序。示例中的“行走骨架”展示了如何通过采用流行的工具来支持降级服务。通过使用 Kubernetes,它被设计用来监控您的服务并在服务出现异常时采取行动,平台将尝试自我修复您的应用程序。
-
每次会议活动都需要单体应用的不同版本: 在处理不同事件时,每次会议都需要一个与其它事件略有不同的单体应用版本。这导致了代码库的分歧和整个项目的重复。大多数为会议所做的更改在活动结束后都丢失了。在云原生方法中,我们通过拥有可以互换的细粒度服务来促进可重用性,避免了整个应用程序的重复。
虽然单体应用程序在操作和开发方面比云原生应用程序简单得多,但本书的其余部分将专注于理解和减少构建分布式应用程序的复杂性。我们将通过采用正确的工具和实践来实现这一点,这将使你的团队能够更加独立和高效,同时促进应用程序的弹性和健壮性。
如果你目前正在使用单体应用程序,我希望这本书能帮助你比较不同的方法,并介绍构建分布式应用程序所需的工具和实践。
1.4.3 我们的“行走骨架”和构建平台
现在我们有一个简单的应用程序,我们的客户将会使用,我们可以专注于理解我们的团队需要所有工具来持续改进这些服务。本书中我们将涵盖的平台是那些为特定领域目的而构建的组织,而不是通用的。通过为特定场景创建我们的“行走骨架”,我们可以模拟一个优化工具和工作流程的平台,以改善这些团队软件交付的方式。我们的“行走骨架”不是一个简单的“Hello World”应用程序,因此它允许进行更多实验,编写更复杂的功能,并使用工具使应用程序更加健壮。
现在,我们将开始一段云原生之旅。首先,我们将探讨分布式应用如何在 Kubernetes 上运行,Kubernetes 提供了什么,以及其挑战。紧接着,我们将开始探讨扩展基本 Kubernetes 功能的工具,以帮助我们构建、部署和运行我们的云原生应用。
在第六章中,在评估构建和交付分布式应用的一些挑战之后,我们将构建我们的平台原型,这将帮助团队在安全的环境中创建新功能,与现有应用一起工作,而不会与其他团队冲突。一旦我们有了平台原型,我们将构建和提供更高级别的平台功能,以使我们的团队能够更高效地工作,并减少他们理解 Kubernetes 以及本书中将要讨论的所有工具的复杂性。
最后,为了结束这本书,我们将探讨如何衡量我们所构建的平台的好坏。就像任何软件一样,我们需要对其进行衡量,以确保我们引入的新工具或更改使事情变得更好,而不是更糟。
这段旅程将迫使我们做出艰难的决定和选择,这些决定和选择将对我们平台工程实践至关重要。以下列表概述了这段旅程中的主要里程碑,而不涉及每个章节中涵盖的具体工具的细节。
-
第二章:云原生应用挑战:在 Kubernetes 集群中使会议应用运行起来后,我们将分析你在 Kubernetes 上开发和运行云原生应用时将面临的主要和最常见的挑战。在本章中,你将从运行时角度检查应用,并尝试以不同的方式破坏它,以了解事情出错时它的行为。
-
第三章:服务管道:构建云原生应用:一旦应用运行起来,你和你的团队将更改应用的服务以添加新功能或修复错误。本章涵盖了构建这些应用服务所需的内容,包括使用服务管道创建部署这些新版本到生产环境所需的工件发布的最新更改。
-
第四章:环境管道:部署云原生应用:如果我们解决了如何打包和发布服务的新版本,那么我们需要有一个明确的策略来将这些新版本推广到不同的环境,以便在面临真实客户之前进行测试和验证。本章涵盖了环境管道的概念,以及在云原生社区中流行的 GitOps 趋势,用于在不同环境中配置和部署应用。
-
第五章:多云(应用)基础设施: 您的应用程序不能孤立运行。应用程序服务需要应用程序基础设施组件,如数据库、消息代理、身份服务等,才能工作。本章重点介绍如何使用多云和 Kubernetes 原生方法来配置我们应用程序服务所需的组件。
-
第六章:在 Kubernetes 上构建平台: 一旦我们了解了应用程序的运行方式、构建和部署方式以及它与云基础设施的连接方式,我们将把注意力集中在从对应用程序进行更改的团队中抽象出我们使用所有工具引入的复杂性。我们不希望我们的开发团队在设置云提供商账户、配置构建管道将运行的服务器或担心他们的环境在哪里运行时分心。欢迎加入平台工程团队!
-
第七章:平台功能 I:共享应用程序关注点: 我们如何减少应用程序和运维团队之间的摩擦和依赖?我们如何进一步解耦我们应用程序的逻辑与这些应用程序需要运行的组件?本章介绍了一系列平台功能,使应用程序开发者能够专注于编写代码。平台团队可以专注于决定如何连接应用程序所需的所有组件,然后向开发者提供简单且标准化的 API。
-
第八章:平台功能 II:使团队能够进行实验: 现在我们有一个平台,可以为我们团队提供工作环境,那么平台还能为应用程序开发团队做些什么呢?如果您允许您的团队同时运行多个版本的应用程序服务,新功能或修复可以逐步推出。有空间进行实验可以让组织更快地发现问题,并减少每次发布相关的压力。本章介绍如何为您的云原生应用程序实施不同的发布策略。
-
第九章:衡量您的平台: 平台的价值在于它为组织带来的改进。我们需要衡量平台性能,以了解其表现如何,因为我们应采用持续改进的方法来确保我们使用的工具正在帮助我们的团队更快、更有效地交付。本章重点介绍使用 DORA 指标来了解组织交付软件的情况以及平台变更如何提高我们的交付管道吞吐量。
现在您已经知道了即将发生的事情,让我们部署我们的云原生会议应用程序。
摘要
-
(云)平台为团队提供了一套服务,以构建他们特定的应用程序。
-
平台通常提供三个主要功能:API、仪表板和 SDK,供不同团队使用,以适应他们的工作流程。
-
云平台提供按需付费的模式来消费硬件和软件。你越往上层走,服务费用就越高。
-
Kubernetes 提供了基本的构建块,以便以独立于底层云提供商的方式构建平台,甚至可以在本地部署我们的平台。
-
云原生计算基金会促进和培养云原生空间中开源项目之间的合作。跟踪这些社区中的动态是一项全职工作。
-
在 Kubernetes 上进行的平台工程(特别是针对本书)有助于管理选择哪些工具和实践以使团队更高效地交付将在 Kubernetes 上运行的软件的复杂性。
2 云原生应用挑战
本章涵盖
-
与在 Kubernetes 集群中运行的云原生应用一起工作
-
在本地和远程 Kubernetes 集群之间进行选择
-
理解主要组件和 Kubernetes 资源
-
理解与云原生应用一起工作的挑战
当我想尝试新事物时,无论是框架、新工具还是新应用,我往往缺乏耐心;我想立即看到它运行。然后,当它运行时,我想深入了解并理解它是如何工作的。我破坏事物以进行实验并验证我是否理解这些工具、框架或应用的内部工作原理。这正是我们在本章中将要采取的方法!
要使云原生应用启动并运行,您需要一个 Kubernetes 集群。在本章中,您将使用名为 KinD(Docker 中的 Kubernetes,kind.sigs.k8s.io/) 的项目与本地 Kubernetes 集群一起工作。这个本地集群将允许您在本地部署应用以进行开发和实验。要安装一组微服务,您将使用 Helm,这是一个帮助打包、部署和分发 Kubernetes 应用的项目。您将安装第一章中引入的行走骨架服务,该服务实现了一个会议应用。
一旦会议应用的服务启动并运行,您将使用 kubectl 检查其 Kubernetes 资源,以了解应用是如何架构的以及其内部工作原理。一旦您对应用内部的主要组件有了概述,您将跳到尝试破坏应用,寻找您的云原生应用可能面临的常见挑战和陷阱。本章涵盖了在基于 Kubernetes 的现代技术堆栈中运行云原生应用的基础,突出了开发、部署和维护分布式应用所带来的利弊。接下来的章节将通过研究主要关注加快和使项目交付更高效的项目来应对这些相关挑战。
2.1 运行我们的云原生应用
要理解云原生应用的固有挑战,我们需要能够对我们可以控制、配置和为了教育目的而破坏的简单示例进行实验。在云原生应用的背景下,“简单”不能是一个单一的服务,因此对于简单应用,我们需要处理分布式应用的复杂性,如网络延迟、某些应用服务的容错性以及最终的不一致性。要运行云原生应用,在这种情况下,第一章中引入的“行走骨架”,您需要一个 Kubernetes 集群。这个集群将安装在哪里,谁将负责设置它,是开发者首先会有的问题。开发者希望在本地运行事物,在他们的笔记本电脑或工作站上,而 Kubernetes 使得这一点成为可能——但这是否是最优的?让我们分析在本地集群与其他选项相比运行的优势和劣势。
2.1.1 为您选择最佳的 Kubernetes 环境
本节不涵盖所有可用的 Kubernetes 风味的完整列表,但它关注于 Kubernetes 集群可以如何配置和管理的一些常见模式。有三种可能的替代方案——它们都有优点和缺点:
-
笔记本电脑/台式电脑上的本地 Kubernetes: 我倾向于不鼓励人们在他们的笔记本电脑上运行 Kubernetes。正如您将在本书的其余部分看到的那样,在类似生产环境的环境中运行您的软件被高度推荐,以避免可以总结为“但在我的笔记本电脑上它运行正常”的问题。这些问题大多是由于当您在笔记本电脑上运行 Kubernetes 时,您并不是在真实的机器集群上运行。因此,没有网络往返和真正的负载均衡。
-
优点: 轻量级,快速启动,适合测试、实验和本地开发。适合运行小型应用程序。
-
缺点: 这不是一个真正的集群,其行为不同,且硬件资源减少,无法运行工作负载。您无法在笔记本电脑上运行大型应用程序。
-
-
数据中心内的本地 Kubernetes: 这是对拥有私有云的公司来说的一个典型选择。这种方法要求公司拥有一个专门的团队和硬件来创建、维护和运行这些集群。如果您的公司足够成熟,可能已经有一个自助服务平台,允许用户按需请求新的 Kubernetes 集群。
-
优点: 在真实硬件之上的真实集群将更接近于生产集群的工作方式。您将清楚地了解在您的环境中,哪些功能可供应用程序使用。
-
缺点: 需要一个成熟的运维团队来设置集群并向用户提供凭证,并且需要为开发者提供专用硬件来进行实验。
-
-
云提供商提供的托管服务 Kubernetes: 我倾向于支持这种方法,因为使用云提供商的服务允许你按使用付费,像 Google Kubernetes Engine (GKE)、Azure AKS 和 AWS EKS 这样的服务都是基于自助服务理念构建的,使开发者能够快速启动新的 Kubernetes 集群。有两个主要考虑因素:
-
你需要选择一个云提供商,并拥有一个信用卡账户来支付你的团队将消耗的费用。这可能涉及到在预算中设置一些上限并定义谁有权访问。如果不小心,选择云提供商可能会使你陷入供应商锁定的情况。
-
所有的东西都是远程的,对于习惯本地工作的开发者和其他团队来说,这是一个很大的变化。开发者需要时间来适应,因为工具和大部分的工作负载都将远程运行。这也是一个优势,因为你的开发者和他们部署的应用程序将表现得好像它们正在生产环境中运行。
-
优点: 你正在与真实的(完全成熟的)集群一起工作。你可以定义你的任务需要多少资源,完成工作后,你可以删除它们以释放资源。你不需要预先投资硬件。
-
缺点: 你可能需要一个大的信用卡,并且你需要你的开发人员与远程集群和服务进行工作。
-
最后的建议是检查以下存储库,其中包含主要云提供商的免费 Kubernetes 信用额度:github.com/learnk8s/free-kubernetes。我创建了此存储库以保持这些免费试用版本的最新列表,你可以使用这些试用版在真实基础设施上运行本书中的所有示例。图 2.1 总结了前面项目符号中包含的信息。

图 2.1 Kubernetes 集群本地与远程设置。
虽然这三个选项都是有效的,但都有缺点,在接下来的几节中,你将使用 Kubernetes KinD(Docker 中的 Kubernetes,[kind.sigs.k8s.io/](https://kind.sigs.k8s.io/))在你的笔记本电脑/电脑上运行的本地 Kubernetes 环境中部署第一章中介绍的行走骨架。查看位于 https://github.com/salaboy/platforms-on-k8s/tree/main/chapter-2#creating-a-local-cluster-with-kubernetes-kind 的分步教程,以创建我们将用于部署行走骨架、会议应用程序的本地 KinD 集群。
注意,教程创建了一个本地 KinD 集群,模拟拥有三个节点和特殊的端口映射,以允许我们的 Ingress 控制器路由我们发送到 http://localhost 的传入流量。
2.1.2 安装行走骨架
要在 Kubernetes 上运行容器化应用程序,您需要将每个服务打包成容器镜像,并且您需要定义这些容器将如何配置以在您的 Kubernetes 集群中运行。为此,Kubernetes 允许您定义不同类型的资源(使用 YAML 格式)来配置您的容器将如何运行和相互通信。最常见的资源类型是:
-
部署: 声明性地定义您的容器需要多少个副本才能使应用程序正确运行。部署还允许我们选择我们想要运行的容器(或容器集),以及这些容器必须如何配置(使用环境变量)。
-
服务: 声明性地定义一个高级抽象,将流量路由到由您的部署创建的容器。它还充当部署内部副本之间的负载均衡器。服务使集群内的其他服务和应用程序能够使用服务名称而不是容器的物理 IP 地址进行通信,提供所谓的服务发现。
-
入口: 声明性地定义一个路由,用于将集群外部的流量路由到集群内部的服务。使用入口定义,我们可以暴露集群外运行的客户端应用程序所需的服务。
-
ConfigMap/secret: 声明性地定义和存储配置对象以设置我们的服务实例。Secret 被认为是敏感信息,应该受到保护访问。
如果您有包含数十或数百个服务的大型应用程序,这些 YAML 文件将变得复杂且难以管理。跟踪更改并使用kubectl应用这些文件来部署应用程序成为一项复杂的工作。本书的范围不包括对这些资源的详细描述,其他资源如官方 Kubernetes 文档页面(kubernetes.io/docs/concepts/workloads/)也是可用的。在本书中,我们将专注于如何处理大型应用程序的资源以及可以帮助我们完成这项任务的工具。以下部分提供了将这些工具打包和安装到您的 Kubernetes 集群中的概述。
打包和安装 Kubernetes 应用程序
有不同的工具用于打包和管理您的 Kubernetes 应用程序。大多数时候,我们可以将这些工具分为两大类:模板引擎和包管理器。在现实场景中,为了完成任务,您可能需要这两种类型的工具。让我们讨论这两种类型的工具:为什么你需要一个模板引擎?你想要管理哪种类型的包?
模板引擎允许你在不同的环境中重用相同的资源定义,在这些环境中,应用程序可能需要略微不同的参数。资源模板化的典型例子是数据库 URL。如果你的服务需要连接到不同环境中的不同数据库实例,例如测试环境中的测试数据库和生产环境中的生产数据库,你希望避免维护两个具有不同 URL 的相同 YAML 文件副本。图 2.2 展示了你现在如何向 YAML 文件添加变量,然后引擎会根据你想要使用最终(渲染)资源的位置找到并替换这些变量。

图 2.2 模板引擎通过替换变量来渲染 YAML 资源。
使用模板引擎可以节省你维护相同文件不同副本的大量时间,因为当文件开始堆积时,维护它们就变成了一项全职工作。社区中有几个工具可以处理 Kubernetes 文件的模板化。有些工具仅处理 YAML 文件,而有些工具则更专注于 Kubernetes 资源。以下是一些你应该检查的项目:
-
Kustomize:
kustomize.io/ -
Carvel YTT:
carvel.dev/ytt/ -
Helm Templates:
helm.sh/docs/chart_best_practices/templates/#helm
现在,你该如何处理所有这些文件呢?将这些文件组织成逻辑包是一种很自然的冲动。如果你正在构建一个由不同服务组成的应用程序,将所有与某个服务相关的资源放在同一个目录内,甚至是在包含该服务源代码的同一个仓库中,这可能是有意义的。你还想确保能够将这些文件分发给部署这些服务的不同环境中的团队,并且你很快就会意识到你需要以某种方式对这些文件进行版本控制。这种版本控制可能与你的服务本身的版本有关,或者与对应用程序有意义的较高层次的逻辑聚合有关。当我们谈论分组、版本控制和分发这些资源时,我们正在描述包管理器的职责。开发者和运维团队已经习惯了使用包管理器,无论他们使用的技术栈是什么。Java 的 Maven/Gradle,NodeJS 的 NPM,Linux/Debian/Ubuntu 软件包的 APT-GET,以及最近,用于云原生应用的容器和容器注册库。那么,YAML 文件的包管理器是什么样的呢?包管理器的主要职责是什么?
作为用户,包管理器允许你浏览可用的包及其元数据,以便决定你想安装哪个包。一旦你决定使用哪个包,你应该能够下载它然后安装它。一旦包安装完成,作为用户,你期望能够升级到新版本的包。升级/更新包需要手动干预,这意味着作为用户,你需要明确告诉包管理器将某个包的安装升级到新版本(或最新版本)。
从包提供者的角度来看,包管理器应该提供创建包的约定和结构,以及打包你想要分发的文件的工具。包管理器处理版本和依赖关系,这意味着如果你创建了一个包,你必须为其关联一个版本号。一些包管理器使用semver(语义版本控制)方法,它使用三个数字来描述包的成熟度(例如 1.0.1,其中这些数字代表主版本、次版本和修补版本)。包管理器不需要提供集中的包仓库,但它们通常这么做。这个包仓库负责为用户提供包托管服务。集中式仓库很有用,因为它们为开发者提供了成千上万的可用包。这些集中式仓库的例子包括 Maven Central、NPM、Docker Hub、GitHub Container Registry 等。这些仓库负责索引包的元数据(这可能包括版本、标签、依赖关系和简短描述),以便用户可以搜索。这些仓库还处理访问控制,以拥有公共和私有包,但最终,包仓库的主要责任是允许包生产者上传包,以及包消费者从它们那里下载包(见图 2.3)。

图 2.3 包管理器的责任:构建、打包和分发
当我们谈论 Kubernetes 时,Helm 是一个非常流行的工具,它既提供包管理器,又提供模板引擎。但还有其他值得关注的工具,例如:
-
Imgpkg (
carvel.dev/imgpkg/),它使用容器注册库来存储包。 -
Kapp (
carvel.dev/kapp/),它提供了更高层次的抽象,可以将资源作为应用程序分组。 -
类似于 Terraform 和 Pulumi 这样的工具,允许你以代码的方式管理基础设施。
在下一节中,我们将探讨如何使用 Helm (helm.sh)将会议应用程序安装到我们的 Kubernetes 集群中。
2.2 使用单个命令安装会议应用程序
让我们将第一章第 1.4 节中介绍的 Conference 应用程序使用 Helm 安装到我们的 Kubernetes 集群中。此 Conference 应用程序允许会议组织者接收潜在演讲者的提案,评估这些提案,并保持已批准提交的更新议程。我们将在这本书中使用此应用程序来举例说明你在构建现实应用程序时可能遇到的挑战。
注意:要获取完整步骤列表,请遵循位于 github.com/salaboy/platforms-on-k8s/tree/main/chapter-2 的逐步教程。它包括运行本节中描述的命令所需的所有先决条件,例如创建集群和安装示例工作所需的命令行工具。
此应用程序被构建为一个“行走骨架”,这意味着它不是一个完整的应用程序,但它包含了“提案征集”流程所需的所有组件。这些服务可以进一步迭代以支持其他流程和现实场景。在以下章节中,你将安装应用程序到集群中,并与它交互以查看它在 Kubernetes 上运行时的行为。让我们使用以下命令安装应用程序:
helm install conference oci://docker.io/salaboy/conference-app --version v1.0.0
你应该看到与列表 2.1 相似的输出。
列表 2.1 Helm 安装了 conference-app 版本 1.0.0
> helm install conference oci://docker.io/salaboy/conference-app --version
➥v1.0.0
Pulled: registry-1.docker.io/salaboy/conference-app:v1.0.0
Digest: sha256:e5dd1a87a867fd7d6c6caecef3914234a12f23581c5137edf63bfd9add7d5459
NAME: conference
LAST DEPLOYED: Mon Jun 26 08:19:15 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Cloud-Native Conference Application v1.0.0
Chart Deployed: conference-app - v1.0.0
Release Name: conference
For more information visit: https://github.com/salaboy/platforms-on-k8s
Access the Conference Application Frontend by running
➥'kubectl port-forward svc/frontend -n default 8080:80'
注意:自 Helm 3.7+ 开始,你可以将 Helm 图表打包并作为 OCI 容器镜像分发,Helm 图表的 URL 包含 oci://,因为此图表托管在 Docker Hub 上,应用程序容器存储在那里。在 Helm 支持 OCI 镜像之前,你需要手动从 Helm 图表仓库添加和检索包,这些图表使用 tar 文件进行分发。
helm install 创建一个 Helm 发布,这意味着你已经创建了一个应用程序实例,在这种情况下,实例被称为 conference。使用 Helm,如果你想要的话,可以部署多个应用程序实例。你可以通过运行以下命令列出 Helm 发布:
helm list
输出应该看起来像图 2.4。

图 2.4 列出 Helm 发布版本
注意:如果你不使用 helm install,而是运行 helm template oci://docker.io/salaboy/conference-app --version v1.0.0,Helm 将输出 YAML 文件,这些文件将应用于集群。在某些情况下,你可能想这样做而不是 helm install,例如,如果你想覆盖 Helm 图表不允许参数化的值或在对 Kubernetes 发送请求之前应用任何其他转换。
2.2.1 验证应用程序是否正在运行
一旦应用程序部署,容器将被下载到您的笔记本电脑上运行,这可能需要一段时间。根据您的互联网连接,这个过程可能需要长达 10 分钟,因为 Kafka、PostgreSQL 和 Redis 将与应用程序容器一起下载。RESTARTS 列显示了容器由于错误而重新启动的频率。在分布式应用程序中,这是正常的,因为组件可能相互依赖,当它们同时启动时,连接可能会失败。按照设计,应用程序应该能够从问题中恢复,并且 Kubernetes 将自动重启失败的容器。
您可以通过列出您集群中运行的所有 Pod 来监控进度,再次使用-owide标志来获取更多信息:
kubectl get pods -owide
输出应类似于图 2.5。

列表 2.5 列出应用程序 Pod
您可能在 Pod 列表中注意到,我们不仅运行了应用程序的服务,还运行了 Redis、PostgreSQL 和 Kafka,因为 C4P(Call for Proposals)和议程服务需要持久存储。应用程序将使用 Kafka 在服务之间交换异步消息。除了服务之外,我们还将运行这两个数据库和一个消息代理(Kafka)在我们的 Kubernetes 集群内部。
在图 2.5 所示的输出中,您需要注意 READY 和 STATUS 列,其中 READY 列中的 1/1 表示一个容器副本正在运行,另一个预期正在运行。如您所见,RESTART 列显示 Call for Proposals Service (conference-c4p-service)为 7。这是因为该服务依赖于 Redis 处于运行状态,以便服务能够连接到它。当 Redis 启动时,应用程序将尝试连接,如果失败,它将尝试重新连接。一旦 Redis 启动,服务将连接到它。同样适用于 Kafka 和 PostgreSQL。为了快速回顾,我们运行的应用程序服务、数据库和消息代理如图 2.6 所示。

图 2.6 应用程序服务、数据库和消息代理
注意 Pod 可以在不同的节点上调度。您可以在NODE列中检查这一点;这是 Kubernetes 高效使用集群资源。如果所有 Pod 都处于运行状态,您就成功了!现在应用程序正在运行,您可以通过将您喜欢的浏览器指向http://localhost来访问它。
如果您对 Helm 和构建自己的 Conference 应用程序 Helm Chart 感兴趣,我建议您查看教程中提供的源代码:github.com/salaboy/platforms-on-k8s/tree/main/conference-application/helm/conference-app。
2.2.2 与您的应用程序交互
在上一节中,我们将应用程序安装到我们的本地 Kubernetes 集群中。在本节中,我们将快速与该应用程序交互,以了解服务如何交互以完成一个简单的用例:接收和批准提案。请记住,您可以通过将浏览器指向http://localhost来访问应用程序。会议应用程序应类似于图 2.7。

图 2.7 会议着陆页
如果您现在切换到议程部分,您应该会看到类似于图 2.8 的内容。

图 2.8 首次安装应用程序时的会议空议程
应用程序的议程页面列出了会议安排的所有演讲。潜在的演讲者可以提交会议组织者将审查的提案。当您第一次启动应用程序时,议程上不会有任何演讲,但现在您可以从提案征集部分提交提案。查看图 2.9。

图 2.9 向组织者提交提案以供审查
注意,在提交提案的表单中有四个字段(标题、描述、作者和电子邮件)。填写所有字段,然后通过点击表单底部的提交提案按钮提交。组织者将使用这些信息来评估您的提案,并在您的提案被批准或拒绝时通过电子邮件与您联系。一旦提交提案,您就可以转到后台办公室(点击顶部菜单中的指向右方的箭头)并检查审查提案标签页,在那里您可以批准或拒绝提交的提案。您将在这个屏幕上扮演会议组织者的角色;见图 2.10。

图 2.10 会议组织者可以接受或拒绝传入的提案
已批准的提案将出现在主议程页面上。在此阶段访问该页面的与会者可以浏览会议的主要演讲者。图 2.11 显示了我们在主要会议页面议程部分的新批准提案。

图 2.11 您的提案现在已在议程上实时发布!
在此阶段,潜在的演讲者应该已经收到了关于其提案批准或拒绝的电子邮件。您可以通过查看通知服务日志来检查这一点,使用终端中的kubectl;查看图 2.12 中的命令输出:
kubectl logs -f conference-notifications-service -deployment-<POD_ID>

图 2.12 通知服务日志(电子邮件和事件)
这些日志显示了应用程序的两个重要方面。首先,通知通过电子邮件发送给潜在的演讲者。组织者需要跟踪这些通信。在会议后台办公室页面上,您可以找到通知标签页,其中显示了通知的内容(见图 2.13)。

图 2.13 在后台办公室显示的通知
这里显示的第二方面是事件。当执行相关操作时,此应用程序的所有服务都会发出事件。通知服务正在发出事件,在这种情况下是向 Kafka 发送通知。这允许其他服务和应用程序异步地与应用程序服务集成。图 2.14 显示了后台办公室的事件部分。

图 2.14 后台办公室中所有服务事件
图 2.14 显示了应用程序服务发出的所有事件;请注意,您可以看到服务执行的所有有意义操作,以完成提案征集流程(新提案 > 新议程项 > 提案批准 > 发送通知)。
如果您已经做到这一步,恭喜您,会议应用程序按预期工作。我鼓励您提交另一个提案并拒绝它,以验证是否向潜在演讲者发送了正确的通知和事件。
在本节中,您使用 Helm 安装了会议应用程序。然后我们验证了应用程序正在运行,并且潜在演讲者可以提交提案,同时会议组织者可以批准或拒绝这些提案。这些决定将通过电子邮件通知潜在演讲者。
这个简单的应用程序使我们能够演示一个基本用例,我们现在可以扩展和改进以支持真实用户。我们已经看到安装应用程序的新实例相当简单。我们使用 Helm 安装了一组相互连接的服务以及一些基础设施组件,如 Redis、PostgreSQL,在下一节中,我们将更深入地了解我们安装了什么以及应用程序是如何工作的。
2.3 检查行走骨架
如果您已经使用 Kubernetes 一段时间了,您可能已经对 kubectl 非常熟悉。因为这个应用程序版本使用的是原生 Kubernetes 部署和服务,您可以使用 kubectl 检查和调试这些 Kubernetes 资源。
通常,除了查看正在运行的 pod(使用 kubectl get pods)之外,为了理解和操作应用程序,您将查看服务和部署。让我们首先探索部署资源。
2.3.1 Kubernetes 部署基础
让我们从部署开始。在 Kubernetes 中,部署负责包含运行我们容器的配方。部署还负责定义容器将如何运行,以及当需要时如何升级到新版本。通过查看部署详情,您可以获得非常有用的信息,例如:
-
这个部署所使用的 容器。请注意,这只是一个简单的 Docker 容器,这意味着如果您想的话,甚至可以在本地使用
docker run运行这个容器。这对于故障排除是基本的。 -
部署所需的 副本 数量。在本例中,它被设置为 1,但在下一节中你将更改此设置。更多的副本可以增加应用程序的弹性,因为这些副本可能会失败。Kubernetes 将会启动新的实例以保持所需副本数始终不变。
-
容器的 资源分配。根据负载和构建服务所使用的负载和技术堆栈,你可能需要微调 Kubernetes 允许容器使用的资源数量。
-
就绪 和 存活 探针的状态。默认情况下,Kubernetes 会监控你的容器健康状况。它是通过执行两个探针来完成的:1) “就绪探针”检查容器是否准备好响应请求,2) “存活探针”检查容器的主进程是否正在运行。
-
滚动更新策略定义了我们的 Pods 将如何更新以避免对用户造成停机。使用
RollingUpdateStrategy,你可以定义在触发和更新到新版本时允许多少副本。
首先,让我们列出所有可用的部署:
kubectl get deployments
输出应类似于列表 2.2。
列表 2.2 列出应用程序的部署
NAME READY UP-TO-DATE AVAILABLE
conference-agenda-service-deployment 1/1 1 1
conference-c4p-service-deployment 1/1 1 1
conference-frontend-deployment 1/1 1 1
conference-notifications-service-deployment 1/1 1 1
2.3.2 探索部署
在以下示例中,你将描述前端部署。你可以使用 kubectl describe deploy conference-frontend-deployment(参见列表 2.3)来更详细地描述每个部署。
列表 2.3 描述部署以查看其详细信息
> kubectl describe deploy conference-frontend-deployment
Name: conference-frontend-deployment
Namespace: default
CreationTimestamp: Tue, 27 Jun 2023 08:21:21 +0100
Labels: app.kubernetes.io/managed-by=Helm
Annotations: deployment.kubernetes.io/revision: 1
meta.helm.sh/release-name: conference
meta.helm.sh/release-namespace: default
Selector: app=frontend
Replicas: 1 desired | 1 updated | 1 total | 1 available ①
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=frontend
Containers:
frontend:
Image: salaboy/frontend-go... ②
Port: 8080/TCP
Host Port: 0/TCP
...
...
Environment: ③
AGENDA_SERVICE_URL: agenda-service.default.svc.cluster.local
C4P_SERVICE_URL: c4p-service.default.svc.cluster.local
NOTIFICATIONS_SERVICE_URL: notifications-service.default.svc.cluster.local
KAFKA_URL: conference-kafka.default.svc.cluster.local
POD_NODENAME: (v1:spec.nodeName)
POD_NAME: (v1:metadata.name)
POD_NAMESPACE: (v1:metadata.namespace)
POD_IP: (v1:status.podIP)
POD_SERVICE_ACCOUNT: (v1:spec.serviceAccountName)
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: conference-frontend-deployment-<ID> (1/1 replicas created)
Events: ④
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 48m deployment-controller Scaled up replica ➥set conference-frontend-deployment-59d988899 to 1
① 显示此部署可用的副本。这为你提供了关于部署状态的快速指示。
② 容器镜像,包括用于此服务的名称和标签。
③ 用于配置此容器的环境变量。
④ 事件显示有关我们的 Kubernetes 资源的相关信息——在本例中,当副本被创建时。
列表 2.3 显示,如果由于某种原因部署未按预期工作,以这种方式描述部署非常有帮助。例如,如果所需的副本数未满足,描述资源将帮助你了解问题可能出在哪里。始终检查底部与资源相关的事件,以获取更多关于资源状态的见解。在这种情况下,部署在 48 分钟前扩展到有一个副本。
如前所述,部署还负责协调版本或配置升级和回滚。默认情况下,部署更新策略设置为“滚动”,这意味着部署将逐步升级一个接一个的 pods 以最小化停机时间。可以设置一个名为 Recreate 的替代策略,该策略将关闭所有 pods 并创建新的。
与 pod 相比,部署不是短暂的;因此,如果您创建了一个 Deployment,无论底层的容器是否失败,它都会在那里供您查询。默认情况下,当您创建部署资源时,Kubernetes 会创建一个中间资源来处理和检查部署请求的副本。
2.3.3 ReplicaSets
拥有多个容器副本是扩展应用程序的重要功能。如果您的应用程序正在经历来自用户的巨大流量,您可以轻松增加服务的副本数量以适应所有传入的请求。同样,如果您的应用程序没有经历大量的请求,这些副本可以缩小以节省资源。Kubernetes 创建的对象称为 ReplicaSet,可以通过运行以下命令进行查询:
kubectl get replicaset
输出应该看起来像列表 2.4。
列表 2.4 列出部署的 ReplicaSets
> kubectl get replicasets
NAME DESIRED CURRENT READY
conference-agenda-service-deployment-7cc9f58875 1 1 1
conference-c4p-service-deployment-76dfc94444 1 1 1
conference-frontend-deployment-59d988899 1 1 1
conference-notifications-service-deployment-7cbcb8677b 1 1 1
这些 ReplicaSet 对象完全由部署的资源管理,通常,您不需要处理它们。ReplicaSets 在处理滚动更新时也非常重要,您可以在 kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/ 找到更多关于这个主题的信息。您将在后面的章节中使用 Helm 对应用程序进行更新,届时这些机制将启动。
如果您想更改部署的副本数量,您仍然可以使用 kubectl 来实现:
> kubectl scale --replicas=2 deployments/<DEPLOYMENT_ID>
您可以用前端部署来尝试这个操作:
> kubectl scale --replicas=2 deployments/conference-frontend-deployment
如果我们现在列出应用程序的 pod,我们会看到前端服务有两个副本:
conference-frontend-deployment-<ID>-8gpgn 1/1 Running 7 (53m ago) 59m
conference-frontend-deployment-<ID>-z4c5c 1/1 Running 0 13s
此命令更改 Kubernetes 中的部署资源并触发为前端部署创建第二个副本。增加面向用户服务的副本数量相当常见,因为所有用户在访问会议页面时都会访问该服务。
如果我们现在访问应用程序,作为最终用户,我们不会注意到任何区别,但每次刷新时,可能由不同的副本为我们提供服务。为了使这一点更加明显,我们可以打开内置在前端服务中的一个功能,该功能显示有关应用程序容器的更多信息。您可以通过设置环境变量来启用此功能:
kubectl set env deployment/conference-frontend-deployment ➥FEATURE_DEBUG_ENABLED=true
注意,当你更改部署对象配置(spec.template.spec 块内的任何内容)时,Deployment 资源的滚动更新机制将会启动。该部署管理的所有现有 Pod 都将升级到新的规范(在本例中包括新的 FEATURE_DEBUG_ENABLED 环境变量)。默认情况下,这将启动一个新的 Pod 并使用新的规范,在终止旧版本的 Pod 之前等待它就绪。这个过程将重复进行,直到所有 Pod(部署的副本)都使用新的配置。
如果你再次在浏览器中访问应用程序(如果浏览器缓存了网站,你可能需要使用隐身模式访问),在后台办公室部分,有一个新的调试标签页。你可以看到所有服务的 Pod 名称、Pod IP、命名空间以及 Pod 运行的节点名称(如图 2.15)。

图 2.15 前端第一个副本响应你的请求(运行在节点名称:dev-worker)
如果你等待 3 秒钟,页面将自动刷新,你应该看到这次是第二个副本在响应,如果不是,请等待下一个周期(如图 2.16)。

图 2.16 前端第二个副本响应你的请求(运行在节点名称:dev-worker3)
默认情况下,Kubernetes 将在副本之间进行请求的负载均衡。只需通过更改副本的数量即可进行扩展,无需部署任何新内容,Kubernetes 将分配一个新的 Pod(其中包含新的容器)来处理更多的流量。Kubernetes 还将确保始终存在所需数量的副本。你可以通过删除一个 Pod 并观察 Kubernetes 如何自动重新创建它来测试这一点。对于这种场景,你需要小心,因为网络应用程序的前端正在执行多个请求以获取 HTML、CSS 和 JavaScript 库;因此,每个请求都可能落在不同的副本上。
2.3.4 连接服务
我们已经探讨了部署,它们负责将我们的容器启动并运行,并保持这种状态,但到目前为止,这些容器只能在 Kubernetes 集群内部访问。如果我们想让其他服务与这些容器交互,我们需要查看另一个名为 Service 的 Kubernetes 资源。Kubernetes 提供了一种高级的服务发现机制,允许服务通过仅知道它们的名称来相互通信。这对于连接许多服务而不需要知道 Kubernetes Pod 的 IP 地址至关重要,因为随着时间的推移,这些 IP 地址可能会发生变化,例如,它们可以被升级、重新调度到不同的节点,或者在出现问题时使用新的 IP 地址重新启动。
2.3.5 探索服务
要将您的容器暴露给其他服务,您需要使用一个 Kubernetes Service 资源。每个应用程序服务都定义了这个 Service 资源,因此其他服务和客户端可以连接到它们。在 Kubernetes 中,服务将负责将流量路由到您的应用程序容器。这些服务代表了一个逻辑名称,您可以使用它来抽象容器运行的位置。如果您有多个容器的副本,服务资源将负责在所有副本之间进行流量负载均衡。您可以通过运行以下命令来列出所有服务:
kubectl get services
运行命令后,您应该看到类似于列表 2.5 的内容。
列表 2.5 列出应用程序的服务
NAME TYPE CLUSTER-IP PORT(S)
agenda-service ClusterIP 10.96.90.100 80/TCP
c4p-service ClusterIP 10.96.179.86 80/TCP
conference-kafka ClusterIP 10.96.67.2 9092/TCP
conference-kafka-headless ClusterIP None 9092/TCP,9094/TCP,9093/TCP
conference-postgresql ClusterIP 10.96.121.167 5432/TCP
conference-postgresql-hl ClusterIP None 5432/TCP
conference-redis-headless ClusterIP None 6379/TCP
conference-redis-master ClusterIP 10.96.225.138 6379/TCP
frontend ClusterIP 10.96.60.237 80/TCP
kubernetes ClusterIP 10.96.0.1 443/TCP
notifications-service ClusterIP 10.96.65.248 80/TCP
您还可以使用以下命令来描述一个服务,以获取更多关于它的信息:
kubectl describe service frontend
这应该会给出类似于列表 2.6 中的内容。服务和部署通过选择器属性链接,如下图中突出显示。换句话说,服务将路由流量到包含标签 app=frontend 的部署创建的所有 pod。
列表 2.6 描述前端服务
Name: frontend
Namespace: default
Labels: app.kubernetes.io/managed-by=Helm
Annotations: meta.helm.sh/release-name: conference
meta.helm.sh/release-namespace: default
Selector: app=frontend ①
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.60.237
IPs: 10.96.60.237
Port: <unset> 80/TCP
TargetPort: 8080/TCP
Endpoints: 10.244.1.6:8080,10.244.2.9:8080
Session Affinity: None
Events: <none>
① 用于匹配服务和部署的选择器
2.3.6 Kubernetes 中的服务发现
通过使用服务,如果您的应用程序服务需要向任何其他服务发送请求,它可以使用 Kubernetes 服务的名称和端口;在大多数情况下,如果您使用 HTTP 请求,您可以使用端口 80,因此您只需要使用服务名称。如果您查看服务的源代码,您将看到 HTTP 请求是对服务名称的创建;不需要 IP 地址或端口。
最后,如果您想将服务暴露在 Kubernetes 集群之外,您需要一个 Ingress 资源。正如其名称所表示的,这个 Kubernetes 资源负责将集群外部的流量路由到集群内部的服务。通常,您不会暴露多个服务,这限制了您应用程序的入口点。
您可以通过运行以下命令来获取所有可用的 Ingress 资源:
kubectl get ingress
输出应该看起来像列表 2.7。
列表 2.7 列出应用程序的 Ingress 资源
NAME CLASS HOSTS ADDRESS PORTS AGE
conference-frontend-ingress nginx * localhost 80 84m
然后,您可以像描述其他资源类型一样描述 Ingress 资源,以获取更多关于它的信息:
kubectl describe ingress conference-frontend-ingress
您应该期望输出看起来像列表 2.8。
列表 2.8 描述 Ingress 资源
Name: conference-frontend-ingress
Labels: app.kubernetes.io/managed-by=Helm
Namespace: default
Address: localhost
Ingress Class: nginx
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
*
/ frontend:80 (10.244.1.6:8080,10.244.2.9:8080) ①
Annotations: meta.helm.sh/release-name: conference
meta.helm.sh/release-namespace: default
nginx.ingress.kubernetes.io/rewrite-target: /
Events: <none>
① 所有前往 '/' 的流量都将路由到前端:80 服务。
如您所见,Ingress 也使用服务的名称来路由流量。为了使其工作,您需要一个 Ingress 控制器,就像我们在创建 KinD 集群时安装的那样。如果您在云服务提供商上运行,可能需要安装一个 Ingress 控制器。
以下电子表格是一个社区资源,用于跟踪您可用的不同 Ingress 控制器的选项:mng.bz/K9Bn。
使用 Ingress,您可以配置单个入口点,并使用基于路径的路由将流量重定向到您需要公开的每个服务。列表 2.8 中的上一个 Ingress 资源将发送到 / 的所有流量路由到 frontend 服务。请注意,Ingress 规则相当简单,您不应在此级别添加任何业务逻辑路由。
2.3.7 故障排除内部服务
有时,访问内部服务以调试或排除不工作的服务的问题很重要。对于此类情况,您可以使用 kubectl port-forward 命令临时访问使用 Ingress 资源未公开于集群外部的服务。例如,要访问 Agenda 服务而不通过前端,可以使用以下命令:
kubectl port-forward svc/agenda-service 8080:80
您应该看到以下输出(列表 2.9)并确保不要终止该命令。
列表 2.9 kubectl port-forward 允许您为调试目的公开服务
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
然后使用您的浏览器,在另一个标签页中使用 curl 或其他任何工具指向 http://localhost:8080/service/info 以访问公开的 Agenda 服务。以下列表显示了如何使用 curl 访问 Agenda 服务信息端点,并使用 jq(您必须单独安装)打印一个漂亮的/彩色的 JSON 有效负载。
列表 2.10 使用 port-forward 访问 Agenda 服务
> curl -s localhost:8080/service/info | jq --color-output
{
"Name": "AGENDA",
"Version": "1.0.0",
"Source": "https://github.com/salaboy/platforms-on-k8s/tree/main/
conference-application/agenda-service",
"PodName": "conference-agenda-service-deployment-7cc9f58875-28wrt",
"PodNamespace": "default",
"PodNodeName": "dev-worker3",
"PodIp": "10.244.2.2",
"PodServiceAccount": "default"
}
在本节中,您已经检查了为在 Kubernetes 内运行您的应用程序容器而创建的主要 Kubernetes 资源。通过查看这些资源及其关系,您可以在出现问题时进行故障排除。
对于日常操作,kubectl 命令行工具可能不是最佳选择,可以使用不同的仪表板来探索和管理您的 Kubernetes 工作负载,例如 k9s (k9scli.io/)、Kubernetes 仪表板 (kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/) 和 Skooner (github.com/skooner-k8s/skooner)。
2.4 云原生应用程序挑战
与如果出现问题整个应用程序会崩溃的单体应用程序相比,云原生应用程序在服务崩溃时不应崩溃。云原生应用程序是为故障而设计的,应在出现错误的情况下继续提供有价值的功能。在修复问题期间提供降级服务比无法访问应用程序要好。在本节中,您将更改 Kubernetes 中的一些服务配置,以了解应用程序在不同情况下的行为。
在某些情况下,应用程序/服务开发人员需要确保他们构建的服务具有弹性,Kubernetes 或基础设施将解决一些问题。
本节涵盖了与云原生应用程序相关的一些最常见挑战。我发现提前知道将要出错的事情比我在构建和交付应用程序时更有用。这不是一个详尽的列表;这只是确保您不会遇到众所周知的问题的开始。以下各节将通过会议应用程序举例说明并突出这些挑战:
-
不允许停机:如果您在 Kubernetes 上构建和运行云原生应用程序,并且您仍在遭受应用程序停机,那么您没有充分利用您所使用的技术堆栈的优势。
-
服务的内置弹性:下游服务会崩溃,您需要确保您的服务为这种情况做好准备。Kubernetes 帮助进行动态服务发现,但这对于您的应用程序具有弹性来说还不够。
-
处理应用程序状态并非易事:我们必须了解每个服务的架构需求,以便让 Kubernetes 能够高效地扩展和缩减我们的服务。
-
数据不一致:与分布式应用程序一起工作的一个常见问题是数据不是存储在单一位置,而是倾向于分布。应用程序需要准备好处理不同服务对世界状态有不同的看法的情况。
-
理解应用程序的工作方式(监控、跟踪和遥测):在事情出错时,了解应用程序的表现以及它是否正在执行其应有的操作对于快速发现问题至关重要。
-
应用程序安全和身份管理:处理用户和安全问题总是被放在次要位置。对于分布式应用程序,在早期就明确记录和实现这些方面将帮助您通过定义“谁可以在何时做什么”来细化应用程序需求。
让我们从第一个挑战开始:不允许停机。
2.4.1 不允许停机
当使用 Kubernetes 时,我们可以轻松地扩展和缩减服务的副本。当您的服务是基于平台将通过创建运行服务的容器的新副本来扩展它们的假设而设计时,这是一个很棒的功能。那么,当服务没有准备好处理复制或没有给定服务的副本可用时会发生什么呢?
让我们将前端服务扩展到有两个副本运行。为了实现这一点,您可以运行以下命令:
kubectl scale --replicas=2 deployments/conference-frontend-deployment
如果其中一个副本停止运行或因任何原因损坏,Kubernetes 将尝试启动另一个副本,以确保始终有两个副本处于运行状态。图 2.17 显示了两个前端副本正在为用户提供流量。

图 2.17 通过运行两个 Frontend 容器的副本,我们允许应用容忍故障,并增加应用可以处理的并发请求数量。
您可以通过终止应用 Frontend 的两个 Pod 中的任意一个来快速尝试 Kubernetes 的这项自愈功能。您可以通过运行以下命令来完成此操作,如列表 2.11 和 2.12 所示。
列表 2.11 检查两个副本是否正在运行
> kubectl get pods
NAME READY STATUS RESTARTS AGE
conference-agenda-service-deployment-<ID> 1/1 Running 7 (92m ago) 100m
conference-c4p-service-deployment-<ID> 1/1 Running 7 (92m ago) 100m
conference-frontend-deployment-<ID> 1/1 Running 0 25m
conference-frontend-deployment-<ID> 1/1 Running 0 25m
conference-kafka-0 1/1 Running 0 100m
conference-notifications-service-deployment-<ID> 1/1 Running 7 (91m ago) 100m
conference-postgresql-0 1/1 Running 0 100m
conference-redis-master-0 1/1 Running 0 100m
现在,复制两个 Pod ID 之一并删除它:
> kubectl delete pod conference-frontend-deployment-c46dbbb9-ltrgs
然后再次列出 Pods(列表 2.12)。
列表 2.12 一旦一个副本失败,就会自动创建一个新的副本
> kubectl get pods
NAME READY STATUS RESTARTS AGE
conference-agenda-service-deployment-<ID> 1/1 Running 7 (92m ago) 100m
conference-c4p-service-deployment-<ID> 1/1 Running 7 (92m ago) 100m
conference-frontend-deployment-<NEW ID> 0/1 ContainerCreating 0 1s
conference-frontend-deployment-<ID> 1/1 Running 0 25m
conference-kafka-0 1/1 Running 0 100m
conference-notifications-service-deployment-<ID> 1/1 Running 7 (91m ago) 100m
conference-postgresql-0 1/1 Running 0 100m
conference-redis-master-0 1/1 Running 0 100m
您可以看到,当 Kubernetes(更具体地说,是 ReplicaSet)检测到只有一个正在运行的副本时,它会立即创建一个新的 Pod。在新的 Pod 被创建并启动的过程中,您只有一个副本在响应您的请求,直到第二个副本启动并运行。这种机制确保至少有两个副本来响应用户的请求。图 2.18 显示,由于我们仍然有一个 Pod 在处理请求,因此应用仍然可以工作。

图 2.18 如果其中一个实例失败,Kubernetes 将自动终止并重新创建该实例。但至少其他正在运行的容器可以继续响应用户请求。
如果您只有一个副本并终止了正在运行的 Pod,您的应用将在新容器创建并准备好服务请求之前出现停机时间。您可以使用以下命令恢复到单个副本:
> kubectl scale --replicas=1 deployments/conference-frontend-deployment
继续尝试。仅删除 Frontend Pod 可用的副本:
> kubectl delete pod <POD_ID>
图 2.19 显示应用不再工作,因为没有 Frontend Pod 来服务来自用户的请求。

图 2.19 当单个副本重启时,没有备份来响应用户请求。如果没有副本可供服务用户的请求,您将经历停机时间。这正是我们想要避免的。
在终止 Pod 后,通过刷新浏览器(http://localhost)尝试访问应用。您应该在浏览器中看到“503 服务暂时不可用”,因为 Ingress 控制器(为了简化,在之前的图中未显示)找不到位于 Frontend 服务后面的正在运行的副本。如果您稍等片刻,您将看到应用重新启动。图 2.20 显示了负责将流量路由到 Frontend 服务的 NGINX Ingress 控制器组件返回的 503“服务暂时不可用”。

图 2.20 当单个副本重启时,没有备份来响应用户请求
这个错误信息相当棘手,因为应用大约需要一秒钟的时间才能重启并完全恢复正常功能,所以如果您没有看到它,您可以尝试使用kubectl scale --replicas=0 deployments/conference-frontend-deployment将前端服务缩放到零个副本来模拟停机。
这种行为是可以预期的,因为前端服务是一个面向用户的服务。如果它关闭,用户将无法访问任何功能,因此建议有多个副本。从这个角度来看,前端服务是整个应用程序最重要的服务,因为我们的主要目标是避免应用程序的停机时间。
总结来说,请特别注意集群外部暴露的面向用户的服务。无论是用户界面还是 API,确保您有足够的副本来处理传入的请求。除了开发之外,大多数用例应避免使用单个副本。
2.4.2 服务内置的弹性
但是现在,如果其他服务也关闭了怎么办?例如,Agenda 服务仅负责向会议参与者列出所有接受的提案。这项服务同样关键,因为议程列表就在应用程序的主页上。所以,让我们缩小服务规模:
kubectl scale --replicas=0 deployments/conference-agenda-service-deployment
图 2.21 显示了即使其中一个服务表现不佳,应用程序仍然可以继续工作。

图 2.21 没有 Agenda 服务的 Pod。如果一个服务失败,用户应该能够继续使用应用程序,但功能有限。
运行此命令后,容器将被终止,并且没有容器会响应用户的请求。尝试在浏览器中刷新应用程序,你应该会看到一个如图 2.22 所示的缓存响应。

图 2.22 如果 Agenda 服务没有正在运行的副本,前端足够智能,会向用户显示一些缓存条目。
如您所见,应用程序仍在运行,但 Agenda 服务目前不可用。检查后台办公室部分的调试选项卡,它应该显示 Agenda 服务不健康(图 2.23)。

图 2.23 如果处于调试模式,后台办公室应显示不健康的服务。
您可以为这样的场景准备您的应用程序;在这种情况下,前端有一个缓存响应,至少可以向用户显示一些内容。如果由于某种原因,Agenda 服务关闭,至少用户将能够访问其他服务和应用程序的其他部分。从应用程序的角度来看,重要的是不要将错误传播回用户。用户应该能够继续使用其他应用程序服务,例如,提案征集表单,直到 Agenda 服务恢复。
在开发将在 Kubernetes 中运行的服务时,你需要特别注意,因为现在你的服务负责处理由下游服务生成的错误。这很重要,以确保错误或服务崩溃不会使你的整个应用程序崩溃。简单的机制,如缓存响应,会使你的应用程序更加健壮,并允许你逐步升级这些服务,而不用担心将一切关闭。对于我们的会议场景,有一个定期缓存议程条目的 CronJob 可能就足够了。记住,不允许停机时间。
让我们谈谈如何处理我们应用程序中的状态,以及从可扩展性的角度来看理解我们的应用程序服务如何处理状态的重要性。由于我们将讨论可扩展性,数据一致性是我们接下来要尝试解决的问题。
2.4.3 处理应用程序状态并不简单
让我们再次扩展 Agenda 服务,使其只有一个副本:
> kubectl scale --replicas=1 deployments/conference-agenda-service-deployment
如果你之前创建过提案,你会注意到,一旦 Agenda 服务恢复,你会在 Agenda 页面上再次看到已接受的提案。这仅因为 Agenda 服务和 C4P 服务都将所有提案和议程项存储在外部数据库中(PostgreSQL 和 Redis)。在这个上下文中,外部意味着在 pod 内存之外。如果我们将 Agenda 服务扩展到两个副本,会发生什么?请参见列表 2.13。
列表 2.13 运行 Agenda 服务的两个副本
> kubectl scale --replicas=2 deployments/conference-agenda-service-deployment
NAME READY STATUS AGE
conference-agenda-service-deployment-<ID> 1/1 Running 2m30s
conference-agenda-service-deployment-<ID> 1/1 Running 22s
conference-c4p-service-deployment-<ID> 1/1 Running 150m
conference-frontend-deployment-<ID> 1/1 Running 8m55s
conference-kafka-0 1/1 Running 150m
conference-notifications-service-deployment-<ID> 1/1 Running 150m
conference-postgresql-0 1/1 Running 150m
conference-redis-master-0 1/1 Running 150m
图 2.24 展示了 Agenda 服务同时运行两个服务副本的情况。

图 2.24 现在两个副本可以处理更多的流量。由前端转发的请求可以被两个可用的副本回答,这使得应用程序能够处理更多的负载。
当两个副本处理你的用户请求时,现在前端将有两个实例进行查询。Kubernetes 将在两个副本之间进行负载均衡,但你的应用程序将无法控制请求击中的副本。因为我们使用数据库在 pod 的上下文之外备份数据,所以我们可以将副本扩展到许多处理应用程序需求的 pod。图 2.25 显示了 Agenda 服务如何依赖 Redis 来存储应用程序状态,而 Call for Proposals 则使用 PostgreSQL 来完成同样的工作。

图 2.25 两个数据敏感的服务都使用持久存储。将状态存储委托给外部组件,使你的服务无状态且更容易扩展。
这种方法的一个局限性是数据库在其默认配置中支持的数据库连接数。如果你继续扩展副本,始终需要考虑审查数据库连接池设置,以确保数据库可以处理所有副本创建的所有连接。但为了学习,让我们假设我们没有数据库,我们的议程服务将所有议程项都保存在内存中。如果我们开始扩展议程服务 Pod,应用程序将如何表现?图 2.26 展示了应用程序内部有内存数据的假设情况。

图 2.26 如果议程服务将状态保存在内存中会发生什么?如果状态保存在内存中,就很难在副本之间共享。这使得扩展服务变得更加困难。
通过扩大这些服务规模,我们发现应用程序服务设计中存在一个问题。议程服务正在内存中保持状态,这将影响 Kubernetes 的扩展能力。对于这种场景,当 Kubernetes 在不同的副本之间平衡请求时,前端服务将根据哪个副本处理了请求而接收不同的数据。
当在 Kubernetes 中运行现有应用程序时,你需要深入了解它们在内存中保持多少数据,因为这将影响你如何扩展它们。对于保持 HTTP 会话并需要粘性会话(后续请求都发送到同一副本)的 Web 应用程序,你需要设置 HTTP 会话复制以使多个副本工作。这可能需要在基础设施级别配置更多组件,例如缓存。
了解你的服务需求将帮助你规划和自动化你的基础设施需求,例如数据库、缓存、消息代理等。应用程序越复杂,它对这些基础设施组件的依赖性就越大。
如我们之前所见,我们已经将 Redis 和 PostgreSQL 作为应用程序 Helm 图的一部分安装。这通常不是一个好主意,因为数据库和像消息代理这样的工具需要运营团队的特别关注,他们可以选择不在 Kubernetes 内运行这些服务。我们将在第四章中进一步探讨这个主题,我们将更深入地探讨在 Kubernetes 和云提供商合作时如何处理基础设施。
2.4.4 处理不一致的数据
在关系型数据库存储如 PostgreSQL 或 NoSQL 方法如 Redis 中存储数据并不能解决不同存储之间数据不一致的问题。因为这些存储应该通过服务 API 隐藏起来,你需要有机制来检查服务处理的数据是否一致。在分布式系统中,经常谈论“最终一致性”,意味着最终系统将会是一致的。拥有最终一致性比完全没有一致性要好。在这个例子中,我们可以构建一个简单的检查机制,偶尔(想象一下每天一次)检查议程服务中已接受的演讲是否在提案征集服务中得到批准。如果有一个提案服务(C4P)尚未批准的条目,那么我们可以发出一些警报或给会议组织者发送电子邮件(图 2.27)。

图 2.27 一致性检查可以作为 CronJobs 运行。我们可以定期执行针对应用程序服务的检查,以确保状态的一致性。例如:(1)每天午夜我们查询议程服务(2)以验证发布的会议是否在(3)提案征集服务中得到批准,并且(4)通知服务已经发送了相应的通知。
在图 2.27 中,我们可以看到 CronJob(1)将每隔 X 个周期执行一次,这取决于我们修复一致性问题的紧迫性。然后它将查询议程服务的公共 API(2)以检查哪些已接受的提案被列出,并将其与提案征集服务批准的列表(3)进行比较。最后,如果发现任何不一致性,可以使用通知服务的公共 API(4)发送电子邮件。
考虑这个应用程序被设计用于的简单用例;你还需要进行哪些检查?一个立即想到的是验证已拒绝和批准的提案是否正确发送了电子邮件。对于这个用例,电子邮件非常重要,我们需要确保这些电子邮件被发送到我们接受的和拒绝的演讲者。
2.4.5 理解应用程序的工作方式
分布式系统是复杂的生物,从第一天开始就完全理解它们是如何工作的,这有助于你在事情出错时节省时间。这促使监控、跟踪和遥测社区努力开发出帮助我们理解在任何给定时间事物是如何运作的解决方案。
opentelemetry.io/ OpenTelemetry 社区与 Kubernetes 一起发展,现在它可以提供您监控服务运行情况所需的大部分工具。正如他们的网站所述,“您可以使用它来对软件进行仪器化、生成、收集和导出遥测数据(指标、日志和跟踪)以进行分析,以了解软件的性能和行为。” 图 2.28 展示了一个常见用例,其中所有服务都将指标、跟踪和日志推送到一个集中位置,该位置存储和聚合信息,以便在仪表板中显示或由其他工具使用。

图 2.28 将所有服务的可观察性聚合到单一位置可以减少负责保持应用程序正常运行团队的认知负荷。
重要的是要注意,OpenTelemetry 专注于您软件的行为和性能,因为它们都会影响您的用户和用户体验。从行为角度来看,您想确保应用程序正在执行它应该执行的操作,为此,您需要了解哪些服务正在调用哪些其他服务或基础设施来执行任务。
使用 Prometheus 和 Grafana 可以让我们查看服务遥测数据并构建特定领域的仪表板来突出某些应用级指标,例如,随着时间的推移,批准与拒绝提案的数量对比,如图 2.29 所示。

图 2.29 使用 Prometheus 和 Grafana 监控遥测数据
从性能角度来看,您需要确保服务遵守其服务级别协议(SLA),这意味着它们以很短的时间回答请求。如果您的某个服务表现不佳,耗时超过正常水平,您希望知道。
对于跟踪,您必须修改您的服务以了解其内部操作和性能。OpenTelemetry 在大多数语言中提供即插即用的仪器库,以外部化服务指标和跟踪。图 2.30 展示了 OpenTelemetry 架构,您可以看到 OpenTelemetry 收集器从每个应用程序代理接收信息,同时也从共享基础设施组件接收信息。

图 2.30 OpenTelemetry 架构和库(来源:https://opentelemetry.io/docs/)
这里的建议是,如果您正在创建一个“行走骨架”,请确保它内置了 OpenTelemetry。如果您将监控推迟到项目的后期阶段,那就太晚了,事情会出错,找出责任人会花费太多时间。
2.4.6 应用程序安全和身份管理
如果你曾经构建过 Web 应用程序,你就会知道提供身份管理(用户账户和用户身份)以及认证和授权是一项相当艰巨的任务。破坏任何应用程序(无论是云原生还是非云原生)的一个简单方法就是执行你不应该执行的操作,比如删除所有提议的演示文稿,除非你是会议组织者。
这在分布式系统中也变得具有挑战性,因为授权和用户身份必须在不同的服务之间传播。在分布式架构中,有一个代表用户生成请求的组件而不是直接暴露所有服务供用户交互是很常见的。在我们的例子中,前端服务就是这样的组件。大多数时候,你可以使用这个面向外部的组件作为外部和内部服务之间的屏障。因此,将前端服务配置为连接到使用 OAuth2 协议的授权和认证提供者是相当常见的。图 2.31 显示了前端服务与身份管理服务的交互,该服务负责连接到身份提供者(Google、GitHub、你内部的 LDAP 服务器)以验证用户凭据,并提供定义用户在不同服务中可以做什么和不能做什么的角色或组成员资格。前端服务处理登录流程(认证和授权),但一旦完成,只有上下文才会传播到后端服务。

图 2.31 身份管理:角色/组被传播到后端服务。
在身份管理方面,你已经看到应用程序不处理用户或他们的数据,这对于 GDPR 等法规来说是个好事。我们可能希望允许用户使用他们的社交媒体账户登录到我们的应用程序,而无需他们创建单独的账户。这通常被称为社交登录。
一些流行的解决方案将 OAuth2 和身份管理结合在一起,例如 Keycloak (www.keycloak.org/) 和 Zitadel (zitadel.com/opensource)。这些开源项目为单点登录解决方案和高级身份管理提供了一站式服务。在 Zitadel 的情况下,它还提供了一种托管服务,如果你不想在你的基础设施内安装和维护 SSO 和身份管理组件,你可以使用这项服务。
追踪和监控也是如此。如果你计划拥有用户(你迟早会这么做),包括单点登录和身份管理到行走骨架中会促使你思考“谁将能够做什么”的具体细节,进一步细化你的用例。
2.4.7 其他挑战
在前几节中,我们已经介绍了一些你在构建云原生应用时可能会遇到的一些常见挑战,但这些并非全部。你能想到其他破坏这个应用第一版的方法吗?
注意,解决本章讨论的挑战会有所帮助,但还有其他与如何交付由不断增长的服务组成的持续演变的应用相关的挑战。
2.5 回顾平台工程
在前几节中,我们讨论了许多主题。我们回顾了打包和分发 Kubernetes 应用程序的选择,然后使用 Helm 在 Kubernetes 集群中安装我们的“行走骨架”。我们通过与它交互来测试应用程序的功能,最后,我们深入分析了团队在构建分布式应用程序时可能会遇到的一些常见的云原生挑战。
但你可能想知道所有这些话题如何与本书的标题“持续交付”以及平台工程的一般概念相关。在本节中,我们将更明确地将第一章中介绍的主题与这些话题联系起来。
首先,创建 Kubernetes 集群并在其上运行应用程序的目的是确保我们涵盖 Kubernetes 内置的弹性和扩展应用程序服务的能力。Kubernetes 提供了构建块,使我们能够在不中断服务的情况下运行我们的应用程序,即使我们不断更新它们。这使得我们,Kubernetes 用户,可以更频繁地发布我们组件的新版本,因为我们不应该停止整个应用程序来更新其某个部分。在第八章中,我们将看到 Kubernetes 内置机制如何扩展以实现不同的发布策略。
如果你没有使用 Kubernetes 提供的能力来持续向客户发布软件,那么你需要拉响警报。这种情况通常是由于 Kubernetes 之前的老旧做法阻碍了进程,缺乏自动化,或者服务之间没有明确定义的合同,这阻碍了依赖服务独立发布。我们将在未来的几章中多次涉及这个话题,因为这是尝试改进你的持续交付实践的基本原则,也是平台工程团队需要优先考虑的事项。
在本章中,我们也看到了如何使用封装了部署我们应用程序所需配置文件的包管理器来安装一个云原生应用程序。这些配置文件(以 YAML 文件形式表达 Kubernetes 资源)描述了我们的应用程序拓扑,并包含每个应用程序服务使用的容器的链接。这些 YAML 文件还包含每个服务的配置,例如配置每个服务的环境变量。对这些配置文件进行打包和版本控制,使我们能够轻松地在不同的环境中创建新的应用程序实例,这将在第四章中介绍。
如果你想要深入了解配置作为代码如何帮助你更可靠地交付更多软件的持续交付方面,我强烈推荐 Christie Wilson 的书籍《Grokking Continuous Delivery》(Manning Publications,2018 年出版)。
因为我想确保你有一个可以玩的应用程序,并且我们需要涵盖 Kubernetes 内置机制,所以我做出了一个有意识的决策,从可以轻松部署到任何 Kubernetes 集群(无论它是本地运行还是在云提供商上)的已打包应用程序开始。我们可以识别出两个不同的阶段。一个是我们还没有覆盖的,那就是如何生产这些可以部署到任何 Kubernetes 集群的包,第二个阶段,我们已经开始尝试,就是在我们具体的集群中运行这个应用程序(我们可以将这个集群视为一个环境,也许是一个开发环境),如图 2.32 所示。

图 2.32 应用程序的生命周期,从构建和打包到在环境中运行
理解这一点很重要,即为我们本地环境执行的步骤将适用于任何 Kubernetes 集群,无论集群大小和位置如何。虽然每个云提供商都将有自己的安全和身份机制,但我们安装应用程序 Helm 图表到集群时创建的 Kubernetes API 和资源将是相同的。如果你现在使用 Helm 模板功能来微调你的应用程序(例如,资源消耗和网络配置)以适应目标环境,你可以轻松地将这些部署自动化到任何 Kubernetes 集群。
在继续之前,让我们明确一点,推动开发者配置应用程序实例可能不是他们时间的最佳利用方式。访问生产环境(用户/客户正在访问的环境)的开发者可能也不是最佳选择。我们希望确保开发者专注于构建新功能和改进我们的应用程序。图 2.33 展示了我们应该自动化所有涉及构建、发布和部署开发者创建的工件步骤,确保他们可以专注于向应用程序添加功能,而不是在准备就绪时手动处理打包、分发和部署新版本。这是本章的主要关注点。

图 2.33 开发者可以专注于构建功能,但平台团队需要在更改后自动化整个流程。
了解我们可以使用的工具来自动化从源代码更改到在 Kubernetes 集群中运行软件的路径,这对于使开发者能够专注于他们最擅长的事情“编写新功能”是至关重要的。我们将解决的另一个重大差异是,云原生应用程序不是静态的。正如您在前面的图中可以看到的,我们不会安装静态的应用程序定义。我们希望随着服务的可用性,发布和部署新版本的服务。
手动安装应用程序容易出错;手动更改我们的 Kubernetes 集群中的配置可能会使我们陷入不知道如何在不同的环境中复制应用程序当前状态的情况。因此,在第三章和第四章中,我们将讨论使用通常称为管道的自动化。
在下一章中,我们将通过管道来覆盖我们分布式应用的更多动态方面,以交付我们服务的新版本。第四章将探讨我们如何使用基于 Kubernetes 的 GitOps 工具来管理我们的环境。
摘要
-
在本地和远程 Kubernetes 集群之间进行选择需要认真考虑:
-
您可以使用 Kubernetes KinD 引导本地 Kubernetes 集群以开发您的应用程序。主要缺点是您的集群受限于您的本地资源(CPU 和内存),并且不是一个真正的机器集群。
-
您可以在云提供商中拥有一个账户,并针对远程集群进行所有开发。这种方法的主要缺点是,大多数开发者不习惯于一直远程工作,并且需要有人为远程资源付费。
-
-
包管理器,如 Helm,可以帮助您打包、分发和安装您的 Kubernetes 应用程序。在本章中,您只需一条命令行就能将应用程序安装到 Kubernetes 集群中。
-
了解您的应用程序创建的 Kubernetes 资源可以给您一个关于当事情出错时应用程序将如何表现以及在实际场景中需要考虑哪些额外因素的印象。
-
即使是非常简单的应用,你也会遇到必须逐一解决的挑战。提前了解这些挑战有助于你以正确的思维方式规划和设计你的服务。
-
拥有一个“行走骨架”可以帮助你在受控环境中尝试不同的场景和技术。在本章中,你已经尝试了:
-
通过扩展和缩减你的服务来亲眼看到当事情出错时应用程序的表现。
-
维护状态很困难,我们需要专门的组件来高效地完成这项工作。
-
至少为我们的服务保留两个副本可以最小化停机时间。确保用户界面组件始终处于运行状态,可以保证即使在出现问题的情况下,用户也能与应用程序的部分进行交互。
-
当问题出现时,拥有回退和内置机制来处理问题可以使你的应用更具弹性。
-
-
如果你已经遵循了链接的逐步教程,你现在已经有了一手经验创建本地 Kubernetes 集群,安装应用程序,扩展和缩减服务,最重要的是,检查应用程序是否按预期运行。
3 个服务管道:构建云原生应用程序
本章涵盖
-
发现交付云原生应用程序的组件
-
学习创建和标准化服务管道的优势
-
使用 Tekton、Dagger 和 GitHub Actions 构建云原生应用程序
在上一章中,你安装并交互了一个由四个服务组成的简单分布式会议应用程序。本章将介绍如何使用 管道 作为交付机制来持续交付每个组件。本章描述并展示了如何构建、打包、发布和发布这些服务,以便它们可以在你的组织环境中运行。
本章介绍了 服务管道 的概念。服务管道包括从源代码构建软件到工件准备运行的所有步骤。本章分为两个主要部分:
-
连续交付云原生应用程序需要哪些条件?
-
服务管道
-
什么是服务管道?
-
使用以下方式实现服务管道:
-
Tekton,一个 Kubernetes 原生管道引擎
-
使用 Dagger 编写你的管道,然后在任何地方运行
-
我应该使用 Tekton、Dagger 还是 GitHub Actions?
-
-
3.1 连续交付云原生应用程序需要哪些条件?
当与 Kubernetes 一起工作时,团队现在负责更多的移动部件和涉及容器以及如何在 Kubernetes 中运行的任务。这些额外任务并非免费提供。团队必须学会自动化和优化保持每个服务运行所需的步骤。原本由运维团队负责的任务现在越来越多地成为负责每个单独服务的团队的责任。新的工具和新的方法赋予开发者开发、运行和维护他们所产生服务的权力。本章我们将探讨的工具旨在自动化从源代码到在 Kubernetes 集群内部署并运行的服务所需的所有任务。本章描述了将软件组件(我们的应用程序服务)交付到多个环境的机制。但在深入研究工具之前,让我们快速看一下我们面临的挑战。
构建和交付云原生应用程序面临着显著的挑战,团队必须应对:
-
在构建应用程序的不同部分时处理不同的团队互动: 这需要团队之间的协调,并确保服务被设计成负责某个服务的团队不会阻碍其他团队的进度或他们改进服务的能力。
-
我们需要支持在不中断或停止所有其他运行服务的情况下升级服务: 如果我们想要实现持续交付,服务应该能够独立升级,而不必担心整个应用程序会崩溃。这需要我们考虑新版本的后向兼容性,以及新版本是否可以与旧版本并行运行,以避免大爆炸式升级。
-
存储和发布每个服务所需的多件工件,这些工件可以从不同的环境访问/下载,这些环境可能位于不同的地区: 如果我们在云环境中工作,所有服务器都是远程的,所有产生的工件都需要对每个服务器都是可访问的,以便它们可以检索。如果你在本地环境中工作,存储这些工件的所有仓库都必须内部配置、配置和维护。
-
管理和配置不同环境以满足各种目的,如开发、测试、Q&A 和生产: 如果你想加快你的开发和测试工作,开发者和团队应该能够按需配置这些环境。将环境配置得尽可能接近真实的生产环境,将节省你大量时间,在错误影响到你的实际用户之前捕捉到它们。
正如我们在上一章中看到的,与云原生应用程序一起工作时,主要的范式转变是我们的应用程序没有单一的代码库。团队可以独立地在其服务上工作,但这需要新的方法来弥补与分布式系统一起工作的复杂性。如果每次需要向系统中添加新服务时,团队都会感到担忧并浪费时间,那么我们就是在做错事。端到端自动化对于团队来说,是舒适地添加或重构服务所必需的。这种自动化通常由通常被称为管道的机制执行。如图 3.1 所示,这些管道描述了构建和运行我们的服务需要做什么,通常它们可以在没有人为干预的情况下执行。

图 3.1 我们使用管道的概念将源代码转换成一个可以在环境中运行的工件。
你甚至可以拥有管道来自动化新服务的创建或添加新用户到你的身份管理解决方案。但这些管道究竟在做什么?我们需要从头开始创建我们的管道吗?我们如何在项目中实现这些管道?我们需要一个或多个管道来实现这一点?
第 3.2 节专注于使用管道构建可以复制、共享和多次执行以产生相同结果的解决方案。可以为不同的目的创建管道,通常将它们定义为一组步骤(按顺序依次执行)以产生一组预期的输出。基于这些输出,可以将这些管道分类到不同的组中。
大多数管道工具允许您将管道定义为一系列任务(也称为步骤或作业),这些任务将运行特定的作业或脚本以执行具体操作。这些步骤可以是任何事情,从运行测试、将代码从一个地方复制到另一个地方、部署软件、提供虚拟机、创建用户等。
管道定义可以由一个称为管道引擎的组件执行,该组件负责拾取管道定义以创建一个新的管道实例,该实例运行每个任务。任务将按顺序依次执行,每个任务的执行可能会生成可以与后续任务共享的数据。如果管道中涉及任何步骤出现错误,则管道停止,并将管道状态标记为错误(失败)。如果没有错误,则管道执行(也称为管道实例)可以标记为成功。根据管道定义和执行是否成功,我们应该验证是否生成了预期的输出或产生了输出。
在图 3.2 中,我们可以看到管道引擎正在拾取我们的管道定义并创建不同的实例,这些实例可以根据不同的输出进行不同的参数化。例如,管道实例 1 正确完成,而管道实例 2 未能执行定义中包含的所有任务。在这种情况下,管道实例 3 仍在运行。

图 3.2 管道定义可以被管道引擎多次实例化,它描述了需要完成的工作。管道引擎创建管道实例,运行管道定义中包含的任务。这些管道实例可能会失败或运行更长时间,具体取决于它们执行的任务。作为用户,您始终可以询问管道引擎特定管道实例及其任务的状态。
如预期的那样,使用这些管道定义,我们可以创建大量的不同自动化解决方案,并且常见的是找到在管道引擎之上构建更具体解决方案的工具,甚至隐藏处理管道引擎的复杂性以简化用户体验。在接下来的章节中,我们将寻找不同工具的例子,一些更底层和灵活,一些更高级,更有观点,旨在解决一个非常具体的场景。
但这些概念和工具如何应用于交付云原生应用程序呢?对于云原生应用程序,我们对如何构建、打包、发布和发布我们的软件组件(服务)以及它们应该部署在哪里有非常具体的要求。在交付云原生应用程序的上下文中,我们可以定义两种主要的管道类型:
-
服务管道:这些负责构建、单元测试、打包和分发(通常到工件存储库)我们的软件工件。
-
环境管道:这些负责部署和更新给定环境中的所有服务,例如预发布、测试、生产等,通常从事实来源消费需要部署的内容。
第三章侧重于服务管道,而第四章侧重于帮助我们使用称为 GitOps 的更声明式方法定义环境管道的工具。
通过将构建过程(服务管道)和部署过程(环境管道)分开,我们给予负责在客户面前推广新版本的团队更多的控制权。服务管道和环境管道在不同的资源上执行,并且有不同的期望。下一节将详细介绍我们在服务管道中通常定义的步骤。第四章涵盖了环境管道的期望内容。
3.2 服务管道
服务管道定义并执行构建、打包和分发服务工件所需的所有步骤,以便将其部署到环境中。服务管道不负责部署新创建的服务工件,但它可以负责通知感兴趣的各方,服务有新的版本可用。
如果您标准化服务的构建、打包和发布方式,就可以为不同的服务共享相同的管道定义。尽量避免让每个团队为每个服务定义一个完全不同的管道,因为他们可能会重新发明已经被其他团队定义、测试和改进过的东西。需要执行相当多的任务,并且有一套约定,遵循这些约定可以减少完成整个流程所需的时间。
“服务管道”这个名字指的是我们应用程序的每个服务都将有一个管道,描述了该特定服务所需的任务。如果服务相似并且它们使用相似的技术堆栈,那么管道看起来相当相似是有意义的。这些服务管道的主要目标之一是包含足够的细节,以便在没有人工干预的情况下运行,自动化管道中的所有任务。
服务管道可以用作一种机制,以改善创建服务的开发团队和在生产环境中运行该服务的运维团队之间的沟通。开发团队期望这些管道能够运行,并在他们尝试构建的代码出现任何问题时通知他们。如果没有错误,他们期望作为管道执行的一部分生成一个或多个工件。运维团队可以向这些管道添加所有检查,以确保生成的工件已准备好投入生产。这些检查可以包括策略和合规性检查、签名、安全扫描以及其他要求,以验证生成的工件符合预期在生产环境中运行的标准。
注意:可能会诱使人们考虑为整个应用程序(服务集合)创建一个单一的管道,就像我们处理单体应用程序那样。然而,这样做违背了独立按各自节奏更新每个服务的初衷。你应该避免为一系列服务定义单一管道的情况,因为这会阻碍你独立发布服务的能力。
3.3 节省你时间的约定
服务管道可以在结构和范围上更加具有意见性。通过遵循这些强烈的意见和约定,你可以避免让你的团队定义每一个细节,并通过试错来发现这些约定。以下方法已被证明是有效的:
-
主干开发: 这里的想法是确保你源代码仓库的主分支中的内容始终准备好发布。你不合并会破坏此分支构建和发布过程的更改。只有当你合并的更改准备好发布时,你才进行合并。这种方法还包括使用功能分支,允许开发者在不破坏主分支的情况下工作。当功能完成并经过测试后,开发者可以向其他开发者发送拉取请求(变更请求)以供审查和合并。这也意味着当你将某些内容合并到主分支时,你可以自动创建服务(以及所有相关工件)的新版本。这创建了一个连续的发布流,每次新功能合并到主分支后都会生成新的发布。因为每个发布都是一致的并且已经过测试,所以你可以将这个新版本部署到一个包含你应用程序中所有其他服务的环境中。这种方法使得服务背后的团队能够继续前进并持续发布,而无需担心其他服务。
-
源代码和配置管理: 处理软件及其运行所需配置的方法有很多种。当我们谈论服务和分布式应用程序时,存在两种不同的思想流派:
-
一个服务/一个仓库/一个管道: 您将服务的源代码以及所有需要构建、打包、发布和部署的配置保存在同一个仓库中。这使得服务背后的团队能够以他们想要的任何速度推送更改,而不用担心其他服务的源代码。通常的做法是将源代码保存在包含描述如何创建 Docker 镜像的
Dockerfile和部署服务到 Kubernetes 集群所需的 Kubernetes 清单的同一个仓库中。这些配置应包括用于构建和打包您的服务的管道定义。 -
单仓库: 或者,采用单仓库方法,其中使用单个仓库,并为仓库内的不同目录配置不同的管道。虽然这种方法可以工作,但您需要确保您的团队不会因为等待彼此的拉取请求合并而相互阻塞。
-
-
消费者驱动的合同测试: 您的服务使用合同对其他服务进行测试。对单个服务的单元测试不应需要其他服务正在运行。通过创建消费者驱动的合同,每个服务都可以对其功能进行其他 API 的测试。如果任何下游服务发布,新的合同将与所有上游服务共享,以便它们可以对其新版本进行测试。
我强烈推荐以下两本书:
-
《持续交付:通过构建、测试和部署自动化实现可靠的软件发布》(Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation)作者:Jez Humble 和 David Farley(Addison-Wesley Professional,2010 年)
-
《精通持续交付》(Grokking Continuous Delivery)作者:Christie Wilson(Manning Publications,2022 年)
这些书中提到的大多数工具都允许您实现这些实践以提高交付效率。如果我们考虑这些实践和惯例,我们可以将服务管道的职责定义为如下:服务管道将源代码转换为一个或多个可以在环境中部署的工件。
3.4 服务管道结构
带着这个定义,让我们看看在 Kubernetes 上运行的云原生应用的服务管道中包含哪些任务:
-
注册以接收有关源代码仓库主分支更改的通知:(源代码版本控制系统,如今通常是 Git 仓库。)如果源代码发生变化,我们需要创建一个新的版本。我们通过触发服务管道来创建新版本。这通常是通过 Webhook 或基于拉取的机制实现的,该机制检查是否有新更改提交。
-
从仓库克隆源代码: 为了构建服务,我们需要将源代码克隆到一台具有构建/编译源代码为可执行二进制格式的工具的机器上。
-
为即将发布的版本创建一个新的标签: 基于主干开发,每次发生更改时都可以创建一个新的发布版本。这将帮助我们了解正在部署的内容以及每个新版本中包含的更改。
-
构建和测试源代码:
-
作为构建过程的一部分,大多数项目都会执行单元测试,如果出现任何失败,则会中断构建。
-
根据我们的技术栈,我们需要在这个步骤中可用的工具,例如,编译器、依赖项、linters(静态源代码分析器)等。
-
使用 CodeCov 等工具,它测量代码被测试覆盖的程度,如果未达到覆盖率阈值,则用于阻止更改合并。
-
安全扫描器也用于评估我们的应用程序依赖项中的漏洞。如果发现新的 CVE(常见漏洞与暴露),更改也可以被阻止。
-
-
将二进制工件发布到工件存储库: 我们需要确保这些二进制文件可供其他系统消费,包括管道中的下一步。这一步涉及通过网络将二进制工件复制到不同的位置。这个工件将与在仓库中创建的标签具有相同的版本,为我们提供了从二进制文件到用于生成它的源代码的可追溯性。
-
构建容器镜像: 如果我们正在构建云原生服务,我们必须构建一个容器镜像。目前最常见的方法是使用 Docker 或其他容器替代品。这一步需要源代码仓库中有一个
Dockerfile,定义如何构建这个容器镜像以及构建(构建器)容器镜像的机制。一些工具,如 CNCF Buildpacks (buildpacks.io),可以让我们避免使用Dockerfile并自动化容器构建过程。拥有适合工作的正确工具是至关重要的,因为可能需要为不同的平台生成多个容器镜像。对于一个发布的服务,我们可能有多个容器镜像,例如,一个用于amd64平台,另一个用于arm64平台。本书中的所有示例都是为这两个平台构建的。 -
将容器镜像发布到容器注册库: 与我们发布构建服务源代码时生成的二进制工件的方式相同,我们需要将我们的容器镜像发布到一个集中位置,以便其他人可以访问它。这个容器镜像将与在仓库中创建的标签和发布的二进制文件具有相同的版本。这有助于我们了解运行容器镜像时将运行哪个源代码。
-
检查、验证并可选地打包用于 Kubernetes 部署的 YAML 文件 (此处可以使用 Helm): 如果你在这些容器内部署 Kubernetes,你需要管理、存储和版本化一个 Kubernetes 清单,该清单定义了容器将如何部署到 Kubernetes 集群中。如果你使用包管理器如 Helm,你可以使用与二进制文件和容器镜像相同的版本对包进行版本控制。我打包 YAML 文件的规则如下:“如果你有足够多的人尝试安装你的服务(开源项目或非常大的全球分布式组织),你可能想要打包和版本化你的 YAML 文件。如果你只有少数团队和环境要处理,你很可能可以在不使用打包工具的情况下分发 YAML 文件。”
-
(可选) 将这些 Kubernetes 清单发布到集中位置: 如果你使用 Helm,将 Helm 包(称为图表)推送到集中位置是有意义的。这将允许其他工具检索这些图表,以便它们可以在任意数量的 Kubernetes 集群中部署。正如我们在第二章中看到的,这些 Helm 图表现在可以作为 OCI 容器镜像分发到容器注册库。
-
通知相关方关于服务新版本的更新: 如果我们尝试从源自动化到运行中的服务,服务管道可以向所有可能等待新版本部署的相关服务发送通知。这些通知可以是向其他存储库的拉取请求、事件总线的事件、发送给对这些发布感兴趣团队的电子邮件等。基于拉取的方法也可以工作,其中代理持续监控工件存储库(或容器注册库)以查看是否存在给定工件的新版本。
图 3.3 展示了前述要点描述的步骤作为一系列步骤。大多数管道工具都将有一个可视化表示,允许你看到哪些步骤将被执行。

图 3.3 显示服务管道自动化了在多个环境中运行所需的所有步骤。服务管道通常由源代码的变化触发,但并不负责在特定环境中部署创建的工件。它们可以通知其他组件关于这些新版本的信息。
该管道的输出是一组可以部署到环境中以使服务运行起来的工件。服务需要以不依赖于任何特定环境的方式构建和打包。服务可以依赖于其他服务在环境中工作,例如数据库、消息代理或其他下游服务。
无论你选择哪种工具来实现这些管道,你应该关注以下特性:
-
管道根据更改自动运行(如果您遵循基于主干的开发,则为主分支的每个更改创建一个管道实例)。
-
管道执行将通知关于成功或失败状态,并带有清晰的消息。这包括有简单的方法来查找,例如,管道失败的原因和位置,以及执行每个步骤所需的时间。
-
每个管道执行都有一个唯一的
id,我们可以使用它来访问日志和运行管道时使用的参数,这样我们就可以重现用于解决问题的设置。使用这个唯一的id,我们还可以访问管道中所有步骤创建的日志。通过查看管道执行,我们也应该能够找到所有生成的工件以及它们发布的位置。 -
管道也可以手动触发,并针对特殊情况配置不同的参数。例如,测试一个正在开发中的功能分支。
让我们深入了解服务管道在现实生活中的具体细节。
3.4.1 生活中的服务管道
在现实生活中,服务管道将在您将更改合并到存储库主分支的每次都运行。如果您遵循基于主干的开发方法,它应该这样工作:
-
当您将更改合并到主分支时,此服务管道将运行并使用最新的代码库创建一系列工件。如果服务管道成功,我们的工件将可发布。我们希望确保我们的主分支始终处于可发布状态,因此运行在主分支之上的服务管道必须始终成功。如果由于某种原因,此管道失败,负责服务的团队需要尽快将重点转向解决问题。换句话说,团队不应该将破坏其服务管道的代码合并到主分支中。我们还必须在我们的功能分支中运行管道来完成这项工作。
-
对于您的每个功能分支,应该运行一个非常相似的管道来验证分支中的更改是否可以构建、测试和与主分支一起发布。在现代环境中,使用 GitHub 拉取请求的概念来运行这些管道,以确保在合并任何拉取请求之前,管道验证了更改。
-
在将一组功能合并到主分支后,由于我们知道主分支始终可以发布,负责服务的团队决定标记一个新的发布版本。在 Git 中,基于主分支创建一个新的标签(指向特定提交的指针)。标签名称通常用于表示管道将创建的工件版本。
图 3.4 显示了为主分支配置的管道和一个仅当创建拉取请求时才验证功能分支的通用管道。可以触发这些管道的多个实例来持续验证新更改。

图 3.4 主分支和功能分支的服务管道
图 3.4 中所示的服务管道代表了每次将内容合并到主分支时必须执行的常见步骤。尽管如此,还有一些在此管道基础上进行的变体,您可能需要在不同的环境下运行。不同的事件可以触发管道执行,我们可以为不同的目的拥有略微不同的管道,例如:
-
验证功能分支中的变更: 此管道可以执行与主分支中相同的步骤,但生成的工件应包括分支名称,可能作为版本号或作为工件名称的一部分。每次变更后运行管道可能成本太高且不是每次都需要,因此您应根据需要做出决定。
-
验证拉取请求(PR)/变更请求: 管道将验证拉取请求/变更请求的更改是否有效,并且可以使用最近的更改生成工件。通常,管道的结果可以通知给负责合并 PR 的用户,如果管道失败,还可以阻止合并选项。此管道用于验证合并到主分支的内容是否有效并可发布。验证拉取请求和变更请求可以是一个很好的选项,以避免在功能分支的每次变更时运行管道。当开发者准备好从构建系统获取反馈时,它可以创建一个将触发管道的 PR。如果开发者在 PR 之上进行了更改,则管道将被重新触发。
尽管可以添加一些细微的差异和优化到这些管道中,但行为和生成的工件基本上是相同的。这些约定和方法依赖于执行足够的测试以验证生成的服务可以部署到环境中。
3.4.2 服务管道要求
本节涵盖了服务管道工作的基础设施要求以及管道执行工作所需的源存储库内容。
让我们从服务管道需要工作的基础设施要求开始:
-
源代码变更通知的 Webhooks: 首先,它需要访问权限来将 Webhooks 注册到包含服务源代码的 Git 仓库中,以便在新的更改合并到主分支时创建一个管道实例。
-
工件存储库可用以及推送二进制工件的有效凭证: 一旦构建了源代码,我们需要将新创建的工件推送到存储所有工件的工件存储库。这需要配置一个具有有效凭证的工件存储库以推送新工件。
-
容器注册库和有效的凭证以推送新的容器镜像: 就像我们需要推送二进制工件一样,我们还需要分发我们的 Docker 容器,以便 Kubernetes 集群在我们想要部署服务的新实例时可以获取到镜像。需要一个带有有效凭证的容器注册库来完成这一步骤。
-
Helm 图表存储库和有效的凭证: Kubernetes 清单可以打包并作为 Helm 图表分发。如果您使用 Helm,您必须有一个 Helm 图表存储库和有效的凭证来推送这些包。
图 3.5 显示了管道实例将与之交互的最常见外部系统。从 Git 仓库到工件存储库和容器注册库,维护这些管道的团队必须确保正确的凭证到位,并且这些组件可以从管道运行的位置(从网络角度来看)访问。

图 3.5 运行管道需要大量的基础设施就绪。这包括维护服务和存储库、创建用户和凭证,并确保这些服务(存储库)可以从远程位置访问。
为了让服务管道完成其工作,包含服务源代码的存储库也需要有一个Dockerfile或生成容器镜像的方式以及必要的 Kubernetes 清单,以便将服务部署到 Kubernetes 中。
图 3.6 显示了我们的服务源代码存储库的可能目录布局,其中包含所有将被编译成二进制格式的文件所在的源(src)目录。Dockerfile用于构建服务的容器镜像,Helm 图表目录包含创建可以分发以安装到 Kubernetes 集群的 Helm 图表所需的所有文件。您可以选择为每个服务创建一个 Helm 图表,或者为所有应用程序服务创建一个单独的 Helm 图表。
图 3.6 显示了服务布局,包括 Helm 图表定义。这有助于独立打包和分发服务。如果我们把构建、打包和运行我们的服务所需的所有内容都包含到一个 Kubernetes 集群中,服务管道需要在主分支的每次更改后运行,以创建新的服务版本。

图 3.6 服务源代码存储库需要所有配置,以便服务管道能够工作。
总结来说,服务管道负责构建我们的源和相关工件以部署到环境中。如前所述,服务管道不负责将生成的服务部署到实际环境中。环境管道的责任将在下一章中介绍。
3.4.3 关于服务管道的意见、限制和妥协
在创建服务管道方面,没有“一刀切”的解决方案。在现实生活中,您必须根据您的需求做出妥协。在查看 Tekton、Dagger 和 GitHub Actions 等工具之前,让我快速谈谈一些我看到团队在斗争中的实际方面。以下是在设计您的服务管道时需要考虑的一些简短且非详尽的清单:
-
避免制定严格的规则和观点来定义服务管道的起点和终点: 例如,您的服务可能不需要像前几节中提到的那样打包成 Helm 图表。如果没有足够的案例表明您想要安装一个隔离的服务——例如,您的服务严重依赖于其他服务——那么从服务管道和图表定义中移除这一步骤可能很有意义。
-
了解您组件和工件的生命周期: 根据服务变更的频率及其依赖关系,服务管道可以相互链接,共同构建一组服务。映射这些关系并理解操作这些服务的团队的需求,将为您提供创建服务管道的正确粒度。例如,您可以允许您的团队持续发布新版本的服务的新容器镜像,但不同的团队控制着将所有应用程序服务捆绑在一起的 Helm 图表的节奏和发布。
-
找到最适合您组织的方案: 根据业务优先级优化端到端自动化。如果一个关键服务导致发布和部署延迟,请在尝试覆盖其他服务之前,确保服务管道已准备就绪并完全可用。创建通用的解决方案是没有意义的,因为这些方案可能需要一段时间才能发现您的组织在 80%的情况下都只使用单一服务。
-
不要创建不必要的步骤,直到它们是必需的: 我在这本书中多次提到 Helm 这样的工具来打包和分发 Kubernetes 清单,但我并不是建议这就是正确的方法。我使用 Helm 作为一个广泛采用的示例工具,但您可能处于不需要打包您的 Kubernetes 清单进行分发的情况。如果这种情况发生,您的服务管道不应该包含这一步骤。如果以后需要,您可以扩展您的服务管道以包含更多步骤。
现在我们来看看这个领域的一些工具。
3.5 服务管道的实际应用
目前市面上有几种管道引擎,甚至包括像 GitHub Actions(github.com/features/actions)这样的完全托管服务以及几个知名的 CI(持续集成)托管服务,它们将为您提供大量集成,以便您构建和打包应用程序的服务。
在接下来的几节中,我们将考察两个项目:Tekton 和 Dagger。这些项目为你提供了与云原生应用程序一起工作的工具,正如我们将在第六章中看到的,它们使平台团队能够打包、分发和重用组织在一段时间内构建的特定知识。Tekton(tekton.dev)被设计为 Kubernetes 的管道引擎。因为 Tekton 是一个通用的管道引擎,你可以用它创建任何管道。另一方面,一个名为 Dagger(dagger.io)的更新的项目被设计为可以在任何地方运行。我们将通过 GitHub Actions 对比 Tekton 和 Dagger。
3.5.1 Tekton 应用实例
Tekton 最初是由 Google 的 Knative 项目(knative.dev)的一部分创建的。(我们将在第八章中更深入地了解 Knative)。Tekton 最初被称为 Knative Build,后来从 Knative 中分离出来成为一个独立的项目。Tekton 的主要特点是它是一个为 Kubernetes 设计的云原生管道引擎。本节将探讨如何使用 Tekton 定义服务管道。
在 Tekton 中,你有两个主要概念:任务和管道。在 Tekton 中,管道引擎是一组理解如何执行 Tasks 和 Pipelines Kubernetes 资源组件。Tekton,就像本书中涵盖的大多数 Kubernetes 项目一样,可以安装到你的 Kubernetes 集群中。我强烈建议你查看他们的官方文档页面,该页面解释了使用像 Tekton 这样的工具的价值,网址为 tekton.dev/docs/concepts/overview/。
注意:我在这个存储库中包含了一系列逐步教程。你可以从查看如何在你的集群中安装 Tekton 以及 tekton/hello-world/ 示例开始,该示例位于 github.com/salaboy/platforms-on-k8s/tree/main/chapter-3/tekton。
当你安装 Tekton 时,你安装了一套自定义资源定义,这是 Kubernetes API 的扩展,用于定义任务和管道。Tekton 还安装了知道如何处理 Tasks 和 Pipelines 资源的管道引擎。请注意,在安装 Tekton 之后,你还可以安装 Tekton Dashboard 和 tkn 命令行界面工具。
一旦安装了 Tekton 版本,你将看到一个名为 tekton-pipelines 的新命名空间,其中包含管道控制器(管道引擎)和管道 webhook 监听器,后者用于监听来自外部源的事件,例如 Git 仓库。
Tekton 中的任务看起来就像一个普通的 Kubernetes 资源,如列表 3.1 所示。
列表 3.1 简单的 Tekton 任务定义
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: hello-world-task ①
spec:
params:
- name: name ②
type: string
description: who do you want to welcome?
default: tekton user
steps:
- name: echo
image: ubuntu ③
command:
- echo ④
args:
- "Hello World: $(params.name)" ⑤
① 元数据中定义的资源名称代表任务定义名称。
② 我们可以使用 params 部分来定义可以为我们的任务定义配置哪些参数。
③ 这个任务将使用名为 Ubuntu 的 Docker 镜像。
④ 在这种情况下,命令参数(args)只是一个“Hello World”字符串;请注意,你可以为更复杂的命令发送参数列表。
⑤ 在这种情况下,命令参数(args)只是一个“Hello World: $(params.name)”字符串,它将使用任务参数。
你可以在这个存储库中找到任务定义,以及一个逐步教程,教你如何在你的集群中运行它:github.com/salaboy/platforms-on-k8s/blob/main/chapter-3/tekton/hello-world/hello-world-task.yaml。
从这个例子中,你可以为任何你想要的东西创建一个任务,因为你有权定义使用哪个容器以及运行哪些命令。一旦你有任务定义,你需要通过将此文件应用到集群中(kubectl apply -f task.yaml)来使它对 Tekton 可用。通过将文件应用到 Kubernetes 中,我们只是在集群中使定义对 Tekton 组件可用,但任务不会运行。
如果你想要运行这个任务,一个任务可以被多次执行。Tekton 要求你创建一个类似于以下列表的 TaskRun 资源。
列表 3.2 任务运行表示任务定义的一个实例
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: hello-world-task-run-1
spec:
params:
- name: name
value: "Building Platforms on top of Kubernetes reader!" ①
taskRef:
name: hello-world-task ②
① 我们可以为这个 TaskRun 定义特定的参数值。
② 我们需要引用我们想要运行的任务定义的名称。请注意,这个名称对于每个我们定义的任务资源都是唯一的。
TaskRun 资源可以在github.com/salaboy/platforms-on-k8s/blob/main/chapter-3/tekton/hello-world/task-run.yaml找到。
如果你将这个 TaskRun 应用到集群中(kubectl apply -f taskrun.yaml),管道引擎将执行这个任务。你可以通过查看列表 3.3 中的 TaskRun 资源来查看 Tekton 任务的实际操作。
列表 3.3 获取所有 TaskRun 实例
> kubectl get taskrun
NAME SUCCEEDED STARTTIME COMPLETIONTIME
hello-world-task-run-1 True 66s 7s
如果你列出所有正在运行的 Pod,你会注意到每个任务都会创建一个 Pod,如列表 3.4 所示。
列表 3.4 列出与 TaskRuns 关联的所有 Pod
> kubectl get pods
> kubectl get pods
NAME READY STATUS AGE
NAME READY STATUS AGE
hello-world-task-run-1-pod 0/1 Init:0/1 2s
由于你有一个 Pod,你可以使用 Pod 名称来查看任务正在做什么,如列表 3.5 所示。
列表 3.5 使用 Pod 名称访问 TaskRun 日志
> kubectl logs -f hello-world-task-run-1-pod
> kubectl get pods
NAME READY STATUS AGE
Defaulted container "step-echo" out of: step-echo, prepare (init)
> kubectl get pods
NAME READY STATUS AGE
Hello World: Building Platforms on top of Kubernetes reader!
你刚刚执行了你的第一个 Tekton TaskRun。恭喜!但单个任务根本不有趣。如果我们能将多个任务串联起来,我们就可以创建我们的服务管道。让我们看看如何从这个简单的任务示例构建 Tekton 管道。
3.5.2 Tekton 中的管道
一个任务可能很有用,但当你使用管道创建这些任务的序列时,Tekton 才变得有趣。
管道是一系列按具体顺序排列的任务。以下管道使用了我们之前定义的任务定义。它打印一条消息,从 URL 获取一个文件,然后读取其内容,该内容被转发到我们的 Hello World 任务,该任务打印一条消息。
图 3.7 显示了一个包含三个 Tekton 任务的简单 Tekton 管道。

图 3.7 使用我们的 Hello World 任务的简单 Tekton 管道
在这个简单的管道中,我们使用了一个来自 Tekton Hub 的现有任务定义(wget),Tekton Hub 是一个社区仓库,托管通用任务,然后我们在管道内定义了cat任务,以展示 Tekton 的灵活性,最后使用我们在上一节中定义的Hello World任务。
让我们看看 Tekton 中定义的一个简单服务管道(hello-world-pipeline.yaml)。别害怕。这是一大堆 YAML,我警告过您。参见列表 3.6。
列表 3.6 管道定义
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: hello-world-pipeline
annotations:
description: |
Fetch resource from internet, cat content and then say hello
spec:
results: ①
- name: message
type: string
value: $(tasks.cat.results.messageFromFile)
params: ②
- name: url
description: resource that we want to fetch
type: string
default: ""
workspaces: ③
- name: files
tasks:
- name: wget
taskRef: ④
name: wget
params:
- name: url
value: "$(params.url)"
- name: diroptions
value:
- "-P"
workspaces:
- name: wget-workspace
workspace: files
- name: cat
runAfter: [wget]
workspaces:
- name: wget-workspace
workspace: files
taskSpec: ⑤
workspaces:
- name: wget-workspace
results:
- name: messageFromFile
description: the message obtained from the file
steps:
- name: cat
image: bash:latest
script: |
#!/usr/bin/env bash
cat $(workspaces.wget-workspace.path)/welcome.md |
> kubectl get pods
NAME READY STATUS AGE
➥tee /tekton/results/messageFromFile
- name: hello-world
runAfter: [cat]
taskRef:
name: hello-world-task ⑥
params:
- name: name
value: "$(tasks.cat.results.messageFromFile)" ⑦
① 管道资源可以定义在执行时预期的结果数组。任务
② 与任务一样,我们可以定义在运行此管道时用户可以设置的参数。如果需要,这些管道参数可以转发到单个任务。可以在它们执行时设置这些结果值。
③ 管道和任务允许使用 Tekton Workspaces 来存储持久信息。这可以用于在任务之间共享信息。由于每个任务都在其容器中执行,因此使用持久存储来共享信息很容易设置。
④ 我们使用对未创建的任务的任务引用。我们需要确保在为该管道创建 PipelineRun 之前安装此任务定义。
⑤ 如果我们想的话,可以在管道内定义任务。这使得管道文件更复杂,但有时有一个仅仅将其他任务粘合在一起的任务是有用的,就像在这个例子中一样。这个任务的唯一目的是读取下载文件的内容,并将其作为字符串提供给我们的 Hello World 任务,该任务不接受文件。
⑥ 这也要求在集群中安装“hello-world-task”定义。请记住,您始终可以运行“kubectl get tasks”来查看哪些任务可用。
⑦ 我们可以使用 Tekton 强大的模板机制为 Hello World 任务提供值。我们使用对“cat”任务结果的引用。
您可以在github.com/salaboy/platforms-on-k8s/blob/main/chapter-3/tekton/hello-world/hello-world-pipeline.yaml找到完整的管道定义。
在应用管道定义之前,您需要安装由 Tekton 社区创建和维护的wget Tekton 任务:
kubectl apply -f
➥https://raw.githubusercontent.com/tektoncd/catalog/main/task/wget/0.1/wget.yaml
再次强调,您必须将此管道资源应用到您的集群中,以便 Tekton 能够识别:kubectl apply -f hello-world-pipeline.yaml。
正如你在管道定义中所见,spec.tasks字段包含一个任务数组。这些任务需要已经部署到集群中,并且管道定义定义了这些任务将执行的顺序。这些任务引用可以是你的任务,或者如示例所示,它们可以来自 Tekton 目录,这是一个包含社区维护的任务定义的存储库,你可以重用它。
同样地,因为任务需要 TaskRuns 来执行,所以每次你想执行你的管道时,你都需要创建一个 PipelineRun,如下面的列表所示。
列表 3.7 PipelineRun 代表我们管道的一个实例(执行)
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: hello-world-pipeline-run-1
spec:
workspaces: ①
- name: files
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1M
params:
- name: url ②
value:
➥"https://raw.githubusercontent.com/salaboy/salaboy/main/welcome.md"
pipelineRef:
name: hello-world-pipeline ③
① 当我们创建一个 PipelineRun 时,我们需要将管道定义中定义的工作区绑定到实际的存储。在这种情况下,创建了一个 VolumeClaim,请求为 PipelineRun 使用 1 Mb 的存储。
② 管道参数“url”可以是任何你想要的 URL,因为它可以从 PipelineRun 上下文中访问(这意味着它可以访问该 URL,并且它不在防火墙后面)。
③ 与任务一样,我们需要提供我们想要用于此 PipelineRun 的管道定义的名称。
你可以在github.com/salaboy/platforms-on-k8s/blob/main/chapter-3/tekton/hello-world/pipeline-run.yaml找到 PipelineRun 资源。
当你将此文件应用到集群中kubectl apply -f pipeline-run.yaml时,Tekton 将通过运行管道定义中定义的所有任务来执行管道。在运行此管道时,Tekton 将为每个任务创建一个 pod 和三个 TaskRun 资源。管道只是编排任务,换句话说,就是创建 TaskRuns。
要检查 TaskRuns 是否已创建并且管道已成功执行,请参阅列表 3.8。
列表 3.8 从管道执行中获取任务运行
> kubectl get taskrun
NAME SUCCEEDED STARTTIME COMPLETIONTIME
hello-world-pipeline-run-1-cat True 109s 104s
hello-world-pipeline-run-1-hello-world True 103s 98s
hello-world-pipeline-run-1-wget True 117s 109s
对于每个 TaskRun,Tekton 会创建一个 pod(见 3.9 列表)。
列表 3.9 检查属于管道的所有 TaskRun 是否已完成
> kubectl get pods
NAME READY STATUS AGE
hello-world-pipeline-run-1-cat-pod 0/1 Completed 11s
hello-world-pipeline-run-1-hello-world-pod 0/1 Completed 5s
hello-world-pipeline-run-1-wget-pod 0/1 Completed 19s
查看来自hello-world-pipeline-run-1-hello-world-pod的日志,以查看任务打印了什么,如列表 3.10 所示。
列表 3.10 获取最后一个任务的日志
> kubectl logs hello-world-pipeline-run-1-hello-world-pod
Defaulted container "step-echo" out of: step-echo, prepare (init)
Hello World: Welcome, Internet traveler! Do you want to learn more about Platforms on top of Kubernetes? Check this repository: https://github.com/salaboy/platforms-on-k8s
你可以始终在 Tekton 仪表板中查看 Tasks、TaskRuns、Pipelines 和 PipelineRuns。要访问 Tekton 仪表板,如果你在集群中安装了它,你首先需要运行:
> kubectl port-forward -n tekton-pipelines
➥services/tekton-dashboard 9097:9097
图 3.8 显示了 Tekton 仪表板用户界面,在那里我们可以探索我们的任务和管道定义,以及触发新的任务和管道运行并探索每个任务输出的日志。

图 3.8 Tekton 仪表板中的我们的 PipelineRun 执行
如果需要,你可以在以下存储库中找到如何在你的 Kubernetes 集群中安装 Tekton 以及如何运行服务流水线的分步教程:github.com/salaboy/platforms-on-k8s/blob/main/chapter-3/tekton/hello-world/README.md。
在教程的最后,你可以找到链接到我为每个会议应用服务定义的更复杂的流水线的链接。这些流水线更复杂,因为它们需要访问外部服务、发布工件和容器镜像的凭证,以及在集群内部执行一些特权操作的权限。如果你对这个部分感兴趣,请查看教程的此部分:github.com/salaboy/platforms-on-k8s/tree/main/chapter-3/tekton#tekton-for-service-pipelines。
3.5.3 Tekton 优点和附加功能
正如我们所见,Tekton 非常灵活,允许你创建高级流水线,并且它还包括其他功能,例如:
-
输入和输出映射,用于在任务之间共享数据
-
事件触发器,允许你监听将触发流水线或任务的事件
-
一个命令行工具,可以轻松地从终端与任务和流水线交互
-
一个简单的仪表板,用于监控你的流水线和任务执行(图 3.9)

图 3.9 Tekton 仪表板——用于监控你的流水线的用户界面
图 3.9 展示了由社区驱动的 Tekton 仪表板,你可以使用它来可视化你的流水线执行。记住,因为 Tekton 是构建在 Kubernetes 之上的,你可以像监控其他 Kubernetes 资源一样使用 kubectl 来监控你的流水线。然而,对于不太懂技术的用户来说,没有什么能比用户界面更好的了。
但现在,如果你想要使用 Tekton 实现一个服务流水线,你将花费相当多的时间来定义任务、流水线、如何映射输入和输出、定义适合你的 Git 仓库的正确事件监听器,然后更深入地定义每个任务将使用的 Docker 镜像。创建和维护这些流水线及其相关资源可能变成一份全职工作,为此,Tekton 启动了一个项目来定义一个目录,其中可以共享任务(为未来的发布计划了流水线和资源)。Tekton 目录可在 github.com/tektoncd/catalog 找到。
在 Tekton 目录的帮助下,我们可以创建引用目录中定义的任务的管道。在前一节中,我们使用了从该目录下载的 wget 任务;您可以在 hub.tekton.dev/tekton/task/wget 找到 wget 任务的完整描述。因此,我们不需要担心定义它们。您还可以访问 hub.tekton.dev,它允许您搜索任务定义,并提供有关在管道中安装和使用这些任务的详细文档(图 3.10)。
Tekton Hub 和 Tekton 目录允许您重用大量用户和公司创建的任务和管道。我强烈建议您查看 Tekton 概述页面,该页面总结了使用 Tekton 的优势,包括谁应该使用 Tekton 以及原因:tekton.dev/docs/concepts/overview/。

图 3.10 Tekton Hub 是一个共享和重用任务和管道定义的门户
Tekton 是云原生空间中相当成熟的项目,但也带来了一些挑战:
-
您需要在 Kubernetes 集群内部署并维护 Tekton。您不希望您的管道直接运行在应用程序工作负载旁边,因此您可能需要一个单独的集群。
-
在本地运行 Tekton 管道没有简单的方法。出于开发目的,您需要能够访问 Kubernetes 集群以手动运行管道。
-
您需要了解 Kubernetes 来定义和创建任务和管道。
-
虽然 Tekton 提供了一些条件逻辑,但它受到 YAML 中可以执行的操作以及 Kubernetes 的声明式方法的限制。
现在我们将跳入一个名为 Dagger 的项目,该项目旨在缓解一些这些问题,不是为了取代 Tekton,而是为了提供解决日常挑战的不同方法,当构建复杂管道时。
3.5.4 Dagger 在行动
Dagger (dagger.io) 的诞生有一个目标:“让开发者能够使用他们喜欢的编程语言构建管道,并且可以在任何地方运行。”Dagger 只依赖于容器运行时来运行可以使用代码定义的管道,而任何开发者都可以编写这些代码。Dagger 目前支持 Go、Python、TypeScript 和 JavaScript SDKs,但 Dagger 背后的团队正在快速扩展到新的语言。
Dagger 并不专注于 Kubernetes。平台团队必须确保,在团队使用 Kubernetes 强大且声明式特性的同时,开发团队也能高效工作并使用适合工作的适当工具。本节将探讨 Dagger 与 Tekton 的比较,它更适合的地方,以及它如何补充其他工具。
如果您想开始使用 Dagger,您可以查看这些资源:
-
Dagger 文档:
docs.dagger.io -
Dagger 快速入门:
docs.dagger.io/648215/quickstart/ -
Dagger GraphQL playground:
play.dagger.cloud
Dagger,就像 Tekton 一样,也有一个管道引擎,但这个引擎可以在本地和远程工作,为不同环境提供统一的运行时。Dagger 不直接与 Kubernetes 集成。这意味着没有 Kubernetes CRDs 或 YAML 涉及。这取决于负责创建和维护这些管道的团队的技术和偏好,可能很重要。
在 Dagger 中,我们通过编写代码来定义管道。因为管道只是代码,所以这些管道可以使用任何代码打包工具进行分发。例如,如果我们的管道是用 Go 编写的,我们可以使用 Go 模块导入其他团队编写的管道或任务。如果我们使用 Java,我们可以使用 Maven 或 Gradle 打包和分发我们的管道库以促进重用。
图 3.11 展示了开发团队如何使用 Dagger SDKs 编写管道,然后使用 Dagger 引擎执行这些管道,使用任何 OCI 容器运行时,如 Docker 或 PodMan。无论你是在本地开发环境(装有 Docker for Mac 或 Windows 的笔记本电脑)中运行管道,还是在持续集成环境中,甚至是在 Kubernetes 集群内部,这些管道的行为都是相同的。

图 3.11 使用你喜欢的编程语言及其工具编写管道(来源:dagger.io)
Dagger 管道引擎负责协调管道中定义的任务,并优化每个任务使用的容器运行时请求的内容。Dagger 管道引擎的一个显著优势是它从一开始就被设计用来优化管道的运行方式。想象一下,如果你每天要构建成吨的服务多次,你不仅会保持你的 CPU 热起来,而且下载工件所产生的流量,一次又一次地,变得昂贵——如果你在云服务提供商之上运行,他们根据消费量向你收费,那就更贵了。
Dagger,类似于 Tekton,使用容器来执行管道中的每个任务(步骤)。管道引擎通过缓存先前执行的结果来优化资源消耗,防止你重新执行已经使用相同输入执行的任务。此外,你可以在你的笔记本电脑/工作站上本地运行 Dagger 引擎,甚至远程运行,甚至在 Kubernetes 集群内部。
当我将 Dagger 与类似 Tekton 的东西进行比较时,凭借我的开发者背景,我倾向于喜欢使用我熟悉的编程语言来编写管道的灵活性。对于开发者来说,创建、版本控制和共享代码很容易,因为我不需要学习任何新工具。
我不想看一个 Hello World 的例子,而是想展示在 Dagger 中服务管道的样子。因此,让我们看看如何使用 Dagger Go SDK 定义服务管道。以下代码片段展示了定义每个服务想要执行的主要目标的管道。看看 buildService、testService 和 publishService 函数。这些函数将构建、测试和发布每个服务的含义编码化。这些函数使用 Dagger 客户端在 Dagger 将要编排的容器内执行操作,如列表 3.11 所示。
列表 3.11 使用 Dagger 定义任务的 Go 应用程序
func main() {
var err error
ctx := context.Background()
if len(os.Args) < 2 {
...)
}
client := getDaggerClient(ctx)
defer client.Close()
switch os.Args[1] {
case "build":
if len(os.Args) < 3 {
panic(...)
}
_, err = buildService(ctx, client, os.Args[2])
case "test":
err = testService(ctx, client, os.Args[2])
case "publish":
pv, err := buildService(ctx, client, os.Args[2])
err = publishService(ctx, client, os.Args[2], pv, os.Args[3])
case "all":
pv, err := buildService(ctx, client, os.Args[2])
err = testService(ctx, client, os.Args[2])
err = publishService(ctx, client, os.Args[2], pv, os.Args[3])
default:
log.Fatalln("invalid command specified")
}
你可以在 github.com/salaboy/platforms-on-k8s/blob/main/conference-application/service-pipeline.go 找到 service-pipeline.go 的定义。
通过运行 go run service-pipeline.go build notifications-service,Dagger 将使用容器构建我们的 Go 应用程序源代码,然后构建一个准备推送到容器注册库的容器。如果你查看列表 3.12 中的 buildService 函数,你会注意到它构建我们的服务源代码,在这种情况下,遍历目标平台列表(amd64 和 arm64)以为每个平台生成二进制文件。一旦生成了二进制文件,就使用 Dagger 客户端的 client.Container 函数创建一个容器。因为我们是以编程方式定义每个步骤,所以我们还可以定义后续构建需要缓存的文件(使用 client.CacheVolume)。
列表 3.12 任务:使用 Dagger 内置函数的 Go 代码
func buildService(ctx context.Context,
client *dagger.Client,
dir string) ([]*dagger.Container, error) {
srcDir := client.Host().Directory(dir)
platformVariants := make([]*dagger.Container, 0, len(platforms))
for _, platform := range platforms {
ctr := client.Container()
ctr = ctr.From("golang:1.20-alpine")
// mount in our source code
ctr = ctr.WithDirectory("/src", srcDir)
ctr = ctr.WithMountedCache("/go/pkg/mod", client.CacheVolume("go-mod"))
ctr = ctr.WithMountedCache("/root/.cache/go-build",
➥ client.CacheVolume("go-build"))
// mount in an empty dir to put the built binary
ctr = ctr.WithDirectory("/output", client.Directory())
// ensure the binary will be statically linked and thus executable
// in the final image
ctr = ctr.WithEnvVariable("CGO_ENABLED", "0")
// configure go to support different architectures
ctr = ctr.WithEnvVariable("GOOS", "linux")
ctr = ctr.WithEnvVariable("GOARCH", architecture(platform))
// build the binary and put the result at the mounted output directory
ctr = ctr.WithWorkdir("/src")
ctr = ctr.WithExec([]string{"go", "build","-o", "/output/app",".",})
// select the output directory
outputDir := ctr.Directory("/output")
// create a new container with the output and the platform label
binaryCtr := client.Container(dagger.ContainerOpts{Platform: platform}).
WithEntrypoint([]string{"./app"}).
WithRootfs(outputDir)
platformVariants = append(platformVariants, binaryCtr)
}
return platformVariants, nil
}
这些管道是用 Go 编写的,并构建 Go 应用程序,但没有任何阻止你构建其他语言并使用必要工具的限制。每个任务只是一个容器。Dagger 和开源社区将创建所有基本构建块,但每个组织都必须创建特定领域的库以与第三方或内部/遗留系统集成。通过专注于使开发者能够选择合适的工具来创建这些集成,Dagger 让你可以选择合适的工具来创建这些集成。无需编写插件,只需可以像其他库一样分发的代码即可。
尝试运行其中一个服务的管道,或者按照你可以在 github.com/salaboy/platforms-on-k8s/blob/main/chapter-3/dagger/README.md 找到的逐步教程进行操作。如果你运行管道两次,第二次运行几乎会立即完成,因为大多数步骤都是缓存的。
与 Tekton 相比,我们是在本地而不是在 Kubernetes 集群上运行 Dagger 管道,这有一些优势。例如,我们不需要 Kubernetes 集群来运行和测试这个管道,我们也不需要等待远程反馈。开发人员可以在将任何更改推送到 Git 存储库之前,使用本地容器运行时(如 Docker 或 Podman)运行这些管道,包括集成测试。快速的反馈使他们能够更快地工作。
但现在,这如何转化为远程环境?如果我们想在 Kubernetes 集群上远程运行这个管道怎么办?好消息是它的工作方式相同:它只是一个远程的 Dagger 管道引擎,将执行我们的管道。无论这个远程管道引擎在哪里,无论是运行在 Kubernetes 内部还是作为一个托管服务,我们的管道行为和管道引擎提供的缓存机制都将以相同的方式运行。图 3.12 显示了如果我们在 Kubernetes 内部安装 Dagger 管道引擎并运行相同的管道,执行将如何进行。

图 3.12 当配置与远程 Dagger Pipeline Engine 时,Dagger SDK 将收集并发送管道执行的上下文。
当 Dagger Pipeline Engine 安装在远程环境,如 Kubernetes 集群、虚拟机或任何其他计算资源中时,我们可以连接并运行我们的管道。Dagger Go SDK 从本地环境获取所有必要的上下文,并将其发送到 Dagger Pipeline Engine 以远程执行任务。我们不需要担心将应用程序源代码在线发布以供管道使用。
检查这个分步教程,了解如何在 Kubernetes 上运行你的 Dagger 管道:github.com/salaboy/platforms-on-k8s/blob/main/chapter-3/dagger/README.md#running-your-pipelines-remotely-on-kubernetes。
如你所见,Dagger 将使用持久存储(缓存)来缓存所有构建和任务以优化性能并减少管道运行时间。负责在 Kubernetes 内部部署和运行 Dagger 的操作团队需要根据组织运行的管道来跟踪所需的存储量。
在本节中,我们看到了如何使用 Dagger 创建我们的服务管道。我们了解到 Dagger 与 Tekton 非常不同:你不需要使用 YAML 编写你的管道,你可以使用任何支持的编程语言编写你的管道,你可以使用相同的代码在本地或远程运行你的管道,并且你可以使用为你的应用程序使用的相同工具来分发你的管道。
从 Kubernetes 的角度来看,当你使用像 Dagger 这样的工具时,你会失去像管理其他 Kubernetes 资源一样管理你的流水线的 Kubernetes 原生方法。如果他们得到足够的反馈和请求,我认为 Dagger 社区会向那个方向发展。
从平台工程的角度来看,你可以创建和分发复杂的流水线(和任务)供你的团队使用和扩展,他们可以使用他们已经知道的工具。这些流水线无论在哪里执行都会以相同的方式运行,这使得它成为一个极其灵活的解决方案。平台团队可以利用这种灵活性来决定在哪里更有效地运行这些流水线(基于成本和资源),而不会复杂化开发者的生活,因为他们将始终能够在开发目的上本地运行他们的流水线。
3.5.5 我应该使用 Tekton、Dagger 还是 GitHub Actions?
正如你所看到的,Tekton 和 Dagger 为我们提供了构建无特定观点流水线的基本构建块。换句话说,我们可以使用 Tekton 和 Dagger 来构建服务流水线和几乎所有可想象的流水线。使用 Tekton,我们使用基于 Kubernetes 资源的方法、可扩展性和自我修复功能。使用 Kubernetes 原生资源可以帮助将 Tekton 与其他 Kubernetes 工具(如管理和监控 Kubernetes 资源)集成。使用 Kubernetes 资源模型,你可以将你的 Tekton 流水线和 PipelineRuns 视为任何其他 Kubernetes 资源,并重用所有现有的工具。
使用 Dagger,我们可以使用众所周知的编程语言和工具来定义我们的流水线,并在任何地方运行这些流水线(在本地工作站上运行的方式与远程运行相同)。这使得 Tekton 和 Dagger 成为平台构建者可以用来构建更多具有特定观点的流水线的完美工具,这些流水线可以被开发团队使用。
另一方面,你可以使用像 GitHub Actions 这样的托管服务。你可以查看如何使用 GitHub Actions 配置这里提到的所有项目的服务流水线。例如,你可以检查通知服务的服务流水线,github.com/salaboy/platforms-on-k8s/blob/main/.github/workflows/notifications-service-service-pipelines.yaml。
这个 GitHub Action 流水线使用 ko-build 构建服务,然后将新的容器镜像推送到 Docker Hub。请注意,这个流水线没有运行任何测试,它使用一个自定义步骤(github.com/salaboy/platforms-on-k8s/blob/main/.github/workflows/notifications-service-service-pipelines.yaml#L17)来检查服务的代码是否已更改;只有当服务源代码有更改时,才运行构建并将镜像推送到 Docker Hub。
使用 GitHub Actions 的优势是,你不需要维护运行它们的底层基础设施,或者为运行这些管道的机器付费(如果你的量足够小)。但是,如果你正在运行大量的管道,并且这些管道是数据密集型的,GitHub Actions 将会变得昂贵。
由于成本相关的原因,或者由于行业法规限制你无法在云中运行管道,Tekton 和 Dagger 在为你提供构建和运行复杂管道的所有构建块方面表现出色。虽然 Dagger 已经专注于成本和运行时优化,但 Tekton 和其他管道引擎也将实现这一点。
需要注意的是,你可以将 Tekton 和 Dagger 与 GitHub 集成。例如,使用 Tekton Triggers (github.com/tektoncd/triggers/blob/main/docs/getting-started/README.md) 来响应 GitHub 仓库中的提交。你还可以在 GitHub Action 中运行 Dagger,使开发者能够运行在 GitHub Actions 中本地执行的相同管道,而这通常不容易实现。
现在我们已经准备好了要部署到多个环境中的工件和配置,让我们看看通常被称为通过环境管道进行持续部署的 GitOps 方法。
3.6 回到平台工程
作为你的平台倡议的一部分,你需要帮助团队以自动化的方式构建他们的服务。大多数时候,必须做出决定,以标准化跨团队构建和打包服务的方式。如果平台团队能够提供一个团队可以尝试本地使用或测试的解决方案,并在将更改推送到 Git 仓库之前拥有正确的环境,这将提高这些团队所需的移动速度和反馈循环,从而增强他们的信心。可能需要单独的设置来验证拉取请求,并在主分支不可发布时提醒团队。
虽然 GitHub Actions(以及其他托管服务)是一个流行的解决方案,但平台工程团队可能会根据他们的预算和其他平台级决策(例如与 Kubernetes API 保持一致)选择不同的工具或服务。
我为这本书的演示和分步教程(github.com/salaboy/platforms-on-k8s/tree/main/chapter-3)做了有意识的选择,这些选择可能与你的项目大相径庭。首先,因为本书中展示的项目复杂度相当低,而且为了保持资源组织化和版本控制以支持未来的修订,所有应用程序服务的源代码都被保存在一个简单的目录结构下。将所有服务的源代码集中存储在同一存储库中的这一决定影响了我们的服务管道的形状。
提供的服务管道(无论是使用 Tekton 还是 Dagger)都将用户想要构建的存储库目录作为参数。如果你设置了触发管道的 webhooks,你必须过滤更改的位置,以确定要运行哪个服务管道。这增加了整个设置的复杂性。如前几节所建议,一种替代方法是每个服务有一个存储库。这使你能够为每个服务拥有定制的服务管道定义(可以重用通用任务)和简单的 webhook 定义,因为你确切知道在更改时运行什么。拥有每个服务一个存储库的主要问题是处理用户和访问权限,因为添加新服务将迫使你创建新的存储库并确保开发者可以访问它。
平台团队还需要做出的另一个重大决策是关于服务管道的起点和终点。在本例中提供的示例中,服务管道从提交更改开始,在为每个服务发布容器镜像后结束。对于行走骨架服务的服务管道不会打包和发布单个服务的 Helm 图表。图 3.13 显示了由示例定义的服务管道的责任。

图 3.13 服务管道和应用管道有不同的生命周期。
你需要问自己,为每个服务创建 Helm 图表是一个好主意还是过度设计。你应该清楚地了解谁将消费这些工件。尝试回答以下问题,以找到适合你团队的战略:
-
你将单独部署服务,还是它们总是作为一个集合部署?
-
你的服务多久会变化一次?你是否有一些变化更频繁的服务?
-
将有多少团队部署这些服务?
-
你是否在创建一个开源社区将消费的工件,许多用户将单独部署服务?
对于本章提供的示例,提供了一个单独的应用级管道来打包和发布会议应用的 Helm 图表。
这个决策背后的原因很简单:每个读者都会在集群中安装应用程序,我需要一个简单的方法来实现这一点。如果读者不想在他们的集群中使用 Helm 安装应用程序,他们可以导出运行 helm template 命令的输出,并使用 kubectl 应用该输出。那个决策背后的另一个重要因素是 Helm 图表和应用程序服务的生命周期。应用程序的形状变化不大。Helm 图表定义可能只会改变,如果我们需要添加或删除服务。然而,服务的代码变化很大,我们希望让在这些服务上工作的团队能够继续向它们添加更改。
图 3.14 显示了服务管道的两种互补方法。在开发人员环境中运行的服务提供快速的反馈循环,而在远程运行的服务生成团队将用于在不同环境中部署相同应用程序的工件。

图 3.14 本地与远程服务管道
最后,本书中的所有示例都没有提供配置来从 Git 仓库中访问 webhooks,除了使用 GitHub Actions 链接的那些。推动读者获取正确的令牌并使用多个 Git 提供商配置这并不复杂,但解释它将占用我许多页面。消费这些机制的团队不需要担心处理服务管道所需的凭证。作为一个平台团队,自动化开发(和其他)团队访问凭证以连接到服务是加快他们工作流程的基本方法。
摘要
-
服务管道定义了如何从源代码到可以在多个环境中部署的工件的过程。遵循基于主干的开发和“一个服务 = 一个仓库”的实践有助于您的团队更高效地标准化构建和发布软件工件。
-
您需要找到适合您团队和应用程序的方法。没有一种适合所有情况的解决方案,必须做出妥协。您的应用程序的服务多久会发生变化,您又是如何将它们部署到环境中的?回答这些问题可以帮助您定义服务管道的起点和终点。
-
Tekton 是一个为 Kubernetes 设计的管道引擎。您可以使用 Tekton 设计您自己的管道,并使用在 Tekton 目录中公开可用的所有共享任务和管道。您现在可以在您的集群中安装 Tekton 并开始创建管道。
-
Dagger 允许您使用您喜欢的编程语言编写和分发管道。这些管道可以在任何环境中执行,包括您的开发人员的笔记本电脑。
-
像 GitHub Actions 这样的工具非常有用,但可能很昂贵。平台构建者必须寻找提供足够灵活性的工具,以构建和分发其他团队可以重用并遵循公司指南的任务。允许团队在本地运行他们的管道是一个很大的加分项,因为它将改善他们的开发体验并缩短他们的反馈时间。
-
如果您遵循了逐步教程,您就获得了使用 Tekton 和 Dagger 创建和运行服务管道的实践经验。
4 环境管道:部署云原生应用程序
本章涵盖
-
将生成的工件部署到环境中
-
使用环境管道和 GitOps 来管理环境
-
使用 Helm 与 Argo CD 高效交付软件
本章介绍了环境管道的概念。我们涵盖了将服务管道创建的工件部署到具体运行环境直至生产的步骤。我们将探讨在云原生领域出现的一种常见做法,称为 GitOps,它允许我们使用 Git 存储库来定义和配置我们的环境。最后,我们将探讨一个名为 Argo CD 的项目,它实现了在 Kubernetes 上管理应用程序的 GitOps 方法。本章分为三个主要部分:
-
环境管道
-
使用 Argo CD 实现的环境管道
-
服务+环境管道协同工作
4.1 环境管道
我们可以构建尽可能多的服务并产生新版本,但如果这些版本不能自由地跨越不同的环境进行测试,最终被我们的客户使用,我们的组织将难以拥有顺畅的端到端软件交付实践。环境管道负责配置和维护我们的环境。
对于公司来说,根据不同的目的拥有不同的环境是很常见的,例如,一个预发布环境,开发者可以部署他们服务的最新版本;一个质量保证(QA)环境,在这里进行手动测试;以及一个或多个生产环境,这是真实用户与我们的应用程序交互的地方。这些(预发布、QA 和生产)只是例子。我们拥有的环境数量不应该有任何硬性限制。图 4.1 展示了单个发布版本如何在不同的环境中流动,直到达到生产环境,在那里它将面向我们的应用程序用户公开。

图 4.1 发布的服务在不同环境中的流动
每个环境(开发、预发布、QA 和生产)将有一个环境管道。这些管道将负责保持环境配置与运行环境硬件同步。这些环境管道使用包含环境配置的存储库作为真相来源,包括需要部署哪些服务和每个服务的哪个版本(图 4.2)。

图 4.2 将服务推广到不同环境意味着更新环境配置
如果你使用这种方法,每个环境都将有自己的配置存储库。推广新发布的版本意味着更改环境配置存储库以添加新服务或更新配置以指向新发布的版本。一些组织将所有敏感环境配置都保存在单个存储库中;这有助于集中管理读取和修改这些配置所需的凭证。
这些配置更改可以是自动化的,或者需要手动干预。对于更敏感的环境,例如生产环境,你可能需要在添加或更新服务之前要求不同的利益相关者签字。
但环境管道是从哪里来的?为什么你之前没有听说过它们?在深入探讨环境管道可能看起来像什么之前,我们需要了解为什么这从一开始就很重要。
4.1.1 过去这是如何工作的,最近又发生了什么变化?
传统上,创建新的环境既困难又昂贵。由于这两个原因,按需创建新环境并不是一件事情。首先,开发人员用来创建应用程序的环境和应用程序为最终用户运行的环境之间的差异完全不同。这些差异,不仅在计算能力上,给负责运行这些应用程序的运维团队带来了巨大的压力。根据环境的能力,他们需要调整应用程序的配置(他们没有设计)。其次,自动化复杂设置配置的工具已经变得主流。借助容器和 Kubernetes,这些工具的设计和工作方式在云提供商之间实现了标准化。这些工具已经达到了开发者可以使用他们选择的编程语言来编码基础设施,或者依赖 Kubernetes API 来创建这些定义的程度。
在云原生应用兴起之前,部署一个新的应用程序或应用程序的新版本需要关闭服务器,运行一些脚本,复制一些二进制文件,然后再次启动服务器,使新版本运行。服务器再次启动后,应用程序可能会失败启动。因此可能需要更多的配置调整。大多数这些配置都是在服务器本身手动完成的,这使得很难记住并跟踪更改了什么以及为什么。
作为自动化这些流程的一部分,像 Jenkins (www.jenkins.io/, 一个非常流行的管道引擎) 和/或脚本这样的工具被用来简化新二进制的部署。因此,而不是手动停止服务器并复制二进制文件,操作员可以运行一个 Jenkins 作业,定义他们想要部署的工件版本,Jenkins 将运行作业并通知操作员关于输出的信息。这种方法有两个主要优点:
-
像 Jenkins 这样的工具可以访问环境的凭证,避免操作员手动访问服务器。
-
像 Jenkins 这样的工具会记录每次作业执行和参数,使我们能够跟踪执行了什么以及执行结果。
与手动部署新版本相比,虽然使用像 Jenkins 这样的工具进行自动化是一个很大的改进,但仍然存在一些问题,例如具有固定环境的环境与软件开发和测试的地方完全不同。我们需要指定环境是如何创建和配置的,包括操作系统的版本和安装到机器或虚拟机中的软件,以减少不同环境之间的差异。虚拟机在完成这项任务时非常有帮助,因为我们可以轻松地创建两个或更多配置相似的虚拟机。
我们甚至可以将这些虚拟机提供给我们的开发者使用。但现在我们遇到了一个新的问题。我们需要新的工具来管理、运行、维护和存储我们的虚拟机。如果我们有多个物理机想要在上面运行虚拟机,我们不希望我们的运维团队在每个服务器上手动启动这些虚拟机。因此,我们需要一个虚拟化软件来监控和运行物理计算机集群中的虚拟机。
使用像 Jenkins 和虚拟机(带有虚拟化软件)这样的工具是一个巨大的改进。因为我们实现了一些自动化,操作员不需要访问服务器或虚拟机来手动更改配置,并且我们的环境是通过在固定的虚拟机配置中预定义的配置创建的。像 Ansible (www.ansible.com/) 和 Puppet (www.puppet.com/) 这样的工具就是建立在这些概念之上的。
图 4.3 显示了配置为创建托管我们应用程序的虚拟机的 Jenkins 作业。但请注意,这些虚拟机托管了一个完整的操作系统。该操作系统捆绑的所有工具都将与你的应用程序一起运行!

图 4.3 中的 Jenkins 作业或脚本以命令式的方式封装了如何进行部署的操作知识,定义了需要按步骤完成的操作。这是一个复杂且难以维护和修改的任务,并且非常特定于我们使用的工具。另一方面,虚拟机资源密集且不可跨云提供商迁移。
虽然这种方法在业界仍然很常见,但还有很多改进的空间,例如以下方面:
-
Jenkins 作业和脚本本质上是命令式的,这意味着它们指定了需要按步骤执行的操作。这有一个很大的缺点,因为如果有什么变化——比如说服务器不再存在或者需要更多数据来验证服务——管道的逻辑将失败,并且需要手动更新。
-
虚拟机很重。每次你启动一个虚拟机时,你都在启动一个操作系统的完整实例。运行操作系统进程不会增加任何业务价值;集群越大,操作系统开销就越大。在开发者的环境中运行虚拟机可能是不可能的。
-
环境的配置是隐藏的,并且没有版本控制。大多数环境配置以及部署是如何进行的都被编码在像 Jenkins 这样的工具中,其中复杂的管道往往会失去控制,使得更改非常危险,迁移到新的工具和堆栈也非常困难。
-
每个云服务提供商都有创建虚拟机的不标准方式。这可能会使我们陷入供应商锁定的情况。如果我们为亚马逊网络服务创建了虚拟机,我们就无法将这些虚拟机运行在谷歌云平台或微软 Azure 上。
团队是如何使用现代工具来处理这个问题的?这是一个简单的问题。我们现在有 Kubernetes 和容器,它们旨在通过依赖容器和广泛采用的 Kubernetes API 来解决由虚拟机和云提供商的可移植性带来的开销。Kubernetes 还提供了构建块,确保我们不需要关闭服务器来部署新应用程序或更改它们的配置。如果我们按照 Kubernetes 的方式行事,我们的应用程序不应该有任何停机时间。
但仅 Kubernetes 本身并不能解决配置集群本身的过程。我们如何应用更改到它们的配置,或者部署应用程序到这些集群涉及的过程和工具,也同样重要。这就是为什么你可能听说过 GitOps。
什么是 GitOps,它与我们的环境管道有何关联?我们将在下一节回答这个问题。
4.1.2 什么是 GitOps,它与环境管道有何关联?
如果我们不希望在像 Jenkins 这样的工具中编码所有的操作知识,那里很难维护、更改和跟踪它,我们需要不同的方法。
GitOps 这个术语是由 CNCF 的 GitOps 工作组定义的(opengitops.dev/),它定义了使用 Git 作为真相来源,以声明性方式创建、维护和应用我们环境和应用程序配置的过程。OpenGitOps 定义了我们在谈论 GitOps 时需要考虑的四个核心原则:
-
声明式: 由 GitOps 管理的系统(
github.com/open-gitops/documents/blob/v1.0.0/GLOSSARY.md#software-system)必须以声明式(github.com/open-gitops/documents/blob/v1.0.0/GLOSSARY.md#declarative-description)表达其所需状态。如果我们使用 Kubernetes 清单,我们就有了这个保障,因为我们使用 Kubernetes 将进行协调的声明性资源来定义需要部署的内容以及如何配置。 -
版本化和不可变: 所需状态以强制执行不可变性和版本化并保留完整版本历史记录的方式存储(
github.com/open-gitops/documents/blob/v1.0.0/GLOSSARY.md#state-store)。OpenGitOps 倡议不强制使用 Git。一旦我们的定义被存储、版本化和不可变,我们就可以将其视为 GitOps。这为将文件存储在例如 S3 存储桶中打开了大门,这些存储桶也是版本化和不可变的。 -
自动拉取: 软件代理自动从源中拉取所需状态声明。GitOps 软件以自动化的方式定期从源中拉取更改。用户无需担心何时拉取更改。
-
持续协调: 软件代理持续(
github.com/open-gitops/documents/blob/v1.0.0/GLOSSARY.md#continuous)观察系统状态,并尝试应用(github.com/open-gitops/documents/blob/v1.0.0/GLOSSARY.md#reconciliation)所需状态。这种持续的协调有助于我们在环境和整个交付过程中建立弹性,因为我们有负责应用所需状态并监控环境配置漂移的组件。如果协调失败,GitOps 工具将通知我们问题,并持续尝试应用更改,直到达到所需状态。
通过将环境和应用程序的配置存储在 Git 仓库中,我们可以跟踪和版本化我们所做的更改。通过依赖 Git,如果这些更改不符合预期,我们可以轻松地回滚更改。GitOps 涵盖了配置存储以及这些配置如何应用到应用程序运行的计算资源中。
GitOps 是在 Kubernetes 的背景下提出的,但这种方法并不新颖,因为配置管理工具已经存在很长时间了。相反,GitOps 代表了这些经过验证的方法的改进,这些方法可以应用于任何软件操作,而不仅仅是 Kubernetes。随着云提供商管理基础设施即代码的工具的普及,像 Chef (www.chef.io/)、Ansible (www.ansible.com/)、Terraform (www.terraform.io/)和 Pulumi (www.pulumi.com/)这样的工具受到运维团队的喜爱,因为这些工具允许他们定义如何配置云资源,并以可重复的方式一起配置它们。如果您需要一个新的环境,只需运行这个 Terraform 脚本或 Pulumi 应用程序,然后,环境就绪并运行。这些工具还配备了与云提供商的 API 通信的能力,以便我们可以自动化这些集群的创建。
使用 GitOps,我们管理配置并依赖于 Kubernetes API 作为将我们的应用程序部署到 Kubernetes 集群的标准方式。使用 GitOps,我们将 Git 仓库作为我们环境内部配置(Kubernetes YAML 文件)的真相来源,同时消除了手动与 Kubernetes 集群交互的需求,以避免配置漂移和安全问题。当使用 GitOps 工具时,我们可以期待有软件代理定期从真相来源(本例中的 Git 仓库)拉取,并持续监控环境以提供连续的协调循环。这确保 GitOps 工具将尽最大努力确保仓库中表达的期望状态与我们的实际环境相符。
我们可以通过运行环境管道重新配置任何 Kubernetes 集群,使其具有存储在我们 Git 仓库中的相同配置。图 4.4 展示了这些组件是如何组合在一起的。在左侧,我们有可以创建云资源(包括 Kubernetes 集群和我们的环境的应用程序基础设施)的基础设施即代码工具。一旦环境设置完成,使用 GitOps 方法的环境管道可以将我们环境的所有配置同步到目标 Kubernetes 集群,并定期检查 Git 中存储的配置是否与集群同步。

图 4.4 基础设施即代码、GitOps 和环境管道协同工作。基础设施即代码工具通过运行脚本以可重复的方式创建云资源。我们可以使用这些工具创建出所有相同的 Kubernetes 集群。GitOps 工具运行环境管道以持续地协调声明性配置,这些配置存储在版本化和不可变的仓库中。
通过分离基础设施和应用关注点,我们的环境管道使我们能够确保我们的环境易于复制和更新,无论何时需要。通过依赖 Git 作为真相的来源,我们可以根据需要回滚我们的基础设施和应用更改。重要的是要理解,因为我们正在使用 Kubernetes API,我们的环境定义现在以声明式的方式表达,支持在应用这些配置的上下文中进行更改,并让 Kubernetes 处理如何实现这些配置所表达的状态。
图 4.5 展示了这些交互,其中操作团队仅更改包含我们环境配置的 Git 仓库,然后执行一个管道(一系列步骤)来确保此配置与目标环境保持同步。

图 4.5 使用 Git 中的配置定义集群状态(GitOps)。环境管道监控 Git 仓库中的配置更改,并在检测到新更改时将这些更改应用到基础设施(Kubernetes 集群)中。遵循这种方法允许我们通过在 Git 上回滚提交来撤销基础设施中的更改。我们还可以通过在另一个集群上运行相同的管道来复制确切的环境配置。
当您开始使用环境管道时,目标是停止手动交互、更改或修改环境的配置,所有交互都仅通过这些管道进行。为了给出一个非常具体的例子,我们不是直接在我们的 Kubernetes 集群中执行 kubectl apply -f 或 helm install,而是由一个操作员负责根据包含集群中需要安装的定义和配置的 Git 仓库的内容来运行这些命令。
理论上,一个监控 Git 仓库并对更改做出反应的操作员就是您所需要的,但在实践中,需要一系列步骤来确保我们对部署到我们环境中的内容有完全的控制。因此,将 GitOps 视为一个管道有助于我们理解,对于某些场景,我们可能需要在每次环境配置更改时触发的这些管道中添加额外的步骤。
让我们用更具体的工具来看这些步骤,这些工具在现实场景中很常见。
4.1.3 环境管道中涉及到的步骤
无论您将何种应用程序部署到不同的环境中,环境管道通常包含一系列预定义的步骤。图 4.6 展示了这些步骤作为一个序列,因为大多数情况下,这些步骤是在脚本内部定义的,或者编码在负责检查每个步骤是否正确执行的工具中。让我们更深入地探讨这些步骤的细节:
-
响应配置变化:这可以通过轮询或推送来完成:
-
对更改进行投票: 组件可以拉取仓库并检查自上次检查以来是否有新的提交。如果检测到新更改,则会创建一个新的环境管道实例。
-
使用 webhooks 推送更改: 如果仓库支持 webhooks,仓库可以通知我们的环境管道有新的更改需要同步。记住,GitOps 原则声明“自动拉取”,这意味着我们可以使用 webhooks,但不应该完全依赖它们来获取配置更改更新。
-

图 4.6 Kubernetes 环境的环境管道
-
从包含我们环境所需状态的仓库中克隆源代码: 此步骤从包含环境配置的远程 Git 仓库中获取配置。像 Git 这样的工具仅获取远程仓库与我们本地拥有的内容之间的差异。
-
将所需状态应用到实际环境中: 这通常包括执行
kubectl apply -f或helm install命令来安装新版本的工件。请注意,无论是使用kubectl还是helm,Kubernetes 都足够智能,能够识别更改的位置,并且只应用差异。一旦管道在本地拥有所有配置,它将使用一组凭证将这些更改应用到 Kubernetes 集群。请注意,我们可以微调管道对集群的访问权限,以确保它们不会被从安全角度滥用。这也允许您从部署服务的集群中移除个别团队成员的访问权限。 -
验证更改是否已应用,并且状态与 Git 仓库内描述的一致(处理配置漂移**): 一旦更改应用到生产集群,需要检查新版本的服务是否正常运行,以确定是否需要回滚到之前的版本。如果需要回滚更改,由于所有历史记录都存储在 Git 中,所以操作非常简单。应用上一个版本只需查看仓库中的上一个提交。
-
验证您的应用程序按预期工作: 一旦配置正确应用,我们需要验证部署的应用程序是否按预期工作,并且正在执行它们应该执行的操作。
为了使环境管道工作,需要一个能够将更改应用到环境的组件,并且需要根据正确的访问凭证进行相应配置。这个组件背后的主要思想是确保没有人会通过手动与集群交互来更改环境配置。这个组件是唯一允许更改环境配置、部署新服务、升级版本或从环境中删除服务的组件。为了使环境管道工作,需要满足以下两个条件:
-
存储环境所需状态的仓库必须包含所有必要的配置,以确保环境能够成功创建和配置。
-
环境将运行的 Kubernetes 集群需要配置正确的凭证,以便管道可以更改状态。
“环境管道”这个术语指的是每个环境都将有一个与之关联的管道。由于通常需要多个环境(开发、测试、生产)来交付应用程序,因此每个环境都将有一个负责部署和升级其中运行的组件的管道。通过使用这种方法,通过向环境的仓库发送拉取请求/更改请求来实现不同环境之间服务的提升。管道将在目标集群中反映这些更改。
4.1.4 环境管道的要求和不同方法
那么,这些环境仓库的内容是什么呢?如图 4.7 所示,环境仓库的内容仅仅是定义了哪些服务需要存在于环境中。然后,环境管道就可以将这些 Kubernetes 清单应用到目标集群中。

图 4.7 环境配置选项
第一个选项(简单布局)是将所有 Kubernetes YAML 文件存储在 Git 仓库中,然后环境管道将直接对配置的集群使用 kubectl apply -f *。虽然这种方法很简单,但有一个很大的缺点:如果你在服务仓库中有每个服务的 Kubernetes YAML 文件,那么环境仓库将会有这些文件的重复,并且它们可能会不同步。想象一下,如果你有多个环境,你必须维护所有副本的同步,这可能会变得具有挑战性。
第二个选项(使用 Helm 图表)现在使用 Helm 定义集群状态后,变得更加复杂。你可以使用 Helm 依赖关系创建一个父图表,它将包括所有应该存在于环境中的服务作为依赖项。如果你这样做,环境管道可以使用 helm update . 将图表应用到集群中。我不喜欢这种方法的一点是,你为每次更改创建一个 Helm 发布,而且没有为每个服务创建单独的发布。这种方法使用 Helm 依赖关系来获取每个服务定义,因此这种方法的一个先决条件是每个服务包都必须是一个 Helm 图表。
第三个选择是使用一个名为helmfile的项目(github.com/helmfile/helmfile),专为这个特定目的设计,用于定义环境配置。helmfile允许你声明性地定义集群中需要存在的 Helm 发布。当我们运行helmfile sync并定义了一个包含我们希望在集群中存在的 Helm 发布的helmfile时,这些 Helm 发布将被创建。
无论你使用这些方法中的任何一种还是其他工具来完成这项任务,期望都是明确的。你有一个包含配置的仓库(每个环境一个仓库或每个环境一个目录),一个管道负责获取配置并使用工具将其应用到集群中。
通常会有几个环境(预发布、QA、生产),甚至允许团队创建按需环境来运行测试或日常开发任务。如果你使用图 4.8 所示的“每个命名空间一个环境”的方法,通常为每个环境有一个单独的 Git 仓库,因为这有助于保持对环境的访问隔离和安全。这种方法很简单,但它在 Kubernetes 集群上提供的隔离性不足,因为 Kubernetes 命名空间是为了集群的逻辑分区而设计的。在这种情况下,预发布环境将与生产环境共享集群资源。

图 4.8 展示了每个 Kubernetes 命名空间一个环境的方法。一种策略是使用不同的命名空间来区分不同的环境。虽然这样做简化了将服务部署到不同环境所需的配置,但命名空间并不提供强大的隔离保证。
另一种方法是为每个环境使用一个全新的集群。主要区别在于隔离和访问控制。通过为每个环境拥有一个集群,你可以更严格地定义谁和哪些组件可以部署和升级这些环境中的内容,并为每个集群配置不同的硬件配置,例如多区域设置和其他可能在预发布和测试环境中没有意义的可扩展性关注点。使用不同的集群,你还可以追求多云设置,其中不同的云提供商可以托管不同的环境。
图 4.9 展示了如何使用命名空间方法为开发环境,这些环境将由不同的团队创建,然后分别有用于预发布和生产的不同集群。这里的想法是将预发布和生产的集群配置得尽可能相似,以便部署到不同环境中的应用程序表现一致。

图 4.9 不同环境配置,基于需求。更现实的方法可以使用相同的集群为多个团队进行日常工作,而更敏感的环境,如预发布和生产,则在自己的集群和 Git 仓库中分离,以存储它们的配置。要将服务提升到新环境,需要向相应的 Git 仓库提交一个拉取请求。
好的,但我们如何实现这些管道?我们应该使用 Tekton 来实现这些管道吗?在下一节中,我们将探讨 Argo CD (argo-cd.readthedocs.io/en/stable/),这是一个将环境管道逻辑和最佳实践编码到非常具体的持续部署工具中的工具。
4.2 环境管道的实际应用
你可以使用 Tekton 或 Dagger 实现如前所述的环境管道。这在像 Jenkins X (jenkins-x.io)这样的项目中已经实现,但如今,环境管道的步骤被编码在像 Argo CD (argo-cd.readthedocs.io/en/stable/)这样的持续部署专用工具中。
与服务管道不同,我们可能需要根据所使用的特定技术栈使用专门的工具来构建我们的工件,Kubernetes 的环境管道在 GitOps 的框架下已经得到了很好的标准化。考虑到所有我们的工件都是由我们的服务管道构建和发布的,我们首先需要创建我们的环境 Git 仓库,它将包含环境的配置,包括部署到该环境的服务。
Argo CD 提供了一个非常具有意见但非常灵活的 GitOps 实现。我们将把将软件部署到我们的环境中所需的全部步骤委托给 Argo CD。Argo CD 可以开箱即用地监控包含我们的环境(们)配置的 Git 仓库,并定期将配置应用到实时集群。这使得我们能够减少与目标集群的手动交互,因为 Git 成为了我们的真相来源。
使用像 Argo CD 这样的工具允许我们声明性地定义我们想在环境中安装的内容,而 Argo CD 则负责在出现问题时或我们的集群不同步时通知我们。Argo CD 不仅限于单个集群,这意味着我们的环境可以存在于不同的集群中,甚至在不同的云服务提供商中。图 4.10 显示了 Argo CD 在不同的集群上管理不同的环境,使用不同的 Git 仓库作为真相来源来保持每个环境的配置。

图 4.10 Argo CD 将同步环境,从 Git 到实时集群的配置
就像我们现在为每个服务都有单独的服务管道一样,我们也可以为我们的环境配置拥有单独的仓库、分支或目录。Argo CD 可以监控仓库或仓库内部的目录,以同步我们的环境配置。
在这个例子中,我们将安装 Argo CD 到我们的 Kubernetes 集群中,并使用 GitOps 方法配置我们的预发布环境。为此,我们需要一个 Git 仓库作为我们的真相来源。你可以遵循位于 github.com/salaboy/platforms-on-k8s/blob/main/chapter-4/README.md 的分步教程。
对于安装 Argo CD,我建议你查看他们的入门指南,你可以在 argo-cd.readthedocs.io/en/stable/getting_started/ 找到。此指南安装了 Argo CD 运作所需的所有组件,因此完成此指南后,我们应该拥有启动我们的预发布环境所需的一切。它还指导你安装 argocd CLI(命令行界面),这在某些情况下非常有用。在接下来的章节中,我们将关注用户界面,但你也可以使用 CLI 访问相同的功能。Argo CD 提供了一个非常有用的用户界面,让你可以监控你的环境和应用程序的表现,并快速找出是否存在任何问题。
本节的主要目标是复制我们在第二章第 2.1.3 节中做的事情,在那里我们安装并交互了应用程序,但在这里我们旨在完全自动化使用 git 仓库配置的环境的过程。再次,我们将使用 Helm 来定义环境配置,因为 Argo CD 提供了开箱即用的 Helm 集成。
注意:Argo CD 使用了与这里不同的命名约定。在 Argo CD 中,你配置应用程序而不是环境。在下面的屏幕截图中,你会看到我们将配置一个 Argo CD 应用程序来表示我们的预发布环境。由于 Helm 图表中没有包含内容的限制,我们将使用 Helm 图表来配置我们的会议应用程序到这个环境中。
4.2.1 创建 Argo CD 应用程序
如果你访问 Argo CD 用户界面,你会在屏幕的左上角看到 + 新应用按钮(图 4.11)。

图 4.11 Argo CD 用户界面—创建新应用程序
按下那个按钮,看看应用程序创建表单。除了添加一个名称并选择我们的 Argo CD 应用程序将驻留的项目(我们将选择 default 项目)外,我们还将检查 自动创建命名空间 选项,如图 4.12 所示。

图 4.12 新的应用参数、手动同步和自动创建命名空间
通过将我们的环境与集群中的新命名空间关联起来,我们只能使用 Kubernetes RBAC 机制来允许管理员修改该命名空间中的 Kubernetes 资源。记住,通过使用 Argo CD,我们希望确保开发者不会意外更改应用程序配置或手动将配置更改应用到集群中。Argo CD 将同步 Git 仓库中定义的资源。那么那个 Git 仓库在哪里?这正是我们需要配置的下一个步骤(图 4.13)。

图 4.13 Argo CD 应用的配置仓库、修订版本和路径
如前所述,我们将在github.com/salaboy/platforms-on-k8s/仓库内部的一个目录中定义我们的预发布环境。你应该复制这个仓库(然后使用你的复制 URL)来对环境配置进行任何你想要的更改。包含环境配置的目录可以在 chapter-4/argo-cd/staging/下找到。如图 4.14 所示,你还可以在不同的分支和标签之间进行选择,这允许你对配置的来源和配置如何演变有更精细的控制。

图 4.14 配置目标,在本例中,是安装了 Argo CD 的集群
下一步是定义 Argo CD 将应用此环境配置的位置。我们可以使用 Argo CD 在不同的集群中安装和同步环境,但在这个例子中,我们将使用我们安装 Argo CD 和staging命名空间的同一个 Kubernetes 集群。Argo CD 有一个选项可以为你创建这个命名空间,或者你可以在设置集群和不同命名空间的权限时手动创建它。
最后,由于在类似环境中重用相同的配置是有意义的,Argo CD 使我们能够配置特定于此安装的不同参数。由于我们使用 Helm,并且 Argo CD 用户界面足够智能,可以扫描我们输入的仓库/路径的内容,因此它知道它正在处理 Helm Chart。如果我们没有使用 Helm Chart,Argo CD 允许我们为配置脚本设置环境变量作为参数(图 4.15)。

图 4.15 预发布环境的 Helm 配置参数
如前图所示,Argo CD 还识别出我们在提供的仓库路径内部的一个空的 values.yaml 文件。如果 values.yaml 文件有任何参数,用户界面将解析它们并显示给你进行验证。我们可以在VALUES文本框中添加更多参数来覆盖任何其他图表(或子图表)的配置。
在我们提供所有这些配置后,我们就可以点击表单顶部的创建按钮。Argo CD 将创建应用程序并自动同步更改,因为我们选择了自动同步选项(图 4.16)。

图 4.16 创建的应用程序和自动同步
如果你点击进入应用程序,你将深入到应用程序的完整视图,该视图显示了与应用程序关联的所有资源的状态,如图 4.17 所示。

图 4.17 我们的中转环境运行正常,所有服务都在运行。
如果你在一个本地集群或真实的 Kubernetes 集群中创建环境,你应该访问应用程序并与它交互。让我们回顾一下我们已经取得的成果:
-
我们已经将 Argo CD 安装到我们的 Kubernetes 集群中。使用提供的 Argo CD 仪表板(用户界面),我们为我们的中转环境创建了一个新的 Argo CD 应用程序。
-
我们在 GitHub 上托管的 Git 仓库中创建了我们的中转环境配置,该配置使用 Helm Chart 定义来配置我们的会议应用程序服务及其依赖项(Redis、PostgreSQL 和 Kafka)。
-
我们已经将配置同步到了与安装 Argo CD 的同一集群中的命名空间(
staging)。 -
最重要的是,我们已经消除了与目标集群手动交互的需求。理论上,将不再需要针对
staging命名空间执行kubectl命令。
为了使此设置生效,我们需要确保 Helm Charts(以及它们内部的 Kubernetes 资源)中的工件对目标集群可用以便拉取。我强烈建议你遵循逐步教程(github.com/salaboy/platforms-on-k8s/tree/main/chapter-4),以亲身体验 Argo CD,了解这个工具的工作原理以及它如何帮助你的团队将应用程序持续部署到多个环境中。
4.2.2 以 GitOps 方式处理更改
假设现在负责开发用户界面(frontend)的团队决定引入一个新功能。他们向frontend仓库提交了一个 pull request。一旦这个 pull request 与main分支合并,团队就可以决定为服务创建一个新的版本。发布过程应包括使用发布号创建标记的工件。这些工件创建的责任属于服务管道,正如我们在前面的章节中看到的。图 4.18 显示了 Argo CD 在这种情况下如何同步从中转配置仓库的配置更改。

图 4.18 使用 Argo CD 设置中转环境的组件
一旦我们有了发布的工件,我们现在可以更新环境。我们可以通过向我们的 GitHub 仓库提交拉取请求来更新预发布环境,该请求在合并到主分支之前可以进行审查,主分支是我们用于配置 Argo CD 应用程序的分支。环境配置存储库中的更改通常包括:
-
提升或回滚服务版本: 对于我们的示例,这就像更改一个或多个服务的图表版本一样简单。将其中一个服务回滚到上一个版本就像在环境图表中回退版本号,甚至回退最初增加版本号的提交。请注意,回退提交始终是推荐的,因为回滚到上一个版本可能还包括对服务的配置更改,如果这些更改未应用,旧版本可能无法工作。
-
添加或删除服务: 添加新服务稍微复杂一些,因为你需要添加图表引用和服务配置参数。为了使其工作,图表定义需要可通过 Argo CD 安装访问。假设服务(的)图表可用,配置参数有效。在这种情况下,下一次我们同步 Argo CD 应用程序时,新的服务(们)将被部署到环境中。删除服务更为直接,因为一旦你从环境 Helm 图表中删除依赖项,服务将从环境中删除。
-
调整图表参数: 有时,我们不想更改任何服务版本,我们可能正在尝试微调应用程序参数以适应性能或可扩展性要求、监控配置或一组服务的日志级别。这些更改也是版本化的,应被视为新功能和错误修复。
如果我们将此与手动安装 Helm 将应用程序安装到集群中进行比较,我们会很快注意到差异。首先,开发者可能在自己的笔记本电脑上拥有环境配置,这使得环境很难从不同位置复制。未使用版本控制系统跟踪的环境配置更改将会丢失,我们将无法验证这些更改是否在实时集群中工作。配置漂移的跟踪和故障排除要困难得多。
使用 Argo CD 的这种自动化方法可以为更高级的场景打开大门。例如,我们可以为我们的拉取请求创建预览环境(图 4.19),以便在合并和发布工件之前测试更改。

图 4.19 预览环境以加快迭代
使用预览环境可以帮助更快地迭代,并使团队能在合并到项目的主要分支之前验证更改。当拉取请求合并时,预览环境也可以收到通知,这使得实现自动清理机制变得简单。
注意:在使用 Argo CD 和 Helm 时,还有一个重要的细节需要提及,那就是与手动使用 Helm Charts 相比,每次我们在集群中更新图表时,Helm 都会创建发布资源,而 Argo CD 不会使用这个 Helm 功能。Argo CD 采用使用 Helm 模板来渲染 Kubernetes 资源 YAML 的方法,然后使用kubectl apply应用输出。这种方法依赖于 Git 中的一切都是版本化的,并允许统一不同的 YAML 模板引擎。除了某些安全优势外,这是在 Argo CD 中启用 diff 功能的关键,它允许我们指定哪些资源应由 Argo CD 管理,哪些元素可能由不同的控制器管理。
最后,为了使事情更加连贯,让我们看看服务管道和环境管道是如何交互以提供端到端自动化,从代码更改到将新版本部署到多个环境。
4.3 服务+环境管道
让我们看看服务管道和环境管道是如何连接的。这两个管道之间的连接是通过 Git 仓库的拉取/更改请求来实现的,因为当提交和合并更改时,管道将被触发(图 4.20)。

图 4.20 一个服务管道可以通过拉取请求触发环境管道。
开发者完成一个新功能后,会向仓库的主要分支创建一个拉取/更改请求。这个拉取/更改请求可以被专门的服务管道审查和构建。当这个新功能合并到仓库的主要分支时,会触发一个新的服务管道实例。这个实例创建一个新的发布,以及部署服务新版本到 Kubernetes 集群所需的所有工件。正如我们在第三章中看到的,这包括一个包含编译源代码的二进制文件、一个容器镜像以及可以使用 Helm 等工具打包的 Kubernetes Manifests。
作为服务管道的最后一步,你可以包含一个通知步骤,该步骤可以通知感兴趣的运行环境,它们正在运行的服务有新版本可用。这种通知通常是一个自动的拉取/更改请求到环境的仓库。或者,你可以监控(或订阅通知)你的工件仓库,当检测到新版本时,就会创建一个拉取/更改请求到配置的环境。
为环境仓库创建的拉取/变更请求可以由专门的环境管道自动测试。与我们对服务管道所做的方式相同,对于低风险环境,这些拉取/变更请求可以自动合并,无需任何人工干预。
通过实施此流程,我们可以让开发者专注于修复错误和创建新功能,这些功能将自动发布并推广到低风险环境。一旦新版本在预发布等环境中经过测试,并且我们知道这些新版本或配置没有引起任何问题,就可以为包含生产环境配置的仓库创建拉取/变更请求。
环境越敏感,所需的检查和验证就越多。在这种情况下,如图 4.21 所示,要将新服务版本推送到生产环境,需要创建一个新的测试环境来验证和测试在提交的拉取/变更请求中引入的更改。一旦完成这些验证,就需要手动签核以合并拉取请求并触发环境管道同步。

图 4.21 推送到生产环境的更改
环境管道是您用来编码组织需求以将软件发布和推广到不同环境的机制。在本章中,我们已经看到了像 Argo CD 这样的工具能为我们做什么。接下来,我们需要评估单个 Argo CD 安装是否足够,以及谁将负责管理和保持其安全性。您是否需要通过自定义钩点扩展 Argo CD?您是否需要将其与其他工具集成?我们将在第六章探讨这些问题,因此在结束本章之前,让我们看看环境管道和像 Argo CD 这样的工具如何融入平台工程的故事。
4.4 回到平台工程
从平台工程的角度来看,为团队提供 GitOps 方法正变得越来越流行,以便配置不同的环境。随着像 Argo CD 这样的工具的普及,越来越多的人对在版本控制系统(如 Git)上存储和操作环境配置感到舒适。作为平台工程团队,您可以使团队能够使用这种方法,而无需强迫他们学习如何安装、维护和配置这些工具。
平台可以自动化创建环境仓库,并确保正确的团队有权读取和写入配置以推广服务。这些平台的消费者预计知道如何与其环境交互,但不知道平台提供的工具是如何工作或如何配置的。例如,在开发环境中,使用 GitOps 方法可能不起作用,因为某些开发团队可能希望直接访问集群,而您的平台应该足够灵活,以便在需要时允许这种访问。
如第 4.3 节所述,服务和环境管道协同工作,生成软件工件并在环境之间移动。服务和环境管道是实现所谓的黄金路径的关键机制。随着平台日益成熟,环境管道之间的协调变得至关重要,以自动化新软件发布从源到生产环境的过程,并经过最终用户(客户)的验证。这些黄金路径是自动化工作流程,将我们团队产生的更改移动到我们的生产环境中,以便客户能够访问它们。图 4.22 从高层次展示了我们的应用程序的黄金路径是什么样的。

图 4.22 将新版本提升到我们的生产环境需要哪些步骤?
考虑一下需要执行多少服务和环境管道才能将我们在开发环境中产生的软件带到我们的生产集群中,客户可以访问单个服务的发布。这些管道是如何协调和连接的,以确保我们的部署按预期工作?在整个过程中你需要进行多少手动验证?最重要的是,你能为你的团队自动化哪些内容,以便他们不必担心所有这些复杂的交互?
到目前为止,我们已经介绍了如何将应用程序安装到 Kubernetes 集群中,构建和打包应用程序服务到容器中,以及打包和分发部署这些服务到 Kubernetes 集群所需的配置文件。本章补充了如何使用 GitOps 方法管理应用程序将运行的不同环境的情况。图 4.23 展示了所有部件的组合。

图 4.23 将 GitOps 添加到管理多个环境
在深入探讨黄金路径(第六章)之前,我们必须探索我们在将应用程序部署到不同环境时面临的另一个挑战:应用基础设施,下一章将介绍。
摘要
-
环境管道负责将软件工件部署到实际环境中。环境管道避免团队直接与运行应用程序的集群交互,从而减少错误和配置错误。环境管道应在更新其配置后检查环境是否完全运行。
-
使用像 Argo CD 这样的工具,你可以将每个环境的内容定义到一个 Git 仓库中,该仓库用作环境配置应如何看起来的事实来源。Argo CD 将跟踪运行环境所在集群的状态,并确保在集群中应用配置时没有漂移。
-
团队可以通过向存储环境配置的仓库提交拉取/更改请求来升级或降级环境中运行的服务版本。一个团队或自动化流程可以验证这些更改,一旦获得批准并合并,这些更改将在实际环境中体现。如果出现问题,可以通过回滚 git 仓库中的提交来回滚更改。
-
如果你遵循了逐步教程,你将获得通过使用 Argo CD 采用 GitOps 方法部署应用程序工作负载的动手经验。
5 多云(应用程序)基础设施
本章涵盖
-
定义和管理云原生应用程序的基础设施
-
识别管理基础设施组件的挑战
-
学习如何使用 Crossplane 以 Kubernetes 的方式处理基础设施
在前面的章节中,我们安装了一个行走骨架,并学习了如何使用服务管道构建每个单独的组件,然后如何使用环境管道将它们部署到不同的环境中。我们现在面临一个重大挑战:处理我们的应用程序基础设施,这意味着不仅要运行和维护我们的应用程序服务,还要运行和维护我们的服务所需的组件。这些服务期望其他组件能够正确工作,例如数据库、消息代理、身份管理解决方案、电子邮件服务器等。虽然存在一些工具可以自动化这些组件的安装(对于本地设置)或在不同云提供商中的配置,但本章将专注于仅以 Kubernetes 方式完成这一点的工具。本章有三个主要部分:
-
处理基础设施的挑战
-
如何使用 Kubernetes 结构处理基础设施
-
如何使用 Crossplane 为我们的行走骨架配置基础设施
让我们开始吧。为什么管理我们的应用程序基础设施如此困难?
5.1 Kubernetes 中管理基础设施的挑战
当你设计像第一章中介绍的那种行走骨架的应用程序时,你会面临一些并非实现业务目标核心的特定挑战。安装、配置和维护支持我们应用程序服务的应用程序基础设施组件是一项需要由具备正确专业知识的团队精心计划的大任务。
这些组件被归类为应用程序基础设施,通常涉及第三方组件,这些组件不是内部开发的,例如数据库、消息代理、身份管理解决方案等。现代云服务提供商成功的一个重要原因是他们擅长提供和维护这些组件,并允许你的开发团队专注于构建应用程序的核心功能,这为业务带来了价值。
区分应用程序基础设施和硬件基础设施至关重要,因为本书不涉及硬件配置,其余内容主要关注应用程序空间。我假设对于公共云服务,提供商解决了所有与硬件相关的问题。对于本地场景,你可能有一个专门的团队负责硬件(根据需要移除、添加和维护硬件)。
依赖云服务提供商来配置应用程序基础设施是很常见的。这样做有很多优点,例如按需付费服务、易于大规模配置和自动化维护。但到了那个阶段,你将严重依赖提供商特定的操作方式和他们的工具。一旦你在云服务提供商中创建数据库或消息代理,你就已经跳出了 Kubernetes 的领域。现在你依赖于他们的工具和自动化机制,并且正在在业务和云服务提供商之间建立强烈的依赖关系。
让我们来看看配置和维护应用程序基础设施所面临的挑战,以便您的团队能够规划和选择合适的工具:
-
配置组件以进行扩展: 每个组件都需要不同的专业知识来配置(数据库管理员对数据库,消息代理专家,机器学习专家等),以及深入了解我们的应用程序服务将如何使用它,以及可用的硬件。这些配置需要版本控制和密切监控,以便可以快速创建新环境来重现问题或测试我们应用程序的新版本。
-
长期维护组件: 数据库和消息代理不断发布和修补以改进性能和安全。这种持续的变化迫使运维团队确保他们可以升级到新版本并保持所有数据的安全,而不会使整个应用程序崩溃。所有这些复杂性都需要在提供和消费这些组件的团队之间进行大量的协调和影响分析。
-
云服务提供商服务影响我们的多云策略: 如果我们依赖于特定于云的应用程序基础设施和工具,我们需要找到一种方法来使开发者能够为他们开发和服务创建和配置组件。我们需要一种方法来抽象基础设施的配置,以便应用程序可以定义它们需要的基础设施,而无需直接依赖于特定于云的工具。
有趣的是,我们在拥有分布式应用程序之前就已经面临这些挑战,配置和配置架构组件一直很困难,通常远离开发者。云服务提供商通过将这些主题带到开发者身边,做得非常出色,使他们能够更加自主和快速迭代。不幸的是,当与 Kubernetes 一起工作时,我们有更多的选项需要仔细考虑,以确保我们理解权衡。下一节将介绍我们如何在 Kubernetes 内部管理我们的应用程序基础设施。虽然这通常不推荐,但对于某些场景来说,它可能是实际且成本更低的。
5.1.1 管理您的应用程序基础设施
应用基础设施已经成为一个令人兴奋的领域。随着容器技术的兴起,每个开发者都可以通过几条命令启动数据库或消息代理,这对于开发目的通常已经足够。在 Kubernetes 世界中,这转化为 Helm 图表,它使用容器来配置和提供数据库(关系型和非关系型)、消息代理、身份管理解决方案等。正如我们在第二章中看到的,您只需一条命令就可以安装包含四个服务、两个数据库(Redis 和 PostgreSQL)和一个消息代理(Kafka)的行走骨架应用程序。
对于我们的行走骨架,我们正在为议程服务提供 Redis NoSQL 数据库的一个实例,为提案征集(C4P)服务提供一个 PostgreSQL 数据库的实例,以及一个 Kafka 集群的实例,所有这些都使用 Helm 图表。目前可用的 Helm 图表数量令人印象深刻,很容易想到安装 Helm 图表将成为一种趋势。示例应用程序中使用的 Helm 图表都可以在 Bitnami Helm 图表存储库bitnami.com/stacks/helm中找到。
如第二章所述,如果我们想扩展保持状态的服务,我们必须提供专门的组件,如数据库。应用程序开发者将根据需要存储的数据及其结构来定义最适合他们的数据库类型。图 5.1 展示了应用程序服务对我们为行走骨架确定的某些应用程序基础设施组件的依赖关系。

图 5.1 服务及其对应用程序基础设施组件的依赖关系
在您的 Kubernetes 集群内部署这些(PostgreSQL、Redis 和 Kafka)组件的过程涉及以下步骤:
-
寻找或创建适合您要启动的组件的 Helm 图表。对于行走骨架,PostgreSQL (
bitnami.com/stack/postgresql/helm)、Redis (bitnami.com/stack/redis/helm) 和 Kafka (bitnami.com/stack/kafka/helm) 都可以在 Bitnami Helm 图表存储库中找到。如果您找不到 Helm 图表,但有一个您要提供的组件的 Docker 容器,您可以在定义部署所需的基本 Kubernetes 构造之后创建您的图表。 -
研究图表配置和参数,您必须设置以适应您的需求。每个图表都提供了一组参数,您可以根据不同的用例进行调整。检查图表网站以了解可用的选项。包括您的运维团队和数据库管理员(DBAs)来检查针对您的用例的最佳数据库配置;这不是开发者可以完成的事情。这项分析还需要 Kubernetes 专业知识,以确保组件可以在 Kubernetes 内部以高可用性(HA)模式工作。
-
使用
helm install将图表安装到您的 Kubernetes 集群中。通过运行helm install,您将下载一组 Kubernetes 清单(YAML 文件),这些文件描述了这些组件需要如何部署。然后 Helm 将继续将这些 YAML 文件应用到您的集群中。对于我们在第二章中安装的 Conference 应用 Helm 图表(第 2.1.3 节),所有应用程序基础设施组件都被添加为图表的依赖项。 -
配置您的服务以连接到新配置的组件。您可以通过提供新配置的实例 URL 和连接凭据来实现这一点。对于数据库,将是处理请求的数据库 URL,可能还包括用户名和密码。一个有趣的细节是,您的应用程序需要某种类型的驱动程序才能连接到目标数据库。更多内容将在第八章中介绍。
-
在长期维护这些组件,进行备份并确保故障转移机制按预期工作。
图 5.2 展示了将应用程序基础设施组件安装和连接到我们的应用程序服务的步骤。

图 5.2 使用 PostgreSQL Helm 图表配置新的 PostgreSQL 实例。#1 在 Kubernetes 集群内的命名空间中安装 helm 图表;#2 图表创建 Kubernetes 资源,如 StatefulSets 和 Deployments,以配置 PostgreSQL 实例;#3 需要一个服务连接到新创建的实例,这可以通过手动操作或通过引用包含凭据和连接详情的 Kubernetes 密钥来实现。
如果您正在使用 Helm 图表,有一些注意事项和技巧您需要了解:
-
如果图表不允许您配置您想要更改的参数,您始终可以使用
helm template,然后修改输出以添加或更改您需要最终使用kubectl apply -f安装组件的参数。或者,您可以向图表存储库提交一个拉取请求。不公开所有可能的参数并等待社区成员建议更多要公开的参数是常见的做法。如果这种情况发生,不要害羞,联系维护者。无论您进行何种修改,都必须维护和记录图表内容。使用helm template会失去 Helm 发布管理功能,允许您在新版本可用时升级图表。 -
大多数图表都有一个默认配置,旨在进行扩展,这意味着默认部署将针对高可用性场景。这导致安装时消耗大量资源(CPU 和内存)的图表,这些资源在使用笔记本电脑上的 Kubernetes KinD 或 Minikube 时可能不可用。再次强调,图表文档通常包括针对开发和资源受限环境的特殊配置。
-
如果您在 Kubernetes 集群内安装数据库,每个数据库容器(Pod)必须能够访问底层 Kubernetes 节点的存储。对于数据库,您可能需要一种特殊的存储类型,以使数据库能够弹性扩展,这可能需要在 Kubernetes 之外进行高级配置。
例如,对于我们的“行走骨架”,我们设置了 Redis 图表使用 architecture 参数为 standalone,(如环境管道配置和 Agenda 服务 Helm Chart values.yaml 文件所示),以便在您可能拥有有限资源的环境中更容易运行,例如您的笔记本电脑/工作站。这会影响 Redis 的可用性以容忍故障,因为它将只运行一个副本,而与默认设置中创建一个主节点和两个从节点的情况相比。
5.1.2 将我们的服务连接到新配置的基础设施
安装图表不会使我们的应用程序服务自动连接到 Redis、PostgreSQL 或 Kafka 实例。我们需要提供服务所需的配置以连接,同时也要意识到这些组件(如数据库)启动所需的时间。
图 5.3 展示了通常的连接方式,因为大多数图表会自动创建一个 Kubernetes 机密,托管所有应用程序服务需要连接的详细信息。

图 5.3 使用 secrets 连接服务到已配置的资源。#1 创建了一个 Kubernetes 部署来运行你的一个服务,并且 pod 模板包含配置 pod 的环境变量,这些 pod 将由该部署创建;#2 使用部署资源中指定的模板创建了 pod,该模板指向包含连接到 db 实例详细信息的 secret;#3 运行在 pod 内部的容器需要准备就绪,以便消费环境变量以连接到 db 实例。
常见的做法是使用 Kubernetes secrets 来存储这些应用基础设施组件的凭证。我们用于构建行走框架的 Redis 和 PostgreSQL Helm Chart 会创建一个新的 Kubernetes secret,其中包含连接所需的详细信息。这些 Helm Charts 还创建了一个 Kubernetes 服务,用作实例运行的地点(URL)。
要将提案征集服务(Call for Proposals,C4P)连接到 PostgreSQL 实例,你需要确保 C4P 服务的 Kubernetes Deployment (conference-c4p-service-deployment) 具有正确的环境变量(见 5.1 列表)。
列表 5.1 连接到应用基础设施(PostgreSQL)的环境变量
- name: KAFKA_URL
value: <KAFKA SERVICE URL>
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: <POSTGRESQL SECRET NAME>
key: postgres-url
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: <POSTGRESQL SECRET NAME>
key: postgres-password
粗体部分突出显示了我们在安装图表时如何消费动态生成的密码以及 DB 端点 URL,该 URL 是由图表创建的 PostgreSQL Kubernetes 服务。如果你使用了不同的图表发布名称,DB 端点将会有所不同。
类似的配置也适用于议程服务(conference-agenda-service-deployment)和 Redis(见 5.2 列表)。
列表 5.2 连接到应用基础设施(Redis)的环境变量
- name: KAFKA_URL
value: <KAFKA SERVICE URL>
- name: REDIS_HOST
valueFrom:
secretKeyRef:
name: <REDIS SECRET NAME>
key: redis-url
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: <REDIS SECRET NAME>
key: redis-password
如前所述,我们从安装 Redis Helm Chart 时生成的 Kubernetes secret 中提取密码。secret 的名称将派生自我们使用的 Helm Chart 发布名称。REDIS_HOST 从图表创建的 Kubernetes 服务的名称中获取,这取决于你使用的 helm release 名称。对于应用的所有服务,我们都需要设置 KAFKA_URL 环境变量,以便服务可以连接到 Kafka。为应用基础设施组件配置不同的实例为我们打开了将供应和维护委托给其他团队甚至云提供商的大门。
5.1.3 我听说过 Kubernetes operators。我应该使用它们吗?
现在你的 Kubernetes 集群内部有四个应用服务、两个数据库和一个消息代理。信不信由你,你现在需要负责七个组件的维护和扩展,这些组件根据应用的需求而定。构建服务的团队将确切知道如何维护和升级每个服务,但他们不是维护和扩展数据库或消息代理的专家。
根据服务需求的不同,您可能需要帮助来处理这些数据库和消息代理。想象一下,您在 Agenda 服务上收到了太多的请求,因此您决定将议程部署的副本数量扩展到 200 个。在那个时刻,Redis 必须拥有足够的资源来处理连接到 Redis 集群的 200 个 Pod。在这个场景中使用 Redis 的优势在于,在会议进行期间我们可能会遇到大量的读取操作,Redis 集群允许我们从副本中读取数据,从而实现负载的分散。
图 5.4 展示了高需求的一个典型情况,我们可能会被诱惑增加应用服务的副本数量,而不会检查或更改我们的 PostgreSQL 实例的配置。在这些场景中,即使应用服务可以扩展,如果未相应配置(以支持 200+并发连接),PostgreSQL 实例将成为瓶颈。

图 5.4 显示,应用基础设施的配置需要根据我们的服务扩展方式来设置。#1 如果您注意到某个服务的需求激增,您可能会想增加副本数量,使用 ReplicaSet 的部署不会对此提出异议。如果集群有足够的资源,副本将被创建;#2 如果应用基础设施配置不正确,您可能会遇到许多问题,例如耗尽数据库连接池或数据库 Pod 过载,因为当您扩展部署时,它们并没有进行扩展。
如果您使用 Helm 安装应用基础设施,请注意,Helm 不会检查这些组件的健康状况——它只是在进行安装。如今,找到另一种在 Kubernetes 集群中安装组件的方法非常普遍,这种方法被称为 Operators。通常与应用基础设施相关联,您会发现更多活跃的组件,这些组件将安装并监控已安装的组件。这些 operator 的一个例子是 Zalando PostgreSQL Operator,您可以在github.com/zalando/postgres-operator找到它。虽然这些 operator 专注于允许您为 PostgreSQL 数据库提供新实例,但它们还实现了其他专注于维护的功能,例如:
-
在 Postgres 集群更改上进行滚动更新,包括快速的小版本更新
-
在不重启 Pod 的情况下实时调整卷大小(AWS EBS,PVC)
-
使用 PGBouncer 进行数据库连接池
-
支持快速就地主要版本升级
通常,Kubernetes 运营商试图封装与特定组件相关的操作任务,在本例中是 PostgreSQL。虽然使用运营商可能为安装的组件添加更多功能,但您现在仍然需要维护该组件和运营商本身。每个运营商都附带一个非常具有意见的流程,您的团队将需要研究和学习以进行管理。在研究和决定使用哪个运营商时,请考虑这一点。
如果您计划在集群内部运行这些组件,您和您的团队决定使用哪些应用程序基础设施,请相应地规划,以便拥有管理、维护和扩展这些额外组件所需的内部专业知识。
在下一节中,我们将探讨如何通过查看一个开源项目来解决这些挑战,该项目旨在通过声明性方法简化应用程序基础设施组件的云和本地资源的配置。
5.2 使用 Crossplane 实现声明性基础设施
使用 Helm 在 Kubernetes 内部安装应用程序基础设施组件远非理想,尤其是在大型应用程序和面向用户的场景中,因为维护这些组件及其要求,如高级存储配置,可能变得过于复杂,难以处理。
云提供商在允许我们配置基础设施方面做得非常出色,但它们都依赖于云提供商特定的工具,这些工具超出了 Kubernetes 的范畴。
在本节中,我们将探讨一个替代工具——一个名为 Crossplane 的 CNCF 项目(crossplane.io),它使用 Kubernetes API 和扩展点以声明性方式允许用户使用 Kubernetes API 部署真实基础设施。Crossplane 依赖于 Kubernetes API 来支持多个云提供商;这也意味着它与所有现有的 Kubernetes 工具很好地集成。
通过了解 Crossplane 的工作原理以及如何扩展它,您可以构建多云方法,并使用不同的提供商运行您的云原生应用程序及其依赖项,而无需担心被锁定在单个供应商上。因为 Crossplane 使用与 Kubernetes 相同的声明性方法,您可以创建关于您试图部署和维护的应用程序的高级抽象。
要使用 Crossplane,您必须首先在 Kubernetes 集群中安装其控制平面。您可以遵循官方文档(docs.crossplane.io/)或第 5.3 节中介绍的逐步教程。
仅靠 Crossplane 的核心组件对您来说帮助不大。根据您的云提供商,您将安装和配置一个或多个 Crossplane 提供商。让我们看看 Crossplane 提供商能为我们提供什么。
5.2.1 Crossplane 提供商
Crossplane 通过安装一组称为 Crossplane 提供商的组件来扩展 Kubernetes,这些组件负责理解和与云提供商特定的服务交互,代表我们配置云资源。图 5.5 展示了通过安装 GCP 提供商和 AWS 提供商,我们的 Crossplane 安装可以在两个云上配置资源。

图 5.5 配置了 GCP 和 AWS 提供商的 Crossplane
通过安装 Crossplane 提供商,您正在扩展 Kubernetes API 的功能,以配置外部资源,如数据库、消息代理、桶以及其他将存在于您的 Kubernetes 集群之外但位于云提供商领域内的云资源。有几个 Crossplane 提供商涵盖了主要的云提供商,如 GCP、AWS 和 Azure。您可以在 Crossplane GitHub 组织中找到这些 Crossplane 提供商:docs.crossplane.io/latest/concepts/providers/。
一旦安装了 Crossplane 提供商,您就可以以声明性方式创建特定于提供商的资源,这意味着您可以创建一个 Kubernetes 资源,使用 kubectl apply -f 应用它,将这些定义打包在 Helm 图表中,或者使用环境管道将这些资源存储在 Git 仓库中。
例如,使用 Crossplane GCP 提供商在 Google Cloud 中创建一个桶看起来像列表 5.4。
列表 5.4 Google Cloud Platform 桶资源定义
cat <<EOF | kubectl create -f -
apiVersion: storage.gcp.upbound.io/v1beta1 ①
kind: Bucket ②
metadata:
generateName: crossplane-bucket-
labels:
docs.crossplane.io/example: provider-gcp
spec: ③
forProvider:
location: US
providerConfigRef:
name: default
EOF
① apiVersion 和 kind 都由 Crossplane GCP 提供商定义。您可以在 Crossplane 提供商文档中找到所有支持的资源类型。
通过在我们的 Kubernetes 集群中创建一个桶资源,其中安装了 Crossplane,您正在创建一个请求,让 Crossplane 代表您配置和监控此资源。
③ 对于每种资源类型,您都有一组参数来配置资源。在这种情况下,我们希望桶位于美国。不同的资源将公开不同的配置
依赖于 Kubernetes API 配置云特定资源是一个很大的进步,但 Crossplane 并没有停止在这里。如果您看看在任何一个主要云提供商中配置数据库需要什么,您将意识到配置组件只是使组件准备好使用的任务之一。您需要额外的网络和安全配置、用户凭证以及其他云提供商特定的配置来连接到这些已配置的资源。欢迎 Crossplane 组合!
5.2.2 Crossplane 组合
Crossplane 旨在服务于两个不同的角色:平台团队和应用团队。虽然平台团队是云服务提供商的专家,了解如何配置特定云服务提供商的组件,但应用团队了解应用程序的需求,并从应用程序基础设施的角度理解需要什么。这个方法有趣的地方在于,当使用 Crossplane 时,平台团队可以为特定云服务提供商定义这些复杂的配置,并为应用团队提供简化的接口。
在现实场景中,创建单个组件的情况很少见。例如,如果我们想要配置一个数据库实例,应用团队还需要正确的网络和安全配置,以便能够访问新创建的实例。能够组合和连接多个组件是一个非常方便的功能,为了实现这些抽象和简化接口,Crossplane 引入了两个概念,组合资源定义(XRDs)和组合资源(XRs)。
图 5.6 展示了如何使用 Crossplane XRD 为不同的云服务提供商定义抽象。平台团队可能非常了解 Google Cloud 或 Azure,因此他们将负责定义针对特定应用程序需要连接在一起的具体资源。应用团队有一个简单的资源接口来请求他们感兴趣的资源。但正如通常情况一样,抽象是复杂的,有助于展示谁负责什么,但让我们通过一个具体例子来了解 Crossplane 组合的强大功能。

图 5.6 通过 Crossplane 组合资源进行资源组合抽象
图 5.7 展示了应用团队如何创建一个简单的 PostgreSQL 资源,并在 Google Cloud 中配置一个 CloudSQL 实例,同时还需要网络配置和一个存储桶。应用团队对创建的资源类型或它们在哪个云服务提供商下创建并不感兴趣。他们只关心拥有一个 PostgreSQL 实例,以便将应用程序连接到它。

图 5.7 使用 Crossplane 组合在 Google Cloud 中配置 PostgreSQL 实例
这将带我们到图中的 Secret 框,代表 Crossplane 将为我们应用程序/服务 pod 连接到已部署的资源而创建的 Kubernetes 机密。Crossplane 使用所有应用程序连接到新创建的资源所需的所有详细信息创建此 Kubernetes 机密(或仅包含与应用程序相关的信息)。此机密通常包含 URL、用户名、密码、证书或任何应用程序连接所需的内容。平台团队在定义复合资源时定义了机密中应包含的内容。在以下章节中,当我们向我们的会议应用程序添加真实的基础设施时,我们将探讨这些 CompositeResourceDefinitions 的外观以及它们如何应用于创建我们应用程序所需的所有组件。
5.2.3 Crossplane 组件和需求
要与 Crossplane 提供程序和 CompositeResourceDefinitions 一起工作,我们需要了解 Crossplane 组件将如何协同工作以在不同的云提供商内部提供和管理这些组件。
本节介绍了 Crossplane 需要运行的方式以及 Crossplane 组件将如何管理我们的 CompositeResources。首先,重要的是要理解您必须在 Kubernetes 集群中安装 Crossplane。这可以是运行您应用程序的集群,也可以是 Crossplane 将运行的单独集群。此集群将包含一些 Crossplane 组件,它们将理解我们的 CompositeResourceDefinitions 并在云平台上拥有足够的权限代表我们提供资源。

图 5.8 Google Cloud Platform 中的 Crossplane
图 5.8 展示了 Crossplane 在 Kubernetes 集群内部安装的情况,其中已安装并配置了 Crossplane GCP 提供程序,以使用具有足够权限来部署 PostgreSQL 和 Redis 实例的 Google Cloud Platform 账户。这意味着在某些情况下,您将拥有在云提供商上创建资源的管理员访问权限。
为了使图 5.8 在 GCP 中正常工作,您需要在云提供商上进行以下配置:
-
在 GCP 中创建 Redis 实例。
-
您的 GCP 项目需要启用
redis.googleapis.comAPI。 -
您还需要对 Redis 资源拥有管理员权限
roles/redis.admin。
-
-
在 GCP 中创建 PostgreSQL 实例:
-
您的 GCP 项目需要启用
sqladmin.googleapis.comAPI。 -
您还需要对 SQL 资源
roles/cloudsql.admin拥有管理员权限。
-
每个可用的 Crossplane 提供程序都需要特定的安全配置才能工作,并在我们想要创建资源的云提供商内部有一个账户。一旦安装并配置了 Crossplane 提供程序(在本例中为 GCP 提供程序),我们就可以开始创建由该提供程序管理的资源。您可以在以下文档网站上找到每个提供程序提供的资源:doc.crds.dev/github.com/crossplane/provider-gcp(图 5.9)。

图 5.9 Crossplane 支持的 GCP 资源
如前图所示,GCP 提供者版本 0.22.0 支持 29 个不同的 CRD(自定义资源定义),用于在 Google Cloud Platform 中创建资源。Crossplane 将这些资源定义为托管资源。每个托管资源都需要启用,以便 Crossplane 提供者能够访问列表、创建和修改这些资源。
在第 5.3 节中,我们将探讨如何使用不同的 Crossplane 提供者和 Crossplane 组合为我们的应用程序配置云或本地资源。在深入技术细节之前,让我们看看在使用 Kubernetes 空间中的工具时应寻找的 Crossplane 核心行为。
5.2.4 Crossplane 行为
与在我们的 Kubernetes 集群中安装 Helm 组件相比,我们使用 Crossplane 与云提供者特定的 API 交互,以在云基础设施内部配置资源。这应该简化与这些资源相关的维护任务和成本。另一个重要的区别是,Crossplane 提供者(在本例中为 GCP 提供者)将为我们观察创建的托管资源。与仅使用 Helm 安装的资源相比,这些托管资源提供了一些优势。托管资源具有非常明确的行为。以下是您可以从 Crossplane 托管资源中期待的内容摘要:
-
与其他 Kubernetes 资源一样可见: Crossplane 托管资源只是 Kubernetes 资源。这意味着我们可以使用任何 Kubernetes 工具来监控和查询这些资源的状态。
-
持续同步: 当创建一个托管资源时,提供者将不断监控该资源以确保其存在且正在运行,并将状态报告给 Kubernetes 资源。托管资源内部定义的参数被认为是期望状态(真实来源)并且 Crossplane 提供者将努力将这些配置应用到云提供者资源上。再次强调,我们可以使用标准的 Kubernetes 工具来监控状态变化并触发修复流程。
-
不可变属性 提供者负责报告用户是否手动更改了云提供者的属性。这里的想法是避免配置漂移,即从定义的状态到云提供者中运行的实际状态。如果是这样,状态将被报告回托管资源。Crossplane 不会删除云提供者资源,而是会通知以便采取行动。其他工具如 Terraform (
www.terraform.io) 将自动删除远程资源以重新创建它们。 -
延迟初始化: 托管资源中的一些属性可能是可选的,这意味着每个提供程序将为这些属性选择默认值。当这种情况发生时,Crossplane 将使用默认值创建资源,然后将选定的值设置到托管资源中。这简化了创建资源所需的配置,并可以重用云提供商定义的合理默认值,通常在他们的用户界面中。
-
删除: 当删除托管资源时,云提供商将立即触发操作。然而,托管资源将保留,直到资源完全从云提供商中移除。在云提供商上删除过程中可能发生的错误将被添加到托管资源状态字段中。
-
导入现有资源: Crossplane 不一定需要创建资源来管理它们。您可以为之前安装 Crossplane 之前创建的组件创建托管资源,并开始监控它们。您可以使用托管资源上的特定 Crossplane 注释来实现这一点:
crossplane.io/external-name。
为了总结 Crossplane、Crossplane GCP 提供程序和我们的托管资源之间的交互,让我们看看图 5.10。

图 5.10 使用 Crossplane 的托管资源生命周期
下面的点表明了图 5.10 中观察到的顺序:
-
首先,我们需要创建一个资源。我们可以使用任何工具来创建 Kubernetes 资源;这里的
kubectl只是一个例子。 -
如果创建的资源是 Crossplane 托管资源,让我们想象一个 GCP Crossplane 提供程序将选择并管理的 CloudSQLInstance 资源。
-
管理资源的第一步将是检查它是否存在于基础设施中(即在配置的 GCP 账户中)。如果不存在,提供程序将请求在基础设施中创建该资源。将根据资源上设置的属性(例如所需的 SQL 数据库类型)提供适当的 SQL 数据库。假设我们为了示例的目的选择了 PostgreSQL 数据库。
-
云提供商在收到请求后,如果资源已启用,将在托管资源中配置的参数下创建一个新的 PostgreSQL 实例。
-
PostgreSQL 的状态将被报告回托管资源,这意味着我们可以使用
kubectl或其他任何工具来监控已配置资源的状态。Crossplane 提供程序将保持这些状态同步。 -
当数据库运行起来后,Crossplane 提供程序将创建一个密钥来存储我们的应用程序连接到新创建的实例所需的凭据和属性。
Crossplane 将定期检查 PostgreSQL 实例的状态,并更新托管资源。
通过遵循 Kubernetes 设计模式,Crossplane 使用控制器实现的 reconciliation 周期来跟踪外部资源。让我们看看这是如何付诸实践的!以下部分将探讨我们如何使用 Crossplane 与我们的行走骨架应用一起工作。
5.3 我们行走骨架的基础设施
在本节中,我们将使用 Crossplane 来抽象化我们为会议应用提供基础设施的方式。由于您可能无法访问像 GCP、AWS 或 Azure 这样的云提供商,我们将与一个名为 Crossplane Helm provider 的特殊提供商一起工作。这个 Crossplane Helm provider 允许我们将 Helm 图表作为云资源进行管理。这里的想法是展示如何使用 Crossplane——更具体地说,使用 Crossplane 组合——来使用户能够使用简化的 Kubernetes 资源请求资源,以提供本地或不同云提供商托管的不同云资源。
为了我们的会议申请,我们需要 Redis、PostgreSQL 和 Kafka 实例。从应用的角度来看,一旦这三个组件可用,我们就可以连接到它们,然后就可以开始了。这些组件如何配置是运维团队的责任。
我们在第二章中安装的会议应用 Helm 图表包括了使用安装时可以设置的条件值将 Redis、PostgreSQL 和 Kafka 作为 Helm 依赖项安装。让我们快速看一下这是如何为我们的 Helm 图表配置的:github.com/salaboy/platforms-on-k8s/blob/main/conference-application/helm/conference-app/Chart.yaml#L13。
会议 Helm 图表包括了 Redis、PostgreSQL 和 Kafka 图表依赖项,如列表 5.5 所示。
列表 5.5 使用 Helm 图表依赖项的会议应用
apiVersion: v2
description: A Helm chart for the Conference App
name: conference-app
version: v1.0.0
type: application
icon: https://www.salaboy.com/content/images/2023/06/avatar-new.png
appVersion: v1.0.0
home: http://github.com/salaboy/platforms-on-k8s
dependencies: ①
- name: redis ②
version: 17.11.3
repository: https://charts.bitnami.com/bitnami
condition: install.infrastructure ③
- name: postgresql
version: 12.5.7
repository: https://charts.bitnami.com/bitnami
condition: install.infrastructure
- name: kafka
version: 22.1.5
repository: https://charts.bitnami.com/bitnami
condition: install.infrastructure
① 您可以为您的 Helm 图表包含任意数量的依赖项。这允许复杂的组合。
② 每个依赖项都需要图表名称、它所在的存储库(注意,您也可以在这里使用 oci://引用),以及您想要安装的图表版本。
③ 可以定义自定义条件来决定在安装图表时是否注入此依赖项。
在这个例子中,所有应用基础设施依赖项都在应用级别定义(在 Chart.yaml 文件的依赖项部分),但这并不妨碍你为每个服务创建一个 Helm 图表,该图表内部定义了自己的依赖项。
这种图表依赖关系适用于希望使用单个命令安装整个应用程序及其所需所有组件的开发团队。然而,我们希望将所有应用程序基础设施的关注点与应用程序服务解耦,以适应更大的场景。幸运的是,Conference 应用程序 Helm 图表允许我们关闭这些组件依赖关系,从而允许我们将由不同团队托管和管理的 Redis、PostgreSQL 和 Kafka 实例插入(如图 5.11)。

图 5.11 使用 Helm 图表依赖关系进行应用程序基础设施
通过分离谁请求和谁提供应用程序基础设施组件,我们使不同的团队能够控制和管理这些组件何时更新、备份,或者在发生故障时如何恢复。通过使用 Crossplane,我们可以使团队能够按需请求这些数据库,然后可以将它们连接到我们的应用程序服务。我们将在下一节中使用的机制的一个重要方面是,我们请求的组件可以在本地(使用 Crossplane Helm 提供程序)或远程(使用 Crossplane 云提供程序)进行配置。让我们看看这会是什么样子。您可以遵循一个逐步教程来安装、配置和创建您的 Crossplane 组合:github.com/salaboy/platforms-on-k8s/tree/main/chapter-5。
在这个例子中,我们将创建一个 KinD 集群并配置 Crossplane,以便团队可以使用 Crossplane Helm 提供程序按需请求开发目的的应用程序基础设施。在生产环境中,相同的请求将通过可扩展的云资源得到满足。更具体地说,我们通过这种方式使团队能够使用简化的接口请求 Redis、PostgreSQL 和 Kafka 实例。
对于我们的 Conference 应用程序示例,平台团队决定创建两个不同的概念:
-
数据库: 如 Redis 和 PostgreSQL 之类的 NoSQL 和 SQL 数据库。
-
消息代理: 用于管理和未管理的消息代理,如 Kafka。
在安装了 Crossplane 和 Crossplane Helm 提供程序之后,平台团队需要定义两个 Kubernetes 资源:
-
交叉平面复合资源定义(XRDs): 定义了我们希望向团队公开的资源——在这个例子中,是数据库和消息代理。这些复合资源定义定义了一个接口,多个组合可以实现。
-
Crossplane 组合:Crossplane 组合允许我们定义一组资源清单。我们可以将一个组合链接到组合资源定义并实现该 XRD。通过这样做,当用户从 XRD 定义的资源请求新资源时,组合中的所有组合资源清单将在集群中创建。我们可以提供多个组合(例如,针对不同的云提供商),所有这些组合都实现了相同的 XRD,然后使用资源中的标签来选择哪个组合应该启动。
我知道一开始这可能会听起来有些令人困惑,所以让我们看看这些概念在实际中的应用。让我们看看列表 5.6 中的数据库 Crossplane 组合资源定义(github.com/salaboy/platforms-on-k8s/blob/main/chapter-5/resources/app-database-resource.yaml)。
列表 5.6 数据库组合资源定义
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: databases.salaboy.com ①
spec:
group: salaboy.com ②
names:
kind: Database ③
plural: databases
shortNames:
- "db"
- "dbs"
versions:
- additionalPrinterColumns:
- jsonPath: .spec.parameters.size
name: SIZE
type: string
- jsonPath: .spec.parameters.mockData
name: MOCKDATA
type: boolean
- jsonPath: .spec.compositionSelector.matchLabels.kind
name: KIND
type: string
name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters: ④
type: object
properties:
size:
type: string
mockData:
type: boolean
required:
- size
required: ⑤
- parameters
① 就像每个 Kubernetes 资源一样,CompositeResourceDefinition 需要一个唯一的名称。
② 本 CompositeResourceDefinition 定义了一种新的资源类型,该类型需要有一个组和一种类型。
③ 我们为用户可以请求的新资源定义了一个新的资源类型,即数据库,因为我们希望让他们能够请求新的数据库。
④ 我们正在定义的新资源也可以定义自定义参数。在这个例子中,仅用于演示目的,我们只定义了两个:size 和 mockData。
⑤ 因为 Kubernetes API 服务器可以验证所有资源,所以我们可以定义哪些参数是必需的,它们的类型以及其他验证。如果这些参数未提供或无效,Kubernetes API 服务器将拒绝我们的资源请求。
我们定义了一种名为数据库的新资源类型,其中包含两个我们可以设置的参数,size 和 mockData。用户可以通过设置 size 参数来定义为该实例分配多少资源。他们不必担心需要多少存储空间或需要多少副本来为数据库实例提供服务,他们只需从可能值列表(小、中或大)中指定一个大小即可。使用 mockData 参数,您可以在需要时实现向实例注入数据的机制。这只是一个可以做到的例子,但定义这些接口和哪些参数对您的团队有意义取决于您。
让我们看看实现此 XRD 的 Crossplane 组合在列表 5.7 中的样子。
列表 5.7 关键/值数据库 Crossplane 组合
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: keyvalue.db.local.salaboy.com ①
labels: ②
type: dev
provider: local
kind: keyvalue
spec:
writeConnectionSecretsToNamespace: crossplane-system
compositeTypeRef: ③
apiVersion: salaboy.com/v1alpha1
kind: Database
resources:
- name: redis-helm-release ④
base:
apiVersion: helm.crossplane.io/v1beta1
kind: Release
metadata:
annotations:
crossplane.io/external-name: # patched
spec:
rollbackLimit: 3
forProvider:
namespace: default
chart: ⑤
name: redis
repository: https://charts.bitnami.com/bitnami
version: "17.8.0"
values:
architecture: standalone
providerConfigRef: ⑥
name: default
patches: ⑦
- fromFieldPath: metadata.name
toFieldPath: metadata.annotations[crossplane.io/external-name]
policy:
fromFieldPath: Required
- fromFieldPath: metadata.name
toFieldPath: metadata.name
transforms:
- type: string
string:
fmt: "%s-redis"
readinessChecks: ⑧
- type: MatchString
fieldPath: status.atProvider.state
matchString: deployed
① 组合资源还需要一个唯一的名称。
② 对于每个组合,我们还可以定义标签。然后我们将使用这些标签来匹配请求的数据库资源。
③ 通过使用 compositeTypeRef 属性,我们将数据库组合资源定义链接到这个组合。
④ 在资源数组内部,我们可以定义这个组合将配置的所有资源。在这里有多个资源是很常见的。在这个例子中,我们正在配置 Crossplane Helm 提供者中定义的单一类型的 Release 资源。
⑤ 我们需要提供为 Release 资源定义的值,在这种情况下,是我们想使用 Crossplane Helm 提供者安装的 Helm 图表的详细信息。正如你所见,我们指向由 Bitnami 托管的 Redis Helm 图表。
使用 providerConfigRef,我们可以针对不同的 Crossplane Helm 提供者配置进行定位。这意味着我们可以拥有指向不同目标集群的不同 Helm 提供者,并且这种组合可以选出使用哪一个。为了简化,这个组合使用了本地 Helm 提供者安装的默认配置。
因为我们在连接多个资源,我们可以修补资源以配置它们协同工作或应用请求资源的参数。请查阅 Crossplane 文档以获取更多关于这些机制可以实现的内容的详细信息。
⑧ 对于每个组合,我们可以定义一个条件来标记资源状态。在这个例子中,我们将组合标记为就绪状态,当 Helm Release 资源状态.atProvider.state 属性设置为已部署时。如果你正在配置多个资源,作为定义组合的人,你需要定义这个条件是什么。
通过这个组合,我们将我们的Database声明与一组资源相连接,在这种情况下,是使用我们在 Kubernetes 集群中与 Crossplane 一起安装的默认 Helm 提供者安装 Redis Helm 图表。图 5.12 显示了针对相同数据库类型的两个用户请求。

图 5.12 Crossplane 组合和组合资源定义协同工作
重要的是要注意,这个 Helm 图表将安装在安装 Crossplane 的同一 Kubernetes 集群中。尽管如此,我们仍然可以配置 Helm 提供者,使其拥有正确的凭证来安装图表到完全不同的集群。
在逐步教程(github.com/salaboy/platforms-on-k8s/tree/main/chapter-5)中,你将安装三个组合资源定义和三个组合。一旦安装完成,如图 5.12 所示,你可以请求新的数据库和消息代理,并且对于每个请求,组合中定义的所有资源都将被配置。为了简化,键值数据库组合仅安装 Redis,但你可以创建的资源数量没有限制(除了可用的硬件或配额)。
Database资源只是另一个 Kubernetes 资源,现在我们的集群理解它,看起来像列表 5.8。
列表 5.8 团队创建数据库资源以请求新的数据库实例
apiVersion: salaboy.com/v1alpha1
kind: Database
metadata:. name: my-db-keyavalue ①
spec:
compositionSelector:
matchLabels: ②
provider: local
type: dev
kind: keyvalue
parameters: ③
size: small
mockData: false
① 资源的唯一名称
② 我们使用 matchLabels 来选择合适的组成。
③ 我们需要设置我们的数据库资源声明所需的参数。
这个数据库资源的模式定义在 Crossplane 的 CompositeResourceDefinition 内部。注意 spec.compositionSelector.matchLabels 与用于组成的标签相匹配。我们可以使用这个机制为相同的数据库定义选择不同的组成。
如果你正在按照逐步教程进行,尝试创建多个资源,并查看 Crossplane 官方文档以了解如何实现 small 或 mockData 等参数,因为这些值目前尚未使用,仅用于演示目的。
这些机制的真正力量在于当你为相同的接口(复合资源定义)有不同的组成(实现)时。例如,我们现在可以创建另一个组成来为提案请求服务提供 PostgreSQL 实例,如列表 5.9 所示。PostgreSQL 的组成将类似于 Redis 的,但它将安装 PostgreSQL Helm 图表。
列表 5.9 SQL 数据库 Crossplane 组成
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: sql.db.local.salaboy.com ①
labels:
type: dev
provider: local
kind: sql ②
spec:
...
compositeTypeRef:
apiVersion: salaboy.com/v1alpha1
kind: Database
resources:
- name: postgresql-helm-release
base:
apiVersion: helm.crossplane.io/v1beta1
kind: Release
spec:
forProvider:
chart: ③
name: postgresql
repository: https://charts.bitnami.com/bitnami
version: "12.2.7"
providerConfigRef:
name: default
…
① 我们需要一个唯一的名称来区分我们的组成与之前用于 Redis 的 keyvalue 组成。
② 我们使用不同的标签来描述这个组成,注意提供者与之前相同。
③ 我们想要安装由 Bitnami 托管的 PostgreSQL Helm 图表。
让我们看看如何使用这个组成创建一个 PostgreSQL 实例。创建 PostgreSQL 实例的过程将非常类似于我们之前为 Redis 所做的,如列表 5.10 所示。
列表 5.10 使用 kind: sql 标签选择实现的数据库资源
apiVersion: salaboy.com/v1alpha1
kind: Database
metadata:
name: my-db-sql ①
spec:
compositionSelector:
matchLabels:
provider: local
type: dev
kind: sql ②
parameters:
size: small
mockData: false
① 用于 PostgreSQL 数据库的唯一名称。
② 我们使用“sql”标签来匹配之前定义的组成。
我们只是使用标签来选择哪个组成将触发我们的数据库资源。图 5.13 展示了这些概念在实际中的应用。注意标签是如何根据 kind 标签值选择正确的组成的。

图 5.13 使用标签选择组成
哈喽!我们可以创建数据库了!但当然,这并没有结束。如果你可以访问云提供商,你可以在云提供商内部提供创建数据库实例的组成,这正是 Crossplane 发挥其优势的地方。
如果我们以 Google Cloud Platform (GCP) 为例,对于使用 GCP 云资源的组成,你需要安装 Crossplane GCP 提供者并相应地配置它,如官方 Crossplane 文档中所述:docs.crossplane.io/latest/getting-started/provider-gcp/。

图 5.14 使用不同提供者选择组成,仍然使用标签
我们仍然可以通过匹配标签与我们的期望组合来选择不同的提供商。通过更改图 5.14 中的标签,我们可以使用本地 Helm 提供商或 GCP 提供商来实例化 Redis 实例。
注意:检查使用 Crossplane AWS 提供商的社区贡献 AWS 组合,请参阅github.com/salaboy/platforms-on-k8s/tree/main/chapter-5/aws。
然后,创建将在 Google Cloud Platform 中部署的新数据库资源将类似于列表 5.11。
列表 5.11 请求新的 SQL 数据库
apiVersion: salaboy.com/v1alpha1
kind: Database
metadata:
name: my-db-cloud-sql ①
spec:
compositionSelector:
matchLabels:
provider: gcp ②
type: dev
kind: sql ③
parameters:
size: small
mockData: false
① 我们资源的唯一名称需要与之前使用的所有名称都不同。
② 提供者标签选择带有 provider: gcp 标签的组合。换句话说,使用这个标签,我们选择数据库将在哪里部署。
③ 类型标签允许我们选择我们想要部署哪种类型的数据库。
无论我们的数据库或其他应用程序基础设施组件在哪里部署,我们都可以通过遵循一些约定来连接我们的应用程序服务。我们可以使用资源名称(例如,my-db-cloud-sql)来了解将用于服务发现的 Kubernetes 服务。我们还可以使用创建的密钥来获取我们需要的凭证。
步骤分解教程还提供了一个用于消息代理的 CompositeResourceDefinition,以及一个安装 Kafka Helm 图表的组合,您可以在github.com/salaboy/platforms-on-k8s/blob/main/chapter-5/resources/app-messagebroker-kafka.yaml找到它。
对于这个例子,一个非常重要的事情要考虑的是,Google Cloud Platform 不提供托管 Kafka 服务。这促使您的团队在应用程序部署到 Google Cloud Platform 时决定替换 Kafka,在 Google Cloud 计算上安装和管理 Kafka,或者雇佣第三方服务。在 AWS 的例子中,我们有一个可以使用的 Kafka 管理服务,因此不需要更改我们的应用程序代码。但仍然,抽象出我们如何连接这些基础设施服务不是很好吗?关于这一点,请参阅第七章。
图 5.15 展示了为键值数据库提供复合资源定义的简便性,这些数据库可以使用 Helm 本地部署或由云服务提供商管理。但在 Kafka 的情况下,事情会变得稍微复杂一些,因为您可能需要集成第三方服务或领导一个团队来管理 Kafka 实例。

图 5.15 组合推动团队定义哪些云服务可供我们的应用程序使用。
除了 Kafka 和 Google Cloud Platform 之外,您的团队还需要一个策略来处理跨云提供商的基础设施,或者至少对如何处理这种情况做出有意识的选择。从应用程序服务的角度来看,如果您决定用 Google PubSub 代替 Kafka,您会维护两个相同服务的副本吗?一个包含 Kafka 依赖项,另一个包含连接到 Google PubSub 的 Google GCP SDK。如果您只使用 Google PubSub,您将失去在 Google Cloud 外运行应用程序的能力。
5.3.1 将我们的服务与新的配置基础设施连接
当我们创建新的数据库或消息代理资源时,Crossplane 将监控这些 Kubernetes 资源与特定云提供商内配置的组件的状态,保持它们同步,并确保应用了所需的配置。这意味着 Crossplane 将确保我们的数据库和消息代理正在运行。如果由于某种原因发生变化,Crossplane 将尝试重新应用我们请求的配置,直到请求的内容运行起来。
如果我们没有在我们的 KinD 集群中部署应用程序,我们可以不安装 PostgreSQL、Redis 和 Kafka 就部署它。正如我们在第二章中看到的,这可以通过设置一个标志来禁用:install.infrastructure=false:
> helm install conference oci://docker.io/salaboy/conference-app
➥--version v1.0.0 --set install.infrastructure=false
我强烈建议您查看位于 github.com/salaboy/platforms-on-k8s/tree/main/chapter-5 的分步教程,以亲身体验 Crossplane 和 Conference 应用程序。最好的学习方式就是动手实践!
如果我们只运行这个命令,Helm 不会通过部署任何组件(Redis、PostgreSQL 或 Kafka)。然而,应用程序的服务将不知道如何连接到我们使用 Crossplane 组合创建的 Redis、PostgreSQL 和 Kafka 实例。我们需要向应用程序图表中添加更多参数,以便服务知道连接的位置。首先,检查您的集群中可用的数据库,如列表 5.12 所示。
列表 5.12 列出所有数据库资源
> kubectl get dbs
NAME SIZE KIND SYNCED READY COMPOSITION
my-db-keyavalue small keyvalue True True keyvalue.db.local.salaboy.com
my-db-sql small sql True True sql.db.local.salaboy.com
本教程还指导您创建一个消息代理,并检查您是否也拥有一个实例,如列表 5.13 所示。
列表 5.13 列出所有消息代理资源
> kubectl get mbs
NAME SIZE KIND SYNCED READY COMPOSITION
my-mb-kafka small kafka True True kafka.mb.local.salaboy.com
列表 5.14 显示了我们的数据库实例和消息代理的 Kubernetes Pod。
列表 5.14 我们的应用基础设施的 Pod
> kubectl get pods
NAME READY STATUS RESTARTS AGE
my-db-keyavalue-redis-master-0 1/1 Running 0 25m
my-db-sql-postgresql-0 1/1 Running 0 25m
my-mb-kafka-0 1/1 Running 0 25m
除了 Pod 之外,还创建了四个 Kubernetes 机密:两个用于存储我们的 Crossplane 组合使用的 Helm 发布,另外两个包含我们新的数据库密码,我们的应用程序将需要使用这些密码来连接(请参阅列表 5.15)。
列表 5.15 包含连接到我们数据库的凭证的 Kubernetes 机密
> kubectl get secret
NAME TYPE DATA AGE
my-db-keyavalue-redis Opaque 1 26m
my-db-sql-postgresql Opaque 1 25m
sh.helm.release.v1.my-db-keyavalue.v1 helm.sh/release.v1 1 26m
sh.helm.release.v1.my-db-sql.v1 helm.sh/release.v1 1 25m
sh.helm.release.v1.my-mb-kafka.v1 helm.sh/release.v1 1 25m
在我们配置了数据库之后,查看默认命名空间中可用的服务,请参阅列表 5.16:
列表 5.16 用于连接新基础设施的自定义 values.yaml 文件
> kubectl get services
NAME TYPE CLUSTER-IP PORT(S)
kubernetes ClusterIP 10.96.0.1 443/TCP
my-db-keyavalue-redis-headless ClusterIP None 6379/TCP
my-db-keyavalue-redis-master ClusterIP 10.96.49.121 6379/TCP
my-db-sql-postgresql ClusterIP 10.96.129.115 5432/TCP
my-db-sql-postgresql-hl ClusterIP None 5432/TCP
my-mb-kafka ClusterIP 10.96.239.45 9092/TCP
my-mb-kafka-headless ClusterIP None 9092/TCP
使用数据库和消息代理服务名称和机密,我们可以配置我们的会议应用程序图表,不仅不部署 Redis、PostgreSQL 和 Kafka,而且通过运行以下命令连接到正确的实例:
> helm install conference oci://docker.io/salaboy/conference-app
➥--version v1.0.0 -f app-values.yaml
我们不是在命令中设置所有参数,而是使用一个文件来应用图表的值。在这个例子中,app-values.yaml 文件看起来像列表 5.17。
列表 5.17 定制的 Helm 图表 values.yaml 文件
install:
infrastructure: false ①
frontend:
kafka:
url: my-mb-kafka.default.svc.cluster.local ②
agenda:
kafka:
url: my-mb-kafka.default.svc.cluster.local
redis:
host: my-db-keyavalue-redis-master.default.svc.cluster.local
secretName: my-db-keyavalue-redis
c4p:
kafka:
url: my-mb-kafka.default.svc.cluster.local
postgresql:
host: my-db-sql-postgresql.default.svc.cluster.local
secretName: my-db-sql-postgresql ③
notifications:
kafka:
url: my-mb-kafka.default.svc.cluster.local
① 我们禁用了 Redis、PostgreSQL 和 Kafka 的 Helm 依赖项。当我们安装应用程序时,这些组件将不会安装。
② 我们使用为我们的 Kafka 集群创建的 Kubernetes 服务将所有应用程序服务连接到创建的实例。对于 Redis 和 PostgreSQL 也采用相同的方法。
③ 对于 Redis 和 PostgreSQL,通过组合资源创建 Kubernetes 机密。我们的 Helm 图表理解如何从机密中获取凭证,因此我们只需要指定机密名称。
在这个 app-values.yaml 文件中,我们不仅关闭了 PostgreSQL、Redis 和 Kafka 的 Helm 依赖项,而且还配置了服务连接到我们新配置的数据库所需的变量。请注意,如果数据库是在不同的命名空间或不同的名称下创建的,kafka.url、postgresql.host 和 redis.host 应该包含服务完全限定名称中的适当命名空间,例如,my-db-sql-postgresql.default.svc.cluster.local(其中 default 是命名空间)。
图 5.16 显示了会议应用程序服务连接到使用 Crossplane 创建的应用程序基础设施。现在,开发者和平台团队之间的界限变得更加明确,因为对获取所需基础设施感兴趣的开发者有一组由平台团队精心选择并暴露给开发者的选项,这些选项使用更简单的接口。

图 5.16 启用不同团队协作并专注于手头任务
所有这些努力使我们能够将定义、配置和运行所有应用程序基础设施的责任分配给另一个团队,该团队不负责处理应用程序的服务。服务可以独立发布,无需担心使用的是哪个数据库或何时需要升级。开发者不应担心云服务提供商账户或他们是否有权创建不同资源。因此,另一个具有完全不同技能组合的团队可以负责创建 Crossplane 组合和配置 Crossplane 提供商。
我们还使团队能够通过使用 Kubernetes 资源来请求应用程序基础设施组件。这使得他们能够快速创建用于实验和测试的设置,或者快速设置应用程序的新实例。这对于我们(作为开发者)习惯的做法是一个重大转变,因为在之前,云服务提供商和大多数公司必须能够访问数据库和票务系统,以便请求另一团队为你提供该资源,这可能需要几周时间!
为了总结我们迄今为止所取得的成就,我们可以这样说:
-
我们抽象了如何提供本地和云特定的组件,例如 PostgreSQL 和 Redis 数据库以及 Kafka 等消息代理,以及访问这些新实例所需的所有配置。
-
我们为应用团队提供了一个简化的界面,该界面不依赖于云服务提供商,因为它依赖于 Kubernetes API。
-
最后,我们通过依赖由 Crossplane 创建的 Kubernetes Secrets 将我们的应用程序服务连接到新提供的实例,这些 Secrets 包含连接到新创建的实例所需的所有详细信息。
如果你使用像 Crossplane 组合这样的机制来创建高级抽象,你将创建特定领域的概念,你的团队可以通过自助服务的方式使用这些概念。我们通过创建一个使用 Crossplane 组合的 Crossplane 组合资源来创建我们的数据库和消息代理概念,该组合资源知道需要提供哪些资源(以及在哪里提供云服务提供商)。
注意:您可以在github.com/salaboy/platforms-on-k8s/tree/main/chapter-5找到涵盖本节中所有步骤的逐步教程。
5.4 回到平台工程
我们需要谨慎行事。我们不能期望每个开发者都理解或愿意使用我们讨论过的工具(如 Crossplane、ArgoCD、Tekton 等)。我们需要一种方法来减少这些工具引入的复杂性。平台旨在减少其用户的认知负荷,正如我们在第一章中查看 Google Cloud Platform 以及它如何通过几点击就能创建 Kubernetes 集群时所描述的那样。对于 GCP 和其他平台,与平台交互的用户不需要了解底层发生了什么,使用了哪些工具,或者整个平台的设计,就可以使用它。
Crossplane 是为了服务于平台团队和开发团队(或消费者),他们有不同的优先级、兴趣和技能。通过创建正确的抽象(XRDs),平台团队可以暴露出简单的资源,开发团队可以根据他们的需求进行配置,而在幕后,一个复杂的组合正在建立,以创建和连接一组云资源。我们也看到了如何通过使用标签和选择器,我们可以在不同的组合之间进行选择,使得在不同的云提供商中创建基础设施成为可能,同时为创建请求的团队保持相同的用户体验。通过扩展 Kubernetes API,Crossplane 统一了我们管理工作负载的方式以及我们如何管理跨云提供商的应用程序基础设施。换句话说,如果我们将 Crossplane 安装到 Kubernetes 集群中,我们不仅能够部署和运行我们的集群,还可以通过使用我们用于工作负载的工具来配置和管理云资源。
尽管 Crossplane 带来了许多好处,但你必须准备好面对一些缺点和挑战。正在考虑使用 Crossplane 的平台团队有其他更受欢迎的选项来配置云资源,例如 Hashicorp 的 Terraform 和 Pulumi。Crossplane 比 Terraform 更新,而且因为 Crossplane 专注于 Kubernetes,它要求平台团队完全投入 Kubernetes。不习惯管理 Kubernetes 集群的团队可能会发现像 Crossplane 这样的工具一开始很有挑战性,因此你需要提升你的 Kubernetes 技能来运行和维护像 Crossplane 这样的工具。
平台团队将不得不在是否使用 Crossplane 或 Terraform 等工具之间做出决定,我的建议是考虑你希望将使用的工具与 Kubernetes API 对齐的程度。理论上,能够以管理应用程序相同的方式管理基础设施(云资源)是非常有意义的。然而,这也需要管理并维护这些组件的团队觉得这样做是有意义的。在过去的几年里,云原生领域在可观察性、安全和运维方面的成熟度有了巨大的提升。越来越多的团队开始感到舒适地管理和运营大规模的 Kubernetes。对于这些团队来说,Crossplane 可以是一个很好的补充,因为它将与他们现有的所有 Kubernetes 可观察性堆栈、策略执行器和仪表板协同工作。
当拥有像 Crossplane 这样灵活的工具时,你打开了通往新可能性的大门,这些可能性可以跨越云提供商。平台团队现在有更多的选择可用,这可能会适得其反,但有一点是明确的。如果你使用正确的抽象,平台可以更加灵活,因为消费者界面不会改变。同时,平台团队可以迭代他们的先前决策,并在幕后提供新的实现。
图 5.17 展示了通过使用 Crossplane,我们可以为开发团队提供自助抽象,以便他们可以请求数据库、消息代理、身份服务以及他们可能需要用于其应用程序的任何其他内部或外部服务。但他们从应用程序的角度需要什么?想想之前提供的 Kafka 示例。如果你从 Kafka 迁移到 Google PubSub,你的应用程序需要做出哪些改变?

图 5.17 开发者需要消费所有这些平台服务吗?
我们已经覆盖了很多内容,从将简单应用程序安装到集群中,到构建服务并使用 GitOps 方法部署它们,现在是以声明式方式配置应用程序基础设施。图 5.18 展示了通过使用 GitOps 方法,我们不仅可以定义哪些服务/应用程序应该在环境中运行,还可以定义哪些云资源需要配置并连接到我们的应用程序服务。

图 5.18 使用声明式 GitOps 方法配置应用程序基础设施
是时候将所有内容整合到一个平台中,因为让我们的应用程序在运行我们的管道和其他工具的同一集群中运行并没有太多意义。在 Kubernetes 之上的平台会是什么样子?当你的团队试图构建一个平台时,他们会面临哪些主要挑战?只有一个方法可以找到答案:让我们在 Kubernetes 之上构建一个平台!
摘要
-
云原生应用程序依赖于应用程序基础设施来运行,因为每个服务可能需要不同的持久存储、消息代理来发送消息以及其他组件来工作。
-
在云提供商内部创建应用程序基础设施很容易,并且可以节省我们大量时间,但那时我们依赖于他们的工具和生态系统。
-
通过依赖 Kubernetes API 和像 Crossplane 这样的工具,以云无关的方式配置基础设施可以实现,这些工具抽象了底层云提供商,并允许我们定义必须使用 Crossplane 组合来配置哪些资源。
-
Crossplane 为主要云提供商提供支持。它可以扩展到其他服务提供商,包括可能不在云提供商上运行(例如,我们希望使用 Kubernetes API 管理的旧系统)的第三方工具。
-
通过使用 Crossplane 组合资源定义,我们创建了一个接口,应用程序团队可以使用它以自助方式请求云资源。
-
如果你遵循了逐步教程,你将获得使用 Crossplane 通过多云方法配置应用程序基础设施的实践经验。
6 让我们在 Kubernetes 之上构建一个平台
本章涵盖了
-
确定平台在 Kubernetes 上应提供的功能
-
了解多集群和多租户设置的挑战
-
看看在 Kubernetes 之上的平台是什么样的
到目前为止,我们已经探讨了平台工程是什么,为什么在 Kubernetes 的背景下我们需要考虑平台,以及团队必须如何从 CNCF 景观中选择他们可以使用的工具(第一章)。然后我们跳到了探讨我们的应用程序如何在 Kubernetes 之上运行(第二章),以及如何构建、打包和部署(第三章和第四章),以及将这些应用程序连接到它们需要工作的其他服务(第五章)。本章将所有这些部分组合起来,为我们平台的骨架构建一个行走模型。我们将使用前几章中介绍的一些开源项目和新的工具来解决我们在创建平台的第一迭代时可能面临的挑战。本章分为三个主要部分:
-
平台 API 的重要性
-
Kubernetes 平台架构以及如何在多租户和多集群挑战下构建可扩展的平台
-
介绍我们的平台行走骨架,以及如何构建在 Kubernetes 之上的平台
让我们先考虑一下为什么定义平台 API 是平台构建的第一步。
6.1 平台 API 的重要性
在第一章中,我们研究了现有的平台,例如 Google Cloud Platform,以了解它们为构建和运行云应用程序的团队提供了哪些关键功能。我们现在需要将这一点与我们在 Kubernetes 之上构建的平台进行比较,因为这些平台在共享一些与云提供商的共同目标和功能的同时,也与我们组织的领域更接近。
平台不过是我们将要设计、创建和维护的软件。就像任何优秀的软件一样,我们的平台将不断进化,以帮助团队应对新的场景,通过提供自动化来提高团队效率,并为我们提供使业务更成功的工具。就像其他任何软件一样,我们将从查看平台 API 开始,这将为我们提供一个可管理的范围来开始,并定义我们的平台将为用户提供哪些合约和行为。
图 6.1 显示了平台 API 是如何成为平台消费者(在这种情况下,开发者)的主要入口点。这些 API 应该隐藏平台为用户提供到工具、决策、支持的流程和黄金路径的复杂性,同时同时提供一个自助服务的地方,让团队获取他们所需的东西。

图 6.1 平台工程团队负责平台 API。
我们的平台 API 非常重要,因为好的 API 可以简化希望从我们的平台消费服务的开发团队的生活。如果我们的平台 API 设计良好,可以创建更多定制工具,如 CLIs、SDKs 或甚至用户界面,以帮助用户消费我们的平台服务。
如果我们为我们的平台构建定制和更特定领域的 API,我们可以从一次解决一个问题开始,然后将这些 API/接口扩展以覆盖更多和更多的工作流程,甚至为不同的团队。一旦我们了解我们想要覆盖哪些工作流程,并且有一个初始的平台 API 仪表板,就可以创建更多工具来帮助团队采用该平台。
让我们用一个例子来使其更加具体。我希望你能够将我这里展示的例子翻译成你组织内部更具体的例子。所有机制都应以相同的方式应用。让我们让我们的开发团队能够请求新的开发环境。
6.1.1 请求开发环境
一个平台可以帮助团队在开始处理新功能时快速上手的常见场景是为他们提供完成工作所需的一切。为了完成这项任务,平台工程团队必须了解他们将做什么工作,他们需要哪些工具,以及哪些其他服务必须可用才能成功。
一旦平台工程团队了解开发团队的需求,他们可以定义一个 API,以按需提供新的开发环境。在这些 API 背后,平台有创建、配置和为请求团队提供连接访问权限的机制。
对于我们的会议应用程序示例,如果一个开发团队正在扩展应用程序,我们(平台工程团队)必须确保他们有一个可运行的版本来工作并测试更改。这个隔离的应用程序实例还需要其数据库和其他基础设施组件才能工作。更高级的用例包括用模拟数据加载应用程序,允许团队使用预先填充的数据测试他们的更改,并拥有验证更改的正确工具。应用程序开发团队与平台之间的交互应类似于图 6.2。

图 6.2 应用程序开发团队与平台的交互。#1 应用程序开发团队可以向平台 API 请求他们需要的任意数量的开发环境;#2 平台已经编码了如何提供应用程序开发团队工作所需的所有组件和工具;#3 平台需要为应用程序开发团队提供访问权限,以便使用新配置的环境。
如前所述,开发环境只是一个例子。你必须问自己你的团队需要哪些工具来完成他们的工作。例如,对于数据科学家团队来说,开发环境可能不是向他们展示工具的最佳方式,因为他们可能需要其他工具来收集和处理数据或训练机器学习模型。
在 Kubernetes 中实现这个简单的流程并不容易。为了实现此场景,我们需要:
-
图 6.3 显示了最简单的环境定义,它包括环境的名称和我们想要创建的环境类型(我们可能允许团队请求不同的设置,在这个例子中,我们想要一个新的
开发环境)。环境定义还包括对消费团队有意义的自定义配置。在这种情况下,因为我们将会安装会议应用程序,我们希望允许团队决定是否需要安装基础设施组件。 -
拥有机制来编码开发环境对我们团队的意义。
-
拥有机制来配置和设置组件和工具。
-
拥有机制使团队能够连接到新配置的环境。
实现此场景有多种选择,包括创建自定义的 Kubernetes 扩展或使用更专业的开发环境工具。但在深入实施细节之前,让我们定义一下在此场景下我们的平台 API 应该是什么样子。
就像面向对象编程(OOP)一样,我们的 API 是 接口,可以被不同的 类 实现,最终提供具体的行为。对于配置开发环境,我们可以定义一个非常简单的接口,称为 Environment。请求新开发环境的开发团队可以通过创建新的环境资源向平台创建新的请求。Environment 接口代表了用户和平台之间的合同。此合同可以包括定义团队请求的环境类型或他们需要调整的选项和参数的参数。
创建理解开发环境请求的新 API。

图 6.3 平台 API 定义的资源环境
重要的一点是,Environment 接口不应包含(或泄露)关于我们环境的任何实现细节。这些资源(在这种情况下是环境)作为我们的抽象层,用于隐藏从平台用户那里隐藏这些环境将如何创建的复杂性。这些资源越简单,对平台用户来说越好。在这个例子中,平台可以使用 Environment Type 参数来决定创建哪种环境,并且随着我们平台机制的演变,我们可以插入新的类型。
一旦我们认识到需要哪些接口,我们就可以逐渐添加团队可以配置的参数。对于我们的示例,如果我们还想创建应用程序基础设施或连接我们的服务到现有组件,那么为我们在环境中要部署的服务参数化一些功能可能是有意义的。图 6.4 显示了需要平台安装所需应用程序基础设施的环境定义。我们还想在我们的前端服务上启用一些调试功能。这里的可能性是无限的,具体取决于对您的团队来说什么是有意义的参数化。平台团队可以控制什么可行,什么不可行。扩展环境接口以涵盖更多用例可能看起来像图 6.4。

图 6.4 扩展环境资源以启用/禁用应用程序的服务。
将此环境资源编码为 JSON 或 YAML 等格式以实现平台 API 是直接的,如列表 6.1 所示。
列表 6.1 JSON 格式中的环境定义
{
"name": "my-dev-env",
"parameters":{
"type": "development",
"installInfra": true,
"frontend": {
"debug": "true",
}
}
}
一旦定义了接口,下一步合乎逻辑的步骤是为我们的平台用户提供这些环境的实施。在深入实现细节之前,我们需要解决两个主要挑战,这些挑战是在决定实现这些环境的机制将驻留何处时您将面临的。
注意:在构建这些接口时,我们正在设计用户体验。从平台工程的角度来看,将这些接口视为我们正在构建的层,以简化团队与我们的平台交互的方式。但我们也必须认识到,我们并不是试图构建一个黑盒方法,其中这个接口是唯一与我们的平台交互的方式。如果团队有与底层层和工具交互的技术经验,他们应该能够这样做。
6.2 平台架构
本节讨论我们将如何构建我们的平台。在构建平台的技术方面,我们将遇到需要平台团队做出一些艰难选择的挑战。在本节中,我们将讨论如何构建一个平台,使我们能够将一组工具封装在我们的平台 API 后面,并使开发团队能够执行他们的任务,而无需担心平台使用哪些工具来提供和连接复杂资源。
由于我们已经在会议应用程序中使用 Kubernetes 来部署我们的工作负载,因此将平台服务运行在 Kubernetes 之上也是合理的,对吧?但是,您会在工作负载旁边运行平台服务和组件吗?可能不会。让我们稍微退后一步。
如果您的组织采用 Kubernetes,您可能已经处理了多个 Kubernetes 集群。如第四章所述,您的组织可能已经建立了生产、预演或 QA 环境。如果您希望应用程序开发团队在感觉像生产环境的环境中工作,您必须通过 Kubernetes 集群来使他们具备这种能力。
图 6.5 展示了组织内部 Kubernetes 集群的典型分布,其中可能会为开发目的在短时间内创建大量小型集群。一个或多个中等规模的集群可用于预演和测试目的;这些集群往往保持不变,因为它们可能是为了运行性能测试或大量集成测试而有意创建的。最后,创建一个或多个大型集群用于运行我们的生产工作负载。根据我们想要覆盖多少地区,我们可能需要多个地理分布的生产集群。这些集群的配置是静态的,不会改变。与开发和测试集群相比,生产集群是站点可靠性工程团队的责任,确保这些集群及其上运行的应用程序全天候正常运行。

图 6.5 环境集群,如果您想使开发者能够拥有自己的环境。
虽然生产集群和预演/QA 集群应谨慎处理并加固以处理真实流量,但开发环境往往更短暂,有时甚至运行在开发团队的笔记本电脑上。当然,您不希望在任何一个环境中运行任何与平台相关的工具。原因很简单:像 Crossplane、ArgoCD 或 Tekton 这样的工具不应与我们的应用程序工作负载竞争资源。安全考虑也可能适用;我们不希望我们的应用程序的安全因平台工具中的漏洞而受到损害。
当考虑在 Kubernetes 之上构建平台时,团队往往会创建一个或多个特殊集群来运行平台特定的工具。术语仍需标准化,但创建一个平台或管理集群来安装平台级工具正变得越来越流行。
图 6.6 展示了通过拥有独立的平台集群,您可以在同时构建一套管理工具来控制工作负载运行的环境的同时,安装实现平台功能所需的工具。

图 6.6 平台集群与平台工具管理环境
现在你有了一个单独的地方来安装这些工具,你还可以在这个集群上托管平台 API,再次避免让你的工作负载集群因平台组件而超载。如果能够重用或扩展 Kubernetes API 以作为我们的平台 API,那岂不是很好?这种做法有优点也有缺点。例如,如果我们希望我们的平台 API 遵循 Kubernetes 的约定和行为,那么我们的平台将使用 Kubernetes 的声明性特性,并推广 Kubernetes API 遵循的所有最佳实践,例如版本控制、资源模型等。这个 API 可能对非 Kubernetes 用户来说过于复杂,或者组织在创建不遵循 Kubernetes 风格的 API 时可能会遵循其他标准。如果我们重用 Kubernetes API 作为我们的平台 API,所有为这些 API 设计的 CNCF 工具将自动与我们的平台一起工作。我们的平台自动成为生态系统的一部分。在过去的几年里,我看到了一个趋势,即团队采用 Kubernetes API 作为他们的平台 API。你依赖于 Kubernetes API 的程度是平台工程团队需要做出的决定,而且总是有权衡。
图 6.7 展示了在同时使用 CNCF 和云原生生态系统中的 Kubernetes API 的同时,暴露一个遵循公司标准的特定组织 API 的关系。为了确保信息清晰,这些不是互斥的,正如我们将在第 6.3 节中看到的,同时拥有两者是非常有意义的。

图 6.7 基于 Kubernetes 的平台 API 与公司特定 API 的互补
采用 Kubernetes API 作为你的平台 API 不会阻止你构建一个层,以便其他工具消费或遵循公司的标准。通过拥有基于 Kubernetes 的 API 层,你可以访问 CNCF 和云原生空间中创建的所有令人惊叹的工具。在基于 Kubernetes 的 API 之上,可以再添加一层,遵循公司标准和合规性检查,从而更容易与其他现有系统集成。
在我们之前的例子中,我们可以扩展 Kubernetes 以理解我们的环境请求,并提供定义这些环境如何配置的机制。
图 6.8 展示了用于定义我们环境的 Kubernetes 资源。这个资源可以被发送到安装了扩展集的 Kubernetes API 服务器,以便理解当新的环境定义到达时应该做什么。

图 6.8 扩展 Kubernetes 以理解环境和作为我们的平台 API
在原则上,这似乎看起来不错且可行。然而,在实现这些 Kubernetes 扩展以作为我们的平台 API 和平台工具的中心枢纽之前,我们需要了解我们的平台实现将试图回答的问题。让我们看看在这些场景中团队将面临的主要平台挑战。
6.2.1 平台挑战
无论何时,如果你在处理多个 Kubernetes 集群,你必须管理它们以及与这些集群相关的所有资源。要管理所有这些资源需要什么?理解潜在问题的第一步是了解我们平台的使用者是谁。我们是在为外部客户还是内部团队构建平台?他们的需求是什么,他们需要达到何种隔离水平才能在没有打扰邻居的情况下独立操作?他们需要哪些指导方针才能成功?
虽然我不能为所有用例回答这些问题,但有一点很清楚——平台工具和工作负载需要分离。我们需要在我们的平台中编码基于每个租户期望的租户边界。无论这些租户是客户还是内部团队,我们都必须明确设定关于我们的租户模型和平台用户的保证,以便他们了解平台提供给他们的资源限制,以便他们完成工作。
我们将要构建的平台需要编码所有这些决策。在接下来的两个部分中,我们将探讨平台团队在其旅程早期将需要做出的两个最常见决策:(1)管理多个集群和(2)隔离和多租户。
6.2.2 管理多个集群
我们将要构建的平台需要管理和理解哪些环境可供团队使用。更重要的是,它应该允许团队在需要时请求自己的环境。
使用 Kubernetes API 作为我们的平台 API 来请求环境,我们可以使用像 ArgoCD(在第四章中介绍)这样的工具来持久化和同步我们的环境配置到实际的 Kubernetes 集群。管理我们的集群和环境变成了仅仅管理必须同步到我们的平台集群(们)的 Kubernetes 资源。
图 6.9 展示了使用我们已经使用过的两个工具(Crossplane 和 ArgoCD)来管理我们的会议应用程序,但现在是在管理平台级资源的环境中。
通过在我们的平台集群内部结合像 ArgoCD 和 Crossplane 这样的工具,我们推广了在第四章中讨论的技术,即环境管道技术,这些技术同步应用级别的组件,我们现在使用它们来管理高级平台问题。在这种情况下,像 Crossplane 这样的工具可以帮助我们在云提供商上配置完整的平台环境。


如您在前面的图中所见,我们的平台配置本身将变得更加复杂,因为它需要有一个真实来源(Git 仓库)来存储平台管理的环境和资源。它还需要访问一个密钥存储库,如 HashiCorp Vault,以使 Crossplane 能够连接并创建不同云提供商中的资源。换句话说,您现在有两个额外的关注点。首先,您需要定义、配置并授予访问权限给一个或多个 Git 仓库以包含平台中创建的资源配置。其次,您必须管理一组云提供商账户及其凭证,以便平台集群可以访问和使用这些账户。
如果您能够像管理您的作业(使用 GitOps 方法、管理凭证和用户、以及公开正确的抽象/API)一样管理所有平台资源,平台工件就只是您开发和持续交付实践的扩展。
虽然第 6.3 节的示例没有专注于配置所有这些关注点,但它提供了一个很好的游乐场,可以在其基础上构建并根据自己的团队需求进行更高级的设置实验。
我建议优先考虑哪些配置是有意义的,以便了解您的团队或租户将如何使用资源、期望和要求。让我们更深入地探讨这个领域。
6.2.3 隔离和多租户
根据您的租户(团队、内部或外部客户)的需求,您可能需要创建不同的隔离级别,这样他们在同一平台屋顶下工作时不会相互干扰。
多租户是 Kubernetes 生态系统中的一个复杂话题。使用 Kubernetes RBAC(基于角色的访问控制)、Kubernetes 命名空间以及可能设计有不同租户模型的多重 Kubernetes 控制器使得在同一个集群内定义租户之间的隔离级别变得困难。
正在采用 Kubernetes 的公司倾向于采取以下一种隔离方法:
-
Kubernetes 命名空间:
-
优点:
-
创建命名空间非常简单,并且几乎没有开销。
-
创建命名空间成本很低,因为它只是 Kubernetes 用来在集群内部部分隔资源的逻辑边界。
-
-
缺点:
-
命名空间之间的隔离非常基础,它将需要 RBAC 角色来限制用户在他们被分配的命名空间之外的可视性。还必须定义资源配额以确保单个命名空间不会消耗所有集群资源。
-
提供对单个命名空间的访问需要共享与管理员和所有其他租户使用的相同 Kubernetes API 端点。这限制了客户端可以在集群上执行的操作,例如安装集群范围内的资源。
-
所有租户都将与同一个 Kubernetes API 服务器交互,这可能会根据每个租户的规模和需求引起问题。
-
共享相同的 Kubernetes API 服务器限制了可以在集群中安装的全局资源。例如,安装同一扩展的两个不同版本是不可能的。
-
-
-
Kubernetes 集群:
-
优点:
-
与不同集群交互的用户可以拥有完整的管理员权限,使他们能够安装他们需要的任何工具。
-
您可以在集群之间实现完全隔离,连接到不同集群的租户不会共享相同的 Kubernetes API 服务器端点。每个集群都可以有不同的配置,以确定其可扩展性和弹性。这允许您根据其需求定义不同的租户类别。
-
-
缺点:
-
这种方法成本高昂,因为您将支付运行 Kubernetes 的计算资源费用。您创建的集群越多,运行 Kubernetes 的费用就越高。
-
如果您允许团队创建(或请求)他们自己的集群,管理多个 Kubernetes 集群会变得复杂。僵尸集群(无人使用且被遗弃的集群)开始出现,浪费了宝贵的资源。
-
在多个不同的 Kubernetes 集群之间共享资源、安装和维护工具具有挑战性,并且是一项全职工作。
-
-
根据我的经验,团队会为敏感环境,如生产环境和性能测试,创建隔离的 Kubernetes 集群。这些敏感环境通常保持不变,并且仅由运维和站点可靠性团队管理。当您转向开发团队和更短暂的测试或日常开发任务环境时,使用带有命名空间的集群是一种常见做法。
在这两种选项之间做出选择很难,但重要的是不要过度承诺于单一选项。不同的团队可能有不同的需求,因此在下文中,我们将探讨平台如何抽象这些决策,使团队能够根据他们的需求访问不同的配置。
我对做出这些决策的平台团队的建议是,建立并实施能够让您从一种解决方案切换到另一种解决方案的实践。从简单的解决方案,如命名空间隔离开始是很常见的,但过了一段时间,当单个集群和大量命名空间不足以满足需求时,您需要更稳健的计划。为了使这个决定更容易,问问自己您的消费者是否需要访问 Kubernetes API。如果他们不需要,您可能希望评估采用类似于 Google Cloud Run (cloud.google.com/run)、Azure Container Apps (azure.microsoft.com/en-us/products/container-apps) 或 AWS App Runner (aws.amazon.com/apprunner/) 的方法,这些方法使团队能够在不访问编排器 API 的情况下运行容器。
6.3 我们的平台原型
本节探讨了创建一个简单的平台,允许内部团队创建开发环境。由于我们的团队正在将会议应用程序部署到 Kubernetes 集群中,我们希望为他们提供相同的开发者体验。
注意:您可以通过一个逐步教程来跟随,在该教程中,您将安装并交互使用平台行走骨架,请参阅github.com/salaboy/platforms-on-k8s/tree/main/chapter-6。
为了实现这一点,我们将使用之前使用的一些工具,如 Crossplane,来扩展 Kubernetes 以理解开发环境。然后,我们将使用一个名为vcluster的项目(vcluster.com)为我们的团队提供小型 Kubernetes 集群。这些集群是隔离的,允许团队安装额外的工具而不用担心其他团队正在做什么。因为团队将能够访问 Kubernetes API,他们可以自由地对集群进行操作,而无需请求复杂的权限来调试他们的工作负载。
图 6.10 展示了这个过程是如何工作的。团队可以通过创建环境 Kubernetes 资源来请求新的环境。平台将获取这些资源,并为它们使用vcluster提供小型 Kubernetes 集群。对于这个行走骨架,我们将保持简单,但平台本身是复杂的。

图 6.10 构建一个平台原型以提供开发环境
我必须强调,这个例子故意使用现有工具而不是创建我们自己的自定义 Kubernetes 扩展的重要性。如果你创建自定义控制器来管理环境,你将创建一个复杂的组件,这将需要维护,并且可能与本例中展示的机制重叠 95%。换句话说,在构建这个例子时,没有创建任何自定义 Kubernetes 控制器。
正如我们在本章开头讨论我们的平台 API 时一样,让我们看看我们如何构建这些 API,而不需要创建我们需要测试、维护和发布的自定义 Kubernetes 扩展。我们将使用与第五章中我们的数据库和消息代理相同的 Crossplane 组合,但现在我们将实现我们的环境自定义 Crossplane 组合资源定义。我们可以保持环境资源简单,并使用 Kubernetes 标签匹配和选择器来匹配一个资源与我们可以创建的可能的组合之一,以提供我们的环境。
图 6.11 展示了如何通过更改环境中的属性/标签来帮助 Crossplane 为我们的团队选择正确的组合。

图 6.11 将环境资源映射到 Crossplane 组合
Crossplane 组合提供了使用不同的提供程序一起配置和部署资源的灵活性,正如我们在第五章中看到的,可以为不同类型的多个环境提供不同的组合(实现)。对于这个例子,我们希望每个环境都能与其他环境隔离,以避免团队无意中删除其他团队的资源。创建隔离环境的最直观的两种方式可能是为每个环境创建一个新的命名空间,或者为每个环境创建一个完整的 Kubernetes 集群。
图 6.12 展示了如何使用另一个 Crossplane 提供程序(称为 Kubernetes 提供程序)来创建 Kubernetes 资源,例如命名空间。这与使用允许我们创建完整集群的云提供商 Crossplane 提供程序形成对比,在这种情况下是在 Google Cloud Platform(GCP)中。一旦我们有一个集群,我们就可以安装我们的会议应用程序 Helm 图表。

图 6.12 不同环境组成、命名空间和 GKECluster
虽然为每个开发团队创建一个完整的 Kubernetes 集群可能有些过度,但 Kubernetes 命名空间可能不足以满足你的使用案例,因为所有团队都将与同一个 Kubernetes API 服务器交互。因此,我们将使用 vcluster 与 Crossplane Helm 提供程序结合使用,这让我们在不需要创建新集群的成本下,获得了两者的最佳之处。图 6.13 展示了我们可以如何重复使用 Crossplane Helm 提供程序来创建 vclusters。

图 6.13 使用 vcluster 创建隔离环境
你可能想知道:什么是 vcluster?为什么我们要使用 Crossplane Helm 提供程序来创建一个?虽然 vcluster 只是你可以使用来构建你的平台的选择之一,但我认为它是每个平台工程师工具箱中的关键工具。
6.3.1 vcluster 用于虚拟 Kubernetes 集群
我是非常喜欢 vcluster 项目。如果你在讨论基于 Kubernetes 的多租户,vcluster 往往会在对话中出现,因为它为 Kubernetes 命名空间与 Kubernetes 集群之间的讨论提供了一个非常好的替代方案。
vcluster 通过在现有的 Kubernetes 集群(宿主集群)内部创建虚拟集群,专注于为不同的租户提供 Kubernetes API 服务器隔离。图 6.14 展示了 vcluster 在现有 Kubernetes 集群(宿主)内部的工作方式。

图 6.14 vcluster 在 Kubernetes (K8s) API 服务器上提供隔离
通过创建新的虚拟集群,我们可以与租户共享一个隔离的 API 服务器,他们可以在其中做任何需要的事情,无需担心其他租户正在做什么或安装什么。对于希望每个租户都能拥有集群级访问权限和完全控制 Kubernetes API 服务器的场景,vcluster提供了一个简单的替代方案来实现这一点。如果你不需要为你的团队提供访问 Kubernetes API 的权限,我建议使用之前提到的命名空间方法。
创建一个vcluster很简单:你可以通过安装vcluster Helm 图表来创建一个新的vcluster。或者,你可以使用vcluster CLI 来创建一个并连接到它。
最后,可以在他们的文档中找到一个很好的比较vcluster、Kubernetes Namespaces 和 Kubernetes 集群的表格。如果你已经与你的团队进行了这些对话,这个表格以清晰的语言解释了优势和权衡(图 6.15)。

图 6.15 Kubernetes Namespaces 与 vcluster 与 Kubernetes 集群租户的优缺点
我强烈建议查看他们的网站(vcluster.com)和可用的博客文章www.salaboy.com/2023/06/19/cost-effective-multi-tenancy-on-kubernetes/,以了解更多关于这个项目以及它如何帮助你的团队配置成本效益高的集群。
接下来,让我们看看我们的平台行走骨架对于想要创建、连接并针对使用vcluster的新环境工作的团队是什么样的。
6.3.2 平台体验
在 GitHub 仓库github.com/salaboy/platforms-on-k8s/tree/main/chapter-6中实现的平台行走骨架允许连接到平台 API 的团队创建新的环境资源,并向平台提交请求以供其为他们配置。
图 6.16 展示了我们平台行走骨架的架构。首先,应用开发团队可以向平台 API 提交创建新开发环境的请求。平台将为开发团队配置一个新的环境——在这种情况下,遵循使用 Crossplane Helm 提供程序创建新虚拟集群(使用vcluster)的 Crossplane 组合——然后为开发团队安装 Conference 应用 Helm 图表以进行工作。其次,应用开发团队可以连接到这个新的隔离环境,无需担心破坏其他团队的设置。

图 6.16 使用 Crossplane 和vcluster为应用开发团队创建隔离环境
注意:拥有一个大型集群来托管所有临时开发环境集群是非常有意义的。我们用于构建平台行走骨架的工具可以轻松配置以实现该设置,但在单个本地 KinD 集群上运行则相当困难。
平台集群使用 Crossplane 和 Crossplane 组合来定义如何配置环境。为了在一个本地 Kubernetes 集群中运行 Crossplane 组合(而不需要访问特定的云提供商),行走骨架使用vcluster为其自己的(虚拟)Kubernetes 集群配置每个环境。拥有独立的 Kubernetes 集群使团队能够连接到这些环境,并使用我们默认在创建环境时安装的会议应用程序完成他们需要的工作。
应用团队需要连接到平台 API(托管平台工具的 Kubernetes 集群——在本例中,是 Crossplane 和vcluster配置),使用如kubectl之类的工具来请求新的环境。对于行走骨架,将环境资源发送到平台 API 将导致平台配置一个新的vcluster,团队可以连接到它。请参阅列表 6.2,它显示了我们可以发送到 Kubernetes API 服务器的环境资源定义。
列表 6.2 作为 Kubernetes 资源的环境定义
apiVersion: salaboy.com/v1alpha1
kind: Environment
metadata:
name: team-a-dev-env ①
spec:
compositionSelector:
matchLabels:
type: development ②
parameters: ③
installInfra: true
① 我们想要创建的环境名称
② 我们想要的环境类型是通过标签定义的
③ 参数是针对您特定用例定制的。根据您想要使团队能够配置的内容,您可以为他们在请求环境时迭代定义更多和更多的参数以进行微调。
因为这些是 Kubernetes 资源,团队可以使用kubectl查询这些资源,如列表 6.3 所示。
列表 6.3 列出环境资源
> kubect get environments
NAME CONNECT-TO TYPE INFRA READY
team-a-dev-env team-a-dev-env-jp7j4 development true True
一旦环境准备就绪,团队就可以连接到它。因为我们使用vcluster,连接到它就像连接到任何其他 Kubernetes 集群一样。幸运的是,vcluster使我们的生活变得更简单,我们可以使用它们的 CLI 为我们配置访问令牌。
运行以下命令将连接到刚刚创建的vcluster实例,并托管由 Crossplane 组合安装的会议应用程序:
vcluster connect team-a-dev-env-jp7j4 --server https://localhost:8443 -- zsh
注意:当运行vcluster connect时,您现在连接到了一个新的集群上下文,这意味着如果您列出所有 Pod 和 Namespaces,您将只会看到在这个新集群中可用的资源。您不应该看到任何 Crossplane 资源,例如。
对行走骨架的自然扩展是使用 Crossplane 组合在云服务提供商上创建环境,以启动 Kubernetes 集群。使用 ArgoCD 在 Git 仓库内管理这些环境资源也是一个自然的进步。在这种情况下,与要求应用开发团队直接与平台 API 连接相比,团队可以通过向一个可以验证并自动合并的仓库发送拉取请求来请求新的环境。
逐步教程(github.com/salaboy/platforms-on-k8s/tree/main/chapter-6)以部署自定义平台管理员用户界面应用程序结束。这个平台管理员应用程序使团队能够在不连接到 Kubernetes 平台 API 的情况下使用平台功能,通常称为“点击操作”,因为我们试图避免团队编写复杂的 YAML 文件或像云服务提供商那样执行长命令。此应用程序公开 REST 端点以及用户界面提供的功能,以减少需要了解平台幕后操作的应用团队的认知负荷。图 6.17 显示了平台门户管理员界面(这不是会议应用程序的一部分)。

图 6.17 平台管理员用户界面允许团队在不连接到平台 Kubernetes API 的情况下创建和管理环境。
此平台管理员应用程序还公开 REST 端点,通过发送 REST 请求执行所有操作,这可以用于进一步的自动化和与现有系统的集成。
总结一下,行走骨架为平台用户提供不同的交互方式。首先,它扩展了 Kubernetes API,以启用平台工作流程,例如使用 Crossplane 创建开发环境。然后,它为不希望或不能使用扩展的 Kubernetes API 的团队提供用户界面和简化的 REST 端点。这些简化的 REST API、SDK 和 CLI 可以为团队创建,以便他们管理自己的环境。
总是提供这两种选项都是有价值的。当可能的时候,使用 Kubernetes API 和云原生生态系统的力量是好的,但同样重要的是,在需要时有一个简化的选项来减少认知负荷并遵循公司 API 标准。
在结束本章之前,让我们回顾一下我们共同看到的主题和项目。所有这些工具和配置与平台工程有何关联?谁负责哪个组件?接下来又是什么?
6.4 回顾平台工程
到目前为止,我们已经探讨了处理我们在构建分布式应用程序时面临的不同挑战的开源项目。这些工具中的大多数都不是针对应用程序开发者的,需要通常在构建业务应用程序和功能时不需要的技能和知识。所有工具的共同点是 Kubernetes,在大多数情况下,项目都扩展了 Kubernetes 以执行除了运行我们的工作负载之外的任务。在本节中,我想回顾一下所有这些项目是如何结合在一起以界定责任、合同和期望的。
如果我们从远处观察所有这些例子,有两种类型的团队:平台和应用开发团队。这两个团队有不同的责任,需要不同的工具来完成他们的工作。从我们迄今为止所看到的情况来看:
-
平台团队负责以下事项:
-
理解与 IT 服务、云资源和工具相关的不同团队的需求。
-
便于访问凭证和不同资源。
-
为其他团队创建自动化工具以满足他们的需求。
-
-
应用程序开发团队负责以下事项:
-
定义面向客户的架构和技术栈。
-
创建面向客户的特性。
-
发布新版本以持续改进业务运营方式。
-
这些责任体现在可以类似管理的软件工件中。图 6.18 显示了我们在平台行走骨架中使用的工件。未包含在逐步教程中的工具用虚线表示。

图 6.18 平台行走骨架工具、配置和服务
如您所见,即使是对于一个非常简单的平台,平台团队也在管理和维护需要高度可用以供我们的应用程序开发团队使用的不同工具。我还没有专注于管理凭证或机密,但这是平台团队在其旅程早期将面临的问题。使用像外部机密项目(github.com/external-secrets/external-secrets)和/或像 HashiCorp 的 Vault(www.vaultproject.io/)这样的工具将使管理和存储凭证变得更加容易和集中化。这种复杂程度在历史上导致了两种实施场景:
-
购买提供出色应用程序开发者体验但平台工程定制或可操作性有限的解决方案(例如,Heroku、CloudFoundry)
-
从一组原语构建解决方案,包括脚本语言(BASH、Python 等)、声明式基础设施语言(Crossplane、Terraform、Chef、Ansible)和工作流引擎(ArgoCD Workflows、CircleCI、GitHub Actions)。
最近,出现了大量新的工具,使得第一种场景(例如,Vercel、Fly.io)成为可能。然而,对于许多组织来说,这些解决方案在全面管理其业务流程和合规性要求方面仍需要帮助。为了应对这一挑战,人们更加关注降低定制内部产品构建的成本。例如,有一个名为 Kratix(kratix.io/)的项目,它是一个框架,旨在优化定义和实施作为服务提供给其他内部团队的经验。
Kratix 围绕平台构建体验,而不是应用用户体验。像 Kratix 这样的框架可以启用一个内部市场,专家可以在保持提供的一致性同时,将能力作为服务提供,类似于我们通过 Crossplane 组合所探索的。
无论您使用外部工具还是构建自己的工具,平台工程团队都必须构建关于他们用于构建平台的项目知识库,并有一个发布流程来管理工具上线供其他团队使用时的变更。
与本书的示例存储库github.com/salaboy/platforms-on-k8s/类似,平台工程团队需要管理所有配置文件,以安装和重新创建平台工作所需的所有工具和资源。
注意:理想情况下,与 Kubernetes 一样,如果控制平面(我们安装的工具)出现故障,我们的团队应该能够继续工作。我们(作为平台工程团队)需要构建具有弹性的平台,并确保如果出现问题,我们不会阻碍团队及其正在进行的重要工作。虽然我们构建的平台应该加快软件交付,但它不应该是团队成功的关键路径。换句话说,总应该有绕过平台的方法,这意味着如果团队想直接访问平台使用的某些工具,他们应该能够做到。
本章中我们构建的行走骨架为不同用户提供了不同的层,以便他们进行工作和集成。如果你的团队理解了平台工具,他们可以访问平台集群的 Kubernetes API,以获得完全的灵活性和控制。如果他们选择,他们也可以使用提供的用户界面和 REST 端点与其他系统集成。图 6.19 展示了我们的平台行走骨架,它如何为团队提供由平台 API 公开的预定义工作流程,以及平台团队在底层实现的工具和行为。
我强烈建议平台团队记录他们使用计划作为其平台倡议一部分的每个工具的旅程,因为让团队成员熟悉这些决策通常是维护像这里描述的平台最具有挑战性的方面。

图 6.19 平台责任和边界
在接下来的几章中,我们将探讨平台在为团队创建环境时应提供的一些核心功能。可以为应用开发团队提供哪些功能,以便他们在交付软件时更加高效?第七章涵盖了发布策略及其为什么对团队进行实验和发布更多软件很重要。第八章涵盖了你需要为应用程序的所有服务提供的一些共享关注点,以及为开发者提供这些机制的不同方法。
摘要
-
在 Kubernetes 之上构建平台是一项复杂的工作,需要结合不同的工具来满足不同团队的需求。
-
平台就像你的业务应用程序一样是软件项目。首先理解主要用户是谁,并定义清晰的 API,这是确定如何构建平台时任务优先级的关键。
-
管理多个 Kubernetes 集群和处理租户隔离是平台团队在平台构建初期面临的主要挑战。
-
拥有一个平台的基础框架可以帮助你向内部团队展示可以构建的内容,以加快他们的云原生之旅。
-
使用 Crossplane、ArgoCD、
vcluster和其他工具可以帮助你在平台级别推广云原生最佳实践,但最重要的是,避免创建自定义工具和用于配置和维持云原生资源复杂配置的方法的冲动。 -
如果你遵循了逐步教程,你将获得使用 Crossplane 和
vcluster等工具来提供按需开发环境的实践经验。你还与一个简化的 API 进行了交互,这有助于减少不希望或无法与完整的 Kubernetes API 服务器交互的团队的认知负荷。
7 平台功能 I:共享应用程序关注点
本章涵盖
-
95%的云原生应用程序的学习需求
-
减少应用程序与基础设施之间的摩擦
-
使用标准 API 和组件解决共同关注的问题
在第五章中,我们创建了数据库和消息代理等抽象,以配置和配置应用程序服务所需的所有组件。在第六章中,我们扩展了这些机制来构建我们的平台步行骨架。这个平台使团队能够请求新的开发环境,这些环境不仅创建隔离的环境,还安装了会议应用程序(以及应用程序所需的所有组件),以便团队能够开展工作。通过构建平台的过程,我们定义了平台团队的责任以及每个工具所属的位置及其原因。在第六章结束时,我们提供了关于像 Crossplane、Argo CD 和 Tekton 这样的工具在哪里运行以及如何管理并启用具有团队交付更多软件所需功能的不同的环境的明确指南。
到目前为止,我们为开发者提供了运行应用程序实例的 Kubernetes 集群。本章探讨了提供更接近应用程序需求的功能的机制。这些功能中的大多数将通过抽象应用程序基础设施需求的 API 来访问,允许平台团队在不更新任何应用程序代码的情况下演变(更新、重新配置、更改)基础设施组件。同时,开发者将与这些平台功能交互,而无需了解它们的实现方式,也不会因为大量依赖而使应用程序膨胀。本章分为三个部分:
-
大多数应用程序 95%的时间在做什么?
-
标准 API 和抽象,用于将应用程序代码与基础设施分离。
-
使用 Dapr(分布式应用运行时)更新我们的会议应用程序,Dapr 是一个由 CNCF 和开源项目创建的,旨在为分布式应用挑战提供解决方案的项目。
让我们先分析一下大多数应用程序在做什么。不用担心;我们也会涵盖边缘情况。
7.1 大多数应用程序 95%的时间在做什么?
我们已经与我们的步行骨架会议应用程序合作了七个章节。我们学习了如何在 Kubernetes 上运行它,以及如何将服务连接到数据库、键值存储和消息代理。回顾这些步骤并在步行骨架中包含这些行为是有充分理由的。大多数应用程序,如会议应用程序,将需要以下功能:
-
调用其他服务发送或接收信息: 应用程序服务并不是孤立的。它们需要调用其他服务,并被其他服务调用。服务可以是本地的或远程的,你可以使用不同的协议,最常见的是 HTTP 和 GRPC。我们在会议应用程序的行走骨架中使用服务之间的 HTTP 调用。
-
存储和读取持久化存储中的数据: 这可以是数据库、键值存储、类似 S3 存储桶的 blob 存储,甚至是从文件中写入和读取。对于会议应用程序,我们使用 Redis 和 PostgreSQL。
-
异步发射和消费事件或消息: 在分布式系统中,使用异步消息进行通信系统实现事件驱动架构是一种常见做法。使用像 Kafka、RabbitMQ 甚至云提供商的消息系统是常见的。会议应用程序中的每个服务都在使用 Kafka 发射或消费事件。
-
访问凭证以连接到服务: 当连接到应用程序的基础设施组件,无论是本地还是远程时,大多数服务都需要凭证来对其他系统进行身份验证。在这本书中,我只提到了像外部密钥(
github.com/external-secrets/external-secrets)或 HashiCorp 的 Vault(www.vaultproject.io/)这样的工具,但我们还没有深入探讨。
无论我们是在构建商业应用程序还是机器学习工具,大多数应用程序都将从这些能力易于消费中受益。虽然复杂的应用程序需要更多,但总有办法将复杂部分与通用部分分离。
图 7.1 展示了几个服务之间以及与可用基础设施的交互示例。服务 A 通过 HTTP 调用服务 B(对于这个主题,GRPC 同样适用)。服务 B 存储并从数据库读取数据,并需要正确的凭证来连接。服务 A 还连接到消息代理并将消息放入其中。服务 C 可以从消息代理中提取消息,并使用一些凭证连接到存储桶,以存储基于接收到的消息的一些计算。

图 7.1 分布式应用程序中的常见通信模式
无论这些服务实现什么逻辑,我们都可以提取一些常量行为,并使开发团队能够消费,而无需处理低级细节,或推动他们做出关于可以在平台级别解决的问题的决策。
要了解这是如何工作的,我们必须仔细观察这些服务内部发生的事情。正如你可能已经知道的,魔鬼藏在细节中。从高层次的角度来看,我们习惯于处理如图 7.1 所示的服务执行其描述的操作,但如果我们想要在软件交付管道中实现更高的速度,我们需要再深入一层,了解我们应用程序组件之间的复杂关系。让我们快速看一下应用程序团队在尝试更改不同的服务和所需的基础设施时面临的挑战。
7.1.1 应用与基础设施耦合的挑战
幸运的是,这并不是编程语言竞赛,与你的选择无关。如果你想连接到数据库或消息代理,你必须向你的应用程序代码中添加一些依赖项。虽然这在软件开发行业中是一种常见做法,但它也是交付速度比预期慢的原因之一。
不同团队之间的协调是发布软件时大多数阻塞问题的原因。我们创建了架构并采用了 Kubernetes,因为我们希望更快地发展。通过使用容器,我们采用了更简单、更标准的方式来运行我们的应用程序。无论应用程序是用哪种语言编写的,或者使用了哪种技术栈,如果你给我一个包含应用程序的容器,我就可以运行它。我们已经消除了应用程序对操作系统的依赖,以及我们需要在机器(或虚拟机)上安装的软件,以便运行你的应用程序,现在这些软件都封装在容器中。
不幸的是,我们还没有解决容器(我们应用程序的服务)之间的关系和集成点。我们也没有解决这些容器将如何与本地(自托管)或由云服务提供商管理的应用程序基础设施组件交互的问题。
让我们更仔细地看看这些应用程序在哪些方面严重依赖其他服务,并且可能阻碍团队进行更改,推动他们进行复杂的协调,这可能导致我们的用户出现停机。我们将从将之前的示例分解为每个交互的具体情况开始。
7.1.2 服务间交互挑战
要将数据从一个服务发送到另一个服务,你必须知道另一个服务运行的位置以及它使用哪种协议来接收信息。因为我们处理的是分布式系统,我们还需要确保服务间的请求能够到达另一个服务,并具备处理意外网络问题或另一个服务可能失败的情况的机制。换句话说,我们需要在我们的服务中构建弹性。我们并不能总是信任网络或其他服务按预期行为。
让我们以服务 A 和服务 B 为例,深入探讨细节。在图 7.2 中,服务 A 需要向服务 B 发送一个请求。

图 7.2 服务间交互挑战
但让我们更深入地探讨服务内部可以使用的机制。假设我们暂时不考虑服务 A 依赖于服务 B 的合同(API)需要稳定且不改变的事实,以便于理解这个机制。还有什么可能出错?正如提到的,开发团队应该在他们的服务内部添加一个弹性层,以确保服务 A 的请求能够到达服务 B。实现这一功能的一种方法是通过框架在请求失败时自动重试。所有编程语言都有实现这一功能的框架。像go-retryablehttp(github.com/hashicorp/go-retryablehttp)或 Spring Boot 的 Spring Retry(github.com/spring-projects/spring-retry)这样的工具可以为你的服务间交互添加弹性。其中一些机制还包括指数退避功能,以避免在出现问题时过载服务和网络。
不幸的是,没有跨技术堆栈共享的标准库可以为所有应用程序提供相同的行为和功能,所以即使你使用相似的参数配置 Spring Retry 和go-retryablehttp,也很难保证它们在服务开始失败时会有相同的行为。

图 7.3 服务间交互重试机制
图 7.3 展示了使用 Spring Retry 库在 Java 中编写的服务 A,当请求未能被服务 B 确认时,它将重试三次,每次请求之间的等待时间为 3 秒。使用go-retryablehttp库编写的服务 C,用 Go 语言编写,配置为重试五次,但在出现问题时使用指数退避机制(请求之间的重试周期不是固定的;这可以为其他服务的恢复提供时间,并避免被重试请求淹没)。
即使应用程序是用相同的语言编写的并使用相同的框架,两个服务(A 和 B)也必须具有兼容的依赖项和配置版本。如果我们将服务 A 和服务 B 都推向使用框架的版本,这意味着我们将它们耦合在一起,这意味着每当这些内部依赖项版本中的任何一个发生变化时,我们都需要协调另一个服务的更新。这可能会导致更多的延迟并增加协调工作的复杂性。
注意:在本节中,我使用了重试机制作为示例,但请考虑您可能希望包括在这些服务间交互中的其他横切关注点,例如断路器(也用于弹性)、速率限制和可观察性。考虑您将需要添加到您的应用程序代码中以便从中获取指标的框架和库。
另一方面,为每个服务使用不同的框架(和版本)将使我们的操作团队对这些服务的故障排除变得复杂。如果有一种方法可以在不修改应用程序的情况下为我们的应用程序添加弹性,那岂不是很好?在回答这个问题之前,还有其他什么可能出错?
开发者经常忽视的一些事情与这些通信的安全性方面有关。服务 A 和服务 B 并不孤立存在,这意味着其他服务围绕着它们。如果这些服务中的任何一个被恶意行为者攻破,所有服务之间的自由服务间调用会使我们的整个系统不安全。这就是为什么拥有服务身份和正确的安全机制来确保,例如,服务 A 只能调用服务 B,对于图 7.4 所示的情况,极为重要。

图 7.4 如果一个服务被攻破,它可能会影响整个系统。
有了允许我们定义我们的服务身份的机制,我们可以定义哪些服务间调用是被允许的,以及允许哪些协议和端口进行通信。图 7.5 展示了我们如何通过定义规则来减少影响范围(如果发生安全漏洞,受影响的服务的数量),这些规则强制规定哪些服务允许在我们的系统中,以及它们应该如何交互。

图 7.5 通过定义系统级规则来减少影响范围
定义和验证这些规则的正确机制不能轻易地构建在每个服务内部。因此,开发者倾向于假设将有一个外部机制负责执行这些检查。
正如我们将在以下章节中看到的,服务身份是我们需要跨领域的东西,而不仅仅是对于服务间交互。如果有一种简单的方法可以将服务身份添加到我们的系统中而不改变我们的应用程序服务,那岂不是很好?
在回答这个问题之前,让我们看看团队在架构分布式应用程序时面临的其他挑战。让我们谈谈存储和读取状态,这是大多数应用程序都会做的。
7.1.3 存储/读取状态挑战
我们的应用程序需要从持久存储中存储或读取状态。这是一个相当常见的需求,对吧?你需要数据来进行一些计算,然后将结果存储在某个地方,以防应用程序崩溃时丢失。在我们的示例中,图 7.6,服务 B 需要连接到数据库或持久存储来读取和写入数据。

图 7.6 存储/读取状态挑战
这里可能会出什么问题?开发者习惯于连接到不同类型的数据库(关系型、NoSQL、文件、桶)并与它们交互。但是,有两个主要的摩擦点会减缓团队推进服务的步伐:依赖项和凭证。
让我们从查看依赖项开始。服务 B 需要什么类型的依赖项才能连接到数据库?图 7.7 显示了服务 B 连接到关系型数据库和 NoSQL 数据库。为了实现这些连接,服务 B 需要包含一个驱动程序和客户端库,以及用于微调应用程序如何连接到这两个数据库的配置。这些配置定义了连接池的大小(多少个应用程序线程可以同时连接到数据库)、缓冲区、健康检查以及其他可能改变应用程序行为的重要细节。

图 7.7 数据库依赖项和客户端版本
除了驱动程序和客户端的配置之外,它们的版本需要与我们所运行的数据库版本兼容,这正是挑战开始的地方。
注意:请注意,每个驱动程序/客户端都是针对您连接到的数据库(关系型或 NoSQL)特定的。本节假设您使用了特定的数据库,因为它符合您应用程序的需求。每个数据库供应商都有针对不同用例优化的独特功能。在本章中,我们更感兴趣的是不使用供应商特定功能的 95%的情况。
一旦应用程序的服务通过客户端 API 连接到数据库,与它交互应该相当简单。无论是通过发送 SQL 查询或命令来获取数据,还是使用键值 API 从数据库实例中读取键和值,开发者应该了解基础知识以开始读取和写入数据。
您是否拥有多个服务与同一数据库实例交互?它们是否都使用相同的库和版本?这些服务是否使用相同的编程语言和框架编写?即使您设法控制所有这些依赖项,仍然存在一种耦合关系,这将减慢您的速度。每当运维团队决定升级数据库版本时,连接到此实例的每个服务可能需要也可能不需要升级其依赖项和配置参数。您是先升级数据库还是依赖项?
对于凭据,我们面临类似的问题。从像 HashiCorp 的 Vault(www.vaultproject.io/)这样的凭据存储中消费凭据相当普遍。如果没有由平台提供并且不在 Kubernetes 中管理,应用程序服务可以轻松地从应用程序代码中包含一个依赖来消费凭据。图 7.8 显示了服务 B 通过特定的客户端库连接到凭据存储,以获取连接到数据库的令牌。

图 7.8 凭据存储依赖
在第二章和第五章中,我们使用 Kubernetes Secrets 将会议服务连接到不同的组件。通过使用 Kubernetes Secrets,我们消除了应用程序开发者担心从哪里获取这些凭据的需求。
否则,如果你的服务连接到其他服务或组件,这些服务或组件可能以这种方式需要依赖,那么服务将需要升级以适应任何组件的任何变化。这种服务代码和依赖之间的耦合创造了在应用程序开发团队、平台团队以及负责保持这些组件运行的操作团队之间进行复杂协调的需求。
我们能否消除一些这些依赖?能否将这些担忧推给平台团队,从而减少开发者更新它们的麻烦?如果我们通过一个干净的接口解耦这些服务,那么基础设施和应用可以独立更新。
在进入下一个主题之前,我想简要谈谈为什么在这一级别拥有服务身份也可以帮助减少与应用程序基础设施组件交互时的安全问题。图 7.9 展示了如何应用类似的服务身份规则来验证谁可以与基础设施组件交互。一旦服务被攻破,系统将再次限制影响范围。

图 7.9 基于服务身份执行规则
但异步交互怎么办?在深入解决方案空间之前,让我们看看这些挑战如何与异步消息传递相关联。
7.1.4 异步消息传递挑战
在使用异步消息传递时,你希望解耦生产者和消费者。当使用 HTTP 或 GRPC 时,服务 A 需要了解服务 B,并且两个服务都需要运行以交换信息。当使用异步消息传递时,服务 A 对服务 C 一无所知。你可以更进一步,服务 C 可能在服务 A 将消息放入消息代理时甚至没有运行。图 7.10 展示了服务 A 将消息放入消息代理;在稍后的某个时间点,服务 C 可以连接到消息代理并从中获取消息。

图 7.10 异步消息传递交互
与 HTTP/GRPC 服务之间的交互类似,当使用消息代理时,我们需要知道消息代理的位置,以便发送消息或订阅以接收消息。消息代理还提供隔离性,使应用程序能够使用主题的概念将消息分组在一起。服务可以连接到同一个消息代理实例,但可以从不同的主题发送和消费消息。
当使用消息代理时,我们面临与数据库描述的相同问题。我们需要根据我们决定使用的消息代理、其版本以及我们选择的编程语言,向我们的应用程序添加依赖项。消息代理将使用不同的协议来接收和发送信息。在这个领域越来越被采用的标准是 CNCF 的 CloudEvent 规范(cloudevents.io/)。虽然 CloudEvents 是一个巨大的进步,但它并不能免除你的应用程序开发者添加依赖项以连接和与你的消息代理交互。
图 7.11 展示了服务 A,它包括用于连接到 Kafka 并发送消息的 Kafka 客户端库。除了连接到 Kafka 实例的 URL、端口和凭证外,Kafka 客户端还会接收有关客户端在连接到代理时的行为配置,类似于数据库。服务 C 使用相同的客户端,但使用不同的版本,以连接到相同的代理。

图 7.11 依赖项和 API 挑战
消息代理面临与数据库和持久化存储相同的问题。但不幸的是,在使用消息代理时,开发者需要学习特定的 API,这些 API 可能一开始并不容易。使用不同的编程语言发送和消费消息,对于没有具体消息代理经验的团队来说,会带来更多的挑战和认知负荷。
与数据库类似,如果你选择了 Kafka,例如,这意味着 Kafka 符合你的应用程序需求。你可能会想使用其他消息代理不提供的 Kafka 高级功能。然而,让我在这里重申:我们对 95%的情况感兴趣,在这些情况下,应用程序服务想要交换消息以外部化状态并让其他感兴趣的各方知道。对于这些情况,我们希望从我们的应用程序团队中移除认知负荷,让他们能够轻松地发送和消费消息,而无需学习所选消息代理的所有具体细节。通过减少开发者学习特定技术所需的认知负荷,你可以让经验较少的开发者加入,并让专家处理细节。与数据库类似,我们可以使用服务身份来控制哪些服务可以连接、读取和从消息代理中写入消息。同样的原则适用。
7.1.5 处理边缘情况(剩余的 5%)
总是有不止一个很好的理由将库添加到你的应用程序服务中。有时这些库会给你控制如何连接到供应商特定组件和功能的最终权力。其他时候,我们添加库是因为这是开始的最简单方式,或者因为我们被指示这样做。组织中的某个人决定使用 PostgreSQL,最快的方法是将 PostgreSQL 驱动程序添加到我们的应用程序代码中。我们通常没有意识到我们正在将应用程序耦合到特定的 PostgreSQL 版本。对于边缘情况,或者更具体地说,需要使用某些供应商特定功能的情况,考虑将那个特定功能作为一个单独的单元封装起来,从你可能会从数据库或消息代理中消耗的所有通用功能中分离出来。

图 7.12 常见与边缘情况封装对比
我选择在图 7.12 中使用异步消息作为示例,但同样的情况也适用于数据库和凭证存储。如果我们能将 95%的服务解耦,使用通用能力来完成工作,并将边缘情况作为单独的单元封装,我们就能减少耦合和新团队成员对这些服务进行修改时的认知负荷。图 7.12 中的服务 A 正在使用平台团队提供的消息 API 异步地消费和发布消息。我们将在下一节更深入地探讨这种方法。但更重要的是,那些需要使用一些特定于 Kafka 的功能的边缘情况,例如,被提取到一个单独的服务中,服务 A 仍然可以通过 HTTP 或 GRPC 与之交互。请注意,消息 API 也使用 Kafka 来移动信息。然而,对于服务 A 来说,这已经不再相关,因为一个简化的 API 作为平台能力被暴露出来。
当我们需要更改这些服务时,95%的情况下,我们不需要团队成员担心 Kafka。消息 API 从我们的应用程序开发团队中移除了这种担忧。对于修改服务 Y,你需要 Kafka 专家,如果 Kafka 被升级,服务 Y 的代码也需要升级,因为它直接依赖于 Kafka 客户端。对于这本书,平台工程团队应该专注于尝试减少团队在常见情况下的认知负荷,同时允许团队为边缘情况和特定场景选择合适的工具,这些场景不适合常见解决方案。
以下部分将探讨一些方法来解决我们一直在讨论的一些挑战。然而,请记住,这些都是通用解决方案,在你的特定环境中可能还需要进一步的步骤。
7.2 将应用程序与基础设施分离的标准 API
如果我们将所有这些常用功能(存储和读取数据、消息传递、凭证存储、弹性策略)封装成开发者可以在其应用程序中使用以解决常见挑战的 API,同时,又使平台团队能够以不要求应用程序代码更改的方式连接基础设施,那会怎样呢?在图 7.13 中,我们可以看到相同的服务,但它们不是通过添加与基础设施交互的依赖项,而是使用 HTTP/GRPC 请求。

图 7.13 作为 API 的平台能力
假设我们公开了一组 HTTP/GRPC API,我们的应用程序服务可以消费这些 API。在这种情况下,我们可以从应用程序代码中移除供应商特定的依赖,并使用标准的 HTTP 或 GRPC 调用消费这些服务。
应用程序服务和平台能力之间的这种分离使得不同的团队能够处理不同的责任。平台可以独立于应用程序进行演变,并且应用程序代码现在将仅依赖于平台能力接口,而不是底层的组件版本。图 7.14 显示了由应用开发团队管理的应用程序代码(我们的三个服务)与由平台团队管理的平台能力之间的分离。

图 7.14 将应用开发团队和平台能力之间的责任解耦
当使用这里建议的方法时,平台团队能够扩展平台功能,为应用开发团队引入新的服务。更重要的是,他们可以这样做而不影响现有的应用程序或强迫它们发布新版本。这使得团队可以根据其功能和希望消费的能力来决定何时发布其服务的新版本。
通过遵循这种方法,平台团队能够为服务提供新的功能,并推广最佳实践。因为这些平台功能对所有服务都是可访问的,它们可以促进标准化,并在幕后实施最佳实践。每个团队可以根据可用的功能决定需要哪些能力来解决他们特定的难题。如果功能版本正确,团队可以决定如何以及何时升级到最新版本,允许团队根据自己的节奏进行迁移,而无需平台在每次有新版本可用时推动每个团队升级。
为了论证,假设平台团队决定向所有服务公开一致的功能标志能力。使用这项能力,所有服务都可以一致地定义和使用功能标志,而无需在它们的代码中添加任何东西,除了功能标志的条件检查。然后,团队可以一致地管理、可视化以及打开和关闭所有标志。平台团队引入并管理的能力,如功能标志,直接影响到开发者的性能,因为他们不需要担心在底层如何处理功能标志(持久性、刷新、一致性等),并且他们确信他们正在做的事情与其他服务保持一致。
图 7.15 展示了平台团队如何添加额外功能,例如,例如,功能标志,直接使团队能够在所有服务中统一使用这项新功能。不需要新的依赖项。

图 7.15 通过提供一致和统一的能力,如功能标志,来启用团队
在继续前进之前,这里有一个警告。让我们看看当你像前一个图所示的那样外部化能力(如 API)时,你将面临的一些挑战。
7.2.1 公开平台能力面临的挑战
为团队使用外部化 API 将需要首先稳定(且版本化)的合同,应用程序团队可以信任。当这些 API 发生变化时,所有使用这些 API 的应用程序都会崩溃,并且必须进行更新。平台团队可以采用非破坏性更改策略,确保对团队及其应用程序的向后兼容性。采用此类策略使你的平台更容易被消费,因为平台 API 和合同对团队来说是可靠的。
将依赖项添加到你的应用程序代码中,例如使用容器的一个主要优势是,对于本地开发,你始终可以使用 Docker 或 Docker Compose 启动一个 PostgreSQL 实例,并将你的应用程序本地连接到它。如果你转向平台提供的功能,你必须确保可以为你的团队提供本地开发体验,除非你的组织足够成熟,始终可以针对远程服务进行工作。
另一个重大区别是,你的服务和平台提供的 API 之间的连接将引入延迟并默认需要安全性。在此之前,调用 PostgreSQL 驱动 API 是在与你的应用程序相同的进程中进行的本地调用。HTTPS 或安全协议建立了与数据库本身的连接,但设置应用程序和数据库之间的安全通道是运维团队的责任。
在将这种方法应用于实际项目时,识别我们能够找到的所有边缘情况也是至关重要的。如果您想构建这些平台功能并推动您的团队使用它们,您需要确保始终为边缘情况留有通道,这样团队(甚至平台团队)就不会被迫使常见情况更加复杂,以适应仅会使用 1%时间的晦涩功能。图 7.16 显示了服务 A、B 和 C 通过平台能力 API 使用平台提供的功能。另一方面,服务 Y 对如何连接数据库有非常具体的要求,维护该服务的团队已决定绕过平台能力 API,直接使用数据库客户端连接到数据库。
将边缘情况单独处理,允许服务 A、B 和 C 独立于平台组件(数据库、消息代理、凭证存储)发展,而服务 Y 现在严重依赖于连接到的数据库,并需要客户端的特定版本。虽然这听起来很糟糕,但在实践中,这是可以接受的,应该被视为平台功能。那些无法使用公开 API 解决其业务问题的团队将讨厌这个平台,并默默地寻找解决方案。好的平台(和平台团队)将推广覆盖广泛用例的 API,解决并促进应用开发者常见功能的实现。如果这些 API 对所有团队来说都不够用,那么记录和深入理解边缘情况将导致新的 API 和平台功能,平台团队可以在未来的版本中实现。

图 7.16 处理边缘情况;不要忽略它们
以下部分将探讨几个 CNCF 倡议,这些倡议将这些想法向前推进,并帮助我们实现了大多数应用程序所需的平台功能。
7.3 提供应用级平台功能
在本节中,我们将探讨两个项目,这些项目可以帮助开发团队在标准化这些通用 API 方面节省时间,这些 API 是我们大多数应用程序所需的。我们将首先了解 Dapr 项目(dapr.io/),它是什么,如何工作,以及它能为我们的开发和平台团队带来什么。然后我们将探讨 OpenFeature(openfeature.dev/),这是一个 CNCF 倡议,为我们的应用程序提供适当的抽象,以便定义和使用功能标志,而无需绑定到特定的功能标志提供者。
一旦我们对这两个项目的工作原理以及它们如何通过提供应用级平台能力来相互补充有了基本的了解,我们将探讨这些项目如何应用于我们的会议应用程序,需要做出哪些改变,采用这种方法的优势,以及一些展示边缘情况的示例。让我们从 Dapr,我们的分布式应用程序运行时开始。
7.3.1 Dapr 在行动
Dapr 提供了一套一致的 API 来解决常见的和反复出现的分布式应用程序挑战。Dapr 项目在过去四年中实施了一套 API(称为构建块 API),以抽象出分布式应用程序在 95% 的时间里需要面对的常见挑战和最佳实践。由微软于 2019 年创建并于 2021 年捐赠给 CNCF,Dapr 项目拥有一个庞大的社区,他们通过扩展和改进项目 API 为项目做出贡献,使其成为 2023 年 CNCF 中增长最快的第 10 个项目。
Dapr 定义了一系列构建块,它们提供具体的 API 来解决分布式应用程序的挑战,以及平台团队可以配置的可互换实现。如果你访问 dapr.io 网站,你会看到构建块 API 的列表,包括服务调用、状态管理、发布/订阅、密钥存储、输入/输出绑定、演员、配置管理,以及最近的工作流。图 7.17 展示了 Dapr 官方网站描述的当前 Dapr 构建块 API,这些 API 可以供团队用来构建他们的分布式应用程序。更多信息请查看 docs.dapr.io/concepts/overview/ 上的 Dapr 概述页面。

图 7.17 用于构建分布式应用程序的 Dapr 组件
虽然 Dapr 执行的任务远不止暴露 API,但在本章中,我想要专注于项目提供的 API 以及项目用来使应用程序/服务能够消费这些 API 的机制。
因为这是一本关于 Kubernetes 的书,所以我们将从 Kubernetes 的角度来探讨 Dapr,但该项目也可以在 Kubernetes 集群之外使用,这使得 Dapr 成为一个通用的工具,无论你在哪里运行它们,都可以用来构建分布式应用程序。作为旁注,Dapr 目前是 Azure 容器应用服务的一部分(azure.microsoft.com/en-us/products/container-apps),在那里它与另一个 CNCF 项目 KEDA (keda.sh/) 配置在一起,用于自动扩展你的分布式应用程序。
7.3.2 Dapr 在 Kubernetes 中
Dapr 作为 Kubernetes 扩展或附加组件运行。您必须在您的 Kubernetes 集群上安装一组 Dapr 控制器(Dapr 控制平面)。图 7.15 显示了在安装了 Dapr 的 Kubernetes 集群中部署的服务 A。服务 A 需要添加两个注解:dapr.io/enabled: "true" 以让 Dapr 控制平面了解应用程序,以及 dapr.io/appid: "service-a" 以使用 Dapr 服务身份功能。
一旦在您的集群中安装了 Dapr,您在集群中部署的应用程序可以通过向您的部署添加一组注解来开始使用 Dapr API。这使 Dapr 控制平面服务能够理解您的应用程序想要使用 Dapr API,如图 7.18 所示。

图 7.18 带有 Dapr 注解的应用程序的 Dapr 控制平面监控器
默认情况下,Dapr 将所有 Dapr API 作为侧边车(daprd 是将运行在您的应用程序/服务容器旁边的容器)提供给您的应用程序/服务。使用侧边车模式,我们使我们的应用程序能够与位于应用程序容器附近的本地 API(localhost)交互,从而避免网络往返。图 7.19 显示了 Dapr 控制平面如何将 daprd 侧边车注入带有 Dapr 注解的应用程序。这使得应用程序能够访问配置的 Dapr 组件。

图 7.19 Dapr 侧边车(daprd)使您的应用程序能够本地访问 Dapr 组件。
一旦 Dapr 侧边车在您的应用程序/服务容器旁边运行,它就可以通过向 localhost 发送请求(使用 HTTP 或 GRPC)来使用 Dapr API,因为 daprd 侧边车与应用程序位于同一个 pod 中,共享相同的网络空间。
现在,为了让 Dapr API 有所作为,平台团队需要配置这些 API 的实现(或称为 Dapr 组件的后备机制)以使其工作。例如,如果您想从您的应用程序/服务中使用 Statestore Dapr API (docs.dapr.io/operations/components/setup-state-store/),您必须定义并配置一个 Statestore 组件。
当在 Kubernetes 上使用 Dapr 时,您可以使用 Kubernetes 资源配置 Dapr 组件规范。例如,您可以配置一个用于 Redis 的 Statestore Dapr 组件。请参阅列表 7.1 以获取 Dapr 组件资源定义的示例。
列表 7.1 Dapr Statestore 组件定义
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis ①
version: v1
metadata:
- name: keyPrefix
value: name
- name: redisHost ②
value: redis-master:6379
- name: redisPassword ③
secretKeyRef:
name: redis
key: redis-password
auth:
secretStore: kubernetes
① Statestore 组件 API 支持不同的实现,您可以在 docs.dapr.io/reference/components-reference/supported-state-stores/ 找到它们。在此示例中,我们正在设置 state.redis 实现。
② 通过设置 redisHost,平台团队可以定义 Redis 实例的位置。此实例无需位于 Kubernetes 集群内部;它可以是任何可访问的 Redis 实例。
③ redisPassword 属性(由 state.redis 实现所需)可以使用,如本例所示,通过 Kubernetes Secret 引用来获取密码。
如果组件资源在 Kubernetes 集群中可用,daprd边车可以读取其配置并连接到本例中的 Redis 实例。从应用的角度来看,无需知道是否使用了 Redis 或 Statestore 组件的任何其他实现。图 7.20 显示了 Dapr 组件是如何连接的,以便服务 A 可以使用 Statestore 组件 API。在本例中,通过调用本地 API,服务 A 将能够从 Redis 实例存储和读取数据。

图 7.20 Dapr 边车使用组件配置来连接到组件的基础设施。
Dapr 使得使用本地/自托管 Redis 实例构建应用变得容易,但随后可以将其迁移到云中,在那里可以使用托管的 Redis 服务。无需更改代码或依赖项,只需不同的 Dapr 组件配置。
您想在不同的应用之间发出和消费消息吗?您只需配置 Dapr PubSub 组件(docs.dapr.io/operations/components/setup-pubsub/)及其实现。现在,您的服务可以使用本地 API 发出异步消息。您想使所有服务交互(包括基础设施)调用都具有弹性吗?您可以使用 Dapr 弹性策略(docs.dapr.io/operations/resiliency/policies/)来避免在应用代码中编写自定义逻辑。
图 7.21 显示了服务 A 和服务 B 如何使用服务调用 API 相互发送请求,而不是直接调用其他服务。使用这些 API(通过daprd边车发送流量)使平台团队能够在平台级别配置弹性策略,统一配置,无需添加任何依赖或更改应用代码。

图 7.21 Dapr 启用服务可以使用服务间通信和弹性策略。
好的,所以 Dapr 控制平面将注入 Dapr 边车(daprd)到感兴趣使用 Dapr 组件的应用程序中。但从应用的角度来看,这看起来是怎样的呢?
7.3.3 Dapr 与您的应用
如果我们回到上一节中介绍的示例,其中服务 A 想要使用 Statestore 组件从持久化存储(如 Redis)中存储/读取一些数据,应用程序代码很简单。无论你使用哪种编程语言,只要你知道如何创建 HTTP 或 GRPC 请求,你就拥有了与 Dapr 一起工作的所有所需。
例如,要使用 Statestore API 存储数据,你的应用程序代码需要向以下端点发送 HTTP/GRPC 请求:
http://localhost:<DAPR_HTTP_PORT>/v1.0/state/<STATESTORE_NAME>
使用 curl,请求看起来是这样的,其中 -d 显示我们想要持久化的数据,3500 是默认的 DAPR_HTTP_PORT,我们的 Statestore 组件名为 statestore:
> curl -X POST -H "Content-Type: application/json"
➥-d '[{ "key": "name", "value": "Bruce Wayne"}]'
➥http://localhost:3500/v1.0/state/statestore
要读取我们已持久化的数据,我们只需发送一个 GET 请求,而不是发送 POST 请求。使用 curl,它看起来是这样的:
curl http://localhost:3500/v1.0/state/statestore/name
通常,你不会在应用程序内部使用 curl。你会使用你的编程语言工具来编写这些请求。所以,如果你使用 Python、Go、Java、.NET 或 JavaScript,你可以在网上找到使用流行库或内置机制编写这些请求的教程。
另一个选项是使用适用于不同编程语言的 Dapr SDK(软件开发工具包)。将 Dapr SDK 添加到你的应用程序作为依赖项,可以使开发者的生活更轻松,他们不需要手动构建 HTTP 或 GRPC 请求。重要的是要注意,虽然你现在正在向应用程序添加一个新的依赖项,但这个依赖项是可选的,并且仅用作辅助工具以加快速度,因为这个依赖项与 Dapr API 交互的任何基础设施组件都没有关联。
检查 Dapr 网站,了解如果你使用 Dapr SDK,你的代码将如何看起来。例如,对于使用 SDKs 的多编程语言示例,了解如何使用 Statestore 组件,你可以访问 docs.dapr.io/getting-started/quickstarts/statemanagement-quickstart/。
当我决定专注于 Dapr 进行 API 抽象时,Dapr 提供了更多功能。通过允许平台团队交换 Dapr 组件的实现,应用程序可以在不更改任何应用程序代码的情况下跨云提供商迁移。默认情况下,整个系统是可观察的 (docs.dapr.io/operations/observability/),安全的 (docs.dapr.io/operations/security/),并且具有弹性 (docs.dapr.io/operations/resiliency/),因为 Dapr 侧车将强制执行服务身份和平台团队指定的规则,同时从所有启用了 Dapr 的应用程序和组件中提取指标。我建议平台团队熟悉 Dapr 项目,因为这个项目是为了解决团队在处理分布式应用程序时将面临的一些常见挑战而构建的。查看本章 7.3.5 节,了解我们如何使我们的会议应用程序启用 Dapr。现在让我们谈谈功能标志。
7.3.4 功能标志的实际应用
功能标志使团队能够发布包含新功能的软件,同时不立即使这些功能可用。新功能可以隐藏在可以稍后启用的功能标志后面。换句话说,功能标志允许团队持续部署其服务或应用程序的新版本,一旦这些应用程序运行,可以根据公司的需求开启或关闭功能。
与直接为开发者提供开箱即用行为的 API 相比,功能标志可以启用其他团队,这些团队在何时启用功能方面做出业务相关决策,并将这些决策传达给客户。
虽然大多数公司可能会构建机制来实现功能标志,但将其封装到专用服务或库中是一个公认的通用模式。在 Kubernetes 世界中,你可以考虑使用 ConfigMaps 作为参数化容器的最简单方法。一旦你的容器能够读取环境变量来开启和关闭功能,你就可以开始了。我们在第二章中使用了这种方法,通过 FEATURE_DEBUG_ENABLED=true 环境变量。
不幸的是,这种方法过于简单,不适用于现实世界的场景。首先,一个主要原因是如果 ConfigMap 发生变化,你的容器将需要重新启动以重新读取其内容。其次,你可能需要为不同的服务设置许多标志,因此你可能需要多个 ConfigMaps 来管理你的功能标志。第三,如果你使用环境变量,你需要制定一个约定来定义每个标志的状态、默认值和类型,因为你不能仅仅通过定义变量作为普通字符串来解决问题。
由于这是一个众所周知的问题,一些公司已经推出了工具和管理服务,如 LaunchDarkly (launchdarkly.com/) 和 Split (www.split.io/product/feature-flags/) 等,这些服务使团队能够在提供简化访问以查看和修改功能标志的远程服务中托管其功能标志,而无需技术知识。对于这些服务中的每一个,要获取和评估复杂的功能标志,您需要下载并将依赖项添加到您的应用程序中。由于每个功能标志提供者将提供不同的功能,因此在不同提供者之间切换将需要许多更改。
OpenFeature (openfeature.dev/) 是一个 CNCF 倡议,旨在统一在云原生应用程序中消费和评估功能标志的方式。与 Dapr 抽象化与 Statestores(存储和读取状态)或 PubSub(异步消息代理)组件交互的方式相同,OpenFeature 提供了一个一致的 API,无论我们使用哪个功能标志提供者,都可以消费和评估功能标志。
在本节中,我们将通过一个简单示例查看使用 ConfigMap 来保存一组功能标志定义的情况。我们还将使用 OpenFeature 提供的flagd实现,但这种方法的美妙之处在于,您可以在不更改应用程序中任何一行代码的情况下,轻松地更换存储功能标志的提供者。
图 7.22 展示了包含配置为连接到 OpenFeature 提供者的 OpenFeature SDK 的简单应用实例——在这种情况下,flagd负责托管我们的功能标志定义。

图 7.22 从我们的应用程序服务消费和评估功能标志
在这个简单的示例中,我们的应用程序是用 Go 编写的,并使用 OpenFeature Go SDK 从flagd服务获取功能标志。本例中的flagd服务配置为监视包含一些复杂功能标志定义的 Kubernetes ConfigMap。
虽然这是一个简单的示例,但它使我们能够看到像flagd这样的服务如何使我们能够抽象出作为我们平台一部分提供功能标志能力所需的所有存储和实现机制的复杂性。
与 Dapr 相比,OpenFeature SDK 是必需的,因为我们不仅获取功能标志定义,还执行可能涉及复杂功能标志的评估。
您可以将应用程序中的每个服务连接到 OpenFeature 提供者以执行功能标志评估。与仅使用纯 ConfigMap 相比,一个重要的区别是,通过使用 OpenFeature,如果容器中的值发生变化,则不需要重新启动容器来获取值;现在这是 OpenFeature 标志提供者的责任。
在下一节中,我们将探讨如何将 Dapr 和 OpenFeature 应用于会议应用程序的原型。
7.3.5 更新我们的会议应用程序以使用应用程序级平台功能
从概念上和平台角度来看,在不泄露实现不同行为所使用的工具的情况下,使用所有这些功能将非常棒。这将使平台团队能够更改/交换实现,并减少使用这些功能的团队的认知负荷。但正如我们与 Kubernetes 讨论的那样,了解这些工具的工作原理、它们的行为以及它们的功能是如何设计的,会影响我们构建应用程序和服务的方式。在本章的最后部分,我想展示像 Dapr 和 OpenFeature 这样的工具如何影响你的应用程序架构,同时展示这些工具如何提供构建块来创建更高级的抽象,以减少消费者的认知负荷。
对于我们的会议应用程序,我们可以使用以下 Dapr 组件,因此让我们关注这些:
-
Dapr 状态存储组件:使用状态存储组件 API 可以使我们从会议应用程序中包含的议程服务中移除 Redis 依赖。如果出于某种原因,我们想用另一个持久存储替换 Redis,我们将能够做到这一点,而无需更改任何应用程序代码。
-
Dapr PubSub 组件:对于发出事件,我们可以用 PubSub 组件 API 替换所有服务中的 Kafka 客户端,使我们能够测试不同的实现,例如 RabbitMQ 或云提供商服务,以在应用程序之间交换异步消息。
-
Dapr 服务间调用和 Dapr 弹性策略:如果我们使用服务间调用 API,我们可以在不向我们的服务代码添加库或自定义代码的情况下配置服务之间的弹性策略。默认情况下,如果没有提供自定义配置,所有服务都有弹性策略定义。
虽然我们可以选择使用状态存储组件 API 也从我们的提案征集服务中移除 PostgreSQL 依赖,但我选择不这样做,以支持团队为该服务所需的 SQL 和 PostgreSQL 功能。在采用 Dapr 时,你必须避免采取“全有或全无”的方法。
让我们看看如果我们决定使用 Dapr,应用程序将如何改变。图 7.23 显示了使用 Dapr 组件的应用程序服务,因为所有服务都被注释为使用 Dapr,并且 daprd 伴随容器已经注入到所有服务中。一旦配置了 PubSub 和状态存储组件,它们就可以被提案征集服务、议程服务和通知服务访问。最后,Dapr 订阅将事件推送到前端应用程序。

图 7.23 使用 Dapr 组件构建我们的原型/会议应用程序
可以为“提案征集”服务配置和定义弹性策略,以与议程和通知服务交互,如图 7.24 所示。

图 7.24 显示了服务到服务的交互可以通过 daprd 侧边车来处理,允许平台团队能够定义不同的弹性策略。
如果我们不进行配置,Dapr 将应用默认的弹性策略。这些弹性策略也适用于我们的示例,例如,联系 statestore 和 pubsub 组件。这意味着不仅我们的服务到服务的调用是弹性的,而且每次我们的应用程序代码想要与数据库、缓存和消息代理等基础设施组件交互时,弹性策略都会启动。
应用程序代码需要稍作修改,因为当服务想要相互通信时,它们需要使用 Dapr API 来使用弹性策略。
最后,因为我们希望所有服务都能使用功能标志,所以每个服务现在都包含了 OpenFeature SDK,这使得平台团队能够定义所有服务将使用哪种功能标志实现。
在图 7.25 中,每个服务都包含了 OpenFeature SDK 库,并配置为指向 flagd 服务,这使得平台团队能够配置用于存储、检索和管理所有服务使用的所有功能标志的机制。

图 7.25 显示了使用 flagd 功能标志提供者的服务。
使用 OpenFeature SDK,我们可以更改功能标志提供者,而无需更改我们的应用程序代码。OpenFeature SDK 现在标准化了我们服务代码的所有功能标志消费和评估。
虽然 Dapr 中使用 SDK 是可选的(因为你可以手动制作 HTTP 或 GRPC 请求),但在 OpenFeature 中,情况要复杂一些。因为 SDKs 提供了一些评估逻辑,以了解每个标志的类型以及它是开启还是关闭。
逐步教程(github.com/salaboy/platforms-on-k8s/tree/v2.0.0/chapter-7)部署了使用 Dapr 和 OpenFeature 标志的会议应用程序的 v2.0.0 版本,以便应用程序团队能够不断演进应用程序服务。应用程序服务的 v2.0.0 版本不包括 Kafka 或 Redis 客户端以与基础设施交互。这些服务可以在不同的环境中(包括云提供商)部署,并针对这些标准 API 的不同实现进行连接。图 7.26 显示了我们使用 Dapr 组件 API 为应用程序的 v2.0.0 版本管理的依赖关系。

图 7.26 显示了从服务的依赖中移除了 Kafka 和 Redis 客户端。
从平台的角度来看,Dapr Statestore 组件、Dapr PubSub 组件和 Dapr 订阅定义了三个 Kubernetes 资源。
我们已经在 7.3.1 节中看到如何定义 Dapr Statestore 组件。在列表 7.2 中,我们可以看到如何定义 PubSub 组件,在这种情况下选择类型为 pubsub.kafka,它使用通过 Helm 安装的 Kafka 实例。
列表 7.2 Dapr PubSub 组件定义
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: conference-pubsub
spec:
type: pubsub.kafka
version: v1
metadata:
- name: brokers ①
value: kafka.default.svc.cluster.local:9092
- name: authType ②
value: "none"
① 我们需要指定 PubSub 组件可连接到的 Kafka 代理。
② 默认情况下,Bitnami 提供的 Kafka Helm 图表不需要身份验证。
您可以在官方 Dapr 网站上找到所有支持的 PubSub 实现(docs.dapr.io/reference/components-reference/supported-pubsub/)。最后,Dapr 订阅资源允许我们声明性地配置对 PubSub 组件的订阅并将事件路由到应用程序的端点,如列表 7.3 所示。
列表 7.3 Dapr 订阅定义
apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
name: frontend-subscription
spec:
pubsubname: conference-pubsub ①
topic: events-topic ②
route: /api/new-events/ ③
scopes:
- frontend ④
① 我们想要注册订阅的 PubSub 组件
② 订阅将监听的 PubSub 组件内的主题
③ Dapr 将事件转发到的路由
④ 范围允许我们定义哪些 Dapr 应用程序被允许接收此订阅的事件。在这种情况下,唯一的消费者是前端应用程序。范围严重依赖于服务身份来阻止消息转发到未经授权的服务。
从应用程序开发人员的角度来看,v2.0.0 版本中的更改使用 Dapr Go SDK 调用 Dapr 组件 API。例如,要从 Statestore 组件读取状态,Agenda 服务执行列表 7.4 中所示的调用。
列表 7.4 使用 Dapr SDK 从 Statestore 获取状态
s.APIClient.GetState(ctx,
STATESTORE_NAME, ①
KEY, ②
nil)
① 要存储状态,您只需提供在 Dapr 中配置的 Statestore 组件名称。
② 您还需要提供您想要从 Statestore 中检索的密钥。
此处的 APIClient 实例只是一个提供与 DAPR HTTP 和 GRPC API 交互的辅助器的 Dapr 客户端。同样,要存储状态,您可以使用 SaveState 方法;请参阅列表 7.5。
列表 7.5 使用 Dapr SDK 从 Statestore 保存状态
s.APIClient.SaveState(ctx,
STATESTORE_NAME, ①
KEY, ②
jsonData, ③
nil)
① 与之前相同,我们需要提供 Statestore 组件名称。请注意,应用程序可以访问多个 Statestore 组件,用于不同的目的。
② KEY 将用于存储有效负载,然后可以通过调用 GetState 方法检索。
③ 状态作为 JSON 有效负载发送到 API。
最后,采用完全相同的方法,应用程序可以通过使用列表 7.6 中所示的 API 将事件发布到 PubSub 组件。
列表 7.6 使用 Dapr SDK 发布事件
s.APIClient.PublishEvent(ctx,
PUBSUB_NAME, ①
PUBSUB_TOPIC, ②
eventJson) ③
① 要发布事件,我们需要指定我们想要使用的 Dapr PubSub 组件以及主题。
② 该主题使我们能够将 PubSub 组件划分为不同的逻辑桶,应用程序可以使用这些桶来交换事件和消息。
③ 事件有效负载以 JSON 格式表示。
在 OpenFeature 方面,功能标志配置定义在一个 ConfigMap 中 (github.com/salaboy/platforms-on-k8s/blob/v2.0.0/conference-application/helm/conference-app/templates/openfeature.yaml#L49)。教程展示了添加到会议应用程序中的三个不同功能标志,以控制前端和后端功能。通过修改包含标志定义的 ConfigMap,我们可以更改应用程序行为,而无需重新启动任何容器。列表 7.7 中的 eventsEnabled 功能标志显示了一个包含每个服务属性的 Object 类型功能标志。通过定义不同的变体,我们可以将配置文件编码化,从而允许我们定义复杂的场景。
列表 7.7 功能标志定义,包括变体
"eventsEnabled": {
"state": "ENABLED",
"variants": {
"all": {
"agenda-service": true,
"notifications-service": true,
"c4p-service": true
},
"decisions-only": {
"agenda-service": false,
"notifications-service": false,
"c4p-service": true
},
"none": {
"agenda-service": false,
"notifications-service": false,
"c4p-service": false
}
},
"defaultVariant": "all"
列表 7.7 展示了一个对象功能标志,它定义了三个变体:all、decisions-only 和 none。通过更改 defaultVariant 属性,我们可以更改所选的配置文件,在这种情况下是启用和禁用哪些服务将发出事件。在议程服务源代码内部,我们使用 OpenFeature GO SDK 来获取和评估标志,如下所示。
列表 7.8 使用 OpenFeature SDK 进行功能标志评估
s.FeatureClient.ObjectValue(ctx, "eventsEnabled",
EventsEnabled{},
openfeature.EvaluationContext{})
列表 7.8 展示了使用 OpenFeature 客户端获取 eventsEnabled 功能标志。EventsEnabled{} 结构是当获取功能标志出现问题时应该返回的默认值。最后,EvaluationContext 结构允许你为 OpenFeature 添加额外的参数,以便在更复杂的场景中评估标志。
你可以通过比较应用程序存储库中的 main 分支和 v2.0.0 分支来找到 v1.0.0 和 v2.0.0 之间的差异。github.com/salaboy/platforms-on-k8s/compare/v2.0.0。同时,平台团队可以自由配置和连接应用程序基础设施,并定义所有支持机制和功能标志、存储、消息传递、配置、管理凭证、弹性和其他他们不想直接向开发者暴露的常见挑战的实现。
7.4 返回到平台工程
在本章中,我们看到了如何以 API 的形式为具有平台级能力的团队启用功能。我们旨在通过为团队提供解决日常挑战的通用和标准 API 来加速他们编写和交付复杂软件的过程,这些挑战包括创建分布式应用程序和机制,如功能标志。
通过将应用基础设施与应用代码分离,我们不仅从我们的服务中移除了依赖,还使平台团队能够决定如何配置应用基础设施组件以及服务如何连接到它们。如果不同的环境需要不同的实现,平台团队能够在 API 背后工作,为不同的场景提供不同的配置。
图 7.27 展示了我们如何减少与应用基础设施相关的摩擦和依赖。这允许我们的应用服务在各种平台团队能够控制的环境中工作。使用像 Dapr 这样的项目,你还可以获得跨云提供商的应用可移植性,一致的 API,这些 API 可以从任何编程语言中使用,并使团队能够将他们的应用从本地开发环境带到生产环境,允许平台团队能够连接起应用运行所需的基础设施。通过功能标志,我们使开发者能够通过隐藏在可开关的功能标志背后的功能来持续发布软件,从而让更接近客户的团队(如产品团队)决定何时应该公开这些功能。

图 7.27 在环境中提供一致的能力,使通往生产的路径更加顺畅。
通过在环境中提供一致的能力,我们使通往生产的路径更加容易,因为我们可以在将新版本发布到生产后控制向客户公开哪些功能。开发者可以继续构建功能,依赖于平台提供的应用级 API,而无需知道可用的基础设施在哪里,或者生产环境中使用了哪些数据库和消息代理的版本。
为了节省空间,关于可观察性、指标和日志,以及服务网格等主题在这些部分中没有涉及,因为这些功能目前更加成熟,更侧重于运营。我决定专注于建立在运营和基础设施团队之上的功能,以加快开发团队的进度并解决日常挑战。平台团队将提前定义他们将在各个环境中使用的可观察性堆栈,以及这些数据如何可供开发者在解决问题时使用。在讨论中经常提到的服务网格和用于相互 TLS(服务之间的加密)的证书轮换工具,因为这些是开发团队不希望花费时间的话题,并且应该在平台级别提供。图 7.28 展示了我们的平台如何负责定义、检索和聚合每个环境中可用的工具数据。我们的平台应提供一个单一的入口点来了解不同环境中正在发生的事情,并为团队提供足够的信息来解决问题并访问组织交付软件给客户所需的工具。

图 7.28 我们构建的平台需要定义、管理和监控每个环境中可用的工具。
下一章将探讨使团队能够在发布软件时进行实验的工具。沿着使用功能标志的相同思路,我们将更深入地探讨如何使用不同的发布策略来在发布过程中更早地发现问题,并使利益相关者能够同时尝试不同的方法。
摘要
-
将依赖项移动到应用程序基础设施中,使应用程序代码能够保持对平台级升级的无感知。将应用程序和基础设施的生命周期分开,使团队能够依赖稳定的 API,而不是在日常用例中处理特定供应商的客户端和驱动程序。
-
将边缘情况单独处理,使专家能够根据其应用需求做出更自觉的案例。这也允许经验较少的团队成员处理常见场景,当他们只想从应用程序代码中存储或读取数据或发出事件时,他们不需要了解工具的具体细节,例如供应商特定的数据库功能或低级消息代理配置。
-
Dapr 在构建分布式应用程序时解决常见和共享的担忧。能够编写 HTTP/GRPC 请求的开发者可以与平台团队将连接的基础设施进行交互。
-
功能标志使开发者能够通过隐藏在可以开启和关闭的功能标志后面的新功能来持续发布软件。
-
OpenFeature 标准化了应用程序消费和评估功能标志的方式。依赖 OpenFeature 抽象允许平台团队决定功能标志的存储位置以及如何管理它们。不同的提供商可以为非技术人员提供仪表板,让他们可以查看和操作标志。
-
如果你遵循了逐步教程,你将在云原生应用程序的上下文中获得使用工具如 Dapr 和 OpenFeature 的实践经验,该应用程序由四个服务组成,这些服务与 SQL 和 NoSQL 数据库以及像 Kafka 这样的消息代理进行交互。你还修改了运行中的应用程序上的功能标志,以改变其行为而无需重新启动其任何组件。
8 平台功能 II:使团队能够进行实验
本章节涵盖
-
通过提供发布策略功能来使团队能力得到提升
-
识别使用 Kubernetes 内置机制实现发布策略的挑战
-
使用 Knative Serving 高级流量管理来发布我们的云原生应用程序
-
利用 Argo Rollouts 的开箱即用发布策略
在第七章中,我们探讨了如何通过为开发团队提供应用级 API 来降低开发者解决常见分布式应用程序挑战的认知负荷,同时使平台团队能够连接和配置这些组件,以便应用程序可以访问。我们还评估了使用功能标志,使开发者能够继续发布新功能,并使更接近业务的团队决定何时将这些新功能向客户公开。
在本章中,我们将探讨引入不同的发布策略如何帮助组织在流程中更早地捕捉错误,验证假设,并使团队能够同时实验同一应用程序的不同版本。
我们希望避免团队担心部署您服务的全新版本,因为这会减慢您的发布节奏,并给参与发布过程的每个人带来压力。降低风险并拥有适当的机制来部署新版本,可以极大地提高对系统的信心。它还缩短了从请求更改到在用户面前上线的时间。带有修复和新功能的全新发布与商业价值直接相关,因为软件如果不为我们公司的用户提供服务,就没有价值。
虽然 Kubernetes 内置资源如部署、服务和入口为我们提供了部署和向用户公开服务的基本构建块,但为了实现众所周知的发布策略,必须进行大量手动且容易出错的工作。因此,云原生社区创建了专门的工具,通过提供机制来实现本章中我们将讨论的最常见的发布策略模式,以帮助团队提高生产力。本章节分为三个主要部分:
-
发布策略基础:
-
金丝雀发布、蓝/绿部署和 A/B 测试
-
使用 Kubernetes 内置机制的限制和复杂性
-
-
Knative Serving:自动缩放、高级流量管理和发布策略
-
Knative Serving 简介
-
使用 Knative Serving 和 Conference 应用程序的实际发布策略
-
-
Argo Rollouts:使用 GitOps 自动化的发布策略
-
介绍 Argo Rollouts
-
Argo Rollouts 和渐进式交付
-
本章的第一部分从高层次概述了最常见且记录良好的发布策略,我们将快速探讨为什么使用 Kubernetes 构建块实现这些发布策略可能具有挑战性。第 8.2 节探讨了 Knative Serving,它提供了更高层次的构建块,极大地简化了实现这些发布策略的方法,同时为我们的工作负载提供高级流量管理和动态自动缩放。第 8.3 节介绍了 Argo Rollouts,这是 Argo 家族的另一个项目,专注于为团队提供开箱即用的发布策略和渐进式交付。让我们开始介绍发布策略的基础。
8.1 发布策略基础
如果你寻找团队在推广服务到敏感环境时最常采用的发布策略,你会发现有金丝雀发布、蓝绿部署和 A/B 测试。每种发布策略都有不同的目的,并且可以应用于各种场景。在接下来的简短部分中,我们将探讨每种发布策略的预期效果,这些机制实施后的预期好处,以及它们与 Kubernetes 的关系。让我们首先了解一下金丝雀发布。
8.1.1 金丝雀发布
在金丝雀发布中,我们希望使团队能够部署服务的新版本,并完全控制有多少实时流量被路由到这个新版本。这允许团队缓慢地将流量路由到新版本以验证在将所有生产流量路由到它之前没有引入任何问题。
图 8.1 显示了用户访问我们的软件,其中 95%的请求被转发到我们已知稳定的版本,只有 5%被转发到服务的新版本。

图 8.1 显示了将 5%的流量路由到新版本(金丝雀)的服务新版本发布
“金丝雀发布”这个术语来自煤矿工人,他们使用金丝雀鸟来警告他们当有毒气体达到危险水平时。在这种情况下,我们的金丝雀发布可以帮助我们在新版本引入问题或回归的早期阶段就识别出来,而将 100%的流量回滚到稳定版本并不包括完整的部署。
在 Kubernetes 的上下文中,如图 8.2 所示,你可以通过使用两个 Kubernetes 部署资源(一个用于稳定版本,一个用于新版本)和一个匹配这两个部署的单个 Kubernetes 服务来实现一种金丝雀发布。如果每个部署只有一个副本,则流量将平分,即 50%和 50%。向每个版本添加更多副本将创建不同的流量分割百分比(例如,稳定版本有三个副本,而新版本只有一个副本,将给出 75%到 25%的流量分割比率),因为 Kubernetes 服务使用轮询方式将请求路由到所有匹配服务标签的 Pod。

图 8.2 使用两个部署和一个服务在 Kubernetes 中进行金丝雀发布。
工具如 Istio (istio.io/) 或 Linkerd (linkerd.io/) 服务网格可以让你更精细地控制流量如何路由到每个服务。我强烈建议你查看马丁·福勒的网站,其中更详细地解释了这种发布策略,网址为 martinfowler.com/bliki/CanaryRelease.xhtml。
8.1.2 蓝绿部署
使用蓝绿部署,我们的目标是让团队能够在两个并行运行的服务或应用程序版本之间切换。这个并行版本可以作为测试的预发布实例,当团队足够自信时,他们可以将流量切换到这个并行实例。这种方法给团队提供了在新版本开始出现问题时,有另一个实例准备好的安全性。这种方法需要足够的资源同时运行两个版本,这可能很昂贵,但它给了你的团队在具有与生产工作负载相同资源的实例上进行实验的自由。
图 8.3 展示了内部团队在测试服务新版本的生产环境配置。每当这个新版本准备就绪时,团队可以决定将生产流量切换到新版本,同时仍然保留稳定的版本以备出错时回滚。

图 8.3 蓝绿部署与生产级设置并行运行,允许团队在他们对新版本有信心时切换流量。
在 Kubernetes 的上下文中,你可以通过使用两个 Kubernetes 部署资源和 Kubernetes 服务来实现蓝绿部署,但在此情况下,服务应仅匹配单个部署的 pod。将服务配置更新为匹配绿色部署的标签将自动将流量切换到新版本。
图 8.4 展示了通过将服务的 matchLabel 更改为“绿色”,流量将自动路由到服务的新版本。在此同时,为了测试,内部团队可以使用不同的服务来匹配新版本的部署。

图 8.4 显示蓝绿部署并行运行。服务 matchLabel 用于定义请求的路由位置。
再次强调,我强烈建议你查看马丁·福勒关于蓝绿部署的网站 (martinfowler.com/bliki/BlueGreenDeployment.xhtml),因为那里有链接和更多可能对你有用的上下文。
8.1.3 A/B 测试
A/B 测试与金丝雀发布和蓝绿部署不同,因为它更关注最终用户而不是内部团队。通过 A/B 测试,我们希望使业务相关的其他团队能够尝试不同的方法来解决业务问题。例如,有两个不同的页面布局以查看哪一个对用户更有效,或者有不同的注册流程以验证哪一个花费用户更少的时间并引起更少的挫败感。正如第七章中讨论的功能标志一样,我们希望使其他团队(而不仅仅是开发者)能够进行实验,在这种情况下,通过为不同的用户组提供不同版本的应用程序。然后,这些团队可以验证每个功能的有效性,并决定保留哪一个。
图 8.5 展示了两种不同的服务实现,为用户提供替代的注册流程。通过 A/B 测试,我们可以同时运行这两种实现并收集数据,以便业务团队决定哪种选项效果更好。

图 8.5 A/B 测试使业务相关的团队能够评估不同的方法并收集数据,以便做出改善业务成果的决策。
由于 A/B 测试不是一种技术发布策略,它可以根据应用程序的需求以不同的方式实现。拥有两个独立的 Kubernetes 服务和部署来运行和访问同一应用程序的两个不同版本是有意义的。图 8.6 展示了使用两个 Kubernetes 服务和两个部署将用户路由到同一功能的不同版本。它还显示需要一个应用程序级别的路由器来定义用户如何路由到每个替代方案。

图 8.6 A/B 测试需要一些业务和应用级别的规则来定义如何将用户路由到不同的选项。
A/B 测试可以使用与金丝雀发布类似的机制实现,我们将在以下几节中探讨几个选项。"Continuous Delivery" by Jez Humble and David Farley (Addison-Wesley Professional, 2010)详细介绍了这些发布策略,所以我强烈建议你查看那本书。
8.1.4 使用内置 Kubernetes 构建块的局限性和复杂性
金丝雀发布、蓝绿部署和 A/B 测试可以使用内置的 Kubernetes 资源实现。但正如你所见,这需要创建不同的部署、更改标签和计算请求的百分比分布所需的副本数量,这是一项相当重大且容易出错的任务。即使你使用 GitOps 方法,如第四章中展示的 ArgoCD 或其他类似工具,创建所需资源并配置正确也是相当困难且需要大量工作。
我们可以总结使用 Kubernetes 构建块实现这些模式的缺点如下:
-
手动创建更多的 Kubernetes 资源,例如部署、服务和入口规则,以实现这些不同的策略可能会出错且繁琐。实施发布策略的团队必须了解 Kubernetes 的行为,以实现预期的输出。
-
开箱即用的自动化机制不提供协调和实施每个发布策略所需资源的功能。
-
它们可能会出错,因为需要同时在不同资源中应用多个更改,以确保一切按预期工作。
-
假设我们注意到我们的服务需求有所增加或减少。在这种情况下,我们需要手动更改部署的副本数量或安装和配置自定义自动缩放器(关于这一点,本章后面会详细介绍)。不幸的是,如果您将副本数量设置为 0,则不会有任何实例来响应请求,这意味着您至少需要一直运行一个副本。
开箱即用的 Kubernetes 不包括任何自动化或简化这些发布策略的机制,如果您处理许多相互依赖的服务,这会迅速成为一个问题。
注意:有一点很清楚:您的团队需要了解 Kubernetes 对 12 因素应用施加的隐含合同以及他们的服务 API 如何演变,以避免停机。您的开发者需要了解 Kubernetes 内置机制的工作原理,以便更好地控制应用程序的升级。
如果我们想降低发布新版本的风险,我们希望赋予我们的开发者这些发布策略,以便他们在日常实验中使用。
在接下来的章节中,我们将探讨 Knative Serving 和 Argo Rollouts,这些是在 Kubernetes 之上构建的工具和机制,旨在简化我们在尝试设置 Kubernetes 构建块以使具有不同发布机制的团队受益时遇到的所有手动工作和限制。让我们首先从 Knative Serving 开始,它通过一系列构建块扩展了我们的 Kubernetes 集群,简化了之前描述的发布策略的实施。
8.2 Knative Serving:高级流量管理和发布策略
Knative 是那些一旦了解其能为您做什么就很难不使用的科技之一。在与该项目合作了近三年并观察其某些组件的演变后,每个 Kubernetes 集群都应该安装 Knative Serving;您的团队会感激它的。Knative Serving 是一个 Kubernetes 扩展,它在上层 Kubernetes 内置资源之上提供高级抽象,以实现良好的实践和常见模式,使您的团队能够更快地工作并对其服务拥有更多控制。
虽然本章重点介绍发布策略,但如果您对以下主题感兴趣,您应该考虑研究 Knative Serving:
-
为您的团队提供容器即服务的方法。
-
为您的负载提供动态自动缩放,为您的团队提供一种函数即服务(Functions-as-a-Service)的方法。Knative Serving 会安装自己的自动缩放器,该缩放器对所有 Knative 服务自动可用。
-
为您的服务提供高级和细粒度的流量管理。
正如本节标题所指定的,以下章节将重点介绍 Knative 提供的功能子集,称为 Knative Serving。Knative Serving 允许您定义Knative 服务,这极大地简化了实现前几节中展示的发布策略。Knative 服务将为您创建 Kubernetes 内置资源,并跟踪其更改和版本,从而实现需要同时存在多个版本的场景。Knative 服务还提供高级流量处理和自动缩放,以实现无服务器方法,将副本数缩放到零。
注意:有关如何使用 Knative Serving 与会议应用程序一起实现不同发布策略的逐步教程,请参阅github.com/salaboy/platforms-on-k8s/blob/main/chapter-8/knative/README.md。
解释 Knative Serving 组件和资源的工作原理超出了本书的范围;我的建议是,如果我在以下章节中的示例中成功吸引了您的注意,您应该查阅 Jacques Chester 所著的《Knative 实战》(Manning Publications, 2021)。
8.2.1 Knative 服务:容器即服务
一旦您安装了 Knative Serving,您就可以创建 Knative 服务。我能听到您在想:“但我们已经有了 Kubernetes 服务。为什么我们还需要 Knative 服务?”相信我,当我看到相同的名称时,我也有同样的感觉,但请继续阅读——这确实是有道理的。
当我们在第二章(会议应用程序)中部署我们的“行走骨架”时,我们至少创建了两个 Kubernetes 资源:一个 Kubernetes 部署和一个 Kubernetes 服务。正如我们在第二章中讨论的,通过使用 ReplicaSets,部署可以通过跟踪部署资源中的配置更改来执行滚动更新。我们还在第二章中讨论了创建 ingress 资源以将集群外部的流量路由到集群的需求。通常,您只创建 ingress 资源来映射公开可用的服务,例如会议应用程序的前端或会议管理门户。
注意:我们创建的 Ingress 资源将所有流量直接路由到集群内的 Kubernetes 服务,教程中使用的 ingress 控制器作为一个简单的反向代理工作。它没有任何高级功能来分割流量、速率限制或检查请求头以对其进行动态决策。
您可以遵循逐步教程来创建集群、安装 Knative Serving 并部署应用程序服务,请参阅 github.com/salaboy/platforms-on-k8s/blob/main/chapter-8/knative/README.md#installation。
Knative 服务是在这些资源(服务、部署、ReplicaSets)之上构建的,以简化我们定义和管理应用程序服务生命周期的过程。虽然这简化了任务并减少了我们需要维护的 YAML 文件数量,但它也增加了一些令人兴奋的功能。在深入探讨这些功能之前,让我们看看 Knative 服务在实际操作中的样子。
Knative 服务向用户提供了类似于 AWS App Runner 和 Azure Container Apps 这样的 容器即服务 接口的简化合约。实际上,Knative 服务与 Google Cloud Run 使用的接口相同,使用户能够按需运行容器,而无需了解 Kubernetes。
由于 Knative Serving 安装了自己的自动扩展器,Knative 服务会自动根据需求进行配置以进行扩展。这使得 Knative Serving 成为实现 函数即服务 平台的一种非常好的方式,因为未使用的负载将自动缩减到零。
让我们看看这些功能在实际操作中的表现,从 Knative 服务 Kubernetes 资源开始。我们将从简单开始,并使用来自 Conference 应用程序的通告服务来演示 Knative 服务的工作原理。检查 notifications-service.yaml 资源定义(可在 github.com/salaboy/platforms-on-k8s/blob/main/chapter-8/knative/notifications-service.yaml 获取),如下所示。
列表 8.1 Knative 服务定义
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: notifications-service ①
spec:
template:
spec:
containers:
- image: salaboy/notifications-service:v1.0.0 ②
Env: ③
- name: KAFKA_URL
value: <URL>
① 您需要为资源指定一个名称,就像任何其他 Kubernetes 资源一样。
② 您需要指定您想要运行的容器镜像。
③ 您可以使用环境变量来参数化您的容器。
就像部署会选择 spec.template.spec 字段来裁剪 pod 一样,Knative 服务定义了使用相同字段创建其他资源的配置。
到目前为止,没有什么太奇怪的,但这与 Kubernetes 服务有何不同?如果您使用 kubectl apply -f 创建此资源,您就可以开始探索它们之间的差异。
注意:本节中的所有示例都是基于在 KinD 集群上运行逐步教程。如果您在云提供商上运行,输出将不同。请参阅 github.com/salaboy/platforms-on-k8s/blob/main/chapter-8/knative/README.md#knative-services-quick-intro。
您还可以使用 kubectl get ksvc(ksvc 代表 Knative 服务)列出所有 Knative 服务,并且您应该在那里看到您新创建的 Knative 服务:
NAME URL LATEST CREATED READY
notifications-service http://notificationsl-service...notifications-service-00001 True
在这里有几个细节需要注意;首先,有一个可以复制到浏览器中访问服务的 URL。如果您在云提供商上运行并安装 Knative 时配置了 DNS,这个 URL 应该可以立即访问。LASTCREATED 列显示服务的最新 Knative 修订版本名。Knative 修订是指向我们服务特定配置的指针,这意味着我们可以将流量路由到它们。
您可以使用 curl 或将浏览器指向 http://notifications-service.default.127.0.0.1.sslip.io/service/info 来测试 Knative 服务 URL。请注意,我们正在使用 jq (jqlang.github.io/jq/download/),一个非常流行的 JSON 工具,来美化输出。您应该在列表 8.2 中看到输出。
列表 8.2 与我们新创建的 Knative 服务交互
curl http://notifications-service.default.127.0.0.1.sslip.io/service/info
{
"name" : "NOTIFICATIONS",
"podIp" : "10.244.0.18",
"podName" : "notifications-service-00001-deployment-74cf6f5f7f-h8kct",
"podNamespace" : "default",
"podNodeName" : "dev-control-plane",
"podServiceAccount" : "default",
"source" : "https://github.com/salaboy/platforms-on-k8s/tree/main/
conference-application/notifications-service",
"version" : "1.0.0"
}
与任何其他 Kubernetes 资源一样,您也可以使用 kubectl describe ksvc notifications-service 来获取资源的更详细描述。如果您列出其他知名资源,例如部署、服务和 Pod,您会发现 Knative Serving 正在为您创建并管理它们。因为这些现在是托管资源,通常不建议手动更改它们。如果您想更改应用程序配置,您应该编辑 Knative 服务资源。
我们之前在集群中应用 Knative 服务时,默认行为与手动创建服务、部署和入口不同。Knative 服务默认:
-
可访问: 它在公共 URL 下暴露自己,因此您可以从集群外部访问它。它不会创建入口资源,因为它使用您之前安装的可用 Knative 网络堆栈。因为 Knative 对网络堆栈有更多控制权,并管理部署和服务,所以它知道服务何时准备好处理请求,从而减少了服务和部署之间的配置错误。
-
管理 Kubernetes 资源: 它创建两个服务和一项部署。Knative Serving 允许我们同时运行同一服务的多个版本。因此,它将为每个版本创建一个新的 Kubernetes 服务(在 Knative Serving 中称为修订版)。
-
收集服务使用情况: 它创建一个具有指定
user-container的 Pod 和一个名为queue-proxy的边车容器。 -
根据需求进行扩展和缩减: 如果没有请求击中服务(默认情况下 90 秒后),它会自动将自己缩减到零:
-
它通过使用
queue-proxy收集的数据将 Deployment 副本缩减到 0 来实现这一点。 -
如果有请求到达但没有可用的副本,它会排队请求的同时进行扩展,所以它不会丢失。
-
我们的通知服务已将副本的最小数量设置为 1,以确保始终运行。
-
-
配置更改历史由 Knative Serving 管理: 如果您更改 Knative 服务配置,将创建一个新的修订版。默认情况下,所有流量都将路由到最新的修订版。
当然,这些都是默认设置,但您可以微调每个 Knative 服务以满足您的需求,例如实现之前描述的发布策略。
在下一节中,我们将探讨如何使用 Knative Serving 的高级流量处理功能来实现金丝雀发布、蓝/绿部署、A/B 测试和基于头部的路由。
8.2.2 高级流量分配功能
让我们首先看看如何使用 Knative 服务实现我们应用程序服务的一个金丝雀发布。本节首先探讨使用基于百分比的流量分配进行金丝雀发布。然后,它将进入基于标签和基于头部的流量分配的 A/B 测试。
基于百分比的流量分配的金丝雀发布
如果您获取 Knative 服务资源(使用kubectl get ksvc notifications-service -oyaml),您会注意到spec部分现在还包含一个默认创建的spec.traffic部分(如列表 8.3 所示),因为我们没有指定任何内容。默认情况下,100%的流量将被路由到服务的最新 Knative 修订版。
列表 8.3 Knative 服务允许我们设置流量规则
traffic:
- latestRevision: true
percent: 100
现在假设您在服务中对发送电子邮件的方式进行了更改,以提高其效果,但您的团队不确定人们会接受得如何,我们希望避免因网站问题导致人们不愿意注册我们的会议。因此,我们可以同时运行两个版本,并控制将多少流量路由到每个版本(在 Knative 术语中称为修订版)。
让我们编辑 Knative 服务(kubectl edit ksvc notifications-service),并应用列表 8.4 中所示的变化。
列表 8.4 更改我们的 Knative 服务
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: notifications-service
spec:
template:
spec:
containers:
- image: salaboy/image: salaboy/notifications-service-0e27884e01429ab7e350cb5dff61b525:v1.1.0 ①
env:
name: KAFKA_URLvalue: <URL>
traffic: ②
- percent: 50
revisionName: notifications-service-00001
- latestRevision: true
percent: 50
① 您已将服务将使用的容器镜像从“notifications-service-0e27884e01429ab7e350cb5dff61b525:v1.0.0”更新到“notifications-service-0e27884e01429ab7e350cb5dff61b525:v1.1.0”。
② 您已创建了一个 50% / 50%的流量分配,其中 50%的流量将继续流向您的稳定版本,另外 50%流向您刚刚更新的最新版本。
如果您现在使用curl尝试,您应该能够看到流量分配的实际操作。
列表 8.5 新请求正在影响并行运行的不同版本
curl http://notifications-service.default.127.0.0.1.sslip.io/service/info
{
"name":"NOTIFICATIONS-IMPROVED", ①
"version":"1.1.0",
…
}
curl http://notifications-service.default.127.0.0.1.sslip.io/service/info
{
"name":"NOTIFICATIONS",
"version":"1.0.0",
…
}
curl http://notifications-service.default.127.0.0.1.sslip.io/service/info
{
"name":"NOTIFICATIONS-IMPROVED",
"version":"1.1.0",
…
}
curl http://notifications-service.default.127.0.0.1.sslip.io/service/info
{
"name":"NOTIFICATIONS",
"version":"1.0.0",
…
}
① 每 5 个请求中有一个将流向新的“NOTIFICATIONS-IMPROVED”版本。请注意,这可能需要一段时间,直到新的 Knative 修订版开始运行。
一旦你验证了服务的新版本运行正确,你就可以开始发送更多流量,直到你确信可以将 100%的流量移动到它。如果出现问题,你可以将流量分割回稳定版本。
注意,你不仅限于只有两个服务版本;只要你所有版本的流量百分比总和为 100%,你就可以创建尽可能多的版本。Knative 将遵循这些规则,并扩展所需的服务版本以处理请求。你不需要创建任何新的 Kubernetes 资源,因为 Knative 会为你创建这些资源,从而降低同时修改多个资源时出现错误的可能性。
图 8.7 显示了在使用此功能时你将面临的挑战。通过使用百分比,你无法控制后续请求将落在何处。Knative 将确保根据你指定的百分比保持公平的分布。如果,例如,你有用户界面而不是简单的 REST 端点,这可能会成为一个问题。

图 8.7 基于百分比的流量分割场景和挑战
用户界面很复杂,因为浏览器将执行多个相关的 GET 请求来渲染页面 HTML、CSS、图像等。你可能会迅速陷入每个请求都击中你应用程序不同版本的情况。让我们看看一种可能更适合测试用户界面或需要确保多个请求最终进入正确版本的应用程序的场景的不同方法。
基于标签的路由 A/B 测试
如果你想要对会议应用包含的不同版本的用户界面进行 A/B 测试,你需要为 Knative 提供一种区分请求发送位置的方法。你有两种选择。首先,你可以指向一个用于尝试服务的特殊 URL,第二种是使用请求头区分请求发送的位置。让我们看看这两个替代方案的实际操作。
步骤分解教程(github.com/salaboy/platforms-on-k8s/tree/main/chapter-8/knative#run-the-conference-application-with-knative-services)定义了所有要作为 Knative 服务的会议应用服务并将它们部署到集群中。前端 Knative 服务看起来像列表 8.6。
列表 8.6 前端应用程序的 Knative 服务定义
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: frontend ①
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: "1" ②
spec:
containers:
- image: salaboy/frontend-go-1739aa83b5e69d4ccb8a5615830ae66c:v1.0.0 ③
env:
- name: KAFKA_URL
value: kafka.default.svc.cluster.local
…
①你需要为这个服务指定一个名称。
②我们不希望 Knative Serving 在没有人在使用时缩小前端服务。我们希望始终保持至少一个实例运行。
③你现在定义前端容器镜像,因为我们将要测试多个请求发送到同一版本。
再次强调,我们刚刚创建了一个 Knative 服务,但由于这个容器镜像包含由 HTML、CSS、图像和 JavaScript 文件组成的 Web 应用程序,我们无法指定基于百分比的路由规则。Knative 不会阻止您这样做。不过,您会发现请求被路由到不同的版本,并出现错误,因为给定的镜像不在任何一个版本中,或者您最终得到了来自应用程序错误版本的样式表(CSS)。
让我们从定义一个标签开始,这个标签可以用来测试新的样式表,并在后台办公室部分包含调试标签。您可以通过修改 Knative 服务资源来实现这一点,就像我们之前做的那样。首先,将镜像更改为salaboy/frontend-go-1739aa83b5e69d4ccb8a5615830ae66c:v1.1.0,添加值为true的FEATURE_DEBUG_ENABLED环境变量,然后使用traffic.tag属性创建一些新的流量规则:
traffic:
- percent: 100 ①
revisionName: frontend-00001
- latestRevision: true ②
tag: version110
① 100%的流量将流向我们的稳定版本,不会有请求发送到我们的新修订版本,版本号为 v1.1.0。
② 我们创建了一个名为“color”的新标签;您可以通过描述 Knative 服务资源来找到这个新标签的 URL。
如列表 8.7 所示,如果您描述 Knative 服务(kubectl describe ksvc frontend),您将找到我们刚刚创建的标签的 URL,如下所示。
列表 8.7 使用标签时的流量规则
Traffic:
Latest Revision: false
Percent: 100
Revision Name: frontend-00001
Latest Revision: true
Percent: 0
Revision Name: frontend-00001
Tag: version110
URL: http://version110-frontend.default.127.0.0.1.sslip.io ①
① 您可以在 ksvc 流量部分找到标签及其生成的 URL。
图 8.8 展示了当未指定标签时,Knative 服务将 100%的流量路由到版本 v1.0.0。如果指定了标签“version110”,Knative 服务将流量路由到版本 v1.1.0。

图 8.8 Knative Serving 基于标签的路由版本 v1.1.0。
使用网络浏览器,检查您是否可以通过以下 URL(http://version110-frontend.default.127.0.0.1.sslip.io)一致地访问版本 v1.1.0,并使用原始服务 URL(http://frontend.default.127.0.0.1.sslip.io)访问版本 v.1.0.0。图 8.9 展示了两者并排使用不同的调色板。

图 8.9 基于标签的路由 A/B 测试
使用标签可以确保所有请求都击中了服务正确版本的 URL。还有一个选项可以避免您在 A/B 测试时指向不同的 URL,这可能对调试很有用。下一节将探讨使用 HTTP 头而不是不同 URL 进行基于标签的路由。
基于头部的 A/B 测试
最后,让我们看看一个 Knative Serving 功能(knative.dev/docs/serving/configuration/feature-flags/#tag-header-based-routing),该功能允许您使用 HTTP 头部来路由请求。此功能也使用标签来确定路由流量,但不是使用不同的 URL 来访问特定的修订版本,而是可以添加一个 HTTP 头部来完成这项工作。
假设您想启用开发者访问应用程序的调试版本。应用程序开发者可以在他们的浏览器中设置一个特殊头部,然后访问特定的修订版本。
要启用此实验性功能,您或安装 Knative 的管理员需要修补 knative-serving 命名空间内的 ConfigMap:
kubectl patch cm config-features -n knative-serving ➥-p ‘{"data":{"tag-header-based-routing":"Enabled"}}’
一旦启用此功能,您可以通过使用我们之前创建的 version110 标签来测试它。列表 8.8 展示了我们定义的流量规则。我们想要使用 HTTP 头部路由来针对的标签名称已突出显示。
列表 8.8 使用标签名称进行基于 HTTP 头部的路由
traffic:
- percent: 100
revisionName: frontend-00001
- latestRevision: true
tag: version110
如果您将浏览器指向 Knative 服务 URL (kubectl get ksvc),您将看到与之前相同的应用程序,如图 8.10 所示,但如果您使用像 ModHeader 扩展程序(chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj?hl=en)这样的 Chrome 工具,您可以设置浏览器产生的每个请求中都将包含的自定义 HTTP 头部。对于此示例,并且因为您创建的标签名为 version110,您需要设置以下 HTTP 头部:Knative-Serving-Tag: version110。一旦 HTTP 头部存在,Knative Serving 将将传入请求路由到 version110 标签。
图 8.10 展示了 Knative Serving 如何通过使用 ModHeader 设置的 HTTP 头部将请求路由到我们的 version110 标签。请注意,我们正在使用默认服务 URL http://frontend.default.127.0.0.1.sslip.io。

图 8.10 使用 ModHeader Chrome 扩展程序设置基于头部的自定义 HTTP 头部以进行路由。
标签和基于头部的路由都旨在确保如果访问特定的 URL(为标签创建的)或存在特定的头部,所有请求都将被路由到相同的修订版本。最后,让我们看看如何使用 Knative Serving 进行蓝绿部署。
蓝绿部署
对于我们需要在特定时间点从一个版本切换到下一个版本的情况,因为不存在向后兼容性,我们仍然可以使用基于标签的路由和百分比。我们不是逐渐从一个版本过渡到下一个版本,而是使用百分比作为新版本从 0 到 100 的开关,以及旧版本从 100 到 0 的开关。
大多数蓝绿部署场景需要不同团队和服务的协调,以确保服务和客户端同时更新。Knative Serving 允许你以声明式的方式定义何时从当前版本切换到下一个版本。图 8.11 显示了我们要部署一个与v1.x版本不兼容的新版本v2.0.0的通知服务场景。这意味着这次升级将需要修改客户端。通过使用 Knative Serving 流量规则和标签,我们可以决定何时进行切换。负责客户端和通知服务v2.0.0升级的团队需要协调升级。

图 8.11 使用 Knative Serving 基于标签的路由进行蓝绿部署
为了实现图 8.11 中描述的场景,我们可以在 Knative 服务内部为新版本创建“green”标签,如图表 8.9 所示。
列表 8.9 使用标签定义蓝绿版本
...
traffic:
- revisionName: <blue-revision-name>
percent: 100 # All traffic is still being routed to the first revision
- revisionName: <green-revision-name>
percent: 0 # 0% of traffic routed to the second revision
tag: green # A named route
通过创建一个新的标签(称为“green”),我们现在将有一个新的 URL 来访问用于测试的新版本。这对于测试客户端的新版本特别有用,因为如果服务 API 发生了非向后兼容的更改,客户端可能也需要更新。一旦所有测试完成,我们可以安全地将所有流量切换到服务的“green”版本,如图表 8.10 所示。注意,我们从“green”版本中移除了标签,并为“blue”版本创建了一个新的标签。
列表 8.10 使用 Knative 声明式方法切换流量
...
traffic:
- revisionName: <first-revision-name>
percent: 0 # All traffic is still being routed to the first revision
tag: blue # A named route
- revisionName: <second-revision-name>
percent: 100 # 100% of traffic routed to the second revision
注意,更新前的“blue”原始版本现在可以通过基于头部或标签的路由访问,并接收发送到服务的所有流量。
通常,我们不能逐步将流量从当前版本移动到下一个版本,因为消费服务的客户端需要理解请求可能会落在不同的(且不兼容的)服务版本上。
在前面的章节中,我们一直在探讨 Knative Serving 如何简化团队实现不同发布策略的过程,以便持续交付功能和服务的最新版本。Knative Serving 减少了手动实现本章中描述的发布策略所需的创建多个 Kubernetes 内置资源的需要。它提供了高级抽象,例如 Knative 服务,它创建和管理 Kubernetes 内置资源以及用于高级流量管理的网络栈。
让我们切换到另一种使用 Argo Rollouts 在 Kubernetes 中管理发布策略的替代方案。
8.3 Argo Rollouts:使用 GitOps 自动化的发布策略
在大多数情况下,你会看到 Argo Rollouts 与 ArgoCD 一起协同工作。这很有道理,因为我们希望实现一个交付管道,该管道可以消除手动手动应用配置更改与我们的环境交互的需求。在以下章节的示例中,我们将仅关注 Argo Rollouts,但在实际场景中,你不应该使用 kubectl 将资源应用到环境中,因为 Argo CD 会为你完成这项工作。
如网站所定义,Argo Rollouts 是“一个 Kubernetes 控制器和一组 CRDs,它们提供了高级部署功能,如蓝绿部署、金丝雀发布、金丝雀分析、实验和渐进式交付功能。”正如我们通过其他项目所看到的那样,Argo Rollouts 通过引入 Rollouts、Analysis 和 Experimentations 的概念来扩展 Kubernetes,以实现渐进式交付功能。Argo Rollouts 的主要思想是使用 Kubernetes 内置的块,而无需手动修改和跟踪部署和服务资源。
Argo Rollouts 由两部分组成:一个 Kubernetes 控制器,它实现了处理我们的发布、定义(以及分析和实验)的逻辑,以及一个允许你控制这些发布如何进展的 kubectl 插件,它使得手动升级和回滚成为可能。使用 kubectl Argo Rollouts 插件,你还可以安装 Argo Rollouts 仪表板并在本地运行它。
注意:你可以在 github.com/salaboy/platforms-on-k8s/blob/main/chapter-8/argo-rollouts/README.md 上找到一个关于如何在本地 Kubernetes KinD 集群上安装 Argo Rollouts 的教程。请注意,这个教程需要创建一个不同于我们用于 Knative Serving 的不同 KinD 集群。
让我们先看看如何使用 Argo Rollouts 实现金丝雀发布,以了解它与使用纯 Kubernetes 资源或 Knative 服务相比如何。
8.3.1 Argo Rollouts 金丝雀发布
我们将从创建我们的第一个 Rollout 资源开始。使用 Argo Rollouts,我们不会定义部署,因为我们将会将这项责任委托给 Argo Rollouts 控制器。相反,我们定义一个 Argo Rollouts 资源,它也提供了我们的 pod 规范(与 Deployment 以相同的方式定义 pod 需要如何创建的 PodSpec)。
在这些示例中,我们将只使用来自 Conference 平台应用程序的通知服务,并且不会使用 Helm。当使用 Argo Rollouts 时,我们需要处理目前未包含在 Conference 应用程序 Helm 图表中的不同资源类型。Argo Rollouts 可以与 Helm 完美地协同工作,但我们将创建文件来测试 Argo Rollouts 在这些示例中的行为。你可以在 argoproj.github.io/argo-rollouts/features/helm/ 查看使用 Helm 的 Argo Rollouts 示例。让我们首先在列表 8.11 中创建一个用于通知服务的 Argo Rollouts 资源。
列表 8.11 Argo Rollouts 资源定义
apiVersion: argoproj.io/v1alpha1
kind: Rollout ①
metadata:
name: notifications-service-canary
spec:
replicas: 3 ②
strategy:
canary: ③
steps: ④
- setWeight: 25
- pause: {}
- setWeight: 75
- pause: {duration: 10}
revisionHistoryLimit: 2
selector:
matchLabels:
app: notifications-service
template:
metadata:
labels:
app: notifications-service
spec:
containers:
- name: notifications-service
image: salaboy/notifications-service-<HASH>:v1.0.0
env:
- name: KAFKA_URL
value: kafka.default.svc.cluster.local
...
① Rollouts 资源定义允许我们配置我们的工作负载使用不同的发布。
② 注意,与部署类似,我们可以设置我们想要的通知服务的副本数量。
③ 此示例将 spec.strategy 属性设置为 canary,这需要一组特定的步骤来配置 canary 发布将如何为这个特定的服务行为。
④ 当我们对服务进行任何更新时,定义的步骤将按顺序执行。在这个例子中,canary 将从 25% 的流量开始,等待手动提升,然后切换到 75%,等待 10 秒,最后移动到 100%。
注意:你可以在这里找到完整的文件 github.com/salaboy/platforms-on-k8s/blob/main/chapter-8/argo-rollouts/canary-release/rollout.yaml。
这个 Rollout 资源通过我们在 spec.template 和 spec.replicas 字段中定义的内容来管理 Pods 的创建。但它还添加了 spec.strategy 部分,在这个情况下设置为 canary,并定义了 rollout 将发生的步骤(将发送到 canary 的流量(权重))。正如你所看到的,你还可以定义每个步骤之间的暂停。duration 以秒为单位表示,允许我们精细控制流量如何转移到 canary 版本。如果你不指定 duration 参数,rollout 将会等待直到手动干预发生。让我们看看这个 rollout 是如何实际工作的。
让我们将 Rollout 资源应用到我们的 Kubernetes 集群中(请查看在 github.com/salaboy/platforms-on-k8s/tree/main/chapter-8/argo-rollouts#canary-releases 可用的逐步教程):
> kubectl apply -f argo-rollouts/canary-release/
注意:此命令还将创建一个 Kubernetes 服务和一个 Kubernetes 入口资源。
记住,如果你正在使用 ArgoCD,那么你将不会手动应用资源,而是将此资源推送到 Argo CD 监控的 Git 仓库。一旦资源被应用,我们可以通过使用 kubectl 来看到一个新的 Rollout 资源可用,如列表 8.12 所示。
列表 8.12 获取所有 Argo Rollouts 资源
> kubectl get rollouts.argoproj.io
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE
notifications-service-canary 3 3 3 3
这看起来几乎就像一个正常的 Kubernetes 部署,但它不是。如果您使用 kubectl get deployments,您不应该看到任何针对我们的 email-service 的部署资源。Argo Rollouts 通过使用 Rollouts 资源来替代 Kubernetes 部署的使用,这些资源负责创建和操作副本集,我们可以使用 kubectl get rs 检查我们的 Rollout 是否创建了一个新的 ReplicaSet。请参阅列表 8.13。
列表 8.13 获取由我们的 Rollout 创建的 ReplicaSet
> kubectl get rs
NAME DESIRED CURRENT READY
notifications-service-canary-7f6b88b5fb 3 3 3
Argo Rollouts 将创建和管理我们以前用部署资源管理的这些副本集,但以一种使我们能够平滑地进行金丝雀发布的方式。
如果您已安装了 Argo Rollouts Dashboard,您应该在主页上看到我们的 Rollout(见图 8.12)。

图 8.12 Argo Rollouts Dashboard
与部署一样,我们仍然需要一个服务和入口来将流量从集群外部路由到我们的服务;这些资源包含在逐步教程中(github.com/salaboy/platforms-on-k8s/tree/main/chapter-8/argo-rollouts/canary-release)。如果您创建了以下资源,您就可以开始与稳定服务以及金丝雀进行交互,如图 8.13 所示。

图 8.13 Argo Rollouts 金丝雀发布 Kubernetes 资源。Rollout 控制副本集,并根据每个副本集中的 Pod 数量管理近似权重。
如果您创建了一个服务和入口,您应该能够使用以下 curl 命令查询通知服务的 service/info 端点:
> curl localhost/service/info | jq
输出应该类似于列表 8.14。
列表 8.14 与通知服务版本 v1.0.0 交互
{
"name": "NOTIFICATIONS",
"version": "1.0.0",
"source": "https://github.com/salaboy/platforms-on-k8s/tree/main/
➥conference-application/notifications-service",
"podName": "notifications-service-canary-7f6b88b5fb-fq8mm",
"podNamespace": "default",
"podNodeName": "dev-worker2",
"podIp": "10.244.1.5",
"podServiceAccount": "default"
}
请求显示了我们的通知服务 service/info 端点的输出。因为我们刚刚创建了此 Rollout 资源,所以 Rollout 金丝雀策略机制尚未启动。现在,如果我们想通过更新 Rollout spec.template 部分使用新的容器镜像引用或更改环境变量,将创建一个新的修订版,并启动金丝雀策略。
在新的终端中,我们可以在进行任何修改之前查看 Rollout 状态,这样我们就可以在更改 Rollout 规范时看到 Rollout 机制的实际操作。如果我们想查看在做出一些更改后 Rollout 的进度,您可以在另一个终端中运行以下命令:
> kubectl argo rollouts get rollout notifications-service-canary --watch
您应该看到类似于图 8.14 的内容。

图 8.14 使用 kubectl 的 argo 插件查看 Rollout 详细信息
让我们通过运行以下命令修改我们的 notification-service-canary Rollout:
> kubectl argo rollouts set image notifications-service-canary notifications-service=salaboy/notifications-service-0e27884e01429ab7e350cb5dff61b525:v1.1.0
一旦我们替换了 Rollout 使用的容器镜像,滚动部署策略就会启动。如果你回到你在那里观察滚动部署的终端,你应该会看到创建了一个新的# revision: 2;参见图 8.15。

图 8.15 更新服务后的滚动部署进度
你可以看到修订版 2 被标记为“金丝雀”,滚动部署的状态为“॥ 暂停”,并且只为金丝雀创建了一个 Pod。到目前为止,滚动部署只执行了第一步,如列表 8.15 所示。
列表 8.15 Rollout 中的步骤定义
strategy:
canary:
steps:
- setWeight: 25
- pause: {}
你也可以在仪表板中检查金丝雀滚动部署的状态,如图 8.16 所示。

图 8.16 已创建金丝雀发布,大约 20%的流量被路由到它。
Rollout 目前处于暂停状态,等待人工干预。我们现在可以测试我们的金丝雀是否正在接收流量,以查看我们是否对金丝雀的工作情况满意,然后再继续滚动部署过程。为此,我们可以再次查询“service/info”端点,以查看大约 25%的时间我们命中了金丝雀,如列表 8.16 所示。
列表 8.16 从我们的通知服务中命中版本 v1.1.10 的示例输出
> curl localhost/service/info | jq
{
"name":"NOTIFICATIONS-IMPROVED",
"version":"1.1.0",
…
}
我们可以看到有一个请求击中了我们的稳定版本,另一个请求则去了金丝雀。
Argo Rollouts 不处理流量管理;在这种情况下,Rollout 资源仅处理底层的 ReplicaSet 对象及其副本。你可以通过运行kubectl get rs来检查ReplicaSets,如列表 8.17 所示。
列表 8.17 检查与我们的 Rollout 关联的 ReplicaSets
> kubectl get rs
NAME DESIRED CURRENT READY AGE
notifications-service-canary-68fd6b4ff9 1 1 1 12s
notifications-service-canary-7f6b88b5fb 3 3 3 17m
这些不同 Pod(金丝雀和稳定 Pod)之间的流量管理是由 Kubernetes 服务资源管理的,因此要看到我们的请求同时击中了金丝雀和稳定版本 Pod,我们需要通过 Kubernetes 服务。我之所以提到这一点,是因为如果你使用kubectl port-forward svc/notifications-service 8080:80,例如,你可能会想交通是被转发到了 Kubernetes 服务(因为我们使用了svc/notifications-service),但kubectl port-forward解析为一个 Pod 实例并连接到单个 Pod,这仅允许你击中金丝雀或稳定 Pod。因此,我们使用了 ingress,它将使用服务来负载均衡流量并击中所有匹配服务选择器的 Pod。
如果我们对结果满意,我们可以通过执行以下命令继续滚动部署过程,该命令将金丝雀提升为稳定版本:
> kubectl argo rollouts promote notifications-service-canary
尽管我们刚刚手动提升了滚动部署,但最佳实践是利用 Argo Rollouts 的自动化分析步骤,我们将在第 8.3.2 节中深入探讨。
如果您查看 Argo Rollouts 仪表板,您会注意到您还可以使用 Rollout 中的“提升”按钮来提升 rollout 以继续前进。在这个上下文中,提升仅意味着 rollout 可以继续执行在 spec.strategy 部分中定义的下一步,如图表 8.18 所示。
列表 8.18 带有 10 秒暂停的 Rollouts 步骤定义
strategy:
canary:
steps:
- setWeight: 25
- pause: {}
- setWeight: 75
- pause: {duration: 10}
在手动提升后,权重将被设置为 75%,然后暂停 10 秒,最终将等待时间设置为 100%。此时,您应该看到修订版 1 正在逐步缩放,而修订版 2 正在逐步提升以接管所有流量。请参阅图 8.17,它显示了 rollout 的最终状态。

图 8.17 所有流量已切换到修订版 2 的 Rollout 完成
您也可以在图 8.18 中的仪表板中实时查看此 rollout 进展。

图 8.18 金丝雀修订版被提升为稳定版本。
如您所见,修订版 1 已缩放到零个 pod,修订版 2 现在标记为稳定版本。如果您检查 ReplicaSets,您将看到与列表 8.19 中相同的输出。
列表 8.19 负责修订版 1 的 ReplicaSet 已缩放到 0
> kubectl get rs
NAME DESIRED CURRENT READY
notifications-service-canary-68fd6b4ff9 3 3 3
notifications-service-canary-7f6b88b5fb 0 0 0
我们已经成功创建、测试并提升了 Argo Rollouts 的金丝雀发布!
与我们在 8.1 节中看到的金丝雀发布相比,使用两个部署资源通过 Argo Rollouts 实现相同的模式,你可以完全控制金丝雀发布如何提升,你希望在将更多流量切换到金丝雀之前等待多少时间,以及你希望添加多少手动干预步骤。现在让我们看看 Argo Rollouts 中的蓝绿部署是如何工作的。
8.3.2 Argo Rollouts 蓝绿部署
在 8.1 节中,我们介绍了使用 Kubernetes 基本构建块进行蓝绿部署的优势以及您可能感兴趣的原因。我们还看到了这个过程是多么手动,以及这些手动步骤如何打开可能导致我们的服务崩溃的愚蠢错误的门。在本节中,我们将探讨 Argo Rollouts 如何使我们能够以与之前用于金丝雀部署相同的方法实现蓝绿部署。请查看 Argo Rollouts 蓝绿部署的逐步教程github.com/salaboy/platforms-on-k8s/tree/main/chapter-8/argo-rollouts#bluegreen-deployments。让我们看看具有蓝绿策略的 Rollout 在列表 8.20 中的样子。
列表 8.20 定义蓝绿策略的 Rollout
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: notifications-service-bluegreen
spec:
replicas: 2
revisionHistoryLimit: 2
selector:
matchLabels:
app: notifications-service
template:
metadata:
labels:
app: notifications-service
spec:
containers:
- name: notifications-service
image: salaboy/notifications-service-<HASH>:v1.0.0
env:
- name: KAFKA_URL
value: kafka.default.svc.cluster.local
..
strategy:
blueGreen:
activeService: notifications-service-blue
previewService: notifications-service-green
autoPromotionEnabled: false
注意:您可以在github.com/salaboy/platforms-on-k8s/blob/main/chapter-8/argo-rollouts/blue-green/rollout.yaml找到完整的文件。
让我们应用这个 Rollout 资源的资源,使其工作(两个 Kubernetes 服务和入口):
> kubectl apply -f argo-rollouts/blue-green/
我们正在使用与之前相同的spec.template,但现在我们将 Rollout 的策略设置为blueGreen,因此我们需要配置对两个 Kubernetes 服务的引用。一个服务将是活动服务(蓝色),它正在处理生产流量,另一个是我们想要预览的绿色服务,但不将其路由到生产流量。autoPromotionEnabled: false是必需的,以允许手动干预以进行提升。默认情况下,一旦新的 ReplicaSet 准备好/可用,部署将自动提升。你可以通过以下命令或 Argo Rollouts 仪表板来监视部署:
> kubectl argo rollouts get rollout notifications-service-bluegreen --watch
在以下图中,你应该看到与我们在金丝雀发布中看到的输出类似的输出。

图 8.19 检查我们的蓝绿部署状态
在仪表板中,见图 8.20。

图 8.20 Argo Rollouts 仪表板中的蓝/绿部署
我们可以使用服务入口与修订版#1 进行交互,然后发送类似于列表 8.21 的请求。
列表 8.21 访问我们的服务的修订版 1
> curl localhost/service/info
{
"name":"NOTIFICATIONS",
"version":"1.0.0",
…
}
如果我们现在更改我们的 Rollout spec.template,蓝绿策略将启动。对于这个例子,我们想要看到的结果是 previewService 现在正在路由流量到我们在更改部署时创建的第二修订版:
> kubectl argo rollouts set image notifications-service-bluegreen ➥notifications-service=salaboy/notifications-service-<HASH>:v1.1.0
部署机制将启动,并且它将自动创建一个新的修订版 2 的 ReplicaSet,其中包含我们的更改。Argo Rollouts 用于蓝/绿部署将使用选择器通过修改我们在 Rollout 定义中引用的previewService来路由流量到我们的新修订版。
如果你描述notifications-service-green Kubernetes 服务,你会注意到添加了一个新的选择器,如列表 8.22 所示。
列表 8.22 Argo Rollouts 管理的 Kubernetes 服务选择器
> kubectl describe svc notifications-service-green
Name: notifications-service-green
Namespace: default
Labels: <none>
Annotations: argo-rollouts.argoproj.io/managed-by-rollouts: notifications-service-bluegreen
Selector: app=notifications-service,rollouts-pod-template-hash=645d484596
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.198.251
IPs: 10.96.198.251
Port: http 80/TCP
TargetPort: http/TCP
Endpoints: 10.244.2.5:8080,10.244.3.6:8080
Session Affinity: None
Events: <none>
此选择器与我们在进行更改时创建的修订版 2 的 ReplicaSet 匹配,如列表 8.23 所示。
列表 8.23 ReplicaSet 使用相同的标签来匹配服务定义
> kubectl describe rs notifications-service-bluegreen-645d484596
Name: notifications-service-bluegreen-645d484596
Namespace: default
Selector: app=notifications-service,rollouts-pod-template-hash=645d484596
Labels: app=notifications-service
rollouts-pod-template-hash=645d484596
Annotations: rollout.argoproj.io/desired-replicas: 2
rollout.argoproj.io/revision: 2
Controlled By: Rollout/notifications-service-bluegreen
Replicas: 2 current / 2 desired
Pods Status: 2 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=notifications-service
rollouts-pod-template-hash=645d484596
通过使用选择器和标签,具有blueGreen策略的 Rollout 正在为我们自动处理这些链接。这避免了手动创建这些标签的需要,并确保它们匹配。如图 8.21 所示,你现在可以检查现在有两个修订版(和 ReplicaSets),每个都有两个 Pod。

图 8.21 蓝色和绿色服务运行着相同数量的副本
在 Argo Rollouts 仪表板中,你应该看到与图 8.22 相同的信息。

图 8.22 Argo Rollouts 仪表板蓝绿修订版已启动
现在我们可以通过以下列表中的不同路径与 Green 服务(修订版#2)进行交互。
列表 8.24 与修订版 2(我们的 Green 服务)交互
> curl localhost/green/service/info | jq
{
"name": "NOTIFICATIONS-IMPROVED",
"version": "1.1.0",
"source": "https://github.com/salaboy/platforms-on-k8s/tree/v1.1.0/
➥conference-application/notifications-service",
"podName": "notifications-service-bluegreen-645d484596-rsj6z",
"podNamespace": "default",
"podNodeName": "dev-worker",
"podIp": "10.244.2.5",
"podServiceAccount": "default"
}
一旦 Green 服务启动运行,Rollout 将处于暂停状态,直到我们决定将其提升为稳定服务。图 8.23 展示了 Rollout 资源将如何根据 Rollout 的进度来协调 Green 和 Blue 服务所拥有的多个副本。

图 8.23 使用 Kubernetes 资源的蓝绿部署
由于我们现在有两个服务,我们可以同时访问它们,并在将其提升为主要(蓝色)服务之前确保我们的 Green(green-service)按预期工作。当服务处于预览状态时,集群中的其他服务可以开始将其用于测试目的的路由流量,但要路由所有流量并用我们的 Green 服务替换 Blue 服务,我们还可以再次使用终端中的 CLI 或从 Argo Rollouts 仪表板使用 Argo Rollouts 提升机制。现在尝试使用仪表板而不是kubectl来提升 Rollout。请记住,从终端提升 Rollout 的命令看起来像这样:
>kubectl argo rollouts promote notifications-service-bluegreen
注意,在缩小修订版#1 之前默认添加了 30 秒的延迟(这可以通过名为scaleDownDelaySeconds的属性来控制),但提升(切换标签到服务)发生在我们点击PROMOTE按钮的瞬间,如图 8.24 所示。

图 8.24 使用 Argo Rollouts 仪表板进行 Green 服务提升
这次提升仅将标签切换到服务的资源上,这会自动更改路由表,现在将所有来自活动服务的流量转发到我们的 Green 服务。如果我们对 Rollout 进行更多更改,过程将重新开始,预览服务将指向一个包含这些更改的新修订版。现在我们已经了解了使用 Argo Rollouts 进行金丝雀发布和蓝绿部署的基本知识,让我们来看看 Argo Rollouts 提供的更多高级机制。
8.3.3 Argo Rollouts 的渐进式交付分析
到目前为止,我们已经能够更好地控制我们的不同发布策略,但 Argo Rollouts 通过提供 AnalysisTemplate CRD 而显得出色,这使我们能够确保在 Rollout 过程中,我们的金丝雀和 Green 服务按预期工作。这些分析是自动化的,并作为 Rollout 的关卡,确保分析探测成功后才会继续进行。
这些分析可以使用不同的提供商来运行探针,包括 Prometheus、Datadog (www.datadoghq.com/)、New Relic (newrelic.com/)和 Dynatrace (www.dynatrace.com/)等,从而提供最大的灵活性来定义针对我们服务新版本的这些自动化测试。
图 8.25 展示了 AnalysisTemplates 如何允许 Argo Rollouts 创建 AnalysisRuns 以验证推出的新版本是否通过查看服务指标按预期运行。AnalysisRuns 将对服务进行指标探测,并且只有当指标匹配在 AnalysisTemplate 中定义的成功条件时,才会继续执行 Rollout 步骤。

图 8.25 Argo Rollouts 和分析协同工作以确保我们的新版本在流量转移之前是可靠的。当收到前进到 Rollout 下一个步骤的信号时,将创建一个 AnalysisRun 来通过运行在 AnalysisTemplate 中定义的查询来探测服务。AnalysisRun 的结果将影响 Rollout 的更新是继续、中止还是暂停。
对于金丝雀发布,分析可以作为步骤定义的一部分触发,这意味着在任意步骤之间,或者在 Rollout 中定义的每个步骤开始,或者为每个步骤定义。使用 Prometheus 提供商定义的 AnalysisTemplate 看起来像列表 8.25。
列表 8.25 Argo Rollouts 提供的 AnalysisTemplate 资源
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 5m
# NOTE: prometheus queries return results in the form of a vector.
# It is common to access the index 0 to obtain the value
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.example.com:9090
query: <Prometheus Query here>
然后,在我们的 Rollout 中,我们可以引用此模板并定义何时创建新的 AnalysisRun,例如,如果我们想在步骤 2 之后运行第一次分析(列表 8.26)。
列表 8.26 在定义金丝雀发布时选择分析模板
strategy:
canary:
analysis:
templates:
- templateName: success-rate
startingStep: 2 # delay starting analysis run until setWeight: 40%
args:
- name: service-name
value: notifications-service-canary.default.svc.cluster.local
如前所述,分析也可以定义为步骤的一部分。在这种情况下,我们的步骤定义将类似于列表 8.27。
列表 8.27 在 Rollout 中使用 AnalysisTemplate 引用作为步骤
strategy:
canary:
steps:
- setWeight: 20
- pause: {duration: 5m}
- analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: notifications-service-canary.default.svc.cluster.local
对于使用蓝绿策略的 Rollout,我们可以在推广前后触发分析运行。图 8.26 显示了通过运行 SmokeTestTemplate 来执行 PrePromotionAnalysis 步骤。如果分析运行失败,这将阻止 Rollout 切换流量到绿色服务。

图 8.26 Argo Rollouts 与蓝绿部署和 PrePromotionAnalysis 一起工作。当在 Rollout 上触发推广时,它将使用 SmokeTestsTemplate 创建一个新的 AnalysisRun,然后在切换标签以路由流量到预览服务之前。只有当 AnalysisRun 成功时,预览服务才成为新的活动服务。
下面是我们在 Rollout 中配置的 PrePromotionAnalysis 的示例,见列表 8.28。
列表 8.28 在蓝绿 Rollout 中定义 PrePromotionAnalysis
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: notifications-service-rollout
spec:
...
strategy:
blueGreen:
activeService: notifications-service-blue
previewService: notifications-service-green
prePromotionAnalysis:
templates:
- templateName: smoke-tests
args:
- name: service-name
value: notifications-service-preview.default.svc.cluster.local
对于预促销测试,在将流量切换到绿色服务之前,运行一个新的 AnalysisRun 测试,并且只有当测试成功时才会更新标签。对于后促销,测试将在标签切换到绿色服务之后运行,如果 AnalysisRun 失败,Rollout 可以自动将标签回滚到上一个版本。这是可能的,因为蓝色服务不会在 AnalysisRun 完成之前进行缩放。
我建议您查看官方文档中的分析部分,因为它包含了所有提供者和旋钮的详细解释,这些提供者和旋钮可以帮助确保您的 Rollouts 顺利运行:argoproj.github.io/argo-rollouts/features/analysis/。
8.3.4 Argo Rollouts 和流量管理
最后,值得一提的是,Rollouts 使用可用的 Pod 数量来近似我们为金丝雀发布定义的权重。虽然这是一个良好的开始,也是一个简单的机制,但有时我们需要对如何将流量路由到不同的版本有更多的控制。我们可以利用服务网格和负载均衡器的力量来编写更精确的规则,关于哪些流量被路由到我们的金丝雀发布。
根据我们 Kubernetes 集群中可用的流量管理工具,Argo Rollouts 可以配置不同的trafficRouting规则。目前,Argo Rollouts 支持:Istio、AWS ALB Ingress Controller、Ambassador Edge Stack、Nginx Ingress Controller、Service Mesh Interface (SMI)和 Traefik Proxy 等。如文档所述,如果我们有更高级的流量管理功能,我们可以实现如下技术:
-
原始百分比(即,5%的流量应发送到新版本,其余发送到稳定版本)
-
基于头的路由(即,发送带有特定头的请求到新版本)
-
镜像流量,其中所有流量都并行复制并发送到新版本(但忽略响应)
通过结合使用 Istio 等工具和 Argo Rollouts,我们可以使开发者能够测试只有通过设置特定头或转发生产流量的副本到金丝雀以验证它们是否按预期行为的功能。
这里是一个配置 Rollout 以将 35%的流量镜像到具有 25%权重的金丝雀发布的示例。这意味着将有 35%的流量被路由到稳定服务后,会复制并转发到金丝雀。通过使用这种技术,我们不会冒任何生产流量的风险,因为 Istio 正在复制请求以进行测试,如列表 8.29 所示。
列表 8.29 使用 Istio 进行高级(基于权重的)流量分割
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
...
strategy:
canary:
canaryService: notifications-service-canary
stableService: notifications-service-stable
trafficRouting:
managedRoutes:
- name: mirror-route
istio:
virtualService:
name: notifications-service-vsvc
steps:
- setCanaryScale:
weight: 25
- setMirrorRoute:
name: mirror-route
percentage: 35
match:
- method:
exact: GET
path:
prefix: /
- pause:
duration: 10m
- setMirrorRoute:
name: "mirror-route" # removes mirror based traffic route
如您所见,这个简单的例子已经需要了解 Istio 虚拟服务以及超出本节范围的更高级配置。如果您想了解 Istio,我强烈推荐阅读 Christian Posta 和 Rinor Maloku(Manning Publications,2022 年)的《Istio in Action》。图 8.27 显示了配置为使用 Istio 流量管理能力进行基于权重的路由的 Rollouts。

图 8.27 使用 Istio 进行流量镜像到金丝雀发布。使用 Istio 等工具设置 trafficRouting 可以使我们的金丝雀工作负载体验到稳定服务正在接收的真实流量。Rollout 控制器负责配置 Istio 虚拟服务为我们完成工作,并对哪些流量被发送到服务有精细的控制。
当使用“trafficManagement”功能时,Rollout 金丝雀策略的行为将与不使用任何规则时不同。更具体地说,当通过金丝雀发布滚动时,服务的稳定版本不会被缩放。这确保了稳定服务可以处理 100% 的流量。通常的计算适用于金丝雀副本数量。
我强烈建议查看官方文档(argoproj.github.io/argo-rollouts/features/traffic-management/)并遵循那里的示例,因为根据您可用的服务网格,Rollouts 的配置可能需要不同。
8.4 回到平台工程
在本章中,我们看到了使用基本的 Kubernetes 构建块可以实现什么,以及像 Argo Rollouts 或 Knative Serving 这样的工具如何通过发布它们应用程序的新版本到 Kubernetes 来简化团队的生活。
很遗憾,截至 2023 年今天,Argo Rollouts 和 Knative Serving 尚未集成(github.com/argoproj/argo-rollouts/issues/2186),因为这两个社区都将从定义发布策略的统一方式中受益,而不是重复功能。我喜欢 Knative Serving 的构建块,它有助于实现这些发布策略。另一方面,我喜欢 Argo Rollouts 通过 AnalysisTemplates 的概念将事物提升到下一个层次,以确保我们可以自动测试和验证新版本。未来是光明的,因为这两个项目都在寻求与 Gateway API 标准的进一步集成(gateway-api.sigs.k8s.io/),以统一在 Kubernetes 中管理高级流量路由能力。像 Istio、Knative Serving 和 Argo Rollouts 这样的工具都有积极的倡议来支持这个新标准。
我坚信,你在 Kubernetes 之旅中迟早会遇到交付挑战,而在你的集群内部拥有这些机制将增加你更快发布更多软件的信心。因此,我不会轻率地评估这些工具。确保你为你的团队规划时间,让他们研究和选择他们将用于实施这些发布策略的工具;许多软件供应商也可以提供帮助和建议。
从平台工程的角度来看,我们研究了如何通过提供开发者可以消费的应用级 API 来提高他们的效率,无论他们的语言是什么。我们已经使其他团队,如产品经理或更多以业务为导向的团队,能够决定何时启用某些功能以及如何根据他们的需求执行不同的发布策略。我们还使运维团队能够安全地定义规则,以验证新的 Rollouts 是否安全且按预期工作。
虽然本章的重点不是详细分析像 Knative Serving 这样的工具,但在构建平台时,提及容器即服务(container-as-a-service)和函数即服务(function-as-a-service)功能是很重要的,因为这些代表了平台团队可能希望向用户公开的常见特性。我还建议检查 Knative Functions(knative.dev/docs/functions/),现在是一个官方的 Knative 模块,因为这个项目强调了基于 Knative 构建基于函数的开发工作流程并利用 Kubernetes 的多语言方法的重要性。
图 8.28 展示了像 Knative Serving 这样的工具为平台团队提供了基本的构建块,以便以不同的方式部署和运行不同团队的负载。通过添加高级流量管理,团队可以实现更复杂的发布策略。Argo Rollouts 和 Knative Serving 与 Istio 服务网格协同工作,这将涵盖其他重要方面,例如用于加密和可观察性的 mTLS。像 Dapr 和 OpenFeature 这样的工具通过为团队提供标准接口来使用,同时使平台团队能够定义后端实现,而不必承诺单一解决方案。

图 8.28 定义的平台能力以管理环境。
我确实看到像 Knative、Argo Rollouts、Dapr、Istio 和 OpenFeature 这样的工具在这个领域引领潮流,尽管如此,即使团队需要弄清楚这些工具的每个细节,模式也在出现。这些工具已经存在了三年多,你可以注意到它们的功能、路线图以及参与人员的成熟度。随着一些项目从 CNCF 的孵化过程中毕业,我预计会有更多的集成来帮助用户处理今天大多数公司手动实施的标准工作流程。
最后,为了回顾到目前为止的旅程,图 8.29 展示了发布策略如何融入我们的平台骨架,以及业务团队(产品团队、利益相关者)如何使用这些机制在将所有客户完全迁移到最新版本之前验证新版本。

图 8.29 允许团队实验新版本的 环境
在下一章中,为了结束本书,我决定谈谈我们如何衡量我们构建在 Kubernetes 之上的平台。这两章中描述的平台功能以及本书中描述的工具组合都是好的,因为我们正在提高我们团队交付软件的速度。因此,使用关注我们团队在交付软件方面效率的指标与平台为这些团队提供的工具直接相关。
摘要
-
使用 Kubernetes 内置资源实现常见的发布策略,如金丝雀发布、蓝/绿部署和 A/B 测试可能具有挑战性。
-
Knative Serving 引入了一个高级网络层,使我们能够精细控制流量如何路由到可以同时部署的不同版本的服务。此功能是在 Knative 服务之上实现的,减少了创建多个 Kubernetes 资源以实现金丝雀发布、蓝/绿部署和 A/B 测试发布策略的手动工作。Knative Serving 简化了将流量移动到新版本的运营负担,并且借助 Knative 自动扩展器,可以根据需求进行扩展和缩减。
-
Argo Rollouts 与 ArgoCD(在第四章中讨论)集成,并提供了使用 Rollouts 概念实现发布策略的替代方案。Argo Rollouts 还包括自动化测试新版本的功能,以确保我们在版本之间安全迁移(AnalysisTemplates 和 AnalysisRuns)。
-
平台团队必须通过提供灵活的机制和工作流程,使利益相关者(业务、产品经理、运营)能够通过实验来降低他们正在工作的应用程序新版本发布的风险。
-
按照逐步的教程,您可以通过使用 Knative 服务和不同的模式将流量路由到会议应用程序来获得实际操作经验。您还获得了使用 Argo Rollouts 来实现金丝雀发布和蓝/绿部署的经验。
9 衡量你的平台
本章涵盖了
-
学习衡量平台性能的重要性
-
实施 DORA 指标和了解持续改进的秘密
-
使用工具和标准来收集和计算指标
在第八章中,我们介绍了如何构建一个帮助您交付软件并使团队在需要时拥有所需工具的平台的原则。这一章的全部内容都是确保平台不仅对应用开发团队,而且对整个组织都是有效的。为了了解平台的表现,我们需要能够衡量它。我们对运行的软件有不同方式进行测量。然而,在本章中,我们将重点关注 DORA(DevOps 研究和评估)指标,这些指标为我们理解组织的软件交付速度以及我们在发生故障时恢复能力提供了良好的基础。
本章分为两个主要部分:
-
要衡量什么:DORA 指标和高性能团队
-
如何衡量我们的平台举措:
-
CloudEvents 和 CDEvents 来拯救
-
Keptn 生命周期工具包
-
让我们从了解我们应该衡量什么开始,为此,我们需要查看 DORA 指标。
9.1 要衡量什么:DORA 指标和高性能团队
在对行业进行彻底研究后,DevOps 研究和评估(DORA)团队确定了五个关键指标,这些指标突出了交付软件的软件开发团队的性能。最初,在 2020 年,只定义了四个关键指标,因此您可能会找到对“DORA 四个关键”指标的引用。在调查了数百个团队后,DORA 发现了哪些指标和指标将高性能/精英团队与其他团队区分开来,数字相当令人震惊。DORA 使用以下四个关键来对团队及其实践进行排名:
-
部署频率: 组织成功向客户发布软件的频率
-
变更领先时间: 应用团队产生的变更达到实时客户所需的时间
-
变更失败率: 新变化引入到我们的生产环境中导致的问题数量
-
恢复服务时间: 从我们的生产环境中解决问题的恢复所需时间
图 9.1 显示了按类别划分的 DORA 指标,其中前两个与团队的速率相关。后两个,变更失败率和恢复服务时间,表明我们作为一个组织从故障中恢复的可能性。

图 9.1 按类别划分的 DORA 指标
在 2022 年,一个关注可靠性的第五个关键指标被添加,以涵盖运营性能。我们只讨论四个软件交付指标,因为这本书的重点是应用开发团队,而不是运营团队。
如报告中所示,这五个关键指标建立了高性能团队与其通过这些指标表达的速率之间的明确关联。如果您管理团队以减少他们的部署频率(即他们向用户展示新版本的速度)并减少事件引起的时间,您的软件交付性能将会提高。
在本章中,我们将探讨如何计算我们正在构建的平台上的这些指标,以确保这些平台正在提高我们的持续交付实践。为了收集数据和计算这些指标,您需要利用您的团队用于交付软件的不同系统。例如,如果您想计算部署频率,您将需要每次新版本部署时访问生产环境的数据(见图 9.2)。另一个选择是使用执行向生产环境发布的环境管道的数据。图 9.2 显示了我们可以如何观察我们的 CI/CD 管道和生产环境,以计算部署频率等指标。

图 9.2 部署频率数据来源。
如果您想计算变更的领先时间,您将需要汇总来自您的源代码版本控制系统(如 GitHub/GitLab/BitBucket)的数据,并有一种方法将此信息与部署到生产环境的工件关联起来(见图 9.3)。

图 9.3 变更的领先时间数据来源
假设您有一种直接将提交与工件关联,然后再与部署关联的方法。在这种情况下,您可以依赖几个来源,但如果您想更详细地了解瓶颈在哪里,您可能选择汇总更多数据,以便能够看到时间是如何被花费的。
您可能需要利用事件管理和监控工具来计算变更失败率和恢复服务时间,如图 9.4 所示。

图 9.4 恢复指标数据来源
对于恢复指标(变更失败率和恢复服务时间),数据收集可能更具挑战性,因为我们需要找到一种方法来衡量应用性能下降或出现停机的时间。这可能需要来自实际用户的问题报告。
9.1.1 集成问题
这迅速变成一个系统集成挑战。一般来说,我们需要观察我们软件交付过程中涉及的系统,捕获相关数据,然后有机制来汇总这些信息。一旦这些信息可用,我们可以使用这些指标来优化我们的交付流程,找到并解决瓶颈。
虽然一些项目已经提供了开箱即用的 DORA 指标,但你必须评估它们是否足够灵活,以便将你的系统连接到它们。谷歌的“四要素”项目提供了一个开箱即用的体验,用于根据外部输出计算这些指标。你可以在cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance了解更多相关信息。
不幸的是,“四要素”项目要求你在 Google Cloud Platform 上运行,因为它使用 BigData 和 Google Cloud run 进行计算。遵循本书的原则,我们需要一个可以在不同云服务提供商之间工作且以 Kubernetes 为基础解决方案。其他工具,如 LinearB (linearb.io/),提供了一种 SaaS 解决方案来跟踪不同的工具。我还推荐 Codefresh (codefresh.io/learn/software-deployment/dora-metrics-4-key-metrics-for-improving-devops-performance/) 的一篇博客文章,该文章解释了计算这些指标所面临的挑战以及你需要的数据点。
要有一种 Kubernetes 原生的方法来计算这些指标,我们需要标准化我们从不同系统获取信息的方式,将此信息转换成我们可以用来计算这些指标的模型,并确保不同的组织能够通过它们的指标和非常多样化的信息来源扩展此模型。在下一节中,我们将探讨两个可以帮助我们完成这项任务的标准:CloudEvents (cloudevents.io/) 和 CDEvents (cdevents.dev/)。
9.2 如何衡量我们的平台:CloudEvents 和 CDEvents
越来越多的工具和服务提供商正在采用 CloudEvents (cloudevents.io) 作为封装事件数据的标准方式。在这本书中,我们已经介绍了 Tekton (tekton.dev) 和 Dapr PubSub (dapr.io),但如果您查看官方 CloudEvents 网站(访问 cloudevents.io 并滚动到 CloudEvents Adopters 部分),您将找到所有已经支持该标准的项目。在该列表中,您会发现 Argo Events (argoproj.github.io/argo-events/) 和 Knative Eventing (knative.dev/docs/eventing/),这些是我们没有介绍但与前面章节中描述的工具配合得非常好的项目。我发现云服务提供商的服务,如 GoogleCloud Eventarc (cloud.google.com/eventarc/docs) 和阿里巴巴云 EventBridge (www.alibabacloud.com/help/en/eventbridge) 出现在列表中,这表明 CloudEvents 将会持续存在。
虽然看到更多的采用是一个很好的指标,但在接收或想要发射 CloudEvent 时,还有很多工作要做。CloudEvents 是我们事件数据的简单且轻薄的信封。图 9.5 展示了 CloudEvent 的非常简单的结构。规范定义了 CloudEvent 所需的元数据,并验证 CloudEvent 将包含一个包含我们想要发送到其他系统的数据的事件有效载荷。

图 9.5 CloudEvents,一个简单的信封来封装我们的事件数据
使用 CloudEvents,开发者通过依赖 CloudEvents 规范来了解事件至少包含什么内容,从而发射和消费事件。由于 CloudEvents 规范不是传输特定的,我们可以使用不同的传输方式来移动 CloudEvents。该规范包括 AMQP、HTTP、AVRO、KAFKA、NATS、MQQT、JSON、XML、websockets 和 webhooks 等协议的绑定定义。您可以在github.com/cloudevents/spec/tree/main#cloudevents-documents找到完整的列表。
当我们在第七章中使用 Dapr PubSub 时,我们使用了 CloudEvents SDK 来验证事件类型并获取 CloudEvent 有效负载 (github.com/salaboy/platforms-on-k8s/blob/v2.0.0/conference-application/frontend-go/frontend.go#L118)。像 Tekton、Knative Eventing 和 Argo Events 这样的项目已经产生并提供了我们可以消费的 CloudEvents 源。例如,Knative Eventing 提供了 GitHub、GitLab、Kubernetes API 服务器、Kafka、RabbitMQ 等源 (knative.dev/docs/eventing/sources/#knative-sources)。Argo Events 增加了 Slack 和 Stripe 到列表中,但它提供了 20 多个开箱即用的事件源 (argoproj.github.io/argo-events/concepts/event_source/)。虽然像 Tekton 这样的项目为我们提供了它们自己管理的资源(如管道、任务、pipelineRuns 和 taskRuns)的内部事件,但以统一的方式收集关于其他工具的事件会更好。
如果我们想衡量我们平台中包含的工具如何帮助我们的团队发布更多软件,我们需要利用这些事件源来收集数据、聚合数据并提取有意义的指标。图 9.6 展示了我们可以利用的不同事件源来衡量工具如何帮助团队交付更多软件,但如果我们想计算指标,我们需要将这些事件存储在某处以进行进一步处理。

图 9.6 事件源和事件存储
如果我们想使用这些事件来计算指标,我们需要打开信封,读取数据,并根据这些数据聚合和关联这些事件。
这已经证明是一个挑战,因为每个生成 CloudEvents 的工具都可以定义其 CloudEvent 有效负载的架构。我们需要了解每个系统是如何编码有效负载的,以便提取我们用于计算指标所需的数据。如果有一个标准模型,可以快速根据它们对我们软件交付需求的意义来过滤和消费这些事件,那岂不是很好?欢迎 CDEvents (cdevents.dev)。
9.2.1 持续交付的 CloudEvents:CDEvents
CDEvents 只是 CloudEvents,但具有更具体的目的。它们映射到我们持续交付实践的不同阶段。CDEvents 是由持续交付基金会 (cd.foundation) 推动的倡议,正如其网站定义的,它们专注于在不同与持续交付相关的工具之间实现互操作性:“CDEvents 是持续交付事件的通用规范,使整个软件生产生态系统中的互操作性成为可能” (cdevents.dev)。
为了提供互操作性,CDEvents 规范定义了四个阶段(github.com/cdevents/spec/blob/v0.3.0/spec.md#vocabulary-stages)。这些阶段用于将概念上与我们的软件交付生态系统中的不同阶段和工具相关的事件进行分组:
-
核心:与任务编排相关的事件通常来自管道引擎。在这里,您可以找到关于“taskRun”和“pipelineRun”主题的事件规范。在此阶段可以找到像“PipelineRun started”或“TaskRun queued”这样的事件。
-
源代码版本控制:与源代码变更相关的事件。规范侧重于涵盖“repository”、“branch”和“change”等主题。在此阶段可以找到像“Change created”或“Change Merged”这样的事件。
-
持续集成:与构建软件、生成工件和运行测试相关的事件。此阶段涵盖“工件”、“构建”、“testCase”和“testSuite”等主题。在此阶段可以找到像“Artifact published”或“Build finished”这样的事件。
-
持续部署:与在不同环境中部署软件相关的事件。此阶段涵盖的主题是“services”和“environments”。在此阶段可以找到像“service deployed”或“environment modified”这样的事件。
-
持续运营:与我们的运行服务相关的事件。
图 9.7 显示了这些类别以及每个类别的示例事件。

图 9.7 CDEvents 规范定义的四个阶段
我们可以轻松地使用 CDEvents 来计算我们的软件交付指标,因为它们已经涵盖了这些指标感兴趣的主题。例如,我们可以使用持续部署阶段的事件来计算部署频率指标。我们可以结合持续部署事件和源代码版本控制事件来计算变更领先时间。
那么,问题随之而来,我们从哪里获取 CDEvents?CDEvents 是一个相对较新的规范,目前正在 CDFoundation 进行孵化,我坚信作为互操作性故事的一部分,这个规范可以作为不同工具和实现的钩子机制,将它们的工具映射到我们可以用来计算所有这些指标的标准模型,同时允许旧系统(以及不发射云事件的工具)也能从中受益。
本章将使用 CDEvents 规范来定义我们的标准化数据模型。我们将使用 CloudEvents 从各种系统中收集信息,并依靠 CDEvents 将传入的事件映射到我们软件交付实践的不同阶段。图 9.8 显示了与软件交付相关的最常见事件来源。

图 9.8 CDEvents 是为持续交付而设计的更专业的 CloudEvents。
工具如 Tekton 已经为 CDEvents 提供了实验性支持 (www.youtube.com/watch?v=GAm6JzTW4nc),正如我们在下一节中将要看到的,我们可以使用函数将 CloudEvents 转换为 CDEvents。更重要的是,CDEvents 工作组还专注于提供不同语言的软件开发工具包 (SDKs),这样无论你使用哪种编程语言,都可以构建消费和发射 CDEvents 的应用程序。
下一节将探讨如何构建一个基于 Kubernetes 的解决方案来计算 DORA 指标,并将其扩展以支持不同的指标和事件源。这很重要,以确保使用不同工具的不同平台可以使用它们的性能,并检测早期瓶颈和改进点。请注意,这只是一个示例,说明在不同与 Kubernetes 相关的项目背景下,不同的工具如何相互连接。
9.2.2 构建 CloudEvents 基础的指标收集管道
为了计算 DORA 团队提出的指标(部署频率、变更的领先时间、变更失败率和恢复服务的时间),我们需要收集数据。一旦我们有了来自不同系统的数据,我们需要将这些数据转换成标准化的模型,以便我们可以用它来计算指标。然后我们需要处理这些数据以计算每个指标的价值。我们需要存储这些计算的结果,然后我们需要使它们对所有人可用,可能使用一个图形仪表板来总结收集的数据和计算出的指标。
可以使用不同的工具来构建此数据收集、转换和聚合管道。然而,为了构建一个简单且可扩展的解决方案,我们将使用上一章中介绍的一些工具,例如 Knative Serving 来构建我们的聚合和转换函数,CloudEvents 和 CDEvents。我们还将使用 Knative Eventing 事件源,但这个演示可以很容易地扩展以支持任何其他 CloudEvent 源,例如 Argo Events。本节分为三个小节:
-
从事件源收集数据
-
数据转换为 CDEvents
-
指标计算
这些部分与提出的架构一一对应,从高层次来看,类似于图 9.9。

图 9.9 收集和转换数据以计算 DORA 指标
从一个高层次的角度来看,我们需要设计我们的数据收集和转换管道以支持任意数量的事件源,因为不同的公司和实现将收集我们无法预见系统的数据。我们在数据进入我们的系统之前就要求数据以 CloudEvents 的形式。如果您有不符合 CloudEvents 规范的事件源,您必须调整它们的数据以符合规范。这可以通过使用 CloudEvents SDKs(cloudevents.io/ > SDKs 部分)轻松实现,以包装您现有的事件以符合规范。
一旦数据进入我们的系统,我们将将其存储在持久存储中。在这种情况下,我们使用 PostgreSQL 数据库来存储所有传入的数据和计算。组件不会直接调用下一阶段(数据转换)。相反,每个组件定期从数据库中获取数据,并处理尚未处理的所有数据。这个阶段(数据转换)将已存储在数据库中的传入 CloudEvents 转换为用于计算指标的 CDEvents 结构。一旦转换到 CDEvents 结构,结果将存储在我们 PostgreSQL 数据库的单独表中。最后,“指标计算”阶段定期从数据库中读取所有未处理的新 CDEvents 并计算我们定义的指标。
这种简单的架构使我们能够根据接收到的数据插入新的数据源、新的转换逻辑,以及最终为您的特定领域指标(不仅限于 DORA 指标)的新指标计算逻辑。同样重要的是要注意,一旦我们保证传入的数据被正确存储,如果指标数据丢失,所有转换和计算都可以重新计算。让我们更深入地看看计算最简单的 DORA 四个关键指标“部署频率”所需的阶段。
9.2.3 从事件源收集数据
如图 9.9 所示,我们希望从多个来源消费数据,但我们已将 CloudEvents 设置为标准输入格式。虽然 CloudEvents 已被广泛采用,但许多系统仍然不支持该标准。本节将探讨 Knative Sources 作为一种机制,可以声明性地定义我们的事件源,并将非 CloudEvent 数据转换为 CloudEvents。
提出的解决方案随后暴露了一个 REST 端点以接收传入的 CloudEvents。一旦我们有了 CloudEvents,我们将验证数据并将其存储在一个名为cloudevents_raw的 PostgreSQL 表中。让我们看看 Knative Eventing 的事件源,因为我们只需安装和配置这些事件源,它们就可以自动为我们生成事件。
9.2.4 Knative Eventing 事件源
使用 Knative Eventing 事件源,您可以安装现有的事件源或创建新的事件源。图 9.10 显示了一些开箱即用的事件源以及这些事件将如何路由到我们的数据转换管道的数据收集步骤。

图 9.10 Knative Sources 和数据收集
Knative Eventing 提供了几个 Knative Eventing 事件源,由 Knative 社区和不同的软件供应商提供。以下列表并不详尽,但它涵盖了您可能想要用于计算您的指标的一些源:
-
APIServerSource
-
PingSource
-
GitHubSource
-
GitLabSource
-
RabbitMQSource
-
KafkaSource
在knative.dev/docs/eventing/sources/#third-party-sources检查第三方源的完整列表。这些源将事件转换,例如,从 Kubernetes API 服务器、GitHub 或 RabbitMQ AMQP 消息转换为 CloudEvents。
如果您想使用可用的 Knative Sources 之一,例如 APIServerSource,您只需确保源已安装到您的集群中,然后根据您的需求配置源(参见列表 9.1)。为了计算部署频率指标,我们将利用与部署相关的 Kubernetes 事件。您可以通过定义一个 APIServerSource 资源来声明性地配置源以及事件将被发送到何处。
列表 9.1 Knative Source APIServerSource 定义
apiVersion: sources.knative.dev/v1
kind: ApiServerSource ①
metadata:
name: main-api-server-source ②
spec:
serviceAccountName: api-server-source-sa ③
mode: Resource
resources:
- apiVersion: v1
kind: Event ④
sink: ⑤
ref:
apiVersion: v1
kind: Service
name: cloudevents-raw-service
namespace: dora-cloudevents
① ApiServerSource 是我们用来配置从 Kubernetes 事件流中读取的 Knative ApiServerSource 组件的资源类型(www.cncf.io/blog/2021/12/21/extracting-value-from-the-kubernetes-events-feed/),将这些事件转换为 CloudEvents,并将它们发送到接收器(目标目的地)。
② 与每个 Kubernetes 资源一样,我们需要为此资源定义一个名称。我们可以配置任意数量的 ApiServerSource。
③ 由于我们从 Kubernetes API 服务器读取事件,我们需要有访问权限。因此,需要存在一个 ServiceAccount 以启用 ApiServerSource 组件从内部事件流中读取。您可以检查为使此 ApiServerSource 资源正常工作所需的 ServiceAccount、Role 和 RoleBinding 资源,请参阅github.com/salaboy/platforms-on-k8s/blob/main/chapter-9/dora-cloudevents/api-serversource-deployments.yaml。
④ 如前所述,此源对类型为 Event 的资源感兴趣。
⑤ 在接收器部分,我们定义了我们希望将从这个源生成的 CloudEvents 发送到何处。在这种情况下,我们使用了一个指向名为 cloudevents-raw-service 的 Kubernetes 服务的服务引用,该服务位于 four-keys 命名空间中。Knative 源在引用其他 Kubernetes 资源时,会检查这些资源是否存在,并且只有当目标服务被找到时才会准备就绪。或者,如果服务不在 Kubernetes API 上下文中,我们也可以指向一个 URI,但我们会失去这个有价值的检查,这可以帮助我们解决将事件发送到不存在端点的情况。
如您所想,ApiServerSource将生成大量的事件,这些事件被发送到cloudevents-raw-service并存储在 PostgreSQL 数据库中。我们只能配置更复杂的路由和过滤来转发我们感兴趣的事件,但我们也可以在下一阶段应用过滤,从而实现一种方法,使我们能够在数据收集过程演变时添加更多指标。使用这个源,每当创建、修改或删除新的部署资源时,我们都会接收到一个或多个 CloudEvents 并将它们存储在数据库中。
如果您已经有了一个正在生成事件的系统但需要 CloudEvents,您可以创建自己的自定义 Knative Eventing 事件源。查看以下教程以获取有关如何执行此操作的更多信息:knative.dev/docs/eventing/custom-event-source/custom-event-source/。
声明和管理您的事件源使用 Knative Eventing 事件源的一个大优点是,您可以像查询任何其他 Kubernetes 资源一样查询您的源,监控和管理它们的状态,并在出现问题时使用 Kubernetes 生态系统中的所有工具进行故障排除。一旦 CloudEvents 存储在我们的数据库中,我们就可以分析它们并将它们映射到 CDEvents 以进行进一步计算。
9.2.5 数据转换为 CDEvents
现在我们已经在我们的 PostgreSQL 数据库中有 CloudEvents,我们已经验证了它们是有效的 CloudEvents。我们希望将其中一些非常通用的 CloudEvents 转换为 CDEvents,我们将使用它们来计算我们的指标。
如介绍中所述,这些转换将取决于您试图计算哪种类型的指标。对于这个例子,我们将查看与部署资源相关的内部 Kubernetes 事件来计算部署频率指标,但可以使用完全不同的方法。例如,您不必查看 Kubernetes 内部事件,也可以查看 ArgoCD 事件或 Tekton Pipeline 事件来监控何时触发部署,但来自集群外部。图 9.11 显示了将 CloudEvent 映射到 CDEvents 所需的映射和转换过程。

图 9.11 从 CloudEvents 到 CDEvents 的映射和转换
我们需要一种方法将一个非常通用的 CloudEvent 映射到具体的 CDEvent,以指示服务部署已发生或已更新。这种映射和转换逻辑可以用任何编程语言编写,因为我们只处理 CloudEvents 和 CDEvents。由于我们可能接收到的事件量很大,因此不阻塞并处理所有到达的事件至关重要。因此,这里选择了更异步的方法。数据转换逻辑被安排在固定的时间间隔,这可以根据我们希望/能够处理传入事件的多寡进行配置。
在这个例子中,我们将映射和转换具有type等于dev.knative.apiserver.resource.add和data.InvolvedObject.Kind等于Deployment的传入事件到类型为dev.cdevents.service.deployed.0.1.0的 CDEvent。这种转换特别符合我们的需求,因为它将来自 Knative APIServerSource 的事件与 CDEvents 规范中定义的事件相关联,如图 9.12 所示。

图 9.12 部署的实体映射和 CDEvent 创建
为了计算不同的指标,我们需要更多的这些转换。一个选择是将所有转换逻辑添加到一个单独的容器中。这种方法将允许我们将所有转换作为一个单一单元进行版本控制,但同时也可能使编写新转换的团队复杂化或受限,因为他们只有一个地方可以更改代码。我们可以采取的另一种方法是使用基于函数的方法,我们可以提升创建单一用途函数来执行这些转换。通过使用函数,只有当前正在转换事件的函数才会运行。所有未被使用的函数都可以进行降级。如果我们有太多事件需要处理,可以根据流量需求进行函数的升级。


如图 9.13 所示,需要一个新组件来路由从数据库中读取的 CloudEvents 到具体的函数。每个转换函数可以通过检查其有效载荷、使用外部数据源丰富内容或简单地将整个 CloudEvent 包装成 CDEvent 来转换传入的 CloudEvent。
数据转换路由组件必须足够灵活,以便允许将新的转换函数插入到系统中,并允许多个函数处理同一事件(同一个 CloudEvent 被发送到一个或多个转换函数)。
转换和映射函数不需要关心 CDEvents 如何持久化。这使我们能够保持这些函数简单且仅关注转换。一旦转换完成并生成新的 CDEvent,函数将事件发送到 CDEvents 端点组件,该组件将 CDEvent 存储在我们的数据库中。
在转换完成后,我们将在数据库中存储零个或多个 CDEvents。这些 CDEvents 可以被我们在下一节中将要查看的指标计算函数使用。
9.2.6 指标计算
为了计算我们的指标(DORA 或自定义指标),我们将使用与 CDEvents 转换和映射相同的基于函数的方法。在这种情况下,我们将编写用于计算不同指标的函数。因为每个指标都需要从不同的事件和可能系统中聚合数据,所以每个指标计算函数可以实现不同的逻辑,见图 9.14。用于计算指标的机制取决于编写计算代码的开发者。

图 9.14 使用函数计算 DORA 指标
为了计算指标,每个函数可以被配置为从数据库中获取非常具体的 CDEvents,并且根据我们需要为特定指标获取更新的频率不同,有不同的时间段。指标结果可以存储在数据库中或发送到外部系统,具体取决于你想要如何处理计算出的数据。
如果我们以计算更具体的部署频率指标为例,我们需要实现一些自定义机制和数据结构来跟踪该指标,如图 9.15 所示。

图 9.15 部署频率计算流程
计算部署频率指标的简化流程如图 9.15 所示,其中步骤#1 是从cdevents_raw表中获取与部署相关的 CDEvents。Create Deployments structure function负责读取类型为dev.cdevents.service.deployed.0.1.0的 CDEvents,检查有效载荷和元数据,并创建一个可以稍后查询的新结构。步骤#2 负责将这个新结构持久化到我们的数据库中。这个结构的主要目的是使我们的数据更容易和更高效地查询我们正在实施的指标。在这种情况下,创建了一个新的deployment结构(和表)来记录我们想要用于计算部署频率指标的数据。在这个简单的例子中,部署结构包含服务的名称、时间戳和部署的名称。在步骤#3 中,我们可以使用这些数据通过服务获取我们的部署频率,并按日、周或月显示这些信息。这些函数需要是无状态的,这意味着我们可以使用相同的 CDEvents 作为输入重新触发指标的计算,并且应该获得相同的结果。
可以向此流程添加优化;例如,可以创建一个自定义机制来避免重新处理已经处理过的 CDEvents。这些定制可以被视为每个指标的内部机制,开发者应该能够根据需要添加与其他系统和工具的集成。为了示例的目的,获取部署频率函数可以从数据库中检索指标。然而,在更现实的场景中,你可以有一个仪表板直接查询存储简化结构的数据库,因为许多仪表板解决方案都提供了开箱即用的 SQL 连接器。
既然我们已经覆盖了计算部署频率指标的流程,让我们看看一个工作示例,其中我们将安装所有用于数据收集、数据转换和指标计算所需的组件。
9.2.7 工作示例
本节将探讨一个工作示例,展示我们如何结合数据收集、将数据转换为 CDEvents 以及为我们基于 Kubernetes 的平台进行指标计算。它涵盖了一个非常基础的示例以及如何安装和运行计算部署频率指标所需组件的逐步教程(github.com/salaboy/platforms-on-k8s/blob/main/chapter-9/dora-cloudevents/README.md)。
在本例中实现的架构将前几节中定义的阶段组合在一起:数据收集、数据转换和指标计算。该架构涵盖的主要方面之一是数据转换和指标计算组件的可扩展性和可插拔性。该架构假设我们将以 CloudEvents 的形式收集数据,因此用户负责将他们的事件源转换为 CloudEvents 以使用此架构。
图 9.16 展示了所有组件如何相互关联,以提供决定我们想要收集哪些事件以及如何将它们转换为 CDEvents 以计算 DORA 指标的功能。

图 9.16 捕获和计算 DORA 指标的示例架构
虽然该架构最初可能看起来很复杂,但它被设计成允许收集和处理来自各种来源的事件所需的自定义扩展和映射。
按照逐步教程,你将创建一个新的 Kubernetes 集群来安装收集 CloudEvent 和计算指标所需的所有组件。然而,架构并不局限于单个集群。在你创建并连接到集群后,你将安装如 Knative Serving 这样的工具用于我们的函数运行时,以及仅用于我们的事件源的 Knative Eventing。一旦集群准备就绪,你将创建一个新的namespace来托管所有积极处理收集数据的组件,以及一个 PostgreSQL 实例来存储我们的事件。
存储事件和指标
一旦我们有了数据库来存储事件和指标信息,我们需要为我们的组件创建存储和读取事件的表。对于这个例子,我们将创建以下表:cloudevents_raw、cdevents_raw和deployments,如图 9.17 所示。

图 9.17 表格、CloudEvents、CDEvents 和指标计算
让我们看看我们将要存储在这三个表中的信息。cloudevents_raw表存储了来自不同来源的所有传入 CloudEvent。这个表的主要目的是数据收集:
-
这个表的架构非常简单,只有三个列:
-
event_id: 这个值由数据库生成。 -
event_timestamp: 存储事件接收的时间戳。这可以用于稍后对事件进行重新处理排序。 -
content: 存储 CloudEvent 序列化的 JSON 版本在一个 JSON 列中。
-
-
这个表尽可能保持简单,因为我们不知道我们会得到什么类型的云事件,在这个阶段,我们不想反序列化和读取有效负载,因为这可以在数据转换阶段完成。
cdevents_raw表存储了我们过滤和转换所有传入的 CloudEvent 后感兴趣存储的所有 CDEvent。由于 CDEvent 更具体,并且我们关于这些事件的元数据更多,因此这个表有更多的列:
-
cd_id: 存储原始 CloudEvent 的 ID。 -
cd_timestamp: 存储原始 CloudEvent 接收的时间戳。 -
cd_source: 存储原始 CloudEvent 生成的来源。 -
cd_type: 存储并允许我们根据不同的 CDEvent 类型进行过滤。存储在这个表中的 CDEvent 类型由我们设置中运行的转换函数定义。 -
cd_subject_id: 存储与该 CDEvent 关联的实体的 ID。这个信息是在我们的转换函数分析原始 CloudEvent 的内容时获得的。 -
cd_subject_source: 存储与该 CDEvent 关联的实体的来源。 -
content: 我们 CDEvent 的 JSON 序列化版本,其中包含原始 CloudEvent 作为有效负载。
deployments 表是自定义的,用于计算部署频率指标。用于计算不同指标的自定义表中存储的内容没有规则。为了简化,此表只有三个列:
-
deploy_id: 用于识别服务部署的 ID。 -
time_created: 部署创建或更新的时间。 -
deploy_name: 用于计算指标的部署名称。
一旦我们准备好了存储事件和指标数据的表,我们需要让事件流进入我们的组件,为此,我们需要配置事件源。
配置事件源
最后,在安装数据转换或指标计算函数之前,我们将从 Knative Eventing 配置 Kubernetes API 服务器事件源以检测新部署的创建。见图 9.18。

图 9.18 使用 Knative Eventing API 服务器源的示例。我们可以通过使用 Knative Eventing API 服务器源来访问 Kubernetes 事件流,该源将内部事件转换为 CloudEvents,这些事件可以被路由到不同的系统进行过滤和处理。
在这里,您可以使用任何 CloudEvent 兼容的数据源。Knative API 服务器源是展示如何轻松消费和路由事件进行进一步处理的示例。
检查类似 Argo Events (argoproj.github.io/argo-events/) 和其他 Knative Eventing 源 (knative.dev/docs/eventing/sources/) 的项目,以熟悉开箱即用的功能。还要检查 CloudEvents 规范采用者列表 (cloudevents.io/),因为所有这些工具都已经生成 CloudEvents,您可以使用它们进行消费并将它们映射到计算指标。
部署数据转换和指标计算组件
现在我们有了存储事件和指标数据的地方,事件源已配置并准备好在用户与我们的集群交互时发出事件,我们可以部署将接收这些事件、过滤它们并将它们转换为计算部署频率指标的组件。逐步教程部署以下组件:
-
CloudEvents 端点: 提供一个 HTTP 端点以接收 CloudEvents 并将它们连接到数据库进行存储。
-
CDEvents 端点: 提供一个 HTTP 端点以接收 CDEvents 并将它们连接到数据库进行存储。
-
CloudEvents 路由器: 从数据库中读取 CloudEvents 并将它们路由到配置的转换函数。此组件允许用户将他们的转换函数插入以将 CloudEvent 转换为 CDEvent 以进行进一步处理。CloudEvents 路由器通过从数据库中检索未处理的事件定期运行。
-
(CDEvents)转换函数: 用户可以定义转换函数并将 CloudEvents 映射到 CDEvents。这里的想法是使用户能够添加所需的所有函数来计算 DORA 和其他指标。
-
(部署频率)计算函数: 指标计算函数提供了一种通过从数据库中读取 CDEvents 来计算不同指标的方法。如果需要,这些函数可以将计算出的指标存储在自定义数据库表中。
-
(部署频率) 指标端点: 这些指标端点可以可选地暴露给应用程序以消费计算出的指标。或者,仪表板可以直接从数据库查询数据。
图 9.19 展示了 CloudEvents 如何流经我们已安装的不同组件。

图 9.19 数据从数据源流向产生 CloudEvents 的 CloudEvents 端点,其唯一任务是将这些事件存储到事件存储中。从那里,CloudEvents Router 拥有逻辑来决定将事件路由到转换函数,这使我们能够将 CloudEvents 映射到 CDEvents 以进行进一步处理。一旦我们有了 CDEvents,计算函数就可以读取这些事件来聚合数据并产生指标。指标消费者可以通过与指标端点交互来获取指标,该端点将从指标数据库中检索计算出的指标。
一旦我们的组件启动并运行,我们就可以开始使用我们的集群来生成由这些组件过滤和处理的事件,以产生部署频率指标。
部署频率指标针对您的部署
我们需要将新的工作负载部署到我们的集群中,以计算部署频率指标。教程包括所有转换和指标计算函数,以监控来自部署资源的事件。
当开发团队能够创建和更新他们现有的部署时,平台团队能够透明地监控平台效率,以便团队能够执行他们的工作。图 9.20 显示了涉及的团队以及本例中如何计算指标。

图 9.20 组件和数据流以测量性能指标
最后,如果您在 KinD 上运行示例,您可以curl以下端点。
> curl http://dora-frequency-endpoint.dora-cloudevents.127.0.0.1.sslip.io/
➥deploy-frequency/day | jq
您应该看到以下类似列表。
列表 9.2 获取部署频率指标
[
{
"DeployName":"nginx-deployment-1",
"Deployments":3,
"Time":"2022-11-19T00:00:00Z"
},
{
"DeployName":"nginx-deployment-3",
"Deployments":1,
"Time":"2022-11-19T00:00:00Z"
}
]
转换和指标计算函数每分钟调度运行一次。因此,这些指标只有在函数执行完毕后才会返回。或者,您可以使用 Grafana 这样的仪表板解决方案来连接到我们的 PostgreSQL 数据库并配置指标。仪表板工具可以专注于存储特定指标数据的表。对于我们的部署频率示例,deployments表是唯一与显示指标相关的表。
我强烈建议您检查示例并尝试在本地运行它,遵循逐步教程,如果您有问题或想帮助改进它,请与我联系。修改示例以不同方式计算指标或添加您自定义的指标将为您提供这些指标计算复杂性的良好概述,同时,这也说明了为什么让我们的应用开发和运维团队能够几乎实时地了解情况是多么重要。
在下一节中,我们将探讨 Keptn 生命周期工具包 (keptn.sh),这是一个开源的 CNCF 项目,它构建了不同的机制,不仅用于监控、观察和计算我们云原生应用的指标,而且在预期之外或需要与其他系统集成时也能采取行动。
9.3 Keptn 生命周期工具包
Keptn 生命周期工具包 (KLT) 是一个云原生生命周期编排工具包。KLT 专注于部署可观察性、部署数据访问和部署检查编排。Keptn 不仅关注监控和观察我们的工作负载的状态,而且还提供了在出现问题时进行检查和采取行动的机制。
正如我们在上一节中看到的,获取基本指标,如部署频率,可以非常有助于衡量团队的表现。虽然部署频率只是一个指标,但我们可以用它来开始衡量我们的早期平台倡议。在本节中,我想展示 KLT 如何通过采用与第 9.2 节中讨论的不同但互补的方法来帮助您完成这项任务。
Keptn 扩展了 Kubernetes 调度器组件(它决定我们的工作负载将在我们的集群上运行的位置),以监控和提取关于我们工作负载的信息,如图 9.21 所示。这种机制使得团队可以通过提供 Keptn 任务定义资源来设置自定义的预/后部署任务。Keptn 正在计划使用 Kubernetes 内置的调度门功能,这是一个在撰写本文时被提议给 Kubernetes 社区的功能 (mng.bz/PRW2)。
注意:您可以通过以下链接跟随一个逐步教程,以了解 Keptn 的实际操作:github.com/salaboy/platforms-on-k8s/blob/main/chapter-9/keptn/README.md。

图 9.21 Keptn 架构提供即插即用的可观察性和应用程序生命周期钩子。
Keptn 使用标准的 Kubernetes 注解来识别哪些应用程序希望被监控和管理。我为 Conference 应用程序包含了以下注解,以便 Keptn 了解我们的服务。Agenda 服务部署资源包括以下注解,如列表 9.3 所示 (github.com/salaboy/platforms-on-k8s/blob/main/conference-application/helm/conference-app/templates/agenda-service.yaml#L14)。
列表 9.3 Kubernetes 标准应用程序注解
app.kubernetes.io/name: agenda-service
app.kubernetes.io/part-of: agenda-service
app.kubernetes.io/version: v1.0.0
Keptn 现在已经了解 Agenda 服务,并且可以监控和执行与此服务生命周期相关的操作。注意 part-of 注解,它允许我们监控单个服务并将一组服务分组在同一个逻辑应用下。这种分组允许 Keptn 为每个服务以及逻辑应用(共享相同 app.kubernetes.io/part-of 注解值的多个服务组)执行部署前后的操作。本例中没有使用该功能,因为我希望保持内容简单并专注于单个服务。
逐步教程安装了 Keptn、Prometheus、Grafana 和 Jaeger,以便我们了解 Keptn 的功能。一旦 Keptn 在您的集群中安装完成,您需要让 Keptn 知道哪些命名空间需要被监控,通过在命名空间资源上添加 Keptn 注解来实现。您可以通过运行以下命令在默认命名空间中启用 Keptn:
kubectl annotate ns default keptn.sh/lifecycle-toolkit="enabled"
一旦 Keptn 开始监控特定的命名空间,它将寻找带有注解的部署以开始获取 Keptn 应用程序 Grafana 仪表板可以消费的指标,如图 9.22 所示。

图 9.22 通知服务的 Keptn 应用程序 Grafana 仪表板
此仪表板显示了我们在默认命名空间中运行带有注解的部署(所有 Conference 应用程序的服务)的部署频率。在逐步教程中,我们对通知服务部署进行更改,以便 Keptn 可以检测到更改并在仪表板中显示新版本。如图 9.22 所示,部署的平均时间为 5.83 分钟。在旁边,您可以确切地看到部署 v1.0.0 和 v1.10 所花费的时间。这些仪表板可供每个服务的责任团队使用,有助于提供整个发布新版本过程的可见性。从第一天起就有这些信息可以帮助展示团队改进工作流程或找到可以轻松解决的瓶颈和重复性问题。
除了获得所有这些信息和前面提到的开箱即用的指标外,KLT 还更进一步,通过提供执行预/部署任务的钩子点。我们可以使用这些任务在发布前验证环境状态,向值班团队发送通知,或者只是审计流程。部署后,我们可以使用部署后钩子运行验证测试,向客户发送关于更新的自动化通知,或者只是祝贺团队出色的表现。
Keptn 引入了 KeptnTaskDefinitions 资源,它支持 Deno (deno.land/)、Python3 或任何容器镜像引用 (lifecycle.keptn.sh/docs/yaml-crd-ref/taskdefinition/) 来定义任务行为。用于逐步教程的 KeptnTaskDefinition 资源相当简单,看起来像列表 9.4。
列表 9.4 使用 Deno 的 Keptn TaskDefinition
apiVersion: lifecycle.keptn.sh/v1alpha3
kind: KeptnTaskDefinition
metadata:
name: stdout-notification ①
spec:
function:
inline:
code: |
let context = Deno.env.get("CONTEXT"); ②
console.log("Keptn Task Executed with context: \n");
console.log(context);
① 团队将使用此资源名称来定义此任务将在哪里执行。这是一个可重用的任务定义,因此可以从不同服务的生命周期钩子中调用它。
② 我们可以通过调用 Deno.env.get("CONTEXT") 来访问正在执行的任务的上下文。这为我们提供了创建任务时使用的所有详细信息,例如哪个工作负载请求执行此任务。
要将任务定义与我们的某个服务绑定,我们在部署中使用 Keptn 特定的注释:
keptn.sh/post-deployment-tasks: stdout-notification
此注释将配置 Keptn 在更改通知服务部署并部署新版本后执行此任务。Keptn 将创建一个新的 Kubernetes Job 来运行 KeptnTaskDefinition。这意味着您可以通过查看默认命名空间中的作业执行来查询所有预/部署任务定义的执行。
通过使用注释和 KeptnTaskDefinitions,平台工程团队可以创建一个共享任务库,团队可以在他们的工作负载中重用这些任务,或者更好的是,他们可以使用突变 Webhook 或 OPA 这样的策略引擎自动突变部署资源以添加 Keptn 注释。
如果你更改了通知服务部署并跟踪日志,你应该会看到以下内容(列表 9.5)。
列表 9.5 TaskDefinition 执行的预期输出
Keptn Task Executed with context:
{
"workloadName":"notifications-service-notifications-service",
"appName":"notifications-service",
"appVersion":"",
"workloadVersion":"v1.1.0",
"taskType":"post",
"objectType":"Workload"
}
如果你查看图 9.23 中的 Jaeger,你可以通过查看 Keptn 生命周期操作符跟踪来了解部署我们通知服务新版本所涉及的所有步骤。

图 9.23 Keptn 生命周期操作符跟踪服务更新
如果你在你自己的环境中运行逐步教程,你可以看到在服务的新版本启动并运行后,部署后钩子正在被安排。
在这个简短的章节中,我们学习了 Keptn 生命周期工具包能为我们做什么的基础知识,以及我们如何从第一天开始就受益于这些指标,以及我们如何通过使用声明性方式添加预/部署任务来增加对我们服务生命周期的控制。
我强烈建议您查看 Keptn 网站以及他们提供的其他更高级的机制,例如评估(lifecycle.keptn.sh/docs-klt-v0.8.1/concepts/evaluations/),它允许我们做出决策,甚至可以阻止不符合某些要求(如内存消耗增加或 CPU 使用过多)的部署。
虽然 Keptn 使用的方法与第 9.2 节中描述的方法完全不同,但我坚信这些方法是互补的。我希望看到 Keptn 和 CloudEvents 之间的进一步集成。如果您对这个话题感兴趣,我鼓励您加入github.com/keptn/lifecycle-toolkit/issues/1841的讨论。
9.4 平台工程之旅的下一步是什么?
本章涵盖的示例强调了衡量我们的技术决策的重要性。无论好坏,每个决策都会影响所有参与软件交付的团队。
我们平台内建这些指标可以帮助我们衡量改进并证明投资于促进我们的软件交付实践的工具有价值。如果我们想在平台中包含一个新工具,你可以测试你的假设并衡量每个工具或采用的方法的影响。让这些指标对所有团队都易于访问和可见是一种相当常见的做法,这样当事情出错或工具不符合预期时,你将会有确凿的证据来支持你的主张。
从平台工程的角度来看,我强烈建议不要将这个话题留到最后一刻(就像我在书中这一章所做的那样)。使用 KLT 等工具,你可以以较小的投入获得洞察力,并使用在业界被广泛理解的标准化监控技术。研究 CloudEvents 和 CDEvents 是值得的,不仅从监控和指标计算的角度来看,而且对于与其他工具和系统的基于事件的集成来说也是如此。图 9.24 显示,通过利用我们在黄金路径中使用的工具的事件源,我们可以让我们的团队能够了解他们的决策如何影响整个软件交付链。

图 9.24 我们平台提供的黄金路径和工作流程是计算团队性能指标的最佳原始信息来源。
确保您的平台的基本指标可以计算,这将帮助您的团队思考每个发布版本的端到端流程——瓶颈在哪里,他们花了或浪费了大部分时间在哪里。如果 DORA 指标对您的组织来说太难实施,您可以专注于衡量您平台上的黄金路径或主要工作流程。例如,根据第六章提供的示例,您可以衡量配置开发环境所需的时间,提供哪些功能,以及团队多久请求一次新实例,如图 9.25 所示。

图 9.25 平台和应用行走骨架指标
通过收集指标,不仅来自客户应用程序,还来自平台特定的流程,如创建开发环境,您的团队将能够全面了解他们使用的工具以及工具的变化如何影响和释放软件交付的速度。图 9.26 展示了我们平台之旅的回顾以及这些指标对我们平台团队的重要性。记住,如果您正在衡量您的平台项目,您的平台将会变得更好。

图 9.26 利用平台组件收集数据和计算指标
9.5 最后的想法
我希望阅读这本书的示例已经给您提供了足够的实践经验来应对现实生活中的挑战。虽然这里涵盖的示例并不全面或深入,但目的是展示平台工程团队必须处理的各种主题。云原生空间正在不断演变,我在写这本书时评估的工具在两年内已经完全改变,推动全球团队在决策上非常灵活。犯错误和审查决策是平台工程师必须为小型和大型组织做的日常工作的一部分。
回到这本书的开头,平台工程师必须将这些决策封装在可以维护和演化的平台 API 之后,因此了解不同团队所需的能力对于成功进行平台工程之旅至关重要。提供自助服务功能和关注团队的需求应该是平台工程师优先事项列表上的重要影响因素。
不幸的是,我没有无限的页面或无限的时间来不断添加内容到这本书中,但我尽力包括了我在云原生空间工作期间看到组织和社会面临的话题和挑战。我们已经到达了一个点,在 Kubernetes 生态系统中,工具正在成熟,更多项目正在毕业,这表明越来越多的公司正在重用工具而不是自己构建。
我故意省略了诸如使用自定义控制器扩展 Kubernetes 等主题,因为平衡为您的平台构建的内部构建内容需要由平台工程团队仔细定义。创建和维护您的扩展应留给非常特殊的情况,在这些情况下,没有工具可以解决您试图解决的问题。对于最常见的情况,正如我们在本书中所看到的,CI/CD、GitOps、云中的基础设施配置、开发者工具、平台构建工具和其他工具已经足够成熟,您可以使用并在必要时扩展它们。
将诸如服务网格、策略引擎、可观察性、事件管理、操作工具和云开发环境等主题排除在本书之外是非常困难的。有一些非常棒的项目需要整章来介绍。但作为一个平台工程师,您必须继续研究并关注云原生社区,以了解新的发展和项目如何帮助您的组织团队。
我强烈建议您参与您当地的 Kubernetes 社区,并在开源生态系统中保持活跃。这不仅为您提供了一个极佳的学习游乐场,而且有助于您就采用哪些技术做出正确的知情决策。了解这些项目背后的社区有多强大,对于验证它们是否在解决首先需要解决方案且足够普遍以至于可以用通用(非组织)特定方式解决的问题至关重要。像 OSS Insight (ossinsight.io/) 这样的工具为决策提供了巨大的价值,并确保如果您在开源项目中投入时间和资源,一个活跃的社区将维护您的更改和改进。
最后,请关注我的博客 (salaboy.com),因为将会有更多与本书相关的文章发表,以探讨我认为对平台工程团队重要的其他主题。如果您对开源感兴趣,扩展或修复本书中提供的示例是一个很好的方式,可以亲身体验大多数开源项目使用的所有工具。
摘要
-
使用 DORA 指标可以清楚地了解组织如何向客户交付软件。这可以用来理解导致我们在构建的平台上的改进的瓶颈。使用基于我们软件交付实践的团队绩效指标将帮助您了解您的平台倡议如何影响团队的工作以及整体组织的益处。
-
CloudEvents 标准化了我们消费和发射事件的方式。在过去的几年里,我们看到了 CNCF 生态系统中不同项目对 CloudEvents 的采用率上升。这种采用使我们能够依赖 CloudEvents 来获取有关组件和其他系统的信息,我们可以聚合和收集有助于决策的有用信息。
-
CDEvents 提供了一个 CloudEvents 扩展,这是一组与持续交付软件实践相关的更具体的 CloudEvents。虽然我预计 CDEvents 的采用率会随着时间的推移而增长,但我们已经看到了如何将 CloudEvents 映射到 CDEvents 来计算 DORA 指标。通过使用 CDEvents 作为计算这些指标的基础模型,我们可以将任何事件源映射以贡献这些指标的计算。
-
如果我们能衡量我们的平台,我们将知道需要改进什么以及组织在交付实践中遇到哪些困难。这些指标提供的反馈循环为负责持续改进我们团队日常使用的工具和流程的平台团队提供了宝贵的信息。
-
如果你遵循了本章的逐步教程,你将获得实际操作经验,包括设置 CloudEvent 源、监控部署以及 CDEvents 如何帮助我们标准化关于软件交付生命周期的信息。你还安装了 Keptn 作为另一种监控工作负载和执行预/后部署任务以验证新版本是否按预期工作的方法。


浙公网安备 33010602011771号