一个月的-Kubernetes-学习指南-全-

一个月的 Kubernetes 学习指南(全)

原文:Learn Kubernetes in a Month of Lunches

译者:飞龙

协议:CC BY-NC-SA 4.0

前置内容

前言

当我完成《一个月午餐时间学会 Docker》时,我知道续集必须关于 Kubernetes。对于大多数人来说,那将是他们容器之旅的下一阶段,但学习 Kubernetes 并不容易。这部分的困难在于它是一个非常强大的平台,具有庞大的功能集,这些功能总是在不断演变。但这也因为它需要一个可靠的指南,这个指南以正确的水平进行教学——在技术知识上深入足够,但保持关注平台能为你和你的应用程序做什么。我希望《一个月午餐时间学会 Kubernetes》将是那个指南。

Kubernetes 是一个在容器中运行和管理应用程序的系统。它是生产环境中运行容器的最流行方式,因为它得到了所有主要云平台的支持,并且在数据中心中运行同样出色。这是一个世界级的平台,被 Netflix 和 Apple 等公司使用——你甚至可以在你的笔记本电脑上运行它。你必须投资学习 Kubernetes,但回报是你可以带到任何组织、任何项目的技能,并且有信心能够迅速上手。

你需要投入的是时间。熟悉 Kubernetes 能做什么以及你如何在 Kubernetes 建模语言中表达你的应用程序需要时间。你提供时间,而《一个月午餐时间学会 Kubernetes》将提供其余部分。这里有动手练习和实验室,将使你熟悉所有平台功能,以及 Kubernetes 周围的工作实践和工具生态系统。这是一本实用的书,让你准备好真正使用 Kubernetes。

致谢

这是我在 Manning 出版的第二本书,写作过程和第一本书一样愉快。很多人为了这本书的出版和帮助我使其更好而付出了辛勤的努力。我想感谢出版团队提供的所有反馈。

向所有审稿人,Alex Davies-Moore,Anthony Staunton,Brent Honadel,Clark Dorman,Clifford Thurber,Daniel Carl,David Lloyd,Furqan Shaikh,George Onofrei,Iain Campbell,Marc-Anthony Taylor,Marcus Brown,Martin Tidman,Mike Lewis,Nicolantonio Vignola,Ondrej Krajicek,Rui Liu,Sadhana Ganapathiraju,Sai Prasad Vaddepally,Sander Stad,Tobias Getrost,Tony Sweets,Trent Whiteley,Vadim Turkov 和 Yogesh Shetty,你们的建议帮助使这本书变得更好。

我还想感谢所有在审查周期和早期访问计划中尝试所有练习并告诉我事情何时不工作的人。感谢大家抽出时间。

关于这本书

适合阅读这本书的人

我希望你能从这本书中获得真实的 Kubernetes 体验。当你阅读完所有章节并完成练习后,你应该自信地认为你学到的技能是人们真正使用 Kubernetes 的方式。这本书包含了很多内容,你会发现典型的午餐时间对于许多章节来说是不够的。那是因为我想给每个主题都提供应有的覆盖范围,这样你才能真正全面地理解,并在完成书籍时感觉自己像一位经验丰富的 Kubernetes 专业人士。

你不需要对 Kubernetes 有任何了解就可以开始使用这本书,但你应该熟悉像容器和镜像这样的核心概念。如果你对整个容器领域是新手,你会在我的另一本书《一个月午餐时间学会 Docker》(Manning,2020)的电子书版本中找到一些章节作为附录——它们将帮助你设定场景。

Kubernetes 经验将帮助你在工程或运营领域进一步发展你的职业生涯,而且这本书对您的背景没有任何假设。Kubernetes 是一个高级主题,它建立在几个其他概念之上,但当我谈到这些概念时,我会给出它们的概述。这是一本非常实用的书,为了最大限度地利用它,你应该计划通过实际操作练习来学习。你不需要任何特殊的硬件:一台 Mac 或 Windows 笔记本电脑,或者一台 Linux 台式机,就足够了。

GitHub 是我在这本书中使用的所有样本的真实来源。你将在第一章设置你的实验室时下载这些材料,你应该确保收藏该存储库并关注通知。

如何使用这本书

这本书遵循了“一个月午餐时间”的原则:你应该能够在午餐时间完成每一章,并在一个月内完成整本书。这里的“工作”是关键,因为你应该考虑留出时间阅读章节,完成“现在试试”练习,并在最后尝试实际操作实验室。你应该预计会有几次较长的午餐时间,因为章节不会省略任何角落或跳过关键细节。你需要大量的肌肉记忆才能有效地与 Kubernetes 一起工作,并且每天练习将真正巩固你在每个章节中获得的知识。

你的学习之旅

Kubernetes 是一个庞大的主题,但我已经多年在培训课程和研讨会中教授它,无论是面对面的还是虚拟的,并建立了一个我知道有效的渐进式学习路径。我们将从核心概念开始,逐渐增加更多细节,将最复杂的话题留到你对 Kubernetes 更熟悉的时候。

第二章至第六章将跳转到在 Kubernetes 上运行应用。你将学习如何在 YAML 清单中定义应用,这些应用作为容器由 Kubernetes 运行。你将了解如何配置容器之间的网络访问以及外部世界流量如何到达你的容器。你将学习你的应用如何从 Kubernetes 读取配置并将数据写入由 Kubernetes 管理的存储单元,以及你如何扩展你的应用。

第七章至第十一章基于基础知识,涉及与实际 Kubernetes 使用相关的主题。你将学习如何运行共享相同环境的容器,以及如何使用容器来运行批处理作业和计划作业。你将学习 Kubernetes 如何支持自动化滚动更新,以便你可以零停机时间发布新的应用程序版本,以及如何使用 Helm 提供一种可配置的方式来部署你的应用。你还将了解使用 Kubernetes 构建应用的实用性,包括不同的开发者工作流程和持续集成/持续交付(CI/CD)管道。

第十二章至第十六章全部关于生产就绪,不仅限于在 Kubernetes 中运行你的应用,而是以足够好的方式运行它们以便上线。你将学习如何配置自我修复的应用程序,收集和集中所有日志,并构建监控仪表板来可视化系统的健康状况。安全也是其中之一,你将学习如何保护对应用的公共访问以及如何保护应用程序本身。

第十七章至第二十一章进入专家领域。在这里,你将学习如何处理大型 Kubernetes 部署,并配置你的应用程序以自动扩展和缩小。你将学习如何实现基于角色的访问控制来保护对 Kubernetes 资源的访问,我们还将介绍一些 Kubernetes 的更多有趣用途:作为无服务器函数的平台,以及作为一个可以运行为 Linux 和 Windows、Intel 和 Arm 构建的应用的多架构集群。

到本书结束时,你应该对将 Kubernetes 引入日常工作充满信心。最后一章提供了关于继续使用 Kubernetes 的指导,包括对书中每个主题的进一步阅读建议以及选择 Kubernetes 提供商的建议。

现在就试试练习

每一章都有许多指导练习供你完成。本书的源代码全部在 GitHub 上github.com/sixeyed/kiamol——当你设置实验室环境时,你将克隆它,并使用它来运行所有示例,这将使你在 Kubernetes 中运行越来越复杂的应用程序。

许多章节都是基于书中前面的内容,但你不需要按顺序阅读所有章节,所以你可以跟随自己的学习路径。章节内的练习通常都是相互关联的,所以如果你跳过了练习,可能会在后面发现错误,这有助于磨练你的故障排除技能。所有练习都使用容器镜像,这些镜像在 Docker Hub 上公开可用,你的 Kubernetes 集群将下载它需要的任何镜像。

这本书包含了很多内容。如果你在阅读章节的同时完成示例,你会从中获得最多的收获,并且在使用 Kubernetes 时会感到更加自在。如果你没有时间完成每个练习,跳过一些是可以的;每个练习后面都附有显示你将看到的输出的截图,每个章节都以总结部分结束,以确保你对主题有信心。

动手实验室

每一章都以一个动手实验室结束,邀请你超越“现在试试”的练习,更进一步。这些实验室不是指导性的——你将获得一些指导和提示,然后就要靠你自己来完成实验室。所有实验室的样本答案都存放在 sixeyed/kiamol GitHub 仓库中,所以你可以检查你完成了什么——或者如果你没有时间完成某个实验室,可以看看我是如何完成的。

其他资源

《Kubernetes 实战》由 Marko Lukša(Manning,2017)所著,是一本很好的书,涵盖了我在这里没有涉及到的许多管理细节。除此之外,进一步阅读的主要资源是官方的 Kubernetes 文档,你可以在两个地方找到它。文档网站(kubernetes.io/docs/home/)涵盖了从集群架构到引导教程以及如何自己为 Kubernetes 做贡献的各个方面。Kubernetes API 参考(kubernetes.io/docs/reference/generated/kubernetes-api/v1.20)包含了你可以创建的每种类型的对象的详细规范——这是一个需要书签的网站。

Twitter 是 Kubernetes @kubernetesio 账户的家园,你还可以关注 Kubernetes 项目和社区的一些创始人,如 Brendan Burns (@brendandburns)、Tim Hockin (@thockin)、Joe Beda (@jbeda)和 Kelsey Hightower (@kelseyhightower)。

我也经常谈论这些内容。你可以在 Twitter 上关注我@EltonStoneman;我的博客是blog.sixeyed.com;我在youtube.com/eltonstoneman上发布 YouTube 视频。

关于代码

本书包含许多源代码示例,既有编号列表,也有与普通文本并列。在这两种情况下,源代码都以 fixed-width font like this 这样的 固定宽度 字体格式化,以将其与普通文本区分开来。有时代码也会用 in bold 突出显示,以强调与章节中先前步骤相比有所改变的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新调整了缩进,以适应书籍中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中删除。代码注释伴随着许多列表,突出显示重要概念。

本书示例的代码可以从 Manning 网站 www.manning.com/books/learn-kubernetes-in-a-month-of-lunches, 下载,也可以从 GitHub github.com/sixeyed/kiamol 下载。

liveBook 讨论论坛

购买《在一个月的午餐时间内学习 Kubernetes》包括免费访问由 Manning Publications 运营的私人网络论坛,您可以在论坛上对书籍发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问 livebook.manning.com/#!/book/learn-kubernetes-in-a-month-of-lunches/discussion。您还可以在 livebook.manning.com/#!/discussion 了解更多关于 Manning 的论坛和行为准则。

Manning 对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要书籍在印刷中,论坛和先前讨论的存档将可通过出版社的网站访问。

关于作者

Elton Stoneman 是一位 Docker Captain,一位多年的 Microsoft MVP,同时也是 Pluralsight 和 Udemy 上数十门在线培训课程的作者。他在微软领域的大部分职业生涯中担任顾问,设计和交付大型企业系统。然后他迷上了容器,加入了 Docker,在那里度过了三年忙碌而充满乐趣的时光。现在,他作为一名自由顾问和培训师,帮助处于容器之旅各个阶段的组织。Elton 在 blog.sixeyed.com 和 Twitter @EltonStoneman 上撰写关于 Docker 和 Kubernetes 的文章,并在 eltons.show 上定期进行 YouTube 直播。

第 1 周:快速掌握 Kubernetes

欢迎来到《一个月午餐学会 Kubernetes》。本节将立即引导您使用 Kubernetes,重点关注核心概念:部署、Pod、服务以及卷。您将学习如何使用 Kubernetes YAML 规范来建模您的应用程序,以及 Kubernetes 如何提供计算、网络和存储的抽象。到本节结束时,您将在所有基本概念上拥有丰富的经验,并且对如何建模和部署您自己的应用程序有一个良好的理解。

1 开始之前

Kubernetes 很大。真的很大。它在 2014 年作为 GitHub 上的开源项目发布,现在每周平均有来自全球 2,500 名贡献者的 200 次更改。KubeCon 年度会议从 2016 年的 1,000 名参与者增长到最近活动的超过 12,000 名,现在已成为一个全球系列,在美国、欧洲和亚洲都有活动。所有主要的云服务都提供托管 Kubernetes 服务,您可以在数据中心或您的笔记本电脑上运行 Kubernetes——它们都是相同的 Kubernetes

独立性和标准化是 Kubernetes 如此受欢迎的主要原因。一旦您的应用程序在 Kubernetes 上运行良好,您就可以在任何地方部署它们,这对正在向云迁移的组织来说很有吸引力,因为它使他们能够在数据中心和其他云之间迁移而无需重写。它对从业者来说也非常有吸引力——一旦您掌握了 Kubernetes,您就可以在项目和组织之间迁移,并快速变得高效。

虽然达到这个目标很困难,因为 Kubernetes 本身就很复杂。即使是简单的应用程序也作为多个组件部署,这些组件以自定义的文件格式描述,很容易跨越数百行。Kubernetes 将基础设施级别的关注点,如负载均衡、网络、存储和计算,纳入应用程序配置中,这取决于您的 IT 背景,可能是一些新概念。此外,Kubernetes 始终在扩展——每个季度都会发布新版本,通常带来大量的新功能。

但这是值得的。我花费了许多年帮助人们学习 Kubernetes,一个常见的模式出现了:问题“为什么这么复杂?”转变为“你可以做到这一点?这太神奇了!” Kubernetes 确实是一项令人惊叹的技术。你了解得越多,你会越喜欢它——这本书将加速你通往 Kubernetes 精通的旅程。

1.1 理解 Kubernetes

这本书为您提供了 Kubernetes 的实战入门。每一章都提供了即学即用的练习和实验室,让您能够获得大量使用 Kubernetes 的经验。除了这一章。 😃 我们将在下一章开始实际工作,但在此之前我们需要一点理论知识。让我们先来了解一下 Kubernetes 实际上是什么以及它解决的问题。

Kubernetes 是一个运行容器的平台。它负责启动您的容器化应用程序、部署更新、维护服务级别、按需扩展、确保访问安全以及更多。Kubernetes 的两个核心概念是API,您使用它来定义您的应用程序,以及集群,它运行您的应用程序。集群是一组配置了容器运行时(如 Docker)的独立服务器,然后通过 Kubernetes 联合成一个单一的逻辑单元。图 1.1 显示了集群的高级视图。

图 1.1 Kubernetes 集群是一组可以运行容器的服务器,它们被联合成一个组。

集群管理员管理着称为 Kubernetes 中的 节点 的单个服务器。您可以通过添加节点来扩展集群的容量,将节点下线进行维护,或者在整个集群中推出 Kubernetes 的升级。在像 Microsoft Azure Kubernetes 服务 (AKS) 或 Amazon Elastic Kubernetes 服务 (EKS) 这样的托管服务中,这些功能都封装在简单的网页界面或命令行中。在正常使用中,您会忘记底层的节点,并将集群视为一个单一实体。

Kubernetes 集群的存在是为了运行您的应用程序。您在 YAML 文件中定义您的应用程序,并将这些文件发送到 Kubernetes API。Kubernetes 会查看您在 YAML 中请求的内容,并将其与集群中已运行的内容进行比较。它会进行任何必要的更改以到达期望的状态,这可能包括更新配置、删除容器或创建新的容器。容器被分布在整个集群中以实现高可用性,并且它们都可以通过 Kubernetes 管理的虚拟网络进行通信。图 1.2 展示了部署过程,但没有显示节点,因为我们在这个层面上并不真正关心它们。

图片

图 1.2 当您将应用程序部署到 Kubernetes 集群时,通常可以忽略实际的节点。

定义应用程序的结构是您的职责,但运行和管理一切则是 Kubernetes 的工作。如果一个节点在集群中下线并带走了某些容器,Kubernetes 会注意到这一点,并在其他节点上启动替换容器。如果一个应用程序容器变得不健康,Kubernetes 可以重新启动它。如果一个组件因为高负载而处于压力之下,Kubernetes 可以在新的容器中启动该组件的额外副本。如果您在 Docker 镜像和 Kubernetes YAML 文件上投入了工作,您将得到一个自我修复的应用程序,该应用程序在任何 Kubernetes 集群上都能以相同的方式运行。

Kubernetes 管理的不仅仅是容器,这也是它成为一个完整应用程序平台的原因。集群有一个分布式数据库,您可以使用它来存储应用程序的配置文件以及像 API 密钥和连接凭证这样的机密信息。Kubernetes 将这些无缝地传递到您的容器中,让您可以在每个环境中使用相同的容器镜像,并从集群中应用正确的配置。Kubernetes 还提供存储,这样您的应用程序就可以在容器之外维护数据,为有状态应用程序提供高可用性。Kubernetes 还通过将其发送到正确的容器进行处理来管理进入集群的网络流量。图 1.3 展示了这些其他资源,它们是 Kubernetes 的主要功能。

图片

图 1.3 Kubernetes 不仅管理容器,集群还管理其他资源。

我还没有谈论容器中的应用程序看起来是什么样子;这是因为 Kubernetes 实际上并不关心。你可以在多个容器中的微服务上运行使用云原生设计构建的新应用程序。你可以在一个大型容器中运行作为单体构建的遗留应用程序。它们可以是 Linux 应用程序或 Windows 应用程序。你可以使用相同的 API 在 YAML 文件中定义所有类型的应用程序,并在单个集群上运行它们。与 Kubernetes 一起工作的乐趣在于,它为所有应用程序添加了一层一致性——无论是旧的 .NET 和 Java 单体还是新的 Node.js 和 Go 微服务,都是以相同的方式进行描述、部署和管理的。

这就是我们需要开始学习 Kubernetes 的所有理论,但在我们继续之前,我想把我一直在谈论的概念用一些正确的名字来命名。那些 YAML 文件正确地被称为 应用程序清单,因为它们是所有组成应用的组件列表。这些组件是 Kubernetes 资源;它们也有正确的名字。图 1.4 将图 1.3 中的概念应用于正确的 Kubernetes 资源名称。

图片

图 1.4 真实的情况:这些是你需要掌握的最基本的 Kubernetes 资源。

我告诉你 Kubernetes 很难。 😃 但我们将在接下来的几章中逐一介绍这些资源,逐步加深理解。在你完成第六章时,那张图将完全有意义,你将在定义这些资源并在自己的 Kubernetes 集群中运行它们方面获得大量的经验。

1.2 这本书适合你吗?

本书的目标是加速你的 Kubernetes 学习,让你在完成本书后,能够自信地定义和运行自己的应用在 Kubernetes 中,并了解生产路径的样子。学习 Kubernetes 的最佳方式是实践,如果你遵循章节中的所有示例并完成实验室练习,那么在完成本书时,你将对 Kubernetes 的所有最重要的组成部分有一个坚实的理解。

但 Kubernetes 是一个庞大的主题,我不会涵盖所有内容。最大的差距在于管理。我不会深入探讨集群设置和管理,因为它们在不同的基础设施中各不相同。如果你计划在云中运行 Kubernetes 作为你的生产环境,那么许多这些担忧在托管服务中已经得到解决。如果你想获得 Kubernetes 认证,这本书是一个很好的起点,但它不会带你走完全程。有两个主要的 Kubernetes 认证:认证 Kubernetes 应用开发者(CKAD)和认证 Kubernetes 管理员(CKA)。本书涵盖了 CKAD 课程的大约 80% 和 CKA 的大约 50%。

此外,你还需要一定的背景知识才能有效地使用这本书。我将在遇到 Kubernetes 功能时解释许多核心原则,但不会填补关于容器的任何空白。如果你不熟悉像镜像、容器和注册表这样的概念,我建议从我的书《一个月午餐时间学习 Docker》(Manning,2020)开始。你不需要与 Kubernetes 一起使用 Docker,但它是最简单、最灵活的方式来打包你的应用程序,以便你可以在 Kubernetes 容器中运行它们。

如果你将自己归类为新手或正在提高的 Kubernetes 用户,并且对容器有合理的实际操作知识,那么这本书就是为你准备的。你的背景可能是开发、运维、架构、DevOps 或站点可靠性工程(SRE)——Kubernetes 触及了所有这些角色,所以所有这些角色都欢迎加入,你将学到大量的东西。

1.3 创建你的实验室环境

一个 Kubernetes 集群可以有数百个节点,但在这本书的练习中,单个节点集群就足够了。我们现在将设置你的实验室环境,以便你准备好在下一章开始。有数十种 Kubernetes 平台可供选择,这本书中的练习应该适用于任何认证的 Kubernetes 设置。我将描述如何在 Linux、Windows、Mac、Amazon Web Services(AWS)和 Azure 上创建你的实验室,这涵盖了所有主要选项。我使用的是 Kubernetes 版本 1.18,但早期或晚期的版本也应该没问题。

在本地运行 Kubernetes 最简单的方法是 Docker Desktop,这是一个包含 Docker、Kubernetes 以及所有命令行工具的单个包。它还很好地集成了你的计算机网络,并有一个方便的重置 Kubernetes 按钮,如果需要,可以清除所有内容。Docker Desktop 支持 Windows 10 和 macOS,如果这对你不起作用,我还会介绍一些替代方案。

你应该知道的一个要点是:Kubernetes 自身的组件需要以 Linux 容器的形式运行。你无法在 Windows 上运行 Kubernetes(尽管你可以在多节点 Kubernetes 集群中运行 Windows 应用程序的容器),所以如果你在 Windows 上工作,你需要一个 Linux 虚拟机(VM)。Docker Desktop 会为你设置并管理它。

对于 Windows 用户,请注意:请使用 PowerShell 来跟随练习。PowerShell 支持许多 Linux 命令,并且“立即尝试”练习是构建在 Linux(和 Mac)shell 以及 PowerShell 上运行的。如果你尝试使用经典的 Windows 命令行终端,你可能会从一开始就遇到问题。

1.3.1 下载本书的源代码

每个示例和练习都在 GitHub 上本书的源代码仓库中,包括所有实验室的示例解决方案。如果你熟悉 Git 并且安装了 Git 客户端,你可以使用以下命令将仓库克隆到你的计算机上:

git clone https://github.com/sixeyed/kiamol

如果你不是 Git 用户,你可以浏览到这本书的 GitHub 页面github.com/sixeyed/kiamol并点击克隆或下载按钮来下载一个 zip 文件,然后你可以展开它。

源代码的根目录是一个名为kiamol的文件夹,其中包含每个章节的文件夹:ch02ch03等等。章节中的第一个练习通常要求你打开一个终端会话并切换到chXX目录,所以你需要首先导航到你的kiamol文件夹。

GitHub 仓库是我发布任何练习更正的最快方式,所以如果你有任何问题,你应该检查章节文件夹中的更新 README 文件。

1.3.2 安装 Docker Desktop

Docker Desktop 可以在 Windows 10 或 macOS Sierra(版本 10.12 或更高)上运行。浏览到www.docker.com/products/docker-desktop并选择安装稳定版本。下载安装程序并运行它,接受所有默认设置。在 Windows 上,这可能包括重启以添加新的 Windows 功能。当 Docker Desktop 运行时,你会在 Windows 任务栏或 Mac 菜单栏附近看到 Docker 的海豚图标。如果你是 Windows 上的经验丰富的 Docker Desktop 用户,你需要确保你处于 Linux 容器模式(这是新安装的默认设置)。

Kubernetes 默认没有设置,所以你需要点击海豚图标打开菜单并点击设置。这会打开图 1.5 所示的窗口;从菜单中选择 Kubernetes 并选择启用 Kubernetes。

图 1.5 Docker Desktop 创建并管理一个 Linux 虚拟机来运行容器,并且它可以运行 Kubernetes。

Docker Desktop 下载 Kubernetes 运行时的所有容器镜像——这可能需要一段时间——然后启动一切。当你看到设置屏幕底部的两个绿色圆点时,你的 Kubernetes 集群就准备就绪了。Docker Desktop 安装了你需要的所有其他东西,所以你可以跳到 1.4.7 节。

其他 Kubernetes 发行版可以在 Docker Desktop 之上运行,但它们与 Docker Desktop 使用的网络设置集成得不好,所以你会在运行练习时遇到问题。Docker Desktop 中的 Kubernetes 选项具有这本书所需的所有功能,并且绝对是最佳选择。

1.3.3 安装 Docker 社区版和 K3s

如果你正在使用 Linux 机器或 Linux 虚拟机,运行单节点集群有多种选择。Kind 和 minikube 很受欢迎,但我的首选是 K3s,它是一个最小化安装,但包含了你进行练习所需的所有功能。(这个名字是对“K8s”的戏谑,K8s 是 Kubernetes 的缩写。K3s 精简了 Kubernetes 代码库,名字表明它的大小是 K8s 的一半。)

K3s 与 Docker 兼容,因此首先,您应该安装 Docker Community Edition。您可以在 rancher.com/docs/k3s/latest/en/quick-start/ 查看完整的安装步骤,但这将帮助您启动并运行:

# install Docker:
curl -fsSL https://get.docker.com | sh

# install K3s:
curl -sfL https://get.k3s.io | sh -s - --docker --disable=traefik --write-kubeconfig-mode=644

如果您更喜欢在虚拟机中运行实验室环境,并且熟悉使用 Vagrant 来管理虚拟机,您可以使用以下 Vagrant 设置,其中包含 Docker 和 K3s,这些都可以在本书的源代码库中找到:

# from the root of the Kiamol repo:
cd ch01/vagrant-k3s

# provision the machine:
vagrant up

# and connect:
vagrant ssh

K3s 安装了您需要的所有其他内容,因此您可以跳到 1.4.7 节。

1.3.4 安装 Kubernetes 命令行工具

您可以使用名为 kubectl 的工具(发音为“cube-cuttle”,就像“章鱼”——不要让任何人告诉您不同)来管理 Kubernetes。它连接到 Kubernetes 集群并与 Kubernetes API 一起工作。Docker Desktop 和 K3s 都会为您安装 kubectl,但如果您正在使用以下描述的另一种选项,您需要自行安装它。

完整的安装说明请参阅kubernetes.io/docs/tasks/tools/install-kubectl/. 您可以在 macOS 上使用 Homebrew,在 Windows 上使用 Chocolatey,而对于 Linux,您可以下载二进制文件:

# macOS:
brew install kubernetes-cli

# OR Windows:
choco install kubernetes-cli

# OR Linux:
curl -Lo ./kubectl https://storage.googleapis.com/kubernetes-release/release/v1.18.8/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl

1.3.5 在 Azure 中运行单个节点 Kubernetes 集群

您可以使用 AKS 在 Microsoft Azure 中运行托管 Kubernetes 集群。如果您希望从多台机器访问集群或拥有 Azure 信用额的 MSDN 订阅,这可能是一个不错的选择。您可以运行一个最小化单节点集群,这不会花费太多,但请注意,没有停止集群的方法,您将全天候付费,直到您将其删除。

Azure 门户提供了一个很好的用户界面来创建 AKS 集群,但使用 az 命令会更简单。您可以在 docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough 查看最新文档,但您可以通过下载 az 命令行工具并运行几个命令来开始,如下所示:

# log in to your Azure subscription:
az login

# create a resource group for the cluster:
az group create --name kiamol --location eastus

# create a single-code cluster with 2 CPU cores and 8GB RAM:
az aks create --resource-group kiamol --name kiamol-aks --node-count 1
              --node-vm-size Standard_DS2_v2 --kubernetes-version 1.18.8 --generate-ssh-keys

# download certificates to use the cluster with kubectl:
az aks get-credentials --resource-group kiamol --name kiamol-aks

最后一条命令将下载凭证,以便从您的本地 kubectl 命令行连接到 Kubernetes API。

1.3.6 在 AWS 中运行单个节点 Kubernetes 集群

AWS 中的托管 Kubernetes 服务被称为弹性 Kubernetes 服务 (EKS)。您可以使用与 Azure 相同的注意事项创建单个节点的 EKS 集群——那就是您将一直为该节点及其相关资源付费,只要它在运行。

您可以使用 AWS 门户创建 EKS 集群,但推荐的方式是使用一个名为 eksctl 的专用工具。该工具的最新文档位于 eksctl.io,但使用起来相当简单。首先,按照以下步骤安装适用于您操作系统的最新版本的工具:

# install on macOS:
brew tap weaveworks/tap
brew install weaveworks/tap/eksctl

# OR on Windows:
choco install eksctl

# OR on Linux:
curl --silent --location
 "https://github.com/weaveworks/eksctl/releases/download/latest/eksctl_$(uname -s)_amd64.tar.gz"
 | tar xz -C /tmp
sudo mv /tmp/eksctl /usr/local/bin

假设您已经安装了 AWS CLI,eksctl 将使用 CLI 的凭证(如果没有,请检查 eksctl 的认证指南)。然后按照以下步骤创建一个简单的单节点集群:

# create a single node cluster with 2 CPU cores and 8GB RAM:
eksctl create cluster --name=kiamol --nodes=1 --node-type=t3.large

工具设置了从您本地的 kubectl 到 EKS 集群的连接。

1.3.7 验证您的集群

现在您有一个正在运行的 Kubernetes 集群,无论您选择了哪个选项,它们都以相同的方式工作。运行以下命令以检查您的集群是否正在运行:

kubectl get nodes

您应该看到如图 1.6 所示的输出。这是一个包含您集群中所有节点及其一些基本详情(如状态和 Kubernetes 版本)的列表。您的集群详情可能不同,但只要您看到有节点列出并且处于就绪状态,那么您的集群就可以正常运行了。

图片

图 1.6 如果您可以运行 kubectl 并且您的节点已就绪,那么您就可以继续进行了。

1.4 立即有效

“立即有效”是《午餐月系列》的核心原则。总的来说,重点是学习技能并将它们付诸实践,在接下来的每一章中都是如此。

每一章都从一个简短的主题介绍开始,然后是“立即尝试”练习,您可以使用自己的 Kubernetes 集群将这些想法付诸实践。然后是一个总结,其中包含更多细节,以填补您在深入研究时可能有的疑问。最后,有一个供您自己尝试的动手实验室,以真正增强您对新理解的自信。

所有主题都围绕在现实世界中真正有用的任务。您将在本章中学习如何立即有效地使用主题,并通过理解如何应用新技能来结束学习。让我们开始运行一些容器化应用程序吧!

2 在 Kubernetes 中使用 Pods 和 Deployments 运行容器

Kubernetes 为您的应用程序工作负载运行容器,但容器本身不是您需要与之交互的对象。每个容器都属于一个 Pod,Pod 是 Kubernetes 对象,用于管理一个或多个容器,而 Pod 反过来由其他资源管理。这些高级资源抽象掉了容器的细节,这为自我修复应用程序提供了动力,并允许您使用期望状态工作流程:您告诉 Kubernetes 您想要发生什么,然后它决定如何实现。

在本章中,我们将从 Kubernetes 的基本构建块开始:Pods,它们运行容器,以及 Deployments,它们管理 Pods。我们将使用一个简单的 Web 应用程序进行练习,您将通过使用 Kubernetes 命令行工具来管理应用程序和使用 Kubernetes YAML 规范来定义应用程序来获得实践经验。

2.1 Kubernetes 如何运行和管理容器

容器是一个虚拟化环境,通常运行单个应用程序组件。Kubernetes 将容器包装在另一个虚拟化环境中:Pod。Pod 是一个计算单元,在集群的单个节点上运行。Pod 有自己的虚拟 IP 地址,由 Kubernetes 管理,集群中的 Pod 可以通过该虚拟网络与其他 Pod 进行通信,即使它们运行在不同的节点上。

您通常在 Pod 中运行单个容器,但您可以在一个 Pod 中运行多个容器,这为一些有趣的部署选项打开了大门。Pod 中的所有容器都是同一虚拟环境的一部分,因此它们共享相同的网络地址,可以使用 localhost 进行通信。图 2.1 显示了容器和 Pod 之间的关系。

图片

图 2.1 容器在 Pod 内运行。您管理 Pod,Pod 管理容器。

这关于多容器 Pod 的业务在早期介绍可能有点多,但如果我略过了它,只谈论单容器 Pod,您会合理地询问为什么 Kubernetes 使用 Pod 而不是仅仅使用容器。让我们运行一个 Pod,看看与容器抽象交互是什么样的。

现在尝试一下 您可以使用 Kubernetes 命令行运行一个简单的 Pod,而无需 YAML 规范。语法类似于使用 Docker 运行容器:您指定要使用的容器镜像以及任何其他配置 Pod 行为的参数。

# run a Pod with a single container; the restart flag tells Kubernetes
# to create just the Pod and no other resources:
kubectl run hello-kiamol --image=kiamol/ch02-hello-kiamol --restart=Never

# wait for the Pod to be ready:
kubectl wait --for=condition=Ready pod hello-kiamol

# list all the Pods in the cluster:
kubectl get pods

# show detailed information about the Pod:
kubectl describe pod hello-kiamol

您可以在图 2.2 中看到我的输出,其中我简化了来自最终describe pod命令的响应。当您自己运行时,您会在其中看到很多听起来更晦涩的信息,比如节点选择器和容忍度。它们都是 Pod 规范的一部分,Kubernetes 为我们在run命令中未指定的所有内容都应用了默认值。

图片

图 2.2 运行最简单的 Pod 并使用 kubectl 检查其状态

现在你的集群中有一个单独的应用容器,运行在单个 Pod 内部。如果你习惯于使用 Docker,这将是一个熟悉的流程,而且事实证明,Pod 并没有看起来那么复杂。你的大多数 Pod 将运行单个容器(直到你开始探索更高级的选项),因此你可以有效地将 Pod 视为 Kubernetes 用来运行容器的机制。

虽然 Kubernetes 实际上并不运行容器,但它将这项责任转交给节点上安装的容器运行时,这可能是 Docker、containerd 或更复杂的其他东西。这就是为什么 Pod 是一个抽象:它是 Kubernetes 管理的资源,而容器则由 Kubernetes 之外的东西管理。你可以通过使用 kubectl 获取有关 Pod 的特定信息来感受到这一点。

现在尝试一下 Kubectl 通过get pod命令返回基本信息,但你可以通过应用输出参数来请求更多信息。你可以在输出参数中命名你想要看到的单个字段,并且可以使用 JSONPath 查询语言或 Go 模板来获取复杂输出。

# get the basic information about the Pod:
kubectl get pod hello-kiamol

# specify custom columns in the output, selecting network details:
kubectl get pod hello-kiamol --output custom-columns=NAME:metadata.name,NODE_IP:status.hostIP,POD_IP:status.podIP 

# specify a JSONPath query in the output,
# selecting the ID of the first container in the Pod:
kubectl get pod hello-kiamol -o jsonpath='{.status.containerStatuses[0].containerID}'

我的输出显示在图 2.3 中。我正在使用 Windows 上的 Docker Desktop 运行一个单节点 Kubernetes 集群。第二个命令中的node IP是我的 Linux VM 的 IP 地址,而Pod IP是集群中 Pod 的虚拟地址。第三个命令返回的container ID前面带有容器运行时的名称;我的容器运行时是 Docker。

图片

图 2.3 Kubectl 为 Pod 和其他对象提供了许多自定义输出的选项。

这可能感觉像是一项相当枯燥的练习,但它带来了两个重要的启示。第一个是 kubectl 是一个非常强大的工具——作为你与 Kubernetes 的主要接触点,你将花费大量时间与之打交道,因此了解它能够做什么是非常有价值的。查询命令的输出是一种查看你关心的信息的有用方式,而且因为你可以访问资源的所有详细信息,它对于自动化也非常有用。第二个启示是提醒我们,Kubernetes 并不运行容器——Pod 中的容器 ID 是另一个运行容器的系统的引用。

当 Pod 创建时,它们会被分配到一个节点上,并且管理 Pod 及其容器的责任属于该节点。它通过使用一个名为容器运行时接口(CRI)的已知 API 与容器运行时协作来完成这项工作。CRI 允许节点以相同的方式管理所有不同的容器运行时中的容器。它使用标准 API 来创建和删除容器,并查询它们的状态。当 Pod 运行时,节点会与容器运行时协作,确保 Pod 拥有它需要的所有容器。

现在尝试一下 所有 Kubernetes 环境都使用相同的 CRI 机制来管理容器,但并非所有容器运行时都允许您访问 Kubernetes 之外的容器。这个练习向您展示了 Kubernetes 节点如何保持 Pod 容器运行,但您只有在使用 Docker 作为容器运行时的情况下才能理解它。

# find the Pod’s container:
docker container ls -q --filter label=io.kubernetes.container.name=hello-kiamol

# now delete that container:
docker container rm -f $(docker container ls -q --filter label=io.kubernetes.container.name=hello-kiamol)

# check the Pod status:
kubectl get pod hello-kiamol

# and find the container again:
docker container ls -q --filter label=io.kubernetes.container.name=hello-kiamol

您可以从图 2.4 中看到,当我在删除我的 Docker 容器时,Kubernetes 做出了反应。一瞬间,Pod 中的容器数量为零,但 Kubernetes 立即创建了一个替换容器来修复 Pod 并将其恢复到正确状态。

是从容器到 Pod 的抽象让 Kubernetes 能够修复这类问题。失败的容器是一个暂时性的故障;Pod 仍然存在,并且可以通过一个新的容器将其恢复到规范状态。这只是 Kubernetes 提供的自我修复的一个层次,在 Pod 之上还有更多的抽象,这为您的应用程序提供了更多的弹性。

图 2.4 Kubernetes 确保 Pod 拥有所有需要的容器。

其中一个抽象是 Deployment,我们将在下一节中探讨。在继续之前,让我们看看 Pod 中实际运行的是什么。它是一个 Web 应用程序,但您无法浏览到它,因为我们还没有配置 Kubernetes 将网络流量路由到 Pod。我们可以使用 kubectl 的另一个功能来解决这个问题。

现在尝试一下 Kubectl 可以将节点流量转发到 Pod,这是一种从集群外部与 Pod 进行通信的快捷方式。您可以在您的机器上的特定端口上监听——这是您集群中的单个节点——并将流量转发到 Pod 中运行的应用程序。

# listen on port 8080 on your machine and send traffic
# to the Pod on port 80:
kubectl port-forward pod/hello-kiamol 8080:80

# now browse to http://localhost:8080

# when you’re done press ctrl-c to end the port forward

我的输出如图 2.5 所示,您可以看到它是一个非常基础的网站(不要联系我进行网页设计咨询)。Web 服务器和所有内容都打包在 Docker Hub 上的容器镜像中,这是公开可用的。所有兼容 CRI 的容器运行时都可以拉取这个镜像并从中运行容器,所以我知道,无论您使用的是哪种 Kubernetes 环境,当您运行应用程序时,它将以与我相同的方式为您工作。

图 2.5 此应用程序未配置为接收网络流量,但 kubectl 可以转发它。

现在我们已经很好地掌握了 Pod,这是 Kubernetes 中最小的计算单元。您需要了解这一切是如何工作的,但 Pod 是一个原始资源,在正常使用中,您永远不会直接运行 Pod;您总是会创建一个控制器对象来为您管理 Pod。

2.2 使用控制器运行 Pod

这只是第二章的第二部分,我们已经开始接触到一个新的 Kubernetes 对象,它是对其他对象的抽象。Kubernetes 确实很快就会变得复杂,但这样的复杂性是这样一个强大且可配置的系统所必需的。学习曲线是进入世界级容器平台的入场费。

Pod 本身过于简单,单独使用没有太大用处;它们是应用程序的隔离实例,每个 Pod 都分配给一个节点。如果该节点离线,Pod 就会丢失,Kubernetes 不会替换它。你可以尝试通过运行多个 Pod 来获得高可用性,但无法保证 Kubernetes 不会将它们全部运行在同一个节点上。即使你确实将 Pod 分散在多个节点上,你也需要自己管理它们。为什么要在有可以为你管理它们的编排器的情况下还这么做呢?

正是在这里,控制器发挥了作用。控制器是 Kubernetes 资源,用于管理其他资源。它与 Kubernetes API 协同工作,监视系统的当前状态,将其与资源的期望状态进行比较,并做出必要的更改。Kubernetes 有许多控制器,但用于管理 Pod 的主要控制器是 Deployment,它解决了我刚才描述的问题。如果一个节点离线并且你丢失了一个 Pod,Deployment 将在另一个节点上创建一个替换 Pod;如果你想扩展你的 Deployment,你可以指定你想要的 Pod 数量,Deployment 控制器将在多个节点上运行它们。图 2.6 显示了 Deployment、Pod 和容器之间的关系。

图片

图 2.6 部署控制器管理 Pod,Pod 管理容器。

你可以使用 kubectl 创建 Deployment 资源,指定你想要运行的容器镜像和 Pod 的任何其他配置。Kubernetes 创建 Deployment,Deployment 创建 Pod。

现在尝试一下:创建另一个 Web 应用的实例,这次使用 Deployment。唯一必需的参数是 Deployment 的名称和要运行的镜像。

# create a Deployment called "hello-kiamol-2", running the same web app:
kubectl create deployment hello-kiamol-2 --image=kiamol/ch02-hello-kiamol

# list all the Pods:
kubectl get pods

你可以在图 2.7 中看到我的输出。现在你的集群中有两个 Pod:你使用 kubectl run命令创建的原始 Pod,以及由 Deployment 创建的新 Pod。由 Deployment 管理的 Pod 有一个由 Kubernetes 生成的名称,即 Deployment 的名称后跟一个随机后缀。

图片

图 2.7 创建控制器资源,它将创建自己的资源——部署创建 Pod。

从这个练习中,你需要意识到的一个重要事情是:你创建了 Deployment,但你并没有直接创建 Pod。Deployment 规范描述了你想要的 Pod,Deployment 创建了那个 Pod。Deployment 是一个控制器,它会与 Kubernetes API 核对正在运行的资源,意识到它应该管理的 Pod 不存在,并使用 Kubernetes API 创建它。确切的机制并不重要;你只需与 Deployment 协同工作,并依赖它来创建你的 Pod。

虽然部署如何跟踪其资源确实很重要,因为这是 Kubernetes 大量使用的模式。任何 Kubernetes 资源都可以应用标签,这些标签是简单的键值对。你可以添加标签来记录自己的数据。例如,你可能会为部署添加一个名为 release 的标签,其值为 20.04,以指示此部署来自 20.04 发布周期。Kubernetes 还使用标签来松散耦合资源,映射部署和其 Pod 等对象之间的关系。

现在试试看 部署为它管理的 Pod 添加标签。使用以下 kubectl 命令来打印部署添加的标签,然后列出匹配该标签的 Pod:

# print the labels that the Deployment adds to the Pod:
kubectl get deploy hello-kiamol-2 -o jsonpath='{.spec.template.metadata.labels}'

# list the Pods that have that matching label:
kubectl get pods -l app=hello-kiamol-2

我的输出显示在图 2.8 中,你可以看到资源配置的一些内部细节。部署使用模板创建 Pod,该模板的一部分是元数据字段,它包括 Pod 的标签。在这种情况下,部署为 Pod 添加了一个名为app的标签,其值为hello-kiamol-2。查询具有匹配标签的 Pod 会返回由部署管理的单个 Pod。

图 2.8 部署在创建 Pod 时添加标签,你可以使用这些标签作为过滤器。

使用标签来识别资源之间的关系是 Kubernetes 中的一个核心模式,值得展示一个图表以确保其清晰。资源可以在创建时应用标签,并在其生命周期内添加、删除或编辑。控制器使用标签选择器来识别它们管理的资源。这可以是一个简单的查询,匹配具有特定标签的资源,如图 2.9 所示。

图 2.9 控制器通过使用标签和选择器来识别它们管理的资源。

这个过程是灵活的,因为它意味着控制器不需要维护它们管理的所有资源的列表;标签选择器是控制器规范的一部分,控制器可以通过查询 Kubernetes API 在任何时候找到匹配的资源。这也需要你小心处理,因为你可以编辑资源的标签,最终破坏它与控制器之间的关系。

现在试试看 部署与其创建的 Pod 没有直接关系;它只知道需要有一个 Pod 具有与它的标签选择器匹配的标签。如果你编辑 Pod 上的标签,部署将不再识别它。

# list all Pods, showing the Pod name and labels:
kubectl get pods -o custom-columns=NAME:metadata.name,LABELS:metadata.labels

# update the "app" label for the Deployment’s Pod:
kubectl label pods -l app=hello-kiamol-2 --overwrite app=hello-kiamol-x

# fetch Pods again:
kubectl get pods -o custom-columns=NAME:metadata.name,LABELS:metadata.labels

你期望发生什么?从图 2.10 所示的输出中可以看出,更改 Pod 标签实际上将 Pod 从部署中移除。此时,部署看到没有匹配其标签选择器的 Pod 存在,因此它创建了一个新的 Pod。部署已经完成了它的任务,但通过直接编辑 Pod,你现在有一个未管理的 Pod。

图 2.10 如果你干涉 Pod 上的标签,你可以将其从部署的控制中移除。

这可以是一种有用的调试技术——从控制器中移除一个 Pod,以便你可以连接并调查问题,同时控制器启动一个替换 Pod,保持你的应用程序以期望的规模运行。你也可以做相反的事情:编辑 Pod 的标签,让控制器误以为该 Pod 是它管理的集合的一部分。

现在尝试一下 通过设置应用程序标签使其与标签选择器匹配,将原始 Pod 返回到 Deployment 的控制之下。

# list all Pods with a label called "app," showing the Pod name and
# labels:
kubectl get pods -l app -o custom-columns=NAME:metadata.name,LABELS:metadata.labels

# update the "app" label for the the unmanaged Pod:
kubectl label pods -l app=hello-kiamol-x --overwrite app=hello-kiamol-2

# fetch the Pods again:
kubectl get pods -l app -o custom-columns=NAME:metadata.name,LABELS:metadata.labels

这个练习实际上逆转了之前的练习,将应用程序标签重新设置为 hello-kiamol-2,以便在 Deployment 中的原始 Pod。现在当 Deployment 控制器通过 API 检查时,它看到两个与它的标签选择器匹配的 Pods。然而,它应该只管理一个 Pod,因此它删除了一个(使用一系列删除规则来决定删除哪一个)。你可以在图 2.11 中看到 Deployment 删除了第二个 Pod 并保留了原始的。

图 2.11

图 2.11

Pods 运行你的应用程序容器,但就像容器一样,Pods 的设计目的是短暂的。你通常会使用更高层次的资源,如 Deployment,来为你管理 Pods。这样做可以让 Kubernetes 在容器或节点出现问题时更好地保持你的应用程序运行,但最终 Pods 运行的是你将运行的相同容器,并且你的应用程序的用户体验将保持不变。

现在尝试一下 Kubectl 的 port-forward 命令将流量发送到 Pod,但你不需要为 Deployment 找到随机的 Pod 名称。你可以在 Deployment 资源上配置端口转发,并且 Deployment 会选择其 Pod 中的一个作为目标。

# run a port forward from your local machine to the Deployment:
kubectl port-forward deploy/hello-kiamol-2 8080:80

# browse to http://localhost:8080

# when you’re done, exit with ctrl-c

你可以看到我的输出,如图 2.12 所示,同一个应用程序在同一个 Docker 镜像中运行,但这次是在由 Deployment 管理的 Pod 中。

图 2.12

图 2.12

Pods 和 Deployments 是本章我们将涵盖的唯一资源。你可以通过使用 kubectl 的 runcreate 命令来部署非常简单的应用程序,但对于更复杂的应用程序,需要更多的配置,而这些命令无法做到。现在是时候进入 Kubernetes YAML 的世界了。

2.3 在应用程序清单中定义 Deployment

应用程序清单是 Kubernetes 最吸引人的特性之一,但也是最具挫败感之一。当你正在处理数百行 YAML,试图找到导致你的应用程序崩溃的小错误配置时,可能会觉得 API 是故意编写来混淆和激怒你的。在这些时候,请记住 Kubernetes 清单是应用程序的完整描述,可以在源代码控制中进行版本控制和跟踪,并在任何 Kubernetes 集群上产生相同的部署。

清单可以写成 JSON 或 YAML;JSON 是 Kubernetes API 的本地语言,但 YAML 是清单的首选,因为它更容易阅读,允许你在单个文件中定义多个资源,最重要的是,可以在规范中记录注释。列表 2.1 是你能编写的最简单的应用程序清单。它使用我们在本章中已经使用过的相同容器镜像定义了一个单个 Pod。

列表 2.1 pod.yaml,一个运行单个容器的单个 Pod

# Manifests always specify the version of the Kubernetes API
# and the type of resource.
apiVersion: v1
kind: Pod

# Metadata for the resource includes the name (mandatory) 
# and labels (optional).
metadata:
 name: hello-kiamol-3

# The spec is the actual specification for the resource.
# For a Pod the minimum is the container(s) to run, 
# with the container name and image.
spec:
 containers:
  - name: web
    image: kiamol/ch02-hello-kiamol

这比 kubectl run命令所需的信息要多得多,但应用程序清单的大优点是它是声明性的。kubectl runcreate是命令式操作——是你告诉 Kubernetes 做什么。清单是声明性的——你告诉 Kubernetes 你想要的结果是什么,然后它去决定需要做什么来实现这一点。

现在尝试一下:你仍然使用 kubectl 从清单文件部署应用程序,但使用apply命令,它告诉 Kubernetes 将文件中的配置应用到集群中。使用与列表 2.1 相同内容的 YAML 文件运行本章示例应用程序的另一个 Pod。

# switch from the root of the kiamol repository to the chapter 2 folder:
cd ch02

# deploy the application from the manifest file:
kubectl apply -f pod.yaml

# list running Pods:
kubectl get pods

新的 Pod 与使用 kubectl run命令创建的 Pod 工作方式相同:它被分配到一个节点上,并运行一个容器。图 2.13 中的输出显示,当我应用清单时,Kubernetes 决定需要创建一个 Pod 来将集群的当前状态提升到我所期望的状态。这是因为清单指定了一个名为 hello-kiamol-3 的 Pod,而这样的 Pod 并不存在。

图片

图 2.13 应用清单将 YAML 文件发送到 Kubernetes API,该 API 应用更改。

现在 Pod 正在运行,你可以使用 kubectl 以相同的方式管理它:通过列出 Pod 的详细信息并运行端口转发将流量发送到 Pod。最大的不同之处在于清单易于分享,并且基于清单的部署是可重复的。我可以多次运行相同的 kubectl apply命令和相同的清单,结果始终相同:一个名为 hello-kiamol-3 的 Pod 运行我的 Web 容器。

现在尝试一下:kubectl 甚至不需要本地清单文件的副本。它可以从任何公共 URL 读取内容。直接从 GitHub 上的文件部署相同的 Pod 定义。

# deploy the application from the manifest file:
kubectl apply -f https://raw.githubusercontent.com/sixeyed/kiamol/
master/ch02/pod.yaml

图 2.14 显示了输出。资源定义与集群中运行的 Pod 相匹配,因此 Kubernetes 不需要做任何事情,kubectl 显示匹配的资源没有变化。

图片

图 2.14 Kubectl 可以从 Web 服务器下载清单文件并将其发送到 Kubernetes API。

当你处理更高级的资源时,应用程序清单开始变得更有趣。当你在一个 YAML 文件中定义 Deployment 时,所需字段之一是 Deployment 应该运行的 Pod 的规范。这个 Pod 规范与定义 Pod 的相同 API,因此 Deployment 定义是一个组合,它包括 Pod 规范。列表 2.2 显示了 Deployment 资源的最小定义,运行的是同一网络应用的另一个版本。

列表 2.2 deployment.yaml,Deployment 和 Pod 规范

# Deployments are part of the apps version 1 API spec.
apiVersion: apps/v1
kind: Deployment
# The Deployment needs a name.
metadata:
 name: hello-kiamol-4
# The spec includes the label selector the Deployment uses 
# to find its own managed resources--I’m using the app label,
# but this could be any combination of key-value pairs.
spec:
 selector:
  matchLabels:
   app: hello-kiamol-4
 # The template is used when the Deployment creates a Pod
 template.
  # Pods in a Deployment don’t have a name, 
  # but they need to specify labels that match the selector
  # metadata.
   labels:
    app: hello-kiamol-4
  # The Pod spec lists the container name and image
  spec.
   containers:
    - name: web
      image: kiamol/ch02-hello-kiamol

这个清单是为一个完全不同的资源(碰巧运行了相同的应用程序),但所有 Kubernetes 清单都是使用 kubectl apply 以相同的方式部署的。这为你提供了所有应用程序的一致性层——无论它们多么复杂,你都会在一个或多个 YAML 文件中定义它们,并使用相同的 kubectl 命令部署它们。

现在试试看 应用 Deployment 清单以创建一个新的 Deployment,这将反过来创建一个新的 Pod。

# run the app using the Deployment manifest:
kubectl apply -f deployment.yaml

# find Pods managed by the new Deployment:
kubectl get pods -l app=hello-kiamol-4

图 2.15 中的输出显示了与使用 kubectl create 创建 Deployment 相同的最终结果,但我的整个应用程序规范清晰地定义在一个单独的 YAML 文件中。

图 2.15 应用清单创建 Deployment,因为不存在匹配的资源。

随着应用程序复杂性的增长,我需要指定我想要多少个副本,应该应用什么 CPU 和内存限制,Kubernetes 如何检查应用程序是否健康,以及应用程序配置设置从何而来以及写入数据的位置——我可以通过添加 YAML 来完成所有这些。

2.4 在 Pod 中处理应用程序

Pods 和 Deployments 存在是为了让你的应用程序运行,但所有真正的工作都在容器中发生。你的容器运行时可能不会直接给你访问容器的能力——托管 Kubernetes 集群不会给你 Docker 或 containerd 的控制权——但你仍然可以使用 kubectl 在 Pods 中与容器一起工作。Kubernetes 命令行让你可以在容器中运行命令、查看应用程序日志和复制文件。

现在试试看 你可以使用 kubectl 在容器内运行命令,并连接一个终端会话,这样你就可以像连接到远程机器一样连接到 Pod 的容器。

# check the internal IP address of the first Pod we ran: 
kubectl get pod hello-kiamol -o custom-columns=NAME:metadata.name,POD_IP:status.podIP

# run an interactive shell command in the Pod:
kubectl exec -it hello-kiamol -- sh

# inside the Pod, check the IP address:
hostname -i

# and test the web app:
wget -O - http://localhost | head -n 4

# leave the shell:
exit

我的输出显示在图 2.16 中,你可以看到容器环境中的 IP 地址是由 Kubernetes 设置的,运行在容器中的 Web 服务器可以通过 localhost 地址访问。

图 2.16 你可以使用 kubectl 在 Pod 容器内运行命令,包括交互式 shell。

在 Pod 容器内运行交互式 shell 是一种有用的方式,可以看到对该 Pod 的世界观。你可以读取文件内容以检查配置设置是否正确应用,运行 DNS 查询以验证服务是否按预期解析,以及 ping 端点以测试网络。这些都是很好的故障排除技术,但对于持续管理,一个更简单的选项是读取应用程序日志,而 kubectl 有一个专门的命令专门用于此。

现在尝试一下 Kubernetes 从容器运行时获取应用程序日志。你可以使用 kubectl 读取日志,如果你有权访问容器运行时,你可以验证它们与容器日志相同。

# print the latest container logs from Kubernetes:
kubectl logs --tail=2 hello-kiamol

# and compare the actual container logs--if you’re using Docker:
docker container logs --tail=2 $(docker container ls -q --filter label=io.kubernetes.container.name=hello-kiamol)

你可以从图 2.17 中看到我的输出,Kubernetes 正是按照容器运行时接收到的日志条目原样转发日志。

图片

图 2.17 Kubernetes 从容器中读取日志,因此你不需要访问容器运行时。

所有 Pods 都提供相同的功能,无论它们是如何创建的。由控制器管理的 Pods 具有随机名称,因此你不会直接引用它们。相反,你可以通过控制器或标签来访问它们。

现在尝试一下 你可以在不知道 Pod 名称的情况下运行由 Deployment 管理的 Pods 中的命令,并且可以查看所有匹配标签选择器的 Pods 的日志。

# make a call to the web app inside the container for the 
# Pod we created from the Deployment YAML file: 
kubectl exec deploy/hello-kiamol-4 -- sh -c 'wget -O - http://localhost > /dev/null'

# and check that Pod’s logs:
kubectl logs --tail=1 -l app=hello-kiamol-4

图 2.18 显示了在 Pod 容器内运行的命令,这会导致应用程序写入日志条目。我们在 Pod 日志中看到了这一点。

图片

图 2.18 你可以使用 kubectl 与 Pods 一起工作,而无需知道 Pod 的名称。

在生产环境中,你可以收集所有 Pods 的日志并发送到中央存储系统,但在到达那里之前,这是一种有用且简单的方法来读取应用程序日志。在那个练习中,你也看到了访问由控制器管理的 Pods 的不同方式。Kubectl 允许你在大多数命令中提供标签选择器,并且一些命令——如exec——可以针对不同的目标运行。

你可能最常与 Pods 一起使用的功能是与文件系统交互。Kubectl 允许你在本地机器和 Pods 中的容器之间复制文件。

现在尝试一下 在你的机器上创建一个临时目录,并将文件从 Pod 容器复制到该目录中。

# create the local directory:
mkdir -p /tmp/kiamol/ch02

# copy the web page from the Pod:
kubectl cp hello-kiamol:/usr/share/nginx/html/index.html /tmp/kiamol/ch02/index.html

# check the local file contents:
cat /tmp/kiamol/ch02/index.html

在图 2.19 中,你可以看到 kubectl 将文件从 Pod 容器复制到我的本地机器。这适用于你的 Kubernetes 集群是本地运行还是远程服务器,并且它是双向的,因此你可以使用相同的命令将本地文件复制到 Pod。这可能是一种有用——如果有些不正规——的方法来绕过应用程序问题。

图片

图 2.19 在 Pod 容器和本地机器之间复制文件对于故障排除很有用。

这就是本章我们将要涵盖的所有内容,但在我们继续之前,我们需要删除我们正在运行的 Pods,这比你想象的要复杂一些。

2.5 理解 Kubernetes 资源管理

你可以使用 kubectl 轻松地删除 Kubernetes 资源,但资源可能不会保持删除状态。如果你使用控制器创建了一个资源,那么管理该资源就是控制器的职责。它拥有资源生命周期,并且不期望任何外部干扰。如果你删除了一个受管理的资源,那么它的控制器将创建一个替代品。

现在尝试一下 使用 kubectl 的delete命令删除所有 Pods 并验证它们是否真的被删除了。

# list all running Pods:
kubectl get pods

# delete all Pods:
kubectl delete pods --all

# check again:
kubectl get pods

你可以在图 20.20 中看到我的输出。这是你预期的结果吗?

图片

图 2.20 控制器拥有自己的资源。如果其他东西删除了它们,控制器会重新创建。

其中有两个 Pod 是通过run命令和 YAML Pod 规范直接创建的。它们没有控制器来管理它们,所以当你删除它们时,它们会保持删除状态。另外两个是由 Deployments 创建的,当你删除 Pod 时,Deployment 控制器仍然存在。它们看到没有 Pod 匹配它们的标签选择器,所以它们会创建新的 Pods。

当你知道这一点时,这似乎很明显,但这是一个可能会在你与 Kubernetes 共度的每一天中不断出现的陷阱。如果你想删除由控制器管理的资源,你需要删除控制器。当控制器被删除时,它们会清理自己的资源,所以删除 Deployment 就像是一个级联删除,也会删除所有 Deployment 的 Pods。

现在尝试一下 检查你正在运行的 Deployments,然后删除它们并确认剩余的 Pods 已经被删除。

# view Deployments:
kubectl get deploy

# delete all Deployments:
kubectl delete deploy --all

# view Pods:
kubectl get pods

# check all resources:
kubectl get all

图 2.21 显示了我的输出。我足够快地看到了 Pods 正在被移除,所以它们显示在终止状态。几秒钟后,Pods 和 Deployment 都被移除了,所以我唯一运行的资源就是 Kubernetes API 服务器本身。

图片

图 2.21 删除控制器会引发级联效应,控制器会删除其所有资源。

现在你的 Kubernetes 集群没有运行任何应用程序,它已经回到了原始状态。

我们在本章中涵盖了大量的内容。你对 Kubernetes 如何使用 Pods 和 Deployments 管理容器有了很好的理解,对 YAML 规范也有了一定的了解,并且通过使用 kubectl 与 Kubernetes API 交互积累了丰富的经验。我们逐步构建了核心概念,但你可能已经有一个相当清晰的认识,那就是 Kubernetes 是一个复杂的系统。如果你有时间去完成下面的实验,那将无疑有助于巩固你所学的知识。

2.6 实验

这是你的第一个实验;这是一个需要你自己完成的挑战。目标是编写一个 Kubernetes YAML 规范,用于在 Pod 中运行应用程序,然后测试应用程序以确保它按预期运行。以下是一些帮助你开始的提示:

  • 在 ch02/lab 文件夹中,有一个名为 pod.yaml 的文件,你可以尝试运行。它运行应用程序,但定义的是 Pod 而不是 Deployment。

  • 应用程序容器运行一个监听 80 端口的网站。

  • 当你转发流量到该端口时,Web 应用程序会响应运行在其上的机器的主机名。

  • 那个主机名实际上是 Pod 的名称,你可以使用 kubectl 来验证。

如果你觉得这有点棘手,我有一个在 GitHub 上的示例解决方案,你可以作为参考使用:github.com/sixeyed/kiamol/blob/master/ch02/lab/README.md

3 通过服务在网络中连接 Pod

Pods 是运行在 Kubernetes 中的应用程序的基本构建块。大多数应用程序都分布在多个组件上,你可以在 Kubernetes 中使用每个组件的 Pod 来模拟这些组件。例如,你可能有一个网站 Pod 和一个 API Pod,或者你可能在一个微服务架构中有数十个 Pod。它们都需要进行通信,Kubernetes 支持标准的网络协议,TCP 和 UDP。这两个协议都使用 IP 地址来路由流量,但是当 Pod 被替换时,IP 地址会发生变化,因此 Kubernetes 提供了一个带有服务的网络地址发现机制。

服务是灵活的资源,支持在 Pod 之间、从集群外部到 Pod 以及从 Pod 到外部系统之间路由流量。在本章中,你将了解 Kubernetes 提供的所有不同服务配置,以粘合系统,并了解它们是如何为你的应用程序透明工作的。

3.1 Kubernetes 如何路由网络流量

在上一章中,你了解了关于 Pod 的两个重要事情:Pod 是一个由 Kubernetes 分配 IP 地址的虚拟环境,Pod 是可丢弃的资源,其生命周期由另一个资源控制。如果一个 Pod 想要与另一个 Pod 通信,它可以使用 IP 地址。然而,这有两个问题:首先,如果 Pod 被替换,IP 地址会发生变化,其次,没有简单的方法可以找到 Pod 的 IP 地址——它只能通过 Kubernetes API 来发现。

现在试试看 如果你部署了两个 Pod,你可以从另一个 Podping 它,但首先你需要找到它的 IP 地址。

# start up your lab environment--run Docker Desktop if it's not running--
# and switch to this chapter’s directory in your copy of the source code:
cd ch03

# create two Deployments, which each run one Pod:
kubectl apply -f sleep/sleep1.yaml -f sleep/sleep2.yaml

# wait for the Pod to be ready:
kubectl wait --for=condition=Ready pod -l app=sleep-2

# check the IP address of the second Pod:
kubectl get pod -l app=sleep-2 --output jsonpath='{.items[0].status.podIP}'

# use that address to ping the second Pod from the first:
kubectl exec deploy/sleep-1 -- ping -c 2 $(kubectl get pod -l app=sleep-2 --output jsonpath='{.items[0].status.podIP}')

我的输出显示在图 3.1 中。容器内的 ping 操作正常,第一个 Pod 能够成功到达第二个 Pod,但我不得不使用 kubectl 找到 IP 地址并将其传递给ping命令。

图 3.1 使用 IP 地址的 Pod 网络——你只能从 Kubernetes API 中找到地址。

Kubernetes 中的虚拟网络跨越整个集群,因此即使 Pod 在不同的节点上运行,它们也可以通过 IP 地址进行通信。这个例子在单个节点的 K3s 集群和 100 个节点的 AKS 集群上以相同的方式工作。这是一个有用的练习,可以帮助你看到 Kubernetes 不做任何特殊的网络魔法;它只是使用了你的应用程序已经使用的标准协议。你通常不会这样做,因为 IP 地址是特定于一个 Pod 的,当 Pod 被替换时,替换的 Pod 将有一个新的 IP 地址。

现在试试看 这些 Pod 由 Deployment 控制器管理。如果你删除了第二个 Pod,其控制器将启动一个新的 IP 地址的替换 Pod。

# check the current Pod’s IP address:
kubectl get pod -l app=sleep-2 --output jsonpath='{.items[0].status.podIP}'

# delete the Pod so the Deployment replaces it:
kubectl delete pods -l app=sleep-2

# check the IP address of the replacement Pod:
kubectl get pod -l app=sleep-2 --output jsonpath='{.items[0].status.podIP}'

在图 3.2 中,我的输出显示替换后的 Pod 有一个不同的 IP 地址,如果我尝试 ping 旧地址,命令会失败。

图 3.2 Pod 的 IP 地址不是其规范的一部分;替换后的 Pod 有一个新的地址。

需要为可能变化的资源提供永久地址的问题是一个老问题——互联网使用 DNS(域名系统)解决了这个问题,将友好名称映射到 IP 地址,Kubernetes 也使用了相同的系统。Kubernetes 集群内置了一个 DNS 服务器,它将服务名称映射到 IP 地址。图 3.3 展示了 Pod 到 Pod 通信的域名查找过程。

图片

图 3.3 显示服务允许 Pod 使用固定域名进行通信。

这种类型的服务是对 Pod 及其网络地址的抽象,就像 Deployment 是对 Pod 及其容器的抽象一样。服务有自己的 IP 地址,它是静态的。当消费者向该地址发起网络请求时,Kubernetes 将其路由到 Pod 的实际 IP 地址。服务与其 Pod 之间的连接是通过标签选择器设置的,就像 Deployment 与 Pod 之间的连接一样。

列表 3.1 展示了服务的最小 YAML 规范,使用 app 标签来识别网络流量的最终目标 Pod。

列表 3.1 sleep2-service.yaml,最简单的服务定义

apiVersion: v1    # Services use the core v1 API.
kind: Service

metadata:
  name: sleep-2   # The name of a Service is used as the DNS domain name.

# The specification requires a selector and a list of ports.
spec:
  selector:
    app: sleep-2  # Matches all Pods with an app label set to sleep-2.
  ports:
    - port: 80    # Listens on port 80 and sends to port 80 on the Pod 

此服务定义与之前练习中运行的一个 Deployment 兼容。当您部署它时,Kubernetes 创建一个名为 sleep-2 的 DNS 条目,将流量路由到由 sleep-2 Deployment 创建的 Pod。其他 Pod 可以使用服务名称作为域名向该 Pod 发送流量。

现在试试看。您可以使用 YAML 文件和常规的 kubectl apply 命令部署一个服务。部署服务后,验证网络流量是否被路由到 Pod。

# deploy the Service defined in listing 3.1:
kubectl apply -f sleep/sleep2-service.yaml

# show the basic details of the Service:
kubectl get svc sleep-2

# run a ping command to check connectivity--this will fail:
kubectl exec deploy/sleep-1 -- ping -c 1 sleep-2

我的输出显示在图 3.4 中,您可以看到名称解析工作正常,尽管 ping 命令没有按预期工作,因为 ping 使用的是 Kubernetes 服务不支持的网络协议。

图片

图 3.4 部署服务创建了一个 DNS 条目,为服务名称提供了一个固定的 IP 地址。

这就是 Kubernetes 中服务发现背后的基本概念:部署一个服务资源,并使用服务的名称作为组件通信的域名。

不同的服务类型支持不同的网络模式,但您以相同的方式与它们一起工作。接下来,我们将更详细地研究 Pod 到 Pod 的网络,通过一个简单分布式应用的示例来展示。

3.2 在 Pod 之间路由流量

Kubernetes 中服务的默认类型称为 ClusterIP。它创建一个集群范围内的 IP 地址,任何节点上的 Pod 都可以访问。该 IP 地址仅在集群内部有效,因此 ClusterIP 服务仅适用于 Pod 之间的通信。这正是分布式系统所需要的,其中某些组件是内部的,不应该在集群外部访问。我们将使用一个简单的网站来演示这一点,该网站使用内部 API 组件。

现在尝试一下 运行两个 Deployment,一个用于 Web 应用,一个用于 API。此应用尚未定义任何服务,并且它将无法正常工作,因为网站找不到 API。

# run the website and API as separate Deployments: 
kubectl apply -f numbers/api.yaml -f numbers/web.yaml

# wait for the Pod to be ready:
kubectl wait --for=condition=Ready pod -l app=numbers-web

# forward a port to the web app:
kubectl port-forward deploy/numbers-web 8080:80

# browse to the site at http://localhost:8080 and click the Go button
# --you'll see an error message

# exit the port forward:
ctrl-c

您可以从我在图 3.5 中显示的输出中看到,应用失败,显示 API 不可用的消息。

图片

图 3.5 Web 应用运行但无法正常工作,因为对 API 的网络调用失败。

错误页面还显示了网站期望找到 API 的域名——http://numbers-api。这并不是一个完全限定的域名(如blog.sixeyed.com);这是一个应由本地网络解析的地址,但 Kubernetes 中的 DNS 服务器无法解析它,因为没有名为numbers-api的服务。列表 3.2 中的规范显示了一个具有正确名称的 Service 和一个匹配 API Pod 的标签选择器。

列表 3.2 api-service.yaml,随机数 API 的服务

apiVersion: v1
kind: Service

metadata:
  name: numbers-api     # The Service uses the domain name numbers-api.

spec:
  ports:
    - port: 80
  selector:
    app: numbers-api    #  Traffic is routed to Pods with this label.
  type: ClusterIP       #  This Service is available only to other Pods.

此服务与列表 3.1 中的类似,只是名称已更改,并且明确指出了 ClusterIP 服务类型。这可以省略,因为它是默认的服务类型,但我认为包含它可以使规范更清晰。部署服务将路由 Web Pod 和 API Pod 之间的流量,修复应用而无需更改部署或 Pod。

现在尝试一下 为 API 创建一个服务,以便域名查找工作,并将流量从 Web Pod 发送到 API Pod。

# deploy the Service from listing 3.2:
kubectl apply -f numbers/api-service.yaml

# check the Service details:
kubectl get svc numbers-api

# forward a port to the web app:
kubectl port-forward deploy/numbers-web 8080:80

# browse to the site at http://localhost:8080 and click the Go button

# exit the port forward:
ctrl-c

我在图 3.6 中显示的输出显示了应用正常工作,网站显示由 API 生成的随机数。

图片

图 3.6 部署服务修复了 Web 应用和 API 之间的断链。

在服务、部署和 Pod 之外,这里的重要教训是您的 YAML 规范描述了您的整个应用程序在 Kubernetes 中——那就是所有组件及其之间的网络。Kubernetes 不会对您的应用程序架构做出假设;您需要在 YAML 中指定它。这个简单的 Web 应用需要定义三个 Kubernetes 资源才能在当前状态下工作——两个 Deployment 和一个 Service,但所有这些移动部件的优势是提高了弹性。

现在尝试一下 API Pod 由 Deployment 控制器管理,因此您可以删除 Pod,并将创建一个替换品。替换品也符合 API Service 中的标签选择器,因此流量被路由到新的 Pod,应用继续工作。

# check the name and IP address of the API Pod:
kubectl get pod -l app=numbers-api -o custom-columns=NAME:metadata.name,POD_IP:status.podIP

# delete that Pod:
kubectl delete pod -l app=numbers-api

# check the replacement Pod:
kubectl get pod -l app=numbers-api -o custom-columns=NAME:metadata.name,POD_IP:status.podIP 

# forward a port to the web app:
kubectl port-forward deploy/numbers-web 8080:80

# browse to the site at http://localhost:8080 and click the Go button

# exit the port forward:
ctrl-c

图 3.7 显示 Deployment 控制器创建了一个替换 Pod。它是相同的 API Pod 规范,但在一个新的 Pod 中运行,具有新的 IP 地址。不过,API 服务的 IP 地址没有改变,Web Pod 可以在相同的网络地址上到达新的 API Pod。

图片

图 3.7 服务将 Web Pod 与 API Pod 隔离开来,因此 API Pod 是否更改无关紧要。

在这些练习中,我们手动删除 Pods 以触发控制器创建替换,但在 Kubernetes 应用程序的正常生命周期中,Pod 替换一直在发生。每次你更新应用程序的任何组件——添加功能、修复错误或发布依赖项的更新——你都在替换 Pods。任何节点宕机时,其 Pods 都会在其他节点上替换。服务抽象层保持应用程序通过这些替换进行通信。

这个演示应用程序还不完整,因为它还没有配置好以接收来自集群外部的流量并将其发送到 Web Pod。我们到目前为止已经使用了端口转发,但这实际上是一种调试技巧。真正的解决方案是为 Web Pod 也部署一个服务。

3.3 将外部流量路由到 Pods

你有几种方法可以配置 Kubernetes 以监听进入集群的流量并将其转发到 Pod。我们将从一种简单灵活的方法开始,这对于从本地开发到生产的一切都适用。这是一种称为 LoadBalancer 的服务类型,它解决了将流量引导到可能运行在不同节点上的 Pod 的问题;图 3.8 显示了其外观。

图 3.8

图 3.8 LoadBalancer 服务将来自任何节点的外部流量路由到匹配的 Pod。

这看起来像是一个棘手的问题,特别是因为你可能有多个匹配服务标签选择器的 Pod,因此集群需要选择一个节点来发送流量,然后在该节点上选择一个 Pod。所有这些复杂性都由 Kubernetes 处理——这就是世界级的编排——所以你只需要部署一个 LoadBalancer 服务。列表 3.3 显示了 Web 应用程序的服务规范。

列表 3.3 web-service.yaml,一个用于外部流量的 LoadBalancer 服务

apiVersion: v1
kind: Service
metadata:
  name: numbers-web
spec:
  ports:
    - port: 8080          # The port the Service listens on
      targetPort: 80      # The port the traffic is sent to on the Pod
  selector:
    app: numbers-web
  type: LoadBalancer      # This Service is available for external traffic.

此服务监听 8080 端口并将流量发送到端口 80 的 Web Pod。当你部署它时,你将能够使用 Web 应用程序而无需在 kubectl 中设置端口转发,但如何到达应用程序的确切细节将取决于你如何运行 Kubernetes。

现在尝试一下 部署服务,然后使用 kubectl 找到服务的地址。

# deploy the LoadBalancer Service for the website--if your firewall checks 
# that you want to allow traffic, then it is OK to say yes:
kubectl apply -f numbers/web-service.yaml

# check the details of the Service:
kubectl get svc numbers-web

# use formatting to get the app URL from the EXTERNAL-IP field:
kubectl get svc numbers-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080'

图 3.9 显示了我在 Docker Desktop Kubernetes 集群上运行练习的输出,我可以浏览到地址为 http://localhost:8080 的网站。

图 3.9

图 3.9 Kubernetes 从其运行的平台上请求 LoadBalancer 服务的 IP 地址。

使用 K3s 或云中的托管 Kubernetes 集群时,输出会有所不同,其中服务部署为负载均衡器创建一个专用的外部 IP 地址。图 3.10 显示了使用同一 YAML 规范在 Linux VM 上的 K3s 集群执行相同练习的输出(这里网站位于 http://172.28.132.127:8080)。

图 3.10

图 3.10 不同 Kubernetes 平台为 LoadBalancer 服务使用不同的地址。

同样的应用清单为什么结果会不同呢?我在第一章提到过,你可以以不同的方式部署 Kubernetes,它们都是相同的 Kubernetes(我的强调),但这并不完全正确。Kubernetes 包含了许多扩展点,各个发行版在实现某些功能时都有一定的灵活性。LoadBalancer 服务就是一个很好的例子,其实现方式因发行版的目标而异。

  • Docker Desktop 是一个本地开发环境。它运行在单个机器上,并与网络栈集成,因此 LoadBalancer 服务在 localhost 地址上可用。每个 LoadBalancer 服务都会发布到 localhost,因此如果你部署了多个负载均衡器,你需要使用不同的端口。

  • K3s 支持使用自定义组件设置路由表,从而支持 LoadBalancer 服务。每个 LoadBalancer 服务都会发布到你的机器(或虚拟机)的 IP 地址,因此你可以通过 localhost 或网络上的远程机器访问服务。与 Docker Desktop 类似,你需要为每个负载均衡器使用不同的端口。

  • 类似于 AKS 和 EKS 这样的云 Kubernetes 平台是高度可用的多节点集群。部署 Kubernetes LoadBalancer 服务会在你的云中创建一个实际的负载均衡器,它覆盖了集群中的所有节点——云负载均衡器将传入的流量发送到集群中的一个节点,然后 Kubernetes 将其路由到 Pod。每个 LoadBalancer 服务都会分配一个不同的 IP 地址,并且它是一个公网地址,可以从互联网访问。

在其他 Kubernetes 功能中,我们还会看到这种模式,即各个发行版提供了不同的资源,有不同的目标。最终,YAML 清单是相同的,最终结果也是一致的,但 Kubernetes 允许发行版在实现过程中有所差异。

在标准 Kubernetes 的世界中,你还可以使用另一种 Service 类型来监听进入集群的网络流量并将其导向 Pod——即 NodePort。NodePort 服务不需要外部负载均衡器——集群中的每个节点都会监听服务中指定的端口,并将流量发送到 Pod 的目标端口。图 3.11 展示了其工作原理。

图 3.11

图 3.11 NodePort 服务也会将外部流量路由到 Pod,但它们不需要负载均衡器。

NodePort 服务没有 LoadBalancer 服务的灵活性,因为每个服务都需要不同的端口,你的节点需要是公开可访问的,而且你无法在多节点集群中实现负载均衡。NodePort 服务在各个发行版中的支持级别也不同,因此在 K3s 和 Docker Desktop 中可以按预期工作,但在 Kind 中则不太理想。列表 3.4 展示了一个 NodePort 规范以供参考。

列表 3.4 web-service-nodePort.yaml,一个 NodePort 服务规范

apiVersion: v1
kind: Service
metadata:
  name: numbers-web-node
spec:
  ports:
    - port: 8080          # The port on which the Service is available to
                          # other Pods
      targetPort: 80      # The port on which the traffic is sent to on 
                          # the Pod
      nodePort: 30080     # The port on which the Service is available
                          # externally
  selector:
  app: numbers-web
  type: NodePort          # This Service is available on node IP addresses.

没有部署此 NodePort 服务的练习(尽管 YAML 文件在章节文件夹中,如果您想尝试的话)。这部分原因是因为它不是在每种发行版上以相同的方式工作,所以这一节将结束于许多需要尝试才能理解的 if 分支。但更重要的是——您通常不会在生产环境中使用 NodePort,并且最好让您的配置文件在不同环境中尽可能一致。坚持使用 LoadBalancer 服务意味着您从开发到生产都有相同的规范,这意味着需要维护和同步的 YAML 文件更少。

我们将通过深入了解服务在底层是如何工作的来结束本章,但在那之前,我们将探讨您可以使用服务的一种更多方式,即从 Pod 与集群外部的组件进行通信。

3.4 在 Kubernetes 外部路由流量

您几乎可以在 Kubernetes 中运行任何服务器软件,但这并不意味着您应该这样做。例如,数据库等存储组件通常是运行在 Kubernetes 外部的典型候选者,尤其是如果您正在云中部署,并且可以使用托管数据库服务。或者,您可能正在数据中心运行,需要与不会迁移到 Kubernetes 的现有系统集成。无论您使用什么架构,您仍然可以使用 Kubernetes 服务进行域名解析,以访问集群外部的组件。

实现这一点的第一种方法是使用 ExternalName 服务,它就像一个从一个域名到另一个域名的别名。ExternalName 服务允许您在应用程序 Pod 中使用本地名称,当 Pod 进行查找请求时,Kubernetes 中的 DNS 服务器将本地名称解析为完全限定的外部名称。图 3.12 展示了它是如何工作的,一个 Pod 使用本地名称,该名称解析为外部系统地址。

图片

图 3.12 使用 ExternalName 服务允许您使用本地集群地址访问远程组件。

本章的演示应用程序预期使用本地 API 生成随机数,但只需部署一个 ExternalName 服务,就可以切换到从 GitHub 上的文本文件中读取静态数字。

现在尝试一下:您不能在 Kubernetes 的每个版本中都将服务从一种类型切换到另一种类型,因此您在部署 ExternalName 服务之前需要删除 API 的原始 ClusterIP 服务。

# delete the current API Service:
kubectl delete svc numbers-api

# deploy a new ExternalName Service:
kubectl apply -f numbers-services/api-service-externalName.yaml

# check the Service configuration:
kubectl get svc numbers-api

# refresh the website in your browser and test with the Go button

我的结果如图 3.13 所示。您可以看到应用程序以相同的方式工作,并且它使用相同的 API URL。但是,如果您刷新页面,您会发现它总是返回相同的数字,因为它不再使用随机数 API。

图片

图 3.13 ExternalName 服务可以用作重定向,将请求发送到集群外部。

ExternalName 服务可以是一种有用的方式来处理你无法在应用配置中绕过的环境差异。也许你有一个应用组件,它使用硬编码的字符串作为数据库服务器的名称。在开发环境中,你可以创建一个带有预期域名的 ClusterIP 服务,该服务解析到在 Pod 中运行的测试数据库;在生产环境中,你可以使用一个解析到数据库服务器真实域名的 ExternalName 服务。列表 3.5 显示了 API 外部名称的 YAML 规范。

列表 3.5 api-service-externalName.yaml,一个 ExternalName 服务

apiVersion: v1
kind: Service
metadata:
  name: numbers-api   # The local domain name of the Service in the cluster
spec:
  type: ExternalName
  externalName: raw.githubusercontent.com   # The domain to resolve

Kubernetes 使用 DNS 的一个标准功能——规范名称(CNAMEs)来实现 ExternalName 服务。当 Web Pod 对numbers-api域名进行 DNS 查找时,Kubernetes DNS 服务器返回 CNAME,即 raw.githubusercontent.com。然后 DNS 解析继续使用节点上配置的 DNS 服务器,因此它会连接到互联网以找到 GitHub 服务器的 IP 地址。

现在尝试一下,服务是集群范围内的 Kubernetes Pod 网络的一部分,所以任何 Pod 都可以使用服务。本章第一个练习中的 sleep Pods 在容器镜像中有一个DNS lookup命令,你可以使用它来检查 API 服务的解析。

# run the DNS lookup tool to resolve the Service name:
kubectl exec deploy/sleep-1 -- sh -c 'nslookup numbers-api | tail -n 5'

当你尝试这样做时,你可能会得到一些看起来像错误的混乱结果,因为 Nslookup 工具返回了大量的信息,并且每次运行时顺序都不相同。不过,你想要的数据确实在里面。我重复执行了几次命令,以获得图 3.14 中看到的适合打印的输出。

图 3.14

图 3.14 在 Kubernetes 中,应用默认不是隔离的,所以任何 Pod 都可以为任何服务进行查找。

关于 ExternalName 服务,有一件重要的事情需要理解,你可以从这个练习中看到:它们最终只是给你的应用提供一个地址来使用,但它们实际上并没有改变应用发出的请求。这对于像数据库这样的组件来说是可以的,因为它们通过 TCP 进行通信,但对于 HTTP 服务来说就不那么简单了。HTTP 请求在头字段中包含目标主机名,这不会与 ExternalName 响应的实际域名匹配,所以客户端调用可能会失败。本章中的随机数应用有一些绕过这个问题的代码,手动设置主机头,但这种方法最适合非 HTTP 服务。

在集群中路由本地域名到外部系统还有一个其他选项。它不能修复 HTTP 头问题,但当你想要将流量路由到 IP 地址而不是域名时,它允许你使用与 ExternalName 服务类似的方法。这些是无头服务,定义为 ClusterIP 服务类型,但没有标签选择器,因此它们永远不会匹配任何 Pod。相反,服务通过一个端点资源来部署,该资源明确列出服务应解析的 IP 地址。

列表 3.6 显示了一个端点具有单个 IP 地址的无头服务。它还展示了 YAML 的新用法,通过三个短横线分隔定义了多个资源。

列表 3.6 api-service-headless.yaml,一个具有显式地址的服务

apiVersion: v1
kind: Service
metadata:
  name: numbers-api
spec:
  type: ClusterIP      # No selector field makes this a headless Service.
  ports:
    - port: 80
---
kind: Endpoints        # The endpoint is a separate resource.
apiVersion: v1
metadata:
  name: numbers-api
subsets: 
  - addresses:         # It has a static list of IP addresses . . . 
      - ip: 192.168.123.234
    ports:
      - port: 80       # and the ports they listen on.

该端点规范中的 IP 地址是一个假的,但 Kubernetes 不会验证该地址是否可达,因此这段代码将无错误地部署。

现在尝试一下 替换外部名称服务为这个无头服务。这将导致应用失败,因为 API 域名现在解析到一个不可访问的 IP 地址。

# remove the existing Service:
kubectl delete svc numbers-api

# deploy the headless Service:
kubectl apply -f numbers-services/api-service-headless.yaml

# check the Service:
kubectl get svc numbers-api

# check the endpoint: 
kubectl get endpoints numbers-api

# verify the DNS lookup:
kubectl exec deploy/sleep-1 -- sh -c 'nslookup numbers-api | grep "^[^*]"'

# browse to the app--it will fail when you try to get a number

我在图 3.15 中显示的输出确认了 Kubernetes 会愉快地让你部署一个会破坏你应用的服务变更。域名解析了内部集群 IP 地址,但任何对该地址的网络调用都会失败,因为它们被路由到端点中实际存在的 IP 地址。

图片

图 3.15 服务配置错误可能会破坏你的应用,即使没有部署应用变更。

该练习的输出引发了一些有趣的问题:为什么 DNS 查找返回的是集群 IP 地址而不是端点地址?为什么域名以.default.svc.cluster.local 结尾?你不需要网络工程背景就可以使用 Kubernetes 服务,但如果你理解服务解析的实际工作方式,这将有助于你追踪问题——这正是我们结束本章的方式。

3.5 理解 Kubernetes 服务解析

Kubernetes 通过使用服务(Services),这些服务建立在成熟的网络技术之上,支持所有应用可能需要的网络配置。应用组件在 Pod 中运行,并使用标准传输协议和 DNS 名称进行发现来与其他 Pod 进行通信。你不需要任何特殊的代码或库;你的应用在 Kubernetes 中的工作方式与你在物理服务器或虚拟机上部署它们时相同。

在本章中,我们已经涵盖了所有服务类型及其典型用例,因此现在你对可以使用的设计模式有了很好的理解。如果你觉得这里有很多细节,请放心,大多数时候你将部署集群 IP 服务,这需要很少的配置。它们大多无缝工作,但深入理解堆栈是有用的。图 3.16 显示了下一级别的细节。

图片

图 3.16 显示 Kubernetes 运行 DNS 服务器和代理,并使用标准网络工具。

关键要点是 ClusterIP 是一个虚拟 IP 地址,它不在网络上存在。Pod 通过节点上运行的 kube-proxy 访问网络,它使用数据包过滤将虚拟 IP 发送到实际端点。Kubernetes 服务只要存在就保持它们的 IP 地址,服务可以独立于应用程序的任何其他部分存在。服务有一个控制器,每当 Pod 发生变化时都会更新端点列表,因此客户端始终使用静态虚拟 IP 地址,kube-proxy 始终有最新的端点列表。

现在尝试一下 您可以看到当 Pod 发生变化时,通过列出 Pod 变化之间的服务端点,Kubernetes 如何立即更新端点列表。端点使用与服务相同的名称,您可以使用 kubectl 查看端点详情。

# show the endpoints for the sleep-2 Service:
kubectl get endpoints sleep-2

# delete the Pod:
kubectl delete pods -l app=sleep-2

# check the endpoint is updated with the IP of the replacement Pod:
kubectl get endpoints sleep-2

# delete the whole Deployment:
kubectl delete deploy sleep-2

# check the endpoint still exists, with no IP addresses:
kubectl get endpoints sleep-2

您可以在图 3.17 中看到我的输出,这是对第一个问题的回答——Kubernetes DNS 返回集群 IP 地址而不是端点,因为端点地址会变化。

图 3-17

图 3.17 服务的集群 IP 地址不会改变,但端点列表始终在更新。

使用静态虚拟 IP 意味着客户端可以无限期地缓存 DNS 查找响应(许多应用程序错误地将其作为性能节省),并且无论随着时间的推移发生多少 Pod 替换,该 IP 地址都将继续工作。关于域名后缀的第二个问题需要通过侧向思考来回答,即查看 Kubernetes 的命名空间

每个 Kubernetes 资源都存在于一个命名空间内,这是一个您可以使用它来分组其他资源的资源。命名空间是逻辑上划分 Kubernetes 集群的一种方式——您可以为每个产品创建一个命名空间,为每个团队创建一个,或者使用一个共享的命名空间。我们暂时不会使用命名空间,但我在这里介绍它们,因为它们在 DNS 解析中扮演着一定的角色。图 3.18 显示了命名空间如何进入服务名称。

图 3-18

图 3.18 命名空间逻辑上划分了一个集群,但服务可以在命名空间之间访问。

您的集群中已经有了几个命名空间——我们迄今为止部署的所有资源都已在default命名空间中创建(这是默认的;这就是为什么我们不需要在我们的 YAML 文件中指定命名空间)。内部 Kubernetes 组件,如 DNS 服务器和 Kubernetes API,也在kube-system命名空间中的 Pod 中运行。

现在尝试一下 Kubectl 具有命名空间感知能力——您可以使用命名空间标志来处理默认命名空间之外的资源。

# check the Services in the default namespace:
kubectl get svc --namespace default

# check Services in the system namespace:
kubectl get svc -n kube-system

# try a DNS lookup to a fully qualified Service name:
kubectl exec deploy/sleep-1 -- sh -c 'nslookup numbers-api.default.svc.cluster.local | grep "^[^*]"'

# and for a Service in the system namespace:
kubectl exec deploy/sleep-1 -- sh -c 'nslookup kube-dns.kube-system.svc.cluster.local | grep "^[^*]"'

我在图 3.19 中展示的输出回答了第二个问题——服务的本地域名只是服务名称,但这是对包括 Kubernetes 命名空间在内的完全限定域名的一个别名。

图 3-19

图 3.19 您可以使用相同的 kubectl 命令查看不同命名空间中的资源。

在您的 Kubernetes 之旅早期了解命名空间很重要,仅因为这样可以帮助您看到核心 Kubernetes 功能也是作为 Kubernetes 应用程序运行的,但除非您明确设置命名空间,否则您在 kubectl 中看不到它们。命名空间是一种强大的方式,可以将您的集群细分以增加利用率,同时不牺牲安全性,我们将在第十一章中再次讨论它们。

目前我们已经完成了命名空间和服务的操作。在本章中,您了解到每个 Pod 都有自己的 IP 地址,Pod 通信最终使用该地址通过标准的 TCP 和 UDP 协议。尽管如此,您永远不会直接使用 Pod IP 地址——您总是创建一个服务资源,Kubernetes 使用该资源通过 DNS 提供服务发现。服务支持多种网络模式,不同的服务类型配置 Pod 之间的网络流量、从外部世界进入 Pod 的网络流量以及从 Pod 到外部世界的网络流量。您还了解到服务有自己的生命周期,独立于 Pod 和部署,因此在继续之前,我们最后要做的就是清理。

现在尝试一下 删除部署也会删除其所有 Pod,但服务没有级联删除。它们是独立的对象,需要单独删除。

# delete Deployments:
kubectl delete deploy --all 

# and Services:
kubectl delete svc --all 

# check what’s running:
kubectl get all

现在您的集群又清空了,尽管如此,如图 3.20 所示,您在使用一些 kubectl 命令时需要小心。

图 3-20

图 3.20 您需要明确删除您创建的任何服务,但要注意all参数。

3.6 实验室

这个实验将让您练习创建服务,但也会让您思考标签和选择器,这些都是 Kubernetes 的强大功能。目标是部署随机数字应用程序的更新版本的服务,该版本已经进行了用户界面改造。以下是您的提示:

  • 本章的实验文件夹中有一个deployments.yaml文件。使用该文件通过 kubectl 部署应用程序。

  • 检查 Pods——有运行着两个版本的 Web 应用程序。

  • 编写一个服务,使 API 可以通过域名numbers-api供其他 Pod 使用。

  • 编写一个服务,使网站的第 2 版可以在外部端口 8088 上使用。

  • 您需要仔细查看 Pod 标签才能得到正确的结果。

这个实验是本章练习的扩展,如果您想检查我的解决方案,可以在 GitHub 上找到该书的存储库:github.com/sixeyed/kiamol/blob/master/ch03/lab/README.md

4 配置应用程序的 ConfigMaps 和 Secrets

在容器中运行应用程序的一个巨大优势是消除了环境之间的差距。部署过程通过所有测试环境直至生产环境推广相同的容器镜像,因此每个部署都使用与先前环境完全相同的二进制文件集。你将再也不必担心生产部署失败,因为服务器缺少在测试服务器上手动安装但忘记记录的依赖项。当然,环境之间仍然存在差异,你通过将配置设置注入容器来提供这种差异。

Kubernetes 支持使用两种资源类型进行配置注入:ConfigMaps 和 Secrets。这两种类型都可以以任何合理的格式存储数据,并且这些数据在集群中独立于其他资源存在。Pod 可以定义访问 ConfigMaps 和 Secrets 中的数据,以及如何展示这些数据的不同选项。在本章中,你将学习所有管理 Kubernetes 中配置的方法,这些方法足够灵活,可以满足任何应用程序的需求。

4.1 Kubernetes 如何向应用程序提供配置

你可以像创建其他 Kubernetes 资源一样创建 ConfigMap 和 Secret 对象——使用 kubectl,无论是通过 create 命令还是通过应用 YAML 规范。与其它资源不同,它们不执行任何操作;它们只是用于存储少量数据的存储单元。这些存储单元可以被加载到 Pod 中,成为容器环境的一部分,因此容器中的应用程序可以读取这些数据。在我们接触到这些对象之前,我们将看看提供配置设置的 simplest 方法:使用环境变量。

现在尝试一下 环境变量是 Linux 和 Windows 的核心操作系统功能,它们可以在机器级别设置,因此任何应用程序都可以读取它们。它们被广泛使用,所有容器都有一些,这些由容器内的操作系统和 Kubernetes 设置。确保你的 Kubernetes 实验室正在运行。

# switch to the exercise directory for this chapter:
cd ch04 

# deploy a Pod using the sleep image with no extra configuration:
kubectl apply -f sleep/sleep.yaml

# wait for the Pod to be ready:
kubectl wait --for=condition=Ready pod -l app=sleep

# check some of the environment variables in the Pod container:
kubectl exec deploy/sleep -- printenv HOSTNAME KIAMOL_CHAPTER

你可以从图 4.1 中我的输出中看到,容器中存在 hostname 变量,并由 Kubernetes 填充,但自定义的 Kiamol 变量不存在。

图 4.1 所有 Pod 容器都由 Kubernetes 和容器操作系统设置了一些环境变量。

在这个练习中,应用程序只是 Linux 的 printenv 工具,但对于任何应用程序,原理都是相同的。许多技术栈使用环境变量作为基本的配置系统。在 Kubernetes 中提供这些设置的 simplest 方法是在 Pod 规范中添加环境变量。列表 4.1 显示了 sleep 部署的更新 Pod 规范,其中添加了 Kiamol 环境变量。

列表 4.1 sleep-with-env.yaml,一个包含环境变量的 Pod 规范

spec:
  containers:
    - name: sleep
      image: kiamol/ch03-sleep
      env:                     # Sets environment variables
      - name: KIAMOL_CHAPTER   # Defines the name of the variable to create
        value: "04"            # Defines the value to set for the variable

环境变量在 Pod 的整个生命周期中是静态的;在 Pod 运行期间无法更新任何值。如果您需要做出配置更改,您需要使用替换 Pod 来执行更新。您应该习惯于这样的想法:部署不仅仅用于新功能发布;您还会使用它们进行配置更改和软件补丁,并且您必须设计应用程序以处理频繁的 Pod 替换。

现在试试看 使用列表 4.1 中的新 Pod 规范更新 sleep 部署,添加一个在 Pod 容器内部可见的环境变量。

# update the Deployment:
kubectl apply -f sleep/sleep-with-env.yaml

# check the same environment variables in the new Pod:
kubectl exec deploy/sleep -- printenv HOSTNAME KIAMOL_CHAPTER

我在图 4.2 中的输出显示了结果——一个新的容器,其中设置了 Kiamol 环境变量,在一个新的 Pod 中运行。

图 4.2 向 Pod 规范中添加环境变量使值在 Pod 容器中可用。

之前练习的重要之处在于新应用程序使用的是相同的 Docker 镜像;这是一个具有所有相同二进制文件的应用程序——只是在部署之间配置设置发生了变化。在 Pod 规范中直接设置环境值对于简单的设置来说是可以的,但真实的应用程序通常有更复杂的配置需求,这就是您使用 ConfigMaps 的时候。

ConfigMap 只是一个存储可以加载到 Pod 中的数据的资源。数据可以是一组键值对、一段文本或甚至是二进制文件。您可以使用键值对来加载带有环境变量的 Pods,使用文本来加载任何类型的配置文件——JSON、XML、YAML、TOML、INI——以及使用二进制文件来加载许可证密钥。一个 Pod 可以使用多个 ConfigMaps,每个 ConfigMap 也可以被多个 Pods 使用。图 4.3 展示了一些这些选项。

图 4.3 ConfigMaps 是独立的资源,可以附加到零个或多个 Pod 上。

我们将坚持使用简单的 sleep 部署来展示创建和使用 ConfigMaps 的基本原理。列表 4.2 显示了更新后的 Pod 规范的环境部分,它使用在 YAML 中定义的一个环境变量,以及从 ConfigMap 中加载的第二个变量。

列表 4.2 sleep-with-configMap-env.yaml,将 ConfigMap 加载到 Pod 中

  env:                      # The environment section of the container spec
  - name: KIAMOL_CHAPTER
    value: "04"             # This is the variable value.
  - name: KIAMOL_SECTION
    valueFrom:
      configMapKeyRef:              # This value comes from a ConfigMap.
        name: sleep-config-literal  # Names the ConfigMap
        key: kiamol.section         # Names the data item to load

如果在 Pod 规范中引用了 ConfigMap,则在部署 Pod 之前,ConfigMap 必须存在。此规范期望找到一个名为 sleep-config-literal 的 ConfigMap,其中包含数据中的键值对,并且创建它的最简单方法是通过将键和值传递给 kubectl 命令。

现在试试看 通过在命令中指定数据来创建一个 ConfigMap,然后检查数据并将更新的 sleep 应用程序部署以使用 ConfigMap。

# create a ConfigMap with data from the command line:
kubectl create configmap sleep-config-literal --from-literal=kiamol.section='4.1'

# check the ConfigMap details:
kubectl get cm sleep-config-literal

# show the friendly description of the ConfigMap:
kubectl describe cm sleep-config-literal

# deploy the updated Pod spec from listing 4.2:
kubectl apply -f sleep/sleep-with-configMap-env.yaml

# check the Kiamol environment variables:
kubectl exec deploy/sleep -- sh -c 'printenv | grep "^KIAMOL"'

在这本书中,我们不会过多使用 kubectl describe 命令,因为输出通常是冗长的,可能会占用整章的内容,但它确实是一个值得实验的东西。描述服务和 Pod 可以以可读的格式提供大量有用的信息。您可以在图 4.4 中看到我的输出,其中包括从描述 ConfigMap 中显示的键值数据。

图 4.4 Pods 可以从 ConfigMaps 中加载单个数据项并重命名键。

从字面值创建 ConfigMaps 对于单个设置来说是可以的,但如果你有很多配置数据,这会很快变得繁琐。除了在命令行上指定字面值之外,Kubernetes 还允许你从文件中加载 ConfigMaps。

4.2 在 ConfigMaps 中存储和使用配置文件

创建和使用 ConfigMaps 的选项在许多 Kubernetes 版本中已经演变,因此现在它们几乎支持你所能想到的每一种配置变体。这些 sleep Pod 练习是展示这些变分的良好方式,但它们有点无聊,所以我们将在转向更有趣的内容之前再进行一次练习。列表 4.3 显示了一个环境文件——一个包含键值对的文本文件,可以加载以创建一个包含多个数据项的 ConfigMap。

列表 4.3 ch04.env,环境变量文件

# Environment files use a new line for each variable.
KIAMOL_CHAPTER=ch04
KIAMOL_SECTION=ch04-4.1
KIAMOL_EXERCISE=try it now

环境文件是分组多个设置的有用方式,Kubernetes 明确支持将它们加载到 ConfigMaps 中,并在 Pod 容器中将所有设置作为环境变量公开。

现在试试看 创建一个新的 ConfigMap,它由列表 4.3 中的环境文件填充,然后部署 sleep 应用程序的更新以使用新设置。

# load an environment variable into a new ConfigMap:
kubectl create configmap sleep-config-env-file --from-env-file=sleep/ch04.env

# check the details of the ConfigMap:
kubectl get cm sleep-config-env-file

# update the Pod to use the new ConfigMap:
kubectl apply -f sleep/sleep-with-configMap-env-file.yaml

# check the values in the container:
kubectl exec deploy/sleep -- sh -c 'printenv | grep "^KIAMOL"'

我的输出,如图 4.5 所示,显示 printenv 命令正在读取所有环境变量并显示具有 Kiamol 名称的变量,但可能不是你预期的结果。

图 4.5 ConfigMap 可以有多个数据项,Pod 可以加载它们全部。

这个练习向你展示了如何从文件中创建 ConfigMap。它还展示了 Kubernetes 在应用环境变量方面有优先级规则。你刚刚部署的 Pod 规范,如列表 4.4 所示,从 ConfigMap 中加载了所有环境变量,但它还指定了一些具有相同键的显式环境值。

列表 4.4 sleep-with-configMap-env-file.yaml,Pod 中的多个 ConfigMaps

  env:                             # The existing environment section
  - name: KIAMOL_CHAPTER
    value: "04"
  - name: KIAMOL_SECTION
    valueFrom:
      configMapKeyRef:              
        name: sleep-config-literal
        key: kiamol.section
  envFrom:                         # envFrom loads multiple variables
  - configMapRef:                  # from a ConfigMap
      name: sleep-config-env-file

因此,在 Pod 规范中使用 env 定义的环境变量会覆盖 envFrom 中定义的具有重复键的值。记住这一点很有用,即你可以通过在 Pod 规范中显式设置它们来覆盖容器镜像或 ConfigMaps 中设置的任何环境变量——这是一种快速更改配置设置的方法,当你正在追踪问题时。

环境变量得到了很好的支持,但它们只能带你走这么远,并且大多数应用程序平台更喜欢一种更结构化的方法。在本章的其余练习中,我们将使用一个支持配置源层次结构的 Web 应用程序。默认设置打包在 Docker 镜像中的一个 JSON 文件中,应用程序在运行时会查找其他位置的 JSON 文件,这些文件包含覆盖默认设置的设置,并且所有 JSON 设置都可以用环境变量覆盖。列表 4.5 显示了我们将要使用的第一个部署的 Pod 规范。

列表 4.5 todo-web.yaml,一个带有配置设置的 Web 应用

spec:
  containers:
  - name: web
    image: kiamol/ch04-todo-list
    env:
    - name: Logging__LogLevel__Default
      value: Warning

这次的运行将使用镜像中 JSON 配置文件的默认设置,除了默认的日志级别,它被设置为环境变量。

现在试试看 运行应用而不进行任何额外配置,并检查其行为。

# deploy the app with a Service to access it:
kubectl apply -f todo-list/todo-web.yaml

# wait for the Pod to be ready:
kubectl wait --for=condition=Ready pod -l app=todo-web

# get the address of the app:
kubectl get svc todo-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080'

# browse to the app and have a play around
# then try browsing to /config

# check the application logs:
kubectl logs -l app=todo-web

演示应用是一个简单的待办事项列表(对《一个月午餐时间学 Docker》的读者来说可能会感到令人沮丧的熟悉)。在其当前设置中,它允许你添加和查看项目,但还应该有一个 /config 页面,我们可以在非生产环境中使用它来查看所有配置设置。如图 4.6 所示,该页面为空,应用记录了一个警告,表明有人尝试访问它。

图 4.6 应用基本工作,但我们还需要设置额外的配置值。

这里使用的配置层次结构是一个非常常见的做法。如果你不熟悉它,电子书的附录 C 中的章节“容器中的应用配置管理”来自《一个月午餐时间学 Docker》,它对此进行了详细解释。这个例子是一个使用 JSON 的 .NET Core 应用,但你也会看到在 Java Spring 应用、Node.js、Go、Python 等多种文件格式中使用的类似配置系统。在 Kubernetes 中,你可以使用相同的所有应用配置方法。

  • 默认应用设置已经嵌入到容器镜像中。这可能只是适用于每个环境的设置,或者可能是一个完整的配置选项集,因此无需任何额外设置,应用就可以在开发模式下运行(这对开发者来说很有帮助,他们可以用简单的 Docker run 命令快速启动应用)。

  • 每个环境的实际设置都存储在 ConfigMap 中,并显示在容器文件系统中。Kubernetes 将配置数据作为已知位置的一个文件呈现,应用会检查并合并来自默认文件的内容。

  • 需要调整的任何设置都可以作为 Deployment 的 Pod 规范中的环境变量应用。

列表 4.6 显示了待办事项应用的开发配置的 YAML 规范。它包含一个 JSON 文件的全部内容,应用将与此容器镜像中的默认 JSON 配置文件合并,并设置一个使配置页面可见的选项。

列表 4.6 todo-web-config-dev.yaml,一个 ConfigMap 规范

apiVersion: v1
kind: ConfigMap                  # ConfigMap is the resource type.
metadata:
  name: todo-web-config-dev      # Names the ConfigMap.
data:
  config.json: |-                # The data key is the filename.
    {                            # The file contents can be any format.
      "ConfigController": {
        "Enabled" : true
      }
    }

你可以将任何类型的文本配置文件嵌入到 YAML 规范中,只要你注意空格的使用。我更喜欢这种方法,因为它意味着你可以一致地使用 kubectl apply 命令来部署应用的所有部分。如果我想直接加载 JSON 文件,我需要使用 kubectl create 命令来配置资源,并使用 apply 命令来处理其他所有内容。

列表 4.6 中的 ConfigMap 定义只包含一个设置,但它以应用的本地配置格式存储。当我们部署更新的 Pod 规范时,该设置将被应用,并且配置页面将可见。

现在试试看 新的 Pod 规范引用了 ConfigMap,因此需要先通过应用 YAML 创建它,然后我们更新待办事项应用的 Deployment。

# create the JSON ConfigMap:
kubectl apply -f todo-list/configMaps/todo-web-config-dev.yaml

# update the app to use the ConfigMap:
kubectl apply -f todo-list/todo-web-dev.yaml

# refresh your web browser at the /config page for your Service 

你可以在图 4.7 中看到我的输出。现在配置页面正确加载了,因此新的 Deployment 配置正在合并 ConfigMap 中的设置以覆盖镜像中的默认设置,这阻止了对该页面的访问。

图片

图 4.7 将 ConfigMap 数据加载到容器文件系统中,其中应用加载配置文件

这种方法需要两件事:你的应用需要能够合并 ConfigMap 数据,你的 Pod 规范需要将数据从 ConfigMap 加载到容器文件系统中的预期文件路径。我们将在下一节中看到它是如何工作的。

4.3 从 ConfigMap 中暴露配置数据

将配置项加载到环境变量中的替代方法是将其作为容器目录内的文件呈现。容器文件系统是一个虚拟结构,由容器镜像和其他源构建。Kubernetes 可以使用 ConfigMap 作为文件系统源——它们作为目录挂载,每个数据项对应一个文件。图 4.8 显示了你刚刚部署的设置,其中 ConfigMap 中的数据项以文件的形式呈现。

图片

图 4.8 ConfigMap 可以作为容器文件系统中的目录加载。

Kubernetes 通过 Pod 规范的两个功能管理这种奇怪的魔法:,它使 ConfigMap 的内容可用于 Pod,以及 卷挂载,它将 ConfigMap 卷的内容加载到 Pod 容器中的指定路径。列表 4.7 显示了你之前练习中部署的卷和挂载。

列表 4.7 todo-web-dev.yaml,将 ConfigMap 作为卷挂载加载

spec:
  containers:
    - name: web
      image: kiamol/ch04-todo-list 
      volumeMounts:                  # Mounts a volume into the container
        - name: config               # Names the volume
          mountPath: "/app/config"   # Directory path to mount the volume
          readOnly: true             # Flags the volume as read-only

  volumes:                          # Volumes are defined at the Pod level.
    - name: config                  # Name matches the volume mount.
      configMap:                    # Volume source is a ConfigMap.
        name: todo-web-config-dev   # ConfigMap name

这里要认识到的重要事情是,ConfigMap 被当作一个目录处理,包含多个数据项,每个数据项在容器文件系统中都成为文件。在这个例子中,应用从 /app/appsettings.json 中的文件加载其默认设置,然后它寻找 /app/config/config.json 中的文件,该文件可以包含覆盖默认设置的设置。/app/config 目录在容器镜像中不存在;它是由 Kubernetes 创建并填充的。

现在试试看 容器文件系统对应用来说表现为一个单一的存储单元,但它是由镜像和 ConfigMap 构建的。这些源有不同的行为。

# show the default config file:
kubectl exec deploy/todo-web -- sh -c 'ls -l /app/app*.json'

# show the config file in the volume mount:
kubectl exec deploy/todo-web -- sh -c 'ls -l /app/config/*.json'

# check it really is read-only:
kubectl exec deploy/todo-web -- sh -c 'echo ch04 >> /app/config/config.json'

我的输出,如图 4.9 所示,显示应用预期的位置存在 JSON 配置文件,但 ConfigMap 文件由 Kubernetes 管理,并以只读文件的形式交付。

图片

图 4.9 容器文件系统是由 Kubernetes 从镜像和 ConfigMap 构建的。

将 ConfigMap 作为目录加载是灵活的,你可以用它来支持不同的应用程序配置方法。如果你的配置分散在多个文件中,你可以将它们全部存储在一个 ConfigMap 中,并将它们全部加载到容器中。列表 4.8 展示了更新待办 ConfigMap 的数据项,其中包含两个 JSON 文件,分别用于应用程序行为和日志记录的设置。

列表 4.8 todo-web-config-dev-with-logging.yaml,一个包含两个文件的 ConfigMap

data:
  config.json: |-                     # The original app config file
    {
      "ConfigController": {
        "Enabled" : true
      }
    }
  logging.json: |-                    # A second JSON file, which will be
    {                                 # surfaced in the volume mount
      "Logging": {
        "LogLevel": {
          "ToDoList.Pages" : "Debug"
        }
      }
    }

当你部署一个正在使用的 ConfigMap 的更新时会发生什么?Kubernetes 将更新后的文件传递到容器中,但接下来发生什么取决于应用程序。一些应用程序在启动时将配置文件加载到内存中,然后忽略配置目录中的任何更改,因此更改 ConfigMap 不会实际更改应用程序配置,直到 Pod 被替换。这个应用程序更加周到——它会监视配置目录并重新加载任何文件更改,因此部署到 ConfigMap 的更新将更新应用程序配置。

现在试试看 使用列表 4.9 中的 ConfigMap 更新应用程序配置。这将提高日志级别,因此相同的 Pod 现在将开始写入更多的日志条目。

# check the current app logs:
kubectl logs -l app=todo-web

# deploy the updated ConfigMap:
kubectl apply -f todo-list/configMaps/todo-web-config-dev-with-logging.yaml

# wait for the config change to make it to the Pod:
sleep 120

# check the new setting:
kubectl exec deploy/todo-web -- sh -c 'ls -l /app/config/*.json'

# load a few pages from the site at your Service IP address

# check the logs again:
kubectl logs -l app=todo-web

你可以在图 4.10 中看到我的输出。这里的 sleep 是为了给 Kubernetes API 时间将新的配置文件滚动到 Pod;几分钟之后,新的配置被加载,应用程序以增强的日志记录方式运行。

图片

图 4.10 ConfigMap 数据被缓存,因此更新需要几分钟才能到达 Pod。

卷是一个强大的选项,用于加载配置文件,特别是对于这种对更改做出反应并实时更新设置的应用程序。在不重启应用程序的情况下提高日志级别,有助于追踪问题。然而,你需要小心配置,因为卷挂载不一定按你预期的样子工作。如果卷的挂载路径已经在容器镜像中存在,那么 ConfigMap 目录将覆盖它,替换所有内容,这可能导致你的应用程序以令人兴奋的方式失败。列表 4.9 展示了一个例子。

列表 4.9 todo-web-dev-broken.yaml,一个配置错误的 Pod 规范

spec:
  containers:
    - name: web
      image: kiamol/ch04-todo-list   
      volumeMounts:
        - name: config                  # Mounts the ConfigMap volume
          mountPath: "/app"             # Overwrites the directory

这是一个损坏的 Pod 规范,其中 ConfigMap 被加载到 /app 目录而不是 /app/config 目录。作者可能本意是想合并这两个目录,将 JSON 配置文件添加到现有的应用程序目录中。然而,它将会清除应用程序的二进制文件。

现在试试看 列表 4.9 中的 Pod 规范移除了所有应用程序的二进制文件,所以替换后的 Pod 不会启动。看看接下来会发生什么。

# deploy the badly configured Pod:
kubectl apply -f todo-list/todo-web-dev-broken.yaml

# browse back to the app and see how it looks

# check the app logs:
kubectl logs -l app=todo-web

# and check the Pod status:
kubectl get pods -l app=todo-web

这里得到的结果很有趣:部署破坏了应用,但应用仍在继续工作。这是 Kubernetes 在为你留意。应用更改创建了一个新的 Pod,但该 Pod 中的容器立即因为错误而退出,因为它试图加载的二进制文件不再存在于应用目录中。Kubernetes 重启容器几次以给它一个机会,但它一直失败。尝试了三次后,Kubernetes 休息一下,如图 4.11 所示。

图 4.11 如果更新的部署失败,则原始 Pod 不会被替换。

现在我们有两个 Pod,但 Kubernetes 不会删除旧的 Pod,直到替换的 Pod 成功运行,但在这个情况下永远不会发生,因为我们破坏了容器设置。旧的 Pod 没有被删除,仍然愉快地处理请求;新的 Pod 处于失败状态,但 Kubernetes 定期重启容器,希望它可能已经修复了自己。这是一个需要注意的情况:apply 命令似乎工作正常,应用仍在继续工作,但它没有使用你应用的模式。

我们现在将修复它,并展示在容器文件系统中暴露 ConfigMaps 的最后一个选项。你可以选择性地将数据项加载到目标目录中,而不是将每个数据项作为自己的文件加载。列表 4.10 显示了更新的 Pod 规范。挂载路径已经修复,但卷被设置为仅传递一个项目。

列表 4.10 todo-web-dev-no-logging.yaml,挂载单个 ConfigMap 项目

spec:
  containers:
    - name: web
      image: kiamol/ch04-todo-list
      volumeMounts:
        - name: config                # Mounts the ConfigMap volume
          mountPath: "/app/config"    # to the correct direcory
          readOnly: true
  volumes:
    - name: config
      configMap:
        name: todo-web-config-dev     # Loads the ConfigMap volume
        items:                        # Specifies the data items to load
        - key: config.json            # Loads the config.json item
          path: config.json           # Surfaces it as the file config.json

这个规范使用相同的 ConfigMap,因此它只是对部署的更新。这将是一个级联更新:它将创建一个新的 Pod,该 Pod 将正确启动,然后 Kubernetes 将删除两个之前的 Pod。

现在尝试一下:部署列表 4.10 中的规范,该规范将更新卷挂载以修复应用,但同时也忽略了 ConfigMap 中的日志 JSON 文件。

# apply the change:
kubectl apply -f todo-list/todo-web-dev-no-logging.yaml

# list the config folder contents:
kubectl exec deploy/todo-web -- sh -c 'ls /app/config'

# now browse to a few pages on the app

# check the logs:
kubectl logs -l app=todo-web

# and check the Pods:
kubectl get pods -l app=todo-web

图 4.12 展示了我的输出。应用再次工作,但它只看到一个配置文件,因此增强的日志设置没有得到应用。

图 4.12 卷可以将 ConfigMap 中的选定项目暴露到挂载目录中。

ConfigMaps 支持广泛的配置系统。在环境变量和卷挂载之间,你应该能够在 ConfigMaps 中存储应用设置,并以应用期望的方式应用它们。配置规范与应用规范之间的分离也支持不同的发布工作流程,允许不同的团队拥有流程的不同部分。然而,你不应该使用 ConfigMaps 来存储任何敏感数据——它们实际上是没有任何额外安全语义的文本文件的包装器。对于需要保持安全性的配置数据,Kubernetes 提供了 Secrets。

4.4 使用 Secrets 配置敏感数据

Secrets 是一种独立的资源类型,但它们具有与 ConfigMaps 相似的 API。您以相同的方式与它们交互,但由于它们旨在存储敏感信息,Kubernetes 以不同的方式管理它们。主要区别都围绕着最小化暴露。Secrets 仅发送给需要使用它们的节点,并且存储在内存中而不是磁盘上;Kubernetes 还支持对 Secrets 进行传输和静止加密。

尽管 Secrets 并非总是加密的。任何可以访问您集群中 Secret 对象的用户都可以读取未加密的值。有一个混淆层:Kubernetes 可以使用 Base64 编码读取和写入 Secret 数据,这并不是真正的安全功能,但可以防止秘密意外暴露给在您身后窥视的人。

现在尝试一下 您可以从字面值创建 Secrets,通过将键和数据传递给 kubectl 命令。检索到的数据是 Base64 编码的。

# FOR WINDOWS USERS--this script adds a Base64 command to your session: 
. .\base64.ps1

# now create a secret from a plain text literal:
kubectl create secret generic sleep-secret-literal --from-literal=secret=shh...

# show the friendly details of the Secret:
kubectl describe secret sleep-secret-literal

# retrieve the encoded Secret value:
kubectl get secret sleep-secret-literal -o jsonpath='{.data.secret}'

# and decode the data:
kubectl get secret sleep-secret-literal -o jsonpath='{.data.secret}' | base64 -d

您可以从图 4.13 的输出中看到,Kubernetes 对待 Secrets 和 ConfigMaps 的方式不同。数据值在 kubectl describe命令中不会显示,只有项目键的名称,并且当您实际获取数据时,它会以编码的形式显示,因此您需要将其传递到解码器中才能读取。

图 4-13

图 4.13 显示,Secrets 与 ConfigMaps 具有相似的 API,但 Kubernetes 试图避免意外暴露。

当 Secrets 在 Pod 容器内部暴露时,这种预防措施不适用。容器环境看到的是原始的纯文本数据。列表 4.11 显示了返回到 sleep 应用程序,配置为将新的 Secret 作为环境变量加载。

列表 4.11 sleep-with-secret.yaml,一个加载 Secret 的 Pod 规范

spec:
  containers:
    - name: sleep
      image: kiamol/ch03-sleep
      env:                                # Environment variables
      - name: KIAMOL_SECRET               # Variable name in the container
        valueFrom:                        # loaded from an external source
          secretKeyRef:                   # which is a Secret
            name: sleep-secret-literal    # Names the Secret
            key: secret                   # Key of the Secret data item

消费 Secrets 的规范几乎与 ConfigMaps 相同——一个命名环境变量可以从 Secret 中的命名项中加载。此 Pod 规范以原始形式将 Secret 项传递到容器中。

现在尝试一下 运行一个简单的 sleep Pod,它使用 Secret 作为环境变量。

# update the sleep Deployment:
kubectl apply -f sleep/sleep-with-secret.yaml

# check the environment variable in the Pod:
kubectl exec deploy/sleep -- printenv KIAMOL_SECRET

图 4.14 展示了输出结果。在这种情况下,Pod 仅使用了一个 Secret,但 Secrets 和 ConfigMaps 可以在同一个 Pod 规范中混合使用,填充环境变量或文件或两者兼而有之。

图 4-14

图 4.14 显示,加载到 Pod 中的 Secrets 不是 Base64 编码的。

您应该小心将 Secrets 加载到环境变量中。保护敏感数据的关键在于最小化其暴露。环境变量可以从 Pod 容器中的任何进程中读取,并且一些应用程序平台在遇到关键错误时会记录所有环境变量的值。另一种选择是将 Secrets 作为文件暴露出来,如果应用程序支持的话,这为您提供了使用文件权限来保护访问权限的选项。

为了结束本章,我们将以不同的配置运行待办事项应用程序,其中它使用单独的数据库来存储项目,在它自己的 Pod 中运行。数据库服务器是 Postgres,使用 Docker Hub 上的官方镜像,它从环境中的配置值读取登录凭证。列表 4.12 显示了创建数据库密码作为秘密的 YAML 规范。

列表 4.12 -todo-db-secret-test.yaml,数据库用户的秘密

apiVersion: v1
kind: Secret                            # Secret is the resource type.
metadata:
  name: todo-db-secret-test             # Names the Secret
type: Opaque                            # Opaque secrets are for text data.
stringData:                             # stringData is for plain text.
  POSTGRES_PASSWORD: "kiamol-2*2*"      # The secret key and value.

此方法在stringData字段中以纯文本形式声明密码,当创建秘密时,该密码会被编码为 Base64。使用 YAML 文件作为秘密会带来一个棘手的问题:它提供了一个非常好的一致部署方法,但代价是所有敏感数据都可见于源代码控制。

在生产场景中,你会将真实数据从 YAML 文件中移除,使用占位符代替,并在部署过程中进行一些额外的处理——例如,从 GitHub 秘密中注入数据到占位符。无论你采取哪种方法,请记住,一旦秘密存在于 Kubernetes 中,任何有权访问的人都可以轻松地读取其值。

现在试试看:从列表 4.12 中的清单创建一个秘密,并检查数据。

# deploy the Secret:
kubectl apply -f todo-list/secrets/todo-db-secret-test.yaml

# check the data is encoded:
kubectl get secret todo-db-secret-test -o                  
   jsonpath='{.data.POSTGRES_PASSWORD}'

# see what annotations are stored:
kubectl get secret todo-db-secret-test -o 
   jsonpath='{.metadata.annotations}'

你可以在图 4.15 中看到字符串被编码为 Base64。结果与如果规范使用了正常的数据字段并在 YAML 中直接以 Base64 设置密码值是相同的。

图 4-15

图 4.15 从字符串数据创建的秘密被编码,但原始数据也存储在对象中。

要将此秘密作为 Postgres 密码使用,镜像为我们提供了几个选项。我们可以将值加载到名为POSTGRES_PASSWORD的环境变量中——这不是最佳选择——或者我们可以将其加载到文件中,并通过设置POSTGRES_PASSWORD_FILE环境变量告诉 Postgres 在哪里加载该文件。使用文件意味着我们可以在卷级别控制访问权限,这就是代码列表 4.13 中数据库的配置方式。

列表 4.13 todo-db-test.yaml,一个从秘密挂载卷的 Pod 规范

spec:
  containers:
    - name: db
      image: postgres:11.6-alpine
      env:
      - name: POSTGRES_PASSWORD_FILE       # Sets the path to the file
        value: /secrets/postgres_password
      volumeMounts:                        # Mounts a Secret volume
        - name: secret                     # Names the volume
          mountPath: "/secrets"            
  volumes:
    - name: secret
      secret:                             # Volume loaded from a Secret 
        secretName: todo-db-secret-test   # Secret name
        defaultMode: 0400                 # Permissions to set for files
        items:                            # Optionally names the data items 
        - key: POSTGRES_PASSWORD  
          path: postgres_password

当此 Pod 部署时,Kubernetes 将秘密项的值加载到路径为/secrets/postgres_password 的文件中。该文件将设置为 0400 权限,这意味着它只能被容器用户读取,不能被任何其他用户读取。为 Postgres 设置环境变量,以便从该文件加载密码,该密码 Postgres 用户可以访问,因此数据库将使用从秘密设置的凭据启动。

现在试试看:部署数据库 Pod,并验证数据库是否正确启动。

# deploy the YAML from listing 4.13
kubectl apply -f todo-list/todo-db-test.yaml

# check the database logs:
kubectl logs -l app=todo-db --tail 1

# verify the password file permissions:
kubectl exec deploy/todo-db -- sh -c 'ls -l $(readlink -f /secrets/postgres_password)'

图 4.16 显示了数据库正在启动并等待连接——表明它已正确配置——并且最终输出验证了文件权限已设置为预期。

图 4-16

图 4.16 如果应用程序支持,配置设置可以通过从秘密中填充的文件读取。

剩下的只是以测试配置运行应用程序本身,因此它连接到 Postgres 数据库而不是使用本地数据库文件进行存储。为此需要更多的 YAML,以创建 ConfigMap、Secret、Deployment 和服务,但这些都是我们已经介绍过的功能,所以我们直接部署。

现在尝试一下 运行待办事项应用程序,使其使用 Postgres 数据库进行存储。

# the ConfigMap configures the app to use Postgres:
kubectl apply -f todo-list/configMaps/todo-web-config-test.yaml

# the Secret contains the credentials to connect to Postgres:
kubectl apply -f todo-list/secrets/todo-web-secret-test.yaml

# the Deployment Pod spec uses the ConfigMap and Secret:
kubectl apply -f todo-list/todo-web-test.yaml

# check the database credentials are set in the app:
kubectl exec deploy/todo-web-test -- cat /app/secrets/secrets.json

# browse to the app and add some items

我的结果如图 4.17 所示,其中 Secret JSON 文件的纯文本内容显示在 Web Pod 容器内。

图片

图 4.17 将应用程序配置加载到 Pod 中,并将 ConfigMaps 和 Secrets 作为 JSON 文件暴露出来

现在当您在应用程序中添加待办事项时,它们将存储在 Postgres 数据库中,因此存储与应用程序运行时分离。您可以删除 Web Pod;其控制器将启动具有相同配置的替换 Pod,该 Pod 连接到相同的数据库 Pod,因此原始 Web Pod 的所有数据仍然可用。

这是对 Kubernetes 中配置选项的相当详尽的审视。原则相当简单——将 ConfigMaps 或 Secrets 加载到环境变量或文件中——但是有很多变体。您需要很好地理解这些细微差别,以便以一致的方式管理应用程序配置,即使您的应用程序都具有不同的配置模型。

4.5 在 Kubernetes 中管理应用程序配置

Kubernetes 为您提供了使用适合您组织的任何工作流程来管理应用程序配置的工具。核心要求是您的应用程序从环境中读取配置设置,理想情况下具有文件和环境变量的层次结构。然后您可以使用 ConfigMaps 和 Secrets 来支持您的部署过程。在设计时,您需要考虑两个因素:您是否需要应用程序响应实时配置更新,以及您将如何管理 Secrets?

如果对于您来说,无需替换 Pod 的实时更新很重要,那么您的选择有限。您不能使用环境变量进行设置,因为对那些变量的任何更改都会导致 Pod 替换。您可以使用卷挂载并从文件中加载配置更改,但您需要通过更新现有的 ConfigMap 或 Secret 对象来部署更改。您不能更改卷以指向新的配置对象,因为这同样属于 Pod 替换。

更新相同配置对象的一种替代方案是在对象名称中包含某种版本控制方案,每次部署一个新对象,并将应用的部署更新为引用新对象。您将失去实时更新,但可以获得配置更改的审计跟踪,并且可以轻松地恢复到以前的设置。图 4.18 显示了这些选项。

图片

图 4.18 您可以选择自己的配置管理方法,由 Kubernetes 支持。

另一个问题是如何管理敏感数据。大型组织可能拥有专门的配置管理团队,负责部署配置文件的过程。这与对 ConfigMaps 和 Secrets 的版本化方法非常吻合,配置管理团队在部署之前从字面值或受控文件中部署新对象。

另一个选择是完全自动化的部署,其中 ConfigMaps 和 Secrets 从源控制中的 YAML 模板创建。YAML 文件包含占位符而不是敏感数据,在应用之前,部署过程将它们替换为从安全存储(如 Azure KeyVault)中的实际值。图 4.19 比较了这些选项。

图 4.19 在部署中可以自动化管理密钥,或者由一个独立的团队严格控制。

你可以使用适合你团队和应用程序堆栈的任何方法,记住目标是从平台加载所有配置设置,因此相同的容器镜像在所有环境中部署。

是时候清理你的集群了。如果你已经跟随所有的练习(当然你做到了!),你将有一二十个资源需要移除。我将介绍一些 kubectl 的有用功能来帮助你清理所有内容。

现在试试看 kubectl delete 命令可以读取 YAML 文件并删除文件中定义的资源。如果你在目录中有多个 YAML 文件,你可以使用目录名作为delete(或apply)的参数,它将运行所有文件。

# delete all the resources in all the files in all the directories:
kubectl delete -f sleep/
kubectl delete -f todo-list/
kubectl delete -f todo-list/configMaps/
kubectl delete -f todo-list/secrets/

4.6 实验室

如果你被 Kubernetes 提供的配置应用选项搞得晕头转向,这个实验室将帮助你。在实践中,你的应用将有自己的配置管理想法,你需要根据你的应用期望的配置方式来建模你的 Kubernetes 部署。这就是在这个实验室中一个简单的名为 Adminer 的应用需要做的事情。让我们开始吧:

  • Adminer,一个用于管理 SQL 数据库的 Web UI,当你在调试数据库问题时,在 Kubernetes 中运行它可能是一个方便的工具。

  • 首先,部署ch04/lab/postgres文件夹中的 YAML 文件,然后部署ch04/lab/adminer.yaml文件以在基本状态下运行 Adminer。

  • 找到你的 Adminer 服务的公网 IP,并浏览到端口 8082。请注意,你需要指定一个数据库服务器,并且 UI 设计卡在 20 世纪 90 年代。你可以通过使用postgres作为数据库名称、用户名和密码来确认与 Postgres 的连接。

  • 你的任务是创建并使用一些配置对象在 Adminer 部署中,以便数据库服务器名称默认为实验室的 Postgres 服务,并且 UI 使用名为price的更美观的设计。

  • 你可以在名为ADMINER_DEFAULT_SERVER的环境变量中设置默认数据库服务器。让我们称这些为敏感数据,因此它们应该使用密钥。

  • UI 设计设置在环境变量ADMINER_DESIGN中;这不是敏感信息,所以使用 ConfigMap 就足够好了。

这需要一点调查和对如何展示配置设置的思考,因此这对于实际应用程序的配置来说是一个好的实践。我的解决方案已发布在 GitHub 上,供您检查您的方案:github.com/sixeyed/kiamol/blob/master/ch04/lab/README.md.

5 使用卷、挂载和声明存储数据

在集群环境中访问数据很困难。移动计算资源是容易的部分——Kubernetes API 始终与节点保持联系,如果一个节点停止响应,那么 Kubernetes 可以假设它已离线,并在其他节点上启动所有 Pod 的替代品。但如果 Pod 中的应用程序在节点上存储了数据,那么在另一个节点上启动的替代品将无法访问这些数据,如果这些数据包含一个客户尚未完成的大订单,那将令人失望。你真的需要集群级别的存储,这样 Pod 就可以从任何节点访问相同的数据。

Kubernetes 没有内置的集群级存储,因为没有一种解决方案适用于所有场景。应用程序有不同的存储需求,而可以运行 Kubernetes 的平台有不同的存储能力。数据始终是访问速度和持久性之间的平衡,Kubernetes 通过允许你定义集群提供的不同存储类别以及为你的应用程序请求特定的存储类别来支持这一点。在本章中,你将学习如何处理不同类型的存储以及 Kubernetes 如何抽象存储实现细节。

5.1 Kubernetes 如何构建容器文件系统

Pod 中的容器由 Kubernetes 使用多个来源构建其文件系统。容器镜像提供文件系统的初始内容,每个容器都有一个可写存储层,它使用该层来写入新文件或更新镜像中的任何文件。(Docker 镜像为只读,因此当容器从镜像更新文件时,它实际上是在更新其自己的可写层中的文件副本。)图 5.1 显示了在 Pod 内部的外观。

图片

图 5.1 容器并不知道,但它们的文件系统是一个虚拟结构,由 Kubernetes 构建。

在容器中运行的应用程序只看到一个它有读写访问的单个文件系统,所有这些层细节都被隐藏起来。这对于将应用程序迁移到 Kubernetes 来说很好,因为它们不需要更改就可以在 Pod 中运行。但如果你应用程序确实写入数据,你需要了解它们如何使用存储,并设计你的 Pod 以支持它们的需求。否则,你的应用程序看起来似乎运行良好,但当你遇到任何意外情况时(如 Pod 使用新的容器重启),你将面临数据丢失的风险。

现在试试看 如果容器中的应用程序崩溃并退出,Pod 将启动一个替代品。新的容器将使用容器镜像的文件系统和一个新的可写层开始,并且前一个容器在其可写层中写入的任何数据都将消失。

# switch to this chapter’s exercise directory:
cd ch05

# deploy a sleep Pod:
kubectl apply -f sleep/sleep.yaml

# write a file inside the container:
kubectl exec deploy/sleep -- sh -c 'echo ch05 > /file.txt; ls /*.txt'

# check the container ID:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'
# kill all processes in the container, causing a Pod restart:
kubectl exec -it deploy/sleep -- killall5

# check the replacment container ID:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'

# look for the file you wrote--it won’t be there:
kubectl exec deploy/sleep -- ls /*.txt

记住这个练习中的两个重要事项:Pod 容器的文件系统具有容器的生命周期,而不是 Pod 的生命周期,当 Kubernetes 提到 Pod 重启时,它实际上是指替换容器。如果你的应用程序在容器内部愉快地写入数据,这些数据不会在 Pod 级别存储——如果 Pod 使用新的容器重启,所有数据都会丢失。图 5.2 中的我的输出显示了这一点。

图片

图 5.2 可写层具有容器的生命周期,而不是 Pod 的生命周期。

我们已经知道 Kubernetes 可以从其他来源构建容器文件系统——我们在第四章中介绍了 ConfigMaps 和 Secrets 到文件系统目录的映射。那个机制是在 Pod 级别定义一个卷,使另一个存储源可用,然后将其挂载到容器文件系统中的指定路径。ConfigMaps 和 Secrets 是只读存储单元,但 Kubernetes 支持许多其他可写类型的卷。图 5.3 显示了如何设计一个 Pod,该 Pod 使用卷存储在重启之间持久化的数据,甚至可能在整个集群中可访问。

图片

图 5.3 虚拟文件系统可以从引用外部存储单元的卷构建。

我们将在本章后面讨论集群范围内的卷,但到目前为止,我们将从一个更简单的卷类型开始,这种类型在许多场景中仍然很有用。列表 5.1 展示了使用一种称为EmptyDir的卷类型的 Pod 规范,它只是一个空目录,但它存储在 Pod 级别而不是容器级别。它被挂载为卷进入容器,因此作为一个目录可见,但它不是镜像或容器层之一。

列表 5.1 sleep-with-emptyDir.yaml,一个简单的卷规范

spec:
  containers:
    - name: sleep
      image: kiamol/ch03-sleep
      volumeMounts:
       - name: data                 # Mounts a volume called data
          mountPath: /data          # into the /data directory
  volumes:
    - name: data                    # This is the data volume spec,
      emptyDir: {}                  # which is the EmptyDir type.

一个空目录听起来像是你可以想象的最无用的存储部分,但实际上它有很多用途,因为它具有与 Pod 相同的生命周期。存储在EmptyDir卷中的任何数据在重启之间都保留在 Pod 中,因此替换容器可以访问其前任写入的数据。

现在尝试一下:使用列表 5.1 中的规范更新 sleep 部署,添加一个EmptyDir卷。现在你可以写入数据并杀死容器,替换容器可以读取这些数据。

# update the sleep Pod to use an EmptyDir volume:
kubectl apply -f sleep/sleep-with-emptyDir.yaml

# list the contents of the volume mount:
kubectl exec deploy/sleep -- ls /data

# create a file in the empty directory:
kubectl exec deploy/sleep -- sh -c 'echo ch05 > /data/file.txt; ls /data'

# check the container ID:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'

# kill the container processes:
kubectl exec deploy/sleep -- killall5

# check replacement container ID:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'

# read the file in the volume:
kubectl exec deploy/sleep -- cat /data/file.txt

你可以在图 5.4 中看到我的输出。容器只看到文件系统中的一个目录,但它指向的是 Pod 的一部分存储单元。

图片

图 5.4 像空目录这样基本的东西仍然很有用,因为它可以被容器共享。

你可以使用EmptyDir卷为任何使用文件系统进行临时存储的应用程序——也许你的应用程序调用一个 API,该 API 需要几秒钟才能响应,而响应在很长时间内都是有效的。应用程序可能会将 API 响应保存在本地文件中,因为从磁盘读取比重复调用 API 更快。EmptyDir卷是本地缓存的合理来源,因为如果应用程序崩溃,替换的容器仍然会有缓存的文件,并仍然从速度提升中受益。

EmptyDir卷仅与 Pod 的生命周期共享,所以如果 Pod 被替换,则新的 Pod 将以一个空目录开始。如果你想让数据在 Pod 之间持久化,那么你可以挂载其他类型的卷,这些卷有自己的生命周期。

5.2 在节点上使用卷和挂载存储数据

这就是为什么与数据打交道比与计算打交道更复杂,因为我们需要考虑数据是否会绑定到特定的节点——这意味着任何替换的 Pod 都需要在该节点上运行才能看到数据,或者数据是否具有集群级别的访问权限,Pod 可以在任何节点上运行。Kubernetes 支持许多变体,但你需要知道你想要什么以及你的集群支持什么,并为 Pod 指定这些。

最简单的存储选项是使用映射到节点上目录的卷,因此当容器写入卷挂载时,数据实际上存储在节点磁盘上的一个已知目录中。我们将通过运行一个使用EmptyDir卷进行缓存数据的真实应用程序来演示这一点,了解其局限性,然后将其升级为使用节点级存储。

现在试试这个运行一个使用代理组件来提高性能的 Web 应用程序。Web 应用程序在一个带有内部 Service 的 Pod 中运行,代理在另一个 Pod 中运行,该 Pod 在 LoadBalancer Service 上公开。

# deploy the Pi application:
kubectl apply -f pi/v1/ 

# wait for the web Pod to be ready:
kubectl wait --for=condition=Ready pod -l app=pi-web

# find the app URL from your LoadBalancer:
kubectl get svc pi-proxy -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/?dp=30000'

# browse to the URL, wait for the response then refresh the page

# check the cache in the proxy
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache

这是对 Web 应用程序的一种常见配置,其中代理通过直接从其本地缓存提供响应来提高性能,这也有助于减少 Web 应用程序的负载。你可以在图 5.5 中看到我的输出。第一次 Pi 计算响应时间超过一秒,而刷新几乎是瞬间的,因为它是从代理那里来的,不需要计算。

图片

图 5.5 在EmptyDir卷中缓存文件意味着缓存在 Pod 重启时仍然存在。

对于这种应用程序,EmptyDir卷可能是一个合理的方法,因为存储在卷中的数据不是关键的。如果 Pod 重启,则缓存会保留,新的代理容器可以提供由前一个容器缓存的响应。如果 Pod 被替换,则缓存会丢失。替换的 Pod 以一个空的缓存目录开始,但缓存不是必需的——应用程序仍然可以正确运行;只是它开始时速度较慢,直到缓存再次被填满。

现在尝试一下。移除代理 Pod。由于它由部署控制器管理,因此将被替换。替换过程从一个新的 EmptyDir 卷开始,对于此应用程序而言,这意味着一个空的代理缓存,因此请求将直接发送到 Web Pod。

# delete the proxy Pod: 
kubectl delete pod -l app=pi-proxy

# check the cache directory of the replacement Pod:
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache

# refresh your browser at the Pi app URL

我的输出显示在图 5.6 中。结果是相同的,但我不得不等待另一秒钟,因为 Web 应用程序需要时间来计算,因为替换代理 Pod 是没有缓存启动的。

图片 5-6

图 5.6 一个新的 Pod 以一个新的空目录开始。

下一个耐用级别来自于使用映射到节点磁盘上目录的卷,Kubernetes 称之为 HostPath 卷。HostPath 作为 Pod 中的卷指定,并以通常的方式挂载到容器文件系统。当容器将数据写入挂载目录时,实际上是在节点磁盘上写入。图 5.7 显示了节点、Pod 和卷之间的关系。

图片 5-7

图 5.7 HostPath 卷在 Pod 替换之间保持数据,但前提是 Pods 使用同一节点。

HostPath 卷可能很有用,但你需要了解它们的限制。数据实际上存储在节点上,就是这样。Kubernetes 不会神奇地将这些数据复制到集群中的所有其他节点。列表 5.2 显示了一个更新后的 Web 代理 Pod 规范,它使用 HostPath 卷而不是 EmptyDir。当代理容器将缓存文件写入 /data/nginx/cache 时,它们实际上会被存储在节点上的 /volumes/nginx/cache

列表 5.2 nginx-with-hostPath.yaml,挂载 HostPath

spec:               # This is an abridged Pod spec;  
  containers:       # the full spec also contains a configMap volume mount. 
   - image: nginx:1.17-alpine
     name: nginx
     ports:
        - containerPort: 80
     volumeMounts:  
        - name: cache-volume
          mountPath: /data/nginx/cache    # The proxy cache path  
  volumes:
    - name: cache-volume
      hostPath:                           # Using a directory on the node
        path: /volumes/nginx/cache        # The volume path on the node 
        type: DirectoryOrCreate           # creates a path if it doesn’t exist

此方法将数据的耐用性扩展到 Pod 生命周期之外,到节点磁盘的生命周期,前提是替换 Pod 总是在同一节点上运行。在单节点实验室集群中将会是这样,因为只有一个节点。替换 Pod 启动时会加载 HostPath 卷,如果它包含来自先前 Pod 的缓存数据,则新的代理可以立即开始提供缓存数据。

现在尝试一下。更新代理部署以使用列表 5.2 中的 Pod 规范,然后使用应用程序并删除 Pod。替换 Pod 使用现有的缓存响应。

# update the proxy Pod to use a HostPath volume:
kubectl apply -f pi/nginx-with-hostPath.yaml

# list the contents of the cache directory:
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache

# browse to the app URL

# delete the proxy Pod:
kubectl delete pod -l app=pi-proxy

# check the cache directory in the replacement Pod:
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache

# refresh your browser

我的输出显示在图 5.8 中。初始请求响应时间不到一秒,但刷新几乎是瞬间的,因为新的 Pod 继承了存储在节点上的旧 Pod 的缓存响应。

图片 5-8

图 5.8 在单节点集群中,Pod 总是在同一节点上运行,因此它们都可以使用 HostPath

HostPath 卷的明显问题是它们在具有多个节点的集群中没有意义,这在简单的实验室环境之外的大多数集群中都是如此。你可以在 Pod 规范中包含一个要求,说明 Pod 应始终在同一个节点上运行,以确保它去到数据所在的地方,但这样做会限制你解决方案的弹性——如果节点离线,则 Pod 不会运行,你将丢失你的应用程序。

一个不那么明显的问题是,这种方法提出了一种很好的安全漏洞。Kubernetes 并没有限制节点上哪些目录可用于 HostPath 卷。列表 5.3 中显示的 Pod 规范是完全有效的,这使得节点的整个文件系统都可以供 Pod 容器访问。

列表 5.3 sleep-with-hostPath.yaml,一个具有对节点磁盘完全访问权限的 Pod

spec:
  containers:
    - name: sleep
      image: kiamol/ch03-sleep
      volumeMounts:
        - name: node-root
          mountPath: /node-root
  volumes:
    - name: node-root
      hostPath:
        path: /                  # The root of the node’s filesystem
        type: Directory          # path needs to exist.

现在,任何有权从该规范创建 Pod 的人都可以访问 Pod 运行的节点上的整个文件系统。你可能想使用这样的卷挂载作为快速读取主机上多个路径的方法,但如果你的应用程序被入侵,攻击者可以在容器中执行命令,那么他们也可以访问节点的磁盘。

现在试试 Run 一个 Pod,从列表 5.3 中显示的 YAML 文件开始,然后在 Pod 容器中运行一些命令来探索节点的文件系统。

# run a Pod with a volume mount to the host:
kubectl apply -f sleep/sleep-with-hostPath.yaml

# check the log files inside the container:
kubectl exec deploy/sleep -- ls -l /var/log

# check the logs on the node using the volume:
kubectl exec deploy/sleep -- ls -l /node-root/var/log

# check the container user:
kubectl exec deploy/sleep -- whoami

如图 5.9 所示,Pod 容器可以看到节点上的日志文件,在这种情况下包括 Kubernetes 日志。这相对无害,但这个容器以 root 用户身份运行,这映射到节点上的 root 用户,因此容器对文件系统有完全的访问权限。

图 5-9

图 5.9 危险!挂载 HostPath 可以让你完全访问节点上的数据。

如果这一切听起来像是一个糟糕的想法,请记住 Kubernetes 是一个具有广泛功能的平台,可以满足许多应用程序的需求。你可能有一个需要访问其运行节点上的特定文件路径的旧应用程序,HostPath 卷让你可以做到这一点。在这种情况下,你可以采取更安全的做法,使用一个可以访问节点上某个路径的卷,通过声明卷挂载的子路径来限制容器可以看到的内容。列表 5.4 展示了这一点。

列表 5.4 sleep-with-hostPath-subPath.yaml,使用子路径限制挂载

spec:
  containers:
    - name: sleep
      image: kiamol/ch03-sleep
      volumeMounts:
        - name: node-root                 # Name of the volume to mount
          mountPath: /pod-logs            # Target path for the container
          subPath: var/log/pods           # Source path within the volume
        - name: node-root
          mountPath: /container-logs
          subPath: var/log/containers
  volumes:
    - name: node-root
      hostPath:
        path: /
        type: Directory

在这里,卷仍然定义在节点的根路径上,但访问它的唯一方式是通过容器中的卷挂载,这些挂载被限制在定义的子路径中。在卷规范和挂载规范之间,你在构建和映射容器文件系统方面有很多灵活性。

现在试试 Update sleep Pod,使其容器的卷挂载限制在列表 5.4 中定义的子路径,并检查文件内容。

# update the Pod spec:
kubectl apply -f sleep/sleep-with-hostPath-subPath.yaml

# check the Pod logs on the node:
kubectl exec deploy/sleep -- sh -c 'ls /pod-logs | grep _pi-'

# check the container logs:
kubectl exec deploy/sleep -- sh -c 'ls /container-logs | grep nginx'

在这个练习中,除了通过挂载到日志目录之外,没有其他方法可以探索节点的文件系统。如图 5.10 所示,容器只能访问子路径中的文件。

图 5-10

图 5.10 限制对子路径卷的访问可以限制容器可以执行的操作。

HostPath 卷是开始使用有状态应用程序的好方法;它们易于使用,并且在任何集群上工作方式相同。在现实世界的应用程序中,它们也很有用,但仅当您的应用程序使用状态进行临时存储时。对于永久存储,我们需要转向任何集群中的节点都可以访问的卷。

5.3 使用持久卷和卷声明存储集群级数据

Kubernetes 集群就像一个资源池:它有多个节点,每个节点都提供一些 CPU 和内存容量供集群使用,Kubernetes 使用这些资源来运行您的应用程序。存储只是 Kubernetes 向您的应用程序提供的另一种资源,但它只能提供集群级存储,如果节点可以连接到分布式存储系统。图 5.11 展示了如果卷使用分布式存储,Pod 可以如何访问来自任何节点的卷。

图 5-11

图 5.11 分布式存储使您的 Pod 能够访问来自任何节点的数据,但它需要平台支持。

Kubernetes 支持许多由分布式存储系统支持的卷类型:AKS 集群可以使用 Azure Files 或 Azure Disk,EKS 集群可以使用弹性块存储,在数据中心,您可以使用简单的网络文件系统 (NFS) 共享,或者使用 GlusterFS 这样的网络文件系统。所有这些系统都有不同的配置要求,您可以在 Pod 的卷规范中指定它们。这样做会使您的应用程序规范与一种存储实现紧密耦合,而 Kubernetes 提供了一种更灵活的方法。

Pods 是计算层的抽象,而 Services 是网络层的抽象。在存储层,抽象是 PersistentVolumes (PV) 和 PersistentVolumeClaims。PersistentVolume 是一个 Kubernetes 对象,它定义了一个可用的存储部分。集群管理员可以创建一组 PersistentVolumes,每个 PersistentVolume 都包含底层存储系统的卷规范。列表 5.5 展示了一个使用 NFS 存储的 PersistentVolume 规范。

列表 5.5 persistentVolume-nfs.yaml,由 NFS 挂载支持的卷

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv01                  # A generic storage unit with a generic name

spec:
  capacity:
    storage: 50Mi             # The amount of storage the PV offers
  accessModes:                # How the volume can be accessed by Pods
    - ReadWriteOnce           # It can only be used by one Pod.    

  nfs:                                # This PV is backed by NFS.  
    server: nfs.my.network            # Domain name of the NFS server
    path: "/kubernetes-volumes"       # Path to the NFS share

您无法在您的实验室环境中部署该规范,除非您恰好有一个名为 nfs.my.network 的域和名为 kubernetes-volumes 的共享的网络文件服务器。您可以在任何平台上运行 Kubernetes,因此对于接下来的练习,我们将使用一个在任何地方都可以工作的本地卷。(如果我在练习中使用 Azure Files,它们只能在 AKS 集群上工作,因为 EKS、Docker Desktop 和其他 Kubernetes 发行版没有配置 Azure 卷类型。)

现在尝试一下:创建一个使用本地存储的 PV。PV 是集群范围的,但卷是本地化的,仅存在于一个节点上,因此我们需要确保 PV 与存储所在节点的节点相关联。我们将使用标签来完成这项工作。

# apply a custom label to the first node in your cluster: 
kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') kiamol=ch05

# check the nodes with a label selector:
kubectl get nodes -l kiamol=ch05

# deploy a PV that uses a local volume on the labeled node:
kubectl apply -f todo-list/persistentVolume.yaml

# check the PV:
kubectl get pv

我的输出显示在图 5.12 中。节点标签化是必要的,仅因为我没有使用分布式存储系统;你通常会指定可从任何节点访问的 NFS 或 Azure Disk 卷配置。本地卷仅存在于一个节点上,PV 使用标签来识别该节点。

图 5-12

图 5.12 如果你没有分布式存储,你可以通过将 PV 锁定到本地卷来作弊。

现在 PV 作为已知功能集(包括大小和访问模式)的可用存储单元存在于集群中。Pod 无法直接使用该 PV;相反,它们需要使用 PersistentVolumeClaim (PVC) 来声明它。PVC 是 Pod 使用的存储抽象,它只为应用程序请求一些存储。PVC 由 Kubernetes 匹配到 PV,并将底层的卷细节留给 PV。列表 5.6 显示了对一些存储的声明,它将被匹配到我们创建的 PV。

列表 5.6 postgres-persistentVolumeClaim.yaml,一个与 PV 匹配的 PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc          # The claim will be used by a specific app.
spec:
  accessModes:                # The required access mode
    - ReadWriteOnce
  resources:
    requests:
      storage: 40Mi           # The amount of storage requested
  storageClassName: ""        # A blank class means a PV needs to exist.

PVC 规范包括访问模式、存储量和存储类别。如果没有指定存储类别,Kubernetes 会尝试找到一个与声明中要求相匹配的现有 PV。如果找到匹配项,则 PVC 将绑定到 PV——存在一对一的链接,因此一旦 PV 被声明,它就不再可用于其他 PVC。

现在尝试一下:从列表 5.6 中部署 PVC。其需求由我们在上一练习中创建的 PV 满足,因此声明将绑定到该卷。

# create a PVC that will bind to the PV:
kubectl apply -f todo-list/postgres-persistentVolumeClaim.yaml

# check PVCs:
kubectl get pvc

# check PVs:
kubectl get pv

我的输出显示在图 5.13 中,你可以看到一对一的绑定:PVC 绑定到卷上,PV 通过声明绑定。

图 5-13

图 5.13 PV 是集群中的存储单元;你使用 PVC 为你的应用程序声明它们。

这是一个静态配置方法,其中 PV 需要被明确创建,以便 Kubernetes 可以绑定到它。如果你在创建 PVC 时没有匹配的 PV,声明仍然会被创建,但它不可用。它将留在系统中,等待创建一个满足其要求的 PV。

现在尝试一下:你的集群中的 PV 已经绑定到一个声明上,因此不能再使用。创建另一个将保持未绑定的 PVC。

# create a PVC that doesn’t match any available PVs:
kubectl apply -f todo-list/postgres-persistentVolumeClaim-too-big.yaml

# check claims:
kubectl get pvc

在图 5.14 中,你可以看到新的 PVC 处于挂起状态。它将保持这种状态,直到集群中出现至少 100 MB 容量的 PV,这是声明中的存储请求。

图 5-14

图 5.14 在静态配置下,PVC 将无法使用,直到有可以绑定到的 PV。

在 Pod 可以使用 PVC 之前,必须将其绑定。如果你部署了一个引用未绑定 PVC 的 Pod,该 Pod 将保持在挂起状态,直到 PVC 被绑定,因此你的应用程序将无法运行,直到它获得所需的存储。我们创建的第一个 PVC 已经被绑定,因此它可以被使用,但只能由一个 Pod 使用。声明的访问模式是 ReadWriteOnce,这意味着卷是可写的,但只能由一个 Pod 挂载。列表 5.7 显示了一个用于存储的 PVC 的简化 Postgres 数据库 Pod 规范。

列表 5.7 todo-db.yaml,一个消耗 PVC 的 Pod 规范

spec:
  containers:
    - name: db
      image: postgres:11.6-alpine
      volumeMounts:
        - name: data                           
          mountPath: /var/lib/postgresql/data   
  volumes:
    - name: data
      persistentVolumeClaim:             # Volume uses a PVC
        claimName: postgres-pvc          # PVC to use

现在我们已经拥有了部署使用卷的 Postgres 数据库 Pod 所需的所有组件,这个卷可能由分布式存储支持,也可能不是。应用程序设计者拥有 Pod 规范和 PVC,并不关心 PV——这取决于 Kubernetes 集群的基础设施,可能由不同的团队管理。在我们的实验室环境中,我们拥有所有这些。我们需要再走一步:在卷期望使用的节点上创建目录路径。

现在试试看 你可能无法登录到真实 Kubernetes 集群的节点,所以我们将通过运行一个 sleep Pod 来作弊,该 Pod 将节点根目录挂载到 HostPath,并使用挂载来创建目录。

# run the sleep Pod, which has access to the node’s disk:
kubectl apply -f sleep/sleep-with-hostPath.yaml

# wait for the Pod to be ready:
kubectl wait --for=condition=Ready pod -l app=sleep

# create the directory path on the node, which the PV expects:
kubectl exec deploy/sleep -- mkdir -p /node-root/volumes/pv01

图 5.15 显示了运行具有 root 权限的 sleep Pod,因此它可以在节点上创建目录,尽管我无法直接访问节点。

图片 5-15

图 5.15 在这个例子中,HostPath 是访问节点上 PV 源的另一种方式。

现在一切准备就绪,可以运行带有持久存储的待办事项列表应用程序。通常,你不需要走这么多步骤,因为你将了解你的集群提供的功能。然而,我不知道你的集群能做什么,所以这些练习适用于任何集群,并且它们是所有存储资源的有用介绍。图 5.16 显示了我们迄今为止部署的内容,以及我们即将部署的数据库。

图片 5-16

图 5.16 将 PV 和 HostPath 映射到相同的存储位置——稍微复杂一点

让我们运行数据库。当创建 Postgres 容器时,它将卷挂载到 Pod 中,该卷由 PVC 支持。这个新的数据库容器连接到一个空卷,因此当它启动时,它将初始化数据库,创建预写日志(WAL),这是主要的数据文件。Postgres Pod 并不知道,但 PVC 由节点上的本地卷支持,我们在那里也运行了一个 sleep Pod,我们可以用它来查看 Postgres 文件。

现在试试看 部署数据库,给它一些时间来初始化数据文件,然后使用 sleep Pod 检查卷中写入的内容。

# deploy the database:
kubectl apply -f todo-list/postgres/

# wait for Postgres to initialize:
sleep 30

# check the database logs: 
kubectl logs -l app=todo-db --tail 1

# check the data files in the volume:
kubectl exec deploy/sleep -- sh -c 'ls -l /node-root/volumes/pv01 | grep wal'

图 5.17 中的输出显示数据库服务器正确启动并等待连接,已经将所有数据文件写入卷。

图片 5-17

图 5.17 数据库容器写入本地数据路径,但实际上这是一个 PVC 的挂载点。

最后要做的就是运行应用,测试它,并确认如果数据库 Pod 被替换,数据仍然存在。

现在试试吧 运行待办事项应用的 Web Pod,它连接到 Postgres 数据库。

# deploy the web app components:
kubectl apply -f todo-list/web/

# wait for the web Pod:
kubectl wait --for=condition=Ready pod -l app=todo-web

# get the app URL from the Service:
kubectl get svc todo-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8081/new'

# browse to the app, and add a new item 
# delete the database Pod:
kubectl delete pod -l app=todo-db
# check the contents of the volume on the node:
kubectl exec deploy/sleep -- ls -l /node-root/volumes/pv01/pg_wal

# check that your item is still in the to-do list

您可以在图 5.18 中看到,我的待办事项应用正在显示一些数据,您只需相信我的话,这些数据已经被添加到第一个数据库 Pod 中,并从第二个数据库 Pod 中重新加载。

图 5.18 存储抽象意味着数据库只需挂载 PVC 即可获得持久存储。

现在我们有一个很好地解耦的应用,其中包含一个可以独立于数据库更新和扩展的 Web Pod,以及一个使用 Pod 生命周期外持久存储的数据库 Pod。这个练习使用了本地卷作为持久数据的后端存储,但您需要为生产部署做的唯一改变是将 PV 中的卷规范替换为集群支持的分布式卷。

您是否应该在 Kubernetes 中运行关系型数据库是我们将在本章末尾解决的问题,但在我们这样做之前,我们将看看真正的存储问题:让集群根据抽象的存储类动态配置卷。

5.4 动态卷配置和存储类

到目前为止,我们使用了一个静态配置工作流程。我们明确创建了 PV,然后创建了 PVC,Kubernetes 将其绑定到 PV 上。这对所有 Kubernetes 集群都适用,可能在那些对存储访问严格控制的组织中是首选的工作流程,但大多数 Kubernetes 平台支持一个更简单的替代方案,即动态配置。

在动态配置工作流程中,您只需创建 PVC,而支持它的 PV 将在集群中按需创建。集群可以配置多个存储类,这些存储类反映了提供的不同卷功能以及一个默认存储类。PVC 可以指定它们想要的存储类名称,或者如果它们想使用默认类,则可以在声明规范中省略存储类字段,如列表 5.8 所示。

列表 5.8 postgres-persistentVolumeClaim-dynamic.yaml,动态 PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc-dynamic
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi      
      # There is no storageClassName field, so this uses the default class.

您可以将此 PVC 部署到您的集群中,而无需创建 PV——但我不能告诉您会发生什么,因为这取决于您的集群配置。如果您的 Kubernetes 平台支持使用默认存储类的动态配置,那么您将看到会创建一个 PV 并将其绑定到声明上,并且该 PV 将使用集群为默认设置的任何卷类型。

现在试试吧 部署一个 PVC,看看它是否是动态配置的。

# deploy the PVC from listing 5.8:
kubectl apply -f todo-list/postgres-persistentVolumeClaim-dynamic.yaml

# check claims and volumes:
kubectl get pvc
kubectl get pv

# delete the claim:
kubectl delete pvc postgres-pvc-dynamic

# check volumes again:
kubectl get pv

当你运行这个练习时会发生什么?Docker Desktop 使用默认存储类中的 HostPath 卷来动态预配 PV;AKS 使用 Azure Files;K3s 使用 HostPath,但与 Docker Desktop 的配置不同,这意味着你不会看到 PV,因为它仅在创建使用 PVC 的 Pod 时创建。图 5.19 展示了我在 Docker Desktop 上的输出。PV 被创建并绑定到 PVC 上,当 PVC 被删除时,PV 也会被删除。

图 5-19

图 5.19 Docker Desktop 对默认存储类有一套行为;其他平台不同。

存储类提供了很多灵活性。你将它们作为标准的 Kubernetes 资源创建,并在规范中,你通过以下三个字段定义存储类的工作方式:

  • provisioner——按需创建 PV 的组件。不同的平台有不同的 provisioner,例如,默认 AKS 存储类中的 provisioner 集成了 Azure Files 以创建新的文件共享。

  • reclaimPolicy——定义在删除请求时对动态创建的卷进行什么操作。底层卷也可以被删除,或者可以保留。

  • volumeBindingMode——确定 PV 是否在 PVC 创建时立即创建,或者直到创建一个使用 PVC 的 Pod。

结合这些属性,你可以将你的集群中的存储类选项组合起来,这样应用程序就可以请求它们需要的属性——从快速本地存储到高可用集群存储——而无需指定卷或卷类型的确切细节。我无法给你一个我可以确信将在你的集群上工作的存储类 YAML,因为集群并不都有相同的 provisioner 可用。相反,我们将通过克隆你的默认类来创建一个新的存储类。

现在尝试一下 获取默认存储类并将其克隆包含一些棘手的细节,所以我将这些步骤封装在一个脚本中。如果你好奇,你可以检查脚本内容,但之后你可能需要躺下休息一下。

# list the storage classes in the cluster:
kubectl get storageclass

# clone the default on Windows:
Set-ExecutionPolicy Bypass -Scope Process -Force; ./cloneDefaultStorageClass.ps1

# OR on Mac/Linux:
chmod +x cloneDefaultStorageClass.sh && ./cloneDefaultStorageClass.sh

# list storage classes:
kubectl get sc

你从列出存储类中看到的输出显示了你的集群配置了什么。在运行脚本后,你应该有一个名为 kiamol 的新类,它与默认存储类有相同的设置。我在 Docker Desktop 上的输出如图 5.20 所示。

图 5-20

图 5.20 将默认存储类克隆以创建一个可在 PVC 规范中使用的自定义类

现在你已经有一个自定义存储类,你的应用程序可以在 PVC 中请求它。这是一种更直观、更灵活的存储管理方式,尤其是在动态预配简单快捷的云平台上。列表 5.9 展示了一个请求新存储类的 PVC 规范。

列表 5.9 postgres-persistentVolumeClaim-storageClass.yaml

spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: kiamol      # The storage class is the abstraction.
  resources:
    requests:
      storage: 100Mi

生产集群中的存储类将具有更有意义的名称,但我们现在在集群中都有一个具有相同名称的存储类,因此我们可以更新 Postgres 数据库以使用该显式类。

现在试试这个:创建新的 PVC,并更新数据库 Pod 规范以使用它。

# create a new PVC using the custom storage class:
kubectl apply -f storageClass/postgres-persistentVolumeClaim-storageClass.yaml

# update the database to use the new PVC:
kubectl apply -f storageClass/todo-db.yaml

# check the storage:
kubectl get pvc
kubectl get pv

# check the Pods:
kubectl get pods -l app=todo-db

# refresh the list in your to-do app

这个练习将数据库 Pod 切换到使用新的动态预配的 PVC,如图 5.21 所示。新的 PVC 由一个新的卷支持,因此它将开始为空,你将丢失之前的数据。之前的卷仍然存在,因此你可以为数据库 Pod 部署另一个更新,将其回滚到旧 PVC,并查看你的条目。

图 5.21

图 5.21 使用存储类极大地简化了你的应用规范;你只需在 PVC 中命名该类。

5.5 理解 Kubernetes 中的存储选择

所以这就是 Kubernetes 中的存储。在你的常规工作中,你将为你的 Pods 定义 PersistentVolumeClaims 并指定你需要的尺寸和存储类,这可能是一个自定义值,如 FastLocal 或 Replicated。我们在这章中走了一段很长的路,因为了解当你声明存储时实际上会发生什么,涉及哪些其他资源,以及如何配置它们,这是很重要的。

我们还介绍了卷类型,这是一个你需要更多研究的领域,以了解你的 Kubernetes 平台上可用的选项以及它们提供的功能。如果你在云环境中,你应该有多集群存储选项的奢侈,但记住存储成本,快速存储成本很高。你需要理解你可以使用一个配置为保留底层卷的快速存储类来创建一个 PVC,这意味着即使你删除了你的部署,你仍然需要为存储付费。

这就引出了一个大问题:你是否应该使用 Kubernetes 来运行像数据库这样的有状态应用?功能都在那里,可以为你提供高度可用的、复制的存储(如果你的平台提供的话),但这并不意味着你应该急忙退役你的 Oracle 环境,并用在 Kubernetes 中运行的 MySQL 来替换它。管理数据会给你的 Kubernetes 应用程序增加很多复杂性,运行有状态的应用程序只是问题的一部分。你需要考虑数据备份、快照和回滚,如果你在云中运行,一个托管数据库服务可能会为你提供这些功能。但是,将整个堆栈定义在 Kubernetes 清单中是非常诱人的,而且一些现代数据库服务器被设计为在容器平台上运行;TiDB 和 CockroachDB 是值得考虑的选项。

现在我们继续到实验室之前,只剩下整理你的实验室集群了。

现在试试这个:删除本章中使用的所有对象。你可以忽略你得到的任何错误,因为当你运行这个时,并不是所有的对象都会存在。

# delete deployments, PVCs, PVs, and Services:
kubectl delete -f pi/v1 -f sleep/ -f storageClass/ -f todo-list/web -f todo-list/postgres -f todo-list/

# delete the custom storage class:
kubectl delete sc kiamol

5.6 实验

这些实验室旨在让你在现实世界的 Kubernetes 问题中获得一些经验,所以我不会要求你复制练习来克隆默认的存储类。相反,我们有一个新的待办事项应用部署,它有几个问题。我们在 Web Pod 前使用代理来提高性能,并在 Web Pod 内部使用本地数据库文件,因为这只是开发部署。我们需要在代理层和 Web 层配置一些持久化存储,这样你就可以删除 Pods 和部署,数据仍然会持续存在。

  • 首先,部署 ch05/lab/todo-list 文件夹中的应用清单;这会创建代理和 Web 组件的服务和部署。

  • 找到 LoadBalancer 的 URL,并尝试使用该应用。你会发现它没有响应,你需要深入查看日志来找出问题所在。

  • 你的任务是配置代理缓存文件和 Web Pod 中数据库文件的持久化存储。你应该能够从日志条目和 Pod 规范中找到挂载目标。

  • 当应用运行时,你应该能够添加一些数据,删除所有你的 Pods,刷新浏览器,并看到你的数据仍然在那里。

  • 你可以使用你喜欢的任何卷类型或存储类。这是一个很好的机会来探索你的平台提供了什么。

我的解决方案通常在 GitHub 上,你可以检查是否需要:github.com/sixeyed/kiamol/blob/master/ch05/lab/README.md

6 使用控制器在多个 Pod 上扩展应用

扩展应用的基本想法很简单:运行更多的 Pod。Kubernetes 将网络和存储从计算层抽象出来,这样你就可以运行许多 Pod,它们是相同应用的副本,并将它们仅插入到相同的抽象中。Kubernetes 将这些 Pod 称为副本,在多节点集群中,它们将分布到许多节点上。这为你提供了所有扩展的好处:更大的处理负载的能力和故障情况下的高可用性——所有这些都在一个可以在几秒钟内扩展和缩减的平台中。

Kubernetes 还提供了一些替代的扩展选项来满足不同的应用需求,我们将在本章中逐一介绍。你将最常使用的是 Deployment 控制器,它实际上是简单的,但我们也将在其他方面花费时间,这样你就能了解如何在你的集群中扩展不同类型的应用。

6.1 Kubernetes 如何运行大规模应用

Pod 是 Kubernetes 中的计算单元,你在第二章中了解到你通常不会直接运行 Pod;相反,你定义另一个资源来为你管理它们。这个资源是一个控制器,我们自从那时起就一直在使用 Deployment 控制器。控制器规范包括一个 Pod 模板,它使用该模板来创建和替换 Pod。它可以使用相同的模板来创建 Pod 的多个副本。

Deployments 可能是你在 Kubernetes 中最常用的资源,你已经对它们有很多经验了。现在,是时候深入挖掘一下,了解 Deployments 实际上并不直接管理 Pods——这是由另一个称为 ReplicaSet 的资源完成的。图 6.1 显示了 Deployment、ReplicaSet 和 Pods 之间的关系。

图 6-1

图 6.1 每个软件问题都可以通过添加另一层抽象来解决。

在大多数情况下,你会使用 Deployment 来描述你的应用;Deployment 是一个控制器,它管理 ReplicaSet,而 ReplicaSet 是一个控制器,它管理 Pods。你可以直接创建 ReplicaSet 而不是使用 Deployment,我们将在前几个练习中这样做,只是为了看看扩展是如何工作的。ReplicaSet 的 YAML 几乎与 Deployment 相同;它需要一个选择器来找到它拥有的资源,以及一个 Pod 模板来创建资源。列表 6.1 显示了简化的规范。

列表 6.1 whoami.yaml,一个没有 Deployment 的 ReplicaSet

apiVersion: apps/v1
kind: ReplicaSet          # The spec is almost identical to a Deployment.
metadata:
  name: whoami-web
spec:
  replicas: 1
  selector:               # The selector for the ReplicaSet to find its Pods
    matchLabels:
      app: whoami-web
  template:               # The usual Pod spec follows.

与我们之前使用的 Deployment 定义相比,这个规范中唯一不同的是对象类型 ReplicaSet 和replicas字段,它说明了要运行多少个 Pod。这个规范使用单个副本,这意味着 Kubernetes 将运行一个 Pod。

现在试试看 部署 ReplicaSet,以及一个使用与 ReplicaSet 相同标签选择器的 LoadBalancer 服务,将流量发送到 Pods。

# switch to this chapter's exercises:
cd ch06

# deploy the ReplicaSet and Service:
kubectl apply -f whoami/

# check the resource:
kubectl get replicaset whoami-web

# make an HTTP GET call to the Service:
curl $(kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8088')

# delete all the Pods:
kubectl delete pods -l app=whoami-web

# repeat the HTTP call:
curl $(kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8088')

# show the detail about the ReplicaSet:
kubectl describe rs whoami-web

你可以在图 6.2 中看到我的输出。这里没有新的内容;ReplicaSet 拥有一个 Pod,当你删除该 Pod 时,ReplicaSet 会替换它。我在最后的命令中移除了kubectl describe的输出,但如果你运行它,你会看到它以一系列事件结束,其中 ReplicaSet 记录了它是如何创建 Pod 的活动日志。

图片

图 6.2 使用 ReplicaSet 就像使用 Deployment 一样:它创建和管理 Pod。

ReplicaSet 替换已删除的 Pod,因为它不断运行一个控制循环,检查它拥有的对象数量是否与它应有的副本数量相匹配。当你扩展你的应用程序时,你使用相同的机制——你更新 ReplicaSet 规范以设置新的副本数量,然后控制循环看到它需要更多,并从相同的 Pod 模板中创建它们。

现在尝试一下 扩展应用程序,通过部署一个更新的 ReplicaSet 定义,指定三个副本。

# deploy the update:
kubectl apply -f whoami/update/whoami-replicas-3.yaml

#check Pods:
kubectl get pods -l app=whoami-web

# delete all the Pods:
kubectl delete pods -l app=whoami-web

# check again:
kubectl get pods -l app=whoami-web

# repeat this HTTP call a few times:
curl $(kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8088')

我在图 6.3 中展示的输出引发了一些问题:Kubernetes 是如何如此快速地扩展应用的,以及 HTTP 响应是如何从不同的 Pod 中产生的?

图片

图 6.3 扩展 ReplicaSet 很快,在扩展规模时,Service 可以分配请求到多个 Pod。

第一个问题很简单回答:这是一个单节点集群,所以每个 Pod 都会运行在同一个节点上,而这个节点已经拉取了应用的 Docker 镜像。当你在一个生产集群中扩展时,新 Pod 可能会被调度到没有本地镜像的节点上,它们在运行 Pod 之前需要拉取镜像。你可以扩展的速度受限于你可以拉取镜像的速度,这就是为什么你需要投入时间来优化你的镜像。

至于我们如何向同一个 Kubernetes Service 发起 HTTP 请求并从不同的 Pod 获取响应,这完全归因于 Service 和 Pod 之间的松散耦合。当你扩展 ReplicaSet 时,突然出现了多个匹配 Service 标签选择器的 Pod,当这种情况发生时,Kubernetes 会在 Pod 之间负载均衡请求。图 6.4 展示了相同的标签选择器如何维护 ReplicaSet 和 Pod 之间以及 Service 和 Pod 之间的关系。

图片

图 6.4 与 ReplicaSet 具有相同标签选择器的 Service 将使用其所有 Pod。

网络和计算之间的抽象使得在 Kubernetes 中扩展变得如此容易。你现在可能感到一种温暖的喜悦——突然间,所有的复杂性开始变得有序,你看到了资源分离是如何成为一些非常强大功能的推动力的。这是扩展的核心:你需要运行多少 Pod 就运行多少,它们都位于一个 Service 后面。当消费者访问 Service 时,Kubernetes 会在 Pod 之间分配负载。

负载均衡是 Kubernetes 中所有服务类型的一个功能。我们在这些练习中部署了一个 LoadBalancer 服务,它接收集群中的流量并将其发送到 Pod。它还创建了一个 ClusterIP 供其他 Pod 使用,当 Pod 在集群内部通信时,它们也受益于负载均衡。

现在试试看:部署一个新的 Pod,并使用它通过 ClusterIP 调用内部的 who-am-I 服务,Kubernetes 会根据服务名称解析这个 ClusterIP。

# run a sleep Pod:
kubectl apply -f sleep.yaml

# check the details of the who-am-I Service:
kubectl get svc whoami-web

# run a DNS lookup for the Service in the sleep Pod:
kubectl exec deploy/sleep -- sh -c 'nslookup whoami-web | grep "^[^*]"'

# make some HTTP calls:
kubectl exec deploy/sleep -- sh -c 'for i in 1 2 3; do curl -w \\n -s http://whoami-web:8088; done;'

如图 6.5 所示,Pod 消耗内部服务的行为与外部消费者相同,请求在 Pod 之间进行负载均衡。当你运行这个练习时,你可能看到请求被完全均匀地分配,或者你可能看到一些 Pod 响应多次,这取决于网络的不可预测性。

图片

图 6.5 集群内部的世界:Pod 到 Pod 的网络也受益于服务负载均衡。

在第三章中,我们介绍了服务以及 ClusterIP 地址是如何从 Pod 的 IP 地址抽象出来的,所以当一个 Pod 被替换时,应用程序仍然可以通过相同的服务地址访问。现在你看到服务可以在许多 Pod 之间进行抽象,并且路由流量到任何节点上的 Pod 的网络层也可以在多个 Pod 之间进行负载均衡。

6.2 使用 Deployment 和 ReplicaSet 进行负载扩展

ReplicaSet 使得扩展你的应用变得极其简单:你只需通过在规范中更改副本的数量,就能在几秒钟内进行扩展或缩减。这对于运行在小巧、精简容器中的无状态组件来说非常完美,这也是为什么为 Kubernetes 构建的应用程序通常使用分布式架构,将功能分解成许多部分,这些部分可以单独更新和扩展。

Deployment 在 ReplicaSet 之上添加了一个有用的管理层。现在我们知道了它们是如何工作的,我们就不会再直接使用 ReplicaSet 了——Deployment 应该是定义应用程序的首选。我们不会在第九章中探索 Deployment 的所有功能,直到我们讨论应用程序的升级和回滚,但了解额外的抽象能给你带来什么是有用的。图 6.6 展示了这一点。

图片

图 6.6 显示,零是期望副本的有效数量;Deployment 将旧的 ReplicaSet 缩放到零。

Deployment 是 ReplicaSet 的控制器,为了实现大规模运行,你需要在 Deployment 规范中包含相同的replicas字段,并将其传递给 ReplicaSet。列表 6.2 显示了 Pi 网络应用程序的缩写 YAML,它明确设置了两个副本。

列表 6.2 web.yaml,一个运行多个副本的 Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pi-web
spec:
  replicas: 2            # The replicas field is optional; it defaults to 1.
selector:
  matchLabels:
    app: pi-web
  template:              # The Pod spec follows.

部署的标签选择器需要与 Pod 模板中定义的标签匹配,这些标签用于表达从 Pod 到 ReplicaSet 再到 Deployment 的所有权链。当你扩展 Deployment 时,它会更新现有的 ReplicaSet 以设置新的副本数量,但如果你在 Deployment 中更改 Pod 规范,它会替换 ReplicaSet 并将之前的副本数降至零。这使得 Deployment 在管理更新和处理任何问题时拥有很大的控制权。

现在尝试一下:为 Pi 网络应用程序创建一个 Deployment 和 Service,并进行一些更新以查看如何管理 ReplicaSet。

# deploy the Pi app:
kubectl apply -f pi/web/

# check the ReplicaSet:
kubectl get rs -l app=pi-web

# scale up to more replicas:
kubectl apply -f pi/web/update/web-replicas-3.yaml

# check the RS:
kubectl get rs -l app=pi-web

# deploy a changed Pod spec with enhanced logging:
kubectl apply -f pi/web/update/web-logging-level.yaml

# check ReplicaSets again:
kubectl get rs -l app=pi-web

这个练习表明 ReplicaSet 仍然是扩展机制:当你增加或减少 Deployment 中的副本数量时,它只是更新 ReplicaSet。Deployment 是,嗯,部署机制,它通过多个 ReplicaSet 管理应用程序更新。我的输出,如图 6.7 所示,显示了 Deployment 在完全缩减旧的副本之前等待新的 ReplicaSet 完全运行。

图 6.7 展示了 Deployment 如何管理 ReplicaSet 以在更新期间保持所需数量的 Pod。

你可以使用 kubectl scale 命令作为扩展控制器的快捷方式。你应该谨慎使用它,因为它是一种命令式的工作方式,而使用声明式的 YAML 文件会更好,这样你的应用程序在生产中的状态总是与源控制中存储的规范完全匹配。但如果你的应用程序性能不佳,自动部署需要 90 秒,那么这是一个快速扩展的方法——只要记得更新 YAML 文件。

现在尝试一下:使用 kubectl 直接扩展 Pi 应用程序,然后看看在再次进行完整部署时 ReplicaSet 会发生什么。

# we need to scale the Pi app fast:
kubectl scale --replicas=4 deploy/pi-web

# check which ReplicaSet makes the change:
kubectl get rs -l app=pi-web

# now we can revert back to the original logging level:
kubectl apply -f pi/web/update/web-replicas-3.yaml

# but that will undo the scale we set manually:
kubectl get rs -l app=pi-web

# check the Pods:
kubectl get pods -l app=pi-web

当你应用更新的 YAML 文件时,你会看到两件事:应用程序的副本数缩减到三个,Deployment 通过将新的 ReplicaSet 的 Pod 数量缩减到零并恢复旧的 ReplicaSet 到三个 Pod 来实现这一点。图 6.8 展示了更新后的 Deployment 导致创建了三个新的 Pod。

图 6.8 展示了 Deployment 了解其 ReplicaSet 的规范,并且可以通过扩展旧的 ReplicaSet 来回滚。

Deployment 更新覆盖了手动缩放级别并不令人惊讶;YAML 定义是期望状态,如果两者不同,Kubernetes 不会尝试保留当前规范中的任何部分。更令人惊讶的是,Deployment 重新使用了旧的 ReplicaSet 而不是创建一个新的,但这是一种更高效的工作方式,这要归功于更多的标签。

从 Deployment 创建的 Pod 有一个看起来随机的生成名称,但实际上并非如此。Pod 名称包含 Deployment 的 Pod 规范中的模板哈希,因此如果你对规范进行更改,它与之前的 Deployment 匹配,那么它将具有与缩小的 ReplicaSet 相同的模板哈希,Deployment 可以找到该 ReplicaSet 并将其再次扩展以实施更改。Pod 模板哈希存储在标签中。

现在尝试一下:检查 Pi Pods 和 ReplicaSets 的标签以查看模板哈希。

# list ReplicaSets with labels:
kubectl get rs -l app=pi-web  --show-labels

# list Pods with labels:
kubectl get po -l app=pi-web  --show-labels

图 6.9 显示模板哈希包含在对象名称中,但这只是为了方便——Kubernetes 使用标签进行管理。

图片 6-9

图 6.9 Kubernetes 生成的对象名称不仅仅是随机的——它们包括模板哈希。

了解部署与其 Pods 之间的内部关系将有助于你理解变更是如何分阶段实施的,并在你看到许多 ReplicaSet 具有零期望 Pod 计数时消除任何混淆。但 Pod 中的计算层与 Service 中的网络层之间的交互方式是相同的。

在一个典型的分布式应用中,你将为每个组件有不同的扩展需求,并且你会使用 Service 在它们之间实现多层的负载均衡。我们迄今为止部署的 Pi 应用只有一个 ClusterIP Service——它不是一个面向公众的组件。公众组件是一个代理(实际上,它是一个反向代理,因为它处理传入流量而不是传出流量),它使用一个 LoadBalancer Service。我们可以以规模运行 Web 组件和代理,并从客户端到代理 Pod 以及从代理到应用 Pod 实现负载均衡。

现在尝试一下:创建一个运行两个副本的代理 Deployment,以及一个 Service 和 ConfigMap,以设置与 Pi 网络应用的集成。

# deploy the proxy resources:
kubectl apply -f pi/proxy/ 

# get the URL to the proxied app:
kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/?dp=10000'

# browse to the app, and try a few different values for 'dp' in the URL

如果你打开浏览器中的开发者工具并查看网络请求,你可以找到代理发送的响应头。这些包括代理服务器的主机名——实际上是 Pod 名称——以及网页本身包含生成响应的 Web 应用 Pod 的名称。我的输出,如图 6.10 所示,显示了一个来自代理缓存的响应。

图片 6-10

图 6.10 Pi 响应包括发送它们的 Pod 名称,因此你可以看到负载均衡的工作情况。

此配置很简单,这使得它易于扩展。代理的 Pod 规范使用两个卷:一个 ConfigMap 用于加载代理配置文件,一个EmptyDir用于存储缓存的响应。ConfigMap 是只读的,因此一个 ConfigMap 可以被所有代理 Pod 共享。EmptyDir卷是可写的,但它们对 Pod 是唯一的,因此每个代理都得到自己的卷来用于缓存文件。图 6.11 显示了设置。

图片 6-11

图 6.11 在规模上运行 Pods——某些类型的卷可以共享,而其他类型的卷则是 Pod 独有的。

这种架构会带来一个问题,如果你请求 Pi 的高精度数字并不断刷新浏览器,你会看到这个问题。第一个请求会慢,因为它是由网络应用程序计算的;后续的响应会快,因为它们来自代理缓存,但很快你的请求就会转到没有该响应的缓存的不同代理 Pod,所以页面会再次加载缓慢。

如果使用共享存储来修复这个问题,那么每个代理 Pod 都可以访问相同的缓存会很好。这样做将把我们带回到我们在第五章中认为已经留下的分布式存储的棘手领域,但让我们先从一个简单的方法开始,看看它能带我们走到哪里。

现在尝试一下:部署一个更新到代理规范,它使用 HostPath 卷来存储缓存文件,而不是 EmptyDir。同一节点上的多个 Pod 将使用相同的卷,这意味着它们将共享代理缓存。

# deploy the updated spec:
kubectl apply -f pi/proxy/update/nginx-hostPath.yaml

# check the Pods--the new spec adds a third replica:
kubectl get po -l app=pi-proxy

# browse back to the Pi app, and refresh it a few times

# check the proxy logs:
kubectl logs -l app=pi-proxy --tail 1 

现在,你应该能够随心所欲地刷新,而且无论你被引导到哪个代理 Pod,响应总是来自缓存。图 6.12 显示了所有我的代理 Pods 正在响应请求,这些请求通过 Service 在它们之间共享。

图 6-12

图 6.12 在规模上,你可以使用 kubectl 和标签选择器查看所有 Pod 的日志。

对于大多数有状态的应用程序,这种方法是不可行的。写入数据的应用程序往往假设它们对文件有独占访问权,如果相同应用程序的另一个实例尝试使用相同的文件位置,你可能会得到意外但令人失望的结果——比如应用程序崩溃或数据损坏。我使用的反向代理是 Nginx;在这里它非常宽容,并且愿意与其他实例共享其缓存目录。

如果你的应用需要扩展和存储,你可以选择使用不同类型的控制器。在本章的剩余部分,我们将探讨 DaemonSet;最后一种类型是 StatefulSet,它很快就会变得复杂,我们将在第八章中详细讨论,那时它将占据大部分章节。DaemonSet 和 StatefulSet 都是 Pod 控制器,尽管你使用它们的频率会比 Deployments 低得多,但你仍需要了解你可以用它们做什么,因为它们可以启用一些强大的模式。

6.3 使用 DaemonSet 实现高可用性扩展

DaemonSet 的名字来源于 Linux 守护进程,它通常是一个在后台以单个实例持续运行的系统进程(在 Windows 世界中相当于 Windows 服务)。在 Kubernetes 中,DaemonSet 在集群的每个节点上运行一个 Pod 的单个副本,或者如果你在规范中添加了一个选择器,它可以在节点的一个子集上运行。

守护集在基础设施级别的关注点中很常见,您可能希望从每个节点获取信息并将其发送到中央收集器。每个节点上运行一个 Pod,仅获取该节点的数据。您不需要担心任何资源冲突,因为节点上只有一个 Pod。我们将在本书的后面使用守护集从 Pod 收集日志以及关于节点活动的指标。

当您想要高可用性而不需要每个节点上许多副本的负载要求时,您也可以在自己的设计中使用它们。反向代理是一个很好的例子:单个 Nginx Pod 可以处理成千上万的并发连接,因此您不一定需要很多,但您可能想确保每个节点上都有一个运行,以便本地 Pod 可以在流量到达的地方响应。列表 6.3 显示了守护集的缩写 YAML——它看起来与其他控制器很相似,但没有副本数量。

列表 6.3 nginx-ds.yaml,代理组件的守护集

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: pi-proxy
spec:
  selector:
    matchLabels:      # DaemonSets use the same label selector mechanism.
      app: pi-proxy   # Finds the Pods that the set owns
template:
  metadata:
    labels:
      app: pi-proxy   # Labels applied to the Pods must match the selector.
spec:
# Pod spec follows

此代理的规范仍然使用HostPath卷。这意味着每个 Pod 都将有自己的代理缓存,因此我们无法从共享缓存中获得最佳性能。这种方法适用于其他比 Nginx 更挑剔的有状态应用程序,因为没有多个实例使用相同的数据文件的问题。

现在尝试手动删除。您不能从一种控制器类型转换为另一种类型,但我们可以在不破坏应用程序的情况下将更改从部署更改为守护集。

# deploy the DaemonSet:
kubectl apply -f pi/proxy/daemonset/nginx-ds.yaml

# check the endpoints used in the proxy service:
kubectl get endpoints pi-proxy

# delete the Deployment:
kubectl delete deploy pi-proxy

# check the DaemonSet:
kubectl get daemonset pi-proxy

# check the Pods:
kubectl get po -l app=pi-proxy

# refresh your latest Pi calculation on the browser

图 6.13 显示了我的输出。在删除部署之前创建守护集意味着始终有 Pod 可用以接收来自服务的请求。如果首先删除部署,则应用程序将不可用,直到守护集启动。如果您检查 HTTP 响应头,您也应该看到您的请求来自代理缓存,因为新的守护集 Pod 使用与部署 Pod 相同的HostPath卷。

图片

图 6.13 在进行重大更改时,您需要规划部署的顺序以保持您的应用程序在线。

我正在使用单节点集群,因此我的守护集运行一个 Pod;如果有更多节点,我将在每个节点上有一个 Pod。控制循环监视加入集群的节点,任何新节点都会在加入后立即调度启动一个副本 Pod。控制器还监视 Pod 状态,如果 Pod 被删除,则会启动一个替换。

现在尝试手动删除代理 Pod。守护集将启动一个替换。

# check the status of the DaemonSet:
kubectl get ds pi-proxy

# delete its Pod:
kubectl delete po -l app=pi-proxy

# check the Pods:
kubectl get po -l app=pi-proxy

当 Pod 正在被删除时,如果您刷新浏览器,您会看到它不会响应,直到守护集启动了一个替换。这是因为您正在使用单节点实验室集群。服务只向正在运行的 Pod 发送流量,所以在多节点环境中,请求会发送到仍然有健康 Pod 的节点。图 6.14 显示了我的输出。

图片

图 6.14 守护集监控节点和 Pod,以确保始终满足所需的副本数量。

需要使用 DaemonSet 的情况通常比仅仅想在每个节点上运行 Pod 要复杂一些。在这个代理示例中,你的生产集群可能只有一小部分节点可以接收来自互联网的流量,因此你只想在这些节点上运行代理 Pod。你可以通过标签来实现这一点,添加任何你想要的任意标签来识别你的节点,然后在 Pod 规范中选择该标签。列表 6.4 使用 nodeSelector 字段展示了这一点。

列表 6.4 nginx-ds-nodeSelector.yaml,具有节点选择的 DaemonSet

# This is the Pod spec within the template field of the DaemonSet.
spec:
  containers:
    # ...
  volumes:
    # ...
  nodeSelector:       # Pods will run only on certain nodes.
    kiamol: ch06      # Selected with the label kiamol=ch06

DaemonSet 控制器不仅监视节点加入集群,它还会查看所有节点,以查看它们是否与 Pod 规范中的要求匹配。当你部署这个更改时,你是在告诉 DaemonSet 只在设置了标签 kiamolch06 值的节点上运行。在你的集群中将没有匹配的节点,因此 DaemonSet 将缩放到零。

现在尝试一下 更新 DaemonSet 以包括列表 6.4 中的节点选择器。现在没有节点符合要求,因此现有的 Pod 将被删除。然后标记一个节点,将调度一个新的 Pod。

# update the DaemonSet spec:
kubectl apply -f pi/proxy/daemonset/nginx-ds-nodeSelector.yaml

# check the DS:
pi-proxy

# check the Pods:
kubectl get po -l app=pi-proxy

# now label a node in your cluster so it matches the selector:
kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') kiamol=ch06 --overwrite

# check the Pods again:
kubectl get ds pi-proxy

你可以在图 6.15 中看到 DaemonSet 的控制循环正在起作用。当应用节点选择器时,没有节点符合选择器,因此 DaemonSet 的期望副本计数降至零。现有的 Pod 对于期望计数来说过多,因此它被删除。然后,当节点被标记时,有一个匹配选择器的节点,因此期望计数增加到一,因此创建一个新的 Pod。

图片 6-15

图 6.15 DaemonSet 监视节点及其标签,以及当前的 Pod 状态。

DaemonSet 与 ReplicaSet 有不同的控制循环,因为它们的逻辑需要监视节点活动以及 Pod 数量,但本质上,它们都是管理 Pod 的控制器。所有控制器都负责其管理对象的生命周期,但链接可能会断开。我们将在下一个练习中使用 DaemonSet 来展示如何使 Pod 从其控制器中解放出来。

现在尝试一下 Kubectl 的 delete 命令有一个 cascade 选项,你可以使用它来删除控制器而不删除其管理对象。这样做会在后面留下孤儿 Pod,如果它们与之前所有者的匹配,则可以被另一个控制器收养。

# delete the DaemonSet, but leave the Pod alone: 
kubectl delete ds pi-proxy --cascade=false

# check the Pod:
kubectl get po -l app=pi-proxy

# recreate the DS:
kubectl apply -f pi/proxy/daemonset/nginx-ds-nodeSelector.yaml

# check the DS and Pod:
kubectl get ds pi-proxy

kubectl get po -l app=pi-proxy

# delete the DS again, without the cascade option:
kubectl delete ds pi-proxy

# check the Pods:
kubectl get po -l app=pi-proxy

图 6.16 显示了相同的 Pod 在 DaemonSet 被删除和重新创建后仍然存活。新的 DaemonSet 需要单个 Pod,现有的 Pod 与其模板匹配,因此它成为 Pod 的管理者。当这个 DaemonSet 被删除时,Pod 也会被删除。

图片 6-16

图 6.16 孤儿 Pod 已经失去了其控制器,因此它们不再是高可用集的一部分。

禁止级联删除是那些你很少会使用但当你需要时会非常高兴知道的功能之一。在这种情况下,你可能对现有的所有 Pod 都感到满意,但节点上即将有一些维护任务。与其在处理节点时让 DaemonSet 添加和删除 Pod,你可以在维护完成后删除它并重新启用它。

我们在这里使用的 DaemonSets 示例是关于高可用性的,但它仅限于某些类型的应用程序——你想要多个实例,并且可以接受每个实例都有自己的独立数据存储。其他需要高可用性的应用程序可能需要在实例之间同步数据,对于这些应用程序,你可以使用 StatefulSets。不过,现在不要跳到第八章,因为在第七章中你将学习一些有助于有状态应用程序的巧妙模式。

StatefulSets、DaemonSets、ReplicaSets 和 Deployments 是您用来建模应用程序的工具,它们应该为您提供足够的灵活性,在 Kubernetes 中运行几乎任何东西。我们将以快速查看 Kubernetes 实际上如何管理拥有其他对象的对象来结束本章,然后我们将回顾我们在本书第一部分的进展情况。

6.4 理解 Kubernetes 中的对象所有权

控制器使用标签选择器来查找它们管理的对象,而对象本身则在元数据字段中保留其所有者的记录。当你删除一个控制器时,它管理的对象仍然存在,但不会持续太久。Kubernetes 运行一个垃圾收集器进程,寻找所有者已被删除的对象,并将它们也删除。对象所有权可以模拟一个层次结构:Pods 属于 ReplicaSets,而 ReplicaSets 属于 Deployments。

现在试试看。查看所有 Pod 和 ReplicaSets 的元数据字段中的所有者引用。

# check which objects own the Pods:
kubectl get po -o custom-columns=NAME:'{.metadata.name}',
OWNER:'{.metadata.ownerReferences[0].name}',OWNER_KIND:'{.metadata.ownerReferences[0].kind}'

# check which objects own the ReplicaSets:
kubectl get rs -o custom-columns=NAME:'{.metadata.name}',
OWNER:'{.metadata.ownerReferences[0].name}',OWNER_KIND:'{.metadata.ownerReferences[0].kind}'

图 6.17 显示了我的输出,其中所有 Pod 都由某个其他对象拥有,而除了一个之外的所有 ReplicaSets 都由一个 Deployment 拥有。

图片

图 6.17 对象知道它们的拥有者是谁——你可以在对象元数据中找到这一点。

Kubernetes 在管理关系方面做得很好,但你需要记住,控制器仅使用标签选择器跟踪其依赖项,所以如果你篡改标签,可能会破坏这种关系。默认的删除行为是大多数时候你想要的,但你可以使用 kubectl 停止级联删除,只删除控制器——这会从依赖项的元数据中删除所有者引用,因此它们不会被垃圾收集器选中。

我们将结束对最新版本的 Pi 应用程序架构的探讨,该应用程序在本章中已部署。图 6.18 展示了它的全部辉煌。

图片

图 6.18 Pi 应用程序:无需注释——图表应该非常清晰。

相当多的事情在这个图中正在进行:它是一个简单的应用程序,但由于它使用了大量的 Kubernetes 功能来实现高可用性、可扩展性和灵活性,所以部署很复杂。到现在你应该对所有的这些 Kubernetes 资源都很熟悉,你应该了解它们是如何配合在一起以及何时使用它们。大约 150 行的 YAML 定义了应用程序,但那些 YAML 文件就是你需要在你的笔记本电脑上或在云中的 50 节点集群上运行此应用程序所需的所有内容。当新成员加入项目时,如果他们有扎实的 Kubernetes 经验——或者如果他们已经阅读了这本书的前六章——他们可以立即开始工作。

第一部分就到这里。如果你这周不得不延长午餐时间,我感到很抱歉,但现在你已经掌握了 Kubernetes 的所有基础知识,其中包含了最佳实践。我们只需要在你尝试实验室之前整理一下。

现在试试吧!本章所有顶级对象都应用了kiamol标签。现在你了解了级联删除,你会知道当你删除所有这些对象时,它们的依赖项也会被删除。

# remove all the controllers and Services:
kubectl delete all -l kiamol=ch06

6.5 实验室

过去几年中,Kubernetes 发生了很大变化。本章中我们使用的控制器是推荐的,但过去有过替代方案。在这个实验室中,你的任务是取一个使用一些较旧方法的 app 规范,并将其更新为使用你学到的控制器。

  • 首先,在ch06/lab/numbers目录下部署应用程序——它是第三章中的随机数应用程序,但配置很奇怪。而且它坏了。

  • 你需要更新 Web 组件以使用支持高负载的控制器。我们希望在生产中运行数十个这样的实例。

  • API 也需要更新。它需要复制以实现高可用性,但应用程序使用连接到服务器的硬件随机数生成器,一次只能由一个 Pod 使用。具有正确硬件的节点具有标签rng=hw(你需要在你的集群中模拟这一点)。

  • 这不是一个干净的升级,所以你需要规划你的部署以确保 Web 应用没有停机时间。

听起来很可怕,但你不必觉得这太糟糕。我的解决方案在 GitHub 上供你检查:github.com/sixeyed/kiamol/blob/master/ch06/lab/README.md

第二周:Kubernetes 在现实世界中的应用

当你开始真正使用 Kubernetes 时,你很快会发现并非所有应用程序都适合简单的模式。本节介绍了更多高级功能,帮助你为这一挑战做好准备。你将了解多个容器如何协同工作,使旧应用程序表现得像新应用程序一样,以及 Kubernetes 如何为有状态应用程序提供稳定的环境。你还将获得管理应用程序的经验——配置升级过程、使用 Helm 打包和分发应用程序,以及理解开发者工作流程。

7 使用多容器 Pod 扩展应用程序

我们在第二章中遇到了 Pod,当时你了解到你可以在一个 Pod 中运行多个容器,但你实际上并没有这样做。在本章中,你将了解它是如何工作的,并理解它所支持的模式。这是本书这一部分更高级主题中的第一个,但它不是一个复杂的话题——只是需要所有来自前几章的背景知识。从概念上讲,它相当简单:一个 Pod 运行多个容器,通常是你的应用程序容器和一些辅助容器。正是这些助手的功能使得这个特性如此有趣。

Pod 中的容器共享相同的虚拟环境,因此当一个容器执行操作时,其他容器可以看到并对此做出反应。它们甚至可以在原始容器不知情的情况下修改预期的操作。这种行为允许您将应用程序建模为非常简单的容器——它只专注于自己的工作,并且有助手负责将应用程序与其他组件以及 Kubernetes 平台集成。这是为所有应用程序添加一致的管理 API 的绝佳方式,无论是新应用程序还是遗留应用程序。

7.1 Pod 中容器如何通信

Pod 是一个虚拟环境,为一个或多个容器创建共享的网络和文件系统空间。容器是隔离的单元;它们有自己的进程和环境变量,并且可以使用不同技术栈的不同镜像。Pod 是一个单一单元,因此当它被分配到节点上运行时,所有 Pod 容器都在同一节点上运行。您可以有一个运行 Python 的容器和另一个运行 Java 的容器,但不能在同一个 Pod 中运行一些 Linux 和一些 Windows 容器(目前还不行),因为 Linux 容器需要在 Linux 节点上运行,而 Windows 容器需要在 Windows 节点上运行。

Pod 中的容器共享网络,因此每个容器都有相同的 IP 地址——Pod 的 IP 地址。多个容器可以接收外部流量,但它们需要监听不同的端口,Pod 内的容器可以使用 localhost 地址进行通信。每个容器都有自己的文件系统,但它可以从 Pod 挂载卷,因此容器可以通过共享相同的挂载来交换信息。图 7.1 展示了包含两个容器的 Pod 布局。

图 7.1 Pod 是许多容器的共享网络和存储环境。

那就是我们现在需要的所有理论,随着我们进入本章,你将对仅使用共享网络和磁盘就能做到的一些智能事情感到惊讶。我们将从本节的一些简单练习开始,以探索 Pod 环境。列表 7.1 显示了 Deployment 的多容器 Pod 规范。定义了两个容器,它们恰好使用相同的镜像,并且它们都挂载了一个在 Pod 中定义的 EmptyDir 卷。

列表 7.1 sleep-with-file-reader.yaml,一个简单的多容器 Pod 规范

spec:
  containers:                     # The containers field is an array.
    - name: sleep
      image: kiamol/ch03-sleep   
      volumeMounts:
        - name: data
          mountPath: /data-rw     # Mounts a volume as writable
    - name: file-reader           # Containers need different names. 
      image: kiamol/ch03-sleep    # But containers can use the same or
                                  # different images.
      volumeMounts:
        - name: data
          mountPath: /data-ro
          readOnly: true          # Mounts the same volume as read-only
  volumes:
    - name: data                  # Volumes can be mounted by many containers.
        emptyDir: {}              

这是一个运行两个容器的单个 Pod 规范。当你部署它时,你会看到在处理多容器 Pod 时有一些工作方式上的差异。

现在试试看:部署列表 7.1,并运行一个包含两个容器的 Pod。

# switch to the chapter folder:
cd ch07

# deploy the Pod spec:
kubectl apply -f sleep/sleep-with-file-reader.yaml

# get the detailed Pod information:
kubectl get pod -l app=sleep -o wide

# show the container names:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[*].name}'

# check the Pod logs--this will fail:
kubectl logs -l app=sleep

我在图 7.2 中显示的输出表明,Pod 有两个容器,具有单个 IP 地址,它们都在同一个节点上运行。你可以将 Pod 作为一个单一单元查看其详细信息,但你不能在 Pod 级别打印日志;你需要指定一个容器来获取日志。

图 7-2

图 7.2:你总是将 Pod 作为一个单一单元来工作,除非你需要指定一个容器。

在那个练习中,两个容器都使用了 sleep 镜像,所以它们没有做任何事情,但容器仍然在运行,Pod 保持可用以供工作。这两个容器都挂载了 Pod 的EmptyDir卷,所以这是文件系统的共享部分,你可以在两个容器中使用它。

现在试试看:一个容器将卷挂载为可读写,另一个容器挂载为只读。你可以在一个容器中写入文件,并在另一个容器中读取它们。

# write a file to the shared volume using one container:
kubectl exec deploy/sleep -c sleep -- sh -c 'echo ${HOSTNAME} > /data-rw/hostname.txt'

# read the file using the same container:
kubectl exec deploy/sleep -c sleep -- cat /data-rw/hostname.txt

# read the file using the other container:
kubectl exec deploy/sleep -c file-reader -- cat /data-ro/hostname.txt

# try to add to the file to the read-only container--this will fail:
kubectl exec deploy/sleep -c file-reader -- sh -c 'echo more >> /data-ro/hostname.txt'

当你运行这个练习时,你会看到第一个容器可以将数据写入共享卷,第二个容器可以读取它,但它本身不能写入数据。这是因为在这个 Pod 规范中,卷挂载被定义为第二个容器的只读。这不是一个通用的 Pod 限制;如果需要,挂载可以被定义为多个容器的可写。图 7.3 显示了我的输出。

图 7-3

图 7.3:容器可以将相同的 Pod 卷挂载以共享数据,但具有不同的访问级别。

一个古老的空目录卷在这里再次显示了它的价值;它是一个所有 Pod 容器都可以访问的简单便笺。卷在 Pod 级别定义,在容器级别挂载,这意味着你可以使用任何类型的卷或 PVC,并将其提供给许多容器使用。将卷定义与卷挂载解耦还允许选择性共享,因此一个容器可能能够看到 Secrets,而其他容器则不能。

另一个共享空间是网络,容器可以在不同的端口上监听并提供独立的功能块。如果你的应用容器正在做一些后台工作但没有任何功能来报告进度,这很有用。同一个 Pod 中的另一个容器可以提供一个 REST API,报告应用容器正在做什么。

列表 7.2 显示了此过程的简化版本。这是对 sleep 部署的更新,用新的容器规范替换了文件共享容器,该规范运行一个简单的 HTTP 服务器。

列表 7.2 sleep-with-server.yaml,在第二个容器中运行 Web 服务器

spec:
  containers:
    - name: sleep
      image: kiamol/ch03-sleep   # The same container spec as listing 7.1
    - name: server
      image: kiamol/ch03-sleep   # The second container is different.
      command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 ..."]
      ports:
        - containerPort: 8080    # Including the port just documents
                                 # which port the application uses.

现在,Pod 将运行原始应用程序容器——sleep 容器,它实际上并没有做什么——和一个服务器容器,该服务器容器在端口 8080 上提供了一个 HTTP 端点。这两个容器共享相同的网络空间,因此 sleep 容器可以使用 localhost 地址访问服务器。

现在尝试一下 使用 7.2 列表中的文件更新 sleep 部署,并确认服务器容器可访问。

# deploy the update:
kubectl apply -f sleep/sleep-with-server.yaml

# check the Pod status:
kubectl get pods -l app=sleep

# list the container names in the new Pod:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[*].name}'

# make a network call between the containers:
kubectl exec deploy/sleep -c sleep -- wget -q -O - localhost:8080

# check the server container logs:
kubectl logs -l app=sleep -c server

您可以在图 7.4 中看到我的输出。尽管这些是独立的容器,但在网络层面上,它们就像在同一台机器上运行的不同进程一样工作,使用本地地址进行通信。

图 7.4

图 7.4 显示,同一 Pod 中的容器之间的网络通信是通过 localhost 进行的。

网络共享不仅限于 Pod 内部。Pod 在集群上有一个 IP 地址,如果 Pod 中的任何容器正在监听端口,则其他 Pod 可以访问它们。您可以为特定端口上的 Pod 创建一个服务,将流量路由到该 Pod,并且监听该端口的任何容器都将接收到请求。

现在尝试一下 使用 kubectl 命令公开 Pod 端口——这是一种不编写 YAML 即可快速创建服务的方法,然后测试 HTTP 服务器是否可以从外部访问。

# create a Service targeting the server container port:
kubectl expose -f sleep/sleep-with-server.yaml --type LoadBalancer --port 8020 --target-port 8080

# get the URL for your service:
kubectl get svc sleep -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8020'
# open the URL in your browser

# check the server container logs:
kubectl logs -l app=sleep -c server

图 7.5 显示了我的输出。从外部世界来看,这只是流向服务的网络流量,该服务被路由到 Pod。Pod 正在运行多个容器,但这是对消费者隐藏的细节。

图 7.5

图 7.5 显示,服务可以将网络请求路由到任何已发布端口的 Pod 容器。

您应该已经感受到了在 Pod 中运行多个容器的强大之处,在接下来的章节中,我们将将这些想法应用于实际场景。不过,有一件事需要强调:Pod 不是虚拟机的替代品,所以不要以为您可以在一个 Pod 中运行应用程序的所有组件。您可能会倾向于将应用程序建模为那样,即一个运行在同一个 Pod 中的 Web 服务器容器和一个 API 容器——不要这样做。Pod 是一个单一单元,应该用于您的应用程序的单个组件。可以使用额外的容器来支持应用程序容器,但您不应该在同一个 Pod 中运行不同的应用程序。这样做会破坏您独立更新、扩展和管理这些组件的能力。

7.2 使用初始化容器设置应用程序

到目前为止,我们已经运行了包含多个容器的 Pod,其中所有容器都并行运行:它们同时启动,Pod 只有在所有容器都准备好后才被认为是就绪的。您会听到这被称为边车模式,这强化了额外容器(边车)作为应用程序容器(摩托车)的辅助角色的想法。当您需要容器在应用程序容器之前运行以设置环境的一部分时,Kubernetes 还支持另一种模式。这被称为初始化容器

初始化容器的工作方式与边车容器不同。你可以为 Pod 定义多个初始化容器,并且它们按顺序运行,顺序与 Pod 规范中编写的顺序相同。每个初始化容器在下一个开始之前都需要成功完成,并且所有初始化容器都必须成功完成,Pod 容器才能启动。图 7.6 显示了具有初始化容器的 Pod 的启动顺序。

图片 7-6

图 7.6 初始化容器对于启动任务很有用,可以为应用程序容器准备 Pod。

所有容器都可以访问 Pod 中定义的卷,因此主要用例是初始化容器写入为应用程序容器准备环境的数据。列表 7.3 展示了从上一个练习中 sleep Pod 的 HTTP 服务器的简单扩展。初始化容器运行并生成一个 HTML 文件,它将该文件写入 EmptyDir 卷的挂载点。服务器容器通过发送该文件的内容来响应 HTTP 请求。

列表 7.3 sleep-with-html-server.yaml,Pod 规范中的初始化容器

spec:                              # Pod spec in the Deployment template
  initContainers:                  # Init containers have their own array,
    - name: init-html              # and they run in sequence.
      image: kiamol/ch03-sleep
      command: ['sh', '-c', "echo '<!DOCTYPE html...' > /data/index.html"]
      volumeMounts:
     - name: data
       mountPath: /data            # Init containers can mount Pod volumes.

这个例子使用与初始化容器相同的 sleep 镜像,但它可以是任何镜像。你可能会使用初始化容器通过安装 Git 命令行工具来设置应用程序环境,并将存储库克隆到共享文件系统中。应用程序容器可以访问这些文件,而无需你在应用程序镜像中设置 Git 客户端。

现在尝试一下:部署列表 7.3 中的更新,看看初始化容器是如何工作的。

# apply the updated spec with the init container:
kubectl apply -f sleep/sleep-with-html-server.yaml

# check the Pod containers:
kubectl get pod -l app=sleep -o 
   jsonpath='{.items[0].status.containerStatuses[*].name}'

# check the init containers:
kubectl get pod -l app=sleep -o 
   jsonpath='{.items[0].status.initContainerStatuses[*].name}'

# check logs from the init container--there are none:
kubectl logs -l app=sleep -c init-html

# check that the file is available in the sidecar:
kubectl exec deploy/sleep -c server -- ls -l /data-ro

你将从这次练习中学到一些东西。应用程序容器只有在初始化容器成功完成后才会运行,因此你的应用程序可以安全地假设初始化容器准备的环境。在这种情况下,HTML 文件在服务器容器启动之前肯定存在。初始化容器是 Pod 规范的另一个部分,但某些管理功能与应用程序容器的工作方式相同——即使初始化容器已经退出,你也可以读取其日志。我的输出显示在图 7.7 中。

图片 7-7

图 7.7 初始化容器对于为应用程序和边车容器准备 Pod 环境很有用。

尽管如此,这仍然不是一个非常贴近现实世界的例子,所以让我们做一些更好的事情。我们在第四章中介绍了应用程序配置,并看到了如何使用环境变量、ConfigMaps 和 Secrets 来构建配置设置的层次结构。如果你的应用程序支持这一点,那真是太好了,但许多较老的应用程序没有这种灵活性;它们期望在某个地方找到一个单独的配置文件,并且不会在其他地方寻找。让我们看看这样的应用程序。

现在尝试一下 本章有一个新的演示应用程序,因为如果我看着 Pi 感到无聊,那么你肯定也是。这个程序并没有更多乐趣,但至少它是不同的。它每隔几秒将时间戳写入日志文件。它有一个旧式的配置框架,因此我们无法使用我们迄今为止学到的任何配置技术。

# run the app, which uses a single config file:
kubectl apply -f timecheck/timecheck.yaml

# check the container logs--there won’t be any:
kubectl logs -l app=timecheck

# check the log file inside the container:
kubectl exec deploy/timecheck -- cat /logs/timecheck.log

# check the config setup:
kubectl exec deploy/timecheck -- cat /config/appsettings.json

你可以在图 7.8 中看到我的输出。有限的配置框架并不是这个应用程序在容器平台中不是好公民的唯一原因——Pod 中也没有日志——但我们可以通过 Pod 中的额外容器来解决所有问题。

图片

图 7.8 使用单个配置源的老旧应用程序无法从配置层次结构中受益。

初始化容器是一个完美的工具,可以将这个应用程序调整为我们为所有应用程序想要使用的配置方法。我们可以将设置存储在 ConfigMaps、Secrets 和环境变量中,并使用初始化容器从所有不同的输入中读取,合并内容,并将输出写入应用程序使用的单个文件位置。列表 7.4 显示了 Pod 规范中的初始化容器。

列表 7.4 timecheck-with-config.yaml,一个写入配置的初始化容器

spec:
  initContainers:
    - name: init-config
      image: kiamol/ch03-sleep    # This image has the jq tool.
      command: ['sh', '-c', "cat /config-in/appsettings.json | jq
        --arg APP_ENV \"$APP_ENVIRONMENT\" '.Application.Environment=$APP_ENV' > /config-out/appsettings.json"]  
      env:
        - name: APP_ENVIRONMENT   # All containers have their own environment
          value: TEST             # variables--they're not shared in the Pod.
      volumeMounts:
        - name: config-map        # Mounts a ConfigMap volume to read  
          mountPath: /config-in   
        - name: config-dir
          mountPath: /config-out  # Mounts an EmptyDir volume to write to

在我们更新部署之前,有一些需要注意的事项:

  • 初始化容器使用 jq 工具,而应用程序不需要这个工具。容器使用不同的镜像,每个镜像都只包含运行该步骤所需的工具。

  • 初始化容器中的命令从ConfigMap卷挂载读取,合并环境变量值,并将输出写入到EmptyDir卷挂载。

  • 应用程序容器将EmptyDir卷挂载到需要配置文件的路径。由初始化容器生成的文件隐藏了应用程序镜像中的默认配置。

  • 容器不共享环境变量。设置是为初始化容器指定的;应用程序容器看不到这些设置。

  • 容器映射它们需要的卷。两个容器都挂载了EmptyDir卷,它们共享这个卷,但只有初始化容器挂载了ConfigMap

当我们应用这个更新时,应用程序的行为将根据ConfigMap和环境变量而改变,即使应用程序容器不使用它们作为配置源。

现在尝试一下 使用列表 7.4 更新 timecheck 应用程序,以便应用程序容器从多个源进行配置。

# apply the ConfigMap and the new Deployment spec:
kubectl apply -f timecheck/timecheck-configMap.yaml -f timecheck/timecheck-with-config.yaml

# wait for the containers to start:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck,version=v2

# check the log file in the new app container:
kubectl exec deploy/timecheck -- cat /logs/timecheck.log

# see the config file built by the init container:
kubectl exec deploy/timecheck -- cat /config/appsettings.json

当你运行这个程序时,你会看到应用程序与新的配置一起工作,应用程序容器规范中唯一的变化是配置目录是从EmptyDir卷挂载的。我的输出显示在图 7.9 中。

图片

图 7.9 初始化容器可以在不更改应用程序代码或 Docker 镜像的情况下改变应用程序的行为。

这种方法之所以有效,是因为配置文件是从一个专用目录加载的。请记住,如果已经存在,卷挂载会覆盖镜像中的目录。如果应用程序从与应用程序二进制文件相同的目录加载配置文件,你就无法这样做,因为EmptyDir挂载会覆盖整个应用程序文件夹。在这种情况下,你需要在应用程序容器启动时添加一个额外的步骤,将配置文件从挂载复制到应用程序目录中。

将标准配置方法应用于非标准应用程序是 init 容器的一个很好的用途,但较老的应用程序仍然不会在现代平台上很好地运行,这就是边车容器可以提供帮助的地方。

7.3 使用适配器容器应用一致性

将应用程序迁移到 Kubernetes 是一个很好的机会,可以在所有应用程序之间添加一层一致性,这样你就可以使用相同的工具以相同的方式部署和管理它们,无论应用程序做什么,使用什么技术栈,或者何时开发。我的同事 Docker Captain Sune Keller 曾经谈到他们在 Alm Brand 使用的服务酒店(bit.ly/376rBcF)概念。他们的容器平台为“客户”(如高可用性和安全性)提供了一系列保证,前提是他们遵守规则(如从平台拉取配置并将其日志写入平台)。

并非所有应用程序都知道规则,并且有些规则无法由平台从外部应用,但边车容器与应用程序容器并行运行,因此它们处于有利的地位。你可以将它们用作适配器,这些适配器理解应用程序工作的一些方面,并将其适应平台希望它工作的方式。日志是一个经典的例子。

每个应用程序都会将一些输出写入日志条目——或者应该这样做;否则,它将完全无法管理,你应该拒绝与之合作。现代应用程序平台,如 Node.js 和.NET Core,会将输出写入标准输出流,这是 Docker 获取容器日志和 Kubernetes 获取 Pod 日志的地方。较老的应用程序对日志有不同的看法,它们可能将日志写入文件或其他永远不会作为容器日志出现的目标,因此你永远不会看到任何 Pod 日志(请参阅电子书的附录 D 以了解更多关于 Docker 中的日志信息)。这就是 timecheck 应用程序所做的事情,我们可以通过一个非常简单的边车容器来修复它。其规范出现在列表 7.5 中。

列表 7.5 timecheck-with-logging.yaml,使用边车容器公开日志

containers:
  - name: timecheck
    image: kiamol/ch07-timecheck
    volumeMounts:                   
      - name: logs-dir               # The app container writes the log file
        mountPath: /logs             # to an EmptyDir volume mount.
      # Abbreviated--the full spec also includes the config mount.
  - name: logger
    image: kiamol/ch03-sleep         # The sidecar just watches the log file.
    command: ['sh', '-c', 'tail -f /logs-ro/timecheck.log']
    volumeMounts:
      - name: logs-dir
        mountPath: /logs-ro          # Uses the same volume as the app 
        readOnly: true

边车所做的只是挂载日志卷(使用EmptyDir!)并使用标准的 Linux tail命令从日志文件中读取。-f选项意味着命令将跟踪文件;实际上,它只是坐着并监视新的写入,当文件中写入任何行时,它们会被回显到标准输出。它是一个中继器,将应用程序的实际日志实现适配到 Kubernetes 的期望。

现在尝试一下 应用列表 7.5 中的更新,并检查应用程序日志是否可用。

# add the sidecar logging container:
kubectl apply -f timecheck/timecheck-with-logging.yaml

# wait for the containers to start:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck,version=v3

# check the Pods:
kubectl get pods -l app=timecheck

# check the containers in the Pod:
kubectl get pod -l app=timecheck -o jsonpath='{.items[0].status.containerStatuses[*].name}'

# now you can see the app logs in the Pod:
kubectl logs -l app=timecheck -c logger

这里有一些低效之处,因为应用程序容器将日志写入文件,然后日志容器再次读取它们。这将产生一小段时间延迟,并且可能浪费大量磁盘空间,但在下一个应用程序更新中,Pod 将被替换,并且所有在卷中使用的空间都将被回收。好处是,这个 Pod 现在表现得像其他所有 Pod 一样,使应用程序日志可用于 Kubernetes,而无需对应用程序本身进行任何更改,如图 7.10 所示。

图片

图 7.10 适配器为 Pods 带来一层一致性,使旧应用程序表现得像新应用程序。

从平台接收配置并将日志写入平台是任何应用程序的基本操作,但随着平台的成熟,你会有更多对标准行为的期望。你希望能够测试容器内的应用程序是否健康,并且你希望能够从应用程序中提取指标以了解它在做什么以及它工作得多努力。

边车也能在这方面提供帮助,要么通过运行定制的容器,这些容器提供针对应用程序的信息,要么通过拥有标准的健康和指标容器镜像,你可以将这些镜像应用到所有的 Pod 规范中。我们将使用 timecheck 应用程序来完成练习,并添加使其成为 Kubernetes 良好公民的功能。不过,我们将使用一些额外的静态 HTTP 服务器容器来作弊,这些容器可以在列表 7.6 中看到。

列表 7.6 timecheck-good-citizen.yaml,更多边车以扩展应用程序

containers:            # The previous app and logging containers are the same.
  - name: timecheck
 # ...
  - name: logger
 # ...

  - name: healthz                # A new sidecar that exposes a healthcheck API
    image: kiamol/ch03-sleep     # This is just a static response.
    command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-Type:
    application/json\nContent-Length: 17\n\n{\"status\": \"OK\"}' | nc -l -p 8080; done"]                 
    ports:
      - containerPort: 8080      # Available at port 8080 in the Pod

  - name: metrics                # Another sidecar, which adds a metrics API
    image: kiamol/ch03-sleep     # The content is static again.
    command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-Type:
    text/plain\nContent-Length:
    104\n\n# HELP timechecks_total The total number timechecks.\n# 
    TYPE timechecks_total counter\ntimechecks_total 6' | nc -l -p 8081; done"]
    ports:
      - containerPort: 8081      # The content is avaialable on a different
                                 # port.

完整的 YAML 文件还包括一个 ClusterIP 服务,该服务在端口 8080 上发布健康端点,在端口 8081 上发布指标端点。在生产集群中,这些端口将由其他组件用于收集监控统计信息。部署是之前版本的扩展,因此应用程序使用初始化容器进行配置,并带有新的边车容器以及日志边车。

现在试试看:部署更新,并检查应用程序的健康和性能新管理端点。

# apply the update:
kubectl apply -f timecheck/timecheck-good-citizen.yaml

# wait for all the containers to be ready:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck,version=v4

# check the running containers:
kubectl get pod -l app=timecheck -o jsonpath='{.items[0].status.containerStatuses[*].name}'

# use the sleep container to check the timecheck app health:
kubectl exec deploy/sleep -c sleep -- wget -q -O - http://timecheck:8080

# check its metrics:
kubectl exec deploy/sleep -c sleep -- wget -q -O - http://timecheck:8081

当你运行练习时,你会看到一切按预期工作,如图 7.11 所示。你也可能看到更新没有你习惯的那么快,新的 Pod 启动时间更长,旧的 Pod 终止时间更长。额外的启动时间是初始化容器、应用程序容器和所有边车都需要准备好的时间——所有这些都需要在新的 Pod 被认为是准备好之前完成。额外的终止时间是因为被替换的 Pod 也包含多个容器,每个容器都会给容器进程关闭一个宽限期。

图片

图 7.11 多个适配器边车为应用程序提供了一个一致的管理 API。

运行所有这些作为适配器的边车容器会有一些开销。你已经看到这增加了部署时间,但它也增加了应用程序的持续计算需求——甚至只是跟踪日志文件和提供简单 HTTP 响应的基本边车,也都消耗内存和计算周期。但是,如果你想要将没有这些功能的现有应用程序迁移到 Kubernetes,那么让所有应用程序以相同的方式运行是一种可接受的方法,如图 7.12 所示。

图 7-12

图 7.12 一致的管理 API 使得与 Pod 一起工作变得容易——无论 API 如何在 Pod 内部提供。

在上一个练习中,我们使用了一个闲置的旧 sleep Pod 来调用 timecheck 应用程序的新 HTTP 端点。记住,Kubernetes 有一个扁平的网络模型,其中 Pod 可以通过 Service 向任何其他 Pod 发送流量。你可能想要对你的应用程序中的网络通信有更多的控制,你同样可以通过运行一个管理出站流量的代理容器来实现这一点。

7.4 使用代理容器抽象连接

代理模式让你可以控制和简化应用程序的出站连接:你的应用程序向 localhost 地址发起网络请求,这些请求被代理捕获并执行。在几种情况下,你可以使用一个通用的代理容器,或者一个特定于你的应用程序组件的代理容器。图 7.13 显示了几个例子。代理中的逻辑可能是为了提高性能、增加可靠性或安全性。

图 7-13

图 7.13 代理模式具有很大的潜力,从简化应用程序逻辑到提高性能。

从应用程序中接管网络控制是非常强大的。代理容器可以进行服务发现、负载均衡、重试,甚至在不加密的通道上添加加密层。你可能听说过服务网格架构,使用像 Linkerd 和 Istio 这样的技术——它们都是通过代理边车容器在代理模式的不同变体中实现的。

我们在这里不会使用服务网格架构,因为这会让我们远远超出午餐时间,进入夜晚,但我们将通过一个简化的例子来了解它能够做什么。起点是我们之前使用的随机数应用程序。有一个 Web 应用程序在一个 Pod 中运行,它消费另一个 Pod 中运行的 API。API 是 Web 应用程序使用的唯一组件,因此理想情况下,我们应该限制对任何其他地址的网络调用,但在初始部署中并没有这样做。

现在尝试一下 运行随机数应用程序,并验证 Web 应用程序容器是否可以使用任何网络地址。

# deploy the app and Services:
kubectl apply -f numbers/

# find the URL for your app:
kubectl get svc numbers-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8090'

# browse and get yourself a nice random number

# check that the web app has access to other endpoints:
kubectl exec deploy/numbers-web -c web -- wget -q -O - http://timecheck:8080

Web Pod 可以使用 ClusterIP 服务和域名numbers-api来访问 API,但它也可以访问任何其他地址,这可能是公共互联网上的 URL 或另一个 ClusterIP 服务。图 7.14 显示应用程序可以读取 timecheck 应用程序的健康端点——这应该是一个私有端点,并且可能暴露对某些人有用的信息。

图 7.14 Kubernetes 对 Pod 容器发出的连接没有任何默认限制。

除了使用代理侧边车之外,您还有许多选项可以限制网络访问,但大使模式附带一些额外的功能,使其值得考虑。列表 7.7 显示了 Web 应用程序规范的更新,使用简单的代理容器作为大使。

列表 7.7 web-with-proxy.yaml,使用代理作为大使

containers:
  - name: web
    image: kiamol/ch03-numbers-web 
    env:
      - name: http_proxy                 # Sets the container to use the proxy
        value: http://localhost:1080     # so traffic goes to the ambassador
      - name: RngApi__Url
        value: http://localhost/api      # Uses a localhost address for the API
  - name: proxy
    image: kiamol/ch07-simple-proxy      # This is a basic HTTP proxy.
      env:
        - name: Proxy__Port              # Routes network requests from the app
          value: "1080"                  # using the configured URI mapping
        - name: Proxy__Request__UriMap__Source
          value: http://localhost/api
        - name: Proxy__Request__UriMap__Target
          value: http://numbers-api/sixeyed/kiamol/master/ch03/numbers/rng

此示例展示了大使模式的要点:应用程序容器使用 localhost 地址来消费任何服务,并且配置为将所有网络调用通过代理容器路由。代理是一个自定义应用程序,它记录网络调用,将 localhost 地址映射到实际地址,并阻止任何未列在映射中的地址。所有这些功能都成为 Pod 中的功能,但对应用程序容器来说是透明的。

现在尝试一下 更新随机数应用程序,并确认网络现在已锁定。

# apply the update from listing 7.5:
kubectl apply -f numbers/update/web-with-proxy.yaml

# refresh your browser, and get a new number

# check the proxy container logs:
kubectl logs -l app=numbers-web -c proxy

# try to read the health of the timecheck app:
kubectl exec deploy/numbers-web -c web -- wget -q -O - http://timecheck:8080

# check proxy logs again:
kubectl logs -l app=numbers-web -c proxy

现在,Web 应用程序与 API 的解耦程度更高,因为它甚至不知道 API 的 URL——这是在大使中设置的,可以独立于应用程序进行配置。Web 应用程序也被限制为使用单个地址进行出站请求,并且所有这些调用都由代理记录,如图 7.15 所示。

图 7.15 所有网络访问都通过大使进行,它可以实现自己的访问规则。

此 Web 应用程序的大使代理 HTTP 调用在 Pod 外部,但大使模式比这更广泛。它在传输层连接到网络,因此它可以处理任何类型的流量。数据库大使可以做出一些明智的选择,例如将查询发送到只读数据库副本,并仅使用主数据库进行写入。这将提高性能和可扩展性,同时将复杂逻辑排除在应用程序之外。

我们将通过更仔细地研究将 Pod 作为许多容器的共享环境意味着什么来结束本章。

7.5 理解 Pod 环境

Pod 是一个或多个容器的边界,就像容器是一个或多个进程的边界一样。Pod 创建虚拟化层而不增加开销,因此它们灵活且高效。这种灵活性的代价是——就像往常一样——复杂性,并且您需要了解一些与多容器 Pod 一起工作的细微差别。

需要理解的主要一点是,即使 Pod 内部运行了大量的容器,Pod 仍然是单个计算单元。只有当 Pod 中的所有容器都准备好时,Pod 才会准备好,并且服务只会将流量发送到准备好的 Pod。添加侧边容器和初始化容器增加了您应用程序的故障模式。

现在尝试一下 如果初始化容器失败,您可能会破坏您的应用程序。对数字应用程序的此更新将不会成功,因为初始化容器配置错误。

# apply the update:
kubectl apply -f numbers/update/web-v2-broken-init-container.yaml

# check the new Pod:
kubectl get po -l app=numbers-web,version=v2

# check the logs for the new init container:
kubectl logs -l app=numbers-web,version=v2 -c init-version

# check the status of the Deployment:
kubectl get deploy numbers-web

# check the status of the ReplicaSets:
kubectl get rs -l app=numbers-web

您可以在这次练习中看到,失败的初始化容器有效地阻止了应用程序的更新。新的 Pod 永远不会进入运行状态,并且不会从服务接收流量。部署永远不会缩小旧的 ReplicaSet,因为新的一个没有达到所需的可用性级别,但部署的基本细节看起来像更新已经成功,如图 7.16 所示。

图 7.16

图 7.16 向您的 Pod 规范添加更多容器为 Pod 失败提供了更多机会

如果启动时侧边容器失败,同样会出现这种情况——Pod 没有所有容器都在运行,所以 Pod 本身还没有准备好。您设置的任何部署检查都需要扩展到多容器 Pod,以确保所有初始化容器运行完成,并且所有 Pod 容器都在运行。您还需要注意以下重启条件:

  • 如果替换了具有初始化容器的 Pod,则新 Pod 将再次运行所有初始化容器。您必须确保您的初始化逻辑可以重复运行。

  • 如果您将 Pod 的初始化容器镜像(s)的更改部署,则 Pod 将重新启动。初始化容器将再次执行,应用程序容器被替换。

  • 如果您将 Pod 规范更改部署到应用程序容器镜像(s),则应用程序容器将被替换,但初始化容器不会再次执行。

  • 如果应用程序容器退出,则 Pod 会重新创建它。直到容器被替换,Pod 才是完整运行的,并且不会接收服务流量。

Pod 是一个单一的计算环境,但是当您在该环境中添加多个移动部件时,您需要测试所有故障场景,并确保您的应用程序按预期运行。

我们还没有涵盖 Pod 环境的最后一部分:计算层。Pod 容器有一个共享的网络,并且可以共享文件系统的一部分,但它们不能访问彼此的进程——容器边界仍然提供计算隔离。这是默认行为,但在某些情况下,您可能希望您的侧边容器可以访问应用程序容器的进程,无论是为了进程间通信还是让侧边容器可以获取有关应用程序进程的指标。

您可以通过 Pod 规范中的简单设置启用此访问:shareProcessNamespace: true。这意味着 Pod 中的每个容器都共享相同的计算空间,并且可以看到彼此的进程。

现在试试看 部署对睡眠 Pod 的更新,以便容器使用共享的计算空间并可以访问彼此的进程。

# check the processes in the current container:
kubectl exec deploy/sleep -c sleep -- ps

# apply the update:
kubectl apply -f sleep/sleep-with-server-shared.yaml

# wait for the new containers:
kubectl wait --for=condition=ContainersReady pod -l app=sleep,version=shared

# check the processes again:
kubectl exec deploy/sleep -c sleep -- ps

您可以在图 7.17 中看到我的输出。睡眠容器可以看到所有服务器容器的进程,并且它可以愉快地杀死它们所有,并使 Pod 处于混乱状态。

图 7.17 您可以配置一个 Pod,使所有容器都可以看到所有进程 - 请谨慎使用。

这就是多容器 Pod 的全部内容。您在本章中看到,您可以使用初始化容器为您的应用程序容器准备环境,并运行边车容器以向您的应用程序添加功能,而无需更改应用程序代码或 Docker 镜像。使用多个容器有一些注意事项,但这是一个您将经常使用的模式来扩展您的应用程序。只需记住,Pod 应该是一个逻辑组件:我不想看到您仅仅因为可以这样做就在一个 Pod 中运行 Nginx、WordPress 和 MySQL。现在让我们整理一下,为实验做好准备。

现在试试看 移除与本章标签匹配的所有内容。

kubectl delete all -l kiamol=ch07

7.6 实验

现在回到这个实验的 Pi 应用程序。Docker 镜像 kiamol/ch05-pi 实际上可以用不同的方式使用,要将它作为 Web 应用程序运行,您需要覆盖容器规范中的启动命令。我们在前几章的 YAML 文件中已经这样做过了,但现在我们被要求使用一种标准的设置 Pod 的方法。以下是要求和一些提示:

  • 应用程序容器需要使用所有 Pod 在我们平台上使用的标准启动命令。它应该运行 /init/startup.sh

  • Pod 应该使用端口 80 用于应用程序容器。

  • Pod 还应发布端口 8080 以供 HTTP 服务器使用,该服务器返回应用程序的版本号,

  • 应用程序容器镜像不包含启动脚本,因此您需要使用可以创建该脚本并将其设置为可执行文件以供应用程序容器运行的东西。

  • 应用程序没有在端口 8080(或任何其他地方)发布版本 API,因此您需要一些可以提供该功能的东西(它可以是任何静态文本)。

起始点是 ch07/lab/pi 中的 YAML 文件,目前该文件已损坏。您需要调查应用程序在前几章中的运行情况,并应用本章学到的技术。您有多种方法可以解决这个问题,您可以在通常的位置找到我的示例解决方案:github.com/sixeyed/kiamol/blob/master/ch07/lab/README.md

8 使用状态集(StatefulSets)和作业(Jobs)运行数据密集型应用程序

“数据密集型”不是一个非常科学的术语,但本章是关于运行一类不仅具有状态性,而且对如何使用状态有较高要求的程序。数据库是这类程序的一个例子。它们需要在多个实例上运行以实现高可用性,每个实例需要一个本地数据存储以实现快速访问,而这些独立的数据存储需要保持同步。数据有其自己的可用性要求,并且你需要定期运行备份以防止永久性故障或损坏。其他数据密集型应用程序,如消息队列和分布式缓存,也有类似的要求。

你可以在 Kubernetes 中运行这类应用程序,但你需要围绕一个固有的冲突进行设计:Kubernetes 是一个动态环境,而数据密集型应用程序通常期望在一个稳定的环境中运行。期望在已知网络地址找到对等体的集群应用程序在 ReplicaSet 中不会运行良好,而期望从磁盘驱动器读取的备份作业与 PersistentVolumeClaims 也不会很好地工作。如果你的应用程序有严格的数据要求,你需要以不同的方式建模你的应用程序,我们将在本章中介绍如何使用一些更高级的控制器:状态集(StatefulSets)、作业(Jobs)和定时作业(CronJobs)来做到这一点。

8.1 Kubernetes 如何使用状态集(StatefulSets)来建模稳定性

状态集(StatefulSet)是一种具有可预测管理功能的 Pod 控制器:它允许你在稳定的框架内以规模运行应用程序。当你部署 ReplicaSet 时,它会创建具有随机名称的 Pod,这些 Pod 在域名系统(DNS)中无法单独寻址,并且并行启动它们。当你部署状态集(StatefulSet)时,它会创建具有可预测名称的 Pod,这些 Pod 可以通过 DNS 单独访问,并且按顺序启动;第一个 Pod 需要启动并运行,第二个 Pod 才能创建。

集群应用程序是状态集(StatefulSet)的绝佳候选者。通常,它们设计有一个主实例和一个或多个辅助实例,这使它们具有高可用性。你可能能够扩展辅助实例,但它们都需要达到主实例并使用它来同步它们自己的数据。你不能用 Deployment 来建模,因为在 ReplicaSet 中,没有方法可以识别单个 Pod 作为主实例,因此你最终会得到奇怪且不可预测的条件,有多个主实例或没有主实例。

图 8.1 展示了这样一个例子,它可以用来运行我们在前几章中用于待办事项应用程序的 Postgres 数据库,但它使用状态集(StatefulSet)来实现数据复制和高可用性。

图 8.1

图 8.1 在状态集中,每个 Pod 都可以拥有从第一个 Pod 复制的自己的数据副本。

这个设置相当复杂,我们将分几个部分逐步进行,以便你了解一个工作状态集的所有组件是如何组合在一起的。这是一个不仅对数据库有用的模式——许多旧应用都是为静态运行时环境设计的,并假设了稳定性,而这种稳定性在 Kubernetes 中并不成立。StatefulSets 允许你模拟这种稳定性,如果你的目标是迁移现有的应用到 Kubernetes,那么它们可能是你在旅途中早期就会使用的东西。

让我们从简单的 StatefulSet 开始,它展示了基础知识。列表 8.1 显示,StatefulSets 几乎与其他 Pod 控制器具有相同的规范,只是它们还需要包含一个服务的名称。

列表 8.1 todo-db.yaml,一个简单的 StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: todo-db
spec:
  selector:            # StatefulSets use the same selector mechanism.
    matchLabels:
      app: todo-db
  serviceName: todo-db      # StatefulSets must be linked to a Service. 
  replicas: 2
  template:
    # pod spec...

当你部署这个 YAML 文件时,你会得到一个运行两个 Postgres Pod 的 StatefulSet,但不要过于兴奋——它们只是两个由同一个控制器管理的独立数据库服务器。要使两个 Pod 成为复制的数据库集群,还需要做更多的工作,我们将在接下来的几节中实现这一点。

现在尝试一下 从列表 8.1 中部署 StatefulSet,并观察它创建的 Pod 与由 ReplicaSet 管理的 Pod 的比较。

# switch to the chapter's source:
cd ch08

# deploy the StatefulSet, Service, and a Secret for the Postgres
# password:
kubectl apply -f todo-list/db/

# check the StatefulSet:
kubectl get statefulset todo-db

# check the Pods:
kubectl get pods -l app=todo-db

# find the hostname of Pod 0:
kubectl exec pod/todo-db-0 -- hostname

# check the logs of Pod 1:
kubectl logs todo-db-1 --tail 1

从图 8.2 中你可以看到,StatefulSet 的工作方式与 ReplicaSet 或 DaemonSet 非常不同。Pod 有一个可预测的名称,即 StatefulSet 名称后跟 Pod 的索引,因此你可以使用 Pod 的名称来管理它们,而无需使用标签选择器。

图 8.2 StatefulSet 可以为集群应用创建环境,但应用需要自行配置。

Pod 仍然由控制器管理,但比 ReplicaSet 更可预测。Pod 按顺序从零到 n 创建;如果你缩小集合,控制器将按相反的顺序删除它们,从 n 开始向下工作。如果你删除一个 Pod,控制器将创建一个替换。它将具有与原始 Pod 相同的名称和配置,但将是一个新的 Pod。

现在尝试一下 删除 StatefulSet 的 Pod 0,并观察 Pod 0 是否会再次出现。

# check the internal ID of Pod 0:
kubectl get pod todo-db-0 -o jsonpath='{.metadata.uid}'

# delete the Pod:
kubectl delete pod todo-db-0

# check Pods:
kubectl get pods -l app=todo-db

# check that the new Pod is a new Pod:
kubectl get pod todo-db-0 -o jsonpath='{.metadata.uid}'

你可以从图 8.3 中看到,StatefulSet 为应用提供了一个稳定的环境。Pod 0 被一个相同的 Pod 0 替换,但这不会触发整个新集;原始的 Pod 1 仍然存在。排序仅应用于创建和扩展,不应用于替换缺失的 Pod。

图 8.3 StatefulSets 正确替换了缺失的副本。

StatefulSet 只是建模稳定环境的第一步。你可以为每个 Pod 获取 DNS 名称,将 StatefulSet 连接到服务,这意味着你可以配置 Pod 通过与其他已知地址的副本一起工作来自行初始化。

8.2 在 StatefulSets 中使用 init 容器引导 Pod

Kubernetes API 由其他对象组成对象:状态集定义中的 Pod 模板与你在 Deployment 模板和裸 Pod 定义中使用的对象类型相同。这意味着所有 Pod 功能都可用于状态集,尽管 Pod 本身是以不同的方式管理的。我们在第七章中学习了初始化容器,它们是解决集群应用程序中经常需要的复杂初始化步骤的完美工具。

列表 8.2 显示了 Postgres 部署更新的第一个初始化容器。在这个 Pod 规范中,多个初始化容器按顺序运行,因为 Pod 也是按顺序启动的,所以你可以保证 Pod 1 中的第一个初始化容器不会在 Pod 0 完全初始化和准备好之前运行。

列表 8.2 todo-db.yaml,带有初始化的复制 Postgres 设置

initContainers:
  - name: wait-service
    image: kiamol/ch03-sleep
    envFrom:                       # env file for sharing between containers
      - configMapRef:
        name: todo-db-env
    command: ['/scripts/wait-service.sh']
    volumeMounts:
      - name: scripts              # Volume loads scripts from ConfigMap.
        mountPath: "/scripts"

在这个初始化容器中运行的脚本有两个功能:如果它在 Pod 0 中运行,它只是打印一条日志以确认这是数据库主节点,然后容器退出;如果它在任何其他 Pod 中运行,它会对主节点进行 DNS 查找调用,以确保在继续之前它是可访问的。下一个初始化容器将启动复制过程,因此这个容器确保一切就绪。

这个例子中的确切步骤是针对 Postgres 的,但对于许多集群和复制应用程序(如 MySQL、Elasticsearch、RabbitMQ 和 NATS)的模式是相同的。图 8.4 显示了如何使用状态集中的初始化容器来模拟该模式。

图 8-4

图 8.4 状态集的稳定环境为你提供了初始化时可以使用的保证。

你通过在规范中标识一个服务来为状态集中的单个 Pod 定义 DNS 名称,但它需要是特殊配置的无头服务。列表 8.3 显示了数据库服务如何配置没有 ClusterIP 地址以及具有 Pod 选择器。

列表 8.3 todo-db-service.yaml,状态集的无头服务

apiVersion: v1
kind: Service
metadata:
  name: todo-db
spec:
  selector:
    app: todo-db      # The Pod selector matches the StatefulSet.
  clusterIP: None     # The service will not get its own IP address.
  ports:
    # ports follow

没有 ClusterIP 的服务在集群中仍然可以作为 DNS 条目使用,但它不使用固定的 IP 地址作为服务。没有虚拟 IP 是通过网络层路由到实际目的地的。相反,服务的 DNS 条目为状态集中的每个 Pod 返回一个 IP 地址,并且每个 Pod 还额外获得自己的 DNS 条目。

现在尝试一下 我们已经部署了无头服务,因此我们可以使用 sleep Deployment 来查询 DNS 以查看状态集与典型 ClusterIP 服务相比如何。

# show the Service details:
kubectl get svc todo-db

# run a sleep Pod to use for network lookups:
kubectl apply -f sleep/sleep.yaml

# run a DNS query for the Service name:
kubectl exec deploy/sleep -- sh -c 'nslookup todo-db | grep "^[^*]"'

# run a DNS lookup for Pod 0:
kubectl exec deploy/sleep -- sh -c 'nslookup todo-db-0.todo-db.default.svc.cluster.local | grep "^[^*]"'

在这个练习中,你会发现对该服务的 DNS 查找返回了两个 IP 地址,这些是内部 Pod IP。Pod 本身在pod-name.service-name的格式下有自己的 DNS 条目,带有常规的集群域名后缀。图 8.5 显示了我的输出。

图 8-5

图 8.5 状态集为每个 Pod 提供自己的 DNS 条目,因此它们可以单独寻址。

可预测的启动顺序和可单独寻址的 Pod 是初始化 StatefulSet 中集群应用的基石。不同应用之间的细节可能会有很大差异,但总体上,Pod 的启动逻辑可能如下:如果我是 Pod 0,那么我是主节点,所以我将执行所有主节点的设置工作;否则,我是辅助节点,所以我将给主节点一些时间来设置,检查一切是否正常工作,然后使用 Pod 0 的地址进行同步。

Postgres 的实际设置相当复杂,所以在这里我将跳过它。它使用 ConfigMap 中的脚本在初始化容器中设置主节点和辅助节点。我在 StatefulSet 的规范中使用了我们在书中已经介绍过的各种技术,这值得探索,但脚本的细节都是特定于 Postgres 的。

现在尝试一下:更新数据库以使其成为复制设置。ConfigMap 中有配置文件和启动脚本,StatefulSet 被更新以在初始化容器中使用它们。

# deploy the replicated StatefulSet setup:
kubectl apply -f todo-list/db/replicated/

# wait for the Pods to spin up
kubectl wait --for=condition=Ready pod -l app=todo-db

# check the logs of Pod 0--the primary:
kubectl logs todo-db-0 --tail 1

# and of Pod 1--the secondary:
kubectl logs todo-db-1 --tail 2

Postgres 使用主动-被动模型进行复制,因此主节点用于数据库的读取和写入,辅助节点从主节点同步数据,并且可以被客户端使用,但仅限于读访问。图 8.6 展示了初始化容器如何识别每个 Pod 的角色并初始化它们。

图片

图 8.6 显示 Pod 是副本,但它们可以有不同的行为,使用初始化容器来选择角色。

初始化此类复制应用的大部分复杂性在于对工作流程的建模,这是特定于应用的。这里的初始化容器脚本使用pg_isready工具来验证主节点是否准备好接收连接,并使用pb_basebackup工具来启动复制。这些实现细节被抽象化,以便系统管理员管理。他们可以通过扩展 StatefulSet 来添加更多副本,就像使用任何其他复制控制器一样。

现在尝试一下:扩展数据库以添加另一个副本,并确认新的 Pod 也以辅助节点的方式启动。

# add another replica:
kubectl scale --replicas=3 statefulset/todo-db

# wait for Pod 2 to spin up
kubectl wait --for=condition=Ready pod -l app=todo-db

# check that the new Pod sets itself up as another secondary:
kubectl logs todo-db-2 --tail 2

我不会称这为一个企业级的生产设置,但这是一个很好的起点,真正的 Postgres 专家可以在这里接管。你现在有一个功能齐全的、具有主节点和两个辅助节点的复制 Postgres 数据库集群——Postgres 称它们为备用节点。如图 8.7 所示,所有备用节点都以相同的方式启动,从主节点同步数据,并且它们都可以被客户端用于只读访问。

图片

图 8.7 使用可单独寻址的 Pod 意味着辅助节点总能找到主节点。

这里缺少一个明显的部分——实际的数据存储。我们设置的配置并不是真正可用的,因为它没有存储卷,所以每个数据库容器都在自己的可写层中写入数据,而不是在持久卷中。StatefulSets 有一种定义卷需求的好方法:你可以在规范中包含一组持久卷声明(PVC)模板。

8.3 使用卷声明模板请求存储

卷是标准 Pod 规范的一部分,你可以将 ConfigMaps 和 Secrets 加载到 Pod 中以供 StatefulSet 使用。你甚至可以包括一个 PVC 并将其挂载到应用程序容器中,但这会给所有 Pod 提供共享的卷。这对于只读配置设置来说是可以的,你希望每个 Pod 都有相同的数据,但如果挂载标准 PVC 用于数据存储,那么每个 Pod 都会尝试写入相同的卷。

你实际上希望每个 Pod 都有自己的 PVC,Kubernetes 通过 spec 中的 volumeClaimTemplates 字段为 StatefulSet 提供了这一点。卷声明模板可以包括存储类以及容量和访问模式要求。当你使用卷声明模板部署 StatefulSet 时,它会为每个 Pod 创建一个 PVC,并且它们是链接的,所以如果 Pod 0 被替换,新的 Pod 0 将连接到之前 Pod 0 使用的 PVC。

列表 8.4 显示了一个简单的 sleep 规范,它使用了卷声明模板。正如我们在第五章中学到的,不同的 Kubernetes 平台提供不同的存储类,我无法确定你的集群提供什么。这个规范省略了存储类,这意味着卷将使用你的集群的默认存储类动态配置。

列表 8.4 sleep-with-pvc.yaml,一个带有卷声明模板的 StatefulSet

spec:
  selector:     
    # pod selector...
  serviceName:  
    # headless service name...
  replicas: 2
  template:     
    # pod template...

  volumeClaimTemplates:      
    - metadata:
        name: data          # The name to use for volume mounts in the Pod
      spec:                 # This is a standard PVC spec.
        accessModes: 
          - ReadWriteOnce
        resources:
          requests:
            storage: 5Mi

我们将使用这个练习来查看在简单环境中 StatefulSet 中的卷声明模板是如何工作的,然后再将其作为数据库集群的存储层添加。

现在尝试一下 从列表 8.4 中部署 StatefulSet,并探索它创建的 PVC。

# deploy the StatefulSet with volume claim templates:
kubectl apply -f sleep/sleep-with-pvc.yaml

# check that the PVCs are created:
kubectl get pvc

# write some data to the PVC mount in Pod 0:
kubectl exec sleep-with-pvc-0 -- sh -c 'echo Pod 0 > /data/pod.txt'

# confirm Pod 0 can read the data:
kubectl exec sleep-with-pvc-0 -- cat /data/pod.txt

# confirm Pod 1 can’t--this will fail:
kubectl exec sleep-with-pvc-1 -- cat /data/pod.txt

你会看到集合中的每个 Pod 都会动态创建一个 PVC,这反过来又使用默认存储类(或者如果我在规范中包含了一个,则是请求的存储类)创建一个 PersistentVolume。所有 PVC 都有相同的配置,并且它们使用与 StatefulSet 中的 Pod 相同的稳定方法:它们有一个可预测的名称,正如你在图 8.8 中看到的,每个 Pod 都有自己的 PVC,这为副本提供了独立的存储。

图 8-8

图 8.8 卷声明模板在 StatefulSet 中动态为 Pods 创建存储。

当 Pod 被替换时,Pod 和其 PVC 之间的链接被保留,这正是 StatefulSet 能够运行数据密集型应用程序的真正原因。当你推出应用程序的更新时,新的 Pod 0 将连接到之前 Pod 0 的 PVC,新的应用程序容器将能够访问与被替换的应用程序容器完全相同的状态。

现在尝试一下 通过移除 Pod 0 触发 Pod 替换。它将被另一个 Pod 0 替换,该 Pod 0 将连接到相同的 PVC。

# delete the Pod:
kubectl delete pod sleep-with-pvc-0

# check that the replacement gets created:
kubectl get pods -l app=sleep-with-pvc

# check that the new Pod 0 can see the old data:
kubectl exec sleep-with-pvc-0 -- cat /data/pod.txt

这个简单的例子使这一点变得清晰——你可以在图 8.9 中看到新的 Pod 0 可以访问原始 Pod 的所有数据。在生产集群中,你会指定一个使用任何节点都可以访问的卷类型的存储类,这样替换的 Pod 就可以在任何节点上运行,应用程序容器仍然可以挂载 PVC。

图 8-9

图 8.9 StatefulSet 的稳定性延伸到保留 Pod 替换之间的 PVC 链接。

卷声明模板是我们需要添加到 Postgres 部署中的最后一部分,以模拟一个完全可靠的数据库。StatefulSets 旨在为您的应用程序提供一个稳定的环境,因此它们在更新方面不如其他控制器灵活——您不能更新现有的 StatefulSet 并做出基本更改,例如添加卷声明。您需要确保您的设计满足 StatefulSet 的应用程序要求,因为在重大变化期间很难维护服务水平。

现在试试看 我们将更新 Postgres 部署,但首先我们需要删除现有的 StatefulSet。

# apply the update with volume claim templates--this will fail:
kubectl apply -f todo-list/db/replicated/update/todo-db-pvc.yaml 

# delete the existing set:
kubectl delete statefulset todo-db

# create a new one with volume claims:
kubectl apply -f todo-list/db/replicated/update/todo-db-pvc.yaml

# check the volume claims:
kubectl get pvc -l app=todo-db  

当你运行这个练习时,你应该清楚地看到 StatefulSet 如何保持顺序,并在启动下一个 Pod 之前等待每个 Pod 运行。从我的输出中,你可以看到 PVCs 也是按顺序为每个 Pod 创建的,如图 8.10 所示。

图 8-10

图 8.10 PVCs 被创建并分配给 Postgres Pods。

感觉我们已经在 StatefulSets 上花费了很长时间,但这是一个你应该很好地理解的课题,这样当有人要求你将他们的数据库迁移到 Kubernetes(他们会的)时,你不会感到惊讶。StatefulSets 附带了很多复杂性,你大部分时间都不会使用它们。但如果你正在寻找将现有应用程序迁移到 Kubernetes,StatefulSets 可能是能够在同一平台上运行所有内容或必须保留几个 VM 来运行一个或两个应用程序之间的区别。

我们将用一项练习来结束本节,以展示我们集群数据库的力量。Postgres 从主副本复制所有数据,并且可以被客户端用于只读访问。如果我们的事务列表应用出现了严重的生产问题,导致它丢失数据,我们可以选择切换到只读模式并使用副本来调查问题。这样可以在最小功能的情况下安全地运行应用程序,这绝对比将其关闭要好。

现在试试看 运行待办事项 Web 应用并输入一些条目。在默认配置中,它连接到 StatefulSet 的 Pod 0 中的 Postgres 主副本。然后我们将切换应用程序配置以将其置于只读模式。这使得它连接到 Pod 1 中的只读 Postgres 备用副本,该副本已复制了 Pod 0 的所有数据。

# deploy the web app:
kubectl apply -f todo-list/web/

# get the URL for the app:
kubectl get svc todo-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8081/new'

# browse and add a new item

# switch to read-only mode, using the database secondary:
kubectl apply -f todo-list/web/update/todo-web-readonly.yaml

# refresh the app--the /new page is read-only;
# browse to /list and you'll see your original data

# check that there are no clients using the primary in Pod 0:
kubectl exec -it todo-db-0 -- sh -c "psql -U postgres
 -t -c 'SELECT datname, query FROM pg_stat_activity WHERE datid > 0'"

# check that the web app really is using the secondary in Pod 1:
kubectl exec -it todo-db-1 -- sh -c "psql -U postgres
 -t -c 'SELECT datname, query FROM pg_stat_activity WHERE datid > 0'"

您可以在图 8.11 中看到我的输出,其中包含一些小截图,展示了应用程序在只读模式下运行,但仍能访问所有数据。

图 8-11

图 8.11 如果存在数据问题,将应用程序切换到只读模式是一个有用的选项

Postgres 自 1996 年以来一直作为 SQL 数据库引擎存在——它比 Kubernetes 早了近 25 年。使用 StatefulSet,你可以创建一个适合 Postgres 和其他类似集群应用的应用环境,提供稳定的网络、存储和初始化,在容器动态世界中。

8.4 使用作业和 CronJobs 运行维护任务

数据密集型应用需要与计算对齐的复制数据存储,并且通常还需要对存储层进行一些独立的管理。数据备份和校验非常适合另一种类型的 Pod 控制器:作业。Kubernetes 作业通过 Pod 规范定义,并以批处理作业的方式运行 Pod,确保其运行至完成。

作业不仅仅适用于有状态的应用;它们是解决任何批处理问题的标准方法,在这些情况下,你可以将所有调度、监控和重试逻辑交给集群。你可以在 Pod 中运行任何容器镜像作为作业,但应该启动一个结束进程;否则,你的作业将永远运行。列表 8.5 展示了运行 Pi 应用程序的批处理模式的作业规范。

列表 8.5 pi-job.yaml,一个简单的计算π的作业

apiVersion: batch/v1
kind: Job                           # Job is the object type.
metadata:
  name: pi-job
spec:
  template:
    spec:                           # The standard Pod spec
      containers:
        - name: pi                  # The container should run and exit.
          image: kiamol/ch05-pi     
          command: ["dotnet", "Pi.Web.dll", "-m", "console", "-dp", "50"]
      restartPolicy: Never          # If the container fails, replace the Pod.

作业模板包含一个标准的 Pod 规范,并添加了一个必需的restartPolicy字段。该字段控制作业在失败时的行为。你可以选择在运行失败时让 Kubernetes 使用新容器重启相同的 Pod,或者始终创建一个替换 Pod,可能是在不同的节点上。在作业的正常运行中,如果 Pod 成功完成,作业和 Pod 将被保留,以便容器日志可用。

现在尝试运行 Pi 作业,并检查 Pod 的输出。

# deploy the Job:
kubectl apply -f pi/pi-job.yaml

# check the logs for the Pod:
kubectl logs -l job-name=pi-job

# check the status of the Job:
kubectl get job pi-job

作业会给它们创建的 Pod 添加自己的标签。job-name标签始终被添加,这样你就可以从作业导航到 Pod。我在图 8.12 中的输出显示,作业已经成功完成一次,计算结果可在日志中找到。

图片

图 8.12 作业创建 Pod,确保它们完成,然后将其留在集群中。

拥有不同的计算π的选项总是很有用,但这只是一个简单的例子。你可以在 Pod 规范中使用任何容器镜像,因此你可以使用作业运行任何类型的批处理过程。你可能有一组需要执行相同工作的输入项;你可以为整个集合创建一个作业,它为每个项目创建一个 Pod,Kubernetes 将工作分布在整个集群中。作业规范通过以下两个可选字段支持这一点:

  • completions——指定作业应运行多少次。如果你的作业正在处理工作队列,那么应用容器需要理解如何获取下一个要处理的项目。作业本身只确保运行与所需完成次数相等的 Pod 数量。

  • parallelism——指定对于具有多个完成设置的作业,并行运行多少个 Pod。此设置允许您调整作业的运行速度,同时平衡集群的计算需求。

本章最后一个 Pi 示例:一个新作业规格,它并行运行多个 Pod,每个 Pod 计算到随机位数的π。此规格使用初始化容器生成要使用的位数,应用程序容器使用共享EmptyDir挂载读取该输入。这是一个很好的方法,因为应用程序容器不需要修改就可以在并行环境中工作。您可以使用初始化容器从队列中获取工作项,这样应用程序本身就不需要知道队列的存在。

现在试试看:运行一个使用并行处理并展示来自同一规格的多个 Pod 可以处理不同工作负载的替代 Pi 作业。

# deploy the new Job:
kubectl apply -f pi/pi-job-random.yaml

# check the Pod status:
kubectl get pods -l job-name=pi-job-random

# check the Job status:
kubectl get job pi-job-random

# list the Pod output:
kubectl logs -l job-name=pi-job-random

这个练习可能需要一段时间才能运行,具体取决于您的硬件和生成的位数数量。您将看到所有 Pod 并行运行,各自进行计算。最终输出将是三组π,可能精确到数千位。我已经将我的结果简化,如图 8.13 所示。

图片

图 8.13 作业可以运行来自同一规格的多个 Pod,每个 Pod 处理不同的工作负载。

作业是您口袋里的一大法宝。它们非常适合任何计算密集型或 I/O 密集型任务,您希望确保一个进程完成,但不介意何时完成。您甚至可以从自己的应用程序提交作业——运行在 Kubernetes 中的 Web 应用程序可以访问 Kubernetes API 服务器,并创建作业来为用户运行工作。

作业的真正力量在于它们在集群的上下文中运行,因此它们可以使用集群的所有资源。回到 Postgres 示例,我们可以在作业中运行数据库备份过程,并且运行的 Pod 可以根据需要访问 StatefulSet 中的 Pod 或 PVC。这解决了这些数据密集型应用程序的培育方面,但这些作业需要定期运行,这就是 CronJob 的作用。CronJob 是一个作业控制器,它按固定的时间表创建作业。图 8.14 显示了工作流程。

图片

图 8.14 CronJobs 是作业 Pod 的最终所有者,因此可以通过级联删除删除所有内容。

CronJob 规格包括作业规格,因此您可以在 CronJob 中执行任何在作业中可以执行的操作,包括并行运行多个完成。作业运行的计划使用 Linux Cron 格式,允许您表达从简单的“每分钟”或“每天”计划到更复杂的“每周日早上 4 点和 6 点”等复杂计划。列表 8.6 显示了运行数据库备份的 CronJob 规格的一部分。

列表 8.6 todo-db-backup-cronjob.yaml,数据库备份的 CronJob

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: todo-db-backup
spec:
  schedule: "*/2 * * * *"          # Creates a Job every 2 minutes
  concurrencyPolicy: Forbid        # Prevents overlap so a new Job won’t be
  jobTemplate:                     # created if the previous one is running
    spec:
      # job template...

完整规范使用 Postgres Docker 镜像,并包含一个运行 pg_dump 备份工具的命令。Pod 从与 StatefulSet 使用相同的 ConfigMaps 和 Secrets 加载环境变量和密码,因此在配置文件中没有重复。它还使用自己的 PVC 作为写入备份文件的存储位置。

现在尝试一下:根据列表 8.6 中的规范创建一个 CronJob,每两分钟运行一次数据库备份作业。

# deploy the CronJob and target PVC for backup files:
kubectl apply -f todo-list/db/backup/

# wait for the Job to run--this is a good time to make tea:
sleep 150

# check the CronJob status:
kubectl get cronjob todo-db-backup

# now run a sleep Pod that mounts the backup PVC:
kubectl apply -f sleep/sleep-with-db-backup-mount.yaml

# check if the CronJob Pod created the backup:
kubectl exec deploy/sleep -- ls -l /backup

CronJob 设置为每两分钟运行一次,所以在这次练习中你需要给它一些时间来启动。按照计划,CronJob 创建一个作业,该作业创建一个 Pod,并运行 backup 命令。作业确保 Pod 成功完成。你可以通过在另一个 Pod 中挂载相同的 PVC 来确认备份文件已写入。你可以在图 8.15 中看到所有操作都正确无误。

图 8.15 CronJobs 运行 Pods,这些 Pods 可以访问其他 Kubernetes 对象。这个示例连接到一个数据库 Pod。

CronJobs 不会自动清理 Pods 和 Jobs。生存时间 (TTL) 控制器负责这项工作,但它是一个 alpha 级别的功能,在许多 Kubernetes 平台上不可用。如果没有它,当你确定不再需要它们时,你需要手动删除子对象。你还可以将 CronJobs 移动到挂起状态,这意味着对象规范仍然存在于集群中,但直到 CronJob 再次激活,它不会运行。

现在尝试一下:挂起 CronJob,使其不再创建备份作业,然后探索 CronJob 及其作业的状态。

# update the CronJob, and set it to suspend:
kubectl apply -f todo-list/db/backup/update/todo-db-backup-cronjob-suspend.yaml

# check the CronJob:
kubectl get cronjob todo-db-backup

# find the Jobs owned by the CronJob:
kubectl get jobs -o jsonpath="{.items[?(@.metadata.ownerReferences[0]
.name=='todo-db-backup')].metadata.name}"

如果你探索对象层次结构,你会看到 CronJobs 不遵循标准的控制器模型,没有使用标签选择器来识别它拥有的作业。你可以在 CronJob 的作业模板中添加自己的标签,但如果你不这样做,你需要识别所有所有者引用为 CronJob 的作业,如图 8.16 所示。

图 8.16 CronJobs 不使用标签选择器来建模所有权,因为它们不跟踪作业。

当你开始更多地使用 Jobs 和 CronJobs 时,你会发现规范的简单性掩盖了过程中的某些复杂性,并呈现了一些有趣的故障模式。Kubernetes 尽力确保你的批处理作业在你想要的时候启动并运行到完成,这意味着你的容器需要具有弹性。完成作业可能意味着重启一个带有新容器的 Pod 或者在新的节点上替换 Pod,而对于 CronJobs,如果过程持续时间超过计划间隔,则可能运行多个 Pod。你的容器逻辑需要允许所有这些场景。

现在你已经知道如何在 Kubernetes 中运行数据密集型应用了,使用 StatefulSets 来建模稳定的运行时环境并初始化应用,以及使用 CronJobs 来处理数据备份和其他定期维护工作。我们将思考这是否真的是一个好主意来结束本章。

8.5 选择状态化应用的平台

Kubernetes 的伟大承诺是它为你提供了一个单一的平台,可以在任何基础设施上运行所有应用程序。想象一下,你可以在一小块 YAML 中模拟任何应用程序的所有方面,使用一些 kubectl 命令部署它,并知道它将在任何集群中以相同的方式运行,利用平台提供的所有扩展功能,这非常吸引人。但是数据是宝贵的,通常不可替代,所以在你决定 Kubernetes 是运行数据密集型应用程序的地方之前,你需要仔细思考。

图 8.17 显示了我们在本章中构建的完整设置,以在 Kubernetes 中运行几乎生产级别的 SQL 数据库。只需看看所有移动部件——你真的想管理所有这些吗?而且你需要投入多少时间来测试这个设置与你的数据大小:验证副本是否正确同步,验证备份可以恢复,运行混沌实验以确保失败以你期望的方式处理?

图 8.17

图 8.17 哎呀!而且这是一个简化版本,没有显示卷或初始化容器。

将其与云中的托管数据库进行比较。Azure、AWS 和 GCP 都为 Postgres、MySQL 和 SQL Server 提供托管服务,以及他们自己的定制云规模数据库。云服务提供商负责扩展和高可用性,包括备份到云存储和更高级的选项,如威胁检测。另一种架构只是使用 Kubernetes 进行计算,并连接到托管云服务以处理数据和通信。

哪个选项更好?嗯,我白天是一名顾问,我知道唯一真正的答案是:“这取决于。”如果你在云中运行,那么我认为你需要一个非常充分的理由在生产中使用托管服务,因为数据至关重要。在非生产环境中,通常在 Kubernetes 中运行等效服务是有意义的,这样你就可以在开发和测试环境中以较低的成本和易于部署的方式运行容器化的数据库和消息队列,并在生产中切换到托管版本。Kubernetes 通过所有 Pod 和服务配置选项使这种切换变得非常简单。

在数据中心,情况略有不同。如果你已经在自己的基础设施上运行 Kubernetes,那么你承担了大量的管理工作,这可能意味着最大化集群的利用率并将它们用于一切。如果你选择这样做,Kubernetes 为你提供了将数据密集型应用程序迁移到集群并使用所需可用性和扩展级别的工具。只是不要低估达到这一目标复杂性。

我们现在已经完成了 StatefulSets 和 Jobs,所以在进入实验室之前我们可以进行清理。

现在试试看 所有顶层对象都被标记了,所以我们可以通过级联删除移除所有内容。

# delete all the owning objects:
kubectl delete all -l kiamol=ch08

# delete the PVCs
kubectl delete pvc -l kiamol=ch08

8.6 实验室

那么,你还有多少午餐时间可以用来做这个实验?你能从头开始构建一个 MySQL 数据库,包括备份吗?可能不行,但别担心——这个实验并没有那么复杂。目标只是让你获得一些使用 StatefulSets 和 PVCs 的工作经验,所以我们将会使用一个更简单的应用程序。你将在 StatefulSet 中运行 Nginx 网络服务器,其中每个 Pod 将日志文件写入自己的 PVC,然后你将运行一个作业来打印每个 Pod 日志文件的大小。基本组件已经为你准备好了,所以这主要是应用章节中的一些技术。

  • 起始点是 ch08/lab/nginx 中的 Nginx 规范,它运行一个 Pod 将日志写入一个 EmptyDir 卷。

  • Pod 规范需要迁移到 StatefulSet 定义,该定义配置为运行三个 Pod,并为每个 Pod 提供单独的存储。

  • 当你的 StatefulSet 运行正常时,你应该能够调用你的 Service 并看到 Pod 中正在写入的日志文件。

  • 然后,你可以在文件 disk-calc-job.yaml 中完成作业规范,添加卷挂载,以便它可以从 Nginx Pods 中读取日志文件。

实际情况并没有看起来那么糟糕,这会让你开始思考存储和作业。我的解决方案在 GitHub 上,你可以像往常一样在以下位置查看:github.com/sixeyed/kiamol/blob/master/ch08/lab/README.md

9 使用滚动更新和回滚管理应用发布

你将更频繁地更新现有应用,而不是部署新的东西。容器化应用从它们使用的基镜像中继承了多个发布周期;Docker Hub 上的操作系统、平台 SDK 和运行时官方镜像通常每月都有一个新版本。你应该有一个流程来重建你的镜像并在依赖项更新时发布更新,因为这些依赖项可能包含关键的安全补丁。这个过程的关键是能够安全地推出更新,并在更新出错时提供暂停和回滚更新的选项。Kubernetes 为 Deployments、DaemonSets 和 StatefulSets 提供了这些场景的解决方案。

单一更新方法并不适用于所有类型的应用,因此 Kubernetes 为控制器提供了不同的更新策略,并提供了调整策略工作方式的选项。我们将在本章中探讨所有这些选项。如果你因为对 6000 字关于应用更新的内容不感兴趣而考虑跳过这一部分,我建议你坚持下来。更新是导致应用停机时间最长的原因,但如果你理解 Kubernetes 提供的工具,你可以显著降低风险。而且,我会尽力在这个过程中加入一些兴奋的元素。

9.1 Kubernetes 如何管理滚动更新

我们将从部署(Deployments)开始——实际上,你已经进行了很多部署更新。每次我们对现有的部署进行更改(我们每章都会做 10 次这样的操作),Kubernetes 都会通过一个滚动更新来实现。在滚动更新中,部署会创建一个新的副本集(ReplicaSet),并将其扩展到所需的副本数量,同时将之前的副本集缩小到零副本。图 9.1 显示了正在进行的更新。

图片

图 9.1 部署控制多个副本集,以便它们可以管理滚动更新。

滚动更新不是由对部署的每一次更改触发的,而只是由对 Pod 规范的更改触发的。如果你做出的是部署可以用当前副本集管理的更改,比如更新副本数量,那么这个更改将不会通过滚动更新来完成。

现在尝试一下:部署一个具有两个副本的简单应用,然后更新它以增加规模,看看副本集是如何被管理的。

# change to the exercise directory:
cd ch09

# deploy a simple web app:
kubectl apply -f vweb/

# check the ReplicaSets:
kubectl get rs -l app=vweb

# now increase the scale:
kubectl apply -f vweb/update/vweb-v1-scale.yaml

# check the ReplicaSets:
kubectl get rs -l app=vweb

# check the deployment history:
kubectl rollout history deploy/vweb

kubectl rollout命令有选项可以查看和管理滚动更新。你可以从图 9.2 中的我的输出中看到,在这个练习中只有一个滚动更新,那就是创建副本集的初始部署。规模更新只更改了现有的副本集,所以没有第二次滚动更新。

图片

图 9.2 部署通过滚动更新管理更改,但仅当 Pod 规范更改时。

您的应用程序持续更新将集中在部署运行更新版本容器镜像的新 Pods 上。您应该通过更新 YAML 规范来管理这一点,但 kubectl 提供了使用set命令的快速替代方案。使用此命令是更新现有 Deployment 的强制方式,您应该将其视为与scale命令相同——这是一个有用的技巧,可以帮助您摆脱困境,但需要随后更新 YAML 文件。

现在尝试一下:使用 kubectl 更新 Deployment 的镜像版本。这是一个对 Pod 规范的更改,因此将触发一个新的滚动发布。

# update the image for the web app:
kubectl set image deployment/vweb web=kiamol/ch09-vweb:v2

# check the ReplicaSets again:
kubectl get rs -l app=vweb

# check the rollouts:
kubectl rollout history deploy/vweb

kubectl 的set命令更改现有对象的规范。您可以使用它来更改 Pod 的镜像或环境变量或服务的选择器。它是应用新 YAML 规范的快捷方式,但它是以相同的方式实现的。在这个练习中,更改导致了一个滚动,创建了一个新的 ReplicaSet 来运行新的 Pod 规范,而旧的 ReplicaSet 被缩放到零。您可以在图 9.3 中看到这一点。

图 9.3

图 9.3 强制更新会经过相同的滚动过程,但现在您的 YAML 已经不同步。

Kubernetes 使用与其他 Pod 控制器(如 DaemonSets 和 StatefulSets)相同的滚动概念。它们是 API 的一个奇怪部分,因为它们不直接映射到对象(您不使用“rollout”类型创建资源),但它们是与您的发布一起工作的重要管理工具。您可以使用滚动来跟踪发布历史并回滚到以前的发布。

9.2 使用滚动和回滚更新 Deployment

如果您再次查看图 9.3,您会看到滚动历史记录非常不实用。每个滚动都记录了一个修订号,但没有其他信息。不清楚是什么导致了更改或哪个 ReplicaSet 与哪个修订号相关。为 Pod 添加版本号(或 Git 提交 ID)作为标签是个好主意,然后 Deployment 也会将此标签添加到 ReplicaSet 中,这使得跟踪更新更容易。

现在尝试一下:应用对 Deployment 的更新,它使用相同的 Docker 镜像,但更改了 Pod 的版本标签。这是一个对 Pod 规范的更改,因此将创建一个新的滚动发布。

# apply the change using the record flag:
kubectl apply -f vweb/update/vweb-v11.yaml --record

# check the ReplicaSets and their labels:
kubectl get rs -l app=vweb --show-labels

# check the current rollout status:
kubectl rollout status deploy/vweb

# check the rollout history:
kubectl rollout history deploy/vweb

# show the rollout revision for the ReplicaSets:
kubectl get rs -l app=vweb -o=custom-columns=NAME:.metadata.name,
REPLICAS:.status.replicas,REVISION:.metadata.annotations.deployment\.kubernetes\.io/revision

我的输出出现在图 9.4 中。添加record标志将 kubectl 命令作为细节保存到滚动中,如果您的 YAML 文件有标识名称,这可能很有帮助。通常它们不会有,因为您将部署整个文件夹,所以 Pod 规范中的版本号标签是一个有用的补充。然而,然后您需要一些尴尬的 JSONPath 来找到滚动修订版和 ReplicaSet 之间的链接。

图 9.4

图 9.4 Kubernetes 使用标签来存储关键信息,额外的详细信息存储在注释中。

随着你的 Kubernetes 成熟度提高,你将希望有一个标准的标签集,你将包括在所有对象规范中。标签和选择器是核心功能,你将经常使用它们来查找和管理对象。应用程序名称、组件名称和版本是良好的起始标签,但区分你为方便而包含的标签和 Kubernetes 用于映射对象关系的标签很重要。

列表 9.1 显示了在之前练习中的 Pod 标签和 Deployment 的选择器。app标签用于选择器,Deployment 使用它来找到其 Pod。Pod 还包含一个version标签以方便我们使用,但它不是选择器的一部分。如果它是,那么 Deployment 就会与一个版本相关联,因为你一旦创建了 Deployment 就不能更改选择器。

列表 9.1 vweb-v11.yaml,一个在 Pod 规范中包含额外标签的 Deployment

spec:
  replicas: 3
  selector:
    matchLabels:
      app: vweb           # The app name is used as the selector.
  template:
    metadata:
      labels:
        app: vweb
        version: v1.1     # The Pod spec also includes a version label.

你需要提前仔细规划选择器,但你应该将你需要的所有标签添加到 Pod 规范中,以便使更新可管理。Deployments 保留多个 ReplicaSets(默认为 10 个),名称中的 Pod 模板哈希值使得它们在经过几次更新后仍然难以直接操作。让我们看看我们部署的应用实际上做了什么,然后看看另一个 rollout 中的 ReplicaSets。

现在试试看。向 web 服务发起 HTTP 调用以查看响应,然后开始另一个更新并再次检查响应。

# we’ll use the app URL a lot, so save it to a local file:
kubectl get svc vweb -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8090/v.txt' > url.txt

# then use the contents of the file to make an HTTP request:
curl $(cat url.txt)

# deploy the v2 update:
kubectl apply -f vweb/update/vweb-v2.yaml --record

# check the response again:
curl $(cat url.txt)

# check the ReplicaSet details:
kubectl get rs -l app=vweb --show-labels

在这个练习中,你会发现 ReplicaSets 不是容易管理的对象,这就是标准化标签发挥作用的地方。通过检查具有所有所需副本的 ReplicaSet 的标签,你可以轻松地看到哪个版本的 app 是活动的——如图 9.5 所示——但标签只是文本字段,所以你需要处理保障来确保它们的可靠性。

图 9-5

图 9.5 Kubernetes 为你管理 rollouts,但如果你添加标签以便了解情况,那就更有帮助了。

Rollouts 确实有助于抽象出 ReplicaSets 的细节,但它们的主要用途是管理发布。我们已经从 kubectl 中看到了 rollout 历史,你也可以运行命令来暂停正在进行的 rollout 或将部署回滚到早期版本。一个简单的命令会回滚到上一个部署,但如果你想要回滚到特定版本,你需要使用一些 JSONPath 技巧来找到你想要的修订版本。我们现在将展示这一点,并使用 kubectl 的一个非常实用的功能,它会在实际执行命令之前告诉你命令执行的结果。

现在试试看。检查 rollout 历史并尝试将应用回滚到 v1 版本。

# look at the revisions:
kubectl rollout history deploy/vweb

# list ReplicaSets with their revisions:
kubectl get rs -l app=vweb -o=custom-columns=NAME:.metadata.name,
    REPLICAS:.status.replicas,VERSION:.metadata.labels.version,
    REVISION:.metadata.annotations.deployment\.kubernetes\.io/revision

# see what would happen with a rollback:
kubectl rollout undo deploy/vweb --dry-run

# then start a rollback to revision 2:
kubectl rollout undo deploy/vweb --to-revision=2

# check the app--this should surprise you:
curl $(cat url.txt)

如果你在看到图 9.6 中显示的最终输出时感到困惑,请举手(这是本章的精彩部分)。我的手举起来了,我早就知道会发生什么。这就是为什么你需要一个一致的发布流程,最好是完全自动化的,因为一旦你开始混合方法,你就会得到令人困惑的结果。我回滚到了修订版 2,根据 ReplicaSets 上的标签,这应该会回滚到应用程序的 v1 版本。但修订版 2 实际上是来自第 9.1 节中的kubectl set image练习,所以容器镜像版本是 v2,但 ReplicaSet 标签是 v1。

图 9.6

图 9.6 标签是关键管理功能,但它们是由人类设置的,因此是易出错的。

你会发现发布流程的移动部分相当简单:部署创建和重用 ReplicaSets,根据需要对其进行扩展和缩减,而 ReplicaSets 的更改则记录为滚动更新。Kubernetes 让你能够控制滚动策略中的关键因素,但在我们继续之前,我们将看看也涉及配置更改的发布,因为这增加了另一个复杂因素。

在第四章中,我谈到了更新 ConfigMaps 和 Secrets 内容的不同方法,你做出的选择会影响你干净回滚的能力。第一种方法是说配置是可变的,因此发布可能包括 ConfigMap 更改,这是对现有 ConfigMap 对象的更新。但如果你发布的是配置更改,那么你将没有滚动更新的记录,也没有回滚的选项。

现在试试这个:移除现有的 Deployment,以便我们有干净的历史记录,然后部署一个使用 ConfigMap 的新版本,看看当你更新相同的 ConfigMap 时会发生什么。

# remove the existing app:
kubectl delete deploy vweb

# deploy a new version that stores content in config:
kubectl apply -f vweb/update/vweb-v3-with-configMap.yaml --record

# check the response:
curl $(cat url.txt)

# update the ConfigMap, and wait for the change to propagate:
kubectl apply -f vweb/update/vweb-configMap-v31.yaml --record
sleep 120

# check the app again:
curl $(cat url.txt)

# check the rollout history:
kubectl rollout history deploy/vweb

正如你在图 9.7 中看到的,ConfigMap 的更新改变了应用程序的行为,但这不是对 Deployment 的更改,因此如果配置更改导致问题,就没有可回滚的修订版。

图 9.7

图 9.7 配置更新可能会改变应用程序的行为,但不会记录滚动更新。

这是热重载方法,如果你的应用程序支持它,这种方法工作得很好,因为仅配置更改不需要滚动更新。现有的 Pods 和容器继续运行,因此没有服务中断的风险。代价是失去了回滚选项,你必须决定这是否比热重载更重要。

你的另一种选择是将所有 ConfigMaps 和 Secrets 视为不可变的,因此在对象名称中包含一些版本控制方案,一旦创建配置对象就不再更新。相反,你创建一个新的配置对象并使用新名称发布,同时与更新 Deployment 一起发布,该 Deployment 引用新的配置对象。

现在试试这个:部署一个具有不可变配置的应用程序新版本,这样你可以比较发布流程。

# remove the old Deployment:
kubectl delete deploy vweb

# create a new Deployment using an immutable config:
kubectl apply -f vweb/update/vweb-v4-with-configMap.yaml --record

# check the output:
curl $(cat url.txt)

# release a new ConfigMap and updated Deployment:
kubectl apply -f vweb/update/vweb-v41-with-configMap.yaml --record

# check the output again:
curl $(cat url.txt)

# the update is a full rollout:
kubectl rollout history deploy/vweb

# so you can rollback:
kubectl rollout undo deploy/vweb
curl $(cat url.txt)

图 9.8 显示了我的输出,其中配置更新伴随着部署更新,这保留了滚动发布的历史并启用了回滚。

图 9.8 不可变配置保留了滚动发布历史,但这意味着每次配置更改都需要进行滚动发布。

Kubernetes 实际上并不关心你采取哪种方法,你的选择将部分取决于你组织中谁拥有配置。如果项目团队也拥有部署和配置,那么你可能更喜欢可变配置对象以简化发布过程和管理对象的数量。如果有一个独立的团队负责配置,那么不可变的方法会更好,因为他们可以在发布之前部署新的配置对象。你的应用程序规模也会影响这个决定:在高规模下,你可能更喜欢减少应用程序部署的数量并依赖于可变配置。

这个决策会产生文化影响,因为它决定了人们如何看待应用程序的发布——是日常事件,没什么大不了的,还是稍微有些可怕的事情,应尽可能避免。在容器世界中,发布应该是简单的事件,一旦需要,你就可以以最少的仪式进行。测试和调整你的发布策略将大大增强你的信心。

9.3 配置 Deployments 的滚动更新

部署支持两种更新策略:RollingUpdate 是默认的,也是我们迄今为止使用的策略,另一种是 Recreate。你知道滚动更新的工作原理——通过缩小旧的 ReplicaSet 同时扩大新的 ReplicaSet,这提供了服务连续性和在更长时期内分阶段更新能力。重构策略则不提供这些。它仍然使用 ReplicaSets 来实现更改,但在扩大替代 ReplicaSet 之前,它会将前一个集合缩小到零。列表 9.2 显示了在部署规范中使用重构策略。这只是一个设置,但它有重大影响。

列表 9.2 vweb-recreate-v2.yaml,使用重构更新策略的部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vweb
spec:
  replicas: 3
  strategy:                       # This is the update strategy.
    type: Recreate                # Recreate is the alternative to the
                                  # default strategy, RollingUpdate.
  # selector & Pod spec follow

当你部署它时,你会看到它只是一个普通的带有 Deployment、ReplicaSet 和一些 Pod 的应用程序。如果你查看 Deployment 的详细信息,你会看到它使用重构更新策略,但只有在部署更新时才会有影响。

现在尝试一下 部署列表 9.2 中的应用程序,并探索对象。这就像一个普通的 Deployment。

# delete the existing app:
kubectl delete deploy vweb

# deploy with the Recreate strategy:
kubectl apply -f vweb-strategies/vweb-recreate-v2.yaml

# check the ReplicaSets:
kubectl get rs -l app=vweb

# test the app:
curl $(cat url.txt)

# look at the details of the Deployment:
kubectl describe deploy vweb

如图 9.9 所示,这是一个相同的老式 Web 应用程序的新部署,使用容器镜像的版本 2。有三个 Pod,它们都在运行,应用程序按预期工作——到目前为止一切顺利。

图 9.9 重构更新策略在发布更新之前不会影响行为。

然而,这种配置是危险的,你应该只在你的应用程序的不同版本无法共存时使用——例如数据库模式更新,你需要确保只有应用程序的一个版本连接到数据库。即使在那种情况下,你也有更好的选择,但如果你的场景确实需要这种方法,那么在上线之前确保测试所有更新会更好。如果你部署了一个新的 Pod 失败的更新,你直到所有的旧 Pod 都被终止了才会知道,你的应用程序将完全不可用。

现在尝试一下,Web 应用程序的版本 3 已经准备好部署。它已经损坏,正如你将在应用程序离线时看到的那样,因为没有任何 Pod 在运行。

# deploy the updated Pod spec:
kubectl apply -f vweb-strategies/vweb-recreate-v3.yaml

# check the status, with a time limit for updates:
kubectl rollout status deploy/vweb --timeout=2s

# check the ReplicaSets:
kubectl get rs -l app=vweb

# check the Pods:
kubectl get pods -l app=vweb

# test the app-this will fail:
curl $(cat url.txt)

你会在这个练习中看到,Kubernetes 会愉快地将你的应用程序离线,因为这就是你请求的。重新创建策略使用更新的 Pod 模板创建一个新的 ReplicaSet,然后将之前的 ReplicaSet 缩小到零,并将新的 ReplicaSet 扩展到三个。新的镜像已损坏,所以新的 Pod 失败,没有东西可以响应请求,正如你在图 9.10 中看到的那样。

图片 9-10

图 9.10 重新创建策略如果新的 Pod 规范损坏,会愉快地关闭你的应用程序。

现在你已经看到了,你可能应该尝试忘记重新创建策略。在某些情况下,它可能看起来很有吸引力,但当你这样做时,你仍然应该考虑其他选项,即使这意味着再次审视你的架构。你应用程序的大规模关闭将导致停机时间,而且可能比你计划的停机时间更长。

滚动更新是默认的,因为它们可以防止停机时间,但即使在这种情况下,默认行为也非常激进。对于生产发布,你可能需要调整一些设置,以设置发布的速度和监控方式。作为滚动更新规范的一部分,你可以添加选项来控制新的 ReplicaSet 的扩展速度和旧的 ReplicaSet 的缩减速度,使用以下两个值:

  • maxUnavailable 是缩小旧 ReplicaSet 的加速器。它定义了在更新期间相对于期望的 Pod 数量,可以有多少个 Pod 不可用。你可以将其视为在旧 ReplicaSet 中终止 Pod 的批量大小。在一个包含 10 个 Pod 的 Deployment 中,将此设置为 30% 意味着将有三个 Pod 立即终止。

  • maxSurge 是扩展新 ReplicaSet 的加速器。它定义了在期望的副本数量之上可以存在多少额外的 Pod,就像在新的 ReplicaSet 中创建 Pod 的批量大小。在一个包含 10 个 Pod 的 Deployment 中,将此设置为 40% 将创建四个新的 Pod。

简单明了,但是这两个设置在滚动发布过程中都会使用,因此会产生一种摇摆效应。新的 ReplicaSet 会扩展,直到 Pod 数量达到所需的副本数量加上 maxSurge 值,然后 Deployment 会等待旧 Pods 被移除。旧的 ReplicaSet 会缩小到所需的数量减去 maxUnavailable 数量,然后 Deployment 会等待新 Pods 达到就绪状态。您不能将这两个值都设置为 0,因为这意味着没有任何变化。图 9.11 展示了如何组合这些设置,以优先选择创建后删除、删除后创建或删除并创建的新发布方法。

图 9.11

图 9.11 展示了使用不同的发布选项进行中的部署更新。

如果您在集群中有额外的计算能力,您可以调整这些设置以加快发布速度。您还可以在您的缩放设置之上创建额外的 Pods,但如果新版本有问题,这会更危险。较慢的发布更保守:它使用的计算资源更少,给您更多机会发现任何问题,但会减少发布期间应用程序的整体容量。让我们先看看这些设置如何,首先通过保守的发布方式修复我们损坏的应用程序。

现在尝试一下,将镜像回滚到工作版本 2,在滚动更新策略中使用 maxSurge=1maxUnavailable=0

# update the Deployment to use rolling updates and the v2 image:
kubectl apply -f vweb-strategies/vweb-rollingUpdate-v2.yaml

# check the Pods for the app:
kubectl get po -l app=vweb

# check the rollout status:
kubectl rollout status deploy/vweb

# check the ReplicaSets:
kubectl get rs -l app=vweb

# test the app:
curl $(cat url.txt)

在这个练习中,新的部署规范将 Pod 镜像版本改回 2,并且也将更新策略更改为滚动更新。您可以在图 9.12 中看到,策略更改首先进行,然后按照新的策略进行 Pod 更新,每次创建一个新 Pod。

图 9.12

图 9.12 部署更新将使用与新的规范匹配的现有 ReplicaSet。

您需要快速工作才能在前一个练习中看到发布过程,因为这个简单的应用程序启动很快,一旦一个新 Pod 运行,发布就会继续,另一个新 Pod 开始运行。您可以通过 Deployment 规范中的以下两个字段来控制发布的速度:

  • minReadySeconds 添加了一个延迟,让 Deployment 等待以确保新 Pods 稳定。它指定了 Pod 在没有容器崩溃的情况下应该运行多少秒才被认为是成功的。默认值为零,这就是为什么在滚动发布期间新 Pods 会快速创建。

  • progressDeadlineSeconds 指定了 Deployment 更新在被视为失败之前可以运行的时间。默认值为 600 秒,所以如果一个更新在 10 分钟内没有完成,它会被标记为没有进展。

监控发布所需的时间听起来很有用,但截至 Kubernetes 1.19,超过截止时间实际上并不会影响发布——它只是在 Deployment 上设置一个标志。Kubernetes 没有自动回滚失败发布的特性,但当这个特性出现时,它将由这个标志触发。等待并检查 Pod 的失败容器是一个相当直接的工具,但比完全没有检查要好,您应该考虑在所有 Deployment 中指定 minReadySeconds

这些安全措施对于添加到您的部署中很有用,但它们实际上对我们的 Web 应用并没有太大的帮助,因为新的 Pod 总是会失败。我们可以通过滚动更新使这个部署安全并保持应用在线。下一个版本 3 的更新将 maxUnavailablemaxSurge 都设置为 1。这样做与默认值(每个 25%)的效果相同,但在规范中使用确切值更清晰,并且在小规模部署中,Pod 数量比百分比更容易处理。

现在试试部署版本 3 的更新再次。它仍然会失败,但通过使用滚动更新策略,它不会使应用离线。

# update to the failing container image:
kubectl apply -f vweb-strategies/vweb-rollingUpdate-v3.yaml

# check the Pods:
kubectl get po -l app=vweb

# check the rollout:
kubectl rollout status deploy/vweb

# see the scale in the ReplicaSets:
kubectl get rs -l app=vweb

# test the app:
curl $(cat url.txt)

当您运行这个练习时,您会看到更新永远不会完成,Deployment 会卡在两个具有两个所需 Pod 数量的 ReplicaSet 上,如图 9.13 所示。旧的 ReplicaSet 不会进一步缩放,因为 Deployment 已经将 maxUnavailable 设置为 1;它已经缩放了 1,并且没有新的 Pods 准备就绪以继续发布。新的 ReplicaSet 不会再缩放,因为 maxSurge 设置为 1,并且部署的 Pod 总数已经达到。

图片

图 9.13 失败的更新不会自动回滚或暂停;它们只是继续尝试。

如果您过几分钟再检查新的 Pods,您会看到它们处于 CrashLoopBackoff 状态。Kubernetes 通过创建替换容器来不断重启失败的 Pods,但在每次重启之间添加一个暂停,这样就不会使节点的 CPU 过载。这个暂停就是回退时间,它会呈指数增长——第一次重启为 10 秒,然后是 20 秒,然后是 40 秒,最多可达 5 分钟。这些版本 3 的 Pods 永远不会成功重启,但 Kubernetes 会继续尝试。

Deployments 是您最常用的控制器,花时间研究更新策略和定时设置以确保您理解对您的应用的影响是值得的。DaemonSets 和 StatefulSets 也具有滚动更新功能,由于它们管理 Pods 的方式不同,因此它们在发布方面也有不同的方法。

9.4 在 DaemonSets 和 StatefulSets 中的滚动更新

DaemonSet 和 StatefulSet 有两种更新策略可供选择。默认是滚动更新,我们将在本节中探讨。另一种是 OnDelete,用于需要严格控制每个 Pod 更新时间的情况。你部署更新,控制器监视 Pod,但它不会终止任何现有的 Pod。它等待其他进程删除它们,然后使用新规范中的 Pod 替换它们。

当你考虑这些控制器的用例时,这并不像听起来那么毫无意义。你可能有一个 StatefulSet,其中每个 Pod 在删除之前都需要将数据刷新到磁盘上,你可以有一个自动化的过程来完成这个任务。你可能有一个 DaemonSet,其中每个 Pod 都需要从硬件组件断开连接,以便下一个 Pod 可以使用。这些是罕见的情况,但 OnDelete 策略让你可以控制 Pod 何时被删除,同时仍然让 Kubernetes 自动创建替换。

在本节中,我们将重点关注滚动更新,为此我们将部署待办事项列表应用的一个版本,该版本在 StatefulSet 中运行数据库,在 Deployment 中运行 Web 应用,并在 DaemonSet 中运行 Web 应用的反向代理。

现在尝试一下 待办事项应用运行在六个 Pod 上,所以首先清除现有的应用以腾出空间。然后部署应用,并测试它是否正确运行。

# remove all of this chapter’s apps:
kubectl delete all -l kiamol=ch09

# deploy the to-do app, database, and proxy:
kubectl apply -f todo-list/db/ -f todo-list/web/ -f todo-list/proxy/

# get the app URL:
kubectl get svc todo-proxy -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8091'

# browse to the app, add an item, and check that it’s in the list

这只是为了设置更新。现在你应该有一个可以添加项目并查看列表的工作应用。我的输出显示在图 9.14 中。

图 9.14

图 9.14 使用各种控制器运行待办事项应用

第一次更新是针对 DaemonSet 的,我们将推出 Nginx 代理镜像的新版本。DaemonSet 在集群的所有(或某些)节点上运行单个 Pod,并且在使用滚动更新时,没有激增选项。在更新期间,节点永远不会同时运行两个 Pod,因此这始终是先删除后删除的策略。你可以添加maxUnavailable设置来控制并行更新的节点数量,但如果你关闭多个 Pod,你将运行在降低的容量下,直到替换的 Pod 就绪。

我们将使用maxUnavailable设置为 1 和minReadySeconds设置为 90 来更新代理。在单节点实验室集群中,延迟不会有任何影响——只有一个 Pod 在一个节点上需要替换。在更大的集群中,这意味着一次替换一个 Pod,并在 Pod 证明其稳定后等待 90 秒,然后再进行下一个。

现在尝试一下 开始 DaemonSet 的滚动更新。在单节点集群中,在替换 Pod 启动期间将发生短暂的停机。

# deploy the DaemonSet update:
kubectl apply -f todo-list/proxy/update/nginx-rollingUpdate.yaml

# watch the Pod update:
kubectl get po -l app=todo-proxy --watch

# Press ctrl-c when the update completes

kubectl 中的 watch 标志用于监控更改——它持续查看一个对象,并在状态改变时打印更新行。在这个练习中,你会看到在创建新 Pod 之前终止了旧 Pod,这意味着在新的 Pod 启动期间应用程序会有停机时间。图 9.15 显示我在发布过程中有一个秒的停机时间。

图 9.15:在创建替换 Pod 之前删除现有 Pod 以更新 DaemonSets。

多节点集群不会有任何停机时间,因为 Service 只将流量发送到已准备好的 Pods,并且每次只更新一个 Pod,所以其他 Pods 总是可用的。不过,你的容量会减少,如果你通过更高的 maxUnavailable 设置加快滚动更新,这意味着随着更多 Pods 并行更新,容量减少得更多。这是你为 DaemonSets 唯一可用的设置,所以这是一个简单的选择:手动通过删除 Pods 控制更新,或者让 Kubernetes 通过并行指定数量的 Pods 来滚动更新。

StatefulSets 更有趣,尽管它们只有一个选项来配置滚动更新。Pods 由 StatefulSet 按顺序管理,这也适用于更新——滚动更新从集合中的最后一个 Pod 向前进行到第一个。这对于 Pod 0 是主节点的集群应用程序特别有用,因为它首先在辅助节点上验证更新。

StatefulSets 没有设置 maxSurgemaxUnavailable。更新总是逐个 Pod 进行。你的配置选项是使用 partition 设置来定义总共应该更新多少个 Pods。此设置定义了滚动更新停止的截止点,这对于执行有阶段性的状态应用程序的滚动更新很有用。如果你在集合中有五个副本,并且你的规范包括 partition=3,那么只有 Pod 4 和 Pod 3 将被更新;Pod 0、1 和 2 将继续运行之前的规范。

现在试试这个:将分区更新部署到 StatefulSet 中的数据库镜像,更新在 Pod 1 后停止,这样 Pod 0 就不会更新。

# deploy the update:
kubectl apply -f todo-list/db/update/todo-db-rollingUpdate-partition.yaml

# check the rollout status:
kubectl rollout status statefulset/todo-db

# list the Pods, showing the image name and start time:
kubectl get pods -l app=todo-db -o=custom-columns=NAME:.metadata.name,
IMAGE:.spec.containers[0].image,START_TIME:.status.startTime

# switch the web app to read-only mode, so it uses the secondary
# database:
kubectl apply -f todo-list/web/update/todo-web-readonly.yaml

# test the app--the data is there, but now it’s read-only

这个练习是一个分区更新,它滚动推出 Postgres 容器镜像的新版本,但只更新到辅助 Pods,在这个例子中是一个 Pod,如图 9.16 所示。当你以只读模式使用应用程序时,你会看到它连接到更新的辅助节点,该节点仍然包含来自上一个 Pod 的复制数据。

图 9.16:对 StatefulSets 进行分区更新,允许更新辅助节点并保持主节点不变。

即使该集中的 Pod 运行的是不同的规范,这次滚动更新也已完成。对于在 StatefulSet 中的数据密集型应用程序,你可能有一套验证作业需要在每个更新的 Pod 上运行,你才会满意地继续滚动更新,分区更新允许你这样做。你可以通过运行具有递减分区值的连续更新来手动控制发布的节奏,直到在最终的更新中完全移除分区,以完成集合。

现在尝试一下 部署更新到数据库主节点。这个规范与之前的练习相同,但去除了分区设置。

# apply the update:
kubectl apply -f todo-list/db/update/todo-db-rollingUpdate.yaml

# check its progress:
kubectl rollout status statefulset/todo-db

# Pods should now all have the same spec:
kubectl get pods -l app=todo-db -o=custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,START
_TIME:.status.startTime

# reset the web app back to read-write mode:
kubectl apply -f todo-list/web/todo-web.yaml

# test that the app works and is connected to the updated primary Pod

你可以在图 9.17 中看到我的输出,其中完整更新已完成,主节点正在使用与辅助节点相同的更新版 Postgres。如果你之前进行过复制数据库的更新,你会知道这已经非常简单了——当然,除非你使用的是托管数据库服务。

图 9-17

图 9.17 完成有状态集的滚动更新,更新未分区

滚动更新是 Deployments、DaemonSets 和 StatefulSets 的默认设置,并且它们都以大致相同的方式工作:逐渐用运行新规范的新 Pod 替换运行旧规范的老 Pod。实际的细节不同,因为控制器以不同的方式工作,有不同的目标,但它们对你的应用程序提出了相同的要求:当多个版本同时运行时,应用程序需要正确工作。这并不总是可能的,并且有几种不同的方法可以在 Kubernetes 中部署应用程序更新。

9.5 理解发布策略

以 Web 应用程序为例。滚动更新很棒,因为它允许每个 Pod 在处理完所有客户端请求后优雅地关闭,并且滚动更新可以快或保守,取决于你的喜好。滚动更新的实际方面很简单,但你也要考虑用户体验(UX)方面。

应用程序更新可能会改变 UX——不同的设计、新功能或更新的工作流程。如果用户在滚动更新期间看到新版本,然后刷新并发现自己回到了旧版本,因为请求是由运行应用程序不同版本的 Pod 服务的,那么这样的变化对用户来说会很奇怪。

处理这些问题的策略超出了你控制器中的滚动更新规范。你可以在你的 Web 应用程序中设置 cookie,将客户端与特定的 UX 关联起来,然后使用更高级的流量路由系统来确保用户始终看到新版本。当我们第十五章介绍这一点时,你会看到它引入了更多的动态部分。对于这种方法过于复杂或无法解决同时运行多个版本的问题的情况,你可以通过蓝绿部署自行管理发布。

蓝绿部署是一个简单的概念:你同时部署了应用程序的旧版本和新版本,但只有一个版本是活跃的。你可以切换一个开关来选择哪个版本是活跃的。在 Kubernetes 中,你可以通过更新服务的标签选择器来实现,如图 9.18 所示,将流量发送到不同部署中的 Pods。

图 9.18 你在蓝绿部署中运行应用程序的多个版本,但只有一个版本是活跃的。

你需要在你的集群中拥有运行应用程序两个完整副本的能力。如果它是 Web 或 API 组件,那么新版本应该使用最少的内存和 CPU,因为它没有收到任何流量。你通过更新服务的标签选择器在版本之间切换,因此更新几乎是瞬时的,因为所有 Pods 都在运行并准备好接收流量。你可以轻松地来回切换,因此你可以回滚有问题的发布版本,而无需等待 ReplicaSets 进行扩展和缩减。

蓝绿部署比滚动更新更复杂,但正因为如此,它们更简单。对于有大规模部署历史记录的组织来说,蓝绿部署可能更适合迁移到 Kubernetes,但它们是一种计算密集型方法,需要多个步骤,并且不会保留应用程序的部署历史。你应该将滚动更新作为首选的部署策略,但蓝绿部署在你获得信心时是一个很好的过渡步骤。

目前关于滚动更新的内容就到这里,但当我们讨论到生产准备、网络入口和监控等主题时,我们还会回到这些概念。在我们进入实验室之前,现在我们需要整理一下集群。

现在试试看 移除本章创建的所有对象。

kubectl delete all -l kiamol=ch09
kubectl delete cm -l kiamol=ch09
kubectl delete pvc -l kiamol=ch09

9.6 实验室

我们在上一节学习了蓝绿部署的理论,现在在实验室中,你将让它成为现实。完成这个实验室将帮助你清楚地了解选择器如何将 Pods 与其他对象相关联,并为你提供使用滚动更新的替代方案的经验。

  • 起始点是 Web 应用程序的版本 1,你可以从lab/v1文件夹中部署它。

  • 你需要为应用程序的版本 2 创建一个蓝绿部署。规范将与版本 1 规范类似,但使用:v2容器镜像。

  • 当你部署更新时,你应该能够通过更改服务来在版本 1 和版本 2 之间切换,而不需要对 Pods 进行任何更新。

这是在复制 YAML 文件并尝试确定你需要更改哪些字段时的良好实践。你可以在 GitHub 上找到我的解决方案:github.com/sixeyed/kiamol/blob/master/ch09/lab/README.md

10 使用 Helm 打包和管理应用

尽管 Kubernetes 功能强大,但它并不能自行解决所有问题;存在一个庞大的生态系统来填补这些空白。其中之一就是打包和分发应用,而 Helm 就是解决方案。您可以使用 Helm 将一组 Kubernetes YAML 文件组合成一个工件,并在公共或私有仓库中共享。任何有权访问仓库的人都可以使用单个 Helm 命令安装应用。该命令可能部署一系列相关的 Kubernetes 资源,包括 ConfigMaps、Deployments 和 Services,您可以在安装过程中自定义配置。

人们以不同的方式使用 Helm。有些团队仅使用 Helm 从公共仓库安装和管理第三方应用。其他团队则使用 Helm 为自己的应用打包和发布到私有仓库。在本章中,您将学习如何做这两件事,并带着自己对 Helm 如何适应您组织的想法离开。您不需要学习 Helm 就能有效地使用 Kubernetes,但它被广泛使用,因此您应该熟悉它。该项目由云原生计算基金会(CNCF)管理——与托管 Kubernetes 的同一基金会——这是一个可靠成熟度和长期性的指标。

10.1 Helm 为 Kubernetes 增加的功能

Kubernetes 应用在设计时以 YAML 文件的形式进行建模,在运行时使用标签集进行管理。Kubernetes 中没有“应用”的原生概念,这显然将一组相关资源分组在一起,这也是 Helm 解决的问题之一。它是一个与仓库服务器交互以查找和下载应用包的命令行工具,并且与您的 Kubernetes 集群一起安装和管理应用。

Helm 是另一层抽象,这次是在应用层面。当您使用 Helm 安装应用时,它会在您的 Kubernetes 集群中创建一组资源——它们是标准的 Kubernetes 资源。Helm 打包格式扩展了 Kubernetes YAML 文件,因此 Helm 包实际上是一组 Kubernetes 清单,附带一些元数据存储在一起。我们将首先使用 Helm 部署前几章中的一个示例应用,但在此之前,我们需要安装 Helm。

现在就试试吧!Helm 是一个跨平台工具,可在 Windows、macOS 和 Linux 上运行。您可以在以下链接中找到最新的安装说明:helm.sh/docs/intro/install。本练习假设您已经安装了包管理器,如 Homebrew 或 Chocolatey。如果没有,您需要参考 Helm 网站上的完整安装说明。

# on Windows, using Chocolatey:
choco install -y kubernetes-helm

# on Mac, using Homebrew:
brew install helm

# on Linux, using the Helm install script:
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

# check that Helm is working:
helm version

本练习中的安装步骤可能无法在您的系统上工作,在这种情况下,您需要在这里停止并前往 Helm 安装文档。在您安装 Helm 并看到 version 命令的成功输出,如第 10.1 图所示之前,我们无法继续前进。

图片

图 10.1 安装 Helm 有很多选项;使用软件包管理器是最简单的。

Helm 是一个客户端工具。Helm 的早期版本需要在你的 Kubernetes 集群中部署一个服务器组件,但在 Helm 3 的主要更新中,这一点已经改变。Helm CLI 使用与 kubectl 相同的连接信息来连接到你的 Kubernetes 集群,因此你不需要任何额外的配置来安装应用程序。然而,你需要配置一个软件包仓库。Helm 仓库类似于 Docker Hub 这样的容器镜像仓库,但服务器发布了一个所有可用包的索引;Helm 缓存了你仓库索引的本地副本,你可以使用它来搜索包。

现在试试吧 添加书籍的 Helm 仓库,同步它,并搜索一个应用程序。

# add a repository, using a local name to refer to a remote server:
helm repo add kiamol https://kiamol.net

# update the local repository cache:
helm repo update

# search for an app in the repo cache:
helm search repo vweb --versions

Kiamol 仓库是一个公共服务器,你可以在这个练习中看到有两个版本的名为 vweb 的包。我的输出显示在图 10.2 中。

图片 10-2

图 10.2 同步 Kiamol Helm 仓库的本地副本并搜索包

你已经对 Helm 有了一定的感觉,但现在需要一些理论知识,这样我们才能在继续前进之前使用正确的概念和它们的正确名称。在 Helm 中,一个应用程序包被称为 chart;chart 可以在本地开发和部署,或者发布到 repository。当你安装一个 chart 时,这被称为 release;每个 release 都有一个名称,你可以在你的集群中安装同一 chart 的多个实例,作为独立的、命名的 release。

Charts 包含 Kubernetes YAML 清单,清单通常包含参数化值,以便用户可以使用不同的配置设置安装相同的 chart——运行的副本数量或应用程序日志级别可以是参数值。每个 chart 也包含一组默认值,可以使用命令行进行检查。图 10.3 显示了 Helm chart 的文件结构。

图片 10-3

图 10.3 Helm chart 包含应用程序的所有 Kubernetes YAML,以及一些元数据。

vweb charts 包包含我们在第九章中用来演示更新和回滚的简单 Web 应用程序。每个 chart 包含一个 Service 和 Deployment 的规范,以及一些参数化值和默认设置。你可以在安装 chart 之前使用 Helm 命令行检查所有可用的值,然后在安装 release 时使用自定义值覆盖默认值。

现在试试吧 检查 vweb chart 的第 1 版中可用的值,然后使用自定义值安装一个 release。

# inspect the default values stored in the chart:
helm show values kiamol/vweb --version 1.0.0

# install the chart, overriding the default values:
helm install --set servicePort=8010 --set replicaCount=1 ch10-vweb kiamol/vweb --version 1.0.0

# check the releases you have installed:
helm ls

在这个练习中,你可以看到 chart 为 Service 端口和 Deployment 中的副本数量提供了默认值。我的输出显示在图 10.4 中。你使用 helm install 命令中的 set 参数来指定你自己的值,当安装完成后,你就可以在 Kubernetes 中运行一个应用程序,而无需使用 kubectl,也无需直接使用 YAML 清单。

图片 10-4

图 10.4 使用 Helm 安装应用程序——这会创建 Kubernetes 资源,而不使用 kubectl。

Helm 提供了一套用于处理仓库和图表以及安装、更新和回滚发布的功能,但它并不打算用于应用程序的持续管理。Helm 命令行不是 kubectl 的替代品——您需要一起使用它们。现在发布已安装,您可以像往常一样处理 Kubernetes 资源,如果您需要修改设置,也可以返回 Helm。

现在试试吧 检查 Helm 部署的资源,然后返回 Helm 以扩展 Deployment 并检查应用程序是否正常工作。

# show the details of the Deployment:
kubectl get deploy -l app.kubernetes.io/instance=ch10-vweb --show-labels

# update the release to increase the replica count:
helm upgrade --set servicePort=8010 --set replicaCount=3 ch10-vweb kiamol/vweb --version 1.0.0

# check the ReplicaSet:
kubectl get rs -l app.kubernetes.io/instance=ch10-vweb

# get the URL for the app:
kubectl get svc ch10-vweb -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8010'

# browse to the app URL

让我们看看那个练习中的几个要点。首先,标签比您迄今为止看到的标准的“应用程序”和“版本”标签要详细得多。这是因为这是一个公共仓库上的公共图表,所以我使用了 Kubernetes 配置最佳实践指南中推荐的标准标签名称——这是我的选择,而不是 Helm 的要求。其次,Helm 的 upgrade 命令再次指定了服务端口,尽管我只想修改副本计数。这是因为 Helm 使用默认值,除非您指定它们,所以如果端口没有包含在 upgrade 命令中,它将被更改为默认值。您可以在图 10.5 中看到我的输出。

图片

图 10.5 您不使用 Helm 来管理应用程序,但您可以使用它来更新配置。

这是 Helm 工作流程的消费端。您可以从 Helm 命令行搜索应用程序仓库,发现应用程序可用的配置值,然后安装和升级应用程序。它是为在 Kubernetes 中运行而构建的应用程序的包管理工具。在下一节中,您将学习如何打包和发布自己的应用程序,这是工作流程的生产端。

10.2 使用 Helm 打包自己的应用程序

Helm 图表是包含 Kubernetes 清单的文件夹或压缩存档。您可以通过将应用程序清单、识别您想要参数化的任何值,并用模板变量替换实际值来创建自己的图表。列表 10.1 显示了模板化的 Deployment 规范的开始,它使用 Helm 设置的资源名称和标签值。

列表 10.1  web-ping-deployment.yaml,一个模板化的 Kubernetes 清单

apiVersion: apps/v1
kind: Deployment                   # This much is standard Kubernetes YAML.

metadata:
  name: {{ .Release.Name }}              # Contains the name of the release
  labels:
    kiamol: {{ .Values.kiamolChapter }}  # Contains the “kiamolChapter”
                                         # value

双大括号语法用于模板化值——从开头的 {{ 到结尾的 }} 在安装时会被替换,Helm 将处理后的 YAML 发送到 Kubernetes。可以使用多个源作为输入来替换模板化值。列表 10.1 中的片段使用发布对象获取发布名称,并使用值对象获取名为 kiamolChapter 的参数值。发布对象由 installupgrade 命令提供的信息填充,值对象则从图表的默认值和用户覆盖的任何设置中填充。模板还可以访问有关图表的静态详细信息以及有关 Kubernetes 集群功能的运行时详细信息。

Helm 对图表中的文件结构非常讲究。你可以使用 helm create 命令来生成新图表的样板结构。顶级是一个文件夹,其名称必须与你要使用的图表名称匹配,并且该文件夹必须至少包含以下三项:

  • 一个 Chart.yaml 文件,用于指定图表元数据,包括名称和版本

  • 一个 values.yaml 文件,用于设置参数的默认值

  • 包含模板化 Kubernetes 清单的 templates 文件夹

列表 10.1 来自本章源文件中 web-ping/templates 文件夹下的 web-ping-deployment.yaml 文件。web-ping 文件夹包含创建有效图表所需的所有文件,Helm 可以验证图表内容,并从图表文件夹安装发布。

现在就试试 当你开发图表时,你不需要将它们打包成 zip 归档;你可以直接使用图表文件夹。

# switch to this chapter’s source:
cd ch10

# validate the chart contents:
helm lint web-ping

# install a release from the chart folder:
helm install wp1 web-ping/

# check the installed releases:
helm ls

lint 命令仅用于处理本地图表,但 install 命令对本地图表和存储在仓库中的图表都是相同的。本地图表可以是文件夹或压缩归档,你将在本练习中看到,从本地图表安装发布与从仓库安装具有相同的体验。我在图 10.6 中的输出显示,我现在安装了两个发布:一个是来自 vweb 图表的,另一个来自 web-ping 图表。

图 10.6

图 10.6 从本地文件夹安装和升级允许你快速迭代图表开发。

web-ping 应用程序是一个基本的实用程序,通过定期向域名发送 HTTP 请求来检查网站是否正常运行。目前,你有一个 Pod 正在运行,每 30 秒向我的博客发送请求。我的博客运行在 Kubernetes 上,所以我确信它能够处理这些请求。该应用程序使用环境变量来配置要使用的 URL 和调度间隔,这些变量在 Helm 的清单中进行了模板化。列表 10.2 显示了带有模板化变量的 Pod 规范。

列表 10.2 web-ping-deployment.yaml,模板化容器环境

spec:
  containers:
    - name: app
      image: kiamol/ch10-web-ping
      env:
        - name: TARGET
          value: {{ .Values.targetUrl }}
        - name: INTERVAL
          value: {{ .Values.pingIntervalMilliseconds | quote }}

Helm 提供了一套丰富的模板函数,您可以使用它们来操作在 YAML 中设置的值。列表 10.2 中的quote函数如果提供的值没有引号,则将其包裹在引号中。您可以在模板中包含循环和分支逻辑,计算字符串和数字,甚至查询 Kubernetes API 以从其他对象中获取详细信息。我们不会深入探讨这些细节,但重要的是要记住,Helm 让您生成复杂的模板,几乎可以完成任何事情。

您需要仔细思考需要模板化的规范部分。Helm 相对于标准清单部署的一个主要好处是您可以从单个图表运行同一应用程序的多个实例。您不能使用 kubectl 这样做,因为清单包含需要唯一的资源名称。如果您多次部署相同的 YAML 集合,Kubernetes 将仅更新相同的资源。如果您模板化规范中所有唯一的部分——如资源名称和标签选择器——则可以使用 Helm 运行同一应用程序的多个副本。

现在尝试一下:部署 web-ping 应用程序的第二个版本,使用相同的图表文件夹,但指定一个不同的 URL 进行 ping。

# check the available settings for the chart:
helm show values web-ping/

# install a new release named wp2, using a different target:
helm install --set targetUrl=kiamol.net wp2 web-ping/

# wait a minute or so for the pings to fire, then check the logs:
kubectl logs -l app=web-ping --tail 1

在这个练习中,您会看到我需要对我的博客进行一些优化——它大约需要 500 毫秒返回,而 Kiamol 网站只需要 100 毫秒。更重要的是,您可以看到应用程序的两个实例正在运行:两个 Deployment 管理着具有不同容器规范的两组 Pod。我的输出显示在图 10.7 中。

图片

图 10.7 您不能使用纯清单安装应用程序的多个实例,但可以使用 Helm。

现在应该很清楚,Helm 的安装和管理应用程序的工作流程与 kubectl 的工作流程不同,但您还需要理解这两者是不兼容的。您不能通过在图表的模板文件夹中运行kubectl apply来部署应用程序,因为模板变量不是有效的 YAML,该命令会失败。如果您采用 Helm,您需要在为每个环境使用 Helm,这可能会减慢开发人员的工作流程,或者使用纯 Kubernetes 清单进行开发并使用 Helm 处理其他环境之间做出选择,这意味着您将拥有多个 YAML 副本。

记住,Helm 不仅关乎安装,同样关乎分布和发现。Helm 带来的额外摩擦是能够将复杂的应用简化为几个变量并在存储库中共享的代价。存储库实际上只是一个索引文件,其中包含可以存储在任何网络服务器上的图表版本列表(Kiamol 存储库使用 GitHub 页面,您可以在kiamol.net/index.yaml中查看全部内容)。

您可以使用任何服务器技术来托管您的存储库,但在此节的其余部分,我们将使用一个名为 ChartMuseum 的专用存储库服务器,这是一个流行的开源选项。您可以在自己的组织中运行 ChartMuseum 作为私有 Helm 存储库,并且它很容易设置,因为您可以使用 Helm 图表来安装它。

现在尝试一下 ChartMuseum 图表位于官方 Helm 存储库中,通常称为“stable”。添加该存储库,您就可以在本地运行自己的存储库。

# add the official Helm repository:
helm repo add stable https://kubernetes-charts.storage.googleapis.com

# install ChartMuseum--the repo flag fetches details from

# the repository so you don’t need to update your local cache:
helm install --set service.type=LoadBalancer --set service.externalPort=8008
             --set env.open.DISABLE_API=false repo stable/chartmuseum --version 2.13.0 --wait

# get the URL for your local ChartMuseum app:
kubectl get svc repo-chartmuseum -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8008'

# add it as a repository called local:
helm repo add local $(kubectl get svc repo-chartmuseum -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8008')

现在,您已经在 Helm 中注册了三个存储库:Kiamol 存储库、稳定版 Kubernetes 存储库(这是一个精选的图表集,类似于 Docker Hub 中的官方镜像),以及您自己的本地存储库。您可以在图 10.8 中看到我的输出,它被缩减以减少 Helm install命令的输出。

图 10.8 运行自己的 Helm 存储库就像从 Helm 存储库安装一个图表一样简单。

图表在发布到存储库之前需要打包,发布通常是一个三阶段的过程:将图表打包成 zip 归档,将归档上传到服务器,并更新存储库索引以添加新的图表。ChartMuseum 为您处理最后一步,因此您只需打包和上传图表,存储库索引就会自动更新。

现在尝试一下 使用 Helm 为图表创建 zip 归档,并使用curl将其上传到您的 ChartMuseum 存储库。检查存储库——您会看到您的图表已被索引。

# package the local chart:
helm package web-ping

# *on Windows 10* remove the PowerShell alias to use the real curl:
Remove-Item Alias:curl -ErrorAction Ignore

# upload the chart zip archive to ChartMuseum:
curl --data-binary "@web-ping-0.1.0.tgz" $(kubectl get svc repo-chartmuseum
     -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8008/api/charts')

# check that ChartMuseum has updated its index:
curl $(kubectl get svc repo-chartmuseum -o jsonpath='http://{.status
.loadBalancer.ingress[0].*}:8008/index.yaml')

Helm 使用压缩归档来简化图表的分发,文件非常小——它们包含 Kubernetes 清单、元数据和值,但不包含任何大型二进制文件。图表中的 Pod 规范指定了要使用的容器镜像,但镜像本身不是图表的一部分——在安装发布时,它们从 Docker Hub 或您自己的镜像仓库中拉取。您可以在图 10.9 中看到,当您上传图表时,ChartMuseum 会生成存储库索引,并添加新的图表详细信息。

图 10.9 您可以将 ChartMuseum 作为私有存储库运行,以便在团队之间轻松共享图表。

您可以使用 ChartMuseum 或其他组织内的存储库服务器来共享内部应用程序,或者在将发布候选版本发布到公共存储库之前,将图表作为持续集成过程的一部分进行推送。您拥有的本地存储库仅在您的实验室环境中运行,但它使用负载均衡器服务进行发布,因此任何具有网络访问权限的人都可以从它安装 web-ping 应用程序。

现在尝试一下 安装 web-ping 应用程序的另一个版本,这次使用您本地存储库中的图表,并提供一个值文件而不是在install命令中指定每个设置。

# update your repository cache:
helm repo update

# verify that Helm can find your chart:
helm search repo web-ping

# check the local values file:
cat web-ping-values.yaml

# install from the repository using the values file:
helm install -f web-ping-values.yaml wp3 local/web-ping

# list all the Pods running the web-ping apps:
kubectl get pod -l app=web-ping -o custom-columns='NAME:.metadata.name,ENV:.spec.containers[0].env[*].value'

在这个练习中,你看到了另一种使用自定义设置安装 Helm 发布的方法——使用本地值文件。这是一个好习惯,因为你可以将不同环境的设置存储在不同的文件中,并降低在设置未提供时更新回默认值的危险。我的输出如图 10.10 所示。

图 10-10

图 10.10 从本地仓库安装图表与从任何远程仓库安装相同。

你在之前的练习中也看到了,你可以不指定版本从仓库安装一个图表。这不是一个好的做法,因为它会安装最新版本,这是一个移动的目标。最好总是明确地声明图表版本。Helm 要求你使用语义版本,这样图表消费者就知道他们即将升级的包是 beta 版本还是它有破坏性更改。

你可以用图表做比我在这里要介绍的内容多得多的工作。它们可以包括测试,这些是安装后运行的 Kubernetes 作业规范,用于验证部署;它们可以有钩子,这让你可以在安装工作流程的特定点运行作业;并且它们可以带有签名并附带来源签名。在下一节中,我将介绍你在编写模板时使用的一个更多功能,这是一个重要的功能——构建依赖于其他图表的图表。

10.3 在图表中建模依赖关系

Helm 让你设计你的应用程序,使其在不同的环境中都能工作,这为依赖关系提出了一个有趣的问题。在某些环境中可能需要依赖关系,而在其他环境中则不需要。也许你有一个真正需要缓存反向代理来提高性能的 Web 应用程序。在某些环境中,你可能希望与应用程序一起部署代理,而在其他环境中,你可能已经有一个共享代理,所以你只想部署 Web 应用程序本身。Helm 通过条件依赖支持这些情况。

列表 10.3 显示了自第五章以来我们一直在使用的 Pi Web 应用程序的图表规范。它有两个依赖项——一个来自 Kiamol 仓库,另一个来自本地文件系统——并且它们是独立的图表。

列表 10.3 chart.yaml,一个包含可选依赖项的图表

apiVersion: v2      # The version of the Helm spec
name: pi            # Chart name
version: 0.1.0      # Chart version
dependencies:       # Other charts this chart is dependent on
  - name: vweb
    version: 2.0.0
    repository: https://kiamol.net   # A dependency from a repository
    condition: vweb.enabled          # Installed only if required
  - name: proxy
    version: 0.1.0
    repository: file://../proxy      # A dependency from a local folder
    condition: proxy.enabled         # Installed only if required

当你建模依赖关系时,你需要保持你的图表灵活。父图表(在这个例子中是 Pi 应用程序)可能需要子图表(代理和 vweb 图表),但子图表本身需要是独立的。你应该在子图表中对 Kubernetes 规范进行模板化,使其具有通用性。如果它是仅在一个应用程序中有用的东西,那么它应该是该应用程序图表的一部分,而不是子图表。

我的代理是通用的;它只是一个缓存反向代理,可以使用任何 HTTP 服务器作为内容源。图表使用模板化值来指定要代理的服务器名称,因此尽管它主要用于 Pi 应用程序,但它也可以用来代理任何 Kubernetes 服务。我们可以通过安装一个代理现有集群中应用程序的发布来验证这一点。

现在尝试一下 安装代理图,单独使用它作为我们在本章早期安装的 vweb 应用程序的反向代理。

# install a release from the local chart folder:
helm install --set upstreamToProxy=ch10-vweb:8010 vweb-proxy proxy/

# get the URL for the new proxy service:
kubectl get svc vweb-proxy-proxy -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080'

# browse to the URL

在那个练习中的代理图完全独立于 Pi 应用程序;它被用来代理我从 Kiamol 仓库部署的 Helm 网络应用程序。您可以在图 10.11 中看到,它作为一个缓存代理为任何 HTTP 服务器工作。

图片

图 10.11 代理子图被构建成可以作为图表本身使用——它可以代理任何应用程序。

要将代理作为依赖项使用,您需要在父图表中的依赖项列表中添加它,使其成为一个子图。然后您可以在父图表中指定子图设置的值,通过在设置名称前加上依赖项名称——代理图中的 upstreamToProxy 设置在 Pi 图表中引用为 proxy.upstreamToProxy。列表 10.4 显示了 Pi 应用的默认值文件,其中包括应用程序本身的设置和代理依赖项的设置。

列表 10.4 values.yaml,Pi 图表的默认设置

replicaCount: 2               # Number of app Pods to run
serviceType: LoadBalancer     # Type of the Pi Service

proxy:                        # Settings for the reverse proxy  
  enabled: false              # Whether to deploy the proxy
  upstreamToProxy: "{{ .Release.Name }}-web"      # Server to proxy
  servicePort: 8030           # Port of the proxy Service  
  replicaCount: 2             # Number of proxy Pods to run

这些值部署应用程序本身而不使用代理,使用 LoadBalancer 服务为 Pi Pods 设置。proxy.enabled 设置被指定为 Pi 图表中代理依赖项的条件,因此除非安装设置覆盖默认值,否则整个子图将被跳过。完整的值文件还将 vweb.enabled 值设置为 false——那个依赖项仅用于演示子图可以从存储库中获取,因此默认情况下也不部署该图表。

这里有一个额外的细节需要指出。Pi 应用的服务名称在图表中是使用发布名称模板化的。这对于启用同一图表的多个安装很重要,但它增加了代理子图默认值的复杂性。要代理的服务器名称需要与 Pi 服务名称匹配,因此值文件使用与服务名称相同的模板化值,这将代理与同一发布中的服务链接起来。

在您安装或打包图表之前,图表需要其依赖项可用,您使用 Helm 命令行来完成此操作。构建依赖项将它们填充到图表的 charts 文件夹中,无论是从存储库下载存档还是将本地文件夹打包成存档。

现在尝试一下 构建 Pi 图表的依赖项,它下载远程图表,打包本地图表,并将它们添加到图表文件夹中。

# build dependencies:
helm dependency build pi

# check that the dependencies have been downloaded:
ls ./pi/charts

图 10.12 展示了为什么版本控制对于 Helm 图表来说如此重要。图表包使用图表元数据中的版本号进行版本控制。父图表与其依赖项一起打包,并指定版本。如果我没有更新代理图表的版本号,我的 Pi 图表将不同步,因为 Pi 包中代理图表的版本 0.1.0 与最新版本 0.1.0 不同。您应该将 Helm 图表视为不可变的,并且始终通过发布新包版本来发布更改。

图 10.12 Helm 将依赖项打包到父图表中,并作为一个包进行分发。

这种条件依赖的原则是您如何管理一个更复杂的应用程序,例如第八章中的待办事项应用。Postgres 数据库部署将是一个子图表,用户可以选择在需要使用外部数据库的环境中完全跳过。或者,您甚至可以有多个条件依赖,允许用户为开发环境部署简单的 Postgres 部署,在测试环境中使用高度可用的 StatefulSet,并在生产环境中连接到管理的 Postgres 服务。

Pi 应用程序比这简单,我们可以选择是否单独部署它或与代理一起部署。此图表使用模板值来设置 Pi 服务的类型,但可以在模板中通过将其设置为 LoadBalancer(如果未部署代理)或 ClusterIP(如果已部署代理)来计算它。

现在尝试一下 部署启用代理子图表的 Pi 应用程序。使用 Helm 的 dry-run 功能检查默认部署,然后使用自定义设置进行实际安装。

# print the YAML Helm would deploy with default values:
helm install pi1 ./pi --dry-run

# install with custom settings to add the proxy:
helm install --set serviceType=ClusterIP --set proxy.enabled=true pi2 ./pi

# get the URL for the proxied app:
kubectl get svc pi2-proxy -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8030'

# browse to it

您将在这次练习中看到 dry-run 标志非常有用:它将值应用到模板中,并输出它将安装的资源的所有 YAML,而不部署任何内容。然后在实际安装中,设置一些标志将部署一个与主图表集成的附加图表,因此应用程序作为一个单一单元运行。我的 Pi 计算结果出现在图 10.13 中。

图 10.13 通过覆盖默认设置安装带有可选子图表的图表

在本章中,我没有为 Helm 提供足够的空间,因为您需要深入了解 Helm 的复杂性,只有当您对 Helm 进行大量投资并计划广泛使用它时。如果您是这样的人,您会发现 Helm 具有强大的功能来支持您。以下是一个例子:您可以从 ConfigMap 模板的内容生成哈希,并将其用作 Deployment 模板中的标签,这样每次配置更改时,Deployment 标签也会更改,升级配置会触发 Pod 回滚。

这很酷,但并不是适合每个人,所以在下一节中,我们将回到一个简单的演示应用,看看 Helm 如何简化升级和回滚过程。

10.4 升级和回滚 Helm 发布

使用 Helm 升级应用程序并没有什么特别之处;它只是将更新的规范发送到 Kubernetes,Kubernetes 会以通常的方式推出更改。如果你想要配置推出的具体细节,你仍然需要在图表中的 YAML 文件中进行,使用我们在第九章中探讨的设置。Helm 带给升级的是对所有类型资源的一致方法以及轻松回滚到先前版本的能力。

Helm 还为你带来的一项其他优势是,通过在集群中部署额外的实例,你可以安全地尝试新版本。我开始本章时,在我的集群中部署了 vweb 应用程序的 1.0.0 版本,它仍在愉快地运行。现在 2.0.0 版本已经可用,但在升级正在运行的应用程序之前,我可以用 Helm 安装一个单独的发布版本来测试新功能。

现在尝试一下。检查原始的 vweb 发布版本是否仍然存在,然后安装一个版本 2 的发布版本,并指定设置以保持应用程序私有。

# list all releases:
helm ls -q

# check the values for the new chart version:
helm show values kiamol/vweb --version 2.0.0

# deploy a new release using an internal Service type:
helm install --set servicePort=8020 --set replicaCount=1
             --set serviceType=ClusterIP ch10-vweb-v2 kiamol/vweb --version 2.0.0

# use a port-forward so you can test the app:
kubectl port-forward svc/ch10-vweb-v2 8020:8020

# browse to localhost:8020, then exit the port-forward with Ctrl-C or
# Cmd-C

这个练习使用图表支持的参数安装应用程序,而不使其公开可用,使用 ClusterIP 服务类型和 port-forward,这样应用程序就只能对当前用户可访问。原始应用程序没有改变,我有机会在目标集群中对新的 Deployment 进行烟测试。图 10.14 显示了新版本正在运行。

图 10.14 部署服务的图表通常允许你设置类型,因此你可以保持它们私有。

现在,我很高兴 2.0.0 版本运行良好,我可以用 Helm 的 upgrade 命令升级我的实际发布版本。我想确保我使用与上一个发布中设置相同的值进行部署,并且 Helm 有功能可以显示当前值并在升级中重用自定义值。

现在尝试一下。删除临时的版本 2 发布版本,并将版本 1 发布版本升级到版本 2 图表,重用当前发布上设置的相同值。

# remove the test release:
helm uninstall ch10-vweb-v2

# check the values used in the current version 1 release:
helm get values ch10-vweb

# upgrade to version 2 using the same values--this will fail:
helm upgrade --reuse-values --atomic ch10-vweb kiamol/vweb --version 2.0.0

哎呀。这是一个特别棘手的问题,需要一些追踪才能理解。reuse-values 标志告诉 Helm 在新版本上重用为当前发布设置的 所有值,但 2.0.0 版本的图表还包括另一个值,即 Service 的类型,因为在当前发布中它不存在,所以没有设置。最终结果是 Service 类型为空,默认为 Kubernetes 中的 ClusterIP,更新失败是因为它与现有的 Service 规范冲突。你可以在图 10.15 的输出中看到这一点。

图 10.15 一个无效的升级失败,Helm 可以自动回滚到上一个版本。

这种类型的问题正是 Helm 抽象层真正有帮助的地方。你可以在标准的 kubectl 部署中遇到同样的问题,但如果某个资源更新失败,你需要检查所有其他资源并手动回滚它们。Helm 通过atomic标志自动执行此操作。它等待所有资源更新完成,如果其中任何一个失败,它将回滚其他所有资源到之前的状态。检查发布的历史,你可以看到 Helm 已自动回滚到版本 1.0.0。

现在试试回滚。回想一下第九章中提到的 Kubernetes 在滚动更新历史方面提供的信息不多——将这一点与 Helm 提供的信息进行比较。

# show the history of the vweb release:
helm history ch10-vweb

那个命令本身就是一个练习,因为你在标准的 Kubernetes 滚动更新历史中得不到这么多的信息。图 10.16 显示了发布的所有四个版本:第一次安装、一次成功的升级、一次失败的升级和自动回滚。

图片

图 10.16 发布历史清楚地将应用程序和图表版本链接到修订版。

为了修复失败的更新,我可以手动设置upgrade命令中的所有值,或者使用具有当前部署相同设置的值文件。我没有那个值文件,但我可以将get values命令的输出保存到文件中,并在升级时使用它,这样我就有了所有之前的设置,以及任何新设置的图表中的默认值。

现在试试升级到版本 2,这次将当前版本 1 的值保存到文件中,并在upgrade命令中使用它。

# save the values of the current release to a YAML file:
helm get values ch10-vweb -o yaml > vweb-values.yaml

# upgrade to version 2 using the values file and the atomic flag:
helm upgrade -f vweb-values.yaml --atomic ch10-vweb kiamol/vweb
--version 2.0.0

# check the Service and ReplicaSet configuration:
kubectl get svc,rs -l app.kubernetes.io/instance=ch10-vweb

这次升级成功,所以原子回滚没有启动。升级实际上是通过 Deployment 实现的,它以常规方式扩展了替换 ReplicaSet 并缩减了当前 ReplicaSet。图 10.17 显示了在之前版本中设置的配置值已保留,Service 正在 8010 端口监听,并且有三个 Pod 正在运行。

图片

图 10.17 通过将发布设置导出到文件并再次使用它们来实现升级成功。

剩下的就是尝试回滚,它在语法上与 kubectl 中的回滚类似,但 Helm 使跟踪你想要使用的修订版变得容易得多。你已经看到了图 10.16 中显示的有意义的发布历史,你也可以使用 Helm 来检查特定修订版设置的值。如果我想将 Web 应用程序回滚到版本 1.0.0,但保留我在修订版 2 中设置的值,我首先可以检查那些值。

现在试试回滚到第二个修订版,即应用程序的 1.0.0 版本升级到使用三个副本。

# confirm the values used in revision 2:
helm get values ch10-vweb --revision 2

# roll back to that revision:
helm rollback ch10-vweb 2

# check the latest two revisions:
helm history ch10-vweb --max 2 -o yaml

你可以在图 10.18 中看到我的输出,回滚成功,历史显示最新修订版是 6,实际上是一个回滚到修订版 2。

图片

图 10.18 Helm 使检查你正在回滚到的确切内容变得容易。

这个示例的简单性有助于集中精力在升级和回滚工作流程上,并突出一些怪癖,但它隐藏了 Helm 在重大升级中的强大功能。Helm 发布是一个应用程序的抽象,应用程序的不同版本可能以不同的方式建模。一个图表可能在早期版本中使用 ReplicationController,然后改为 ReplicaSet,然后是 Deployment;只要用户界面部分保持不变,内部工作就变成了实现细节。

10.5 理解 Helm 的适用位置

Helm 为 Kubernetes 增加了大量价值,但它具有侵略性——一旦您模板化您的清单,就无法回头。团队中的每个人都必须切换到 Helm,或者您必须承诺拥有多套清单:开发团队的纯 Kubernetes 和其他所有环境的 Helm。您真的不希望两套清单不同步,但同样,即使不添加 Helm,Kubernetes 本身也足够学习。

Helm 是否适合您在很大程度上取决于您打包的应用程序类型以及您团队的工作方式。如果您的应用程序由 50+ 个微服务组成,那么开发团队可能只处理整个应用程序的一个子集,以原生方式或使用 Docker 和 Docker Compose 运行,而另一个团队负责完整的 Kubernetes 部署。在这种情况下,转向 Helm 将减少摩擦而不是增加摩擦,将数百个 YAML 文件集中到可管理的图表中。

其他一些表明 Helm 是一个好的选择包括一个完全自动化的持续部署流程——这可以通过 Helm 更容易地构建——使用相同的图表版本和自定义值文件运行测试环境,以及在部署过程中运行验证作业。当您发现自己需要模板化 Kubernetes 清单——您迟早会这样做——Helm 给您提供了一个标准方法,这比编写和维护自己的工具要好。

本章关于 Helm 的内容就到这里,因此在进入实验室之前,需要整理集群。

现在试试看 本章中所有内容都是使用 Helm 部署的,因此我们可以使用 Helm 来卸载它们。

# uninstall all the releases:
helm uninstall $(helm ls -q)

10.6 实验室

又回到了实验室的待办事项应用。您将从一个工作集的 Kubernetes 清单中提取并打包成 Helm 图表。不用担心——这并不是第八章中包含 StatefulSets 和备份作业的完整应用程序;这是一个更简单的版本。以下是目标:

  • lab/todo-list 文件夹中的清单作为起点(YAML 中有关于需要模板化的提示)。

  • 创建 Helm 图表结构。

  • 模板化资源名称和其他需要模板化的值,以便应用程序可以作为多个发布运行。

  • 添加配置设置的参数以支持以不同环境运行应用程序。

  • 当使用默认值安装时,您的图表应运行为测试配置。

  • 当使用lab/dev-values.yaml值文件安装时,您的图表应运行为开发配置。

如果您打算使用 Helm,您真的应该为这个实验室腾出时间,因为它包含了您在 Helm 中打包应用程序时需要完成的精确任务集。我的解决方案在 GitHub 上,您可以在通常的位置检查:github.com/sixeyed/kiamol/blob/master/ch10/lab/README.md.

开心领导!

11 应用程序开发-开发者工作流程和 CI/CD

这是关于现实世界中 Kubernetes 的最后一章,重点是开发和交付在 Kubernetes 上运行的软件的实用性。无论你自认为是开发者还是你在运维方面与开发者合作,转向容器化都会影响你的工作方式、使用的工具,以及从代码更改到在开发和测试环境中看到运行所需的时间和精力。在本章中,我们将探讨 Kubernetes 如何影响内部循环——本地机器上的开发者工作流程——以及外部循环——将更改推送到测试和生产环境的 CI/CD 工作流程。

你在组织中使用 Kubernetes 的方式将与你在这本书中迄今为止使用的方式大不相同,因为你将使用共享资源,如集群和镜像注册库。在本章中,我们将探讨交付工作流程时,我们还将涵盖许多小细节,这些细节可能会在你向现实世界转变时让你感到困惑——比如使用私有注册库和在共享集群上保持隔离。本章的主要重点是帮助你理解在以 Docker 为中心的工作流程和类似于在 Kubernetes 上运行的 Platform-as-a-Service(PaaS)之间进行选择。

11.1 Docker 开发者工作流程

开发者热爱 Docker。它连续两年在 Stack Overflow 的年度调查中被选为最受欢迎的平台和第二受欢迎的平台。Docker 使开发者工作流程的一些部分变得极其简单,但代价是:Docker 工件成为项目的核心,这对内部循环有影响。你可以在本地环境中使用与生产环境相同的技术运行应用程序,但前提是你接受不同的工作方式。如果你不熟悉使用容器构建应用程序,电子书的附录 A 详细介绍了这一点;它是来自《Learn Docker in a Month of Lunches》(Manning,2020)的章节“从源代码打包应用程序到 Docker 镜像”。

在本节中,我们将逐步介绍开发工作流程,其中在每个环境中都使用 Docker 和 Kubernetes,并且开发者拥有自己的专用集群。如果你想跟随练习,需要确保 Docker 正在运行。如果你的实验室环境是 Docker Desktop 或 K3s,那么你已经准备好了。我们将首先关注开发者入职——加入新项目并尽可能快速地熟悉情况。

现在就试试吧!本章提供了一个全新的演示应用程序——一个简单的公告板,你可以在这里发布即将发生的事件的详细信息。它是用 Node.js 编写的,但你不需要安装 Node.js 就可以使用 Docker 工作流程启动运行。

# switch to this chapter’s folder in the source code:
cd ch11

# build the app:
docker-compose -f bulletin-board/docker-compose.yml build

# run the app:
docker-compose -f bulletin-board/docker-compose.yml up -d

# check the running  containers:
docker ps

# browse to the app at http://localhost:8010/

这只是作为开发者开始新项目的一种最简单的方式。你唯一需要安装的软件是 Docker,然后你获取代码的副本,就可以开始了。你可以看到图 11.1 中的我的输出。我没有在我的机器上安装 Node.js,而且你是否有 Node.js 以及它的版本无关紧要;你的结果将会相同。

图 11.1 使用 Docker 和 Compose 进行开发者入职非常简单——如果没有问题的话。

在魔法背后是两件事:一个 Dockerfile,其中包含构建和打包 Node.js 组件的所有步骤,以及一个 Docker Compose 文件,它指定了所有组件及其 Dockerfile 的路径。在这个应用程序中只有一个组件,但可能有十几个——使用不同的技术——工作流程将是相同的。但这种方式并不是我们在生产环境中运行应用程序的方式,因此如果我们想使用相同的技术堆栈,我们可以切换到在本地运行应用程序在 Kubernetes 中,只需使用 Docker 进行构建。

现在尝试一下 简单的 Kubernetes 清单,用于使用本地镜像运行应用程序,位于源文件夹中。删除应用程序的 Compose 版本,并将其部署到 Kubernetes 中。

# stop the app in Compose:
docker-compose -f bulletin-board/docker-compose.yml down

# deploy in Kubernetes:
kubectl apply -f bulletin-board/kubernetes/

# get the new URL:
kubectl get svc bulletin-board -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8011'

# browse

尽管我们现在有三个容器工件需要处理:Dockerfile、Compose 文件和 Kubernetes 清单,但此工作流程仍然相当简单。我拥有自己的 Kubernetes 集群,因此我可以以生产环境中的方式运行应用程序。图 11.2 中的输出显示,这是同一个应用程序,使用相同的本地镜像,在前一个练习中使用 Docker Compose 构建。

图 11.2 你可以使用 Compose 混合 Docker 和 Kubernetes,以在 Pods 中运行构建的镜像。

Kubernetes 愿意使用你使用 Docker 创建或拉取的本地镜像,但你必须遵循一些规则,关于它是否使用本地镜像或从仓库中拉取。如果镜像名称中没有显式的标签(并使用默认的 :latest 标签),那么 Kubernetes 将始终尝试首先拉取镜像。否则,如果节点上的镜像缓存中存在本地镜像,Kubernetes 将使用本地镜像。你可以通过指定镜像拉取策略来覆盖这些规则。列表 11.1 显示了公告板应用程序的 Pod 规范,其中包含一个显式的策略。

列表 11.1 bb-deployment.yaml,指定镜像拉取策略

spec:                         # This is the Pod spec within the Deployment.
  containers:
    - name: bulletin-board
      image: kiamol/ch11-bulletin-board:dev 
      imagePullPolicy: IfNotPresent   # Prefer the local image if it exists

这类细节可能会成为开发者工作流程中的一个大障碍。Pod 规范可能被配置为优先使用仓库镜像,然后你可以尽可能多地重建你自己的本地镜像,但永远不会看到任何变化,因为 Kubernetes 总是使用远程镜像。在镜像版本方面也存在类似的复杂性,因为可以使用具有相同名称和标签的另一个版本替换镜像。这并不符合 Kubernetes 所需状态的方法,因为如果你部署了一个更新,Pod 规范没有变化,即使镜像内容已经改变,也不会发生任何事情。

回到我们的演示应用程序。您在项目中的第一个任务是向事件列表添加更多细节,这对您来说是一个简单的代码更改。测试您的更改更具挑战性,因为您可以通过重复 Docker Compose 命令来重新构建镜像,但如果您重复 kubectl 命令来部署更改,您会发现没有任何事情发生。如果您对容器感兴趣,您可以做一些调查来了解问题并删除 Pod 以强制替换,但如果您不感兴趣,那么您的工作流程已经中断了。

现在试试看您实际上不需要进行代码更改——新文件中包含了更改。只需替换代码文件并重新构建镜像,然后删除 Pod 以查看在替换 Pod 中运行的新应用程序版本。

# remove the original code file:
rm bulletin-board/src/backend/events.js

# replace it with an updated version:
cp bulletin-board/src/backend/events-update.js bulletin-board/src/backend/events.js

# rebuild the image using Compose:
docker-compose -f bulletin-board/docker-compose.yml build

# try to redeploy using kubectl:
kubectl apply -f bulletin-board/kubernetes/

# delete the existing Pod to recreate it:
kubectl delete pod -l app=bulletin-board

您可以在图 11.3 中看到我的输出。更新的应用程序在屏幕截图上运行,但只有在 Pod 被手动删除并由 Deployment 控制器重新创建后,使用最新的镜像版本,才运行。

图片

图 11.3 Docker 镜像是可以更改的,但重命名镜像不会在 Kubernetes 中触发更新。

如果您选择以 Docker 为中心的工作流程,那么这只是开发团队将遇到并减缓、挫败感的工作流程中的复杂问题之一(调试和实时应用程序更新将是他们接下来会遇到的问题)。容器技术不是容易学习的话题,您需要投入一些专门的时间来理解其原理,并且并非每个团队中的每个人都会愿意进行这种投资。

另一种选择是将所有容器技术集中在一个团队中,该团队提供一个 CI/CD 管道,开发团队可以将其连接到以部署他们的应用程序。该管道负责打包容器镜像并将其部署到集群中,因此开发团队不需要将 Docker 和 Kubernetes 引入自己的工作。

11.2 Kubernetes-as-a-Service 开发工作流程

在 Kubernetes 之上运行的平台即服务(PaaS)体验对于许多组织来说是一个有吸引力的选择。您可以运行一个集群来处理所有测试环境,该集群还托管 CI/CD 服务以处理容器运行中的繁琐细节。所有 Docker 工件都从开发工作流程中移除,因此开发者可以直接在组件上工作,在他们的机器上运行 Node.js 和其他所有他们需要的软件,并且他们不使用本地容器。

此方法将容器移动到外层循环——当开发人员向源代码控制推送更改时,这会触发构建,创建容器镜像,将它们推送到注册表,并将新版本部署到集群中的测试环境中。您将获得在容器平台运行的所有好处,而无需承受容器给开发带来的摩擦。图 11.4 显示了使用一组技术选项时的样子。

图片

图 11.4 在外层循环中使用容器让开发者专注于代码。

这种方法的承诺是,您可以在不影响开发工作流程或不需要每个团队成员都掌握 Docker 和 Compose 技能的情况下在 Kubernetes 上运行您的应用程序。它可以在开发团队专注于小型组件而另一个团队将这些组件组装成工作系统的组织中很好地工作,因为只有组装团队需要容器技能。您还可以完全删除 Docker,如果您的集群使用不同的容器运行时,这很有用。但是,如果您想在没有 Docker 的情况下构建容器镜像,则需要用许多其他组件来替换它。您最终将拥有更多的复杂性,但这些复杂性将集中在交付管道而不是项目中。

我们将在本章中通过一个示例来展示这一点,但为了管理复杂性,我们将分阶段进行,首先从构建服务的内部视角开始。为了简化,我们将运行自己的 Git 服务器,这样我们就可以从我们的实验室集群中推送更改并触发构建。

现在尝试一下 Gogs 是一个简单但强大的 Git 服务器,它作为 Docker Hub 上的镜像发布。这是在您的组织中运行私有 Git 服务器或快速启动备份(如果您的在线服务离线)的好方法。在您的集群中运行 Gogs 以推送书籍源代码的本地副本。

# deploy the Git server:
kubectl apply -f infrastructure/gogs.yaml

# wait for it to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=gogs

# add your local Git server to the book’s repository--
# this grabs the URL from the Service to use as the target:
git remote add gogs $(kubectl get svc gogs -o jsonpath=
'http://{.status.loadBalancer.ingress[0].*}:3000/kiamol/kiamol.git')

# push the code to your server--authenticate with 
# username kiamol and password kiamol 
git push gogs

# find the server URL:
kubectl get svc gogs -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000'

# browse and sign in with the same kiamol credentials

图 11.5 显示了我的输出。您不需要运行自己的 Git 服务器来完成此工作流程;使用 GitHub 或任何其他源代码控制系统也可以以相同的方式工作,但这样做可以创建一个易于复制的环境——本章的 Gogs 设置已预配置了用户账户,因此您可以快速启动并运行。

图 11.5 使用 Gogs 在 Kubernetes 中运行自己的 Git 服务器非常简单。

现在我们有一个本地源代码服务器,我们可以将其与其他组件连接起来。接下来是一个可以构建容器镜像的系统。为了使其可移植,以便在任何集群上运行,我们需要一个不需要 Docker 的东西,因为集群可能使用不同的容器运行时。我们有几种选择,但其中之一是 BuildKit,这是 Docker 团队的一个开源项目。BuildKit 最初是作为 Docker Engine 内部镜像构建组件的替代品,它具有可插拔的架构,因此您可以使用或不需要 Dockerfile 来构建镜像。您可以将 BuildKit 作为服务器运行,这样工具链中的其他组件就可以使用它来构建镜像。

现在尝试一下 在集群内部运行 BuildKit 作为服务器,并确认它拥有构建容器镜像所需的所有工具。

# deploy BuildKit:
kubectl apply -f infrastructure/buildkitd.yaml

# wait for it to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=buildkitd

# verify that Git and BuildKit are available:
kubectl exec deploy/buildkitd -- sh -c 'git version && buildctl --version'

# check that Docker isn’t installed--this command will fail:
kubectl exec deploy/buildkitd -- sh -c 'docker version'

您可以在图 11.6 中看到我的输出,其中 BuildKit Pod 从一个安装了 BuildKit 和 Git 客户端但未安装 Docker 的镜像中运行。重要的是要认识到 BuildKit 是完全独立的——它不会连接到 Kubernetes 中的容器运行时来构建镜像;所有这些都将发生在 Pod 内部。

图 11.6 BuildKit 作为容器镜像运行——构建服务,无需 Docker

在我们可以看到完整的 PaaS 工作流程之前,我们还需要设置一些其他组件,但现在我们已经有了足够的组件来了解构建部分是如何工作的。我们在这里的目标是采用无 Docker 的方法,因此我们将忽略上一节中使用的 Dockerfile,并直接从源代码构建应用程序到容器镜像中。如何做到这一点?通过使用一个名为 Buildpacks 的 CNCF 项目,这是一个 Heroku 领先的技术,用于推动他们的 PaaS 产品。

Buildpacks 使用与多阶段 Dockerfile 相同的概念:在容器内运行构建工具来编译应用程序,然后在具有应用程序运行时的另一个容器镜像上打包编译后的应用程序。你可以使用一个名为 Pack 的工具来完成这项工作,你需要在应用程序的源代码上运行它。Pack 会确定你使用的语言,将其与 Buildpack 匹配,然后将你的应用程序打包成一个镜像——无需 Dockerfile。目前 Pack 只能在 Docker 上运行,但我们没有使用 Docker,因此我们可以使用一个替代方案来将 Buildpacks 与 BuildKit 集成。

现在试试看 我们将进入构建过程,手动运行一个我们将在本章后面自动化的构建。连接到 BuildKit Pod,从你的本地 Git 服务器拉取书籍的代码,并使用 Buildpacks 而不是 Dockerfile 来构建它。

# connect to a session on the BuildKit Pod:
kubectl exec -it deploy/buildkitd -- sh

# clone the source code from your Gogs server:
cd ~
git clone http://gogs:3000/kiamol/kiamol.git

# switch to the app directory:
cd kiamol/ch11/bulletin-board/

# build the app using BuildKit; the options tell BuildKit
# to use Buildpacks instead of a Dockerfile as input and to 
# produce an image as the output:
buildctl build --frontend=gateway.v0  --opt source=kiamol/buildkit-buildpacks
               --local context=src --output type=image,name=kiamol/ch11-bulletin-board:buildkit

# leave the session when the build completes
exit

这个练习需要一段时间才能运行,但请密切关注 BuildKit 的输出,你将看到正在发生的事情——首先,它下载提供 Buildpacks 集成的组件,然后运行并发现这是一个 Node.js 应用程序;它将应用程序打包成一个压缩归档,然后将归档导出到一个已安装 Node.js 运行时的容器镜像中。我的输出如图 11.7 所示。

图片

图 11.7 无 Docker 和 Dockerfile 构建容器镜像增加了许多复杂性。

你不能在 BuildKit Pod 上从这个镜像运行容器,因为它没有配置容器运行时,但 BuildKit 在构建后能够将镜像推送到注册表,这就是我们在完整工作流程中要做的。到目前为止,我们已经看到可以在没有 Dockerfile 或 Docker 的情况下构建和打包应用程序以在容器中运行,这相当令人印象深刻,但这也带来了一定的代价。

最大的问题是构建过程的复杂性和所有组件的成熟度。BuildKit 是一个稳定的工具,但它并没有像标准的 Docker 构建引擎那样被广泛使用。Buildpacks 是一种有希望的方法,但由于对 Docker 的依赖,它们在云中管理的 Kubernetes 集群等无 Docker 环境中工作得并不好。我们用来连接它们的组件是由 BuildKit 项目维护者 Tõnis Tiigi 编写的工具。这实际上只是一个将 Buildpacks 插入 BuildKit 的概念证明;它足够好,可以演示工作流程,但它不是你想要用于生产应用程序构建的东西。

有其他选择。GitLab 是一个将 Git 服务器与使用 Buildpacks 的构建管道结合在一起的产品,而 Jenkins X 是 Kubernetes 的原生构建服务器。它们本身是复杂的产品,你需要意识到,如果你想从你的开发者工作流程中移除 Docker,你将在构建过程中以更多的复杂性为代价。你将在本章结束时能够决定结果是否值得。接下来,我们将看看如何在 Kubernetes 中隔离工作负载,以便单个集群可以运行你的交付管道和所有测试环境。

11.3 使用上下文和命名空间隔离工作负载

在第三章中,我介绍了 Kubernetes 的命名空间——并且很快转到了其他内容。你需要了解它们才能理解 Kubernetes 为服务使用的完全限定 DNS 名称,但你不需要使用它们,直到你开始划分你的集群。命名空间是一种分组机制——每个 Kubernetes 对象都属于一个命名空间——你可以使用多个命名空间从一个真实集群中创建虚拟集群。

命名空间非常灵活,组织以不同的方式使用它们。你可能在生产集群中使用它们来划分不同的产品,或者划分非生产集群以适应不同的环境——集成测试、系统测试和用户测试。你甚至可能有一个开发集群,其中每个开发者都有自己的命名空间,这样他们就不需要运行自己的集群。命名空间是一个边界,你可以在这里应用安全和资源限制,因此它们支持所有这些场景。在我们的 CI/CD 部署中,我们将使用一个专用的命名空间,但我们将从简单的流程开始。

现在尝试一下 Kubectl 是命名空间感知的。你可以显式创建一个命名空间,然后使用namespace标志部署和查询资源——这将创建一个简单的 sleep Deployment。

# create a new namespace:
kubectl create namespace kiamol-ch11-test

# deploy a sleep Pod in the new namespace:
kubectl apply -f sleep.yaml --namespace kiamol-ch11-test

# list sleep Pods--this won’t return anything:
kubectl get pods -l app=sleep

# now list the Pods in the namespace:
kubectl get pods -l app=sleep -n kiamol-ch11-test

我的输出显示在图 11.8 中,你可以看到命名空间是资源元数据的一个基本组成部分。你需要明确指定命名空间才能在 kubectl 中使用对象。我们之所以在前 10 章中避免这样做,唯一的原因是每个集群都有一个名为default的命名空间,如果你没有指定命名空间,就会使用这个命名空间,而且我们到目前为止一直在那里创建和使用一切。

图 11.8

图 11.8 命名空间隔离工作负载——你可以使用它们来表示不同的环境。

命名空间内的对象是隔离的,因此你可以在不同的命名空间中部署具有相同对象名称的相同应用程序。资源不能看到其他命名空间中的资源。Kubernetes 的网络是扁平的,所以不同命名空间中的 Pod 可以通过服务进行通信,但控制器只在其自己的命名空间中查找 Pod。命名空间也是普通的 Kubernetes 资源。列表 11.2 显示了 YAML 中的命名空间规范,以及使用新命名空间的其他 sleep Deployment 的元数据。

列表 11.2 sleep-uat.yaml,一个创建并针对命名空间的清单

apiVersion: v1
kind: Namespace      # Namespace specs need only a name.
metadata:
  name: kiamol-ch11-uat
---
apiVersion: apps/v1
kind: Deployment
metadata:                       # The target namespace is part of the 
  name: sleep                   # object metadata. The namespace needs
  namespace: kiamol-ch11-uat    # to exist, or the deployment fails.    

   # The Pod spec follows.

该 YAML 文件中的 Deployment 和 Pod 规范使用与你在上一个练习中部署的对象相同的名称,但由于控制器设置为使用不同的命名空间,它创建的所有对象也将位于该命名空间中。当你部署此清单时,你会看到创建的新对象而不会出现任何命名冲突。

现在尝试一下:从列表 11.2 中的 YAML 创建一个新的 UAT 命名空间和部署。控制器使用相同的名称,并且你可以使用 kubectl 在命名空间之间查看对象。删除命名空间将删除其所有资源。

# create the namespace and Deployment:
kubectl apply -f sleep-uat.yaml

# list the sleep Deployments in all namespaces:
kubectl get deploy -l app=sleep --all-namespaces

# delete the new UAT namespace:
kubectl delete namespace kiamol-ch11-uat

# list Deployments again:
kubectl get deploy -l app=sleep --all-namespaces

你可以在图 11.9 中看到我的输出。原始的 sleep 部署在 YAML 文件中没有指定命名空间,我们通过在 kubectl 命令中指定它,在 kiamol-ch11-test 命名空间中创建了它。第二个 sleep 部署在 YAML 中指定了 kiamol-ch11-uat 命名空间,因此它在那里创建,无需使用 kubectl 命名空间标志。

图 11.9 命名空间是管理对象组的有用抽象

在共享集群环境中,你可能经常使用不同的命名空间——在自己的开发命名空间中部署应用程序,然后在测试命名空间中查看日志。使用 kubectl 标志在它们之间切换既耗时又容易出错,而 kubectl 提供了一种更简单的方法,即 上下文。上下文定义了 Kubernetes 集群的连接细节,并设置在 kubectl 命令中使用的默认命名空间。你的实验环境已经设置了一个上下文,你可以修改它以切换命名空间。

现在尝试一下:显示你的配置上下文,并将当前上下文更新为将默认命名空间设置为测试命名空间。

# list all contexts:
kubectl config get-contexts

# update the default namespace for the current context:
kubectl config set-context --current --namespace=kiamol-ch11-test

# list the Pods in the default namespace:
kubectl get pods

你可以在图 11.10 中看到,为上下文设置命名空间将设置所有 kubectl 命令的默认命名空间。任何未指定命名空间的查询以及任何 YAML 中未指定命名空间的 create 命令现在都将使用测试命名空间。你可以创建多个上下文,所有这些上下文都使用相同的集群但不同的命名空间,并且可以使用 kubectl 的 use-context 命令在它们之间切换。

图 11.10 上下文是切换命名空间和集群的简单方法。

上下文的另一个重要用途是切换集群。当你设置 Docker Desktop 或 K3s 时,它们会为你的本地集群创建一个上下文——所有细节都存储在配置文件中,该文件存储在你家目录中的 .kube 目录中。托管 Kubernetes 服务通常具有将集群添加到配置文件的功能,因此你可以从本地机器上与远程集群一起工作。远程 API 服务器将使用 TLS 加密,你的 kubectl 配置将使用客户端证书来识别你作为用户。你可以通过查看配置来查看这些安全细节。

现在尝试一下 将上下文重置为使用默认命名空间,然后打印客户端配置的详细信息。

# setting the namespace to blank resets the default:
kubectl config set-context --current --namespace=

# printing out the config file shows your cluster connection:
kubectl config view

图 11.11 展示了我的输出,使用 TLS 证书通过本地连接到我的 Docker Desktop 集群进行验证——这些证书在 kubectl 中没有显示。

图 11-11

图 11.11 上下文包含集群的连接细节,这些细节可能是本地或远程的。

Kubectl 还可以使用令牌与 Kubernetes API 服务器进行身份验证,Pod 被提供了一个令牌,它们可以使用这个令牌作为 Secret,因此运行在 Kubernetes 中的应用程序可以连接到 Kubernetes API 来查询或部署对象。这是我们想要达到的下一个目标:我们将在 Pod 中运行一个构建服务器,当 Git 中的源代码发生变化时触发构建,使用 BuildKit 构建镜像,并将其部署到测试命名空间中的 Kubernetes。

11.4 在 Kubernetes 中不使用 Docker 的持续交付

实际上,我们还没有完全到达那里,因为构建过程需要将镜像推送到注册库,以便 Kubernetes 可以将其拉取以运行 Pod 容器。真实集群有多个节点,每个节点都需要能够访问镜像注册库。到目前为止这很容易,因为我们使用了 Docker Hub 上的公共镜像,但在您自己的构建中,您首先需要将镜像推送到私有仓库。Kubernetes 通过在特殊类型的 Secret 对象中存储注册库凭证来支持拉取私有镜像。

您需要在一个镜像仓库上设置一个账户,以便跟随本节内容——Docker Hub 是可以的,或者您可以在云上使用 Azure 容器注册库 (ACR) 或 Amazon 弹性容器注册库 (ECR) 创建一个私有注册库。如果您在云中运行集群,使用该云的注册库来减少下载时间是有意义的,但所有注册库都使用与 Docker Hub 相同的 API,因此它们可以互换。

现在尝试一下 创建一个密钥来存储注册库凭证。为了便于跟随,有一个脚本来收集凭证到本地变量中。不用担心——脚本不会将您的凭证发送给我。...

# collect the details--on Windows: 
. .\set-registry-variables.ps1

# OR on Linux/Mac:
. ./set-registry-variables.sh

# create the Secret using the details from the script:
kubectl create secret docker-registry registry-creds 
    --docker-server=$REGISTRY_SERVER
    --docker-username=$REGISTRY_USER
    --docker-password=$REGISTRY_PASSWORD

# show the Secret details:
kubectl get secret registry-creds

我的输出如图 11.12 所示。我使用的是 Docker Hub,它允许您创建临时访问令牌,您可以使用它与账户密码相同的方式使用。当我完成这一章时,我会撤销访问令牌——这是 Hub 中一个很好的安全特性。

图 11-12

图 11.12 您的组织可能使用一个私有镜像仓库——您需要一个密钥来验证。

好的,现在我们准备好了。我们有一个无 Docker 的构建服务器在 BuildKit Pod 中运行,一个本地的 Git 服务器,我们可以用它快速迭代构建过程,还有一个存储在集群中的注册表 Secret。我们可以使用所有这些组件与自动化服务器一起运行构建管道,我们将使用 Jenkins 来完成这项工作。Jenkins 作为构建服务器有着悠久的传统,并且非常受欢迎,但你不需要成为 Jenkins 大师就能设置这个构建,因为我已经在一个自定义的 Docker Hub 镜像中配置好了它。

本章的 Jenkins 镜像已安装了 BuildKit 和 kubectl 命令行,Pod 已设置好以在正确位置暴露凭证。你在之前的练习中创建的注册表 Secret 已挂载到 Pod 容器中,因此 BuildKit 可以使用它来在推送镜像时认证到注册表。Kubectl 配置为使用 Kubernetes 在另一个 Secret 中提供的令牌连接到集群中的本地 API 服务器。部署 Jenkins 服务器,并检查一切是否配置正确。

现在试试吧,Jenkins 可以从 Kubernetes Secrets 中获取所有需要的资源,使用容器镜像中的启动脚本。首先部署 Jenkins 并确认它能够连接到 Kubernetes。

# deploy Jenkins:
kubectl apply -f infrastructure/jenkins.yaml

# wait for the Pod to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=jenkins

# check that kubectl can connect to the cluster:
kubectl exec deploy/jenkins -- sh -c 'kubectl version --short'

# check that the registry Secret is mounted:
kubectl exec deploy/jenkins -- sh -c 'ls -l /root/.docker'

在这个练习中,你会看到 kubectl 报告你自己的 Kubernetes 实验室集群的版本——这确认了 Jenkins Pod 容器已正确设置以认证到 Kubernetes,因此它可以在运行它的同一集群中部署应用程序。我的输出显示在图 11.13 中。

图 11.13

图 11.13 Jenkins 运行管道,因此它需要 Kubernetes 和注册表的认证详情。

现在一切准备就绪,Jenkins 可以从 Gogs Git 服务器获取应用程序代码,连接到 BuildKit 服务器使用 Buildpacks 构建容器镜像并将其推送到注册表,并将最新应用程序版本部署到测试命名空间。这项工作已经通过 Jenkins 管道设置好了,但管道步骤只是使用应用程序文件夹中的简单构建脚本。列表 11.3 显示了构建阶段,它打包并推送镜像。

列表 11.3 build.sh,使用 BuildKit 的构建脚本

buildctl --addr tcp://buildkitd:1234 \    # The command runs on Jenkins,
  build \                                 # but it uses the BuildKit server.
  --frontend=gateway.v0 \
  --opt source=kiamol/buildkit-buildpacks \    # Uses Buildpacks as input
  --local context=src \
  --output type=image,name=${REGISTRY_SERVER}/${REGISTRY_USER}/bulletin-board:
${BUILD_NUMBER}-kiamol,push=true  # Pushes the output to the registry

该脚本是对 11.2 节中当你假装自己是构建服务器时运行的更简单的 BuildKit 命令的扩展。buildctl命令使用与 Buildpacks 相同的集成组件,因此这里没有 Dockerfile。这个命令在 Jenkins Pod 内部运行,因此指定了 BuildKit 服务器的地址,该服务运行在名为buildkitd的单独 Pod 后面。这里也没有 Docker。镜像名称中的变量都是由 Jenkins 设置的,但它们都是标准环境变量,因此在构建脚本中没有对 Jenkins 的依赖。

当管道的这一阶段完成时,镜像将被构建并推送到注册表。下一阶段是部署更新后的应用程序,这在一个单独的脚本中,如列表 11.4 所示。您不需要自己运行它——所有这些都在 Jenkins 管道中。

列表 11.4 run.sh,使用 Helm 的部署脚本

helm upgrade --install  --atomic \    # Upgrades or installs the release
  --set registryServer=${REGISTRY_SERVER}, \    # Sets the values for the
        registryUser=${REGISTRY_USER}, \        # image tag, referencing  
        imageBuildNumber=${BUILD_NUMBER} \      # the new image version
  --namespace kiamol-ch11-test \      # Deploys to the test namespace
  bulletin-board \
  helm/bulletin-board 

部署使用 Helm 和一个具有镜像名称部分值的图表。它们来自构建阶段使用的相同变量,这些变量是从 Docker 注册表 Secret 和 Jenkins 中的构建号编译而来的。在我的情况下,第一次构建将镜像推送到 Docker Hub,命名为sixeyed/bulletin-board:1-kiamol,并使用该镜像安装 Helm 发布。要在您的集群中运行构建并将其推送到您的注册表,您只需登录到 Jenkins 并启用构建——管道本身已经设置好了。

现在尝试一下,Jenkins 正在运行并已配置,但管道作业尚未启用。登录以启用作业,您将看到管道执行并将应用程序部署到集群中。

# get the URL for Jenkins:
kubectl get svc jenkins -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/job/kiamol'

# browse and login with username kiamol and password kiamol; 
# if Jenkins is still setting itself up you’ll see a wait screen

# click enable for the Kiamol job and wait . . .

# when the pipeline completes, check the deployment:
kubectl get pods -n kiamol-ch11-test -l app.kubernetes.io/name=bulletin-board
 -o=custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image
# find the URL of the test app:
kubectl get svc -n kiamol-ch11-test bulletin-board
 -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8012'

# browse

构建应该很快,因为它使用的是已经为第 11.2 节中的 Buildpack 构建缓存的相同 BuildKit 服务器。当构建完成后,您可以通过测试命名空间中 Helm 部署的应用程序进行浏览,并看到应用程序正在运行——我的应用程序如图 11.14 所示。

图片

图 11.14 管道执行中的情况,构建并部署到 Kubernetes,无需 Docker 或 Dockerfile

到目前为止一切顺利。我们扮演运维角色,因此我们理解这个应用程序交付过程中的所有动态部分——我们将拥有 Jenkinsfile 中的管道和 Helm 图表中的应用程序规范。其中有很多小的繁琐细节,比如模板化的镜像名称和在 Deployment YAML 中的镜像拉取 Secret,但从开发者的角度来看,这些都已隐藏。

开发者的观点是,您可以使用本地环境对应用程序进行工作,推送更改,并在测试 URL 上看到它们正在运行,无需担心中间发生的事情。我们现在可以看到这个工作流程。您之前对应用程序进行了更改,以添加事件描述到网站,要部署该更改,您只需将更改推送到您的本地 Git 服务器并等待 Jenkins 构建完成。

现在尝试一下,将您的代码更改推送到您的 Gogs 服务器;Jenkins 将在一分钟内看到更改并启动新的构建。这将向您的注册表推送新的镜像版本,并更新 Helm 发布以使用该版本。

# add your code change, and push it to Git:
git add bulletin-board/src/backend/events.js
git commit -m 'Add event descriptions'
git push gogs

# browse back to Jenkins, and wait for the new build to finish

# check that the application Pod is using the new image version:
kubectl get pods -n kiamol-ch11-test -l app.kubernetes.io/name=bulletin-board
 -o=custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image

# browse back to the app

这是将git push PaaS 工作流程应用于 Kubernetes 的示例。我们在这里处理的是一个简单的应用程序,但对于具有许多组件的大型系统,方法是一样的:共享命名空间可以是多个不同团队推送的所有最新版本的部署目标。图 11.15 显示了从代码推送触发的 Kubernetes 中的应用程序更新,无需开发者使用 Docker、Kubernetes 或 Helm。

图片

图 11.15:这是你自己的 Kubernetes 集群上的 PaaS——很多复杂性对开发者来说是隐藏的。

当然,PaaS 方法和 Docker 方法并不是相互排斥的。如果你的集群运行在 Docker 上,你可以利用基于 Docker 的应用的更简单的构建过程,但仍然支持在同一集群中为其他应用提供无 Docker 的 PaaS 方法。每种方法都有其优点和缺点,我们将在最后探讨如何在它们之间做出选择。

11.5 评估 Kubernetes 上的开发者工作流程

在本章中,我们探讨了光谱两端的开发者工作流程,从完全拥抱容器并希望在每个环境中将其置于核心位置的团队,到那些不想在开发过程中增加任何仪式、希望保持本地工作并让所有容器部分都由 CI/CD 管道处理的团队。中间还有很多地方,可能性很大,你将构建一个适合你组织、你的应用程序架构和你的 Kubernetes 平台的方法。

这个决定与文化的关联程度不亚于与技术。你希望每个团队都提升容器知识水平,还是希望将这种知识集中在服务团队中,让开发团队专注于交付软件?虽然我很希望看到每个桌子上都有一本《一个月午餐学会 Docker》和《一个月午餐学会 Kubernetes》,但提升容器技能确实需要相当大的承诺。以下是我在保持 Docker 和 Kubernetes 在你的项目中可见性时看到的主要优势:

  • PaaS 方法复杂且定制化——你将连接许多不同成熟度和支持结构的技术。

  • Docker 方法很灵活——你可以在 Dockerfile 中添加任何所需的依赖和设置,而 PaaS 方法则更为具体,因此它们可能不适合每个应用。

  • PaaS 技术没有你在微调 Docker 镜像时可以获得的优化;Docker 工作流程中的公告板镜像为 95 MB,而 Buildpacks 版本为 1 GB——这为安全提供了更小的表面区域。

  • 对学习 Docker 和 Kubernetes 的承诺是有回报的,因为它们是可移植的技能——开发者可以轻松地使用标准工具集在不同项目之间移动。

  • 团队不必使用完整的容器栈;他们可以在不同阶段选择退出——一些开发者可能只使用 Docker 来运行容器,而其他人可能使用 Docker Compose 和 Kubernetes。

  • 分布式知识有助于形成更好的协作文化——集中的服务团队可能会因为只有他们能够玩所有有趣的技术而受到怨恨。

最终,这是一个由你的组织和团队做出的决定,并且从当前工作流程迁移到期望工作流程的痛苦需要被考虑。在我的咨询工作中,我经常在开发和运维角色之间保持平衡,我倾向于务实。当我积极开发时,我使用原生工具(我通常使用 Visual Studio 在.NET 项目中工作),但在推送任何更改之前,我会在本地运行 CI 流程,使用 Docker Compose 构建容器镜像,然后在本地 Kubernetes 集群中启动一切。这不会适合每个场景,但我发现这是开发速度和信心之间的良好平衡,即我的更改将在下一个环境中以相同的方式工作。

这就是开发工作流程的全部内容,因此在我们继续之前,我们可以整理一下集群。留下你的构建组件运行(Gogs、BuildKit 和 Jenkins)——你将在实验室中需要它们。

现在试试看 移除公告板部署。

# uninstall the Helm release from the pipeline:
helm -n kiamol-ch11-test uninstall bulletin-board

# delete the manual deployment:
kubectl delete all -l app=bulletin-board

11.6 实验室

这个实验室有点棘手,所以我提前道歉——但我希望你能看到,使用自定义工具集走 PaaS 路径是有风险的。本章的公告板应用使用了非常旧的 Node 运行时版本,版本号为 10.5.0,在实验室中,需要将其更新到更近的版本。实验室有一个新的源代码文件夹,使用 Node 10.6.0,你的任务是设置一个管道来构建这个版本,然后找出它失败的原因并修复它。以下是一些提示,因为目标不是让你学习 Jenkins,而是看看如何调试失败的管道:

  • 首先,从 Jenkins 主页创建一个新项目:选择复制现有作业的选项,并复制kiamol作业;你可以将新作业命名为任何你喜欢的。

  • 在“管道”选项卡中的新作业配置中,将管道文件的路径更改为新的源代码文件夹:ch11/lab/bulletin-board/Jenkinsfile

  • 构建作业,并查看日志以找出它失败的原因。

  • 你需要在实验室源文件夹中做出更改,并将其推送到 Gogs 以修复构建。

我的示例解决方案在 GitHub 上,有一些 Jenkins 设置的截图来帮助你:github.com/sixeyed/kiamol/blob/master/ch11/lab/README.md

第三周:为生产做准备

Kubernetes 会为你运行应用程序,但这并不意味着你可以免费获得生产体验。本节将教你所有第二天操作的概念,这样当你将应用程序上线时,你将做好充分准备。你将了解保持应用程序在线的健康检查,集中收集日志和应用指标,以及如何配置 Kubernetes 将流量路由到你的容器。我们还将涵盖一些核心安全主题,这样你可以了解保持应用程序安全所需的内容。

12 激活自我修复的应用程序

Kubernetes 通过计算和网络层的抽象来对应用程序进行建模。这些抽象允许 Kubernetes 控制网络流量和容器生命周期,因此它可以在应用程序的部分失败时采取纠正措施。如果你在规范中有足够的细节,集群可以找到并修复临时问题,并保持应用程序在线。这些是自我修复的应用程序,它们可以应对任何短暂的问题,而无需人工引导。在本章中,你将学习如何在自己的应用程序中实现这一点,使用容器探测来测试健康状态,并施加资源限制,以防止应用程序消耗过多的计算资源。

Kubernetes 的自我修复能力是有限的,你将在本章中了解到这些限制。我们将主要探讨如何在不进行手动管理的情况下保持应用程序的运行,但也会再次讨论应用程序的更新。更新是最可能导致停机的原因,我们将探讨 Helm 的一些附加功能,这些功能可以在更新周期中保持应用程序的健康状态。

12.1 使用就绪性探测将流量路由到健康的 Pod

Kubernetes 知道你的 Pod 容器是否正在运行,但它不知道容器内的应用程序是否健康。每个应用程序都会有自己的“健康”定义——它可能是对 HTTP 请求的 200 OK 响应——Kubernetes 提供了一个通用的机制来测试健康状态,即使用容器探测。Docker 镜像可以配置健康检查,但 Kubernetes 会忽略它们,转而使用自己的探测。探测在 Pod 规范中定义,并按照固定的时间表执行,测试应用程序的某些方面,并返回一个指示器,表示应用程序是否仍然健康。

如果探测响应表明容器不健康,Kubernetes 将采取行动,而它采取的行动取决于探测的类型。就绪性探测在网络层面上采取行动,管理监听网络请求的组件的路由。如果 Pod 容器不健康,Pod 将被从就绪状态中移除,并从服务中移除的活跃 Pod 列表中删除。图 12.1 显示了对于具有多个副本的部署,一个 Pod 不健康时的样子。

图 12.1 服务端点的列表排除了尚未准备好接收流量的 Pod。

就绪性探测是管理临时负载问题的绝佳方式。一些 Pod 可能会过载,对每个请求都返回 503 状态码。如果就绪性探测检查 200 响应,并且这些 Pod 返回 503,它们将被从服务中移除,并停止接收请求。Kubernetes 在探测失败后会继续运行探测,所以如果过载的 Pod 在休息期间有机会恢复,探测将再次成功,Pod 将被重新纳入服务。

我们在这本书中使用的随机数生成器有几个特性,我们可以使用它们来了解它是如何工作的。API 可以在达到一定数量的请求后失败的模式下运行,并且它有一个 HTTP 端点,返回它是否健康或处于失败状态。我们将首先在没有就绪探测的情况下运行它,以便我们可以理解这个问题。

现在试试看:运行具有多个副本的 API,并看看当应用程序在没有容器探测的情况下失败时会发生什么。

# switch to the chapter’s directory:
cd ch12

# deploy the random-number API:
kubectl apply -f numbers/

# wait for it to be ready:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api

# check that the Pod is registered as Service endpoints:
kubectl get endpoints numbers-api

# save the URL for the API in a text file:
kubectl get svc numbers-api -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8013' > api-url.txt

# call the API--after returning, this app server is now unhealthy:
curl "$(cat api-url.txt)/rng"

# test the health endpoints to check:
curl "$(cat api-url.txt)/healthz"; curl "$(cat api-url.txt)/healthz"

# confirm the Pods used by the service:
kubectl get endpoints numbers-api

你会从这次练习中看到,即使其中一个 Pod 不健康并且总是返回 500 错误响应,服务仍然将其保留在其端点列表中。我的输出图 12.2 显示了请求前后端点列表中的两个 IP 地址,这导致一个实例变得不健康。

图 12-2

图 12.2 应用程序容器可能不健康,但 Pod 仍然处于就绪状态。

这是因为 Kubernetes 不知道其中一个 Pod 不健康。Pod 容器中的应用程序仍在运行,Kubernetes 不知道有一个健康端点它可以用来检查应用程序是否运行正确。您可以通过 Pod 容器规范中的就绪探测提供该信息。列表 12.1 显示了 API 规范的更新,其中包含健康检查。

列表 12.1 api-with-readiness.yaml,API 容器的就绪探测

spec:             # This is the Pod spec in the Deployment.
  containers:
    - image: kiamol/ch03-numbers-api
      readinessProbe:        # Probes are set at the container level.
        httpGet:
          path: /healthz     # This is an HTTP GET, using the health URL.
          port: 80       
        periodSeconds: 5     # The probe fires every five seconds.

Kubernetes 支持不同类型的容器探测。这个使用 HTTP GET 操作,非常适合 Web 应用程序和 API。探测告诉 Kubernetes 每五秒测试一次/healthz端点;如果响应的 HTTP 状态码在 200 到 399 之间,则探测成功;如果返回任何其他状态码,则探测失败。当随机数 API 不健康时,它会返回 500 状态码,因此我们可以看到就绪探测的实际操作。

现在试试看:部署更新后的规范,并验证失败的应用程序的 Pod 是否已从服务中移除。

# deploy the updated spec from listing 12.1:
kubectl apply -f numbers/update/api-with-readiness.yaml

# wait for the replacement Pods:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api,version=v2

# check the endpoints:
kubectl get endpoints numbers-api

# trigger an application container to become unhealthy:
curl "$(cat api-url.txt)/rng"

# wait for the readiness probe to take effect:
sleep 10

# check the endpoints again:
kubectl get endpoints numbers-api

如图 12.3 所示,就绪探测检测到一个 Pod 不健康,因为对 HTTP 请求的响应返回了 500。该 Pod 的 IP 地址已从服务端点列表中移除,因此它将不再接收任何流量。

图 12-3

图 12.3 失败的就绪探测将 Pod 从就绪状态移除,从而从服务中移除。

这个应用程序也是一个很好的例子,说明了就绪探测本身可能很危险。随机数 API 中的逻辑意味着一旦它失败,它就会一直失败,因此不健康的 Pod 将一直被排除在服务之外,应用程序将低于预期容量运行。当探测失败时,部署不会替换离开就绪状态的 Pod,所以我们只剩下两个 Pod 在运行,但只有一个在接收流量。如果另一个 Pod 也失败,情况会变得更糟。

现在试试看:服务列表中只有一个 Pod。你将发起一个请求,那个 Pod 也会变得不健康,因此两个 Pod 都会从服务中移除。

# check the Service endpoints:
kubectl get endpoints numbers-api

# call the API, triggering it to go unhealthy:
curl "$(cat api-url.txt)/rng"

# wait for the readiness probe to fire:
sleep 10

# check the endpoints again:
kubectl get endpoints numbers-api

# check the Pod status:
kubectl get pods -l app=numbers-api

# we could reset the API... but there are no Pods ready to 
# receive traffic so this will fail:
curl "$(cat api-url.txt)/reset"

现在我们陷入了困境——两个 Pod 都失败了就绪性探测,Kubernetes 已经将它们都从服务端点列表中移除。这导致服务没有端点,因此应用程序离线,如图 12.4 所示。现在的情况是,任何尝试使用 API 的客户端都会得到连接失败,而不是 HTTP 错误状态码,这对于尝试使用特殊管理 URL 重置应用程序的管理员来说也是如此。

图 12-4

图 12.4 探测器本应帮助应用程序,但它们可以移除服务中的所有 Pod。

如果你认为,“这不是一个自我修复的应用程序”,你完全正确,但请记住,应用程序已经处于失败状态。没有就绪性探测,应用程序仍然无法工作,但有就绪性探测,它被保护免受传入请求,直到它恢复并能够处理它们。你需要了解你应用程序的故障模式,以便知道探测失败时会发生什么,以及应用程序是否可能自行恢复。

随机数 API 永远不会再次变得健康,但我们可以通过重启 Pod 来修复失败状态。如果你在容器规范中包含另一个健康检查:一个存活性探测,Kubernetes 会为你完成这项工作。

12.2 使用存活性探测重启不健康的 Pod

存活性探测使用与就绪性探测相同的健康检查机制——测试配置可能在你的 Pod 规范中是相同的——但失败探测的动作是不同的。存活性探测在计算级别采取行动,如果 Pod 变得不健康,则会重启 Pod。重启是 Kubernetes 用新的容器替换 Pod 容器;Pod 本身不会被替换;它将继续在相同的节点上运行,但使用新的容器。

列表 12.2 显示了一个随机数 API 的存活性探测。这个探测使用相同的 HTTP GET 动作来运行探测,但它有一些额外的配置。重启 Pod 比将其从服务中移除更具侵入性,额外的设置有助于确保只有在真正需要时才会发生。

列表 12.2 api-with-readiness-and-liveness.yaml,添加存活性探测

livenessProbe:
  httpGet:                 # HTTP GET actions can be used in liveness and
    path: /healthz         # readiness probes--they use the same spec.
    port: 80
  periodSeconds: 10        
  initialDelaySeconds: 10  # Wait 10 seconds before running the first probe.
  failureThreshold: 2      # Allow two probes to fail before taking action.

这是对 Pod 规范的更改,因此应用更新将创建新的替换 Pod,这些 Pod 一开始就是健康的。这次,当 Pod 在应用程序失败后变得不健康,它将因为就绪性探测而被从服务中移除。它将因为存活性探测而被重启,然后 Pod 将被重新添加到服务中。

现在试试看:更新 API,并验证存活性和就绪性检查的组合是否使应用程序保持健康。

# update the Pod spec from listing 12.2:
kubectl apply -f numbers/update/api-with-readiness-and-liveness.yaml

# wait for the new Pods:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api,version=v3

# check the Pod status:
kubectl get pods -l app=numbers-api -o wide

# check the Servivce endpoints:
kubectl get endpoints numbers-api  # two

# cause one application to become unhealthy:
curl "$(cat api-url.txt)/rng"

# wait for the probes to fire, and check the Pods again:
sleep 20
kubectl get pods -l app=numbers-api

在这个练习中,你看到存活性探测在应用失败时重启 Pod。重启是一个新的 Pod 容器,但 Pod 环境是相同的——它有相同的 IP 地址,如果容器在 Pod 中挂载了一个 EmptyDir 卷,它将能够访问前一个容器写入的文件。你可以在图 12.5 中看到,重启后两个 Pods 都在运行并就绪,所以 Kubernetes 修复了故障并恢复了应用。

图片 12-5

图 12.5 就绪性探测和存活性探测结合帮助保持应用在线。

如果应用在没有健康序列的情况下持续失败,重启并不是一个永久的解决方案,因为 Kubernetes 不会无限期地重启一个失败的 Pod。对于短暂的问题,如果应用能在替换容器中成功重启,那么这种方法效果很好。探测也是保持应用在升级期间健康的有用工具,因为滚动更新只有在新的 Pods 进入就绪状态时才会进行,所以如果就绪探测失败,这将暂停滚动更新。

我们将通过待办事项列表应用来展示这一点,其中包括对 Web 应用 Pod 和数据库的存活性和就绪性检查。Web 探测使用我们之前已经看到的相同的 HTTP GET 动作,但数据库没有我们可以使用的 HTTP 端点。相反,规范使用了 Kubernetes 支持的其他类型的探测动作——TCP 套接字动作,它检查端口是否打开并正在监听传入流量,以及 exec 动作,它在容器内运行命令。列表 12.3 展示了探测设置。

列表 12.3 todo-db.yaml,使用 TCP 和命令探测

spec:             
  containers:
    - image: postgres:11.6-alpine
      # full spec includes environment config
      readinessProbe:
        tcpSocket:           # The readiness probe tests the
          port: 5432         # database is listening on the port.
        periodSeconds: 5
      livenessProbe:         # The liveness probe runs a Postgres tool,
        exec:                # which confirms the database is running.
          command: ["pg_isready", "-h", "localhost"]
        periodSeconds: 10
        initialDelaySeconds: 10

当你部署这段代码时,你会看到应用以同样的方式工作,但现在它已经能够抵御 Web 和数据库组件的短暂故障。

现在尝试一下 运行带有新自我修复规范的待办事项列表应用。

# deploy the web and database:
kubectl apply -f todo-list/db/ -f todo-list/web/

# wait for the app to be ready:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web

# get the URL for the service:
kubectl get svc todo-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8081'

# browse to the app, and add a new item

这里没有新的内容,正如你在图 12.6 的输出中看到的那样。但是,数据库探测意味着 Postgres 不会收到任何流量,直到数据库就绪。如果 Postgres 服务器失败,那么数据库 Pod 将会被重启,替换容器将使用 Pod 中的 EmptyDir 卷中的相同数据文件。

图片 12-6

图 12.6 探测正在触发并返回健康响应,因此应用以通常的方式工作。

如果更新出错,容器探针也可以使应用程序继续运行。待办事项应用程序有一个新的数据库规范,它升级了 Postgres 的版本,但它也覆盖了容器命令,使其休眠而不是启动 Postgres。这是一个经典的调试遗留错误:有人想以正确的配置启动 Pod,但不运行应用程序,以便他们可以在容器内运行 shell 来检查环境,但他们没有撤销他们的更改。如果 Pod 没有任何探针,更新将成功并使应用程序崩溃。sleep命令使 Pod 容器继续运行,但没有为网站使用的数据库服务器。探针阻止这种情况发生,并保持应用程序可用。

现在试试看:部署这个错误的更新,并验证新 Pod 中的失败探针阻止了原始 Pod 被移除。

# apply the update:
kubectl apply -f todo-list/db/update/todo-db-bad-command.yaml

# watch the Pod status changing:
kubectl get pods -l app=todo-db --watch

# refresh the app to check that it still works
# ctrl-c or cmd-c to exit the Kubectl watch

您可以在图 12.7 中看到我的输出。替换数据库 Pod 已创建,但它从未进入就绪状态,因为就绪探针检查端口 5342 是否有监听进程,但没有。Pod 也会不断重启,因为存活探针运行一个命令来检查 Postgres 是否准备好接收客户端连接。当新的 Pod 持续失败时,旧的 Pod 会继续运行,应用程序也会继续工作。

图片

图 12.7 滚动更新等待新的 Pod 就绪,因此探针可以防止更新失败。

如果你让这个应用程序再运行大约五分钟,然后再次检查 Pod 状态,你会看到新的 Pod 进入 CrashLoopBackOff 状态。这就是 Kubernetes 如何保护集群免受在持续失败的应用程序上浪费计算资源的影响:它在 Pod 重启之间添加了一个时间延迟,并且每次重启延迟都会增加。如果你看到一个 Pod 处于 CrashLoopBackOff 状态,通常意味着应用程序已经无法修复。

待办事项应用程序现在的情况与我们在第九章首次看到滚动更新失败的情况相同。部署正在管理两个 ReplicaSet,其目标是当新的 ReplicaSet 达到容量时,立即将旧的 ReplicaSet 缩放到零。但新的 ReplicaSet 永远不会达到容量,因为新 Pod 中的探针不断失败。部署会保持这种状态,希望它最终能够完成滚动更新。Kubernetes 没有自动回滚选项,但 Helm 有,你可以扩展你的 Helm 图表以支持健康的升级。

12.3 使用 Helm 安全部署升级

一点点的 Helm 就能走得很远。你在第十章学习了基础知识,你不需要深入研究模板函数和依赖管理,就可以很好地利用 Helm 进行安全的应用程序升级。Helm 支持原子安装和升级,如果失败会自动回滚,它还有一个你可以挂钩的部署生命周期,可以在安装前后运行验证作业。

本章的源文件夹包含多个用于待办事项应用的 Helm 图表,代表不同的版本(通常每个发布都会有一个随时间演变的单个 Helm 图表)。版本 1 的图表使用我们在第 12.2 节中使用的相同的存活性和就绪性检查来部署应用程序;唯一的区别是数据库使用 PersistentVolumeClaim,因此数据在升级之间得到保留。我们将首先清理之前的练习并安装 Helm 版本。

现在试试这个 使用相同的 Pod 规范运行待办应用程序,但使用 Helm 图表部署。

# remove all existing apps from the chapter:
kubectl delete all -l kiamol=ch12

# install the Helm release:
helm install --atomic todo-list todo-list/helm/v1/todo-list/

# browse to the app, and add a new item

应用程序的版本 1 现在通过 Helm 运行,这里没有新内容,除了图表中包含一个位于 templates 文件夹中的 NOTES.txt 文件,该文件显示安装后看到的帮助文本。我的输出如图 12.8 所示。我没有包含应用程序的截图,所以你只能相信我浏览并添加了一条“完成第十二章”的条目。

图 12.8 使用 Helm 安装应用时等待容器探测处于健康状态。

Helm 图表的版本 2 尝试与我们在第 12.2 节中看到的相同的数据库镜像升级,包括 Postgres 容器命令中的配置错误。当你使用 Helm 部署它时,在底层发生相同的事情:Kubernetes 更新 Deployment,添加一个新的 ReplicaSet,但该 ReplicaSet 永远不会达到容量,因为 Pod 的就绪性探测失败。但是 Helm 检查滚出的状态,如果在特定时间段内没有成功,它会自动回滚。

现在试试这个 使用 Helm 升级待办应用程序发布。升级失败,因为 Pod 规范配置错误,Helm 进行了回滚。

# list the current Pod status and container image:
kubectl get pods -l app=todo-list-db
 -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image

# upgrade the release with Helm--this will fail:
helm upgrade --atomic --timeout 30s todo-list todo-list/helm/v2/todo-list/

# list the Pods again:
kubectl get pods -l app=todo-list-db
 -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image

# browse back to the app, and refresh the list

如果你在这项练习中多次检查 Pod 列表,你会看到回滚发生,如图 12.9 所示。最初,有一个运行 Postgres 11.6 的 Pod 在运行,然后一个新的运行 11.8 的 Pod 加入,但那是失败的容器探测 Pod。Pod 在 Helm 超时期间没有就绪,因此升级被回滚,新的 Pod 被移除;它不会像 kubectl update 那样不断重启并触发 CrashLoopBackOff。

图 12.9 升级失败,因为新的 Pod 没有变为就绪状态,Helm 进行了回滚。

在版本 2 的升级失败期间,待办事项应用程序一直在线,没有中断或容量减少。下一个版本通过移除 Pod 规范中的错误容器命令来修复升级,并且它还添加了一个用于 Kubernetes Job 的额外模板,你可以使用 Helm 作为部署测试运行它。测试是按需运行的,而不是作为安装的一部分,因此它们非常适合烟雾测试——这是你运行以确认成功发布是否正常工作的自动化测试套件。列表 12.4 显示了待办数据库的测试。

列表 12.4 todo-db-test-job.yaml,一个作为 Helm 测试运行的 Kubernetes Job

apiVersion: batch/v1
kind: Job                   # This is a standard Job spec.
metadata:
  # metadata includes name and labels
  annotations:
    "helm.sh/hook": test    # Tells Helm the Job can be run in the test
spec:                       # suite for the release 
  completions: 1
  backoffLimit: 0           # The Job should run once and not retry.
  template:
    spec:                   # The container spec runs a SQL query.
      containers:
        - image: postgres:11.8-alpine 
          command: ["psql", "-c", "SELECT COUNT(*) FROM \"public\".\"ToDos\""]

我们在第八章遇到了乔布斯,赫尔姆对其进行了很好的利用。工作规范包括对它们应该成功运行多少次的期望,赫尔姆正是利用这一点来评估测试是否成功。版本 3 的升级应该成功,当它完成时,你可以运行测试工作,该工作运行一个 SQL 语句以确认待办数据库是可访问的。

现在试试看 升级到版本 3 的图表,它修复了 Postgres 更新。然后使用赫尔姆运行测试,并检查作业 Pod 的日志。

# run the upgrade:
helm upgrade --atomic --timeout 30s todo-list todo-list/helm/v3/todo-list/

# list the database Pods and images:
kubectl get pods -l app=todo-list-db -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image,IP:.status.podIPs[].ip

# check the database Service endpoints:
kubectl get endpoints todo-list-db

# now run the test Job with Helm:
helm test todo-list

# check the output:
kubectl logs -l job-name=todo-list-db-test

我在图 12.10 中剪裁了我的输出,但细节都在那里——升级是成功的,但upgrade命令中没有测试。现在数据库正在使用升级后的 Postgres 版本,当测试运行时,作业连接到数据库并确认数据仍然存在。

图片

图 12.10 使用赫尔姆按需运行测试套件,让你可以在任何时间进行应用烟雾测试。

赫尔姆为你管理工作。它不会清理已完成的工作,所以如果你需要,你可以检查 Pod 状态和日志,但当你重复测试命令时,它会替换它们,这样你就可以按需重新运行测试套件。工作还有另一个用途,通过在升级前运行它们,以确保升级的安全性,你可以检查当前发布版本是否处于有效状态,可以升级。

如果你的应用支持多个版本,但只有增量升级,这个功能特别有用,因为版本 1.1 需要升级到版本 1.2,然后才能升级到版本 2。这个逻辑可能涉及查询不同服务的 API 版本或数据库的模式版本,赫尔姆可以在一个具有访问所有其他与应用程序 Pod 共享相同 ConfigMaps 和 Secrets 的 Kubernetes 对象的作业中运行所有这些。列表 12.5 显示了待办赫尔姆图表的版本 4 中的预升级测试。

列表 12.5 todo-db-check-job.yaml,一个在赫尔姆升级前运行的工作

apiVersion: batch/v1
kind: Job                         # The standard Job spec again
metadata:
  # metadata has name and labels
  annotations:
    "helm.sh/hook": pre-upgrade   # This runs before an upgrade and
    "helm.sh/hook-weight": "10"   # tells Helm the order in which to create
spec:                             # the object after the ConfigMap
  template:                       # that the Job requires
    spec:
      restartPolicy: Never
      containers:
        - image: postgres:11.8-alpine
          # env includes secrets
          command: ["/scripts/check-postgres-version.sh"]
          volumeMounts:
            - name: scripts           # Mounts the ConfigMap volume
              mountPath: "/scripts"

预升级检查有两个模板:一个是作业规范,另一个是包含作业中要运行的脚本的 ConfigMap。你使用注解来控制作业需要在赫尔姆生命周期中的哪个位置运行,而这个作业只会为升级运行,不会作为新安装的一部分运行。权重注解确保在作业之前创建 ConfigMap。生命周期和权重让你可以在赫尔姆中模拟复杂的验证步骤,但这个很简单——它升级数据库镜像,但只有当发布版本当前运行的是 11.6 时。

现在试试看 从版本 3 到版本 4 的升级是无效的,因为版本 3 已经升级了 Postgres 版本。运行升级以验证它不会被部署。

# run the upgrade to version 4--this will fail:
helm upgrade --atomic --timeout 30s todo-list todo-list/helm/v4/todo-list/

# list the Jobs:
kubectl get jobs --show-labels

# print the output of the pre-upgrade Job:
kubectl logs -l job-name=todo-list-db-check

# confirm that the database Pod is unchanged:
kubectl get pods -l app=todo-list-db
 -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image

在这个练习中,您将看到 Helm 有效地阻止了升级,因为预升级钩子运行并且作业失败。所有这些都会记录在发布的记录中,这将显示最新的升级失败,并且发布被回滚到最后一个良好版本。我的输出显示在图 12.11 中,在整个更新过程中,应用程序仍然可用。

图 12.11 Helm 图表中预升级作业让您可以验证发布是否适合升级。

了解 Helm 在保持应用程序健康方面的作用是很好的,因为预升级验证和自动回滚有助于保持应用程序升级的自愈能力。Helm 不是这一点的先决条件,但如果您不使用 Helm,您应该考虑在您的部署管道中使用 kubectl 实现这些功能。

在本章中,我们还将涵盖应用程序健康的一个其他方面——管理您的 Pod 容器可用的计算资源。

12.4 使用资源限制保护应用程序和节点

容器是您的应用程序进程的虚拟化环境。Kubernetes 构建这个环境,您知道 Kubernetes 创建容器文件系统并设置网络。容器环境还包括内存和 CPU,这些也可以由 Kubernetes 管理,但默认情况下,它们不是。这意味着 Pod 容器可以访问它们所在节点上的所有内存和 CPU,这有两个原因:应用程序可能会耗尽内存并崩溃,或者它们可能会耗尽节点的资源,导致其他应用程序无法运行。

您可以在 Pod 规范中限制容器可用的资源,并且这些限制就像容器探测一样——您真的不应该在没有它们的情况下进入生产环境。具有内存泄漏的应用程序可以非常快地破坏您的集群,而引起 CPU 峰值是一个很好的、简单的攻击向量。在本节中,您将学习如何指定您的 Pods 以防止这种情况发生,我们将从一个对内存有大量需求的新应用程序开始。

现在尝试一下 清除上一个练习,并运行新应用程序——它除了分配内存和记录分配了多少内存之外,什么都不做。这个 Pod 运行时没有任何容器限制。

# remove the Helm release to free up resources:
helm uninstall todo-list

# print how much memory your nodes have:
kubectl get nodes -o jsonpath='{.items[].status.allocatable.memory}'

# deploy the memory-allocating app:
kubectl apply -f memory-allocator/

# wait a few minutes, and then see how much memory it has allocated:
kubectl logs -l app=memory-allocator --tail 1

内存分配器应用程序每五秒钟会占用大约 10 MB 的内存,并且它会一直进行下去,直到耗尽您实验室集群中的所有内存。您可以从图 12.12 中的我的输出中看到,我的 Docker Desktop 节点可以访问大约 25 GB 的内存,当我截图时,分配器应用程序已经占用了大约 1.5 GB。

图 12.12 不要在生产环境中运行此应用程序——它只是不断分配内存,直到用完为止。

只要应用程序在运行,它就会继续分配内存,所以我们需要尽快行动,以免我的机器崩溃,我丢失了本章的编辑。列表 12.6 显示了一个更新的 Pod 规范,其中包括资源限制,将应用程序限制在 50 MB 的内存。

列表 12.6 memory-allocator-with-limit.yaml,向容器添加内存限制

spec:                       # The Pod spec in the Deployment
  containers:
    - image: kiamol/ch12-memory-allocator
      resources:
        limits:             # Resource limits constrain the compute power
          memory: 50Mi      # for the container; this limits RAM to 50 MB.

资源是在容器级别指定的,但这是一个新的 Pod 规范,因此当你部署更新时,你会得到一个新的 Pod。替换将从零内存分配开始,并且它将每五秒再次分配 10 MB。现在,然而,它将在 50 MB 处达到限制,Kubernetes 将采取行动。

现在尝试一下:使用第 12.6 节中定义的资源限制部署内存分配器应用的更新。你应该会看到 Pod 已重启,但这仅在你运行的 Linux 主机未启用交换内存的情况下。K3s 没有这种设置(除非你使用 Vagrant 虚拟机设置),所以你不会看到 Docker Desktop 或云 Kubernetes 服务相同的输出结果。

# appy the update:
kubectl apply -f memory-allocator/update/memory-allocator-with-limit.yaml

# wait for the app to allocate a chunk of memory:
sleep 20

# print the application logs:
kubectl logs -l app=memory-allocator --tail 1

# watch the status of the Pod:
kubectl get pods -l app=memory-allocator --watch 

在这个练习中,你会看到 Kubernetes 强制执行内存限制:当应用尝试分配超过 50 MB 的内存时,容器将被替换,你可以看到 Pod 进入 OOMKilled 状态。超过限制会导致 Pod 重启,因此这具有与失败的存活探针相同的缺点——如果替换容器持续失败,Pod 重启将越来越长,因为 Kubernetes 应用了 CrashLoopBackOff,如图 12.13 所示。

图片

图 12.13 内存限制是硬限制——如果容器超过它们,它将被杀死,Pod 将重启。

应用资源约束的难点在于确定应该设置什么限制。你需要进行一些性能测试,看看你的应用能够处理多少资源——请注意,如果应用平台看到大量可用内存,它们可能会占用比实际需要的更多资源。你应该对你的初始发布慷慨一些,然后根据从监控中获得的更多反馈来降低限制。

你还可以通过指定命名空间的最大配额来应用资源限制。这种方法对于使用命名空间来划分集群以供不同团队或环境使用的共享集群特别有用;你可以对命名空间可以使用的总资源量实施限制。列表 12.7 显示了 ResourceQuota 对象的规范,它将命名空间 kiamol-ch12-memory 中可用的总内存限制为 150 MB。

列表 12.7 02-memory-quota.yaml,为命名空间设置内存配额

apiVersion: v1
kind: ResourceQuota                  # The ResourceQuota is applied
metadata:                            # at the specified namespace.
  name: memory-quota
  namespace: kiamol-ch12-memory
spec:
  hard:                              # Quotas can include CPU and memory.
    limits.memory: 150Mi             

容器限制是反应性的,因此当内存限制被超过时,Pod 将会重启。由于资源配额是主动性的,如果它们指定的限制超过了配额中可用的资源,则不会创建 Pod。如果已存在配额,则每个 Pod 规范都需要包含一个资源部分,以便 Kubernetes 可以比较规范所需与当前命名空间中可用的资源。以下是一个更新的内存分配器规范示例,其中 Pod 指定了一个大于配额的限制。

现在尝试一下:在具有资源配额的命名空间中部署内存分配器的新版本。

# delete the existing app:
kubectl delete deploy memory-allocator

# deploy namespace, quota, and new Deployment:
kubectl apply -f memory-allocator/namespace-with-quota/

# print the staus of the ReplicaSet:
kubectl get replicaset -n kiamol-ch12-memory 

# show the events in the ReplicaSet:
kubectl describe replicaset -n kiamol-ch12-memory 

你会从 ReplicaSet 的输出中看到,它有 0 个 Pod,而期望的总数是 1。它不能创建 Pod,因为它会超出命名空间配额,如图 12.14 所示。控制器会不断尝试创建 Pod,但除非有足够的配额可用,例如其他 Pod 终止,但在这个案例中没有,所以它需要更新配额。

图 12-14

图 12.14 硬限制配额会阻止 Pod 创建,如果它们会超过配额。

Kubernetes 也可以将 CPU 限制应用于容器和配额,但它们的工作方式略有不同。具有 CPU 限制的容器以固定的处理能力运行,并且它们可以使用尽可能多的 CPU——如果达到限制,它们不会被替换。你可以将容器限制为 CPU 核心的一半,并且它可以以 100%的 CPU 运行,而节点上的所有其他核心都保持空闲,可供其他容器使用。计算π是一个计算密集型操作,我们可以看到在书中之前使用的π应用程序上应用 CPU 限制的效果。

现在试试看 运行带有和不带有 CPU 限制的π应用程序,并比较其性能。

# show the total CPU available to the nodes:
kubectl get nodes -o jsonpath='{.items[].status.allocatable.cpu}'

# deploy Pi without any CPU limits:
kubectl apply -f pi/

# get the URL for the app:
kubectl get svc pi-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8012/?dp=50000'

# browse to the URL, and see how long the calculation takes

# now update the Pod spec with a CPU limit:
kubectl apply -f pi/update/web-with-cpu-limit.yaml

# refresh the Pi app, and see how long the calculation takes

我的输出显示在图 12.15 中。你的时间将根据你的节点上可用的 CPU 量而有所不同。我的有八个核心,在没有限制的情况下,应用程序可以在 3.4 秒内持续计算π到 50000 位小数。更新后,应用程序容器限制为四分之一核心,同样的计算需要 14.4 秒。

图 12-15

图 12.15 略微眯起眼睛,你会看到限制 CPU 对计算速度有影响。

Kubernetes 使用固定单位定义 CPU 限制,其中一代表单个核心。你可以使用倍数来给你的应用程序容器提供对多个核心的访问,或者将单个核心分成“毫核心”,其中一毫核心是核心的一千分之一。列表 12.8 显示了之前练习中应用于π容器的 CPU 限制,其中 250 毫核心是四分之一核心。

列表 12.8 web-with-cpu-limit.yaml

spec:
  containers:
    - image: kiamol/ch05-pi
      command: ["dotnet", "Pi.Web.dll", "-m", "web"]
      resources:
        limits:
             cpu: 250m    # 250 millicores limits the container to 0.25 cores.

我一次关注一个资源,这样你可以清楚地看到影响,但通常你应该包括 CPU 和内存限制,这样你的应用程序就不会激增并使集群饿死。资源规范还可以包括一个请求部分,它声明容器预期将使用多少 CPU 和内存。这有助于 Kubernetes 决定哪个节点应该运行 Pod,我们将在第十八章的调度部分进一步介绍它。

我们将以一个额外的练习结束本章,以展示如何将 CPU 限制应用于命名空间的配额,以及当配额超出时意味着什么。π应用程序的新规范尝试在具有最大 500 毫核心配额的命名空间中运行具有 300 毫核心 CPU 限制的两个副本。

现在试试看 在其自己的命名空间中运行更新的π应用程序,该命名空间已应用 CPU 配额。

# remove the existing app:
kubectl delete deploy pi-web

# deploy the namespace, quota, and new app spec:
kubectl apply -f pi/namespace-with-quota/

# print the ReplicaSet status:
kubectl get replicaset -n kiamol-ch12-cpu

# list the endpoints for the Service:
kubectl get endpoints pi-web -n kiamol-ch12-cpu

# show the events for the ReplicaSet:
kubectl describe replicaset -n kiamol-ch12-cpu

在这个练习中,你可以看到配额适用于命名空间中的所有 Pod。ReplicaSet 正在运行一个 Pod 而不是两个,因为第一个 Pod 分配了 300 m CPU,这仅剩下 200 m 的配额——不足以让第二个 Pod 运行。图 12.16 显示了 ReplicaSet 事件中的失败原因。Pi 应用程序仍在运行,但容量不足,因为没有足够的 CPU 可用。

图片

图 12.16 配额中强制执行硬 CPU 限制以阻止对象超过总限制。

配额主要是为了保护你的集群,而不是应用程序本身,但它们是确保所有 Pod 规范都有限制指定的好方法。如果你没有使用命名空间来划分你的集群,你仍然可以将具有大 CPU 和内存限制的配额应用到默认命名空间,以确保 Pod 规范包含自己的限制。

资源限制、容器探测和原子升级都有助于在正常故障条件下保持应用程序的运行。这些应该在你的生产路线图中,但你也需要意识到 Kubernetes 无法修复所有类型的故障。

12.5 理解自愈应用程序的限制

Kubernetes 将 Pod 分配给一个节点,它将在该节点上运行。除非节点离线,否则 Pod 不会被替换,因此我们在这章中看到的所有修复机制都是通过重启 Pod——替换应用程序容器来工作的。你需要确保你的应用程序可以容忍这一点,尤其是在第七章中我们讨论的多容器场景中,因为当 Pod 重启时,初始化容器会再次执行,而边车会被替换。

对于大多数具有暂时性故障的场景,Pod 的重启是可行的,但重复的故障最终会导致 CrashLoopBackOff 状态,这可能导致应用程序离线。Kubernetes 不提供任何配置选项来指定允许的重启次数或退避时间,并且它不支持在另一个节点上用新的 Pod 替换失败的 Pod。这些功能已被请求,但直到它们实现,你精心配置的自愈应用程序仍然有可能所有 Pod 都处于退避状态,且服务中没有端点。

那个边缘情况通常是由于配置不当的规范或应用程序的致命问题导致的,这些问题需要比 Kubernetes 自己能够处理的干预更多。对于典型的故障状态,容器探测和资源限制的组合可以大大帮助应用程序独立平稳运行。

那就是关于自愈应用程序的所有内容,因此我们可以整理集群,为实验室做准备。

现在尝试一下 移除本章中的对象。

# delete namespaces:
kubectl delete ns -l kiamol=ch12
kubectl delete all -l kiamol=ch12

# delete all the leftover objects:
kubectl delete secret,configmap,pvc -l kiamol=ch12

12.6 实验室

在这个实验室中,我有一个很好的小容量规划练习。目标是把您的集群分成三个环境来运行 Pi 应用:开发(dev)、测试(test)和用户验收测试(UAT)。UAT 应限制在节点总 CPU 的 50%,而开发和测试各占 25%。您的 Pi 部署应设置限制,以便在每个环境中至少运行四个副本,然后您需要验证在 UAT 中可以扩展到多大。

  • 首先在实验室文件夹中部署命名空间和服务。

  • 然后计算您节点的 CPU 容量,并将资源配额部署到每个命名空间以限制 CPU(您需要编写配额规范)。

  • 将 web.yaml 中的部署规范更新,以包括一个 CPU 限制,允许每个命名空间运行四个副本。

  • 当一切运行正常时,将 UAT 部署扩展到八个副本,并尝试找出为什么它们不能全部运行。

这是一个很好的练习,可以帮助您了解 CPU 资源是如何共享的,并练习与多个命名空间一起工作。我的解决方案在 GitHub 上供您检查:github.com/sixeyed/kiamol/blob/master/ch12/lab/README.md

13 使用 Fluentd 和 Elasticsearch 集中化日志

应用程序会产生大量的日志,这些日志往往并不很有用。当你将应用程序扩展到集群中多个运行的 Pod 时,使用标准的 Kubernetes 工具管理这些日志会变得困难。组织通常会部署自己的日志框架,该框架使用收集和转发模型来读取容器日志并将它们发送到中央存储,以便进行索引、过滤和搜索。在本章中,你将学习如何使用这个领域中最流行的技术:Fluentd 和 Elasticsearch。Fluentd 是收集组件,它与 Kubernetes 有一些很好的集成;Elasticsearch 是存储组件,它可以在集群中的 Pod 中运行,也可以作为外部服务运行。

在我们开始之前,你应该注意几个要点。第一个是,这个模型假设你的应用程序日志被写入容器的标准输出流,这样 Kubernetes 就可以找到它们。我们在第七章中讨论了这一点,包括直接写入标准输出或使用日志边车来中继日志的示例应用程序。第二个是,Kubernetes 中的日志模型与 Docker 非常不同。电子书附录 D 展示了如何使用 Fluentd 与 Docker 一起使用,但与 Kubernetes 一起,我们将采取不同的方法。

13.1 Kubernetes 如何存储日志条目

Kubernetes 对日志管理采用了一种非常简单的处理方法:它从容器运行时收集日志条目,并将它们作为文件存储在运行容器的节点上。如果你想要进行更高级的操作,那么你需要部署自己的日志管理系统,幸运的是,你有一个世界级的容器平台可以运行它。日志系统的各个组件从节点收集日志,将它们转发到集中存储,并提供一个用户界面用于搜索和过滤。图 13.1 展示了本章我们将使用的技术。

图 13.1 Kubernetes 中的日志使用 Fluentd 这样的收集器从节点读取日志文件。

节点以容器提供的原始形式存储日志条目,使用包含命名空间、Pod 和容器名称的文件名。标准的命名系统使得日志收集器很容易添加元数据到日志条目中,以识别来源,并且因为收集器本身作为一个 Pod 运行,它可以查询 Kubernetes API 服务器以获取更多详细信息。Fluentd 添加 Pod 标签和镜像标签作为额外的元数据,你可以使用这些元数据来过滤或搜索日志。

部署日志收集器很简单。我们将首先探索节点上的原始日志文件,看看我们正在处理什么。所有这些的前提是从容器中提取应用程序日志,无论应用程序是否直接写入这些日志,或者你是否使用边车容器。首先,以几种不同的配置部署第七章中的 timecheck 应用程序,以生成一些日志。

现在试试看 使用不同的命名空间运行 timecheck 应用程序,然后检查日志以查看你如何使用 kubectl 本地处理它们。

# switch to the chapter’s folder:
cd ch13

# deploy the timecheck app in development and test namespaces:
kubectl apply -f timecheck/

# wait for the development namespace to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck -n kiamol-ch13-dev

# check the logs:
kubectl logs -l app=timecheck --all-containers -n kiamol-ch13-dev --tail 1

# wait for the test namespace to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck -n kiamol-ch13-test

# check those logs:
kubectl logs -l app=timecheck --all-containers -n kiamol-ch13-test --tail 1

从这个练习中,你会看到在现实集群环境中,直接处理容器日志是困难的,正如我在图 13.2 中的输出所示。你必须一次使用一个命名空间,你不能识别记录消息的 Pod,你只能通过日志条目数量或时间段进行过滤。

图 13.2

图 13.2 Kubectl 对于快速检查日志来说很棒,但在许多命名空间中有许多 Pods 时会更困难。

Kubectl 是读取日志最简单的选项,但最终日志条目来自每个节点上的文件,这意味着你有其他选项来处理日志。本章的源代码包括一个简单的 sleep 部署,它将节点上的日志路径挂载为 HostPath 卷,你可以使用它来探索日志文件,即使你没有直接访问节点。

现在试试看 运行一个挂载主机日志目录的 Pod,并使用挂载来探索文件。

# run the Deployment:
kubectl apply -f sleep.yaml

# connect to a session in the Pod container:
kubectl exec -it deploy/sleep -- sh

# browse to the host log mount:
cd /var/log/containers/

# list the timecheck log files:
ls timecheck*kiamol-ch13*_logger* 

# view the contents of the dev log file:
cat $(ls timecheck*kiamol-ch13-dev_logger*) | tail -n 1

# exit from the session:
exit

每个 Pod 容器都有一个日志输出的文件。timecheck 应用程序使用一个名为 logger 的边车容器来中继应用程序容器的日志,你可以在图 13.3 中看到 Kubernetes 用于日志文件的标准命名约定:pod-name _namespace_container-name-container-id.log. 文件名包含足够的数据来识别日志的来源,文件内容是容器运行时的原始 JSON 日志输出。

图 13.3

图 13.3 对于一个现代平台来说,Kubernetes 对日志存储的方法有点过时。

Pod 重启后,日志文件会被保留,但大多数 Kubernetes 实现都包括在节点上运行的日志轮转系统——在 Kubernetes 之外,以防止日志占用所有磁盘空间。收集并将日志转发到中央存储库可以让你保存更长时间,并将日志存储集中在一个地方——这也适用于核心 Kubernetes 组件的日志。Kubernetes DNS 服务器、API 服务器和网络代理都作为 Pods 运行,你可以像处理应用程序日志一样查看和收集它们的日志。

现在试试看 并非每个 Kubernetes 节点都运行相同的核心组件,但你可以使用 sleep Pod 来查看你的节点上运行哪些常见组件。

# connect to a session in the Pod:
kubectl exec -it deploy/sleep -- sh

# browse to the host path volume:
cd /var/log/containers/
# the network proxy runs on every node:
cat $(ls kube-proxy*) | tail -n 1

# if your cluster uses Core DNS, you’ll see logs here:
cat $(ls coredns*) | tail -n 1

# if your node is running the API server, you’ll see these logs:
cat $(ls kube-apiserver*) | tail -n 1

# leave the session:
exit

根据你的实验室集群的设置,你可能从那个练习中得到不同的输出。网络代理 Pod 在每个节点上运行,所以你应该能看到这些日志,但只有当你的集群使用 CoreDNS(这是默认的 DNS 插件)时,你才会看到 DNS 日志,只有当你的节点运行 API 服务器时,你才会看到 API 服务器日志。我的 Docker Desktop 输出显示在图 13.4 中;如果你看到不同的内容,你可以运行 ls *.log 来查看你节点上的所有 Pod 日志文件。

图 13.4

图 13.4 从节点收集和转发日志也将包括所有系统 Pod 的日志。

现在您知道了 Kubernetes 如何处理和存储容器日志,您可以看到集中式日志系统如何使故障排除变得如此简单。在每个节点上运行一个收集器,从日志文件中抓取条目并转发它们。在本章的其余部分,您将学习如何使用 EFK 堆栈实现它:Elasticsearch、Fluentd 和 Kibana。

13.2 使用 Fluentd 收集节点日志

Fluentd 是一个 CNCF 项目,因此它有一个坚实的后盾,是一个成熟且流行的产品。存在其他日志收集组件,但 Fluentd 是一个好的选择,因为它有一个强大的处理管道来操纵和过滤日志条目,以及一个可插拔的架构,因此它可以转发日志到不同的存储系统。它还有两个变体:完整的 Fluentd 速度快且效率高,有超过 1,000 个插件,但我们将使用最小化的替代品,称为 Fluent Bit。

Fluent Bit 最初被开发为一个用于嵌入式应用程序(如物联网设备)的轻量级 Fluentd 版本,但它具有在完整的 Kubernetes 集群中进行日志聚合所需的所有功能。每个节点都会运行一个日志收集器,因此保持该组件的影响很小是有意义的,Fluent Bit 可以在几十兆内存中愉快地运行。Kubernetes 中的 Fluent Bit 架构很简单:一个 DaemonSet 在每台节点上运行一个收集器 Pod,它使用HostPath卷挂载来访问日志文件,就像我们在 sleep 示例中所使用的那样。Fluent Bit 支持不同的输出,因此我们将从简单开始,只在 Fluent Bit Pod 的控制台中记录日志。

现在尝试一下:部署 Fluent Bit,配置为读取 timecheck 日志文件并将它们写入 Fluent Bit 容器的标准输出流。

# deploy the DaemonSet and ConfigMap:
kubectl apply -f fluentbit/

# wait for Fluent Bit to start up:
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging

# check the logs of the Fluent Bit Pod:
kubectl logs  -l app=fluent-bit -n kiamol-ch13-logging --tail 2

我的输出显示在图 13.5 中,您可以看到 timecheck 容器在 Fluent Bit 容器中暴露的日志。创建日志条目的 Pod 位于不同的命名空间中,但 Fluent Bit 从节点的文件中读取它们。内容是原始 JSON 加上一个更精确的时间戳,这是 Fluent Bit 添加到每个日志条目的。

图 13-5

![图 13-5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/lrn-k8s-mth-lch/img/图 13-5)

Fluent Bit 的 DaemonSet 规范中没有您之前没有见过的内容。我使用一个单独的命名空间进行日志记录,因为您通常希望它作为一个由集群上运行的所有应用程序使用的共享服务运行,而命名空间是隔离所有对象的好方法。运行 Fluent Bit Pods 很简单——复杂性在于配置日志处理管道,我们需要深入研究以充分利用日志模型。图 13.6 显示了管道的阶段以及如何使用它们。

图 13-6

图 13-6

我们目前运行的是一个简单的配置,包含三个阶段:输入阶段读取日志文件,解析阶段分解 JSON 日志条目,输出阶段将每个日志作为单独的行写入 Fluent Bit 容器中的标准输出流。JSON 解析器对所有容器日志都是标准的,并不很有趣,所以我们将在列表 13.1 中关注输入和输出配置。

列表 13.1 fluentbit-config.yaml,一个简单的 Fluent Bit 管道

[INPUT]
    Name              tail         # Reads from the end of a file
    Tag               kube.*       # Uses a prefix for the tag
    Path              /var/log/containers/timecheck*.log
    Parser            docker       # Parses the JSON container logs
    Refresh_Interval  10        # Sets the frequency to check the file list

[OUTPUT]
    Name            stdout         # Writes to standard out
    Format          json_lines     # Formats each log as a line
    Match           kube.*         # Writes logs with a kube tag prefix

Fluent Bit 使用标签来识别日志条目的来源。标签在输入阶段添加,可以用来将日志路由到其他阶段。在这个配置中,日志文件名用作标签,前面加上kube前缀。匹配规则将所有kube标签条目路由到输出阶段,因此每个日志都会打印出来,但输入阶段只读取 timecheck 日志文件,所以您只能看到这些日志条目。

您真的不想过滤输入文件——这只是一个快速开始的方法,避免因日志条目过多而让您感到困扰。最好读取所有输入,然后根据标签路由日志,这样您就只存储您感兴趣的条目。Fluent Bit 内置了对 Kubernetes 的支持,有一个过滤器可以丰富日志条目,用元数据来识别创建它的 Pod。过滤器还可以配置为为每个日志构建一个包含命名空间和 Pod 名称的自定义标签;使用它,您可以修改管道,以便只有来自测试命名空间的日志写入标准输出。

现在尝试一下 更新 Fluent Bit ConfigMap 以使用 Kubernetes 过滤器,重启 DaemonSet 以应用配置更改,然后打印 timecheck 应用程序的最新日志以查看过滤器的作用。

# update the data pipeline configuration files:
kubectl apply -f fluentbit/update/fluentbit-config-match.yaml

# restart the DaemonSet so a new Pod gets the changed configuration:
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging

# wait for the new logging Pod:
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging

# print the last log entry:
kubectl logs  -l app=fluent-bit -n kiamol-ch13-logging --tail 1

您可以从图 13.7 中的我的输出中看到,通过 Fluent Bit 传输的数据量要多得多——日志条目是相同的,但它已经丰富了日志来源的详细信息。Kubernetes 过滤器从 API 服务器获取所有这些数据,这为您在分析日志以追踪问题时提供了真正需要的额外上下文。查看容器的图像哈希将让您能够完全确信地检查软件版本。

图 13-7

图 13.7 显示过滤器丰富了日志条目——单个日志消息现在有 14 个额外的元数据字段。

这个 Fluent Bit 的配置有点棘手。Kubernetes 过滤器默认情况下可以获取所有 Pod 元数据,但为路由构建自定义标签需要一些繁琐的正则表达式。所有这些都在您在之前的练习中部署的 ConfigMap 的配置文件中,但我不打算关注它,因为我真的很不喜欢正则表达式。而且也没有必要——设置是完全通用的,所以您可以将输入、过滤器和解析配置插入到自己的集群中,它将为您的应用程序工作而无需任何更改。

输出配置将不同,因为那是你配置目标的方式。在我们连接日志存储和搜索组件之前,我们将查看 Fluent Bit 的另一个特性——将日志条目路由到不同的输出。输入配置中的正则表达式为格式为kube.namespace.container_name.pod_name的条目设置了一个自定义标签,这可以用于匹配,根据它们的命名空间或 Pod 名称将日志路由到不同的地方。列表 13.2 显示了一个具有多个目的地的更新输出配置。

列表 13.2 fluentbit-config-match-multiple.yaml,路由到多个输出

[OUTPUT]
    Name       stdout                     # The standard out plugin will
    Format     json_lines                 # print only log entries where
    Match      kube.kiamol-ch13-test.*    # the namespace is test.

[OUTPUT]
    Name       counter                    # The counter prints a count of
    Match      kube.kiamol-ch13-dev.*     # logs from the dev namespace.

Fluent Bit 支持许多输出插件,从简单的 TCP 到 Postgres 和云服务如 Azure Log Analytics。我们迄今为止使用的是标准输出流,它只是将日志条目中继到控制台。计数器插件是一个简单的输出,仅显示已收集的日志条目数量。当你部署新的配置时,你将继续看到测试命名空间中的日志行,你还将看到来自开发命名空间的日志条目计数。

现在试试看 更新配置以使用多个输出,并从 Fluent Bit Pod 打印日志。

# update the configuration and restart Fluent Bit:
kubectl apply -f fluentbit/update/fluentbit-config-match-multiple.yaml

kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging

kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging

# print the last two log lines:
kubectl logs  -l app=fluent-bit -n kiamol-ch13-logging --tail 2

本练习中的计数器并不特别有用,但它存在是为了向你展示管道早期部分的复杂部分如何使管道后期的路由变得简单。图 13.8 显示了我对不同命名空间中的日志有不同的输出,并且我可以仅通过输出阶段的匹配规则来配置这一点。

图 13-8

图 13.8 Fluent Bit 中的不同输出可以重塑数据——计数器仅显示计数。

应该很清楚,你可以在 Kubernetes 写入的简单日志文件之上添加一个复杂的日志系统。Fluent Bit 中的数据管道允许你丰富日志条目并将它们路由到不同的输出。如果你想要的输出不被 Fluent Bit 支持,那么你可以切换到父 Fluentd 项目,该项目拥有更多的插件(包括 MongoDB 和 AWS S3)——管道阶段和配置非常相似。我们将使用 Elasticsearch 进行存储,这对于高性能搜索来说非常完美,并且与 Fluent Bit 的集成也很简单。

13.3 将日志发送到 Elasticsearch

Elasticsearch 是一个生产级的开源数据库。它将项目作为文档存储在称为索引的集合中。它与关系型数据库的存储模型非常不同,因为它不支持索引中每个文档的固定模式——每个数据项都可以有自己的字段集。这对于集中式日志记录来说效果很好,因为来自不同系统的日志项将具有不同的字段。Elasticsearch 作为一个单一组件运行,具有 REST API 以插入和查询数据。一个名为 Kibana 的配套产品提供了一个非常实用的前端来查询 Elasticsearch。你可以在与 Fluent Bit 相同的共享日志命名空间中运行这两个组件。

现在尝试一下 部署 Elasticsearch 和 Kibana——日志系统的存储和前端组件。

# create the Elasticsearch deployment, and wait for the Pod:
kubectl apply -f elasticsearch/

kubectl wait --for=condition=ContainersReady pod -l app=elasticsearch -n kiamol-ch13-logging

# create the Kibana deployment, and wait for it to start:
kubectl apply -f kibana/

kubectl wait --for=condition=ContainersReady pod -l app=kibana -n kiamol-ch13-logging

# get the URL for Kibana:
kubectl get svc kibana -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:5601' -n kiamol-ch13-logging

如图 13.9 所示,这种基本的 Elasticsearch 和 Kibana 部署使用每个服务一个 Pod。日志很重要,因此你希望在生产中为高可用性建模。Kibana 是一个无状态组件,因此你可以增加副本数量以提高可靠性。Elasticsearch 可以作为跨多个 Pod 的有状态集使用持久存储,或者你可以在云中使用托管 Elasticsearch 服务。当你运行 Kibana 时,你可以浏览到该 URL。我们将在下一个练习中使用它。

图 13.9

图 13.9 使用服务运行 Elasticsearch,以便 Kibana 和 Fluent Bit 可以使用 REST API。

Fluent Bit 有一个 Elasticsearch 输出插件,它使用 Elasticsearch REST API 为每个日志条目创建一个文档。该插件需要配置 Elasticsearch 服务器的域名,并且你可以选择指定应创建文档的索引。这允许你使用多个输出阶段将不同命名空间的日志条目隔离到不同的索引中。列表 13.3 将测试命名空间中的 Pod 和 Kubernetes 系统 Pod 的日志条目分开。

列表 13.3 fluentbit-config-elasticsearch.yaml,将日志存储在 Elasticsearch 索引中

[OUTPUT]
    Name       es                            # Logs from the test namespace
    Match      kube.kiamol-ch13-test.*       # are routed to Elasticsearch
    Host       elasticsearch                 # and created as documents in 
    Index      test                          # the "test" index.

[OUTPUT]
    Name       es                            # System logs are created in
    Match      kube.kube-system.*            # the "sys" index in the same
    Host       elasticsearch                 # Elasticsearch server.
    Index      sys

如果有不符合任何输出规则的日志条目,它们将被丢弃。当你部署此更新配置时,Kubernetes 系统日志和测试命名空间的日志将保存在 Elasticsearch 中,但开发命名空间的日志不会被保存。

现在尝试一下 更新 Fluent Bit 配置以将日志发送到 Elasticsearch,然后连接到 Kibana 并设置对测试索引的搜索。

# deploy the updated configuration from listing 13.3
kubectl apply -f fluentbit/update/fluentbit-config-elasticsearch.yaml

# update Fluent Bit, and wait for it to restart
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging

kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging

# now browse to Kibana and set up the search:
# - click Discover on the left navigation panel 
# - create a new index pattern
# - enter "test" as the index pattern
# - in the next step, select @timestamp as the time filter field
# - click Create Index Pattern 
# - click Discover again on the left navigation panel to see the logs

此过程包含几个手动步骤,因为 Kibana 并不是一个易于自动化的产品。图 13.10 中的输出显示了正在创建的索引模式。当你完成这个练习后,你将拥有一个强大、快速且易于使用的搜索引擎,用于测试命名空间中所有容器的日志。Kibana 中的 Discover 选项卡会显示随时间存储的文档速率——这是处理日志的速率——你可以深入到每个文档中查看日志详情。

图 13.10

图 13.10 设置 Fluent Bit 将日志发送到 Elasticsearch,并设置 Kibana 搜索测试索引

Elasticsearch 和 Kibana 是成熟的技术,但如果你是新手,现在是一个很好的时候浏览一下 Kibana 的用户界面。你会在 Discover 页面的左侧看到一个字段列表,你可以使用它来过滤日志。这些字段包含所有 Kubernetes 元数据,因此你可以通过 Pod 名称、主机节点、容器镜像等进行过滤。你可以构建显示按应用程序划分的日志摘要统计信息的仪表板,这对于显示错误日志的突然激增非常有用。你还可以在所有文档中搜索特定值,这是一种在用户给你错误消息的 ID 时查找应用程序日志的好方法。

我不会在 Kibana 上花费太多时间,但一个额外的练习将展示拥有一个集中式日志系统是多么有用。我们将向测试命名空间部署一个新的应用程序,并且它的日志将自动被 Fluent Bit 捕获,并通过 Elasticsearch 流转,而无需对配置进行任何更改。当应用程序向用户显示错误时,我们可以在 Kibana 中轻松追踪。

现在尝试一下:部署我们之前使用过的随机数 API——第一次使用后崩溃的那个——以及一个缓存响应并几乎解决问题的代理。尝试 API,当您收到错误时,您可以在 Kibana 中搜索故障 ID。

# deploy the API and proxy:
kubectl apply -f numbers/

# wait for the app to start up:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api -n kiamol-ch13-test

# get the URL to use the API via the proxy:
kubectl get svc numbers-api-proxy -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/rng' -n kiamol-ch13-test

# browse to the API, wait 30 seconds, and refresh until you get an error
# browse to Kibana, and enter this query in the search bar:
# kubernetes.labels.app:numbers-api AND log:<failure-ID-from-the-API>

图 13.11 中的我的输出很小,但你可以看到发生了什么:我从 API 获取了一个故障 ID,并将其粘贴到 Kibana 的搜索栏中,它返回了一个单一匹配项。日志条目包含了我需要调查 Pod 所需的所有信息。Kibana 还有一个有用的选项,可以在匹配前后显示文档,我可以使用它来显示围绕故障日志的日志条目。

图 13.11

图 13.11 日志系统运行中的情况——从用户界面错误消息追踪故障

可搜索的集中式日志消除了许多故障排除的摩擦,额外的好处是这些组件都是开源的,因此您可以在每个环境中运行相同的日志堆栈。在开发、测试环境和生产环境中使用相同的诊断工具应该有助于产品团队了解有用的日志级别,并提高系统日志的质量。高质量的日志很重要,但在产品待办事项列表中很少排名很高,因此在某些应用程序中,您可能会遇到不太有用的日志。Fluent Bit 也有一些额外的功能可以帮助那里。

13.4 解析和过滤日志条目

理想的应用程序会生成具有条目严重性字段、写入输出的类名称以及事件类型和事件关键数据项 ID 的结构化日志数据。您可以使用这些字段的值在 Fluent Bit 管道中过滤消息,并且这些字段会在 Elasticsearch 中显示,以便您可以构建更精确的查询。大多数系统不会生成这样的日志——它们只是发出文本,但如果文本使用已知格式,那么 Fluent Bit 可以在通过管道时将其解析为字段。

随机数 API 是一个简单的例子。日志条目是看起来像这样的文本行:<6>Microsoft.Hosting.Lifetime[0] Now listening on: http://[::]:80。第一部分,在尖括号内,是消息的优先级,后面跟着类名和事件 ID(方括号内),然后是日志的实际内容。每个日志条目的格式都是相同的,因此 Fluent Bit 解析器可以将日志分割成单独的字段。你必须使用正则表达式来做这件事,列表 13.4 显示了我的最佳尝试,它只提取了优先级字段,并将其他所有内容留在消息字段中。

列表 13.4 fluentbit-config-parser.yaml,应用程序日志的自定义解析器

[PARSER]
    Name      dotnet-syslog            # Name of the parser
    Format    regex                    # Parses with a regular expression 
    Regex     ^\<(?<priority>[0-9]+)\>*(?<message>.*)$       # Yuck

当你部署此配置时,Fluent Bit 将提供一个名为 dotnet-syslog 的新自定义解析器可供使用,但它不会应用于任何日志。管道需要知道哪些日志条目应该使用自定义解析器,Fluent Bit 允许你通过在 Pods 中的注释来设置这一点。这些注释就像提示一样,告诉管道将命名解析器应用于来自此 Pod 的任何日志。列表 13.5 显示了随机数 API Pod 的解析器注释——就这么简单。

列表 13.5 api-with-parser.yaml,带有自定义 Fluent Bit 解析器的 Pod 规范

# This is the Pod template in the Deployment spec.
template:
  metadata:                        # Labels are used for selectors and 
    labels:                        # operations; annotations are often  
      app: numbers-api             # used for integration flags.
    annotations:
      fluentbit.io/parser: dotnet-syslog     # Uses the parser for Pod logs

解析器可以比我的自定义解析器更有效,Fluent Bit 团队在他们的文档中提供了一些示例解析器,包括一个用于 Nginx 的解析器。我正在使用 Nginx 作为随机数 API 的代理,在下一个练习中,我们将为每个组件添加解析器并带有注释,看看结构化日志如何在 Kibana 中实现更有针对性的搜索和过滤。

现在试试看 更新 Fluent Bit 配置以添加随机数应用和 Nginx 代理的解析器,然后更新这些部署以添加指定解析器的注释。尝试该应用,并检查 Kibana 中的日志。

# update the pipeline configuration:
kubectl apply -f fluentbit/update/fluentbit-config-parser.yaml

# restart Fluent Bit:
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging

# update the app Deployments, adding parser annotations:
kubectl apply -f numbers/update/

# wait for the API to be ready:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api -n kiamol-ch13-test

# use the API again, and browse to Kibana to see the logs

你可以在图 13.12 中看到,解析器提升的字段可供 Kibana 过滤,而无需我构建自己的查询。在我的屏幕截图中,我已经过滤以显示具有优先级值为 4(这是一个警告级别)的日志条目来自一个 Pod。当你自己运行时,你也会看到你可以过滤 API 代理 Pod。日志条目包括 HTTP 请求路径和响应代码字段,所有这些都是从 Nginx 文本日志中解析出来的。

图片

图 13.12 日志的解析字段被索引,因此过滤和搜索更快、更简单。

Fluent Bit 集中式日志系统的最后一个好处是:数据处理管道与应用程序独立,并且它是一个更好的应用过滤的地方。那个传说中的理想应用程序能够实时增加或减少日志级别,而无需重启应用程序。然而,您从第四章中知道,许多应用程序需要重启 Pod 才能获取最新的配置更改。当您正在处理实时问题时,这并不好,因为这意味着如果您需要提高日志级别,则需要重启受影响的应用程序。

Fluent Bit 本身不支持实时配置重载,但重启日志收集器 Pod 比重启应用 Pod 更少侵入性,Fluent Bit 会从上次停止的地方继续,所以您不会错过任何日志条目。采用这种方法,您可以在应用程序中以更详细的级别进行日志记录,并在 Fluent Bit 管道中进行过滤。列表 13.6 展示了一个过滤器,只有当优先级字段值为 2、3 或 4 时才包含来自随机数 API 的日志——它会过滤掉优先级较低的条目。

列表 13.6 fluentbit-config-grep.yaml,基于字段值过滤日志

[FILTER]
    Name     grep                       # grep is a search filter.
    Match    kube.kiamol-ch13-test.api.numbers-api*
    Regex    priority [234]             # Even I can manage this regular 
                                        # expression.

这里有一些正则表达式的处理,但您可以看到为什么将文本日志条目拆分成管道可以访问的字段很重要。grep 过滤器可以通过评估字段上的正则表达式来包含或排除日志。当您部署这个更新后的配置时,API 可以愉快地以级别 6 写入日志条目,但它们会被 Fluent Bit 丢弃,只有更重要的条目才会到达 Elasticsearch。

现在尝试一下:部署更新后的配置,以便只保存来自随机数 API 的高优先级日志。删除 API Pod,在 Kibana 中您将看不到任何启动日志条目,但它们仍然存在于 Pod 日志中。

# apply the grep filter from listing 13.6:
kubectl apply -f fluentbit/update/fluentbit-config-grep.yaml

kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging

# delete the old API pod so we get a fresh set of logs:
kubectl delete pods -n kiamol-ch13-test -l app=numbers-api

kubectl wait --for=condition=ContainersReady pod -l app=numbers-api -n kiamol-ch13-test

# use the API, and refresh until you see a failure

# print the logs from the Pod:
kubectl logs -n kiamol-ch13-test -l app=numbers-api

# now browse to Kibana, and filter to show the API Pod logs

这个练习展示了 Fluent Bit 如何有效地过滤日志,只将您关心的日志条目转发到目标输出。它还表明,低级别日志并没有消失——原始容器日志都可以通过 kubectl 查看。只是后续的日志处理阻止了它们进入 Elasticsearch。在实际的故障排除场景中,您可能能够使用 Kibana 识别导致问题的 Pod,然后使用 kubectl 进行深入调查,如图 13.13 所示。

图片

图 13.13 显示在 Fluent Bit 中过滤日志条目可以节省存储空间,并且您可以轻松更改过滤器。

除了我们在这些简单管道中提到的内容,Fluent Bit 还有很多其他功能:你可以修改日志内容,限制传入日志的速率,甚至可以运行由日志条目触发的自定义脚本。但我们已经涵盖了您可能需要的所有主要功能,最后我们将通过比较收集和转发日志模型与其他选项来结束讨论。

13.5 理解 Kubernetes 中的日志选项

Kubernetes 期望您的应用程序日志来自容器的标准输出流。它收集并存储这些流中的所有内容,这就是我们本章中讨论的日志模型的基础。这是一个通用且灵活的方法,我们使用的技术堆栈是可靠且性能良好的,但在过程中存在一些低效。图 13.14 显示了将日志从容器传输到可搜索存储中的一些问题。

图 13.14 目标是将应用程序日志放入 Elasticsearch,但到达那里需要许多步骤。

您可以使用更简单且组件更少的替代架构。您可以直接从应用程序代码将日志写入 Elasticsearch,或者在每个应用程序 Pod 中运行一个 sidecar,从应用程序使用的任何日志接收器读取并推送条目到 Elasticsearch。这将使您对存储的日志数据有更多的控制,而无需使用正则表达式来解析文本字符串。这样做会使您依赖于 Elasticsearch(或您使用的任何存储系统),但如果该系统提供了您所需的一切,这可能不是一个大问题。

对于在 Kubernetes 上运行的第一个应用程序,自定义日志框架可能很有吸引力,但随着您将更多工作负载移动到集群,它将限制您。要求应用程序直接将日志记录到 Elasticsearch 将不适合将日志写入操作系统日志的现有应用程序,并且您很快会发现您的日志 sidecar 不够灵活,需要为每个新应用程序进行调整。Fluentd/Fluent Bit 模型的优势在于它是一个有社区支持的标准方法;与编写和维护自己的日志收集和转发代码相比,调整正则表达式要容易得多。

这就是应用程序日志的全部内容,因此我们可以清理集群,为实验室做准备。

现在尝试一下 移除本章的命名空间和剩余的 Deployment。

kubectl delete ns -l kiamol=ch13
kubectl delete all -l kiamol=ch13

13.6 实验室

在这个实验室中,您将扮演一个操作员的角色,需要将一个新应用程序部署到使用本章中日志模型的集群中。您需要检查 Fluent Bit 配置以找到您应该为您的应用程序使用的命名空间,然后部署我们在书中之前使用过的简单版本化网站。以下是实验室的各个部分:

  • 首先部署位于lab/logging文件夹中的日志组件。

  • vweb文件夹中的应用程序部署到正确的命名空间,以便收集日志,并验证您是否能在 Kibana 中看到日志。

  • 您将看到日志是纯文本,因此下一步是更新您的 Deployment 以使用正确的解析器。应用程序运行在 Nginx 上,Fluent Bit 配置中已经为您设置了一个 Nginx 解析器。

  • 当你在 Kibana 中确认新的日志时,你会看到其中一些状态码为 304 的日志,这表示浏览器应使用其缓存的页面版本。这些日志并不有趣,因此最终任务是更新 Fluent Bit 配置以过滤掉它们。

这是一个非常贴近实际的任务,你需要掌握在 Kubernetes 中导航的所有基本技能来查找和更新所有相关部分。我的解决方案在 GitHub 的常规位置供你检查:github.com/sixeyed/kiamol/blob/master/ch13/lab/README.md.

14 使用 Prometheus 监控应用程序和 Kubernetes

监控是日志记录的伴侣:你的监控系统会告诉你有什么问题,然后你可以深入日志以找出详细信息。像日志记录一样,你希望有一个集中式系统来收集和可视化所有应用组件的指标。在 Kubernetes 中进行监控的一种既定方法使用另一个 CNCF 项目:Prometheus,它是一个收集和存储指标的服务器应用程序。在本章中,你将学习如何在 Kubernetes 中部署一个共享的监控系统,该系统包含显示单个应用程序和整个集群健康状况的仪表板。

Prometheus 在许多平台上运行,但它特别适合 Kubernetes。你在具有访问 Kubernetes API 服务器的 Pod 中运行 Prometheus,然后 Prometheus 查询 API 以找到它需要监控的所有目标。当你部署新应用时,你不需要进行任何设置更改——Prometheus 会自动发现它们并开始收集指标。Kubernetes 应用程序也非常适合 Prometheus。你将在本章中看到如何充分利用边车模式,以便每个应用程序都可以向 Prometheus 提供一些指标,即使应用程序本身不是 Prometheus 准备好的。

14.1 Prometheus 如何监控 Kubernetes 工作负载

Prometheus 中的指标是完全通用的:你想要监控的每个组件都有一个 HTTP 端点,它返回对该组件重要的所有值。一个 Web 服务器包括它服务的请求数量的指标,而 Kubernetes 节点包括可用内存的指标。Prometheus 不关心指标中有什么;它只是存储组件返回的所有内容。对 Prometheus 来说重要的是它需要收集的目标列表。图 14.1 显示了在 Kubernetes 中它是如何使用 Prometheus 内置的服务发现来工作的。

图片

图 14.1 Prometheus 使用拉模型收集指标,自动查找目标。

本章的重点是让 Prometheus 与 Kubernetes 顺利配合,为你提供一个动态的监控系统,随着你的集群随着更多运行更多应用的节点而扩展时,它仍然可以正常工作。我不会过多地详细介绍如何将监控添加到你的应用程序中或你应该记录哪些指标——电子书附录 B 中的章节“使用容器化监控添加可观察性”来自《一个月午餐学 Docker》,它将提供这些额外的细节。

我们首先让 Prometheus 启动并运行。Prometheus 服务器是一个负责服务发现和指标收集及存储的单个组件,它有一个基本的 Web UI,你可以使用它来检查系统状态并运行简单的查询。

现在尝试一下:在专用的监控命名空间中部署 Prometheus,配置为在测试命名空间中查找应用(测试命名空间尚不存在)。

# switch to this chapter’s folder:
cd ch14

# create the Prometheus Deployment and ConfigMap:
kubectl apply -f prometheus/

# wait for Prometheus to start:
kubectl wait --for=condition=ContainersReady pod -l app=prometheus -n kiamol-ch14-monitoring

# get the URL for the web UI:
kubectl get svc prometheus -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:9090' -n kiamol-ch14-monitoring

# browse to the UI, and look at the /targets page

Prometheus 将指标收集称为 scraping。当你浏览到 Prometheus UI 时,你会看到没有抓取目标,尽管有一个名为 test-pods 的类别,其中列出零个目标。图 14.2 显示了我的输出。test-pods 的名称来自你在 ConfigMap 中部署的 Prometheus 配置,Pod 会从中读取。

图片

图 14.2 目前没有目标,但 Prometheus 会持续检查 Kubernetes API 中的新 Pods。

配置 Prometheus 在 Kubernetes 中查找目标是相当直接的,尽管一开始术语可能令人困惑。Prometheus 使用 jobs 来定义一组相关的目标以进行抓取,这些目标可能是应用程序的多个组件。抓取配置可以是一个静态域名列表,Prometheus 会轮询以抓取指标,或者它可以使用动态服务发现。列表 14.1 显示了 test-pods 作业配置的开始,它使用 Kubernetes API 进行服务发现。

列表 14.1 prometheus-config.yaml,带有 Kubernetes 的抓取配置

scrape_configs:                # This is the YAML inside the ConfigMap.
  - job_name: 'test-pods'      # Used for test apps
    kubernetes_sd_configs:     # Finds targets from the Kubernetes API
    - role: pod                # Searches for Pods
    relabel_configs:           # Applies these filtering rules
    - source_labels:          
        - __meta_kubernetes_namespace
      action: keep             # Includes Pods only where the namespace
      regex: kiamol-ch14-test  # is the test namespace for this chapter

需要解释的是 relabel_configs 部分。Prometheus 使用 labels 存储指标,这些是标识源系统和其他相关信息的键值对。你将在查询中使用标签来选择或聚合指标,你还可以在它们存储在 Prometheus 之前使用它们来过滤或修改指标。这是 relabeling,从概念上讲,它与 Fluent Bit 中的数据管道相似——这是你丢弃不需要的数据和重塑你想要的数据的机会。

正则表达式在 Prometheus 中也显得过于复杂,但通常不需要进行更改。你在重命名阶段设置的管道应该足够通用,以适用于所有应用程序。配置文件中的完整管道应用以下规则:

  • 仅包括来自 kiamol-ch14-test 命名空间的 Pods。

  • 将 Pod 名称用作 Prometheus instance 标签的值。

  • 将 Pod 元数据中的应用程序标签用作 Prometheus job 标签的值。

  • 在 Pod 元数据中使用可选的注释来配置抓取目标。

这种方法是由约定驱动的——只要你的应用程序按照规则建模,它们就会自动被识别为监控目标。Prometheus 使用规则来查找匹配的 Pods,并为每个目标,通过向 /metrics 路径发起 HTTP GET 请求来收集指标。Prometheus 需要知道使用哪个网络端口,因此 Pod 规范需要明确包含容器端口。这始终是一个好的做法,因为它有助于记录你的应用程序的设置。让我们将一个简单的应用程序部署到测试命名空间,看看 Prometheus 会如何处理它。

现在尝试一下:将 timecheck 应用程序部署到测试命名空间。规范与所有 Prometheus 抓取规则匹配,因此新的 Pod 应该会被找到并添加为抓取目标。

# create the test namespace and the timecheck Deployment:
kubectl apply -f timecheck/ 

# wait for the app to start:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck -n kiamol-ch14-test

# refresh the target list in the Prometheus UI, and confirm the
# timecheck Pod is listed, then browse to the /graph page, select 
# timecheck_total from the dropdown list, and click Execute

我的输出如图 14.3 所示,我在其中打开了两个浏览器窗口,以便你可以看到应用程序部署时发生了什么。Prometheus 看到了 timecheck Pod 的创建,并且它在重命名阶段匹配了所有规则,因此它被添加为目标。Prometheus 配置设置为每 30 秒抓取一次目标。timecheck 应用程序有一个 /metrics 端点,它返回已写入的时间检查日志的数量。当我查询 Prometheus 中的该指标时,应用程序已写入 22 条日志条目。

图片

图 14.3 将应用程序部署到测试命名空间-Prometheus 发现它并开始收集指标。

在这里,你应该意识到两个重要的事情:应用程序本身需要提供指标,因为 Prometheus 只是一个收集器,并且这些指标代表应用程序一个实例的活动。timecheck 应用程序不是一个 Web 应用程序——它只是一个后台进程——因此没有 Service 将流量导向它。当 Prometheus 查询 Kubernetes API 时,它会获取 Pod IP 地址,并直接向 Pod 发送 HTTP 请求。你也可以配置 Prometheus 查询 Service,但那样你会得到一个跨越多个 Pod 的负载均衡器目标,而你希望 Prometheus 独立抓取每个 Pod。

你将使用 Prometheus 中的指标来驱动显示应用程序整体健康状况的仪表板,并且你可以跨所有 Pod 进行聚合以获取关键值。你还需要能够深入挖掘,以查看 Pod 之间是否存在差异。这将帮助你识别某些实例是否表现不佳,并将这些信息反馈到你的健康检查中。我们可以将 timecheck 应用程序扩展以查看在单个 Pod 级别收集的重要性。

现在尝试一下:向 timecheck 应用程序添加另一个副本。这是一个新的 Pod,它符合 Prometheus 规则,因此它将被发现并作为另一个抓取目标添加。

# scale the Deployment to add another Pod:
kubectl scale deploy/timecheck --replicas 2 -n kiamol-ch14-test

# wait for the new Pod to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck -n kiamol-ch14-test

# back in Prometheus, check the target list, and in the graph page,
# execute queries for timecheck_total and dotnet_total_memory_bytes

在这个练习中,你会看到 Prometheus 找到了新的 Pod 并开始抓取它。两个 Pod 记录了相同的指标,并且 Pod 名称被设置为每个指标的标签。对 timecheck_total 指标的查询现在返回两个结果——每个 Pod 一个——你可以在图 14.4 中看到其中一个 Pod 做了比另一个 Pod 多得多的工作。

图片

图 14.4 每个实例都会记录自己的指标,因此你需要从每个 Pod 收集。

timecheck 计数器是一个在应用程序代码中明确捕获的指标。大多数语言都有 Prometheus 客户端库,您可以将它集成到您的构建中。这些库允许您捕获特定于应用程序的详细信息,例如这些,并且它们还收集关于应用程序运行时的一般信息。这是一个 .NET 应用程序,Prometheus 客户端库记录运行时细节,如使用的内存和 CPU 数量以及正在运行的线程数。在下一节中,我们将运行一个分布式应用程序,其中每个组件都暴露 Prometheus 指标,我们将看到当应用程序仪表板包括运行时性能以及应用程序细节时是多么有用。

14.2 使用 Prometheus 客户端库构建的应用监控

电子书附录 B 介绍了如何向一个展示 NASA 天文每日照片(APOD)服务图片的应用添加指标。该应用的组件使用 Java、Go 和 Node.js 编写,并且每个组件都使用 Prometheus 客户端库来暴露运行时和应用指标。本章包括将应用部署到测试命名空间的 Kubernetes 清单,因此所有应用 Pod 都将被 Prometheus 发现。

现在试试看:将 APOD 应用部署到测试命名空间,并确认应用的三部分组件已添加为 Prometheus 目标。

# deploy the app:
kubectl apply -f apod/

# wait for the main component to start:
kubectl wait --for=condition=ContainersReady pod -l app=apod-api -n kiamol-ch14-test

# get the app URL:
kubectl get svc apod-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8014' -n kiamol-ch14-test

# browse to the app, and then refresh the Prometheus targets

您可以在图 14.5 中看到我的输出,其中展示了一幅名为 Lynds Dark Nebula 1251 的非常令人愉悦的图像。应用按预期工作,Prometheus 已经发现了所有新的 Pod。在部署应用后的 30 秒内,您应该看到所有新目标的状态都是“up”,这意味着 Prometheus 已经成功抓取了它们。

图片

图 14.5 APOD 组件都有服务,但它们仍然在 Pod 级别被抓取。

在这个练习中,我有两个额外的重要事项要指出。首先,Pod 规范都包含一个容器端口,这表示应用程序容器正在监听端口 80,这也是 Prometheus 找到要抓取的目标的方式。实际上,用于 Web UI 的服务是在端口 8014 上监听的,但 Prometheus 直接连接到 Pod 端口。其次,API 目标没有使用标准的 /metrics 路径,因为 Java 客户端库使用不同的路径。我在 Pod 规范中使用了注解来指明正确的路径。

基于约定的发现方式非常棒,因为它减少了大量重复配置和错误的可能性,但并非每个应用都符合这些约定。我们在 Prometheus 中使用的重命名管道提供了一个很好的平衡。默认值适用于符合约定的任何应用,但不符合约定的应用可以通过注解覆盖默认值。列表 14.2 展示了如何配置覆盖以设置指标路径。

列表 14.2 prometheus-config.yaml,使用注解覆盖默认值

- source_labels:   # This is a relabel configuration in the test-pods job.

  - __meta_kubernetes_pod_annotationpresent_prometheus_io_path
  - __meta_kubernetes_pod_annotation_prometheus_io_path

regex: true;(.*)   # If the Pod has an annotation named prometheus.io/path . . .

target_label:  __metrics_path__  # sets the target path from the annotation.

这比看起来要简单得多。规则说明:如果 Pod 有一个名为 prometheus.io/path 的注解,则使用该注解的值作为指标路径。Prometheus 使用标签完成所有操作,因此每个 Pod 注解都成为名为 meta_kubernetes_pod_annotation_<annotation-name> 的标签,还有一个伴随的标签名为 meta_kubernetes_pod_annotationpresent_<annotation-name>,你可以用它来检查注解是否存在。任何使用自定义指标路径的应用程序都需要添加注解。列表 14.3 显示了 APOD API 的示例。

列表 14.3 api.yaml,API 规范中的路径注解

template:                 # This is the pod spec in the Deployment.
  metadata:
    labels:
      app: apod-api       # Used as the job label in Prometheus
    annotations:
      prometheus.io/path: "/actuator/prometheus"   # Sets the metrics path

复杂性集中在 Prometheus 配置中,并且为应用清单指定覆盖项非常简单。当你稍微熟悉一些后,重命名规则并不复杂,你通常遵循的是完全相同的模式。完整的 Prometheus 配置包括类似规则,用于应用覆盖指标端口和完全退出抓取。

当你在阅读这段内容时,Prometheus 一直在忙于抓取 timecheck 和 APOD 应用程序。查看 Prometheus UI 的“图形”页面,可以看到大约 200 个指标正在收集。UI 非常适合运行查询并快速查看结果,但你不能用它来构建一个在单个屏幕上显示应用程序所有关键指标的仪表板。为此,你可以使用 Grafana,这是容器生态系统中另一个开源项目,由 Prometheus 团队推荐。

现在试试看:部署 Grafana,使用 ConfigMaps 设置与 Prometheus 的连接,并包含 APOD 应用程序的仪表板。

# deploy Grafana in the monitoring namespace:
kubectl apply -f grafana/

# wait for it to start up:
kubectl wait --for=condition=ContainersReady pod -l app=grafana -n kiamol-ch14-monitoring

# get the URL for the dashboard:
kubectl get svc grafana -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000/d/kb5nhJAZk' -n kiamol-ch14-monitoring

# browse to the URL; log in with username kiamol and password kiamol

图 14.6 中显示的仪表板虽小,但它能让你了解如何将原始指标转换为系统活动的信息视图。仪表板中的每个可视化都是由 Prometheus 查询驱动的,Grafana 在后台运行。每个组件都有一个行,这包括运行时指标——处理器和内存使用情况——以及应用程序指标——HTTP 请求和缓存使用情况。

图片

图 14.6 应用程序仪表板可以快速了解性能。所有图表都是由 Prometheus 指标驱动的。

这样的仪表板将是跨组织的共同努力。支持团队将设定他们需要查看的要求,应用开发和运维团队确保应用程序捕获数据,仪表板显示数据。就像我们在第十三章中查看的日志系统一样,这是一个由轻量级开源组件构建的解决方案,因此开发者可以在他们的笔记本电脑上运行与生产环境中相同的监控系统。这有助于开发中的性能测试和调试。

转向使用 Prometheus 的集中式监控将需要开发工作,但可以是一个逐步的过程,从基本的指标开始,随着团队提出更多要求,逐渐添加。我为本章的待办事项列表应用添加了 Prometheus 支持,大约需要 dozen 行代码。Grafana 中已经有一个简单的仪表板可以立即使用,所以当你部署应用时,你将能够看到仪表板的起点,该仪表板将随着未来的版本更新而改进。

现在试试看 运行带有指标启用的待办事项应用,并使用该应用生成一些指标。Grafana 中已经有一个仪表板可以用来可视化这些指标。

# deploy the app:
kubectl apply -f todo-list/

# wait for it to start:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web -n kiamol-ch14-test

# browse to the app, and insert an item
# then run some load in with a script - on Windows:
.\loadgen.ps1

# OR on macOS/Linux:
chmod +x ./loadgen.sh && ./loadgen.sh

# get the URL for the new dashboard:
kubectl get svc grafana -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000/d/Eh0VF3iGz' -n kiamol-ch14-monitoring

# browse to the dashboard

在那个仪表板上没有太多内容,但比完全没有仪表板要提供的信息多得多。它告诉你应用在容器内使用的 CPU 和内存量,任务创建的速率,以及 HTTP 请求的平均响应时间。你可以在图 14.7 中看到我的输出,我在那里添加了一些任务,并使用负载生成脚本来发送了一些流量。

图片

图 14.7 由 Prometheus 客户端库和几行代码驱动的简单仪表板

所有这些指标都来自待办事项应用的 Pod。在这个版本的应用中还有另外两个组件:用于存储的 Postgres 数据库和 Nginx 代理。这些组件都没有对 Prometheus 的原生支持,因此它们被排除在目标列表之外。否则,Prometheus 会不断尝试抓取指标并失败。知道某个组件不暴露指标并指定应该排除它的责任在于建模应用的人。列表 14.4 显示了如何通过简单的注解来完成这项工作。

列表 14.4 proxy.yaml,一个排除自身监控的 Pod 规范

  template:                # This is the Pod spec in the Deployment.
    metadata:
      labels:
        app: todo-proxy
      annotations:                      # Excludes the target in Prometheus
        prometheus.io/scrape: "false"   

组件不需要有 Prometheus 的原生支持并提供自己的指标端点,以便包含在你的监控系统中。Prometheus 有自己的生态系统——除了你可以用来向自己的应用程序添加指标的客户端库之外,还有一套导出器可以提取和发布第三方应用的指标。我们可以使用导出器来添加代理和数据库组件缺失的指标。

14.3 使用指标导出器监控第三方应用

大多数应用都以某种方式记录指标,但旧应用不会以 Prometheus 格式收集和暴露它们。导出器是独立的应用程序,了解目标应用如何进行监控,并能将这些指标转换为 Prometheus 格式。Kubernetes 提供了一种完美的方式,通过在每个应用实例旁边运行一个边车容器来运行导出器。这是我们第七章中提到的适配器模式。

Nginx 和 Postgres 都有可用的导出器,我们可以将其作为边车运行以改进待办事项应用程序的监控仪表板。Nginx 导出器从 Nginx 服务器上的状态页面读取数据并将其转换为 Prometheus 格式。请记住,Pod 中的所有容器都共享网络命名空间,因此导出器容器可以访问 localhost 地址上的 Nginx 容器。导出器在其自定义端口上提供了一个自己的 HTTP 端点用于指标,因此完整的 Pod 规范包括边车容器和一个注释来指定指标端口。列表 14.5 显示了关键部分。

列表 14.5 proxy-with-exporter.yaml,添加指标导出器容器

template:                 # Pod spec in the Deployment
    metadata:
      labels:
        app: todo-proxy
      annotations:                    # The exclusion annotation is gone.
        prometheus.io/port: "9113"    # Specifies the metrics port
    spec:
      containers:
        - name: nginx
          # ... nginx spec is unchanged

        - name: exporter              # The exporter is a sidecar.
          image: nginx/nginx-prometheus-exporter:0.8.0
          ports:
            - name: metrics
              containerPort: 9113     # Specifies the metrics port 
          args:                       # and loads metrics from Nginx
            - -nginx.scrape-uri=http://localhost/stub_status

刮擦排除已移除,因此当您部署此更新时,Prometheus 将在端口 9113 上抓取 Nginx Pod,那里有导出器正在监听。所有 Nginx 指标都将由 Prometheus 存储,并且 Grafana 仪表板可以更新以添加一行用于代理。我们不会在本章中详细介绍 Prometheus 查询语言(PromQL)或构建 Grafana 仪表板——仪表板可以从 JSON 文件中导入,并且有一个准备就绪的仪表板可供部署。

现在尝试一下 更新代理部署以添加导出器边车,并将更新的仪表板加载到 Grafana ConfigMap 中。

# add the proxy sidecar:
kubectl apply -f todo-list/update/proxy-with-exporter.yaml

# wait for it to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=todo-proxy -n kiamol-ch14-test

# print the logs of the exporter:
kubectl logs -l app=todo-proxy -n kiamol-ch14-test -c exporter

# update the app dashboard: 
kubectl apply -f grafana/update/grafana-dashboard-todo-list-v2.yaml

# restart Grafana to load the new dashboard:
kubectl rollout restart deploy grafana -n kiamol-ch14-monitoring

# refresh the dashboard, and log in with kiamol/kiamol again

Nginx 导出器不提供大量信息,但基本细节都在那里。您可以在图 14.8 中看到我们获取了 HTTP 请求的数量以及 Nginx 处理连接请求的更低级别的分解。即使在这个简单的仪表板中,您也可以看到 Nginx 处理的流量与 Web 应用处理的流量之间的相关性,这表明代理没有缓存响应,并且对每个请求都调用 Web 应用。

图片

图 14.8 使用导出器收集代理指标为仪表板增加了另一个细节级别。

从 Nginx 获取更多信息的想法很好——比如响应中 HTTP 状态码的分解——但导出器只能传递来自源系统的信息,对于 Nginx 来说这并不多。其他导出器提供了更多的细节,但您需要集中仪表板以显示关键指标。超过十几项可视化后,仪表板会变得令人不知所措,而且如果它不能一眼看出有用的信息,那么它的工作效果就不太好了。

需要添加到待办事项仪表板的一个更多组件是:Postgres 数据库。Postgres 在数据库内部存储各种有用的信息,包括表和函数,导出器运行查询以提供其指标端点。Postgres 导出器的设置与我们在 Nginx 中看到的模式相同。在这种情况下,边车配置为通过 localhost 访问 Postgres,使用与 Postgres 容器用于管理员密码相同的 Kubernetes Secret。我们将对应用程序仪表板进行最终更新,以显示导出器中的关键数据库指标。

现在尝试一下 更新数据库部署规范,添加 Postgres 导出器作为侧车容器。然后更新待办事项列表仪表板,添加一行以显示数据库性能。

# add the exporter sidecar to Postgres:
kubectl apply -f todo-list/update/db-with-exporter.yaml

# wait for the new Pod to start:
kubectl wait --for=condition=ContainersReady pod -l app=todo-db -n kiamol-ch14-test

# print the logs from the exporter:
kubectl logs -l app=todo-db -n kiamol-ch14-test -c exporter

# update the dashboard and restart Grafana:
kubectl apply -f grafana/update/grafana-dashboard-todo-list-v3.yaml
kubectl rollout restart deploy grafana -n kiamol-ch14-monitoring

我在图 14.9 中放大并向下滚动,以便您可以看到新的可视化,但整个仪表板在全屏模式下观看非常愉快。单页显示有多少流量进入代理,应用程序工作有多努力,用户实际上在做什么,以及数据库内部发生了什么。您可以通过客户端库和导出器在您自己的应用程序中获得相同级别的详细程度,而这只需要几天的工作。

图 14-9

图 14.9 数据库导出器记录有关数据活动的指标,这些指标为仪表板增添了细节。

导出器用于向没有 Prometheus 支持的应用程序添加指标。如果您的目标是将一组现有应用程序迁移到 Kubernetes,那么您可能没有开发团队添加自定义指标的奢侈。对于这些应用程序,您可以使用 Prometheus 黑盒导出器,将某些监控优于无监控的方法推向极致。

黑盒导出器可以在侧车容器中运行,并向您的应用程序容器发送 TCP 或 HTTP 请求,同时提供一个基本的指标端点来表示应用程序是否正在运行。这种方法类似于在 Pod 规范中添加容器探针,但黑盒导出器仅用于信息目的。如果您觉得 Kubernetes 的自愈机制,如本书中使用的随机数 API,不适合应用程序,您可以通过运行仪表板来显示应用程序的状态。

现在尝试一下 部署随机数 API,使用黑盒导出器和最简单的 Grafana 仪表板。您可以通过重复使用 API 来破坏它,然后重置它使其再次工作,仪表板会跟踪状态。

# deploy the API to the test namespace:
kubectl apply -f numbers/

# add the new dashboard to Grafana:
kubectl apply -f grafana/update/numbers-api/

# get the URL for the API:
kubectl get svc numbers-api -o jsonpath='#app
 - http://{.status.loadBalancer.ingress[0].*}:8016/rng' -n kiamol-ch14-test

# use the API by visiting the /rng URL
# it will break after three calls; 
# then visit /reset to fix it

# get the dashboard URL, and load it in Grafana:
kubectl get svc grafana -o jsonpath='# dashboard
 - http://{.status.loadBalancer.ingress[0].*}:3000/d/Tb6isdMMk'
 -n kiamol-ch14-monitoring

随机数 API 没有 Prometheus 支持,但将黑盒导出器作为侧车容器运行可以提供对应用程序状态的基本洞察。图 14.10 显示了一个大部分为空的仪表板,但两个可视化显示了应用程序在健康和不健康状态之间切换时的历史趋势。

图 14-10

图 14.10 即使是一个简单的仪表板也是有用的。这显示了 API 的当前和历史状态。

随机数 API 的 Pod 规范与待办事项应用程序中的 Nginx 和 Postgres 类似:黑盒导出器配置为附加容器,并指定了指标暴露的端口。Pod 注解自定义了指标 URL 的路径,因此当 Prometheus 从侧车抓取指标时,它会调用黑盒导出器,该导出器会检查 API 是否对 HTTP 请求做出响应。

现在我们有三个不同应用的仪表板,它们的详细程度不同,因为应用程序组件与它们收集的数据不一致。但所有组件都有一个共同点:它们都在 Kubernetes 上的容器中运行。在下一节中,您将学习如何通过配置 Prometheus 从集群本身收集平台指标来获取更详细的级别。

14.4 监控容器和 Kubernetes 对象

Prometheus 与 Kubernetes 集成以进行服务发现,但它不会从 API 收集任何指标。您可以从两个额外的组件中获取有关 Kubernetes 对象和容器活动的指标:cAdvisor,一个谷歌开源项目,以及kube-state-metrics,它是 GitHub 上更广泛的 Kubernetes 组织的一部分。这两个组件都在集群中以容器的形式运行,但它们从不同的来源收集数据。cAdvisor 从容器运行时收集指标,因此它作为每个节点的 Pod 上的 DaemonSet 运行以报告该节点的容器。kube-state-metrics 查询 Kubernetes API,因此它可以在任何节点上作为具有单个副本的 Deployment 运行。

现在尝试一下:部署 cAdvisor 和 kube-state-metrics 的指标收集器,并更新 Prometheus 配置以将它们包括为抓取目标。

# deploy cAdvisor and kube-state-metrics:
kubectl apply -f kube/ 

# wait for cAdvisor to start:
kubectl wait --for=condition=ContainersReady pod -l app=cadvisor -n kube-system

# update the Prometheus config:
kubectl apply -f prometheus/update/prometheus-config-kube.yaml

# wait for the ConfigMap to update in the Pod:
sleep 30 

# use an HTTP POST to reload the Prometheus configuration:
curl -X POST $(kubectl get svc prometheus
 -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:9090/-/reload' -n kiamol-ch14-monitoring)

# browse to the Prometheus UI--in the Graph page you’ll see
# metrics listed covering containers and Kubernetes objects

在这个练习中,您将看到 Prometheus 正在收集数千个新的指标。原始数据包括每个容器使用的计算资源以及每个 Pod 的状态。我的输出显示在图 14.11 中。当您运行此练习时,您可以在 Prometheus UI 中的“目标”页面检查以确认新的目标是正在被抓取的。Prometheus 不会自动重新加载配置,因此在此练习中,有一个延迟以给 Kubernetes 时间传播 ConfigMap 更新,而 curl 命令强制 Prometheus 重新加载配置。

图片

图 14.11 新指标显示集群和容器级别的活动。

您刚刚部署的更新后的 Prometheus 配置包括两个新的作业定义,如列表 14.6 所示。kube-state-metrics 使用服务的完整 DNS 名称指定为静态目标。单个 Pod 收集所有指标,因此这里没有负载均衡问题。cAdvisor 使用 Kubernetes 服务发现来找到每个 DaemonSet 中的每个 Pod,在多节点集群中每个节点将提供一个目标。

列表 14.6 prometheus-config-kube.yaml,Prometheus 中的新抓取目标

- job_name: 'kube-state-metrics'        # Kubernetes metrics use a 
  static_configs:                       # static configuration with DNS.
  - targets:
         - kube-state-metrics.kube-system.svc.cluster.local:8080
      - kube-state-metrics.kube-system.svc.cluster.local:8081

- job_name: 'cadvisor'                   # Container metrics use
  kubernetes_sd_configs:                 # Kubernetes service discovery
  - role: pod                            # to find all the DaemonSet
  relabel_configs:                       # Pods, by namespace and label.
    - source_labels: 
        - __meta_kubernetes_namespace
        - __meta_kubernetes_pod_labelpresent_app
        - __meta_kubernetes_pod_label_app
      action: keep
      regex: kube-system;true;cadvisor

现在我们面临与随机数仪表板相反的问题:新的指标中信息量过多,因此如果平台仪表板要发挥作用,它需要非常具有选择性。我已经准备了一个示例仪表板,这是一个很好的起点。它包括集群当前资源使用情况和所有可用的资源数量,以及按命名空间进行的一些高级分解和节点健康警告指标。

现在试试看 部署一个关键集群指标的仪表板,并更新 Grafana 以加载新的仪表板。

# create the dashboard ConfigMap and update Grafana:
kubectl apply -f grafana/update/kube/

# wait for Grafana to load:
kubectl wait --for=condition=ContainersReady pod -l app=grafana -n kiamol-ch14-monitoring

# get the URL for the new dashboard:
kubectl get svc grafana -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000/d/oWe9aYxmk' -n kiamol-ch14-monitoring

# browse to the dashboard

这又是一个旨在大屏幕上的仪表板,所以图 14.12 中的截图并不能公正地展示它。当您运行练习时,您可以更仔细地检查它。最上面一行显示内存使用情况,中间一行显示 CPU 使用情况,最下面一行显示 Pod 容器的状态。

图 14.12

图 14.12 另一个小截图——在自己的集群中运行练习以查看全尺寸。

这样的平台仪表板相当底层——它实际上只是显示您的集群是否接近饱和点。驱动这个仪表板的查询将作为警报更有用,警告您资源使用是否失控。Kubernetes 有一些有用的压力指标。仪表板中显示了内存压力和进程压力值,以及磁盘压力指示器。这些值很重要,因为如果一个节点承受计算压力,它可能会终止 Pod 容器。这些将是很好的警报指标,因为如果您达到那个阶段,您可能需要叫人来帮助集群恢复健康。

平台指标还有另一个用途:为应用程序仪表板添加细节,其中应用程序本身没有提供足够详细的指标。平台仪表板显示了整个集群的计算资源使用情况汇总,但 cAdvisor 在容器级别收集它。kube-state-metrics 也是如此,您可以为特定的工作负载过滤指标,将平台信息添加到应用程序仪表板中。我们将在本章中做一个最终的仪表板更新,将平台细节添加到随机数应用程序中。

现在试试看 更新随机数 API 的仪表板以添加平台指标。这只是一个 Grafana 更新;应用程序本身或 Prometheus 没有发生变化。

# update the dashboard:
kubectl apply -f grafana/update/grafana-dashboard-numbers-api-v2.yaml

# restart Grafana so it reloads the dashboard:
kubectl rollout restart deploy grafana -n kiamol-ch14-monitoring

# wait for the new Pod to start:
kubectl wait --for=condition=ContainersReady pod -l app=grafana -n kiamol-ch14-monitoring

# browse back to the random-number API dashboard

如图 14.13 所示,仪表板仍然是基本的,但至少我们现在有一些细节可以帮助关联任何问题。如果 HTTP 状态码显示为 503,我们可以快速查看 CPU 是否也在激增。如果 Pod 标签包含应用程序版本(它们应该包含),我们可以确定哪个应用程序版本遇到了问题。

图 14.13

图 14.13 通过容器和 Pod 指标增强基本健康状态增加了相关性。

监控还有很多内容我没有在这里涵盖,但现在你已经对 Kubernetes 和 Prometheus 如何协同工作有了坚实的基础。你所缺少的主要是服务器级别的指标收集和配置警报。服务器指标提供如磁盘和网络使用等数据。你通过在节点上直接运行导出器来收集它们(使用 Linux 服务器的 Node Exporter 和 Windows 服务器的 Windows Exporter),并使用服务发现来将节点添加为抓取目标。Prometheus 有一个复杂的警报系统,它使用 PromQL 查询来定义警报规则。你配置警报,以便当规则被触发时,Prometheus 会发送电子邮件、创建 Slack 消息或通过 PagerDuty 发送通知。

我们将通过查看 Prometheus 在 Kubernetes 中的完整架构,并深入研究哪些部分需要定制工作以及努力的方向来结束本章。

14.5 理解你在监控上的投资

当你走出核心 Kubernetes 并进入生态系统时,你需要了解你所依赖的项目在五年后、一年后,或者在你所写的章节被印刷出来之前是否仍然存在。我在这本书中非常小心地只包括那些开源、使用广泛、有既定历史和治理模式的生态系统组件。图 14.14 中的监控架构使用的组件都符合这些标准。

图 14-14

图 14.14 监控并非免费提供——它需要开发和依赖开源项目。

我提出这一点是因为向 Prometheus 的迁移将涉及开发工作。你需要记录你应用程序中的有趣指标,以便使你的仪表板真正有用。你应该对自己的投资有信心,因为 Prometheus 是监控容器化应用程序最受欢迎的工具,该项目是 CNCF 的第二批毕业项目——在 Kubernetes 本身之后。还有工作正在进行中,将 Prometheus 指标格式纳入一个开放标准(称为 OpenMetrics),这样其他工具就能读取以 Prometheus 格式公开的应用程序指标。

你包含在那些指标中的内容将取决于你应用程序的性质,但一个好的通用方法是从谷歌的站点可靠性工程实践指南中获取指导。通常,将四个“黄金信号”添加到你的应用程序指标中相当简单:延迟、流量、错误和饱和度。(电子书的附录 B 介绍了这些在 Prometheus 中的样子。)但真正的价值在于从用户体验的角度思考应用程序性能。一个显示你的数据库磁盘使用量高的图表并不能告诉你太多,但如果你能看到由于你的网站结账页面加载时间过长,导致高比例的用户无法完成购买,那么这一点就值得了解。

现在监控部分就到这里,我们可以清理集群,为实验室做准备。

现在尝试一下:删除本章的命名空间,以及系统命名空间中创建的对象。

kubectl delete ns -l kiamol=ch14
kubectl delete all -n kube-system -l kiamol=ch14

14.6 实验室

本章的另一个调查实验室。在实验室文件夹中,有一组用于稍微简单一些的 Prometheus 部署和 Elasticsearch 基础部署的清单。目标是让 Elasticsearch 将指标流到 Prometheus。以下是详细信息:

  • Elasticsearch 不提供自己的指标,因此你需要找到一个为你完成这一功能的组件。

  • Prometheus 的配置会告诉你需要使用哪个命名空间来部署 Elasticsearch 以及需要用于指标路径的注解。

  • 你应该在 Elasticsearch Pod 规范中包含一个版本标签,这样 Prometheus 就会捕获它并将其添加到指标标签中。

你需要查阅 Prometheus 的文档来开始,这应该会指引你的方向。我的解决方案在 GitHub 上,你可以像往常一样在以下位置检查:github.com/sixeyed/kiamol/blob/master/ch14/lab/README.md

15 使用入口管理入站流量

服务将网络流量引入 Kubernetes,你可以有多个具有不同公共 IP 地址的 LoadBalancer 服务,以便将你的 Web 应用程序提供给全世界。这样做会带来管理上的麻烦,因为这意味着为每个应用程序分配一个新的 IP 地址,并使用你的 DNS 提供商将地址映射到应用程序。将流量引导到正确的应用程序是一个路由问题,但你可以在 Kubernetes 内部管理它,使用入口。入口使用一组规则将域名和请求路径映射到应用程序,因此你可以为整个集群使用单个 IP 地址,并内部路由所有流量。

通过域名进行路由是一个老问题,通常使用反向代理来解决,Kubernetes 使用可插拔的入口架构。你定义路由规则作为标准资源,并部署你选择的反向代理来接收流量并执行规则。所有主要的反向代理都支持 Kubernetes,以及一种新的容器感知反向代理。它们都有不同的功能和操作模型,在本章中,你将了解到如何使用入口在集群中托管多个应用程序,其中最流行的是 Nginx 和 Traefik。

15.1 Kubernetes 如何使用入口路由流量

在这本书中,我们已经多次使用 Nginx 作为反向代理(据我统计有 17 次),但我们每次都是为单个应用程序使用它。在第六章中,我们有一个反向代理来缓存来自 Pi 应用程序的响应,在第十三章中,我们还有一个用于随机数 API 的反向代理。入口将反向代理移至中心角色,作为名为入口控制器的组件运行,但方法是一样的:代理从负载均衡器服务接收外部流量,并使用集群 IP 服务从应用程序获取内容。图 15.1 显示了架构。

图 15-1

图 15.1 入口控制器是集群的入口点,根据入口规则路由流量。

在这个图中,重要的是入口控制器,它是一个可插拔的反向代理——它可以是 Nginx、HAProxy、Contour 和 Traefik 等十几种选项之一。入口对象以通用方式存储路由规则,控制器将这些规则输入到代理中。代理有不同的功能集,入口规范并不试图模拟每个可能的选择,因此控制器通过注解添加对这些功能的支持。你将在本章中了解到,路由和 HTTPS 支持的核心功能简单易用,但复杂性在于入口控制器部署及其附加功能。

我们将从运行第二章中提到的基本的 Hello, World 网页应用程序开始,将其作为一个内部组件,使用 ClusterIP 服务,并使用 Nginx 入口控制器来路由流量。

现在尝试一下 运行 Hello, World 应用程序,并确认它只能在集群内部或通过 kubectl 中的port-forward在外部访问。

# switch to this chapter’s folder:
cd ch15

# deploy the web app:
kubectl apply -f hello-kiamol/

# confirm the Service is internal to the cluster:
kubectl get svc hello-kiamol

# start a port-forward to the app:
kubectl port-forward svc/hello-kiamol 8015:80

# browse to http://localhost:8015
# then press Ctrl-C/Cmd-C to exit the port-forward

对于该应用程序的部署或服务规范没有新内容——没有特殊的标签或注解,没有你已经使用过的新字段。你可以在图 15.2 中看到服务没有外部 IP 地址,我只能在运行port-forward时访问应用程序。

图片

图 15.2 集群 IP 服务使应用程序在内部可用——它可以通过入口公开。

要使用入口规则使应用程序可用,我们需要一个入口控制器。控制器管理其他对象。你知道部署管理副本集,副本集管理 Pod。入口控制器略有不同;它们在标准 Pod 中运行并监视入口对象。当它们看到任何更改时,它们会更新代理中的规则。我们将从 Nginx 入口控制器开始,它是更广泛 Kubernetes 项目的一部分。控制器有一个生产就绪的 Helm 图表,但我使用了一个更简单的部署。即便如此,在清单中还有一些我们尚未覆盖的安全组件,但我现在不会详细介绍。(如果需要调查,YAML 中有注释。)

现在尝试一下 部署 Nginx 入口控制器。这使用服务中的标准 HTTP 和 HTTPS 端口,因此你需要在你的机器上开启 80 和 443 端口。

# create the Deployment and Service for the Nginx ingress controller:
kubectl apply -f ingress-nginx/

# confirm the service is publicly available:
kubectl get svc -n kiamol-ingress-nginx

# get the URL for the proxy:
kubectl get svc ingress-nginx-controller
 -o jsonpath='http://{.status.loadBalancer.ingress[0].*}' -n kiamol-ingress-nginx

# browse to the URL--you’ll get an error

当你运行这个练习时,当你浏览时你会看到一个 404 错误页面。这证明了服务正在接收流量并将其定向到入口控制器,但还没有任何路由规则,所以 Nginx 没有内容可以显示,并返回默认的未找到页面。我的输出显示在图 15.3 中,你可以看到服务正在使用标准的 HTTP 端口。

图片

图 15.3 入口控制器接收传入的流量,但它们需要路由规则来知道如何处理它。

现在我们有一个运行的应用程序和一个入口控制器,我们只需要部署一个带有路由规则的入口对象,告诉控制器每个传入请求应使用哪个应用程序服务。列表 15.1 显示了入口对象的简单规则,它将把每个进入集群的请求路由到 Hello, World 应用程序。

列表 15.1 localhost.yaml,Hello, World 应用程序的路由规则

apiVersion: networking.k8s.io/v1beta1  # Beta API versions mean the spec
kind: Ingress                          # isn’t final and could change.
metadata:
  name: hello-kiamol
spec:
  rules:                                  
  - http:                              # Ingress is only for HTTP/S traffic
      paths:
      - path: /                        # Maps every incoming request
        backend:                       # to the hello-kiamol Service
          serviceName: hello-kiamol
          servicePort: 80

入口控制器正在监视新的和更改的入口对象,因此当你部署任何对象时,它将规则添加到 Nginx 配置中。在 Nginx 术语中,它将设置一个代理服务器,其中 hello-kiamol 服务是上游——内容的来源——并且它将为传入请求到根路径提供该内容。

现在尝试一下 创建发布 Hello, World 应用程序的入口规则。

# deploy the rule:
kubectl apply -f hello-kiamol/ingress/localhost.yaml

# confirm the Ingress object is created:
kubectl get ingress

# refresh your browser from the previous exercise

嗯,这很简单——在 Ingress 对象中将路径映射到应用的后端服务,控制器会处理其他所有事情。我在图 15.4 中的输出显示了本地地址,之前返回了 404 错误,现在返回了完整的 Hello, World 应用。

图 15.4 Ingress 对象规则将 Ingress 控制器链接到应用服务。

Ingress 通常是在集群中的集中式服务,如日志记录和监控。管理员团队可能会部署和管理 Ingress 控制器,而每个产品团队则拥有路由流量到其应用的 Ingress 对象。这个过程可能会产生冲突——Ingress 规则不必是唯一的,一个团队的更新可能会导致另一个团队的流量被重定向到其他应用。实际上这种情况不会发生,因为这些应用将托管在不同的域名上,并且 Ingress 规则将包括一个域名以限制其作用范围。

15.2 使用 Ingress 规则路由 HTTP 流量

Ingress 只适用于 Web 流量——HTTP 和 HTTPS 请求——因为它需要使用请求中指定的路由来匹配后端服务。HTTP 请求中的路由包含两部分:主机和路径。主机是域名,如 www.manning.com,路径是资源的位置,如 /dotd 是每日交易页面的位置。列表 15.2 显示了对 Hello, World Ingress 对象的更新,它使用特定的主机名。现在,只有当传入请求是为主机 hello.kiamol.local 时,路由规则才会生效。

列表 15.2 hello.kiamol.local.yaml,指定 Ingress 规则的主域名

spec:
  rules:
  - host: hello.kiamol.local            # Restricts the scope of the
    http:                               # rules to a specific domain
      paths:                           
      - path: /                         # All paths in that domain will
        backend:                        # be fetched from the same Service.
          serviceName: hello-kiamol
          servicePort: 80

当您部署此代码时,您将无法访问该应用,因为域名 hello.kiamol.local 不存在。Web 请求通常从公共 DNS 服务器查找域名的 IP 地址,但所有计算机也有它们自己的本地列表,即 hosts 文件。在下一个练习中,您将部署更新的 Ingress 对象并在您的本地 hosts 文件中注册域名——您需要在终端会话中拥有管理员权限。

现在尝试一下 编辑 hosts 文件是受限的。您需要在 Windows 的终端会话中使用“以管理员身份运行”选项,并且使用 Set-ExecutionPolicy 命令启用脚本。在 Linux 或 macOS 上,准备好输入您的管理员(sudo)密码。

# add domain to hosts--on Windows:
./add-to-hosts.ps1 hello.kiamol.local ingress-nginx

# OR on Linux/macOS:
chmod +x add-to-hosts.sh && ./add-to-hosts.sh hello.kiamol.local ingress-nginx

# update the Ingress object, adding the host name:
kubectl apply -f hello-kiamol/ingress/hello.kiamol.local.yaml

# confirm the update:
kubectl get ingress

# browse to http://hello.kiamol.local

在这个练习中,现有的 Ingress 对象被更新,因此仍然只有一个路由规则供 Ingress 控制器映射。现在这个规则被限制为显式的域名。您可以在图 15.5 中看到,对 hello.kiamol.local 的请求返回了应用,我还浏览到了本地的 Ingress 控制器,它返回了一个 404 错误,因为没有为 localhost 域设置规则。

图 15.5 您可以使用 Ingress 规则通过域名发布应用,并通过编辑您的 hosts 文件在本地使用它们。

路由是基础设施级别的关注点,但就像我们在本书本节中看到的其他共享服务一样,它运行在轻量级容器中,这样你就可以在开发、测试和生产环境中使用完全相同的设置。这让你可以在你的非生产集群中运行多个应用程序,使用友好的域名,而无需使用不同的端口——ingress 控制器的 Service 使用每个应用程序的标准 HTTP 端口。

如果你想在实验室环境中运行具有不同域的多个应用程序,你需要调整你的主机文件。通常,所有域名都会解析到 127.0.0.1,这是你的机器的本地地址。组织可能在测试环境中运行自己的 DNS 服务器,因此任何人都可以从公司网络访问 hello.kiamol.test,并且它将解析到数据中心运行测试集群的 IP 地址。然后,在生产环境中,DNS 解析来自公共 DNS 服务,因此 hello.kiamol.net 解析到在云中运行的 Kubernetes 集群。

你可以在 Ingress 规则中组合主机名和路径,以向你的应用程序展示一组一致的地址,尽管你可能在后端使用不同的组件。你可能有一个 REST API 和一个网站在不同的 Pod 中运行,并且你可以使用 Ingress 规则使 API 在子域名(api.rng.com)或主域名上的路径(rng.com/api)上可用。列表 15.3 展示了来自第九章的简单版本化 Web 应用程序的 Ingress 规则,其中应用程序的两个版本都从一个域名中可用。

列表 15.3 vweb/ingress.yaml,带有主机名和路径的 Ingress 规则

apiVersion: networking.k8s.io/v1beta1 
kind: Ingress
metadata:
  name: vweb                             # Configures a specific feature
  annotations:                           # in Nginx 
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: vweb.kiamol.local              # All rules apply to this domain.
    http:
      paths:
      - path: /                          # Requests to the root path are
        backend:                         # proxied from the version 2 app.
          serviceName: vweb-v2
          servicePort: 80
      - path: /v1                        # Requests to the /v1 path are
        backend:                         # proxied from the version 1 app.
          serviceName: vweb-v1
          servicePort: 80

路径建模增加了复杂性,因为你提供了一个虚假的 URL,它需要修改以匹配服务中的真实 URL。在这种情况下,ingress 控制器将响应对 http://vweb.kiamol.local/v1 的请求,并从 vweb-v1 服务获取内容。但是应用程序在 /v1 上没有任何内容,因此代理需要重写传入的 URL——这就是列表 15.3 中注释所做的工作。这是一个基本示例,它忽略了请求中的路径,并且始终使用后端的根路径。你不能使用 Ingress 规范表达 URL 重写,因此它需要从 ingress 控制器那里获得自定义支持。一个更现实的重写规则将使用正则表达式将请求的路径映射到目标路径。

我们将部署这个简单的版本以避免使用正则表达式,并查看 ingress 控制器如何使用路由规则来识别后端服务并修改请求路径。

现在尝试一下 部署一个新的应用程序,并使用新的 Ingress 规则,然后向你的主机文件中添加一个新的域名,以查看 ingress 控制器从同一域名服务多个应用程序。

# add the new domain name--on Windows:
./add-to-hosts.ps1 vweb.kiamol.local ingress-nginx

# OR on Linux/macOS:
./add-to-hosts.sh vweb.kiamol.local ingress-nginx

# deploy the app, Service, and Ingress:
kubectl apply -f vweb/

# confirm the Ingress domain:
kubectl get ingress

# browse to http://vweb.kiamol.local 
# and http://vweb.kiamol.local/v1

在图 15.6 中,你可以看到两个不同的应用在同一域名下可用,通过请求路径在不同的组件之间进行路由,这些组件是本练习中应用程序的不同版本。

图 15-6

图 15.6 展示了主机名和路径上的 Ingress 路由,它在同一域名下呈现多个应用。

将路由规则映射是发布新应用到您的 Ingress 控制器的最复杂部分,但它确实给了您很多控制权。Ingress 规则是您应用的公共面孔,您可以使用它们来组合多个组件——或者限制对功能的访问。在本节中,我们看到了如果应用有用于容器探针的健康端点和用于 Prometheus 收集的度量端点,它们在 Kubernetes 中工作得更好,但那些不应该公开可用。您可以使用 Ingress 来控制这一点,使用精确路径映射,因此只有明确列出的路径才在集群外部可用。

列表 15.4 展示了待办事项应用的示例。由于这种方法的一个缺点是您需要指定要发布的每个路径,因此未指定的任何路径都将被阻止。

列表 15.4 ingress-exact.yaml,使用精确路径匹配来限制访问

rules:
  - host: todo.kiamol.local
    http:
      paths:
      - pathType: Exact          # Exact matching means only the /new
        path: /new               # path is matched--there are other 
        backend:                 # rules for the /list and root paths.
          serviceName: todo-web
          servicePort: 80
      - pathType: Prefix         # Prefix matching means any path that 
        path: /static            # starts with /static will be mapped,
        backend:                 # including subpaths like /static/app.css.
          serviceName: todo-web
          servicePort: 80

待办事项应用有几个不应该在集群外部可用的路径——以及 /metrics,还有一个列出所有应用程序配置的 /config 端点和诊断页面。这些路径都没有包含在新的 Ingress 规范中,并且我们可以看到当应用规则时,它们实际上被阻止了。PathType 字段是 Ingress 规范的后期添加,因此您的 Kubernetes 集群至少需要运行版本 1.18;否则,您将在本练习中遇到错误。

现在尝试一下:部署一个允许所有访问的 Ingress 规范的待办事项应用,然后使用精确路径匹配来更新它,并确认敏感路径不再可用。

# add a new domain for the app--on Windows:
./add-to-hosts.ps1 todo.kiamol.local ingress-nginx

# OR on Linux/macOS:
./add-to-hosts.sh todo.kiamol.local ingress-nginx

# deploy the app with an Ingress object that allows all paths:
kubectl apply -f todo-list/

# browse to http://todo.kiamol.local/metrics

# update the Ingress with exact paths:
kubectl apply -f todo-list/update/ingress-exact.yaml

# browse again--the app works, but metrics and diagnostics blocked

当您运行此练习时,您将看到当部署更新的 Ingress 规则时,所有敏感路径都被阻止。我的输出如图 15.7 所示。这不是一个完美的解决方案,但您可以将您的 Ingress 控制器扩展以显示友好的 404 错误页面,而不是 Nginx 的默认页面。(Docker 有一个很好的例子:尝试 www.docker.com/not-real-url。)应用仍然显示诊断页面的菜单,因为这不是移除页面的应用设置;它发生在处理过程的更早阶段。

图 15.7 Ingress 规则中的精确路径匹配可用于阻止对功能的访问。

Ingress 规则和 Ingress 控制器之间的分离使得比较不同的代理实现并查看哪个提供了您满意的特性和可用性变得容易。但是,它也带来一个警告,因为没有一个严格的 Ingress 控制器规范,并且不是每个控制器都以相同的方式实现 Ingress 规则。一些控制器忽略了 PathType 字段,因此如果您依赖于它来构建具有精确路径的访问列表,您可能会发现如果切换到不同的 Ingress 控制器,您的网站将变成一个可以访问所有区域的网站。

Kubernetes 确实允许你运行多个入口控制器,在一个复杂的环境中,你可能需要这样做,为不同的应用提供不同的功能集。

15.3 比较入口控制器

入口控制器分为两类:反向代理,它们存在已久,在网络层工作,使用主机名获取内容;以及现代代理,它们具有平台意识,可以与其他服务集成(云控制器可以配置外部负载均衡器)。选择它们取决于功能集和你的技术偏好。如果你与 Nginx 或 HAProxy 有既定的关系,你可以在 Kubernetes 中继续使用它们。或者,如果你与 Nginx 或 HAProxy 有既定的关系,你可能很高兴尝试一个更轻量级、更现代的选项。

你的入口控制器成为你集群中所有应用的唯一公共入口点,所以它是集中处理常见问题的好地方。所有控制器都支持 SSL 终止,因此代理提供了安全层,你为所有应用都获得了 HTTPS。大多数控制器支持 Web 应用防火墙,因此你可以在代理层提供对 SQL 注入和其他常见攻击的保护。一些控制器具有特殊功能——我们之前已经使用 Nginx 作为缓存代理,你还可以在入口级别使用它进行缓存。

现在试试看:使用入口部署π应用,然后更新入口对象,使π应用利用入口控制器中的 Nginx 缓存。

# add the Pi app domain to the hosts file--Windows:
./add-to-hosts.ps1 pi.kiamol.local ingress-nginx

# OR Linux/macOS:
./add-to-hosts.sh pi.kiamol.local ingress-nginx

# deploy the app and a simple Ingress:
kubectl apply -f pi/

# browse to http://pi.kiamol.local?dp=30000 
# refresh and confirm the page load takes the same time

# deploy an update to the Ingress to use caching:
kubectl apply -f pi/update/ingress-with-cache.yaml

# browse to the 30K Pi calculation again--the first 
# load takes a few seconds, but now a refresh will be fast

在这个练习中,你会发现入口控制器是集群中的一个强大组件。你只需指定新的入口规则,就可以给你的应用添加缓存——无需更新应用本身,也无需管理新的组件。唯一的要求是,你的应用返回的 HTTP 响应包含正确的缓存头信息,这本来就应该做到。图 15.8 展示了我的输出,其中π的计算耗时 1.2 秒,但响应来自入口控制器的缓存,所以页面几乎瞬间就加载完成了。

图 15-8

图 15.8 如果你的入口控制器支持响应缓存,那将是一个简单的性能提升。

并非每个入口控制器都提供响应缓存,所以这并不是入口规范的具体部分。任何自定义配置都是通过注解来应用的,控制器会捕获这些注解。列表 15.5 展示了你在上一个练习中应用的更新缓存设置的元数据。如果你熟悉 Nginx,你会认出这些是你在配置文件中通常设置的代理缓存设置。

列表 15.5 ingress-with-cache.yaml,使用入口控制器中的 Nginx 缓存

apiVersion: networking.k8s.io/v1beta1 
kind: Ingress
metadata:               # The ingress controller looks in annotations for
  name: pi              # custom configuration--this adds proxy caching.
  annotations:              
    nginx.ingress.kubernetes.io/proxy-buffering: "on" 
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_cache static-cache;
      proxy_cache_valid 10m;

Ingress 对象中的配置适用于其所有规则,但如果你需要为应用的不同部分提供不同的功能,你可以有多个 Ingress 规则。这对于需要从 ingress 控制器获得更多帮助以在扩展时正常工作的待办事项列表应用来说也是如此。如果服务有多个 Pod,ingress 控制器会使用负载均衡,但待办事项应用有一些跨站伪造保护,如果创建新项目的请求被发送到最初渲染新项目页面的不同应用容器,则会中断。许多应用都有这样的限制,代理使用粘性会话来解决这种限制。

粘性会话是 ingress 控制器将来自同一终端用户的请求发送到同一容器的机制,这对于组件不是无状态的旧应用来说通常是必需的。在可能的情况下应避免使用它,因为它限制了集群的负载均衡潜力,所以在待办事项列表应用中,我们希望将其限制在仅一个页面上。图 15.9 显示了我们将应用于应用不同部分的 Ingress 规则。

图片 15-9

图 15.9 一个域可以使用多个 Ingress 规则进行映射,使用不同的代理功能。

我们现在可以将待办事项应用程序扩展以了解问题,然后应用更新的 Ingress 规则来修复它。

现在试试看 将待办事项应用程序扩展以确认在没有粘性会话的情况下它会崩溃,然后部署图 15.9 中的更新后的 Ingress 规则,并确认一切恢复正常。

# scale up--the controller load-balances between the Pods:
kubectl scale deploy/todo-web --replicas 3

# wait for the new Pods to start:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web

# browse to http://todo.kiamol.local/new, and add an item
# this will fail and show a 400 error page

# print the application logs to see the issue:
kubectl logs -l app=todo-web --tail 1 --since 60s

# update Ingress to add sticky sessions:
kubectl apply -f todo-list/update/ingress-sticky.yaml

# browse again, and add a new item--this time it works

你可以在图 15.10 中看到我的输出,但除非你自己运行这个练习,否则你只能相信我的话,来确定哪个是“之前”的截图,哪个是“之后”的截图。增加应用副本的数量意味着来自 ingress 控制器的请求会被负载均衡,这会触发反伪造错误。应用粘性会话停止在新项目路径上的负载均衡,因此用户的请求总是被路由到同一个 Pod,伪造检查通过。

图片 15-10

图 15.10 代理功能既可以解决问题,也可以提高性能。

待办事项应用的 Ingress 资源使用主机、路径和注解的组合来设置所有规则和要应用的功能。在幕后,控制器的任务是把这些规则转换成代理配置,在 Nginx 的情况下意味着编写一个配置文件。控制器有很多优化来最小化文件写入和配置重新加载的次数,但结果是,Nginx 配置文件非常复杂。如果你选择 Nginx ingress 控制器是因为你有 Nginx 经验,并且你愿意调试配置文件,那么你可能会遇到一个不愉快的惊喜。

现在试试看 Nginx 配置位于 ingress 控制器 Pod 中的文件中。在 Pod 中运行一个命令来检查文件的大小。

# run the wc command to see how many lines are in the file:
kubectl exec -n kiamol-ingress-nginx deploy/ingress-nginx-controller -- sh -c 'wc -l /etc/nginx/nginx.conf' 

图 15.11 显示我的 Nginx 配置文件中有多达 1,700 行。如果你运行 cat 而不是 wc,你会发现即使你熟悉 Nginx,内容也很奇怪。(控制器使用 Lua 脚本,因此可以在不重新加载配置的情况下更新端点。)

图 15-11

图 15.11 生成的 Nginx 配置文件并不是为了便于人类阅读的。

入口控制器拥有这种复杂性,但它是你解决方案的关键部分,你需要对你的代理的故障排除和调试方式感到满意。这时,你可能想要考虑一个平台感知的替代入口控制器,它不运行在复杂的配置文件中。我们将在本章中查看 Traefik——它是一个自 2015 年推出以来越来越受欢迎的开源代理。Traefik 理解容器,并从平台 API 构建其路由列表,原生支持 Docker 和 Kubernetes,因此它没有配置文件需要维护。

Kubernetes 支持在单个集群中运行多个入口控制器。它们将作为负载均衡器服务公开,因此在生产环境中,你可能为不同的入口控制器有不同的 IP 地址,你需要在 DNS 配置中将域名映射到入口。在我们的实验室环境中,我们将回到使用不同的端口。我们将首先部署 Traefik,为入口控制器服务使用自定义端口。

现在试试看:在集群中将 Traefik 部署为额外的入口控制器。

# create the Traefik Deployment, Service, and security resources: 
kubectl apply -f ingress-traefik/

# get the URL for the Traefik UI running in the ingress controller:
kubectl get svc ingress-traefik-controller -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080' -n kiamol-ingress-traefik

# browse to the admin UI to see the routes Traefik has mapped

在那个练习中,你会看到 Traefik 有一个管理界面。它显示了代理正在使用的路由规则,并且随着流量的通过,它可以收集并显示性能指标。与 Nginx 配置文件相比,它要容易操作得多。图 15.12 展示了两个 路由器,这是 Traefik 管理的进入路由。如果你探索仪表板,你会看到那些不是入口路由;它们是 Traefik 自身仪表板的内部路由——Traefik 没有在集群中获取任何现有的入口规则。

图 15-12

图 15.12 Traefik 是一个容器原生代理,它从平台构建路由规则,并有一个用户界面来显示它们。

为什么 Traefik 没有为待办事项列表或 Pi 应用程序构建一组路由规则?如果我们配置不同,它就会这样做,并且所有现有路由都将通过 Traefik 服务可用,但你不会使用多个入口控制器,因为它们最终会为进入请求而争斗。你运行多个控制器以提供不同的代理功能,你需要应用程序选择使用哪一个。你可以通过 入口类 来做到这一点,它与存储类有类似的概念。Traefik 已经部署了一个命名的入口类,并且只有请求该类的入口对象将通过 Traefik 路由。

图 15-13

图 15.13 入口控制器的工作方式不同,你的路由模型需要相应地改变。

Ingress 类不是 Ingress 控制器之间唯一的区别,你可能需要为不同的代理对路由进行相当不同的建模。图 15.13 展示了在 Traefik 中 to-do 应用需要如何配置。在 Traefik 中没有响应缓存,所以我们不会为静态资源获得缓存,并且粘性会话是在服务级别配置的,因此我们需要为新的项目路由添加一个额外的服务。

该模型与图 15.9 中的 Nginx 路由有显著不同,所以如果你确实计划运行多个 Ingress 控制器,你需要认识到配置错误的高风险,因为团队可能会混淆不同的功能和方法。Traefik 使用 Ingress 资源上的注解来配置路由规则。列表 15.6 显示了新项目路径的规范,它选择 Traefik 作为 Ingress 类,并使用注解进行精确路径匹配,因为 Traefik 不支持 PathType 字段。

列表 15.6 ingress-traefik.yaml,选择带有 Traefik 注解的 Ingress 类

apiVersion: networking.k8s.io/v1beta1 
kind: Ingress
metadata:                               # Annotations select the Traefik 
  name: todo2-new                       # ingress class and apply exact
  annotations:                          # path matching.
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/router.pathmatcher: Path
spec:
  rules:
  - host: todo2.kiamol.local            # Uses a different host so the app 
    http:                               # stays avaialable through Nginx
      paths:
      - path: /new
        backend:
          serviceName: todo-web-sticky  # Uses the Service that has sticky
          servicePort: 80               # sessions configured for Traefik

我们将使用不同的主机名部署一套新的 Ingress 规则,这样我们就可以通过 Nginx 或 Traefik 将流量路由到同一组 to-do 列表 Pods。

现在尝试一下 通过 Traefik Ingress 控制器发布 to-do 应用,使用图 15.13 中建模的 Ingress 路由。

# add a new domain for the app--on Windows:
./add-to-hosts.ps1 todo2.kiamol.local ingress-traefik

# OR on Linux/macOS:
./add-to-hosts.sh todo2.kiamol.local ingress-traefik

# apply the new Ingress rules and sticky Service:
kubectl apply -f todo-list/update/ingress-traefik.yaml

# refresh the Traefik admin UI to confirm the new routes
# browse to http://todo2.kiamol.local:8015

Traefik 监视来自 Kubernetes API 服务器的事件,并自动刷新其路由列表。当你部署新的 Ingress 对象时,你将在 Traefik 仪表板中看到显示为路由器的路径,链接到后端服务。图 15.14 显示了部分路由列表,以及通过新 URL 可用的 to-do 应用。

图片

图 15.14 Ingress 控制器从不同的配置模型中实现相同的目标。

如果你正在评估 Ingress 控制器,你应该考虑建模你的应用程序路径的简便性,以及故障排除方法和代理的性能。在专用环境中双运行的控制器有助于这一点,因为你可以隔离其他因素,并使用相同的应用程序组件进行对比。一个更真实的应用程序将具有更复杂的 Ingress 规则,你将希望对控制器实现诸如速率限制、URL 重写和客户端 IP 访问列表等功能的实现感到舒适。

Ingress 的另一个主要功能是在不配置应用程序中的证书和安全设置的情况下通过 HTTPS 发布应用程序。这是 Ingress 控制器之间一致的一个领域,在下一节中,我们将通过 Traefik 和 Nginx 来看到这一点。

15.4 使用 Ingress 通过 HTTPS 保护你的应用

您的 Web 应用程序应通过 HTTPS 发布,但加密需要服务器证书,而证书是敏感数据项。将 HTTPS 作为入口关注点是一个好习惯,因为它集中管理证书。入口资源可以在 Kubernetes 机密(TLS 是传输层安全,HTTPS 的加密机制)中指定 TLS 证书。将 TLS 从应用团队移除意味着您可以有一个标准的方法来提供、保护和续订证书——您也不必花费时间解释为什么在容器镜像中打包证书是一个糟糕的想法。

所有入口控制器都支持从机密加载 TLS 证书,但 Traefik 使其更加简单。如果您想在开发和测试环境中使用 HTTPS 而不配置任何机密,Traefik 在运行时可以生成自己的自签名证书。您可以通过入口规则中的注解来配置它,以启用 TLS 和默认证书解析器。

现在尝试一下 使用 Traefik 生成的证书是测试您的应用程序通过 HTTPS 的快速方法。它通过在入口对象中启用更多注解来实现。

# update the Ingress to use Traefik’s own certifcate:
kubectl apply -f todo-list/update/ingress-traefik-certResolver.yaml

# browse to https://todo2.kiamol.local:9443
# you’ll see a warning in your browser

浏览器不喜欢自签名证书,因为任何人都可以创建它们——没有可验证的授权链。当您第一次浏览到该网站时,您会看到一个大的警告,告诉您它不安全,但您可以继续,待办事项应用将加载。如图 15.15 所示,该网站使用 HTTPS 加密,但有一个警告,让您知道它实际上并不安全。

图 15.15

图 15.15 并非所有 HTTPS 都是安全的——自签名证书适用于开发和测试环境。

您的组织可能对证书有自己的看法。如果您能够控制提供过程,您可以拥有一个完全自动化的系统,其中您的集群从证书颁发机构(CA)获取短期证书,安装它们,并在需要时续订它们。Let’s Encrypt 是一个很好的选择:它通过一个易于自动化的过程颁发免费证书。Traefik 与 Let’s Encrypt 有原生集成;对于其他入口控制器,您可以使用开源的 cert-manager 工具(cert-manager.io),它是一个 CNCF 项目。

尽管如此,并非每个人都准备好自动化提供过程。一些颁发者要求人工下载证书文件,或者您的组织可能为非生产域从其自己的证书颁发机构创建证书文件。然后您需要将 TLS 证书和密钥文件作为机密在集群中部署。这种情况很常见,所以我们在下一个练习中会演示如何生成自己的证书。

现在尝试一下 运行一个生成自定义 TLS 证书的 Pod,并连接到 Pod 以将证书文件作为机密部署。Pod 规范配置为连接到其运行的 Kubernetes API 服务器。

# run the Pod--this generates a certificate when it starts:
kubectl apply -f ./cert-generator.yaml

# connect to the Pod:
kubectl exec -it deploy/cert-generator -- sh

# inside the Pod, confirm the certificate files have been created:
ls

# rename the certificate files--Kubernetes requires specific names:
mv server-cert.pem tls.crt
mv server-key.pem tls.key

# create and label a Secret from the certificate files:
kubectl create secret tls kiamol-cert --key=tls.key --cert=tls.crt
kubectl label secret/kiamol-cert kiamol=ch15

# exit the Pod:
exit

# back on the host, confirm the Secret is there:
kubectl get secret kiamol-cert --show-labels

该练习模拟了有人向您提供作为一对 PEM 文件的 TLS 证书的情况,您需要将其重命名并用作在 Kubernetes 中创建 TLS 机密的输入。证书生成全部使用名为 OpenSSL 的工具完成,将其在 Pod 内部运行的唯一原因是为了打包工具和脚本,使其易于使用。图 15.16 显示了我的输出,其中在集群中创建了一个机密,该机密可以被 Ingress 对象使用。

图 15.16 如果您从证书发行者那里收到 PEM 文件,您可以将它们创建为 TLS 机密。

使用入口控制器支持 HTTPS 很简单。您只需在 Ingress 规范中添加一个 TLS 部分,并声明要使用的 Secret 名称——这就完成了。列表 15.7 显示了 Traefik 入口的更新,它将新证书应用于todo2.kiamol .local主机。

列表 15.7 ingress-traefik-https.yaml,使用标准的 Ingress HTTPS 功能

spec:
  rules:
  - host: todo2.kiamol.local
    http:
      paths:
      - path: /new
        backend:
          serviceName: todo-web-sticky
          servicePort: 80
  tls:                              # The TLS section switches on HTTPS
   - secretName: kiamol-cert        # using the certificate in this Secret.

带有 Secret 名称的 TLS 字段就是您所需要的,并且它在所有入口控制器之间都是可移植的。当您部署更新的 Ingress 规则时,网站将通过 HTTPS 使用您的自定义证书提供服务。您仍然会从浏览器收到安全警告,因为证书颁发机构不受信任,但如果您的组织有自己的 CA,那么它将被您的机器和组织证书所信任。

现在试试看 更新待办事项列表中的 Ingress 对象,以使用 Traefik 入口控制器和您自己的 TLS 证书发布 HTTPS。

# apply the Ingress update:
kubectl apply -f todo-list/update/ingress-traefik-https.yaml

# browse to https://todo2.kiamol.local:9443
# there’s still a warning, but this time it’s because 
# the KIAMOL CA isn’t trusted

您可以在图 15.17 中看到我的输出。我在一个屏幕上打开了证书详情,以确认这是我的“kiamol”证书。我在第二个屏幕上接受了警告,现在待办事项列表的流量现在已通过自定义证书加密。生成证书的脚本将其设置为我们在本章中使用的所有kiamol.local域名,因此证书对该地址有效,但它不是来自受信任的发行者。

图 15.17 入口控制器可以从 Kubernetes 机密中应用 TLS 证书。如果证书来自受信任的发行者,则网站将是安全的

我们将切换回 Nginx 进行最后的练习——使用与 Nginx 入口控制器相同的证书,只是为了展示过程是相同的。更新的 Ingress 规范使用与之前 Nginx 部署相同的规则,但现在它们添加了与列表 15.7 相同的 Secret 名称的 TLS 字段。

现在试试看 更新待办事项的 Ingress 规则以 Nginx,以便应用程序可以通过标准端口 443(Nginx 入口控制器正在使用)使用 HTTPS 进行访问。

# update the Ingress resources:
kubectl apply -f todo-list/update/ingress-https.yaml

# browse to https://todo.kiamol.local
# accept the warnings to view the site

# confirm that the HTTP requests are redirected to HTTPS:
curl http://todo.kiamol.local

在我运行那个练习时,我作弊了,并将 Kiamol CA 添加到浏览器中信任的发行者列表中。你可以在图 15.18 中看到,该网站显示为安全,没有任何警告,这就是你看到组织自己的证书时的样子。你还可以看到入口控制器将 HTTP 请求重定向到 HTTPS——curl命令中的 308 重定向响应由 Nginx 处理。

图片

图 15.18 TLS 入口配置与 Nginx 入口控制器的工作方式相同。

Ingress 的 HTTPS 部分稳固且易于使用,在章节结束时留下一个积极的印象是很好的。但是,使用入口控制器具有很多复杂性,在某些情况下,你可能会花更多的时间来制定入口规则,而不是建模应用程序的部署。

15.5 理解入口和入口控制器

你几乎肯定会在你的集群中运行入口控制器,因为它集中管理域名路由,并将 TLS 证书管理从应用程序中移除。Kubernetes 模型使用一个通用的入口规范和一个可插拔的实现,非常灵活,但用户体验并不直观。入口规范仅记录最基本的路由细节,要使用代理的更高级功能,你需要添加配置注释的块。

这些注释是不可移植的,并且没有为入口控制器必须支持的功能提供接口规范。如果你想从 Nginx 迁移到 Traefik 或 HAProxy 或 Contour(一个在撰写本章的同一天被 CNCF 接受的开源项目),将会有一个迁移项目,你可能发现你需要的功能并不全部可用。Kubernetes 社区意识到入口的限制,并正在开发一个长期替代方案,称为服务 API,但截至 2021 年,这仍然处于早期阶段。

这并不是说应该避免使用入口——目前这是最佳选择,并且可能在未来许多年里都是生产环境的选择。评估不同的入口控制器,然后确定一个单一选项是值得的。Kubernetes 支持多个入口控制器,但如果使用不同的实现并需要管理具有不兼容功能集的入口规则集,麻烦就会真正开始。在本章中,我们探讨了 Nginx 和 Traefik,它们都是不错的选择,但还有许多其他选项,包括有支持合同的商业选项。

我们现在完成了入口,因此我们可以整理集群,为实验室做准备。

现在尝试一下 清除入口命名空间和应用程序资源。

kubectl delete ns,all,secret,ingress -l kiamol=ch15

15.6 实验室

这里有一个很好的实验室供你完成,遵循第十三章和第十四章的模式。你的任务是为每日天文图片应用程序构建入口规则。简单……

  • 首先在lab/ingress-nginx文件夹中部署入口控制器。

  • 入口控制器限制在单个命名空间中查找入口对象,因此您需要找出是哪一个,并将lab/apod/文件夹部署到那个命名空间。

  • 网站应用应发布在 www.apod.local,API 则在 api.apod .local

  • 我们希望防止分布式拒绝服务攻击,因此您应该使用入口控制器中的速率限制功能来防止来自同一 IP 地址的过多请求。

  • 入口控制器使用自定义类名,因此您还需要找到那个类名。

这部分内容涉及深入挖掘入口控制器配置以及控制器的文档——请注意,存在两个 Nginx 入口控制器。在本章中,我们使用了来自 Kubernetes 项目的那个,但 Nginx 项目也发布了一个替代版本。我的解决方案已经准备好供您检查:github.com/sixeyed/kiamol/blob/master/ch15/lab/README.md

16 使用策略、上下文和准入控制确保应用程序安全

容器是围绕应用程序进程的轻量级包装。它们启动速度快,并且由于它们使用运行在其上的机器的操作系统内核,因此对应用程序的额外开销很小。这使得它们超级高效,但代价是强大的隔离性——容器可能会被破坏,一个被破坏的容器可能会为服务器以及运行在其上的所有其他容器提供不受限制的访问。Kubernetes 有许多功能来确保你的应用程序安全,但默认情况下,它们都没有启用。在本章中,你将学习如何使用 Kubernetes 中的安全控制,以及如何设置你的集群,以便这些控制对所有工作负载都是必需的。

在 Kubernetes 中确保应用程序安全是关于限制容器可以做什么,所以如果攻击者利用应用程序漏洞在容器中运行命令,他们无法超出该容器。我们可以通过限制对其他容器和 Kubernetes API 的网络访问,限制主机文件系统的挂载,以及限制容器可以使用操作系统功能来做到这一点。我们将介绍基本方法,但安全领域很大且在不断发展。本章甚至比其他章节更长——你即将学到很多东西,但这只是你通往安全 Kubernetes 环境的旅程的开始。

16.1 使用网络策略确保通信安全

限制网络访问是确保应用程序安全的最简单方法之一。Kubernetes 具有扁平的联网模型,其中每个 Pod 都可以通过其 IP 地址到达其他 Pod,并且服务在整个集群中都是可访问的。没有理由让 Pi 网络应用程序访问待办事项数据库,或者让 Hello, World 网络应用程序使用 Kubernetes API,但默认情况下,它们可以。你在第十五章中学习了如何使用 Ingress 资源来控制对 HTTP 路由的访问,但这仅适用于进入集群的外部流量。你还需要控制集群内的访问,为此,Kubernetes 提供了网络策略

网络策略就像防火墙规则一样工作,在端口级别阻止到或从 Pod 的流量。规则是灵活的,并使用标签选择器来识别对象。你可以部署一个全面拒绝所有流量的策略来阻止所有 Pod 的出站流量,或者你可以部署一个策略,限制对 Pod 的指标端口的入站流量,以便只能从监控命名空间中的 Pod 访问。图 16.1 显示了这在集群中的样子。

图 16-1

图 16.1 网络策略规则是细粒度的——你可以应用集群默认值,并通过 Pod 覆盖来应用。

NetworkPolicy 对象是独立的资源,这意味着它们可以被安全团队在应用程序之外建模,或者它们可以被产品团队构建。或者,当然,每个团队可能都认为其他团队已经覆盖了,应用在没有策略的情况下进入生产,这是一个问题。我们将部署一个没有策略就通过的 app,并查看它存在的问题。

现在试试 Deploy 天文图片每日一图(APOD)应用,并确认应用组件可以被任何 Pod 访问。

# switch to the chapter folder:
cd ch16

# deploy the APOD app:
kubectl apply -f apod/

# wait for it to be ready:
kubectl wait --for=condition=ContainersReady pod -l app=apod-web

# browse to the Service on port 8016 if you want to see today’s
# picture

# now run a sleep Pod:
kubectl apply -f sleep.yaml

# confirm that the sleep Pod can use the API:
kubectl exec  deploy/sleep -- curl -s http://apod-api/image

# read the metrics from the access log:
kubectl exec deploy/sleep -- sh -c 'curl -s http://apod-log/metrics | head -n 2'

你可以清楚地在这个练习中看到问题——整个集群都是开放的,所以从 sleep Pod,你可以访问 APOD API 和访问日志组件的指标。图 16.2 显示了我的输出。让我们明确一点,sleep Pod 没有什么特别之处;它只是演示问题的简单方式。集群中的任何容器都可以做同样的事情。

图 16.2 Kubernetes 平面网络模型的缺点是每个 Pod 都是可访问的。

Pods 应该被隔离,以便它们只接收需要访问它们的组件的流量,并且只向它们需要访问的组件发送流量。网络策略通过入口规则(不要与入口资源混淆)来模拟这一点,这些规则限制进入的流量,以及出口规则,这些规则限制出去的流量。在 APOD 应用中,唯一应该能够访问 API 的组件是 Web 应用。列表 16.1 显示了在 NetworkPolicy 对象中的入口规则。

列表 16.1 networkpolicy-api.yaml,通过标签限制对 Pod 的访问

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: apod-api
spec:
  podSelector:             # This is the Pod where the rule applies.
    matchLabels:
      app: apod-api       
  ingress:                 # Rules default to deny, so this rule
  - from:                  # denies all ingress except where the 
    - podSelector:         # source of the traffic is a Pod with 
        matchLabels:       # the apod-web label.
          app: apod-web
    ports:                 # This restriction is by port.
    - port: api            # The port is named in the API Pod spec.

NetworkPolicy 规范相当简单,规则可以在应用程序部署之前部署,这样 Pod 启动时就是安全的。入口和出口规则遵循相同的模式,并且两者都可以使用命名空间选择器和 Pod 选择器。你可以创建全局规则,然后在应用程序级别用更细粒度的规则覆盖它们。

网络策略的一个大问题——当你部署规则时,它们可能不会做任何事情。就像 Ingress 对象需要一个入口控制器来对其执行操作一样,NetworkPolicy 对象依赖于集群中的网络实现来强制执行它们。当你在这个下一个练习中部署这个策略时,你可能会失望地发现 APOD API 仍然没有限制为 Web 应用。

现在试试 Apply 网络策略,看看你的集群是否真的强制执行了它。

# create the policy:
kubectl apply -f apod/update/networkpolicy-api.yaml

# confirm it is there:
kubectl get networkpolicy

# try to access the API from the sleep Pod--this is not permitted 
# by the policy:
kubectl exec deploy/sleep -- curl http://apod-api/image

你可以在图 16.3 中看到 sleep Pod 可以访问 API——限制进入 Web Pods 的 NetworkPolicy 被完全忽略。我正在 Docker Desktop 上运行这个,但你在 K3s、AKS 或 EKS 的默认设置中也会得到相同的结果。

图 16.3 在你的 Kubernetes 集群中,网络设置可能不会强制执行网络策略。

Kubernetes 的网络层是可插拔的,并不是每个网络插件都支持 NetworkPolicy 的强制执行。标准集群部署中的简单网络没有支持,所以你可能会遇到这样一个棘手的情况:你可以部署所有的 NetworkPolicy 对象,但你不知道它们是否被强制执行,除非你测试它们。云平台在这里有不同的支持级别。当你创建 AKS 集群时,你可以指定网络策略选项;对于 EKS,你需要在创建集群后手动安装不同的网络插件。

对于跟随这些练习(以及我编写它们)的你来说,这都很令人沮丧,但对于在生产中使用 Kubernetes 的组织来说,这会导致一个更加危险的脱节。你应该在构建周期的早期就采用安全控制措施,以便在开发和测试环境中应用 NetworkPolicy 规则,以运行接近生产配置的应用。配置不当的网络策略可能会轻易破坏你的应用,但如果你在非生产环境中不强制执行策略,你可能不会知道这一点。

如果你想看到 NetworkPolicy 的实际应用,接下来的练习将创建一个使用 Calico 的自定义集群,Calico 是一个开源的网络插件,它强制执行策略。你需要安装 Docker 和 Kind 命令行才能完成这个练习。警告:这个练习会改变 Docker 的 Linux 配置,并使你的原始集群无法使用。Docker Desktop 用户可以通过点击 重置 Kubernetes 按钮来修复一切,而 Kind 用户可以将他们的旧集群替换为新的,但其他配置可能不那么幸运。跳过这些练习,只阅读我的输出是可以的;我们将在下一节切换回你的正常集群。

现在尝试一下 创建一个新的 Kind 集群,并部署一个自定义网络插件。

# install the Kind command line using instructions at 
# https://kind.sigs.k8s.io/docs/user/quick-start/

# create a new cluster with a custom Kind configuration:
kind create cluster --image kindest/node:v1.18.4 --name kiamol-ch16
 --config kind/kind-calico.yaml

# install the Calico network plugin:
kubectl apply -f kind/calico.yaml

# wait for Calico to spin up:
kubectl wait --for=condition=ContainersReady pod -l k8s-app=calico-node -n kube-system

# confirm your new cluster is ready:
kubectl get nodes

图 16.4 中的输出被简化了;你将在 Calico 部署中看到许多更多被创建的对象。最后,我有一个新的集群,它强制执行网络策略。不幸的是,了解你的集群是否使用了一个强制执行策略的网络插件,唯一的办法是设置一个你知道强制执行策略的网络插件。

图 16.4 安装 Calico 为你提供了一个具有网络策略支持的集群——但代价是牺牲了你的其他集群。

现在我们可以再次尝试。这个集群是完全新的,没有任何服务在运行,但当然,Kubernetes 清单是可移植的,所以我们可以快速重新部署 APOD 应用并尝试它。(Kind 支持运行不同 Kubernetes 版本和不同配置的多个集群,因此它是测试环境的一个很好的选择,但它不如 Docker Desktop 或 K3s 对开发者友好)。

现在尝试一下 重复 APOD 和 sleep 部署,并确认网络策略阻止了未经授权的流量。

# deploy the APOD app to the new cluster:
kubectl apply -f apod/

# wait for it to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=apod-web

# deploy the sleep Pod:
kubectl apply -f sleep.yaml

# confirm the sleep Pod has access to the APOD API:
kubectl exec deploy/sleep -- curl -s http://apod-api/image

# apply the network policy:
kubectl apply -f apod/update/networkpolicy-api.yaml

# confirm the sleep Pod can’t access the API:
kubectl exec deploy/sleep -- curl -s http://apod-api/image

# confirm the APOD web app still can:
kubectl exec deploy/apod-web -- wget -O- -q http://apod-api/image

图 16.5 显示了我们的第一次预期:只有 APOD 网络应用可以访问 API,当睡眠应用尝试连接时超时,因为网络插件阻止了流量。

图 16.5 Calico 强制执行策略,因此只有来自 Web Pod 的流量被允许访问 API Pod。

网络策略是 Kubernetes 中的一个重要安全控制,对于习惯于防火墙和隔离网络的架构团队来说很有吸引力。但如果你选择采用它们,你需要了解策略在你的开发工作流程中的位置。如果工程师在没有强制执行的情况下运行自己的集群,而你只在管道的后期应用策略,你的环境配置非常不同,某些东西将会被破坏。

我在这里只介绍了 NetworkPolicy API 的基本细节,因为复杂性更多地在于集群配置而不是策略资源。如果你想进一步探索,有一个由 Google 的工程师 Ahmet Alp Balkan 发布的充满网络策略菜谱的 GitHub 仓库非常棒:github.com/ahmetb/kubernetes-network-policy-recipes

现在让我们清理新的集群,看看你的旧集群是否仍然工作。

现在试试看 移除 Calico 集群,看看旧集群是否仍然可访问。

# delete the new cluster:
kind delete cluster --name kiamol-ch16

# list your Kubernetes contexts:
kubectl config get-contexts

# switch back to your previous cluster:
kubectl config set-context <your_old_cluster_name>

# see if you can connect:
kubectl get nodes

你的前一个集群可能因为 Calico 所做的网络更改而无法访问,即使 Calico 现在没有运行。图 16.6 显示我即将在 Docker Desktop 中点击重置 Kubernetes按钮;如果你使用 Kind,你需要删除并重新创建你的原始集群,如果你使用其他东西并且它不起作用……我确实警告过你。

图 16.6 在容器中运行的 Calico 能够重新配置我的网络并破坏事物。

现在我们都恢复正常了(希望如此),我们可以继续进行保护容器本身,这样应用程序就不会有重新配置网络栈等特权。

16.2 使用安全上下文限制容器功能

容器安全实际上关于 Linux 安全以及容器用户的访问模型(Windows Server 容器有一个不同的用户模型,没有相同的问题)。Linux 容器通常以root超级管理员账户运行,除非你明确配置用户,容器内的 root 也是主机上的 root。如果一个攻击者能够从以 root 运行的容器中突破,那么他们现在就控制了你的服务器。这是一个所有容器运行时的问题,但 Kubernetes 又添加了一些它自己的问题。

在下一个练习中,你将使用基本的部署配置运行 Pi 网络应用。这个容器镜像是在微软的官方.NET Core 应用运行时镜像之上打包的。Pod 规范并非故意不安全,但你将看到默认设置并不令人鼓舞。

现在尝试一下:运行一个简单的应用程序,并检查默认的安全情况。

# deploy the app:
kubectl apply -f pi/

# wait for the container to start:
kubectl wait --for=condition=ContainersReady pod -l app=pi-web

# print the name of the user in the Pod container:
kubectl exec deploy/pi-web -- whoami 

# try to access the Kubernetes API server:
kubectl exec deploy/pi-web -- sh -c 'curl -k -s https://kubernetes.default | grep message'

# print the API access token:
kubectl exec deploy/pi-web -- cat /run/secrets/kubernetes.io/serviceaccount/token

这种行为令人担忧:应用程序以 root 身份运行,它有权访问 Kubernetes API 服务器,并且它甚至设置了一个令牌,以便它可以与 Kubernetes 进行身份验证。图 16.7 显示了所有这些操作。以 root 身份运行放大了攻击者可以在应用程序代码或运行时找到的任何漏洞。有权访问 Kubernetes API 意味着攻击者甚至不需要从容器中逃逸——他们可以使用令牌查询 API 并做些有趣的事情,比如获取 Secrets 的内容(取决于 Pod 的访问权限,你将在第十七章中了解这些内容)。

图 16.7

图 16.7 如果你听说过“默认安全”这个短语,那么它并不是针对 Kubernetes 说的。

Kubernetes 在 Pod 和容器级别提供了多个安全控制,但它们默认并未启用,因为它们可能会破坏你的应用程序。你可以以不同的用户身份运行容器,但有些应用程序只有在以 root 身份运行时才能工作。你可以降低 Linux 能力来限制容器能做什么,但这样一些应用程序功能可能会失败。这就是自动化测试发挥作用的地方,因为你可以不断加强应用程序的安全性,在每个阶段运行测试以确认一切仍然正常工作。

你将主要使用的是 SecurityContext 字段,它在 Pod 和容器级别应用安全策略。列表 16.2 展示了如何显式设置用户和 Linux 组(用户集合),因此 Pod 中的所有容器都以unknown用户身份运行,而不是 root 用户。

列表 16.2 deployment-podsecuritycontext.yaml,以特定用户运行

spec:                     # This is the Pod spec in the Deployment.
  securityContext:        # These controls apply to all Pod containers.
    runAsUser: 65534      # Runs as the “unknown” user
    runAsGroup: 3000      # Runs with a nonexistent group

这很简单,但离开 root 用户会有影响,Pi 规范需要一些更多的更改。应用程序在容器内部监听 80 端口,而 Linux 需要提升权限才能监听该端口。root 用户有权限,但新用户没有,所以应用程序将无法启动。需要在环境变量中进行一些额外的配置,将应用程序设置为监听 5001 端口,这对于新用户是有效的。这种细节你需要为每个应用程序或应用程序类别逐一解决,你将在应用程序停止工作时发现这些需求。

现在尝试一下:部署受保护 Pod 规范。这使用了一个非 root 用户和一个不受限制的端口,但服务中的端口映射隐藏了这一细节,对消费者来说是不可见的。

# add the nonroot SecurityContext:
kubectl apply -f pi/update/deployment-podsecuritycontext.yaml

# wait for the new Pod:
kubectl wait --for=condition=ContainersReady pod -l app=pi-web

# confirm the user:
kubectl exec deploy/pi-web -- whoami 

# list the API token files:
kubectl exec deploy/pi-web -- ls -l /run/secrets/kubernetes.io/serviceaccount/token

# print out the access token:
kubectl exec deploy/pi-web -- cat /run/secrets/kubernetes.io/serviceaccount/token

以非 root 用户身份运行可以解决应用程序利用风险升级为完全服务器接管的问题,但如图 16.8 所示,它并不能解决所有问题。Kubernetes API 令牌具有任何账户都可以读取的权限,因此攻击者仍然可以使用此设置中的 API。他们可以使用 API 做什么取决于你的集群是如何配置的——在 Kubernetes 的早期版本中,他们可以做任何事情。访问 API 服务器的身份与 Linux 用户不同,它可能在集群中拥有管理员权限,即使容器进程是以最低权限用户身份运行的。

Pod 规范中的一个选项阻止 Kubernetes 挂载访问令牌,你应该为每个实际上不需要使用 Kubernetes API 的应用程序包含此选项——这几乎将是一切,除了需要找到服务端点的工作负载,如入口控制器。这是一个安全的选项来设置,但下一级别的运行时控制需要更多的测试和评估。容器规范中的 SecurityContext 字段在 Pod 级别上提供了更细粒度的控制。列表 16.3 显示了适用于 Pi 应用程序的一组选项。

图片

图 16.8 你需要对 Kubernetes 采取深入的安全方法;一个设置是不够的。

列表 16.3 deployment-no-serviceaccount-token.yaml,更严格的安全策略

spec:    
  automountServiceAccountToken: false      # Removes the API token
  securityContext:                         # Applies to all containers
    runAsUser: 65534
    runAsGroup: 3000
  containers:
    - image: kiamol/ch05-pi
      # ...
      securityContext:                     # Applies for this container 
        allowPrivilegeEscalation: false    # The context settings block
        capabilities:                      # the process from escalating to
          drop:                            # higher privileges and drops
            - all                          # all additional capabilities.

capabilities 字段允许您显式添加和删除 Linux 内核能力。此应用程序在没有能力的情况下也能愉快地运行,但其他应用程序可能需要添加一些能力。此应用程序不支持的一个功能是readOnlyRootFilesystem选项。如果你的应用程序可以与只读文件系统一起工作,这是一个非常有用的选项,因为它意味着攻击者无法写入文件,因此他们无法下载恶意脚本或二进制文件。你采取多远取决于你组织的安全配置文件。你可以强制要求所有应用程序都需要以非 root 用户身份运行,所有能力都被删除,并且具有只读文件系统,但这可能意味着你需要重写大多数应用程序。

一种实用方法是尽可能在容器级别保护现有的应用程序,并确保你围绕其他政策和流程有深入的安全措施。Pi 应用程序的最终规范并不完美安全,但与默认设置相比有了很大的改进——并且应用程序仍然可以工作。

现在试试看 更新 Pi 应用程序的最终安全配置。

# update to the Pod spec in listing 16.3:
kubectl apply -f pi/update/deployment-no-serviceaccount-token.yaml

# confirm the API token doesn’t exist:
kubectl exec deploy/pi-web -- cat /run/secrets/kubernetes.io/serviceaccount/token

# confirm that the API server is still accessible:
kubectl exec deploy/pi-web -- sh -c 'curl -k -s https://kubernetes.default | grep message'

# get the URL, and check that the app still works:
kubectl get svc pi-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8031'

如图 16.9 所示,应用程序仍然可以访问 Kubernetes API 服务器,但它没有访问令牌,因此攻击者需要做更多工作才能发送有效的 API 请求。应用一个 NetworkPolicy 来拒绝 API 服务器的入站访问将完全移除这个选项。

图片

图 16.9 对于用户来说,安全的应用程序是一样的,但对于攻击者来说则乐趣大减。

您需要在您的应用程序中投资增加安全性,但如果您的应用程序平台范围相对较小,您可以构建通用的配置文件:您可能会发现所有您的 .NET 应用程序都可以以非 root 用户身份运行,但需要一个可写文件系统,而所有您的 Go 应用程序都可以使用只读文件系统运行,但需要添加一些 Linux 功能。那么,挑战就在于确保您的配置文件实际上被应用了,Kubernetes 有一个很好的功能来实现这一点:准入控制

16.3 使用 webhook 阻止和修改工作负载

您在 Kubernetes 中创建的每个对象都会经过一个检查过程,以确定集群是否可以运行该对象。这个过程是准入控制,我们在第十二章中看到了准入控制器在工作,尝试部署一个请求比命名空间可用资源更多的 Pod 规范。ResourceQuota 准入控制器是一个内置控制器,它会阻止超出配额的工作负载运行,Kubernetes 有一个插件系统,因此您可以添加自己的准入控制规则。

两个其他控制器增加了这种可扩展性:ValidatingAdmissionWebhook,它类似于 ResourceQuota,允许或阻止对象创建,以及 MutatingAdmissionWebhook,它可以实际编辑对象规范,因此创建的对象与请求不同。这两个控制器以相同的方式工作:您创建一个配置对象,指定您想要控制的对象生命周期和应用于规则的 web 服务器的 URL。图 16.10 显示了这些组件是如何组合在一起的。

图片

图 16.10 准入 webhook 允许在创建对象时应用您自己的规则。

准入 webhook 非常强大,因为 Kubernetes 会调用您自己的代码,该代码可以运行在任何您喜欢的语言中,并应用您需要的任何规则。在本节中,我们将应用一些我使用 Node.js 编写的 webhook。您不需要编辑任何代码,但您可以在列表 16.4 中看到代码并不特别复杂。

列表 16.4 validate.js,验证 webhook 的自定义逻辑

# the incoming request has the object spec--this checks to see
# if the service token mount property is set to false;
# if not, the response stops the object from being created:

if (object.spec.hasOwnProperty("automountServiceAccountToken")) {
    admissionResponse.allowed = 
   (object.spec.automountServiceAccountToken == false);
}

webhook 服务器可以在任何地方运行——集群内部或外部——但它们必须通过 HTTPS 提供服务。唯一的问题是如果您想在您的集群内部运行由您自己的证书颁发机构(CA)签名的 webhook,因为 webhook 配置需要一种信任 CA 的方式。这是一个常见的场景,所以我们将在这个练习中逐步解决这个复杂性。

现在尝试一下 首先创建一个证书并将 webhook 服务器部署以使用该证书。

# run the Pod to generate a certificate:
kubectl apply -f ./cert-generator.yaml

# when the container is ready, the certificate is done:
kubectl wait --for=condition=ContainersReady pod -l app=cert-generator

# the Pod has deployed the cert as a TLS Secret:
kubectl get secret -l kiamol=ch16

# deploy the webhook server, using the TLS Secret:
kubectl apply -f admission-webhook/

# print the CA certificate:
kubectl exec -it deploy/cert-generator -- cat ca.base64

那个练习中的最后一个命令会在您的屏幕上填充 Base64 编码的文本,您将在下一个练习中使用它(尽管不必担心记录下来;我们将自动化所有步骤)。现在您已经有了运行中的 webhook 服务器,由自定义 CA 签发的 TLS 证书进行保护。我的输出如图 16.11 所示。

图片

图片

Node.js 应用正在运行,并有两个端点:一个验证 webhook,它检查所有 Pod 规范是否将 automountServiceAccountToken 字段设置为 false,以及一个变异 webhook,它应用一个带有 runAsNonRoot 标志的容器 SecurityContext。这两个策略旨在协同工作,以确保所有应用程序都有一个基本的安全级别。列表 16.5 显示了 ValidatingWebhookConfiguration 对象的规范。

列表 16.5 validatingWebhookConfiguration.yaml,应用 webhook

apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  name: servicetokenpolicy
webhooks:
  - name: servicetokenpolicy.kiamol.net
    rules:                                   # These are the object 
      - operations: [ "CREATE", "UPDATE" ]   # types and operations
        apiGroups: [""]                      # that invoke the 
        apiVersions: ["v1"]                  # webhook--all Pods.
        resources: ["pods"]
    clientConfig:
      service:
        name: admission-webhook         # The webhook Service to call
        namespace: default
        path: "/validate"               # URL for the webhook
      caBundle: {{ .Values.caBundle }}  # The CA certificate

Webhook 配置是灵活的:您可以设置操作类型和 webhook 操作的对象类型。您可以针对同一对象配置多个 webhook——验证 webhook 都会并行调用,并且任何一个都可以阻止操作。此 YAML 文件是我为这个配置对象使用的一个 Helm 图表的一部分,作为注入 CA 证书的简单方法。一个更高级的 Helm 图表将包括一个生成证书并部署 webhook 服务器以及配置的任务——但那样您就看不到它们是如何结合在一起的。

现在试试看:部署 webhook 配置,将生成器 Pod 作为值传递给本地 Helm 图表中的 CA 证书。然后尝试部署一个应用程序,该应用程序会违反策略。

# install the configuration object:
helm install validating-webhook admission-webhook/helm/validating-webhook/
 --set caBundle=$(kubectl exec -it deploy/cert-generator
 -- cat ca.base64)

# confirm it’s been created:
kubectl get validatingwebhookconfiguration

# try to deploy an app:
kubectl apply -f vweb/v1.yaml

# check the webhook logs:
kubectl logs -l app=admission-webhook --tail 3

# show the ReplicaSet status for the app:
kubectl get rs -l app=vweb-v1

# show the details:
kubectl describe rs -l app=vweb-v1

在这个练习中,您可以看到验证 webhook 的优势和局限性。webhook 在 Pod 级别操作,如果 Pod 不匹配服务令牌规则,它会阻止 Pod 的创建。但是,是 ReplicaSet 和 Deployment 尝试创建 Pod,它们不会被 admission 控制器阻止,所以您需要深入挖掘以找到应用程序为何无法运行的原因。我的输出显示在图 16.12 中,其中 describe 命令被简化以仅显示错误行。

图 16.12

图 16.12 验证 webhook 可以阻止对象创建,无论是由用户还是控制器触发的。

您需要仔细思考您希望 webhook 作用的对象和操作。这种验证可以在 Deployment 级别进行,这将提供更好的用户体验,但它会错过直接创建或由其他类型的控制器创建的 Pods。在 webhook 响应中返回一个清晰的消息也很重要,这样用户就知道如何修复问题。ReplicaSet 将会不断尝试创建 Pod 并失败(在我写这段话的时候,我的集群已经尝试了 18 次),但失败信息告诉我该怎么做,而且这个问题很容易解决。

Admission webhook 的一个问题是它们的可发现性得分非常低。你可以使用 kubectl 来检查是否配置了任何验证 webhook,但这不会告诉你任何关于实际规则的信息,因此你需要将它们在集群外部进行文档化。当涉及到 mutating webhook 时,情况变得更加混乱,因为如果它们按预期工作,它们会向用户提供一个与尝试创建的对象不同的对象。在下一个练习中,你会看到一个有良好意图的 mutating webhook 可能会破坏应用。

现在试试看。使用相同的 webhook 服务器但不同的 URL 路径配置一个 mutating webhook。这个 webhook 为 Pod 规范添加安全设置。部署另一个应用,你会看到来自 webhook 的更改阻止了应用运行。

# deploy the webhook configuration:
helm install mutating-webhook admission-webhook/helm/mutating-webhook/
     --set caBundle=$(kubectl exec -it deploy/cert-generator -- cat ca.base64)

# confirm it’s been created:
kubectl get mutatingwebhookconfiguration

# deploy a new web app:
kubectl apply -f vweb/v2.yaml

# print the webhook server logs:
kubectl logs -l app=admission-webhook --tail 5

# show the status of the ReplicaSet:
kubectl get rs -l app=vweb-v2

# show the details:
kubectl describe pod -l app=vweb-v2

哎呀。mutating webhook 将一个带有runAsNonRoot字段设置为 true 的 SecurityContext 添加到 Pod 规范中。这个标志告诉 Kubernetes 不要在 Pod 中运行任何配置为以 root 身份运行的容器——这个应用就是这样,因为它基于官方的 Nginx 镜像,它确实使用了 root。正如你在图 16.13 中可以看到的,描述 Pod 会告诉你问题是什么,但它没有声明规范已经被修改。当用户再次检查他们的 YAML 文件并发现没有runAsNonRoot字段时,他们会非常困惑。

图 16.13

图 16.13 Mutating webhook 可能会导致应用失败,这些失败很难调试。

mutating webhook 内部的逻辑完全由你决定——你可能会意外地更改对象以设置一个无效的规范,它们将永远不会部署。为你的 webhook 配置设置一个更严格的对象选择器是个好主意。列表 16.5 适用于每个 Pod,但你也可以添加命名空间和标签选择器来缩小范围。这个 webhook 已经构建了合理的规则,如果 Pod 规范已经包含runAsNonRoot值,webhook 就会保持不变,这样应用就可以被建模为明确要求 root 用户。

Admission controller webhook 是一个有用的工具,你应该了解它,并且它让你可以做些很酷的事情。你可以使用 mutating webhook 向 Pod 添加 sidecar 容器,所以你可以使用标签来识别所有写入日志文件的应用,并让 webhook 自动将这些 Pod 添加一个日志 sidecar。Webhook 可能很危险,但你可以通过良好的测试和配置对象中的选择性规则来减轻这种风险,但它们始终是看不见的,因为逻辑隐藏在 webhook 服务器内部。

在下一节中,我们将探讨一种替代方法,它使用验证 webhook 作为底层,但将其包装在管理层中。Open Policy Agent (OPA)允许你在 Kubernetes 对象中定义你的规则,这些规则在集群中是可发现的,并且不需要自定义代码。

16.4 使用 Open Policy Agent 控制准入

OPA 是一种统一的方法来编写和实施策略。目标是提供一个标准语言来描述所有类型的策略,以及在不同平台上应用策略的集成。你可以描述数据访问策略并在 SQL 数据库中部署它们,你也可以描述 Kubernetes 对象的准入控制策略。OPA 是另一个 CNCF 项目,它为使用 OPA Gatekeeper 的自定义验证 webhook 提供了一个更干净的替代方案。

OPA Gatekeeper 有三个部分:你在你的集群中部署 Gatekeeper 组件,这些组件包括一个 webhook 服务器和一个通用的 ValidatingWebhookConfiguration;然后你创建一个 约束模板,它描述了准入控制策略;然后你基于模板创建一个具体的 约束。这是一个灵活的方法,你可以为策略“所有 Pod 必须具有预期的标签”构建一个模板,然后部署一个约束来指定在哪个命名空间中需要哪些标签。

我们将从移除我们添加的自定义 webhook 并部署 OPA Gatekeeper 开始,准备应用一些准入策略。

现在试试看 取消卸载 webhook 组件,并部署 Gatekeeper。

# remove the webhook configurations created with Helm:
helm uninstall mutating-webhook
helm uninstall validating-webhook

# remove the Node.js webhook server:
kubectl delete -f admission-webhook/

# deploy Gatekeeper:
kubectl apply -f opa/

我在图 16.14 中简化了我的输出——当你运行练习时,你会看到 OPA Gatekeeper 部署安装了许多更多的对象,包括我们尚未遇到的东西,称为 CustomResourceDefinitions(CRDs)。我们将在第二十章中详细讨论这些内容,当我们查看扩展 Kubernetes 时,但现在,了解 CRDs 允许你定义 Kubernetes 为你存储和管理的新类型对象就足够了。

图片

图 16.14 OPA Gatekeeper 负责处理运行 webhook 服务器的所有复杂部分。

Gatekeeper 使用 CRDs,因此你可以创建模板和约束作为普通的 Kubernetes 对象,这些对象在 YAML 中定义,并使用 kubectl 部署。模板包含在称为 Rego 的语言中定义的通用策略定义(发音为“ray-go”)。它是一种表达性语言,允许你评估某些输入对象的属性,以检查它们是否满足你的要求。这是一件需要学习的事情,但 Rego 有一些显著的优势:策略相对容易阅读,并且它们位于你的 YAML 文件中,因此它们不会隐藏在自定义 webhook 的代码中;并且有许多示例 Rego 策略来强制执行我们在本章中查看的规则。列表 16.6 显示了一个要求对象具有标签的 Rego 策略。

列表 16.6 requiredLabels-template.yaml,一个基本的 Rego 策略

# This fetches all the labels on the object and all the 
# required labels from the constraint; if required labels
# are missing, that’s a violation that blocks object creation.

violation[{"msg": msg, "details": {"missing_labels": missing}}] {
  provided := {label | input.review.object.metadata.labels[label]}
  required := {label | label := input.parameters.labels[_]}
  missing := required - provided
  count(missing) > 0
  msg := sprintf("you must provide labels: %v", [missing])
}

你使用 Gatekeeper 作为约束模板部署该策略,然后部署一个强制执行模板的约束对象。在这种情况下,名为 RequiredLabels 的模板使用参数来定义所需的标签。列表 16.7 显示了一个特定约束,要求所有 Pod 都必须有 appversion 标签。

列表 16.7 requiredLabels.yaml,来自 Gatekeeper 模板的一个约束

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: RequiredLabels             # The API and Kind identify this as
metadata:                        # a Gatekeeper constraint from the 
  name: requiredlabels-app       # RequiredLabels template.
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]           # The constraint applies to all Pods.
  parameters:
    labels: ["app", "version"]   # Requires two labels to be set

这更容易阅读,你可以从同一个模板部署许多约束。OPA 方法让你能够构建一个标准的策略库,用户可以在他们的应用程序规范中应用,而无需深入研究 Rego。在下一个练习中,你将部署列表 16.7 中的约束,以及另一个要求所有 Deployments、Services 和 ConfigMaps 都具有kiamol标签的约束。然后你将尝试部署一个待办事项应用程序的版本,该版本将违反所有这些策略。

现在尝试一下:使用 Gatekeeper 部署所需的标签策略,并查看它们是如何应用的。

# create the constraint template first: 
kubectl apply -f opa/templates/requiredLabels-template.yaml

# then create the constraint:
kubectl apply -f opa/constraints/requiredLabels.yaml

# the to-do list spec doesn’t meet the policies:
kubectl apply -f todo-list/

# confirm the app isn’t deployed:
kubectl get all -l app=todo-web

你可以在图 16.15 中看到,这个用户体验是干净的——我们试图创建的对象没有所需的标签,因此它们被阻止,我们在 kubectl 的输出中看到了 Rego 策略的消息。

图 16-15

图 16.15 部署失败显示了 Rego 策略返回的清晰错误消息。

Gatekeeper 使用验证 webhook 来评估约束,当你在创建的对象中遇到失败时,这非常明显。当由控制器创建的对象失败验证时,这就不那么明显了,因为控制器本身可能没有问题。我们在 16.3 节中看到了这一点,因为 Gatekeeper 使用相同的验证机制,所以它也有同样的问题。如果你更新待办事项应用程序,使其部署满足标签要求,但 Pod 规范不满足,你将会看到这一点。

现在尝试一下:部署一个更新的待办事项列表规范,其中所有对象(除了 Pod)都有正确的标签。

# deploy the updated manifest:
kubectl apply -f todo-list/update/web-with-kiamol-labels.yaml

# show the status of the ReplicaSet:
kubectl get rs -l app=todo-web

# print the detail:
kubectl describe rs -l app=todo-web

# remove the to-do app in preparation for the next exercise:
kubectl delete -f todo-list/update/web-with-kiamol-labels.yaml

在这个练习中,你会发现招生政策是有效的,但你只有在深入查看失败的 ReplicaSet 的描述时,如图 16.16 所示,才会看到问题。这并不是一个很好的用户体验。你可以通过一个更复杂的政策来修复这个问题,该政策在部署级别应用并检查 Pod 模板中的标签——这可以通过约束模板的 Rego 扩展逻辑来实现。

图 16-16

图 16.16 OPA Gatekeeper 提供了一个更好的流程,但它仍然是一个验证 webhook 的包装器。

我们将用以下一系列招生政策结束本节,这些政策涵盖了更多生产最佳实践,所有这些都有助于使你的应用程序更加安全:

  • 所有 Pod 都必须定义容器探测。这是为了保持你的应用程序健康,但失败的健康检查也可能表明攻击的意外活动。

  • Pods 只能从批准的镜像仓库运行容器。将容器限制在一系列“黄金”仓库中,这些仓库包含受保护的生产镜像,确保恶意有效载荷无法部署。

  • 所有容器都必须设置内存和 CPU 限制。这可以防止受损害的容器耗尽节点的计算资源,并使所有其他 Pod 饿死。

这些通用策略适用于几乎每个组织。您可以通过为每个应用程序的网络策略和每个 Pod 的安全上下文添加约束来扩展它们。正如您在本章中学到的,并非所有规则都是通用的,因此您可能需要选择性地应用这些约束。在下一个练习中,您将应用生产约束集到单个命名空间。

现在尝试一下 部署一组新的约束和一个待办事项应用程序版本,其中 Pod 规范大多数策略都失败了。

# create templates for the production constraints:
kubectl apply -f opa/templates/production/

# create the constraints:
kubectl apply -f opa/constraints/production/

# deploy the new to-do spec:
kubectl apply -f todo-list/production/

# confirm the Pods aren’t created:
kubectl get rs -n kiamol-ch16 -l app=todo-web

# show the error details:
kubectl describe rs -n kiamol-ch16 -l app=todo-web

图 16.17 显示 Pod 规范除了一个规则外都失败了——我的镜像仓库策略允许kiamol组织中的任何来自 Docker Hub 的镜像,因此待办事项应用程序的镜像有效。但没有版本标签,没有健康检查,没有资源限制,这个规范不适合生产。

图片

图 16.17 所有约束都已评估,您可以在 Rego 输出中看到完整的错误列表。

只为证明这些策略是可行的,OPA Gatekeeper 实际上会允许待办事项应用程序运行,您可以应用一个更新的规范,该规范符合所有生产规则。如果您比较生产文件夹和更新文件夹中的 YAML 文件,您会看到新规范只是向 Pod 模板添加了所需的字段;应用程序没有显著变化。

现在尝试一下 应用一个生产就绪版本的待办事项规范,并确认应用程序确实正在运行。

# this spec meets all production policies:
kubectl apply -f todo-list/production/update

# wait for the Pod to start:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web -n kiamol-ch16

# confirm it’s running:
kubectl get pods -n kiamol-ch16 --show-labels

# get the URL for the app and browse:
kubectl get svc todo-web -n kiamol-ch16
 -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8019'

图 16.18 显示了应用程序在更新部署被 OPA Gatekeeper 允许后正在运行。

图片

图 16.18 约束功能强大,但您需要确保应用程序实际上可以遵守。

Open Policy Agent 是应用准入控制比自定义验证 webhook 更干净的方法,我们查看的示例策略只是让您入门的一些简单想法。Gatekeeper 没有突变功能,但如果您有明确的案例来修改规范,您可以将它与您自己的 webhook 结合使用。您可以使用约束来确保每个 Pod 规范都包含一个应用程序配置文件标签,然后根据您的配置文件突变规范——将您的.NET Core 应用程序设置为以非 root 用户运行,并将所有 Go 应用程序切换到只读文件系统。

保护您的应用程序是关闭利用路径的过程,一个彻底的方法包括本章中涵盖的所有工具以及更多。我们将以查看一个安全的 Kubernetes 景观作为结束。

16.5 在 Kubernetes 中深入理解安全性

构建管道可能会被破坏,容器图像可能会被修改,容器可以以特权用户身份运行易受攻击的软件,并且能够访问 Kubernetes API 的攻击者甚至可以控制你的集群。直到应用程序被替换并且你可以确认在它的操作期间没有发生安全漏洞,你才知道你的应用程序是 100% 安全的。达到这个快乐的地方意味着在整个软件供应链中应用深度安全。本章重点介绍了运行时应用程序的安全性,但你应该在此之前开始扫描容器图像以查找已知漏洞。

安全扫描器会检查图像内部,识别二进制文件,并在 CVE(常见漏洞和暴露)数据库中进行检查。扫描结果会告诉你应用堆栈、依赖项或图像中的操作系统工具中是否存在已知的漏洞利用。商业扫描器与受管理的注册表集成(你可以使用与 Azure 容器注册表集成的 Aqua Security),或者你可以运行自己的(Harbor 是 CNCF 注册表项目,它支持开源扫描器 Clair 和 Trivy;Docker Desktop 与 Snyk 集成以进行本地扫描)。

你可以设置一个管道,只有当扫描结果为清白时,图像才会被推送到生产存储库。结合存储库准入策略,你可以有效地确保只有当图像安全时,容器才会运行。然而,在安全配置的容器中运行的图像仍然是一个目标,你应该使用一个工具来监控容器的异常活动,并可以生成警报或关闭可疑行为,以关注运行时安全性。Falco 是 CNCF 的运行时安全项目,Aqua 和 Sysdig(以及其他一些公司)提供了受支持的商业选项。

感到不知所措?你应该将 Kubernetes 的安全视为一个起点,这个起点是我在本章中介绍的技术。你可以首先采用安全上下文,然后是网络策略,当你清楚对你重要的规则时,再转向准入控制。基于角色的访问控制,我们在第十七章中介绍,是下一个阶段。如果你的组织有增强的安全要求,安全扫描和运行时监控是你可以采取的进一步步骤。但现在我不会给你更多的东西——让我们整理一下,为实验室做好准备。

现在尝试一下 删除我们创建的所有对象。

kubectl delete -f opa/constraints/ -f opa/templates/ -f opa/gatekeeper.yaml
kubectl delete all,ns,secret,networkpolicy -l kiamol=ch16

16.6 实验室

在本章的开头,我说过主机路径的卷挂载是一个潜在的攻击向量,但在练习中我们没有解决这个问题,所以我们在实验室中解决它。这是一个完美的准入控制场景,如果 Pod 使用挂载在主机上敏感路径的卷,它们应该被阻止。我们将使用 OPA Gatekeeper,我已经为你编写了 Rego,所以你只需要编写一个约束条件。

  • 首先在实验室文件夹中部署 gatekeeper.yaml

  • 然后部署 restrictedPaths-template.yaml 中的约束模板——你需要查看规范以了解如何构建你的约束条件。

  • 编写并部署一个使用模板并限制以下主机路径的约束://bin/etc。该约束应仅应用于带有标签 kiamol=ch16-lab 的 Pods。

  • 在实验室文件夹中部署 sleep.yaml。你的约束应该阻止 Pod 的创建,因为它使用了受限的卷挂载。

这个问题相当直接,尽管你可能需要阅读有关匹配表达式的相关内容,这是 Gatekeeper 实现标签选择器的方式。我的解决方案已上传至 GitHub:github.com/sixeyed/kiamol/blob/master/ch16/lab/README.md

第四周:纯 Kubernetes 和应用 Kubernetes

在本节最后部分,你将深入探讨技术主题,以完善你对 Kubernetes 的理解。你将学习 Kubernetes 的架构,了解如何控制容器的运行位置,以及如何通过自定义资源和新增功能扩展平台。你还将学习如何使用基于角色的访问控制来限制用户和应用程序在集群中可以执行的操作。本节结束时,将提供一些关于在 Kubernetes 之旅中下一步该去哪里以及如何成为 Kubernetes 社区一员的指导。

17 使用基于角色的访问控制保护资源

您对您的实验室集群拥有完全控制权:您可以部署工作负载、读取机密,甚至如果您想看看它们返回得有多快,还可以删除控制平面组件。您不希望在生产集群中让任何人拥有如此大的权力,因为如果他们拥有完整的管理员控制权,那么这实际上就是他们的集群。他们的账户可能会被破坏,然后某个恶意方会删除您所有的应用程序,并将您的集群变成他们的个人比特币矿工。Kubernetes 通过基于角色的访问控制(RBAC)支持最小权限访问。在本章中,您将了解 RBAC 是如何工作的以及随之而来的限制访问的一些挑战。

RBAC 适用于使用 kubectl 进行工作的最终用户以及使用服务账户令牌通过 Kubernetes API 进行内部组件的访问。您需要为这些不同的用途采用不同的 RBAC 方法,我们将在本章中介绍,包括最佳实践。您还将了解 Kubernetes 如何获取外部用户的凭证,以及如果您没有外部身份验证系统,您如何管理集群内的最终用户。RBAC 是一个简单的模型,但有很多可移动的部分,因此很难跟踪谁可以做什么,所以我们将通过查看管理工具来结束本章。

17.1 Kubernetes 如何保护对资源的访问

RBAC 通过授予对资源执行操作的权限来工作。每种资源类型都可以被保护,因此您可以设置权限来获取 Pod 详情、列出服务以及删除机密。您将权限应用于一个主题,这可能是用户、系统账户或组,但您不会直接应用它们,因为这会创建一个难以管理的权限蔓延。您在角色中设置权限,并通过角色绑定将角色应用于一个或多个主题,如图 17.1 所示。

图片

图 17.1 RBAC 是一种安全抽象;对象权限通过角色和绑定授予。

一些 Kubernetes 资源是特定于命名空间的,而有些是集群范围的,因此 RBAC 结构实际上有两套对象来描述和分配权限:Role 和 RoleBinding 对象在命名空间对象上工作,而 ClusterRole 和 ClusterRoleBinding 对象在整个集群上工作。从技术上讲,RBAC 是 Kubernetes 中的一个可选组件,但现在几乎在所有平台上都启用了它。您可以使用标准的 kubectl 命令来检查它是否已启用并查看一些默认的角色。

现在试试看。您可以通过打印它支持的 API 版本来检查您集群中的功能。Docker Desktop、K3s 以及所有云平台默认支持 RBAC。

# switch to this chapter’s folder:
cd ch17

# PowerShell doesn’t have a grep command; run this on Windows to add it:
. .\grep.ps1

# check that the API versions include RBAC:
kubectl api-versions | grep rbac

# show the admin cluster roles:
kubectl get clusterroles | grep admin 

# show the details of the cluster admin:
kubectl describe clusterrole cluster-admin

你可以在图 17.2 中看到许多内置的角色都是集群级别的。其中一个就是 cluster-admin,这就是你在你的实验室集群中的角色。它对所有资源上的所有操作(Kubernetes 称它们为动词)都有权限,这就是为什么你可以做任何你想做的事情。下一个最强大的角色是 admin,它在所有对象上几乎都有权限,但限制在单个命名空间内。

图 17-2

图 17.2 RBAC 为用户和服务账户提供了一组默认的角色和绑定。

这一切都很不错,但拥有 cluster-admin 角色?你不会用你的实验室的用户名和密码登录 kubectl,而且在 Kubernetes 中没有用户对象,所以 Kubernetes 是如何知道用户是谁的呢?Kubernetes 不验证最终用户;它依赖于外部身份提供者,并信任它们进行验证。在生产系统中,你的集群将被配置为使用你组织现有的身份验证系统——Active Directory (AD)、LDAP 和 OpenID Connect (OIDC)都是可行的选项。

云平台将 Kubernetes 与其自身的身份验证集成,因此 AKS 用户使用 Azure AD 账户进行身份验证。你可以配置自己的 OIDC 提供者,但设置相当复杂,所以在我们实验室集群中,我们将坚持使用证书。Kubernetes 可以为最终用户颁发客户端证书,你可以通过用户名来请求这些证书。当 Kubernetes API 服务器看到传入请求中的证书时,它会信任发行者(即自身)并接受用户是他们所说的那个人。我们将首先为将在集群中拥有有限访问权限的用户生成一个新的证书。

现在尝试一下 创建 Kubernetes 签名证书请求需要几个步骤,我已经在容器镜像中编写了脚本。从这个镜像中运行一个 Pod 来生成证书,然后将证书和密钥复制到你的本地机器上。

# run the certificate generator: 
kubectl apply -f user-cert-generator.yaml

# wait for the container to start:
kubectl wait --for=condition=ContainersReady pod user-cert-generator

# print the logs:
kubectl logs user-cert-generator --tail 3

# copy the files onto your local disk:
kubectl cp user-cert-generator:/certs/user.key user.key
kubectl cp user-cert-generator:/certs/user.crt user.crt

你会在第一个命令的输出中看到,练习创建了一些自己的角色和绑定。Pod 容器运行一个脚本,使用 kubectl 颁发客户端证书。这是一个特权操作,所以清单确保 Pod 有它需要的权限。步骤有点复杂,这就是为什么我把它们封装在一个容器镜像中——脚本user-cert-generator/start.sh执行工作,如果你想深入了解细节。我图 17.3 中的输出显示,证书和密钥文件在我的本地机器上,这就是我作为已验证用户访问集群所需的一切。

图 17-3

图 17.3 Kubernetes 可以为新的已验证用户颁发自己的客户端证书。

如果您对 OpenSSL 和证书感兴趣,您可以解码该证书文件,并看到通用名称是 reader@kiamol.net。Kubernetes 将其视为用户名称,这将是我们可以用来应用 RBAC 的主题。权限在 RBAC 中都是累加的,因此主题初始时没有权限,随着角色绑定的应用,最终将拥有所有角色权限的总和。RBAC 模型是仅授权的——您不能拒绝权限。权限的缺失等同于拒绝。我们可以使用新证书设置 kubectl 上下文,并确认用户初始时没有访问权限。

现在尝试一下:使用生成的证书作为凭据,在 kubectl 中创建一个新的上下文,并确认您可以使用它访问集群。

# set the credentials for a new context from the client certificate:
kubectl config set-credentials reader --client-key=./user.key 
--client-certificate=./user.crt --embed-certs=true

# set the cluster for the context:
kubectl config set-context reader --user=reader --cluster $(kubectl config view -o jsonpath='{.clusters[0].name}')

# try to deploy a Pod using the new context--
# if your cluster is configured with authentication
# this won’t work as the provider tries to authenticate:
kubectl apply -f sleep/ --context reader 

# impersonate the user to confirm their permissions:
kubectl get pods --as reader@kiamol.net

在 kubectl 中,一个上下文有两个部分:用户凭据和要连接的集群。在图 17.4 中,您可以看到用户配置了客户端证书,这些证书嵌入到 kubectl 配置文件中。如果您使用这样的客户端证书,您的配置文件是敏感的:如果有人获得了副本,他们就可以使用您的上下文中之一连接到您的集群。

图片

图 17.4 已认证但未授权——用户初始时没有 RBAC 角色。

练习中的最后一个命令使用模拟来确认新用户没有任何权限,但 Kubernetes 不存储用户。您可以在as参数中使用任何随机的字符串作为用户名,输出将告诉您它没有权限。Kubernetes 实际上寻找的是任何与请求中名称匹配的用户名称的角色绑定。如果没有绑定,则没有权限,因此操作被阻止,无论用户名是否存在于认证系统中。列表 17.1 显示了我们可以应用的角色绑定,以给新用户在默认命名空间中只读访问资源的权限。

列表 17.1 reader-view-default.yaml,使用角色绑定应用权限

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: reader-view
  namespace: default                    # The scope of the binding
subjects:
- kind: User
  name: reader@kiamol.net               # The subject is the new user
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view                            # Gives them the view role for  
  apiGroup: rbac.authorization.k8s.io   # objects in the default namespace

这是一种开始使用 RBAC 的好方法——使用预定义的集群角色,并将它们绑定到指定命名空间的主题。随着我们进入本章,您将看到如何构建自定义角色,当您需要明确的访问权限时,它们非常棒,但规模扩大时管理起来会变得困难。角色绑定将主题从它们拥有的角色中抽象出来,因此您可以在不更改角色或对象的情况下更改访问权限。当您从列表 17.1 部署角色绑定时,新用户将能够查看默认命名空间中的资源。

现在尝试一下:应用一个角色绑定,并模拟新用户以确认他们只有对资源的只读访问权限。

# deploy a sleep Pod as your normal user:
kubectl apply -f sleep/

# deploy the role binding so the reader can view the Pod:
kubectl apply -f role-bindings/reader-view-default.yaml

# confirm the user sees Pods in the default namespace:
kubectl get pods --as reader@kiamol.net 

# confirm the user is blocked from the system namespace:
kubectl get pods -n kube-system --as reader@kiamol.net

# confirm the user can’t delete Pods--this will fail:
kubectl delete -f sleep/ --as reader@kiamol.net

您可以在图 17.5 中看到视图角色的实际应用——新用户可以列出 Pods,但仅限于默认命名空间。该角色没有删除对象的权限,因此读者用户可以看到 Pods 但不能删除它们。

图片

图 17.5 角色绑定的作用域仅限于一个命名空间。

用户和角色之间的脱节感觉有点奇怪,可能会导致问题。Kubernetes 与认证系统没有真正的集成,因此它不会验证用户名是否正确或组是否存在。对于最终用户而言,RBAC 中配置的完全是严格的授权。但你知道,从第十六章中你可以了解到,你还需要保护使用 Kubernetes API 的应用程序的集群内部安全,集群管理服务账户的认证和授权。

17.2 在集群内保护资源访问

每个命名空间都会自动创建一个默认服务账户,任何未指定服务账户的 Pod 都会使用默认账户。默认服务账户就像任何其他 RBAC 主体一样:它一开始没有任何权限,直到你添加一些,你将使用与最终用户主体相同的角色绑定和集群角色绑定来添加它们。服务账户之所以不同,是因为应用程序通常需要更有限的权限集,因此最佳实践是为每个组件创建一个专用的服务账户。

创建服务账户并设置角色和绑定会增加很多开销,但请记住,这并不是限制你应用程序模型中资源访问的问题。你可以在你的清单中包含 ConfigMaps 和 Secrets 以及你需要的任何其他内容,它们在运行时不会受到服务账户权限的影响。服务账户的 RBAC 仅关于保护使用 Kubernetes API 服务器的应用程序——比如 Prometheus,它会查询 API 以获取 Pod 列表。这种情况在你的标准商业应用程序中应该是很少见的,所以这个程序只是关于保护那些如果你为每个应用程序在同一个命名空间中使用默认服务账户会遇到问题的特殊情况。我们将从这个部分开始,创建一个新的命名空间来查看其默认服务账户。

现在试试看 创建一个新的命名空间,并检查其服务账户的权限——如果你使用 Docker Desktop,还需要修复其上的一个 bug。

# on Docker Desktop for Mac run this to fix the RBAC setup:
kubectl patch clusterrolebinding docker-for-desktop-binding --type=json
     --patch $'[{"op":"replace", "path":"/subjects/0/name", 
                 "value":"system:serviceaccounts:kube-system"}]'

# OR on Docker Desktop for Windows:
kubectl patch clusterrolebinding docker-for-desktop-binding 
--type=json --patch '[{\"op\":\"replace\",
                       \"path\":\"/subjects/0/
name\", \"value\":\"system:serviceaccounts:kube-system\"}]'

# create the new namespace:
kubectl apply -f namespace.yaml

# list service accounts:
kubectl get serviceaccounts -n kiamol-ch17

# check permissions for your own account:
kubectl auth can-i "*" "*"

# check permissions for the new service account:
kubectl auth can-i "*" "*" --as system:serviceaccount:kiamol-ch17:default

kubectl auth can-i get pods -n kiamol-ch17 --as system:serviceaccount:kiamol-ch17:default

can-i 命令是一个检查权限而不实际影响任何对象的有用方法。你可以在图 17.6 中看到,你可以将此命令与模拟和命名空间范围结合使用,以显示另一个主体的权限,这可以是用户或服务账户。

图 17.6

图 17.6 系统控制器确保每个命名空间都有一个默认服务账户。

你在练习中看到,新的服务账户从零权限开始,如果你为所有应用使用默认账户,麻烦就从此开始了。每个应用可能都需要一组权限——它们都被添加到默认账户中,很快这个账户就拥有了比所需更多的权力。如果使用该服务账户的任何应用被攻破,攻击者就会发现自己获得了一组额外的角色。第 17.1 节中的证书生成器就是一个很好的例子:它使用 Kubernetes API 发布客户端证书,这是一个特权操作。列表 17.2 展示了应用的服务账户使用的集群角色,以获取所需的权限。完整的清单还包含了一个集群角色绑定。

列表 17.2 user-cert-generator.yaml,用于证书生成的自定义集群角色

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: create-approve-csr
rules:
- apiGroups: ["certificates.k8s.io"]        # Generating certificates
  resources: ["certificatesigningrequests"] # needs permission to create 
  verbs: ["create", "get", "list", "watch"] # a signing request.
- apiGroups: ["certificates.k8s.io"]
  resources: ["certificatesigningrequests/approval"]
  verbs: ["update"]                          # And to approve the request
- apiGroups:  ["certificates.k8s.io"]
  resources:  ["signers"]                    # Uses the cluster to sign
  resourceNames:  ["kubernetes.io/kube-apiserver-client"]
  verbs: ["approve"]

生成证书需要使用 API 服务器发行者创建签名请求并批准它们,如果我很懒,我可以基于这个应用不是公开可访问的、攻击面很小、没有其他应用使用该账户的论据,将这个角色绑定到默认的服务账户上。但是,当然,这是默认服务账户,Kubernetes 默认挂载账户令牌。如果有人在同一个命名空间中部署了一个有漏洞的 Web 应用,它将有权生成用户证书,攻击者就可以生成他们自己的凭证。相反,我创建了一个专门用于证书生成器的服务账户。

现在试试 Confirm that the custom service account for the certificate generator app has permission to create certificate-signing requests, but standard service accounts don’t have that permission.

# run a can-i check inside the certificate generator Pod:
kubectl exec user-cert-generator -- kubectl auth can-i create csr
 --all-namespaces

# use impersonation for the same check:
kubectl auth can-i create csr -A --as system:serviceaccount:default:user-cert-generator

# confirm the new service account doesn’t have the permission:
kubectl auth can-i create csr -A --as system:serviceaccount:kiamol-ch17:default

在这个练习中,你会发现只有证书生成器服务账户才有证书权限。实际上,账户只有这些权限,所以你不能用它来列出命名空间或删除持久卷声明或其他任何东西。你还在图 17.7 中看到,引用服务账户的语法与 Kubernetes 中的其他资源引用不同——system:serviceaccount 是前缀,后面跟着冒号分隔的命名空间和账户名称。

图片

图 17.7 使用单独的服务账户为应用确保最小权限方法。

发布客户端证书不是典型应用的需求,但为每个使用 Kubernetes API 的应用创建单独的服务账户仍然是一个最佳实践。在本节中,我们还有两个示例来展示应用可能需要如何使用 API 以及如何保护它们。在 RBAC 中,你需要理解绑定的作用域,以确保你在正确的级别应用权限——通常是单个命名空间。第一个例子是一个简单的 Web 应用,它可以列出 Pods 并允许你删除它们。

现在试试 Run the Kube Explorer app,它通过 Web UI 列出 Pods。这个部署使用了一个自定义的服务账户和角色来管理权限。

# deploy the app and the RBAC rules:
kubectl apply -f kube-explorer/

# wait for the Pod:
kubectl wait --for=condition=ContainersReady pod -l app=kube-explorer

# get the URL for the app:
kubectl get svc kube-explorer -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8019' 

# browse to the app, and confirm you can view and delete
# Pods in the default namespace; then add ?ns=kube-system 
# to the URL, and you’ll see an error.

我的输出在图 17.8 中,你可以看到应用程序正在愉快地列出和删除默认命名空间中的 Pods。当你切换到不同的命名空间时,你会看到一个授权错误,这来自 Kubernetes API(它是一个 HTTP API,所以你实际上会收到一个 403 禁止响应)。应用程序使用挂载在 Pod 中的服务账户令牌进行身份验证,并且该账户只有默认命名空间的列出和删除 Pod 权限,没有其他命名空间的权限。

图片

图 17.8 拥有自己的服务账户和足够权限完成其工作的应用程序

这样的小程序构建起来不费时,因为大多数语言都有 Kubernetes 库,该库通过使用默认路径中的令牌来处理身份验证。它们对于不需要 kubectl 访问权限且不需要学习 Kubernetes 的工作原理仅为了查看正在运行的内容的团队非常有用。此应用程序需要 RBAC 权限来列出 Pods、显示它们的详细信息以及删除它们。目前,这些权限在一个绑定到默认命名空间的角色中。为了使其他命名空间可用,我们需要添加更多角色和绑定。

列表 17.3 显示了授予在kube-system命名空间中获取和列出 Pod 权限的新规则。这里最重要的要点是,角色元数据中的命名空间不仅仅是角色创建的地方,也是角色应用的范围。此角色授予对kube-system命名空间中 Pods 的访问权限。

列表 17.3 rbac-with-kube-system.yaml,将角色应用于系统命名空间

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: system-pod-reader
  namespace: kube-system        # Scoped to the system namespace
rules:
- apiGroups: [""]               # The API group of the object spec
  resources: ["pods"]           # Pods are in the core group, which
  verbs: ["get", "list"]        # is identified with an empty string.

将该角色添加到应用程序服务账户的绑定角色位于同一清单文件中,但我将其拆分为两个列表,以便分别检查它们并理解所有命名空间。角色的命名空间是权限的范围;角色绑定引用角色,并且它需要在同一命名空间中,但它也引用主体,这可以是在不同的命名空间中。在列表 17.4 中,角色绑定与角色一起在kube-system命名空间中创建。它们之间,它们提供了对该命名空间中 Pods 的访问权限。主体是应用程序的服务账户,它在default命名空间中。

列表 17.4 rbac-with-kube-system.yaml,将角色绑定到主体

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: kube-explorer-system
  namespace: kube-system         # Needs to match the role
subjects:
- kind: ServiceAccount
  name: kube-explorer            # The subject can be in a
  namespace: default             # different namespace.
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: system-pod-reader

当你遇到不适合标准集群角色的权限要求时,你可以看到事情是如何螺旋上升的。在这种情况下,不同命名空间的权限是不同的,所以你需要为每个命名空间设置一组角色和角色绑定。当你部署这些新规则时,应用程序将能够显示系统 Pods。访问规则在 API 调用时进行评估,所以应用程序或服务账户没有变化;新的权限立即生效。

现在试试它 向 Kube Explorer 应用程序添加系统 Pods 的访问规则。

# apply the new role and binding
kubectl apply -f kube-explorer/update/rbac-with-kube-system.yaml

# refresh the explorer app with the path /?ns=kube-system. 
# you can see the Pods now, but you can’t delete them.

如图 17.9 所示,管理许多角色和绑定所带来的回报是您可以构建非常细粒度的访问策略。此应用可以列出和删除默认命名空间中的 Pod,但它只能列出系统命名空间中的 Pod。其他应用不可能意外获得这些权限;它们需要指定 Kube Explorer 服务账户,并由有权使用该账户的人部署。

图片

图 17.9 RBAC 规则在应用或删除绑定时立即生效。

本节最后一个示例展示了应用可能需要从不在 ConfigMap 或 Secret 中的集群获取一些配置数据。我们将再次使用待办事项应用。这个版本在主页上显示横幅消息,并从 Pod 运行的命名空间上的kiamol标签获取横幅内容。一个初始化容器使用服务账户令牌设置 kubectl,获取命名空间的标签值,并将其写入配置文件,然后应用容器拾取该文件。这不是一个非常现实的场景,但它展示了如何将集群中的数据注入到应用配置中。

现在试试看:部署新的待办事项应用,并确认横幅消息是否已从命名空间标签中填充。

# print the label value on the namespace:
kubectl get ns kiamol-ch17 --show-labels

# deploy the to-do list app:
kubectl apply -f todo-list/

# wait for the Pod to start:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web -n kiamol-ch17

# print the logs of the init container:
kubectl logs -l app=todo-web -c configurator --tail 1 -n kiamol-ch17

# get the URL, and browse to the app:
kubectl get svc todo-web -n kiamol-ch17
 -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8020'

我的输出显示在图 17.10 中,您可以看到初始化容器已从命名空间中获取了元数据。应用容器不使用 Kubernetes API,但服务账户令牌被挂载到 Pod 中的所有容器上,因此应用可能被破坏,攻击者可能利用服务账户的上下文使用 Kubernetes API。

图片

图 17.10 应用可能需要访问来自其他集群资源的配置数据。

如果攻击者从待办事项应用中获得了对 API 的访问权限,他们能做的事情并不多。该应用使用一个专用的服务账户,并且该账户只有一个权限:它可以获取命名空间kiamol-ch17的详细信息。列表 17.5 展示了角色和集群角色内部的规则如何限制对命名资源的权限。

列表 17.5 02-rbac.yaml,命名资源规则

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ch17-reader
rules:
- apiGroups: [""]                 # Namespace access requires a
  resources: ["namespaces"]       # ClusterRole; this grants 
  resourceNames: ["kiamol-ch17"]  # get access for one namespace.
  verbs: ["get"]

RBAC 的一个缺点是规则在应用之前需要资源存在,因此在这种情况下,命名空间和服务账户必须在创建角色和角色绑定之前存在。当您使用 kubectl 应用一个 manifest 文件夹时,它不会寻找依赖关系以正确顺序创建资源。它只是按照文件名顺序应用文件。这就是为什么列表 17.6 中的 RBAC manifest 被称为02-rbac.yaml,以确保它在服务账户和命名空间存在之后创建。

我们已经探讨了深入到特定应用的权限,但 RBAC 的另一个主要功能是将同一组规则应用于一组主体。我们将继续探讨这一点。

17.3 将角色绑定到用户组和服务账户

角色绑定和集群角色绑定可以应用于组,用户和服务帐户都可以属于组,尽管它们的工作方式不同。最终用户在 Kubernetes 外部进行身份验证,API 信任提供的用户名和组信息。用户可以是多个组的成员,组名和成员资格由身份验证系统管理。服务帐户的限制更多;它们始终属于两个组:集群中所有服务帐户的组,以及命名空间中所有服务帐户的组。

组是绑定的另一种主题,所以规范是相同的,只是你将角色或集群角色绑定到组而不是用户或服务帐户。Kubernetes 不验证组名,所以确保绑定中的组与身份验证系统设置的组相匹配的责任在你。证书可以包含组信息,因此我们可以为属于不同组的用户创建证书。

现在试试看 使用证书生成器创建更多经过身份验证的用户,这次在证书中设置组成员资格,为一个站点可靠性工程师(SRE)组和测试者组。

# create two users:
kubectl apply -f user-groups/

# confirm that the SRE can’t delete Pods:
kubectl exec sre-user -- kubectl auth can-i delete pods

# print the username and group in the certificate:
kubectl exec sre-user -- sh -c 'openssl x509 -text -noout -in /certs/user.crt | grep Subject:'

# confirm the test user can’t read Pod logs:
kubectl exec test-user -- kubectl auth can-i get pod/logs

# print this certificate’s details:
kubectl exec test-user -- sh -c 'openssl x509 -text -noout -in /certs/user.crt | grep Subject:'

现在你有了两个用户证书,其中用户属于代表他们团队的组:SRE 和测试者。集群中不存在任何用户名或组的绑定,所以用户目前什么都不能做。图 17.11 显示 Kubernetes 使用证书主题的标准字段——通用名是用户名,组织是组(多个组织可以映射到多个组)。

图 17.11

图 17.11 证书可以包含零个或多个组以及用户名。

我们确实希望 SRE 能够删除 Pods,我们也希望测试者能够读取 Pod 日志。我们可以在组级别应用绑定,以给团队中的所有用户相同的权限。然后我们将管理组成员资格的责任移交给身份验证系统。对于 SRE 来说,最简单的事情是让他们在整个集群中拥有查看角色,这样他们就可以帮助诊断问题,以及拥有他们团队管理的命名空间中的编辑角色。这些是内置的集群角色,所以我们只需要绑定,其中主题是 SRE 组。对于测试者,我们希望拥有列表 17.6 中非常受限的权限集。

列表 17.6 test-group.yaml,一个用于获取 Pod 详细信息和读取日志的角色

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: logs-reader
rules:
- apiGroups: [""]
  resources: ["pods ", "pods/log"]  # Logs are a subresource of Pods
  verbs: ["get"]                    # that need explicit permissions.

列表 17.6 中的完整清单包括一个集群角色绑定,将此角色授予测试组,这实际上赋予了测试者查看任何 Pods 和查看其日志的权限,但不能列出 Pods 或对任何其他资源进行操作。某些资源有子资源,这些子资源是单独授权的,所以如果你有获取 Pods 的权限,除非你也有获取日志子资源的权限,否则你不能读取日志。当你为两个组应用绑定时,用户将拥有他们需要的权限。

现在尝试一下 通过将他们需要的角色绑定到他们所属的组来赋予新用户权力。

# apply the roles and bindings:
kubectl apply -f user-groups/bindings/

# confirm the SRE cannot delete in the default namespace:
kubectl exec sre-user -- kubectl auth can-i delete pods

# confirm they can delete in the ch17 namespace:
kubectl exec sre-user -- kubectl auth can-i delete pods -n kiamol-ch17

# confirm the tester can’t list Pods:
kubectl exec test-user -- kubectl get pods

# confirm the tester can read the logs for a known Pod:
kubectl exec test-user -- kubectl logs test-user --tail 1

您可以在图 17.12 中看到,这正如预期的那样工作:用户拥有所需的权限,但我们是在更高级别应用它们,将角色绑定到组。我们可以更改权限而无需修改用户或组。新用户可以到来,人们可以在团队之间移动,只要认证系统保持最新,Kubernetes 在 API 调用中看到当前的组成员关系即可,无需进行任何 RBAC 更改。

图片

图 17.12 在组级别设置权限,为用户提供所需权限并具有易于管理的体验。

在配置了 Active Directory 的企业环境中或具有集成认证的云环境中,这一切都很正常。在较小规模的集群中,您可能希望在 Kubernetes 内管理认证,但像本节中那样生成证书实际上并不可行。该脚本创建的证书有效期为一年,并且组列表已嵌入到证书中,因此很难更改组成员关系。如果您想撤销访问权限,您需要轮换 Kubernetes 颁发者证书并向每个用户分发新证书。

一种常见的替代方法是误用服务账户,为每个最终用户创建一个服务账户,并使用 kubectl 分发令牌进行认证。这种方法无法扩展到数百个用户,但如果您没有外部认证系统,并且希望为数不多的用户提供对 Kubernetes 的安全访问,它是一个可行的选项。您需要对组采取稍微古怪的方法,因为您不能创建一个组并将服务账户添加到其中。相反,您需要将命名空间视为组,为每个您想要的组创建一个命名空间,并将所有服务账户放入该组。图 17.13 显示了设置。

图片

图 17.13 服务账户有令牌和组,因此它们可以像用户账户一样被误用。

服务账户作为用户账户工作,因为您可以在集群内管理它们,轻松地创建或删除它们,并通过删除令牌来撤销访问权限。我们可以使用服务账户重新创建我们的 SRE 和测试用户,使用单独的命名空间来表示这两个组,并将绑定应用于这些组。

现在尝试一下 为 SRE 和测试组创建命名空间,并为每个命名空间中的用户创建一个服务账户和令牌。

# create namespaces, service accounts, and tokens:
kubectl apply -f user-groups/service-accounts/

# apply bindings to the groups:
kubectl apply -f user-groups/service-accounts/role-bindings/

# confirm the SRE group has view access across the cluster:
kubectl get clusterrolebinding sre-sa-view-cluster
 -o custom-columns='ROLE:.roleRef.name,SUBJECT KIND:.subjects[0].kind,SUBJECT NAME:.subjects[0].name'

新的集群角色绑定适用于服务账户组,这是服务账户已被创建的命名空间。这些集群角色与用户证书集群角色的区别只是组名:system:serviceaccounts:kiamol-authn-sre用于 SRE,这是kiamol-authn-sre命名空间中所有服务账户的组,如图 17.14 所示。

图片

图 17.14 使用服务账户、令牌和命名空间伪造认证系统

服务账户使用 JSON Web Token (JWT)进行身份验证。此令牌预先填充在 Pod 卷中。令牌作为类型为kubernetes.io/ service-account-token的 Secret 创建,Kubernetes 确保每个服务账户至少有一个令牌。您也可以创建自己的令牌,这使得分发、轮换和吊销令牌变得容易。这个过程很简单,因为 Kubernetes 实际上生成令牌;您只需创建一个正确类型的空 Secret,集群就会添加数据——您在之前的练习中已经做到了这一点,所以这些账户已经准备好使用。

Kubectl 支持多种不同的身份验证选项——我们已经看到的证书以及用户名和密码、第三方身份验证和 JWT。您可以通过设置使用服务账户令牌的凭据的新上下文来对集群进行身份验证。任何使用该上下文的 kubectl 命令都将作为服务账户运行,因此将应用服务账户组的权限。

现在试试看 创建一个新的上下文,用于 SRE 服务账户的 kubectl,并确认您能够访问集群。这将在使用您集群的任何身份验证系统时都有效。

# add a Base64 command if you’re using Windows:
. .\base64.ps1

# decode the Secret for the token, and save to a file:
kubectl get secret sre2-sa-token -n kiamol-authn-sre -o jsonpath='{.data.token}' | base64 -d > sa-token

# load new credentials into kubectl from the token file:
kubectl config set-credentials ch17-sre --token=$(cat sa-token)

# create a new context using the SRE credentials:
kubectl config set-context ch17-sre --user=ch17-sre --cluster $(kubectl config view -o jsonpath='{.clusters[0].name}')

# confirm you can delete Pods as the SRE account:
kubectl delete pods -n kiamol-ch17 -l app=todo-web --context ch17-sre

这个练习适用于所有集群,即使它们配置了第三方身份验证,因为它使用 Kubernetes 自己的服务账户身份验证。您可以在图 17.15 中看到,我可以以 SRE 服务账户的身份发出命令,并且该用户有权删除kiamol-ch17命名空间中的 Pod。

图 17-15

图 17.15 分发服务账户令牌使用户能够以该账户的身份进行身份验证。

如果您对 JWT 感兴趣,可以解码sa-token文件的内容(jwt.io上的在线工具会为您完成),您会看到 Kubernetes 是发行者,而主题是服务账户名称system:serviceaccount:kiamol-authn-sre:sre2。RBAC 权限是该服务账户的联合角色、服务账户命名空间中的组以及所有服务账户的组。在此提醒一点:在授予服务账户组角色时,请非常小心主题。很容易不小心通过省略主题中的命名空间将每个账户都变成集群管理员(这正是某些版本的 Docker Desktop 的问题;请参阅github.com/docker/for-mac/issues/4774的历史记录)。

现在我们管理集群内的身份验证,很容易通过删除特定用户的令牌来撤销其访问权限。他们从 kubectl 提供的令牌不再验证服务账户,因此他们会对每个操作收到未经授权的错误。

现在试试看 通过删除令牌来停止用户以 SRE 服务账户的身份进行身份验证。

# delete the access token:
kubectl delete secret sre2-sa-token -n kiamol-authn-sre

# wait for the token removal to reach controllers:
sleep 30

# now try to get Pods as the SRE account:
kubectl get pods --context ch17-sre

撤销访问权限很简单,如图 17.16 所示。轮换令牌需要更多一些过程;您可以创建一个新的令牌并发送给用户(安全地!),然后用户更新了上下文后删除旧令牌。更改组更复杂,因为您需要在组的命名空间中创建一个新的服务账户,创建并发送新的令牌,然后删除旧的服务账户。

图 17.16

图 17.16 您可以通过删除令牌来移除访问权限,而不必删除服务账户。

对于用户的服务账户是一种简单的保护集群的方法,如果您能接受其局限性。这可能是一个合理的入门方式,但您应该了解您的路线图,这很可能会使用 OpenID Connect (OIDC) 进行身份验证并将您的 RBAC 规则绑定到 OIDC 用户名和组声明。

当您的身份验证系统已经配置好并且 RBAC 规则已经设置后,您仍然面临挑战。Kubernetes 没有一套很好的工具用于审计权限,所以在下文中,我们将探讨第三方选项以验证权限。

17.4 使用插件发现和审计权限

kubectl can-i 命令对于检查用户能否执行某个功能很有用,但这是您用于验证权限的唯一工具,而且它并没有真正深入到足够远。您通常希望从另一个角度来处理,询问谁可以执行某个功能或打印出访问权限矩阵,或者搜索 RBAC 主体并查看他们的角色。Kubectl 有一个插件系统来支持额外的命令,以及插件来满足所有这些不同的 RBAC 接近方式。

添加 kubectl 插件的最佳方式是使用 Krew,即插件管理器。您可以直接在您的机器上安装 Krew,但安装过程并不十分顺畅,您可能不希望在机器上安装插件(如果您这样做,安装文档在这里 krew.sigs.k8s.io/docs/user-guide/setup/install)。我已经准备了一个已经安装了 Krew 的容器镜像——您可以使用这个镜像以安全的方式尝试插件。我们将首先查看的是 who-can,它类似于 can-i 的逆操作。

现在试试看 启动 Krew Pod,并连接以安装和使用 who-can 插件。

# run a Pod with kubectl and Krew already installed:
kubectl apply -f rbac-tools/

# wait for the Pod:
kubectl wait --for=condition=ContainersReady pod -l app=rbac-tools

# connect to the container:
kubectl exec -it deploy/rbac-tools -- sh

# install the who-can plugin:
kubectl krew install who-can

# list who has access to the todo-list ConfigMap:
kubectl who-can get configmap todo-web-config

任何二进制文件都可以是 kubectl 插件,但 Krew 通过精选有用的插件简化了设置并增加了一些流程。(Krew 是 Kubernetes 特别兴趣小组项目。)插件在您的认证用户上下文中运行,因此您需要确保它们不会执行不应该做的事情。本练习中的 who-can 插件由 Aqua Security 发布,如图 17.17 所示,它通过遍历 RBAC 角色以找到匹配的权限,然后打印出具有这些角色绑定的主体。

图 17.17

图 17.17 插件为 kubectl 添加了新功能:who-can 是 RBAC 查询的有用工具。

我们将查看另外两个插件,它们填补了 RBAC 审计的空白。下一个是 access-matrix,它在一个资源类型或特定资源上操作,并打印出该对象的全部访问规则,显示谁可以做什么。这是持续审计中最有用的工具,您可以在计划作业中运行,收集一系列访问矩阵,并确认没有设置意外的权限。

现在试试吧 安装 access-matrix 插件,并打印不同资源的矩阵。

# install the plugin:
kubectl krew install access-matrix

# print the matrix for Pods:
kubectl access-matrix for pods -n default

# the print the matrix for the to-do list ConfigMap:
kubectl access-matrix for configmap todo-web-config -n default

access-matrix 的默认输出会打印出漂亮的图标,如图 17.18 所示,但您可以在命令行中配置纯 ASCII 输出。我的输出被大量截断,因为完整的权限列表包括了所有控制器和其他系统组件。ConfigMap 的访问矩阵更容易管理,您可以看到在第 17.3 节中创建的 SRE 组具有列表权限,因此它们可以看到这个对象,但 rbac-tools 服务账户具有完全更新和删除权限。这是因为我在这个练习中偷懒,给了账户 cluster-admin 角色,而不是构建一个只包含插件所需权限的自定义角色。

图片

图 17.18 access-matrix 插件显示了谁可以使用不同的资源进行什么操作。

我们将要查看的最后一个插件是 rbac-lookup,它对于搜索 RBAC 实体非常有用。它会在用户、服务账户和组中找到匹配项,并显示绑定到实体的角色。这是一个从用户角度检查 RBAC 的好工具,当您想确认一个实体是否分配了正确的角色时。

现在试试吧 安装 rbac-lookup 插件,并搜索 SRE 和测试实体。

# install the plugin:
kubectl krew install rbac-lookup

# search for SRE:
kubectl rbac-lookup sre

# search for test:
kubectl rbac-lookup test

没有任何插件能为您提供一个用户及其所属所有组的综合权限,因为 Kubernetes 对组成员关系一无所知。RBAC 通过在用户展示其组列表时获取综合权限来工作,但在传入请求之外,用户和组之间没有联系。这与服务账户不同,因为它们始终属于已知的组——您可以在图 17.19 中看到 SRE 组。没有方法可以找到属于 SRE 用户组的用户,但您可以通过列出 kiamol-authn-sre 命名空间中的所有服务账户来查看属于服务账户组的成员。

图片

图 17.19 搜索 RBAC 实体并打印它们所绑定的角色

值得探索 Krew 目录以找到更多可以帮助您日常工作的插件。本节重点介绍了帮助 RBAC 的成熟插件;它们很受欢迎,并且存在很长时间,但还有许多其他宝藏等待发现。本章的实用工作到此结束。我们将完成对 RBAC 如何影响您的工作负载以及实施访问控制的指导的探讨。

17.5 规划您的 RBAC 策略

RBAC(基于角色的访问控制)为外部用户和集群中运行的应用程序提供了统一的方式来保护 Kubernetes。角色和绑定机制对这两种类型的主体都是相同的,但它们需要非常不同的方法。用户通过一个独立的、受信任的系统进行认证,该系统声明用户名和用户所属的组。这里的指导是,从预定义的集群角色——查看、编辑和管理——开始,并将它们应用于组。集群管理员角色应严格保护,并且仅在真正需要时使用,最好将其限制为自动化流程的服务账户。

考虑将命名空间用作安全边界,以便进一步限制范围。如果你的身份验证系统有分组信息,可以识别某人的团队及其在该团队中的职能,这可能足以将绑定映射到角色和产品命名空间。对集群范围内的角色保持警惕,因为它们可以用来获得提升的访问权限。如果用户可以在任何命名空间中读取机密信息,他们可能能够获取特权服务账户的 JWT 令牌,并获取该命名空间的管理员访问权限。

应谨慎使用服务账户,并且仅由需要访问 Kubernetes API 服务器的应用程序使用。你可以通过将默认服务账户的属性设置为禁用自动挂载令牌来禁用 Pod 中的令牌自动挂载,这样 Pod 永远不会看到令牌,除非它们主动请求它。记住,RBAC 并不是关于在部署时限制访问。Pod 不需要配置服务账户来使用作为卷挂载的 ConfigMap。你需要服务账户仅用于使用 Kubernetes API 的应用程序,在这些情况下,每个应用程序都应该有一个专门的服务账户,它仅具有足够的权限来完成应用程序所需的工作,最好是与命名资源相关联的权限。

应用 RBAC 并不是故事的结束。随着你的安全配置文件成熟,你将希望添加审计以确保策略不会被规避,并添加准入控制器以确保新应用程序具有所需的安全控制。我们在这里不会涉及这些内容;只需知道,保护你的集群是关于使用多种方法来获得深度安全。

好的,在你进入实验室之前,先清空集群。

现在试试看 移除本章的所有资源。

kubectl delete all,ns,rolebinding,clusterrolebinding,role,clusterrole,serviceaccount -l kiamol=ch17

17.6 实验室

你在 17.2 节中运行的 Kube Explorer 应用程序也可以显示服务账户,但它需要更多的权限才能做到这一点。实验室文件夹中有该应用程序的新一组清单,它赋予了它访问默认命名空间中 Pods 的权限。本实验室的任务是为该应用程序添加角色和绑定,以便它可以执行以下操作:

  • kiamol-ch17-lab命名空间中显示和删除 Pods

  • default命名空间中显示服务账户

  • kiamol-ch17-lab命名空间中显示服务账户

仔细查看现有的 RBAC 规则是如何设置的——这应该会让这个过程变得简单。只需记住绑定中的命名空间如何影响作用域。我的解决方案可供您检查:github.com/sixeyed/kiamol/blob/master/ch17/lab/README.md.

18 部署 Kubernetes:多节点和多架构集群

您可以在不了解集群架构以及所有组件如何协同工作的情况下做很多事情——您在前 17 章中已经做到了这一点。但额外的知识将帮助您了解 Kubernetes 中的高可用性是什么样的,以及如果您想运行自己的集群,您需要考虑什么。了解所有 Kubernetes 组件的最好方式是从头开始安装集群,这正是本章要做的。练习从普通的虚拟机开始,引导您完成多节点集群的基本 Kubernetes 设置,您可以使用它来运行书中熟悉的一些示例应用程序。

到目前为止,我们运行的所有应用程序都使用了为 Intel 64 位处理器构建的 Linux 容器,但 Kubernetes 是一个多架构平台。单个集群可以包含具有不同操作系统和不同类型 CPU 的节点,因此您可以运行各种工作负载。在本章中,您还将向您的集群添加一个 Windows Server 节点并运行一些 Windows 应用程序。这部分是可选的,但如果您不是 Windows 用户,那么跟随这些练习了解 Kubernetes 如何使用相同的建模语言来处理不同的架构,只需对清单进行一些调整,这也是值得的。

18.1 Kubernetes 集群内部有什么?

您需要一些不同的工具来跟随本章的内容,以及一些虚拟机镜像,下载这些镜像需要一些时间。您现在可以开始安装,等到我们完成对 Kubernetes 架构的查看时,您应该已经准备好开始使用了。

现在试试吧!您将使用 Vagrant,这是一个用于虚拟机管理的免费开源工具,来运行虚拟机。您还需要一个虚拟机运行时;您可以使用 VirtualBox、Hyper-V(在 Windows 上)或 Parallels(在 macOS 上)来运行这些机器。安装 Vagrant,然后下载您将用于集群的基础虚拟机镜像。

# install Vagrant-
# browse to https://www.vagrantup.com to download
# OR on Windows you can use: choco install vagrant
# OR on macOS you can use: brew cask install vagrant

# Vagrant packages VM images into "boxes"
# download a Linux box--this will ask you to choose a provider,
# and you should select your VM runtime:
vagrant box add bento/ubuntu-20.04

# if you want to add Windows, download a Windows box:
vagrant box add kiamol/windows-2019 

好的,当这一切在进行时,是时候学习 Kubernetes 的架构了。您知道集群由一个或多个称为节点的服务器组成,但这些节点可以扮演不同的角色。有 Kubernetes 的 控制平面,这是集群的管理方面(之前称为主节点),然后是 节点,它们运行您的工作负载(这些曾经被称为从节点)。在较高层次上,控制平面是接收您的 kubectl 部署请求的东西,并通过在节点上调度 Pods 来执行这些操作。图 18.1 展示了从用户体验级别的集群。

图 18-1

图 18.1 从用户的角度来看,这是一个包含管理和应用端点的集群。

在云中的托管 Kubernetes 平台上,控制平面由您负责,因此您只需关注自己的节点(在 AKS 中,控制平面完全抽象,您只能看到并支付工作节点)。这就是托管平台如此吸引人的一个原因——控制平面包含多个组件,如果您运行自己的环境,则需要管理这些组件。以下组件对于集群的正常运行至关重要:

  • API 服务器是管理接口。它是一个 HTTPS 端点的 REST API,您可以通过 kubectl 连接到它,Pod 也可以内部使用。它运行在 kube-apiserver Pod 中,并且可以扩展以实现高可用性。

  • 调度器监视新的 Pod 请求并选择一个节点来运行它们。它运行在 kube-scheduler Pod 中,但它是一个可插拔组件——您可以部署自己的自定义调度器。

  • 控制器管理器运行核心控制器,这些是内部组件,不是像 Deployments 这样的可见控制器。kube-controller-manager Pod 运行观察节点可用性和管理服务端点的控制器。

  • etcd是 Kubernetes 数据存储,所有集群数据都存储在这里。它是一个分布式键值数据库,数据在多个实例之间进行复制。

您需要运行多个控制平面节点以实现高可用性。使用奇数以支持故障,因此如果管理节点宕机,剩余的节点可以投票选举一个新的管理节点。每个控制平面节点运行所有这些组件的实例,如图 18.2 所示。API 服务器是负载均衡的,后端数据是复制的,因此任何控制平面节点都可以处理请求,并且将以相同的方式进行处理。

图 18.2 生产集群需要多个控制平面节点以实现高可用性。

您的下载应该现在几乎完成了,所以我们将查看每个节点上运行的组件的更详细的一级——节点负责创建 Pod 并确保其容器持续运行,以及将 Pod 连接到 Kubernetes 网络。

  • kubelet是一个在服务器上运行的背景代理——不在 Pod 或容器中。它接收创建 Pod 的请求,管理 Pod 的生命周期,并向 API 服务器发送心跳以确认节点健康。

  • kube-proxy是网络组件,负责在 Pod 之间或从 Pod 到外部世界路由流量。它作为一个 DaemonSet 运行,每个节点上的 Pod 管理该节点的流量。

  • 由 kubelet 用于管理 Pod 容器的容器运行时。通常是 Docker、containerd 或 CRI-O,这是可插拔的,可以与支持 CRI(容器运行时接口)的任何运行时一起使用。

图 18.3 显示了每个节点上的内部组件,这些组件也运行在控制平面节点上。

图 18.3 节点的下一级细节——kubelet、kube-proxy 和容器运行时

你可以看到,Kubernetes 是一个由许多组件组成的复杂平台。这些只是核心组件;还有 Pod 网络、DNS,以及在云中,一个独立的云控制器管理器,它集成了云服务。你可以使用 100%的开源组件来部署和管理自己的 Kubernetes 集群,但你需要意识到你将要承担的复杂性。而且理论已经足够多了——让我们去构建一个集群。

18.2 初始化控制平面

在部署 Kubernetes 的大部分工作都由一个名为 kubeadm 的工具来完成。它是一个管理命令行工具,可以初始化新的控制平面,将节点加入集群,并升级 Kubernetes 版本。在你使用 kubeadm 之前,你需要安装几个依赖项。在生产环境中,你会在虚拟机镜像中已经安装了这些依赖项,但为了展示它是如何工作的,我们将从头开始。

现在尝试一下 运行一个 Linux 虚拟机,它将成为控制平面节点,并安装所有的 Kubernetes 依赖项。如果你在 Windows 上使用 Hyper-V,你需要以管理员身份运行你的 shell。

# switch to this chapter’s source:
cd ch18

# use Vagrant to start a new VM--depending on your VM runtime,
# you’ll get prompts asking you to choose a network and to
# provide your credentials to mount folders from your machine: 
vagrant up kiamol-control

# connect to the VM:
vagrant ssh kiamol-control

# this folder maps the ch18 source folder:
cd /vagrant/setup

# make the install script executable, and run it:
sudo chmod +x linux-setup.sh && sudo ./linux-setup.sh

# confirm Docker has been installed:
which docker

# confirm all the Kubernetes tools have been installed:
ls /usr/bin/kube*

图 18.4 展示了重点——创建虚拟机、运行设置脚本,并验证所有工具都已安装。如果你检查本章源代码中的linux-setup.sh脚本,你会看到它安装了 Docker 和 Kubernetes 工具,并为服务器设置了一些内存和网络配置。

图片

图 18.4 Kubeadm 是设置集群的工具,但它需要容器运行时和 kubelet。

现在这台机器已经准备好成为 Kubernetes 控制平面节点了。经过所有这些准备工作,接下来的练习将会很平淡:你只需要运行一个命令来初始化集群并启动所有控制平面组件。注意输出,你将看到你在 18.1 节中学到的所有组件开始启动。

现在尝试一下 使用 kubeadm 初始化一个新的集群,并为 Pod 和 Service 分配一组固定的网络地址。

# initialize a new cluster:
sudo kubeadm init --pod-network-cidr="10.244.0.0/16" --service-cidr="10.96.0.0/12"
                  --apiserver-advertise-address=$(cat /tmp/ip.txt)

# create a folder for the kubectl config file:
mkdir ~/.kube

# copy the admin configuration to your folder:
sudo cp /etc/kubernetes/admin.conf ~/.kube/config

# make the file readable so kubectl can access it:
sudo chmod +r ~/.kube/config

# confirm you have a Kubernetes cluster:
kubectl get nodes

初始化集群的输出会告诉你下一步该做什么,包括你将在其他节点上运行的命令以加入集群(你将在后面的练习中需要它,所以请确保将其复制到某个文本文件中)。该命令还会生成一个配置文件,你可以使用它通过 kubectl 管理集群。你可以在图 18.5 中看到,集群存在,但只有一个控制平面节点,而这个节点不在 Ready 状态。

图片

图 18.5 初始化集群很简单:kubeadm 启动所有控制平面组件。

集群尚未准备好,因为它没有安装 Pod 网络。你从第十六章知道 Kubernetes 有一个网络插件模型,不同的插件有不同的功能。在第十六章中,我们使用 Calico 来演示网络策略执行,而在本章中,我们将使用 flannel(另一个开源选项),因为它对混合架构集群的支持最为成熟。你将以与 Calico 相同的方式安装 flannel:在控制平面节点上应用 Kubernetes 清单。

现在试试看 向您的新的集群添加一个网络插件,使用现成的 flannel 清单。

# deploy flannel:
kubectl apply -f flannel.yaml

# wait for the DNS Pods to start:
kubectl -n kube-system wait --for=condition=ContainersReady pod -l k8s-app=kube-dns

# print the node status:
kubectl get nodes

# leave the control plane VM:
exit

Kubeadm 在kube-system命名空间中以 Pod 的形式部署集群的 DNS 服务器,但那些 Pod 在网络插件运行之前无法启动。一旦 flannel 部署完成,DNS Pods 就会启动,节点就准备好了。我在图 18.6 中的输出显示了 flannel 的多架构支持。如果你想将 IBM 大型机添加到你的 Kubernetes 集群中,你可以做到。

图 18.6 Kubeadm 不部署 Pod 网络;flannel 是一个好的多架构选项。

对于控制平面,你需要的就这些。我略过了网络设置——kubeadm 命令中使用的 IP 地址范围是 flannel 期望的配置——但你需要规划好,以便与你的网络兼容。而且我跳过了 kubeadm 的其他 25 个选项,但如果你认真管理自己的集群,你将需要研究它们。目前,你有一个简单的单节点集群。你还不能用它来运行应用程序工作负载,因为默认设置限制了控制平面节点,它们只能运行系统工作负载。接下来,我们将添加另一个节点并运行一些应用程序。

18.3 添加节点和运行 Linux 工作负载

kubeadm 初始化的输出会给你运行在其他服务器上的命令,以便将它们加入集群。新的 Linux 节点需要与控制平面节点相同的设置,包括容器运行时和所有 Kubernetes 工具。在下一个练习中,你将创建另一个虚拟机,并使用与控制平面相同的设置脚本安装先决条件。

现在试试看 使用 Vagrant 创建第二个虚拟机,并安装它加入 Kubernetes 集群所需的先决条件。

# start the node VM:
vagrant up kiamol-node

# connect to the VM:
vagrant ssh kiamol-node

# run the setup script:
sudo /vagrant/setup/linux-setup.sh

图 18.7 几乎与图 18.5 相同,只是机器名称不同,因此你可以看到设置脚本确实应该是虚拟机配置的一部分。这样,当你使用 Vagrant(或 Terraform 或其他适用于您基础设施的工具)启动新机器时,它将具备所有先决条件,并准备好加入集群。

图 18.7 安装所需依赖项——节点需要与控制平面相同的初始设置。

现在你可以通过加入一个新节点来将你的 Kubernetes 集群的大小加倍。kubeadm init命令的输出包含你需要的一切——一个 CA 证书哈希,以便新节点可以信任控制平面,以及一个join令牌,以便控制平面允许新服务器加入。join令牌是敏感的,你需要安全地分发它以防止恶意节点加入你的集群。任何可以访问控制平面和令牌的网络机器都可能加入。你的新虚拟机与控制平面虚拟机位于同一虚拟网络中,所以你只需要运行join命令。

现在尝试一下 使用 kubeadm 和从控制平面执行的join命令加入集群。

# you’ll need to use your own join command;
# the control plane IP address,  token, and CA hash 
# will be different--this just shows you how it looks:

sudo kubeadm join 172.21.125.229:6443 
--token 3sqpc7.a19sx21toelnar5i 
--discovery-token-ca-cert-hash sha256:ed01ef0e33f7ecd56f1d39b5db0fbaa56811ac055f43adb37688a2a2d9cc86b9

# if your token has expired, run this on the control plane node:
kubeadm token create --print-join-command

在这个练习中,你会看到 kubelet 关于 TLS 引导的日志。控制平面为新节点生成 TLS 证书,以便 kubelet 可以与 API 服务器进行身份验证。你可以自定义 kubeadm 安装以提供自己的证书颁发机构,但这又是一个额外的细节层(包括证书续订和外部 CA),我们在这里不会讨论。图 18.8 显示我的新节点成功使用简单的默认设置加入集群。

图 18-8

图 18.8 将节点加入集群设置了与控制平面的安全通信。

新节点运行控制平面已运行的组件的子集。kubelet 作为 Kubernetes 之外的背景进程运行,与 Docker 通信,Docker 也作为背景进程运行。它在 Pod 中运行另外两个组件:网络代理和网络插件。所有其他组件——DNS、控制器管理器和 API 服务器——都是针对控制平面节点特定的,不会在标准节点上运行。切换回控制平面节点,你就可以看到 Pods。

现在尝试一下 Kubectl 仅在控制平面设置,尽管你可以共享配置文件从另一台机器连接。切换回那个控制平面节点以查看所有集群资源。

# connect to the control plane node:
vagrant ssh kiamol-control

# print the node status:
kubectl get nodes

# list all the Pods on the new node:
kubectl get pods --all-namespaces --field-selector spec.nodeName=kiamol-node

我的输出显示在图 18.9 中。除非法兰绒豆还在启动中,否则你应该看到相同的结果,在这种情况下,新节点可能还没有准备好。

图 18-9

图 18.9 中的 DaemonSets 在每个节点上运行一个 Pod;新 Pod 在加入时运行系统组件。

现在你可以部署应用程序了,但你需要意识到这个集群的限制。没有设置默认的存储类,也没有卷提供程序,所以你无法部署动态持久卷声明(我们在第六章中讨论过),你将只能使用HostPath卷。也没有负载均衡器集成,所以你不能使用负载均衡器服务。在数据中心,你可以使用网络文件系统(NFS)共享来实现分布式存储,以及一个名为 MetalLB 的项目来支持负载均衡器。这一切都超出了本章的范围,所以我们将继续使用没有存储要求的简单应用程序,并使用 NodePort 服务将流量引入集群。

NodePorts 是一种更简单的服务类型:它们与其他服务一样工作,将流量分发到 Pods,但它们在节点上的特定端口上监听传入的流量。每个节点都监听相同的端口,因此任何服务器都可以接收请求并将其路由到正确的 Pod,即使该 Pod 在不同的节点上运行。如果您在本地集群中有一个现有的负载均衡器,则可以使用 NodePorts,但 NodePorts 限制在特定的端口范围内,因此您的负载均衡器需要进行一些端口映射。列表 18.1 展示了用于每日天文图片 web 应用的 NodePort 服务规范。

列表 18.1 web.yaml,一个以 NodePort 暴露的服务

apiVersion: v1
kind: Service
metadata:
  name: apod-web
spec:
  type: NodePort       # Every node will listen on the port.
 ports:
    - port: 8016       # The internal ClusterIP port
      targetPort: web  # The container port to send to
      nodePort: 30000  # The port the node listens on--
  selector:            # it must be >= 30000, a security restriction.
    app: apod-web

APOD 应用包含三个组件,与我们已经部署的其他规范相比,服务类型是唯一的区别。当您运行应用程序时,您可能会期望 Kubernetes 将 Pods 分布在集群的各个地方,但请记住,默认情况下控制平面节点与用户工作负载是隔离的。

现在尝试一下 将应用程序部署到您的新集群,并查看 Pods 被调度到哪个节点运行。

# deploy the manifests in the usual way:
kubectl apply -f /vagrant/apod/

# print the Pod status:
kubectl get pods -o wide

您可以在图 18.10 中看到,每个 Pod 都被调度在相同的节点上。这是一个没有任何本书容器镜像的新 VM,因此它将从 Docker Hub 下载它们。在发生这种情况时,Pod 将处于 ContainerCreating 状态。此应用程序的最大镜像只有 200 多 MB,因此启动 shouldn’t take too long.

图片

图 18.10 对于所有 Kubernetes 集群,用户体验基本上是相同的。

如果这是您第一次使用与您正常的实验室环境不同的 Kubernetes 集群,那么现在您就可以看到 Kubernetes 的强大之处。这是一个完全不同的设置,可能使用的是 Kubernetes 的不同版本,可能使用的是不同的容器运行时,以及不同的主机操作系统。通过更改规范的一次,您可以以与之前完全相同的方式部署和管理 APOD 应用程序。您可以从这本书的任何练习中选择并在这里部署,但您必须更改服务和卷,以便它们使用新集群的有效类型。

Kubernetes 停止控制平面节点运行应用程序工作负载,以确保它们不会因计算资源不足而受饿。如果您的控制平面节点在计算 Pi 到一百万位小数时达到最大 CPU,那么 API 服务器和 DNS Pods 就没有剩余的资源了,您的集群将变得不可用。您应该在真实集群中保留这个安全防护,但在实验室设置中,您可以放松它以充分利用您的服务器。

现在尝试一下 Kubernetes 使用污点来分类节点,而 master 污点阻止应用程序工作负载运行。移除该污点,并扩展应用程序以查看新的 Pods 被调度到控制平面节点。

# remove the master taint from all nodes:
kubectl taint nodes --all node-role.kubernetes.io/master-

# scale up, adding two more APOD API Pods:
kubectl scale deploy apod-api --replicas=3

# print the Pods to see where they’re scheduled:
kubectl get pods -l app=apod-api -o wide

您将在第十九章中了解有关污点和调度的所有内容;目前,只需知道污点是一种标记特定节点以防止 Pod 在其上运行的方法。移除污点使控制平面节点有资格运行应用程序工作负载,因此当您扩展 API 部署时,新的 Pods 将在控制平面节点上调度。您可以在图 18.11 中看到这些 Pods 处于 ContainerCreating 状态,因为每个节点都有自己的镜像存储,控制平面节点需要下载 API 镜像。您的容器镜像的大小直接影响到您在这个场景中扩展的速度,这也是为什么您需要投资优化您的 Dockerfile 的原因。

图 18.11 您可以在控制平面节点上运行应用程序 Pod,但在生产环境中不应这样做。

应用程序正在运行,NodePort 服务意味着所有节点都在监听端口 30000,包括控制平面节点。如果您浏览到任何节点的 IP 地址,您将看到 APOD 应用程序。您的请求将被导向标准节点上的 Web Pod,并执行 API 调用,这可能被导向任意节点的 Pod。

现在试试看 您的虚拟机运行时设置您的网络,以便您可以通过 IP 地址访问虚拟机。获取任意节点的地址,然后在您的宿主机上浏览到它。

# print the IP address saved in the setup script:
cat /tmp/ip.txt

# browse to port 30000 on that address--if your VM 
# provider uses a complicated network stack, you may
# not be able to reach the VM externally :(

我的输出显示在图 18.12 中。我在做练习时看到的图片要生动得多,但我没有截图,所以我们只有这张彗星图片。

图 18.12 NodePort 和 ClusterIP 服务跨越 Pod 网络,以便流量可以路由到任意节点。

如果您愿意保持简单,使用 NodePorts、HostPaths 和可能还有 NFS 卷,构建自己的 Kubernetes 集群并不复杂。如果您想扩展这个集群并添加更多节点;Vagrant 设置包括 kiamol-node2kiamol-node3 的机器定义,因此您可以使用这些虚拟机名称重复本节的前两个练习来构建一个四节点集群。但这仅仅给您一个无聊的全 Linux 集群。Kubernetes 的一个巨大好处是它可以运行各种应用程序。接下来,我们将看到如何向集群添加不同的架构——一个 Windows 服务器——这样我们就可以运行全 Windows 或混合 Linux-Windows 应用程序。

18.4 添加 Windows 节点并运行混合工作负载

Kubernetes 网站本身就说 Windows 应用程序构成了许多组织中运行的服务和应用程序的大部分,如果您考虑跳过这一节,请记住这一点。我不会过多地详细介绍 Windows 容器和与 Linux 的区别(关于这一点,您可以阅读我的书 Windows 上的 Docker;Packt 出版,2019)——只是基础知识,以便您了解 Windows 应用程序如何在 Kubernetes 中适配。

容器镜像是为特定架构构建的:操作系统和 CPU 的组合。容器使用它们运行所在机器的内核,因此必须与镜像的架构相匹配。您可以在树莓派上构建 Docker 镜像,但不能在您的笔记本电脑上运行,因为 Pi 使用 Arm CPU 而您的笔记本电脑使用 Intel。操作系统也是如此——您可以在 Windows 机器上构建用于运行 Windows 应用程序的镜像,但不能在 Linux 服务器上的容器中运行该镜像。Kubernetes 通过在同一个集群中拥有不同架构的节点来支持不同类型的工作负载。

您对集群的多样性有一些限制,但图 18.13 中的内容是您可以真正构建的。控制平面仅支持 Linux,但 kubelet 和代理是跨平台的。AWS 有基于 Arm 的服务器,其价格几乎相当于 Intel 服务器的半价,您可以将它们用作 EKS 中的节点。如果您有一个大型应用程序套件,其中一些应用程序在 Arm 上的 Linux 中运行,一些需要 Intel 上的 Linux,还有一些是 Windows 系统,您可以在一个集群中运行和管理它们所有。

图 18-13

图 18.13 谁没有闲置的树莓派和积满灰尘的 IBM Z 大型机?

让我们开始操作,将一个 Windows 节点添加到您的集群中。方法与添加 Linux 节点相同——启动一个新的虚拟机,添加容器运行时和 Kubernetes 工具,并将其加入集群。Windows Server 2019 是 Kubernetes 支持的最小版本,Docker 是当时可用的唯一容器运行时——Windows 的 containerd 支持正在进行中。

现在试试看 创建一个 Windows 虚拟机,并安装 Kubernetes 的先决条件。Vagrant 中的文件夹共享对于 Windows 并不总是有效,因此您需要从 GitHub 上的书籍源下载设置脚本。

# spin up a Windows Server 2019 machine:
vagrant up kiamol-node-win

# connect--you need the password, which is vagrant
vagrant ssh kiamol-node-win

# switch to PowerShell:
powershell

# download the setup script:
curl.exe -s -O windows-setup.ps1 https://raw.githubusercontent.com/sixeyed/kiamol/master/ch18/setup/windows-setup.ps1

# run the script--this reboots the VM when it finishes:
./windows-setup.ps1

Windows 设置需要启用操作系统功能并安装 Docker;完成这些操作后,脚本会重启虚拟机。您可以在图 18.14 中看到,我的会话已经回到了正常的命令行。

图 18-14

图 18.14 添加 Windows 节点第一阶段是安装容器运行时。

这只是设置的第一部分,因为控制平面需要配置以支持 Windows。标准的 flannel 和 kube proxy 部署不会为 Windows 节点创建 DaemonSet,因此我们需要将其作为额外步骤来设置。新的 DaemonSets 在规范中使用 Windows 容器镜像,并且 Pod 被配置为与 Windows 服务器一起设置网络。

现在试试看 部署 Windows 节点的新的系统组件。

# connect to the control plane:
vagrant ssh kiamol-control

# create the Windows proxy:
kubectl apply -f /vagrant/setup/kube-proxy.yml

# create the Windows network:
kubectl apply -f /vagrant/setup/flannel-overlay.yml

# confirm that the new DaemonSets are there:
kubectl get ds -n kube-system

再次强调,如果你认真考虑运行自己的混合集群,这将是你需要添加到初始集群设置中的内容。我将其作为一个单独的步骤,以便你可以看到需要更改什么以添加 Windows 支持——只要你运行的集群是 Kubernetes 1.14 或更高版本,你就可以使用现有的集群来实现这一点。图 18.15 中的输出显示了新的特定于 Windows 的 DaemonSets,期望计数为零,因为 Windows 节点尚未加入。

图 18.15

图 18.15 更新控制平面以在 Windows 节点上调度系统组件

加入 Windows 节点。它将为代理和网络组件安排 Pod,因此它将下载镜像并启动容器。一个不同之处在于,Windows 容器比 Linux 容器更受限制,因此 flannel 设置略有不同。Kubernetes 的 Windows 特别兴趣小组(SIG)发布了一个辅助脚本,用于设置 flannel 和 kubelet。我在本章的源文件夹中有一个该脚本的快照,它与我们在运行的 Kubernetes 版本相匹配;在第二个设置脚本之后,节点就准备好加入集群了。

现在试试看,将剩余的依赖项添加到 Windows 节点,并将其加入集群。

# connect to the Windows node:
vagrant ssh kiamol-node-win

# run PowerShell:
powershell

# download the second setup script:
curl.exe -s -o PrepareNode.ps1
 https://raw.githubusercontent.com/sixeyed/kiamol/master/ch18/setup/PrepareNode.ps1

# run the script:
.\PrepareNode.ps1

# run the join command--remember to use your command; this
# is just a reminder of how the command looks:
kubeadm join 172.21.120.227:6443 
--token 5wbq7j.bew48gsfy0maa2bo     
--discovery-token-ca-cert-hash sha256:2c520ea15a99bd68b74d04f40056996dff5b6ed1e76dfaeb0211c6db18ba0393

你在图 18.16 中看到的“此节点已加入集群”的愉快信息有点过于乐观。新节点需要下载代理和网络镜像。flannel 镜像为 5 GB,因此 Windows 节点准备就绪可能需要几分钟。

图 18.16

图 18.16 显示加入 Windows 节点使用与 Linux 节点相同的 kubeadm 命令。

当下载进行时,我们将探讨如何对运行在不同架构上的应用程序进行建模。Kubernetes 不会自动确定哪些节点适合哪些 Pod——仅从镜像名称本身很难做到这一点。相反,你需要在 Pod 规范中添加一个选择器来指定 Pod 需要的架构。列表 18.2 显示了一个将 Pod 设置为在 Windows 节点上运行的选择器。

列表 18.2 api.yaml,使用节点选择器请求特定的操作系统

spec:                                        # Pod spec in the Deployment
  containers:
    - name: api
    image: kiamol/ch03-numbers-api:windows   # A Windows-specific image
  nodeSelector:
    kubernetes.io/os: windows                # Selects nodes with Windows OS

就这些了。记住,Pod 在单个节点上运行,所以如果你的 Pod 规范中有多个容器,它们都需要使用相同的架构。如果你正在运行一个多架构集群,为每个 Pod 包含一个节点选择器是一个好习惯,以确保 Pod 总是出现在它们应该出现的地方。你可以包括操作系统、CPU 架构或两者都包括。

18.2 节中的列表是随机数 API 的 Windows 版本,它还附带了一个 Linux 版本的网站。网站规范包括 Linux 操作系统的节点选择器。你可以部署应用程序,Pod 将在不同的节点上运行,但网站仍然通过 ClusterIP 服务以通常的方式访问 API Pod,即使它运行在 Windows 上。

现在试试这个 这是一个混合应用程序,包含一个 Windows 组件和一个 Linux 组件。两者都使用相同的 YAML 格式,在规格说明中,只有节点选择器显示它们需要在不同的架构上运行。

# connect to the control plane:
vagrant ssh kiamol-control

# wait for all the nodes to be ready:
kubectl -n kube-system wait --for=condition=Ready node --all

# deploy the hybrid app:
kubectl apply -f /vagrant/numbers

# wait for the Windows Pod to be ready:
kubectl wait --for=condition=ContainersReady pod -l app=numbers,component=api

# confirm where the Pods are running:
kubectl get pods -o wide -l app=numbers

# browse to port 30001 on any node to use the app

真遗憾,图 18.17 中的演示应用程序如此基础,因为这个功能已经花费了 Kubernetes 社区和微软、Docker 的工程团队多年的时间和大量努力。

图片

图 18.17 一个世界级的容器编排器运行混合应用程序以生成一个随机数

白天,我作为顾问帮助公司采用容器技术,这里的模式正是许多组织希望对其 Windows 应用程序做的事情——在不做任何更改的情况下将它们迁移到 Kubernetes,然后逐步通过在轻量级 Linux 容器中运行的新组件来分解单体架构。这是一种实用且风险低的现代化应用程序的方法,充分利用了 Kubernetes 的所有功能,并为你提供了轻松迁移到云的途径。

在第一章中,我说 Kubernetes 运行你的应用程序,但它实际上并不关心那些应用程序是什么。我们将通过最后一次部署到集群:Windows 宠物商店应用程序来证明这一点。微软在 2008 年构建了这个演示应用程序来展示 .NET 的最新功能。它使用的技术和方法早已被取代,但源代码仍然存在,我已经打包好以在 Windows 容器中运行,并在 Docker Hub 上发布了镜像。这个练习表明,你真的可以在 Kubernetes 中运行十年前的应用程序,而无需对代码进行任何更改。

现在试试这个 部署一个遗留的 Windows 应用程序。这个应用程序会下载更多的容器镜像,所以启动需要一段时间。

# on the control plane, deploy the Petshop app:
kubectl apply -f /vagrant/petshop/

# wait for all the Pods to start--it might need more than five minutes:
kubectl wait --for=condition=ContainersReady pod -l app=petshop --timeout=5m
kubectl get pods -o wide -l app=petshop

# browse to port 30002 on any node to see the app 

就这样了。图 18.18 可能是伪造的——但不是——你可以自己运行这个来证明它。(我必须承认我试了两次——我的 Windows 虚拟机第一次尝试时失去了网络连接,这很可能是 Hyper-V 的问题。)宠物商店是一个最后一次代码更改是在 12 年前的应用程序,现在运行在 Kubernetes 的最新版本中。

图片

图 18.18 我打赌这本书很久没有宠物商店的截图了。

我们就到这里为止这个集群。如果你想添加更多的 Windows 节点,你可以重复本节的设置和加入练习,用于 Vagrant 中定义的两个更多机器:kiamol-node-win2kiamol-node-win3。如果你至少有 16 GB 的内存,你几乎可以将控制平面节点、三个 Linux 节点和三个 Windows 节点挤在你的机器上。我们将以查看多节点 Kubernetes 集群的考虑因素和多架构的未来结束。

18.5 规模化理解 Kubernetes

无论您是否认真遵循本章中的练习,还是只是浏览了一下,您现在对设置和管理 Kubernetes 集群的复杂性有了很好的了解,您也会理解为什么我建议在实验室环境中使用 Docker Desktop 或 K3s。部署多节点集群是一个很好的学习练习,可以了解所有部件是如何组合在一起的,但这不是我为生产推荐的事情。

Kubernetes 的一切都是关于高可用性和可扩展性,您拥有的节点越多,管理起来就越复杂。您需要多个控制平面节点来实现高可用性;如果您丢失了控制平面,您的应用程序将继续在节点上运行,但您无法使用 kubectl 来管理它们,它们也不再是自我修复的。控制平面将所有数据存储在 etcd 中。为了更好的冗余,您可以在集群外部运行 etcd。为了提高性能,您可以运行一个额外的 etcd 数据库,专门用于存储 Kubernetes 为对象记录的事件。事情开始看起来很复杂,但我们仍在处理单个集群,而不是多个集群的高可用性。

您可以构建运行在巨大规模的 Kubernetes 集群:最新版本支持单个集群中最多 5,000 个节点和 150,000 个 Pod。在实践中,当您达到大约 500 个节点时,您可能会遇到 etcd 或您的网络插件的性能问题,然后您需要独立扩展控制平面的部分。好消息是,如果您那些节点相当强大,您可以使用仅由三个节点管理的控制平面运行一个拥有数百个工作节点的集群。坏消息是您需要管理所有这些,并且您需要决定您是更倾向于一个大型集群还是多个较小的集群。

尺度的另一面是能够在单个平台上尽可能多地运行你的应用程序目录。在本章中,您可以看到通过添加 Windows 节点可以运行多架构集群——Arm 和 IBM 节点以相同的方式工作——这意味着您几乎可以在 Kubernetes 中运行任何东西。旧应用程序会带来自己的挑战,但 Kubernetes 的一个重大优势是您不需要重写这些应用程序。将单体应用程序分解为更云原生架构可以带来好处,但这可以是长期计划的一部分,该计划从将应用程序迁移到 Kubernetes 开始。

我们没有空间继续讨论了。您应该让您的集群在实验室中继续运行,但完成之后,请回到这个最后的练习来清理它。

现在尝试一下 您有几种关闭集群的方法——在尝试实验室之后选择一个。

# suspend the VMs--this preserves state so the VMs 
# still consume disk or memory:
vagrant suspend

# OR stop the VMs--you can start them again, but they might 
# get new IP addresses, and then your cluster won’t be accessible:
vagrant halt

# Or, if you’re really done with the cluster, delete everything:
vagrant destroy

18.6 实验室

这里是本章的一个简单实验,但需要一些研究。在你部署你的集群之后,时间会流逝,在某个时刻,节点将需要进行维护工作。Kubernetes 允许你安全地将一个节点从集群中移除——将其 Pods 移动到另一个节点——完成维护后再将其重新上线。让我们为集群中的 Linux 节点做这个操作。只有一个提示:你可以用 kubectl 完成所有操作。

这是一个有用的实验,值得尝试,因为无论你使用哪个平台,你都会希望暂时从服务中移除一个节点。我的解决方案在 GitHub 的常规位置供你参考:github.com/sixeyed/kiamol/blob/master/ch18/lab/README.md

19 控制工作负载放置和自动扩展

Kubernetes 决定在哪里运行你的工作负载,将它们分散在集群中以充分利用你的服务器并为你的应用程序提供最高的可用性。决定哪个节点将运行新的 Pod 是调度器的任务,它是控制平面组件之一。调度器使用它能获取的所有信息来选择一个节点。它查看服务器的计算能力以及现有 Pod 使用的资源。它还使用你可以将其钩入应用程序规范中的策略,以对 Pod 运行的地点有更多的控制。在本章中,你将学习如何将 Pod 指向特定的节点以及如何根据其他 Pod 来安排 Pod 的放置。

我们还将在本章中介绍工作负载放置的两个其他方面:自动扩展和 Pod 驱逐。自动扩展允许你指定应用程序副本的最小和最大数量,以及 Kubernetes 用来衡量应用程序工作强度的某些指标。如果 Pod 过载,集群会自动扩展,添加更多副本,并在负载减少时再次缩小。驱逐是节点资源达到极限的极端情况,Kubernetes 会移除 Pod 以保持服务器稳定。我们将介绍一些复杂细节,但了解原则对于获得健康集群和性能良好的应用程序的正确平衡至关重要。

19.1 Kubernetes 如何调度工作负载

当你创建一个新的 Pod 时,它进入挂起状态,直到它被分配到一个节点。调度器看到新的 Pod 并尝试找到运行它的最佳节点。调度过程包括两个部分:首先是过滤,排除任何不合适的节点,然后是评分,对剩余的节点进行排名并选择最佳选项。图 19.1 显示了简化示例。

图片

图 19.1 调度器根据节点的适用性和当前工作负载选择节点。

你已经在第十七章中看到了过滤阶段的实际应用,当时你了解到控制平面节点与应用工作负载是隔离的。这是通过污点(taint)实现的,污点是一种标记节点的方式,表示它不适合一般工作。默认情况下,master污点应用于控制平面节点,但污点实际上是一种特殊的标签类型,你可以向节点添加自己的污点。污点与标签一样有一个键值对,它们还具有一个效果,告诉调度器如何处理这个节点。你将使用污点来识别与其他节点不同的节点。在下一个练习中,我们将向节点添加一个污点来记录节点所拥有的磁盘类型。

现在试试 Run a simple sleep app, and then add a taint to your node to see how it affects the workload.

# switch to the chapter’s source:
cd ch19

# print the taints already on the nodes:
kubectl get nodes -o=jsonpath='{range .items[*]}{.metadata.name}{.spec.taints[*].key}{end}'

# deploy the sleep app:
kubectl apply -f sleep/sleep.yaml

# add a taint to all nodes:
kubectl taint nodes --all kiamol-disk=hdd:NoSchedule

# confirm that the sleep Pod is still running:
kubectl get pods -l app=sleep

污点的键值部分是任意的,你可以用它来记录你关心的节点方面的任何内容——也许有些节点内存较少或网卡较慢。这个污点的效果是NoSchedule,这意味着除非它们明确容忍污点,否则工作负载不会在这个节点上调度。如图 19.2 所示,应用NoSchedule污点不会影响现有工作负载——在节点被污染后,sleep Pod 仍然在运行。

图片

图 19.2 Pod 需要在受污染的节点上运行时需要容忍,除非它们在污染之前就已经在运行。

现在有了污点,调度器将过滤掉所有新的 Pod,除非 Pod 规范包含对污点的容忍。容忍表示工作负载承认污点并愿意与其一起工作。在这个例子中,我们标记了带有旋转磁盘的节点,这些节点可能比带有固态磁盘的节点性能较低。列表 19.1 包含一个容忍,表示这个 Pod 愿意在这些较慢的节点上运行。

列表 19.1 sleep2-with-tolerations.yaml,容忍受污染的节点

spec:                           # The Pod spec in a Deployment
  containers:
    - name: sleep
      image: kiamol/ch03-sleep      
  tolerations:                  # Lists taints this Pod is happy with
      - key: "kiamol-disk"      # The key, value, and effect all need 
        operator: "Equal"       # to match the taint on the node.
        value: "hdd"
        effect: "NoSchedule"

在这个练习中,我们污染了每个节点,以便调度器将它们全部过滤掉以供新的 Pod 请求使用,除非规范包含容忍。无法调度的 Pod 将保持挂起状态,直到发生变化——一个没有污点的节点加入,或者从现有节点移除污点,或者 Pod 规范发生变化。一旦发生变化并且调度器可以找到合适的放置位置,Pod 将被调度以运行。

现在试试看:不使用容忍来部署 sleep 应用的副本。它将保持挂起状态。更新它以添加容忍,然后它将运行。

# create a Pod without a toleration:
kubectl apply -f sleep/sleep2.yaml

# confirm the Pod is pending:
kubectl get po -l app=sleep2

# add the toleration from listing 19.1:
kubectl apply -f sleep/update/sleep2-with-tolerations.yaml
kubectl get po -l app=sleep2

这个练习使用了 Deployment,因此容忍实际上被添加到了一个新的 Pod 中,并且新的 Pod 被调度了——你可以在图 19.3 中看到这一点。但是,如果你创建了一个没有容忍的普通 Pod,它将进入挂起状态,当你添加容忍时,同一个 Pod 将被调度;调度器会继续尝试为任何未调度的 Pod 找到节点。

图片

图 19.3 如果 Pod 的容忍与节点的污点匹配,那么它可以在该节点上运行。

NoSchedule效果是一个硬污点——它在调度器的过滤阶段,因此除非有容忍,否则 Pod 不会在受污染的节点上运行。一个较软的替代方案是PreferNoSchedule,它将限制移动到评分阶段。受污染的节点不会被过滤掉,但它们的评分低于没有污点的节点。PreferNoSchedule污点意味着 Pod 不应该在该节点上运行,除非它们有对该污点的容忍,除非没有其他合适的节点。

重要的是要理解污点(taints)和容忍度(tolerations)是用来表达关于节点的负面信息,这意味着它只适合某些 Pod;它不是节点和 Pod 之间的积极关联。具有容忍度的 Pod 可能运行在污点化的节点上,也可能不运行,因此容忍度不是确保 Pod 仅在特定节点上运行的良策。你可能需要像 PCI 合规性这样的东西,其中财务应用程序应该仅在经过加固的节点上运行。为此,你需要使用NodeSelector,它根据标签过滤节点——我们在第十七章中使用了它来确保 Pod 在正确的 CPU 架构上运行。列表 19.2 显示了不同类型的调度提示如何协同工作。

列表 19.2 sleep2-with-nodeSelector.yaml,一个容忍度和一个节点选择器

spec:
  containers:
    - name: sleep
      image: kiamol/ch03-sleep      
  tolerations:                            # The Pod tolerates nodes 
    - key: "kiamol-disk"                  # with the hdd taint.
      operator: "Equal"
      value: "hdd"
      effect: "NoSchedule"
  nodeSelector:                           # The Pod will run only on nodes
    kubernetes.io/arch: zxSpectrum        # that match this CPU type.

这个规范说明 Pod 可以容忍具有硬盘污点的节点,但架构必须是 ZX Spectrum。你的集群中不会有 ZX Spectrum,所以当你部署这个时,新的 Pod 不会被调度。我选择那个 CPU 不仅是因为怀旧,还为了强调这些标签只是没有验证的键值对。osarch标签由 Kubernetes 在节点上设置,但在你的 Pod 规范中,你可能会不小心使用错误的值,并且你的 Pod 会保持挂起状态。

现在试试看:部署列表 19.2 中的 sleep 应用更新,看看你的集群中是否有匹配的节点。

# show the node’s labels:
kubectl get nodes --show-labels

# update the Deployment with an incorrect node selector:
kubectl apply -f sleep/update/sleep2-with-nodeSelector.yaml

# print the Pod status:
kubectl get pods -l app=sleep2 

你可以在图 19.4 的输出中看到我们为什么使用 Deployment 来运行这个应用程序。新的 Pod 进入挂起状态,并且它将保持在那里,直到你在你的集群中添加 ZX Spectrum(这意味着构建 kubelet 的八位版本和容器运行时)。应用程序仍然在运行,因为 Deployment 不会在替换达到所需容量之前缩小旧的 ReplicaSet。

图片

图 19.4 如果 Pod 无法调度,当它在 Deployment 中运行时不会中断应用程序。

节点选择器确保应用程序仅在具有特定标签值的节点上运行,但你通常希望比直接相等匹配有更多的灵活性。更细粒度的控制可以通过亲和力反亲和力来实现。

19.2 使用亲和力和反亲和力指导 Pod 放置

Kubernetes 为节点应用一组标准的标签,但标准会随时间变化。系统提供的标签以命名空间为前缀,命名空间以与对象规范 API 版本相同的方式进行版本控制。新集群使用kubernetes.io作为标签前缀,但旧版本使用beta.kubernetes.io。beta 标签表示一个功能还不稳定,规范可能会改变,但功能可以通过多个 Kubernetes 版本保持 beta 状态。如果你想要 Pod 仅限于特定的架构,你需要允许 beta 命名空间,以便你的规范可以在不同的 Kubernetes 版本之间移植。

亲和力提供了一种丰富的方式来向调度器表达偏好或要求。您可以通过声明对某些节点的亲和力来确保 Pod 落在那些节点上。亲和力使用节点选择器,但使用匹配表达式而不是简单的相等检查。匹配表达式支持多个子句,因此您可以构建更复杂的请求。列表 19.3 使用亲和力来说明 Pod 应该在 64 位 Intel 节点上运行,这种方式适用于新旧集群。

列表 19.3 sleep2-with-nodeAffinity-required.yaml,一个具有节点亲和力的 Pod

affinity:                           # Affinity expresses a requirement
  nodeAffinity:                     # or a preference for nodes.
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:               # Match expressions work on
          - key: kubernetes.io/arch       # labels, and you can supply
            operator: In                  # a list of values that 
            values:                       # should match or not match.
              - amd64
        - matchExpressions:               # Multiple match expressions
          - key: beta.kubernetes.io/arch  # work as a logical OR.
            operator: In
            values:
              - amd64

听起来令人恐惧的requiredDuringSchedulingIgnoredDuringExecution实际上只是意味着这是一个对调度器的硬性规则,但它不会影响任何现有的 Pod——一旦它们被调度,即使节点标签发生变化,它们也不会被移除。这两个匹配表达式涵盖了新旧标签命名空间中的“或”情况,取代了列表 19.2 中的简单节点选择器。列表 19.3 的完整规范包含硬盘容忍度,因此当您部署此规范时,sleep 应用将停止等待 ZX Spectrum 加入集群,并将在您的 Intel 节点上运行。

现在尝试一下 更新 sleep 应用。您的节点上应该有一个架构标签,因此新的 Pod 将运行并替换现有的 Pod。

# deploy the spec from listing 19.3:
kubectl apply -f sleep/update/sleep2-with-nodeAffinity-required.yaml

# confirm that the new Pod runs:
kubectl get po -l app=sleep2

图 19.5 对您来说应该不会带来惊喜:它只是展示了正在实施的新亲和规则。现在调度器可以找到一个符合要求的节点,以便 Pod 运行。

图片

图 19.5 显示了节点选择规则的更复杂集合。

我还有一个节点亲和力的例子,因为语法有点复杂,但了解您可以做什么是很好的。节点亲和力是一种清晰的方式来表达结合硬性规则和软性偏好的调度要求,您可以使用它做的比容忍度和简单的节点选择器更多。列表 19.4 是一个规范的缩写,告诉调度器:Pod 必须在 Intel 节点上运行,并且它必须是 Windows 或 Linux,但最好是 Linux。

列表 19.4 sleep2-with-nodeAffinity-preferred.yaml,要求和偏好

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/arch        # This rule requires 
            operator: In                   # an Intel CPU.
            values:
              - amd64
          - key: kubernetes.io/os  
            operator: In
            values:                        # And either Linux
              - linux                      # or Windows OS
              - windows
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:                        # But the preference
        matchExpressions:                  # is for Linux.
        - key: kubernetes.io/os
          operator: In
           values:
            - linux

如果您有一个以 Linux 为主、仅有少数 Windows 节点运行旧应用的混合架构集群,这个过程就非常棒。您可以构建混合架构的 Docker 镜像,因此相同的镜像标签可以在 Linux 和 Windows(或 Arm 或任何其他操作系统和架构组合)上使用,所以一个容器规范适用于多个系统。具有此规范的 Pod 将优先选择 Linux 节点,但如果 Linux 节点已满,而 Windows 节点有容量,那么我们将使用该容量并运行 Windows Pods。

亲和力语法有点难以操作,因为它非常通用。在必需规则中,多个匹配表达式作为一个逻辑 AND 工作,多个选择器作为一个 OR 工作。在首选规则中,多个匹配表达式是一个 AND,你使用多个首选来描述一个 OR。列表 19.4 的完整规范包括 OR 逻辑来覆盖多个命名空间;我们不会运行它,因为输出与之前的练习相同,但如果你在表达亲和力时遇到困难,它是一个很好的参考。图 19.6 显示了规则的外观。

图 19.6

图 19.6 你可以使用不同的节点标签使用多个条件来表示亲和力规则。

你应该很好地掌握亲和力,因为你可以用它不仅仅与节点一起使用:Pod 可以表达对其他 Pod 的亲和力,以便它们被调度在同一个节点上,或者反亲和力,以便它们被调度在不同的节点上。这种能力支持两个常见的用例。第一个是你希望不同组件的 Pod 被放置在一起以减少它们之间的网络延迟。第二个是你希望同一组件的副本在集群中分布以增加冗余。列表 19.5 显示了随机数 Web 应用程序的第一个场景。

列表 19.5 web.yaml,Pod 亲和力以放置组件

affinity:                           # Affinity rules for Pods use
  podAffinity:                      # the same spec as node affinity.
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:         # This looks for the app and
            - key: app              # component labels to match.
              operator: In
              values:
                - numbers
            - key: component
              operator: In
              values:
                - api
        topologyKey: "kubernetes.io/hostname"  

Pod 亲和力遵循与节点亲和力相同的规范,如果你真的想让你团队感到困惑,你可以包括两者。列表 19.4 是一个必需的规则,所以如果调度器无法满足它,Pod 将被留下等待。匹配表达式作为一个标签选择器工作,所以这意味着 Pod 必须被调度在已经运行具有标签app=numberscomponent=api的 Pod 的节点上。这就是放置,所以只剩下拓扑键来描述,这将需要它自己的段落。

拓扑描述了你的集群的物理布局——节点所在的位置——这在不同细节级别的节点标签中设置。主机名标签始终存在,并且对于节点是唯一的;集群可以添加自己的细节。云提供商通常会添加区域和区域标签,这些标签说明了服务器所在的位置;本地集群可能会添加数据中心和机架标签。拓扑键设置亲和力应用的水平:主机名实际上意味着将 Pod 放在同一个节点上,而区域则意味着将 Pod 放在与另一个 Pod 在同一区域内的任何节点上。主机名是一个足够好的拓扑键来观察亲和力的作用,你可以在单个节点上做到这一点。

现在试试看 部署随机数应用程序,使用 Pod 亲和力以确保 Web 和 API Pod 在同一个节点上运行。

# remove the taint we applied to simplify things:
kubectl taint nodes --all kiamol-disk=hdd:NoSchedule-

# deploy the random-number app:
kubectl apply -f numbers/

# confirm that both Pods are scheduled on the same node:
kubectl get pods -l app=numbers -o wide

您可以在图 19.7 中看到,这正如预期的那样——我只有一个节点,当然,两个 Pod 都调度在那里。在一个更大的集群中,web Pod 将保持挂起状态,直到 API Pod 被调度,然后它会跟随 API Pod 到同一个节点。如果该节点没有运行 web Pod 的容量,它将保持挂起状态,因为需要这个规则(一个首选规则将允许 Pod 在没有 API Pod 的节点上运行)。

图片

图 19.7 Pod 亲和性控制工作负载相对于现有工作负载的放置。

反亲和性使用相同的语法,您可以使用它来使 Pod 远离节点或其他 Pod。反亲和性在需要高可用性的大规模组件中非常有用——回想一下第八章中的 Postgres 数据库。该应用程序使用了一个具有多个副本的 StatefulSet,但 Pod 本身可能都最终在同一个节点上。这违背了使用副本的全部目的,因为如果节点宕机,它会带走所有的数据库副本。反亲和性可以用来表达规则:让我远离像我这样的其他 Pod,这样可以将 Pod 分散到不同的节点上。我们不会回到 StatefulSet;我们将保持简单,为随机数 API 部署该规则,并看看扩容时会发生什么。

现在尝试一下:更新 API 部署以使用 Pod 反亲和性,使所有副本都在不同的节点上运行。然后扩容并确认状态。

# add an antiaffinity rule to the API Pod:
kubectl apply -f numbers/update/api.yaml

# print the API Pod status:
kubectl get pods -l app=numbers

#scale up the API and the web components:
kubectl scale deploy/numbers-api --replicas 3
kubectl scale deploy/numbers-web --replicas 3

# print all the web and API Pod statuses:
kubectl get pods -l app=numbers 

图片

图 19.8 在单节点集群上,节点反亲和性产生了意外的结果。

您需要仔细查看图 19.8,因为结果可能并非您所预期。更新的 API 部署创建了一个新的 ReplicaSet,它创建了一个新的 Pod。该 Pod 保持挂起状态,因为反亲和性规则不允许它在与现有 API Pod(它试图替换的那个 Pod)相同的节点上运行。当 API 部署扩容时,另一个副本确实运行了,因此我们在同一节点上有两个 API Pod——但这是由前一个 ReplicaSet 创建的,它不包括反亲和性规则。部署试图在两个 ReplicaSet 之间遵守三个副本的请求,但由于新 Pod 没有上线,它又扩展了旧 Pod 的另一个副本。

那么 web Pods 呢——您期望看到三个都在运行吗?好吧,不管您是否期望,现在有三个正在运行。亲和性和反亲和性规则只检查 Pod 标签的存在,而不是 Pod 的数量。web Pod 的亲和性规则表示它需要在有 API Pod 的地方运行,而不是只有一个 API Pod 的地方。如果您只想有一个 web Pod 与一个 API Pod 一起运行,您需要在 web Pod 规范中为其他 web Pods 设置反亲和性规则。

调度偏好变得复杂,因为调度器在决策中考虑了众多因素。你简单的亲和规则可能不会按预期工作,你可能需要调查污点、节点标签、Pod 标签、资源限制和配额——甚至控制平面节点的调度器日志文件。记住,如果调度器找不到节点,所需的规则将阻止你的 Pod 运行,所以考虑有一个备份首选规则。这个主题是本章中较为复杂的话题之一;接下来,我们将探讨如何让 Kubernetes 为我们自动调度更多的副本,这实际上比尝试控制工作负载放置要简单得多。

19.3 使用自动扩展控制容量

Kubernetes 可以通过添加或删除 Pod 来自动扩展你的应用程序。这种扩展是水平的,因为它利用了现有的节点;还有集群扩展,它添加和删除节点,但你主要会在云平台上看到它。我们将坚持使用水平 Pod 自动扩展,它的用户体验与我们在第十六章中讨论的 NetworkPolicy 对象略有不同。你可以部署一个自动扩展规范,描述你希望如何扩展 Pod,但除非 Kubernetes 可以检查现有 Pod 的负载,否则它不会对此采取任何行动。更广泛地说,Kubernetes 项目提供了 metrics-server 组件来进行基本的负载检查——一些发行版默认包含它;对于其他发行版,你需要手动部署它。

现在尝试一下 确认你的集群是否已安装 metrics-server 组件,如果没有,部署它,启用指标自动扩展,并使用 kubectl top命令。

# top shows resource usage if you have metrics-server installed:
kubectl top nodes

# if you get an error about “heapster,” 
# you need to install metrics-server:
kubectl apply -f metrics-server/

# wait for it to spin up:
kubectl wait --for=condition=ContainersReady pod -l k8s-app=metrics-server -n kube-system

# it takes a minute or so for collection to start:
sleep 60

# print the metric-server Pod logs:
kubectl logs -n kube-system -l k8s-app=metrics-server --tail 2

# look at node usage again:
kubectl top nodes

图 19.9 显示,我的实验室环境尚未部署metrics-server(它不是 Docker Desktop 或 Kind 的一部分,但已安装在 K3s 中),但幸运的是,补救措施比选择 Pod 网络简单得多。metrics-server部署是一个单一实现:如果你从 kubectl top命令中获得响应,你的集群正在收集所有用于自动扩展所需的指标;如果没有,只需部署metrics-server

图片

图 19.9 metrics-server收集 CPU 和内存统计数据,但它是一个可选组件。

不要将这些与我们第十四章中设置的 Prometheus 指标混淆。metrics-server收集的统计数据仅跟踪基本的计算资源,CPU 和内存,并且当被查询时只返回当前值。如果你的工作负载是 CPU 或内存密集型,这是一个用于自动扩展的简单选项,因为 Kubernetes 知道如何使用它而无需任何额外配置。列表 19.6 显示了使用 CPU 作为扩展指标的 Pod 自动扩展规范。

列表 19.6 hpa-cpu.yaml,基于 CPU 负载的水平 Pod 自动扩展

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler            # I love this name.
metadata:
  name: pi-cpu
spec:
  scaleTargetRef:                        # The target is the controller
    apiVersion: apps/v1                  # to scale--this targets the
    kind: Deployment                     # Pi app Deployment.
    name: pi-web
  minReplicas: 1                         # Range of the replica count
  maxReplicas: 5
  targetCPUUtilizationPercentage: 75     # Target CPU usage

自动扩展参数定义在单独的对象中,即 HorizontalPodAutoscaler (HPA),它作用于一个缩放目标——例如 Deployment 或 StatefulSet 这样的 Pod 控制器。它指定了副本数量的范围和期望的 CPU 利用率。自动扩展器通过监控所有当前 Pod 的平均 CPU 使用率(作为 Pod 规范中请求 CPU 数量的百分比)来工作。如果平均利用率低于目标,副本数量会减少,直到最小值。如果利用率高于目标,则会添加新的副本,直到最大值。我们在这本书中使用的 Pi web 应用是计算密集型的,因此它将展示自动扩展是如何工作的。

现在尝试一下 部署带有 HPA 的 Pi web 应用,并检查 Pods 的状态。

# deploy the manifests:
kubectl apply -f pi/

# wait for the Pod to start:
kubectl wait --for=condition=ContainersReady pod -l app=pi-web

# print the status of the autoscaler:
kubectl get hpa pi-cpu

现在,Pi 应用正在运行,规范请求了 125 毫核的 CPU(一个核心的八分之一)。最初,有一个副本,这是在 Deployment 规范中设置的,现在 HPA 正在监视是否需要创建更多的 Pods。HPA 从metrics-server获取数据,需要一两分钟才能跟上。你可以在图 19.10 中看到,当前的 CPU 利用率未知,但很快就会变成 0%,因为 Pod 没有进行任何工作。

图 19-10

图 19.10 HPA 与 metrics-server 合作收集统计数据,并与 Deployment 一起管理缩放。

这个 Pod 规范有一个 250 毫核的 CPU 限制,是请求量的两倍。向计算 Pi 的请求发送一个高精度的十进制数,你很快就会用完 0.25 个核心,平均利用率会急剧上升到 200%。然后 HPA 会启动并扩展,添加新的副本以帮助处理负载。

现在尝试一下 运行一个脚本,对 Pi web 应用进行一些并发调用,请求 10 万位十进制数,造成高 CPU 负载。确认 HPA 进行了扩展。

# run the load script--on Windows: 
.\loadpi.ps1

# OR on Linux/macOS:
chmod +x ./loadpi.sh && ./loadpi.sh

# give the metrics-server and HPA time to work:
sleep 60

# confirm that the Deployment has been scaled up:
kubectl get hpa pi-cpu

# print the Pod compute usage:
kubectl top pods -l app=pi-web

额外的 Pods 是否真正帮助处理负载取决于你的应用程序。在这种情况下,接收 Web 请求的任何 Pod 都会处理计算直到完成,因此新的 Pod 不会共享现有的负载。你可以在图 19.11 中看到,原始 Pod 正在以大约最大 250 毫核的速度运行,而新的 Pod 在 1 毫核下什么也不做。但那些额外的 Pod 增加了应用程序的容量,并且它们可以处理任何新到达的请求。

图 19-11

图 19.11 自动扩展的实际应用:当 CPU 使用率激增时,HPA 触发新的 Pods。

当你运行这个练习时,你应该看到类似的结果。HPA 会扩展到三个 Pod,在它进一步扩展之前,应用程序返回 Pi 响应,CPU 利用率下降,它不再需要扩展。HPA 每 15 秒添加更多的 Pod,直到利用率在目标范围内。当一个 Pod 达到最大容量而另外两个 Pod 无所事事时,平均 CPU 下降到 66%,这低于 75%的目标,所以 HPA 不会添加更多的 Pod(你可以重复几次负载脚本以确认它达到五个 Pod 的峰值)。当你停止发送请求时,负载将再次下降到 0%,然后 HPA 等待确保应用程序在五分钟内保持在目标范围内,然后它将缩放到一个副本。

我们这里有几个参数:在扩展或缩放之前等待多长时间,添加或删除多少 Pod,添加或删除 Pod 的速度有多快。在版本 1 的 HPA 规范中,这些值都不能更改,但在版本 2 规范中它们都被公开了。新的规范仍然是 Kubernetes 1.18 中的 beta-2 功能,这是一个相当重大的变化。基于 CPU 的单一缩放选项已被替换为通用的指标部分,并且可以控制缩放行为。列表 19.7 显示了更新 Pi HPA 的新规范。

列表 19.7 hpa-cpu-v2.yaml,版本 2 中的扩展 HPA 规范

metrics:                        # Metrics spec inside a version 2 HPA
- type: Resource
  resource:
    name: cpu                   # The resource to monitor is generic.
    target:                     # This checks for CPU, but you can use
      type: Utilization         # other metrics. 
      averageUtilization: 75
  behavior:
    scaleDown:                            # This sets the parameters 
      stabilizationWindowSeconds: 30      # when the HPA is scaling 
      policies:                           # down--it waits for 30 seconds
      - type: Percent                     # and scales down by 50% 
        value: 50                         # of the Pod count.
        periodSeconds: 15

我说自动缩放比亲和性更简单,但我的意思只是版本 1。版本 2 规范复杂,因为它支持其他类型的指标,并且你可以使用 Prometheus 指标作为缩放决策的来源。你需要更多的组件来实现这一点,所以我不深入细节,但请记住这是一个选项。这意味着你可以根据你收集的任何指标进行缩放,比如传入 HTTP 请求的速率或队列中的消息数量。

我们在这里坚持 75%的 CPU 目标,它使用相同的metrics-server统计信息,但我们已经调整了缩放行为,所以一旦处理完 Pi 请求,我们会看到 Pod 数量下降得更快。

现在试试看 更新 HPA 以使用版本 2 规范;这为缩放事件设置了一个较短的稳定期,所以你会看到 Pod 计数下降得更快。

# update the HPA settings:
kubectl apply -f pi/update/hpa-cpu-v2.yaml

# run the load script again--on Windows:
.\loadpi.ps1 

# OR on macOS/Linux:
./loadpi.sh

# wait for the HPA to scale up:
sleep 60

# confirm there are more replicas:
kubectl get hpa pi-cpu # go up

# there’s no more load, wait for the HPA to scale down:
sleep 60

# confirm there’s only one replica:
kubectl get hpa pi-cpu

# print the Deployment status:
kubectl get deploy pi-web

在这个练习中,你会看到缩放行为以相同的方式工作,因为版本 2 的默认值与版本 1 的默认值相同。这次缩放不会花费那么长时间,尽管你可能看到——如图 19.12 所示——查询 HPA 状态并不像部署本身那样快速反映变化。

图片

图 19.12 新的 HPA 规范倾向于快速缩放,适合突发性工作负载。

改变行为可以让您模拟 HPA 如何响应扩展事件。默认值是一个相当保守的版本,快速扩展和缓慢缩减,但您可以将其切换,使扩展更渐进,缩减更立即。“立即”并不是真的,因为收集的指标和提供给 HPA 之间的延迟只有几十秒。HPA 针对一个特定的目标,因此您可以为应用程序的不同部分设置不同的扩展规则和速率。

我们已经涵盖了 Pod 放置、扩展和缩减,而 HPA 只是指示控制器进行扩展,因此 Pod 规范中的任何调度要求都适用于所有 Pod。工作负载管理中的最后一个主题是抢占,即故意导致 Pod 失败的过程。

19.4 使用抢占和优先级保护资源

有时 Kubernetes 意识到一个节点工作过于努力,它会抢占一些 Pod 可能会失败,并提前关闭它们,给节点时间恢复。这是驱逐,仅在极端情况下发生,如果集群不采取行动,节点可能会变得无响应。被驱逐的 Pod 仍然留在节点上,这样您就有证据来追踪问题,但 Pod 容器被停止并移除,释放内存和磁盘。如果 Pod 由控制器管理,将创建一个替换 Pod,该 Pod 可能被调度到不同的节点上。

如果您在资源规格、配额、调度和扩展方面都做错了,就会发生抢占,因此节点运行了比它能管理的更多 Pod,并且内存或磁盘资源不足。如果发生这种情况,Kubernetes 认为该节点处于压力之下,并且驱逐 Pod 直到压力情况结束。同时,它会在节点上添加污点,以防止新的 Pod 被调度到该节点上。随着压力情况的缓解,它会移除污点,节点能够接受新的工作负载。

在演示或练习中,无法伪造内存或磁盘压力,因此要了解这是如何工作的,我们需要将您的实验室内存使用率最大化。与磁盘相比,使用内存更容易做到这一点,但这仍然不容易;默认情况下,当节点可用内存少于 100 MB 时,就会开始驱逐,这意味着几乎用完了您的所有内存。如果您想跟随本节中的练习,您真的需要在虚拟机中启动一个单独的实验室,这样您就可以调整 Kubernetes 设置并在虚拟机上而不是在您的机器上最大化内存。

现在尝试一下 使用 Vagrant 启动一个专用虚拟机。此设置已安装 Kind 并分配了 3 GB 的 RAM。创建一个新的 Kind 集群,并使用自定义 kubelet 配置降低驱逐的内存阈值。

# from the kiamol source root, create a VM with Vagrant:
cd ch01/vagrant/

# create the new VM:
vagrant up

# connect to the VM:
vagrant ssh

# switch to this chapter’s source inside the VM:
cd /kiamol/ch19

# create a customized Kind cluster:
kind create cluster --name kiamol-ch19--config ./kind/kiamol-ch19-config.yaml --image kindest/node:v1.18.8 

# wait for the node to be ready:
kubectl -n kube-system wait --for=condition=Ready node --all

# print the VM’s memory stats:
./kind/print-memory.sh

本练习中的脚本使用与 kubelet 相同的逻辑来确定它可以访问多少空闲内存。您可以在图 19.13 中看到,我的虚拟机报告总共有不到 3 GB 的内存和不到 1.5 GB 的空闲内存,这正是 Kubernetes 所看到的。

图片

图 19.13 如果你想测试内存压力,最好在一个专用环境中进行。

如果你不想为这些练习启动一个单独的虚拟机,那也行——但请记住默认的内存阈值是 100 MB。为了强制内存压力情况,你需要分配你机器上几乎所有的内存,这可能会也导致 CPU 峰值,并使整个系统变得无响应。你可以通过检查 kubelet 的实时配置来确认内存限制;它有一个可以通过使用 kubectl 代理请求的 HTTP 端点。

现在试试看 查询节点的配置 API 以查看 kubelet 的活动设置,并确认驱逐级别已被设置。

# run a proxy to the Kubernetes API server:
kubectl proxy

# in a new terminal connect to the VM again: 
cd ch01/vagrant/
vagrant ssh

# make a GET request to see the kubelet config:
curl -sSL "http://localhost:8001/api/v1/nodes/$(kubectl get node -o jsonpath={‘.items[0].metadata.name’})/proxy/configz"

在这个环境中,kubelet 被配置为当节点上只有 40%的内存可用时触发驱逐,正如你在图 19.14 中看到的。这是一个故意设置得很低的阈值,这样我们就可以轻松地触发驱逐;在生产环境中,你会将其设置得更高。如果你使用了一个没有在 kubelet 中明确设置的不同的实验室环境,你将使用默认的 100 MB。

图片

图 19.14 低级设置在 kubelet 配置中指定,你可以使用代理查看。

警告已经足够了……好吧——还有一个:当我计划这些练习时,试图强制内存压力时,我破坏了我的 Docker Desktop 环境。Kubectl 没有响应,我不得不卸载并重新安装一切。让我们继续触发预选。我已经构建了一个容器镜像,其中包含了 Linux 的stress工具,并且我有一个包含四个副本的 Deployment 规范,每个副本分配 300 MB 的内存。这应该会让节点只剩下不到 40%的总内存可用,并将其推入内存压力状态。

现在试试看 运行一个分配大量内存的应用程序,看看当节点处于内存压力时,Kubernetes 是如何驱逐 Pods 的。

# the stress Pods will allocate 1.2 GB of RAM:
kubectl apply -f stress/stress.yaml

# wait for them all to start:
kubectl wait --for=condition=ContainersReady pod -l app=stress

# print the node’s memory stats:
./kind/print-memory.sh

# list the Pods:
kubectl get pods -l app=stress

# remove the Deployment:
kubectl delete -f stress/stress.yaml

预选发生得很快,压力状态之间的转换也发生得很快。我在图 19.15 中剪掉了输出,因为我运行 Pod list命令太慢了,等到我运行的时候,节点已经驱逐了 23 个 Pod。

为什么会有这么多驱逐?Kubernetes 为什么不在节点处于内存压力时只驱逐一个 Pod,而将替换 Pod 留在挂起状态?它确实这样做了。但是,一旦 Pod 被驱逐,它就释放了一大批内存,节点迅速脱离了内存压力。与此同时,Deployment 创建了一个新的 Pod 来替换被驱逐的 Pod,这个 Pod 运行在不再被标记的节点上,但它立即分配了更多的内存,并再次触发了压力开关,导致另一个驱逐。由于这个应用程序在启动时分配了大量内存,这种情况可能会快速发生,但如果你的集群过载,使用真实的应用程序也可能出现相同的情况。

在生产环境中,预占事件应该是罕见的,但如果它确实发生了,你想要确保你的最不重要的工作负载被驱逐。kubelet 通过对它们进行排名来决定在内存压力情况下驱逐哪个 Pod。这种排名考虑了 Pod 相对于 Pod 规范中请求的内存量以及 Pod 的 优先级类别。优先级类别,本章的最后一个新概念,是分类你的工作负载重要性的简单方法。列表 19.8 显示了一个具有低值的自定义优先级类别。

图 19.15 许多 Pods。资源密集型 Pods 可能会导致驱逐/创建循环。

列表 19.8 low.yaml,一个用于低优先级工作负载的类别

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: kiamol-low    # The Pod spec uses this name to state its priority.
value: 100            # Priority is an integer up to 1 billion.
globalDefault: true
description: "Low priority--OK to be evicted"

数字值是决定优先级的因素:越大意味着越重要。Kubernetes 没有任何默认的优先级类别,所以如果你想保护更重要的工作负载,你需要自己映射。你通过在 Pod 规范中添加 PriorityClassName 字段来为一个工作负载分配优先级。在最后的练习中,我们将部署压力应用的两个版本:一个具有高优先级,一个具有低优先级。当内存压力到来时,我们将看到低优先级 Pods 被驱逐。

现在试试看 再次运行压力练习,但这次使用两个 Deployment,每个 Deployment 运行两个 Pods。内存分配是相同的,但 Pods 具有不同的优先级。

# create the custom priority classes:
kubectl apply -f priority-classes/

# deploy apps with high and low priorities:
kubectl apply -f stress/with-priorities/

# wait for the Pods to run:
kubectl wait --for=condition=ContainersReady pod -l app=stress
wait

# print the memory stats:
./kind/print-memory.sh

# confirm that the node is under memory pressure:
kubectl describe node | grep MemoryPressure

# print the Pod list:
kubectl get pods -l app=stress

当我运行练习时,一切工作得非常完美,你可以在图 19.16 中看到输出。节点可以管理三个 Pods 而不会陷入内存压力,被驱逐的第四个 Pod 总是来自低优先级规范;高优先级 Pods 继续运行。

图 19.16 添加优先级类别是保护关键 Pod 避免驱逐的简单保障措施。

不仅优先级类别可以保持高优先级 Pods 运行。所有 Pods 都使用了比它们请求的更多内存,所以它们都有资格被驱逐。这就是考虑优先级的时候。如果所有有资格的 Pods 具有相同的优先级(或没有优先级),Kubernetes 将根据实际使用量超过请求量的多少来做出选择,从而驱逐最严重的违规者。在 Pod 规范中包含资源请求以及限制是很重要的,但优先级类别是保护更重要工作负载的有用保护措施。

这是对工作负载管理主要方面的广泛巡礼。它们都是你将在生产中使用的功能,我们将通过回顾它们能提供什么以及它们如何协同工作来结束本章。

19.5 理解管理工作负载的控制方法

调度、自动扩展和驱逐都是比我们这里所涵盖的更高级的话题,它们具有更多的细微差别。你肯定会在你的 Kubernetes 之旅中使用它们,因此尽早引入一些控制措施是值得的。它们解决你在管理你的应用程序时遇到的不同问题,但它们都会相互影响,所以你需要谨慎使用。

在大型集群中,亲和性是你将使用最多的功能。节点亲和性允许你通过比仅使用命名空间更严格的隔离来分离工作负载,而 Pod 亲和性允许你模拟应用程序的可用性要求。你可以将 Pod 亲和性与节点拓扑结构结合使用,以确保副本在不同的故障域中运行,这样即使一个机架或区域丢失,也不会导致你的应用程序崩溃,因为其他 Pod 在不同的区域运行。记住,所需的亲和性是调度器的硬规则:如果你要求 Pod 在不同的区域,而你只有三个区域,第四个副本将永远处于挂起状态。

如果你的应用程序受 CPU 限制,自动扩展是一个很棒的功能,也很容易使用。然后你可以使用默认的 metrics-server 和简单的版本 1 HPA,确保你的 Pod 规范中有 CPU 请求。如果你想要基于更高级别的指标进行扩展,事情会变得更加复杂,但这绝对值得调查。当关键服务级别未达到时,应用程序自动扩展是 Kubernetes 的一个主要好处,这也是在生产环境中建立时应该努力实现的目标。扩展只是增加或减少副本的数量,所以如果你在规范中有亲和性规则,你需要确保它们可以在最大扩展级别上得到满足。

预占是 Kubernetes 处理内存或磁盘不足节点的安全机制。CPU 则不同,因为 Pod 可以通过限制来回收 CPU,而不需要停止容器。Kubernetes 通过驱逐 Pod 来缓解内存或磁盘压力,如果你的集群和应用程序大小合适,你很少会看到这种情况。你应该在你的 Pod 规范中包含资源请求,以便驱逐最严重的违规者,如果你有一些比其他工作负载更重要的工作负载,请考虑优先级类别。如果你确实遇到了预占情况,你需要迅速调查以确保节点不会不断在压力下翻转,不断添加然后驱逐 Pod(我们添加了节点压力指标到第十四章的集群仪表板,就是为了这个原因)。

那就是关于工作负载管理的一切。现在是时候清理集群(们)以腾出空间进行实验室操作了。

现在试试吧 清理你的主要实验室环境;如果你创建了一个自定义环境,它可以被移除。

# remove objects on your main cluster:
kubectl delete all,priorityclass,hpa -l kiamol=ch19

# if you deployed the metrics-server in section 19.3, remove it:
kubectl delete -f metrics-server/

# if you created a new VM in section 19.4, it can go too:
cd ch01/vagrant/
vagrant destroy

19.6 实验室

我们将使用 Pi 应用程序进入生产!在这个实验室中,你的任务是添加到规范中,这样我们就可以控制工作负载。这是我们需要的设置:

  • 由于数据主权问题,应用程序必须在欧洲地区运行。

  • 它应该基于目标 CPU 利用率 50% 自动扩展。

  • 必须运行两个到五个副本。

  • 负载最好在欧洲地区多个节点之间分散。

你可以在本章的练习中找到所有这些示例,除了一个,你可能需要查看 API 文档来了解如何构建最后一条规则。记住,节点拓扑是通过标签完成的,并且你可以为你的节点添加任何你想要的标签。我的解决方案在通常的位置:github.com/sixeyed/kiamol/blob/master/ch19/lab/README.md.

20 通过自定义资源和 Operators 扩展 Kubernetes

Kubernetes 的核心是一个高可用数据库和一个具有一致工作方式的 REST API。当你通过 API 创建一个 Pod 时,其定义会被存储在数据库中,并且控制器会收到通知,知道它需要将 Pod 分配给一个节点以使其运行。这是一个通用的模式,其中不同的控制器处理不同类型的对象,并且它是可扩展的,因此你可以添加自己的资源定义和自定义控制器来作用于这些资源。这可能听起来像是一个晦涩难懂的话题,但许多产品都会扩展 Kubernetes 以使产品本身更容易使用。这也是一种直接的方式来定制 Kubernetes,使其在你的组织中工作得更好。

自定义资源和控制器可以隐藏应用程序中的许多复杂性,在本章中,你将了解如何定义和操作它们。定义部分很简单,但控制器需要自定义代码。我们不会专注于代码,但我们会提供一些自定义化的示例,以便你可以看到它们能做什么。我们还将在本章中介绍 Operator 模式,这是一种使用自定义资源和控制器来自动化应用程序部署和持续操作任务的方法。

20.1 如何通过自定义资源扩展 Kubernetes

kubectl 命令与 Kubernetes REST API 密切对应。当你运行 kubectl get 时,它会向 API 发送请求以获取资源或资源列表。所有资源都有一组标准的操作可用,并且根据第十七章中介绍的 RBAC 规则,它们被定义为动词:create(创建)、get(获取)、list(列出)、watch(监视)和 delete(删除)。当你定义 Kubernetes 中的自定义资源时,它会自动支持 API 中的所有这些操作;Kubernetes 客户端也理解自定义资源,因此你可以像处理任何其他对象一样使用 kubectl 与它们一起工作。图 20.1 展示了集群如何支持自定义资源。

图片

图 20.1 Kubernetes 通过自定义资源进行扩展,它们的工作方式与标准资源相同。

你通过在 YAML 中指定资源的类型和在规范中所有字段来定义标准的 Kubernetes 对象——Pod 规范有一个容器列表,容器规范有一个镜像名称和一组端口。Kubernetes 将这些字段存储在模式中,因此它知道资源的结构并可以验证新对象。这就是 API 版本字段发挥作用的地方——HorizontalPodAutoscaler 资源的第 1 版结构与 v2beta2 不同。自定义资源也有一个已知的结构,你可以在自定义资源定义(CRD)对象中创建自己的模式。列表 20.1 展示了一个简单的 CRD,用于将待办事项记录为 Kubernetes 对象。

列表 20.1 todo-crd.yaml,一个用于在 Kubernetes 中存储待办事项的 CRD

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition     
metadata:                           # The name of the CRD needs to match
  name: todos.ch20.kiamol.net       # the names in the resource spec.
spec:
  group: ch20.kiamol.net      # Classifies a set of CRDs
  scope: Namespaced           # Can be clusterwide or namespaced
  names:                      # The names are how you refer to  
    plural: todos             # custom resources in YAML and kubectl.
    singular: todo
    kind: ToDo
  versions:                   # You have multiple versions.
    - name: v1                # Each version has a schema.
      served: true            # Makes resources available in the API
      storage: true           # Saves resources in etcd
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:             # The schema sets the structure of the
              type: object    # custom resource--ToDo objects have a
              properties:     # spec field, which has an item field.
                item:
                  type: string 

CRD 结构本身很冗长,尤其是模式部分读起来特别别扭,但它使用标准的 JSONSchema 项目来定义资源的结构,你的定义可以像你需要的那样复杂或简单。列表 20.1 中的 CRD 在视觉上可能有些刺眼,但它构成了一个简单的自定义资源。列表 20.2 展示了一个使用这种结构的 ToDo 项目。

列表 20.2 todo-ch20.yaml,一个 ToDo 自定义资源

apiVersion: "ch20.kiamol.net/v1"   # Group name and version of the CRD
kind: ToDo                         # The resource type  
metadata:                          # Standard metadata 
  name: ch20
spec:                              
  item: "Finish KIAMOL Ch20"       # The spec needs to match the CRD schema.

这看起来就像一个正常的 Kubernetes YAML,这正是 CRD 的全部意义——在 Kubernetes 中存储你自己的资源,并使它们感觉像标准对象。API 版本标识它为本书这一章节中的自定义资源,版本为 1。元数据是标准的元数据,因此它可以包括标签和注解,而 spec 是在 CRD 中定义的自定义结构。现在我们已经有了足够的 YAML,让我们将其付诸实践,并使用 Kubernetes 作为我们的待办事项列表应用程序。

现在试试看 部署一个自定义资源定义和一些资源,看看你是如何使用 kubectl 与它们一起工作的。

# switch to this chapter’s source:
cd ch20

# deploy the CRD:
kubectl apply -f todo-custom/

# print CRD details:
kubectl get crd -l kiamol=ch20

# create some custom resources:
kubectl apply -f todo-custom/items/

# list all the resources:
kubectl get todos

你可以在图 20.2 中看到,你与自定义资源的工作方式就像与其他任何资源一样。一旦 CRD 部署完成,API 现在支持 ToDo 对象,你可以通过应用 YAML 来创建项目。Kubectl 是管理自定义资源的工具,现在 getdescribedelete 命令对 ToDo 对象和 Pods 的工作方式相同。

图片

图 20.2 中的 CRDs 和自定义资源是用 YAML 描述并通过 kubectl 部署的。

CRD 规范丰富,允许你在资源周围构建大量逻辑,包括验证规则、子资源和多个版本。我们不会深入到那个层面的细节,但你可以确信自定义资源是 Kubernetes 中的一个成熟特性,它提供了你管理不断发展的对象定义集所需的所有功能。CRD 的目的是提供简化的用户体验,我们可以对 ToDo CRD 进行一些小的修改,使其更易于使用。

现在试试看 CRD 规范的更新使得 kubectl 输出更加有用。这次更新不会影响自定义资源;它只是增加了打印的列。

# update the CRD, adding output columns:
kubectl apply -f todo-custom/update/

# list the to-do resources again:
kubectl get todos

# delete one of the resources:
kubectl delete todo ch21

# show the detail of the other resource:
kubectl describe todo ch20

这个练习通过添加额外的打印列更新了 CRD,因此 API 在 get 请求中返回了额外的信息。你可以在图 20.3 中看到,这现在是一个功能齐全的待办事项列表应用程序。它甚至允许你删除项目,所以它比我们在本书中运行的待办事项 Web 应用程序更好。

图片

图 20.3 是一个由 Kubernetes 驱动的完全功能的待办事项应用程序,没有任何自定义代码!

这对于简单的演示来说是可以的,我们可能会分心编写一个自定义控制器,该控制器监视这些资源,将项目添加到你的 Google 日历中,并在到期时发送电子邮件提醒,但我们不会这样做。这不是自定义资源的良好用途,因为我们存储的对象和它们触发的操作与 Kubernetes 无关;我们不是与其他对象集成或扩展集群的功能——我们只是在将 Kubernetes 作为一个过度指定的内容管理系统使用。我们可以做得更好,我们将从清理 ToDo 资源开始。

现在试试看:移除待办 CRD,并确认自定义资源也被删除了。

# list the CRDs registered in your cluster:
kubectl get crds

# delete the to-do item CRD:
kubectl delete crd todos.ch20.kiamol.net

# try to list the to-do items:
kubectl get todos

你可以在这个练习和图 20.4 中看到,没有 CRD(Custom Resource Definition)自定义资源就无法存在——Kubernetes 不会存储任何未知对象。删除 CRD 会删除所有其资源,所以如果你使用了自定义资源,你需要确保围绕 CRD 本身的 RBAC(Role-Based Access Control)权限是严格的。

图片

图 20.4 当定义自定义资源的 CRD 被移除时,自定义资源也会被移除。

我们将继续添加不同的 CRD,并将其与自定义控制器配对,以向 Kubernetes 添加用户认证系统。

20.2 使用自定义控制器触发工作流程

你从第十七章知道,生产 Kubernetes 集群通常与外部身份提供者集成以认证用户。较小的组织通常使用服务账户作为最终用户账户,这意味着你不需要外部系统。然而,你需要管理组别的命名空间,并处理创建账户和令牌。这是一个 Kubernetes 为你提供所有部件的情况,但你将需要做相当多的工作来将它们组合起来。这正是你应该考虑自定义资源和自定义控制器的时候。

这里的自定义资源是用户,主要的工作流程是添加和删除用户。一个简单的用户 CRD 只需要存储一个名称和一个组,也许还有联系详情。你可以使用 kubectl 添加和删除用户。当这样做时,工作流程将由自定义控制器处理。控制器只是一个运行在 Pod 中的应用程序,并连接到 Kubernetes API。它监视用户对象的变化,然后创建或删除必要的资源:命名空间、服务账户和令牌。图 20.5 显示了添加工作流程,而删除工作流程实际上是相反的。

图片

图 20.5 在自定义认证系统中添加用户会创建所有的 Kubernetes 资源。

我们将首先部署用户 CRD 和一些用户。由于 schema(模式),CRD 难以阅读,但其中没有新的内容,所以我们将跳过列表(如果你想要查看,它是文件user-crd.yaml)。用户资源本身很简单。列表 20.3 显示了 SRE 团队中的一个用户。

列表 20.3 user-crd.yaml,用户资源的 spec

apiVersion: "ch20.kiamol.net/v1"     # This is the same group and version
kind: User                           # as the other CRDs in this chapter.
metadata:
  name: sre3
spec:                                # Records the user details
  email: sre3@kiamol.net
  group: sre

你需要意识到 CRD 需要几秒钟的时间在 API 服务器上注册,所以你通常不能一次性部署一个 CRD 和自定义资源的文件夹,因为 CRD 往往没有及时准备好。你需要先部署 CRD,然后才能部署资源。我们现在将使用 SRE 用户和一个测试用户来做这件事。

现在试试看 创建用户的 CRD 和一些用户资源。

# deploy the CRD first:
kubectl apply -f users/crd/

# deploy the users:
kubectl apply -f users/

# print the user list:
kubectl get users

图 20.6 的输出显示用户体验良好。CRD 只需部署一次,然后你可以使用像列表 20.3 中的简单 YAML 那样添加尽可能多的用户。现在自定义用户对象存储在 Kubernetes 中,但没有控制器运行,所以目前还没有任何事情发生。

图片

图 20.6 创建 CRD 和一些用户——没有控制器,这不会触发任何事情。

自定义控制器通常用 Go 编写。一些包负责处理你需要做的样板连接。不过,Kubernetes API 客户端存在于所有主要语言中,我的用户控制器是用 .NET 编写的。我不想给你一堆源代码,但你应该意识到构建自定义控制器的一些事情。列表 20.4 是一些 C# 代码,它是添加用户工作流程的一部分(完整文件在本章的源代码中)。

列表 20.4 UserAddedHandler.cs,使用客户端库调用 Kubernetes API

// lists service accounts in a namespace, using a 
// field selector to search for an account by name:
var accounts = _client.ListNamespacedServiceAccount(
           groupNamespaceName,
           fieldSelector: $"metadata.name={serviceAccountName}");

// if there’s no match then we need to create the account:
if (!serviceAccounts.Items.Any())
{
  var serviceAccount = new V1ServiceAccount
  {
    // set up the spec of the service account
  };

  // create the resource:
  _client.CreateNamespacedServiceAccount(
            serviceAccount, 
            groupNamespaceName);
}

你首先会注意到,使用 Kubernetes API 感觉很自然,因为所有的操作实际上都是你在 kubectl 中所做的相同事情,只是语法不同,所以编写控制器并不困难。第二点是,你通常在代码中构建 Kubernetes 资源,所以你需要将你心中的 YAML 转换为一系列对象——因此编写控制器比较繁琐。幸运的是,我有我来为你做这件事,当你部署用户控制器时,它会立即通知新用户,并运行添加用户的工作流程。

现在试试看 部署自定义控制器,并验证添加用户过程是否被触发并创建了所有认证资源。

# on Windows, you’ll need to run this so you can decode Secrets:
. .\base64.ps1

# deploy the custom controller:
kubectl apply -f user-controller/

# wait for it to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=user-controller

# check the controller logs:
kubectl logs -l app=user-controller

# print the Secret, which is the user’s token:
kubectl get secret tester3-token -n kiamol-ch20-authn-test -o jsonpath='{.data.token}' | base64 -d

你会看到在这个练习中,控制器自动执行了我们手动在 17.3 节中做的所有操作——为组创建命名空间、在命名空间中创建服务账户,并请求新的令牌。实际上,它做的不仅仅是这些,因为它首先检查这些资源是否存在,只有在需要时才创建它们。图 20.7 显示结果是服务账户,可以通过对组(即命名空间)应用 RBAC 规则来保护,以及令牌,可以分发给用户,让他们存储在他们的 kubectl 上下文中。

图片

图 20.7 控制器自动化了入职流程,除了分发凭证。

请不要将此控制器用作您的生产认证系统;它只是快速展示如何使用自定义控制器与 CRDs 一起扩展您的 Kubernetes 体验。代码不处理对象的更新,设计允许每个用户只有一个组,但您可以看到管理 Kubernetes 内认证的基础都在那里。您可以将所有用户 YAML 和组 RBAC 规则存储在一个源代码库中,并将其作为部署任何新集群的一部分。

任何控制器的基本作用是实现控制循环,持续监视对象的变化,并执行将系统的实际状态转换为对象中指定的所需状态的任何任务。它们通过监视资源的变化来实现这一点——就像使用 kubectl 中的watch参数一样。watch是一个无限循环,当对象被创建、更新或删除时,它会收到通知。用户控制器在启动时添加了它找到的用户,并且它仍在后台运行,等待您添加另一个用户。

现在尝试一下。控制器正在监视新用户。创建另一个用户资源,并确认控制器执行了其操作。

# deploy a new user in the same SRE group:
kubectl apply -f users/update/

# print the latest controller logs:
kubectl logs -l app=user-controller --tail 4

# confirm the new user has a token:
kubectl get secret sre4-token -n kiamol-ch20-authn-sre -o jsonpath='{.data.token}' | base64 -d

我在图 20.8 中的输出显示,控制器表现正常;期望状态是用户应该作为一个服务账户被创建,并为组创建一个命名空间。命名空间已经存在,因此控制器在那里不需要做任何事情;它只是创建服务账户和令牌。自定义控制器需要按照与标准控制器相同的原则工作:无论初始状态如何,它们都能达到所需状态。

图 20-8

图 20.8 在自定义控制器中应使用声明性期望状态方法。

自定义控制器也需要拥有清理逻辑,因为创建自定义资源和看到大量额外资源被创建之间存在脱节。这是扩展 Kubernetes 的一个问题——您的控制器代码需要稳固,以确保任何故障都不会留下集群管理员不期望的对象。这对于存储在 Secrets 中的敏感数据尤其重要,例如为用户创建的令牌。

现在尝试一下。删除测试用户。现在组中没有用户了,因此命名空间也应该被删除。

# check the authentication namespaces:
kubectl get ns -l kiamol=ch20

# delete the test user:
kubectl delete user tester3

# print the controller logs:
kubectl logs -l app=user-controller --tail 3

# confirm the tester namespace has been removed:
kubectl get ns -l kiamol=ch20

您可以在图 20.9 中看到,控制器没有明确删除 Secret,但此类型的 Secret 在服务账户被删除时,Kubernetes 会自动删除。控制器会检查组中是否还有其他用户,如果没有,它会删除命名空间。如果您在那个命名空间中添加了任何其他资源,那么它们现在都将消失。

图 20-9

当对象被移除时,控制器会收到通知,以便它们可以清理它们创建的资源。

自定义资源是扩展 Kubernetes 的强大方式,特别是对于像这样的用例,你希望提供比标准 Kubernetes 对象更高层次的概念抽象。但那些对象只是集群中的普通资源,你的控制器代码需要允许管理员在不知道它们由控制器管理的情况下删除它们。用户控制器还应监视机密、服务帐户和命名空间,以重新创建控制器过程外删除的任何内容。

更复杂的控制器可能会部署自己的 RBAC 规则以限制干扰,并支持跨多个 Pod 运行以实现高可用性。如果你想探索 CRDs 和自定义控制器的生产级示例,cert-manager 项目(cert-manager.io)是一个很好的例子。它是一个 CNCF 项目,为 Kubernetes 添加了 TLS 证书管理功能,可以请求证书并将它们应用到你的 Web 应用程序中。更高级的复杂性来自于操作符模式。

20.3 使用操作符管理第三方组件

操作符使用自定义资源和控制器来提供应用程序的全生命周期管理。它们用于需要大量操作任务且超出标准 Kubernetes 功能集的复杂应用程序。有状态应用程序是一个很好的例子——如果你决定在 Kubernetes 中运行数据库,那么升级数据库服务器可能意味着将数据库置于只读模式并在升级前进行备份。

你不能用标准 Kubernetes 资源表达这样的要求;你可以通过 Helm 安装钩子实现类似的功能,但通常逻辑相当复杂,你需要更多的控制。操作符的目标是使用控制器和自定义资源实现所有这些操作要求,通过简单的资源如数据库对象和备份对象来抽象复杂性。

如果第三方组件可以通过操作符部署,那么与你的应用程序依赖的组件一起工作会容易得多,因为它为你提供了一个 as-a-service 体验,你可以专注于你的应用程序,而将依赖项的管理留给它们自己。在本节中,我们将部署待办事项 Web 应用程序的修改版本,使用操作符来管理依赖项:一个数据库和一个消息队列,用于组件之间的异步通信。

现在试试这个待办事项应用版本使用了一个名为 NATS 的消息队列服务器。NATS 团队发布了一个操作符,该操作符运行高度可用的队列服务器集群。

# deploy the CRDs and RBAC rules for the Operator:
kubectl apply -f nats/operator/00-prereqs.yaml

# deploy the Operator itself:
kubectl apply -f nats/operator/10-deployment.yaml

# wait for it to spin up:
kubectl wait --for=condition=ContainersReady pod -l name=nats-operator

# list the CRDs to see the new NATS types:
kubectl get crd

NATS 是一个充当应用程序组件之间的中介的消息队列,因此它们通过传递消息而不是直接连接来通信。它是一个强大且非常能够的技术(另一个 CNCF 项目),但在生产环境中,为了确保消息不会丢失,需要仔细设置以实现高可用性。没有比 NATS 团队更好的团队来做这件事了,他们提供了你刚刚部署的操作员。如图 20.10 所示,它为 NatsCluster 对象添加了一个 CRD,你可以使用它来部署分布式消息队列。

图片

图 20.10 操作员应该简单易部署,并创建它们需要的所有资源。

更新后的待办事项应用使用消息队列来提高性能和可伸缩性。当用户在新版本中保存消息时,Web 应用会将消息发送到队列,然后返回给用户。另一个组件监听这些消息并将项目添加到数据库中。你可以扩展到数百个 Web Pod,而无需扩展数据库,因为队列充当缓冲区,平滑任何流量峰值。队列成为应用中的关键组件,列表 20.5 展示了使用 NATS 操作员部署生产级队列是多么简单。

列表 20.5 todo-list-queue.yaml,一个消息队列的自定义资源

apiVersion: nats.io/v1alpha2     # The CRD uses an alpha version,  
kind: NatsCluster                # but it has been stable for 
metadata:                        # a few years.
  name: todo-list-queue
spec:                            # The spec defines the size of    
  size: 3                        # the queue cluster and NATS version.
  version: "1.3.0"

NatsCluster 资源包含两个字段:你希望在高度可用的队列集群中运行的队列服务器 Pod 数量以及要使用的 NATS 版本。当你部署它时,操作员会为应用使用队列创建一个服务,为 NATS 实例创建一个服务以相互协调,以及一组 Pod,每个 Pod 运行 NATS 并配置了一个 Secret 以作为高度可用、分布式的队列运行。

现在试试看 创建 NATS 集群资源,并确认操作员是否创建了所有队列资源。

# deploy the queue spec from listing 20.5:
kubectl apply -f  todo-list/msgq/

# list the queues:
kubectl get nats

# list the Pods created by the Operator:
kubectl get pods -l app=nats

# list the Services:
kubectl get svc -l app=nats

# list Secrets:
kubectl get secrets -l app=nats

图 20.11 显示我的 NATS 集群已经启动并运行。容器镜像的大小只有几兆字节,所以 Pod 会很快启动,即使在需要拉取镜像的节点上也是如此。如果你描述一个 Pod,你会看到规范使用了你从这本书中学到的一些最佳实践,比如容器探测和 Pod 优先级。但 Pod 不是由 Deployment 或 StatefulSet 管理的;NATS 操作员是 Pod 控制器,这意味着它可以使用自己的方法来维护可用性。

图片

图 20.11 在自定义资源中使用两行 YAML,即可获得一个分布式消息队列。

Operator 模式是一个宽泛的定义;Kubernetes 中没有 Operator 对象,设计、构建和分发它们的决定权在项目团队。NATS Operator 是从 GitHub 上发布的 YAML 清单部署的;其他项目可能使用 Helm 或称为 Operator Lifecycle Manager(OLM)的工具。OLM 通过一个目录来发布和分发 Operator,增加了一些一致性,但它是在 Kubernetes 生态系统边缘的技术之一,到目前为止还没有得到广泛的应用。

您可以访问 OperatorHub 网站(operatorhub.io),查看通过 OLM 可用的项目类型。其中一些由产品团队维护;其他由第三方或个人发布。在撰写本文时,有三个 Postgres 数据库的 Operator——它们都没有得到 Postgres 项目的支持——它们在功能和易用性方面差异很大。没有 MySQL 的 Operator,尽管有一个 MariaDB(MySQL 的分支)的 Operator,但它由 GitHub 上的一位个人维护——这可能不是您对核心组件所期望的支持结构。

这并不是说 Operator 不是一种可行的技术;只是这种模式并不局限于 OLM。如果您正在寻找某个产品的 Operator,您需要比 OperatorHub 网站更广泛地搜索,并调查选项的成熟度。待办事项应用可以使用 MySQL 作为数据存储——来自 Presslabs 团队的一个非常好的 MySQL Operator 可用于在 Kubernetes 中大规模运行 MySQL,为他们的 WordPress 平台提供服务。该 Operator 易于使用,文档齐全,维护良好,并且使用 Helm 安装简单。

现在尝试一下 使用 Helm 部署 MySQL Operator,它可以在集群中部署和管理复制的 MySQL 数据库。

# add the Helm repository for the Operator:
helm repo add presslabs https://presslabs.github.io/charts

# deploy a known version:
helm install mysql-operator presslabs/mysql-operator --version v0.4.0 --atomic 

# wait for the Operator Pod to spin up:
kubectl wait --for=condition=ContainersReady pod -l app=mysql-operator

# list the CRDs it installs:
kubectl get crd -l app=mysql-operator

MySQL Operator 为您提供了数据库即服务(DBaaS)的体验:Helm 发布创建了数据库和数据库备份对象的 CRD 以及运行这些对象的 Operator。我在图 20.12 中的输出被截断,但 Helm 发布说明还显示了如何创建数据库——您只需要一个 MySQL 密码的 Secret 和一个 MysqlCluster 对象。

如果您找到一个好的 Operator,复杂软件的部署和管理就变得容易了。

您现在可以使用简单的资源规范部署高度可用的数据库。列表 20.6 显示了待办事项数据库的清单,同时也说明了自定义资源的某些局限性。CRD 模式允许您设置 MySQL 配置,并自定义 Operator 为数据库服务器生成的 Pod 定义,因此您可以设置资源请求和限制、亲和规则和优先级类别。这些 Kubernetes 细节会泄露到数据库对象规范中,所以它不仅仅是您需要的数据库的描述,但它比我们在第八章从头开始设置的复制的 Postgres 数据库要简单得多。

列表 20.6 todo-list-db.yaml,一个使用操作员的复制的 MySQL 数据库

apiVersion: mysql.presslabs.org/v1alpha1
kind: MysqlCluster
metadata:
  name: todo-db
spec:
  mysqlVersion: "5.7.24"
  replicas: 2
  secretName: todo-db-secret  
  podSpec:    
    resources:
      limits:
        memory: 200Mi
        cpu: 200m

当你部署 MysqlCluster 对象时,操作员创建一个 StatefulSet 来运行一个复制的 MySQL 数据库,并为消费者连接到数据库创建一组服务。有针对整个集群以及管理器和副本节点的一组单独的服务,所以你可以选择你的客户端应用程序如何连接。

现在试试它 部署数据库,并确认操作员创建了预期的资源。

# create the MysqlCluster resource:
kubectl apply -f todo-list/db/

# confirm the status:
kubectl get mysql

# show the StatefulSet:
kubectl get statefulset todo-db-mysql -o wide

# show the database Services:
kubectl get svc -l app.kubernetes.io/component=database

当你查看 StatefulSet 时,你会看到 Pod 运行一个 MySQL 容器和一组边车容器,包括 MySQL 的 Prometheus 导出器,如图 20.13 所示。这是操作员的一个大优点:它们使用最佳实践来建模应用程序,这样你就不需要自己深入研究细节。如果你查看 Pod 的规范,你会看到它有我们在第十四章中使用的标准 Prometheus 注释,所以如果你在集群中运行 Prometheus,操作员将自动检测新的数据库 Pod,无需任何额外配置,你还可以将 MySQL 度量添加到你的仪表板中。

图 20-13

图 20.13 操作员使用常见的最佳实践设置了一个有偏见的 MySQL 数据库。

现在我们有一个生产级别的数据库和消息队列正在运行,仅用 20 行 YAML 定义。我们可以将 NATS 和 MySQL 标准化为所有我们的应用程序,操作员将负责多个数据库和队列。操作员通常是集群范围的,所以你仍然可以在不同的命名空间中隔离应用程序工作负载。这就是新待办事项应用程序的所有依赖项,因此我们可以部署其余的组件,即网站和消息处理器,它将数据保存到数据库中。

现在试试它 部署应用程序组件,一个网站和一个消息处理器,它们都使用队列和数据库。

# create shared app configuration:
kubectl apply -f todo-list/config/

# deploy the web app and handler:
kubectl apply -f todo-list/save-handler/ -f todo-list/web/

# wait for the Pods to start:
kubectl wait --for=condition=ContainersReady pod -l app=todo-list

# print the app logs:
kubectl logs -l app=todo-list,component=save-handler

# browse to the app Service on port 8020, and add a new item

# print the latest logs from the message handler:
kubectl logs -l app=todo-list,component=save-handler --tail 3

在这个练习中,你会看到应用程序工作得就像它一直以来的那样,与我们定制的 Postgres 版本相比,我们显著减少了需要维护的 YAML 数量。我的输出,如图 20.14 所示,实际上隐藏了这样一个事实:应用程序并不完全按原来的方式工作——现在待办数据被保存在一个单独的进程中。你会发现添加一个条目和看到它在列表中之间有一个延迟,所以你需要刷新。欢迎来到最终一致性,这是新消息架构的副作用;这与操作员无关,所以如果你对此概念是新手,我会让你去研究。

图 20-14

图 20.14 消息队列和数据库是关键组件,操作员以高可用性运行它们。

我们从 Operator 中获得的不只是部署的便捷性和高可用性;它们还会负责核心组件的安全升级,并且可以通过创建 MysqlBackup 对象将 MySQL 数据库备份到云存储。我们不会进一步探讨这一点,因为我们实际上并没有运行一个生产级别的待办事项列表应用。事实上,我们现在运行的设置可能正在消耗你实验室机器上相当多的资源,所以在我们继续之前,我们将将其清除。

现在试试吧!移除应用、自定义资源和 Operator。

# delete the app components:
kubectl delete -f todo-list/web/ -f todo-list/save-handler/ -f todo-list/config/

# delete the custom resources:
kubectl delete -f todo-list/db/ -f todo-list/msgq/

# uninstall the NATS Operator:
kubectl delete -f nats/operator/

# uninstall the MySQL Operator:
helm uninstall mysql-operator

你可以在图 20.15 中看到我的输出,其中卸载所有内容只是部署的反向操作。Operator 并不一定删除它们创建的所有资源,因为它们可能包含你不希望丢失的数据。预期在移除 Operator 后,ConfigMaps、Secrets、PersistentVolumeClaims 甚至 CRDs 仍然存在,这又是使用单独的命名空间为你的 app 提供的一个好理由,这样你可以干净利落地移除所有内容。

图 20.15

图 20.15 Operator 在删除时并不一定会清理,所以你需要手动检查剩余的资源。

Operator 是一种管理第三方依赖的整洁方式。你需要投入一些精力去找到一个适合你的 Operator,并且记住,这些中的许多都是开源项目,可能没有太多的动力。比较 Prometheus Operator(OperatorHub 上最好的例子之一),它有 350 位贡献者,几乎每天都有新的更新,以及来自 Oracle 的 MySQL Operator,它有 18 位贡献者,在撰写本文时,已经两年没有更新了。许多 Operator 被标记为 alpha 或 beta 软件,这些通常是关键组件,所以你需要对你集群中引入的任何东西的成熟度水平感到舒适。

但 Operator 不仅限于第三方软件;你可以构建自己的 Operator 来简化你自己的应用的部署和持续维护。

20.4 为自己的应用构建 Operator

构建自己的 Operator 有两个主要原因。第一个原因是针对那些具有复杂操作需求的 app,第二个原因是针对那些作为许多项目服务安装的通用组件。为待办事项 app 设计的 Operator 可能包含自定义升级逻辑——例如更新服务以将流量导向“维护中”页面,或者等待消息队列为空然后备份数据库。任何具有可自动化常规操作任务的 app 都可能是定制 Operator 的潜在候选者。

构建自己的操作员不是一项简单的工作,因为它涉及到多个自定义资源类型和多个自定义控制器。复杂性在于规划所有场景,不仅包括操作员拥有的工作流程,还包括它需要执行的其他任何工作以纠正人为操作员的干扰。本章的资源中有一个自定义操作员,但我不打算关注代码——它是一个用于我们在第十章中使用的 web-ping 应用程序的 as-a-service 操作员的示例。这是一个按计划对 Web 地址执行GET请求并记录响应时间的应用程序。如果可能,我可能会说服你这是一个许多团队都会使用的用于监控应用程序正常运行时间的服务。

现在试试看 The web-ping 操作员使用与 NATS 操作员相同的 YAML 清单进行部署。安装它以查看其运行方式和部署的 CRD。

# deploy RBAC rules and the Operator:
kubectl apply -f web-ping/operator/

# wait for it to be running:
kubectl wait --for=condition=ContainersReady pod -l app=web-ping-operator

# print the logs from the installer container:
kubectl logs -l app=web-ping-operator -c installer

# list new CRDs:
kubectl get crd -l operator --show-labels

# list the Pods for the Operator:
kubectl get pods -l app=web-ping-operator 

你会看到操作员 Pod 有几个角色:它安装了两个 CRD 并运行了两个容器,每个自定义资源都有一个自定义控制器。图 20.16 显示了 CRD 是用于 WebPinger 资源,它定义了要使用的地址和计划,以及 WebPingerArchive 资源,用于归档 WebPinger 资源的结果。

图片

图 20.16 web-ping 操作员具有最小的清单,并在运行时部署其他资源。

操作员模式的一个目标是将用户体验保持简单,因此尽可能在操作员内部处理安装。这保持了部署规范简单,并消除了任何潜在的错误——操作员不依赖于复杂的清单(除了 RBAC 规则,这些规则需要提前准备)。你刚刚部署的操作员规范在列表 20.7 中显示;有一个初始化容器,用于创建 CRD,以及两个容器,用于运行控制器。

列表 20.7 02-wpo-deployment.yaml,操作员的 Pod 规范

# This is the Pod spec in the Deployment resource.
spec:
  serviceAccountName: web-ping-operator    # Uses an account set up with
  automountServiceAccountToken: true       # RBAC rules for access
  initContainers:
    - name: installer                      # Creates CRDs
      image: kiamol/ch20-wpo-installer
  containers:                              # App containers are controllers.
    - name: pinger-controller
      image: kiamol/ch20-wpo-pinger-controller
    - name: archive-controller
      image: kiamol/ch20-wpo-archive-controller

在这里不太可能出错。如果操作员需要 ConfigMaps、Secrets、Services 和 PersistentVolumeClaims,它将拥有创建它们的全部权限,从而将复杂性从管理员那里移开。web-ping 应用程序有一些参数可以指定要测试的地址、HTTP 请求类型和请求之间的间隔。CRD 允许用户声明这些字段,并且运行在操作员中的自定义控制器为应用程序的每个实例创建一个正确配置的 Deployment。列表 20.8 显示了一个配置为测试我的博客的 WebPinger 资源。

列表 20.8 webpinger-blog.yaml,一个用于测试 Web 地址的自定义资源

apiVersion: "ch20.kiamol.net/v1"
kind: WebPinger
metadata:
  name: blog-sixeyed-com
spec:                               # Parameters for the app are
  target: blog.sixeyed.com          # much easier to specify in a
  method: HEAD                      # custom resource than using
  interval: "7s"                    # environment variables in a Pod.

当你部署这个操作员时,它会创建一个具有特殊配置的 web-ping 应用程序实例,将响应记录到 JSON 格式的文件中进行分析。Pod 还包括一个侧边容器,它提供了一个 HTTP API 来清除日志文件,并支持归档功能。

现在试试看 创建一个 web ping 资源,并确认操作员创建了一个应用程序实例,该实例向我的博客发送 HTTP 请求。

# create the custom resource:
kubectl apply -f web-ping/pingers/webpinger-blog.yaml

# print the latest logs from the Operator:
kubectl logs -l app=web-ping-operator -c pinger-controller --tail 4

# list web-ping Pods:
kubectl get po -l app=web-ping --show-labels

# print the logs from the app:
kubectl logs -l app=web-ping,target=blog.sixeyed.com -c web --tail 2

# confirm logs are also written to the JSON file:
kubectl exec deploy/wp-blog-sixeyed-com -c web -- tail /logs/web-ping.log -n 2

这是一个简单且易于部署对网站进行简单黑盒观察的好方法,每个团队都可以在他们的生产部署中包含一个 WebPinger 规范,以监控他们应用程序的运行时间。如果团队熟悉 web-ping 应用,它将以与手动部署相同的方式运行,将可读日志打印到标准输出流。正如你在图 20.17 中看到的,日志也被写入为 JSON,这就是存档需求出现以保护磁盘空间的原因。

图 20.17

图 20.17 网络 ping 应用现在可以轻松地在多个实例中部署和管理。

存档是 web-ping Operator 提供的唯一操作功能,使用起来很简单:创建一个指定目标域名名的 WebPingerArchive 资源。该资源的自定义控制器会寻找与域名匹配的 web-ping Pod,并使用边车容器中的 API 抓取当前日志文件的快照,然后清除文件。这个存档功能是自动化操作任务所需工作的一个好例子。这不仅仅是 CRD 和控制器;应用程序本身需要一个边车来提供额外的管理功能。

现在试试看——测试 web-ping 应用的操作方面——创建博客请求的日志存档。

# print the number of lines in the log file:
kubectl exec deploy/wp-blog-sixeyed-com -c web -- wc -l /logs/web-ping.log

# create the archive resource:
kubectl apply -f web-ping/pingers/archives/webpingerarchive-blog.yaml

# confirm that the Operator creates a Job for the archive task:
kubectl get jobs -l kiamol=ch20

# print the logs from the archive Pod:
kubectl logs -l app=web-ping-archive,target=blog.sixeyed.com --tail 2

# confirm the application log file has been cleared down:
kubectl exec deploy/wp-blog-sixeyed-com -c web -- wc -l /logs/web-ping.log

我的输出显示在图 20.18 中。这是一个人为设计的例子,但它是观察 Operators 如何解决复杂问题而不迷失在实际问题中的好方法。在归档运行后,ping 结果将在作业的 Pod 日志中可用,而 web-ping Pod 仍在愉快地消耗我的带宽,并且有一个空日志文件开始再次填充。

图 20.18

图 20.18 存档工作流程由 Operator 管理,并通过创建另一个自定义资源来触发。

Kubernetes Operators 通常是用 Go 编写的,如果你有 Go,两个工具会为你处理很多样板代码:来自 Google 的 Kubebuilder 和 Operator SDK,后者是 Operator Lifecycle Manager 工具集的一部分。我的 Go 并非真正精通,所以我用 .NET 编写了 Operator,构建这个部分的 Operator 大约花费了我一天的时间进行编码。深入挖掘 Kubernetes API 并编写创建和管理资源的代码,以及在代码中构建对象确实让你更加欣赏 YAML。

但现在是停止 ping 我的博客的时候了。这个 Operator 没有任何准入控制器来阻止你删除它的 CRDs,所以你可以删除它们。这将触发自定义资源的删除,然后控制器将清理它们创建的资源。

现在试试看——删除 Operator 的 CRDs。自定义资源将被删除,这将触发 Operator 中的删除工作流程。

# delete the CRDs by their labels:
kubectl delete crd -l operator=web-ping

# print the latest logs from the web-ping controller:
kubectl logs -l app=web-ping-operator -c pinger-controller --tail 4

# delete the latest logs from the archive controller:
kubectl logs -l app=web-ping-operator -c archive-controller --tail 2

在图 20.19 中,你可以看到在这个 Operator 中,控制器贯穿始终。WebPing 自定义控制器删除 Deployment 资源,然后系统控制器删除 ReplicaSet 和 Pod。Operator 并不试图取代或复制 Kubernetes 的功能——它是基于它的,使用已经在全球范围内使用多年的标准资源,将它们抽象化以提供简单的用户体验。

图片

图 20.19 这个 Operator 中的自定义控制器管理标准的 Kubernetes 资源。

你需要理解 Operator 模式是如何工作的,因为你肯定会在你的 Kubernetes 之旅中遇到它,尽管你更有可能使用他人的 Operator 而不是自己构建。关键是要理解,它是一种松散的分类,用于使应用程序易于使用和维护,利用 Kubernetes 的可扩展性,并充分利用核心系统资源。

20.5 理解何时扩展 Kubernetes

在本章中,我们没有深入细节,但已经覆盖了很多内容。扩展 Kubernetes 是让集群运行你自己的代码,而在代码中发生什么取决于你试图解决的问题。尽管你更有可能使用他人的 Operator 而不是自己构建,但这些模式是通用的,图 20.20 显示了 Operator、自定义资源和自定义控制器如何与 web-ping 应用程序的所有部分协同工作。

图片

图 20.20 Operator 和自定义控制器通过抽象复杂性使应用程序易于管理。

扩展 Kubernetes 有一些指导原则。首先,确保你真的需要这样做。为运行 Deployment、ConfigMap 和 Service 的应用程序编写 Operator 以节省 YAML 是过度的。如果你的目标是确保应用程序以适当的规范部署,那么使用 admission controllers 会更好。编写和维护自定义控制器和 Operator 是一项大量工作,如果你没有规划好所有工作流程,你的应用程序可能会进入不一致的状态,然后 Operator 会使维护变得更加困难。管理员不会喜欢手动构建规范和部署自定义控制器应该拥有的资源。

如果你确实有明确的需求,那么就从 CRDs 和控制器开始,并专注于用户体验;自定义资源的全部意义在于简化复杂问题。如果你用 Go 编写,请使用开发工具包,并设计你的控制器以与 Kubernetes 协同工作,基于标准资源而不是重新发明它们。当你有几个具体的例子可以参考时,构建一个通用系统总是更好的,这样你就知道通用方法将涵盖所有需求。当你完成了一些复杂的升级并且了解了工作流程,或者当你多次部署了通用组件并且了解了变体,那么就是时候设计你的 Operator 了。

第三方操作符是使用他人的生产经验来提高你自己的应用程序可靠性的好方法。关键在于找到一个好的,这需要一些调查和尝试不同的选项。使用操作符来管理第三方组件是一个大的依赖。你不想发现项目停滞不前,你需要逆向工程操作符并自己承担所有权。操作符框架是拥有 OLM 和操作符 SDK 的母项目,它在我写这一章的前几周被添加为新的 CNCF 项目,这可能会给操作符 Hub 带来一些新的活力。

这就是扩展 Kubernetes 的全部内容,所以我们可以在进入实验室之前进行清理。

现在尝试一下 清理掉任何剩余的资源。

kubectl delete all,crd,secret,clusterrolebinding,clusterrole,serviceaccount,ns -l kiamol=ch20

kubectl delete pvc -l app=mysql-operator

kubectl delete configmap mysql-operator-leader-election

20.6 实验室

本实验室将为你提供一些编写 CRD 和管理自定义控制器的一些经验。别担心;控制器已经为你准备好了。在实验室文件夹中,有一个为 timecheck 应用定制的资源规范,但没有 CRD,所以你不能部署它。任务是构建 CRD,部署控制器和资源,并验证它们是否按预期工作。这里有一些提示:

  • 自定义控制器已经在timecheck-controller文件夹中准备好了。

  • 你的 CRD 名称需要与资源中的名称匹配。

  • 部署控制器时,你需要查看日志;根据你接近实验室的顺序,它可能不会按预期工作。

你可以像往常一样在 GitHub 上查看我的解决方案:github.com/sixeyed/kiamol/blob/master/ch20/lab/README.md.

21 在 Kubernetes 中运行无服务器函数

欢迎来到本书的最后一章!我们将以高调结束,学习如何将你的 Kubernetes 集群转变为无服务器平台。许多无服务器平台都在云中,但它们大多是定制系统,你无法轻易地将 AWS Lambda 组件迁移到 Azure Functions。Kubernetes 的可扩展性使得在集群中部署无服务器运行时变得容易,这与其他所有应用程序一样具有可移植性。在本章中,我们将介绍一些开源项目,它们为你提供了非常类似 Lambda 的体验,你只需关注代码,平台会为你打包和部署。无服务器函数作为容器在 Pods 中运行,因此你以通常的方式管理它们,但平台添加了一些高级抽象。

Kubernetes 生态系统中存在多个无服务器平台,它们采取了略微不同的方法。最受欢迎的是来自 Google 的 Knative 项目,但它有一个不寻常的工作流程:你需要自己打包函数到 Docker 镜像中,然后 Knative 会为你部署它们。我更倾向于代码优先的方法,即你带来代码,平台在容器中运行它;这符合无服务器函数简单工作流程的目标。在本章中,我们将使用另一个流行的平台 Kubeless,我们还将看到如何使用 Serverless 项目抽象无服务器平台本身。

21.1 Kubernetes 中无服务器平台的工作原理

在 Kubernetes 的背景下,无服务器意味着什么?显然,服务器是集群中的节点,因此肯定涉及服务器。这实际上是在编写代码和将其运行在 Pod 中之间去除所有仪式——消除了编译应用程序、构建容器镜像、设计部署和编写 YAML 规范的所有开销。AWS Lambda 和 Azure Functions 都有一个命令行界面 (CLI),你可以上传代码文件,函数就会在云中的某个地方开始运行。Kubernetes 的无服务器为你提供了相同的流程,但你确切地知道函数运行的位置:在你的集群中的 Pod 中。

Kubeless 工作流程特别简洁:你只需将源代码文件部署为函数,使用 Kubeless CLI 即可。无需额外描述函数的工件,CLI 会创建一个包含所有细节和源代码的自定义资源。Kubeless 控制器作用于函数资源,并创建一个 Pod 来运行该函数。你可以通过 CLI 手动触发函数,或者创建一个永久触发器,使函数能够监听 HTTP 请求、订阅消息队列或按计划运行。图 21.1 展示了 Kubeless 函数的架构。

图片

图 21.1 使用 Kubeless 的无服务器函数将你的代码转换为正在运行的 Pod。

This workflow means you run one command to get a code file running in a Pod and another command if you want to expose it over HTTP. It’s perfect for webhooks, integration components, and simple APIs. Other serverless platforms support Kubernetes and work in similar ways: Nuclio, OpenWhisk, and Fn Project all take your code, package it into a container to run, and support multiple triggers to invoke the function. They use standard resources, like Pods and Services, and standard patterns, like ingress controllers and message queues. In this chapter, you’ll use Kubeless to add new features to an existing app without changing the app itself. We’ll start simple with a Hello, Kiamol example.

Try it now Start by deploying Kubeless in your cluster. There’s a snapshot of the most recent release in this chapter’s folder.

# switch to this chapter’s source:
cd ch21

# deploy the CRDs and controllers:
kubectl apply -f kubeless/

# wait for the controller to start:
kubectl wait --for=condition=ContainersReady pod -l kubeless=controller -n kubeless

# list the CRDs:
kubectl get crd

You can see in figure 21.2 that Kubeless uses the techniques you learned in chapter 20: CustomResourceDefinitions for the HTTP and schedule triggers, and for serverless functions themselves. A controller monitors all those resources and turns functions into Pods, HTTP triggers into Ingress rules, and scheduled triggers into CronJobs.

图 21.2 Kubeless 架构通过添加新资源来提供无服务器抽象。

You can create Kubeless custom resources yourself, which fits neatly if your function code is all in source control and you already have a CI/CD process that uses kubectl. The Kubeless CLI is an easier option: you run simple commands, and it creates the resources for you. The CLI is a single binary you can install on macOS, Linux, or Windows. You’ve installed enough software already, though, so we’ll run Kubeless in a Pod that has the CLI installed and kubectl configured to work with your cluster.

现在试试 Run the Kubeless CLI in a Pod, and confirm it can connect to your cluster.

# create a Pod with the Kubeless CLI installed:
kubectl apply -f kubeless-cli.yaml

# wait for it to start:
kubectl wait --for=condition=ContainersReady pod kubeless-cli

# connect to a session in the Pod:
kubectl exec -it kubeless-cli -- sh

# print the Kubeless setup:
kubeless get-server-config

# stay connected to the Pod for the next exercise

Kubeless supports a lot of languages, as you see in figure 21.3, from common ones like Java, .NET, and Python, to interesting newcomers like Ballerina and Vert.x (which itself supports multiple JVM variants like Java, Kotlin, and Groovy). If any of those fit with your tech stack, you can deploy functions with Kubeless—it’s a great way to evaluate new versions of your runtime or try out new languages.

图 21.3 使用 Kubeless,无服务器函数可以用所有主要语言编写。

无服务器函数旨在执行单个专注的任务,源代码通常应该是一个文件,但 Kubeless 确实允许你部署更大的项目。它理解所有运行时的依赖管理系统,并在部署过程中获取依赖项。当你创建一个新的函数时,你可以上传包含整个项目结构的 zip 存档,或者你可以上传单个文件。列表 21.1 显示了一个简单的 Java 欢迎函数。不必太担心源代码——这个例子只是为了向你展示,无论你使用什么语言,编写 Kubeless 函数都有一种标准的方法。

列表 21.1 hello-kiamol.java,一个简单的 Java 无服务器函数

# the code is in a normal Java class:
public class Kiamol {

    # this is the method that Kubeless invokes:
    public String hello(io.kubeless.Event event, io.kubeless.Context context) {

        # it just returns a string:
        return "Hello from chapter 21!";
    }
}

每个函数接收两个字段:一个包含关于事件的详细信息,包括触发器的类型和调用者发送的任何数据,另一个包含函数本身的上下文,包括函数的运行时间和为函数完成设置的超时时间。没有服务账户令牌用于与 Kubernetes API 服务器进行身份验证,并且你的函数通常将是应用程序功能而不是 Kubernetes 扩展(尽管它们确实在 Pod 中运行,所以如果需要,令牌可以自动挂载到文件系统中)。

当函数被调用时,它们执行所需的所有操作,并且可以返回一个字符串,如果函数是由 HTTP 请求触发的,则将该字符串作为响应发送给调用者。函数代码在 Pod 容器内部执行,因此你可以将日志条目写入标准输出流并在 Pod 日志中查看它们。你可以部署列表 21.1 中的简单函数并检查 Pod 规范以了解 Kubeless 的工作方式。

现在试试看 部署简单的 Hello Java 函数使用 Kubeless CLI,并查看它创建的 Kubernetes 对象。

# inside the Pod is a copy of the book’s code:
cd /kiamol/ch21

# deploy the Java function from listing 21.1:
kubeless function deploy hello-kiamol --runtime java11 --handler Kiamol.hello
                                      --from-file functions/hello-kiamol/hello-kiamol.java

# list all functions:
kubeless function ls

# list Pods and ConfigMaps for the function:
kubectl get pods -l function=hello-kiamol
kubectl get cm -l function=hello-kiamol

# print the details, showing the build steps:
kubectl describe pod -l function=hello-kiamol | grep INFO | tail -n 5

图 21.4 以 Pod 中 init 容器的日志结束。Kubeless 有一种打包应用程序的好方法,无需构建和推送容器镜像。每个支持的运行时都有一个 init 容器镜像,其中包含运行时的所有构建工具——在本例中,是 Java JDK 和 Maven 用于依赖管理。init 容器从 ConfigMap 卷加载函数源代码,构建应用程序,并将输出复制到 EmptyDir 卷。应用程序容器从一个包含语言运行时的镜像运行,并从共享的 EmptyDir 卷启动编译后的应用程序。

图 21.4 Kubeless 充分利用 init 容器来编译函数而不构建镜像。

这种方法意味着与为每个函数构建和推送镜像的平台相比,函数的启动时间较慢,但它为开发者消除了许多摩擦。这也意味着你的集群不需要配置具有对注册表写入权限的 Secrets,而且你甚至不需要使用可以构建和推送镜像的容器运行时。现在你有一个在 Pod 中运行的函数,而你不需要构建服务器或安装 Java 或 Maven。

函数 Pod 有一个监听请求的 HTTP 服务器。当你创建一个触发器时,它会向 Pod 的服务发送请求。你可以像标准应用程序 Pod 一样扩展和自动扩展函数,并且请求由服务以通常的方式负载均衡。Kubeless 建立在已建立的 Kubernetes 资源之上,为你提供了一个简单的方法来运行你的应用程序。这个函数还没有任何触发器,因此你无法从集群外部调用它,但你可以使用 kubectl 启动代理并直接调用服务。

现在尝试一下 你可以使用 kubectl 代理的 HTTP 请求来调用函数,或者你可以使用 Kubeless CLI——我们仍然在这个练习的 Pod 会话中。

# show the Service for the function:
kubectl get svc -l function=hello-kiamol

# start a proxy to route HTTP requests to the cluster:
kubectl proxy -p 8080 &

# call the function using the proxy:
curl http://localhost:8080/api/v1/namespaces/default/services/hello-kiamol:http-function-port/proxy/

# but it’s simpler to call it with the CLI:
kubeless function call hello-kiamol

# we’re done with the Pod session for now:
exit

你可以在图 21.5 中看到,Kubeless CLI 为你提供了一个与函数交互的简单方法,但每个函数都是一个 Kubernetes 应用程序,因此你也可以使用通常的 kubectl 命令来与之交互。

图 21.5 无服务器实际上是一种部署抽象:Kubeless 创建了标准的 Kubernetes 资源。

这个函数并不太有用。无服务器函数真正发光的地方是向现有应用程序添加新功能,而无需对主应用程序进行任何更改或部署。在接下来的几节中,我们将使用无服务器函数向备受喜爱的(或者可能现在不再如此)待办事项应用程序添加一些急需的功能。

21.2 从 HTTP 请求触发函数

你在第十五章学习了入口的概念。它是将进入请求路由到集群中运行的多个应用程序的常用方法。入口规则还隐藏了单个应用程序是如何组合起来的细节,因此同一域名中的不同路径可能由一个组件或不同的组件提供服务。你可以利用这一点,通过无服务器函数添加看起来像是主应用程序一部分的新功能。

我们将为此为待办事项应用程序添加一个新的 REST API,基于我们在第二十章所做的工作。在那里,我们介绍了一个用于网站和将新项目保存到数据库的消息处理程序之间的通信的消息队列。任何具有访问权限的组件都可以向队列中发布消息,因此我们可以在无服务器函数中运行一个简单的 API 来完成这个任务。让我们先让待办事项应用程序重新运行起来。

现在尝试一下 使用简单的 Deployment 规范部署待办事项应用程序,用于 NATS 消息队列和数据库。

# deploy all the components of the app:
kubectl apply -f todo-list/config/ -f todo-list/db/ -f todo-list/msgq/ -f todo-list/web/ -f todo-list/save-handler/

# wait for the application Pods to start:
kubectl wait --for=condition=ContainersReady pod -l app=todo-list

# fetch the URL for the app:
kubectl get svc todo-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8021'

# browse the app to confirm it’s working

图 21.6 没有什么特别之处——只是运行着的老式待办事项应用程序。这不是使用操作员管理消息队列和数据库的完整生产部署,但它具有相同的架构和功能。

图 21.6 显示,如果应用程序具有正确的架构,无服务器函数可以很好地与你的应用程序集成。

在本节中,我将使用多种不同的语言来编写无服务器函数,这样您可以了解它们是如何工作的,并看到运行时之间的相似性。待办 API 使用 Node.js,它使用一些额外的库来向 NATS 发送消息。Kubeless 负责在函数 Pod 启动时在初始化容器中加载依赖项;您只需在文件中使用运行时的标准格式指定依赖项即可。列表 21.2 显示了使用 NATS 库发送消息的 API 函数的主要部分。

列表 21.2 server.js,Node.js 中的无服务器 API

# the function method receives the same event and context data:
function handler(event, context) {

  # inside the function, the code builds a message:
  var newItemEvent = {
    Item: {
      Item: event.data,
      DateAdded: new Date().toISOString()
    }
  }

  # and publishes it to the NATS queue:
  nc.publish('events.todo.newitem', newItemEvent)
} 

Node.js 函数的结构与列表 21.1 中的 Java 函数相同:它接收包含调用详细信息的 event 和 context 对象。数据是由调用者发送的新待办事项,代码将其构建成一个消息,并将其发布到队列中。格式与网站发布的格式相同,因此消息处理程序将接收来自 API 和 Web 应用的消息,并将新项目保存到数据库中。与代码文件并列的是包文件,它列出了依赖项,因此它可以与 Kubeless 一起部署。

现在尝试一下;具有依赖关系的函数以相同的方式部署;您只需在 Deployment 命令中指定依赖文件以及代码文件即可。

# connect to a session in the CLI Pod:
kubectl exec -it kubeless-cli -- sh

# switch to the chapter folder:
cd /kiamol/ch21

# deploy the API function with the dependencies:
kubeless function deploy todo-api --runtime nodejs12 --handler server.handler
 --from-file functions/todo-api/server.js
 --dependencies functions/todo-api/package.json

# show the function:
kubeless function ls todo-api

# wait for the Pod to be ready:
kubectl wait --for=condition=ContainersReady pod -l function=todo-api

# call the function:
kubeless function call todo-api --data 'Finish KIAMOL ch21'

# print the function logs:
kubeless function logs todo-api | grep event

# print the message handler logs:
kubectl logs -l component=save-handler --tail 1

# leave the session, and refresh your browser
exit

消息架构使得这种新的功能变得简单。消息处理程序监听当创建新项目时的事件,并将它们保存到数据库中。事件来源无关紧要。如图 21.7 所示,API 函数发布了一条带有随机事件 ID 的消息,这就是处理程序接收到的消息。如果您在浏览器中刷新待办事项列表,您会看到新项目已经在那里。

图片

图 21.7 在 20 行代码内运行的待办应用 API

无服务器函数非常适合事件驱动架构,因为它们可以简单地连接到消息流,生成或消费不同类型的事件。这并不意味着消息是必需的,因为函数始终可以以不同级别与应用程序集成。没有消息队列,新的 API 函数可以使用数据库集成并写入新行到表中。更好的是拥有高级别的集成,这样您可以让组件拥有自己的数据,但您的函数代码可以执行与您当前架构相匹配的任何操作。

目前您已经有了待办事项 Web 应用的服务和 API 功能。下一步是将它们两者都通过 Ingress 发布。当您将无服务器函数与现有应用混合时,您可以选择您的 URL 结构。在这种情况下,我将为函数使用一个子域名,因此应用将在todo.kiamol.local上可用,而函数在api.todo.kiamol.local上。为了使其工作,您需要部署一个 ingress 控制器并在您的 hosts 文件中设置一些域名。

现在尝试一下:部署一个入口控制器,并将一些域名添加到你的 hosts 文件中。在 Windows 上,你需要以管理员身份运行终端,而在 Linux 或 macOS 上使用 sudo。

# run an Nginx ingress controller:
kubectl apply -f ingress-nginx/

# deploy ingress rules for the app and API:
kubectl apply -f todo-list/web/ingress/ -f functions/todo-api/ingress/

# print the ingress rules:
kubectl get ingress

# add domains to your hosts file--on Windows:
.\add-todo-to-hosts.ps1

# OR on Linux/macOS:
chmod +x ./add-todo-to-hosts.sh && ./add-todo-to-hosts.sh

# insert a new item with the API:
curl --data 'Plan KIAMOL ch22' http://api.todo.kiamol.local/todos

# browse to http://todo.kiamol.local/list

在那个练习中,你会发现入口规则隐藏了关于应用 Pod 和函数 Pod 的所有细节,消费者只使用 URL,这些 URL 看起来像是单个大型应用程序的不同部分。图 21.8 中的一个小截图显示了列表中的两个项目;这两个项目都是由新的 API 函数添加的,但它们的行为就像它们是在网站上添加的一样。

图 21.8

图 21.8 入口规则隐藏了内部架构,这可能是一个应用程序或多个函数。

你可以使用无服务器函数构建整个 API,每个路径使用不同的函数,但对于大型 API,这意味着会有很多 Pod,每个 Pod 都有自己的计算需求。Kubeless 默认不应用资源请求或限制,因此运行数百个函数 Pod 更有可能让你的节点承受内存压力,而不是运行单个 API Pod 的数十个副本。如果你严重依赖无服务器函数,我们讨论的第十九章中提到的驱逐场景更有可能出现,因为每个函数都会使用一些内存来加载语言运行时。

这并不是说无服务器 API 不可行;只是需要一些额外的规划。如果你在 YAML 中自己创建自定义资源而不是使用 Kubeless CLI,你可以向功能规范中添加资源块。你还需要仔细考虑你使用的运行时,因为镜像大小会影响你快速扩展的能力,并且更大的镜像提供了更大的攻击面。截至 Kubeless 1.0.7 版本,Go 运行时镜像小于 60 MB,而 Node.js 镜像的大小是其 10 倍。

现在你已经看到了无服务器函数如何扩展现有应用程序,我们将使用不同的语言和不同的触发器添加一些更多功能来完善待办事项应用。

21.3 从事件和计划触发函数

所有无服务器平台都具有类似的架构,其中函数可以通过不同的触发器被调用;HTTP 请求、队列上到达的消息和计划是常见的触发类型。将触发器与函数本身分离简化了代码,因为平台为你连接一切,你可以以不同的方式调用同一个函数。我们可以使用消息队列触发器为待办事项应用添加审计功能,记录新项目创建的时间。

新功能将监听现有消息处理器用于将项目保存到数据库的相同新项目消息。像 NATS 这样的队列支持发布-订阅模式,这意味着可以有任意数量的订阅者监听新项目消息,并且他们都会收到一份副本。Kubeless 将订阅队列,并在有传入事件时调用函数,因此函数内部无需特殊消息代码。审计处理器为它看到的每个项目写入日志条目,函数代码只是 Python 中显示的 21.3 列表中的两行。

列表 21.3 audit.py,一个 Python 审计函数

def handler(event, context):    
    print(f"AUDIT @ {event['data']['Item']['DateAdded']}: {event['data']['Item']['Item']}")

函数的设置没有差异;Kubeless 提供标准的事件和上下文对象,无论哪种类型的触发器调用函数。Kubeless CLI 中的call命令也以相同的方式工作,因此您可以部署此函数,并通过发送与新项目消息格式相同的数据来验证它。

现在试试吧 部署 Python 审计函数,并使用 Kubeless CLI 直接调用它以测试它。

# deploy the function:
kubeless function deploy todo-audit --runtime python3.7 --handler audit.handler
                                    --from-file functions/todo-audit/audit.py

# wait for the function Pod to be ready:
kubectl wait --for=condition=ContainersReady pod -l function=todo-audit

# confirm the function status:
kubeless function ls todo-audit

# connect to a Kubelss CLI session:
kubectl exec -it kubeless-cli -- sh

# call the new function:
kubeless function call todo-audit
 --data '{"Item":{"Item":"FAKE ITEM!","DateAdded":"2020-07-31T08:37:41"}}'

# print function logs:
kubeless function logs todo-audit | grep AUDIT

# leave the Pod session:
exit

图 21.9 显示这是一个简单的开发者体验。当函数部署时,没有默认触发器,因此除了从 Kubeless CLI(或通过代理访问函数服务)之外,没有其他方式可以调用它。开发者可以快速部署函数并测试它,使用kubeless update命令迭代代码,并且只有在他们对它满意时才发布触发器来连接函数。

图 21-9

图 21.9 这就是无服务器工作流的值:使用单个命令进行部署和测试。

Kubeless 原生支持 Kafka 消息系统的消息触发器,并且它具有可插拔的架构,因此您可以添加一个 NATS 触发器。Kubeless 项目维护该触发器(以及其他插件,如 AWS Kinesis 数据流触发器),您可以将它部署以创建一个新的 CRD 和控制器,用于 NATS 触发器资源。

现在试试吧 部署 Kubeless 的 NATS 插件,并为新项目队列添加一个 NATS 触发器,以便在消息发布到新项目队列时调用审计函数。

# deploy the NATS trigger:
kubectl apply -f kubeless/nats-trigger/

# wait for the controller to be ready:
kubectl wait --for=condition=ContainersReady pod -l kubeless=nats-trigger-controller -n kubeless

# connect to a Kubeless session:
kubectl exec -it kubeless-cli -- sh

# create the trigger:
kubeless trigger nats create todo-audit
    --function-selector function=todo-audit --trigger-topic events.todo.newitem

# leave the session:
exit

# call the API function:
curl --data 'Promote DIAMOL serialization on YouTube' http://api.todo.kiamol.local/todos

# print the audit logs:
kubectl logs -l function=todo-audit 

函数 Pod 的完整日志相当冗长,因为它们包括来自容器存活探测的 HTTP 请求条目。我在图 21.10 中的输出被截断,但您可以看到新工作流程的实际操作:通过 API 函数使用其 HTTP 触发器添加项目,该函数将消息放入队列,这触发了审计函数,该函数写入日志条目。

图 21-10

图 21.10 显示消息队列解耦组件;在这里,当 API 函数发布消息时,会调用审计函数,而函数之间没有直接通信。

这又是灵活架构如何帮助您快速、轻松地添加功能的一个好例子——而且安全,因为现有应用程序没有任何改变。像银行这样的高度监管行业通常有几乎完全由新法律驱动的产品积压,而将逻辑注入现有工作流程的能力是服务器无服务器架构的一个强大论据。幕后,NATS 触发控制器订阅事件消息,当它们到达时,它使用其 HTTP 端点调用函数。所有这些都从函数代码中抽象出来,函数代码只需专注于任务。

本节将再举一个例子,以总结 Kubeless 的主要功能:使用计划触发器和通过 YAML 创建函数而不是使用 CLI。Kubeless CLI 只是一个包装器,为您创建自定义资源。在 todo-mutating-handler 文件夹中有两个自定义资源的 YAML 清单:一个用于函数,一个用于 CronJobTrigger。我不会在这里重复规格说明,但如果您查看函数,您会看到它使用 PHP,源代码位于自定义资源规范内部。这种方法与 CI/CD 管道配合得很好,因为您可以使用 kubectl 部署而无需构建 Kubeless 命令。

现在试试看。将新函数作为自定义资源部署。您不需要 Kubeless CLI 来完成此工作流程,因此不需要连接到 CLI Pod 中的会话。

# create the Kubeless resources:
kubectl apply -f functions/todo-mutating-handler/

# print all the schedule triggers:
kubectl get cronjobtriggers

# print the Kubernetes CronJobs:
kubectl get cronjobs

# wait for the Job to run:
sleep 90

# print the logs from the Job Pod:
kubectl logs -l job-name --tail 2

# refresh your to-do list in the browser

当您运行此练习时,您会看到它为待办事项应用程序添加了一些急需的功能,以清理传入的数据。CronJob 每分钟调用一次函数,PHP 脚本执行以清理数据并确保待办事项列表项是有用的任务。我的输出如图 21.11 所示。

图 21.11

图 21.11 此处理程序有一些不寻常的行为,但它展示了您可以使用函数做什么。

Kubeless 是一种很好的入门服务器无服务器的方式,看看函数即服务模型是否适合您。对代码的关注使 Kubeless 成为使用 Kubernetes 进行服务器无服务器的一个更好的平台之一,但该项目最近不太活跃,部分原因是因为所有主要功能已经稳定了一段时间。当您将任何开源项目引入您的组织时,您需要接受它可能变得过时的风险,并且您需要投入自己的工程时间来帮助支持它。您可以通过使用名为 Serverless 的通用项目来抽象服务器无服务器实现来减轻这种情况。

21.4 使用 Serverless 抽象服务器无服务器函数

请注意本节中的大小写——Serverless 是一个标准化函数定义并集成底层 无服务器 平台以执行实际工作的项目。因此,你可以在 Kubeless 之上部署 Serverless,并使用 Serverless 规范为你的函数而不是直接使用 Kubeless。这意味着如果你想在某个时候从 Kubeless 转移到 Knative 或 OpenWhisk 或 Fn Project,你可以通过最小的工作量来完成,因为 Serverless 也支持这些平台。图 21.12 展示了 Serverless 与 Kubeless 的架构。

图 21.12 说明了 Serverless 引入了自己的规范语言,但使用底层的无服务器平台来运行函数。

Serverless 并不完全像 Kubeless 那样干净利落,因为它为函数添加了一个额外的 YAML 规范,所以你无法直接带上代码文件并启动它。优点在于这个规范相当简单,它将函数定义和触发器放在一个地方。列表 21.4 展示了 to-do API 函数的规范。这个文件位于项目文件夹中,与源代码一起,代码文件本身与你在 21.2 节中用 Kubeless 部署的文件相同。

列表 21.4 serverless.yml,一个 Serverless 函数规范

service: todo-api            # A service can group many functions.
provider:                    
  name: kubeless             # The provider is the actual platform.
  runtime: nodejs12          # You can use any runtime it supports.  

  hostname: api.todo.kiamol.local    # This is used for ingress rules.

plugins:
  - serverless-kubeless
functions:                   # This is the function definition.
  todo-api:                    
    description: 'ToDo list - create item API'
    handler: server.handler
    events:                  # Complete with the trigger events.
      - http:
          path: /todos

Serverless 开发者体验也不如 Kubeless 那么干净。Serverless 使用一个命令行工具,这是一个 Node.js 包,所以你需要安装 Node.js,然后安装 Serverless,这将下载大量的依赖项。我已经将 CLI 打包在一个容器镜像中,这样你就不需要这样做,在本节中,我们将用 Serverless 的版本替换 Kubeless 函数。

现在试试看!移除 Kubeless 函数,并使用 Serverless 作为抽象层重新部署它们。

# delete the custom resources to remove functions and triggers:
kubectl delete cronjobtriggers,natstriggers,httptriggers,functions --all

# create a Pod with the Serverless CLI:
kubectl apply -f serverless-cli.yaml

# wait for it to be ready:
kubectl wait --for=condition=ContainersReady pod serverless-cli

# confirm the Serverless CLI is set up:
kubectl exec serverless-cli -- serverless --version

Serverless CLI 使用 提供者 将通用的函数规范适配为平台组件。它实际上替换了 Kubeless CLI,使用 Kubeless 提供者和 Kubernetes 客户端库来创建自定义资源,这些资源由正常的 Kubeless 控制器管理。图 21.13 展示了 CLI 已安装并正在运行,但这还不是你需要的全部。提供者和 Kubernetes 客户端库需要与大约 100 个其他依赖项一起安装到项目文件夹中。

图 21.13 Serverless 提供了一种替代的部署体验,最终会创建与 Kubeless 相同的函数资源。

无服务器不是一个简单的项目,但它非常受欢迎。它不仅适用于在 Kubernetes 上运行的无服务器平台,还可以用作 AWS Lambda 和 Azure Functions 的抽象层。你不能直接将为 Kubeless 编写的函数提升并作为 Azure Functions 部署,因为平台以不同的方式使用不同的参数调用方法,但函数代码的核心将是相同的。接下来,我们将看到使用 Serverless 部署待办事项 API 函数的部署情况。

现在试试看 再次使用相同的代码文件创建待办事项 API 函数,但使用 Serverless 来定义和部署它。

# connect to a Serverless CLI session:
kubectl exec -it serverless-cli -- sh

# switch to the API code folder:
cd /kiamol/ch21/serverless/todo-api

# install all the deployment dependencies:
npm install

# deploy the function:
serverless deploy

# list Kubeless functions:
kubectl get functions
# confirm the Pod has been created:
kubectl get pods -l function=todo-api

# list HTTP triggers:
kubectl get httptriggers

你可以从图 21.14 中看到,使用 Kubeless 作为提供者的无服务器函数的安装结果与使用 Kubeless CLI 或直接部署自定义资源相同。你必须为每个项目执行设置阶段,但只有在第一次部署它或升级提供者时才需要这样做,因为 Serverless 实际上只是一个用于部署和管理函数的客户端工具。

图 21.14

图 21.14 无服务器是一个无服务器平台的抽象。这个部署创建了一个 Kubeless 函数和触发器。

我们不会部署修改函数,因为我预计你在上一节已经收到了消息,但我们将继续部署审计函数,并确认一切仍然按预期工作。Serverless 支持不同的事件类型来触发函数,审计函数规范包括一个用于 NATS 新项目消息的队列触发器。

现在试试看 仍然处于你的无服务器 CLI 会话中,切换到审计函数的文件夹,并使用消息队列触发器部署它。

# switch to the function folder:
cd /kiamol/ch21/serverless/todo-audit

# deploy the Serverless Node.js dependencies:
npm install

# deploy the function itself:
serverless deploy

# confirm the function has been deployed:
kubectl get functions

# along with its trigger:
kubectl get natstriggers

# we’re done with the CLI Pod now:
exit

我的输出如图 21.15 所示,你可以看到无论你使用 Kubeless CLI、Serverless CLI 还是 kubectl 应用自定义资源规范,结果都是相同的。这些都是围绕无服务器模型的不同抽象,而该模型本身是标准 Kubernetes 应用模型的一个抽象。

图 21.15

图 21.15 不同触发类型的函数在 Serverless YAML 规范中定义,并以相同的方式部署。

Serverless 的一项限制是 CLI 只在单个函数的上下文中运行——你需要在该函数目录中运行命令,以便 CLI 可以读取规范并找到函数。你可以将多个函数分组在一个文件夹和一个 Serverless 规范中,但它们都必须使用相同的运行时,所以这不适合本节的多语言函数。实际上,如果你使用 Serverless,你将混合使用 Serverless CLI 和 kubectl 来获得完整的管理体验。现在函数和触发器都已部署,我们实际上根本不需要使用 Serverless 来与之交互。

现在试试看 API 函数的 HTTP 触发器使用相同的入口规则,审计函数的 NATS 触发器使用相同的队列,因此端到端可以以相同的方式进行测试。

# if you’re running in PowerShell, add a grep function to your session:
.\grep.ps1

# make a call to the API:
curl --data 'Sketch out volume III of the trilogy' http://api.todo.kiamol.local/todos

# print the latest logs from the message handler:
kubectl logs -l component=save-handler --tail 2

# print the audit function:
kubectl logs -l function=todo-audit | grep AUDIT

通过这个练习和图 21.16 可以清楚地看出,Kubeless 和 Serverless 在构建和部署阶段添加了抽象层,但在运行阶段并不需要使用它们。函数可以直接从单个代码文件中交付,而不需要复杂的 CI/CD 管道,甚至不需要构建容器镜像。部署的组件只是标准的 Kubernetes 资源,你可以像往常一样管理它们。如果你在集群中设置了集中的日志记录和监控,你的无服务器函数将以与其他应用程序相同的方式与它们集成。

图 21.16 使用 Serverless 部署的函数与 Kubeless 提供程序的行为方式相同。

我们就到这里讨论无服务器(以及无服务器)了。它是一个有用的架构,并且在使用 Kubernetes 的过程中积累一些经验是很好的,这样你可以理解从代码到 Pod 的转换是如何工作的。

21.5 理解无服务器函数的位置

云端无服务器平台所承诺的是,你带来你的代码,你就可以忘记操作方面——部署很简单,平台会根据需求自动扩展和缩减,而不需要你任何干预。这对于许多用例来说非常有吸引力,但它有一个主要的缺点,那就是你的应用程序的无服务器组件与其他所有组件都不同。围绕无服务器的流程是最小的,但它们仍然是必需的,这让你有了与所有其他应用程序不同的部署管道、监控工具和故障排除工作流程。

这就是 Kubernetes 上的无服务器的作用所在。这是一个折衷方案,因为你不会得到云无服务器平台的零操作承诺,你也不能随意扩展,因为你需要平衡计算资源与你的其他应用程序。但正如你在本章中看到的,你确实得到了流畅的开发者工作流程,并且你可以像管理你的其他 Kubernetes 部署一样管理你的无服务器函数。这可能不是你第二天就带到你的集群中的东西,但它是一个作为选项的强大工具。

这就留下了一个问题:究竟哪个工具?截至 2021 年,没有无服务器项目在 CNCF 下,我在本章中提到的选项是开源和商业项目的混合体,它们的采用率和活跃度相当多样。目前,Kubeless 和 Knative 是主要的选择,值得对两者都进行评估,考虑到 Serverless 项目在隔离你与底层平台的同时,需要承担更多的 YAML 规范。CNCF 运行了一个无服务器工作组,该工作组演变成了 CloudEvents,这是一个通用的规范,用于触发函数及其数据结构的事件,这正在为无服务器平台带来标准化。

这就带我们到了结尾,所以剩下的就是整理一下,然后尝试实验室。

现在尝试一下。移除 Kubeless 组件,这将移除所有正在运行的功能,然后清理其余的部署。

kubectl delete -f kubeless/

kubectl delete ns,all,secret,configmap,pvc,ingress -l kiamol=ch21

21.6 实验室

我在本章中提到了 Knative 几次,现在轮到你来尝试了。你的任务是部署一个 Knative 版本的待办事项 API,该 API 可在 21.4 节中使用的相同 URL 上找到。Docker 镜像已经构建完成,但像你这样的经验丰富的 Kubernetes 用户不需要太多提示。这是你探索 Knative 文档并看看你是否更喜欢 Kubeless 方法的时机。

  • 你的 API 镜像名为 kiamol/ch21-todo-api。你可以使用 Knative CLI 或自定义资源定义来创建它作为一个 Knative 函数。

  • 实验室文件夹包含一个 Knative 部署和待办事项应用的新版本。

  • 这个 Knative 设置使用了一个名为 Contour 的 CNCF 项目作为入口控制器。访问你的应用的 IP 地址位于 contour-external 命名空间中的 envoy 服务中。

  • Knative 使用 Knative Service 的名称和 Kubernetes 命名空间来构建入口域名,因此你在部署函数时需要小心谨慎。

实际上有很多提示。这个可能需要一些调查,但不要气馁——解决方案很简单,尽管你需要调整很多因素才能使函数按预期运行。我的解决方案在 GitHub 上:github.com/sixeyed/kiamol/blob/master/ch21/lab/README.md

22 永不结束

午餐月系列书中,传统上以一个名为“永不结束”的章节结束,以强调总有更多东西可以学习。我认为这一点在 Kubernetes 上比任何其他技术都更真实;这本书最终比最初计划的要大 25%,而且我甚至没有提到每个主题。我尽力涵盖了您使用 Kubernetes 时可能认为重要的所有内容,并在本章中,我将突出一些我没有时间涉及的内容,以便您可以继续探索。我还提供了一些关于选择 Kubernetes 平台和社区简介的指导。

22.1 按章节进一步阅读

进一步阅读的主要来源是官方 Kubernetes 网站(kubernetes.io),其中包含文档和指南,以及 API 参考(kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/),其中详细描述了所有资源和它们的规范。这些是填补本书中一些空白的好地方——而且它们也是如果您参加 Kubernetes 认证考试时可以使用的唯一资源。在我规划每一章时,我发现了一些我没有空间包含的主题,如果您想进一步学习,以下是我们跳过的一些内容:

  • 第二章介绍了我们在其他所有章节中使用的 YAML 规范,但没有涵盖Kustomize。这是一个简化 YAML 规范的工具。您可以有一个基础规范集和一组覆盖规范,它们为不同的环境提供不同的配置。它功能强大但比 Helm 中的完整模板体验简单。

  • 第三章介绍了网络和 Service 资源。一个新的扩展名为Service Topology,它为您提供了对 Pod 之间负载均衡的更精细控制。您可以指定网络连接应使用与客户端在同一节点上的 Pod,或同一区域中的节点,以减少网络延迟。

  • 第四章详细介绍了配置,但跳过了投影卷,这是一种将多个 ConfigMaps 和 Secrets 暴露到容器文件系统中的一个目录中的有用方法——如果你希望为需要在一个地方看到所有配置项的应用独立存储配置项,这会很有用。

  • 第五章没有涵盖所有平台特定的卷类型,这些是在生产中您真正需要理解的内容。概念是相同的——您创建一个 PVC 并将其绑定到 Pod 上——但底层存储系统有很大差异。例如,Azure Kubernetes Service 支持使用 Azure Files 或 Azure Disks 的卷。文件更容易在节点之间共享,但磁盘提供了更好的性能。

  • 第六章展示了 Deployments 和 DaemonSets 的扩展工作原理,运行相同 Pod 规范的多个副本。扩展意味着运行更多的 Pods,它们进入挂起状态,等待调度器分配给节点。您可以使用第十九章中学到的亲和性和反亲和性规则来控制副本的位置。

  • 第七章介绍了多容器 Pods。有一种新的类型称为临时容器,这对于调试特别有用;它允许您在现有的 Pod 内运行一个临时容器。临时容器共享 Pod 的网络空间,可以共享卷和进程,因此非常适合调查问题,尤其是如果您的应用程序容器使用没有工具如curl(它应该有)的最小 Docker 镜像。

  • 第八章探讨了使用 StatefulSets 为应用程序创建稳定环境。在某些情况下,您可能希望 Pod 命名一致,而不保证启动顺序,这样 Pod 可以并行启动。规范中的Pod 管理策略允许这样做。

  • 第九章介绍了部署和回滚,但您还希望了解Pod 中断预算(PDBs)。这些是独立的对象,确保保持一定数量的 Pod 可用,这对于集群或部署问题是一个重要的安全控制。PDBs 优先于其他操作,因此您不能排空节点,如果移除其 Pod 会违反 PDB。

  • 第十章为您介绍了 Helm,并涵盖了主要功能,但省略了很多内容。Helm 的模板语言支持使用 if 和 else 语句和循环进行控制流。您可以在图表中创建命名模板和段,并多次使用它们,这使得复杂的应用程序设计或部署选项更容易管理。

  • 第十一章介绍了 Kubernetes 中的一些 CI/CD 模式,但没有提到GitOps,这是一种吸引人的新方法,其中集群监视 Git 存储库以检查应用程序发布。当创建发布时,集群会自动部署它,因此,管理员不需要访问集群进行应用程序管理,集群会自行更新应用程序。Argo CD 是 GitOps 的 CNCF 项目。

  • 第十二章涵盖了自我修复应用程序和资源限制,但没有涉及到服务质量类别,这是 Kubernetes 根据 Pod 的资源规范提供服务保证的方式。类别包括 BestEffort、Burstable 和 Guaranteed,您应该了解它们的工作原理,因为它们会影响 Pod 驱逐。

  • 第十三章向您展示了如何从 Pods 中收集、转发和存储日志条目。这包括在 Pods 中运行的 Kubernetes 组件,如 API 服务器和 DNS 插件,但您还希望收集 kubelet 和容器运行时的日志。如何做到这一点取决于平台,但将日志放入您的集中式日志系统也很重要。

  • 第十四章通过快速浏览导出器和客户端库介绍了 Prometheus。Prometheus 的部署很简单,你应该花时间了解Prometheus Operator。它是一个生产级别的部署,为 Prometheus 和 Alertmanager 实例以及抓取目标和警报规则添加了自定义资源定义(CRDs)。

  • 第十五章以 Nginx 和 Traefik 为例介绍了入口控制器。如果你正在评估选项,你应该将Contour添加到你的列表中。它是一个 CNCF 项目,使用 Envoy 作为底层代理——这是一个非常快速且功能丰富的技术。

  • 第十六章探讨了大量的安全选项,但没有包括PodSecurityPolicy资源,它允许你定义 Pod 在集群运行之前必须遵守的规则。PodSecurityPolicies 使用准入控制器来阻止不符合你定义的策略的 Pod,它们是一种强大的安全控制,但这是一个新特性,并不是每个平台都提供。

  • 第十七章关于 RBAC 的内容没有涵盖服务账户投影,这是一种 Pod 规范请求带有声明性过期时间和 JWT 受众的定制令牌的方式。如果你只为初始化容器需要令牌,你可以请求一个在初始化后过期的令牌。你还需要了解 Kubernetes API 服务器如何审计请求,GitHub 上有一个名为audit2rbac的有用工具,可以从审计日志生成 RBAC 规则。

  • 第十八章为你提供了安装和管理自己的多节点集群的体验,但如果你真的打算这样做,还有很多东西需要了解。Rancher Kubernetes Engine(RKE)是 Rancher 的一个开源产品,它简化了本地集群的安装和管理。如果你正在考虑混合环境,你可以使用 Azure Arc 等服务在云中管理你的本地集群。

  • 第十九章介绍了 HorizontalPodAutoscaler,但还有两种其他类型的自动扩展。VerticalPodAutoscaler是 Kubernetes 项目的一个附加组件,它根据实际使用情况管理资源请求和限制,因此 Pod 可以向上或向下扩展,并且集群有一个准确资源视图。集群自动扩展监控调度器。如果没有足够的计算资源来运行挂起的 Pod,它将在集群中添加一个新的节点。云提供商通常提供集群自动扩展服务。

  • 第二十章遗漏了扩展 Kubernetes 的另一种选择——API聚合。CRDs 向标准 API 添加了新的资源类型,但聚合层允许你将全新的功能类型插入到 API 服务器中。它并不常用,但它让你能够向你的集群添加新的功能,这些功能由 Kubernetes 进行认证。

  • 第二十一章介绍了许多无服务器平台选项,但还有一种关于无服务器的看法——当没有工作要做时缩小到零。一个名为 KEDA(Kubernetes 事件驱动自动扩展)的 CNCF 项目涵盖了这一点。它监控事件源,如消息队列或 Prometheus 指标,并根据传入的负载自动扩展或缩小现有应用程序。如果你想要与现有工作流程的自动管理,这是一个很好的无服务器替代方案。

我还没有提到 Kubernetes 的 仪表板。这是一个在集群中运行的 Web UI,它显示你工作负载的图形视图及其健康状况。你还可以使用仪表板部署应用程序和编辑现有资源,所以如果你使用它,你需要注意你的 RBAC 规则以及谁可以访问 UI。

你可以根据自己的需要决定对那些额外主题进行深入挖掘的程度。这些主题没有出现在书中,因为它们不是你 Kubernetes 经验的核心,或者它们使用了尚未得到广泛应用的全新功能。无论你决定走多远,你的下一个真正任务是了解你将使用哪个 Kubernetes 平台。

22.2 选择 Kubernetes 平台

Kubernetes 为发行版和托管平台提供了一个认证流程。如果你正在评估一个平台,你应该从认证列表开始,你可以在 CNCF 景观网站上找到这个列表(landscape.cncf.io/)。Kubernetes 特别适合云环境,你可以在几分钟内启动一个集群,并且这个集群可以很好地与其他服务提供商的服务集成。至少,你应该期待以下功能集,你将在所有主要云服务中找到这些功能:

  • 与负载均衡器服务的集成,这样它们就会与云负载均衡器一起提供,该负载均衡器跨越集群节点和公共 IP 地址

  • 多种存储类,例如 SMB 和 SSD,这样你可以在你的 PersistentVolumeClaims 中在 I/O 性能和可用性之间进行选择

  • 密钥存储,因此部署到集群中的密钥实际上是在一个安全的加密服务中创建的,但仍然可以通过常规的密钥 API 访问

  • 集群自动扩展,这样你可以设置最小和最大集群大小,并根据你的 Pods 规模需求添加或删除节点

  • 多种身份验证选项,因此最终用户可以使用他们的云凭证访问集群,但你也可以为自动化系统提供访问权限

如果你的组织与某个提供商有现有关系,你的云提供商选择可能已经被决定了。Azure Kubernetes 服务、Amazon 的 Elastic Kubernetes 服务和 Google Kubernetes Engine 都是很好的选择。如果你确实有选择,你也应该考虑平台推出新 Kubernetes 版本的速度以及服务的管理程度——节点是否完全由平台管理,或者你是否需要推出操作系统补丁?

当你确定了一个托管平台,你需要花一些时间学习云的其他功能是如何与 Kubernetes 集成的。对象元数据中的注释通常用作桥梁,以包含标准 Kubernetes 模型之外的云服务配置设置。这是一种有用的配置部署的方式,以便你得到你想要的确切结果,但你需要意识到任何定制都会降低你应用程序的可移植性。

对于本地部署的情况,情况略有不同。你仍然有认证流程,许多产品提供带有定制管理体验和支持团队的标准 Kubernetes 集群。其他产品将 Kubernetes 包装在自己的工具和模型中——OpenShift 就是以这种方式做的,它使用自己的资源定义进行运行时和网络抽象,甚至使用替代的 CLI。包装后的发行版可能提供你更喜欢于标准 Kubernetes 的功能或流程,只要你知道这不仅仅是一个 Kubernetes,而且可能很难将你的应用程序迁移到另一个平台。

运行自己的开源 Kubernetes 集群绝对是一个选择,这是初创公司或拥有大量数据中心资产的组织的首选。如果你正在考虑这条路线,你需要了解你将要承担多少责任。给你的运维团队每个人都发一本这本书,然后让他们去学习和通过认证 Kubernetes 管理员考试。如果他们仍然热衷于运行集群,那就去做吧,但要注意,他们的角色可能会转变为全职 Kubernetes 管理员。这不仅仅是管理集群,还要跟上 Kubernetes 项目本身的发展。

22.3 理解 Kubernetes 的构建方式

Kubernetes 有季度发布计划,因此每三个月就会推出新功能,beta 功能升级为通用可用性,旧功能被弃用。这些都是小版本更新——比如说,从 1.17 升级到 1.18。主版本仍然是 1,而且没有计划在不久的将来跳转到版本 2。项目支持最近的三个小版本,这意味着你应该计划至少每年升级你的集群两次。补丁发布会在出现关键错误或安全问题时进行;例如,1.18.6 版本的发布修复了一个问题,即容器可能会在没有触发驱逐的情况下填满节点的磁盘。

所有发布都在 GitHub 的 kubernetes/kubernetes 仓库中,每个发布都附带详尽的变更日志。无论你是运行自己的集群还是使用托管平台,在升级之前,你需要了解发布中有什么内容,因为有时会有破坏性的更改。随着功能通过 alpha 和 beta 阶段,规范中的 API 版本会发生变化,最终旧版本将不再受支持。如果你有使用 apps/v1beta2 API 版本的 Deployment 规范,你无法从 1.16 版本的 Kubernetes 上部署它们——你需要更改规范以使用 apps/v1 版本。

特别兴趣小组(SIGs)拥有 Kubernetes 项目的不同方面,包括发布、技术主题和流程——从身份验证到贡献者体验和 Windows 工作负载的一切。SIGs 是公开的;他们有定期的在线会议,任何人都可以加入,他们有专门的 Slack 频道,并且所有决定都在 GitHub 上有记录。一个技术 SIG 覆盖广泛的领域,并分为子项目,这些子项目负责 Kubernetes 一个组件的设计和交付。总体而言,指导委员会负责项目的治理,委员会成员将当选为期两年的任期。

Kubernetes 的开放结构推动了创新和质量。治理模型确保个别组织不会有不公平的代表性,因此它们不能将路线图引导到适合其产品的方向。最终,Kubernetes 本身由 CNCF 管理员负责,CNCF 被授权促进技术、保护品牌并为社区服务。

22.4 加入社区

这个社区包括你和我在内。如果你想了解项目的发展情况,最好的开始方式是加入 Slack,网址是 kubernetes.slack.com(你可以在那里找到我)。每个 SIG 都有自己的频道,因此你可以关注你感兴趣的区域,同时存在一些通用频道,用于发布版本公告、用户和初学者。世界各地都有定期的聚会小组,KubeCon 和 CloudNativeCon 大会分别在欧洲、美洲和亚洲举行。所有的 Kubernetes 代码和文档都是开源的,你可以在 GitHub 上进行贡献。

就这些了。感谢你阅读这本书,希望它帮助你变得对 Kubernetes 舒适和自信。它可能是一项改变职业生涯的技术,我希望你在这里学到的所有东西都能帮助你走上旅程。

附录 A:将源代码打包到 Docker 镜像中

构建 Docker 镜像非常简单。你需要知道的一件事是,你可以也在 Dockerfile 中运行命令来打包你自己的应用程序。

命令在构建过程中执行,并且从命令中产生的任何文件系统更改都会保存在镜像层中。这使得 Dockerfile 成为最灵活的打包格式之一;你可以展开 zip 文件,运行 Windows 安装程序,以及做几乎所有其他事情。在本章中,你将利用这种灵活性来从源代码打包应用程序。

本附录摘自第四章“将源代码打包到 Docker 镜像中”,来自 Elton Stoneman 的《一个月午餐时间学习 Docker》(Manning,2020)。任何章节引用或对代码仓库的引用都指的是该书的章节或代码仓库。

A.1 当你有 Dockerfile 时,还需要构建服务器吗?

在你的笔记本电脑上构建软件是你在本地开发时做的事情,但当你在一个团队中工作时,有一个更严格的交付过程。有一个共享的源代码控制系统,如 GitHub,每个人都会推送他们的代码更改,通常还有一个单独的服务器(或在线服务),当更改被推送时,它会构建软件。

这个过程存在是为了尽早发现问题。如果一个开发者在推送代码时忘记添加文件,构建将在构建服务器上失败,并且团队将会被通知。它保持了项目的健康,但代价是必须维护一个构建服务器。大多数编程语言需要大量的工具来构建项目——图 A.1 展示了几个例子。

图 A.1 每个人都需要一套相同的工具来构建一个软件项目。

这里有一个很大的维护开销。新加入团队的成员将花费他们第一天的大部分时间来安装工具。如果一个开发者更新了他们的本地工具,导致构建服务器运行的是不同版本,构建可能会失败。即使你使用的是托管构建服务,你也会遇到同样的问题,在那里你可能只能安装有限的一组工具。

一次性打包构建工具集并共享会更好,这正是你可以使用 Docker 做到的。你可以编写一个 Dockerfile 来脚本化所有工具的部署,并将其构建到镜像中。然后你可以在你的应用程序 Dockerfile 中使用该镜像来编译源代码,最终输出就是你的打包应用程序。

让我们从一个非常简单的例子开始,因为在这个过程中有几个新事物需要理解。列表 A.1 展示了包含基本工作流程的 Dockerfile。

列表 A.1 多阶段 Dockerfile

FROM diamol/base AS build-stage
RUN echo 'Building...' > /build.txt

FROM diamol/base AS test-stage
COPY --from=build-stage /build.txt /build.txt
RUN echo 'Testing...' >> /build.txt

FROM diamol/base
COPY --from=test-stage /build.txt /build.txt
CMD cat /build.txt

这被称为多阶段 Dockerfile,因为构建有几个阶段。每个阶段都以FROM指令开始,你可以选择性地使用AS参数给阶段命名。列表 A.1 有三个阶段:build-stagetest-stage和最终的未命名阶段。尽管有多个阶段,但输出将是一个包含最终阶段内容的单个 Docker 镜像。

每个阶段都是独立运行的,但你可以从早期阶段复制文件和目录。我使用带有-from参数的COPY指令,它告诉 Docker 从 Dockerfile 中的早期阶段复制文件,而不是从主机计算机的文件系统中复制。在这个例子中,我在构建阶段生成一个文件,将其复制到测试阶段,然后从测试阶段将文件复制到最终阶段。

这里有一个新的指令,RUN,我正在用它来写入文件。RUN指令在构建过程中在容器内执行命令,并且该命令的任何输出都将保存在镜像层中。你可以在RUN指令中执行任何操作,但你想运行的命令需要存在于你在FROM指令中使用的 Docker 镜像中。在这个例子中,我使用了diamol/base作为基础镜像,它包含了echo命令,所以我确定我的RUN指令会工作。

图 A.2 显示了构建此 Dockerfile 时将要发生的事情——Docker 将按顺序运行这些阶段。

图片

图 A.2 执行多阶段 Dockerfile

重要的是要理解各个阶段是相互独立的。你可以使用安装了不同工具集的不同基础镜像,并运行你喜欢的任何命令。最终阶段的输出将只包含你从早期阶段显式复制的部分。如果任何阶段的命令失败,整个构建将失败。

现在尝试一下 打开终端会话到存储本书源代码的文件夹,并构建此多阶段 Dockerfile:

cd ch04/exercises/multi-stage
docker image build -t multi-stage .

你会看到构建将按照 Dockerfile 中的顺序执行步骤,这通过图 A.3 中可以看到的阶段进行顺序构建。

图片

图 A.3 构建多阶段 Dockerfile

这是一个简单的例子,但构建任何复杂性的应用程序的单个 Dockerfile 的模式是相同的。图 A.4 显示了 Java 应用程序的工作流程。

图片

图 A.4 Java 应用程序的多阶段构建

在构建阶段,你使用一个安装了应用程序构建工具的基础镜像。你将主机机器上的源代码复制进来,并运行build命令。你可以添加测试阶段来运行单元测试,该测试阶段使用安装了测试框架的基础镜像,从构建阶段复制编译后的二进制文件,并运行测试。最终阶段从一个只安装了应用程序运行时的基础镜像开始,并从测试阶段成功测试过的构建阶段复制二进制文件。

这种方法使您的应用程序真正可移植。您可以在任何地方运行应用程序,也可以在任何地方构建应用程序——Docker 是唯一的前提条件。您的构建服务器只需要安装 Docker;新团队成员可以在几分钟内设置好,构建工具都集中存储在 Docker 镜像中,因此不会出现不同步的情况。

所有主要的应用程序框架已经在 Docker Hub 上有了带有构建工具的公共镜像,并且还有带有应用程序运行时的单独镜像。您可以直接使用这些镜像,或者将它们包装在您自己的镜像中。您将获得使用由项目团队维护的最新更新的好处。

A.2 应用程序概述:Java 源代码

现在我们将转向一个真实示例,我们将使用 Docker 构建和运行一个简单的 Java Spring Boot 应用程序。您不需要是 Java 开发者或在自己的机器上安装任何 Java 工具来使用此应用程序;您所需的一切都将包含在 Docker 镜像中。如果您不使用 Java,您也应该阅读本节——它描述了一个适用于其他编译语言(如.NET Core 和 Erlang)的模式。

源代码位于书籍的仓库中,路径为ch04/exercises/image-of-the-day。应用程序使用了一套相当标准的 Java 工具:Maven,用于定义构建过程和获取依赖项,以及 OpenJDK,它是一个可自由分发的 Java 运行时和开发工具包。Maven 使用 XML 格式来描述构建,Maven 命令行称为mvn。这些信息应该足以理解列表 A.2 中的应用程序 Dockerfile。

列表 A.2 使用 Maven 构建 Java 应用的 Dockerfile

FROM diamol/maven AS builder

WORKDIR /usr/src/iotd
COPY pom.xml .
RUN mvn -B dependency:go-offline

COPY . .
RUN mvn package

# app
FROM diamol/openjdk

WORKDIR /app
COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .

EXPOSE 80
ENTRYPOINT ["java", "-jar", "/app/iotd-service-0.1.0.jar"]

这里的大多数 Dockerfile 指令都是您之前见过的,模式与您构建的示例中的模式相似。这是一个多阶段 Dockerfile,您可以通过存在多个FROM指令来判断,步骤安排旨在从 Docker 的镜像层缓存中获得最大好处。

第一个阶段被称为builder。以下是构建阶段发生的情况:

  • 它使用diamol/maven镜像作为基础。该镜像已安装了 OpenJDK Java 开发工具包,以及 Maven 构建工具。

  • 构建阶段首先在镜像中创建一个工作目录,然后复制pom.xml文件,这是 Maven 对 Java 构建的定义。

  • 第一个RUN语句执行 Maven 命令,获取所有应用程序依赖项。这是一个昂贵的操作,因此它有自己的步骤来利用 Docker 层缓存。如果有新的依赖项,XML 文件将更改,该步骤将运行。如果依赖项没有更改,则使用层缓存。

  • 接下来,将剩余的源代码复制进来——COPY . .表示“从 Docker 构建运行的位置,将所有文件和目录复制到镜像中的工作目录。”

  • 构建的最后一步是运行mvn package,它编译并打包应用程序。输入是一组 Java 源代码文件,输出是一个名为 JAR 文件的 Java 应用程序包。

当这个阶段完成后,编译的应用程序将存在于构建阶段的文件系统中。如果 Maven 构建过程中有任何问题——如果网络离线且获取依赖失败,或者源代码中存在编码错误——RUN指令将失败,整个构建将失败。

如果构建阶段成功完成,Docker 将继续执行最终阶段,该阶段生成应用程序镜像:

  • 它从diamol/openjdk开始,其中包含 Java 11 运行时,但没有包含任何 Maven 构建工具。

  • 这个阶段创建了一个工作目录并将构建阶段的编译 JAR 文件复制进来。Maven 将应用程序及其所有 Java 依赖项打包在这个单一的 JAR 文件中,因此构建阶段只需要这个。

  • 该应用程序是一个监听 80 端口的 Web 服务器,因此该端口在EXPOSE指令中明确列出,告诉 Docker 该端口可以发布。

  • ENTRYPOINT指令是CMD指令的替代方案——它告诉 Docker 从镜像启动容器时要做什么,在这种情况下是运行 Java 并指定应用程序 JAR 的路径。

现在尝试一下 浏览到 Java 应用程序源代码并构建镜像:

cd ch04/exercises/image-of-the-day
docker image build -t image-of-the-day .

由于你会看到 Maven、获取依赖和 Java 构建的所有日志,所以这个构建会有很多输出。图 A.5 显示了构建的一个简略部分。

图片

图 A.5 Docker 中运行 Maven 构建的输出

那么你刚刚构建了什么?它是一个简单的 REST API,它封装了对 NASA 每日天文图片服务的访问(apod.nasa.gov)。Java 应用程序从 NASA 获取当天图片的详细信息并将其缓存,这样你就可以重复调用此应用程序,而无需反复调用 NASA 的服务。

Java API 只是本章中你将运行的全应用的一部分——它实际上会使用多个容器,并且它们需要相互通信。容器通过虚拟网络访问彼此,使用 Docker 在创建容器时分配的虚拟 IP 地址。你可以通过命令行创建和管理虚拟 Docker 网络。

现在尝试一下 为容器创建一个用于相互通信的 Docker 网络:

docker network create nat

如果你看到该命令的错误,那是因为你的设置已经有一个名为nat的 Docker 网络,你可以忽略该消息。现在当你运行容器时,你可以使用-network标志显式地将它们连接到该 Docker 网络,并且该网络上的任何容器都可以通过容器名称相互访问。

现在尝试一下 从镜像运行容器,将端口 80 发布到主机计算机,并连接到nat网络:

docker container run --name iotd -d -p 800:80 --network nat image-of-the-day

现在,你可以浏览到 http://localhost:800/image,你会看到关于 NASA 当日图像的一些 JSON 详细信息。在我运行容器的那天,图像来自日食——图 A.6 显示了我 API 的详细信息。

图 A.6

图 A.6 我的应用容器中缓存的来自 NASA 的详细信息

在此容器中的实际应用程序并不重要(但请不要删除它——我们将在本章后面使用它)。重要的是,你可以在安装了 Docker 的任何机器上构建它,只需有一个包含 Dockerfile 的源代码副本即可。你不需要安装任何构建工具,也不需要特定的 Java 版本——你只需克隆代码仓库,然后通过几个 Docker 命令就可以运行应用程序。

这里还有另一件需要非常清楚的事情:构建工具不是最终应用程序图像的一部分。你可以从新的 image-of-the-day Docker 图像运行一个交互式容器,你会发现里面没有 mvn 命令。只有 Dockerfile 中的最终阶段的全部内容被制作成应用程序图像;任何你想要从先前阶段的内容都需要在最终阶段显式复制。

A.3 应用程序操作流程:Node.js 源代码

我们将浏览另一个多阶段 Dockerfile,这次是为 Node.js 应用程序。随着组织越来越多地使用多样化的技术栈,了解不同的构建在 Docker 中的外观是很有帮助的。Node.js 是一个很好的选择,因为它很受欢迎,而且它也是一个不同类型构建的例子——这种模式也适用于其他脚本语言,如 Python、PHP 和 Ruby。此应用程序的源代码位于文件夹路径 ch04/exercises/ access-log

Java 应用程序是编译的,因此源代码被复制到构建阶段,从而生成一个 JAR 文件。JAR 文件是编译后的应用程序,它被复制到最终应用程序图像中,但源代码不是。对于 .NET Core 也是如此,编译后的工件是 DLL(动态链接库)。Node.js 是不同的——它使用 JavaScript,这是一种解释型语言,因此没有编译步骤。容器化的 Node.js 应用程序需要在应用程序图像中包含 Node.js 运行时和源代码。

尽管如此,仍然需要一个多阶段 Dockerfile:它优化了依赖项加载。Node.js 使用一个名为 npm(Node 包管理器)的工具来管理依赖项。附录 A.3 列出了本章 Node.js 应用程序的完整 Dockerfile。

附录 A.3 构建 Node.js 应用程序的 Dockerfile

FROM diamol/node AS builder

WORKDIR /src
COPY src/package.json .

RUN npm install

# app
FROM diamol/node

EXPOSE 80
CMD ["node", "server.js"]

WORKDIR /app
COPY --from=builder /src/node_modules/ /app/node_modules/
COPY src/ .

这里的目标与 Java 应用程序相同——仅安装 Docker 就可以打包和运行应用程序,无需安装任何其他工具。两个阶段的基镜像都是 diamol/node,其中包含 Node.js 运行时和 npm。Dockerfile 中的构建阶段复制 package.json 文件,这些文件描述了应用程序的所有依赖项。然后它运行 npm install 来下载依赖项。没有编译,所以这就是它需要做的。

此应用程序是另一个 REST API。在最终的应用程序阶段,步骤暴露 HTTP 端口并指定 node 命令行作为启动命令。最后要做的是创建一个工作目录并将应用程序工件复制进去。下载的依赖项从构建阶段复制过来,源代码从主机计算机复制过来。src 文件夹包含 JavaScript 文件,包括 server.js,这是由 Node.js 进程启动的入口点。

在这里,我们有不同的技术堆栈,用于打包应用程序的模式也不同。Node.js 应用程序的基镜像、工具和命令都与 Java 应用程序不同,但这些差异都记录在 Dockerfile 中。构建和运行应用程序的过程完全相同。

现在试试 Browse to the Node.js application source code and build the image:

cd ch04/exercises/access-log
docker image build -t access-log .

您将看到 npm(可能还会显示一些错误和警告消息,但您可以忽略这些)的大量输出。图 A.7 显示了我构建的部分输出。下载的软件包被保存在 Docker 镜像层缓存中,所以如果您只对应用程序进行代码更改,下一次构建将非常快。

图 A.7 为 Node.js 应用程序构建多阶段 Dockerfile

您刚刚构建的 Node.js 应用程序并不有趣,但您仍然应该运行它以检查它是否正确打包。这是一个 REST API,其他服务可以调用它来记录日志。有一个 HTTP POST 端点用于记录新的日志,还有一个 GET 端点显示记录了多少日志。

现在试试 Run a container from the log API image, publishing port 80 to host and connecting it to the same nat network:

docker container run --name accesslog -d -p 801:80 --network nat access-log

现在,浏览到 http://localhost:801/stats,您将看到服务记录了多少日志。图 A.8 显示我目前还没有任何日志——Firefox 很好地格式化了 API 响应,但您可能在其他浏览器中看到原始 JSON。

图 A.8 在容器中运行 Node.js API

日志 API 正在 Node.js 版本 10.16 上运行,但就像 Java 示例一样,您不需要安装任何版本的 Node.js 或其他工具来构建和运行此应用程序。此 Dockerfile 中的工作流程下载依赖项,然后将脚本文件复制到最终镜像中。您可以使用完全相同的方法使用 Python,使用 Pip 进行依赖项管理,或使用 Ruby 的 Gems。

A.4 应用程序概述:Go 源代码

我们还有一个多阶段 Dockerfile 的例子——这是一个用 Go 编写的 Web 应用。Go 是一种现代、跨平台的编程语言,可以编译成本地二进制文件。这意味着你可以编译你的应用以在任何平台上运行(Windows、Linux、Intel 或 Arm),编译输出是完整的应用程序。你不需要像 Java、.NET Core、Node.js 或 Python 那样安装单独的运行时,这使得 Docker 镜像非常小。

还有几种其他语言也可以编译成本地二进制文件——Rust 和 Swift 很受欢迎,但 Go 具有最广泛的平台支持,它也是云原生应用(Docker 本身是用 Go 编写的)非常流行的语言。在 Docker 中构建 Go 应用意味着使用类似于为 Java 应用使用的方法的多阶段 Dockerfile 方法,但有一些重要的区别。列表 A.4 显示了完整的 Dockerfile。

列表 A.4 从源代码构建 Go 应用的 Dockerfile

FROM diamol/golang AS builder

COPY main.go .
RUN go build -o /server

# app
FROM diamol/base

ENV IMAGE_API_URL="http://iotd/image" \
    ACCESS_API_URL="http://accesslog/access-log"
CMD ["/web/server"]

WORKDIR web
COPY index.html .
COPY --from=builder /server .
RUN chmod +x server

Go 编译成本地二进制文件,因此 Dockerfile 中的每个阶段都使用不同的基础镜像。构建阶段使用diamol/golang,其中安装了所有 Go 工具。Go 应用通常不获取依赖项,因此这个阶段直接构建应用程序(仅有一个代码文件,main.go)。最终的应用程序阶段使用最小镜像,它只包含最小的操作系统工具层,称为diamol/base

Dockerfile 捕获了一些配置设置作为环境变量,并指定启动命令为编译后的二进制文件。应用阶段通过从主机复制应用所服务的 HTML 文件和构建阶段的 Web 服务器二进制文件结束。在 Linux 中,二进制文件需要显式标记为可执行,这就是最终chmod命令的作用(在 Windows 上没有影响)。

现在试试吧 浏览到 Go 应用源代码并构建镜像:

cd ch04/exercises/image-gallery
docker image build -t image-gallery .

这次不会有太多的编译器输出,因为 Go 很安静,只有在失败时才写入日志。你可以在图 A.9 中看到我简化的输出。

图 A-9

图 A.9 在多阶段 Dockerfile 中构建 Go 应用

这个 Go 应用确实做了些有用的事情,但在运行它之前,看看输入和输出的镜像大小是值得的。

现在试试吧 比较 Go 应用镜像大小与 Go 工具集镜像:

docker image ls -f reference=diamol/golang -f reference=image-gallery

许多 Docker 命令允许您过滤输出。此命令列出所有镜像,并过滤输出以仅包括具有diamol/golangimage-gallery引用的镜像——引用实际上只是镜像名称。当你运行这个命令时,你会看到选择正确的 Dockerfile 阶段的基础镜像是多么重要:

REPOSITORY     TAG     IMAGE ID      CREATED         SIZE 
image-gallery  latest  b41869f5d153  20 minutes ago  25.3MB 
diamol/golang  latest  ad57f5c226fc  2 hours ago     774MB

在 Linux 上,安装了所有 Go 工具的镜像超过 770 MB;实际的 Go 应用程序镜像只有 25 MB。记住,这是虚拟镜像大小,所以很多层可以在不同的镜像之间共享。重要的节省并不是磁盘空间,而是最终镜像中不存在的所有软件。应用程序在运行时不需要任何 Go 工具。通过为应用程序使用最小的基础镜像,我们节省了近 750 MB 的软件,这大大减少了潜在攻击的表面积。

现在,你可以运行该应用。这总结了本章的工作,因为 Go 应用程序实际上使用了你构建的其他应用的 API。你应该确保那些容器正在运行,并且具有之前“试试看”练习中正确的名称。如果你运行 docker container ls,你应该看到本章的两个容器——名为 accesslog 的 Node.js 容器和名为 iotd 的 Java 容器。当你运行 Go 容器时,它将使用其他容器的 API。

现在试试它 运行 Go 应用程序镜像,发布主机端口并连接到 nat 网络:

docker container run -d -p 802:80 --network nat image-gallery

你可以浏览到 http://localhost:802,你会看到 NASA 的每日天文图片。图 A.10 显示了我运行容器时的图像。

图 A.10

图 A.10 Go 网络应用,显示从 Java API 获取的数据

目前你正在运行一个跨越三个容器的分布式应用。Go 网络应用调用 Java API 获取要显示的图像详情,然后调用 Node.js API 记录网站已被访问。你不需要为任何这些语言安装任何工具来构建和运行所有应用;你只需要源代码和 Docker。

多阶段 Dockerfile 使你的项目完全可移植。你可能现在使用 Jenkins 来构建你的应用,但你可以尝试 AppVeyor 的托管 CI 服务或 Azure DevOps,而无需编写任何新的管道代码——它们都支持 Docker,所以你的管道只是 docker image build

A.5 理解多阶段 Dockerfile

我们在本章中覆盖了很多内容,我将用一些关键点结束,以便你真正清楚地了解多阶段 Dockerfile 的工作原理,以及为什么在容器内构建你的应用是极其有用的。

第一点是关于标准化。我知道当你运行本章的练习时,你的构建将会成功,你的应用将会工作,因为你正在使用与我完全相同的工具集。无论你有什么操作系统,或者你的机器上安装了什么,所有的构建都在 Docker 容器中运行,容器镜像包含了所有正确的工具版本。在你的真实项目中,你会发现这极大地简化了新开发者的入职流程,消除了构建服务器的维护负担,并消除了用户拥有不同版本工具时可能出现的故障风险。

第二点是性能。多阶段构建中的每个阶段都有自己的缓存。Docker 会为每条指令在镜像层缓存中查找匹配项;如果没有找到,缓存就会损坏,并且所有其余的指令都将执行——但仅限于该阶段。下一个阶段将从缓存重新开始。你将花费时间仔细结构你的 Dockerfile,当你完成优化后,你会发现 90% 的构建步骤都使用了缓存。

最后一点是,多阶段 Dockerfile 允许你精细调整构建,使最终的应用程序镜像尽可能精简。这不仅仅是为了编译器——你需要用到的任何工具都可以在早期阶段隔离,因此工具本身不会出现在最终镜像中。一个好的例子是 curl——一个流行的命令行工具,你可以用它从互联网上下载内容。你可能需要它来下载你的应用程序需要的文件,但你可以在 Dockerfile 的早期阶段完成这个操作,这样 curl 本身就不会安装在你的应用程序镜像中。这可以降低镜像大小,意味着更快的启动时间,但也意味着你的应用程序镜像中可用的软件更少,这意味着攻击者有更少的潜在漏洞可以利用。

A.6 实验室

实验时间!你将把关于多阶段构建和优化 Dockerfile 的知识付诸实践。在本书的源代码中,你会在 ch04/lab 文件夹中找到一个起点。这是一个简单的 Go 网络服务器应用程序,它已经有一个 Dockerfile,因此你可以在 Docker 中构建和运行它。但 Dockerfile 非常需要优化,这就是你的任务。

这个实验室有具体的目标:

  • 首先,使用现有的 Dockerfile 构建一个镜像,然后优化 Dockerfile 以生成一个新的镜像。

  • 当前镜像在 Linux 上为 800 MB,在 Windows 上为 5.2 GB。你的优化镜像在 Linux 上应约为 15 MB,在 Windows 上约为 260 MB。

  • 如果你使用当前的 Dockerfile 更改 HTML 内容,构建将执行七个步骤。

  • 当你更改 HTML 时,你的优化 Dockerfile 应只执行一个步骤。

和往常一样,在本书的 GitHub 仓库中有示例解决方案。但这个实验室你真的应该尝试并抽出时间来做,因为优化 Dockerfile 是你在每个项目中都会用到的宝贵技能。如果你需要的话,我的解决方案在这里:github.com/sixeyed/diamol/blob/master/ch04/lab/Dockerfile.optimized

这次没有提示,尽管我可以这么说,这个示例应用程序看起来非常类似于你在本章中已经构建的一个。

附录 B. 通过容器化监控添加可观察性

自主应用程序会根据传入流量自动扩展和缩减,并在出现间歇性故障时自我修复。这听起来太好了,以至于可能不太真实——可能确实如此。如果你在构建 Docker 镜像时包含健康检查,容器平台可以为你完成很多操作工作,但你仍然需要持续的监控和警报,以便在事情变得糟糕时人类可以介入。如果你对你的容器化应用程序没有任何洞察,这将是你无法进入生产环境的头号障碍。

当你在容器中运行应用程序时,可观察性是软件景观中的一个关键部分——它告诉你应用程序在做什么以及它们的性能如何,并且可以帮助你定位问题的根源。在本章中,你将学习如何使用与 Docker 相结合的成熟监控方法:从你的应用程序容器中公开指标,并使用 Prometheus 收集它们,使用 Grafana 在用户友好的仪表板中可视化它们。这些工具是开源的,跨平台的,并且与你的应用程序一起在容器中运行。这意味着你可以在从开发到生产的每个环境中获得对应用程序性能的相同洞察。

本附录摘自 Elton Stoneman 所著的《Learn Docker in a Month of Lunches》(Manning,2020)的第九章,“通过容器化监控添加可观察性”。任何章节引用或对代码仓库的引用都指该书的相关章节或代码仓库。

B.1 容器化应用程序的监控堆栈

当应用程序在容器中运行时,监控方式有所不同。在传统环境中,你可能有一个监控仪表板显示服务器列表及其当前利用率——磁盘空间、内存、CPU——以及警报来告诉你是否有任何服务器过载并且可能停止响应。容器化应用程序更加动态——它们可能运行在数十或数百个短暂存在的容器中,这些容器由容器平台创建或删除。

你需要一个容器感知的监控方法,使用能够连接到容器平台进行发现并找到所有运行中的应用程序的工具,而无需静态的容器 IP 地址列表。Prometheus 是一个开源项目,正是这样做的。它是一个成熟的产品,由云原生计算基金会(Kubernetes 和 containerd 容器运行时的背后基金会)监督。Prometheus 在 Docker 容器中运行,因此你可以轻松地为你的应用程序添加监控堆栈。图 B.1 展示了该堆栈的外观。

图 B.1 监控容器化应用程序的监控堆栈

图 B.1 在容器中运行 Prometheus 以监控其他容器和 Docker 本身

Prometheus 为监控带来了一个非常重要的方面:一致性。您可以导出所有应用程序的相同类型的指标,因此您有一个标准的方式来监控它们,无论它们是 Windows 容器中的 .NET 应用还是 Linux 容器中的 Node.js 应用。您只需学习一种查询语言,就可以将其应用于整个应用程序堆栈。

使用 Prometheus 的另一个好理由是 Docker 引擎也可以以该格式导出指标,这使您能够深入了解容器平台正在发生的事情。您需要在 Docker 引擎配置中显式启用 Prometheus 指标。在 Windows 上,您可以直接在 C:\ProgramData\docker\config 中编辑 daemon.json 文件,或在 Linux 上的 /etc/docker。或者,在 Docker Desktop 上,您可以通过右键单击鲸鱼图标,选择设置,并在守护进程部分编辑配置。

现在尝试一下 打开您的配置设置并添加两个新值:

"metrics-addr" : "0.0.0.0:9323", 
"experimental": true 

这些设置启用了监控并在端口 9323 上发布指标。

您可以在图 B.2 中看到我的完整配置文件。

图 B.2 配置 Docker 引擎以导出 Prometheus 格式的指标

Docker 引擎指标目前是一个实验性功能,这意味着它提供的详细信息可能会改变。但它已经是一个实验性功能很长时间了,并且已经稳定。值得将其包含在您的仪表板中,因为它为系统的整体健康状况添加了另一层细节。现在您已经启用了指标,您可以浏览到 http://localhost:9323/metrics 并查看 Docker 提供的所有信息。图 B.3 显示了我的指标,包括 Docker 运行的机器信息以及 Docker 管理的容器信息。

图 B.3 Docker 捕获的样本指标并通过 HTTP API 暴露

此输出格式为 Prometheus。它是一种简单的基于文本的表示,其中每个指标都显示其名称和值,指标前有一些帮助文本说明指标是什么以及数据类型。这些基本的文本行是您容器监控解决方案的核心。每个组件都会暴露一个类似这样的端点,提供当前指标;当 Prometheus 收集它们时,会在数据中添加时间戳,并将它们与所有之前的收集存储在一起,因此您可以查询聚合数据或跟踪随时间的变化。

现在尝试一下 您可以在容器中运行 Prometheus 以读取 Docker 机器的指标,但首先您需要获取机器的 IP 地址。容器不知道它们运行的服务器的 IP 地址,因此您需要先找到它,并将其作为环境变量传递给容器:

# load your machine's IP address into a variable - on Windows:
$hostIP = $(Get-NetIPConfiguration | Where-Object {$_.IPv4DefaultGateway -ne $null }).IPv4Address.IPAddress

# on Linux:
hostIP=$(ip route get 1 | awk '{print $NF;exit}')

# and on Mac:
hostIP=$(ifconfig en0 | grep -e 'inet\s' | awk '{print $2}')

# pass your IP address as an environment variable for the container:
docker container run -e DOCKER_HOST=$hostIP -d -p 9090:9090 diamol/prometheus:2.13.1

diamol/prometheus Prometheus 镜像中的配置使用DOCKER_HOST IP 地址与你的主机机器通信并收集你在 Docker Engine 中配置的指标。通常情况下,你不需要从容器内部访问主机上的服务,如果你这样做,你通常会使用你的服务器名称,Docker 会找到 IP 地址。在一个开发环境中,这可能不起作用,但 IP 地址方法应该没问题。

Prometheus 现在正在运行。它执行几件事情:它运行一个计划任务从你的 Docker 主机拉取指标,它将这些指标值及其时间戳存储在其自己的数据库中,并且它有一个基本的 Web UI,你可以用它来导航指标。Prometheus UI 显示了 Docker 的/metrics端点的所有信息,你可以过滤指标并在表格或图形中显示它们。

现在尝试一下 浏览到 http://localhost:9090,你会看到 Prometheus 的 Web 界面。你可以通过浏览到状态 > 目标菜单选项来检查 Prometheus 是否可以访问指标。你的DOCKER_HOST状态应该是绿色的,这意味着 Prometheus 已经找到了它。

然后切换到“图形”菜单,你会看到一个下拉列表,显示 Prometheus 从 Docker 收集的所有可用指标。其中之一是engine_daemon_container_actions_seconds_sum,它记录了不同容器操作所花费的时间。选择该指标并点击执行,你的输出将类似于我的图 B.4,显示创建、删除和启动容器所需的时间。

图 B.4 Prometheus 有一个简单的 Web UI,你可以用它来查找指标和运行查询。

Prometheus UI 是一个简单的方式来查看正在收集的内容并运行一些查询。在指标周围看看,你会发现 Docker 记录了大量的信息点。有些是高级读数,如每个状态的容器数量和失败的检查数量;其他提供低级细节,如 Docker Engine 分配的内存量;还有一些是静态信息,如 Docker 可用的 CPU 数量。这些都是基础设施级别的指标,所有这些都可以包括在你的状态仪表板中。

你的应用程序将公开它们自己的指标,这些指标也会在不同级别记录详细信息。目标是每个容器都有一个指标端点,并且 Prometheus 定期从它们中收集指标。Prometheus 将存储足够的信息,让你构建一个仪表板,显示整个系统的整体健康状况。

B.2 从你的应用程序公开指标

我们已经查看 Docker Engine 公开的指标,因为这是一个开始使用 Prometheus 的简单方法。从每个应用程序容器中公开一组有用的指标需要更多的努力,因为你需要代码来捕获指标并为 Prometheus 提供 HTTP 端点。这不像听起来那么困难,因为所有主要编程语言都有 Prometheus 客户端库来为你做这件事。

在本章的代码中,我重新审视了 NASA 图片库应用,并为每个组件添加了 Prometheus 指标。我使用了 Java 和 Go 的官方 Prometheus 客户端,以及 Node.js 的社区客户端库。图 B.5 展示了每个应用程序容器现在都打包了一个 Prometheus 客户端,该客户端收集并公开指标。

图片 B-5

图 B.5 展示了你的应用中的 Prometheus 客户端库使指标端点在容器中可用。

从 Prometheus 客户端库收集的信息点是运行时级别的指标。它们提供了关于你的容器正在做什么以及它工作有多努力的关键信息,这些信息与应用程序运行时相关。Go 应用程序的指标包括活跃的 Goroutines 数量;Java 应用程序的指标包括 JVM 使用的内存。每个运行时都有自己的重要指标,客户端库在收集和导出这些指标方面做得很好。

现在试试看。本章的练习中有一个 Docker Compose 文件,它会启动一个带有每个容器中指标的图片库应用版本。使用该应用,然后浏览到其中一个指标端点:

cd ./ch09/exercises

# clear down existing containers:
docker container rm -f $(docker container ls -aq)

# create the nat network - if you've already created it 
# you'll get a warning which you can ignore:
docker network create nat

# start the project
docker-compose up -d

# browse to http://localhost:8010 to use the app

# then browse to http://localhost:8010/metrics

我的结果在图 B.6 中。这是 Go 前端 Web 应用程序的指标——不需要自定义代码来生成这些数据。你只需将 Go 客户端库添加到应用程序中并设置它,就可以免费获得所有这些数据。

图片 B-6

图片 B-6 Prometheus 关于 Go 运行时的指标,来自图片库的 Web 容器

如果你浏览到 http://localhost:8011/actuator/prometheus,你会看到 Java REST API 的类似指标。指标端点是文本的海洋,但所有关键数据点都在那里,可以构建一个仪表板,显示容器是否运行“过热”——如果它们正在使用大量的计算资源,如 CPU 时间、内存或处理器线程。

这些运行时指标是你在从 Docker 的基础设施指标之后想要查看的下一级详细信息,但这两个级别并没有告诉你整个故事。最终的数据点是应用程序指标,你明确捕获这些指标以记录关于应用程序的关键信息。这些指标可以是操作导向的,显示组件处理的事件数量或处理响应的平均时间。或者它们可以是业务导向的,显示当前活跃用户数量或注册新服务的人数。

Prometheus 客户端库也允许您记录这类指标,但您需要显式编写代码来捕获应用程序中的信息。这并不困难。列表 B.1 展示了一个使用 Node.js 库的示例,该示例位于图像库应用程序 access-log 组件的代码中。我不想向您展示一大堆代码,但随着您在容器方面进一步学习,您肯定会在 Prometheus 上花费更多的时间,而这个来自 server.js 文件的片段展示了几个关键点。

列表 B.1 在 Node.js 中声明和使用自定义 Prometheus 指标值

//declare custom metrics:
const accessCounter = new prom.Counter({
  name: "access_log_total",
  help: "Access Log - total log requests"
});

const clientIpGauge = new prom.Gauge({
  name: "access_client_ip_current",
  help: "Access Log - current unique IP addresses"
});

//and later, update the metrics values:
accessCounter.inc();
clientIpGauge.set(countOfIpAddresses);

在本章的源代码中,您将看到我是如何在用 Go 编写的 image-gallery 网络应用程序和用 Java 编写的 image-of-the-day REST API 中添加指标的。每个 Prometheus 客户端库的工作方式都不同。在 main.go 源文件中,我以类似于 Node.js 应用程序的方式初始化计数器和仪表,但随后使用来自客户端库的仪表化处理程序,而不是显式设置指标。Java 应用程序又有所不同——在 ImageController.java 中,我使用了 @Timed 属性并在源代码中增加了一个 registry.counter 对象。每个客户端库都以对语言最合理的方式工作。

Prometheus 中有不同的指标类型——我在这些应用程序中使用了最简单的类型:计数器和仪表。它们都是数值。计数器保持一个增加或保持不变的值,而仪表保持可以增加或减少的值。选择指标类型并在正确的时间设置其值取决于您或您的应用程序开发者;其余的由 Prometheus 和客户端库处理。

现在试试看 您已经从上一个练习中运行了图像库应用程序,因此这些指标已经被收集。向应用程序运行一些负载,然后浏览到 Node.js 应用程序的指标端点:

# loop to make 5 HTTP GET request - on Windows:
for ($i=1; $i -le 5; $i++) { iwr -useb http://localhost:8010 | Out-Null }

# or on Linux:
for i in {1..5}; do curl http://localhost:8010 > /dev/null; done

# now browse to http://localhost:8012/metrics 

您可以在图 B.7 中看到我的输出——我运行了更多的循环来发送流量。前两条记录显示了我的自定义指标,记录了接收到的访问请求数量和使用的总 IP 地址数。这些是简单的数据点(而 IP 计数实际上是假的),但它们起到了收集和展示指标的作用。Prometheus 允许您记录更复杂的指标类型,但即使使用简单的计数器和仪表,您也可以在应用程序中捕获详细的仪表化信息。

图片

图 B.7 包含自定义数据和 Node.js 运行时数据的指标端点

您捕获的内容取决于您的应用程序,但以下列表提供了一些有用的指南——您可以在月底准备为您的应用程序添加详细监控时返回这些指南。

  • 当您与外部系统通信时,记录调用花费的时间和响应是否成功——您将很快就能看到是否有其他系统正在减慢您的速度或破坏它。

  • 任何值得记录的事件都可能在指标中记录——与写入日志条目相比,在内存、磁盘和 CPU 上增加计数器可能更便宜,而且更容易可视化事件发生的频率。

  • 任何关于应用或用户行为,业务团队希望报告的细节都应该记录为指标——这样你就可以构建实时仪表板,而不是发送历史报告。

B.3 运行 Prometheus 容器以收集指标

Prometheus 使用拉模型来收集指标。它不是让其他系统发送数据给它,而是从这些系统中获取数据。它称之为 抓取,当你部署 Prometheus 时,你需要配置它要抓取的端点。在一个生产容器平台上,你可以配置 Prometheus,使其自动发现集群中的所有容器。在单个服务器的 Docker Compose 中,你使用一个简单的服务名称列表,Prometheus 通过 Docker 的 DNS 来查找容器。

列表 B.2 展示了我为 Prometheus 配置的抓取图像库应用中两个组件的配置。有一个 global 设置,它使用默认的 10 秒间隔进行抓取,然后为每个组件有一个 job。作业有一个名称,配置指定了指标端点的 URL 路径以及 Prometheus 将查询的目标列表。我这里使用了两种类型。首先,static_configs 指定了一个目标主机名,这对于单个容器来说是可以的。我还使用了 dns_sd_configs,这意味着 Prometheus 将使用 DNS 服务发现——这将找到为服务提供的多个容器,并且它支持大规模运行。

列表 B.2 Prometheus 用于抓取应用指标的配置

global:
  scrape_interval: 10s

scrape_configs:
  - job_name: "image-gallery"
    metrics_path: /metrics
    static_configs:
      - targets: ["image-gallery"]

  - job_name: "iotd-api"
    metrics_path: /actuator/prometheus
    static_configs:
      - targets: ["iotd"]

  - job_name: "access-log"
    metrics_path: /metrics
    dns_sd_configs:
      - names:
          - accesslog
        type: A
        port: 80

此配置将 Prometheus 设置为每 10 秒轮询一次所有容器。它将使用 DNS 获取容器 IP 地址,但对于 image-gallery,它只期望找到一个容器,所以如果你扩展该组件,你会得到意外的行为。如果 DNS 响应包含多个 IP 地址,Prometheus 总是使用列表中的第一个 IP 地址,所以当 Docker 对指标端点进行负载均衡时,你会从不同的容器中获得指标。accesslog 组件配置为支持多个 IP 地址,所以 Prometheus 将构建一个包含所有容器 IP 地址的列表,并按照相同的计划轮询它们。图 B.8 展示了抓取过程是如何运行的。

图片 B-8

图 B.8 Prometheus 在容器中运行,配置为从应用容器抓取指标

我为图像库应用构建了一个定制的 Prometheus Docker 镜像。它基于 Prometheus 团队在 Docker Hub 上发布的官方镜像,并复制了我的配置文件(你可以在本章源代码中找到 Dockerfile)。这种方法给我提供了一个预配置的 Prometheus 镜像,我可以无需任何额外配置即可运行,但如果需要,我可以在其他环境中覆盖配置文件。

当运行大量容器时,指标更有趣。我们可以将图像库应用的 Node.js 组件扩展到多个容器上运行,Prometheus 将从所有容器中抓取和收集指标。

现在尝试一下 该章节的练习文件夹中还有一个 Docker Compose 文件,它为 access-log 服务发布了一个随机端口,因此该服务可以大规模运行。运行三个实例并向网站发送更多负载:

docker-compose -f docker-compose-scale.yml up -d --scale accesslog=3

# loop to make 10 HTTP GET request - on Windows:
for ($i=1; $i -le 10; $i++) { iwr -useb http://localhost:8010 | Out-Null }

# or on Linux:
for i in {1..10}; do curl http://localhost:8010 > /dev/null; done

每次网站处理请求时都会调用 access-log 服务——运行该服务的有三个容器,因此调用应该在这所有容器之间进行负载均衡。我们如何检查负载均衡是否有效?该组件的指标包括一个标签,用于捕获发送指标的机器的主机名——在这种情况下是 Docker 容器 ID。打开 Prometheus UI 并检查 access-log 指标。你应该看到三组数据。

现在尝试一下 浏览到 http://localhost:9090/graph。在指标下拉菜单中,选择 access_log_total 并点击执行。

你会看到与我图 B.9 中类似的输出——每个容器都有一个指标值,标签包含主机名。每个容器的实际值将显示负载均衡的均匀程度。在理想情况下,这些数值应该是相等的,但由于存在许多网络因素(如 DNS 缓存和 HTTP 保持连接),这意味着如果你在单台机器上运行,你可能看不到这种情况。

图 B-9

图 B.9 处理指标可以用来验证请求是否正在负载均衡。

使用标签记录额外信息是 Prometheus 最强大的功能之一。它允许你在不同粒度级别上使用单个指标。目前你看到的是指标的原始数据,表格中每行显示每个容器的最新指标值。你可以使用 sum() 查询跨所有容器进行聚合,忽略单个标签并显示总合,你还可以在图中显示随时间增加的使用情况。

现在尝试一下 在 Prometheus UI 中,点击添加图形按钮以添加新的查询。在表达式文本框中,粘贴以下查询:

sum(access_log_total) without(hostname, instance)

点击执行,你会看到一个带有时间序列的折线图,这是 Prometheus 表示数据的方式——一组带有时间戳记录的指标。

在我添加新的图形之前,我向本地应用发送了一些更多的 HTTP 请求——你可以在图 B.10 中看到我的输出。

图 B-10

图 B.10 将指标聚合,从所有容器中汇总值并显示结果图

sum() 查询是用 Prometheus 自有的查询语言 PromQL 编写的。它是一种功能强大的语言,包含统计函数,允许你查询随时间的变化和变化率,并且你可以添加子查询来关联不同的指标。但是,你不需要深入到任何这种复杂性中就可以构建有用的仪表板。Prometheus 的格式结构非常良好,你可以通过简单的查询来可视化关键指标。你可以使用标签来过滤值,并汇总结果以进行聚合,仅这些功能就能为你提供一个有用的仪表板。

图片 B-11

图 B.11 一个简单的 Prometheus 查询。你不需要学习比这更多的 PromQL。

图 B.11 展示了一个典型的查询,它将被用于仪表板。这个查询聚合了所有 image_gallery_request 指标的值,过滤出响应代码为 200 的情况,并且没有使用 instance 标签进行汇总,因此我们将从所有容器中获取指标。结果将是所有运行图像库网络应用程序的容器发送的 200 个“OK”响应的总数。

Prometheus UI 适用于检查你的配置,验证所有抓取目标是否可访问,以及制定查询。但它并不是一个仪表板——这正是 Grafana 的作用所在。

B.4 运行 Grafana 容器以可视化指标

在本章中,我们涵盖了大量的内容,因为监控是容器的一个核心主题,但我们进展很快,因为更详细的内容都是非常依赖于应用程序的。你需要捕获哪些指标将取决于你的业务和运营需求,以及你如何捕获它们将取决于你使用的应用程序运行时以及该运行时的 Prometheus 客户端库的机制。

一旦你的数据在 Prometheus 中,事情就变得简单了——它成为所有应用程序的相当标准的做法。你将使用 Prometheus UI 来导航你记录的指标并制定查询以获取你想要看到的数据。然后你将运行 Grafana 并将这些查询连接到仪表板。每个数据点都以用户友好的可视化形式出现,整个仪表板展示了你的应用程序正在发生的事情。

我们一直在本章中致力于构建图像库应用程序的 Grafana 仪表板,图 B.12 展示了最终结果。这是一种非常整洁的方式来展示所有应用程序组件和 Docker 运行时的核心信息。这些查询也构建来支持扩展,因此相同的仪表板可以在生产集群中使用。

图片 B-12

图 B.12 应用程序的 Grafana 仪表板。看起来很复杂,但实际上构建起来相当简单。

Grafana 仪表板在应用程序的许多不同级别传达关键信息。它看起来很复杂,但每个可视化都由一个单一的 PromQL 查询提供支持,并且没有任何查询比过滤和聚合更复杂。图 B.12 的缩小视图没有给出完整的画面,但我已经将仪表板打包到一个自定义的 Grafana 图像中,这样您就可以在容器中运行它并探索。

现在试试看 您需要再次捕获计算机的 IP 地址,这次作为 Compose 文件查找并注入 Prometheus 容器中的环境变量。然后使用 Docker Compose 运行应用程序并生成一些负载:

# load your machine's IP address into an environment variable - on Windows:
$env:HOST_IP = $(Get-NetIPConfiguration | Where-Object {$_.IPv4DefaultGateway -ne $null }).IPv4Address.IPAddress

# on Linux:
export HOST_IP=$(ip route get 1 | awk '{print $NF;exit}')

# run the app with a Compose file which includes Grafana:
docker-compose -f ./docker-compose-with-grafana.yml up -d --scale accesslog=3

# now send in some load to prime the metrics - on Windows:
for ($i=1; $i -le 20; $i++) { iwr -useb http://localhost:8010 | Out-Null }

# or on Linux:
for i in {1..20}; do curl http://localhost:8010 > /dev/null; done

# and browse to http://localhost:3000

Grafana 使用端口 3000 用于 Web UI。当您首次浏览时,您需要登录——凭据是用户名 admin,密码 admin。您将在第一次登录时被要求更改管理员密码,但如果您点击跳过,我不会评判您。当 UI 加载时,您将进入您的“主页”仪表板——点击左上角的“主页”链接,您将看到图 B.13 中的仪表板列表。点击图像库以加载应用程序仪表板。

图片 B-13

图 B.13 在 Grafana 中导航仪表板——最近使用的文件夹显示在此处

我的仪表板是一个合理的生产系统设置。您需要一些关键数据点,以确保您正在监控正确的事情——Google 在 网站可靠性工程 书籍中讨论了这一点 (mng.bz/EdZj)。他们的重点是延迟、流量、错误和饱和度,他们称之为“黄金信号”。

我将详细说明我的第一组可视化,以便您可以看到一个智能仪表板可以从基本的查询和正确的可视化选择中构建。图 B.14 显示了图像库 Web UI 的指标行——我将这一行分割开来以便更容易查看,但在仪表板上这些指标显示在同一行。

图片 B-14

图 B.14 更仔细地查看应用程序仪表板以及可视化如何与黄金信号相关

这里有四个指标显示了系统被使用的程度以及系统为了支持这种使用水平所付出的努力:

  • HTTP 200 响应——这是网站随时间发送的 HTTP “OK” 响应的简单计数。PromQL 查询是对应用程序的计数器指标的总和:sum(image_gallery_requests_total{code="200"}) without(instance)。我可以添加一个类似的图表,通过查询过滤 code="500" 来显示错误数量。

  • 飞行请求——这显示了在任何给定时间点的活动请求数量。这是一个 Prometheus 仪表,因此它可以上升或下降。对此没有过滤器,图表将显示所有容器中的总数,因此查询是另一个求和:sum(image_gallery_in_flight_requests) without(instance)

  • 内存使用量——这显示了图像库容器使用的系统内存量。这是一个柱状图,对于这类数据来说更容易观察;当我扩展 Web 组件时,它将显示每个容器的条形图。PromQL 查询根据作业名称进行筛选:go_memstats_stack_inuse_bytes{job="image-gallery"}。我需要这个筛选器,因为这是一个标准的 Go 指标,而 Docker Engine 作业返回的指标具有相同的名称。

  • 活跃的 Goroutines——这是一个粗略的指标,表明组件工作有多努力——Goroutine 是 Go 中的工作单元,可以并发运行多个。这个图表将显示 Web 组件是否突然出现处理活动的峰值。这是另一个标准的 Go 指标,所以 PromQL 查询从 Web 作业筛选统计信息并求和:sum(go_goroutines{job="image-gallery"}) without(instance)

仪表板其他行中的可视化都使用类似的查询。不需要复杂的 PromQL——选择正确的指标来显示以及正确的可视化方式来展示它们,这才是你真正需要的。

在这些可视化中,实际值不如趋势有用。我的 Web 应用平均使用 200 MB 内存或 800 MB 实际上并不重要——重要的是当出现突然的峰值偏离正常情况时。组件的指标集应该帮助你快速看到异常并找到相关性。如果错误响应的图表呈上升趋势,并且每几秒钟活跃的 Goroutines 数量翻倍,那么很明显有问题——组件可能已饱和,因此你可能需要通过增加更多容器来扩展以处理负载。

Grafana 是一个非常强大的工具,但使用起来非常简单。它是现代应用中最受欢迎的仪表板系统,因此值得学习——它可以查询许多不同的数据源,并且可以向不同的系统发送警报。构建仪表板与编辑现有仪表板相同——你可以添加或编辑可视化(称为面板),调整大小并移动它们,然后将你的仪表板保存到文件中。

现在试试看 Google SRE 方法认为 HTTP 错误计数是一个核心指标,但这个指标在仪表板中缺失,所以我们现在将其添加到图像库行。如果你还没有运行整个图像库应用,请重新运行它,浏览到 Grafana 的 http://locahost:3000,并使用用户名admin和密码admin登录。

打开图像库仪表板,点击屏幕右上角的添加面板图标——如图 B.15 所示,它是一个带有加号的柱状图。

图 B-15

图 B.15 Grafana 工具栏,用于添加面板、选择时间段和保存仪表板

现在点击新面板窗口中的添加查询,你将看到一个屏幕,你可以捕捉到可视化的所有细节。选择 Prometheus 作为查询的数据源,并在指标字段粘贴以下 PromQL 表达式:

sum(image_gallery_requests_total{code="500"}) without(instance)

您的面板应该看起来像图 B.16 中的我的那样。图像库应用程序大约有 10%的时间会返回错误响应,所以如果您发出足够的请求,您会在图表中看到一些错误。

按下 Esc 键返回主仪表板。

图 B-16

图 B.16 向 Grafana 仪表板添加新面板以显示 HTTP 错误

您可以通过拖动底右角来调整面板大小,通过拖动标题来移动它们。当仪表板看起来符合您的要求时,您可以从工具面板中点击“共享仪表板”图标(再次查看图 B.15),在那里您可以选择将仪表板导出为 JSON 文件。

使用 Grafana 的最终步骤是打包您自己的 Docker 镜像,该镜像已经配置了 Prometheus 作为数据源和应用程序仪表板。我已经为diamol/ch09-grafana镜像做了这件事。列表 B.3 显示了完整的 Dockerfile。

列表 B.3 打包自定义 Grafana 镜像的 Dockerfile

FROM diamol/grafana:6.4.3

COPY datasource-prometheus.yaml ${GF_PATHS_PROVISIONING}/datasources/
COPY dashboard-provider.yaml ${GF_PATHS_PROVISIONING}/dashboards/
COPY dashboard.json /var/lib/grafana/dashboards/

镜像从一个特定的 Grafana 版本开始,然后只是复制一组 YAML 和 JSON 文件。Grafana 遵循我在本书中已经推广的配置模式——内置了一些默认配置,但您可以应用自己的配置。当容器启动时,Grafana 会在特定文件夹中查找文件,并应用它找到的任何配置文件。YAML 文件设置 Prometheus 连接并加载位于/var/lib/Grafana/dashboards文件夹中的任何仪表板。最后一行将我的仪表板 JSON 复制到该文件夹,因此当容器启动时它会加载。

您可以使用 Grafana 配置进行更多操作,您还可以使用 API 创建用户并设置他们的偏好。构建一个包含多个仪表板和具有访问所有这些仪表板权限的只读用户(这些仪表板可以组合成一个 Grafana 播放列表)的 Grafana 镜像并不需要做太多工作。然后您可以在办公室的大屏幕上浏览 Grafana,并自动循环显示所有仪表板。

B.5 理解可观察性的级别

当您从简单的概念验证容器转移到准备生产时,可观察性是一个关键要求。但我在本章引入 Prometheus 和 Grafana 的另一个非常好的原因是:学习 Docker 不仅仅是关于 Dockerfile 和 Docker Compose 文件的机制。Docker 的魔力之一是围绕容器成长起来的巨大生态系统以及围绕该生态系统出现的模式。

当容器最初变得流行时,监控确实是个头疼的问题。我那时的生产发布与今天一样容易构建和部署,但我在应用程序运行时没有洞察力。我必须依赖外部服务如 Pingdom 来检查我的 API 是否仍然可用,并依赖用户报告来确保应用程序运行正确。今天监控容器的做法是一条经过验证且值得信赖的道路。我们在本章中遵循了这条道路,图 B.17 总结了这种方法。

图 B-17

图 B.17 容器化应用程序的监控架构-Prometheus 位于中心。

我已经为图像库应用程序走过了单个仪表板,这是应用程序的整体视图。在生产环境中,你会有额外的仪表板,深入到更详细的层次。会有一个基础设施仪表板显示所有服务器的可用磁盘空间、可用 CPU 和内存以及网络饱和度。每个组件可能都有自己的仪表板,显示额外的信息,例如,为 Web 应用程序的每一页或每个 API 端点提供服务的响应时间分解。

摘要仪表板是关键。你应该能够将应用程序指标中的所有最重要的数据点汇总到一个屏幕上,这样你就可以一眼看出是否有问题,并在问题恶化之前采取规避措施。

B.6 实验室

本章为图像库应用程序添加了监控,这个实验室要求你对待办事项列表应用程序做同样的事情。你不需要深入研究源代码——我已经构建了一个包含 Prometheus 指标的新版本的应用程序镜像。从 diamol/ch09-todo-list 运行一个容器,浏览到应用程序,添加一些项目,你将看到 /metrics URL 上可用的指标。对于实验室,你希望将那个应用程序带到与图像库相同的位置:

  • 编写一个 Docker Compose 文件,你可以使用它来运行应用程序,它还会启动一个 Prometheus 容器和 Grafana 容器。

  • Prometheus 容器应该已经配置为从待办事项列表应用程序抓取指标。

  • Grafana 容器应该配置了一个仪表板,以显示应用程序的三个关键指标:创建的任务数量、处理的 HTTP 请求总数以及当前正在处理的 HTTP 请求数量。

这听起来像是一大堆工作,但实际上并不是——本章的练习涵盖了所有细节。这是一个很好的实验室,因为它将为你提供与新的应用程序一起处理指标的经验。

和往常一样,你可以在 GitHub 上找到我的解决方案,以及我最终仪表板的图形:github.com/sixeyed/diamol/blob/master/ch09/lab/README.md

附录 C. 容器中的应用配置管理

应用程序需要从它们运行的运行环境中加载其配置,这通常是环境变量和从磁盘读取的文件的组合。Docker 为在容器中运行的应用程序创建该环境,它可以设置环境变量并从许多不同的来源构建文件系统。所有这些部件都是为了帮助您为应用程序构建一个灵活的配置方法,因此当您部署到生产环境时,您使用的是通过了所有测试阶段的相同镜像。您只需做一些工作来将这些部件组合在一起,设置您的应用程序以从多个位置合并配置值。

本章将通过.NET Core、Java、Go 和 Node.js 中的示例,向您介绍推荐的方法(以及一些替代方案)。这里的一些工作位于开发空间中,引入库以提供配置管理,其余部分位于开发和运维之间的灰色区域,该区域依赖于沟通,以便双方都了解配置模型的工作方式。

本附录摘自 Elton Stoneman 所著的《Learn Docker in a Month of Lunches》第十八章“容器中的应用配置管理”(Manning, 2020)。任何章节引用或代码仓库引用均指该书的相关章节或代码仓库。

C.1 应用配置的多层方法

您的配置模型应反映您存储的数据的结构,这通常是以下三种类型之一:

  • 版本级别的设置,对于给定版本的所有环境都是相同的

  • 环境级别的设置,对于每个环境都是不同的

  • 特性级别的设置,可用于在版本之间更改行为

其中一些是相当静态的,一些是动态的,具有一组已知的变量,而其他一些是动态的,具有一组未知的变量。图 C.1 展示了某些示例配置设置以及它们可以从环境中读取的位置。

图 C-1

图 C.1 从镜像、文件系统和环境变量中获取设置的配置层次结构

我们将使用的第一个示例是 Node.js,以及一个流行的配置管理库 node-config。该库允许您从层次结构中的多个文件位置读取配置,并用环境变量覆盖它们。本章练习中的 access-log 示例应用使用了 node-config 库,并设置了两个目录来读取配置文件

  • config—这将与默认设置一起打包到 Docker 镜像中。

  • config-override—该镜像中不存在,但可以从卷、配置对象或密钥中在容器文件系统中配置。

现在尝试一下 运行带有默认配置的示例应用,然后是带有开发环境覆盖文件的相同镜像:

cd ch18/exercises/access-log

# run a container with the default config in the image:
docker container run -d -p 8080:80 diamol/ch18-access-log

# run a container loading a local config file override:
docker container run -d -p 8081:80 -v "$(pwd)/config/dev:/app/config-override" diamol/ch18-access-log

# check the config APIs in each container:
curl http://localhost:8080/config
curl http://localhost:8081/config

第一个容器仅使用图像中打包的默认配置文件——该文件指定了发布周期名称(19.12)并设置了要启用的 Prometheus 指标。环境名称有一个UNKNOWN设置——如果你看到这个设置,你就知道环境级别的配置设置还没有被正确应用。第二个容器将本地配置目录作为卷加载到应用程序预期的位置以查找覆盖——它设置了环境名称并将指标功能关闭。当你调用配置 API 时,你会看到来自同一图像的容器应用了不同的设置——我的设置在图 C.2 中。

图 C-2

图 C.2 使用卷、配置对象或秘密合并配置文件很简单。

从应用程序代码中已知路径加载配置覆盖,让你可以从任何来源提供它们到容器中。我正在使用本地绑定挂载,但源可以是配置对象或存储在容器集群中的秘密,行为将是相同的。这个模式有一个细微差别——你的配置目标可以是特定的文件路径或目录。目录目标更灵活(Windows 容器不支持从单个文件加载卷),但源文件名需要与应用程序期望的配置文件名匹配。在这个例子中,绑定源是目录 config/dev,它包含一个文件——容器看到 /app/config-override/local.json,这是它查找覆盖的地方。

节点配置包还可以从环境变量中加载设置,并且它们会覆盖从文件层次结构加载的任何设置。这是在《十二要素应用》中推荐的方法(12factor.net)——一种现代的应用架构风格,其中环境变量始终优先于其他配置源。这是一个有用的方法,有助于你养成容器是瞬时的思维模式,因为更改环境变量以设置应用程序配置意味着替换容器。Node-config 有一个稍微不同寻常的实现:不是将单个设置指定为环境变量,你需要以 JSON 格式的字符串形式在环境变量中提供设置。

现在试试看 运行第三个版本的访问日志容器,以开发模式运行但启用指标。使用卷挂载加载开发配置,并使用环境变量覆盖指标设置:

cd ch18/exercises/access-log

# run a container with an override file and an environment variable:
docker container run -d -p 8082:80 -v "$(pwd)/config/dev:/app/config-override" -e NODE_CONFIG='{\"metrics\": {\"enabled\":\"true\"}}' diamol/ch18-access-log

# check the config:
curl http://localhost:8082/config

第三个容器合并了来自镜像中默认文件、卷中的本地配置覆盖文件和特定环境变量设置的配置。这是一个构建配置以使开发者工作流程顺利运行的优秀示例。开发者可以运行默认设置而不启用度量(这将节省 CPU 周期和内存),但当他们需要为某些调试打开度量时,他们可以使用相同的镜像和环境变量切换来完成。图 C.3 显示了我的输出。

图 C.3 从环境变量合并配置使得更改特定功能变得容易。

这是您应该在所有应用程序中应用的配置核心模式。从这个例子中,您可以清楚地看到模式,但细节很重要,这就是在交付和部署之间知识可能崩溃的灰色区域。访问日志应用程序允许您使用新的配置文件覆盖默认配置文件,但该目标文件必须位于特定位置。您还可以使用环境变量覆盖所有文件设置,但环境变量需要是 JSON 格式。最终,这将在您用于部署的 YAML 文件中记录,但您需要意识到这种模式有可能出错。一种替代方法可以消除这种风险,但代价是使配置管理变得不那么灵活。

C.2 为每个环境打包配置

许多应用程序框架支持一种配置管理系统,其中您将部署中每个环境的所有配置文件捆绑在一起,在运行时设置单个值以指定正在运行的环境名称。应用程序平台加载与匹配环境名称的配置文件,您的应用程序就完全配置好了。.NET Core 通过其默认配置提供程序设置来实现这一点,配置设置从以下来源合并:

  • appsettings.json—所有环境的默认值

  • appsettings.{Environment}.json—指定环境的覆盖

  • 环境变量—用于指定环境名称,以及设置覆盖

本章介绍了一种新的待办事项列表应用程序版本,该版本采用将所有配置文件打包到 Docker 镜像中的方法。您使用特定的环境变量来提供当前环境名称,该名称在加载其他配置文件之前被加载。

现在尝试一下 运行默认配置的待办事项列表应用程序,默认配置设置为环境名称开发,然后使用测试环境设置:

# run the to-do list app with default config:
docker container run -d -p 8083:80 diamol/ch18-todo-list

# run the app with the config for the test environment:
docker container run -d -p 8084:80 -e DOTNET_ENVIRONMENT=Test diamol/ch18-todo-list

两个容器是从相同的镜像运行的,但加载不同的配置文件。在镜像内部,有针对开发、测试和生产环境的配置文件。第一个容器将核心 appsettings.jsonappsettings.Development.json 合并——它以开发模式运行,因为在 Dockerfile 中将 Development 设置为默认环境。第二个容器将 appsettings.jsonappsettings.Test.json 合并。这两个环境配置文件已经存在于 Docker 镜像中,因此不需要挂载外部源以获取新的配置。浏览到 http://localhost:8083/diagnostics 以查看开发配置,并浏览到 http://localhost:8084/diagnostics 以查看测试版本。我的输出在图 C.4 中。

图片 C-4

图 C.4 将每个环境的配置文件打包到镜像中,使得切换环境变得容易。

如果你有一个单独的系统来管理你的配置文件和源代码,这种方法可以很好地工作。CI/CD 管道可以将配置文件作为构建的一部分带入 Docker 镜像,这样你就可以将配置管理从开发中分离出来。缺点是,你仍然不能打包每个设置,因为你需要将机密信息从 Docker 镜像中排除。你需要有一个多层次的安全方法,并假设你的注册表可能会被攻破——在这种情况下,你不想让某人找到你镜像中的所有密码和 API 密钥。

如果你喜欢这种方法,你仍然需要允许覆盖文件和最终的环境变量覆盖。待办事项列表应用程序就是这样做的,如果存在,它会从名为 config-overrides 的文件夹中加载文件,并使用 .NET Core 的标准方法最后加载环境变量。这让你能够做些有用的事情,比如如果你正在尝试复制一个问题,可以在本地运行生产环境,但覆盖环境设置以使用数据库文件而不是远程数据库服务器。

现在尝试一下 尽管所有环境配置都打包在应用程序中,待办事项列表应用程序仍然支持配置覆盖。如果你以生产模式运行,应用程序会失败,因为它期望找到一个数据库服务器,但你可以使用覆盖文件以使用数据库文件而不是数据库服务器来以生产模式运行:

cd ch18/exercises/todo-list

docker container run -d -p 8085:80 -e DOTNET_ENVIRONMENT=Production -v "$(pwd)/config/prod-local:/app/config-override" diamol/ch18-todo-list

你可以浏览到 http://localhost:8085/diagnostics 并看到应用程序正在以生产模式运行,但配置文件覆盖更改了数据库设置,因此应用程序仍然可以在不运行 Postgres 容器的情况下工作。我的输出在图 C.5 中。

图片 C-5

图 C.5 选择运行的环境仍然应该支持来自附加文件的配置覆盖。

此容器将默认的appsettings.json文件与prod-local文件夹中的环境文件appsettings.Production.json和重写文件local.json合并。设置类似于 Node.js 示例,因此在文件夹和文件名方面有一些一致性,但.NET Core 在设置环境变量重写方面采取了不同的方法。在 node-config 中,你通过将 JSON 字符串作为环境变量传递来覆盖设置,但在.NET Core 中,你指定单个设置作为环境变量。

现在试试看 运行与生产相同的本地版本,但通过用环境变量覆盖该设置来使用自定义发布名称:

# run the container with a bind mount and a custom environment variable:
docker container run -d -p 8086:80 -e DOTNET_ENVIRONMENT=Production -e release=CUSTOM -v "$(pwd)/config/prod-local:/app/config-override" diamol/ch18-todo-list

浏览到 http://localhost:8086/diagnostics,你会看到来自环境变量的自定义发布名称。我的输出在图 C.6 中。

图 C-6

图 C.6 配置层次结构覆盖了任何配置文件中的环境变量值。

我必须说我不喜欢这种打包多个配置文件的方式,尽管这在许多应用平台上是一种常见的做法。存在一种风险,你可能会在镜像中包含一些你认为不敏感的配置设置,但你的安全团队可能不同意。服务器名称、URL、文件路径、日志级别,甚至缓存大小都可能是有意攻击你系统的人有用的信息。在你将所有机密设置移动到从运行时应用的重写文件之前,这些打包的环境文件中可能已经所剩无几了。我也不喜欢这种分割,其中一些设置在源代码控制中管理,而其他设置在配置管理系统中。

容器的美丽之处在于你可以遵循你喜欢的任何模式,所以不要让我为你做决定。某些方法更适合你的组织和技术堆栈。如果你要处理多个堆栈,事情也会变得更加复杂——你将在下一个使用 Go 应用的示例中看到这一点。

C.3 从运行时加载配置

Go 有一个流行的配置模块叫做 Viper,它提供了与.NET Core 库或 node-config 类似的许多功能。你将模块添加到你的包列表中,并在你的应用程序代码中指定配置目录的路径以及你是否希望环境变量用来覆盖配置文件。我已经将它添加到本章的图像库应用程序中,使用与其他示例类似的层次结构:

  • 首先从config目录加载文件,该目录在 Docker 镜像中已填充。

  • 特定于环境的文件从config-override目录加载,该目录在镜像中为空,可以是容器文件系统挂载的目标。

  • 环境变量覆盖文件设置。

Viper 支持比其他示例更广泛的配置文件语言集。您可以使用 JSON 或 YAML,但在 Go 世界中流行的格式是 TOML(以创建者 Tom Preston-Werner 命名)。TOML 非常适合配置文件,因为它可以轻松地映射到代码中的字典,并且比 JSON 或 YAML 更容易阅读。表 C.1 显示了图像库应用程序的 TOML 配置。

表 C.1 TOML 格式使得配置文件易于管理

release = "19.12"
environment = "UNKNOWN"

[metrics]
enabled = true

[apis]

 [apis.image]
 url = "http://iotd/image"

 [apis.access]
 url = "http://accesslog/access-log"

您会在许多云原生项目中看到 TOML 的使用,因为它比替代方案容易得多。如果您可以选择格式,TOML 值得考虑,因为“易于阅读”也意味着易于调试,并且易于在合并工具中查看版本之间的差异。除了文件格式之外,此示例与 Node.js 应用程序以相同的方式工作,默认的config.toml文件打包到 Docker 镜像中。

现在尝试一下:运行应用程序,无需任何额外的配置设置,以检查默认值:

# run the container:
docker container run -d -p 8086:80 diamol/ch18-image-gallery

# check the config API:
curl http://localhost:8086/config

当您运行此练习时,您将看到当前应用程序配置,所有这些配置都来自默认的 TOML 文件。我的输出在图 C.7 中,显示了发布周期和应用程序所消耗的 API 的默认 URL。

图片

图 C.7 您可以使用默认设置打包应用程序,这些设置可以工作,但不是完整的开发环境。

输出来自一个配置 API,该 API 返回当前配置设置的 JSON。当您有多个配置源时,配置 API 是您应用程序中的一个非常有用的功能;它使得调试配置问题变得容易得多,但您需要保护这些数据。如果任何人尝试浏览到/config都可以公开读取机密设置,那么使用机密设置来保护机密设置就没有意义,所以如果您要添加配置 API,您需要做三件事:

  • 不要只发布整个配置;要有选择性,永远不要包含机密信息。

  • 保护端点,以确保只有授权用户可以访问它。

  • 将配置 API 制作成一个可以通过配置启用的功能。

图像库应用程序采用与分层配置模型略有不同的方法——默认设置保存在图像中,但不是针对任何特定环境。预期是每个环境都将指定自己的附加配置文件,该文件扩展或覆盖默认文件中的设置,以设置完整的开发环境。

现在尝试一下:再次运行相同的应用程序,使用覆盖文件来构建完整的开发环境:

cd ch18/exercises/image-gallery

# run the container with a bind mount to the local config directory:
docker container run -d -p 8087:80 -v "$(pwd)/config/dev:/app/config-override" diamol/ch18-image-gallery

# check config again:
curl http://localhost:8087/config

图 C.8 我在图 C.8 中的输出显示,应用程序现在已完全配置为开发环境,将镜像中的发布级别配置文件与环境覆盖文件合并。

图片

图 C.8 Go Viper 模块以与 node-config 包相同的方式合并配置文件。

展示所有这些关于配置主题的细微差异并不仅仅是为了填补章节内容。当组织采用 Docker 时,他们往往会发现使用速度加快,很快就会有大量应用程序在容器中运行,每个应用程序都有自己的配置观点。由于应用程序平台在提供的功能和期望的约定上有所不同,因此必然会发生许多这样的小差异。您可以在高层次上应用标准——镜像必须包含默认配置,并且必须支持文件和环境变量覆盖——但是配置文件和环境变量格式的细节将难以标准化。

我们将在最后一个 Go 应用程序的例子中看到这一点。Viper 模块支持使用环境变量覆盖配置文件中的设置,但与 node-config 和.NET Core 的约定不同。

现在尝试一下:使用环境变量覆盖运行容器。这个应用程序中的配置模型只使用以IG为前缀的环境变量:

cd ch18/exercises/image-gallery

# run the container with config override and an environment variable:
docker container run -d -p 8088:80 -v "$(pwd)/config/dev:/app/config-override" -e IG_METRICS.ENABLED=TRUE diamol/ch18-image-gallery

# check the config:
curl http://localhost:8088/config

Viper 的约定是,你应该在环境变量名称前加上前缀,以避免与其他环境变量冲突。在这个应用程序中,前缀是IG,后面跟着一个下划线,然后是配置设置名称的点表示法(因此IG_METRICS.ENABLED与 TOML 文件中metrics组中的enabled值匹配)。您可以从我的输出中看到,图 C.9 中的这种设置在默认设置之上添加了开发环境,但随后覆盖了指标设置以启用 Prometheus 指标。

图 C.9 所有示例应用程序都支持配置环境变量,但有一些小的差异。

我们已经用三个不同的应用程序走过了配置建模,我们有三种略有不同的方法。这些差异是可控的,并且很容易在应用程序清单文件中记录下来,并且实际上它们不会影响您构建镜像或运行容器的方式。在本章中,我们将查看一个最后的例子,它采用相同的配置模型并将其应用于没有漂亮的新配置库的应用程序,因此需要做一些额外的工作来使其表现得像现代应用程序。

C.4 以与新型应用程序相同的方式配置传统应用程序

传统应用程序对配置有自己的想法,通常不涉及环境变量或文件合并。例如,Windows 上的.NET Framework 应用程序——它们期望在特定位置有 XML 配置文件。它们不喜欢在应用程序根文件夹外寻找文件,并且根本不查看环境变量。您仍然可以使用相同的配置方法来处理这些应用程序,但您需要在 Dockerfile 中做一些额外的工作。

这里采用的方法是将实用应用程序或脚本集打包,将容器环境中的配置设置转换为应用程序期望的配置模型。具体的实现将取决于你的应用程序框架以及它如何使用配置文件,但逻辑可能如下所示:

  1. 从容器中指定的源文件读取配置覆盖设置。

  2. 从环境变量中读取覆盖设置。

  3. 合并两组覆盖设置,使环境变量具有优先权。

  4. 将合并后的覆盖设置写入容器中指定的目标文件。

在本章的练习中,有一个使用此方法的每日图像 Java API 的更新版本。它实际上不是一个遗留应用程序,但我已经按照遗留模式构建了镜像,好像应用程序不能使用正常的容器配置选项。有一个在启动时运行的实用程序来设置配置,所以尽管内部配置机制不同,用户仍然可以像其他示例一样配置容器。

现在尝试运行“遗留”应用程序,使用默认配置设置和文件覆盖:

cd ch18/exercises/image-of-the-day

# run a container with default configuration:
docker container run -d -p 8089:80 diamol/ch18-image-of-the-day

# run with a config override file in a bind mount:
docker container run -d -p 8090:80 -v "$(pwd)/config/dev:/config-override"
                     -e CONFIG_SOURCE_PATH="/config-override/application.properties" diamol/ch18-image-of-the-day

# check the config settings:
curl http://localhost:8089/config
curl http://localhost:8090/config

用户体验与其他应用程序非常相似——挂载带有环境覆盖文件的卷(源可以是配置对象或密钥)——但你必须另外指定覆盖文件的位置在环境变量中,这样启动实用程序就知道在哪里查找。你将在输出中看到,镜像中的默认配置指定了发布周期,但没有指定环境——这将在第二个容器中的覆盖文件中合并。我的输出在图 C.10 中。

图片 C-10

图 C.10 此应用程序有一个实用程序来引导配置模型,但用户体验保持不变。

魔法在这里发生在一个简单的 Java 实用应用程序中,它与应用程序的其他部分一起编译和打包在同一个多阶段构建中。列表 C.2 显示了构建实用程序并将其设置为启动时运行的 Dockerfile 的关键部分。

列表 C.2 在 Dockerfile 中构建和使用配置加载实用程序

FROM diamol/maven AS builder
# ...
RUN mvn package

# config util
FROM diamol/maven as utility-builder
WORKDIR /usr/src/utilities
COPY ./src/utilities/ConfigLoader.java .
RUN javac ConfigLoader.java

# app
FROM diamol/openjdk
ENV CONFIG_SOURCE_PATH="" \
    CONFIG_TARGET_PATH="/app/config/application.properties"

CMD java ConfigLoader && \
    java -jar /app/iotd-service-0.1.0.jar

WORKDIR /app
COPY --from=utility-builder /usr/src/utilities/ConfigLoader.class .
COPY --from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar .

这里的重要收获是你可以扩展你的 Docker 镜像,使旧应用程序的行为与新应用程序相同。你控制启动逻辑,因此可以在启动实际应用程序之前运行所需的任何步骤。当你这样做时,你增加了容器启动和应用程序准备就绪之间的时间,同时也增加了容器可能失败的风险(如果启动逻辑有错误)。你应该始终在你的镜像或应用程序清单中包含健康检查,以减轻这种风险。

我的配置加载实用程序应用程序支持 12 因子方法,其中环境变量覆盖其他设置。它将环境变量与覆盖配置文件合并,并将输出写入应用程序期望的位置的配置文件。该实用程序采用与 Viper 相同的方法,寻找具有特定前缀的环境变量,这有助于将应用程序设置与其他容器中的设置分开。

现在试试看。遗留应用程序不使用环境变量,但配置实用程序会设置它们,以便用户体验与现代应用程序相同。

# run a container with an override file and an environment variable:
docker run -d -p 8091:80 -v "$(pwd)/config/dev:/config-override"
                         -e CONFIG_SOURCE_PATH="/config-override/application.properties"
                         -e IOTD_ENVIRONMENT="custom" diamol/ch18-image-of-the-day

# check the config settings:
curl http://localhost:8091/config

该实用程序允许我以与其他应用程序相同的方式使用我的旧应用程序。对用户来说,这主要是透明的——他们只需设置环境变量并将覆盖文件加载到卷中。对应用程序来说也是透明的,它只读取它期望看到的配置文件——这里没有对原始应用程序代码的更改。图 C.11 显示,这个“遗留”应用程序使用了现代的多层配置方法。

图片

图 C.11 环境变量使这个旧应用程序的配置模型表现得像新应用程序。

现在,图像库应用程序中的每个组件都使用相同的配置模式。所有组件之间都有一个标准化水平,但也有一些小的实现差异。每个组件都可以通过文件覆盖来配置以在开发模式下运行,并且每个组件都可以通过环境变量来配置以启用 Prometheus 指标。您实际上如何做到这一点因应用程序而异,这就是我一开始提到的灰色区域——由于应用程序平台的工作方式不同,很难强制执行一个标准,即当环境变量ENABLE_METRICS=true时,每个组件都将运行 Prometheus 端点。

文档是消除这种混淆的方法,在 Docker 世界中,部署文档最好在应用程序清单文件中完成。本章的练习中有一个 Docker Compose 文件,它正好执行了我之前段落中描述的操作——将每个组件设置为开发模式,但启用 Prometheus 指标。列表 C.3 显示了 Compose 文件的配置部分。

列表 C.3 在 Docker Compose 中记录配置设置

version: "3.7"

services:
  accesslog:
    image: diamol/ch18-access-log
    environment:
      NODE_CONFIG: '{"metrics": {"enabled":"true"}}'
    secrets:
      - source: access-log-config
        target: /app/config-override/local.json

  iotd:
    image: diamol/ch18-image-of-the-day
    environment:
      CONFIG_SOURCE_PATH: "/config-override/application.properties"
      IOTD_MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: "health,prometheus"
    secrets:
      - source: iotd-config
        target: /config-override/application.properties

  image-gallery:
    image: diamol/ch18-image-gallery
    environment:
      IG_METRICS.ENABLED: "TRUE"
    secrets:
      - source: image-gallery-config
        target: /app/config-override/config.toml

secrets:
  access-log-config:
    file: access-log/config/dev/local.json
  iotd-config:
    file: image-of-the-day/config/dev/application.properties
  image-gallery-config:
    file: image-gallery/config/dev/config.toml

这是一段相当长的代码列表,但我希望将所有这些内容放在一个地方,以便您可以看到模式是相同的,尽管细节不同。Node.js 应用程序使用环境变量中的 JSON 字符串来启用指标,并加载 JSON 文件作为配置覆盖。

Java 应用程序使用一个环境变量来列出要包含的管理端点;在其中添加 Prometheus 可以启用指标收集。然后它从属性文件中加载配置覆盖,这是一个键/值对的序列。

Go 应用程序使用环境变量中的一个简单的"TRUE"字符串来启用指标,并以 TOML 文件的形式加载配置覆盖。我在 Docker Compose 中使用了文件源的秘密支持,但模式对于集群中的卷挂载或配置对象是一样的。

这里的用户体验既有好的一面也有不好的一面。好的一面是你可以通过更改配置覆盖的源路径来轻松加载不同的环境,并且你可以使用环境变量更改单个设置。不好的一面是你需要了解应用程序的怪癖。项目团队可能会演变各种 Docker Compose 覆盖来覆盖不同的配置,因此编辑配置设置不会是一个常见的活动。运行应用程序将会更加常见,而且这和用 Compose 启动任何应用程序一样简单。

现在试试看。让我们使用一组固定的配置来运行所有组件的应用程序。首先,移除所有正在运行的容器,然后使用 Docker Compose 运行应用程序:

# clear all containers:
docker container rm -f $(docker container ls -aq)

cd ch18/exercises

# run the app with the config settings:
docker-compose up -d

# check all the config APIs:
curl http://localhost:8030/config
curl http://localhost:8020/config
curl http://localhost:8010/config

你可以浏览到http:/ /localhost:8010并按正常方式使用应用程序,并浏览到 Prometheus 端点以查看组件指标(在localhost:8010/metricshttp://localhost:8030/metrics,和 http://localhost:8020/actuator/prometheus)。但实际上,所有确认应用程序配置正确的信息都来自那些配置 API。

你可以在图 C.12 中看到我的输出。每个组件都从镜像中的默认配置文件加载发布周期名称,从配置覆盖文件中加载环境名称,并从环境变量中加载指标设置。

图 C.12 Docker Compose 可以记录应用程序配置设置,并使用该配置启动应用程序。

关于从容器环境获取配置的应用程序构建模式,我们实际上需要涵盖的就是这些。我们将以关于多级配置模型能带你到哪里的思考来结束这一章。

C.5 理解为什么灵活的配置模型会带来回报

你将使用 CI/CD 管道将应用程序打包并部署到生产环境中,该管道的核心设计是构建一个镜像,你的部署过程就是将这个镜像通过你的环境提升到生产环境。你的应用程序在每个环境中都需要稍微有所不同,而为了保持单镜像方法同时支持这一点,你可以使用多级配置模型。

在实践中,你将使用内置在容器镜像中的发布级别设置,几乎在所有情况下,使用容器平台提供的环境级别覆盖文件,但能够使用环境变量设置功能级别的配置是一个有用的补充。这意味着你可以快速响应生产问题——如果这是一个性能问题,则降低日志级别,或者关闭存在安全漏洞的功能。这也意味着你可以在开发机器上创建一个类似生产的环境来复现错误,使用移除机密的配置覆盖,并使用环境变量。

能够在任何环境中运行完全相同的镜像,这是在配置模型上投入时间的回报。图 C.13 显示了从 CI/CD 管道开始的镜像生命周期。

图 C.13 CI/CD 管道生成一个镜像,你使用配置模型来改变行为。

你在创建这个灵活的配置模型时所做的努力将大大有助于确保你的应用在未来具有可维护性。所有容器运行时都支持从配置对象或机密中加载文件到容器中,并设置环境变量。本章图像库应用的 Docker 镜像将以相同的方式与 Docker Compose、Docker Swarm 或 Kubernetes 一起工作。而且不仅限于容器运行时——标准的配置文件和环境变量也是平台即服务(PAAS)产品和无服务器函数中使用的模型。

C.6 实验室

对于新应用深入挖掘配置模型并找出如何设置覆盖文件和配置功能覆盖可能有些棘手,所以你将在本实验中获得一些练习。你将使用相同的图像库应用——在本章的实验室文件夹中有一个指定了应用组件的 Docker Compose 文件,但没有配置。你的任务是设置每个组件以

  • 使用卷来加载配置覆盖文件。

  • 加载测试环境的配置覆盖。

  • 将发布周期重置为“20.01”而不是“19.12”。

这应该相当直接,但花些时间调整应用配置而不对应用进行任何更改将很有用。当你使用 docker-compose up 运行应用时,你应该能够浏览到 http://localhost:8010,并且应用应该能够正常工作。你应该能够浏览到所有三个配置 API,并看到发布名称是 20.01,环境是 TEST。

我的解决方案在同一个文件夹中的 docker-compose-solution.yml 文件中,或者你可以在 GitHub 上查看:github.com/sixeyed/diamol/blob/master/ch18/lab/README.md

附录 D. 使用 Docker 编写和管理应用程序日志

记录日志通常是学习新技术中最无聊的部分,但 Docker 不是这样。基本原理很简单:你需要确保你的应用程序日志被写入标准输出流,因为那是 Docker 寻找它们的地方。有几个方法可以实现这一点,我们将在本章中介绍,然后乐趣就开始了。Docker 有一个可插拔的日志框架——你需要确保你的应用程序日志从容器中输出,然后 Docker 可以将它们发送到不同的地方。这让你可以构建一个强大的日志模型,其中所有容器的应用程序日志都被发送到一个中央日志存储,并在其上方有一个可搜索的用户界面——所有这些都使用开源组件,所有都在容器中运行。

本附录摘自第十九章,“使用 Docker 编写和管理应用程序日志”,来自 Elton Stoneman 的《一个月午餐时间学习 Docker》(Manning,2020)。任何章节引用或对代码仓库的引用都指该书的相关章节或代码仓库。

D.1 欢迎来到 stderr 和 stdout!

Docker 镜像是你应用程序的二进制文件和依赖项的文件系统快照,同时也包含一些元数据,告诉 Docker 当你从镜像运行容器时应该启动哪个进程。该进程在前景运行,所以就像启动一个 shell 会话然后运行一个命令。只要命令是活跃的,它就控制着终端的输入和输出。命令将日志条目写入标准输出和标准错误流(称为stdoutstderr),所以在终端会话中你会在你的窗口中看到输出。在容器中,Docker 会监视 stdout 和 stderr,并从流中收集输出——这就是容器日志的来源。

现在试试看!如果你在容器中运行一个简单的 timecheck 应用,你可以轻松地看到这一点。应用程序本身在前台运行,并将日志条目写入 stdout:

# run the container in the foreground: 
docker container run diamol/ch15-timecheck:3.0

# exit the container with Ctrl-C when you're done

你会在你的终端中看到一些日志行,你会发现你不能再输入任何命令——容器正在前台运行,所以就像在你的终端中运行应用程序本身一样。每隔几秒钟应用程序就会向 stdout 写入另一个时间戳,所以你会在你的会话窗口中看到另一行。我的输出在图 D.1 中。

图 D.1 前台的容器接管终端会话,直到它退出。

这就是容器的标准操作模型——Docker 在容器内启动一个进程,并收集该进程的输出流作为日志。本书中我们使用的所有应用程序都遵循相同的模式:应用程序进程在前台运行——这可能是一个 Go 可执行文件或 Java 运行时——应用程序本身配置为将日志写入 stdout(或 stderr;Docker 以相同的方式处理这两个流)。这些应用程序日志由运行时写入输出流,并由 Docker 收集。图 D.2 显示了应用程序、输出流和 Docker 之间的交互。

图片

图 D.2 Docker 监视容器中的应用程序进程,并收集其输出流。

容器日志以 JSON 文件的形式存储,因此即使没有终端会话的分离容器,或者已经退出的容器(没有应用程序进程),日志条目仍然可用。Docker 会为您管理这些 JSON 文件,它们的生命周期与容器相同——当容器被移除时,日志文件也会被移除。

现在尝试一下:在后台以分离容器的方式运行与同一镜像的容器,然后检查日志以及日志文件路径:

# run a detached container
docker container run -d --name timecheck diamol/ch15-timecheck:3.0

# check the most recent log entry:
docker container logs --tail 1 timecheck

# stop the container and check the logs again:
docker container stop timecheck
docker container logs --tail 1 timecheck

# check where Docker stores the container log file:
docker container inspect --format='{{.LogPath}}' timecheck

如果你使用的是带有 Linux 容器的 Docker Desktop,请记住 Docker Engine 是在 Docker 为您管理的虚拟机(VM)中运行的——你可以看到容器日志文件的路径,但你无法访问 VM,因此无法直接读取文件。如果你在 Linux 上运行 Docker CE 或使用 Windows 容器,日志文件的路径将在你的本地机器上,你可以打开文件以查看原始内容。你可以在图 D.3 中看到我的输出(使用 Windows 容器)。

图片

图 D.3 Docker 将容器日志存储在 JSON 文件中,并管理该文件的生命周期。

日志文件实际上只是一个实现细节,你通常不需要担心。其格式非常简单;它包含一个 JSON 对象,每个日志条目都有一个包含日志的字符串、日志来源的流名称(stdout 或 stderr)和一个时间戳。列表 D.1 显示了 timecheck 容器日志的示例。

列表 D.1 容器日志的原始格式是一个简单的 JSON 对象

{"log":"Environment: DEV; version: 3.0; time check: 09:42.56\r\n","stream":"stdout","time":"2019-12-19T09:42:56.814277Z"}
{"log":"Environment: DEV; version: 3.0; time check: 09:43.01\r\n","stream":"stdout","time":"2019-12-19T09:43:01.8162961Z"}

您唯一需要考虑 JSON 的情况是,如果您有一个产生大量日志的容器,并且您希望保留所有日志条目一段时间,但希望它们在一个可管理的文件结构中。默认情况下,Docker 为每个容器创建一个单一的 JSON 日志文件,并允许它增长到任何大小(直到填满您的磁盘)。您可以配置 Docker 使用滚动文件,并设置最大大小限制,这样当日志文件填满时,Docker 开始写入新文件。您还可以配置要使用多少个日志文件,当它们都满了之后,Docker 开始覆盖第一个文件。您可以在 Docker 引擎级别设置这些选项,以便更改适用于每个容器,或者您可以为单个容器设置它们。为特定容器配置日志选项是获取一个小型轮换日志文件的一种好方法,但可以保留其他容器的所有日志。

现在尝试一下:再次运行相同的应用程序,但这次指定使用三个滚动日志文件,每个文件最大 5 KB:

# run with log options and an app setting to write lots of logs:
docker container run -d --name timecheck2 --log-opt max-size=5k
                        --log-opt max-file=3 -e Timer__IntervalSeconds=1 diamol/ch15-timecheck:3.0

# wait for a few minutes

# check the logs:
docker container inspect --format='{{.LogPath}}' timecheck2

您会看到容器的日志路径仍然只是一个单一的 JSON 文件,但 Docker 实际上正在使用该名称作为基础,但带有日志文件编号后缀来轮换日志文件。如果您正在运行 Windows 容器或在 Linux 上运行 Docker CE,您可以列出存储日志的目录的内容,您将看到那些文件后缀。我的在图 D.4 中显示。

图 D-4

图 D.4 滚动日志文件允许您为每个容器保留已知数量的日志数据。

对于来自 stdout 的应用程序日志有一个收集和处理阶段,您可以在其中配置 Docker 如何处理日志。在上一个练习中,我们配置了日志处理以控制 JSON 文件结构,并且您可以使用容器日志做更多的事情。为了充分利用这一点,您需要确保每个应用程序都将日志推送到容器外,在某些情况下这可能需要更多的工作。

D.2 从其他 sinks 中转发日志到 stdout

并非每个应用程序都与标准日志模型完美匹配;当您将某些应用程序容器化时,Docker 在输出流中看不到任何日志。一些应用程序作为 Windows 服务或 Linux 守护进程在后台运行,因此容器启动过程实际上不是应用程序过程。其他应用程序可能使用现有的日志框架,将日志写入日志文件或其他位置(在日志世界中称为sinks),如 Linux 中的 syslog 或 Windows 事件日志。无论如何,容器启动过程中没有来自应用程序的日志,因此 Docker 看不到任何日志。

现在尝试一下:本章有一个新的 timecheck 应用程序版本,它将日志写入文件而不是 stdout。当您运行此版本时,没有容器日志,尽管应用程序日志正在存储在容器文件系统中:

# run a container from the new image:
docker container run -d --name timecheck3 diamol/ch19-timecheck:4.0

# check - there are no logs coming from stdout:
docker container logs timecheck3

# now connect to the running container, for Linux:
docker container exec -it timecheck3 sh

# OR windows containers:
docker container exec -it timecheck3 cmd

# and read the application log file:
cat /logs/timecheck.log

尽管应用程序本身写入了很多日志条目,但你不会看到任何容器日志。我的输出在图 D.5 中——我需要连接到容器并从容器文件系统中读取日志文件以查看日志条目。

图 D.5

图 D.5 如果应用程序没有写入任何内容到输出流,你将看不到任何容器日志。

这是因为应用正在使用自己的日志接收器——在这个练习中是一个文件,而 Docker 对此接收器一无所知。Docker 只会从 stdout 读取日志;没有方法可以配置它从容器内的不同日志接收器读取。

处理此类应用的模式是在容器启动命令中运行第二个进程,该进程从应用程序使用的接收器读取日志条目并将它们写入 stdout。这个过程可以是 shell 脚本或简单的实用应用,并且它是启动序列中的最后一个进程,因此 Docker 读取其输出流,应用程序日志作为容器日志被传递。图 D.6 显示了它是如何工作的。

图 D.6

图 D.6 你需要在容器镜像中打包一个实用工具来从文件中传递日志。

这不是一个完美的解决方案。您的实用进程正在前台运行,因此它需要健壮,因为如果它失败,容器会退出,即使实际的应用程序仍在后台工作。反之亦然:如果应用程序失败但日志中继仍在运行,容器会保持运行,尽管应用程序已经不再工作。您需要在镜像中添加健康检查以防止这种情况发生。最后,这并不是对磁盘的高效使用,特别是如果您的应用程序写入大量日志——它们会在容器文件系统中填充一个文件,并在 Docker 主机机器上填充一个 JSON 文件。

即使如此,了解这个模式也是有用的。如果你的应用在前台运行,并且你可以调整配置将日志写入 stdout,那么这是一种更好的方法。但如果你的应用在后台运行,就没有其他选择了,最好是接受低效并让应用像所有其他容器一样运行。

本章对 timecheck 应用进行了更新,添加了此模式,构建了一个小型实用应用来监视日志文件并将行传递到 stdout。列表 D.2 显示了多阶段 Dockerfile 的最终阶段——Linux 和 Windows 有不同的启动命令。

列表 D.2 使用您的应用构建和打包日志中继实用工具

# app image
FROM diamol/dotnet-runtime AS base
...
WORKDIR /app
COPY --from=builder /out/ .
COPY --from=utility /out/ .

# windows
FROM base AS windows
CMD start /B dotnet TimeCheck.dll && dotnet Tail.dll /logs timecheck.log

# linux
FROM base AS linux
CMD dotnet TimeCheck.dll & dotnet Tail.dll /logs timecheck.log

这两个CMD指令实现了相同的功能,但使用了两种不同的操作系统方法。首先,在 Windows 中使用start命令在后台启动.NET 应用程序进程,在 Linux 中在命令后添加单个与号&。然后启动.NET tail 实用程序,配置为读取应用程序写入的日志文件。tail 实用程序只是监视该文件,并将新写入的每一行转发,因此日志被暴露到 stdout 并成为容器日志。

现在尝试一下 运行新镜像的容器,并验证日志是否来自容器,并且它们仍然被写入文件系统:

# run a container with the tail utility process:
docker container run -d --name timecheck4 diamol/ch19-timecheck:5.0

# check the logs:
docker container logs timecheck4

# and connect to the container - on Linux:
docker container exec -it timecheck4 sh

# OR with Windows containers:
docker container exec -it timecheck4 cmd

# check the log file:
cat /logs/timecheck.log

现在日志来自容器。这是一个复杂的方法来达到这个目的,需要额外运行一个进程来将日志文件内容转发到标准输出(stdout),但一旦容器运行起来,这一切都是透明的。这种方法的缺点是日志转发使用了额外的处理能力,并且需要额外的磁盘空间来存储两次日志。您可以在图 D.7 中看到我的输出,它显示了日志文件仍然存在于容器文件系统中。

图片

图 D.7 一个日志转发实用程序将应用程序日志输出到 Docker,但使用了两倍的磁盘空间。

在这个例子中,我使用了一个自定义的实用程序来转发日志条目,因为我希望应用程序能够在多个平台上工作。我可以用标准的 Linux tail命令代替,但在 Windows 中没有等效的命令。自定义实用程序方法也更加灵活,因为它可以从任何接收器读取并将数据转发到 stdout。这应该涵盖了任何场景,其中您的应用程序日志被锁定在容器中的某个地方,而 Docker 无法看到。

当您将所有容器镜像设置为将应用程序日志作为容器日志写入时,您就可以开始利用 Docker 的可插拔日志系统,并整合来自所有容器的所有日志。

D.3 收集和转发容器日志

Docker 在所有应用程序上添加了一个一致的管理层——无论容器内部发生什么;您以相同的方式启动、停止和检查一切。当您将集中式日志系统引入架构时,这尤其有用,我们将通过最流行的开源示例之一:Fluentd,来了解这一点。

Fluentd 是一个统一的日志层。它可以从许多不同的来源摄取日志,过滤或丰富日志条目,然后将它们转发到许多不同的目标。它是由云原生计算基金会(它还管理 Kubernetes、Prometheus 以及 Docker 的容器运行时等项目)管理的项目,它是一个成熟且高度灵活的系统。您可以在容器中运行 Fluentd,它将监听日志条目。然后您可以运行其他容器,这些容器使用 Docker 的 Fluentd 日志驱动程序而不是标准的 JSON 文件,这些容器日志将被发送到 Fluentd。

现在试试吧 Fluentd 使用配置文件来处理日志。运行一个具有简单配置的容器,该配置将使 Fluentd 收集日志并将它们回显到容器中的 stdout。然后运行带有该容器发送日志到 Fluentd 的 timecheck 应用程序:

cd ch19/exercises/fluentd

# run Fluentd publishing the standard port and using a config file:
docker container run -d -p 24224:24224 --name fluentd -v "$(pwd)/conf:/fluentd/etc" -e FLUENTD_CONF=stdout.conf diamol/fluentd

# now run a timecheck container set to use Docker's Fluentd log driver:
docker container run -d --log-driver=fluentd --name timecheck5 diamol/ch19-timecheck:5.0

# check the timecheck container logs:
docker container logs timecheck5

# and check the Fluentd container logs:
docker container logs --tail 1 fluentd

当您尝试检查来自 timecheck 容器的日志时,您会看到一个错误——并不是所有的日志驱动程序都允许您直接从容器中查看日志条目。在这个练习中,它们被 Fluentd 收集,并且这个配置将输出写入 stdout,因此您可以通过查看 Fluentd 的日志来查看 timecheck 容器的日志。我的输出如图 D.8 所示。

图 D.8 Fluentd 从其他容器收集日志,并且它可以存储它们或将它们写入 stdout。

当 Fluentd 存储日志时,它会为每条记录添加自己的元数据,包括容器 ID 和名称。这是必要的,因为 Fluentd 成为您所有容器的中央日志收集器,您需要能够识别哪些日志条目来自哪个应用程序。将 stdout 作为 Fluentd 的目标只是一个简单的查看一切如何工作的方法。通常,您会将日志转发到中央数据存储。Elasticsearch 是一个非常受欢迎的选择——它是一个适用于日志的无 SQL 文档数据库。您可以在容器中运行 Elasticsearch 以存储日志,并在另一个容器中运行配套的搜索 UI Kibana。图 D.9 显示了日志模型的外观。

图 D.9 集中式日志模型将所有容器日志发送到 Fluentd 进行处理和存储。

它看起来很复杂,但像 Docker 一样,始终很容易在 Docker Compose 文件中指定所有日志设置的部分,并使用一条命令启动整个堆栈。当您的日志基础设施在容器中运行时,您只需为任何希望加入集中式日志的容器使用 Fluentd 日志驱动程序即可。

现在试试吧 删除任何正在运行的容器,并启动 Fluentd-Elasticsearch-Kibana 日志容器。然后使用 Fluentd 日志驱动程序运行一个 timecheck 容器:

docker container rm -f $(docker container ls -aq)

cd ch19/exercises

# start the logging stack:
docker-compose -f fluentd/docker-compose.yml up -d

docker container run -d --log-driver=fluentd diamol/ch19-timecheck:5.0

给 Elasticsearch 几分钟的时间准备,然后浏览到 http://localhost:5601。点击 Discover 选项卡,Kibana 将要求输入要搜索的文档集合的名称。输入 fluentd*,如图 D.10 所示。

图 D.10 Elasticsearch 将文档存储在名为 indexes 的集合中——Fluentd 使用它自己的索引。

在下一屏幕中,您需要设置包含时间过滤器的字段——选择 @timestamp,如图 D.11 所示。

图 D.11 Fluentd 已经将数据保存到 Elasticsearch 中,因此 Kibana 可以看到字段名称。

你可以自动化 Kibana 的设置,但我还没有这么做,因为如果你是 Elasticsearch 堆栈的新手,逐步操作以了解各个组件如何组合在一起是值得的。Fluentd 收集的每条日志条目都保存为 Elasticsearch 中的一个文档,在一个名为 fluentd-{date} 的文档集中。Kibana 让你可以查看所有这些文档——在默认的 Discover 选项卡中,你会看到一个柱状图显示随时间创建的文档数量,并且你可以深入查看单个文档的详细信息。在这个练习中,每个文档都是来自 timecheck 应用的日志条目。你可以在图 D.12 中看到 Kibana 中的数据。

图片

图 D.12 EFK 堆栈的全貌——收集并存储的容器日志,便于简单搜索

Kibana 允许你在所有文档中搜索特定的文本片段,或按日期或其他数据属性过滤文档。它还具有类似于 Grafana 的仪表板功能,因此你可以构建显示每个应用的日志计数或错误日志计数的图表。Elasticsearch 具有巨大的可扩展性,因此适用于生产中的大量数据,当你开始通过 Fluentd 发送所有容器的日志时,你很快会发现这比在控制台中滚动日志行要容易管理得多。

现在试试看:运行带有每个组件配置为使用 Fluentd 日志驱动的图像库应用:

# from the cd ch19/exercises folder

docker-compose -f image-gallery/docker-compose.yml up -d

浏览到 http://localhost:8010 生成一些流量,容器将开始写入日志。图像库应用的 Fluentd 设置为每条日志添加一个标签,标识生成它的组件,因此日志行可以轻松识别——比使用容器名称或容器 ID 更容易识别。你可以在图 D.13 中看到我的输出。我正在运行完整的图像库应用,但我正在 Kibana 中过滤日志,只显示 access-log 组件——记录应用访问时间的 API。

图片

图 D.13 图像库和时间检查容器正在 Elasticsearch 中收集日志。

为 Fluentd 添加一个标签非常容易,它会显示为 log_name 字段用于过滤;这是日志驱动程序的一个选项。你可以使用一个固定的名称或注入一些有用的标识符——在这个练习中,我使用 gallery 作为应用前缀,然后添加生成日志的容器的组件名称和镜像名称。这是一种很好的方式来识别应用程序、组件以及每个日志行的确切版本。列表 D.3 显示了图像库应用的 Docker Compose 文件中的日志选项。

列表 D.3 使用标签识别 Fluentd 日志条目的来源

services:
  accesslog:
    image: diamol/ch18-access-log
         logging:
      driver: "fluentd"
      options:
        tag: " gallery.access-log.{{.ImageName}}"

  iotd:
    image: diamol/ch18-image-of-the-day
    logging:
      driver: "fluentd"
      options:
        tag: "gallery.iotd.{{.ImageName}}"

  image-gallery:
    image: diamol/ch18-image-gallery
    logging:
      driver: "fluentd"
      options:
        tag: "gallery.image-gallery.{{.ImageName}}"
...

当您为生产准备容器时,中央日志记录模型,包括可搜索的数据存储和用户友好的 UI,是一个您绝对应该考虑的模型。您不仅限于使用 Fluentd——Docker 有许多其他日志驱动程序,因此您可以使用其他流行的工具,如 Graylog,或商业工具,如 Splunk。记住,您可以在 Docker 配置的引擎级别设置默认日志驱动程序和选项,但我认为在应用程序清单中这样做更有价值——它清楚地说明了每个环境中使用的日志系统。

如果您还没有建立日志系统,Fluentd 是一个很好的选择。它易于使用,可以从单个开发机器扩展到完整的生产集群,并且您可以在每个环境中以相同的方式使用它。您还可以配置 Fluentd 以丰富日志数据,使其更容易处理,并过滤日志将它们发送到不同的目标。

D.4 管理您的日志输出和收集

记录日志需要在捕获足够信息以用于诊断问题和不过度存储大量数据之间取得微妙的平衡。Docker 的日志模型为您提供了额外的灵活性,以帮助实现这种平衡,因为您可以在存储之前以比预期更冗长的级别生成容器日志,但过滤掉它们。然后,如果您需要查看更冗长的日志,您可以通过更改过滤配置而不是应用程序配置来替换 Fluentd 容器,而不是应用程序容器。

您可以在 Fluentd 配置文件中配置此级别的过滤。上一练习中的配置将所有日志发送到 Elasticsearch,但 D.4 列表中更新的配置过滤掉了来自更冗长的 access-log 组件的日志。这些日志将发送到 stdout,其余的应用程序日志将发送到 Elasticsearch。

列表 D.4 根据记录的标签将日志条目发送到不同的目标

<match gallery.access-log.**>
  @type copy
  <store>
    @type stdout
  </store>
</match>
<match gallery.**>
  @type copy
  <store>
    @type elasticsearch
...

match块告诉 Fluentd 如何处理日志记录,而过滤参数使用在日志驱动程序选项中设置的标签。当您运行此更新后的配置时,access-log 条目将匹配第一个 match 块,因为标签前缀是gallery.access-log。这些记录将不再在 Elasticsearch 中显示,并且只能通过读取 Fluentd 容器的日志来获取。更新的配置文件还丰富了所有日志条目,将标签拆分为应用程序名称、服务名称和镜像名称的单独字段,这使得在 Kibana 中的过滤变得更加容易。

现在尝试一下 更新 Fluentd 配置,通过部署一个指定新配置文件的 Docker Compose 覆盖文件,并更新图像库应用程序以生成更冗长的日志:

# update the Fluentd config:
docker-compose -f fluentd/docker-compose.yml -f fluentd/override-gallery-filtered.yml up -d

# update the application logging config:
docker-compose -f image-gallery/docker-compose.yml -f image-gallery/override-logging.yml up -d

您可以检查这些覆盖文件的內容,您会发现它们只是指定了应用程序的配置设置;所有镜像都是相同的。现在当您使用 http://localhost:8010 上的应用程序时,访问日志条目仍然会被生成,但它们会被 Fluentd 过滤掉,因此您在 Kibana 中不会看到任何新的日志。您将看到来自其他组件的日志,这些日志被添加了新的元数据字段。您可以在图 D.14 的我的输出中看到这一点。

图 D-14

图 D.14 Fluentd 使用日志中的标签来过滤记录并生成新的字段。

访问日志条目仍然可用,因为它们在 Fluentd 容器内部写入到 stdout。您可以将它们视为容器日志,但它们来自 Fluentd 容器,而不是访问日志容器。

现在尝试一下:检查 Fluentd 容器日志以确保记录仍然可用:

docker container logs --tail 1 fluentd_fluentd_1

您可以在图 D.15 中看到我的输出。访问日志条目已被发送到不同的目标,但它仍然经过了相同的处理,以丰富记录中的应用程序、服务和镜像名称:

图 D-15

图 D.15 这些日志被过滤,因此它们不会存储在 Elasticsearch 中,而是回显到 stdout。

这是一种将核心应用程序日志与可选日志分开的好方法。在生产环境中,您不会使用 stdout,但您可能对不同类别的日志有不同的输出——性能关键组件可以将日志条目发送到 Kafka,面向用户的日志可以发送到 Elasticsearch,其余的可以存储在 Amazon S3 云存储中。这些都是 Fluentd 支持的日志存储。

本章有一个最后的练习来重置日志并将访问日志条目重新添加到 Elasticsearch。这模拟了生产环境中您发现系统问题并希望增加日志以查看发生了什么的情况。在我们现有的日志设置中,日志已经被应用程序写入。我们只需更改 Fluentd 配置文件就可以将其暴露出来。

现在尝试一下:部署一个新的 Fluentd 配置,将访问日志记录发送到 Elasticsearch:

docker-compose -f fluentd/docker-compose.yml -f fluentd/override-gallery.yml up -d

此部署使用一个配置文件,移除了访问日志记录的 match 块,因此所有画廊组件的日志都存储在 Elasticsearch 中。当您在浏览器中刷新图像画廊页面时,日志将被收集并存储。您可以在图 D.16 的我的输出中看到这些日志,其中显示了来自 API 和访问日志组件的最新日志。

图 D-16

图 D.16 通过对 Fluentd 配置的修改,无需更改应用程序即可将日志重新添加到 Elasticsearch。

你确实需要意识到,这种方法可能会导致日志条目丢失。在部署期间,容器可能会发送日志,但此时没有 Fluentd 容器在运行以收集它们。Docker 在这种情况下会优雅地继续运行,你的应用程序容器也会继续运行,但日志条目不会被缓冲,因此它们将会丢失。在集群生产环境中,这不太可能成为问题,但即使发生了这种情况,重启应用程序容器并增加日志配置也是更好的选择——至少因为新的容器可能不会像旧容器那样有问题,所以你的新日志不会告诉你任何有趣的事情。

D.5 理解容器日志模型

Docker 中的日志记录方法非常灵活,但前提是你需要将你的应用程序日志作为容器日志可见。你可以直接通过让应用程序将日志写入 stdout 来实现,或者间接地通过在你的容器中使用一个中继工具,将日志条目复制到 stdout。你需要花一些时间确保所有应用程序组件都写入容器日志,因为一旦你做到了这一点,你就可以按你喜欢的方式处理日志。

在本章中,我们使用了 EFK 堆栈——Elasticsearch、Fluentd 和 Kibana——你已经看到了如何轻松地将所有容器日志拉入一个具有用户友好搜索界面的集中式数据库。所有这些技术都是可互换的,但 Fluentd 是最常用的之一,因为它既简单又强大。该堆栈在单机环境中运行良好,并且也可以扩展到生产环境。图 D.17 显示了集群环境在每个节点上运行 Fluentd 容器的情况,其中 Fluentd 容器收集该节点上其他容器的日志并将它们发送到 Elasticsearch 集群——该集群也运行在容器中。

图 D.17 EFK 堆栈在生产环境中使用集群存储和多个 Fluentd 实例工作。

在我们进入实验室之前,我要提醒大家注意一点。有些团队不喜欢容器日志模型中的所有处理层;他们更喜欢直接将应用程序日志写入最终存储,因此,而不是写入 stdout 并让 Fluentd 将数据发送到 Elasticsearch,应用程序直接写入 Elasticsearch。我真的很不喜欢这种方法。你节省了一些处理时间和网络流量,但代价是完全缺乏灵活性。你将日志堆栈硬编码到所有应用程序中,如果你想切换到 Graylog 或 Splunk,你需要去重新工作你的应用程序。我总是更喜欢保持简单和灵活——将你的应用程序日志写入 stdout 并利用平台来收集、丰富、过滤和存储数据。

D.6 实验室

在本章中,我没有过多关注 Fluentd 的配置,但获得一些设置该配置的经验是值得的,因此我将在实验室中要求您完成这项任务。在本章的实验室文件夹中,有一个随机数字应用的 Docker Compose 文件和一个 EFK 堆栈的 Docker Compose 文件。应用容器尚未配置为使用 Fluentd,Fluentd 设置也没有进行任何丰富操作,因此您有三个任务:

  • 扩展 numbers 应用的 Compose 文件,以便所有组件都使用 Fluentd 日志驱动程序,并设置一个包含应用名称、服务名称和镜像的标签。

  • 将 Fluentd 的配置文件 elasticsearch.conf 扩展,以便将标签拆分为应用名称、服务名称和镜像名称字段,以处理来自 numbers 应用的所有记录。

  • 在 Fluentd 配置中添加一个安全检查 match 块,以便将所有非 numbers 应用记录转发到 stdout。

对于这一部分没有提示,因为这是一个通过配置图像库应用来处理配置设置并查看需要为 numbers 应用添加哪些组件的案例。一如既往,我的解决方案已上传到 GitHub 供您检查:github.com/sixeyed/diamol/blob/master/ch19/lab/README.md

posted @ 2025-11-24 09:14  绝不原创的飞龙  阅读(17)  评论(0)    收藏  举报