Kubernetes-机密管理指南-全-
Kubernetes 机密管理指南(全)
原文:Kubernetes Secrets Management
译者:飞龙
前言
前言
作为技术人员,我们自然会寻求创新的方法来解决各种问题——无论是通过使用新的或现有的方法、框架或技术。过去几年中,两位作者都着迷于一种技术,那就是 Kubernetes。虽然 Docker 将容器引入了大众市场,但正是 Kubernetes 为大规模运行容器提供了一个可扩展的平台。
我们从不同的角度接近 Kubernetes:一个是从基础设施心态出发,了解构建 Kubernetes 集群需要什么,另一个则是专注于应用程序,希望利用底层基础设施提供的功能。有几个交织的主题适用于基础设施和应用导向的个人;其中一个始终如一的区域,无论是否使用 Kubernetes,那就是安全性。
安全性是那些虽然至关重要,但常常与其他兴趣领域相比被降级或忽视的话题之一。我们在与组织和开发者合作的过程中发现,他们可能不理解哪些类型的资源需要保护,或者如何进行保护。那些开始使用 Kubernetes 的人也会惊讶地发现,Kubernetes 在最初发布时在安全性方面几乎没有什么。被称为 Secrets 的保护机制是在 1.0 版本之前作为为敏感资产提供某种形式保护的一种解决方案而开发的。因此,Secrets 提供了最低级别的安全性,考虑到资源的名称,这可能让人感到惊讶。
对于应该保护哪些类型的资产以及如何保护它们的陌生感,原生 Kubernetes 功能提供的虚假安全感,以及这个领域可用的众多解决方案,这些都预示着可能是一场灾难的潜在配方。我们编写这本书的目标是强调“左移”的心态,在这种心态下,当与 Kubernetes 合作时,安全性成为一个关键关注点,并解决包含的工具、替代解决方案以及它们如何在不同的交付过程中被整合的能力和陷阱。我们并不打算——也不太可能——解决所有可能的安全选项,但 Kubernetes Secrets Management 中讨论的概念和实现应该能够使你在与 Kubernetes 合作时更加成功和安全。
致谢
在这些充满挑战的时期,我想感谢 Santa(飞吧,飞吧)、Uri(感谢所有的对话)、Guiri(Vive Le Tour)、Gavina、Gabi(感谢啤酒)、以及 Edgar 和 Ester(是的,今天是星期五);我的工作伙伴 Edson、Sebi、Natale、Ana-Maria、Elder,当然还有 Burr 和 Kamesh(无论你在哪里,你都会在我们团队中)——我们是最棒的团队!此外,感谢 Jonathan Vila、Abel Salgado 和 Jordi Sola 关于 Java 和 Kubernetes 的精彩对话。
特别感谢所有阅读我们的手稿并提供了如此宝贵反馈的审稿人:Alain Lompo、Atila Kaya、Chris Devine、Clifford Thurber、Deepak Sharma、Giuseppe Catalano、John Guthrie、Jon Moore、Michael Bright、Mihaela Barbu、Milorad Imbra、Peter Reisinger、Robin Coe、Sameer Wadhwa、Satadru Roy、Sushant Bhadkamar、Tobias Ammann 和 Werner Dijkerman;你们的贡献帮助提高了这本书的质量。
最后但同样重要的是,我想感谢 Anna,因为她在这里;我的父母,Mili 和 Ramon,他们为我买了第一台电脑;以及我的女儿们 Alexandra 和 Ada,“我是你眼中的灵魂。”
—Alex Soto Bueno
在全球大流行期间,在处理其他责任的同时撰写一本书可能是一项具有挑战性的艰难任务。我想感谢那些帮助强化各种安全概念的人,包括 Raffaele Spazzoli、Bob Callaway 和 Luke Hinds。此外,所有在开源社区中帮助构建知识和保持联系的人。
但最重要的是,我想感谢我的父母,AnneMarie 和 A.J.,他们是我坚实的支持柱,无论逆境如何,都让我保持脚踏实地和专注。
—Andrew Block
关于这本书
Kubernetes Secrets Management 的编写旨在帮助您了解如何在开发过程中管理密钥,并将应用程序发布到 Kubernetes 集群。我们首先介绍 Kubernetes 以及设置运行书中示例的环境。介绍之后,我们讨论开发过程中如何管理密钥以及如何正确存储它们,无论是在代码仓库中还是在 Kubernetes 集群内部。最后,我们展示了在云 Kubernetes 原生方式下实现持续集成和交付以及管理管道中的密钥。
应该阅读这本书的人?
Kubernetes Secrets Management 适合那些在 Kubernetes 方面经验有限的资深开发者,他们希望扩展他们对 Kubernetes 和密钥管理的知识。这本书也适合那些想学习如何管理密钥的运维人员,包括如何配置、部署和适当地存储这些密钥。虽然网上有大量相关的文档和博客文章,但 Kubernetes Secrets Management 将所有这些信息汇集到一本清晰、易于遵循的文本中,以便读者可以逐步了解安全威胁以及如何应对它们。
本书是如何组织的:路线图
本书分为三部分,共涵盖八章:
第一部分解释了安全和秘密的基本原理,以及理解本书其余部分所必需的基本 Kubernetes 概念。
-
第一章介绍了什么是秘密,什么不是秘密,为什么保持秘密的秘密性很重要,以及 Kubernetes 的概述。
-
第二章进一步介绍了 Kubernetes,其架构以及部署包含秘密数据的应用程序的基本概念。它还讨论了为什么标准的 Kubernetes Secrets 不足以提供足够的安全性。
第二部分涵盖了在将应用程序部署到 Kubernetes 的开发和部署过程中可能遇到的安全问题以及如何解决这些问题。此外,第二部分还涵盖了使用秘密存储来管理 Kubernetes 基础设施之外的应用程序秘密。
-
第三章介绍了可以安全存储 Kubernetes Secrets 的工具和方法,并说明了声明式定义 Kubernetes 资源的好处。
-
第四章涵盖了在 Kubernetes 集群内部秘密的加密以及与密钥管理服务的集成。
-
第五章重点介绍了使用秘密管理工具(如 HashiCorp Vault)的重要性,以安全地存储和管理部署到 Kubernetes 的应用程序的敏感资产。它还演示了如何配置应用程序和 Vault 以实现无缝集成。
-
第六章在第五章介绍的外部秘密管理工具的概念基础上进行了扩展,这次聚焦于云秘密存储,包括 Google Secret Manager、Azure Key Vault 和 AWS Secrets Manager。
第三部分介绍了使用 Tekton 和 Argo CD 实现 Kubernetes 原生持续集成和持续交付的方法,并正确管理秘密。
-
第七章涵盖了快速交付高质量应用程序以尽早进入市场的方法,并且更好地管理整个管道中的秘密,以确保在开发阶段这一阶段没有秘密泄露。
-
第八章介绍了使用 Argo CD 快速交付高质量应用程序到 Kubernetes 集群的方法,通过使用 GitOps 方法部署和发布服务,在整个管道中正确管理秘密,并确保在开发阶段这一阶段没有秘密泄露。
如果读者对 Kubernetes(例如,部署、服务、卷和 ConfigMaps)和 minikube 有很好的了解,他们可以跳过第二章。
关于代码
本书包含许多源代码示例,既有编号列表,也有与普通文本混排。在这两种情况下,源代码都使用固定宽度字体格式化,以便与普通文本区分。有时代码也会加粗,以突出显示章节中从先前步骤更改的代码,例如当新功能添加到现有代码行时。
在许多情况下,原始源代码已被重新格式化;我们添加了换行符并重新整理了缩进,以适应书籍中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常已从列表中删除。许多列表都有代码注释,突出显示重要概念。
您可以从本书的 liveBook(在线)版本中获取可执行的代码片段 livebook.manning.com/book/kubernetes-secrets-management。书中示例的完整代码可在 Manning 网站 www.manning.com 和 GitHub github.com/lordofthejars/kubernetes-secrets-source 上下载。
liveBook 讨论论坛
购买 Kubernetes Secrets Management 包括对 Manning 的在线阅读平台 liveBook 的免费访问。使用 liveBook 的独特讨论功能,您可以在全球范围内或特定章节或段落中添加评论。为自己做笔记、提问和回答技术问题,以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问 livebook.manning.com/book/kubernetes-secrets-management/discussion。您还可以在 livebook.manning.com/discussion 了解更多关于 Manning 论坛和行为准则的信息。
Manning 对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量承诺的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他们的兴趣分散!只要本书有售,论坛和先前讨论的存档将可通过出版商的网站访问。
关于作者

Alex Soto Bueno 是 Red Hat 的开发者体验总监。他对 Java 世界、软件自动化充满热情,并相信开源软件模式。Alex 是 Testing Java Microservices、Quarkus Cookbook、Securing Kubernetes Secrets 的合著者,并为多个开源项目做出了贡献。自 2017 年以来,Alex 一直是 Java 冠军,也是国际演讲者、Onda Cero 的广播合作者,以及 Salle URL 大学教师。您可以通过 Twitter (@alexsotob) 关注 Alex,以了解 Kubernetes 和 Java 世界正在发生的事情。

安德鲁·布洛克(Andrew Block)是红帽(Red Hat)的杰出架构师,他与组织合作设计和实施利用云原生技术的解决方案。他专注于持续集成和持续交付方法,以减少交付时间并自动化环境的构建和维护。他还是《Learn Helm》一书的合著者,该书介绍了如何在 Kubernetes 环境中打包应用程序进行部署。安德鲁也是几个开源项目的贡献者,并强调共同努力构建和维护实践社区的好处。您可以在 Twitter 上找到安德鲁,用户名是@sabre1041,他经常分享该领域最新和最好的头条新闻和技巧。
关于封面插图
《Kubernetes Secrets Management》封面上的图像被标注为“Femme Mokschane”,或“Mokshan woman”,取自雅克·格拉塞·德·圣索沃尔(Jacques Grasset de Saint-Sauveur)的作品集,该作品集于 1797 年出版。每一幅插图都是手工精心绘制和着色的。
在那些日子里,仅凭人们的服饰就能轻易识别出他们居住的地方以及他们的职业或社会地位。曼宁通过基于几个世纪前丰富多样的地域文化的书封面,庆祝计算机行业的创新精神和主动性,这些文化通过如这一系列图片的图片被重新带回生活。
第一部分. 秘密与 Kubernetes
无论从基础设施还是应用的角度来看,这些资产的操作都是通过配置属性来驱动的;其中一些可能包含敏感值。本书的第一部分将介绍在 Kubernetes 中管理秘密的基本知识。
第一章提供了识别秘密中常见特性和它们在 Kubernetes 中角色的指导。第二章专注于 Kubernetes 的原始元素、其架构以及部署包含敏感数据的应用程序的基本概念。阅读完第一部分后,你将具备识别包含敏感内容的配置的能力,并了解 Kubernetes Secret 资源的主要限制。
1 Kubernetes Secrets
本章节涵盖
-
专注于安全性
-
充分利用 Kubernetes 生态系统
-
区分什么是密钥,什么不是
-
将所有内容整合在一起
-
开始使用你成功所需的工具
企业级软件系统依赖于准确的配置数据来支持它们的正常操作。配置可以采取多种形式,并且可以根据用例和上下文以多种方式设置。这些可配置属性可能包括支持应用程序框架或程序正常操作所需的详细信息。
虽然许多属性旨在被任何一方查看,但有一些属性,如密码,应该只由某些个人或组件查看或访问。这些敏感数据形式被称为 Secrets,保护这些 Secrets 是 Kubernetes 管理员和开发者的首要任务。正如人们可能预期到的任何复杂系统一样,Kubernetes 配置使用了许多属性来支持正常操作,其中一些可能包含敏感信息,如果泄露,将危及整个平台的安全性和完整性。
随着时间的推移,越来越多的选项可用于管理 Kubernetes 中的配置,但大多数都源于在平台内存储配置的两种主要方法:ConfigMaps 和 Secrets。虽然每个资源都通过使用键值对提供存储配置材料的方式,但两者之间的主要区别在于 Secrets 被设计用来存储机密数据,同时支持更复杂的数据类型,如二进制资产而不是纯文本值。需要注意的是,这些数据类型中可能包含敏感属性。
然而,就 Kubernetes 中的密钥管理而言,包含的 Kubernetes Secrets 资源只是冰山一角。为了补充 Kubernetes Secrets 的本地功能,已经发展出额外的工具和方法。这些工具演变的驱动力之一是本地的 Kubernetes Secrets 资源并没有提供人们可能从密钥管理系统期望的安全级别。
与使用合适的加密算法不同,存储在 Kubernetes Secrets 中的值仅仅是 Base64 编码的,这意味着恶意攻击者可以轻易地解码这些值;这种编码方案是为了存储二进制数据而不是提供任何形式的安全性。但是,是什么使得这些替代工具优于本地的 Kubernetes Secrets 资源?是因为它们提供了更强大的加密机制,使用起来更直观,或者与目标系统或最终应用程序集成得更好?也许吧。这些都是选择可用选项时需要考虑的因素。
在许多情况下,没有“正确”的答案。无论是个人开发者还是跨国组织,每个人对安全的评估方式都不同。对某个人来说可能合适的东西,对另一个人来说可能就不合适,尤其是在必须遵守安全法规的情况下。理解不仅在于保护 Kubernetes 中敏感资产的重要性,还在于了解可以用来保护这些资产的各种方法,这是本书的目标。完成阅读后,你将更好地理解 Secrets 在 Kubernetes 环境中的应用,以及可以用来正确管理敏感资源和支持基础架构的技术和方法。
1.1 专注于安全
安全是一个持续发展的主题。每周都会报道新的漏洞,无疑会随之而来。采用容器原生策略的一个原因是因为云原生应用程序开发和运营带来了许多好处。然而,在许多情况下,安全仍然被视为事后考虑的事情,其优先级低于云原生开发的某些其他方面。系统构建和部署的根本转变为组织提供了一个反思如何优先考虑工作以及基础设施和应用程序设计应考虑的一些因素的机会。组织开始注意到安全的重要性,并“左移”以在开发过程中更早地纳入安全。这也给了他们反思当前安全实践的机会,并思考他们如何设计和发展未来的策略和政策。
采用最小权限原则等概念,将对资源的访问权限限制在完成任务所需的最小必要权限,强调了对敏感资源访问管理的重要性。将基于角色的访问控制(RBAC)应用于限制授权用户的访问是应用此原则最常见的方法之一。但关注安全不应被视为仅在规划和初始实施阶段发生的“一次性和完成”的任务。持续审查这些政策,以确认是否有任何行为者仍然需要访问所需资源,这一点非常重要。这种持续的评估不仅是一种良好的做法,而且还能提高整体安全性。
1.2 充分利用 Kubernetes 生态系统
如同人们可能想象的那样,在 Kubernetes 环境中实施适当的秘密管理远远超出了简单部署 Pods 和服务的范畴。更高级的话题,例如 sidecars(位于主容器旁边的独立容器,提供补充功能,并带有可以响应平台内各种变化的准入 webhook)将 Kubernetes 推向极限,以实现安全解决方案。平台真正的可扩展性通过自定义资源定义(CRD)得到体现,它扩展了基础的应用程序编程接口(API)资源集,可以通过多种方式用于管理安全资产,包括支持如何管理和访问秘密。
但推动项目前进并开辟机会的并不总是技术本身。开源社区的成员与各种组织合作,不懈地投入时间和资源,为这个领域提供解决方案,使更多的选项和方法成为可能,以提供最安全的操作环境。
1.3 并非所有内容都是秘密
虽然拥有以安全为首要考虑的心态很重要,但人们必须考虑到正确执行秘密管理所需的努力。例如,如果你开发了一个应用程序,该应用程序在达到可配置阈值时发送警报,例如冰箱温度达到 5 摄氏度,那么这个 5 摄氏度的阈值是否真正被视为敏感资源?可能不是。必须考虑到完全保护和管理工作所需的时问、资源和努力,因为管理开销和持续的生命周期可能会超过其带来的好处。
认为每个可配置的值都应该一视同仁的观点是一种常见的谬误,阻碍了许多团队。确定哪些值可以保持明文形式,哪些值需要保护,是所有团队都应该进行的练习。定义一个标准化的方法来识别敏感资源将有助于统一应用团队处理秘密管理的方式,同时也会促进做出更明智的决策并减少浪费的时间。
1.4 将秘密管理与 Kubernetes 结合起来
在 Kubernetes 时代之前,秘密管理就已经是一个挑战了。这个过程可能是费时的,并且很可能会涉及仔细的计划和考虑。那么你应该采取哪些步骤呢?让我们从图 1.1 所示的鸟瞰视角逐个分析各个部分,这将有助于理解你需要检查的步骤。

图 1.1 在 Kubernetes 中处理敏感资源所涉及的步骤
首先,在实施或执行任何形式的操作之前,应该就存在哪些类型的可配置材料以及哪些将被视为敏感材料达成一致(图 1.2)。
在某些情况下,答案相当直接,例如数据库的密码。对于其他情况,可能并不那么容易。以一个连接到相同后端数据库的 Web 应用程序为例。数据库的主机名会被认为是敏感材料吗?对一些人来说可能是,因为可能存在隐藏位置的愿望,以减少潜在的攻击向量。然而,如果位置指的是开发环境中团队成员共同使用的公共数据库,那么可能就不是了。在这个过程中,已经进行了许多战斗,有赢有输,答案实际上各不相同。

图 1.2 常见的敏感资产类型
一旦就认为敏感的属性类型达成一致,下一步就是确定它们应该如何存储。应该考虑几个因素:
-
可用的秘密管理解决方案有哪些?
-
敏感资产打算如何使用?
-
正在使用什么应用程序框架,以及注入外部配置的选项有哪些?
在 Kubernetes 环境中,Secret 资源是一种自然默认的选择,因为它提供了一定形式的安全保护,并且包含在 Kubernetes 的每个发行版中。但是,还有其他什么选择呢?图 1.3 展示了两种主要的方法。

图 1.3 存储敏感 Kubernetes 资产的典型工具
如果您的应用程序或组件框架提供了秘密管理系统,那么利用这个本地功能可能是一个可行的选择。许多云服务提供商也提供自己的密钥管理系统(KMS)以及托管服务选项。在云环境中操作时,这非常诱人。其他解决方案,如 HashiCorp Vault,提供了一种可以在任何地方部署的密钥管理系统,无论是在公共云还是其他地方。
在确定秘密管理解决方案并在此特定实现中前进之后,下一步是在工具中存储资产。虽然存储资源的过程可能因工具而异,但大多数工具都提供了一个基于 API 的接口,可以在各种级别上使用以进行集成。API 成为与秘密管理解决方案交互的焦点,以及进行转换。
便捷性,命令行界面(CLI)选项或通过网页浏览器暴露的用户界面抽象了与 API 的底层交互。所有这些选项都有助于存储秘密,并且可以手动实现或集成到持续集成/持续交付(CI/CD)流程中,以实现可重复性和一致性(图 1.4)。

图 1.4 常见与秘密管理工具交互的方法
最后,也是可能最重要的步骤,是从密钥管理工具中检索存储的资源,以便应用程序使用。多亏了 Kubernetes 的强大功能,有各种各样的选项和方法可以用来完成这个任务。最终,这归结为两个不同的步骤:
-
将值从其受保护形式转换为纯文本
-
暴露值,以便它可以被应用程序消费
将存储的值转换回其纯文本表示的过程取决于所使用的密钥管理工具。在许多情况下,可以采用与存储值相同的方法,只是方向相反。根据您如何使这些值可供应用程序使用,事情可能会变得很有趣。最简单的方法,就像标准的 Kubernetes Secrets 一样,在创建的 Kubernetes 清单中引用存储的 Secret,然后该资产作为环境变量暴露给应用程序,或者包含在应用程序文件系统上的文件中。然而,如果使用了更高级的密钥管理工具,或者希望有更多动态功能来进一步限制值暴露给应用程序的方式,可能还有其他选项可用。
某些工具可以与上述边车容器中的运行应用程序一起部署,以与密钥管理存储进行交互,并将敏感值注入到应用程序中以供消费。或者,在部署时,可以通过平台修改应用程序的 Kubernetes 清单来动态地解耦值的注入方式。此外,还有敏感值从未以纯文本形式暴露,而是直接由应用程序在内存中访问的方法。从开始到结束,无论采用何种方法,都需要仔细规划和思考,评估所需的工具、应用程序的要求以及实施解决方案所需的总时间和精力。
1.5 开始的工具
为了指导您在整个书籍的旅程中,您将使用几个工具,这些工具不仅与 Kubernetes 集群交互,还与所讨论的任何密钥管理解决方案交互。拥有一个允许安装和配置软件的环境很重要。至少,要与 Kubernetes 集群交互并管理 Kubernetes Secret 资源,需要 Kubernetes 命令行工具(kubectl)。随着您在密钥管理空间的一些替代解决方案中工作,将引入额外的工具。因此,让我们不再拖延,开始吧!
摘要
-
配置有多种形式——其中一些可能是敏感的——以支持应用程序和基础设施环境。
-
存储在 Kubernetes Secrets 中的值未加密,而是使用 Base64 编码,并且易于解码。
-
最小权限原则仅包括启用必要的最低访问级别。
-
Kubernetes 容器编排平台包含原语,以实现管理机密的方法。
-
概念和方法将在后续章节中介绍,以便读者能够正确管理 Kubernetes 中的机密。
2 Kubernetes 和 Secrets 简介
本章涵盖
-
理解 Kubernetes 集群的基本架构
-
将应用程序部署到 Kubernetes
-
外部管理应用程序配置
-
使用 Kubernetes Secrets 存储敏感信息
由于密钥管理始于初始配置和应用程序的安全需求,因此完全理解初始设置过程非常重要。在本章中,您将了解如何通过部署一个简单的 RESTful Web 服务来管理配置,该服务返回一个问候消息,无论是不可靠的还是可靠的。
注意:您需要 Kubernetes 集群来运行本书中的实现。您可以使用任何由公共云提供或本地运行的 Kubernetes 发行版。
本书中的示例使用 minikube 集群进行测试。Minikube 允许您在笔记本电脑上的虚拟机(VM)中运行单个节点 Kubernetes 集群内的本地 Kubernetes。按照附录 A 中的说明安装您的 Kubernetes 集群,然后返回本章开始。
我们将首先回顾一些关于 Kubernetes 架构和配置的基本知识。如果您已经非常熟悉 Kubernetes,接下来几页,我们将为您建立 Web 服务的初始配置,应该非常熟悉。在完成默认设置后,我们将深入了解 Kubernetes Secrets。
2.1 Kubernetes 架构
了解 Kubernetes 架构的第一件事是存在两种类型的节点——主节点和工作节点——在典型的生产部署中,您可能拥有每种类型的大量节点。
重要提示:Kubernetes 社区已经开始更改节点名称,使用更具包容性的语言(例如,控制平面和二级节点);我们真正支持这一变化,但本书中使用的 minikube 版本尚未实施这一变化。为了与 minikube 输出保持一致,本章使用主节点和工作节点。
图 2.1 展示了 Kubernetes 集群的整体概述以及主节点和工作节点之间的关系。工作节点(s)负责运行您的负载,例如开发的服务或数据库,而主节点(s)管理工作节点并决定工作负载的部署位置。

图 2.1 主节点和工作节点的 Kubernetes 架构概述
符合 Kubernetes 要求所需的最少节点数量只有一个主节点,充当主节点和工作节点。尽管这可能在生产环境中不是典型用例,但在本地开发中却是典型情况。通常,在生产环境中,您将拥有三到五个主节点和多个工作节点,其数量可能取决于要部署的工作负载数量以及您期望的应用程序冗余程度。让我们探索主节点和工作节点内部的内容。
2.1.1 什么是主节点?
主节点负责在 Kubernetes 集群中执行多个任务;例如,它决定应用程序部署的位置,检测并响应异常,存储应用程序配置参数,并且默认情况下,是存储应用程序秘密(或敏感信息)的地方。
Kubernetes 集群必须至少有一个主节点,但在生产环境中,您可能为了冗余而拥有多个。您将在每个主节点内部找到以下四个元素:
-
kube-apiserver—这是 Kubernetes 的前端,并向 Kubernetes 用户公开 Kubernetes API。当操作员对 Kubernetes 集群运行命令时,它通过api-server来执行。 -
etcd—这是一个用于存储所有集群数据的关键值数据库。每次您获取有关集群的信息时,这些数据都是从etcd中检索的。 -
调度器—这是负责选择节点以运行工作负载的过程。在选择部署工作负载的节点时考虑的因素可能取决于其需求,例如硬件、策略约束、亲和力和反亲和力规则、数据本地性等。
-
控制器—控制器的主要任务是监控特定的 Kubernetes 资源。
有四个主要的控制器:
-
节点控制器—这个控制器负责监控任何节点宕机时采取行动。
-
副本控制器—这个控制器负责确保您的工作负载始终处于运行状态。
-
端点控制器—这个控制器使得使用静态 IP 和 DNS 名称访问工作负载成为可能。
-
服务账户和令牌控制器—这为新的命名空间创建默认账户和令牌。
图 2.2 展示了构成主节点的所有元素。

图 2.2 主节点的元素 (kube-apiserver、etcd、调度器和控制器)
现在您已经了解了主节点的组成部分,让我们看看工作节点的组成部分。
2.1.2 什么是工作节点?
工作节点是部署和运行工作负载的实例。由于 Kubernetes 中的工作负载是软件容器,因此容器运行时托管在每个工作节点内部。
每个工作节点由以下三个元素组成:
-
kubelet—这是一个代理,确保容器在 Pod 中运行。
-
代理—这是一个实现了 Kubernetes
Service概念一部分的网络代理。 -
容器运行时—这是负责运行容器的。
在撰写本书时,以下运行时得到支持:Docker、containerd、crio-o 以及任何 Kubernetes 容器运行时接口 (CRI) 的实现。图 2.3 展示了构成工作节点的所有元素。现在您已经很好地理解了 Kubernetes 架构,让我们从开发人员或操作员的视角开始使用 Kubernetes。

图 2.3 工作节点元素
2.2 在 Kubernetes 中部署工作负载
到目前为止,您已经看到了 Kubernetes 集群的架构,但作为开发者,您可能希望将 Web 服务、数据库、消息代理或任何其他应用程序所需元素部署到 Kubernetes 中。您将探索开发者如何通过部署一个返回欢迎信息的简单应用程序来与 Kubernetes 交互。此外,您还希望能够从平台外部配置它们或能够使用网络协议访问它们。
2.2.1 部署工作负载
将工作负载部署到集群的最重要 Kubernetes 资源之一是 Pod。Pod 是 Kubernetes 中可部署的最小单元,由一个或多个容器组成。Pod 中的每个容器共享 IP、存储、资源和生命周期。
Pods 是业务工作负载运行的单元(例如,服务 API、数据库和邮件服务器)。理解 Pod 的一个有用的类比是将其视为一种 VM(当然不是),在其中运行进程,每个进程共享 VM 的资源、网络和生命周期。在 Pod 中,概念相同,但不是运行进程,而是运行容器。
在 Kubernetes 集群中部署 Pod 有许多方法,但最常见的一种是描述 YAML 文件中的部署,并使用 kubectl CLI 工具应用它。要创建包含 Pod 定义的 YAML 文件,请打开一个新的终端窗口,在新的目录中创建一个名为 greeting-pod.yaml 的文件,定义属于 Pod 的容器镜像,如下所示。
列表 2.1 创建 Pod
apiVersion: v1
kind: Pod ①
metadata:
name: greeting-demo ②
spec:
containers:
- name: greeting-demo
image: quay.io/lordofthejars/greetings-jvm:1.0.0 ③
① 将文件设置为 Pod 类型
② 为 Pod 赋予一个名称
③ 设置要运行的容器镜像
创建文件后,您可以使用 kubectl apply 子命令将其应用到集群中:
kubectl apply -f greeting-pod.yaml
重要提示:Minikube 不需要您进行身份验证即可访问集群。但根据 Kubernetes 的实现方式,您可能需要在运行此命令之前进行身份验证。
您需要等待 Pod 被分配到节点并且可以访问。为此,请在终端中使用 kubectl wait 子命令:
kubectl wait --for=condition=Ready pod/greeting-demo
现在通过获取 Pod 的状态来验证 Pod 是否正确分配:
kubectl get pods
Pod 被分配到节点并正确启动,因为最终状态是 Running。
NAME READY STATUS RESTARTS AGE
greeting-demo 1/1 Running 0 18s ①
① 可能的状态有 Pending、Running、Succeeded、Failed、Unknown、Terminating、ContainerCreating 和 Error。
让我们做一个实验,删除您创建的 Pod 来查看其生命周期发生了什么:
kubectl delete pod greeting-demo
等待几秒钟直到 Pod 终止,然后获取 Pod 状态:
kubectl get pods
输出(如果您已经等待足够的时间)将显示 greeting-demo Pod 已不再可用:
No resources found in default namespace.
您可以多次运行 kubectl get pods,但 Pod 总是会永远消失。因此,Pod 本身可能在大多数情况下可能没有用,因为如果服务因任何原因死亡,它将变得不可用,直到您手动重新部署它。
当服务以意外方式死亡(或停止)时(例如,如果运行它的节点关闭,网络关闭或应用程序有致命错误)执行的过程是重启服务以最小化应用程序的停机时间。在 Kubernetes 之前,重启服务的过程是通过(半)手动完成的,但使用 Kubernetes,如果使用 Deployment 或 ReplicaSet 创建,Pod 中运行的服务将自动重启。
2.2.2 部署对象
到目前为止,当一个 Pod 死亡时,它并没有自动重启;这是 Pod 的性质。如果您想为 Pod 生命周期添加一些弹性,以便在出现错误时自动重启,那么您需要创建一个 ReplicaSet。
通常,ReplicaSet 不是手动创建的,而是通过 Deployment 资源创建的。Deployment 总是与一个 ReplicaSet 关联,因此当使用 Deployment 资源部署服务时,它明确地有一个 ReplicaSet 来监控和重启 Pod,以防出现错误,如图 2.4 所示。

图 2.4 部署的性质
要创建一个部署,请在之前打开的终端窗口中创建一个名为 greeting-deployment.yaml 的新文件。部署文件包含比 Pod 文件更多的元素。您需要在启动时设置所需的 Pod 副本数量。一个典型的值是 1,但可以是任何其他数字。
部署还必须定义属于 Pod 的容器镜像和容器的监听端口。部署文件如下所示。
列表 2.2 创建部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeting-demo-deployment ①
spec:
replicas: 1
selector:
matchLabels:
app: greeting-demo
template:
metadata:
labels:
app: greeting-demo ②
spec:
containers: ③
- name: greeting-demo
image: quay.io/lordofthejars/greetings-jvm:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8080
① 部署名称
② 在 Pod 中设置的标签
③ 如在 Pod 定义中看到的容器部分
创建文件后,您可以使用 kubectl apply 子命令将其应用到集群中:
kubectl apply -f greeting-deployment.yaml
您需要等待 Pod 被分配到节点并准备好访问。为此,请在终端中使用 kubectl wait 子命令:
kubectl wait --for=condition=available deployment/greeting-demo-deployment --timeout=90s
通过获取 Pod 状态来确认 Pod 已启动并正确分配:
kubectl get pods
您应该得到以下类似输出:
NAME READY STATUS RESTARTS AGE
greeting-demo-deployment-854c4f4f69-xh5v6 1/1 Running 0 18s
运行以下命令以获取部署状态:
kubectl get deployment
部署的输出略不同于 Pod。在这种情况下,available 字段是最重要的,因为它显示了正在运行和运行的副本数量。由于您在示例中将副本设置为 1,因此只有一个 Pod 通过此部署可用:
NAME READY UP-TO-DATE AVAILABLE AGE
greeting-demo-deployment 1/1 1 1 5m50s
现在重复之前的实验,删除 Pod 以查看其生命周期现在发生了什么。首先,您需要通过运行 kubectl get pods 命令找到之前部署文件中创建的 Pod 名称:
NAME READY STATUS RESTARTS AGE
greeting-demo-deployment-7884dd68c8-4nf6q 1/1 Running 0 14h
获取 Pod 名称,在你的情况下是greeting-demo-deployment-7884dd68c8-4nf6q,然后删除它:
kubectl delete pod greeting-demo-deployment-7884dd68c8-4nf6q
等待几秒钟直到 Pod 被终止,并获取 Pod 状态:
kubectl get pods
现在的输出与之前只创建了一个 Pod 的章节不同。注意,现在有一个新的 Pod 正在运行。你可以通过检查两个字段来看到 Pod 是新的:Pod 的name,与之前的情况不同,以及age,它接近你的时间:
NAME READY STATUS RESTARTS AGE
greeting-demo-deployment-7884dd68c8-qct8p 1/1 Running 0 13s
Pod 与Deployment和ReplicaSet相关联;因此,它已经被自动重启。现在你知道如何正确部署你的工作负载,是时候看看你如何访问它们了。
服务
到目前为止,你已经将应用程序部署到了 Kubernetes 集群中;然而,属于每个部署的 Pod 都会获得自己的 IP 地址。由于 Pod 按定义是短暂的,它们会动态地创建和销毁,新的 IP 地址也会动态分配。通过 IP 地址访问这些 Pod 可能不是最佳选择,因为它们在未来可能会无效。
Kubernetes 服务是将一组 Pod 通过稳定的 DNS 名称和 IP 地址暴露出来的方式,它们之间将进行负载均衡。在完成上一个示例中的greeting-demo部署之后,现在是时候创建一个服务,以便我们可以访问它。
在介绍这个概念之前,我们将先介绍 Kubernetes 中的标签概念。标签是一个与 Kubernetes 资源相关联的键值对,其主要目的是从用户的角度识别这些对象。例如,你可以设置一个带有自定义标签的部署,以标识它属于生产环境的部署。
如果你仔细查看之前的部署,你会看到有一个带有键app和值greeting-demo的标签被应用到所有创建的 Pod 上:
template:
metadata:
labels:
app: greeting-demo
服务针对的 Pod 集通常由 Pod 中注册的标签确定。现在你将使用一个选择带有app: greeting-demo标签的 Pod 的服务来暴露之前部署中创建的 Pod。
在工作目录中创建一个名为greeting-service.yaml的新文件。服务定义应该配置部署中定义的containerPort(8080)与服务暴露的端口的映射。此外,你需要定义选择器值,设置greeting-demo Pods 的标签。服务文件应该看起来像以下列表中所示。
列表 2.3 创建服务
apiVersion: v1
kind: Service
metadata:
name: the-service
spec:
selector:
app: greeting-demo
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
一旦你创建了文件,你可以使用kubectl的apply子命令将其应用到集群中:
kubectl apply -f greeting-service.yaml
由于你正在使用 minikube,将没有外部 IP 可以访问服务,必须使用 minikube 地址。你可以在终端窗口中运行以下命令来验证创建的服务没有关联外部 IP:
kubectl get services
你应该会看到the-service服务的external IP仍然处于Pending状态。
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 35d
the-service LoadBalancer 10.102.78.44 <pending> 80:30951/TCP 4s
在终端窗口中,运行以下命令将连接值作为环境变量设置以访问服务:
IP=$(minikube ip)
PORT=$(kubectl get service/the-service -o jsonpath=
➥"{.spec.ports[*].nodePort}") ①
① 访问端口是随机分配的,不使用你在服务定义中设置的公开端口。
接下来,你可以使用 curl 工具查询服务:
curl $IP:$PORT/hello
欢迎应用程序对请求返回 Hello World 消息。
重要提示:在公共云中运行 Kubernetes 的情况下,外部 IP 将在几秒钟内变成一个真实 IP。你可以通过获取服务配置来获取外部 IP 值:
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
myapp LoadBalancer 172.30.103.41 34.71.122.153 8080:31974/TCP 44s
清理
在你跳到以下概念之前,现在是时候删除你在本节中部署的应用程序了。为此,你可以使用 kubectl delete 命令:
kubectl delete -f greeting-deployment.yaml
服务故意没有被删除,因为稍后你将需要使用它。
2.2.3 卷
Kubernetes 的 volume 是一个包含一些数据的目录,这些数据可以被运行在 Pods 内部的容器访问。卷的物理存储由所使用的卷类型决定。例如,hostPath 类型使用工作节点文件系统来存储数据,或者 nfs 使用 NFS(网络文件系统)来存储数据。
Kubernetes 卷是一个广泛的话题,因为它与持久化存储相关,并且超出了本书的范围。你将在本书中使用卷,但仅作为挂载 ConfigMaps 和 Secrets 的方式。如果你不熟悉这些,不要害怕;我们将在接下来的章节中介绍它们。
2.3 管理应用程序配置
在前面的部分中,你已经看到了如何将带有硬编码欢迎消息的应用程序部署到 Kubernetes 集群中。在本节中,你将外部设置应用程序的欢迎消息,从一个配置参数中设置。
2.3.1 ConfigMaps
ConfigMap 是一个 Kubernetes 对象,用于以映射形式存储非机密数据。ConfigMap 的一个优点是它允许你将环境配置数据从应用程序代码外部化,根据集群设置特定的值,如图 2.5 所示。

图 2.5 ConfigMap 注入
配置映射可以被注入到 Pod 中,作为环境变量或卷来使用。在本章前面的部分中已部署的应用程序返回默认的欢迎消息(Hello World),但这个欢迎消息与代码解耦,因此你可以外部设置它。列表 2.4 展示了服务中加载欢迎消息的逻辑,以便返回给调用者。
首先,代码检查是否设置了 GREETING_MESSAGE 环境变量。如果没有设置,那么它尝试加载位于 /etc/config/conf.properties 的 properties 文件,其中定义了 greeting.message 键。否则,将返回默认消息给调用者。
列表 2.4 欢迎服务
final String envGreeting = System.getenv("GREETING_MESSAGE");
if (envGreeting != null) {
return envGreeting;
}
java.nio.file.Path confFile = Paths.get("/etc/config/conf.properties");
if (Files.exists(confFile)) {
final Properties confProperties = new Properties();
confProperties.load(Files.newInputStream(confFile));
if (confProperties.containsKey("greeting.message")) {
return confProperties.getProperty("greeting.message");
}
}
return "Hello World";
让我们开始使用 ConfigMap 外部配置应用程序。
环境变量
从 ConfigMap 获取值的一种方法是将它作为环境变量注入到 Pod 中。然后你可以使用编程语言提供的任何方法在你的应用程序中获取环境变量。
ConfigMap 资源最重要的部分是 data 部分。这是你将定义配置项的地方,形式为键值对。创建一个名为 greeting-config.yaml 的 ConfigMap 资源,其中 greeting.message 是配置键,Hello Ada 是配置值,如下所示。
列表 2.5 创建 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: greeting-config
data:
greeting.message: "Hello Ada" ①
① 将配置值设置为键值属性
此 ConfigMap 创建了一个带有新欢迎消息的配置项。像其他 Kubernetes 资源一样,使用 kubectl apply 将其应用到集群中:
kubectl apply -f greeting-config.yaml
ConfigMap 已经创建,但它只是一个配置元素。你现在需要更改之前的部署文件,以便它从 ConfigMap 获取配置,并将其作为环境变量注入到容器内部。
在工作目录中创建一个名为 greeting-deployment-configuration.yaml 的新文件。这个 Deployment 文件将类似于你之前创建的文件,但它将包含一个 env 部分,该部分设置要在 Pod 内部创建的环境变量(GREETING_MESSAGE)。环境变量的值来自之前创建的 greeting-config 配置映射。文件应类似于以下列表所示。
列表 2.6 创建带有 ConfigMaps 的部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeting-demo-deployment
spec:
replicas: 1
selector:
matchLabels:
app: greeting-demo
template:
metadata:
labels:
app: greeting-demo
spec:
containers:
- name: greeting-demo
image: quay.io/lordofthejars/greetings-jvm:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: GREETING_MESSAGE ①
valueFrom:
configMapKeyRef:
name: greeting-config ②
key: greeting.message ③
① 定义要使用的环境变量名称
② 设置要使用的 ConfigMap 的名称
③ 设置获取值的键
通过运行 kubectl apply 命令将其应用到集群中:
kubectl apply -f greeting-deployment-configuration.yaml
在 greeting-demo 容器内部创建了一个名为 GREETING_MESSAGE 的环境变量,其值为 Hello Ada。由于你的应用程序知道这个变量,返回的消息是 ConfigMap 中 greeting .message 键下配置的消息。现在检查一下:由于已经设置了 IP 和 PORT 环境变量,如第 2.2.2 节所述,你可以查询服务并看到消息已更新为配置的消息:
curl $IP:$PORT/hello
返回的响应是 Hello Ada,因为它是在 ConfigMap 中配置的消息。现在你已经看到了如何使用 ConfigMap 配置应用程序并将值作为环境变量注入,让我们继续将此配置值作为文件注入。
卷
到目前为止,你已经看到 ConfigMap 可以作为环境变量注入,这在将遗留工作负载迁移到 Kubernetes 时是一个完美的选择,但你也可以使用卷将 ConfigMap 挂载为文件。由于应用程序可以使用属性文件进行配置,你将使用 ConfigMap 和卷在容器内编写一个新的属性文件。
在工作目录中创建一个名为 greeting-config-properties.yaml 的 ConfigMap 资源。为了定义属性文件,在 data 部分,将应用程序所需的文件名(conf.properties)作为键,将属性文件的内容嵌入作为值。新的 Deployment 文件如下所示。
列表 2.7 创建 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: greeting-config
data:
conf.properties: | ①
greeting.message=Hello Alexandra
① 这是属性文件的文件名。文件内容是嵌入的。
通过运行 kubectl apply 命令将其应用到集群中:
kubectl apply -f greeting-config-properties.yaml
现在你需要将 config.properties 文件从 ConfigMap 物化到容器中。为此任务,你需要在容器定义中定义一个 Kubernetes 卷,并将 ConfigMap 中放置的内容存储在其中。
在工作目录中创建一个名为 greeting-deployment-properties.yaml 的新文件。在 Deployment 文件中,你需要定义两个重要的事情:卷配置和内容检索的 ConfigMap。
在 volumeMounts 部分中,设置卷的名称(application-config)和卷挂载的目录。你的应用程序将从 /etc/config 读取配置属性文件。你需要做的第二件事是将 ConfigMap 链接到卷上,这样配置文件就会在定义的卷内以特定的名称(conf.properties)和 ConfigMap 中定义的内容创建。
在以下列表中显示了在卷中注入配置的 Deployment 文件。
列表 2.8 使用 ConfigMap 创建部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeting-demo-deployment
spec:
replicas: 1
selector:
matchLabels:
app: greeting-demo
template:
metadata:
labels:
app: greeting-demo
spec:
containers:
- name: greeting-demo
image: quay.io/lordofthejars/greetings-jvm:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8080
volumeMounts:
- name: application-config ①
mountPath: "/etc/config" ②
readOnly: true
volumes:
- name: application-config ③
configMap:
name: greeting-config ④
items:
- key: conf.properties ⑤
path: conf.properties ⑥
① 为挂载设置名称
② 设置挂载卷的目录
③ 链接 volumeMounts 和 volumes 字段
④ 设置用作卷的 ConfigMap 名称
⑤ 要物化的键值
⑥ 内容物化的文件名

图 2.6 注入 ConfigMap
通过运行 kubectl apply 命令将其应用到集群中:
kubectl apply -f greeting-deployment-properties.yaml
启动的容器在 /etc/config/conf.properties 目录下包含一个新文件,文件内容是嵌入在 ConfigMap 中的。现在检查属性文件中配置的值是否被服务使用。根据 2.2.2 节中所述,已经设置了 IP 和 PORT 环境变量,你可以查询服务并看到消息已更新为配置的值:
curl $IP:$PORT/hello
返回了 Hello Alexandra 消息,因为它是在 Config-Map 中配置的值。你可以使用这两种方法将 ConfigMap 中的所有配置值注入到容器中,但最好的方法是什么?让我们通过一些用例来探索如何最佳地操作。
环境变量和卷之间的区别
在本节中,您将探索何时使用环境变量以及何时使用卷。环境变量方法通常用于与可以使用环境变量配置且无法或不想更新源代码的遗留应用程序一起使用。卷方法可以用于处理绿色字段应用程序或使用文件配置的应用程序。如果应用程序需要设置多个配置属性,则使用卷方法配置过程更简单,因为您可以在文件中一次性配置所有这些属性。此外,如果 ConfigMap 被更新,Kubernetes 会刷新卷的内容。当然,您的应用程序需要处理此用例并提供重新加载配置的能力。请注意,此同步过程不会立即发生;更改与 kubelet 同步更改之间存在延迟,以及 ConfigMap 缓存的生存时间(TTL)。
TIP 如果刷新配置值是您应用程序的关键功能,您可以采取更确定性的方法:使用 Reloader 项目。Reloader是一个 Kubernetes 控制器,它监视 ConfigMaps 和 Secrets 中的更改,并对与其关联的Deployment、StatefulSet、DaemonSet和DeploymentConfig的 Pod 执行滚动升级。它的优点是它可以在环境变量和卷方法中工作,并且您不需要更新服务源代码来处理此用例,因为应用程序在滚动更新期间会重新启动。然而,缺点是必须在集群内部安装Reloader控制器。
到目前为止,您已经看到了两种将配置数据注入容器的 Kubernetes 方法中的第一种。但 ConfigMaps 的内容既不是秘密也不是加密的,因为它是以纯文本形式存在的。这意味着数据没有保密性。如果您试图配置数据库 URL、端口或数据库配置,如超时、重试或数据包大小,您可以使用 ConfigMaps 而无需过多关注安全问题。但对于数据库用户名、数据库密码和 API 密钥等需要高度保密性的参数,您将需要使用第二种可以注入配置数据到容器的 Kubernetes 对象。
2.4 使用 Kubernetes Secrets 存储敏感信息
让我们继续前进这个例子:假设经过一些测试后,你决定问候信息是敏感信息,因此需要将其作为 Kubernetes Secret 存储。Secret 是一个 Kubernetes 对象,用于存储敏感或机密数据,如密码、API 密钥、SSH 密钥等。Secrets 与 ConfigMaps 类似,因为两种方法都用于在容器内注入配置属性;然而,前者是安全的,而后者则不是。它们以类似的方式创建(区别在于 kind 字段,它指定了对象的类型),并且以类似的方式在容器内公开(作为环境变量或挂载为卷)。但显然,它们之间有一些区别,我们将在以下章节中探讨。
2.4.1 Secrets 以 Base64 编码
如第一章所述,Secrets 和 ConfigMaps 之间的一大区别在于数据在 etcd 中的存储方式。Secrets 以 Base64 格式存储数据;ConfigMaps 以纯文本格式存储数据。
让我们更深入地了解 Base64 格式。Base64 是一种编码方案,它以 ASCII 字符串格式表示二进制和文本数据,将其转换为 64 进制表示。例如,在 Base64 格式中,Alex 文本数据被转换为 QWxleA==。重要的是要记住,Base64 不是一个加密方法,因此任何以 Base64 编码的文本都是被掩蔽的纯文本。
现在创建一个包含你希望保持机密的问候信息的 Secret。Secret 对象类似于 ConfigMap 对象,但它包含两种设置数据的方式:data 和 stringData。data 用于当你想手动将配置值编码为 Base64 时,而 stringData 允许你以未编码的字符串设置配置值,这些字符串将自动编码。让我们在当前工作目录中创建一个名为 greeting-secret-config.yaml 的 Secret,如下所示,其中包含在 stringData 字段中的秘密信息。
列表 2.9 创建一个秘密
apiVersion: v1
kind: Secret
metadata:
name: greeting-secret
type: Opaque
stringData:
greeting.message: Hello Anna ①
① 使用 stringData,不需要进行 Base64 编码。
通过运行 kubectl apply 命令将此应用到集群中:
kubectl apply -f greeting-secret-config.yaml
现在 Secret 已经创建,你需要更改部署文件,以便它从之前步骤中创建的 Secret 中获取消息值,并将其注入到容器中。同样,你可以将 ConfigMaps 中的配置属性作为环境变量或作为卷注入。
你将编写一个部署文件,以与 ConfigMap 部分中类似的方式将秘密作为环境变量注入,但在这个案例中,你将使用 secretKeyRef 而不是 configMapKeyRef。在当前工作目录中创建一个名为 greeting-deployment-secret-env.yaml 的新文件,如下所示。
列表 2.10 使用 Secret 创建部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeting-demo-deployment
spec:
replicas: 1
selector:
matchLabels:
app: greeting-demo
template:
metadata:
labels:
app: greeting-demo
spec:
containers:
- name: greeting-demo
image: quay.io/lordofthejars/greetings-jvm:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: GREETING_MESSAGE ①
valueFrom:
secretKeyRef: ②
name: greeting-secret ③
key: greeting.message ④
① 定义要使用的环境变量名称
② 使用 secretKeyRef 而不是 configMapKeyRef,正如 ConfigMaps 中的情况一样
③ 设置要使用的 Secret 的名称
④ 设置将获取值的密钥
通过运行 kubectl apply 命令将其应用于集群:
kubectl apply -f greeting-deployment-secret-env.yaml
现在检查当发送请求时服务是否使用了密钥值。在终端中已经设置了 IP 和 PORT 环境变量,如第 2.2.2 节所述,你可以查询服务并看到消息已更新为配置的值:
curl $IP:$PORT/hello
返回 Hello Anna,因为这是为该应用程序配置的密钥的值。当然,在生产环境中不要这样做;密钥是你在公共 API 中永远不应该暴露的东西,但为了这个练习,我们认为这是一种展示密钥如何工作的好方法。你还可以以类似处理 ConfigMap 的方式将密钥作为卷注入。
在这种情况下,未指定 items 字段;因此,Secret 对象中定义的所有键都会自动挂载。由于挂载目录的键是 greeting.message,因此在配置的卷中创建了一个名为 greeting.message 的文件,其数据内容为 Hello Anna。
volumeMounts:
- name: greeting-sec
mountPath: "/etc/config" ①
readOnly: true
imagePullPolicy: "IfNotPresent"
name: "greeting"
ports:
- containerPort: 8080
name: "http"
protocol: "TCP"
volumes:
- name: greeting-sec ②
secret: ③
secretName: greeting-secret ④
① 存储密钥的路径
② 挂载的名称
③ 使用 secret 而不是 configMap。
④ 设置用于卷的 ConfigMap 名称
现在你已经了解了密钥的基本知识,ConfigMap 和 Secrets 之间的区别,以及如何将它们注入到容器中。但这些都是基础知识;在你可以说你的应用程序正确管理密钥之前,还有很多事情要做。
作为读者,你可能想知道,“如果 Secret 实际上并不真正保密,为什么它会被命名为 Secret?它没有被加密;它只是以 Base64 编码。”这是一个合理的问题,但请继续阅读本章的结尾,以完全理解其背后的推理。
2.4.2 密钥在临时文件系统中挂载
只有当存在需要它的 Pod 时,Secret 才会被发送到 Node。但重要的是,即使 Secret 作为卷挂载,也永远不会写入磁盘,而是使用 tmpfs 文件系统在内存中。tmpfs 代表临时文件系统,正如其名称所暗示的,它是一个文件系统,其中数据存储在易失性内存中而不是持久存储。当包含 Secret 的 Pod 被删除时,kubelet 负责从内存中删除它。
2.4.3 密钥可以在静止状态下加密
静止数据 是指持久化但很少访问的数据。配置属性属于这一类别(Secrets 也是配置属性),因为它们通常存储在文件中,并在启动时访问一次以加载到应用程序中。加密 是将明文数据转换为密文的过程。在文本被加密后,只有授权方才能将其解密回明文。静止数据加密 就是加密静止状态下的敏感数据。
在其他方面,所有来自 ConfigMaps 和机密的数据都存储在 etcd 中,并且默认情况下未加密。请注意,所有这些元素都是静态数据,其中一些应该受到保护。Kubernetes 通过在 etcd 中加密机密对象来支持静态加密,为攻击者提供了额外的保护层。我们知道这只是一个对这个主题的快速介绍,但我们将在第四章中更深入地探讨它,因为这是机密和 Kubernetes 的一个重要概念。
2.4.4 风险
你可能认为通过使用 Kubernetes 机密,你正在正确地管理你的机密,但你并没有。让我们列举一个假设的攻击者可能利用的所有可能的安全漏洞来窃取你的机密。
将机密作为环境变量或作为卷注入是否更安全?
可能你在想,将机密注入为环境变量或作为卷是否更好。直观上,你可能认为将机密作为卷注入比作为环境变量更安全,因为如果攻击者能够访问到 Pod,列出环境变量会比在整个文件系统中搜索尝试找到机密文件要容易。这是一个合理的观点,但以下示例将展示在安全方面,当存在对 Pod 的未授权访问时,环境变量和卷提供了相似的安全级别。
假设攻击者获取了对一个正在运行的 Pod 的访问权限,并且机密作为环境变量注入。攻击者可以通过在 shell 中运行 export 命令来列出所有环境变量:
export
declare -x GREETING_MESSAGE="Hello Anna" ①
declare -x HOME="/"
declare -x HOSTid="greeting-demo-deployment-5664ffb8c6-2pstn"
...
① 机密被泄露。
攻击者很容易就能找出你的机密值。
另一个选择是使用卷。由于任何任意目录都可以挂载,你可能认为你是安全的,因为攻击者应该需要知道卷挂载的位置。是的,这是真的,但遗憾的是,有一种方法可以轻易地找到它。
如果攻击者获取了对一个已挂载机密作为卷的正在运行的 Pod 的访问权限,他们可以通过在 shell 中运行 mount 命令来列出所有挂载的文件系统:
mount | grep tmpfs
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,relatime,mode=755)
tmpfs on /etc/sec type tmpfs (ro,relatime) / ①
...
ls /etc/sec ②
greeting.message
cat /etc/sec/greeting.message ③
Hello Anna
① 使用机密的卷挂载
② 列出机密的密钥
③ 打印机密值
没有完美的解决方案。使用卷而不是环境变量的一个优点是,某些应用程序可能在启动时记录当前的环境变量,这同时也可以发送到中央日志系统,这意味着日志系统的任何安全漏洞都会暴露机密值。
保护 Kubernetes 机密是否是一项无望的任务?当然不是。首先,你需要评估攻击者获取对您的 Pods、节点和基础设施访问权限的可能性。其次,有一些操作可以应用来限制对 Pod 的访问——例如,移除执行 kubectl exec 或 kubectl attach 的权限。
机密存储在 etcd 中
正如你在 2.1.1 节中读到的,etcd 是一个键值数据库,其中存储了所有 Kubernetes 对象,当然,ConfigMaps 和秘密也不例外。创建秘密的过程如下:
-
开发者或操作员创建一个新的秘密资源,并与 Kubernetes API 服务通信以应用它(
kubectl apply -f ...)。 -
Kubernetes API 服务处理资源,并将秘密插入到
etcd中,位于/registry/secrets/<namespace>/<secret-name>键下。
这个过程在图 2.7 中展示。

图 2.7 存储秘密的过程
etcd 仍然是一个数据库。因此,您需要考虑以下方面:
-
etcd的访问必须限制为管理员用户。如果不这样做,任何人都可以查询etcd来获取秘密。 -
etcd数据库的内容持久化到磁盘。由于秘密默认情况下没有加密,任何有权访问磁盘的人都可以读取etcd的内容。 -
执行磁盘备份是正常操作,但要注意备份中发生的事情,因为它们包含未加密的敏感信息。
-
etcd是一个外部服务,由 Kubernetes API 服务器通过网络访问。请确保使用 SSL/TLS 与etcd进行点对点通信(传输中的数据)。
Base64 不是加密
再次强调一次,因为这可能导致误解,Base64 不是一个加密方法,而是一种编码方法。重要的是要记住,秘密默认情况下是没有加密的,您需要启用加密静态数据功能来将您的秘密加密存储到 etcd 中。这一点将在第四章中详细讨论。
访问 Pods
正如您之前看到的,如果攻击者获得了对 Pod 的访问权限,窃取秘密相对容易,即使您正在使用环境变量或卷。还有另一种与这种攻击相关的攻击类型,您应该为此做好准备:能够创建 Pod 的攻击者可以注入和读取秘密。在第五章中,我们将讨论防止这种攻击的方法。
源代码仓库中的秘密
使用 kubectl CLI 工具可以创建一个秘密。
kubectl create secret generic greeting-secret \
--from-literal=greeting.message=Hello
大多数情况下,您通过文件(无论是 JSON 还是 YAML)配置秘密,其中可能包含以 Base64 编码的秘密数据,而不是加密。这种方法存在一些风险,因为秘密可能在以下情况下被泄露:
-
以不安全的方式共享文件(例如,通过电子邮件)
-
将文件提交到源代码仓库
-
失去文件
-
没有任何安全措施地备份文件
根权限
任何具有任何节点上 root 权限的人都可以通过模拟 kubelet 从 API 服务器读取任何秘密。您还需要注意基础设施(工作节点),以避免来自操作系统的直接攻击。
你已经完成了处理秘密和 Kubernetes 的基本策略的实施,但你的秘密管理工作还没有完成,因为你还没有解决本节中识别的大多数风险。让我们从头开始;第三章从管理秘密的开始讨论,即如何根据最佳安全原则创建和管理 Kubernetes 资源文件。请留下来,因为接下来事情会变得有趣起来。
摘要
-
一个 Kubernetes 集群由主节点和可选的 worker 节点组成。
-
任何 Kubernetes 资源和集群的当前状态都存储在
etcd实例中。 -
我们讨论了将应用程序部署到 Kubernetes。
-
我们介绍了如何使用 ConfigMaps 在外部配置应用程序,无论是作为环境变量还是作为文件。
-
在构建和使用方面,Secrets 与 ConfigMaps 并没有太大的不同。
第二部分. 管理机密
现在你已经对机密和 Kubernetes 有了基本的了解,第二部分重点介绍这些资产的管理方式以及它们如何在 Kubernetes 集群的上下文中应用。
第三章概述了在静态存储 Kubernetes 机密的不同方法,包括支持工具以协助此过程。第四章专注于在 Kubernetes 集群内部安全存储机密,包括在基础设施级别支持加密的能力。第五章介绍了一种用于存储和管理敏感资产的秘密管理工具。最后,在第六章中,前一章的概念被扩展以集成云秘密存储。
3 安全存储秘密
本章涵盖
-
将 Kubernetes 清单捕获以存储在版本控制系统
-
启用安全的静态秘密存储
-
使用 Kubernetes Operators 来管理 Kubernetes 资源,包括 Secrets
-
将安全考虑纳入 Kubernetes 软件包管理器
-
实施密钥轮换以改善你的安全态势
第二章概述了 Kubernetes 环境的关键架构组件,以及工作负载的部署方式和通过 ConfigMaps 和 Secrets 注入配置的方法。但是,一旦资源被添加到 Kubernetes 集群中,它们是如何被管理的?如果它们被意外删除会发生什么?确保它们被捕获并存储以备将来使用变得越来越重要。然而,当处理可能包含敏感信息的资源时,必须仔细思考和考虑。本章介绍了可以用来安全存储 Kubernetes Secrets 的工具和方法,并说明了声明性定义 Kubernetes 资源的好处。
3.1 静态存储 Kubernetes 清单
云原生技术的优势之一是资源可以根据需求构建、部署和配置。只需几点击或按键,就可以以最小的努力构建整个架构。如果你刚开始使用 Kubernetes,当你意识到构建复杂应用程序是多么容易时,你会感到兴奋。你甚至可能想要向父母、朋友或同事展示。但是,当你展示你的工作时,你可能会被问到如何复制它。这时,对于每个 Kubernetes 资源,包括包含敏感值的资源,正确管理和存储以备将来使用变得至关重要。
在第二章中,我们介绍了在 Kubernetes 环境中创建资源的两种主要方法:
-
使用 Kubernetes CLI(kubectl),将命令行参数提供的输入进行转换。
-
明确使用 YAML 或 JSON 格式的文件来指定资源配置。
前者,其中 Kubernetes CLI 为你提供转换,被称为命令式方法。例如,当你在上一个章节中使用 kubectl create secret 子命令创建部署的秘密时,Kubernetes CLI 确定了如何解释你提供的输入并向 Kubernetes API 发送请求以创建秘密。虽然这种方法简化了资源的初始设置和配置,但它也带来了长期支持性的挑战。kubectl create secret 命令不是幂等的,再次运行它将导致错误。这可以通过执行以下命令来看到,它将尝试创建一个名为 greeting-secret 的秘密,并将导致错误,因为在第二章中创建的 default 命名空间中已经存在具有相同名称的秘密:
kubectl create secret generic greeting-secret -n default \
--from-literal=greeting.message=Hello
Error from server (AlreadyExists): secrets "greeting-secret" already exists
注意:如果默认命名空间中尚未存在密钥,再次运行此命令将导致类似的错误。
现在不再使用强制方法,资源可以明确地以 YAML 或 JSON 格式表示,并使用 kubectl 工具应用到集群中。这种方法被称为 声明性配置,它对支持 Kubernetes 环境中资源的长期生命周期有好处。
声明性配置是近年来获得流行的一个概念的关键特性:基础设施即代码(IaC)。与手动配置资源或使用随机脚本相比,应用到基础设施或应用程序的配置是明确定义的,这带来了以下好处:
-
减少错误
-
可重复性
-
审计和跟踪
能够审计和跟踪这些配置,尤其是在将清单存储在版本控制系统(VCS)如 Git 中时变得可能。一旦资源被捕获,它们的生命周期,包括更改了什么和谁更改了它们,都可以得到适当的跟踪。因此,如果发生灾难,而无需确定 Kubernetes 集群在那个时刻的状态,之前捕获并存储的清单可以被重新应用,从而最小化停机时间和努力。
3.1.1 捕获用于声明性配置的资源
当采用基于声明的配置或从主要基于强制的做法过渡时,有多种策略可以捕获清单(图 3.1)。回想一下,最终结果将是一系列 YAML 或 JSON 文件。虽然这两种文件类型都可以使用 kubectl 命令行工具应用到 Kubernetes 集群,但由于其可读性,基于 YAML 的文件更受欢迎。

图 3.1 捕获 Kubernetes 资源作为基础设施即代码(IaC)
可以使用两种常见场景来捕获用于声明性存储的清单:
-
捕获
kubectl强制命令的输出。 -
在 Kubernetes 环境中捕获已存在的资源输出。
捕获 kubectl 强制命令
为了帮助以声明性方式存储清单,Kubernetes CLI 提供了两个有用的标志,可以在调用强制命令时添加,例如 kubectl create secret。
-
--dry-run—模拟资源如何应用到 Kubernetes 环境中。Kubernetes 版本低于 1.18 不需要使用参数,如client,因为 APIServer 的 dry-run 在新版本中已被重构。 -
-o—以多种格式输出命令的结果,包括 YAML。
将这些标志添加到强制密钥创建命令中,将输出发送到 Kubernetes 集群的内容表示,而不会对集群的实际状态进行任何更改。
kubectl create secret generic greeting-secret -n default \
--from-literal=greeting.message=Hello --dry-run=client -o yaml
捕获已部署的资源
Kubernetes CLI 的命令式能力仍然具有巨大的价值。在开发过程中,通常将命令式调用和手动配置结合起来,同时测试和验证每个资源的细节。一旦配置就绪,建议捕获这些资产,以便将它们存储在版本控制系统中,以符合基础设施即代码(IaC)原则。
可以使用 kubectl get 子命令查询资源的当前状态。与 3.1 列表所示类似,该命令可以以多种格式输出。执行以下命令以显示在第二章中创建的 greeting-secret 的内容。
列表 3.1 greeting-secret 机密
kubectl get secrets greeting-secret -o yaml
apiVersion: v1
data:
greeting.message: SGVsbG8= ①
kind: Secret
metadata:
creationTimestamp: "2020-12-25T00:22:44Z"
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:greeting.message: {}
f:type: {}
manager: kubectl-create
operation: Update
time: "2020-12-25T00:22:44Z"
name: greeting-secret
namespace: default
resourceVersion: "27935"
selfLink: /api/v1/namespaces/k8s-secrets/secrets/greeting-secret
uid: b6e87686-f4e6-454d-b391-aef37a99076e
type: Opaque
① 创建机密时实例化的字面值的 Base64 编码值
如你可能在输出中注意到的,包括 status 在内的字段,以及 metadata 中的一些字段(例如 uid、resourceVersion 和 creationTimestamp 等)都包含来自当前集群的运行时细节。这些属性不适合存储,应该被移除。它们可以手动移除或使用工具,如 yq(一个轻量级的 YAML 处理器)来移除。以下列表展示了使用 yq 移除运行时属性的一个示例。
列表 3.2 将机密内容输出到文件
kubectl get secrets greeting-secret -o yaml | \
yq e 'del(.metadata.namespace)' - | \
yq e 'del(.metadata.selfLink)' - | \
yq e 'del(.metadata.uid)' - | \
yq e 'del(.metadata.resourceVersion)' - | \
yq e 'del(.metadata.generation)' - | \
yq e 'del(.metadata.creationTimestamp)' - | \
yq e 'del(.deletionTimestamp)' - | \
yq e 'del(.metadata.deletionGracePeriodSeconds)' - | \
yq e 'del(.metadata.ownerReferences)' - | \
yq e 'del(.metadata.finalizers)' - | \
yq e 'del(.metadata.clusterName)' - | \
yq e 'del(.metadata.managedFields)' - | \
yq e 'del(.status)' -
在附录 B 中可以找到如何在您的机器上安装 yq 工具的概述。可以将先前命令的输出重定向到文件中,以在版本控制系统中启用版本存储:
kubectl get secrets greeting-secret -o yaml | \
yq e 'del(.metadata.namespace)' - | \
yq e 'del(.metadata.selfLink)' - | \
yq e 'del(.metadata.uid)' - | \
yq e 'del(.metadata.resourceVersion)' - | \
yq e 'del(.metadata.generation)' - | \
yq e 'del(.metadata.creationTimestamp)' - | \
yq e 'del(.deletionTimestamp)' - | \
yq e 'del(.metadata.deletionGracePeriodSeconds)' - | \
yq e 'del(.metadata.ownerReferences)' - | \
yq e 'del(.metadata.finalizers)' - | \
yq e 'del(.metadata.clusterName)' - | \
yq e 'del(.metadata.managedFields)' - | \
yq e 'del(.status)' - \
> greeting-secret.yaml
由于资源现在以声明性方式描述,并且有资格在版本控制系统中跟踪和可见,因此确保这些值不易被确定的保护措施变得更加重要。由于机密只是 Base64 编码的,因此采用额外的机制来阻止确定其值的能力至关重要。本章的剩余部分将介绍并演示可以用来在静态存储中保护机密的工具。
3.2 安全存储 Kubernetes 资源的工具
虽然 Kubernetes CLI 没有提供任何额外的原生功能来保护存储的清单,但 Kubernetes 的流行使得与其他云原生工具集成以帮助解决这一挑战成为可能。这些工具包括那些已经具有在静态存储中保护清单的功能的工具,以及专门为在 Kubernetes 环境中解决此目的而开发的解决方案。作为 Kubernetes 的消费者,你可能需要管理那些专注于底层基础设施或平台内部署的应用程序,或者两者兼而有之。你最终将使用的工具取决于你的用例。了解哪些工具可用以及如何使用它们将帮助你做出明智的决定,选择适合你特定任务的适当工具。
3.2.1 Ansible Vault
Kubernetes 的典型部署(不包括 minikube)需要对基础设施和应用程序组件都进行考虑。这包括支持控制平面和工作节点的物理和虚拟资源以及 Kubernetes 清单的配置。配置管理工具在这个领域处于有利位置,因为它们不仅管理与 Kubernetes 环境相关的有时复杂的配置,而且有助于说明和实现基础设施即代码(IaC)的概念。
Ansible 是一款流行的配置管理工具,可用于管理 Kubernetes 生态系统的各个方面。与其他类似工具相比,Ansible 的一个关键优势是它非常适合云环境,因为它是无代理的(即,它不需要中央管理服务器)并且通过安全外壳(SSH),一种常见的通信协议进行通信。你只需要在你的本地机器上安装这个工具,就可以立即开始工作了!
安装 Ansible
Ansible 是一个基于 Python 的工具,因此,它是唯一的前提条件,并且可以安装在各种操作系统上。由于安装说明取决于目标操作系统,请参考官方文档(mng.bz/2rew)以了解如何为您的机器安装 Ansible。
Ansible 101
Ansible 将自动化指令组织成一系列 YAML 文件。描述要应用于目标配置的指令被组织到 Playbooks 中,这些 Playbooks 声明了配置应该应用到的主机以及一系列定义每个目标机器期望状态的任务。例如,一个简单的 Playbook 可以强制所有 Linux 机器在用户登录时显示每日消息(MOTD)。以下是一个示例 Playbook。
列表 3.3 一个示例 Ansible Playbook
- hosts: linux ①
tasks:
- name: Set motd
copy: ②
content: "Linux Hosting Environment." ③
dest: /etc/motd ④
① 应应用于更改的主机组
② 复制模块将内容复制到远程位置。
③ 目标文件的值
④ 目标文件的存储位置
目标实例被组织成组并在清单文件中声明;它们定义了 Ansible 如何促进连接,以及在使用 Playbook 调用期间使用的任何变量。然后使用 ansible-playbook 命令调用 Playbooks,该命令将执行自动化:
ansible-playbook <playbook_file>
Ansible 和 Kubernetes
Ansible 的“核心”是基础设施的管理和配置。随着 Kubernetes 的普及继续增长,它正成为许多组织基础设施的关键组成部分,因此,Ansible 和 Kubernetes 之间的集成是可用的。
这种集成的一个有用功能是管理 Kubernetes 资源,这是通过使用 k8s 模块实现的。模块是可以包含在 Playbooks 中的可重用脚本。在先前的 MOTD 示例中,使用了 copy 模块将内容从本地机器复制到远程目标。
现在,您将创建一个 Ansible Playbook,通过使用您在本地机器上已经可用的 greeting-secret 来管理您的 Kubernetes 集群的配置。在您开始之前,准备您的开发环境。首先,在您的机器上复制 greeting-secret.yaml 文件,并创建一个名为 greeting-secret_ansible.yaml 的新文件:
cp greeting-secret.yaml greeting-secret_ansible.yaml
接下来,创建一个名为 kubernetes-secrets-ansible 的命名空间,用于此场景:
kubectl create namespace kubernetes-secrets-ansible
接下来,更改您的 kubectl 客户端的命名空间首选项,以指向新创建的命名空间:
kubectl config set-context --current --namespace=kubernetes-secrets-ansible
注意:在设置命名空间首选项后,所有后续命令都将查询目标命名空间。
现在,在文件 greeting-secret_ansible.yaml 所在的同一目录下,创建一个名为 k8s-secret-mgmt.yaml 的新文件,其中包含以下内容以包含 Playbook。
列表 3.4 k8s-secret-mgmt.yaml
- hosts: localhost ①
gather_facts: no ②
connection: local ③
tasks:
- name: Create Secret on Cluster
k8s: ④
definition: > ⑤
"{{lookup('file', playbook_dir +
'/greeting-secret_ansible.yaml') }}"
state: present
namespace: kubernetes-secrets-ansible ⑥
no_log: True ⑦
① 在本地机器上执行 Playbook。
② 禁用 Ansible 收集与目标机器相关的信息。
③ 使用本地连接而不是默认的 SSH 与目标机器通信。
④ 使用 k8s 模块来管理 Kubernetes 资源。
⑤ 查找功能允许查询执行 Playbook 的控制节点(资源所在)的资源。在这种情况下,名为 greeting-secret.yaml 的文件值来源于包含 Playbook 文件的目录。
⑥ 将资源应用到 Kubernetes 集群的 kubernetes-secrets-ansible 命名空间。
⑦ 由于 Kubernetes Secrets 是作为此 Playbook 的一部分进行管理的,因此不要提供调用输出,因为这会暴露清单的内容。
默认情况下,k8s 模块使用本地机器上的 kubeconfig 文件来确定与 Kubernetes 集群通信的方法。由于您已经对 minikube 实例进行了认证,所以这一点已经处理好了。
在执行 Playbook 之前,必须安装 openshift Python 模块,以便 Ansible 能够与 Kubernetes 集群通信。OpenShift 是 Kubernetes 的一个发行版,k8s Ansible 模块在执行前需要 openshift 模块。这可以通过 pip 完成,这取决于您的操作系统,可能也是 Ansible 本身安装的方式。如果 pip 目前尚未安装,有关其安装的说明可以在附录 C 中找到。通过执行以下命令添加 openshift Python 模块:
pip install openshift
在安装了必要的依赖项后,运行 Playbook:
ansible-playbook k8s-secret-mgmt.yaml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available.
➥Note that the implicit localhost does not match 'all'
PLAY [localhost] **********************************************************
***************************************************************************
***********************
TASK [Create Secret on Cluster] *******************************************
***************************************************************************
***********************
changed: [localhost]
PLAY RECAP ****************************************************************
***************************************************************************
***********************
localhost : ok=1 changed=1 unreachable=0 failed=0
skipped=0 rescued=0 ignored=0
注意:您可以安全地忽略警告,因为它们不会影响 Playbook 的执行。
如输出中强调的,Playbook 在本地定义了密钥后成功运行,现在该密钥已存在于 Kubernetes 集群的 kubernetes-secrets-ansible 命名空间中。这可以通过运行 kubectl get secrets greeting-secret 来确认。
Ansible Vault
如你所想,配置管理工具,如 Ansible,在管理敏感值方面面临相同的挑战。在上一节创建 Ansible Playbook 时,从greeting-secret_ansible.yaml文件中获取的秘密包含 Base64 编码的值,而这些值最终可能被解码。幸运的是,Ansible 通过启用文件加密功能来提供帮助,这些文件可以在运行时通过 Ansible Vault 进行解码。
Ansible Vault 允许保护变量和文件,因此可以安全存储。与 Kubernetes Secrets 不同,Ansible Vault 使用加密而不是编码来避免被轻易逆向工程(图 3.2)。

图 3.2 使用 Ansible Vault 的加密和解密过程
截至 Ansible 2.10 版本,Ansible Vault 仅支持AES256作为加密敏感材料的加密算法。要加密包含敏感值的秘密的greeting-secret_ ansible.yaml文件的内容,请使用ansible-value encrypt命令。你将被提示提供可以用于加密和解密加密密码内容的密码:
ansible-vault encrypt greeting-secret_ansible.yaml
New Vault password:
Confirm New Vault password:
Encryption successful
一旦文件被 Ansible Vault 加密,生成的文件将采取以下形式。
列表 3.5 greeting-secret_ansible.yaml
$ANSIBLE_VAULT;1.1;AES256
<ENCRYPTED_CONTENT>
结果是一个 UTF-8 编码的文件,它包含一个以换行符终止的头部,后面跟着加密内容。头部包含最多四个元素:
-
格式 ID(目前仅支持
$ANSIBLE_VAULT) -
保险库格式版本
-
密码算法
-
保险库 ID 标签(在此示例中未使用)
文件的负载是密文和 SHA256 摘要的连接,这是由于 Python binascii模块的hexlify()方法。具体细节在此不详细描述,但它们在 Ansible Vault 文档中有详细解释(mng.bz/19rR)。
一旦使用 Ansible Vault 加密了文件,在调用ansible-playbook命令时必须提供--ask-vault-password或--vault-password-file。为了使用--vault-password-file标志,密码必须作为引用文件中的内容提供。
提示:除了向ansible-playbook命令提供标志外,还可以通过ANSIBLE_VAULT_PASSWORD_FILE环境变量提供 Vault 密码文件的位置。
运行 Playbook,并添加--ask-vault-pass标志,这将提示输入 Vault 密码。当提示时,输入密码并按 Enter 键。如果提供了正确的密码,Playbook 将成功执行。
ansible-playbook k8s-secret-mgmt.yaml --ask-vault-pass
Vault password:
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available.
➥Note that the implicit localhost does not match 'all'
PLAY [localhost] **********************************************************
***************************************************************************
***********************
TASK [Create Secret on Cluster] *******************************************
***************************************************************************
***********************
ok: [localhost]
PLAY RECAP ****************************************************************
***************************************************************************
***********************
localhost : ok=1 changed=0 unreachable=0 failed=0
skipped=0 rescued=0 ignored=0
如果提供了错误的值,将显示如下消息:
TASK [Create Secret on Cluster] *******************************************
***************************************************************************
***********************
fatal: [localhost]: FAILED! => {"censored": "the output has been hidden
➥due to the fact that 'no_log: true' was specified for this result"}
由于你在任务中指定了no_log,因此不会提供更详细的错误信息。为了进一步调查,你可以暂时注释掉no_log以确定失败的根本原因。
通过使用 Ansible Vault 加密greeting-secret_ansible.yaml文件中 Kubernetes Secret 的内容,可以将 Playbook 和加密文件安全地存储在版本控制系统中。
Ansible Vault 展示了从客户端的角度如何管理加密和解密 Kubernetes 资源。下一节将介绍将责任转移到在 Kubernetes 集群内运行的组件。
3.3 Kubernetes Operators
虽然 Ansible Vault 满足了安全存储敏感 Kubernetes 资产的需求,但仍有几个方面可以改进:
-
执行 Ansible 自动化的用户被赋予了解密敏感资产所需的密码。
-
解密在客户端进行。任何敏感资产要么以明文值的形式传输,要么与 Kubernetes Secret 一起传输,因此是 Base64 编码的。
另一种策略是利用一个模型,其中解密过程完全在 Kubernetes 集群内部进行,将最终用户或自动化过程从管理敏感材料中抽象出来,因为资源被应用。这是 Sealed Secrets 项目所采用的方法,其中解密是通过在集群内运行的运算符来管理的。
回想第二章,主节点的一个关键组件是它们包含控制器,这些控制器名称恰当,因为它们实现了一个非终止的控制循环来管理和监控集群中至少一个资源的期望状态。当目标资源发生变化时,控制器将确保集群的状态与期望状态相匹配。
用户最熟悉的常见控制器之一是 ReplicaSet 控制器。部署是将工作负载注册到 Kubernetes 的常用方法,每当创建一个部署时,都会自动生成一个 ReplicaSet。ReplicaSet 控制器将监视与 ReplicaSet 关联的 Pod,并确保活动 Pod 的数量与 ReplicaSet 中定义的期望状态相匹配。
3.3.1 自定义资源定义(CRDs)
从历史上看,Kubernetes 的资源数量相对较少,例如 Pod 和 Secret。随着该平台知名度的提高,对新的资源类型的需求也随之增加,这既来自核心维护者,也来自用户。任何开发者都可以证明,对核心 API 的更改通常是一个具有挑战性和耗时漫长的过程。
自定义资源定义(CRD),作为一种新的资源类型,是解决这一问题的方法,因为它为开发者提供了注册他们自己的 API 以及与这些资源相关联的属性的机会,同时能够利用 API 服务器中的功能,而不会干扰核心 API 集。例如,可以定义一个新的资源CronTabs,其目的是在特定的时间点执行任务。可以开发一个应用程序来查询 Kubernetes API 的CronTabs资源并执行任何所需的业务逻辑。然而,如果你能够执行许多与包含的控制器集相同的操作,比如能够立即对状态变化做出反应,比如资源的创建或修改,那会怎么样?幸运的是,客户端库,特别是 Go 编程语言的 client-go,提供了这些功能。
开发一个应用程序来监控自定义资源并对其实施操作的这一概念被称为操作员,这一模式在 Kubernetes 社区中得到了广泛的应用;Sealed Secrets 项目也实现了这一模式。开发操作员和自定义控制器的过程曾经是一项巨大的成就,因为开发者需要深入了解 Kubernetes 的内部结构。幸运的是,工具,如 kubebuilder 和 Operator Framework,简化了这一过程(图 3.Figure 3.3)。

图 3.3 操作员在 Kubernetes 中管理资源概述
3.3.2 密封秘密
由于 Sealed Secrets 的大多数解决方案都卸载到控制器或操作员,那么它执行哪些操作呢?Sealed Secrets 包含三个不同的组件:
-
操作员或控制器
-
一个名为kubeseal的 CLI 工具,由最终用户用于加密 Kubernetes Secrets
-
一个名为
SealedSecret的 CRD
在将 CRD 添加到集群并将控制器部署到命名空间后,如果不存在,控制器将创建一个新的 4096 位 RSA 公私钥对,并将其作为秘密保存在控制器部署的同一命名空间中。
最终用户使用 kubeseal 工具将标准的 Kubernetes Secret 转换为SealedSecret资源。加密过程对 Kubernetes Secret 中的每个值执行以下操作(图 3.4):
-
该值使用随机生成的 32 位一次性会话密钥进行对称加密。
-
会话密钥随后使用控制器先前生成的公共证书创建的公钥,通过最优非对称加密填充(RSA-OAEP)进行非对称加密。
-
结果存储在
SealedSecret资源中。

图 3.4 使用 Sealed Secrets 项目加密和解密秘密的过程和组件
注意:有关使用的加密和解密更详细的概述,可以在 Sealed Secret 项目主页上找到:github.com/bitnami-labs/sealed-secrets。
安装 Sealed Secrets
安装 Sealed Secrets 的第一步是将控制器部署到集群中。虽然支持多种方法,但这里将使用通过原始 Kubernetes 清单进行安装的方式。
为了本书的目的,将使用版本v0.13.1,与控制器和相关 kubeseal 命令行工具的安装步骤可以在项目存储库中的发布页面上找到(mng.bz/PoP5)。
与之前操作 Ansible Vault 时完成的步骤类似,创建一个名为kubernetes-secrets-sealedsecrets的新命名空间,并将命名空间首选项设置为针对此命名空间:
kubectl create namespace kubernetes-secrets-sealedsecrets
kubectl config set-context --current \
--namespace=kubernetes-secrets-sealedsecrets
通过执行以下命令将控制器添加到 Kubernetes 集群:
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/
➥download/v0.13.1/controller.yaml
默认情况下,控制器将安装到kube-system命名空间。通过列出kube-system命名空间中的运行中的 Pod 来确认控制器已成功部署:
kubectl get pods -n kube-system -l=name=sealed-secrets-controller
NAME READY STATUS RESTARTS AGE
sealed-secrets-controller-5b54cbfb5f-4gz9j 1/1 Running 0 3m43s
接下来安装 kubeseal 命令行工具。二进制发布和安装步骤可以在本节之前引用的发布页面上找到。务必遵循适用于您操作系统的详细步骤。一旦安装了 kubeseal,请确认 CLI 已正确安装,并且可以获取控制器生成的公钥证书,从而能够加密秘密:
kubeseal --fetch-cert ①
-----BEGIN CERTIFICATE-----
MIIErjCCApagAwIBAgIRAIOgwJnDRCIcZon5GumMT8UwDQYJKoZIhvcNAQELBQAw
ADAeFw0yMDEyMjgxNjI3MDhaFw0zMDEyMjYxNjI3MDhaMAAwggIiMA0GCSqGSIb3
DQEBAQUAA4ICDwAwggIKAoICAQDosr3qLBJ4YQRiKvQgkQgMN+sCp2mQo8vJbj8z
rOaINXdkD6isHqq80uJ0uJ6ZigFpDmoyOUVlHbkprlngu6d41fBpEW0caREZrcd9
2s8yT2/8yJQ2Q1pZawGl0XjHOFMNEtdk3bvepLWGWcY7QUKJJwHpW5vGVs9xLU34
nnbPK0/dY1O6bnhIfVRgYvomO+IIfSDx3t7OGg/hEm2jp7rkNBIdW0qnH7GwTNVx
6FdD+DGztSgqTMdttla7IwRZjfXSf3HAIK0ZY8cq7hsd3+JewSsWwctNCHbeW4Y5
QNjKXcBr9UeReZ6+BOw8p8xSSBYE0DPNLbqccjcjYT/lD/r7Ja2Pb1W4X/tt8Dwc
EccnjGW+3zYdAQulxLN+EZos+hlgFcNAeBHkPwbC9oDAamfsJAihGIWMa/CyBZAm
2eF2aFtU0djDEhrVIuzrw4JKSdatqD0Bu0QOLQl08PM/GnAzDGzZ9jswfdRmjoPS
t20XyRG+9irB4SIv47KjWXulc7h9hYrQWxDlNy/R6TeqirA/hOiBn4ZgaY3xx3+/
tDFJ5YkR+rzEcf+W/5I3SbOzKQ9XvERGVUJUfJbjXoes8JY0qxFZosUyaiwi+xWT
F8R/1k0+OwtH2u1e4pq265I1HBJGQcOpuKpf1U/q1uACncRsi2s+EHA323T7Jkc7
3srk9wIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN
BgkqhkiG9w0BAQsFAAOCAgEAeX4Mf+65e8r2JMTqNKP1XnEYEw/jnq7BpjwxjNxw
AVoF2YdZifi/0U9Xr5SA+uCWYVgRB5wFpZ8trckTaLUiszTeLtlwl1Jouf2VIcbY
N6RF1uHbBEYEyZl7daoF3Sd1stj/oZBPmjEPl2OLus0WpYkGDdy+29fzUz271yA8
P1UE5Uq/7/0P/UIuU9pMQMbciuP0F97ODp/8i2iXyEiXPbe7s+h0GXlsrjyD65Fz
cwc9etAXuHrxcKPyCAtyzW3CmU+WqE6nVCNgwwh4j5r2SEeR3UZVw8Yub45IoZiE
PMcTlfA9e4hw4muKEmygdYCbiFQLsa0G/MtBv+IwpaMPtoY6edjUY+OOpgX7OlI9
ymfnhGLyGqHLbwhZpc3gvJHWCJ9mRkGr66KAHA1+HOlJw/aua0A3Fo2DBP2Ruftu
g3NgE5G6zPnfcalaPjt+Cl7Wu9TfzcIxVtTgM6g+LePgYP3tTRzAMv0DzKHSpBqW
v98pF1cG0vrVk15rLIcAlCMYhP95el4qtfcXwQzKmnQBhW+emaCIudvyFRJdFM2o
f0pSiRYkpLDrqZ2fiqw+eqts80hUDOh9GvJzxtZbOccxTbgaKxX9MtAQllw3vYJx
EHCp06JmUc09GtYCju2gJH29baHWldNDeP/3z9913RmnIWggh4b+G0FmPmB5XOhb
PR4=
-----END CERTIFICATE-----
① --fetch-cert 获取当前公钥证书。
使用 Sealed Secrets 加密秘密
现在,Sealed Secrets 已成功安装,请使用列表 3.2 中创建的kubeseal加密greeting-secret.yaml的内容,创建一个名为greeting-secret_sealedsecrets.yaml的新文件,如下所示。
列表 3.6 使用 kubeseal 加密秘密
kubeseal --format yaml <greeting-secret.yaml \
>greeting-secret_sealedsecrets.yaml -n kubernetes-secrets-sealedsecrets
查看以下列表中新生成的greeting-secret-sealedsecrets.yaml文件的内容。
列表 3.7 greeting-secret-sealedsecrets.yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: greeting-secret
namespace: kubernetes-secrets-sealedsecrets
spec:
encryptedData:
greeting.message: AgAKGnqEn6MRRsDGoH2lhKTwJ0UVeUaN+Kq0Uyr13ZNnQB/eLFjJ
➥qkzN9+kbnMC9J9ptA2MS2WMIkKnF7cRaX3HloCp/SqgsN3eIhZs6zL4EHcpxUkXTPl
➥83ynwa6oC6z/vAFwwJhKHkQPKJ4yrwOpo1bauddL7Oi4fwxqRyK98EHiQ485qv26rK
➥qJgl9q26gsGii0JFyL73OU3/r7nhdzKJ7+eL1EYVqV2Mn95O95ShqVYq970TPpPtLy
➥1MzeA/bT9hhgTmWyBzREZsG+O+knCO3j5NK0QBt7UEEenOlNQBgc5mTFaQ2SBjD9k7
➥MEG979jiPEIOL0LkkLbv2R8cox7HqlEZTOJ0E18ghWwxf3zsxPE2/IJw5WVxcmcAG1
➥O+cy0L+YP36xnaOe15WAtEmWTXk8aVh8SCzaLsYZEoom96Jh8ZGZHMsRuly2gmMUjW
➥4dTGQazeZm+T+q6kJuYxDZ/SDvlui+q9G6IB4joJIRndp16cTxQlqopqhjAO/YZOIc
➥KmAD1YrbXwSNw/Z+X+Y20xZQrp5BMFrvspar+1drNvJ+8/nvhYlo+j3p0MiHI7tyUt
➥5cqxsxhTpZLth5T6/VEt2hBIOQ5AKgJitm6yGnmZKzQwvkHYxGuy15sExI+MJ3LNyO
➥sLTprWzAzSuf/c0KFXM2/fQ+DlysZJsFCDYmiBygcbD65xqLFaQ4oHIcxwhXwmkYnd
➥oA==
template: ①
metadata:
creationTimestamp: null
name: greeting-secret
namespace: kubernetes-secrets-sealedsecrets
① 模板属性向生成的秘密添加字段。可以包括其他属性,如标签和注解。
作为额外的安全措施,与秘密关联的命名空间和名称作为 OAEP 过程的一部分被添加,因此重要的是要考虑在生成SealedSecret资源时这一点。最后,验证新创建的SealedSecret资源是否可以添加到 Kubernetes 集群。一旦添加,Sealed Secrets 控制器应解密内容并在同一命名空间中创建一个新的 Secret:
kubectl apply -f greeting-secret_sealedsecrets.yaml
确认已在新创建的kubernetes-secrets-sealedsecrets命名空间中创建了一个名为greeting-secret的新秘密:
kubectl get secrets -n default greeting-secret
NAME TYPE DATA AGE
greeting-secret Opaque 1 20s
Sealed Secrets 控制器还会根据其执行的操作发出 Kubernetes 事件,可以通过查询default命名空间中的事件来验证:
kubectl get events -n kubernetes-secrets-sealedsecrets
LAST SEEN TYPE REASON OBJECT MESSAGE
3m38s Normal Unsealed sealedsecret/greeting-secret SealedSecret
unsealed
successfully
事件添加提供了对 Sealed Secrets 管理的资源生命周期的洞察。
由于SealedSecret资源与相关 Secret 之间的紧密联系,如果删除SealedSecret资源,Kubernetes 也会删除引用的 Secret。这是因为 Secret 由SealedSecret资源拥有。Kubernetes 垃圾回收会将资源删除的删除操作级联到任何拥有的资源。有关 Kubernetes 垃圾回收的更多详细信息,请参阅 Kubernetes 文档(mng.bz/JVDQ)。
通过展示如何在我们的 Kubernetes 环境中使用 Sealed Secrets,您可以放心地将SealedSecret资源存储在版本控制系统中,而无需担心敏感资产容易被发现。在下一节中,我们将介绍如何在 Kubernetes 包管理器中管理敏感资产。
3.4 在 Kubernetes 包管理器中管理 Secrets
如您迄今为止所看到的,Kubernetes 提供了运行复杂应用程序的原语:ConfigMaps 和 Secrets 用于存储配置资产,Services 用于简化网络访问,Deployments 用于管理容器资源的期望状态。然而,Kubernetes 本身并不提供易于管理这些资源的机制。随着资源开始在 IaC 存储库中积累,这个问题变得越来越明显。Kubernetes 社区成员将此视为挑战,并产生了更好地管理 Kubernetes 应用程序生命周期的愿望,类似于任何其他集群外应用程序。传统上,这些功能是由包管理器(如 yum、apt-get 或 Homebrew)提供的。他们努力的成果导致了 Helm 的创建,并最终使其流行起来,Helm 是 Kubernetes 的包管理器。
Helm 通过提供以下关键功能简化了应用程序开发人员和 Kubernetes 应用程序消费者的生活:
-
生命周期管理(例如,安装、升级和回滚)
-
清单模板
-
依赖管理
Helm 使用一种称为 charts 的打包格式,其中包含与应用程序相关联的 Kubernetes 清单,并作为一个单一单元部署。在 Helm 中,清单被称为 模板,并在部署时通过 Helm 的模板引擎进行处理(它们基于 go-template,并从库(如 Sprig)中获得额外支持)。
值 是注入到模板资源中的参数。一旦部署到 Kubernetes,渲染的清单集合被称为 发布(图 3.5)。

图 3.5 Helm 发布通过组合模板和值来创建 Kubernetes 资源
最后,Helm 使用命令行客户端工具进行管理,该工具有助于简化图表的整个生命周期。这将是下一节中使用的首要工具。
提示:有关 Helm 的更多信息,可以在 Helm 网站上找到 helm.sh/。
3.4.1 部署 Greeting Demo Helm 图表
我们在第二章中提到的问候演示应用程序的清单已被打包成 Helm 图表,以展示 Helm 可以提供的优势。在本节中,你将安装 Helm CLI 工具,审查 Greeting Demo Helm 图表,并将其部署到你的 Kubernetes 集群。
首先,下载 Helm 命令行工具。根据你的操作系统,有多种安装选项。步骤和说明可以在 Helm 项目网站上找到(helm.sh/docs/intro/install/)。
一旦安装了 Helm CLI,请确保你的机器上安装了 Git,因为在本章以及随后的章节中管理版本控制中的资产需要 Git。有关如何安装和配置 Git 的说明,请参阅附录 D。
现在克隆包含 Helm 图表的存储库到你的机器上:
git clone https://github.com/lordofthejars/
➥kubernetes-secrets-source ①
cd greeting-demo-helm ②
① 克隆包含图表的存储库。
② 切换到图表目录。
一旦进入图表目录,你会注意到以下目录结构:
── Chart.yaml
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── configmap.yaml
│ ├── deployment.yaml
│ ├── ingress.yaml
│ ├── secret.yaml
│ ├── service.yaml
│ └── serviceaccount.yaml
└── values.yaml
Chart.yaml 是 Helm 图表的清单,包含关键元数据,包括图表的名称以及版本。模板目录包含在安装图表时将部署到集群中的 Kubernetes 资源。关键区别在于,它们现在是模板化资源,而不是你迄今为止一直在使用的原始清单,如下所示。
列表 3.8 secret.yaml
{{- if not .Values.configMap.create -}} ①
apiVersion: v1
kind: Secret
metadata:
name: {{ include "greeting-demo.fullname" . }}
labels:
{{- include "greeting-demo.labels" . | nindent 4 }}
type: Opaque
stringData:
greeting.message: {{ required "A valid greeting message is required!"
➥$.Values.greeting.message | quote }}
{{- end }}
① 内容被 {{ }} 包围,将通过 Helm 的模板引擎进行处理。
include 函数引用了 _helpers.tpl 文件中存在的命名模板。有关模板和 Helm 目录结构的完整概述,请参阅 Helm 文档(helm.sh/docs/chart_template_guide/named_templates/)。
注意:此图表还包括其他 Kubernetes 资源,如 Ingress 和 Service Account。这些资源在执行 helm create 命令时作为典型模板的一部分创建。默认情况下,这些资源在典型使用此图表时不会部署,但如果需要,可以通过设置适当的值来部署。
与模板关联的参数位于 Values.yaml 文件中。通过浏览该文件,你会注意到许多关键属性,包括副本数量以及镜像位置和标签。在文件底部,你会注意到一个名为 greeting.message 的属性,但没有指定值:
greeting:
message:
如果你回想起列表 3.8 中的片段,这个属性是通过在模板目录中的 secret.yaml 文件中引用 $.Values.greeting.message 来注入的。值得注意的是,required 函数强制在安装此图表之前设置一个值。值可以通过多种方式指定,包括通过文件或使用命令行,并且 Helm 使用优先级来确定最终应用哪个属性。在 values.yaml 文件中定义的属性具有最低优先级。
在将此图表安装到 Kubernetes 集群之前,首先创建一个名为 kubernetes-secrets-helm 的新命名空间,并将当前内容更改到新创建的命名空间中:
kubectl create namespace kubernetes-secrets-helm
kubectl config set-context --current --namespace=kubernetes-secrets-helm
由于必须提供 greeting.message 的值,因此创建一个名为 values-kubernetes-secrets.yaml 的新文件,包含以下内容。
列表 3.9 values-kubernetes-secrets.yaml
greeting:
message: Hello from Helm!
接下来,通过提供发布名称、图表位置和包含所需属性的值文件的引用来安装 Helm 图表:
helm upgrade -i greeting-demo . -f values-kubernetes-secrets.ymal
注意:使用 -i 标志执行了 helm upgrade 命令,因为它提供了一种幂等的方法来安装 Helm 图表。如果存在现有图表,它将被升级。否则,将发生新的安装。
如果安装成功,将提供发布概述以及从模板中包含的 Notes.txt 文件的渲染内容。通过 curl 查询 minikube 服务公开的 IP 地址和端口,以确认在 Helm 值中设置并在 greeting-secret 密钥中存储的问候语是否呈现:
curl $(minikube ip):$(kubectl get svc --namespace kubernetes-secrets-helm \
greeting-demo -o jsonpath="{.spec.ports[*].nodePort}")/hello
3.4.2 使用 Helm Secrets
当我们成功将应用程序作为 Helm 图表部署时,我们再次面临如何管理存储在 values-kubernetes-secrets.yaml 文件中的敏感内容的挑战。正如你所预期的那样,Helm 和 Kubernetes 社区的其他成员试图找到一种解决方案,以提供对 Helm 值的安全性,以便它们可以存储在静态状态下。最流行的选项之一是 Helm Secrets,这是一个提供与各种密钥管理后端集成的 Helm 插件。Helm 中的插件是外部工具,不属于 Helm 代码库的一部分,但可以从 Helm CLI 访问。
Helm Secrets 使用来自 Mozilla 的 Secrets OPerationS (SOPS) 作为其默认的密钥管理驱动程序。SOPS 是一个加密键值文件类型(如 YAML)的工具,并与云(AWS/GCP KMS)和非云密钥管理解决方案集成。
对于将 SOPS 作为 Helm Secrets 后端的集成,最直接且最容易为我们目的设置的选项是利用 Pretty Good Privacy (PGP)。PGP 与 Sealed Secrets 类似,使用非对称公钥/私钥加密来保护文件内容,并且一直是安全传输电子邮件的流行方法。在加密时,使用一个随机的 256 位数据密钥,并将其传递给 PGP 以加密数据密钥,然后加密文件中的属性(图 3.6)。

图 3.6 使用 Helm Secrets 加密机密并将其作为 Helm 发布的一部分使用
现在准备您的机器,安装必要的软件和配置,以保护 Helm 图表的敏感属性。GNU Privacy Guard (GPG) 是专有 PGP 的开放标准实现,相应的 gpg 工具将使您能够适当地管理您的密钥。查看附录 E 以获取有关如何安装的进一步说明。一旦安装了 gpg CLI,创建一个新的公钥/私钥对:
gpg --generate-key
输入您的姓名和电子邮件地址。当提示输入密码短语时,不要输入任何值。
注意,虽然留下 GPG 密码短语为空可能看起来有些反直觉,但它简化了与自动化工具的集成(一些不支持密码短语)以及减少了管理另一个敏感资产的需求。
公钥/私钥对成功创建后,您可以按以下方式列出密钥环中的密钥以进行确认。
列表 3.10 列出 GPG 密钥
gpg --list-keys
pub rsa2048 2020-12-31 [SC] [expires: 2022-12-31]
53696D1AB6954C043FCBA478A23998F0CBF2A552 ①
uid [ultimate] John Doe <jdoe@example.com>
sub rsa2048 2020-12-31 [E] [expires: 2022-12-31]
① GPG 指纹
记录以 5369 开头的值,因为这将是公钥的指纹,将在配置 SOPS 时稍后使用。接下来,使用 Helm CLI 安装 Helm Secrets 插件:
helm plugin install https://github.com/jkroepke/helm-secrets
SOPS 也将作为 Helm Secrets 安装的一部分进行安装。由 Helm Secrets 插件管理的值文件,按照惯例,位于名为 helm_vars 的目录中。在 greeting-demo 图表存储库中创建此目录:
mkdir helm_vars
要完成 Helm Secrets 和 SOPS 之间的集成,请在 helm_vars 目录下创建一个名为 .sops.yaml 的新文件,内容如下:
---
creation_rules:
- pgp: "53696D1AB6954C043FCBA478A23998F0CBF2A552"
---
将 pgp 旁边的内联代码替换为在第 3.10 列表中发现的您自己的公钥指纹。
接下来,将 values-kubernetes-secrets.yaml 文件移动到 helm_vars 目录,使其与 .sops.yaml 文件位于同一目录中。这将确保 SOPS 正确解密文件。
---
mv values-kubernetes-secrets.yaml helm_vars
---
在与 SOPS 集成完成并所需值文件位于适当位置后,使用 Helm Secrets 按以下方式加密文件。
列表 3.11 使用 Helm Secrets 加密
helm secrets enc helm_vars/values-kubernetes-secrets.yaml
Encrypted values-kubernetes-secrets.yaml
确认 values-kubernetes-secrets.yaml 文件的内容已加密,如下所示列表。
列表 3.12 values-kubernetes-secrets.yaml
greeting:
message: ENC[AES256_GCM,data:SYfMBpax8mTOqzPed3ksjA==,iv:OrN/r/WVF+ROR
➥BBaiyqyiRyIRS+LPb3gf2q9gU4OVH8=,tag:
➥B9NYch4tNruKzBQMKqk00g==,type:str] ①
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
lastmodified: '2020-12-31T04:21:40Z'
mac: ENC[AES256_GCM,data:h2fQPc9hzmGMaKIE73aYU2TxbwVYQQLRcYHWh++kAYjK
➥zm+o9KibOcsVXz8iRLVbeER62FR4h2AON6ZC8ZxoWW5MxQm
➥w729YOQc1nkrgWxsx+ST2ucYmUxn3D4Kqb9X8NSu8P3fPcr
➥/Q77fFQ4SK1fLh2Jd92UZt5dJ6hOGJEr0=,iv:cvXEVhZUP
➥9uHbYAASbXL8KS3sso34STMZhpG/phzN4U=,tag:PtCoqDf
➥Z4C+KN7Qxz55DSw==,type:str]
pgp:
- created_at: '2020-12-31T04:21:39Z'
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA0e7sMUYmEkyAQgAhrnbGtCkbRwEDkylTmWTHXeKhoEx+2bbD8vJ+g9m7hYl
iDwat80MKu2lGKgnVj2RAMIxwyjaLdoGY+pDXYxUJ5StFojh1bJbkEYKT5KYaGni
uvkpeYNwLtAZWd6Shjl1vAkVEdMsh3xFtv9ot2uwL/DuxmSvoIR5OxpLwEZSAPgM
UzsK0SMWQjyIT69oUKlJYd+Nwj0sv1oWJMAkra367EZxKzKKi1eKFZ3QHQ4JVAdp
+P8ctjQqeWi8bC/wN6PdRGVYfZD8bF3CxgdtYUKHRseNvjX2H6rD60x15FgADx9v
bGfOu4n5SccgGftgnYI8nXL7vnAntuLREz6XDnLQDNJeAVzDt623nJmlqKGP+x/G
HYfaEqBWI8bcPfBwHv3g9F1sAk86W86IR6pB0mOwD/twW9/J7InW91xEz2Q9KhMR
oWVWwF8IZzNWVb6Sjl6EXIaB+ssJXOtfWXyBD83w8Q==
=TH/P
-----END PGP MESSAGE-----
fp: 53696D1AB6954C043FCBA478A23998F0CBF2A552 ②
unencrypted_suffix: _unencrypted
version: 3.6.1
① 加密值
② 指纹
注意,该文件已被分为两个主要部分。
-
保留原始文件中的密钥。值现在已加密。
-
与 SOPS 相关的元数据,包括足够的信息以启用加密值的解密。
内容可以很容易地根据需要更新,使用helm secrets edit命令。为了表明您现在正在使用 Helm Secrets 以安全的方式管理我们的值文件的内容,将greeting.message属性的值更新为读取Hello from Helm Secrets!:
helm secrets edit helm_vars/values-kubernetes-secrets.yaml
更新文件的内容:
greeting:
message: Hello from Helm Secrets!
现在使用 Helm Secrets 升级图表,并使用更新的加密值文件:
helm secrets upgrade greeting-demo . -i -f
➥helm_vars/values-kubernetes-secrets.yaml
应该显示修订版2,表示成功发布。然而,如果您尝试查询应用程序公开的端点,它将不会反映更新的值,因为只有底层密钥被修改,并且没有使用类似于第二章中引入的重载器。
删除正在运行的 Pod,这将允许更新的值被注入到新创建的 Pod 中:
kubectl delete pod -l=app.kubernetes.io/instance=greeting-demo
一旦新创建的 Pod 运行,查询应用程序端点以确认响应显示您加密值文件中包含的属性:
curl $(minikube ip):$(kubectl get svc --namespace
➥kubernetes-secrets-helm \
greeting-demo -o jsonpath="{.spec.ports[*].nodePort}")/hello
使用 Helm Secrets 的一个好处是它介绍了 SOPS,这是一个可以用于在 Helm 生态系统之外安全存储敏感键值文件的工具。敏感资产的管理并不随着值的加密而结束。在下一节中,我们将讨论如何使用密钥旋转来提高您整体的安全态势,而不仅仅是第一天。
3.5 旋转密钥
个人和组织会不遗余力地保护可能使关键系统访问成为可能的敏感信息。然而,无论任何安全值有多强大或用于保护其访问的工具有多先进,总是存在被破坏的可能性。关键是最大限度地减少或降低这种可能性。
考虑到这一点,可以用来减少攻击向量的一种方法是实现某种形式的密钥旋转。旋转可以在两个主要领域发生:
-
实际上需要保护的值
-
用于生成加密资产的密钥或值
旋转敏感资产是一个我们大多数人应该熟悉的概念——例如,定期重置密码。然而,这种做法在管理资产方面一直存在不足,因为系统使用的是资产而不是人类。幸运的是,到目前为止介绍的所有工具(Ansible Vault、Sealed Secrets 和 Helm Secrets)都支持某种形式的旋转。
3.5.1 Ansible Vault 密钥旋转
由 Ansible Vault 生成的加密文件可以通过ansible-vault的rekey子命令重新加密,以使用不同的密码来保护和访问存储的资产。使用列表 3.11 中加密的greeting-secret_ansible.yaml文件,使用ansible-vault的rekey子命令开始重新加密过程,输入现有密码,然后根据提示输入新密码:
ansible-vault rekey greeting-secret_ansible.yaml
Vault password:
New Vault password:
Confirm New Vault password:
Rekey successful
greeting-secret_ansible.yaml文件的内容已更新。
3.5.2 Sealed Secrets 密钥轮换
每次 Sealed Secret 控制器启动时,它会检查是否存在现有的公钥/私钥对(带有以下标签:sealedsecrets.bitnami.com/sealed-secrets-key=active)。如果没有,将生成新的密钥对。密封密钥本身会自动更新(创建新的密钥),每 30 天更新一次,控制器会将带有sealedsecrets.bitnami.com/sealed-secrets-key=active标签的任何密钥视为用于解密的潜在密钥。
然而,无论是因为泄露还是其他原因,都可以在任何时间启动密钥轮换,通过设置--key-cutoff-time标志或在控制器部署中使用SEALED_ SECRETS_KEY_CUTOFF_TIME环境变量。使用任何一种方法,值都必须是 RFC1123 格式。通过在sealed-secrets-controller部署中添加环境变量来执行以下命令,强制 Sealed Secrets 控制器重新生成新的密钥对:
kubectl -n kube-system set env deployment/sealed-secrets-controller \
SEALED_SECRETS_KEY_CUTOFF_TIME="$(date -R)"
将会启动sealed-secrets-controller的新部署。确认已生成新的密钥对:
kubectl -n kube-system get secrets \
-l=sealedsecrets.bitnami.com/sealed-secrets-key=active
NAME TYPE DATA AGE
sealed-secrets-key6kdnd kubernetes.io/tls 2 25m26s
sealed-secrets-keyqdrb5 kubernetes.io/tls 2 47s
控制器有了新的私钥后,可以重新加密与现有SealedSecrets关联的密钥,或者可以使用 kubeseal CLI 加密新的密钥。
注意:每次添加新键时,现有键不会被删除,因为新键被添加到活动键列表中。只有在创建新键之后,才能手动删除旧键。
3.5.3 SOPS 密钥轮换
作为 Helm Secrets 底层密钥管理工具的 SOPS 也支持密钥轮换。在 SOPS 中可以实施轮换有两种机制:
-
GPG 密钥本身
-
加密时使用的数据密钥
最直接的方法是轮换用于加密文件的数据密钥。要使用位于helm_vars文件夹中的现有 Helm 值文件完成此任务,请使用 SOPS 工具本身,传递-r标志以及要轮换的文件位置:
sops -r --in-place helm_vars/values-kubernetes-secrets.yaml
您可以通过验证sops部分下的lastmodified属性来确认 SOPS 已更新文件。
除了轮换数据密钥外,主 GPG 密钥也可以更新。要这样做,创建一个新的 GPG 密钥或引用现有的 GPG,并使用--add-pgp标志传递相关指纹:
sops -r --in-place --add-pgp <FINGERPRINT> \
helm_vars/values-kubernetes-secrets.yaml
要删除旧键,执行此命令,但将--add-pgp替换为--rm-pgp,并使用您希望删除的键的指纹。在任何情况下,请确保更新helm_vars/.sops.yaml文件的内容,以包含 Helm Secrets 用于管理安全资产所需的键的指纹。
无论使用哪种密钥管理工具,一旦完成轮换,重要的是任何依赖于安全资产的系统或应用程序都应适当更新,以减少因配置错误而导致停机或错误的可能性。
摘要
-
以声明性方式表达 Kubernetes 资源允许它们被捕获并存储在版本控制系统中。
-
如 Ansible 和 Helm 等工具支持安全地存储静态的敏感资源。
-
操作符在 Kubernetes 环境中自动化操作,并可用于从集群内部加密敏感值。
-
SOPS 是一种通用工具,用于加密各种文件格式,并包括与 KMS 提供程序的集成。
-
密钥轮换通过生成新的加密密钥来替换现有的加密密钥,从而降低泄露风险。
4 加密静态数据
本章涵盖
-
Kubernetes 集群存储的静态数据加密
-
启用数据加密的 KMS 提供商
在第三章中,你学习了如何在 Git 中存储密钥时保护密钥,但这只是密钥可以存储的一个地方。在本章中,我们将讨论在 Kubernetes 集群内部存储密钥。
我们将通过直接查询 etcd 数据库来证明密钥默认情况下未加密。然后我们将介绍加密静态数据的过程,以及在 Kubernetes 中启用它以加密密钥(图 4.1)。

图 4.1 从纯文本密钥到加密密钥
最后,我们将使用关键管理服务(KMS)来管理加密密钥,以确保过程安全,如图 4.2 所示。

图 4.2 管理密钥的关键管理服务
4.1 在 Kubernetes 中加密密钥
想象一下,你有一个需要连接到数据库服务器的应用程序;显然,需要用户名和密码来访问它。这些配置值是密钥,它们需要正确存储,以便如果系统(或集群)被入侵,它们将保持保密,攻击者将无法利用它们访问应用程序的任何部分。解决方案是加密这些密钥,这样如果它们被入侵,攻击者只能得到一些字节数据,而不是真实值。
4.1.1 静态数据与传输数据
我们在第三章中详细介绍了 静态数据 的特征——持久数据,很少更改——广泛地。另一方面,传输中的数据,或 运动中的数据,是指从一个位置移动到另一个位置的数据,通常通过网络。您可以通过在各方通信期间使用相互 TLS 协议来保护传输中的数据(图 4.3),但这超出了本书的范围;我们将专注于如何保护静态数据。在解决不加密静态数据的问题之前,您需要创建一个纯文本密钥,并通过直接查询 etcd 服务器来获取它,就像攻击者可能做的那样。

图 4.3 传输中的数据需要在通信级别加密
4.1.2 纯文本密钥
让我们使用 kubectl 工具通过使用 etcdctl CLI 查询 etcd 服务器来创建一个密钥。
创建密钥
在终端窗口中,切换到 default Kubernetes 命名空间,并创建一个新的密钥,包含两个键值对:
kubectl config set-context --current --namespace=default
kubectl create secret generic db-secret --from-literal=username=devuser
➥--from-literal=password=devpassword
我们可以使用 kubectl 工具列出创建的密钥:
kubectl get secrets
NAME TYPE DATA AGE
db-secret Opaque 2 47s
安装 etcdctl
etcdctl 是一个用于与 etcd 服务器交互的命令行客户端,用于查询数据库中存储的键,以及其他操作。这个工具可以帮助理解数据是如何存储在 etcd 数据库中的。
工具的安装过程可能因您使用的操作系统而异;有关更详细的说明,请参阅本书中使用的 etcd 版本的官方安装指南(github.com/etcd-io/etcd/releases/tag/v3.4.14)。
访问 etcd
etcd 服务器正在 kube-system 命名空间下运行,端口为 2379。由于您正在使用 minikube,您可以使用端口转发功能直接从您的本地机器访问 etcd 服务器。在终端窗口中,运行以下命令以在 localhost 主机上暴露 etcd:
kubectl port-forward -n kube-system etcd-minikube 2379:2379
重要 如果您运行前面的命令,您将得到以下错误:Error from server (NotFound): pods "etcd-minikube" not found。运行以下命令以获取您环境的名称:
kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
coredns-66bff467f8-mh55d 1/1 Running 1 5m22s
etcd-vault-book 1/1 Running 1 5m36s
kube-apiserver-vault-book 1/1 Running 0 3m21s
kube-controller-manager-vault-book 1/1 Running 1 5m36s
kube-proxy-lbhd6 1/1 Running 1 5m22s
kube-scheduler-vault-book 1/1 Running 1 5m36s
storage-provisioner 1/1 Running 1 5m36s
在此情况下,etcd Pod 的名称是 etcd-vault-book。
访问 etcd 服务器的第二步是将正在运行的 Pod 中的 etcd 证书复制到本地机器。打开一个新的终端窗口,并使用 kubectl 工具复制证书:
kubectl cp kube-system/etcd-minikube:/var/lib/minikube/certs/etcd/peer.key
➥/tmp/peer.key
kubectl cp kube-system/etcd-minikube:/var/lib/minikube/certs/etcd/peer.crt
➥/tmp/peer.crt
重要 etcd-minikube 是一个与 etcd Pod 名称匹配的目录。根据您的环境进行修改。
最后,您可以将 etcdctl 配置为连接到 etcd 服务器并查询之前步骤中创建的密钥。etcd 以键值格式组织其内容;对于密钥对象的特定情况,内容存储在以下格式的键中:
/registry/secrets/<namespace>/<secret_name>.
按照终端窗口中所示执行以下命令。
列表 4.1 配置 etcdctl
export \ ①
ETCDCTL_API=3 \
ETCDCTL_INSECURE_SKIP_TLS_VERIFY=true \
ETCDCTL_CERT=/tmp/peer.crt \
ETCDCTL_KEY=/tmp/peer.key
etcdctl get /registry/secrets/default/db-secret ②
① 使用环境变量配置 etcdctl
② 查询 etcd 数据库以获取密钥。
输出应该类似于以下列表所示。请注意,尽管输出并不完全清晰,但并不难看出密钥内容。
列表 4.2 etcdctl 输出
/registry/secrets/default/db-secret
k8s
v1Secret
N
db-secretdefault"*$df9e87f7-4eed-4f5b-985a-7888919198472 z
password
devpassword ①
usernamedevuserOpaque"
① 密钥未加密,但以纯文本形式存储。
您可以通过终止进程(在第一个终端上按 Ctrl-C)来停止端口转发,因为您目前不需要它。
您现在应该明白,如果 etcd 服务器被入侵,将没有任何东西阻止攻击者以纯文本形式获取所有密钥。在下一节中,您将探索存储加密密钥的第一个解决方案。
4.1.3 加密密钥
要使用静态加密数据,您需要引入一个新的 Kubernetes 对象,名为 EncryptionConfiguration。在这个对象中,您将指定您想要加密的 Kubernetes 对象;它可以是密钥对象,但实际上可以加密任何其他 Kubernetes 对象。您还需要指定密钥提供者,这是一个可插拔的系统,您在其中指定要使用的加密算法和加密密钥。
在撰写本文时,以下提供者是受支持的:
-
身份—未启用加密;资源按原样写入。
-
aescbc—使用 PKCS#7 填充算法的 AES-CBC;这是静态加密的最佳选项。
-
secretbox—使用 XSalsa20 和 Poly1305 算法;这是一个新标准,但在审查级别高的环境中可能不适用。
-
aesgcm—使用随机 nonce 算法的 AES-GCM;仅在您实现自动密钥轮换时推荐。
-
kms—使用信封加密模式;密钥加密密钥由配置的 KMS 管理。这是最安全的方法,您将在本章后面探索它。
要检查 Kubernetes 中静态加密数据的工作方式,重复上一节中的练习,但这次将 Kubernetes 配置为加密机密。
启用静态数据加密
重要的是要理解,静态数据加密发生在运行在主节点上的 kube-apiserver 上。如果启用,每次将 Kubernetes 对象发送到 Kubernetes 集群时,kube-apiserver 都会将对象加密的任务委托给加密配置部分,在将对象发送到 etcd 数据库存储之前进行加密。显然,当需要使用机密时,它会自动解密,因此从开发者的角度来看,不需要做任何特殊操作;他们像往常一样工作(图 4.4)。

图 4.4 加密层在将机密发送到 etcd 之前自动加密。
现在生成一个 EncryptionConfiguration 对象,以确保任何 Kubernetes 机密都使用 aescbc 算法和随机加密密钥进行加密,如下面的列表所示。
列表 4.3 EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets ①
providers:
- aescbc:
keys:
- name: key1
secret: b6sjdRWAPhtacXo8mO1cfgVYWXzwuls3T3NQOo4TBhk= ②
- identity: {}
① 机密内容被加密,但任何 Kubernetes 资源都是有效的。
② 加密数据使用的加密密钥
提示:要生成 Base64 的随机密钥,您可以使用 openssl 或 head 等工具:
openssl rand -base64 32
head -c 32 /dev/urandom | base64 -i -
如您所记,加密发生在 kube-apiserver 进程中,这意味着您需要将加密配置文件实体化到运行它的主节点上。访问主节点的过程因每个 Kubernetes 平台而异;在 minikube 中,通过运行 minikube ssh 命令来获得主节点上的 SSH 会话。一旦进入主节点,运行 sudo -i 命令以超级用户身份执行以下命令。
列表 4.4 SSH’d minikube
minikube ssh
sudo -i
提示:如果您遇到类似 Error getting config 的错误,则需要使用 -p 标志指定 minikube 配置文件。您可以使用 minikube profile list 列出当前活动配置文件:
minikube profile list
|--------|-----------|---------|----------------|------|---------|---------|
|Profile | VM Driver | Runtime | IP | Port | Version | Status |
|--------|-----------|---------|----------------|------|---------|---------|
|istio | virtualbox| docker | 192.168.99.116 | 8443 | v1.18.6 | Stopped |
|kube | virtualbox| docker | 192.168.99.117 | 8443 | v1.18.6 | Started |
|--------|-----------|---------|----------------|------|---------|---------|
minikube ssh -p kube.
然后在 SSH 实例内部运行 sudo 命令。
列表 4.5 更新到超级用户
sudo -i
到目前为止,您已进入 minikube VM,其中 kube-apiserver 正在运行。让我们在 /var/lib/minikube/certs/encryptionconfig.yaml 创建一个新文件,其内容如下面的列表所示。
列表 4.6 encryptionconfig.yaml
echo "
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc: ①
keys:
- name: key1
secret: b6sjdRWAPhtacXo8mO1cfgVYWXzwuls3T3NQOo4TBhk=
- identity: {}
" | tee /var/lib/minikube/certs/encryptionconfig.yaml ②
① 加密提供者
② tee 命令为指定的文件创建回显内容。
文件在主节点上创建。现在我们可以通过输入 exit 两次来退出 SSH 终端。
exit
exit
现在你已经回到你的电脑上,但你在密钥加密之前仍需要完成最后一步:更新 kube-apiserver 以获取上一步骤中创建的 EncryptionConfiguration 文件。要配置 kube-apiserver 进程,你需要将 --encryption-provider-config 参数值设置为 EncryptionConfiguration 路径。在 minikube 中这样做最简单的方法是停止实例,然后使用 --extra-config 参数重新启动它。
提示:如果你没有使用 minikube,以下链接提供了有关在 Kubernetes kube-apiserver 中设置此配置属性的信息:kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/。
minikube stop
minikube start --vm-driver=virtualbox
➥--extra-config=apiserver.encryption-provider-config=/var/lib/minikube/
➥certs/encryptionconfig.yaml
4.1.4 创建密钥
在一个终端窗口中,切换到 default Kubernetes 命名空间,并创建一个名为 db-secret-encrypted 的新密钥,包含两个键值对。
kubectl config set-context --current --namespace=default
kubectl create secret generic db-secret-encrypted
➥--from-literal=username=devuser --from-literal=password=devpassword
到目前为止,密钥的创建方式与之前相同,但让我们探讨一下数据如何在 etcd 内部存储。
访问 etcd
让我们在访问 etcd 的部分中重复执行完全相同的步骤,以获取 db-secret-encrypted 密钥的内容,并验证现在它是加密的而不是明文。
在一个终端窗口中,在本地主机上公开 etcd 服务器:
kubectl port-forward -n kube-system etcd-minikube 2379:2379
在另一个终端中,重复复制 etcd 证书的过程,并使用环境变量配置 etcdctl:
kubectl cp kube-system/etcd-minikube:/var/lib/minikube/certs/etcd/peer.key
➥/tmp/peer.key
kubectl cp kube-system/etcd-minikube:/var/lib/minikube/certs/etcd/peer.crt
➥/tmp/peer.crt
export \
ETCDCTL_API=3 \
ETCDCTL_INSECURE_SKIP_TLS_VERIFY=true \
ETCDCTL_CERT=/tmp/peer.crt \
ETCDCTL_KEY=/tmp/peer.key
现在,你可以查询 etcd 以获取 db-secret-encrypted 键的值,以验证它是否已加密,其值是否无法解密:
etcdctl get /registry/secrets/default/db-secret-encrypted
输出应类似于以下内容:
/registry/secrets/default/db-secret-encrypted
cm 9> * - ~ 6I =@ e .
8Y
t p b V w 6 ̒ l v Ey q. ^ Z
➥ n xh $ d 1 y Q q LJ } I w %;
与上一节不同,密钥在 kube-apiserver 中加密,然后发送到 etcd 服务器进行存储。你可以通过终止进程(在第一个终端中按 Ctrl-C)来停止端口转发,因为你目前不需要这样做。
现在,你可以看到攻击者可以访问 etcd 服务器或 etcd 的备份(请防止这种情况发生),但密钥是使用 EncryptionConfiguration 对象中配置的密钥加密的。但这足够了吗?
密钥的安全性
使用 EncryptionConfiguration,你增加了攻击者需要克服的障碍才能访问明文密钥,但你仍然有一些弱点。这些弱点源于加密密钥在主节点文件系统中以明文形式存储。如果攻击者能够访问主机器,他们可以访问 EncryptionConfiguration 文件,获取密钥,查询加密密钥,并使用从主节点窃取的密钥解密它们。
这里主要的问题之一是加密数据的密钥与数据本身一起存储(图 4.5)。任何可以访问你的主节点的攻击者都可能访问密钥和解密密钥。换句话说,如果主节点被入侵,你并没有在很大程度上提高安全性。

图 4.5 一个被攻陷的集群意味着数据和用于解密数据的密钥都暴露了。
为了避免这种向量攻击,加密密钥和数据应该存储在不同的机器上。这样,攻击者需要攻破多个系统才能获取到秘密,如图 4.6 所示。

图 4.6 将数据和密钥分割到不同的机器上使系统更安全。
除了这个问题之外,在采用之前的方法时,还需要考虑其他缺点:
-
密钥需要使用外部工具手动生成。
-
密钥管理过程是手动完成的。
-
密钥轮换是一个手动过程,需要更新 EncryptionConfiguration 文件,这将对
kube-apiserver进程的重新启动产生影响。
显然,通过加密秘密,你已经改进了你的安全模型,这可能已经足够,取决于你的用例和预期的安全级别,但仍有改进的空间。在下一节中,我们将深入了解如何在 Kubernetes 中使用 KMS 来在不同机器上存储加密密钥和加密数据。
4.2 密钥管理服务器
之前的应用程序秘密被加密了,但用于加密它们的密钥没有得到保护。任何未授权的访问都会导致它们丢失,并给攻击者解密应用程序秘密的机会。现在你将改进之前的应用程序,在使用 Kubernetes 时保护这些密钥。
为了提高用于加密数据的密钥的安全性,你需要将 KMS 部署在 Kubernetes 集群外部。这样,密钥在集群外部管理,而秘密存储在集群内部(etcd)。
这种新的方法使得可能的攻击者难以获取你的秘密,因为必须攻破两个系统。首先,攻击者需要访问etcd或磁盘备份来获取秘密,假设他们获取到了,这些秘密将只是一堆加密的字节。接下来,攻击者需要获取解密这些密钥,但与前一部分不同的是,现在密钥不在同一台机器上,也不是以明文形式存储。需要攻破第二个系统,因为攻击者需要访问 KMS 并获取用于加密时解密秘密的密钥。当然,这仍然是可能的,但你已经增加了一层需要被破坏的保护。
为了保持你的秘密得到保护,并对可能的攻击具有弹性,你需要将密钥和加密数据存储在同一台机器上的做法改为明确区分密钥和加密数据存储的位置。KMS 是一个服务器,它集中管理加密密钥,并为处理传输中的数据提供一些加密操作的能力。这使得它成为将密钥和数据存储完全分离的完美工具。

图 4.7 用于加密和解密的密钥由 KMS 管理。
4.2.1 Kubernetes 和 KMS 提供商
在启用静态加密数据部分,您已经看到 Kubernetes 可以使用静态加密来加密秘密,并支持多种数据加密提供商——其中之一就是 KMS 提供商。当使用外部 KMS 时,建议使用此提供商。
KMS 加密提供者的重要特性是它使用信封加密方案来加密所有数据。了解这个方案的确切工作原理以及为什么它被用于在etcd中存储数据非常重要。
信封加密
要使用信封加密方案,你需要三份数据:要加密的数据(秘密)、数据加密密钥(DEK)和密钥加密密钥(KEK)。每次需要加密新数据时,都会生成一个新的数据加密密钥(DEK)并用于加密数据。如您所见,每份数据(或秘密)都由一个新的加密密钥(DEK)加密,并且是即时创建的。
除了数据加密密钥之外,信封加密方案还有一个密钥加密密钥。这个密钥用于加密数据加密密钥(DEK)。相比之下,KEK 只生成一次,并存储在第三方系统,如 KMS 中。
此时,有两个加密的字节数据块:使用 DEK 加密的数据和用 KEK 加密的 DEK。图 4.8 显示了这两个数据块以及它们是如何加密的。

图 4.8 数据使用 DEK 加密,DEK 使用 KEK 加密。
这两部分被拼接在一起,并作为单一数据块存储在一起。这种方法的一个重大优点是每个数据都有自己的加密密钥,这意味着如果数据加密密钥被泄露(例如,通过暴力破解),攻击者只能解密那个秘密,而不能解密其他秘密。图 4.9 显示了整个过程。

图 4.9 信封加密方案
要解密一个秘密,使用相反的过程。首先,将数据再次分成两块数据(加密的秘密和加密的 DEK);使用 KEK 解密 DEK,最后使用解密后的 DEK 解密秘密,如图 4.10 所示。

图 4.10 解密信封加密方案
Kubernetes 和信封加密
Kubernetes KMS 加密提供者使用以下方式使用信封加密:每次需要加密数据时,都会使用 AES-CBC 与 PKCS7#填充算法生成一个新的数据加密密钥(DEK)。然后使用由远程 KMS 管理的密钥加密密钥(KEK)加密 DEK。图 4.11 显示了此过程。

图 4.11 Kubernetes 和信封加密方案
HashiCorp Vault 作为 KMS
使用 KMS 提供商是加密和解密秘密最安全的方式,但您需要一个管理密钥加密密钥并加密数据加密密钥的 KMS 实现。但 Kubernetes 集群如何与远程 KMS 通信呢?为了与远程服务器通信,KMS 提供商使用 gRPC 协议与部署在 Kubernetes 主节点上的 Kubernetes KMS 插件进行通信。此插件充当kube-apiserver和 KMS 之间的桥梁,将kube-apiserver的加密和解密流程适配到远程 KMS 所需的协议。图 4.12 显示了此协议。

图 4.12 KMS 提供程序/插件系统
已经有多个 Kubernetes KMS 插件得到原生支持,包括 IBM Key Protect、SmartKey、AWS KMS、Azure Key Vault、Google Cloud KMS 和 HashiCorp Vault 等。由于本书旨在编写时对云服务提供商保持中立,您将使用 HashiCorp Vault 作为远程 KMS,但请记住,这个过程将与这里解释的任何其他 KMS 实现的过程类似。
目前,不必担心 HashiCorp Vault 是什么,因为我们在下一章将深入探讨这个问题,因为它提供了许多针对特定秘密的关键特性。但就这一特定章节而言,将 HashiCorp Vault 视为一种可部署的服务,它提供了一个端点来加密和解密传输中的数据而不存储它。所有密钥管理都在 Vault 服务内部进行,因此从用户的角度来看,数据被发送到服务,并根据用例返回加密或解密的数据。在下一节中,我们将开始将密钥从主节点移动到外部 KMS。
小贴士:KMS 插件系统被设计为可扩展的,因此您可以针对特定的 KMS 实现实现一个新的插件。通常,这不应该需要,因为大多数 KMS 提供商都提供与 Kubernetes 的集成,但请记住,没有任何东西阻止您自己实现 Kubernetes KMS 插件。
安装 HashiCorp Vault
我们反复强调,秘密和加密密钥应该部署在不同的机器上,这意味着 KMS 必须独立于 Kubernetes 主节点运行。
在实际场景中,您应该始终以这种方式进行操作,但在这个学术用例中,我们使用在 VM 内部运行的 minikube,为了简化,您将在 VM 内部但不在 Kubernetes 集群外部安装 HashiCorp Vault。图 4.13 说明了配置。

图 4.13 HashiCorp Vault 作为 KMS
重要提示:在本节末尾,我们将提供一个命令,该命令执行一个脚本,自动化以下各节中解释的所有步骤。尽管这个过程是自动化的,但我们将解释整个过程,以便您可以在任何其他环境中重复它。
要安装 HashiCorp Vault,您需要将其下载并安装到虚拟机中,并在 systemd 中将其注册为服务,以便每次启动虚拟机时自动启动。以下列出步骤。
列表 4.7 安装脚本
curl -sfLo vault.zip https://releases.hashicorp.com/vault/1.1.2/
➥vault_1.1.2_linux_amd64.zip ①
unzip vault.zip
sudo mv vault /usr/bin/
sudo chmod +x /usr/bin/vault ②
cat <<EOF | sudo tee /etc/profile.d/vault.sh
export VAULT_ADDR=http://127.0.0.1:8200
EOF
source /etc/profile.d/vault.sh ③
sudo addgroup vault
sudo adduser -G vault -S -s /bin/false -D vault ④
sudo mkdir -p /etc/vault/{config,data}
cat <<EOF | sudo tee /etc/vault/config/config.hcl
disable_mlock = "true"
backend "file" {
path = "/etc/vault/data"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = "true"
}
EOF
sudo chown -R vault:vault /etc/vault ⑤
cat <<"EOF" | sudo tee /etc/systemd/system/vault.service
[Unit]
Description="HashiCorp Vault - A tool for managing secrets"
Documentation=https://www.vaultproject.io/docs/
Requires=network-online.target
After=network-online.target
[Service]
User=vault
Group=vault
ExecStart=/usr/bin/vault server -config=/etc/vault/config
ExecReload=/bin/kill --signal HUP $MAINPID
ExecStartPost=-/bin/sh -c "/bin/sleep 5 && /bin/vault operator unseal
➥-address=http://127.0.0.1:8200 $(/bin/cat /etc/vault/init.json |
➥/bin/jq -r .unseal_keys_hex[0])"
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
StartLimitBurst=3
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl start vault
① 下载并安装 Vault。
② 设置配置文件。
③ 创建一个运行 Vault 的用户。
④ 创建 Vault 配置。
⑤ 设置 systemd 以启动 Vault。
警告:在此示例中,监听地址已设置为 0.0.0.0,因此任何主机都可以访问 Vault 服务器。这对于非生产环境或教育目的来说是可行的,但在实际环境中,您应该适当地进行配置。
配置传输密钥引擎
Vault 需要解密,以便可以从外部访问,并且传输密钥引擎将被启用,这样 Vault 就可以用于加密和解密传输中的数据。如果您仍然不理解为什么需要这些步骤,请不要担心,我们将在下一章中更详细地介绍它们。
对于这个特定的示例,您将配置 Vault,使其可以通过提供 vault-kms-k8s-plugin-token 值作为令牌的用户访问,并创建一个名为 my-key 的加密密钥:
vault operator init -format=json -key-shares=1 -key-threshold=1 |
➥sudo tee /etc/vault/init.json ①
vault operator unseal "$(cat /etc/vault/init.json |
➥jq -r .unseal_keys_hex[0])" ②
vault login "$(cat /etc/vault/init.json | jq -r .root_token)" ③
vault token create -id=vault-kms-k8s-plugin-token ④
vault secrets enable transit ⑤
vault write -f transit/keys/my-key
① 初始化并解密 Vault。
② 使用根令牌登录 Vault。
③ 启用传输密钥引擎。
④ 创建一个特殊的令牌以访问传输密钥引擎。
⑤ 创建加密密钥。
安装 Vault KMS 提供程序
Vault 启动并运行后,您需要安装和设置 Vault KMS 提供程序/插件。对于 KMS 提供程序,有四个重要的配置项:
-
加密密钥名称(
my-key) -
Vault 服务器运行地址(
127.0.0.1) -
访问 Vault 所需的令牌(
vault-kms-k8s-plugin-token) -
Vault KMS 提供程序的套接字文件(
/var/lib/minikube/certs/vault-k8s-kms-plugin.sock)
记住,KMS 提供程序是一个 gRPC 服务器,它充当 kube-apiserver 和 KMS 之间的桥梁。以下列出步骤。
列表 4.8 安装 kms vault 脚本
curl -sfLo vault-k8s-kms-plugin https://github.com/lordofthejars/ ①
➥kubernetes-vault-kms-plugin/releases/download/book/
➥vault-k8s-kms-plugin-amd64
unzip vault-k8s-kms-plugin.zip
sudo mv vault-k8s-kms-plugin /bin/vault-k8s-kms-plugin
sudo chmod +x /bin/vault-k8s-kms-plugin
sudo mkdir -p /etc/vault-k8s-kms-plugin ②
cat <<EOF | sudo tee /etc/vault-k8s-kms-plugin/config.yaml
keyNames:
- my-key
transitPath: /transit
addr: http://127.0.0.1:8200
token: vault-kms-k8s-plugin-token
EOF
sudo chown -R vault:vault /etc/vault-k8s-kms-plugin ③
cat <<EOF | sudo tee /etc/systemd/system/vault-k8s-kms-plugin.service
[Unit]
Description="KMS transit plugin"
Requires=vault.service
After=vault.service
[Service]
User=root
Group=root ④
ExecStart=/usr/bin/vault-k8s-kms-plugin -socketFile=/var/lib/minikube/
➥certs/vault-k8s-kms-plugin.sock -vaultConfig=/etc/vault-k8s-kms-plugin/
➥config.yaml
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
StartLimitBurst=3
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl start vault-k8s-kms-plugin
① 下载 Vault KMS 插件。
② 将插件配置为我们在上一节中在 Vault 中配置的参数。
③ 设置 systemd 以启动 Vault KMS 插件。
④ 设置 Vault KMS 插件启动的套接字文件。
配置 Kubernetes KMS 提供程序
在之前的示例中,为了启用静态加密数据,您创建了一个 EncryptionConfiguration 文件以在 Kubernetes 集群中启用静态加密数据。现在您需要创建一个 EncryptionConfiguration 文件来配置 Vault KMS 提供程序而不是 aescbc 提供程序。图 4.14 展示了 Kubernetes-api 服务器如何与 Kubernetes KMS 插件交互。

图 4.14 HashiCorp Vault 作为 KMS
需要设置的重要参数是 endpoint,因为那是提供者/插件进行通信的位置。在本例中,它已在之前的步骤中配置,使用 socketFile 参数设置为 /var/lib/minikube/certs/vault-k8s-kms-plugin.sock。
cat <<EOF | sudo tee /var/lib/minikube/certs/encryption-config.yaml
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- secrets
providers:
- kms: ①
name: vault
endpoint: unix:///var/lib/minikube/certs/vault-k8s-kms-plugin.sock ②
cachesize: 100
- identity: {}
EOF
① 使用 KMS 作为提供者。
② 设置之前步骤中指定的 socketFile。
图 4.15 显示了当使用 KMS 时和使用 KMS 时不使用时的 EncryptionConfiguration 文件之间的差异。

图 4.15 加密配置与 KMS 加密配置的比较
重新启动 kube-apiserver
最后一步是重新启动 kube-apiserver,以便新配置生效,并使用 Vault 作为远程 KMS 进行信封加密。在过去,您需要重新启动整个 minikube 实例,但在此情况下,您将通过仅重新启动 kubelet 进程来使用不同的方法。
sudo sed -i '/- kube-apiserver/ a \ \ \ \ - --encryption-provider-config=/ ①
➥var/lib/minikube/certs/encryption-config.yaml' /etc/kubernetes/
➥manifests/kube-apiserver.yaml ②
sudo systemctl daemon-reload
sudo systemctl stop kubelet
docker stop $(docker ps -aq)
sudo systemctl start kubelet
① kube-apiserver 已配置为使用创建以使用 KMS 的 EncryptionConfiguration 文件
② kubelet 已重新启动。
将一切整合
如前所述,提供了一个脚本以自动执行所有之前的步骤。此外,我们建议您现在使用一个新的 minikube 实例,以便您有一个干净且在 VM 内安装了 Vault 的 minikube 实例。
在 vault 配置文件下创建一个 minikube 实例,SSH 到运行 Kubernetes 集群的 VM,并运行执行所有之前解释步骤的脚本。执行以下命令:
minikube stop ①
minikube start -p vault --memory=8192 --vm-driver=virtualbox ②
➥--kubernetes-version='v1.18.6'
minikube ssh "$(curl https://raw.githubusercontent.com/lordofthejars/ ③
➥vault-kubernetes-tutorial/master/scripts/install_vault_kms.sh -s)"
➥-p vault
① 停止之前的 minikube 实例。
② 在 vault 配置文件下启动一个新的 minikube 实例。
③ 执行脚本以配置 KMS 提供者。
4.2.2 创建机密
在一个终端窗口中,创建一个名为 kms-db-secret-encrypted 的新机密,包含两个键值条目:
kubectl create secret generic kms-db-secret-encrypted
➥--from-literal=username=devuser --from-literal=password=devpassword
在此阶段,机密的创建方式与之前相同,但机密使用信封加密方案加密。让我们探索数据如何在 etcd 内部存储。
访问 etcd
重复您在访问 etcd 以获取 kms-db-secret-encrypted 机密内容时遵循的完全相同的流程,并验证它已加密存储而不是以纯文本形式存储。在一个终端窗口中,在本地主机上公开 etcd 服务器:
kubectl port-forward -n kube-system etcd-vault 2379:2379
在另一个终端中,重复复制 etcd 证书的流程,并使用环境变量配置 etcdctl:
kubectl cp kube-system/etcd-vault:/var/lib/minikube/certs/etcd/peer.key
➥/tmp/peer.key
kubectl cp kube-system/etcd-vault:/var/lib/minikube/certs/etcd/peer.crt
➥/tmp/peer.crt
export \
ETCDCTL_API=3 \
ETCDCTL_INSECURE_SKIP_TLS_VERIFY=true \
ETCDCTL_CERT=/tmp/peer.crt \
ETCDCTL_KEY=/tmp/peer.key
您现在可以查询 etcd 以获取 kms-db-secret-encrypted 键的值,以验证它已加密且其值无法解密。
etcdctl get /registry/secrets/default/kms-db-secret-encrypted
输出应类似于以下列表。
列表 4.9 加密 KMS 机密
/registry/secrets/default/kms-db-secret-encrypted
cm 9> * - ~ 6I =@ e .
8Y
t p b V w 6 ̒ l v Ey q. ^ Z
➥ n xh $ d 1 y Q q LJј} I w %;
与上一节不同,机密在 kube-apiserver 中使用信封加密方案加密,然后发送到 etcd 服务器存储。您可以通过终止进程(在第一个终端上按 Ctrl-C)来停止端口转发,因为您目前不需要这样做。您还可以停止当前的 minikube 实例并启动默认实例,该实例仅包含一个运行的 Kubernetes 实例:
minikube stop -p vault
minikube start
摘要
-
默认情况下,
etcd中不加密机密;因此,您需要找到一种方法来加密它们,以防止任何访问etcd的攻击者读取它们。 -
EncryptionConfigurationKubernetes 对象在配置 Kubernetes 加密资源(机密)方面至关重要,但如果未使用远程 KMS,则加密数据和加密密钥都存储在同一台机器上。 -
为了允许数据和密钥存储在不同的机器上,Kubernetes 支持使用远程 KMS。
5 HashiCorp Vault 和 Kubernetes
本章涵盖
-
启用 HashiCorp Vault 以供部署到 Kubernetes 的最终用户应用程序使用
-
集成 Kubernetes 认证以简化对 Vault 资源的访问
-
通过部署到 Kubernetes 的应用程序访问存储在 HashiCorp Vault 中的秘密
第四章介绍了 HashiCorp Vault 作为一种 KMS,可用于为存储在 etcd(Kubernetes 的键/值存储)中的秘密和其他资源提供加密,因此这些值不能轻易访问,因为它们是静态存储的。
本章重点介绍了使用像 HashiCorp Vault 这样的秘密管理工具的重要性,以安全地存储和管理部署到 Kubernetes 的应用程序的敏感资产,并展示了如何配置应用程序和 Vault 以实现彼此的无缝集成。通过使用 Vault 这样的工具,应用团队可以将管理敏感资源的一些责任转移到专门构建的工具上,同时仍然能够与他们的应用程序集成。
5.1 使用 HashiCorp Vault 管理应用程序秘密
如您迄今为止所看到的,敏感资产可以被 Kubernetes 的核心基础设施组件或部署到平台上的应用程序使用。第四章主要关注基础设施部分,以及如何通过使用 HashiCorp Vault 加密值来正确保护 Kubernetes 的主要数据库 etcd。虽然 Vault 可以帮助保持平台的安全,但它更常用于存储和保护用于应用程序的属性。
当像第四章中描述的那样在 etcd 中保护存储的值时,Vault 被用作中介来执行使用 Vault 的传输秘密加密和解密数据所需的加密功能。对于部署到 Kubernetes 的应用程序,它们自身并不是设计为作为秘密存储的,它们会寻找另一个更适合此目的的工具。这就是 Vault 可以作为解决方案的地方。
假设您想使用 Vault 存储全球各地部署的字节代理的秘密位置。虽然在这种情况下可以使用数据库,但我们将演示如何使用部署到 Kubernetes 的 Vault 实例以安全的方式存储任意数据,并在部署到 Kubernetes 的应用程序中访问这些值(图 5.1)。

图 5.1 Vault 中的秘密存储以及请求访问以检索存储值的应用程序
5.1.1 在 Kubernetes 上部署 Vault
在第四章中,您在 minikube VM 中安装了 HashiCorp Vault,以在 Kubernetes 基础设施组件(尤其是 etcd)和 KMS 之间提供清晰的分离。由于 etcd 对 Kubernetes 集群的功能至关重要,您应确保它们之间没有相互依赖,以确保正常操作(即所谓的“先有鸡还是先有蛋”的困境)。
由于应用程序和 KMS(反之亦然)之间的硬依赖性较小,并且为了能够利用 Kubernetes 本身的大多数好处(例如,调度和应用程序健康监控),Vault 将部署到 Kubernetes 以充当应用程序的 KMS。您可以使用几种方法将 Vault 部署到 Kubernetes,但最直接的方法是使用 Helm。从 HashiCorp 可用的图表(github.com/hashicorp/vault-helm)支持大多数部署选项,无论是用于开发实例还是支持生产就绪集群。
要开始使用,请从任何目录使用终端首先将 Hashicorp 仓库添加到 Helm,其中包含 Vault 图表:
helm repo add hashicorp https://helm.releases.hashicorp.com
"hashicorp" has been added to your
然后从远程仓库检索最新更新,这将把内容拉到您的机器上:
helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "hashicorp" chart repository
Update Complete. ⎈ Happy Helming!⎈
Vault 使用集成 RaftHA(高可用性)存储进行高可用性存储
每次部署 Vault 时,下一个考虑因素是您将如何处理其元数据的存储。当 Vault 在 Kubernetes 之外的 minikube VM 中部署时,使用了文件系统存储类型。此配置将持久化到本地文件系统,并且是让 Vault 运行起来的简单方法。然而,当使用生产式部署时,文件系统存储类型是不理想的,因为它只支持单个实例。
在 Kubernetes 上部署 Vault 的另一个好处,除了上述好处以及与更生产式部署的愿望相一致之外,是能够轻松实现高可用性(HA)。然而,要超越单个实例,需要使用除文件系统之外的不同存储类型。虽然 Vault 支持多种外部存储后端,从关系型数据库和 NoSQL 数据库到云对象存储,但一个简单且高可用且没有外部依赖的选项是使用 Raft 的集成存储(图 5.2)。

图 5.2 领导者选举和 Raft 存储的数据复制
Raft 是一种分布式 一致性算法,在发生选举后,多个成员形成一个集群。根据选举结果确定领导者,当他们在确定集群的共享状态并将状态复制到每个跟随者方面承担责任时。任何集群的最小数量是 3(N/2+1,其中 N 是总节点数)以确保在发生故障时有最小数量的节点。Raft 是一种常见协议,当它在高可用配置下运行时,被云原生空间中的其他解决方案使用,包括 etcd。
在了解支持 Vault 高可用性部署所需的存储需求后,通过设置 server.ha.enabled=true 和 server.ha.raft.enabled=true 的值来安装 Vault Helm 图表,这将启用高可用性并启用 Raft。此外,由于您正在尝试将高可用性部署部署到单个 minikube 节点,请将 server.affinity 的值设置为 "";这将跳过默认的 Pod 亲和性配置,该配置会尝试将每个 Vault Pod 调度到不同的节点,从而导致调度失败。执行以下列表中的命令来安装图表。
列表 5.1 使用 Helm 部署 vault
helm upgrade -i vault hashicorp/vault \ ①
--set='server.ha.enabled=true' \ ②
--set='server.ha.raft.enabled=true' \ ③
--set='server.affinity=""' -n vault \ ④
--create-namespace ⑤
① 指定发布名称和要安装的图表名称。
② 在高可用性模式下启用 Vault。
③ 在高可用性模式下启用 Raft。
④ 禁用 Pod 亲和性。
⑤ 如果尚不存在,创建一个名为 vault 的命名空间。
确认已将三个 Vault 实例部署到 vault 命名空间:
kubectl get pods -n vault
NAME READY STATUS RESTARTS AGE
vault-0 0/1 Running 0 76s
vault-1 0/1 Running 0 76s
vault-2 0/1 Running 0 76s
vault-agent-injector-76d54d8b45-dvzww 1/1 Running 0 76s
除了预期的三个 Vault 实例外,还有一个以 vault-agent-injector 为前缀的 Pod 也已部署,其目的是动态地将机密注入到 Pod 中。Vault Agent Injector 将在本章的后面详细讨论。如果 Vault 或 Vault Injector Pod 中任何一个当前不在 Running 状态,或者没有任何 Pod 出现,请使用 kubectl get events -n vault 命令来调查此类问题的原因。
Vault 部署后,第一步是初始化 Vault。由于存在多个 Vault 实例,其中一个实例应被指定为初始领导者——在本例中为 vault-0。
首先检查 Vault 的状态,它应该表明尚未初始化:
kubectl -n vault exec -it vault-0 -- vault operator init -status
Vault is not initialized
现在初始化 Vault,以便您可以开始与之交互,操作如下。
列表 5.2 初始化 vault
kubectl -n vault exec -it vault-0 -- vault operator init ①
① 输出将显示可用于解密 Vault 的每个密钥以及初始根令牌。
当 Vault 初始化时,它将被置于 密封 模式,这意味着它知道如何访问存储层,但不能解密任何内容。当 Vault 处于密封状态时,它类似于银行保险库,资产安全,但无法进行任何操作。要能够与 Vault 交互,必须将其解密。
解封是获取访问主密钥的过程。然而,这个密钥只是 Vault 中数据加密的一部分。当数据存储在 Vault 中时,它使用一个加密密钥进行加密。这个密钥与数据一起存储在 Vault 中,并使用另一个密钥加密,称为主密钥。然而,这还没有结束。主密钥也存储在 Vault 中的数据中,并再次使用解封密钥进行加密。解封密钥随后使用称为 Shamir 的密钥共享算法分布到多个碎片中。为了重建封印密钥,必须提供一定数量的碎片,这将允许访问组合密钥,最终访问 Vault 中存储的数据(图 5.3)。

图 5.3 解封 Vault 的步骤
当 Vault 初始化时,提供了五个解封密钥,代表将用于构建组合密钥的碎片。默认情况下,需要三个密钥来重建组合密钥。开始为vault-0实例解封 Vault 的过程。
执行以下命令以开始解封过程。当提示时,输入在以下列表中执行的vault operator init命令旁边Unseal Key 1的值。
kubectl -n vault exec -it vault-0 -- vault operator unseal
Unseal Key (will be hidden):
列表 5.3 解封 Vault 的进度
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed true
Total Shares 5
Threshold 3
Unseal Progress 1/3 ①
Unseal Nonce fbb3714f-27cb-e362-ba40-03db093aea23
Version 1.7.2
Storage Type raft
HA Enabled true
① 解封进度
解封过程正在进行中,如列表 5.3 中所示的Unseal Progress行表明已输入了一个密钥。执行相同的kubectl -n vault exec -it vault-0—vault operator unseal命令两次,当提示时提供Unseal Key 2、Unseal Key 3等旁边的值。
继续提供密钥,直到您看到以下列表中的输出类似结果。
列表 5.4 一个解封的 Vault 实例
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false ①
Total Shares 5
Threshold 3
Version 1.7.2
Storage Type raft
Cluster Name vault-cluster-c5e28066 ②
Cluster ID 0a3170f4-b486-7358-cba7-381349056f3e
HA Enabled true ③
HA Cluster n/a
HA Mode standby
Active Node Address <none>
Raft Committed Index 24
① 封存状态
② 集群名称
③ 高可用模式状态
vault-0实例现在已解封。然而,由于 Vault 正在运行在 HA 模式,其他两个成员必须加入新创建的集群并经历解封过程。
首先将vault-1实例加入 Raft 集群,如下所示。
列表 5.5 将新节点加入 Raft 集群
kubectl -n vault exec -ti vault-1 -- vault operator \
raft join http://vault-0.vault-internal:8200 ①
Key Value
--- -----
Joined true
① Raft 领导者的地址
接下来执行以下命令三次,提供不同的解封密钥,就像在vault-0中完成的那样:
kubectl -n vault exec -it vault-1 -- vault operator unseal
Unseal Key (will be hidden):
在vault-1实例上完成这些步骤后,在vault-2实例上执行vault operator join和vault operator unseal命令。
要确认高可用 Vault 集群已就绪,请使用vault operator init命令提供的Initial Root Token登录到vault-0实例,如下所示:
kubectl -n vault exec -it vault-0 -- vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.cm8HyaIxR2MPseDxTvOU7ugD
token_accessor Fx23YMxqiYabU8u5Ptt2qpcH
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
现在您已使用 root 令牌登录,通过列出所有 Raft 成员来确认所有成员是否已成功加入 Vault 集群:
kubectl -n vault exec -ti vault-0 -- vault operator raft list-peers
Node Address State Voter
---- ------- ----- -----
8b58cb62-7da7-5e8d-298b-95d4e8203ea5 vault-0.vault-internal:8201 leader true
4cef7124-d40a-bba8-4dac-4830936ceea3 vault-1.vault-internal:8201 follower true
d1a8d5fd-82be-feb7-a3e7-ca298d81d050 vault-2.vault-internal:8201 follower true
注意 vault-0 被列为 leader,其他两个成员是 followers,如 State 列所示。如果出现三个 Raft 实例,请确认每个成员在列表 5.5 中都成功加入。此时,高可用性的 Vault 已部署到 minikube 实例。
密钥管理引擎
我们之前讨论过可以作为 Vault 信息持久化存储使用的存储后端类型。Vault 内部数据的实际存储、生成和加密由支持的密钥管理引擎之一提供便利。密钥管理引擎可以执行简单的操作,如存储或读取数据。然而,更复杂的密钥管理引擎可能会调用外部资源按需生成资产。第四章中描述的传输密钥管理引擎有助于加密和解密存储在 etcd 中的值,但对于需要从 Vault 中检索值的程序,例如本章中你打算实现的程序,可以使用键/值或 kv 密钥管理引擎。
kv 引擎确实如其名称所示:它允许在 Vault 中存储键值对。这个密钥管理引擎随着时间的推移而发展,截至本文撰写时,有两个版本可以使用。
-
kv版本 1—非版本化的键值对存储。更新值会覆盖现有值。由于没有存储支持版本化的额外元数据的要求,存储占用空间较小。 -
kv版本 2—支持版本化的键值对。它提供了增强的数据避免意外覆盖的支持。需要额外的存储空间来存储用于跟踪版本化的元数据。
在这种情况下,可以使用任意的 kv 版本,但鉴于大多数实现都使用版本 2,因此将使用该版本。密钥管理引擎在 Vault 的特定 路径 或位置上启用。对于你的应用程序,你将使用 agents 路径。
在启用 kv 引擎之前,列出所有启用的密钥管理引擎:
kubectl -n vault exec -it vault-0 -- vault secrets list
Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_4cc71c5d per-token private secret storage
identity/ identity identity_9f9aa91a identity store
sys/ system system_8f066be3 system endpoints used for control,
policy and
现在,在 agents 路径上启用 kv-v2 密钥管理引擎以启用存储键值对密钥,如下所示。
列表 5.6 启用 kv 密钥管理引擎
kubectl -n vault exec -it vault-0 -- vault secrets enable \
-path=agents \ ①
-version=2 \ ②
kv-v2 ③
Success! Enabled the kv-v2 secrets engine at: agents/
① Vault 中 kv 密钥管理引擎的位置
② 密钥管理引擎的版本
③ 要启用的引擎名称
在 agents 路径上启用了引擎后,你现在可以存储一些值。每个代理将与它们的键相关联几个属性:
-
名称 -
电子邮件 -
位置
为代理 bill 创建一个新的条目:
kubectl -n vault exec -it vault-0 -- vault kv put agents/bill \
id="Bill Smith" \
email="bill@acme.org" \
location="New York, USA"
Key Value
--- -----
created_time 2021-05-29T17:20:48.24905171Z
deletion_time n/a
destroyed false
version 1
从 Vault 中检索存储的值同样简单:
kubectl -n vault exec -it vault-0 -- vault kv get agents/bill
====== Metadata ======
Key Value
--- -----
created_time 2021-05-29T17:20:48.24905171Z
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
email bill@acme.org
location New York, USA
name Bill Smith
现在你已经确认能够成功添加和检索密钥,添加几个更多代理:
kubectl -n vault exec -it vault-0 -- vault kv put agents/jane \
id="Jane Doe" \
email="jane@acme.org" \
location="London, United Kingdom"
kubectl -n vault exec -it vault-0 -- vault kv put agents/maria \
id="Maria Hernandez" \
email="maria@acme.org" \
location="Mexico City, Mexico"
kubectl -n vault exec -it vault-0 -- vault kv put agents/james \
id="James Johnson" \
email="james@acme.org" \
location="Tokyo, Japan"
到目前为止,Vault 中应该存储了四个密钥。
应用程序访问和安全
当前用于与 Vault 交互的根令牌具有对 Vault 所有能力的无限制访问权限,不建议在与应用程序交互的 Vault 服务器中使用。相反,应用最小权限原则,应使用单独的方法让应用程序对 Vault 进行认证。Vault 支持多种认证方法,供消费者识别自身,包括用户名和密码、TLS 证书以及前面提到的令牌,仅举几例。
可以将一种或多种策略与认证方法关联,这些策略定义了在 Vault 中对不同路径的权限。每个策略都与能力相关联,这些能力提供了对特定路径的细粒度控制(图 5.4)。

图 5.4 能力、策略和认证方法之间的关系
以下是在 Vault 中可用的能力集:
-
create—允许在给定路径上创建数据 -
read—允许在给定路径上读取数据 -
delete—允许在给定路径上删除数据 -
list—允许在给定路径上列出值
策略可以以 JSON 或 HashiCorp 配置语言(HCL [兼容 JSON])格式编写,并在创建时通过 CLI 提交给 Vault 服务器。
创建一个名为agents-policy.hcl的新文件,该文件将定义提供对agents路径中的read和list值访问权限的策略,如下所示。
列表 5.7 创建策略以管理对 Vault 内容的访问
path "agents/data/*" {
capabilities = ["list", "read"] ①
}
path "agents/metadata/*" {
capabilities = ["list", "read"]
}
① 与给定路径关联的能力列表
重要版本化的内容,如kv版本 2,存储以data/为前缀的内容,这在kv版本 1 中被省略,在设计策略时必须考虑。
通过将列表 5.7 中创建的.hcl 策略文件复制到 Vault Pod 并创建策略,创建一个名为agents_reader的新策略:
cat agents-policy.hcl | \
kubectl -n vault exec -it vault-0 -- vault policy write agents_reader -
Unable to use a TTY - input is not a terminal or the right kind of file
Success! Uploaded policy: agents_reader
现在创建一个新的令牌,该令牌可用于代理应用程序,并且仅提供对agents路径的访问权限,并将其分配给名为AGENTS_APP_TOKEN的变量,如下所示。
列表 5.8 为代理应用程序创建令牌
export AGENTS_APP_TOKEN=$(kubectl -n vault exec -it vault-0 \
-- vault token create -policy=agents_reader -format=yaml | \ ①
grep client_token | awk '{ print $2 }') ②
① 创建令牌并以 yaml 格式打印输出
② 从响应中提取令牌
您可以通过查找其详细信息来查看有关令牌的信息:
kubectl -n vault exec -it vault-0 -- vault token lookup $AGENTS_APP_TOKEN
Key Value
--- -----
accessor 9N8JDsdVrGfspYILad3DxZUq
creation_time 1622320692
creation_ttl 768h
display_name token
entity_id n/a
expire_time 2021-06-30T20:38:12.973925141Z
explicit_max_ttl 0s
id s.1ErBLPR3QTrNks8HhLNQcpjv ①
issue_time 2021-05-29T20:38:12.973933315Z
meta <nil>
num_uses 0
orphan false
path auth/token/create
policies [agents_reader default]
renewable true
ttl 767h51m20s
type
① 可以用于与代理路径交互的令牌的 ID
如果命令返回了错误或没有任何值,请确认列表 5.8 中分配的$AGENTS_APP_TOKEN值的正确性。
默认情况下,有效期限(TTL),或令牌有效的时长,设置为 32 天。在许多企业组织中,限制令牌有效的时间长度可以增加安全性,并在令牌被泄露的情况下减少威胁向量。要显式设置令牌的有效期限,可以将-ttl标志添加到vault token create命令中,以自定义令牌的有效时长。
确认令牌只能查看 agents 路径中的资源,如 agents_reader 策略所述。首先,将当前根令牌备份到 Vault Pod 中的另一个位置,以便稍后可以恢复访问:
kubectl -n vault exec -it vault-0 -- \
cp /home/vault/.vault-token \
/home/vault/.vault-token.root
现在使用列表 5.8 中创建的新令牌登录:
echo $AGENTS_APP_TOKEN | kubectl -n vault exec -it vault-0 -- vault login -
Unable to use a TTY - input is not a terminal or the right kind of file
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.1ErBLPR3QTrNks8HhLNQcpjv
token_accessor 9N8JDsdVrGfspYILad3DxZUq
token_duration 767h26m48s
token_renewable true
token_policies ["agents_reader" "default"]
identity_policies []
policies ["agents_reader" "default"]
登录会显示与令牌关联的键信息,包括其有效时间和包含的权限。确认 agents 路径中的密钥可以被列出:
kubectl -n vault exec -it vault-0 -- vault kv list /agents
Keys
---
bill
james
jane
maria
如果发生错误,请确认策略已正确创建,从列表 5.7 开始。
如果密钥被正确列出,尝试访问资源,例如列出令牌不应具有访问权限的已启用密钥管理引擎:
kubectl -n vault exec -it vault-0 -- vault secrets list
Error listing secrets engines: Error making API request.
URL: GET http://127.0.0.1:8200/v1/sys/mounts
Code: 403\. Errors:
* 1 error occurred:
* permission denied
确认权限级别受限后,通过将备份令牌复制到 Vault 期望的默认位置来恢复根令牌,以便您可以再次执行提升权限:
kubectl -n vault exec -it vault-0 -- \
cp /home/vault/.vault-token.root \
/home/vault/.vault-token
确认可以执行提升请求,以便后续步骤可以实施:
kubectl -n vault exec -it vault-0 -- vault secrets list
5.1.2 将应用程序部署到访问 Vault
使用可以访问 Vault 中 agents 路径的令牌,部署一个演示在 Vault 中访问值的应用程序。源代码可在 GitHub 上找到(mng.bz/WMXa),但本章不会从编程角度介绍该应用程序本身。
第一步是创建一个名为 agents-vault-token 的 Kubernetes Secret,它将包含我们的令牌,该令牌将被注入为应用程序中的环境变量,以便它可以与 Vault 进行通信:
kubectl create secret generic agents-vault-token \
--from-literal=token=$(echo -n $AGENTS_APP_TOKEN | tr -d '\r\n')
secret/agents-vault-token created
创建一个名为 serviceaccount.yml 的文件,以定义一个名为 agents 的 Kubernetes 服务账户,将其与应用程序关联,如下所示。始终建议每个工作负载在单独的服务账户下执行,以便仅委派所需的必要权限。这将在列表 5.14 中进一步详细说明。
列表 5.9 serviceaccount.yml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: agents
创建引用以下清单的 agents 服务账户:
kubectl -n vault apply -f serviceaccount.yml
serviceaccount/agents created
创建一个名为 deployment_token_auth.yml 的文件,包含使用之前在 agents-vault-token secret 中定义的令牌进行基于令牌认证的应用程序清单,如下所示。
列表 5.10 deployment_token_auth.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: agents
spec:
replicas: 1
selector:
matchLabels:
app: agents
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: agents
spec:
containers:
- env:
- name: QUARKUS_VAULT_AUTHENTICATION_CLIENT_TOKEN
valueFrom:
secretKeyRef: ①
key: token
name: agents-vault-token
image: quay.io/ablock/agents
imagePullPolicy: Always
name: agents
ports:
- containerPort: 8080
protocol: TCP
restartPolicy: Always
serviceAccountName: agents
① 存储在 Kubernetes secret 中并作为环境变量注入的令牌
将部署应用到集群以创建应用程序:
kubectl -n vault apply -f deployment_token_auth.yml
deployment.apps/agents
部署创建后,确认应用程序正在运行:
kubectl -n vault get pods -l=app=agents
NAME READY STATUS RESTARTS AGE
agents-595b85fc6-6fdbj 1/1 Running 0 66s
Vault 令牌通过 QUARKUS_VAULT_AUTHENTICATION_CLIENT_TOKEN 环境变量暴露给应用程序。然后应用程序框架促进与 Vault 服务器的后端通信。
现在测试应用程序以确认可以接收值。应用程序在端口 8080 上通过 /agents 端点公开一个 restful 服务,可以用于查询 Vault 中存储的代理。
首先通过定位正在运行的agents Pod 的名称,然后调用/agents/bill端点来尝试定位代理bill的记录。
AGENTS_POD=$(kubectl get pods -l=app=agents \
-o jsonpath={.items[0].metadata.name})
kubectl -n vault exec -it $AGENTS_POD -- \
curl http://localhost:8080/agents/bill
{"email":"bill@acme.org","location":"New York, USA","name":"Bill Smith"}
既然您能够检索到一个有效的结果,现在尝试检索一个不存在的值,应该返回一个空的结果:
kubectl -n vault exec -it $AGENTS_POD -- \
curl http://localhost:8080/agents/bad
随意查询存储在 Vault 中的其他代理,以全面测试应用程序。在下一节中,您将探索如何避免使用 Vault 令牌来认证访问 Vault。
5.2 Kubernetes 认证方法
5.1.2 节探讨了应用程序如何使用 Vault 令牌,使用令牌认证方法与 Vault 服务器交互以访问存储的值。虽然使用令牌访问 Vault 相对简单,但它需要额外的步骤来管理令牌的生命周期,这最终可能导致整体安全态势的降低。
如 5.1 节所述,Vault 支持多种认证方法。由于您在 Kubernetes 环境中操作,您可以利用不同的方法——Kubernetes 认证方法——来简化应用程序与 Vault 的交互。使用此方法将防止您需要管理访问 Vault 的令牌的额外任务。
Kubernetes 认证方法并没有消除令牌的概念(恰恰相反),但它不是管理源自 Vault 的单独令牌,而是使用与 Kubernetes 服务账户关联的 JSON Web Tokens (JWTs)。在 Kubernetes 上运行的应用程序通过 Pod 内挂载的服务账户令牌(/var/run/secrets/kubernetes.io/serviceaccount/token)与 Vault 交互。然后在 Vault 中创建角色,这些角色将 Kubernetes 服务账户与定义了授予访问级别的 Vault 策略映射。Kubernetes 认证方法中涉及的组件图示在图 5.5 中。

图 5.5 Kubernetes 认证方法中涉及的组件概述
5.2.1 配置 Kubernetes 认证
您的第一步是在 Vault 中启用 Kubernetes 认证方法:
kubectl -n vault exec -it vault-0 -- vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
由于 Vault 将与 Kubernetes 服务账户交互,它必须能够验证提交的令牌与谁关联,以及它是否仍然有效。幸运的是,Kubernetes 提供了 TokenReview API,正是为了这个目的,并且本质上执行 JWT 令牌的反向查找。通过使用给定服务账户的 JWT 令牌对 TokenReview API 进行认证,将返回有关账户的详细信息,包括但不限于用户名。
为了使 Vault 能够与 Kubernetes TokenReview API 交互以检查应用程序提供的令牌,它必须被授予执行此类请求的权限。在名为vault-tokenreview-serviceaccount.yml的文件中创建一个新的服务账户vault-tokenreview,该账户将被 Kubernetes 认证方法使用,如下所示。
列表 5.11 vault-tokenreview-serviceaccount.yml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-tokenreview ①
① 将由 Kubernetes 认证方法使用的服务账户的名称
现在创建 vault-tokenreview 服务账户,使用列表 5.11 中创建的清单:
kubectl -n vault apply -f vault-tokenreview-serviceaccount.yml
serviceaccount/vault-tokenreview
现在已经创建了 vault-tokenreview 服务账户,必须授予其对 Token Review API 进行请求的权限。有一个内置的 Kubernetes ClusterRole 提供这种级别的访问权限,称为 system:auth-delegator。在名为 vault-tokenreview-binding.yml 的文件中创建一个新的 ClusterRoleBinding,名为 vault-tokenreview-binding,包含以下内容。
列表 5.12 vault-tokenreview-binding.yml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-tokenreview-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-tokenreview
namespace: vault
从 vault-tokenreview-binding.yml 文件中的清单创建 ClusterRoleBinding:
kubectl -n vault apply -f vault-tokenreview-binding.yml
clusterrolebinding.rbac.authorization.k8s.io/vault-tokenreview-binding created
由于 Vault 需要使用 vault-tokenreview 服务账户的 JWT 来与 Kubernetes 通信,请执行以下命令组,首先找到包含 JWT 令牌的 Kubernetes 机密名称,然后是存储在机密中的 Base64 解码的令牌值。
列表 5.13 设置 Vault TokenReview 变量
SA_SECRET_NAME=$(kubectl -n vault get serviceaccount vault-tokenreview \
-o jsonpath={.secrets[0].name}) ①
VAULT_TOKENREVIEW_SA_TOKEN=$(kubectl -n vault get secret $SA_SECRET_NAME \
-o jsonpath='{.data.token}' | base64 -d) ②
① 定位与 vault-tokenreview 服务账户关联的机密名称
② 获取 vault-tokenreview 服务账户的 JWT 令牌
接下来,通过提供 Kubernetes API 的位置、证书颁发机构的证书以及列表 5.13 中获取的服务账户的 JWT,指定 Kubernetes 集群的配置:
kubectl -n vault exec -it vault-0 -- vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc" \
kubernetes_ca_cert="@/var/run/secrets/kubernetes.io/\
serviceaccount/ca.crt" \token_reviewer_jwt=$VAULT_TOKENREVIEW_SA_TOKEN
Success! Data written to: auth/kubernetes/config
现在,Vault 能够进行身份验证请求后,想要从 Vault 内部获取资源的应用程序必须与它们关联一个角色,以获取 Vault 中存储的值。一个 角色 包括提交请求的服务账户的名称和命名空间,以及一组 Vault 策略。回想列表 5.10,Agents 应用程序正在使用名为 agents 的服务账户运行。虽然之前在运行应用程序时使用的服务账户之间几乎没有区别,但在此处需要它以方便使用 Kubernetes 认证方法与 Vault 集成。执行以下操作以在 Vault 中创建一个名为 agents 的新角色:
列表 5.14 创建 Vault 角色
kubectl -n vault exec -it vault-0 -- \
vault write auth/kubernetes/role/agents \ ①
bound_service_account_names=agents \ ②
bound_service_account_namespaces=vault \ ③
policies=agents_reader ④
Success! Data written to: auth/kubernetes/role/agents
① 创建名为 agents 的新角色的路径
② 要与角色关联的服务账户的名称
③ 包含与角色关联的服务账户的命名空间
④ 应用到角色的策略
在 Vault 中创建角色的操作允许 Agents 应用程序使用 Kubernetes 认证方法从 Vault 获取值。
5.2.2 测试和验证 Kubernetes 认证
为了测试和验证 Agents 应用程序中 Kubernetes 认证的集成,首先删除任何可能仍然存在的先前基于令牌认证方法的现有工件,因为它们将不再需要:
kubectl -n vault delete deployment agents
kubectl -n vault delete secrets agents-vault-token
注意:请忽略任何与资源未找到相关的错误。此步骤确保您有一个实现 Kubernetes 认证方法的全新环境。
在先前部署的 Agents 应用程序中,QUARKUS_VAULT_AUTHENTICATION_CLIENT_TOKEN 包含了用于对 Vault 进行认证的令牌值。当迁移到 Kubernetes 认证方法时,将使用 QUARKUS_VAULT_AUTHENTICATION_KUBERNETES_ROLE 代替,并引用 Vault 中在列表 5.14 中创建的 agents 角色。
创建一个名为 deployment_kubernetes_auth.yml 的文件,包含以下 Deployment 定义。
列表 5.15 deployment_kubernetes_auth.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: agents
spec:
replicas: 1
selector:
matchLabels:
app: agents
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: agents
spec:
containers:
- env:
- name: QUARKUS_VAULT_AUTHENTICATION_KUBERNETES_ROLE
value: "agents"
image: quay.io/ablock/agents
imagePullPolicy: Always
name: agents
ports:
- containerPort: 8080
protocol: TCP
restartPolicy: Always
serviceAccountName: agents
现在根据列表 5.15 中创建的清单创建部署:
kubectl -n vault apply -f deployment_kubernetes_auth.yml
deployment.apps/agents
一旦应用程序启动,查询代理 maria 以确认存储在 Vault 中的值可以成功检索,从而验证 Kubernetes 认证方法已成功集成。
kubectl -n vault exec -it $(kubectl get pods -l=app=agents \
-o jsonpath={.items[0].metadata.name}) -- \
curl http://localhost:8080/agents/maria
{"email":"maria@acme.org","location":"Mexico City,
➥Mexico","name":"Maria Hernandez"}
成功的响应展示了如何使用 Kubernetes 认证方法从 Vault 中检索机密,而不需要明确地为应用程序提供一个 Vault 令牌进行认证。如果没有返回成功的结果,请确认本节中描述的步骤。
5.3 Vault 代理注入器
Kubernetes 认证方法简化了在 Kubernetes 上部署的应用程序访问 Vault 中存储的值的方式。使用令牌或 Kubernetes 认证方法(如 5.2.1 节所述)所面临的挑战之一是,应用程序需要了解 Vault。在许多情况下,尤其是在遗留应用程序或第三方供应商提供的应用程序中,可能无法修改源代码来配置此类集成。
为了克服这些挑战,出现了几种方法,利用 Kubernetes 生态系统中的模式来解决如何使存储在 Vault 中的值可供应用程序使用。每种方法都依赖于 Kubernetes 中 Pod 的一个关键特性,通过该特性可以使用 emptyDir 卷类型在容器之间共享卷。然后可以在 Pod 中打包一个单独的容器,其责任是促进与 Vault 的交互并通过共享卷向应用程序提供机密值。
在 Kubernetes 中采用了两种模式来支持这种方法:
-
init container—在应用程序容器启动之前执行的一个容器或一组容器。在 Vault 的上下文中,从 Vault 中检索资产并将其放置在为应用程序预先填充的共享卷中,以便应用程序可以消费。
-
sidecar–与应用程序容器一起运行的容器。在 Vault 的上下文中,它们继续与 Vault 交互并使用 Vault 的资产刷新共享卷的内容。
为了避免要求最终用户开发和维护他们自己的容器集以与 Vault 交互,并提供一种机制以自动将具有 Vault 意识的容器注入 Pod,HashiCorp 创建了 Vault Agent Injector。Vault Agent Injector 在 Kubernetes 中以 Pod 的形式运行,并监视声明在其 Pod 中的注解以寻求成为 Vault 意识的应用程序。一旦将注入器安装到集群中,当创建任何其他 Pod 时,就会向 Vault Agent Injector 发送一个准入 webhook,其中包含 Pod 的详细信息。如果 Pod 中存在特定的注解,特别是 vault.hashicorp.com/agent-inject: true,则修改 Pod 本身的定义以自动注入 initContainer 和/或 sidecar 容器。此过程利用了 Kubernetes 的 MutatingWebhookConfiguration 功能,该功能允许在资源持久化到 etcd 之前修改 Kubernetes 资源(图 5.6)。

图 5.6 通过 MutatingWebhookConfiguration 在准入时间修改 Pod 以注入 Vault Agent Injector
5.3.1 支持 Kubernetes Vault Agent 注入的配置
为了演示应用程序如何通过最小化对应用程序本身的更改将存储在 Vault 中的值注入,你将再次使用代理应用程序作为目标。首先在 Vault 中定义一个新的名为 config 的密钥,包含与应用程序相关的属性。通过 Vault Agent Injector,密钥中的值将被添加到应用程序 Pod 内的一个文件中。
首先通过以下命令在 Vault 中定义 config 密钥。
列表 5.16 创建键值密钥
kubectl -n vault exec -it vault-0 -- \
vault kv put agents/config \ ①
mission="Kubernetes Secrets" \ ②
coordinator="Manning Publishing"
① 将数据写入指定的路径
② 将多个键值对写入密钥
在添加新值之后,下一步是修改代理应用程序的 Deployment,通过定义几个必要的注解,不仅支持自动容器注入,还可以自定义 Pod 中呈现的值。
创建一个名为 deployment_vault_agent.yml 的新文件,如下所示。
列表 5.17 deployment_vault_agent.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: agents
spec:
replicas: 1
selector:
matchLabels:
app: agents
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true" ①
vault.hashicorp.com/role: "agents" ②
vault.hashicorp.com/agent-inject-secret-config.properties:
➥"agents/config" ③
vault.hashicorp.com/agent-inject-template-
➥config.properties: | ④
{{- with secret "agents/config" }}
{{- range $k, $v := .Data.data }}
{{ $k }}: {{ $v }}
{{- end }}
{{- end }}
labels:
app: agents
spec:
containers:
- image: quay.io/ablock/agents
imagePullPolicy: Always
name: agents
ports:
- containerPort: 8080
protocol: TCP
restartPolicy: Always
serviceAccountName: agents
① 启用 Vault Agent 注入
② 与 Kubernetes 认证方法关联的角色名称
③ Vault 密钥的名称作为值以及应用程序 Pod 中的结果文件名
④ 描述密钥内容如何呈现的模板
Vault Agent Injector 建立在 Kubernetes 认证方法之上,因此将重用第 5.1 节中应用的部分配置。
vault.hashicorp.com/role 表示与运行 Pod 的服务账户相关联的角色以及授予访问 Vault 内容的策略。以 vault.hashicorp.com/agent-inject-secret- 开头的代码行用于定义最终将在应用 Pod 中创建的文件。此注释的值引用了 Vault 中的密钥名称。在列表 5.16 中创建的 agents 路径中的 config 密钥。注释键的其余部分引用了将在 Pod 中创建的文件名。因此,具有键 vault.hashicorp.com/agent-inject-secret-config.properties 的注释会在 Pod 中创建一个名为 config.properties 的文件。
最后,以 vault.hashicorp.com/agent-inject-template- 开头的代码行指的是 Consul 语言中的模板,并定义了密钥内容的渲染方式。与以 vault.hashicorp.com/agent-inject-secret- 开头的行类似,其余部分引用了将要创建的文件名。这里定义的模板只是遍历密钥中包含的所有键和值。
如果集群中存在现有的代理部署,请将其删除以确保不会使用 5.1.2 和 5.2 节中描述的与 Vault 的任何现有集成:
kubectl -n vault delete deployment agents
deployment.apps/agents
现在执行以下命令以创建代理部署,该部署将使用 Kubernetes Vault Agent Injector:
kubectl -n vault apply -f deployment_vault_agent.yml
deployment.apps/agents
如果您观察 agents Pod 的状态,您将注意到与这次部署有几种不同的情况。
kubectl -n vault get pod -l=app=agents
NAME READY STATUS RESTARTS AGE
agents-795fcd5565-6f6cg 2/2 Running 0 79s
首先注意,在 READY 列下,现在有两个容器。额外的容器是负责保持从 Vault 获取的密钥内容更新的边车容器。此外,在边车容器或代理容器启动之前,Pod 中还包含一个名为 vault-agent-init 的初始化容器,该初始化容器在 /vault/secrets 挂载点处预填充了 Vault 密钥的内容。(此路径可以通过注释进行修改。)由于初始化容器必须在启动主容器之前成功完成,并且由于 Pod 中的两个容器目前都在运行,您可以放心地认为密钥值已从 Vault 中检索出来。让我们通过查看位于 agents 容器中 /vault/secrets/config.properties 文件的内容来验证这一点。
列表 5.18 查看由 vault secrets injector 创建的文件
kubectl -n vault exec -it $(kubectl get pods -l=app=agents \
-o jsonpath={.items[0].metadata.name}) \ ①
-c agents -- cat /vault/secrets/config.properties ②
coordinator: Manning Publishing
mission: Kubernetes Secrets
① 获取正在运行的代理 Pod 的名称
② 打印由 Vault Agent Injector 创建的文件的内容
您可以随意更新 Vault 中 config 机密的内容。Pod 内捆绑的边车容器将定期检查机密的状态,并更新应用程序容器内文件的内容。通过使用 Kubernetes Vault Agent Injector,可以在不修改应用程序的情况下自动提供 Vault 机密,完全抽象化 Vault 的使用,同时提供引用存储在 HashiCorp Vault 中的敏感值的优点。
摘要
-
使用 Helm 图表,可以快速轻松地将 HashiCorp Vault 安装到 Kubernetes 环境中。
-
解密 HashiCorp Vault 实例是获取纯文本根密钥的过程,以启用对底层数据的访问。
-
Kubernetes 认证方法使用 Kubernetes 服务帐户与 HashiCorp Vault 进行身份验证。
-
Kubernetes TokenReview API 提供了一种从 JWT 令牌中提取用户详情的方法。
-
HashiCorp Vault Agent Injector 修改了 Pod 的定义,使其能够获取存储在 Vault 中的机密。
6 访问云秘密存储库
本章涵盖
-
使用容器存储接口(CSI)和秘密存储 CSI 驱动程序从云秘密存储库注入秘密作为卷
-
将云秘密填充到 Kubernetes 集群作为 Kubernetes Secrets
-
使用秘密存储 CSI 驱动程序中的秘密自动轮换来提高安全态势
-
从云秘密存储库中消费敏感信息
第五章介绍了 HashiCorp Vault,它可以用于安全地存储和管理部署到 Kubernetes 的应用程序的敏感资产,并演示了如何配置应用程序和 Vault 以提供彼此的无缝集成。本章扩展了上一章中介绍的想法,即使用外部秘密管理工具存储秘密并将它们注入 Pod 内,无论是作为卷还是作为环境变量。但在这章中,我们将重点关注云秘密存储库,如 Google Secret Manager、Azure Key Vault 和 AWS Secrets Manager。
首先,您将了解容器存储接口(CSI)和秘密存储 CSI 驱动程序,并使用它们将存储在 HashiCorp Vault 中的秘密注入。然后,您将了解如何使用秘密存储 CSI 驱动程序注入 Kubernetes Secrets 以及秘密自动轮换。最后,我们将讨论 CSI 驱动程序与 Google Secret Manager、Azure Key Vault 和 AWS Secrets Manager 之间的集成,以便可以从秘密存储库直接将秘密注入 Pod。
6.1 容器存储接口和秘密存储 CSI 驱动程序
如您迄今为止所见,etcd 数据库以未加密(默认情况下)或加密的形式存储 Kubernetes Secrets,如第四章所示。但如果你不想在 etcd 中存储你的秘密,而是想将它们存储和管理在 Kubernetes 集群之外呢?第五章中展示的一个选项是使用 HashiCorp Vault 和 HashiCorp Vault Agent 存储秘密并将它们注入 Pod。
HashiCorp Vault 是一个选项,但如今云提供商也提供了它们自己的密钥保管库;例如,Google 提供了 Google Secret Manager,而 Amazon 提供了 AWS Secrets Manager。如果你想使用它们来存储秘密而不是 etcd 或 Hashicorp Vault 会发生什么?你能使用之前相同的方法,但将秘密作为卷或环境变量从外部云存储库注入而不是 etcd 吗?
答案是肯定的!但在我们向您展示如何做之前,我们需要介绍 CSI 创新计划——一个用于将任意块和文件存储系统暴露给容器化工作负载(如 Kubernetes)的标准。
6.1.1 容器存储接口
正如您在第二章中所见,Kubernetes volume 是一个包含对运行在 Pod 内的容器中某些数据访问的目录。卷的物理存储由所使用的卷类型确定,卷在 Pod 初始化期间进行映射,执行以下步骤:
-
API 服务器收到在 Kubernetes 集群中创建新 Pod 的命令。
-
调度器找到一个满足所需标准的节点,并将 Pod 定义发送到该节点。
-
节点 kubelet 读取 Pod 定义,看到您想要将卷附加到 Pod 容器,并通过卷插件创建卷。这个卷插件负责连接到配置的持久存储。
图 6.1 总结了所有这些步骤。

图 6.1 从 Pod 视角创建卷的过程
如您所见,卷插件属于 Kubernetes 核心。然而,这种方法有以下缺点:
-
卷插件开发与 Kubernetes 开发和发布周期相关联。任何新的受支持的卷(和卷插件)都需要 Kubernetes 的新版本。
-
在卷插件(例如,修复错误或改进)中的任何更正都需要发布新的 Kubernetes 版本。
-
由于卷插件在 Kubernetes 内部,源代码是开放的;这并不是问题,直到您需要将插件私有化。
CSI 是一个可以用来统一容器编排器(CO)系统(如 Kubernetes、Mesos、Docker Swarm 等)的存储接口的倡议,结合了存储供应商(如 Ceph、Azure Disk、GCE 持久磁盘等)。
CSI 的第一个含义是任何实现都保证与所有 CO 一起工作。第二个含义是 CSI 元素的定位;它们位于 CO 核心(即 Kubernetes 核心)之外,这使得它们可以独立于 CO 进行开发和发布。图 6.2 展示了 CSI 的简要概述。

图 6.2 CSI 架构
CSI 驱动程序是容器集群和实现 CSI 规范所需操作的持久存储之间的桥梁。CSI 驱动程序提供以下功能:
-
创建持久的外部存储。
-
配置持久的外部存储。
-
管理集群和存储之间的所有输入/输出(I/O)。
-
提供高级磁盘功能,如快照和克隆。
CSI 驱动程序包括阿里云磁盘、AWS 弹性块存储、Azure 磁盘存储、CephFS、DigitalOcean 块存储和 GCE 持久磁盘。
6.1.2 容器存储接口和 Kubernetes
CSI 在 Kubernetes 1.13 版本中获得了一般可用性(GA)状态,并且可以与 Kubernetes 卷组件(例如,持久卷、持久卷声明和存储类)一起使用。Kubernetes 有一些不属于核心的组件,它们与外部可插拔容器存储进行交互。这种交互是通过 Google 远程过程调用(gRPCs)在域套接字上发生的。
安装了 CSI 的 Kubernetes 集群具有以下组件:
-
Kubernetes 核心——这是 Kubernetes 的核心,本书中介绍的大部分元素都生活在这里。
-
CSI 外部组件——这是一组 Linux 容器,其中包含触发适当 Kubernetes 事件到 CSI 驱动程序的通用逻辑。尽管这些容器不是必需的,但它们有助于减少实现 CSI 驱动程序所需的样板代码量。
-
第三方外部组件——这是与持久化存储解决方案通信的供应商特定实现。
这些组件中每个都有子组件,其中最重要的可以在图 6.3 中看到。

图 6.3 带有 CSI 的 Kubernetes 集群
对于本书的范围,这就是您需要了解的关于 CSI 规范及其与 Kubernetes 集成的所有内容。要实现特定持久化存储系统的特定 CSI 驱动程序,需要深入了解该系统。感谢这种关注点的分离,您现在可以编写和部署插件,在 Kubernetes 中暴露新的存储系统,而无需触及 Kubernetes 核心,因此不需要进一步发布 Kubernetes。
6.1.3 CSI 和密钥
CSI 旨在以标准方式将所有不同的存储系统暴露给 Kubernetes 和容器工作负载。有了这个标准,您只需在您的集群上部署插件(CSI 驱动程序)即可。这能否扩展到其他类型的系统,如密钥存储?
Secrets Store CSI 驱动程序与其他任何 CSI 驱动程序没有区别,允许 Kubernetes 将放置在外部密钥存储中的密钥附加到 Pod 作为卷。卷附加后,密钥在容器的文件系统中可用。
Secrets Store CSI 驱动程序可扩展性的一个要素是 Secrets Store CSI 提供者。驱动程序不是直接连接到密钥存储,而是通过提供者这一新的抽象级别,因此,根据所使用的密钥存储,您只需要安装特定的提供者。
在撰写本文时,以下密钥存储得到支持:
-
AWS Secrets Manager Systems Manager Parameter Store(AWS 提供者)
-
Azure Key Vault(Azure 提供者)
-
Google Secret Manager(GCP 提供者)
-
HashiCorp Vault(Vault 提供者)
图 6.4 展示了 Secrets Store CSI 架构的概览。

图 6.4 带有 CSI 的 Kubernetes 集群
在您使用 CSI 和 Secrets Store CSI 驱动程序之前,您需要创建一个 Kubernetes 集群并安装 CSI 和 Secrets Store CSI 驱动程序。我们将在以下部分详细说明此过程。
6.1.4 安装先决条件
在终端窗口中运行以下命令以启动一个新的 minikube 实例。
列表 6.1 启动 minkube
minikube start -p csi --kubernetes-version='v1.21.0'
➥--vm-driver='virtualbox' --memory=8196 ①
① 在 CSI 配置文件下创建一个 Kubernetes 集群
6.1.5 安装 Secrets Store CSI 驱动程序
目前 Secrets Store CSI 驱动程序的最新版本是 0.1.0;尽管您可能认为它是一个不成熟的项目,但事实是它已经开发很长时间了。运行以下列表中显示的命令来安装 Secrets Store CSI。
列表 6.2 安装 Secrets Store CSI 驱动程序
kubectl apply -f https://raw.githubusercontent.com/
➥kubernetes-sigs/secrets-store-csi-driver/v0.1.0/
➥deploy/rbac-secretproviderclass.yaml ①
kubectl apply -f https://raw.githubusercontent.com/
➥kubernetes-sigs/secrets-store-csi-driver/v0.1.0/
➥deploy/csidriver.yaml ②
kubectl apply -f https://raw.githubusercontent.com/
➥kubernetes-sigs/secrets-store-csi-driver/v0.1.0/deploy/
➥secrets-store.csi.x-k8s.io_secretproviderclasses.yaml ③
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/
➥secrets-store-csi-driver/v0.1.0/deploy/
➥secrets-store.csi.x-k8s.io_secretproviderclasspodstatuses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/
➥secrets-store-csi-driver/v0.1.0/deploy/
➥secrets-store-csi-driver.yaml ④
① 为密钥提供程序配置 RBAC
② 配置 CSI 驱动程序
③ 注册 Secrets Store CRDs
④ 安装 Secrets Store CSI 驱动程序
由于我们将在密钥存储库内容与 Kubernetes 密钥之间的映射以及密钥自动轮换中使用,因此需要一些额外的 RBAC 权限来启用它,如下所示。
列表 6.3 安装 RBAC
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/
➥secrets-store-csi-driver/v0.1.0/deploy/
➥rbac-secretprovidersyncing.yaml ①
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/
➥secrets-store-csi-driver/v0.1.0/deploy/
➥rbac-secretproviderrotation.yaml ②
① 将 secrets-store 内容同步为 Kubernetes 密钥。
② 密钥轮换的额外 RBAC 权限
注意:如果您正在 Windows 节点上运行 Kubernetes(请注意,minikube 运行在一个 Linux 虚拟机中,因此您不需要运行该命令),您还需要运行以下命令:
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/
➥secrets-store-csi-driver/v0.1.0/deploy/
➥secrets-store-csi-driver-windows.yaml ①
① 如果您正在运行 Windows 节点,请使用此命令。
为了验证 Secrets Store CSI 驱动程序的安装,运行以下列表中显示的命令以验证 Pods 是否正在正确运行。
列表 6.4 验证 Secrets Store CSI 驱动程序
kubectl get pods --namespace=kube-system ①
① 从 kube-system 命名空间获取 Pods
您应该看到 Secrets Store CSI 驱动程序 Pods 在每个代理节点上运行:
csi-secrets-store-8xlcn 3/3 Running 0 4m
现在您已经将 Secrets Store CSI 驱动程序安装到 Kubernetes 集群中,是时候选择一个 Secrets Store CSI 提供程序并将其部署了。在第一个示例中,您将使用 HashiCorp Vault,因为它是一个无差别的密钥存储库,您已经在上一章中使用过它。
与您之前使用 HashiCorp Vault 的最大不同之处在于密钥将如何注入到 Pod 中。在第五章的一个示例中,您看到了负责执行注入的 HashiCorp Vault 代理;图 6.5 包含了 HashiCorp Vault Agent 的工作回顾。然而,在本章中,将不会使用代理,而是 Secrets Store CSI 驱动程序将注入密钥。

图 6.5 Pod 在 admission 时间被 MutatingWebhookConfiguration 修改以注入 Vault Agent Injector
6.1.6 通过 Secrets Store CSI 驱动程序和 HashiCorp Vault 提供程序消费 HashiCorp Vault 密钥
在本节中,您将使用 HashiCorp Vault 作为密钥存储库,并通过 Secrets Store CSI 驱动程序和 HashiCorp Vault 提供程序来消费密钥。图 6.6 展示了您在本节中将实现的内容概述。

图 6.6 带有 HashiCorp Vault 提供程序的 Secrets Store CSI 驱动程序
您首先需要的是一个在 Kubernetes 集群中运行的 HashiCorp Vault 实例。您已经在第五章中部署了一个 HashiCorp Vault 实例;现在您将在当前集群中再次部署。要开始,首先按照以下步骤将 HashiCorp 仓库添加到 Helm。
列表 6.5 添加 Helm 图表
helm repo add hashicorp https://helm.releases.hashicorp.com ①
"hashicorp" has been added to your
① 添加 HashiCorp Helm 仓库
然后从远程仓库检索最新更新:
helm repo update ①
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "hashicorp" chart repository
Update Complete. ⎈ Happy Helming!⎈
① 更新图表仓库
执行以下列表中的命令以启用 Vault CSI 提供者并安装开发模式下的图表。
列表 6.6 使用 Secrets Store CSI 提供者部署 HashiCorp Vault
helm install vault hashicorp/vault \ ①
--set "server.dev.enabled=true" \ ②
--set "injector.enabled=false" \ ③
--set "csi.enabled=true" ④
① 安装最新的 Vault
② 启用 dev 模式以快速启动
③ 禁用 Vault Agent,因为在使用 CSI 提供者时不需要它
④ 启用 Vault CSI 提供者 Pod
运行以下列表中的命令以等待 HashiCorp Vault 部署启动并运行。
列表 6.7 等待 HashiCorp Vault 就绪
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=vault --timeout=90s ①
① 等待 Vault 启动。
在 HashiCorp Vault 内部创建一个密钥
现在通过在 Vault Pod 上打开交互式 shell 会话,在 Vault 内部创建一个键值密钥。
列表 6.8 打开交互式 shell
kubectl exec -it vault-0 -- /bin/sh ①
① 对 Vault 容器打开交互式 shell
从现在起,发出的命令将在 Vault 容器上执行。现在在 secret/pass 路径下创建一个具有my_secret_password值的密钥。
列表 6.9 创建密钥
vault kv put secret/pass password="my_secret_password" ①
Key Value
--- -----
created_time 2021-08-03T04:59:49.920719431Z
deletion_time n/a
destroyed false
version 1
① 在 Vault 密钥存储中创建一个新的密钥
配置 Kubernetes 认证
当使用 Vault CSI 提供者时,Vault 的主要认证方法是 Pod 关联的服务账户。因此,您需要启用 Kubernetes 认证方法并对其进行配置,以便 Vault CSI 驱动程序可以使用它。仍然在 Vault 容器内部,运行以下列表中的命令。
列表 6.10 启用和配置 Kubernetes 认证方法
vault auth enable kubernetes ①
Success! Enabled kubernetes auth method at: kubernetes/
vault write auth/kubernetes/config \
issuer="https://kubernetes.default.svc.cluster.
➥local" \ ②
token_reviewer_jwt="$(cat /var/run/secrets/
➥kubernetes.io/serviceaccount/token)" \ ③
kubernetes_host="https://$KUBERNETES_PORT_443
➥_TCP_ADDR:443" \ ④
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/
➥serviceaccount/ca.crt ⑤
Success! Data written to: auth/kubernetes/config
① 启用 Kubernetes 认证方法
② 设置发行者字段
③ Kubernetes 在该位置写入令牌。
④ 指向 Kubernetes 主机内部网络地址的引用
⑤ Kubernetes 在该位置写入证书。
使用 Secrets Store CSI 驱动读取密钥需要所有挂载的读取权限以及访问密钥本身。创建一个名为csi-internal-app的策略。
列表 6.11 应用 Vault 策略
vault policy write csi-internal-app - <<EOF ①
path "secret/data/pass" { ②
capabilities = ["read"] ③
}
EOF
Success! Uploaded policy: csi-internal-app
① 设置策略名称
kv-v2 的数据需要在挂载路径之后添加一个额外的数据路径元素。
③ 读取权限
最后,创建一个名为my-app的 Kubernetes 认证角色,将此策略与名为app-sa的 Kubernetes 服务账户绑定,如以下列表所示。该角色用于 Vault CSI 提供者配置(您将在下一节中看到),服务账户用于运行 Pod。
列表 6.12 创建 Kubernetes 认证角色
vault write auth/kubernetes/role/my-app \ ①
bound_service_account_names=app-sa \ ②
bound_service_account_namespaces=default \ ③
policies=csi-internal-app \ ④
ttl=120m ⑤
Success! Data written to: auth/kubernetes/role/my-app
① 设置角色名称
与角色关联的 app-sa 服务账户
③ 命名空间
④ Vault 策略
⑤ 认证后返回的令牌有效期为 120 分钟。
最后,退出 Vault Pod 返回到计算机。
列表 6.13 退出 shell 会话
exit
定义一个 SecretProviderClass 资源
SecretProviderClass Kubernetes 自定义资源描述了提供给 Secrets Store CSI 驱动的配置参数。Vault CSI 提供者需要以下参数:
-
Vault 服务器的地址
-
Vault Kubernetes 身份验证角色的名称
-
要注入 Pod 中的机密
下面的列表显示了连接到 Vault 安装的SecretProviderClass定义。
列表 6.14 vault-spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: vault-secrets ①
spec:
provider: vault ②
parameters:
vaultAddress: "http://vault.default:8200" ③
roleName: "my-app" ④
objects: |
- objectName: "my-password" ⑤
secretPath: "secret/data/pass" ⑥
secretKey: "password" ⑦
① 资源名称
② 设置配置的提供者
③ 保管库 URL
④ 在 Kubernetes 身份验证方法中设置的角色
⑤ 该机密的符号名称。这代表包含机密值的文件名,位于卷内。
⑥ 机密在 Vault 中的路径
⑦ 要注入的机密的关键名称
通过运行以下命令应用 vault-secrets SecretProviderClass。
列表 6.15 应用 Vault 机密SecretProviderClass
kubectl apply -f vault-spc.yaml -n default ①
① 注册服务提供者类
部署一个挂载机密的 Pod
一切准备就绪后,是时候创建一个名为app-sa的服务账户(与“配置 Kubernetes 身份验证”部分中配置的名称相同)以及一个 Pod,该 Pod 使用由 CSI 创建的机密存储创建卷,如下所示。卷配置现在包含一个csi部分,其中你将设置要使用的 CSI 驱动程序——在这种情况下,是机密存储一个(secrets-store.csi.k8s.io)以及之前创建的SecretProviderClass名称(vault-secrets)。
列表 6.16 vault-app-pod.yaml
kind: ServiceAccount
apiVersion: v1
metadata:
name: app-sa ①
---
kind: Pod
apiVersion: v1
metadata:
name: greeting-demo
labels:
app: greeting
spec:
serviceAccountName: app-sa ②
containers:
- image: quay.io/lordofthejars/greetings-jvm:1.0.0
name: greeting-demo
volumeMounts: ③
- name: secrets-store-inline
mountPath: "/mnt/secrets-vault" ④
readOnly: true
volumes:
- name: secrets-store-inline
csi: ⑤
driver: secrets-store.csi.k8s.io ⑥
readOnly: true
volumeAttributes:
secretProviderClass: "vault-secrets" ⑦
① 在 Kubernetes 身份验证方法中设置的服务账户
② 用于运行 Pod 的服务账户
③ 卷挂载部分
④ 存储机密的路径
⑤ 开始 csi 部分
⑥ 设置机密存储驱动程序
⑦ 参考 SecretProviderClass
图 6.7 显示了所有这些元素之间的关系。

图 6.7 机密提供者、Pod 和 Vault 之间的关系
通过运行以下命令部署 Pod。
列表 6.17 应用带有 CSI 卷的 Pod
kubectl apply -f vault-app-pod.yaml -n default ①
① 部署服务账户和应用程序
当 Pod 部署时,它将包含一个卷,其中挂载了所有 Vault 机密作为文件。然后等待 Pod 运行,如下所示。
列表 6.18 等待问候 Pod 运行
kubectl wait --for=condition=ready pod -l app=greeting --timeout=90s ①
① 等待应用程序准备就绪
最后,验证机密是否已挂载在volumesMount部分指定的/mnt/secrets-vault,并且包含机密的文件名为my-password,如objectName字段中设置的那样。运行以下列表中显示的命令。
列表 6.19 读取注入的机密
kubectl exec greeting-demo -- cat /mnt/secrets-vault/my-password
my_secret_password% ①
① 显示的值与机密的密码值匹配。
回顾一下,你已经看到了如何安装 CSI 和 Secrets Store CSI 驱动程序,并创建了一个SecretProviderClass资源来配置 Secrets Store CSI 驱动程序以使用 HashiCorp Vault 作为机密存储。你现在应该理解 Secrets Store CSI 驱动程序是如何工作的,并且你已经看到机密被挂载为磁盘上的卷。但在某些情况下,你可能需要将这些机密挂载为 Kubernetes 机密。让我们来探讨一下这是如何完成的。
6.2 同步 CSI 机密为 Kubernetes 机密
同步在应用程序(通常是遗留应用程序)需要以环境变量或直接使用 Kubernetes API 服务器读取秘密时特别有用。Secrets Store CSI 驱动使用 Kubernetes Volumes 将秘密挂载到磁盘,但通过在 SecretProviderClass 自定义资源(图 6.8)中使用可选的 secretObjects 字段,也支持将这些秘密镜像到 Kubernetes Secrets。

图 6.8 Secrets Store CSI 驱动将秘密作为 Kubernetes Secrets 镜像。
重要提示:卷挂载仍然需要,并且必须定义以与 Kubernetes Secrets 同步。
您将通过将秘密也映射为 Kubernetes Secret 来扩展之前的 HashiCorp Vault 示例。
6.2.1 准备命名空间
在将秘密映射为 Kubernetes Secrets 之前,您可能需要删除 SecretProviderClass 资源并卸载 greeting-demo Pod 以获得一个干净的环境。在终端窗口中,运行以下命令。
列表 6.20 清理环境
kubectl delete -f vault-app-pod.yaml -n default ①
kubectl delete -f vault-spc.yaml -n default ②
① 卸载 Pod。
② 删除 SecretProviderClass 资源。
在示例卸载后,您可以开始同步示例。
6.2.2 使用 secretObjects 定义 SecretProviderClass 资源
要使用 Secrets Store CSI 驱动挂载秘密存储中的秘密,修改 SecretProviderClass 资源,添加 secretObjects 字段。此字段用于定义如何将秘密存储中的秘密映射到 Kubernetes Secret。
在以下片段中,您将看到如何将名为 foo 的秘密存储中的秘密值映射到名为 bar 的键,该键属于名为 foosecret 的 opaque Kubernetes Secret:
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: my-provider
spec:
provider: vault
secretObjects:
- data:
- key: bar ①
objectName: foo ②
secretName: foosecret ③
type: Opaque ④
① 用于在秘密资源中查找秘密值的密钥名称
② 要同步的挂载内容名称
③ Kubernetes 秘密对象名称
④ 秘密类型
图 6.9 显示了秘密存储、SercretProviderClass 定义和 Kubernetes Secret 之间的关系。

图 6.9 秘密存储、SecretProviderClass 和 Kubernetes Secrets
关于支持的秘密类型
在撰写本书时,以下秘密类型受到支持:
-
Opaque
-
kubernetes.io/basic-auth
-
bootstrap.kubernetes.io/token
-
kubernetes.io/dockerconfigjson
-
kubernetes.io/dockercfg
-
kubernetes.io/ssh-auth
-
kubernetes.io/service-account-token
-
kubernetes.io/tls
打开之前创建的 vault-spc.yaml 文件,并添加 secretObjects 节来使存储在 HashiCorp Vault 中的数据成为 Kubernetes Secret。
列表 6.21 vault-spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: vault-secrets
spec:
provider: vault
secretObjects: ①
- data:
- key: password
objectName: my-password ②
secretName: my-secret ③
type: Opaque
parameters: ④
vaultAddress: "http://vault.default:8200"
roleName: "my-app"
objects: |
- objectName: "my-password"
secretPath: "secret/data/pass"
secretKey: "password"
① 定义秘密与 Kubernetes Secrets 之间的同步
② 在参数部分定义 objectName。
③ Kubernetes 秘密名称
④ 定义使用秘密作为文件的卷创建
应用以下资源。
列表 6.22 应用 SecretProviderClass
kubectl apply -f vault-spc.yaml ①
① 部署 SecretProviderClass
部署了 SecretProviderClass 后,是时候部署 Pod 了。
部署一个挂载了机密和机密对象的 Pod
只有在启动一个挂载了机密的 Pod 后,机密才会同步。在部署 Pod 之前,先列出默认命名空间中的当前机密。
列表 6.23 列出机密
kubectl get secrets -n default ①
NAME TYPE DATA AGE
sh.helm.release.v1.vault.v1 helm.sh/release.v1 1 6m51s
vault-csi-provider-token-chhwj kubernetes.io/service-account-token 3 6m51s
vault-token-qj2jk kubernetes.io/service-account-token 3 6m51s
① 列出机密
一切准备就绪后,是时候重新部署上一节定义的 Pod 了。
列表 6.24 使用 CSI 卷应用 Pod
kubectl apply -f vault-app-pod.yaml -n default ①
① 使用 CSI 驱动程序部署 Pod
等待 Pod 运行。
列表 6.25 等待问候 Pod 运行
kubectl wait --for=condition=ready pod -l app=greeting --timeout=90s ①
① 等待 Pod 部署完成
在 Pod 部署后列出机密以确认机密现在已创建。
列表 6.26 列出机密
kubectl get secrets -n default
NAME TYPE DATA AGE
my-secret Opaque 1 28s①
sh.helm.release.v1.vault.v1 helm.sh/release.v1 1 9m19s
vault-csi-provider-token-chhwj kubernetes.io/service-account-token 3 9m19s
vault-token-qj2jk kubernetes.io/service-account-token 3 9m19s
① 使用 secretName 字段设置的名称创建了一个机密。
描述机密以验证其是否正确创建。
列表 6.27 描述机密
kubectl describe secret my-secret
Name: my-secret ①
Namespace: default
Labels: secrets-store.csi.k8s.io/managed=true ②
Annotations: <none>
Type: Opaque ③
Data
====
password: 18 bytes ④
① 在 secretName 字段中设置的名称
② 设置机密的标签由 Secrets Store CSI 管理
③ 在 type 字段中定义的类型
④ 如 key 字段中定义的机密密钥名称
创建与 Secrets Store CSI 创建的机密等效的 kubectl 命令
kubectl create secret generic my-secret
➥--from-literal password=my_secret_password -n default
最后,删除 Pod 以验证它是否会被自动删除,因为没有任何 Pod 消耗机密。
列表 6.28 删除 Pod
kubectl delete -f vault-app-pod.yaml -n default ①
① 删除 Pod。
再次列出机密,你会看到 my-secret 机密已被删除,因为没有 Pod 使用它。
列表 6.29 列出机密
kubectl get secrets -n default
NAME TYPE DATA AGE
sh.helm.release.v1.vault.v1 helm.sh/release.v1 1 11m
vault-csi-provider-token-chhwj kubernetes.io/service-account-token 3 11m
vault-token-qj2jk kubernetes.io/service-account-token 3 11m
提示:一旦创建了机密,你可以将其设置为任何 Kubernetes Secret 的环境变量:
env:
- name: MY_SECRET
valueFrom:
secretKeyRef:
name: my-secret
key: password
记住你正在创建 Kubernetes Secrets,因此它们需要像第三章和第四章中所示那样进行管理(即在 Git 仓库中加密它们,并启用 KMS 以在 Kubernetes 中加密存储而不是编码)。到目前为止,你已经看到了如何使用 Secrets Store CSI 驱动程序将机密从机密存储库挂载为 Kubernetes 卷和 Kubernetes Secrets。但是,当机密在机密存储库中更新时会发生什么?Secrets Store CSI 驱动程序支持机密自动轮换。
6.3 通过自动轮换机密来提高安全态势
假设你收到针对系统的攻击,并且一些机密数据被泄露;你应该做的第一件事之一是重新生成机密到新的值(机密的轮换)。Secrets Store CSI 驱动程序可以在 Pod 运行后检测外部机密存储库中机密的更新,并将此更改填充到相应的卷内容和 Kubernetes Secrets 对象中,如果它们被使用,如图 6.10 所示。

图 6.10 Secrets Store CSI 驱动程序将机密镜像为 Kubernetes Secrets
自动轮换密钥 是指定期自动更改密钥数据的过程。从更改密钥数据中可以获得两个重要优势:第一个优势是,这使得攻击者更难获取值,并且使用可能被窃取的密钥来解密它变得更加困难。第二个优势是,如果发生密钥数据泄露,您需要尽可能快地轮换密钥,以避免出现重大问题。
需要注意的是,Secrets Store CSI 驱动程序仅更新密钥存储位置(卷或 Kubernetes 密钥),但消耗这些密钥的应用程序需要实现一些逻辑来对这些更改做出反应并读取新的密钥值。例如,当将密钥作为卷挂载时,应用程序将需要监视更改。如果密钥作为环境变量注入,那么 Pod 需要重新启动以获取最新的密钥作为环境变量。
提示:像 Reloader (github.com/stakater/Reloader) 这样的项目在检测到关联的 ConfigMap 或 Secret 发生更改时,会自动在 Pod 上推送滚动升级。
6.3.1 准备命名空间
在使用自动轮换功能之前,您可能需要卸载 greeting-demo Pod(尽管这不是必需的),以便有一个全新的 Pod,例如。如果您之前没有删除 Pod,请在终端窗口中运行以下命令。
列表 6.30 清理环境
kubectl delete -f vault-app-pod.yaml -n default ①
① 卸载 Pod。
当 Secrets Store CSI 驱动程序安装时,自动轮换功能默认是禁用的。要启用此功能,Secrets Store CSI 驱动程序 Pod 应该以 --enable-secret-rotation 标志设置为 true 并使用 rotation-poll-interval 标志设置轮换轮询间隔(即检查密钥是否已更改的频率)的方式启动。要启用自动轮换,请在终端中运行以下命令停止之前的部署。
列表 6.31 卸载 Secrets Store CSI 驱动程序
kubectl delete -f https://raw.githubusercontent.com/kubernetes-sigs/
➥secrets-store-csi-driver/v0.1.0/deploy/secrets-store-csi-driver.yaml
接下来修改 Secrets Store CSI 驱动程序的部署文件,以配置 --enable-secret-rotation 和 rotation-poll-interval 标志。在这个例子中,将 rotation-poll-interval 时间设置为 1 分钟,这意味着每分钟,驱动程序将查询密钥存储,检查值是否已更改。
列表 6.32 install-csi-polling.yaml
kind: DaemonSet ①
apiVersion: apps/v1
metadata:
name: csi-secrets-store
namespace: kube-system
spec:
selector:
matchLabels:
app: csi-secrets-store
template:
metadata:
labels:
app: csi-secrets-store
annotations:
kubectl.kubernetes.io/default-logs-container: secrets-store
spec:
serviceAccountName: secrets-store-csi-driver
containers:
- name: node-driver-registrar
image: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.2.0
args:
- --v=5
- --csi-address=/csi/csi.sock
- --kubelet-registration-path=/var/lib/kubelet/plugins/
➥csi-secrets-store/csi.sock
env:
- name: KUBE_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
imagePullPolicy: IfNotPresent
volumeMounts:
- name: plugin-dir
mountPath: /csi
- name: registration-dir
mountPath: /registration
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 10m
memory: 20Mi
- name: secrets-store
image: k8s.gcr.io/csi-secrets-store/driver:v0.1.0
args:
- "--endpoint=$(CSI_ENDPOINT)"
- "--nodeid=$(KUBE_NODE_NAME)"
- "--provider-volume=/etc/kubernetes/secrets-store-csi-providers"
- "--metrics-addr=:8095"
- "--enable-secret-rotation=true" ②
- "--rotation-poll-interval=1m" ③
- "--filtered-watch-secret=true"
- "--provider-health-check=false"
- "--provider-health-check-interval=2m"
env:
- name: CSI_ENDPOINT
value: unix:///csi/csi.sock
- name: KUBE_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
ports:
- containerPort: 9808
name: healthz
protocol: TCP
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthz
port: healthz
initialDelaySeconds: 30
timeoutSeconds: 10
periodSeconds: 15
volumeMounts:
- name: plugin-dir
mountPath: /csi
- name: mountpoint-dir
mountPath: /var/lib/kubelet/pods
mountPropagation: Bidirectional
- name: providers-dir
mountPath: /etc/kubernetes/secrets-store-csi-providers
resources:
limits:
cpu: 200m
memory: 200Mi
requests:
cpu: 50m
memory: 100Mi
- name: liveness-probe
image: k8s.gcr.io/sig-storage/livenessprobe:v2.3.0
imagePullPolicy: IfNotPresent
args:
- --csi-address=/csi/csi.sock
- --probe-timeout=3s
- --http-endpoint=0.0.0.0:9808
- -v=2
volumeMounts:
- name: plugin-dir
mountPath: /csi
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 10m
memory: 20Mi
volumes:
- name: mountpoint-dir
hostPath:
path: /var/lib/kubelet/pods
type: DirectoryOrCreate
- name: registration-dir
hostPath:
path: /var/lib/kubelet/plugins_registry/
type: Directory
- name: plugin-dir
hostPath:
path: /var/lib/kubelet/plugins/csi-secrets-store/
type: DirectoryOrCreate
- name: providers-dir
hostPath:
path: /etc/kubernetes/secrets-store-csi-providers
type: DirectoryOrCreate
nodeSelector:
kubernetes.io/os: linux
① DaemonSet 在所有 Kubernetes 节点上部署驱动程序。
② 启用密钥轮换
③ 设置轮询时间为 1 分钟
最后,使用之前的修改部署 Secrets Store CSI 驱动程序。
列表 6.33 启用自动轮换的 Secrets Store CSI 驱动程序部署
kubectl apply -f install-csi-polling.yaml ①
① 启用轮询功能的 Secret 存储 CSI 安装
现在等待 Pod 运行。
列表 6.34 等待驱动程序 Pod 运行
kubectl wait --for=condition=ready pod -l app=csi-secrets-store
➥--timeout=90s -n kube-system ①
① 等待 Pod 部署完成
6.3.2 部署挂载密钥的 Pod
现在重新部署上一节中定义的 Pod,以注入密钥轮换示例。
列表 6.35 应用带有 CSI 卷的 Pod
kubectl apply -f vault-app-pod.yaml -n default ①
① 将 Pod 部署到默认命名空间
等待 Pod 运行。
列表 6.36 等待问候 Pod 运行
kubectl wait --for=condition=ready pod -l app=greeting --timeout=90s ①
① 等待 Pod 准备就绪。
最后,验证秘密是否已挂载在 /mnt/secrets-vault,如 volumesMount 部分所述,并且包含秘密的文件是 my-password,如 objectName 字段所设置。运行以下列表中所示的命令。
列表 6.37 读取注入的秘密
kubectl exec greeting-demo -- cat /mnt/secrets-vault/my-password
my_secret_password% ①
① 打印秘密的当前值。
6.3.3 更新秘密
现在,通过在 Vault Pod 上打开交互式 shell 会话,在 Vault 中更新密钥的新秘密值。
列表 6.38 打开交互式 shell
kubectl exec -it vault-0 -- /bin/sh ①
① 对 Vault 容器启动交互式 shell
从现在开始,发出的命令将在 Vault 容器上执行。使用 my_new_secret_password 值更新秘密/pass 路径上的秘密。
列表 6.39 更新秘密
vault kv put secret/pass password=
➥"my_new_secret_password" ①
Key Value
--- -----
created_time 2021-08-09T17:26:26.665179027Z
deletion_time n/a
destroyed false
version 2 ②
① 值已更新。
② 版本增加 1。
输入 exit 以退出 kubectl exec 命令。至少等待一分钟,直到 Secrets Store CSI 驱动程序轮询秘密,检测到更改,并将其填充到 Pod 中,然后再次运行命令以打印秘密值。
列表 6.40 读取注入的秘密
kubectl exec greeting-demo -- cat /mnt/secrets-vault/my-password
my_new_secret_password% ①
① 打印新的秘密版本。
输入 exit 以退出 kubectl exec 命令。注意,秘密值已注入,无需重新启动 Pod。
现在你应该理解 Secrets Store CSI 驱动程序的工作方式;然而,示例中没有使用任何云秘密存储。在下一节中,我们将扩展此示例,以涵盖在公共云秘密存储中存储秘密。
6.4 从云秘密存储中消费秘密
到目前为止,你在这个章节和第五章中使用了 HashiCorp Vault 作为秘密存储。但是,如果你使用公共云作为部署 Kubernetes 集群的平台,你可能希望使用他们在基础设施中提供的秘密存储服务。例如,如果你使用 Azure 云服务,你可能希望使用 Azure Key Vault 作为秘密存储。在本节中,你将了解如何使用 Secrets Store CSI 驱动程序从云秘密存储中消费秘密。
Secret Storage CSI 驱动程序支持 AWS Secrets Manager 和 AWS 系统管理器参数存储;Azure Key Vault;以及 Google Secret Manager。你将实现与上一节相同的示例,但将从公共云秘密存储中获取秘密,而不是从 HashiCorp Vault 获取。你将首先将秘密存储 CSI 驱动程序集成到 Azure Key Vault。
重要 无论是哪种情况,集成秘密存储和 Secrets Store CSI 的过程都是相同的:
-
安装和配置秘密存储。
-
根据所使用的秘密存储安装 Secrets Store CSI 提供程序(例如,Vault、AWS 或 Azure)。
-
使用秘密存储配置参数配置
SecretProviderClass。
两者之间最大的区别是秘密存储库的安装和配置。在本节中,我们将向您展示如何将云秘密存储库与 CSI 集成,但我们假设您有云提供商的账户来部署秘密存储库,并且对它们的工作方式有基本的了解。
6.4.1 Azure 密钥保管库
Azure 密钥保管库是 Azure 云服务,提供秘密(例如,密钥、密码和证书)的安全存储。要运行 Azure 密钥保管库,您需要一个至少具有免费服务订阅的 Azure 账户,因为 Azure 密钥保管库不能在 Azure 云之外运行。
安装和配置 Azure 密钥保管库
要安装 Azure 密钥保管库,请登录到您的 Azure 订阅以创建一个具有访问密钥保管库策略的服务主体。
列表 6.41 Azure 登录
az login ①
① 登录到 Azure。
登录到 Azure 后,您可以执行以下列表中的命令来创建服务主体。
列表 6.42 创建 Azure 服务主体
az ad sp create-for-rbac --skip-assignment --name alex ①
① 创建名为 alex 的服务主体
命令返回一个 JSON 文档,其中包含一些您稍后配置 CSI 驱动程序所需的敏感参数:
{
"appId": "7d3498f8-633e-4c58-bbaa5-1b2a015017a7", ①
"displayName": "alex",
"name": "http://alex",
"password": "2CAT7NvT9OzrLneTdi3..rYnU.M4_qGIMP", ②
"tenant": "66ee79ad-f624-4a81-b14e-319d7dd9e699"
}
① 这是 CSI 驱动程序所需的客户端 ID。
② 这是 CSI 驱动程序所需客户端密钥。
提示:为 Azure 密钥保管库创建特定的服务主体不是强制性的,您也可以使用默认服务主体。
接下来,您需要通过 Azure 门户创建 Azure 密钥保管库。访问 portal.azure.com/,并使用您的账户登录。在门户中,转到顶部搜索栏,输入“密钥保管库”,然后点击如图 6.11 所示的 Azure 密钥保管库资源。

图 6.11 Azure 门户
在“密钥保管库”部分,点击“创建密钥保管库”按钮以开始创建 Azure 密钥保管库实例(图 6.12)。

图 6.12 Azure 密钥保管库门户;云提供商持续调整他们的网络界面。
使用图 6.13 中显示的参数填写创建密钥保管库向导。

图 6.13 创建 Azure 密钥保管库
在此示例中,密钥 保管库 名称 为 alexvault,但可以是任何其他名称、区域 和 定价层。点击“审查 + 创建”,审查值,然后点击“创建”。
现在应该显示创建的密钥保管库概述(图 6.14)。在此阶段,点击 JSON 视图链接,它将显示相同的内容,但以 JSON 格式显示,以及密钥保管库的租户 ID。

图 6.14 Azure 密钥保管库概述
您需要创建要注入到 Pod 中的秘密。点击左侧菜单中的“秘密”部分,然后点击如图 6.15 所示的左上角菜单中的“生成/导入”按钮。

图 6.15 创建 Azure 密钥保管库秘密
在 name 字段中填写 password 值,在 value 字段中填写 my_password;然后点击创建,如图 6.16 所示。

图 6.16 创建 Azure Key Vault 秘密
NOTE 可以通过运行 az keyvault secret set --vault-name "alexvault" --name "password" --value "my_password" 命令使用 az CLI 工具创建一个秘密。
最后,您需要将之前创建的服务主体权限分配给您刚刚创建的密钥库。返回终端窗口,并执行以下命令。
列表 6.43 分配权限
az keyvault set-policy -n $KEYVAULT_NAME
➥--key-permissions get --spn $AZURE_CLIENT_ID ①
az keyvault set-policy -n $KEYVAULT_NAME
➥--secret-permissions get --spn $AZURE_CLIENT_ID ②
az keyvault set-policy -n $KEYVAULT_NAME
➥--certificate-permissions get --spn $AZURE_CLIENT_ID
① KEYVAULT_NAME 是分配给密钥库的名称。在本例中,它是 alexvault。
② $AZURE_CLIENT_ID 是服务主体的 appId 字段。在本例中,它是 7d3498f8-633e-4c58-bbaa5-1b2a015017a7。
现在,您已经准备好返回 Kubernetes!在下一节中,您将学习如何配置它以从 Azure Key Vault 消费秘密。
Azure Key Vault CSI 驱动程序
首先,安装并配置 Azure Key Vault CSI 驱动程序。您需要创建一个新的 Kubernetes 命名空间来部署 Azure 示例。
列表 6.44 创建 Azure 命名空间
kubectl create namespace azure ①
kubectl config set-context --current
➥--namespace=azure ②
① 创建一个命名空间。
② 切换到创建的命名空间。
安装 Azure Secrets Store CSI 提供程序,以使用 CSI 接口将 Azure Key Vault 中的秘密注入到 Pod 中。
列表 6.45 安装 Azure Secrets Store CSI 提供程序
kubectl apply -f https://raw.githubusercontent.com/Azure/
➥secrets-store-csi-driver-provider-azure/v0.1.0/deployment/
➥provider-azure-installer.yaml -n azure
TIP 对于 Windows 节点,您需要应用 provider-azure-installer-windows.yaml 文件。
通过执行以下命令来检查提供程序是否正在运行。
列表 6.46 检查 Azure Secrets Store CSI 提供程序
kubectl get pods -n azure ①
NAME READY STATUS RESTARTS AGE
csi-secrets-store-provider-azure-6fcdc 1/1 Running 1 161m
① 列出 Pod。
Warning 对于 AKS 集群,提供程序需要安装到 kube-system 命名空间,以与 Kube API 服务器建立连接。
然后,您将设置服务主体凭据作为 Kubernetes Secrets,使其可通过 Azure Secrets Store CSI 驱动程序访问。驱动程序使用此秘密登录到远程 Azure Key Vault。该秘密必须包含 clientid 和 clientsecret 键,分别设置为服务主体的 app ID 和 password 字段。图 6.17 展示了认证步骤的概述。

图 6.17 Azure Secrets Store CSI 提供程序的认证过程
以下列表显示了使用 Azure 凭据创建 Kubernetes 秘密的示例。
列表 6.47 创建 Kubernetes 秘密
kubectl create secret generic secrets-store-creds --from-literal
➥clientid=$AZURE_CLIENT_ID --from-literal
➥clientsecret=$AZURE_CLIENT_SECRET -n azure ①
① $AZURE_CLIENT_ID 是服务主体的 appId 字段。在本例中,它是 7d3498f8-633e-4c58-bbaa5-1b2a015017a7。$AZURE_CLIENT_SECRET 是服务主体的 password 字段。在本例中,它是 2CAT7NvT9OzrLneTdi3..rYnU.M4_qGIMP。
Important 秘密必须与应用程序 Pod 在同一命名空间中创建。此外,与任何 Kubernetes 秘密一样,它需要按照第三章和第四章中讨论的方式进行管理。
此外,您还可以标记在 nodePublishSecretRef 部分中使用的秘密(如刚刚创建的那个),以限制 CSI 驱动程序使用的内存量。
列表 6.48 创建 Kubernetes 秘密
kubectl label secret secrets-store-creds secrets
➥-store.csi.k8s.io/used=true -n azure ①
① 要限制 CSI 驱动程序消耗的内存量,您可以标记秘密。
在创建包含秘密的 Pod 之前,最后一步是使用 Azure Key Vault 名称和 Azure Key Vault 租户 ID 配置 SecretProviderClass。
列表 6.49 azure-spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: azure-manning ①
spec:
provider: azure ②
parameters:
keyvaultName: "alexvault" ③
objects: |
array:
- |
objectName: password ④
objectType: secret
tenantId: "77aa79ad-f624-7623-b14e-33447dd9e699" ⑤
① 资源名称
② 设置配置的提供者
③ Azure Key Vault 名称
④ 要注入的秘密的密钥名称
⑤ Azure Key Vault 租户 ID
通过运行以下命令应用 azure-manningSecretProviderClass:
列表 6.50 应用 azure-manningSecretProviderClass
kubectl apply -f azure-spc.yaml -n azure ①
① 注册 Azure SercretProviderClass
部署挂载秘密的 Pod
配置类似于您在 HashiCorp Vault 示例中看到的配置;您有 csi 部分,其中将 CSI 驱动程序设置为 secrets-store.csi.k8s.io,以及刚刚创建的 SecretProviderClass 名称(azure-manning)。但在此情况下,设置 nodePublishSecretRef 指向之前创建的包含 Azure 服务主体凭据的 Kubernetes 秘密(secrets-store-creds),以访问 Azure Key Vault。
列表 6.51 azure-app-pod.yaml
kind: Pod
apiVersion: v1
metadata:
name: greeting-demo
labels:
app: greeting
spec:
containers:
- image: quay.io/lordofthejars/greetings-jvm:1.0.0
name: greeting-demo
volumeMounts: ①
- name: secrets-store-inline
mountPath: "/mnt/secrets-azure" ②
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "azure-manning" ③
nodePublishSecretRef:
name: secrets-store-creds ④
① 卷挂载部分
② 秘密挂载的路径
③ 引用 SecretProviderClass
④ 使用服务主体作为秘密时需要。
通过运行以下命令部署 Pod。
列表 6.52 应用带有 CSI 卷的 Pod
kubectl apply -f azure-app-pod.yaml -n azure ①
① 部署配置为使用 Azure Secrets Store CSI 提供者的 Pod。
现在等待 Pod 运行。
列表 6.53 等待问候 Pod 运行
kubectl wait --for=condition=ready pod -l app=greeting --timeout=90s -n azure ①
① 等待 Pod 就绪。
在以下片段中,提供了一个完整的 SecretProviderClass 对象,因此您可以看到所有可能的选项。
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: azure-kvname
spec:
provider: azure
parameters:
usePodIdentity: "false" ①
useVMManagedIdentity: "false" ②
userAssignedIdentityID: "client_id" ③
keyvaultName: "kvname"
cloudName: "" ④
cloudEnvFileName: "" ⑤
objects: | ⑥
array:
- |
objectName: secret1
objectAlias: SECRET_1 ⑦
objectType: secret ⑧
objectVersion: "" ⑨
- |
objectName: key1
objectAlias: ""
objectType: secret
objectVersion: ""
objectFormat: "pem" ⑩
objectEncoding: "utf-8" ⑪
tenantId: "tid"
① 设置为 true 以使用 aad-pod-identity 访问密钥保管库(false)。
② 指定访问模式以启用用户分配的托管身份的使用(false)。
③ 在 User-assigned Managed Identity 模式下,需要分配给身份 ID 的用户。
④ 根据 Azure Go SDK 指定的 Azure 云名称(AzurePublicCloud)
⑤ 填充 Azure 环境时使用的文件路径
⑥ 要注入的秘密数组
⑦ 在写入磁盘时指定对象的文件名(objectName 值)。
⑧ 对象类型:秘密、密钥或证书
⑨ 对象版本(最新版)
⑩ 格式对象;支持类型为 pem 和 pfx
⑪ 对秘密对象的编码;支持类型为 UTF-8、HEX 和 BASE64
提供访问密钥保管库的标识
在本节中,我们已通过服务主体授予身份对密钥保管库的访问权限。在编写本文时,这是从非 Azure 环境连接到 Azure Key Vault 的唯一方式。但如果您处于 Azure 环境中(即 AKS),则支持以下其他身份验证模式:
-
AAD Pod 身份
-
用户分配的托管身份
-
系统分配的托管身份
您知道如何使用 Secrets Store CSI 驱动程序注入存储在 Azure Key Vault 中的秘密。如前所述,Secrets Store CSI 驱动程序支持除 Azure 之外的其他提供程序。在下一节中,您将回顾相同的示例——这次将秘密存储在 GCP 秘密管理器中。
6.4.2 GCP 秘密管理器
GCP 秘密管理器是 Google 云服务,它为秘密(例如,密钥、密码和证书)提供了一个安全的存储库。要运行 GCP 秘密管理器,您需要一个安装了秘密管理器的 GCP 账户,因为它不能在 Google 云之外运行。
安装和配置 GCP 秘密管理器
要安装 GCP 秘密管理器,请使用您的 GCP 账户登录到 GCP 控制台(console.cloud.google.com/home),并启用秘密管理器。在页面顶部的搜索栏中搜索秘密管理器,然后在搜索结果中点击它,如图 6.18 所示。

图 6.18 搜索 GCP 秘密管理器
然后通过点击启用按钮(图 6.19)来启用 GCP 秘密管理器。

图 6.19 启用秘密管理器
启用秘密管理器后,创建一个新的秘密,使用 CSI 将其注入 Pod。从左侧菜单的安全部分选择秘密管理器资源,然后点击创建秘密按钮,如图 6.20 所示。

图 6.20 创建秘密
使用名称为app-secret和值为my_password的创建秘密表单,如图 6.21 所示填写。

图 6.21 创建秘密
创建秘密时,会显示所有创建的秘密列表。现在点击创建的秘密的名称,以检查其详细信息并获取资源名称,因为您稍后需要它来配置 GCP Secrets Store CSI 提供程序(图 6.22)。

图 6.22 选择创建的秘密
记录资源 ID值(projects/466074950013/secrets/app-secret),因为当创建SecretProviderClass时,您将需要它作为参数。图 6.23 显示了秘密概述的示例。

图 6.23 创建的秘密概述
在离开 GCP 控制台之前的最后一步是导出 GCP 服务账户凭证密钥以验证秘密管理器实例。通常,当密钥添加到服务账户时,此 JSON 文件会自动下载。如果您没有密钥,可以通过点击服务账户菜单然后点击添加密钥按钮来添加一个新的密钥,如图 6.24 所示。

图 6.24 创建的秘密概述
生成一个 JSON 格式的密钥,并具有足够的权限访问 GCP Secret Manager 非常重要。最后回到机密概览页面,点击权限选项卡和添加按钮,将之前的服务账户添加为可以消费机密的账户。此过程如图 6.25 所示。

图 6.25 给服务账户赋予访问机密的权限
GCP Secret Manager CSI 驱动程序
让我们安装和配置 GCP Secret Manager CSI 驱动程序。首先,创建一个新的 Kubernetes 命名空间以部署 GCP 示例。
列表 6.54 创建 GCP 命名空间
kubectl create namespace gcp ①
kubectl config set-context --current --namespace=gcp ②
① 创建一个命名空间。
② 切换到创建的命名空间。
按照列表 6.55 的说明安装 GCP Secrets Store CSI Provider,以便使用 CSI 接口将 GCP Secret Manager 中的机密注入到 Pods 中。
列表 6.55 安装 GCP Secrets Store CSI 提供程序
kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/
➥secrets-store-csi-driver-provider-gcp/v0.5.0/deploy/
➥provider-gcp-plugin.yaml ①
① 安装 GCP Secrets Store CSI 提供程序
通过执行以下命令检查提供程序是否正在运行。
列表 6.56 检查 GCP Secrets Store CSI 提供程序
kubectl get pods -n kube-system ①
NAME READY STATUS RESTARTS AGE
coredns-558bd4d5db-4l9tk 1/1 Running 4 5d20h
csi-secrets-store-8xlcn 3/3 Running 11 5d3h
csi-secrets-store-provider-gcp-62jb5 1/1 Running 0 17s
① 列出 kube-system 命名空间中的 Pods。
警告:提供程序安装在 kube-system 命名空间中,以建立与 Kube API 服务器的连接。
然后将服务账户凭据设置为 Kubernetes Secrets,使其可通过 GCP Secrets Store CSI Driver 访问,如列表 6.57 所示。驱动程序使用此机密登录远程 GCP Secret Manager。该机密必须有一个 key.json 密钥,其值为导出的 GCP 服务账户凭据。
列表 6.57 创建 Kubernetes 机密命名空间
KEY_GCP='{"private_key_id": "123","private_key": "a-secret",
➥"token_uri": "https://example.com/token",
➥"type": "service_account"}' ①
kubectl create secret generic secrets-store-creds
➥--from-literal key.json=$KEY_GCP -n gcp ②
① $AKEY_GCP 是上一步中下载的服务账户 JSON 文件。
② 使用 key.json 密钥和 GCP 密钥的值创建一个机密。
重要机密必须在与应用程序 Pod 相同的命名空间中创建。此外,与任何 Kubernetes 机密一样,它需要按照第三章和第四章所述进行管理。
在创建包含机密的 Pod 之前,最后一步是配置 SecretProviderClass,包括机密资源 ID 和机密内容存储的文件名。
列表 6.58 gcp-spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: gcp-manning
spec:
provider: gcp
parameters:
secrets: |
- resourceName: projects/466074950013/secrets/
➥app-secret/versions/latest ①
fileName: app-secret ②
① 指的是以下格式的机密版本:
② 存储机密内容的文件名
通过运行以下命令应用 gcp-manning 的 SecretProviderClass。
列表 6.59 应用 gcp-manning 的 SecretProviderClass
kubectl apply -f gcp-spc.yaml -n gcp ①
① 注册 GCP SecretProviderClass
部署挂载机密的 Pod
此示例中的配置与您在 HashiCorp Vault 示例中看到的内容类似;您有 csi 部分,其中将 CSI 驱动程序设置为 secrets-store.csi.k8s.io,以及之前创建的 SecretProviderClass 名称(gcp-manning)。但在此情况下,设置 nodePublishSecretRef 指向之前创建的、使用 GCP 服务账户凭据(secrets-store-creds)访问 GCP Secret Manager 的 Kubernetes 机密。
列表 6.60 gcp-app-pod.yaml
kind: Pod
apiVersion: v1
metadata:
name: greeting-demo
labels:
app: greeting
spec:
containers:
- image: quay.io/lordofthejars/greetings-jvm:1.0.0
name: greeting-demo
volumeMounts: ①
- name: secrets-store-inline
mountPath: "/mnt/secrets-gcp" ②
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "gcp-manning" ③
nodePublishSecretRef:
name: secrets-store-creds ④
① 卷挂载部分
② 秘密挂载的路径
③ 对 SecretProviderClass 的引用
④ 当使用服务账户作为秘密时需要
通过运行以下命令部署 Pod。
列表 6.61 应用带有 CSI 卷的 Pod
kubectl apply -f gcp-app-pod.yaml -n gcp ①
① 部署一个配置为使用 GCP Secrets Store CSI 提供者的 Pod。
等待 Pod 运行。
列表 6.62 等待问候 Pod 运行
kubectl wait --for=condition=ready pod -l app=greeting --timeout=90s ①
① 等待 Pod 准备就绪。
最后,验证秘密是否已挂载在 volumesMount 部分指定的 /mnt/secrets-gcp,并且包含秘密的文件名为 app-secret,如 fileName 字段所述。运行以下列表中显示的命令。
列表 6.63 读取注入的秘密
kubectl exec greeting-demo -- cat /mnt/secrets-gcp/app-secret
my_secret% ①
① 显示的值与之前设置的密码值匹配。
向 GCP Secret Manager 提供身份
在本节中,你已使用服务账户提供了对 GCP Secret Manager 的身份访问权限。在撰写本文时,这是从非 GCP 环境连接到 GCP Secret Manager 的唯一方法。以下认证模式也得到支持:
-
Pod 工作负载身份
-
GCP 提供者身份
你现在知道如何使用秘密存储 CSI 驱动程序注入存储在 GCP Secret Manager 中的秘密。在下一节中,你将看到相同的示例——这次是存储在 AWS Secrets Manager 中的秘密。
6.4.3 AWS Secrets Manager
AWS Secrets Manager 是一种 AWS 云服务,它为秘密(例如,密钥、密码、证书)提供了一个安全的存储库。要运行 AWS Secrets Manager,你需要一个安装了 AWS Secrets Manager 的 AWS 账户;它不能在 AWS 云之外运行。此外,在撰写本文时,AWS Secrets Manager 可以在 Amazon Elastic Kubernetes Service (EKS) 1.17+ 上运行。
本书不会解释准备 EKS 集群的整个过程。我们还将假设 EKS 集群已启动并配置,并且 eksctl CLI 工具已安装在你的机器上。
在 AWS Secrets Manager 中创建秘密
如前几节所述,你需要将一个秘密存储到秘密存储中——在本例中为 AWS Secrets Manager——以便由秘密存储 CSI 驱动程序使用。使用 aws CLI 工具(mng.bz/ZpaR)创建一个名为 AppSecret 且值为 my_secret 的秘密。
列表 6.64 创建秘密
REGION=us-east-2 ①
aws --region "$REGION" secretsmanager create-secret --name AppSecret
➥--secret-string 'my_secret' ②
① Kubernetes 集群运行的区域
② 在秘密管理器中创建秘密
然后创建一个 IAM 访问策略来访问之前步骤中创建的秘密。这一步很重要,因为你将以类似于在 HashiCorp Vault Kubernetes 认证模式中的方式将此访问策略与运行 greeting-demo Pod 的 Kubernetes 服务账户关联起来。
运行列表 6.65 中显示的命令,创建一个名为 greeting-deployment-policy 的访问策略来访问 AppSecret 秘密。策略名称很重要,因为你需要在这项策略和 Kubernetes 服务账户之间建立联系。
列表 6.65 创建访问策略
$(aws --region "$REGION" --query Policy.Arn --output
➥text iam create-policy --policy-name greeting-deployment-policy
➥--policy-document '{ ①
"Version": "2012-10-17",
"Statement": [ {
"Effect": "Allow", ②
"Action": ["secretsmanager:GetSecretValue",
➥"secretsmanager:DescribeSecret"], ③
"Resource": ["arn:*:secretsmanager:*:*:
➥secret:AppSecret-??????"] ④
} ]
}')
arn:aws:iam::aws:policy/greeting-deployment-policy ⑤
① 创建一个名为 greeting-deployment-policy 的策略
② 允许操作字段中描述的操作
③ 允许获取和描述机密
④ 为名为 AppSecret 的机密设置策略
⑤ 命令返回策略 ARN ID。
您需要一个 IAM OIDC 提供者,以便在集群中创建 IAM 访问策略与 Kubernetes 服务账户之间的关联。如果您还没有一个,可以通过运行以下命令来创建一个。
列表 6.66 创建 IAM OIDC 提供者
eksctl utils associate-iam-oidc-provider --region="$REGION"
➥--cluster="$CLUSTERNAME" --approve ①
① $CLUSTERNAME 是您集群的名称。
最后创建一个 IAM 服务账户,该账户将被 Pod 使用,并将其与之前创建的 IAM 访问策略(arn:aws:iam::aws:policy/greeting-deployment-policy)关联。在这种情况下,使用 greeting-deployment-sa 作为服务账户名称和 Kubernetes 服务账户名称。
列表 6.67 创建访问策略
eksctl create iamserviceaccount \
--name greeting-deployment-sa ①
--region="$REGION" --cluster "$CLUSTERNAME" \
--attach-policy-arn "arn:aws:iam::aws:policy/
➥greeting-deployment-policy" \ ②
--approve --override-existing-serviceaccounts
① 创建 IAM 服务账户
② 将策略附加到以访问机密
该过程总结在图 6.26 中。

图 6.26 Pod-Volume 连接
AWS Secrets Manager CSI 驱动程序
现在安装和配置 AWS Secret Manager CSI 驱动程序。首先创建一个新的 Kubernetes 命名空间来部署 AWS 示例。
列表 6.68 创建 AWS 命名空间
kubectl create namespace aws ①
kubectl config set-context --current --namespace=aws ②
① 创建一个命名空间。
② 切换到创建的命名空间。
按照列表 6.69 中的说明安装 AWS Secrets Store CSI 提供者,以使用 CSI 接口将 AWS Secret Manager 中的机密注入到 Pod 中。
列表 6.69 安装 AWS Secrets Store CSI 提供者
kubectl apply -f https://raw.githubusercontent.com/aws/
➥secrets-store-csi-driver-provider-aws/main/deployment/
➥aws-provider-installer.yaml ①
① 安装 AWS Secrets Store CSI 提供者
通过执行以下命令来检查提供者是否正在运行:
列表 6.70 检查 GCP Secrets Store CSI 提供者
kubectl get pods -n kube-system ①
NAME READY STATUS RESTARTS AGE
coredns-558bd4d5db-4l9tk 1/1 Running 4 5d20h
csi-secrets-store-8xlcn 3/3 Running 11 5d3h
csi-secrets-store-provider-aws-34bj1 1/1 Running 0 17s
① 列出 kube-system 命名空间中的 Pod。
警告:提供者安装在 kube-system 命名空间中,以建立与 Kube API 服务器的连接。
在创建包含机密的 Pod 之前,最后一步是使用机密名称配置 SecretProviderClass。
列表 6.71 aws-spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: aws-manning
spec:
provider: aws
parameters:
objects: |
- objectName: "AppSecret" ①
objectType: "secretsmanager"
① 设置之前创建的机密名称
通过运行以下命令应用 aws-manningSecretProviderClass。
列表 6.72 应用 aws-manning SecretProviderClass
kubectl apply -f aws-spc.yaml -n aws ①
① 注册 AWS SecretProviderClass
部署挂载机密的 Pod
此示例中的配置与您在 HashiCorp Vault 示例中看到的内容类似。您有 csi 部分,其中将 CSI 驱动程序设置为 secrets-store.csi.k8s.io,以及之前创建的 SecretProviderClass 名称(aws-manning)。在这种情况下,将服务账户名称设置为之前步骤中创建的服务账户(greeting-deployment-sa)以访问 AWS Secrets Manager。
列表 6.73 aws-app-pod.yaml
kind: Pod
apiVersion: v1
metadata:
name: greeting-demo
labels:
app: greeting
spec:
serviceAccountName: greeting-deployment-sa
containers:
- image: quay.io/lordofthejars/greetings-jvm:1.0.0
name: greeting-demo
volumeMounts: ①
- name: secrets-store-inline
mountPath: "/mnt/secrets-aws" ②
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "aws-manning" ③
① 卷挂载部分
② 机密挂载的路径
③ 引用 SecretProviderClass
通过运行以下命令部署 Pod。
列表 6.74 应用带有 CSI 卷的 Pod
kubectl apply -f aws-app-pod.yaml -n aws ①
① 部署一个配置为使用 AWS Secrets Store CSI 提供者的 Pod。
等待 Pod 运行。
列表 6.75 等待问候 Pod 运行
kubectl wait --for=condition=ready pod -l app=greeting --timeout=90s ①
① 等待 Pod 准备就绪。
最后,验证密钥是否已挂载在 volumesMount 部分指定的 /mnt/secrets-aws,并且包含密钥的文件名称与 objectName 中设置的一致。运行以下列表中显示的命令。
列表 6.76 读取注入的密钥
kubectl exec greeting-demo -- cat /mnt/secrets-aws/AppSecret
my_secret% ①
① 显示的值与之前设置的密钥集的密码值匹配。
其他配置参数
AWS CSI 提供商还有其他配置参数,在之前的示例中没有显示;在下面的代码片段中,您可以看到一个包含所有可能参数的 SecretProviderClass 示例。
apiVersion: secrets-store.csi.x-k8s.io/v1alpha1
kind: SecretProviderClass
metadata:
name: aws-manning
spec:
provider: aws
parameters:
objects: |
- objectName: "AppSecret"
objectType: "secretsmanager" ①
objectAlias: "secret" ②
objectVersion: "latest" ③
objectVersionLabel: "latest" ④
① 密钥类型之一为 secretsmanager 或 ssmparameter
② 可选的基础文件名,用于存储密钥
③ 可选的密钥版本 ID
④ 可选的密钥版本/阶段标签
使用密钥存储 CSI 驱动时的全局安全考虑
正如我们在第二章中提到的,将密钥作为卷挂载或注入为环境变量会使您面临一些值得考虑的威胁:
-
当密钥在文件系统中挂载时,潜在的漏洞——如目录遍历攻击、未经授权访问节点磁盘或访问 Pod——可能成为问题,因为攻击者可能能够访问密钥数据。您需要在应用级别(例如,针对目录遍历)和 Kubernetes 级别(例如,禁用
kubectl exec)保护这些问题。 -
当密钥通过环境变量注入时,潜在漏洞,如应用级别记录环境数据或访问 Pod,可能成为问题,因为攻击者可能读取密钥数据。
-
当将密钥同步到 Kubernetes 密钥存储时,请记住应用在第四章中学到的所有安全考虑。您可能已经将密钥安全地放置在外部密钥存储中,但将其移动到 Kubernetes 密钥时可能会丢失所有这些机密性。
有不同的工具和项目可以帮助我们自动检测安全威胁并提供一些解决安全问题的指导。在我们看来,有两个工具结合使用可以最好地帮助您检测和审计容器中的安全配置错误以及异常行为。
第一个项目是 KubeLinter (docs.kubelinter.io/)。该项目是一个静态代码分析工具,它分析 Kubernetes YAML 文件和 Helm 图表,检查安全配置错误和最佳实践。它检测的一些问题包括以特权模式运行容器、暴露特权端口、暴露 SSH 端口、未设置资源需求以及从环境变量读取密钥。
第二个项目是 Falco (falco.org/)。该项目在运行时工作,解析来自内核的 Linux 系统调用,并将它们与一系列规则进行比对,以验证它们是否被允许。如果违反了规则,则会触发警报,并可以采取一些行动作为响应。Falco 附带一组规则;其中一些包括通知对知名目录的读写操作,例如 /etc、/usr/bin 和 /usr/sbin;所有者和模式更改;以及执行 SSH 二进制文件,如 ssh、scp 和 sftp。
可以说,现在你知道为什么 CSI 和秘密存储 CSI 驱动程序是处理多个秘密管理器的完美抽象。
摘要
-
容器存储接口是一个旨在统一容器编排器存储接口的倡议。
-
Secrets Store CSI 是一个实现 CSI 规范的方案,用于从外部数据存储中消费机密。
-
Secrets Store CSI 允许我们从 HashiCorp Vault、AWS Secret Manager、GCP Secret Manager 和 Azure Key Vault 将机密注入到 Pod 中。
-
虽然 Secrets Store CSI 支持密钥轮换,但应用程序需要通过监视磁盘更改或重新加载 Pod 来支持它。
第三部分. 持续集成和持续交付
第三部分提供了使用 Kubernetes 原生工具(如 Tekton 和 Argo CD)实现 Kubernetes 原生持续集成和持续交付的端到端概述,以及如何在整个管道中管理敏感资产。
第七章重点介绍了持续集成的各个方面,包括如何安全地构建和发布容器镜像,同时不泄露应用程序的秘密。第八章涵盖了持续交付,用于自动部署和发布应用程序到 Kubernetes。本书中介绍的大多数概念都在这一章中得到了例证,因此您可以安全地将包含敏感资产的应用程序交付到 Kubernetes。
7 Kubernetes-native continuous integration and Secrets
本章涵盖
-
使用持续集成方法对任何更改进行应用程序集成
-
使用 Kubernetes 本地的 Tekton 实现持续集成管道
-
使用 Kubernetes 本地的 CI 管道测试、构建并将 Linux 容器推送到外部注册库
在上一章中,您学习了如何从密钥存储库向容器注入机密,而在前面的章节中,您也了解了如何在应用程序生命周期的不同阶段保持机密的安全。现在,是时候将这些概念结合起来并开始应用它们了。
我们将演示如何使用 Tekton 实现一个 Kubernetes 本地的持续集成管道,以持续和自动地发布应用程序或服务,同时保持机密的安全。在本章中,我们希望快速交付高质量的应用程序,以便更快更好地进入市场,在整个管道中正确管理机密,以确保在开发阶段不会泄露任何机密。
7.1 持续集成简介
开发软件不是一项个人任务,而是一项团队任务,许多人共同且并行工作以创建一个应用程序。在过程结束时整合每个开发者的所有工作可能不是最佳策略,因为可能会出现一些问题,包括合并地狱、组件无法正确集成以及工作部分崩溃。最佳的集成策略是尽可能早地尽可能多地集成,这样任何错误都可以快速检测到,并且可以更容易地定位和修复。
持续集成(或 CI)是一组实践,它自动化了来自多个开发者的代码更改到单个存储库的集成。对存储库的提交必须频繁发生(通常每天几次),并且必须触发一个自动化的过程来验证新代码的正确性。
CI 的最终目标是建立一个稳定和自动化的方式来构建、打包和测试应用程序,以便任何源代码的更改都可以快速集成,无需等待数周,并在提交后进行验证。因此,任何集成过程中的中断都会在早期阶段被发现。
对于每个提交,代码应在以下阶段运行:
-
构建—代码被编译和打包。输出取决于开发应用程序所使用的平台和语言——在 Java 的情况下,它可以是 JAR 或 WAR 文件,而在 Go 的情况下,它将是一个二进制可执行文件。
-
测试—应用程序运行第一轮测试。这些测试不是端到端测试或长时间测试,而是单元测试、一些组件测试以及验证核心业务功能绿色路径的最小子集的端到端测试。
-
安全检查—在这个阶段,代码将进行分析以查找漏洞和不良实践,通常使用静态代码分析工具。
-
发布—交付工件被发布在工件存储库中。它可以是一个 JAR 文件、Go 可执行文件或 Linux 容器。
图 7.1 总结了持续集成管道中的每个步骤。

图 7.1 组成持续集成管道的常见步骤
持续集成的优点包括以下内容:
-
集成错误在早期阶段就被检测到,并且很容易修复,因为原始代码没有对其进行太多更改。
-
应用程序持续集成,因此不需要几周或几个月来集成所有组件。
-
当检测到测试失败时,更容易找到原因(因为只有少量更改),并且在回滚到先前版本的情况下,只有少量功能会丢失。
-
由于应用程序经常集成,总有一个版本准备好部署(或发布)到任何环境(测试、预生产和生产)。
重要 A CI 管道必须提供快速反馈,这意味着它不应超过 10 分钟,因为该管道的主要目标是快速向开发者提供反馈,并尽快通知他们任何集成错误。
在对持续集成进行简要介绍之后,现在是时候以 Kubernetes 原生的方式使用 Tekton 来实现它了。
7.2 Tekton
你将使用 Tekton,一个用于创建云原生 CI 管道的 Kubernetes 原生框架,为 Java 应用程序实现一个简单的 CI 管道。此管道将构建应用程序,将其容器化,并将容器推送到容器注册库。
Tekton (tekton.dev/) 是一个开源的 Kubernetes 原生项目,用于构建持续集成/持续交付 (CI/CD) 管道,它提供了自定义资源定义 (CRDs),这些定义了您可以在管道中创建和重用的构建块。Tekton CRDs 可以分为两大块:一组代表定义管道的任何元素,另一组代表管道执行。
如果这种元素分离对你来说似乎很困惑,可以考虑编程语言中类和实例的类比:类对象是一个概念的定义,而类的实例是在内存中的实际对象,具有特定的参数,并且可以被实例化多次。开发者类定义和创建两个类实例的过程如图 7.2 所示。

图 7.2 类定义与类实例
在安装 Tekton 之前,你需要创建一个 Kubernetes 集群并部署一个 Git 服务器和一个容器注册库。
7.2.1 安装先决条件
在终端窗口中运行以下命令以启动一个新的 minikube 实例。
列表 7.1 启动 minikube
minikube start -p argo --kubernetes-version='v1.19.0' --vm-driver='virtualbox' --memory=8196 ①
① 在 argo 配置文件下创建一个 Kubernetes 集群
在本章中,您需要一个具有对某些仓库写权限的 Git 仓库。为了避免依赖于外部服务(例如 GitHub 或 GitLab),您需要在 Kubernetes 集群中部署一个 Git 服务器 (gitea.io/en-us/)。
列表 7.2 部署 Gitea
kubectl apply -f https://gist.githubusercontent.com/
➥lordofthejars/1a4822dd16c2dbbafd7250bcb5880ca2/
➥raw/65ecee01462426252d124410ca0cc19afac382c3/
➥gitea-deployment.yaml ①
① 应用 Gitea 部署脚本
等待 Gitea 部署启动并运行。
列表 7.3 等待 Gitea 准备就绪
kubectl wait --for=condition=ready pod -l app=gitea-demo --timeout=90s ①
① 等待 Gitea 部署完成
Git 服务器可以通过 Kubernetes 集群中的 gitea DNS 名称访问。使用用户名 gitea 和密码 gitea1234,以推送章节中使用的源代码的权限注册新用户到系统中。
列表 7.4 创建 Gitea 用户
kubectl exec svc/gitea > /dev/null
-- gitea admin create-user --username gitea
➥--password gitea1234 --email gitea@gitea.com
➥--must-change-password=false ①
① 在 Gitea 容器中执行用户创建操作
最后,使用的源代码已从 GitHub 迁移到内部 Git 服务器。
列表 7.5 将应用程序迁移到 Gitea
kubectl exec svc/gitea > /dev/null -- curl -i -X POST -H
➥"Content-Type:application/json" -d '{"clone_addr":
➥"https://github.com/lordofthejars/kubernetes-secrets
➥-source.git","private": false,"repo_name": "kubernetes
➥-secrets-source","uid": 1}'http://gitea:gitea1234@localhost:3000/
➥api/v1/repos/migrate ①
① 在 Gitea 容器中执行 Git 仓库迁移
此外,还需要一个容器注册表来存储 CI 阶段构建的容器。为了避免依赖于外部服务,您需要在 Kubernetes 集群中部署一个容器注册表 (docs.docker.com/registry/)。
列表 7.6 安装 Docker 注册表
kubectl apply -f https://gist.githubusercontent.com/
➥lordofthejars/d386a28c07a54a6fd8717ce78a652b8b/raw/
➥a03b22afd549f8164dca2e38d6fab4fecfbc318a/
➥registry-deployment.yaml ①
① 应用 Docker 注册表部署脚本
等待注册表部署启动并运行。
列表 7.7 等待注册表准备就绪
kubectl wait --for=condition=ready pod -l
➥app=registry-demo --timeout=90s ①
① 等待 Docker 注册表部署完成
容器镜像由 Kubernetes 节点拉取,这意味着在 Kubernetes 服务中使用的 DNS 名称在物理机器(例如,在本例中的 minikube 节点)中是无效的。为了使推送到注册表的容器可以从节点拉取,您需要向 Kubernetes 节点添加一个包含 DNS 名称和 registry 服务 IP 的 /etc/hosts 条目。通过运行以下命令获取 registry 服务 IP。
列表 7.8 获取注册表 IP
kubectl get service/registry -o jsonpath=
➥'{.spec.clusterIP}' ①
10.111.129.197
① 获取服务 IP
然后访问 minikube 机器,并将以下条目添加到 /etc/hosts。
列表 7.9 将注册表 IP 注册到主机
minikube ssh -p argo
sudo -i
echo "10.111.129.197 registry" >> /etc/hosts ①
exit
exit
① 用正确的值替换 IP。
现在,您已准备好在 Kubernetes 集群中安装 Tekton。
7.2.2 安装 Tekton
通过应用列表 7.10 中的代码安装 Tekton 0.20.1。此命令将安装所有基于角色的访问控制 (RBAC)、自定义资源定义 (CRD)、ConfigMaps 和部署以使用 Tekton。
列表 7.10 安装 Tekton
kubectl apply --filename https://github.com/tektoncd/
➥pipeline/releases/download/v0.20.1/release.yaml ①
namespace/tekton-pipelines created
podsecuritypolicy.policy/tekton-pipelines created
clusterrole.rbac.authorization.k8s.io/
➥tekton-pipelines-controller-cluster-access created
clusterrole.rbac.authorization.k8s.io/
➥tekton-pipelines-controller-tenant-access created
clusterrole.rbac.authorization.k8s.io/
➥tekton-pipelines-webhook-cluster-access created
role.rbac.authorization.k8s.io/tekton-pipelines-controller created
role.rbac.authorization.k8s.io/tekton-pipelines-webhook created
role.rbac.authorization.k8s.io/tekton-pipelines-leader-election created
serviceaccount/tekton-pipelines-controller created
serviceaccount/tekton-pipelines-webhook created
clusterrolebinding.rbac.authorization.k8s.io/
➥tekton-pipelines-controller-cluster-access created
clusterrolebinding.rbac.authorization.k8s.io/
➥tekton-pipelines-controller-tenant-access created
clusterrolebinding.rbac.authorization.k8s.io/
➥tekton-pipelines-webhook-cluster-access created
rolebinding.rbac.authorization.k8s.io/tekton-pipelines-controller created
rolebinding.rbac.authorization.k8s.io/tekton-pipelines-webhook created
rolebinding.rbac.authorization.k8s.io/
➥tekton-pipelines-controller-leaderelection created
rolebinding.rbac.authorization.k8s.io/
➥tekton-pipelines-webhook-leaderelection created
customresourcedefinition.apiextensions.k8s.io/
➥clustertasks.tekton.dev created
customresourcedefinition.apiextensions.k8s.io/conditions.tekton.dev created
customresourcedefinition.apiextensions.k8s.io/
➥images.caching.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/pipelines.tekton.dev created
customresourcedefinition.apiextensions.k8s.io/
➥pipelineruns.tekton.dev created
customresourcedefinition.apiextensions.k8s.io/
➥pipelineresources.tekton.dev created
customresourcedefinition.apiextensions.k8s.io/runs.tekton.dev created
customresourcedefinition.apiextensions.k8s.io/tasks.tekton.dev created
customresourcedefinition.apiextensions.k8s.io/taskruns.tekton.dev created
secret/webhook-certs created
validatingwebhookconfiguration.admissionregistration.k8s.io/
➥validation.webhook.pipeline.tekton.dev created
mutatingwebhookconfiguration.admissionregistration.k8s.io/
➥webhook.pipeline.tekton.dev created
validatingwebhookconfiguration.admissionregistration.k8s.io/
➥config.webhook.pipeline.tekton.dev created
clusterrole.rbac.authorization.k8s.io/tekton-aggregate-edit created
clusterrole.rbac.authorization.k8s.io/tekton-aggregate-view created
configmap/config-artifact-bucket created
configmap/config-artifact-pvc created
configmap/config-defaults created
configmap/feature-flags created
configmap/config-leader-election created
configmap/config-logging created
configmap/config-observability created
configmap/config-registry-cert created
deployment.apps/tekton-pipelines-controller created
service/tekton-pipelines-controller created
horizontalpodautoscaler.autoscaling/tekton-pipelines-webhook created
poddisruptionbudget.policy/tekton-pipelines-webhook created
deployment.apps/tekton-pipelines-webhook created
service/tekton-pipelines-webhook created
① 应用 Tekton 部署脚本
提示:Tekton CLI 是用于与 Tekton 资源交互的命令行实用程序。尽管安装它不是强制性的,但它非常有帮助,尤其是在查看管道中发生的事情时。要安装它,请访问 github.com/tektoncd/cli/releases/tag/v0.16.0,下载您平台的包,解压缩它,并将 tkn 文件复制到 PATH 目录中,以便在任何地方都可以访问。
到目前为止你安装的整体情况如图 7.3 所示。

图 7.3 集群内部部署的服务(Tekton、SCM 和容器注册库)
到目前为止,你已经准备好开始在 Kubernetes 集群中学习和使用 Tekton 了。
7.2.3 Tekton 管道
总结来说,Tekton 提供了两组 Kubernetes 对象来定义和执行管道。第一组是用于定义组成 CI 管道的tasks和steps的 Kubernetes 对象集合。最重要的对象是Pipelines,它由Tasks组成,而Tasks又由Steps组成,如图 7.4 所示。

图 7.4 Tekton 元素之间的关系
第二组是用于实例化tasks和pipelines的 Kubernetes 对象集合。最重要的对象是PipelineRun、TaskRun和Triggers。Triggers在本书中没有涉及,因为它们超出了本书的范围,但可以简单地说,触发器可以通过外部事件(例如,对源仓库的提交)来执行pipeline。
PipelineResource
PipelineResource 是一个 Kubernetes 对象,它定义了一组用作task输入和输出参数的资源。输入资源的例子包括 Git 仓库和容器镜像。输出资源的例子包括容器镜像和文件。
要设置 Git 仓库的 URL,创建一个PipelineResource,将类型设置为git并将url参数设置为 Git 仓库位置,如下所示。
列表 7.11 build-resources.yaml
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: git-source
spec:
type: git ①
params:
- name: url ②
value: https://github.com/lordofthejars/kubernetes-secrets-source.git
① 将 PipelineResource 设置为 git 类型
② Git 资源有 url 配置参数。
新创建的PipelineResource命名为 git-source;你稍后会用到它。
Steps
step 代表pipeline中的操作;例如,一些steps包括编译应用程序、运行测试和构建 Linux 容器镜像。每个step都在提供的容器镜像内执行,并且任何step都可以挂载卷或使用环境变量。
step 在steps部分定义,其中设置step的名称、在step中使用的容器镜像以及在该容器内执行的命令。此外,你可以通过使用workingDir属性来设置命令运行的目录。以下是一个使用 Apache Maven 构建 Java 应用程序的示例。
列表 7.12 build-app-task.yaml
steps:
- name: maven-build
image: docker.io/maven:3.6-jdk-11-slim ①
command: ②
- mvn
args:
- clean
- package
workingDir: "/workspace/source/$(inputs.params.contextDir)" ③
① 使用 Maven docker 镜像来构建应用程序。
② 在容器内调用 Maven 命令。
③ 工作目录通过外部输入参数inputs.params.contextDir设置。
Tasks
task 是一个由一系列按顺序排列的steps组成的 Kubernetes 对象。每个task都在一个 Kubernetes Pod 中执行,在该 Pod 中运行一个容器。
由于 Pod 内的所有容器共享资源(例如,CPU、磁盘和内存),并且一个task由在同一个 Pod 中运行的几个步骤(容器)组成,因此一个步骤写入磁盘的内容可以在task的任何步骤内部访问。图 7.5 显示了所有这些元素是如何相互关联的。

图 7.5 Tekton 任务、Pod 和容器之间的关系
在spec部分配置了一个task,在那里你设置要执行的步骤列表和可选的配置参数,如输入参数、task所需的输入和输出资源以及卷。以下列表展示了如何注册上一节中定义的步骤,为workingDir属性定义输入参数,并在执行任何步骤之前定义一个 Git 类型的输入资源来克隆仓库的示例。
列表 7.13 build-app-task.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: build-app
spec:
params: ①
- name: contextDir
description: the context dir within source
default: . ②
resources:
inputs:
- name: source ③
type: git
steps:
- name: maven-build ④
...
① 注册了一个输入参数列表。在这种情况下,contextDir 参数由 maven-build 步骤需要来设置工作目录。
② 如果未在外部设置参数,则设置默认值
③ 使用名称 source 定义了 Git 输入资源。
④ 步骤定义
到达这一点后,你可能会有两个疑问:
-
你在哪里设置 Git 项目仓库?
-
项目被克隆在哪里?
为了回答第一个问题,Git 仓库是在一个PipelineResource对象中外部配置的。第二个问题更容易回答。内容被克隆到/workspace/name是在git类型中给出的输入名称值。因此,之前定义的 Git 资源被克隆到/workspace/source 目录中。
一个Task只是定义,或者执行步骤的描述。要执行它,你需要创建一个TaskRun对象。
TaskRun
TaskRun是一个 Kubernetes 对象,它在集群上实例化和执行 Tekton Task。TaskRun按照定义的顺序执行Task中定义的每个步骤,直到所有步骤都执行完毕。
要执行之前创建的build-appTask,你需要一个TaskRun对象。此对象将引用Task并使用特定值设置输入参数和资源,如下面的列表所示。
列表 7.14 build-app-task-run.yaml
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: build-app-run
spec:
params:
- name: contextDir ①
value: name-generator
resources:
inputs:
- name: source ②
resourceRef:
name: git-source
taskRef:
name: build-app ③
① 将 contextDir 参数设置为 name-generator 目录。
② 通过引用之前创建的 git-source PipelineResource 设置源资源。
③ 对 Task 的引用
注意:你可能想知道为什么contextDir需要设置为一个特定的值,而不是保留其默认值(仓库的根目录)。这是由于github.com/lordofthejars/kubernetes-secrets-source.git仓库的组织方式。如果你仔细查看目录层次结构,你会注意到仓库中包含在目录中的服务(名称和欢迎信息):
.
├── greetings
│ ├── README.md
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pom.xml
│ ├── src
│ └── target
├── name-generator ①
│ ├── README.md
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pipelines
│ ├── pom.xml
│ ├── src
│ └── target
└── welcome-message
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
├── target
└── vault-init
① 服务位置正在建设中
由于你正在构建名称生成器服务,因此将 Maven 的工作目录设置为 name-generator。
TaskRun 是执行单个任务的方式。有时你可能使用它们来执行或测试特定的任务,但大多数时候,你希望执行包含在其上的所有 tasks 的完整 pipeline。
Pipeline
Pipeline 是一个由一系列 tasks 组成的 Kubernetes 对象,这些 tasks 以有向无环图的形式连接。在 Pipeline 定义中,你可以完全控制 tasks 的执行顺序和条件,这使得可以设置并行运行 tasks 的扇入/扇出场景,或者设置在执行之前应该满足的条件。
现在创建一个简单的 Pipeline,使用上一节中创建的 build-appTask。与 tasks 一样,Pipeline 也可以有输入参数和输入资源,这使得 pipeline 可扩展。对于这个特定的例子,只有输入参数(Git 资源)可以从 pipeline 外部进行配置,而 contextDir 参数的值是在 task 中硬编码的。最后,build-apptask 被注册为具有输入参数和资源的 pipeline task。Pipeline 定义应类似于以下列表所示。
列表 7.15 pipeline-name-app.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: name-generator-deployment
spec:
resources:
- name: appSource ①
type: git
tasks:
- name: build-app
taskRef:
name: build-app ②
params: ③
- name: contextDir
value: name-generator
resources:
inputs: ④
- name: source
resource: appSource
① 定义一个 Git 类型的输入资源
② 将 build-app 任务注册到当前 pipeline
③ 为任务输入参数设置一个静态值
④ 将 Pipeline 定义中的 Git 资源链接到 Task
到目前为止,你已经看到了如何使用 Tekton 定义 CI 管道,但还没有发生执行,因为管道需要被实例化,并且需要提供输入参数和资源。在下一节中,你将看到如何执行 Tekton pipeline。
PipelineRun
PipelineRun 是一个 Kubernetes 对象,用于在集群上实例化和执行 Tekton Pipeline。PipelineRun 执行 Pipeline 中定义的每个 tasks,并为每个 tasks 自动创建一个 TaskRun,如下所示。
列表 7.16 pipeline-run-name-app.yaml
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: build-app-pipeline
spec:
resources:
- name: appSource ①
resourceRef:
name: git-source
pipelineRef:
name: name-generator-deployment ②
① 通过引用 git-source PipelineResource 设置 appSource 资源。
② 对 pipeline 的引用
图 7.6 总结了基本的 Tekton 元素以及它们之间的关系。

图 7.6 PipelineRun 与 Tekton 资源之间的关系
你现在已经看到了构建基本 CI 管道最重要的 Tekton 资源,但这还远非一个真正的管道。
7.3 欢迎消息的持续集成
在 Kubernetes 中,一个真正的 CI 管道至少需要以下步骤:
-
使用 Tekton Git 资源检出代码
-
在 Tekton
步骤中定义一个 Apache Maven 容器以构建和测试应用程序 -
将容器注册表的凭证设置为 Kubernetes Secrets,并在 Tekton
步骤中定义一个 Buildah 容器以构建和推送容器
本章中使用的应用程序是一个由两个服务组成的简单服务架构,这两个服务生成欢迎信息:
- 名称生成服务——一个从名称列表中随机选择名称的服务,如下面的列表所示。
列表 7.17 NameGeneratorResource.java
@Path("/generate")
public class NameGeneratorResource {
private static final String[] NAMES = new String[] {
"Ada", "Alexandra", "Burr", "Edson", "Kamesh", "Sebi", "Anna", "Gavina"①
};
private static final Random r = new Random();
@GET
@Path("/name")
@Produces(MediaType.TEXT_PLAIN)
@RolesAllowed("Subscriber") ②
public String generate() {
return NAMES[generateRandomIndex()]; ③
}
}
① 名称列表
② 保护该方法,以确保只有具有订阅者角色的用户才能访问
③ 生成随机名称((CO18-3))
- 欢迎信息服务——一个从数据库中随机选择欢迎信息并将问候的人的姓名委托给名称服务的服务,如下面的列表所示。
列表 7.18 WelcomeResource.java
@Path("/welcome")
public class WelcomeResource {
@RestClient
NameService nameService; ①
@ConfigProperty(name = "name-service-token") ②
String token;
private static Random r = new Random();
@GET
@Path("/message")
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
String welcomeMessage = randomMessage
➥(Welcome.listAll()); ③
String name = nameService.generateName
➥("Bearer " + token); ④
return welcomeMessage + " " + name;
}
}
① 与名称生成服务通信的接口
② 访问名称生成服务所需的 API 密钥
③ 从数据库中查找所有欢迎信息
④ 通过传递 API 密钥向名称生成服务发送请求
图 7.7 显示了应用程序的概述。

图 7.7 名称服务和欢迎服务之间交互的概述
为了最大化安全性,以下考虑因素被采纳:
-
您需要提供一个 API 密钥来访问名称生成服务。此 API 密钥是秘密的,并存储在 HashiCorp Vault 实例中。
-
欢迎信息服务的数据库凭据由 HashiCorp Vault 动态数据库凭据管理。
-
服务使用 Kubernetes 身份验证方法对 HashiCorp Vault 进行身份验证。
图 7.8 显示了这些元素的概述。

图 7.8 安全元素
我们假设您对 CI/CD 有一些经验,以及 Git 和 Linux 容器的基本知识。本章中描述的原则适用于您最终可能选择的技术。
Tekton 和 Kubernetes 元素的管道执行在图 7.9 中显示。创建了一个包含三个容器的 Pod;第一个容器从 Gitea 服务器克隆项目,项目在第二个容器中打包,服务所在的 Linux 容器在第三个容器中构建并推送到容器注册库。执行命令的三个容器在图 7.9 中显示。

图 7.9 欢迎信息服务 Pod 内运行的容器列表
这些步骤都实现为 Tekton step。让我们在以下章节中实现它们。
7.3.1 编译和运行测试
您已经在上一节中看到了如何使用 Apache Maven 编译和运行测试。欢迎信息服务是用 Java 开发的,Apache Maven 用作构建工具。
列表 7.19 构建服务 Tekton step
- name: maven-build
image: docker.io/maven:3.6.3-jdk-11-slim
command:
- mvn
args:
- -DskipTests
- clean
- install
workingDir: "/workspace/source/$(inputs.params.contextDir)"
7.3.2 构建和推送容器镜像
在运行中的容器内构建容器镜像(记住每个 step 都是在容器内执行的)有些复杂,因为需要 Docker 守护进程来构建容器镜像。为了避免处理 Docker-inside-Docker 问题,或者在不方便运行 Docker 主机(如 Kubernetes 集群)的环境中构建容器镜像,有一些无 Docker 工具允许在不依赖 Docker 守护进程的情况下构建容器镜像。例如,Buildah (buildah.io/) 是一个在容器内从 Dockerfile 构建容器镜像的工具,无需 Docker 守护进程。
在列表 7.20 中显示的 step 定义中,使用 Buildah 构建并推送欢迎信息容器到容器注册库。容器名称以 registry:group:name:tag 的形式提供,Dockerfile 的位置作为参数提供。
列表 7.20 构建并推送容器镜像 Tekton step
- name: build-and-push-image
image: quay.io/buildah/stable
script: | ①
#!/usr/bin/env bash
buildah bud --layers -t $DESTINATION_IMAGE $CONTEXT_DIR ②
buildah push --tls-verify=false $DESTINATION_IMAGE docker:
➥//$DESTINATION_IMAGE ③
env: ④
- name: DESTINATION_IMAGE
value: "$(inputs.params.imageName)"
- name: CONTEXT_DIR
value: "/workspace/source/$(inputs.params.contextDir)"
securityContext: ⑤
runAsUser: 0
privileged: true
volumeMounts: ⑥
- name: varlibc
mountPath: /var/lib/containers
① 可以在步骤定义中嵌入脚本。
② 构建容器镜像
③ 推送容器镜像
④ 参数设置为环境变量,因此在脚本执行期间可以访问它们。
⑤ 运行 Buildah 需要以 root 用户身份运行并启用权限提升。
⑥ Linux 容器层保存在本地文件系统中的 /var/lib/containers 目录下。
创建一个名为 welcome-service-task.yaml 的新文件,包含之前定义的 steps,如下所示。
列表 7.21 welcome-service-task.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: welcome-service-app ①
spec:
params: ②
- name: contextDir ③
description: the context dir within source
default: .
- name: imageName ④
description: the container name url-group-artifact-tag
resources:
inputs:
- name: source
type: git ⑤
steps:
- name: maven-build ⑥
image: docker.io/maven:3.6.3-jdk-11-slim
command:
- mvn
args:
- clean
- install
workingDir: "/workspace/source/$(inputs.params.contextDir)"
- name: build-and-push-image ⑦
image: quay.io/buildah/stable
script: |
#!/usr/bin/env bash
buildah bud --layers -t $DESTINATION_IMAGE $CONTEXT_DIR
buildah push --tls-verify=false $DESTINATION_IMAGE docker:
➥//$DESTINATION_IMAGE
env:
- name: DESTINATION_IMAGE
value: "$(inputs.params.imageName)"
- name: CONTEXT_DIR
value: "/workspace/source/$(inputs.params.contextDir)"
securityContext:
runAsUser: 0
privileged: true
volumeMounts:
- name: varlibc
mountPath: /var/lib/containers
volumes:
- name: varlibc
emptyDir: {}
① 任务名称
② 任务参数
③ 仓库内的源代码相对路径
④ 容器镜像名称
⑤ 从 Git 服务器克隆仓库
⑥ 使用 Maven 打包步骤
⑦ 创建容器镜像步骤
执行以下命令将 Task 注册到 Kubernetes 集群。
列表 7.22 注册任务
kubectl apply -f welcome-service-task.yaml ①
① 注册 Tekton 任务定义
7.3.3 管道资源
欢迎信息服务的仓库存储在 Kubernetes 集群中部署的本地 Git 服务器(Gitea)上。在 PipelineResource 中设置服务的 Git 位置。创建一个名为 welcome-service-resource.yaml 的新文件,如下所示。
列表 7.23 welcome-service-resource.yaml
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: welcome-service-git-source
spec:
type: git
params:
- name: url
value: http://gitea:3000/gitea/
➥kubernetes-secrets-source.git ①
① 指向内部仓库
执行以下命令将 PipelineResource 注册到 Kubernetes 集群。
列表 7.24 注册 pipeline 资源
kubectl apply -f welcome-service-resource.yaml ①
① 注册 Tekton 管道资源
7.3.4 管道
最后一步是定义一个 pipeline 来实现欢迎信息服务的 CI 管道。创建一个名为 welcome-service-pipeline.yaml 的新文件,如下所示。
列表 7.25 welcome-service-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: welcome-deployment
spec:
resources:
- name: appSource
type: git
params:
- name: imageTag
type: string
description: image tag
default: v1
tasks:
- name: welcome-service-app
taskRef:
name: welcome-service-app
params:
- name: contextDir
value: welcome-message
- name: imageName
value: "registry:5000/k8ssecrets/
➥welcome-message:$(params.imageTag)"
resources:
inputs:
- name: source
resource: appSource
执行以下命令将 Pipeline 注册到 Kubernetes 集群。
列表 7.26 注册 Pipeline
kubectl apply -f welcome-service-pipeline.yaml ①
① 注册 Tekton 管道定义
图 7.10 显示了 Pipeline 和 Task 参数之间的关系。

图 7.10 Pipeline 和 Task 参数之间的关系
7.3.5 PipelineRun
创建一个 PipelineRun 来触发之前步骤中定义的 welcome-deploymentpipeline。在这个 PipelineRun 中,除了设置 Git 仓库位置外,还提供了容器镜像标签。
列表 7.27 welcome-service-pipeline-run.yaml
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: welcome-pipeline-run
spec:
params:
- name: imageTag
value: "1.0.0" ①
resources:
- name: appSource
resourceRef:
name: welcome-service-git-source
pipelineRef:
name: welcome-deployment
① 将镜像标签设置为版本 1.0.0。
图 7.11 显示了 PipelineRun 和 PipelineResource 参数之间的关系。

图 7.11 PipelineRun 和 PipelineResource 之间的关系
执行以下命令以触发 Pipeline 进入 Kubernetes 集群。
列表 7.28 注册 PipelineRun
kubectl apply -f welcome-service-pipeline-run.yaml ①
① 启动管道
在这一点上,对于 Pipeline 对象 tasks 部分中定义的每个 task,都会自动创建并执行一个 TaskRun。要列出它们,请在终端窗口中运行以下命令。
列表 7.29 列出所有 TaskRuns
tkn tr list ①
① 列出所有 TaskRuns
输出提供了一个列表,列出了在 Kubernetes 集群中执行的所有 TaskRuns 及其状态:
NAME STARTED DURATION STATUS
welcome-pipeline-run-welcome-service-app-l2zns 1 minute ago --- Running
tkn 允许我们检查 TaskRun 的日志,并在出现故障时找到错误原因。在终端中运行以下命令,使用 -f 选项以流式传输当前执行的实时日志。
列表 7.30 从 PipelineRun 流式传输日志
tkn tr logs welcome-pipeline-run-welcome-service-app-l2zns -f ①
① 将 TaskRun ID 更改为前一个命令中显示的正确 ID
你将在控制台看到 pipeline 的日志:
[maven-build] Downloaded from central: https://repo.maven.apache.org/ ①
➥maven2/io/quarkus/quarkus-narayana-jta-deployment/1.11.3.Final/
➥quarkus-narayana-jta-deployment-1.11.3.Final.jar (8.4 kB at 19 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/io/
➥quarkus/quarkus-agroal-deployment/1.11.3.Final/
➥quarkus-agroal-deployment-1.11.3.Final.jar (13 kB at 30 kB/s)
➥[maven-build] Downloading from central: https://repo.maven.apache.org/
➥maven2/io/quarkus/quarkus-hibernate-orm-deployment/1.11.3.Final/
➥quarkus-hibernate-orm-deployment-1.11.3.Final.jar
... ②
[build-and-push-image] STEP 11: ENTRYPOINT [ "/deployments/run-java.sh" ]
[build-and-push-image] STEP 12: COMMIT test.org/k8ssecrets/
➥welcome-message:1.0.0
[build-and-push-image] --> abab5f4192b
[build-and-push-image] abab5f4192b3a5d9317419d61553d91baf0dfc4df16
➥b9ad58d2489f71ee0a30a ③
[build-and-push-image] Getting image source signatures
[build-and-push-image] Copying blob sha256:f0b7ce40f8b0d5a8e10eecc86
➥06f43a8bfbb48255da7d1ddc5e3281434f33b20
[build-and-push-image] Copying blob sha256:ba89bf93365092f038be159229ea
➥fbbc083ff8ffdfd2007e24c4c612e82871ee
[build-and-push-image] Copying blob sha256:04a05557bbadc648beca5cf01b71
➥b152ce7890a454381877144ac7e63b968874
[build-and-push-image] Copying blob sha256:821b0c400fe643d0a9f146c9ab8
➥ec12d8abe59eddd00796673b3154005515b26
[build-and-push-image] Copying blob sha256:7a6b87549e30f9dd8d25021fef3
➥c15626617941f83322ba5f6b1988cade6b1cf
[build-and-push-image] Copying config sha256:abab5f4192b3a5d9317419d61
➥553d91baf0dfc4df16b9ad58d2489f71ee0a30a
[build-and-push-image] Writing manifest to image destination
[build-and-push-image] Storing signatures
① 启动 Maven 进程以构建服务。
② Buildah 构建容器镜像。
③ 将容器镜像推送到容器注册库
记住,Task 作为 Pod 执行,每个 step 都在该 Pod 内的容器中执行。这可以在运行以下命令时看到。
列表 7.31 获取所有 Pods
kubectl get pods ①
NAME READY STATUS RESTARTS AGE
welcome-pipeline-run-welcome- 0/3 Completed 0 6m22s
➥service-app-l2zns-pod-98b2l
① 获取管道执行期间创建的所有 Pods
由于 welcome-service-apptask 由三个 steps(Git 克隆、Maven 构建、Docker 构建/推送)组成,在 task 执行期间创建了三个容器,如 READY 列中所示。
当容器镜像发布到容器注册库时,CI 管道周期结束。但服务尚未部署,也尚未发布到 Kubernetes 集群。在下一章中,你将看到如何使用持续部署和 GitOps 方法将服务部署和发布到集群。
摘要
-
Kubernetes Secrets 既可以用于应用程序代码(例如,用户名、密码和 API 密钥),也可以用于 CI 管道(例如,外部服务的用户名和密码)。
-
持续集成密钥需要得到保护,就像其他任何密钥一样。你可以在 Tekton 和 Argo CD 中使用
SealSecrets将加密的密钥存储在 Git 中。启用 Kubernetes 数据加密以存储 Kubernetes 中的加密密钥。 -
Tekton 是实现 CI 管道的 Kubernetes 原生平台。
-
Git 被用作单一的事实来源——不仅用于源代码,也用于
pipeline脚本。
8 Kubernetes 原生持续交付和机密信息
本章涵盖
-
介绍持续交付和部署方法
-
使用 GitOps 方法实现 Kubernetes 原生持续部署管道
-
展示 ArgoCD 作为实现 GitOps 的 Kubernetes 原生解决方案
在上一章中,你了解了如何在 CI 阶段管理机密信息,构建应用程序,创建容器镜像,并将其发布到容器注册库。但是,服务尚未部署或发布到 Kubernetes 集群。在本章中,你将了解如何安全地交付应用程序。
在本章中,你将了解如何使用持续部署和 GitOps 方法将服务部署和发布到 Kubernetes 集群,使用 Argo CD 快速交付高质量应用程序,同时在整个管道中正确管理机密信息,以防止在开发阶段发生泄露。
8.1 持续交付和部署简介
持续交付是一种涉及更快、更频繁发布软件的方法。这种方法有助于降低可能影响用户体验的更改交付的成本、时间和风险。由于应用程序的交付是持续进行的,并且是增量更新,因此更容易从最终用户那里收集反馈并相应地做出反应。
CD 的核心概念是部署管道。正如其名所示,它是一系列步骤或程序,应用程序必须通过这些步骤或程序才能发布到生产环境。部署管道可能会根据你在发布应用程序时选择遵循的过程而改变,但一个典型的管道通常由以下阶段组成:
-
提交阶段——发布流程的第一部分,在团队成员向 SCM 服务器提交内容后触发。这一阶段是前一章中展示的持续集成阶段。
-
验收测试——这一阶段测试应用程序是否满足业务方面的预期。其中一些测试可能是自动的,但也有一些不是,例如探索性测试。
-
发布——基于每个阶段的反馈,关键用户决定是否将版本发布到生产环境或放弃该版本。
注意,持续交付中的发布流程意味着进行实际发布的手动决策。另一方面,持续部署在构建成功后自动将每个更改发布到生产环境。图 8.1 显示了 CI/CD 管道的阶段。

图 8.1 持续集成与持续交付阶段对比
在以下章节中,你将专注于服务的发布流程,自动将其部署到 Kubernetes 集群,并使用 DevOps 方法保护涉及到的机密信息。
8.2 欢迎信息的持续交付
在本章中,你将部署与上一章相同的应用程序到 Kubernetes 集群中。它从 CI 阶段结束的地方(将欢迎信息容器推送到容器注册库)继续,并将其交付到生产环境中。提醒一下,图 8.2 展示了应用程序及其组成部分的概述。

图 8.2 欢迎和名称服务之间交互的概述
现在你已经对要部署的应用程序有了概述,展示部署它的 Kubernetes 资源文件。
8.2.1 部署名称生成器服务
本章重点介绍在欢迎信息服务中应用 CD 原则,以使用 GitOps 方法自动将其部署到 Kubernetes 集群中。虽然这是一个特定服务的示例,但相同的方法可以应用于任何其他服务,包括支付服务、股票服务或用户管理服务。为了保持简单,使用以下部署文件手动部署名称生成器服务。
列表 8.1 src/main/kubernetes/deployment.yml
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: name-generator
app.kubernetes.io/version: 1.0.0
name: name-generator ①
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app.kubernetes.io/name: name-generator
app.kubernetes.io/version: 1.0.0
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/version: 1.0.0
app.kubernetes.io/name: name-generator
name: name-generator
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/version: 1.0.0
app.kubernetes.io/name: name-generator
template:
metadata:
labels:
app.kubernetes.io/version: 1.0.0
app.kubernetes.io/name: name-generator
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/lordofthejars/name-generator:1.0.0 ②
imagePullPolicy: Always
name: name-generator
ports:
- containerPort: 8080
name: http
protocol: TCP
① 为名称生成器服务创建 Kubernetes 服务
② 部署名称生成器容器
创建部署文件后,运行以下命令。
列表 8.2 部署名称生成器服务
kubectl apply -f src/main/kubernetes/deployment.yml ①
service/name-generator created
deployment.apps/name-generator created
① 为名称生成器服务创建服务和部署
当名称生成器服务运行起来后,使用 GitOps 方法部署和发布欢迎信息服务。
8.2.2 DevOps 和 GitOps
DevOps 是一套自动化并帮助整合软件开发和 IT 团队流程的实践,以便更快、更可靠地构建、测试和发布应用程序。DevOps 不仅关乎开发者和运维团队;它涉及整个组织。每个团队都应从规划阶段开始,直到应用程序在生产环境中发布(图 8.3)成为软件生命周期的一部分。

图 8.3 DevOps 生命周期
GitOps 是一种实现 DevOps 方法的方式,基于 Git 是唯一真相来源的假设。不仅应用程序的源代码存储在 Git 中,构建应用程序和基础设施代码的脚本以及用于发布和更新应用程序的管道定义也存储在 Git 中。这意味着应用程序的所有部分都是版本化的、分叉的,当然,它们也可以被审计。
总结来说,GitOps 原则包括以下内容:
-
Git 是唯一的真相来源。
-
将一切视为代码。
-
运维是通过 Git 工作流程产生的。
GitOps 的重要方面之一是,与基础设施相关的任何 Git 仓库更新都必须触发环境的更新,以满足应用程序的期望状态。当期望状态(在 Git 仓库中设置)与观察状态(集群中的实际状态)之间存在差异时,收敛机制将被执行,以将观察状态驱动到版本控制中定义的期望状态。有两种方式可以导致差异:
-
如果手动更新 Kubernetes 集群,期望状态和观察状态将不同,收敛机制将更新 Kubernetes 集群到 Git 中定义的期望状态。
-
如果在 Git 中更新了文件(例如,需要发布新的容器镜像),期望状态和观察状态将不同,Kubernetes 集群状态将更新到 Git 中定义的新状态。
让我们看看如何使用 Argo CD,一个 Kubernetes 的 GitOps 持续交付工具,通过 Git 更新 Kubernetes 集群。
8.3 Argo CD
现在是时候使用 Argo CD 和 GitOps 方法将欢迎信息服务部署到 Kubernetes 集群了。如前所述,应用程序的源代码以及构建应用程序的脚本和管道定义,以及发布和更新应用程序的基础设施代码都存储在 Git 中。
Argo CD 有三个主要组件:
-
API 服务器——ArgoCD 后端将 API 暴露给 Web UI、CLI 或任何其他系统。该组件的主要职责是应用程序管理、安全问题和管理 Git webhook 事件。
-
仓库服务器——这是一个内部服务,用于维护 Git 仓库的本地缓存。
-
应用程序控制器——这是一个 Kubernetes 控制器,它持续监控 Git 仓库中放置的清单,并将其与集群上的实时状态进行比较。可选地,它可以采取纠正措施。
图 8.4 展示了这些部分及其相互关系。

图 8.4 ArgoCD 元素与 Kubernetes 集群之间部署应用程序的交互
8.3.1 安装 ArgoCD
要安装 ArgoCD 1.8.7,创建一个新的 Kubernetes 命名空间,并将 Argo CD 资源应用到 Kubernetes 集群中,如下所示。
列表 8.3 安装 ArgoCD
kubectl create namespace argocd ①
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/
➥argo-cd/v1.8.7/manifests/install.yaml ②
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io
➥created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io
➥created
serviceaccount/argocd-application-controller created
serviceaccount/argocd-dex-server created
serviceaccount/argocd-redis created
serviceaccount/argocd-server created
role.rbac.authorization.k8s.io/argocd-application-controller created
role.rbac.authorization.k8s.io/argocd-dex-server created
role.rbac.authorization.k8s.io/argocd-redis created
role.rbac.authorization.k8s.io/argocd-server created
clusterrole.rbac.authorization.k8s.io/argocd-application-controller created
clusterrole.rbac.authorization.k8s.io/argocd-server created
rolebinding.rbac.authorization.k8s.io/argocd-application-controller created
rolebinding.rbac.authorization.k8s.io/argocd-dex-server created
rolebinding.rbac.authorization.k8s.io/argocd-redis created
rolebinding.rbac.authorization.k8s.io/argocd-server created
clusterrolebinding.rbac.authorization.k8s.io/argocd-application-controller
➥created
clusterrolebinding.rbac.authorization.k8s.io/argocd-server created
configmap/argocd-cm created
configmap/argocd-gpg-keys-cm created
configmap/argocd-rbac-cm created
configmap/argocd-ssh-known-hosts-cm created
configmap/argocd-tls-certs-cm created
secret/argocd-secret created
service/argocd-dex-server created
service/argocd-metrics created
service/argocd-redis created
service/argocd-repo-server created
service/argocd-server created
service/argocd-server-metrics created
deployment.apps/argocd-dex-server created
deployment.apps/argocd-redis created
deployment.apps/argocd-repo-server created
deployment.apps/argocd-server created
statefulset.apps/argocd-application-controller created
① 创建 argocd 命名空间
② 从 argocd 命名空间中的官方资源安装 Argo CD
TIP argocd CLI 是一个用于与 Argo CD 交互的命令行工具。要安装它,只需访问 github.com/argoproj/argo-cd/releases/tag/v1.7.14,下载您平台上的包,解压缩存档,并将 argocd 文件复制到 PATH 目录中,这样就可以从任何目录访问它。
接下来,通过使用列表 8.4 中所示的补丁命令,将 Argo CD Kubernetes 服务更改为LoadBalancer类型以暴露Argo CD 服务器。
列表 8.4 暴露 ArgoCD 服务器
kubectl patch svc argocd-server -n argocd -p
➥'{"spec": {"type": "LoadBalancer"}}' ①
① 将 Kubernetes 服务类型更改为 LoadBalancer
要使用argocd CLI 工具,你需要外部 IP 和暴露的 Argo CD 服务器端口。你可以通过运行以下列表中的命令来获取它们。
列表 8.5 Argo CD 访问 IP 和端口
IP=$(minikube ip -p argo) ①
PORT=$(kubectl get service/argocd-server -n argocd
➥-o jsonpath="{.spec.ports[?(@.port==443)].nodePort}") ②
① 设置访问 Argo CD 服务器的 IP
② 设置暴露的端口以访问 Argo CD 服务器
在配置 Argo CD 之前,最后一步是使用 CLI 工具登录到 Argo CD 服务器。默认情况下,用户名为admin,初始密码是自动生成的 Argo CD API 服务器 Pod 名称。密码可以通过以下列表中的命令检索。
列表 8.6 获取 Argo CD 密码
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server
➥-o name | cut -d'/' -f 2 ①
① 获取 Argo CD Pod 名称
运行如下所示的login命令,使用在最后一步检索到的admin用户名和密码登录到 Argo CD 服务器。
列表 8.7 ArgoCD 登录
argocd login $IP:$PORT ①
① 登录到 Argo CD。
8.3.2 欢迎服务与 GitOps
在 7.2.1 节中安装 Gitea 时,本章所需源代码的 Git 仓库已迁移到内部。此仓库包含两个源目录:每个服务一个,还有一个名为welcome-message-gitops的目录,其中放置了与欢迎信息服务和 GitOps 定义相关的所有 Kubernetes YAML 文件。仓库布局如下所示。
列表 8.8 仓库布局
├── name-generator ①
│ ├── README.md
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pipelines
│ ├── pom.xml
│ ├── src
│ └── target
├── welcome-message ②
│ ├── Dockerfile
│ ├── README.md
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pom.xml
│ ├── src
│ ├── target
│ └── vault-init
└── welcome-message-gitops ③
├── apps ④
├── cluster ⑤
└── gitops ⑥
① 名称生成器服务源代码
② 欢迎信息服务源代码
③ GitOps 文件
④ 欢迎信息服务部署文件
⑤ 单次操作部署文件
⑥ Argo CD 定义
welcome-message-gitops目录由三个目录组成:
-
应用程序—发布欢迎信息服务到 Kubernetes 的部署 YAML 文件
-
集群—部署欢迎信息服务(PostgreSQL 和 Vault)所需的外部依赖项的 YAML 文件
-
GitOps—在 Argo CD 中注册应用程序所需的 YAML 文件
welcome-message-gitops
├── apps
├── cluster
└── gitops
应用程序
应用程序文件夹包含将服务部署到 Kubernetes 集群所需的部署文件。在这种情况下,有两个标准的 Kubernetes Deployment和Service资源。
welcome-message-gitops
├── apps
│ └── app.yml
│ └── service.yml
要部署欢迎信息服务,创建包含以下列表中内容的apps.yaml文件。
列表 8.9 apps/apps.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: welcome-message
name: welcome-message
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: welcome-message
template:
metadata:
labels:
app.kubernetes.io/name: welcome-message
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/lordofthejars/welcome-message:1.0.0 ①
imagePullPolicy: Always
name: welcome-message
ports:
- containerPort: 8080
name: http
protocol: TCP
① 部署欢迎信息的第一个版本
创建一个service.yml文件,使其内容如下所示,以便使欢迎信息可访问。
列表 8.10 apps/service.yml
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: welcome-message
name: welcome-message
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app.kubernetes.io/name: welcome-message
type: LoadBalancer
集群
集群文件夹包含部署和配置服务所需的所有 YAML 文件,在这种情况下,作为数据库的 PostgreSQL 和作为秘密管理系统 HashiCorp Vault:
welcome-message-gitops
├── cluster
│ ├── postgresql.yaml
│ ├── vault-job.yaml
│ ├── vault-secrets.yaml
│ └── vault.yaml
postgresql.yaml 和 vault.yaml 文件是部署这两个服务到 Kubernetes 集群的标准部署文件,但还有两个文件需要解释。vault-secrets.yaml 文件是一个 Kubernetes Secret 对象,包含需要存储到 HashiCorp Vault 并由应用程序使用的机密。在这种情况下,这些是登录 HashiCorp Vault 的令牌和 Welcome 服务用于验证 Name Generator 服务的 API 令牌。文件的部分内容如下所示。
列表 8.11 cluster/vault-secrets.yml
apiVersion: v1
kind: Secret
metadata:
name: vault-secrets
type: Opaque
data:
VAULT_LOGIN: cm9vdA== ①
NAME_SERVICE_TOKEN: ZXlKcmFXUWlPa........ ②
① HashiCorp Vault 访问令牌
② API 密钥
vault-job.yaml 文件是一个 Kubernetes Job 对象,用于配置 HashiCorp Vault 实例。它在部署 HashiCorp Vault 之后应用,启用 Kubernetes 认证模式和数据库动态机密,配置策略,并将 API 令牌添加到键值机密存储中。文件的部分内容如下所示。
列表 8.12 cluster/vault-job.yml
apiVersion: batch/v1
kind: Job
metadata:
name: init-vault
annotations:
argocd.argoproj.io/hook: PostSync ①
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
volumes:
- name: vault-scripts-volume
configMap:
name: vault-scripts
defaultMode: 0777
containers:
- name: init-vault
image: vault:1.6.2
envFrom:
- secretRef: ②
name: vault-secrets
volumeMounts:
- mountPath: /vault-scripts ③
name: vault-scripts-volume
command:
- /bin/ash ④
- -c
- |
export VAULT_ADDR=http://vault:8200
vault login $VAULT_LOGIN
vault auth enable kubernetes
vault secrets enable database
vault write database/config/mydb
➥plugin_name=postgresql-database-plugin
➥allowed_roles=mydbrole ...
vault write database/roles/mydbrole db_name=mydb ...
vault policy write vault-secrets-policy
➥/vault-scripts/vault-secrets-policy.hcl
vault kv put secret/myapps/welcome/config
➥name-service-token=$NAME_SERVICE_TOKEN
restartPolicy: Never
backoffLimit: 2
① 该文件在部署 HashiCorp Vault 之后应用。
② 机密作为环境变量注入。
③ 将 HashiCorp Vault 配置文件挂载为卷。
④ 配置 HashiCorp Vault 的作业命令
重要的是,为了保持简单,vault-secrets.yaml 是一个标准的 Kubernetes Secrets 文件,但它应该以第三章中解释的任何一种方式得到保护。
Gitops
gitops 文件夹包含所有用于将前面的文件夹注册为 Argo CD 应用程序的文件。
welcome-message-gitops
└── gitops
├── apps-ops.yaml ①
└── cluster-ops.yaml ②
① 配置 Argo CD 以监控 apps 目录上的任何更改
② 配置 Argo CD 以监控集群目录上的任何更改
在下一节中,您将更详细地了解这些文件。
提示:您可以将应用程序和外部依赖项部署文件放入同一目录中。然而,我们的建议是将通常会更改的文件与很少更改的文件分开。
8.3.3 从 Git 仓库创建欢迎信息服务
Argo CD 的 应用程序 是一组 Kubernetes 资源,用于将应用程序部署到目标环境并保持其处于期望状态。应用程序在 Argo CD 自定义资源定义 (CRD) 文件中定义,其中您指定参数,如 Git 仓库、Kubernetes 资源路径或应用程序将要部署的目标。图 8.5 显示了 cluster-ops 和 apps-ops Argo CD 应用程序部署的架构。

图 8.5 Argo CD 应用程序部署
列表 8.13 展示了如何定义一个 Argo CD 应用程序,该程序克隆 Gitea 中定义的 Git 仓库,监听 welcome-message-gitops/apps 目录中的任何更改,并将这些更改应用到满足期望状态。正如我们之前所说的,apps 目录包含将欢迎信息服务部署到 Kubernetes 集群所需的所有资源。在 welcome-message-gitops/gitops 文件夹中创建 apps-ops.yaml 文件。
列表 8.13 welcome-message-gitops/gitops/apps-ops.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: welcome-cluster-apps
namespace: argocd
spec:
project: default
syncPolicy: ①
automated:
prune: true ②
selfHeal: true ③
source:
repoURL: http://gitea.default.svc:3000/gitea/
➥kubernetes-secrets-source.git ④
targetRevision: HEAD
path: welcome-message-gitops/apps ⑤
destination: ⑥
server: https://kubernetes.default.svc
namespace: default
① 自动同步资源
② 当 Argo CD 检测到 Git 中不再定义资源时删除资源
③ 当实时集群的状态漂移时,对 Git 中定义的值执行自动同步。
④ 设置 Git 仓库 URL
⑤ 应用清单的目录
⑥ 应用清单的目标集群
以类似的方式,将集群目录添加为 Argo CD 应用。在 welcome-message-gitops/gitops 文件夹中创建 cluster-ops.yaml 文件。
列表 8.14 welcome-message-gitops/gitops/cluster-ops.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: welcome-cluster-ops
namespace: argocd
spec:
project: default
syncPolicy:
automated:
prune: true
selfHeal: true
source:
repoURL: http://gitea.default.svc:3000/gitea/
➥kubernetes-secrets-source.git
targetRevision: HEAD
path: welcome-message-gitops/cluster ①
destination:
server: https://kubernetes.default.svc
namespace: default%
① 外部依赖的清单目录
将 cluster-ops.yaml 资源应用到安装和配置 Welcome Message 服务所需的外部依赖。在终端窗口中,运行以下列表中显示的命令。
列表 8.15 注册集群应用
kubectl apply -f welcome-message-gitops/gitops/
➥cluster-ops.yaml ①
① 注册集群应用
上次执行将集群目录注册为 Argo CD 应用。由于是第一次,并且 syncPolicy 参数设置为 automated,Argo CD 会自动应用那里定义的资源。通过以下列表中显示的命令获取默认命名空间中的 Pods 来验证 PostgreSQL 和 Vault 是否已部署。
列表 8.16 获取所有 pods
kubectl get pods -n default
NAME READY STATUS RESTARTS AGE
gitea-deployment-7fbbf9c8b-bbcjq 1/1 Running 1 2d17h
name-generator-579ccdc5d5-mhzft 1/1 Running 2 2d23h
postgresql-59ddd57cb6-tjrg2 1/1 Running 0 6m30s ①
registry-deployment-64d49ff847-hljg9 1/1 Running 2 2d23h
vault-0 1/1 Running 0 6m30s ②
welcome-message-55474d6b78-g9l5w 1/1 Running 0 2m1s
① PostgreSQL 已部署。
② Vault 状态集已部署。
argocd CLI 工具还允许你使用以下列表中显示的命令来审查部署的状态。
列表 8.17 列出 ArgoCD 应用
argocd app list ①
NAME CLUSTER NAMESPACE PROJECT
welcome-cluster-ops https://kubernetes.default.svc default default
➥STATUS HEALTH SYNCPOLICY CONDITIONS
Synced Healthy Auto-Prune <none>
➥REPO
https://github.com/lordofthejars/kubernetes-secrets-source.git
➥PATH TARGET
welcome-message-gitops/cluster HEAD
① 列出当前 Argo CD 应用
status 字段显示资源的当前状态。当它设置为 Synced 时,集群与 Git 仓库中指定的状态保持一致。要部署 Welcome Message 服务,使用以下列表中显示的命令应用之前步骤中创建的 apps-ops.yaml 文件。
列表 8.18 注册服务应用
kubectl apply -f welcome-message-gitops/gitops/apps-ops.yaml ①
① 注册应用
使用以下列表中显示的命令,当 Pod 处于运行状态时,部署 Welcome Message 服务。
列表 8.19 获取所有 pods
kubectl get pods -n default
NAME READY STATUS RESTARTS AGE
gitea-deployment-7fbbf9c8b-bbcjq 1/1 Running 1 2d17h
name-generator-579ccdc5d5-mhzft 1/1 Running 2 2d23h
postgresql-59ddd57cb6-tjrg2 1/1 Running 0 6m30s
registry-deployment-64d49ff847-hljg9 1/1 Running 2 2d23h
vault-0 1/1 Running 0 6m30s
welcome-message-55474d6b78-g9l5w 1/1 Running 0 2m1s ①
① Welcome Message 服务已部署。
8.3.4 更新 Welcome 服务
到目前为止,你已经学习了如何使用 Tekton 构建 Welcome Message 服务,以及如何使用 Argo CD 项目自动部署它。但是,当服务已经部署并且需要发布新版本时会发生什么呢?
除了打包服务的新版本、构建容器并将其推送到容器注册库,如第 7.3 节中所述,现在 CI 管道需要更新 Welcome Message 服务的 Kubernetes 部署文件,并使用新容器标签将其推送到 Git 仓库以启动服务的滚动更新。图 8.6 展示了 Tekton(持续集成部分)和 Argo CD(持续交付部分)如何协同工作以实现 CD 管道。

图 8.6 Tekton 和 Argo CD 之间的相互关系
要实现这两个剩余步骤,需要对 Tekton 资源进行一些更改。
GitOps 仓库的 PipelineResource
首先需要注册的是一个新的 PipelineResource,用于注册 GitOps 仓库位置。这个仓库是 Argo CD 监听更改的地方。定义如下所示。
列表 8.20 welcome-service-gitops-resource.yaml
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: welcome-service-git-gitops-source
spec:
type: git
params:
- name: url
value: http://gitea.default.svc:3000/gitea/
➥kubernetes-secrets-source.git ①
① 章节开头迁移的仓库
在终端窗口中,使用以下列表中的命令应用资源。
列表 8.21 注册 GitOps 仓库
kubectl apply -f welcome-service-gitops-resource.yaml ①
① 注册管道资源
您需要设置用户名和密码,以便将部署文件中做出的更改推送到先前存储库。这通过创建包含以下列表内容的 Kubernetes Secret 和服务帐户来完成。
列表 8.22 git-secret.yaml
---
apiVersion: v1
kind: Secret
metadata:
name: git-auth
annotations:
tekton.dev/git-0: http://gitea.default.svc:3000 ①
type: kubernetes.io/basic-auth ②
stringData: ③
username: gitea
password: gitea1234
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: git-service-account
secrets:
- name: git-auth ④
① 设置用于认证的服务 URL(gitea 主机)
② 配置认证模式。
③ 在基本认证机制中使用的用户名和密码
④ 使用创建的密钥的服务帐户
重要提示:请记住使用第三章中展示的任何技术正确管理这些密钥。
在终端窗口中,使用以下列表中的命令应用资源。
列表 8.23 注册 Gitea 密钥
kubectl apply -f git-secret.yaml ①
① 创建密钥
更新 Tekton 任务以更新容器镜像
关于 yq
yq 是一个轻量级且便携的命令行 YAML 处理器。它可以用于查询 YAML 文档或修改它们。yq 工具使用以下结构:
yq eval [flag] [expression] [yaml_file]
给定以下 YAML 文档:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: myboot
name: myboot
spec:
replicas: 1
selector:
matchLabels:
app: myboot
template:
metadata:
labels:
app: myboot
spec:
containers:
- image: quay.io/rhdevelopers/myboot:v1
您可以使用以下表达式通过 image 字段进行引用:.spec.template .spec.containers[0].image。运行以下命令使用 yq 更新 image 字段到新值:
yq eval -i '.spec.template.spec.containers[0].image =
➥"quay.io/rhdevelopers/myboot:v2"' deployment.yml
在配置管道之前,您需要修改先前定义的任务,并添加两个新功能:
-
定义 GitOps 存储库的新资源,以便自动克隆
-
定义一个步骤,使用前一个步骤中创建的容器镜像标签更新部署定义
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: welcome-service-app
spec:
params:
...
resources:
inputs:
- name: source
type: git
- name: gitops ①
type: git
steps:
- name: maven-build
...
- name: build-and-push-image
...
- name: update-deployment-file
image: quay.io/lordofthejars/image-updater:1.0.0 ②
script: |
#!/usr/bin/env ash
cd /workspace/gitops ③
git checkout -b newver ④
git config --global user.email "alex@example.com" ⑤
git config --global user.name "Alex"
yq eval -i '.spec.template.spec.containers[0].image
➥= env(DESTINATION_IMAGE)'
➥welcome-message-gitops/apps/app.yml ⑥
git add . ⑦
git commit -m "Update to $DESTINATION_IMAGE"
git push origin newver:master
env:
- name: DESTINATION_IMAGE
value: "$(inputs.params.imageName)"
volumes:
- name: varlibc
emptyDir: {}
① 使用 gitops 名称定义 GitOps 输入资源
② 使用 git 和 yq 安装的定制镜像
③ 进入克隆的仓库
④ 为更新创建新分支
⑤ 配置 Git 用户
⑥ 使用新容器更新部署文件
⑦ 将更改提交并推送到仓库
以下列表显示了 Task 的完整版本。
列表 8.24 welcome-service-task.yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: welcome-service-app
spec:
params:
- name: contextDir
description: the context dir within source
default: .
- name: imageName
description: the container name url-group-artifact-tag
resources:
inputs:
- name: source
type: git
- name: gitops
type: git
steps:
- name: maven-build ①
image: docker.io/maven:3.6.3-jdk-11-slim
command:
- mvn
args:
- clean
- install
workingDir: "/workspace/source/$(inputs.params.contextDir)"
- name: build-and-push-image
image: quay.io/buildah/stable
script: |
#!/usr/bin/env bash
buildah bud --layers -t $DESTINATION_IMAGE $CONTEXT_DIR ②
buildah push --tls-verify=false
➥$DESTINATION_IMAGE docker://$DESTINATION_IMAGE ③
env:
- name: DESTINATION_IMAGE
value: "$(inputs.params.imageName)"
- name: CONTEXT_DIR
value: "/workspace/source/$(inputs.params.contextDir)"
securityContext:
runAsUser: 0
privileged: true
volumeMounts:
- name: varlibc
mountPath: /var/lib/containers
- name: update-deployment-file
image: quay.io/lordofthejars/image-updater:1.0.0
script: |
#!/usr/bin/env ash
cd /workspace/gitops ④
git checkout -b newver ⑤
git config --global user.email "alex@example.com"
git config --global user.name "Alex"
yq eval -i '.spec.template.spec.containers[0].image
➥= "$DESTINATION_IMAGE"' welcome-message-gitops/apps/app.yml ⑥
git add .
git commit -m "Update to $DESTINATION_IMAGE"
git push origin newver:master ⑦
env:
- name: DESTINATION_IMAGE
value: "$(inputs.params.imageName)"
volumes:
- name: varlibc
emptyDir: {}
① Apache Maven 任务打包应用程序
② 构建容器的 Buildah 命令
③ 将容器推送到容器注册表的 Buildah 命令
④ 移动到 gitops 仓库
⑤ 创建新分支
⑥ 使用新容器镜像标签更新部署 YAML 文件
⑦ 将更改提交并推送到 Git 仓库
在终端窗口中,执行以下列表中的命令以应用资源。
列表 8.25 更新 Welcome 服务任务
kubectl replace -f welcome-service-task.yaml ①
① 更新上一章中的任务
重要:如果你在上一节中没有应用 Tekton task,请将 replace 替换为 apply。
更新 pipeline 定义
pipeline 定义需要在资源部分进行更新,如下所示,以注册 GitOps 仓库。
列表 8.26 welcome-service-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: welcome-deployment
spec:
resources:
- name: appSource
type: git
- name: appGitOps ①
type: git
...
tasks:
- name: welcome-service-app
...
resources:
inputs:
- name: source
resource: appSource
- name: gitops
resource: appGitOps ②
① 定义一个新的 Git 资源
② 将资源设置为任务
在终端窗口中,执行以下列表中的命令以应用资源。
列表 8.27 更新 Welcome 服务 pipeline
kubectl replace -f welcome-service-pipeline.yaml
重要:如果你在上一节中没有应用 Tekton task,请将 replace 替换为 apply。
更新 PipelineRun
最后,创建一个新的 PipelineRun,包含以下更改:
-
增加镜像标签号。
-
设置新的
PipelineResource的引用。 -
配置运行 Pipeline 的服务账户。
新的 PipelineRun 如下所示。
列表 8.28 welcome-service-gitops-resource-2.yaml
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: welcome-pipeline-run-2 ①
spec:
params:
- name: imageTag
value: "1.0.1" ②
resources:
...
- name: appGitOps
resourceRef:
name: welcome-service-git-gitops-source ③
serviceAccountName: git-service-account ④
pipelineRef:
name: welcome-deployment
① 新的 PipelineRun
② 增加容器版本。
③ 注册一个新的 GitOps 资源。
④ 使用 gitea 凭据的 serviceAccount
在终端窗口中,执行以下列表中的命令以应用资源。
列表 8.29 注册 PipelineRun
kubectl apply -f welcome-service-gitops-resource-2.yaml ①
① 注册新的 PipelineRun
检查输出
在应用了之前的 PipelineRun 之后,一个新的 Pipeline 实例被启动。它在 Tekton 中执行以下步骤:
-
从
gitea克隆 service Git 仓库和 GitOps 仓库 -
构建 service
-
创建容器镜像并将其推送到容器
registry实例 -
使用新镜像更新部署文件
-
将部署文件推送到 GitOps 仓库
要查看当前正在执行的日志行,请运行以下列表中的命令。
列表 8.30 列出 PipelineRun
tkn pr logs -f ①
① 显示当前 PipelineRun 的日志
步骤如下面的日志行所示:
[welcome-service-app : git-source-source-8xjvv] {
➥"level":"info","ts":1618242546.5564516,"caller":"git/git.go:165",
➥"msg":"Successfully cloned
➥http://gitea:3000/gitea/kubernetes-secrets-source.git @
➥841cd108640cc2aef9b52250e0ed8f5bf53ec973 (grafted, HEAD)
➥in path /workspace/source"}
[welcome-service-app : git-source-source-8xjvv] {
➥"level":"info","ts":1618242546.5845127,"caller":"git/git.go:203",
➥"msg":"Successfully initialized and updated submodules
➥in path /workspace/source"} ①
[welcome-service-app : git-source-gitops-v6f2q] {
➥"level":"info","ts":1618242546.842591,"caller":"git/git.go:165",
➥"msg":"Successfully cloned
➥http://gitea:3000/gitea/kubernetes-secrets-source.git @
➥841cd108640cc2aef9b52250e0ed8f5bf53ec973 (grafted, HEAD)
➥in path /workspace/gitops"}
[welcome-service-app : git-source-gitops-v6f2q] {
➥"level":"info","ts":1618242546.8661046,"caller":"git/git.go:203",
➥"msg":"Successfully initialized and updated submodules
➥in path /workspace/gitops"} ②
[welcome-service-app : maven-build] [INFO]
➥Scanning for projects... ③
....
[welcome-service-app : maven-build] [INFO] BUILD SUCCESS
[welcome-service-app : maven-build] [INFO] ---------------------------------
[welcome-service-app : maven-build] [INFO] Total time: 01:12 min
[welcome-service-app : maven-build] [INFO] Finished at: 2021-04-12T15:50:22Z
[welcome-service-app : maven-build] [INFO] ---------------------------------
[welcome-service-app : build-and-push-image] STEP 1: FROM
➥registry.access.redhat.com/ubi8/ubi-minimal:8.3 ④
[welcome-service-app : build-and-push-image] Getting image source signatures
....
[welcome-service-app : build-and-push-image]
➥Writing manifest to image destination
[welcome-service-app : build-and-push-image]
➥Storing signatures ⑤
[welcome-service-app : update-deployment-file]
➥Switched to a new branch 'newver' ⑥
....
[welcome-service-app : update-deployment-file]
➥To http://gitea:3000/gitea/kubernetes-secrets-source.git
[welcome-service-app : update-deployment-file]
➥841cd10..472d777 newver -> master ⑦
① 克隆 Welcome 服务 Git 仓库。
② 克隆 Welcome 服务 GitOps 仓库。
③ 构建 Welcome 服务 Java 项目
④ 构建 Welcome 服务 Linux 容器
⑤ 将 Linux 容器推送到容器注册库
更新欢迎服务部署文件
⑦ 将部署文件推送到 GitOps 仓库
执行这些步骤后,Argo CD 将检测 welcome-message-gitops/apps/app.yml 部署文件上的更改。它将应用这些更改,触发服务的滚动更新到新版本。
重要:Argo CD 控制器可以使用 webhook 或每三分钟轮询来检测和同步新的清单。轮询策略是默认的。
等待最多三分钟后,Argo CD 将检测到更改并开始同步过程,应用新的部署文件。假设你持续监控 Pod 的状态,你会看到旧的欢迎信息 Pod 被终止,并自动启动一个新的 Pod,该 Pod 在 Tekton 流程中创建的容器中。连续执行以下列表中的命令以检查更改。
列表 8.31 获取所有 pod
kubectl get pods
NAME READY STATUS RESTARTS AGE
gitea-deployment-7fbbf9c8b-xc27w 1/1 Running 0 16m
name-generator-579ccdc5d5-mhzft 1/1 Running 2 3d5h
postgresql-59ddd57cb6-tjrg2 1/1 Running 0 5h34m
registry-deployment-64d49ff847-hljg9 1/1 Running 2 3d5h
vault-0 1/1 Running 0 5h34m
welcome-message-55799f7dc9-c5j24 1/1 Running 0 13s ①
welcome-message-6778c7978b-tqxkv 1/1 Terminating 0 8m58s ②
welcome-pipeline-run-welcome
-service-app-mj5z6-pod-mkkpx 0/5 Completed 0 6m17s ③
① 已部署服务的最新版本。
② 服务的老版本已卸载。
③ PipelineRun 已完成。
如列表 8.32 所示,描述新部署的 Pod 显示了新容器被使用。
列表 8.32 描述新的欢迎服务 pod
kubectl describe pod welcome-message-55799f7dc9-c5j24 ①
....
Controlled By: ReplicaSet/welcome-message-55799f7dc9
Containers:
welcome-message:
Container ID:
➥docker://fa8b45f46311819adb0fcfc5c8d8a17e4626792aac38f0e6a116d71cb0571718
Image: registry:5000/k8ssecrets/welcome-message:1.0.1 ②
....
① 将 pod 名称更改为正确的名称
② 版本标签已更新。
图 8.7 显示了 Argo CD 仪表板的截图,其中显示了欢迎信息的状态。如果你仔细看,你会看到 rev:2 是当前的部署,因为服务已被更新。

图 8.7 Argo CD 仪表板
摘要
-
Kubernetes Secrets 被用于应用程序代码(例如,用户名、密码和 API 密钥)以及 CI/CD 管道(例如,外部服务的用户名和密码)。
-
启用 Kubernetes 数据静态加密以在 Kubernetes 内部存储加密密钥。
-
CD 密钥需要像其他密钥一样受到保护。使用 Argo CD 中的
SealSecrets将加密密钥存储在 Git 中。 -
Git 被用作源代码和部署脚本的单一真相来源。
-
Argo CD 是一个 Kubernetes 控制器,允许你在 Kubernetes 中实现 GitOps。
附录 A. 工具
要在你的机器上部署和管理 Kubernetes 环境,需要安装和配置几个工具。
A.1 Minikube
Minikube 是一个本地 Kubernetes 集群,专注于使在本地环境中学习和开发 Kubernetes 变得容易。它依赖于容器/虚拟化技术,如 Docker、Podman、HyperKit、Hyper-V、KVM 或 VirtualBox,以启动安装了 Kubernetes 的 Linux 机器。VirtualBox 虚拟化工具因其简单性和通用性而被使用,因为它可以在最常用的操作系统(Microsoft Windows、Linux 和 Mac OS)中工作。
如果尚未安装 VirtualBox,请在浏览器中打开以下网址:www.virtualbox.org/。当网页打开时,点击左侧菜单中的“下载”链接,如图 A.1 所示。

图 A.1 VirtualBox 主页,标注了“下载”部分
然后根据运行示例的操作系统选择你的软件包,如图 A.2 所示。

图 A.2 VirtualBox 下载页面,展示了不同的软件包
下载过程将开始将软件包存储在你的本地磁盘上。当此过程完成后,点击下载的文件以启动安装过程,如图 A.3 所示。

图 A.3 VirtualBox 安装窗口
你可以使用安装过程提供的默认 VirtualBox 配置值,或者根据你的需求进行修改。安装完成后,你可以通过打开它来验证 VirtualBox 是否已正确安装。见图 A.4,展示了 VirtualBox 的打开屏幕,其中安装了三台机器。

图 A.4 VirtualBox 状态窗口,显示三个实例
A.2 Kubectl
要与 Kubernetes 集群交互,你需要安装 kubectl CLI 工具。下载和安装 kubectl 的最佳方式是访问以下网址:kubernetes.io/docs/tasks/tools/。当网页打开时,点击对应你平台的安装链接,如图 A.5 所示。

图 A.5 kubectl 主页
你将安装 Kubernetes 1.19.0;因此,下载 kubectl CLI 版本 1.19.0 是至关重要的。要下载特定版本,请向下滚动页面,直到看到讨论安装特定版本而不是最新稳定版本的“注意”标题。图 A.6 展示了有关下载特定版本的说明部分。

图 A.6 下载特定 kubectl 版本;将 v1.22.0 替换为 v1.19.0
在安装了 VirtualBox 和 kubectl 之后,你可以开始下载 minikube 版本 1.17.1 以启动 Kubernetes 集群。
在浏览器中打开以下 URL:github.com/kubernetes/minikube/releases/tag/v1.17.1。当网页加载完成后,展开“资产”菜单以找到针对您平台的特定 minikube 发布。图 A.7 展示了 minikube 1.17.1 的 GitHub 发布页面。

图 A.7 Minikube 在“资产”部分作为 GitHub 发布
当展开“资产”菜单时,点击与您的平台对应的 minikube 链接。图 A.8 展示了发布列表。

图 A.8 Minikube 已发布到多个平台;下载适合您平台的版本
当文件下载完成后,将其重命名为 minikube,因为文件名也包含平台和架构。例如,minikube-linux-amd64 是适用于 Linux 64 位架构的 minikube 版本。安装 VirtualBox 并将 minikube 文件重命名后,在终端窗口中运行以下命令以创建 Kubernetes 集群:
minikube start --kubernetes-version='v1.19.0'
➥--vm-driver='virtualbox' --memory=8196 ①
① 启动 Kubernetes 集群
输出行应类似于以下内容:
😄 [vault] minikube v1.17.1 en Darwin 11.6
🆕 Kubernetes 1.20.2 is now available. If you would like to upgrade,
specify: --kubernetes-version=v1.20.2
🎉 minikube 1.24.0 is available! Download it:
https://github.com/kubernetes/minikube/releases/tag/v1.24.0
✨ Using the virtualbox driver based on existing profile
💡 To disable this notice, run:
'minikube config set WantUpdateNotification false'
👍 Starting control plane node vault in cluster vault
🔄 Restarting existing virtualbox VM for "vault" ...
🐳 Preparando Kubernetes v1.19.0 en Docker 20.10.2...
🔎 Verifying Kubernetes components...
🌟 Enabled addons: storage-provisioner, default-storageclass
❗ /usr/local/bin/kubectl is version 1.21.3, which may have
incompatibilites with Kubernetes 1.19.0.
▪ Want kubectl v1.19.0? Try 'minikube kubectl -- get pods -A'
🏄 Done! kubectl is now configured to use "" cluster and "default"
namespace by default
附录 B. 安装和配置 yq
大多数操作系统都内置了对终端内文本操作的支持,例如 sed 和 awk。这些工具非常适合进行相对简单的文本操作,但在处理结构化序列化语言(如 YAML)时可能会变得繁琐。yq (mikefarah.gitbook.io/yq) 是一个命令行工具,它提供了查询和操作基于 YAML 内容的支持,并且在与 Kubernetes 环境交互时非常有用,因为大部分资源都是以 YAML 格式表达的。另一个类似且流行的工具,称为 jq,为 JSON 格式的内容提供了类似的功能,这在第四章中有描述。本附录描述了 yq 的安装,以及一些示例来确认工具安装成功。
B.1 安装 yq
yq 支持多个操作系统,可以使用多种方法进行安装,包括包管理器或直接从项目网站 (github.com/mikefarah/yq/releases/latest) 下载二进制文件。直接二进制选项是最直接的方法,因为它没有外部依赖或先决条件。请确保找到版本 4 或更高版本的工具,因为与先前版本相比,它进行了重大重写。发布页面将允许您选择下载压缩归档或直接二进制文件。或者,您可以使用终端将二进制文件下载到本地机器。
警告:还有一个工具,也称为 yq,它作为一个 Python 包提供,并具有类似的功能。安装错误的工具会导致错误,因为这两个工具在语法和功能上存在差异。
可以使用以下命令下载 yq 二进制文件并将其放置在 PATH 目录中:
列表 B.1 下载 yq
sudo curl -o /usr/bin/yq
➥https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} ①
➥&& \
sudo chmod +x /usr/bin/yq
① 版本(VERSION)指的是标记的发布版本,而二进制(BINARY)是 yq 二进制文件名称、操作系统名称和架构的组合。例如,AMD64 Linux 的示例是 yq_linux_amd64。
如果以下命令成功执行,则表示安装成功:
yq --version
如果命令没有成功执行,请确认文件已成功下载,放置在操作系统的 PATH 目录中,并且二进制文件是可执行的。
B.2 以示例说明 yq
yq 可以对 YAML 格式的文件执行操作,例如查询和操作值,当与 Kubernetes 清单一起工作时非常有用。kubectl 具有多种输出格式的功能,例如 JSONPath 或 Go 模板。然而,某些高级功能,如管道,不可用,需要更全面的专用工具。
为了展示 yq 可以用来操作 YAML 内容的一些方法,创建一个名为 book-info.yaml 的文件,包含我们熟悉的一个资源:Kubernetes 密钥。
列表 B.2 book-info.yaml
apiVersion: v1
metadata:
name: book-info
stringData:
title: Securing Kubernetes Secrets
publisher: Manning
type: Opaque
kind: Secret
yq有两个主要模式:评估单个文档(使用evaluate或e子命令)或多个文档(使用eval-all或ea子命令)。评估单个文档是最常见的模式,因此可以使用它来查询列表 B.2 中创建的book-info.yaml文件的 内容。
对 YAML 文件的操作使用表达式来确定要执行的具体操作。由于大多数 YAML 内容使用嵌套内容,这些属性可以通过点符号访问。例如,为了提取stringData下title字段的值,使用表达式.stringData.title。在yq中使用时,需要三个组件:要执行的操作类型(如evaluate)、表达式和 YAML 内容的定位。现在使用以下命令提取title字段:
yq eval '.stringData.title' book-info.yaml
更复杂的表达式也可以用来执行高级操作。管道(|)可以用来连接表达式,使得一个表达式的输出成为另一个表达式的输入。例如,为了确定stringData下publisher字段的长度,可以使用管道将.stringData.publisher表达式的输出传递给length运算符:
yq eval '.stringData.publisher | length' book-info.yaml
命令的结果应该返回7。除了提取属性外,yq还可以用来修改 YAML 内容。现在在stringData下添加一个名为category的新属性,其值为Security。完成此任务的yq表达式为'.stringData.category' = "Security"。执行以下命令以添加category`字段:
yq eval '.stringData.category = "Security"' book-info.yaml
以下列表应该是命令的结果。
列表 B.3 使用yq更新 YAML 内容的输出
apiVersion: v1
metadata:
name: book-info
stringData:
title: Securing Kubernetes Secrets
publisher: Manning
category: Security ①
type: Opaque
kind: Secret
① 添加了一个新的类别字段。
尽管执行的结果导致添加了新的category字段,但重要的是要注意book-info.yaml文件的内容并未被修改。为了更新book-info.yaml字段的内容,必须使用-i选项,这将指示yq执行文件就地更新。执行以下命令以执行就地文件更新:
yq eval -i '.stringData.category = "Security"' book-info.yaml
确认更改已应用于文件。yq提供的提取和操作能力使其成为一个多功能的工具,在处理任何 YAML 格式内容时都非常有用。
附录 C. 安装和配置 pip
与大多数其他编程语言一样,Python 包含了对启用可重用代码部分的支持,例如可以包含在其他应用程序中的语句和定义。这些可重用代码片段被称为 模块。多个模块可以组织在一起并包含在 包 中。标准 Python 库包含了一系列模块和包,这对于任何应用程序都是基本的。然而,标准 Python 库的内容并不涵盖所有可想象的使用案例。这就是用户定义的包和模块发挥作用的地方。随着越来越多的人创建定制的包,有一个简单的方法来分发和消费这些 Python 包变得非常重要。Python 包索引(PyPi)(pypi.org/)试图提供一个解决方案,以提供一个集中位置来存储和发现 Python 社区共享的 Python 包。来自集中式 Python 包索引或自托管实例的 Python 包可以使用 pip 进行管理,pip 是一个包管理器,它简化了 Python 包的发现、下载和生命周期管理。
C.1 安装 pip
存放在 Python 包索引中的 Python 包可以使用 pip 可执行文件进行管理。大多数最新的 Python 发行版(Python 2 的版本 >=2.9.2,Python 3 的版本 >=3.4)在二进制安装中预装了 pip。如果 Python 是通过其他方法安装的,例如包管理器,pip 可能没有被包含在内。您可以通过尝试执行 pip(如果您使用的是 Python 2 版本)或 pip3(如果您使用的是 Python 3 版本)来检查 pip 是否已安装。如果在执行前面的命令时返回错误,则必须安装 pip。
有几种方法可以安装 pip:
-
Python 3 的
ensurepip模块 -
get-pip.py脚本 -
包管理器
让我们使用 ensurepip Python 模块来安装 pip,因为它使用了适用于大多数平台的本地 Python 构造。执行以下命令来安装 pip:
python -m ensurepip --upgrade
通过执行 pip 命令来确认 pip 已安装:
pip
如果命令没有错误返回,则表示 pip 已成功安装。
注意:在某些系统上,可能使用别名或符号链接将 python3 和 python 可执行文件链接起来,以提供向后兼容性或简化与 Python 的交互;同样适用于 pip。在执行 python、python3、pip 或 pip3 时指定 --version 标志可以确认正在使用的特定版本。
C.2 基本 pip 操作
在开始使用 pip 之前,建议您更新它及其支持的工具到最新版本。获取最新更新将确保能够适当访问任何所需的源存档。执行以下命令以更新 pip,以及 setuptools 和 wheel 包到最新版本:
python -m pip install --upgrade pip setuptools wheel
在更新了必要的工具后,让我们开始使用 pip。
对于任何使用包管理器的人来说,第一步是确定要安装的软件组件。这可能事先已知,或者可能需要从可用组件列表中查询。搜索包的最佳位置是 PyPi 网站(pypi.org/),该网站详细介绍了每个可用的包、它们的历史和任何依赖项。
从 Python 索引中可安装的包数不胜数,选择正确的包可能是一项挑战。Python 开发者最常见的用例之一是进行基于 HTTP 的请求。虽然 Python 提供了如http.client等模块,但构建简单的查询可能相当复杂。requests模块试图简化基于 HTTP 的调用。
与 Requests 模块相关的详细信息可以在 PyPi 网站上找到,但让我们使用 pip 来安装包。执行以下命令使用 pip 安装requests包:
pip install requests
使用info子命令可以查看已安装包的相关信息:
pip show requests
命令的响应如下:
Name: requests
Version: 2.26.0
Summary: Python HTTP for Humans.
Home-page: https://requests.readthedocs.io
Author: Kenneth Reitz
Author-email: me@kennethreitz.org
License: Apache 2.0
Location: /usr/local/lib/python3.9/site-packages
Requires: certifi, charset-normalizer, idna, urllib3
Required-by:
安装了requests包后,可以使用简单的 Python 交互会话来说明其用法。执行以下命令以首先启动 Python 交互会话:
python
现在导入 requests,查询远程地址并打印 HTTP 状态。
列表 C.1 使用 requests 包查询远程服务器
import requests ①
response = requests.get("https://google.com") ②
print(response.status_code) ③
① 导入 requests 包。
② 执行请求。
③ 打印请求的 HTTP 状态。
应返回200响应代码,表示成功查询远程 HTTP 服务器。输入exit()退出 Python 交互控制台。
已安装的 Python 包也可以通过 pip 安装。可以使用pip list命令确定当前已安装的哪些包。一旦确定了要删除的所需包,就可以使用pip uninstall命令来删除。要删除之前安装的 requests 包,请执行以下命令。
列表 C.2 删除 requests 包
pip uninstall -y requests ①
① 使用-y 标志将跳过在删除包之前的确认提示。
一旦命令成功完成,Python 包已被删除。
附录 D. 安装和配置 Git
Git 已成为跟踪文件更改的事实上的版本控制系统,并在处理 Kubernetes 内容时经常使用。与其他版本控制系统相比,由于其简化的分支管理功能而获得了流行。在开始在本地机器上使用 Git 之前,必须首先完成一系列步骤。
D.1 安装 Git
要开始使用 Git 内容,必须安装 git 可执行文件。大多数操作系统都支持 git,包括 Linux、OSX 和 Windows,具体步骤可在 Git 网站上找到(git-scm.com/book/en/v2/Getting-Started-Installing-Git)。
当在 Linux 上安装 git 时,最简单的方法是使用包管理器,如 apt 或 dnf。在基于 Debian 的 Linux 操作系统上,请在终端中执行以下命令来安装 Git:
apt install git-all
在基于 RPM 的操作系统上,例如 Fedora 或 Red Hat Enterprise Linux,请在终端中执行以下命令来安装 Git:
dnf install git-all
通过检查版本来确认 Git 已成功安装:
git --version
如果返回了版本信息,则表示 Git 已成功安装。如果发生错误,请确认与安装方法相关的步骤已完全完成。
D.2 配置 Git
尽管 Git 安装后即可使用,但建议采取一些额外的步骤来自定义 Git 环境。某些功能,如提交代码,在没有额外操作的情况下将不可用。
git config 子命令可用于检索和设置配置选项。这些选项可以在三个级别之一中指定:
-
在系统级别,并在
[path]/etc/gitconfig文件中指定。 -
在用户配置文件级别,位于
~/.gitconfig中。此级别可以通过使用git config子命令指定--global选项来定位。 -
在
.git/config文件中的存储库级别。此级别可以通过使用git config子命令指定--local选项来定位。
虽然 Git 中提供了大量的可配置选项,但在安装 Git 时,有两个选项应该被定义:
-
用户名
-
电子邮件地址
这些值将与您执行的任何提交相关联。未能配置这些值将导致在尝试执行提交时出现以下错误。
列表 D.1 未配置 Git 身份时产生的错误信息
Author identity unknown
*** Please tell me who you are.
Run
git config --global user.email "you@example.com" ①
git config --global user.name "Your Name"
to set your accounts default identity.
Omit --global to set the identity only in this repository.
fatal: unable to auto-detect email address (got 'root@machine.(none)')
① 表示应配置的变量
如 D.1 列表中描述的错误所示,user.email 和 user.name 变量都应该被配置。虽然这些变量可以在每个存储库中单独配置,但为了简单起见,在全局级别定义并适当修改各个存储库中的变量更为直接。
执行以下命令以设置 user.email 和 user.name,以配置所需的变量,并适当替换您的用户详细信息:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
通过列出所有全局配置来确认值已适当地设置:
git config --global -l
应返回以下值:
user.name=Your Name
user.email=you@example.com
在此阶段,您的机器已准备好完全与 Git 生态系统进行交互。
附录 E. 安装 GPG
GNU 隐私守护者 (GPG) 是一种开放标准的专有优秀隐私 (PGP) 加密方案的实现,通常用于加密和解密电子邮件和文件系统内容。对称密钥加密和公钥加密的结合提供了一种快速且安全的信息交换方法。用户必须首先使用 GPG 工具生成一个公私钥对,以启用信息的加密和解密。私钥在加密过程中使用,而公钥在解密时使用。公钥可以与任何需要解密加密信息的人共享,也可以托管在互联网密钥服务器上,以简化更广泛受众解密加密内容的方式。通过使用 GPG 工具——特别是 gpg 命令行界面——可以简化公私钥对的创建以及信息的加密和解密。
E.1 获取 GPG 工具
GPG 工具以及 gpg 命令行界面可以安装在大多数主要操作系统上,并且可以通过直接下载或从包管理器(如 apt、dnf 或 brew)获取。
在基于 Debian 的 Linux 操作系统上,请在终端中执行以下命令:
apt install gnupg
在基于 RPM 的操作系统上,例如 Fedora 或 Red Hat Enterprise Linux,请在终端中执行以下命令:
dnf install gnupg
在基于 OSX 的操作系统上,请在终端中执行以下命令:
brew install gpg
通过使用 --version 标志来检查工具的版本,以确认 gpg 命令行界面是否成功安装。
列表 E.1 显示 GPG 版本
gpg (GnuPG) 2.2.20
libgcrypt 1.8.5
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
➥<https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Home: /root/.gnupg ①
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2
① 包含 GPG 文件的目录
如果显示类似于列表 E.1 的响应,则表示 GPG 工具已成功安装。
E.2 生成公私钥对
在能够加密内容之前,必须生成密钥对。生成密钥对的过程在第三章中也有讨论,但此处将再次讨论以确保完整性。
可以使用 gpg 命令行界面通过以下标志之一创建 GPG 密钥:
-
--quick-generate-key——在用户提供USER-ID以及过期和算法详情的同时生成密钥对 -
--generate-key——在提示真实姓名和电子邮件详情的同时生成密钥对 -
--full-generate-key——所有可能的密钥对生成选项的对话框
您选择的选项取决于您的个人需求。--generate-key 和 --full-generate-key 选项支持批处理模式,这允许通过非交互式方法创建密钥对。
使用 --generate-key 标志创建一个新的 GPG 密钥对:
gpg --generate-key
一旦执行此命令,就会在您的家目录中的 .gnupg 文件夹内创建一个 GPG 文件的主目录。此位置可以通过指定 GNUPGHOME 环境变量来更改。
在提示时提供您的姓名和电子邮件地址。然后您将被要求确认这些详情。按 O 确认详情。
接下来,系统会提示您提供一个密码短语来保护您的密钥。第三章的练习建议不要创建密码短语,以简化整合所使用的每个安全工具。然而,在为其他用途创建 GPG 密钥对时,建议提供密码短语。一旦提供了密码短语,将生成一个新的密钥对,并显示与密钥相关的详细信息,如下所示列表。
列表 E.2 显示 GPG 版本
pub rsa2048 2020-12-31 [SC] [expires: 2022-12-31]
53696D1AB6954C043FCBA478A23998F0CBF2A552
uid [ultimate] John Doe <jdoe@example.com>
sub rsa2048 2020-12-31 [E] [expires: 2022-12-31]
您可以通过提供的值确认名称和电子邮件是否已正确添加,包括算法、密钥大小和过期时间。现在密钥对已生成,可以使用 GPG 工具集加密消息。


浙公网安备 33010602011771号