Kubernetes-启动指南-全-
Kubernetes 启动指南(全)
原文:
zh.annas-archive.org/md5/be21eeebe72acd4358284a68e1289aea译者:飞龙
前言
Kubernetes 想要感谢每一位凌晨三点起床重新启动进程的系统管理员。每一位将代码推送到生产环境,却发现它与在他们的笔记本电脑上运行的方式不同的开发人员。每一位系统架构师因为忘记更新剩余主机名而错误地将负载测试指向生产服务器的情况。正是这些痛苦、奇怪的工作时间和奇怪的错误激发了 Kubernetes 的开发。用一句话概括:Kubernetes 的目标是根本简化构建、部署和维护分布式系统的任务。它受到数十年构建可靠系统的实际经验的启发,并从头开始设计,使这种经验至少是愉快的,如果不是令人欣喜的。希望你喜欢本书!
适合阅读本书的人
无论你是初次接触分布式系统,还是多年来一直部署云原生系统,容器和 Kubernetes 都能帮助你实现新的速度、灵活性、可靠性和效率水平。本书描述了 Kubernetes 集群编排器及其工具和 API 如何改善分布式应用程序的开发、交付、安全性和维护。尽管不假设读者有 Kubernetes 的先前经验,但要充分利用本书,你应该能够轻松构建和部署基于服务器的应用程序。熟悉负载均衡器和网络存储等概念将会有所帮助,尽管不是必需的。同样,对 Linux、Linux 容器和 Docker 的经验,虽然不是必需的,但将有助于你充分利用本书的内容。
为什么写这本书
我们从 Kubernetes 的起源开始就参与其中。看到它从一个主要用于实验的好奇心转变为关键的生产级基础设施,支持从机器学习到在线服务等各个领域的大规模生产应用,这一过程真是令人惊叹。随着这种转变的发生,越来越明显的是,一本既捕捉了 Kubernetes 核心概念如何使用,又解释了这些概念背后开发动机的书籍,将对云原生应用开发的状态做出重要贡献。我们希望通过阅读本书,你不仅学会如何在 Kubernetes 上构建可靠、可扩展的应用程序,还能深入了解导致其开发的分布式系统核心挑战。
为什么更新这本书
Kubernetes 生态系统自第一版和第二版以来持续发展和演变。Kubernetes 已经发布了许多版本,并且更多工具和使用 Kubernetes 的模式已成为事实上的标准。在第三版中,我们专注于增加在 Kubernetes 生态系统中日益受关注的主题,包括安全性、从编程语言访问 Kubernetes,以及多集群应用程序部署。我们还更新了所有现有章节,以反映自第一版和第二版以来 Kubernetes 的变化和演进。我们完全期待在未来几年再次修订本书(并期待这样做),因为 Kubernetes 将继续演变。
今天的云原生应用程序简介
从最早的编程语言,到面向对象编程,再到虚拟化和云基础设施的发展,计算机科学的历史是一个隐藏复杂性并赋予您构建越来越复杂应用程序的抽象发展史。尽管如此,开发可靠、可伸缩的应用程序仍然比应有的难度大得多。近年来,容器和像 Kubernetes 这样的容器编排 API 已被证明是一个重要的抽象层,极大简化了可靠、可伸缩分布式系统的开发。容器和编排器使开发人员能够以几年前还像科幻小说一样的速度、敏捷性和可靠性构建和部署应用程序。
浏览本书
本书的组织结构如下。第一章概述了 Kubernetes 的高级好处,而不深入细节。如果您对 Kubernetes 还不熟悉,这是一个理解为什么应该阅读本书其余部分的好地方。
第二章详细介绍了容器和容器化应用程序开发。如果您以前从未使用过 Docker,这一章将是一个有用的介绍。如果您已经是 Docker 专家,这章可能大部分内容是复习。
第三章讲述了如何部署 Kubernetes。虽然本书大部分内容都是关于如何使用 Kubernetes,但您需要在开始使用之前先运行一个集群。尽管本书不涵盖生产环境中运行集群的内容,但本章介绍了几种创建集群的简单方法,以便您了解如何使用 Kubernetes。第四章介绍了与 Kubernetes 集群交互使用的一些常见命令。
从第五章开始,我们深入探讨如何使用 Kubernetes 部署应用程序的细节。我们涵盖了 Pod(第五章)、标签和注释(第六章)、服务(第七章)、Ingress(第八章)和 ReplicaSets(第九章)。这些构成了在 Kubernetes 中部署服务所需的核心基础知识。接着我们讨论了部署(第十章),这将完整应用程序的生命周期联系在一起。
在这些章节之后,我们介绍了 Kubernetes 中一些更专业的对象:DaemonSets(第十一章)、Jobs(第十二章)以及 ConfigMaps 和 Secrets(第十三章)。虽然这些章节对许多生产应用程序至关重要,但如果您正在学习 Kubernetes,可以先跳过它们,等您积累了更多经验和专业知识后再回头来学习。
接下来我们介绍基于角色的访问控制(第十四章),并讨论服务网格(第十五章)以及将存储集成到 Kubernetes 中(第十六章)。我们还讨论了扩展 Kubernetes(第十七章)和从编程语言访问 Kubernetes(第十八章)。然后我们专注于保护 Pod(第十九章)以及 Kubernetes 集群的策略和治理(第二十章)。
最后,我们以一些多集群应用程序的开发和部署示例结束(第二十一章),并讨论如何在源代码控制中组织您的应用程序(第二十二章)。
在线资源
您将需要安装 Docker。如果尚未这样做,您可能还需要熟悉 Docker 文档。
同样,您将需要安装 kubectl 命令行工具。您可能还希望加入 Kubernetes Slack 频道,在那里您将找到一个庞大的用户社区,他们愿意在几乎任何时间回答问题。
最后,随着您的进步,您可能希望参与到开源 GitHub 上的 Kubernetes 仓库 中。
本书使用约定:
本书使用以下排版约定:
斜体
指示新术语、URL、电子邮件地址、文件名和文件扩展名。
固定宽度
用于程序清单,以及段落内引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
固定宽度加粗
显示用户应该直接键入的命令或其他文本。
等宽斜体
显示应由用户提供值或由上下文确定值替换的文本。
注意
此图标表示提示,建议或一般说明。
警告
此图标表示警告或注意事项。
使用代码示例
附加材料(代码示例,练习等)可在https://github.com/kubernetes-up-and-running/examples下载。
本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,则可以在您的程序和文档中使用它。除非您正在复制大量代码,否则无需征得许可。例如,编写一个使用本书中几个代码片段的程序无需许可。售卖或分发包含 O’Reilly 书籍示例的 CD-ROM 需要许可。引用本书并引用示例代码回答问题无需许可。将本书中大量示例代码整合到您产品的文档中需要许可。
我们欣赏但不需要归因。归因通常包括标题,作者,出版商和 ISBN。例如:“Kubernetes: Up and Running,第 3 版,由 Brendan Burns,Joe Beda,Kelsey Hightower 和 Lachlan Evenson(O’Reilly)编著。版权所有 2019 年 Brendan Burns,Joe Beda,Kelsey Hightower 和 Lachlan Evenson,978-1-098-11020-8。”
如果您认为您使用的代码示例超出了公平使用范围或上述许可范围,请随时通过permissions@oreilly.com联系我们。
O’Reilly Online Learning
注意
几乎 40 年来,O’Reilly Media已经提供技术和商业培训,知识和见解,帮助公司取得成功。
我们独特的专家和创新者网络通过书籍,文章,会议和我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程,深度学习路径,交互式编码环境以及来自 O’Reilly 和 200 多家其他出版商的广泛文本和视频集合。更多信息,请访问http://oreilly.com。
如何联系我们
请将有关本书的评论和问题寄给出版商:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
加利福尼亚州塞巴斯托波尔 95472
-
800-998-9938(在美国或加拿大)
-
707-829-0515(国际或本地)
-
707-829-0104(传真)
我们为本书设立了一个网页,列出勘误,示例和任何其他信息。您可以访问https://oreil.ly/kubernetesUR3。
通过邮件bookquestions@oreilly.com提出关于本书的评论或技术问题。
了解更多关于我们的书籍、课程、会议和新闻的信息,请访问我们的网站:http://www.oreilly.com。
在 LinkedIn 找到我们:https://linkedin.com/company/oreilly-media。
关注我们的 Twitter:http://twitter.com/oreillymedia。
观看我们的 YouTube:http://www.youtube.com/oreillymedia。
致谢
我们要感谢所有帮助我们开发这本书的人。这包括我们的编辑维吉尼亚·威尔逊和莎拉·格雷,以及所有奥莱利公司的伟大同事,还有那些提供了极大反馈以显著改进本书的技术审阅者们。最后,我们要感谢所有第一版和第二版读者,他们花时间报告了本书中发现并修正的勘误。谢谢大家!我们非常感激。
第一章:介绍
Kubernetes 是一个用于部署容器化应用程序的开源编排器。它最初由 Google 开发,灵感来自于十年在容器中通过面向应用程序的 API 部署可扩展、可靠系统的经验。^(1)
自 2014 年推出以来,Kubernetes 已经成长为世界上最大和最受欢迎的开源项目之一。它已成为构建云原生应用程序的标准 API,在几乎每个公共云中都可以找到。Kubernetes 是一个经过验证的适用于各种规模云原生开发者的分布式系统基础设施,从一组树莓派计算机到充满最新机器的数据中心。它提供了成功构建和部署可靠、可扩展分布式系统所需的软件支持。
当我们说“可靠、可扩展的分布式系统”时,您可能会想知道我们的意思。越来越多的服务通过 API 在网络上传送。这些 API 通常由分布式系统提供支持,实现 API 的各个部分运行在不同的机器上,通过网络连接并通过网络通信协调它们的操作。因为我们日常生活中对这些 API 的依赖越来越多(例如,寻找到最近医院的路线),这些系统必须具备极高的可靠性。即使系统的某个部分崩溃或停止工作,它们也不能失败。同样,它们必须在软件部署或其他维护事件期间保持可用性。最后,因为越来越多的世界正在上网并使用这类服务,它们必须具备高度可扩展性,以便能够根据不断增长的使用需求扩展其容量,而不需要彻底重新设计实现服务的分布式系统。在许多情况下,这还意味着自动增加(和减少)容量,以使您的应用程序能够达到最大效率。
根据您何时以及为何拿起这本书,您对容器、分布式系统和 Kubernetes 可能有不同的经验程度。您可能计划在公共云基础设施、私有数据中心或混合环境中构建应用程序。无论您的经验如何,本书都应能帮助您充分利用 Kubernetes。
人们开始使用容器和像 Kubernetes 这样的容器 API 的原因有很多,但我们认为它们都可以追溯到以下几个好处之一:
-
开发速度
-
扩展(软件和团队)
-
抽象化您的基础设施
-
效率
-
云原生生态系统
在接下来的几节中,我们将描述 Kubernetes 如何帮助提供每个功能。
速度
速度是当今几乎所有软件开发的关键组成部分。软件行业已经从以盒装 CD 或 DVD 发货的产品转变为通过网络交付的基于 Web 服务的软件,这些软件每小时更新。这种变化的格局意味着,你和竞争对手之间的差异往往是你开发和部署新组件和功能的速度,或者是你对他人开发的创新作出响应的速度。
然而,需要注意的是,速度并不是简单的原始速度定义。虽然用户始终在寻求迭代改进,但他们更感兴趣的是高度可靠的服务。曾经,服务每晚午夜进行维护关闭是可以接受的。但是今天,所有用户都期望服务始终可用,即使运行的软件不断变化。
因此,速度的衡量不是以每小时或每天可以发布的原始功能数量为准,而是以在保持高度可用服务的同时可以发布的事物数量为准。
因此,容器和 Kubernetes 可以提供您需要快速操作同时保持可用性的工具。实现这一点的核心概念包括:
-
不可变性
-
声明性配置
-
在线自愈系统
-
共享可重用的库和工具
所有这些想法都相互关联,以显著提高您可靠部署新软件的速度。
不可变性的价值
容器和 Kubernetes 鼓励开发者构建符合不可变基础设施原则的分布式系统。通过不可变基础设施,一旦在系统中创建了一个构件,它就不会通过用户修改而改变。
传统上,计算机和软件系统被视为可变基础设施。在可变基础设施中,更改被应用为对现有系统的增量更新。这些更新可以一次性完成,也可以在很长一段时间内分散进行。通过 apt-get update 工具进行系统升级是对可变系统的一个很好的例子。运行 apt 时,逐个下载任何更新的二进制文件,将它们复制到旧的二进制文件上,并对配置文件进行增量更新。在可变系统中,基础设施的当前状态不是表示为单个构件,而是作为随时间累积的增量更新和变化的累积。在许多系统中,这些增量更新不仅来自系统升级,还来自操作员的修改。此外,在由大型团队运行的任何系统中,很可能这些变更将由许多不同的人执行,并且在许多情况下,这些变更并没有记录在任何地方。
相比之下,在一个不可变系统中,与增量更新和更改的系列相比,一个全新的、完整的镜像被构建,其中更新只需在单个操作中用新镜像替换整个镜像。没有增量变化。可以想象,这与配置管理的传统世界是一个重大的转变。
为了在容器的世界中更具体化,考虑升级软件的两种不同方式:
-
你可以登录到一个容器中,运行一个命令来下载你的新软件,关闭旧服务器,然后启动新的服务器。
-
你可以构建一个新的容器镜像,推送到容器注册表,杀死现有的容器,然后启动一个新的容器。
乍一看,这两种方法可能看起来几乎无法区分。那么是什么让构建一个新的容器镜像提高了可靠性呢?
关键的区别在于你创建的构件,以及如何创建它的记录。这些记录使得能够准确理解某个新版本的差异,如果出现问题,也能确定发生了什么变化以及如何修复它。
此外,构建新镜像而不是修改现有镜像意味着旧镜像仍然存在,并且可以在错误发生时快速用于回滚。相比之下,一旦你将新的二进制文件复制到现有二进制文件上,回滚几乎是不可能的。
不可变容器镜像是你在 Kubernetes 中构建的一切的核心。虽然可以命令式地更改运行中的容器,但这是一种反模式,只应在极端情况下使用(例如,如果这是唯一的临时修复关键生产系统的方法)。即使如此,这些更改也必须在稍后通过声明式配置更新时记录。
声明式配置对象
不可变性扩展到在集群中运行的容器以及描述你的应用程序给 Kubernetes 的方式上。在 Kubernetes 中,一切都是声明式配置对象,代表系统的期望状态。Kubernetes 的工作是确保实际世界的状态与这个期望状态匹配。
就像可变与不可变基础架构一样,声明式配置是对命令式配置的一种替代,其中世界的状态是通过执行一系列指令来定义的,而不是声明世界期望的状态。虽然命令式命令定义了动作,声明式配置定义了状态。
要理解这两种方法,考虑一下生产软件的三个副本的任务。使用命令式方法,配置会说“运行 A,运行 B 和运行 C”。相应的声明式配置将是“副本等于三”。
因为它描述了世界的状态,声明性配置不必执行就能理解。它的影响被明确声明。由于声明性配置的效果可以在执行之前理解,因此声明性配置的错误可能性要少得多。此外,传统的软件开发工具,如源代码控制、代码审查和单元测试,可以以不可能用于命令式指令的方式用于声明性配置。将声明性配置存储在源代码控制中的想法通常被称为“基础设施即代码”。
近来,GitOps 的理念已经开始通过源代码控制正式化基础设施即代码的实践。当您采用 GitOps 时,对生产环境的更改完全通过推送到 Git 仓库来进行,然后通过自动化反映到您的集群中。实际上,您的生产 Kubernetes 集群被视为一个有效的只读环境。此外,GitOps 被越来越多地集成到由云提供的 Kubernetes 服务中,作为管理您的云原生基础设施的最简单方式。
存储在版本控制系统中的声明性状态与 Kubernetes 使现实与这种声明性状态匹配的能力结合起来,使回滚变更变得极为简单。它只是重新声明系统的先前声明状态。这通常对于命令式系统来说是不可能的,因为尽管命令式指令描述了如何从点 A 到点 B,但很少包括可以使您回到原点的反向指令。
自我修复系统
Kubernetes 是一个在线的、自我修复的系统。当它接收到期望的状态配置时,不只是单次地执行一组操作使当前状态与期望状态匹配。它持续地执行操作,确保当前状态始终与期望状态一致。这意味着 Kubernetes 不仅会初始化您的系统,还会防范可能会破坏系统稳定性和可靠性的任何故障或干扰。
更传统的操作员修复方式包括一系列手动的缓解步骤,或者对某种警报进行人工干预。这样的命令式修复更昂贵(因为通常需要值班操作员随时待命进行修复)。而且通常更慢,因为人类必须经常唤醒并登录以响应。此外,它也不太可靠,因为命令式修复操作序列具有前一节中描述的所有命令式管理问题。像 Kubernetes 这样的自我修复系统通过更快、更可靠地执行可靠的修复操作,不仅减轻了操作员的负担,还提高了系统的整体可靠性。
作为这种自我修复行为的一个具体例子,如果您向 Kubernetes 断言一个期望状态为三个副本,它不仅仅创建三个副本 —— 它会持续确保恰好有三个副本。如果您手动创建第四个副本,Kubernetes 将销毁一个以确保数量恢复到三个。如果您手动销毁一个副本,Kubernetes 将创建一个副本以再次将您带回到期望的状态。
在线自我修复系统提高了开发人员的速度,因为您本来可能花在运营和维护上的时间和精力可以用来开发和测试新功能。
在 Kubernetes 的操作员范式中,有了更高级的自我修复形式。使用操作员,编码进入集群中作为容器运行的操作员应用程序中的更高级逻辑,以维护、扩展和修复特定软件(例如 MySQL)。操作员中的代码负责比 Kubernetes 的通用自我修复更具针对性和高级的健康检测和修复。通常这被打包成“操作员”,这些在第十七章中有所讨论。
扩展您的服务和团队
随着产品的增长,您需要扩展开发它的软件和团队是不可避免的。幸运的是,Kubernetes 可以帮助实现这两个目标。Kubernetes 通过偏爱解耦架构来实现可伸缩性。
解耦
在解耦架构中,每个组件通过定义的 API 和服务负载均衡器与其他组件分离。API 和负载均衡器将系统的每个部分隔离开来。API 提供了实施者和使用者之间的缓冲,负载均衡器为每个服务的运行实例之间提供了一个缓冲区。
通过负载均衡器解耦组件使得可以轻松扩展构成您服务的程序,因为增加程序的规模(因此增加了容量)可以在不调整或重新配置服务的任何其他层的情况下完成。
通过 API 解耦服务器使得扩展开发团队变得更容易,因为每个团队可以专注于单个、更小的微服务,其可理解的表面积。清晰的微服务之间的 API 限制了构建和部署软件所需的跨团队通信开销。这种通信开销通常是扩展团队时的主要限制因素。
应用程序和集群的轻松扩展
具体来说,当你需要扩展你的服务时,Kubernetes 的不可变声明性质使得这种扩展变得轻而易举。因为你的容器是不可变的,并且副本的数量仅仅是配置文件中的一个数字,将服务扩展至更大规模只需要改变配置文件中的一个数字,声明这种新的状态给 Kubernetes,然后让它处理其余的工作。另外,你也可以设置自动扩展,让 Kubernetes 来处理。
当然,这种缩放方式假定你的集群有足够的资源可供使用。有时候,实际上需要扩展集群本身。同样,Kubernetes 使得这项任务更加简单。由于集群中的许多机器与该集合中的其他机器完全相同,并且应用程序本身通过容器与机器的具体细节解耦,因此向集群添加额外的资源只是将一个新的同类机器映像并加入集群的简单任务。这可以通过几个简单的命令或预制的机器映像来完成。
扩展机器资源的一个挑战是预测它们的使用情况。如果你在物理基础设施上运行,获取新机器的时间以天数或周数来计算。在物理和云基础设施上,预测未来的成本很困难,因为很难预测特定应用程序的增长和扩展需求。
Kubernetes 可以简化预测未来的计算成本。要理解这一点为何成立,请考虑将 A、B 和 C 三个团队进行扩展:历史上你会看到每个团队的增长高度不确定,因此难以预测。如果为每个服务预留单独的机器,你只能根据每个服务的最大预期增长进行预测,因为专用于一个团队的机器无法用于另一个团队。相反,如果使用 Kubernetes 将团队与它们使用的具体机器解耦,你可以基于所有三个服务的总体增长来预测增长。将三个变化率合并为单一增长率可以减少统计噪声,并产生更可靠的预期增长预测。此外,将团队与特定机器解耦意味着团队可以共享彼此机器的部分资源,进一步降低了计算资源增长预测所带来的开销。
最后,Kubernetes 使得资源可以自动扩展(扩展和收缩)。特别是在云环境中,可以通过 API 创建新的机器,结合 Kubernetes 和应用程序以及集群本身的自动缩放,意味着你可以根据当前负载始终调整你的成本。
使用微服务扩展开发团队
据多种研究指出,理想的团队规模是“两块披萨团队”,大约六到八人。这种团队规模通常会促进良好的知识共享、快速决策以及共同的目标感。而较大的团队往往会遭受层级问题、能见度不足和内讧等问题,这些问题会阻碍敏捷性和成功。
然而,许多项目需要更多的资源才能成功实现其目标。因此,敏捷性的理想团队规模与产品最终目标所需团队规模之间存在紧张关系。
解决这种紧张关系的常见方法是开发独立、面向服务的团队,每个团队负责构建一个单一的微服务。每个小团队负责设计和交付一个被其他小团队消费的服务。所有这些服务的聚合最终提供了整体产品表面的实现。
Kubernetes 提供了多种抽象和 API,使得构建这些独立的微服务架构变得更加容易:
-
Pods,或者说是容器组,可以将由不同团队开发的容器镜像组合成一个可部署的单元。
-
Kubernetes 服务 提供负载均衡、命名和发现功能,以隔离一个微服务与另一个微服务。
-
命名空间 提供了隔离和访问控制,以便每个微服务可以控制其他服务与其交互的程度。
-
入口 对象提供了一个易于使用的前端,可以将多个微服务组合成单个外部化的 API 表面。
最后,解耦应用程序容器镜像和机器意味着不同的微服务可以共同部署在同一台机器上而不会相互干扰,从而降低微服务架构的开销和成本。Kubernetes 的健康检查和部署功能保证了应用程序部署和可靠性的一致方法,确保微服务团队的增加不会导致服务生产生命周期和运维的不同方法大量增加。
一致性和扩展的关注点分离
除了 Kubernetes 带来的运营一致性之外,Kubernetes 堆栈产生的解耦和关注点分离显著提高了基础设施的一致性。这使得您能够通过一个小而专注的团队扩展基础设施运营,以管理多台机器。我们已经详细讨论了应用容器与机器/操作系统(OS)的解耦,但这种解耦的一个重要方面是容器编排 API 成为一个清晰的合同,分离了应用操作员和集群编排操作员的责任。我们称之为“不是我的猴子,不是我的马戏团”的界线。应用开发人员依赖容器编排 API 提供的服务级别协议(SLA),而不用担心如何实现这个 SLA 的细节。同样,容器编排 API 可靠性工程师专注于提供编排 API 的 SLA,而不用担心运行在其上的应用程序。
解耦关注点意味着运行 Kubernetes 集群的小团队可以负责支持在该集群内运行的数百甚至数千个团队的应用程序(参见图 1-1)。同样,一个小团队可以负责全球各地运行的数十个(甚至更多)集群。需要注意的是,容器和操作系统的这种解耦使得操作系统可靠性工程师能够专注于单个机器操作系统的 SLA。这成为另一条分离责任的界线,Kubernetes 操作员依赖操作系统的 SLA,而操作系统操作员则专注于交付该 SLA。同样,这使得您能够将一小队操作系统专家扩展到数千台机器的集群。

图 1-1. 通过 API 解耦不同运营团队的示意图
当然,即使是将一个小团队用于操作系统管理也超出了许多组织的范围。在这些环境中,由公共云提供商提供的托管 Kubernetes 服务(KaaS)是一个很好的选择。随着 Kubernetes 的普及,KaaS 的可用性也越来越高,几乎每个公共云现在都提供这种服务。当然,使用 KaaS 也有一些限制,因为运营商为您做出了关于如何构建和配置 Kubernetes 集群的决策。例如,许多 KaaS 平台禁用了 alpha 特性,因为它们可能会使托管的集群不稳定。
除了完全托管的 Kubernetes 服务之外,还有许多公司和项目构成的蓬勃生态系统帮助安装和管理 Kubernetes。在“硬方式”和完全托管服务之间有完整的解决方案谱系。
因此,是使用 KaaS 还是自己管理(或介于两者之间),是每个用户根据其情况的技能和需求来决定的。通常对于小型组织来说,KaaS 提供了一个易于使用的解决方案,使他们能够将时间和精力集中在构建支持他们工作的软件上,而不是管理一个集群。对于可以承担专门团队来管理其 Kubernetes 集群的大型组织来说,以这种方式管理可能更有意义,因为它能够在集群能力和操作方面提供更大的灵活性。
抽象你的基础设施
公共云的目标是为开发人员提供易于使用的自助服务基础设施。然而,云 API 往往是围绕着 IT 期望的基础设施(例如“虚拟机”),而不是开发人员希望消费的概念(例如“应用程序”)来定位的。此外,在许多情况下,云服务还伴随着特定的实施细节或服务,这些细节是特定于云提供商的。直接使用这些 API 使得在多个环境中运行您的应用程序或在云和物理环境之间进行分布变得困难。
使用面向应用的容器 API(如 Kubernetes)有两个具体的好处。首先,正如我们之前描述的那样,它将开发人员与具体的机器分离开来。这使得以机器为中心的 IT 角色更加容易,因为机器可以简单地按照总体来增加以扩展集群,并且在云环境中,它还能够提供高度的可移植性,因为开发人员是在使用一个更高级别的 API,这个 API 是基于特定云基础设施的 API 实现的。
当开发人员基于容器镜像构建其应用程序,并基于可移植的 Kubernetes API 部署它们时,将应用程序迁移到不同环境,甚至在混合环境中运行,只是将声明性配置发送到新集群的问题。Kubernetes 拥有许多插件,可以使您从特定的云中抽象出来。例如,Kubernetes 服务知道如何在所有主要的公共云以及几种不同的私有和物理基础设施上创建负载均衡器。同样,Kubernetes 持久卷和持久卷声明可以用来将您的应用程序与特定的存储实现分离开来。当然,为了实现这种可移植性,您需要避免使用云管理服务(例如 Amazon 的 DynamoDB、Azure 的 Cosmos DB 或 Google 的 Cloud Spanner),这意味着您将被迫部署和管理开源存储解决方案,如 Cassandra、MySQL 或 MongoDB。
总之,基于 Kubernetes 应用程序导向的抽象构建确保您在构建、部署和管理应用程序时投入的努力在各种环境中都是真正可移植的。
效率
除了容器和 Kubernetes 提供的开发人员和 IT 管理好处外,抽象化还带来了明显的经济效益。因为开发人员不再考虑机器的问题,他们的应用程序可以共同部署在同一台机器上,而不会影响应用程序本身。这意味着可以将来自多个用户的任务紧密地打包到更少的机器上。
效率可以通过机器或进程执行的有效工作与所花费的总能量的比率来衡量。在部署和管理应用程序时,许多可用的工具和流程(例如 bash 脚本、apt 更新或命令式配置管理)有些效率低下。在讨论效率时,通常有助于同时考虑运行服务器的货币成本和管理所需的人力成本。
运行服务器会产生基于功耗、冷却需求、数据中心空间和原始计算能力的成本。一旦服务器安装并上电(或者点击并启动),计费就开始了。任何空闲的 CPU 时间都是浪费金钱。因此,系统管理员有责任保持利用率在可接受的水平,这需要持续的管理。这就是容器和 Kubernetes 工作流发挥作用的地方。Kubernetes 提供的工具自动化分发应用程序到机群上,确保比传统工具更高的利用率。
进一步提高效率的一项措施是,开发人员的测试环境可以快速、廉价地创建为运行在共享 Kubernetes 集群的一组容器(使用名为命名空间的功能)。过去,为开发人员启动测试集群可能意味着启动三台机器。有了 Kubernetes,所有开发人员共享单个测试集群变得非常简单,将他们的使用聚合到更少的机器上。进一步减少使用的机器总数从而提高了每个系统的效率:因为每台单独机器上的资源(CPU、内存等)被更多利用,每个容器的整体成本大大降低。
减少堆栈中开发实例的成本使得以前可能成本过高的开发实践变得可行。例如,通过 Kubernetes 部署应用程序,可以考虑部署和测试每个开发人员在整个堆栈中贡献的每个提交。
当每次部署的成本是以少量容器而不是多个完整虚拟机(VMs)的形式衡量时,您为此类测试所需的成本大大降低。回到 Kubernetes 的原始价值,这种增加的测试也增加了速度,因为您具有有关代码可靠性的强信号,以及快速识别问题可能已引入的细节粒度。
最后,如前文所述,使用自动扩展在需要时增加资源,但在不需要时删除资源,也可以用来推动应用程序的整体效率,同时保持其所需的性能特征。
云原生生态系统
Kubernetes 从一开始就被设计为一个可扩展的环境和一个广泛而友好的社区。这些设计目标以及它在众多计算环境中的普及,导致了围绕 Kubernetes 形成了一个充满活力和庞大的工具和服务生态系统。跟随 Kubernetes(以及之前的 Docker 和 Linux),大多数这些项目也是开源的。这意味着开发人员在开始构建时无需从头开始。自发布以来的这些年里,几乎为每个任务构建了针对 Kubernetes 的工具,从机器学习到持续开发和无服务器编程模型。事实上,在许多情况下,挑战不在于找到潜在的解决方案,而是决定哪一个解决方案最适合该任务。云原生生态系统中丰富的工具已经成为许多人采用 Kubernetes 的一个重要原因。当您利用云原生生态系统时,您可以使用社区构建和支持的项目来几乎涵盖系统的每个部分,从而让您专注于开发核心业务逻辑和独特的服务。
就像任何开源生态系统一样,主要挑战在于可能的解决方案多样性,以及往往存在端到端集成不足的问题。解决这种复杂性的一种可能方式是依靠云原生计算基金会(CNCF)的技术指导。CNCF 是云原生项目代码和知识产权的行业中立承载机构。它有三个项目成熟度级别,帮助指导你采纳云原生项目。CNCF 中的大多数项目处于 sandbox 阶段。Sandbox 表示项目仍处于早期开发阶段,不建议采纳,除非你是早期采纳者或有兴趣为项目开发做贡献。项目成熟度的下一个阶段是 incubating。Incubating 项目通过采纳和生产使用已经证明了它们的实用性和稳定性,但它们仍在发展和扩展它们的社区。尽管有数百个 sandbox 项目,但 incubating 项目只有 20 多个。CNCF 项目的最终阶段是 graduated。这些项目已经完全成熟并被广泛采用。只有少数几个 graduated 项目,包括 Kubernetes 本身。
通过与 Kubernetes 服务集成,是另一种浏览云原生生态系统的方式。在此阶段,大多数 KaaS 提供的还有来自云原生生态系统的开源项目的额外服务。因为这些服务已经集成到云支持的产品中,你可以放心这些项目是成熟且可用于生产的。
总结
Kubernetes 的建立旨在彻底改变应用程序在云中的构建和部署方式。从根本上讲,它被设计为提供开发人员更高的速度、效率和敏捷性。在此时,你每天使用的许多互联网服务和应用程序都在 Kubernetes 之上运行。你可能已经是 Kubernetes 的用户,只是你还不知道!我们希望本章节让你明白为什么应该使用 Kubernetes 部署你的应用程序。既然你已经被说服了,接下来的章节将教你如何部署你的应用程序。
^(1) Brendan Burns 等人,“Borg、Omega 和 Kubernetes:三种容器管理系统十年来的经验教训”,ACM Queue 14(2016):70–93,可在https://oreil.ly/ltE1B找到。
第二章:创建和运行容器
Kubernetes 是一个用于创建、部署和管理分布式应用的平台。这些应用有各种不同的形状和规模,但最终它们都由在各个单独的机器上运行的一个或多个程序组成。这些程序接受输入,处理数据,然后返回结果。在我们考虑构建分布式系统之前,我们必须首先考虑如何构建应用程序容器镜像,这些镜像包含这些程序并构成我们分布式系统的组成部分。
应用程序通常由语言运行时、库和您的源代码组成。在许多情况下,您的应用程序依赖于外部共享库,如libc和libssl。这些外部库通常作为操作系统的共享组件随特定机器上安装的操作系统一起提供。
这种对共享库的依赖性会在程序员的笔记本上开发的应用程序依赖于在将程序部署到生产操作系统时不可用的共享库时造成问题。即使开发和生产环境共享完全相同版本的操作系统,当开发人员忘记将依赖的资产文件包含在他们部署到生产环境的包中时,也可能会出现问题。
在单个机器上运行多个程序的传统方法要求所有这些程序在系统上共享相同版本的共享库。如果不同的程序由不同的团队或组织开发,这些共享依赖性会增加不必要的复杂性和团队之间的耦合。
一个程序只有在可以可靠地部署到应该运行的机器上时才能成功执行。部署的最新状态往往涉及运行命令式脚本,这些脚本不可避免地会有复杂和扭曲的失败情况。这使得部署分布式系统的新版本或部分版本成为一项费时且困难的任务。
在第一章中,我们强烈主张不可变映像和基础设施的价值。这种不可变性正是容器映像提供的。正如我们将看到的,它轻松解决了刚刚描述的所有依赖管理和封装问题。
在处理应用程序时,将其打包成便于与他人共享的方式通常是很有帮助的。Docker 是大多数人用于容器的默认工具,它使得将可执行文件打包并推送到远程注册表变得很容易,以便其他人稍后可以拉取。在撰写本文时,容器注册表在所有主要公共云中都可用,并且在许多云中也提供构建镜像的服务。您还可以使用开源或商业系统运行自己的注册表。这些注册表使用户能够轻松管理和部署私有镜像,而镜像构建器服务则提供了与持续交付系统的简单集成。
在本章以及本书的其余部分中,我们将使用一个简单的示例应用程序来演示这一工作流程。您可以在 GitHub 上找到该应用程序 on GitHub。
容器镜像将程序及其依赖项捆绑到一个根文件系统下的单个构件中。最流行的容器镜像格式是 Docker 镜像格式,已由开放容器倡议标准化为 OCI 镜像格式。Kubernetes 通过 Docker 和其他运行时同时支持 Docker 和 OCI 兼容镜像。Docker 镜像还包括供容器运行时使用的附加元数据,以根据容器镜像的内容启动运行中的应用实例。
本章涵盖以下主题:
-
如何使用 Docker 镜像格式打包应用程序
-
如何使用 Docker 容器运行时启动应用程序
容器镜像
对于几乎每个人来说,他们与任何容器技术的第一次互动都是通过容器镜像。容器镜像是一个二进制包,封装了在操作系统容器内运行程序所需的所有文件。根据您首次尝试容器的方式,您将从本地文件系统构建容器镜像,或者从容器注册表下载现有镜像。无论哪种情况,一旦容器镜像存在于您的计算机上,您就可以运行该镜像以在操作系统容器内生成运行中的应用程序。
最流行和普遍的容器镜像格式是 Docker 镜像格式,由 Docker 开源项目开发,用于使用docker命令打包、分发和运行容器。随后,Docker 公司及其他人员开始通过 Open Container Initiative(OCI)项目标准化容器镜像格式。虽然 OCI 标准在 2017 年中发布了 1.0 版本,但这些标准的采纳进展缓慢。Docker 镜像格式仍然是事实上的标准,由一系列文件系统层组成。每个层在文件系统中添加、删除或修改前一层的文件。这是覆盖文件系统的一个例子。覆盖系统在打包图像时和实际使用图像时都会使用。在运行时,有多种不同的具体文件系统实现,包括aufs、overlay和overlay2。
容器镜像通常与容器配置文件结合使用,该文件提供了如何设置容器环境并执行应用程序入口点的说明。容器配置通常包括如何设置网络、命名空间隔离、资源约束(cgroups)以及应该对运行中的容器实例施加什么样的syscall限制。容器根文件系统和配置文件通常使用 Docker 镜像格式捆绑在一起。
容器主要分为两大类:
-
系统容器
-
应用容器
系统容器旨在模仿虚拟机,并经常运行完整的启动过程。它们通常包含一组通常在虚拟机中找到的系统服务,例如ssh、cron和syslog。在 Docker 刚推出时,这些类型的容器非常常见。随着时间的推移,它们被认为是不良实践,应用容器逐渐受到青睐。
应用容器与系统容器不同之处在于,它们通常运行单个程序。虽然每个容器运行单个程序可能看起来是一个不必要的约束,但这提供了组合可扩展应用程序所需的理想粒度,并且是 Pods 大量利用的设计哲学。我们将详细查看 Pods 在第五章中的工作原理。
使用 Docker 构建应用程序镜像
总的来说,像 Kubernetes 这样的容器编排系统专注于构建和部署由应用容器组成的分布式系统。因此,本章剩余部分将重点放在应用容器上。
Dockerfile
可以使用 Dockerfile 自动创建 Docker 容器镜像。
让我们从为一个简单的 Node.js 程序构建一个应用程序镜像开始。对于许多其他动态语言(如 Python 或 Ruby),这个例子都非常类似。
最简单的 npm/Node/Express 应用程序有两个文件:package.json(示例 2-1)和 server.js(示例 2-2)。将它们放入一个目录中,然后运行 npm install express --save 来建立对 Express 的依赖并安装它。
示例 2-1. package.json
{
"name": "simple-node",
"version": "1.0.0",
"description": "A sample simple application for Kubernetes Up & Running",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": ""
}
示例 2-2. server.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000, function () {
console.log('Listening on port 3000!');
console.log(' http://localhost:3000');
});
要将其打包为 Docker 映像,创建两个额外的文件:.dockerignore(示例 2-3)和 Dockerfile(示例 2-4)。Dockerfile 是构建容器映像的配方,而 .dockerignore 定义了在将文件复制到映像中时应忽略的文件集。有关 Dockerfile 语法的完整描述,请访问Docker 网站。
示例 2-3. .dockerignore
node_modules
示例 2-4. Dockerfile
# Start from a Node.js 16 (LTS) image 
FROM node:16
# Specify the directory inside the image in which all commands will run 
WORKDIR /usr/src/app
# Copy package files and install dependencies 
COPY package*.json ./
RUN npm install
RUN npm install express
# Copy all of the app files into the image 
COPY . .
# The default command to run when starting the container 
CMD [ "npm", "start" ]
每个 Dockerfile 都基于其他容器映像构建。此行指定我们从 Docker Hub 上的node:16映像开始。这是一个预配置的带有 Node.js 16 的映像。
此行设置容器映像中所有后续命令的工作目录。
这三行初始化了 Node.js 的依赖关系。首先,我们将包文件复制到映像中。这将包括 package.json 和 package-lock.json。然后,RUN 命令在容器中运行正确的命令以安装必要的依赖项。
现在,我们将其余的程序文件复制到映像中。这将包括除了 node_modules 之外的所有内容,因为 .dockerignore 文件排除了它。
最后,我们指定了容器运行时应运行的命令。
运行以下命令以创建 simple-node Docker 映像:
$ docker build -t simple-node .
当您想要运行此映像时,可以使用以下命令。导航至 http://localhost:3000 以访问在容器中运行的程序:
$ docker run --rm -p 3000:3000 simple-node
此时,我们的simple-node映像存储在本地 Docker 注册表中,该映像是在构建时创建的,并且只能被单台机器访问。Docker 的真正强大之处在于能够在成千上万台机器和更广泛的 Docker 社区之间共享映像。
优化映像大小
当人们开始尝试使用容器映像进行实验时,会遇到几个容易导致映像过大的问题。首先要记住的是,系统中后续层次删除的文件实际上仍然存在于映像中;它们只是不可访问。考虑以下情况:
.
└── layer A: contains a large file named 'BigFile'
└── layer B: removes 'BigFile'
└── layer C: builds on B by adding a static binary
您可能认为BigFile不再存在于这个图像中。毕竟,当您运行图像时,它是不可访问的。但事实上,它仍然存在于层 A 中,这意味着每当您推送或拉取图像时,BigFile仍然通过网络传输,即使您无法再访问它。
另一个陷阱围绕图像缓存和构建。请记住,每一层都是独立于其下层的增量。每当您更改一层时,它会改变其后的每一层。更改前面的层意味着它们需要重新构建、重新推送和重新拉取,以便将您的图像部署到开发环境。
要更全面地理解这一点,请考虑两个图像:
.
└── layer A: contains a base OS
└── layer B: adds source code server.js
└── layer C: installs the 'node' package
对比:
.
└── layer A: contains a base OS
└── layer B: installs the 'node' package
└── layer C: adds source code server.js
看起来显而易见,这两个图像的行为将是相同的,确实在它们第一次被拉取时是这样。然而,考虑一下server.js发生变化后会发生什么。在第二种情况下,只需要拉取或推送这个变化,但在第一种情况下,需要拉取和推送server.js和提供node包的层,因为node层依赖于server.js层。一般来说,您希望将图像层按从最不可能改变到最可能改变的顺序排列,以优化推送和拉取的图像大小。这就是为什么在示例 2-4 中,我们首先复制package.json文件并安装依赖项,然后再复制其余的程序文件。开发人员更频繁地会更新和改变程序文件,而不是依赖项。
图像安全性
在安全性方面,没有捷径可走。在构建最终将在生产 Kubernetes 集群中运行的图像时,请务必遵循最佳实践来打包和分发应用程序。例如,不要在容器中嵌入密码——这不仅限于最终层,还包括图像中的任何层。容器层引入的一个违反直觉的问题之一是,在一个层中删除文件并不会从前面的层中删除该文件。它仍然占用空间,并且可以被具备正确工具的任何人访问——一位有进取心的攻击者可以简单地创建一个仅包含包含密码的层的图像。
机密和图像绝对不能混合在一起。如果这样做,您将会被黑客攻击,并给整个公司或部门带来耻辱。我们都希望有朝一日能上电视,但有更好的方法去做这件事。
另外,由于容器图像专注于运行单个应用程序,最佳实践是尽量减少容器图像中的文件。图像中每增加一个库都会为您的应用程序提供一个潜在的漏洞向量。根据语言的不同,您可以通过非常严格的依赖关系实现非常小的图像。这个较小的集合确保您的图像不会受到它永远不会使用的库的漏洞的影响。
多阶段图像构建
意外构建大型镜像的最常见方法之一是将实际的程序编译作为构建应用程序容器镜像的一部分。作为镜像构建的一部分进行代码编译看起来很自然,也是从程序构建容器镜像的最简单方式。但这样做的问题在于,会留下所有不必要的开发工具,这些工具通常非常庞大,仍然保存在镜像内部,从而减慢部署速度。
为了解决这个问题,Docker 引入了 多阶段构建。使用多阶段构建,Docker 文件不再只生成一个镜像,实际上可以生成多个镜像。每个镜像被视为一个阶段。可以从前面的阶段复制构件到当前阶段。
为了具体说明这一点,我们将看一下如何构建我们的示例应用程序 kuard。这是一个稍微复杂的应用程序,涉及到一个有自己构建过程的 React.js 前端,然后嵌入到一个 Go 程序中。该 Go 程序运行一个后端 API 服务器,React.js 前端与其交互。
一个简单的 Dockerfile 可能看起来像这样:
FROM golang:1.17-alpine
# Install Node and NPM
RUN apk update && apk upgrade && apk add --no-cache git nodejs bash npm
# Get dependencies for Go part of build
RUN go get -u github.com/jteeuwen/go-bindata/...
RUN go get github.com/tools/godep
RUN go get github.com/kubernetes-up-and-running/kuard
WORKDIR /go/src/github.com/kubernetes-up-and-running/kuard
# Copy all sources in
COPY . .
# This is a set of variables that the build script expects
ENV VERBOSE=0
ENV PKG=github.com/kubernetes-up-and-running/kuard
ENV ARCH=amd64
ENV VERSION=test
# Do the build. This script is part of incoming sources.
RUN build/build.sh
CMD [ "/go/bin/kuard" ]
这个 Dockerfile 生成一个包含静态可执行文件的容器镜像,但它还包含所有的 Go 开发工具以及构建 React.js 前端和应用程序源代码的工具,这两者对最终应用程序都不是必需的。整个镜像,包括所有层,总共超过 500 MB。
要查看如何使用多阶段构建来做到这一点,请查看以下多阶段 Dockerfile:
# STAGE 1: Build
FROM golang:1.17-alpine AS build
# Install Node and NPM
RUN apk update && apk upgrade && apk add --no-cache git nodejs bash npm
# Get dependencies for Go part of build
RUN go get -u github.com/jteeuwen/go-bindata/...
RUN go get github.com/tools/godep
WORKDIR /go/src/github.com/kubernetes-up-and-running/kuard
# Copy all sources in
COPY . .
# This is a set of variables that the build script expects
ENV VERBOSE=0
ENV PKG=github.com/kubernetes-up-and-running/kuard
ENV ARCH=amd64
ENV VERSION=test
# Do the build. Script is part of incoming sources.
RUN build/build.sh
# STAGE 2: Deployment
FROM alpine
USER nobody:nobody
COPY --from=build /go/bin/kuard /kuard
CMD [ "/kuard" ]
这个 Dockerfile 生成两个镜像。第一个是 构建 镜像,其中包含 Go 编译器、React.js 工具链和程序源代码。第二个是 部署 镜像,只包含编译后的二进制文件。使用多阶段构建构建容器镜像可以将最终镜像大小减少数百兆字节,从而显著加快部署时间,因为通常情况下,部署延迟取决于网络性能。从这个 Dockerfile 生成的最终镜像大约为 20 MB。
这些脚本位于 GitHub 上 kuard 仓库中,您可以使用以下命令构建和运行此镜像:
# Note: if you are running on Windows you may need to fix line-endings using:
# --config core.autocrlf=input
$ git clone https://github.com/kubernetes-up-and-running/kuard
$ cd kuard
$ docker build -t kuard .
$ docker run --rm -p 8080:8080 kuard
在远程注册表中存储镜像
如果一个容器镜像只能在单台机器上使用,那有什么用呢?
Kubernetes 依赖于 Pod 清单中描述的镜像在集群中的每台机器上都可用的事实。将此镜像传输到集群中每台机器的一种选项是在每台机器上导出 kuard 镜像并导入它们。我们认为没有比这种方式更烦人的事情了。手动导入和导出 Docker 镜像过程中充满了人为错误。坚决不要这样做!
Docker 社区的标准是将 Docker 镜像存储在远程注册表中。在选择 Docker 注册表时,有大量选项,您的选择将主要基于安全性和协作功能的需求。
一般来说,关于注册表,您需要做出的第一个选择是使用私有注册表还是公共注册表。公共注册表允许任何人下载存储在注册表中的镜像,而私有注册表则需要身份验证才能下载镜像。在选择公共还是私有注册表时,考虑您的用例是很有帮助的。
公共注册表非常适合与世界分享镜像,因为它们允许轻松、无需身份验证地使用容器镜像。您可以将软件作为容器镜像分发,并确信用户在任何地方都会有完全相同的体验。
相比之下,私有注册表最适合存储您服务中私有的并且您不希望外界使用的应用程序。此外,私有注册表通常提供更好的可用性和安全性保证,因为它们专门为您和您的镜像而设计,而不是全球服务。
无论如何,要推送镜像,您需要对注册表进行身份验证。通常可以使用docker login命令完成这一操作,尽管对于某些注册表可能有一些差异。在本书的示例中,我们将推送到 Google Cloud Platform 注册表,称为 Google Container Registry(GCR);其他云服务商,包括 Azure 和 Amazon Web Services(AWS),也有托管的容器注册表。对于托管公开可读镜像的新用户,Docker Hub是一个很好的起点。
登录后,您可以通过在目标 Docker 注册表前置标签化kuard镜像。您还可以附加一个标识符,通常用于该镜像的版本或变体,用冒号(:)分隔:
$ docker tag kuard gcr.io/kuar-demo/kuard-amd64:blue
然后您可以推送kuard镜像:
$ docker push gcr.io/kuar-demo/kuard-amd64:blue
现在kuard镜像已经在远程注册表上可用,是时候使用 Docker 部署它了。当我们将镜像推送到 GCR 时,它被标记为公共,因此无需身份验证即可在任何地方使用。
容器运行时接口
Kubernetes 提供了描述应用部署的 API,但依赖于容器运行时使用目标操作系统的特定容器 API 来设置应用程序容器。在 Linux 系统上,这意味着配置 cgroups 和命名空间。这种容器运行时的接口由容器运行时接口(Container Runtime Interface,CRI)标准定义。CRI API 由许多不同的程序实现,包括 Docker 构建的containerd-cri和 Red Hat 贡献的cri-o实现。安装 Docker 工具时,也会安装并使用containerd运行时由 Docker 守护进程使用。
从 Kubernetes 1.25 版本开始,只有支持 CRI 的容器运行时才能与 Kubernetes 兼容。幸运的是,托管 Kubernetes 提供商已经使用户在托管 Kubernetes 上的过渡几乎自动化。
使用 Docker 运行容器
在 Kubernetes 中,通常通过每个节点上的一个叫做 kubelet 的守护进程启动容器;然而,使用 Docker 命令行工具更容易开始使用容器。Docker CLI 工具可用于部署容器。要从 gcr.io/kuar-demo/kuard-amd64:blue 镜像部署容器,请运行以下命令:
$ docker run -d --name kuard \
--publish 8080:8080 \
gcr.io/kuar-demo/kuard-amd64:blue
此命令启动 kuard 容器,并将本地机器上的端口 8080 映射到容器中的 8080 端口。--publish 选项可以缩写为 -p。这种转发是必要的,因为每个容器都有自己的 IP 地址,所以在容器内部监听 localhost 不会导致您在本机上监听。如果没有端口转发,连接将无法访问您的机器。-d 选项指定此操作应在后台(守护进程)运行,而 --name kuard 给容器一个友好的名称。
探索 kuard 应用程序
kuard 提供了一个简单的 Web 接口,您可以通过浏览器加载 http://localhost:3000 或通过命令行来访问:
$ curl http://localhost:8080
kuard 还暴露了许多我们将在本书后续部分探索的有趣功能。
限制资源使用
Docker 通过暴露 Linux 内核提供的底层 cgroup 技术,使应用程序能够使用更少的资源。Kubernetes 也利用这些能力来限制每个 Pod 使用的资源。
限制内存资源
在容器内运行应用程序的一个关键好处是能够限制资源利用。这允许多个应用程序在同一台硬件上共存,并确保公平使用。
要限制 kuard 的内存为 200 MB 和交换空间为 1 GB,请使用 docker run 命令的 --memory 和 --memory-swap 标志。
停止并删除当前的 kuard 容器:
$ docker stop kuard
$ docker rm kuard
然后,使用适当的标志启动另一个 kuard 容器以限制内存使用:
$ docker run -d --name kuard \
--publish 8080:8080 \
--memory 200m \
--memory-swap 1G \
gcr.io/kuar-demo/kuard-amd64:blue
如果容器中的程序使用了过多的内存,它将被终止。
限制 CPU 资源
机器上的另一个关键资源是 CPU。使用 docker run 命令的 --cpu-shares 标志来限制 CPU 利用率:
$ docker run -d --name kuard \
--publish 8080:8080 \
--memory 200m \
--memory-swap 1G \
--cpu-shares 1024 \
gcr.io/kuar-demo/kuard-amd64:blue
清理
构建镜像完成后,您可以使用 docker rmi 命令删除它:
docker rmi <*tag-name*>
或者:
docker rmi <*image-id*>
镜像可以通过它们的标签名称(例如 gcr.io/kuar-demo/kuard-amd64:blue)或它们的镜像 ID 来删除。与 docker 工具中的所有 ID 值一样,只要保持唯一性,镜像 ID 可以缩短。通常只需要三到四个字符的 ID。
需要注意的是,除非您明确删除镜像,否则它将永远存在于您的系统中,即使您使用相同名称构建新镜像。构建此新镜像仅将标签移至新镜像;它不会删除或替换旧镜像。
因此,在创建新镜像时进行迭代时,您通常会创建许多不必要占用计算机空间的不同镜像。要查看当前计算机上的镜像,可以使用docker images命令。然后可以删除不再使用的标签。
Docker 提供了一个名为docker system prune的工具用于进行一般清理。这将删除所有停止的容器、所有未标记的镜像以及作为构建过程的一部分缓存的所有未使用的镜像层。请谨慎使用。
更复杂一点的方法是设置一个cron任务来运行镜像垃圾收集器。例如,您可以轻松地将docker system prune设置为定期的cron任务,每天一次或每次,具体取决于您创建的图像数量。
概要
应用程序容器为应用程序提供了一个清晰的抽象,并且当打包为 Docker 镜像格式时,应用程序变得易于构建、部署和分发。容器还在同一台机器上运行的应用程序之间提供隔离,有助于避免依赖冲突。
在接下来的章节中,我们将看到挂载外部目录的能力意味着我们不仅可以在容器中运行无状态应用程序,还可以运行生成大量数据的应用程序,例如 MySQL 和其他应用。
第三章:部署 Kubernetes 集群
现在您已经成功构建了一个应用容器,下一步是学习如何将其转变为一个完整、可靠、可扩展的分布式系统。为了做到这一点,您需要一个工作中的 Kubernetes 集群。在这一点上,大多数公共云都提供了基于云的 Kubernetes 服务,只需通过几条命令行指令即可轻松创建集群。如果您刚开始接触 Kubernetes,我们强烈推荐这种方法。即使最终计划在裸机上运行 Kubernetes,这也是一个快速开始学习 Kubernetes、了解如何在物理机器上安装它的好方法。此外,管理一个 Kubernetes 集群本身就是一项复杂的任务,对于大多数人来说,将这种管理任务推迟到云端是有意义的,特别是当云端的管理服务在大多数云中都是免费的时候。
当然,使用基于云的解决方案需要支付这些基于云的资源的费用,并且需要与云端保持活动的网络连接。出于这些原因,本地开发可能更具吸引力,在这种情况下,minikube工具提供了在本地笔记本电脑或台式机的虚拟机上快速启动本地 Kubernetes 集群的简便方法。虽然这是一个不错的选择,但minikube只创建一个单节点集群,这并不能完全展示出完整 Kubernetes 集群的所有方面。因此,我们建议人们从基于云的解决方案开始,除非真的不适合他们的情况。一个较新的选择是在单台机器上运行 Docker-in-Docker 集群,这可以在单台机器上快速启动多节点集群。尽管这是一个不错的选择,但请记住,这个项目仍处于测试阶段,可能会遇到意外的问题。
如果您确实坚持要从裸机开始,请参阅本书末尾的附录(app01.xhtml#rpi_cluster),了解如何使用一系列树莓派单板计算机构建集群的说明。这些说明使用kubeadm工具,并可以适配树莓派之外的其他机器。
在公共云提供商上安装 Kubernetes
本章介绍了在三大主要云提供商(Google Cloud Platform、Microsoft Azure 和 Amazon Web Services)上安装 Kubernetes 的方法。
如果您选择使用云提供商来管理 Kubernetes,您只需要安装其中一种选项;一旦配置好一个集群并准备就绪,您可以跳到“Kubernetes 客户端”,除非您希望在其他地方安装 Kubernetes。
使用 Google Kubernetes Engine 安装 Kubernetes
Google Cloud Platform(GCP)提供了一种托管的 Kubernetes 即服务,称为 Google Kubernetes Engine(GKE)。要开始使用 GKE,您需要一个启用计费的 Google Cloud Platform 账户,并安装了gcloud工具。
一旦安装了gcloud,设置默认区域:
$ gcloud config set compute/zone us-west1-a
然后你可以创建一个集群:
$ gcloud container clusters create kuar-cluster --num-nodes=3
这将花费几分钟时间。集群就绪后,您可以使用以下命令获取集群的凭据:
$ gcloud container clusters get-credentials kuar-cluster
如果遇到问题,您可以在Google Cloud Platform 文档中找到创建 GKE 集群的完整说明。
使用 Azure Kubernetes Service 安装 Kubernetes
Microsoft Azure 作为 Azure 容器服务的一部分提供托管的 Kubernetes 即服务。开始使用 Azure 容器服务的最简单方法是使用 Azure 门户中内置的 Azure Cloud Shell。您可以通过单击右上角工具栏中的 shell 图标来激活 Shell:

Shell 已经自动安装并配置了az工具,可以与您的 Azure 环境一起使用。
或者,您可以在本地计算机上安装az CLI。
当您的 Shell 准备就绪时,可以运行以下命令:
$ az group create --name=kuar --location=westus
创建资源组后,您可以使用以下命令创建集群:
$ az aks create --resource-group=kuar --name=kuar-cluster
这将花费几分钟时间。创建集群后,您可以使用以下命令获取集群的凭据:
$ az aks get-credentials --resource-group=kuar --name=kuar-cluster
如果您尚未安装kubectl工具,可以使用以下命令安装它:
$ az aks install-cli
您可以在Azure 文档中找到在 Azure 上安装 Kubernetes 的完整说明。
在 Amazon Web Services 上安装 Kubernetes
亚马逊提供了一个名为弹性 Kubernetes 服务(EKS)的托管 Kubernetes 服务。创建 EKS 集群的最简单方法是使用开源eksctl命令行工具。
一旦安装并配置了eksctl并将其添加到路径中,您可以运行以下命令创建集群:
$ eksctl create cluster
有关安装选项的更多详细信息(如节点大小等),请使用此命令查看帮助:
$ eksctl create cluster --help
集群安装包括kubectl命令行工具的正确配置。如果您尚未安装kubectl,请按照文档中的说明操作。
使用 minikube 本地安装 Kubernetes
如果需要本地开发体验或不想支付云资源费用,可以使用minikube安装一个简单的单节点集群。或者,如果已经安装了 Docker Desktop,则它附带了一个单机安装的 Kubernetes。
虽然minikube(或 Docker Desktop)是 Kubernetes 集群的良好模拟,但它实际上是为本地开发、学习和实验设计的。因为它只在单节点的虚拟机上运行,所以不提供分布式 Kubernetes 集群的可靠性。此外,本书中描述的某些功能需要与云提供商集成。这些功能在minikube中要么不可用,要么在有限的方式下工作。
注意
你需要在机器上安装一个虚拟化软件来使用 minikube。对于 Linux 和 macOS,通常是 VirtualBox。在 Windows 上,默认选择是 Hyper-V 虚拟化软件。确保在使用 minikube 之前安装虚拟化软件。
你可以在 GitHub 上找到 minikube 工具。有适用于 Linux、macOS 和 Windows 的二进制文件可供下载。安装了 minikube 工具后,你可以使用以下命令创建一个本地集群:
$ minikube start
这将创建一个本地虚拟机,配置 Kubernetes 并创建一个本地的 kubectl 配置,指向该集群。如前所述,该集群只有一个节点,因此虽然它很有用,但与大多数生产部署的 Kubernetes 有一些不同。
当你完成了对集群的使用,可以停止虚拟机:
$ minikube stop
如果你想删除集群,可以运行:
$ minikube delete
在 Docker 中运行 Kubernetes
一种不同的运行 Kubernetes 集群的方法是最近开发的,它使用 Docker 容器来模拟多个 Kubernetes 节点,而不是在虚拟机中运行所有内容。kind 项目 提供了在 Docker 中启动和管理测试集群的良好体验。(kind 代表 Kubernetes IN Docker.) kind 目前仍在开发中(预 1.0),但被那些构建用于快速和简单测试的 Kubernetes 工具广泛使用。
你可以在 kind 网站 找到适合你平台的安装说明。一旦安装完成,创建集群就像这样简单:
$ kind create cluster --wait 5m
$ export KUBECONFIG="$(kind get kubeconfig-path)"
$ kubectl cluster-info
$ kind delete cluster
Kubernetes 客户端
官方的 Kubernetes 客户端是 kubectl:一个用于与 Kubernetes API 交互的命令行工具。kubectl 可用于管理大多数 Kubernetes 对象,例如 Pods、ReplicaSets 和 Services。kubectl 还可以用来探索和验证集群的整体健康状态。
我们将使用 kubectl 工具来探索刚刚创建的集群。
检查集群状态
第一件事是检查你正在运行的集群的版本:
$ kubectl version
这将显示两个不同的版本:本地 kubectl 工具的版本以及 Kubernetes API 服务器的版本。
注意
如果这些版本不同也不要担心。只要保持工具和集群的两个次要版本号在两个范围内,并且不尝试在较旧的集群上使用更新的功能,Kubernetes 工具与 Kubernetes API 的各个版本都向后兼容和向前兼容。Kubernetes 遵循语义化版本规范,次要版本是中间的数字(例如,在 1.18.2 中是 18)。但是,你需要确保你在支持的版本偏差范围内,该范围为三个版本。如果不是,可能会遇到问题。
现在我们已经确认你可以与 Kubernetes 集群通信,我们将更深入地探索该集群。
首先,您可以为集群获取一个简单的诊断。这是验证您的集群通常是否健康的好方法:
$ kubectl get componentstatuses
输出应该如下所示:
NAME STATUS MESSAGE ERROR
scheduler Healthy ok
controller-manager Healthy ok
etcd-0 Healthy {"health": "true"}
注意
随着 Kubernetes 的变化和改进,kubectl 命令的输出有时会发生变化。如果输出与本书示例中显示的不完全相同,不必担心。
您可以在这里看到组成 Kubernetes 集群的各个组件。controller-manager 负责运行各种控制器,以调节集群中的行为,例如确保服务的所有副本可用且健康。scheduler 负责将不同的 Pod 放置到集群中的不同节点上。最后,etcd 服务器是集群的存储,其中存储着所有的 API 对象。
列出 Kubernetes 节点
接下来,您可以列出集群中的所有节点:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
kube0 Ready control-plane,master 45d v1.22.4
kube1 Ready <none> 45d v1.22.4
kube2 Ready <none> 45d v1.22.4
kube3 Ready <none> 45d v1.22.4
您可以看到这是一个已经运行了 45 天的四节点集群。在 Kubernetes 中,节点被分为包含 API 服务器、调度程序等容器的 control-plane 节点,这些节点管理集群,并且 worker 节点上运行您的容器。Kubernetes 通常不会将工作调度到 control-plane 节点上,以确保用户工作负载不会损害集群的整体运行。
您可以使用 kubectl describe 命令获取有关特定节点(如 kube1)的更多信息:
$ kubectl describe nodes kube1
首先,您会看到节点的基本信息:
Name: kube1
Role:
Labels: beta.kubernetes.io/arch=arm
beta.kubernetes.io/os=linux
kubernetes.io/hostname=node-1
您可以看到此节点正在运行 Linux 操作系统,使用的是 ARM 处理器。
接下来,您会看到关于 kube1 本身操作的信息(本输出已删除日期以简明起见):
Conditions:
Type Status ... Reason Message
----- ------ ------ -------
NetworkUnavailable False ... FlannelIsUp Flannel...
MemoryPressure False ... KubeletHasSufficientMemory kubelet...
DiskPressure False ... KubeletHasNoDiskPressure kubelet...
PIDPressure False ... KubeletHasSufficientPID kubelet...
Ready True ... KubeletReady kubelet...
这些状态显示节点具有足够的磁盘和内存空间,并向 Kubernetes 主节点报告其健康状态。接下来,有关机器容量的信息:
Capacity:
alpha.kubernetes.io/nvidia-gpu: 0
cpu: 4
memory: 882636Ki
pods: 110
Allocatable:
alpha.kubernetes.io/nvidia-gpu: 0
cpu: 4
memory: 882636Ki
pods: 110
然后是有关节点上软件的信息,包括正在运行的 Docker 版本、Kubernetes 和 Linux 内核的版本等:
System Info:
Machine ID: 44d8f5dd42304af6acde62d233194cc6
System UUID: c8ab697e-fc7e-28a2-7621-94c691120fb9
Boot ID: e78d015d-81c2-4876-ba96-106a82da263e
Kernel Version: 4.19.0-18-amd64
OS Image: Debian GNU/Linux 10 (buster)
Operating System: linux
Architecture: amd64
Container Runtime Version: containerd://1.4.12
Kubelet Version: v1.22.4
Kube-Proxy Version: v1.22.4
PodCIDR: 10.244.1.0/24
PodCIDRs: 10.244.1.0/24
最后,有关当前在此节点上运行的 Pods 的信息:
Non-terminated Pods: (3 in total)
Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits
--------- ---- ------------ ---------- --------------- -------------
kube-system kube-dns... 260m (6%) 0 (0%) 140Mi (16%) 220Mi (25%)
kube-system kube-fla... 0 (0%) 0 (0%) 0 (0%) 0 (0%)
kube-system kube-pro... 0 (0%) 0 (0%) 0 (0%) 0 (0%)
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.
CPU Requests CPU Limits Memory Requests Memory Limits
------------ ---------- --------------- -------------
260m (6%) 0 (0%) 140Mi (16%) 220Mi (25%)
No events.
从此输出中,您可以看到节点上的 Pods(例如为集群提供 DNS 服务的 kube-dns Pod)、每个 Pod 从节点请求的 CPU 和内存,以及所请求的总资源。值得注意的是,Kubernetes 跟踪每个 Pod 请求的资源和上限。关于请求和限制之间的差异在 第五章 中有详细描述,但简而言之,Pod 请求的资源保证在节点上存在,而 Pod 的限制是 Pod 可以消耗的给定资源的最大量。如果 Pod 的限制高于其请求,则额外的资源将按最佳努力原则提供。不能保证这些资源在节点上存在。
集群组件
Kubernetes 的一个有趣的方面是,构成 Kubernetes 集群的许多组件实际上是使用 Kubernetes 自身部署的。我们将介绍其中一些。这些组件使用了我们后面章节将介绍的多个概念。所有这些组件都在 kube-system 命名空间中运行。^(1)
Kubernetes Proxy
Kubernetes 代理负责将网络流量路由到 Kubernetes 集群中负载均衡的服务。为了完成其工作,代理必须存在于集群中的每个节点上。Kubernetes 有一个名为 DaemonSet 的 API 对象,你将在第十一章中学习,许多集群都使用它来完成这项任务。如果你的集群使用 DaemonSet 运行 Kubernetes 代理,可以通过运行以下命令查看代理:
$ kubectl get daemonSets --namespace=kube-system kube-proxy
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR
kube-proxy 5 5 5 5 5 ... 45d
根据集群的设置方式,kube-proxy 的 DaemonSet 可能会有其他名称,或者可能根本不使用 DaemonSet。无论如何,kube-proxy 容器应该在集群中的所有节点上运行。
Kubernetes DNS
Kubernetes 还运行一个 DNS 服务器,为集群中定义的服务提供命名和发现功能。这个 DNS 服务器也作为集群中的一个复制服务运行。根据集群的大小,你可能会看到一个或多个运行在集群中的 DNS 服务器。DNS 服务作为一个 Kubernetes 部署运行,管理这些副本(可能也被命名为 coredns 或其他变种):
$ kubectl get deployments --namespace=kube-system core-dns
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
core-dns 1 1 1 1 45d
Kubernetes 还有一个执行 DNS 服务器负载平衡的 Kubernetes 服务:
$ kubectl get services --namespace=kube-system core-dns
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
core-dns 10.96.0.10 <none> 53/UDP,53/TCP 45d
这显示了集群的 DNS 服务地址为 10.96.0.10。如果你登录到集群中的一个容器中,你会看到这个地址已经被填写到了容器的 /etc/resolv.conf 文件中。
Kubernetes UI
如果你想在图形用户界面中可视化你的集群,大多数云提供商都会在其云的 GUI 中集成这样的可视化功能。如果你的云提供商没有提供这样的 UI,或者你更喜欢一个集群内的 GUI,可以安装一个由社区支持的 GUI。查看文档了解如何为这些集群安装仪表板。你也可以使用像 Visual Studio Code 这样的开发环境扩展来一览你的集群状态。
摘要
希望到目前为止你已经建立并运行了一个 Kubernetes 集群(或三个),并且使用了一些命令来探索你创建的集群。接下来,我们将花一些时间探索 CLI,以及教你如何掌握 kubectl 工具。在本书的其余部分,你将使用 kubectl 和你的测试集群来探索 Kubernetes API 中的各种对象。
^(1) 正如你将在下一章中学到的,Kubernetes 中的命名空间是用来组织 Kubernetes 资源的实体。你可以把它想象成文件系统中的一个文件夹。
第四章:常用 kubectl 命令
kubectl命令行实用程序是一个强大的工具,在接下来的章节中,您将使用它来创建对象并与 Kubernetes API 进行交互。然而,在此之前,先了解适用于所有 Kubernetes 对象的基本kubectl命令是有意义的。
命名空间
Kubernetes 使用命名空间来组织集群中的对象。您可以将每个命名空间视为一个包含一组对象的文件夹。默认情况下,kubectl命令行工具与default命名空间交互。如果您想使用不同的命名空间,可以传递kubectl``--namespace标志。例如,kubectl --namespace=mystuff 引用 mystuff 命名空间中的对象。如果您感到简洁,还可以使用缩写 -n 标志。如果您想与所有命名空间交互——例如,列出集群中所有 Pod——可以传递 --all-namespaces 标志。
上下文
如果您想更长期地更改默认命名空间,可以使用上下文。这将记录在一个kubectl配置文件中,通常位于$HOME/.kube/config。这个配置文件还存储了如何找到和认证您的集群。例如,您可以使用以下命令为您的kubectl命令创建一个具有不同默认命名空间的上下文:
$ kubectl config set-context my-context --namespace=mystuff
这将创建一个新的上下文,但实际上并没有开始使用它。要使用这个新创建的上下文,您可以运行:
$ kubectl config use-context my-context
上下文还可以用于管理不同的集群或不同用户,通过使用set-context命令的--users或--clusters标志进行认证。
查看 Kubernetes API 对象
Kubernetes 中的所有内容都由 RESTful 资源表示。在本书中,我们将这些资源称为Kubernetes 对象。每个 Kubernetes 对象存在于唯一的 HTTP 路径;例如,https://your-k8s.com/api/v1/namespaces/default/pods/my-pod 指向默认命名空间中名为my-pod的 Pod 的表示。kubectl命令通过这些 URL 向这些路径发出 HTTP 请求,以访问驻留在这些路径上的 Kubernetes 对象。
通过kubectl查看 Kubernetes 对象的最基本命令是get。如果您运行kubectl get <*resource-name*>,您将获得当前命名空间中所有资源的列表。如果您想获取特定资源,可以使用kubectl get <*resource-name*> <*obj-name*>。
默认情况下,kubectl使用人类可读的打印机来查看来自 API 服务器的响应,但这种人类可读的打印机会移除对象的许多细节以适应每个终端行。要获取稍多一些信息的一种方法是添加-o wide标志,这会在较长的行上提供更多细节。如果您想查看完整的对象,还可以使用-o json或-o yaml标志查看对象的原始 JSON 或 YAML 格式。
用于操作kubectl输出的常见选项是移除标题,这在将kubectl与 Unix 管道结合使用时通常很有用(例如,kubectl ... | awk ...)。如果指定--no-headers标志,kubectl将跳过人类可读表格顶部的标题。
另一个常见的任务是从对象中提取特定字段。kubectl使用 JSONPath 查询语言来选择返回对象中的字段。 JSONPath 的完整细节超出了本章的范围,但作为示例,此命令将提取并打印指定 Pod 的 IP 地址:
$ kubectl get pods my-pod -o jsonpath --template={.status.podIP}
您还可以通过使用逗号分隔的类型列表查看不同类型的多个对象,例如:
$ kubectl get pods,services
这将显示给定命名空间的所有 Pod 和服务。
如果您对特定对象的更详细信息感兴趣,请使用describe命令:
$ kubectl describe <*resource-name*> <*obj-name*>
这将提供对象的丰富多行人类可读描述,以及 Kubernetes 集群中任何其他相关对象和事件。
如果您想查看每种支持类型的 Kubernetes 对象的受支持字段列表,可以使用explain命令:
$ kubectl explain pods
有时候,您可能希望持续观察特定 Kubernetes 资源的状态,以便在发生更改时看到资源的变化。例如,您可能正在等待应用程序重新启动。 --watch标志可以启用此功能。您可以将此标志添加到任何kubectl get命令中,以持续监视特定资源的状态。
创建、更新和销毁 Kubernetes 对象
Kubernetes API 中的对象以 JSON 或 YAML 文件表示。这些文件可以由服务器作为响应查询返回,也可以作为 API 请求的一部分发布到服务器上。您可以使用这些 YAML 或 JSON 文件在 Kubernetes 服务器上创建、更新或删除对象。
假设您在obj.yaml中有一个简单的对象存储。您可以使用kubectl通过运行以下命令在 Kubernetes 中创建此对象:
$ kubectl apply -f obj.yaml
注意您不需要指定对象的资源类型;它从对象文件本身获取。
类似地,在对对象进行更改后,您可以再次使用apply命令来更新对象:
$ kubectl apply -f obj.yaml
apply工具仅会修改与集群中当前对象不同的对象。如果您要创建的对象已经存在于集群中,则它将简单地成功退出而不进行任何更改。这使其在希望确保集群状态与文件系统状态匹配的循环中非常有用。您可以重复使用apply来调和状态。
如果您想查看apply命令在不实际进行更改的情况下将要执行的操作,可以使用--dry-run标志将对象打印到终端而不实际发送到服务器。
注意
如果您希望进行交互式编辑而不是编辑本地文件,您可以使用edit命令,该命令将下载最新的对象状态,然后启动包含定义的编辑器:
$ kubectl edit <*resource-name*> <*obj-name*>
保存文件后,它将自动上传回 Kubernetes 集群。
apply命令还将先前配置的历史记录在对象内的注释中记录下来。您可以使用edit-last-applied、set-last-applied和view-last-applied命令来操作这些记录。例如:
$ kubectl apply -f myobj.yaml view-last-applied
将显示应用于对象的最后状态。
当您想要删除一个对象时,您可以简单地运行:
$ kubectl delete -f obj.yaml
使用 kubectl 删除对象时不会提示确认删除。一旦执行命令,对象将被删除。
同样,您可以使用资源类型和名称来删除对象:
$ kubectl delete <*resource-name*> <*obj-name*>
对象标记和注释
标签和注释是您对象的标记。我们将在第六章中讨论它们的差异,但现在,您可以使用label和annotate命令更新任何 Kubernetes 对象上的标签和注释。例如,要向名为bar的 Pod 添加color=red标签,您可以运行:
$ kubectl label pods bar color=red
注释的语法是相同的。
默认情况下,label和annotate不允许覆盖现有标签。要实现此目的,您需要添加--overwrite标志。
如果要删除标签,您可以使用<label-name>-语法:
$ kubectl label pods bar color-
这将从名为bar的 Pod 中删除color标签。
调试命令
kubectl还提供了许多用于调试您的容器的命令。您可以使用以下命令查看正在运行容器的日志:
$ kubectl logs <*pod-name*>
如果您的 Pod 中有多个容器,您可以使用-c标志选择要查看的容器。
默认情况下,kubectl logs列出当前日志并退出。如果您希望连续将日志流式传输回终端而不退出,则可以添加-f(跟随)命令行标志。
您还可以使用exec命令在运行的容器中执行命令:
$ kubectl exec -it <*pod-name*> -- bash
这将为您提供一个在运行的容器内的交互式 shell,以便您可以执行更多调试。
如果您的容器内没有 bash 或其他终端可用,您始终可以attach到正在运行的进程:
$ kubectl attach -it *<pod-name>*
attach命令类似于kubectl logs,但允许您将输入发送到运行的进程,假设该进程已设置为从标准输入读取。
您还可以使用cp命令在容器之间复制文件:
$ kubectl cp <*pod-name>:</path/to/remote/file> </path/to/local/file>*
这将从正在运行的容器中复制文件到您的本地机器。您还可以指定目录,或者颠倒语法以从本地机器复制文件到容器外。
如果您想通过网络访问您的 Pod,您可以使用port-forward命令将本地机器上的网络流量转发到 Pod。这使您能够安全地通过隧道传输网络流量到可能不会在公共网络上暴露的容器中。例如,以下命令:
$ kubectl port-forward *<pod-name>* 8080:80
打开一个连接,将本地机器上 8080 端口的流量转发到远程容器的 80 端口。
注意
您还可以使用port-forward命令通过指定services/*<service-name>*而不是*<pod-name>*与服务一起使用。但请注意,如果您对服务进行端口转发,请求将只转发到该服务中的单个 Pod。它们不会经过服务负载均衡器。
如果您想查看 Kubernetes 事件,您可以使用kubectl get events命令查看给定命名空间中所有对象的最新 10 个事件列表:
$ kubectl get events
您还可以通过在kubectl get events命令中添加--watch来实时查看事件。您可能还希望添加-A以查看所有命名空间中的事件。
最后,如果您对集群如何使用资源感兴趣,您可以使用top命令查看正在使用的节点或 Pod 资源列表。此命令:
$ kubectl top nodes
将显示节点使用的总 CPU 和内存,以绝对单位(例如,核心)和可用资源的百分比(例如,总核心数)来计算。同样,此命令:
$ kubectl top pods
显示所有 Pod 及其资源使用情况。默认情况下,它只显示当前命名空间中的 Pod,但您可以添加--all-namespaces标志以查看集群中所有 Pod 的资源使用情况。
这些top命令仅在集群中运行有指标服务器时才有效。几乎每个托管的 Kubernetes 环境和许多非托管环境中都存在指标服务器。但如果这些命令失败,可能是因为您需要安装一个指标服务器。
集群管理
kubectl工具也可以用来管理集群本身。人们在管理集群时最常见的操作是针对特定节点进行关闭和排空。当您cordon一个节点时,您阻止将来的 Pod 被调度到该节点上。当您drain一个节点时,您会移除当前在该节点上运行的任何 Pod。这些命令的一个典型用例是移除需要维修或升级的物理机器。在这种情况下,您可以先使用kubectl cordon,然后再使用kubectl drain安全地将该机器从集群中移除。一旦机器修复完成,您可以使用kubectl uncordon重新启用将 Pod 调度到该节点上。并没有undrain命令;Pods 会自然地被调度到空节点上,因为它们被创建。对于影响节点的快速操作(例如,机器重新启动),通常不需要进行 cordon 或 drain 操作;只有当机器将长时间停止服务时,您才需要将 Pods 移动到其他机器。
命令自动补全
kubectl 支持与您的 shell 集成,以启用命令和资源的选项补全。根据您的环境,可能需要先安装 bash-completion 包,然后再激活命令自动补全。您可以使用适当的包管理器执行此操作:
# macOS
$ brew install bash-completion
# CentOS/Red Hat
$ yum install bash-completion
# Debian/Ubuntu
$ apt-get install bash-completion
在 macOS 上安装时,请确保按照 brew 的说明操作,以启用使用你的 ${HOME}/.bash_profile 进行选项补全的方法。
安装了 bash-completion 后,您可以通过以下方式暂时激活终端的自动补全:
$ source <(kubectl completion bash)
要使这对每个终端自动完成,请将其添加到您的 ${HOME}/.bashrc 文件中:
$ echo "source <(kubectl completion bash)" >> ${HOME}/.bashrc
如果您使用 zsh,可以在网上找到类似的 说明。
查看集群的其他方法
除了 kubectl,还有其他用于与 Kubernetes 集群交互的工具。例如,有几个编辑器的插件可以将 Kubernetes 与编辑器环境集成,包括:
如果您正在使用托管的 Kubernetes 服务,大多数服务还提供了集成到他们基于 Web 的用户体验中的 Kubernetes 图形界面。公共云中的托管 Kubernetes 还集成了复杂的监控工具,帮助您深入了解应用程序的运行情况。
Kubernetes 还有几个开源图形界面,包括 Rancher Dashboard 和 Headlamp 项目。
总结
kubectl 是管理 Kubernetes 集群中应用程序的强大工具。本章展示了该工具的许多常见用法,但 kubectl 提供了大量的内置帮助。您可以从以下方式开始查看此帮助:
$ kubectl help
或者:
$ kubectl help *<command-name>*
第五章:Pods
在早些章节中,我们讨论了如何将应用程序容器化,但在容器化应用程序的实际部署中,通常希望将多个应用程序放置在一个原子单元中,安排到单个机器上。
典型的部署示例如图 Figure 5-1 所示,其中包括一个用于处理 web 请求的容器和一个用于将文件系统与远程 Git 仓库同步的容器。

图 5-1. 一个包含两个容器和共享文件系统的示例 Pod
起初,将 web 服务器和 Git 同步器包装到单个容器中似乎很诱人。然而,经过更仔细的检查,分离的原因变得很明显。首先,两个容器在资源使用方面有显著不同的要求。例如,内存方面:因为 web 服务器正在为用户请求提供服务,我们希望确保它始终可用和响应迅速。另一方面,Git 同步器并不是面向用户的,其服务质量是“尽力而为”。
假设我们的 Git 同步器存在内存泄漏问题。我们需要确保 Git 同步器不能使用我们想要为 web 服务器使用的内存,因为这可能影响性能甚至导致服务器崩溃。
这种资源隔离正是容器设计要实现的目标。通过将两个应用程序分开到两个独立的容器中,我们可以确保 web 服务器的可靠运行。
当然,这两个容器是相互依存的;在同一台机器上安排 web 服务器和 Git 同步器是没有意义的。因此,Kubernetes 将多个容器组合成一个称为 Pod 的原子单元。(这个名字与 Docker 容器的鲸鱼主题一致,因为 Pod 也是鲸鱼的一群。)
注意
尽管最初在 Kubernetes 中将多个容器分组到单个 Pod 中似乎颇具争议或令人困惑,但随后被多种不同的应用程序采纳,以部署其基础设施。例如,几种服务网格实现使用第二个 sidecar 容器来将网络管理注入应用程序的 Pod 中。
Kubernetes 中的 Pods
一个 Pod 是运行在同一执行环境中的应用程序容器和卷的集合。在 Kubernetes 集群中,Pods 而不是容器是最小的可部署构件。这意味着 Pod 中的所有容器总是运行在同一台机器上。
Pod 中的每个容器都在自己的 cgroup 中运行,但它们共享多个 Linux 命名空间。
在同一个 Pod 中运行的应用程序共享相同的 IP 地址和端口空间(网络命名空间),拥有相同的主机名(UTS 命名空间),并可以使用 System V IPC 或 POSIX 消息队列(IPC 命名空间)进行本地进程间通信。然而,不同 Pod 中的应用程序是相互隔离的;它们拥有不同的 IP 地址、主机名等。在同一节点上运行的不同 Pods 中的容器实际上可能位于不同的服务器上。
用 Pods 思考
当人们在采用 Kubernetes 时,最常见的问题之一是“我应该在一个 Pod 中放什么?”
有时人们看到 Pods 并认为:“啊哈!一个 WordPress 容器和一个 MySQL 数据库容器结合在一起形成一个 WordPress 实例。它们应该放在同一个 Pod 中。” 然而,这种类型的 Pod 实际上是 Pod 构建的反模式示例。这有两个原因。首先,WordPress 和它的数据库并不是真正共生的。如果 WordPress 容器和数据库容器落在不同的机器上,它们仍然可以通过网络连接有效地进行通信。其次,你并不一定希望将 WordPress 和数据库作为一个单元进行扩展。WordPress 本身大部分是无状态的,因此你可能希望根据前端负载来扩展 WordPress 前端,创建更多的 WordPress Pods。扩展 MySQL 数据库要复杂得多,你更有可能增加单个 MySQL Pod 的资源。如果你将 WordPress 和 MySQL 容器放在同一个 Pod 中,你将被迫使用相同的扩展策略,这并不合适。
一般来说,在设计 Pods 时要问自己的正确问题是:“如果这些容器落在不同的机器上,它们是否能正常工作?” 如果答案是否定的,将容器放在同一个 Pod 中是正确的选择。如果答案是肯定的,使用多个 Pods 可能是正确的解决方案。在本章开头的示例中,这两个容器通过本地文件系统进行交互。如果这些容器被调度到不同的机器上,它们将无法正确运行。
在本章的剩余部分中,我们将描述如何在 Kubernetes 中创建、审视、管理和删除 Pods。
Pod 清单
Pods 在一个 Pod 清单 中描述,这只是 Kubernetes API 对象的文本文件表示。Kubernetes 强烈信奉 声明式配置,这意味着你在配置文件中写下世界的期望状态,然后将该配置提交给一个服务,该服务会采取行动确保期望状态成为实际状态。
注意
声明性配置不同于命令式配置,在后者中,您只需执行一系列操作(例如apt-get install foo)来修改系统状态。多年的生产经验告诉我们,保持系统期望状态的书面记录可以使系统更易管理、更可靠。声明性配置有诸多优势,如使配置能够进行代码审查,并为分布式团队记录系统的当前状态。此外,它是 Kubernetes 中所有自我修复行为的基础,这些行为能够使应用程序在无用户干预的情况下保持运行。
Kubernetes API 服务器在将 Pod 清单存储到持久存储(etcd)之前接受并处理它们。调度器还使用 Kubernetes API 查找尚未调度到节点的 Pod。然后,根据 Pod 清单中表达的资源和其他约束条件,将 Pod 放置在节点上。调度器可以将多个 Pod 放置在同一台机器上,只要资源充足。然而,将同一应用程序的多个副本调度到同一台机器上对可靠性不利,因为机器是单一故障域。因此,Kubernetes 调度器尝试确保同一应用程序的 Pod 被分布到不同的机器上,以增强在这种故障情况下的可靠性。一旦调度到节点上,Pod 就不会移动,必须显式销毁和重新调度。
可以通过重复此处描述的工作流部署 Pod 的多个实例。但是,复制集(第九章)更适合运行 Pod 的多个实例。(事实证明它们在运行单个 Pod 时也更好,但我们稍后再详细介绍。)
创建 Pod
创建 Pod 的最简单方式是通过命令式的kubectl run命令。例如,要运行我们的同一个kuard服务器,请使用:
$ kubectl run kuard --generator=run-pod/v1 \
--image=gcr.io/kuar-demo/kuard-amd64:blue
您可以通过运行以下命令查看此 Pod 的状态:
$ kubectl get pods
最初,您可能会看到容器状态为Pending,但最终您将看到它转换为Running,这意味着 Pod 及其容器已成功创建。
目前,您可以通过运行以下命令删除此 Pod:
$ kubectl delete pods/kuard
现在我们将继续手动编写完整的 Pod 清单。
创建 Pod 清单
您可以使用 YAML 或 JSON 编写 Pod 清单,但通常更喜欢使用 YAML,因为它稍微更容易人工编辑并支持注释。 Pod 清单(以及其他 Kubernetes API 对象)应该像对待源代码一样,而注释之类的内容有助于向新团队成员解释 Pod。
Pod 清单包括几个关键字段和属性:即描述 Pod 及其标签的metadata部分,描述卷的spec部分以及将在 Pod 中运行的容器列表。
在第二章中,我们使用以下 Docker 命令部署了kuard:
$ docker run -d --name kuard \
--publish 8080:8080 \
gcr.io/kuar-demo/kuard-amd64:blue
您也可以通过将 示例 5-1 写入名为 kuard-pod.yaml 的文件中,然后使用 kubectl 命令将该清单加载到 Kubernetes 中,从而实现类似的结果。
示例 5-1. kuard-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP
虽然最初以这种方式管理应用可能会显得更加繁琐,但是这种期望状态的书面记录在长期来看是最佳实践,特别是对于具有多个应用程序的大型团队。
正在运行的 Pods
在前一节中,我们创建了一个 Pod 清单,可用于启动运行 kuard 的 Pod。使用 kubectl apply 命令启动 kuard 的单个实例:
$ kubectl apply -f kuard-pod.yaml
Pod 清单将被提交到 Kubernetes API 服务器。然后,Kubernetes 系统将安排该 Pod 在集群中的健康节点上运行,其中 kubelet 守护进程将对其进行监控。如果您现在不理解 Kubernetes 的所有组成部分,不要担心;我们将在本书中更详细地讨论这些内容。
列出 Pods
现在我们有一个正在运行的 Pod,让我们去了解更多信息。使用 kubectl 命令行工具,我们可以列出在集群中运行的所有 Pods。目前,这应该只是我们在上一步创建的单个 Pod:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
kuard 1/1 Running 0 44s
您可以看到 Pod 的名称 (kuard),这是我们在前面的 YAML 文件中指定的。除了准备就绪的容器数量 (1/1) 外,输出还显示了状态、Pod 重新启动次数以及 Pod 的年龄。
如果在 Pod 创建后立即运行此命令,您可能会看到:
NAME READY STATUS RESTARTS AGE
kuard 0/1 Pending 0 1s
Pending 状态表示 Pod 已被提交但尚未安排。如果发生更大的错误,例如尝试创建一个不存在容器镜像的 Pod,它也将在状态字段中列出。
注意
默认情况下,kubectl 命令行工具在报告信息时是简洁的,但您可以通过命令行标志获取更多信息。在任何 kubectl 命令中添加 -o wide 将会打印出略多一些的信息(同时保持信息在单行)。添加 -o json 或 -o yaml 将分别以 JSON 或 YAML 格式打印完整的对象。如果您想看到 kubectl 正在执行的详尽详细日志记录,可以添加 --v=10 标志进行全面的日志记录,尽管这会牺牲可读性。
Pod 详细信息
有时,单行视图不足以说明问题,因为它过于简洁。此外,Kubernetes 在事件流中维护了许多关于 Pods 的事件,这些事件并未附加到 Pod 对象上。
要了解有关 Pod(或任何 Kubernetes 对象)的更多信息,您可以使用 kubectl describe 命令。例如,要描述我们之前创建的 Pod,您可以运行:
$ kubectl describe pods kuard
这将在不同的部分输出有关 Pod 的大量信息。顶部是关于 Pod 的基本信息:
Name: kuard
Namespace: default
Node: node1/10.0.15.185
Start Time: Sun, 02 Jul 2017 15:00:38 -0700
Labels: <none>
Annotations: <none>
Status: Running
IP: 192.168.199.238
Controllers: <none>
然后是关于在 Pod 中运行的容器的信息:
Containers:
kuard:
Container ID: docker://055095...
Image: gcr.io/kuar-demo/kuard-amd64:blue
Image ID: docker-pullable://gcr.io/kuar-demo/kuard-amd64@sha256:a580...
Port: 8080/TCP
State: Running
Started: Sun, 02 Jul 2017 15:00:41 -0700
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-cg5f5 (ro)
最后,与 Pod 相关的事件,例如其被调度的时间,其镜像被拉取的时间,以及如果/当由于健康检查失败而必须重新启动的时间:
Events:
Seen From SubObjectPath Type Reason Message
---- ---- ------------- -------- ------ -------
50s default-scheduler Normal Scheduled Success...
49s kubelet, node1 spec.containers{kuard} Normal Pulling pulling...
47s kubelet, node1 spec.containers{kuard} Normal Pulled Success...
47s kubelet, node1 spec.containers{kuard} Normal Created Created...
47s kubelet, node1 spec.containers{kuard} Normal Started Started...
删除 Pod
当需要删除一个 Pod 时,可以通过名称删除它:
$ kubectl delete pods/kuard
或者您可以使用创建它时使用的同一文件:
$ kubectl delete -f kuard-pod.yaml
当删除一个 Pod 时,它不会立即被终止。相反,如果运行 kubectl get pods,您会看到 Pod 处于Terminating状态。所有 Pod 都有一个终止宽限期。默认情况下,这是 30 秒。当 Pod 转换为Terminating状态时,它不再接收新请求。在服务场景中,宽限期对可靠性很重要,因为它允许 Pod 在终止之前完成正在处理的任何活动请求。
警告
删除一个 Pod 时,与该 Pod 相关的容器中存储的任何数据也将被删除。如果要在多个 Pod 实例之间持久保存数据,需要使用本章末尾描述的持久卷。
访问您的 Pod
现在您的 Pod 正在运行,出于各种原因您可能想要访问它。您可能想要加载运行在 Pod 中的 web 服务。您可能希望查看其日志以调试您正在看到的问题,甚至在 Pod 内执行其他命令以帮助调试。以下各节详细介绍了您可以与运行在 Pod 中的代码和数据进行交互的各种方式。
使用日志获取更多信息
当您的应用程序需要调试时,能够比describe更深入地了解应用程序正在执行的操作是很有帮助的。Kubernetes 提供了两个命令用于调试正在运行的容器。kubectl logs 命令会从运行实例下载当前的日志:
$ kubectl logs kuard
添加 -f 标志将导致日志连续流式传输。
kubectl logs 命令始终尝试获取当前正在运行的容器的日志。添加 --previous 标志将获取先前容器实例的日志。例如,在容器启动时由于问题而持续重启时,这是有用的。
注意
虽然在生产环境中偶尔使用kubectl logs进行容器调试很有用,但通常最好使用日志聚合服务。有几种开源的日志聚合工具,如 Fluentd 和 Elasticsearch,以及众多的云日志提供商。这些日志聚合服务提供更大的日志存储容量和更长的日志持续时间,以及丰富的日志搜索和过滤功能。许多还提供从多个 Pod 聚合日志到单个视图的能力。
使用 exec 在您的容器中运行命令
有时日志不足以提供足够的信息,要真正确定发生了什么,您需要在容器本身的上下文中执行命令。为此,您可以使用:
$ kubectl exec kuard -- date
您还可以通过添加 -it 标志获得交互式会话:
$ kubectl exec -it kuard -- ash
复制文件到容器和从容器复制文件
在上一章中,我们展示了如何使用kubectl cp命令访问 Pod 中的文件。一般来说,将文件复制到容器是一种反模式。你真的应该将容器的内容视为不可变的。但偶尔这是停止出血并恢复服务健康的最直接方法,因为比构建、推送和部署新镜像更快。然而,一旦停止出血,立即进行镜像构建和部署非常重要,否则你可能会忘记你对容器做出的本地更改,并在后续的定期部署中覆盖它。
健康检查
当你将应用程序作为一个容器在 Kubernetes 中运行时,它会自动通过进程健康检查保持活动状态。这种健康检查简单地确保你的应用程序的主进程一直在运行。如果不是,Kubernetes 将重新启动它。
然而,在大多数情况下,简单的进程检查是不够的。例如,如果你的进程陷入了死锁并且无法响应请求,进程健康检查仍会认为你的应用程序是健康的,因为它的进程仍在运行。
为了解决这个问题,Kubernetes 引入了应用程序存活性健康检查。存活性健康检查运行应用程序特定的逻辑,比如加载一个网页,来验证应用程序不仅仅是在运行,而且是正常运行的。由于这些存活性健康检查是应用程序特定的,你必须在你的 Pod 清单中定义它们。
存活探针
一旦kuard进程启动并运行,我们需要一种方法来确认它实际上是健康的,不应该重新启动。存活性探针是针对每个容器定义的,这意味着 Pod 中的每个容器都单独进行健康检查。在示例 5-2 中,我们为kuard容器添加了一个存活性探针,它对我们容器中的/healthy路径运行了一个 HTTP 请求。
示例 5-2. kuard-pod-health.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
livenessProbe:
httpGet:
path: /healthy
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
ports:
- containerPort: 8080
name: http
protocol: TCP
前面的 Pod 清单使用httpGet探针对kuard容器的 8080 端口上的/healthy端点执行 HTTP GET请求。该探针设置了initialDelaySeconds为5,因此在 Pod 中的所有容器创建后将在 5 秒后调用。探针必须在 1 秒的超时时间内响应,并且 HTTP 状态码必须大于或等于 200 且小于 400 才算是成功。Kubernetes 将每 10 秒调用一次该探针。如果连续失败的探针超过三次,容器将失败并重新启动。
你可以通过查看kuard状态页面来看到这个过程。使用这个清单创建一个 Pod,然后进行端口转发到该 Pod:
$ kubectl apply -f kuard-pod-health.yaml
$ kubectl port-forward kuard 8080:8080
将浏览器指向http://localhost:8080。点击“活跃性探测”选项卡。您应该看到一个列出此kuard实例接收到的所有探测器的表格。如果在该页面上点击“失败”链接,kuard将开始失败的健康检查。等待足够长的时间,Kubernetes 将重新启动容器。此时,显示将重置并重新开始。可以通过运行命令kubectl describe pods kuard找到重新启动的详细信息。 "事件"部分将包含类似以下文本:
Killing container with id docker://2ac946...:pod "kuard_default(9ee84...)"
container "kuard" is unhealthy, it will be killed and re-created.
注意
尽管对于失败的活跃性检查的默认响应是重新启动 Pod,但实际行为受 Pod 的restartPolicy管理。重启策略有三个选项:Always(默认),OnFailure(仅在活跃性失败或非零进程退出代码时重新启动),或Never。
就绪性探测
当然,活跃性不是我们想执行的唯一一种健康检查。Kubernetes 区分liveness和readiness。活跃性确定应用程序是否正常运行。未通过活跃性检查的容器将被重新启动。就绪性描述容器何时准备好为用户请求提供服务。未通过就绪性检查的容器将从服务负载均衡器中移除。就绪性探测与活跃性探测配置类似。我们在第七章详细探讨 Kubernetes 服务。
结合就绪性和活跃性探测有助于确保集群中只运行健康的容器。
启动探测
Startup probes 作为管理启动缓慢容器的替代方式,最近被引入到 Kubernetes 中。当一个 Pod 启动时,启动探测器在任何其他探测之前运行。启动探测器持续进行,直到超时(此时 Pod 将被重启)或成功为止,此时活跃性探测接管。启动探测器使您能够在缓慢启动容器时慢慢轮询,同时在缓慢启动容器初始化后进行响应灵活的活跃性检查。
高级探测器配置
Kubernetes 中的探测器有许多高级选项,包括在 Pod 启动后等待多长时间开始探测,认定多少次失败为真正失败,以及多少次成功必须重置失败计数。所有这些配置在未指定时接收默认值,但对于诸如固有不稳定或启动时间较长的应用程序等更高级用例可能是必要的。
其他类型的健康检查
除了 HTTP 检查之外,Kubernetes 还支持tcpSocket健康检查,打开 TCP 套接字;如果连接成功,则探测器成功。这种探测器样式适用于非 HTTP 应用程序,例如数据库或其他非基于 HTTP 的 API。
最后,Kubernetes 允许 exec 探针。这些在容器的上下文中执行脚本或程序。按照典型约定,如果此脚本返回零退出代码,则探针成功;否则,它失败。exec 脚本通常用于不适合简单 HTTP 调用的自定义应用程序验证逻辑。
资源管理
大多数人转向像 Kubernetes 这样的容器和编排器,因为它们在图像打包和可靠部署方面显著提升。除了简化分布式系统开发的面向应用程序的原语外,同样重要的是它们允许你增加构成集群的计算节点的总体利用率。无论是虚拟还是物理机器,操作的基本成本基本上是恒定的,无论它是空闲还是完全负载。因此,确保这些机器尽可能活跃,可以增加在基础设施上花费的每一美元的效率。
通常,我们用利用率指标来衡量这种效率。利用率定义为正在使用的资源量除以已购买的资源量。例如,如果你购买了一个单核机器,你的应用程序使用了十分之一的核心,那么你的利用率为 10%。通过像 Kubernetes 这样的调度系统管理资源打包,你可以将利用率提高到超过 50%。为了实现这一点,你必须告诉 Kubernetes 应用程序需要的资源,以便 Kubernetes 可以找到将容器最优地打包到机器上的方法。
Kubernetes 允许用户指定两种不同的资源指标。资源请求指定运行应用程序所需的最小资源量。资源限制指定应用程序可以消耗的最大资源量。让我们在接下来的章节中更详细地看看这些内容。
Kubernetes 识别许多不同的资源指定符号,从字面值(“12345”)到毫核(“100m”)。重要的是注意 MB/GB/PB 和 MiB/GiB/PiB 之间的区别。前者是熟悉的二次幂单位(例如,1 MB == 1,024 KB),而后者是十次幂单位(1 MiB == 1000 KiB)。
注意
错误的常见来源是通过小写的m指定毫单位,与通过大写的M指定兆单位相比。具体来说,“400m”是 0.4 MB,而不是 400Mb,这是一个显著的区别!
资源请求:最小要求资源
当一个 Pod 请求其容器运行所需的资源时,Kubernetes 保证这些资源对该 Pod 可用。最常请求的资源是 CPU 和内存,但 Kubernetes 也支持其他类型的资源,比如 GPU。例如,如果要求 kuard 容器在一个拥有半个 CPU 空闲和分配了 128 MB 内存的机器上运行,我们可以按照 示例 5-3 中定义的 Pod 进行设置。
示例 5-3. kuard-pod-resreq.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
resources:
requests:
cpu: "500m"
memory: "128Mi"
ports:
- containerPort: 8080
name: http
protocol: TCP
注意
资源请求是针对每个容器而不是每个 Pod 进行的。Pod 请求的总资源是所有容器中所有请求的资源的总和,因为不同的容器通常具有非常不同的 CPU 需求。例如,如果一个 Pod 包含一个 Web 服务器和数据同步器,Web 服务器面向用户,可能需要大量 CPU,而数据同步器可以使用很少的 CPU。
在将 Pod 调度到节点时使用请求。Kubernetes 调度器将确保节点上所有 Pod 的所有请求的总和不超过节点的容量。因此,当在节点上运行时,Pod 至少保证具有请求的资源。重要的是,“请求”指定了一个最小值。它并不指定 Pod 可能使用的资源的最大限制。为了探索其含义,让我们看一个例子。
假设一个容器的代码尝试使用所有可用的 CPU 核心。假设我们创建了一个此容器的 Pod,请求了 0.5 CPU。Kubernetes 将此 Pod 调度到一个总共有 2 CPU 核心的机器上。只要它是机器上唯一的 Pod,它将消耗所有 2.0 个可用的核心,尽管只请求了 0.5 CPU。
如果第二个具有相同容器和相同请求的 0.5 CPU 的 Pod 着陆在机器上,那么每个 Pod 将会收到 1.0 个核心。如果调度了第三个相同的 Pod,每个 Pod 将收到 0.66 个核心。最后,如果调度了第四个相同的 Pod,每个 Pod 将收到其请求的 0.5 个核心,并且节点将达到容量上限。
CPU 请求使用 Linux 内核中的 cpu-shares 功能实现。
注意
内存请求类似于 CPU,但有一个重要的区别。如果容器超过其内存请求,操作系统不能简单地从进程中移除内存,因为它已经被分配了。因此,当系统内存耗尽时,kubelet 将终止那些内存使用超过其请求内存的容器。这些容器会自动重启,但在机器上可用的内存少了,容器可以消耗的内存也减少了。
由于资源请求保证了 Pod 的资源可用性,因此在高负载情况下,它们对确保容器具有足够的资源至关重要。
通过限制资源使用
除了设置 Pod 所需的资源(以确保其最少可用资源),还可以通过资源限制设置其资源使用的最大值。
在我们之前的例子中,我们创建了一个 kuard Pod,它请求了最少 0.5 个核心和 128 MB 的内存。在 示例 5-4 中的 Pod 配置中,我们扩展了这个配置,添加了 1.0 个 CPU 和 256 MB 的内存的限制。
示例 5-4. kuard-pod-reslim.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
resources:
requests:
cpu: "500m"
memory: "128Mi"
limits:
cpu: "1000m"
memory: "256Mi"
ports:
- containerPort: 8080
name: http
protocol: TCP
当您在容器上设定限制时,内核会配置以确保消耗不会超过这些限制。例如,具有 0.5 核心 CPU 限制的容器将始终只能获得 0.5 核心,即使 CPU 处于空闲状态。具有 256 MB 内存限制的容器将不允许额外的内存使用;例如,如果其内存使用超过 256 MB,malloc 将会失败。
使用卷持久化数据
当 Pod 被删除或容器重新启动时,容器文件系统中的任何和所有数据也将被删除。这通常是一件好事,因为您不希望保留由无状态 Web 应用程序写入的杂物。在其他情况下,访问持久磁盘存储是健康应用程序的重要组成部分。Kubernetes 模型支持持久存储。
使用卷与 Pod
要向 Pod 清单添加卷,需要在我们的配置中添加两个新的部分。第一个是新的 spec.volumes 部分。此数组定义了 Pod 清单中容器可以访问的所有卷。需要注意的是,并非所有容器都需要挂载 Pod 中定义的所有卷。第二个增加的是容器定义中的 volumeMounts 数组。此数组定义了挂载到特定容器中的卷及每个卷应挂载到的路径。请注意,Pod 中的两个不同容器可以将同一卷挂载到不同的挂载路径上。
示例 5-5 中的清单定义了一个名为 kuard-data 的新卷,kuard 容器将其挂载到 /data 路径。
示例 5-5. kuard-pod-vol.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
volumes:
- name: "kuard-data"
hostPath:
path: "/var/lib/kuard"
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
volumeMounts:
- mountPath: "/data"
name: "kuard-data"
ports:
- containerPort: 8080
name: http
protocol: TCP
使用卷与 Pod 的不同方式
您可以在应用程序中使用数据的各种方式。以下是一些这些方式及 Kubernetes 推荐的模式:
通信/同步
在 Pod 的第一个示例中,我们看到两个容器使用共享卷来提供站点,并将其同步到远程 Git 位置(图 5-1)。为了实现这一点,Pod 使用了一个 emptyDir 卷。这样的卷仅限于 Pod 的生命周期,但可以在两个容器之间共享,为我们的 Git 同步和 Web 服务容器之间的通信奠定了基础。
缓存
应用程序可能会使用性能优越但不是应用程序正确运行所必需的卷。例如,应用程序可能会保留大图像的预渲染缩略图。当然,可以从原始图像重新构建这些缩略图,但这样做会增加提供缩略图的成本。您希望这样的缓存在由于健康检查失败导致容器重新启动时仍然存在,因此 emptyDir 也非常适合缓存使用场景。
持久化数据
有时您将使用卷来存储真正持久的数据,即与特定 Pod 寿命无关的数据,并且如果节点失败或 Pod 移动到不同的机器,则应在集群中移动。为了实现这一点,Kubernetes 支持各种远程网络存储卷,包括广泛支持的协议如 NFS 和 iSCSI,以及云提供商的网络存储如 Amazon Elastic Block Store、Azure File 和 Azure Disk,以及 Google 的持久磁盘。
挂载主机文件系统
其他应用程序实际上不需要持久卷,但它们确实需要访问底层主机文件系统的某些内容。例如,它们可能需要访问/dev文件系统,以便对系统上的设备进行原始块级访问。对于这些情况,Kubernetes 支持hostPath卷类型,该卷可以将工作节点上的任意位置挂载到容器中。示例 5-5 使用了hostPath卷类型。所创建的卷是主机上的/var/lib/kuard。
这里是使用 NFS 服务器的示例:
...
# Rest of pod definition above here
volumes:
- name: "kuard-data"
nfs:
server: my.nfs.server.local
path: "/exports"
持久卷是一个深入的主题。第十六章对该主题进行了更深入的探讨。
将所有内容整合在一起
许多应用程序是有状态的,因此我们必须保留任何数据并确保能够访问底层存储卷,无论应用程序在哪台机器上运行。正如我们之前所见,可以通过使用由网络附加存储支持的持久卷来实现这一点。我们还希望确保应用程序的健康实例始终运行,这意味着我们希望在将运行kuard的容器暴露给客户端之前,确保该容器准备就绪。
通过持久卷、就绪和存活探针以及资源限制的组合,Kubernetes 提供了运行有状态应用程序所需的一切。示例 5-6 将所有内容整合到一个清单中。
示例 5-6. kuard-pod-full.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
volumes:
- name: "kuard-data"
nfs:
server: my.nfs.server.local
path: "/exports"
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP
resources:
requests:
cpu: "500m"
memory: "128Mi"
limits:
cpu: "1000m"
memory: "256Mi"
volumeMounts:
- mountPath: "/data"
name: "kuard-data"
livenessProbe:
httpGet:
path: /healthy
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 30
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
本章的过程中,Pod 的定义已经增加。为您的应用程序添加的每个新功能也将为其定义添加一个新的部分。
总结
Pod 在 Kubernetes 集群中代表工作的原子单位。它们由一个或多个容器共同协作而组成。要创建一个 Pod,您需要编写一个 Pod 清单,并通过命令行工具将其提交给 Kubernetes API 服务器,或者(较少情况下)通过直接向服务器进行 HTTP 和 JSON 调用来提交。
当您向 API 服务器提交清单后,Kubernetes 调度程序会找到一个可以容纳 Pod 的机器,并将 Pod 调度到该机器上。在调度完成后,该机器上的kubelet守护程序负责创建与 Pod 对应的容器,并执行在 Pod 清单中定义的任何健康检查。
一旦将 Pod 调度到节点上,如果该节点失败,则不会重新调度。此外,要创建多个相同 Pod 的副本,您必须手动创建并命名它们。在第九章,我们介绍了 ReplicaSet 对象,并展示了如何自动创建多个相同的 Pod,并确保在节点机器故障时重新创建它们。
第六章:标签和注释
Kubernetes 旨在随着应用程序在规模和复杂性上的扩展而成长。标签和注释是 Kubernetes 中的基本概念,让您可以按照您对应用程序的思考方式来处理一组事物。您可以组织、标记和交叉索引所有资源,以表示对应用程序最有意义的组。
标签是可以附加到 Kubernetes 对象(如 Pods 和 ReplicaSets)的键/值对。它们可以是任意的,并且对于向 Kubernetes 对象附加标识信息非常有用。标签为对象分组提供了基础。
Annotations,另一方面,提供了一种类似标签的存储机制:键/值对设计,用于保存工具和库可以利用的非标识信息。与标签不同,注释并不用于查询、过滤或以其他方式区分 Pod 之间的不同。
标签
标签为对象提供标识元数据。这些是对象的基本特性,将用于分组、查看和操作。标签的动机源于谷歌在运行大型复杂应用程序方面的经验。从这些经验中得出了几个教训:
-
生产环境不容忍单例。在部署软件时,用户通常从单个实例开始。然而,随着应用程序的成熟,这些单例经常会增加并成为一组对象。考虑到这一点,Kubernetes 使用标签来处理对象集合,而不是单个实例。
-
系统强加的任何层次结构对许多用户来说都不够用。此外,用户的分组和层次结构随时间而变化。例如,用户可能最初认为所有应用程序都由许多服务组成。然而,随着时间的推移,一个服务可能会跨多个应用程序共享。Kubernetes 标签具有足够的灵活性来适应这些情况及更多。
深入了解谷歌如何处理生产系统的背景,请参阅 Site Reliability Engineering 一书,作者是 Betsy Beyer 等人(O’Reilly)。
标签具有简单的语法。它们是键/值对,其中键和值都由字符串表示。标签键可以分为两部分:可选的前缀和一个名称,用斜杠分隔。如果指定了前缀,则必须是一个 DNS 子域,长度不超过 253 个字符。键名是必需的,最大长度为 63 个字符。名称还必须以字母数字字符开头和结尾,并允许在字符之间使用破折号(-)、下划线(_)和点号(.)。
标签值是最多为 63 个字符的字符串。标签值的内容遵循与标签键相同的规则。表格 6-1 显示了一些有效的标签键和值。
表格 6-1 标签示例
| 键 | 值 |
|---|---|
acme.com/app-version |
1.0.0 |
appVersion |
1.0.0 |
app.version |
1.0.0 |
kubernetes.io/cluster-service |
true |
当标签和注释中使用域名时,它们预期与某个特定实体对齐。例如,一个项目可能会定义一组规范的标签,用于标识应用程序部署的各个阶段,如 staging、canary 和 production。或者云提供商可能会定义提供商特定的注释,扩展 Kubernetes 对象以激活其服务特定的功能。
应用标签
这里我们创建了几个部署(创建一组 Pods 的一种方式),并添加了一些有趣的标签。我们将两个应用(称为 alpaca 和 bandicoot),每个应用有两个环境和两个版本。
首先,创建 alpaca-prod 部署,并设置 ver、app 和 env 标签:
$ kubectl run alpaca-prod \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--replicas=2 \
--labels="ver=1,app=alpaca,env=prod"
接下来,创建 alpaca-test 部署,并设置 ver、app 和 env 标签为适当的值:
$ kubectl run alpaca-test \
--image=gcr.io/kuar-demo/kuard-amd64:green \
--replicas=1 \
--labels="ver=2,app=alpaca,env=test"
最后,创建两个 bandicoot 的部署。这里我们将环境命名为 prod 和 staging:
$ kubectl run bandicoot-prod \
--image=gcr.io/kuar-demo/kuard-amd64:green \
--replicas=2 \
--labels="ver=2,app=bandicoot,env=prod"
$ kubectl run bandicoot-staging \
--image=gcr.io/kuar-demo/kuard-amd64:green \
--replicas=1 \
--labels="ver=2,app=bandicoot,env=staging"
此时,你应该有四个部署——alpaca-prod、alpaca-test、bandicoot-prod 和 bandicoot-staging:
$ kubectl get deployments --show-labels
NAME ... LABELS
alpaca-prod ... app=alpaca,env=prod,ver=1
alpaca-test ... app=alpaca,env=test,ver=2
bandicoot-prod ... app=bandicoot,env=prod,ver=2
bandicoot-staging ... app=bandicoot,env=staging,ver=2
我们可以根据标签绘制一个基于 Venn 图的示意图(见 图 6-1)。

图 6-1. 我们部署的标签可视化
修改标签
在创建对象后,你也可以应用或更新标签:
$ kubectl label deployments alpaca-test "canary=true"
警告
这里有一个注意事项。在本例中,kubectl label 命令只会更改部署本身的标签;它不会影响部署创建的任何对象,比如 ReplicaSets 和 Pods。要更改这些对象,你需要修改部署中嵌入的模板(参见 第十章)。
你也可以使用 -L 选项来将 kubectl get 结果中的标签值显示为一列:
$ kubectl get deployments -L canary
NAME DESIRED CURRENT ... CANARY
alpaca-prod 2 2 ... <none>
alpaca-test 1 1 ... true
bandicoot-prod 2 2 ... <none>
bandicoot-staging 1 1 ... <none>
你可以通过应用一个带破折号后缀的标签来移除一个标签:
$ kubectl label deployments alpaca-test "canary-"
标签选择器
标签选择器用于根据一组标签过滤 Kubernetes 对象。选择器使用布尔表达式的简单语法。它们被最终用户(通过诸如 kubectl 的工具)和不同类型的对象(例如 ReplicaSet 如何与其 Pods 相关联)使用。
每个部署(通过 ReplicaSet)使用部署中嵌入的模板指定的标签创建一组 Pods。这由 kubectl run 命令配置。
运行 kubectl get pods 命令应该返回当前在集群中运行的所有 Pods。我们应该在我们的三个环境中总共有六个 kuard Pods:
$ kubectl get pods --show-labels
NAME ... LABELS
alpaca-prod-3408831585-4nzfb ... app=alpaca,env=prod,ver=1,...
alpaca-prod-3408831585-kga0a ... app=alpaca,env=prod,ver=1,...
alpaca-test-1004512375-3r1m5 ... app=alpaca,env=test,ver=2,...
bandicoot-prod-373860099-0t1gp ... app=bandicoot,env=prod,ver=2,...
bandicoot-prod-373860099-k2wcf ... app=bandicoot,env=prod,ver=2,...
bandicoot-staging-1839769971-3ndv ... app=bandicoot,env=staging,ver=2,...
注意
你可能会看到一个之前没见过的新标签:pod-template-hash。这个标签是由部署应用的,用于跟踪由哪些模板版本生成的 Pod。这使得部署可以清晰地管理更新,详细内容将在 第十章 中介绍。
如果我们只想列出具有 ver 标签设置为 2 的 Pods,我们可以使用 --selector 标志:
$ kubectl get pods --selector="ver=2"
NAME READY STATUS RESTARTS AGE
alpaca-test-1004512375-3r1m5 1/1 Running 0 3m
bandicoot-prod-373860099-0t1gp 1/1 Running 0 3m
bandicoot-prod-373860099-k2wcf 1/1 Running 0 3m
bandicoot-staging-1839769971-3ndv5 1/1 Running 0 3m
如果我们指定两个由逗号分隔的选择器,仅返回满足两个条件的对象。这是一个逻辑 AND 操作:
$ kubectl get pods --selector="app=bandicoot,ver=2"
NAME READY STATUS RESTARTS AGE
bandicoot-prod-373860099-0t1gp 1/1 Running 0 4m
bandicoot-prod-373860099-k2wcf 1/1 Running 0 4m
bandicoot-staging-1839769971-3ndv5 1/1 Running 0 4m
我们还可以询问一个标签是否属于一组值。在这里,我们要求所有具有 app 标签设置为 alpaca 或 bandicoot 的 Pod(这将是所有六个 Pod):
$ kubectl get pods --selector="app in (alpaca,bandicoot)"
NAME READY STATUS RESTARTS AGE
alpaca-prod-3408831585-4nzfb 1/1 Running 0 6m
alpaca-prod-3408831585-kga0a 1/1 Running 0 6m
alpaca-test-1004512375-3r1m5 1/1 Running 0 6m
bandicoot-prod-373860099-0t1gp 1/1 Running 0 6m
bandicoot-prod-373860099-k2wcf 1/1 Running 0 6m
bandicoot-staging-1839769971-3ndv5 1/1 Running 0 6m
最后,我们可以询问一个标签是否设置为任何内容。在这里,我们要求所有具有 canary 标签设置为任何内容的部署:
$ kubectl get deployments --selector="canary"
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
alpaca-test 1 1 1 1 7m
还有这些的“负”版本,如表格 6-2 所示。
表 6-2. 选择器运算符
| 运算符 | 描述 |
|---|---|
key=value |
key 被设置为 value |
key!=value |
key is not set to value |
key in (value1, value2) |
key 是 value1 或 value2 的之一 |
key notin (value1, value2) |
key 不是 value1 或 value2 的之一 |
key |
key 被设置 |
!key |
key 没有设置 |
例如,如果问一个键在本例中是 canary 是否没有设置,可以看作:
$ kubectl get deployments --selector='!canary'
你可以组合正面和负面选择器:
$ kubectl get pods -l 'ver=2,!canary'
标签选择器在 API 对象中
一个 Kubernetes 对象使用标签选择器来引用一组其他 Kubernetes 对象。与上一部分中描述的简单字符串不同,我们使用解析的结构。
出于历史原因(Kubernetes 不破坏 API 兼容性!),有两种形式。大多数对象支持一个更新、更强大的选择器运算符集。一个 app=alpaca,ver in (1, 2) 的选择器会被转换为:
selector:
matchLabels:
app: alpaca
matchExpressions:
- {key: ver, operator: In, values: [1, 2]}
这个示例使用紧凑的 YAML 语法。这是一个列表中的一个条目(matchExpressions),它是一个包含三个条目的映射。最后一个条目(values)具有一个值,这是一组包含两个项目的列表。所有的术语都被评估为逻辑 AND。表示 != 运算符的唯一方法是将其转换为单值的 NotIn 表达式。
旧的选择器指定形式(用于 ReplicationController 和服务)仅支持 = 运算符。= 运算符选择其键/值对集合都与对象匹配的目标对象。选择器 app=alpaca,ver=1 将被表示为:
selector:
app: alpaca
ver: 1
Kubernetes 架构中的标签
除了使用户能够组织其基础设施外,标签在连接各种相关的 Kubernetes 对象方面发挥了关键作用。Kubernetes 是一个有目的地解耦系统。没有层次结构,所有组件都独立操作。然而,在许多情况下,对象需要相互关联,这些关联由标签和标签选择器定义。
例如,ReplicaSets 通过选择器创建和维护多个 Pod 的副本,找到它们管理的 Pod。同样,服务负载均衡器通过选择器查询找到应将流量引导到哪些 Pod。当创建 Pod 时,它可以使用节点选择器标识可以调度到的特定节点集。当人们想要在其集群中限制网络流量时,他们使用网络策略与特定标签配合使用,以识别应允许或不允许彼此通信的 Pod。
标签是将 Kubernetes 应用程序紧密连接在一起的强大而普遍的粘合剂。尽管您的应用程序可能从简单的标签和查询开始,但您应该预期随着时间的推移,其规模和复杂性会不断增长。
注解
注解提供了一个存储 Kubernetes 对象的附加元数据的位置,其中元数据的唯一目的是辅助工具和库。它们是通过 API 驱动 Kubernetes 的其他程序存储对象的一些不透明数据的一种方式。注解可以用于工具本身或在外部系统之间传递配置信息。
虽然标签用于标识和分组对象,但注解用于提供关于对象来源、如何使用对象或关于对象策略的额外信息。存在重叠,何时使用注解或标签是一种品味问题。当存在疑问时,将信息作为注解添加到对象中,并在发现希望在选择器中使用它时将其提升为标签。
注解用于:
-
记录对象最新更新的“原因”。
-
向专门的调度器传达专门的调度策略。
-
扩展关于最后更新资源的工具及其更新方式的数据(用于检测其他工具的更改并进行智能合并)。
-
附加不适合标签的构建、发布或图像信息(可能包括 Git 哈希、时间戳、拉取请求编号等)。
-
启用 Deployment 对象(参见 第十章)来跟踪其管理的 ReplicaSets 以进行部署。
-
提供额外的数据以增强 UI 的视觉质量或可用性。例如,对象可以包含一个指向图标的链接(或图标的 base64 编码版本)。
-
在 Kubernetes 中原型化 alpha 功能(而不是创建一个一流的 API 字段,该功能的参数被编码在一个注解中)。
Kubernetes 中的各个地方都使用注解,主要用例是滚动部署。在滚动部署期间,注解用于跟踪部署状态并提供回滚到先前状态所需的必要信息。
避免将 Kubernetes API 服务器用作通用数据库。注解适用于与特定资源高度关联的小数据块。如果想要在 Kubernetes 中存储数据,但没有明显的对象可关联,考虑将该数据存储在其他更合适的数据库中。
注解键使用与标签键相同的格式。然而,由于它们经常用于在工具之间传递信息,键的“命名空间”部分更为重要。示例键包括deployment.kubernetes.io/revision或kubernetes.io/change-cause。
注解的值组件是一个自由格式的字符串字段。尽管这允许用户存储任意数据,因为这是任意文本,所以没有任何格式的验证。例如,将 JSON 文档编码为字符串并存储在注解中并不罕见。重要的是要注意,Kubernetes 服务器不了解注解值的所需格式。如果注解用于传递或存储数据,则无法保证数据有效性。这可能会使错误追踪变得更加困难。
注解在每个 Kubernetes 对象的公共metadata部分中定义:
...
metadata:
annotations:
example.com/icon-url: "https://example.com/icon.png"
...
警告
注解非常方便,提供了强大的松耦合,但需要谨慎使用,以避免数据混乱。
清理
清理本章中启动的所有部署非常容易:
$ kubectl delete deployments --all
如果您想更加选择性地删除部署,可以使用--selector标志来选择要删除的部署。
摘要
标签用于在 Kubernetes 集群中标识并可选地分组对象。它们还用于选择器查询中,以提供对象的灵活运行时分组,如 Pods。
注解提供了由自动化工具和客户端库使用的对象范围的键/值元数据存储。它们还可用于保存外部工具(如第三方调度器和监控工具)的配置数据。
标签和注解对于理解 Kubernetes 集群中关键组件如何协作以确保所需的集群状态至关重要。正确使用它们可以释放 Kubernetes 灵活性的真正威力,并为构建自动化工具和部署工作流提供起点。
第七章:服务发现
Kubernetes 是一个非常动态的系统。该系统参与将 Pod 放置在节点上,确保它们正常运行,并根据需要重新调度它们。有助于根据负载自动更改 Pod 数量的方法(例如水平 Pod 自动缩放参见[“ReplicaSet 的自动缩放”])。系统的 API 驱动特性鼓励其他人创建越来越高级别的自动化。
Kubernetes 的动态特性使得同时运行许多事物变得容易,但在寻找这些事物时却产生了问题。大多数传统的网络基础设施并不适用于 Kubernetes 所呈现的这种动态级别。
什么是服务发现?
这类问题和解决方案的通用名称是服务发现。服务发现工具有助于解决查找哪些进程在哪些地址上监听哪些服务的问题。一个好的服务发现系统将使用户能够快速可靠地解析此信息。一个好的系统还应该是低延迟的;客户端在与服务关联信息更改后很快就会更新。最后,一个好的服务发现系统可以存储关于该服务的更丰富的定义。例如,也许与服务相关联的有多个端口。
域名系统(DNS)是互联网上服务发现的传统系统。DNS 设计用于相对稳定的名称解析,具有广泛且高效的缓存。它是互联网的一个很好的系统,但在 Kubernetes 动态世界中存在不足。
不幸的是,许多系统(例如默认情况下的 Java)直接在 DNS 中查找名称并且不会重新解析它。这可能导致客户端缓存陈旧的映射并与错误的 IP 进行通信。即使有短暂的 TTL(生存时间)和良好行为的客户端,名称解析更改时客户端注意到之间存在自然延迟。在典型 DNS 查询中,能够返回的信息量和类型也有自然限制。超过 20 到 30 个地址(A 记录)对于单个名称会导致问题。服务(SRV)记录解决了一些问题,但通常很难使用。最后,客户端处理 DNS 记录中多个 IP 的方式通常是取第一个 IP 地址并依赖 DNS 服务器随机化或循环轮询记录的顺序。这不能替代更专门的负载平衡。
Service 对象
Kubernetes 中的真实服务发现始于一个 Service 对象。Service 对象是创建命名标签选择器的一种方式。正如我们将看到的,Service 对象还为我们提供了一些其他好处。
就像kubectl run命令是创建 Kubernetes 部署的简单方法一样,我们可以使用kubectl expose来创建一个服务。我们将在第十章详细讨论部署,但现在您可以将部署视为微服务的一个实例。让我们创建一些部署和服务,以便看看它们是如何工作的:
$ kubectl create deployment alpaca-prod \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--port=8080
$ kubectl scale deployment alpaca-prod --replicas 3
$ kubectl expose deployment alpaca-prod
$ kubectl create deployment bandicoot-prod \
--image=gcr.io/kuar-demo/kuard-amd64:green \
--port=8080
$ kubectl scale deployment bandicoot-prod --replicas 2
kubectl expose deployment bandicoot-prod
$ kubectl get services -o wide
NAME CLUSTER-IP ... PORT(S) ... SELECTOR
alpaca-prod 10.115.245.13 ... 8080/TCP ... app=alpaca
bandicoot-prod 10.115.242.3 ... 8080/TCP ... app=bandicoot
kubernetes 10.115.240.1 ... 443/TCP ... <none>
运行这些命令后,我们有了三个服务。我们刚刚创建的服务是alpaca-prod和bandicoot-prod。kubernetes服务会自动为您创建,以便您可以从应用程序内部找到并与 Kubernetes API 交互。
如果我们查看SELECTOR列,我们会发现alpaca-prod服务只是为选择器指定一个名称,并指定了与该服务通信的端口(在本例中为 8080)。kubectl expose命令将方便地从部署定义中提取标签选择器和相关端口。
此外,该服务被分配了一种新型虚拟 IP,称为集群 IP。这是系统将在所有由选择器标识的 Pod 之间进行负载均衡的特殊 IP 地址。
要与服务交互,我们将进行端口转发到一个alpaca Pod。执行此命令并在终端窗口中保持运行。您可以通过访问http://localhost:48858来查看端口转发的工作情况:
$ ALPACA_POD=$(kubectl get pods -l app=alpaca \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $ALPACA_POD 48858:8080
服务 DNS
因为集群 IP 是虚拟的,所以它是稳定的,并且适合分配 DNS 地址。客户端缓存 DNS 结果的所有问题都不再适用。在命名空间内,只需使用服务名称即可连接到由服务标识的 Pod 之一。
Kubernetes 为运行在集群中的 Pod 提供了一个 DNS 服务。该 Kubernetes DNS 服务在集群创建时作为系统组件安装。DNS 服务本身由 Kubernetes 管理,并且是 Kubernetes 构建在 Kubernetes 上的一个很好的例子。Kubernetes DNS 服务为集群 IP 提供 DNS 名称。
在kuard服务器状态页面上展开“DNS 查询”部分,您可以尝试这样做。查询alpaca-prod的 A 记录。输出应该类似于以下内容:
;; opcode: QUERY, status: NOERROR, id: 12071
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;alpaca-prod.default.svc.cluster.local. IN A
;; ANSWER SECTION:
alpaca-prod.default.svc.cluster.local. 30 IN A 10.115.245.13
这里的完整 DNS 名称是alpaca-prod.default.svc.cluster.local.。让我们来详细了解一下:
alpaca-prod
此服务的名称。
default
此服务所在的命名空间。
svc
确认这是一个服务。这使得 Kubernetes 可以在未来将其他类型的东西暴露为 DNS。
cluster.local.
集群的基本域名。这是大多数集群中的默认设置。管理员可以更改此设置,以允许跨多个集群使用唯一的 DNS 名称。
当引用自己命名空间中的服务时,您可以直接使用服务名称(alpaca-prod)。您还可以引用另一个命名空间中的服务,如alpaca-prod.default。当然,您也可以使用完全限定的服务名称(alpaca-prod.default.svc.cluster.local.)。请在 kuard 的“DNS 查询”部分尝试每一种情况。
就绪检查
通常,当应用程序首次启动时,它无法处理请求。通常需要一些初始化工作,可能需要不到一秒或几分钟。服务对象的一个好处是通过就绪检查跟踪哪些 Pod 是准备就绪的。让我们修改我们的部署,添加一个与 Pod 相关联的就绪检查,如我们在 第五章 中讨论的那样:
$ kubectl edit deployment/alpaca-prod
此命令将获取当前版本的 alpaca-prod 部署,并在编辑器中启动它。保存并退出编辑器后,它将将对象写回 Kubernetes。这是一种在不将对象保存到 YAML 文件中的情况下编辑对象的快速方法。
添加以下部分:
spec:
...
template:
...
spec:
containers:
...
name: alpaca-prod
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 2
initialDelaySeconds: 0
failureThreshold: 3
successThreshold: 1
这将设置此部署将创建的 Pod,以便通过在端口 8080 上的 HTTP GET到/ready进行就绪检查。此检查在 Pod 启动后立即开始,每两秒钟进行一次。如果连续三次检查失败,则认为该 Pod 不可用。但是,如果只有一次检查成功,则再次认为该 Pod 是就绪的。
只有准备就绪的 Pod 才会收到流量。
更新部署定义将删除并重新创建 alpaca Pods。因此,我们需要重新启动先前的 port-forward 命令:
$ ALPACA_POD=$(kubectl get pods -l app=alpaca-prod \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $ALPACA_POD 48858:8080
将您的浏览器指向 http://localhost:48858,您应该可以看到 kuard 实例的调试页面。展开“就绪探针”部分。每次系统进行新的就绪检查时,您应该看到此页面更新,这通常每两秒钟发生一次。
在另一个终端窗口上,对 alpaca-prod 服务的端点启动 watch 命令。端点是查找服务发送流量的更低级别方法,并将在本章后面进行介绍。这里的 --watch 选项导致 kubectl 命令挂起并输出任何更新。这是一种轻松查看 Kubernetes 对象随时间变化的方法:
$ kubectl get endpoints alpaca-prod --watch
现在返回到浏览器,点击就绪检查的“失败”链接。您应该看到服务器现在返回 500 的错误代码。这三次失败后,此服务器将从服务的端点列表中删除。点击“成功”链接并注意,在单个就绪检查后,端点将被重新添加。
这个就绪检查是过载或出现问题的服务器向系统发出信号,表明它不希望再接收流量。这是实现优雅关闭的一种好方法。服务器可以发出不再希望流量的信号,等待现有连接关闭,然后干净地退出。
按下 Ctrl-C 退出终端中的port-forward和watch命令。
超越集群范围
到目前为止,本章涵盖的内容都是关于在集群内部暴露服务。通常情况下,Pod 的 IP 只能在集群内部访问。但在某些时候,我们需要允许新的流量进入!
最便携的方法是使用称为 NodePorts 的功能,进一步增强了服务。除了一个集群 IP 外,系统还选择一个端口(或用户可以指定一个端口),然后集群中的每个节点都将流量转发到该端口的服务上。
有了这个功能,如果你能够访问集群中的任何节点,就可以联系到一个服务。即使不知道运行该服务的任何 Pod 所在的位置,也可以使用 NodePort。这可以与硬件或软件负载均衡器集成,以进一步暴露服务。
尝试修改alpaca-prod服务来测试一下:
$ kubectl edit service alpaca-prod
将spec.type字段改为NodePort。在使用kubectl expose创建服务时,也可以通过指定--type=NodePort来执行此操作。系统将分配一个新的 NodePort:
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
Annotations: <none>
Selector: app=alpaca
Type: NodePort
IP: 10.115.245.13
Port: <unset> 8080/TCP
NodePort: <unset> 32711/TCP
Endpoints: 10.112.1.66:8080,10.112.2.104:8080,10.112.2.105:8080
Session Affinity: None
No events.
在这里,我们看到系统分配端口 32711 给这个服务。现在我们可以访问集群中任何节点上的该端口来访问服务。如果你在同一网络上,可以直接访问。如果你的集群在云端某处,可以使用类似以下的 SSH 隧道:
$ ssh <*node*> -L 8080:localhost:32711
现在,如果你将浏览器指向 http://localhost:8080,你将连接到该服务。每个发送到服务的请求将随机分配给实现该服务的不同 Pod。重新加载页面几次,你会发现请求随机分配到不同的 Pod 上。
当完成后,退出 SSH 会话。
负载均衡器集成
如果你的集群配置了与外部负载均衡器集成,可以使用LoadBalancer类型。它在NodePort类型的基础上,进一步配置云端创建一个新的负载均衡器,并将其指向集群中的节点。大多数基于云的 Kubernetes 集群都支持负载均衡器集成,也有一些项目专门为常见的物理负载均衡器实现负载均衡器集成,尽管这些可能需要更多与集群的手动集成。
再次编辑alpaca-prod服务(kubectl edit service alpaca-prod),并将spec.type改为LoadBalancer。
注意
创建一个LoadBalancer类型的服务将该服务暴露给公共互联网。在执行此操作之前,请确保这是安全的,可以向全世界公开。我们将在本节进一步讨论安全风险。此外,第九章和第二十章提供了如何保护应用程序的指导。
如果立即运行 kubectl get services,您会看到 alpaca-prod 的 EXTERNAL-IP 列现在显示 <pending>。稍等片刻,您应该会看到云端为您分配的公共地址。您可以查看云账户的控制台,了解 Kubernetes 为您完成的配置工作:
$ kubectl describe service alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
Selector: app=alpaca
Type: LoadBalancer
IP: 10.115.245.13
LoadBalancer Ingress: 104.196.248.204
Port: <unset> 8080/TCP
NodePort: <unset> 32711/TCP
Endpoints: 10.112.1.66:8080,10.112.2.104:8080,10.112.2.105:8080
Session Affinity: None
Events:
FirstSeen ... Reason Message
--------- ... ------ -------
3m ... Type NodePort -> LoadBalancer
3m ... CreatingLoadBalancer Creating load balancer
2m ... CreatedLoadBalancer Created load balancer
现在我们看到 alpaca-prod 服务分配了 104.196.248.204 的地址。打开浏览器试试吧!
注意
此示例来自通过 GKE 在谷歌云平台上启动和管理的集群。负载均衡器的配置方式特定于云。一些云有基于 DNS 的负载均衡器(例如 AWS 弹性负载均衡 [ELB])。在这种情况下,您会看到一个主机名而不是 IP。根据云服务提供商的不同,负载均衡器可能需要一段时间才能完全运行。
创建一个基于云的负载均衡器可能需要一些时间。大多数云服务提供商可能需要几分钟,这一点不要感到惊讶。
到目前为止,我们看到的示例都使用了 外部 负载均衡器;也就是说,连接到公共互联网的负载均衡器。虽然这对于向世界公开服务很好,但通常您只想在内部网络中公开应用程序。为了实现这一点,请使用 内部 负载均衡器。不幸的是,由于对内部负载均衡器的支持是较近期添加到 Kubernetes 的,因此通过对象注解以某种临时方式实现。例如,在 Azure Kubernetes 服务集群中创建内部负载均衡器,您需要向您的 Service 资源添加注解 service.beta.kubernetes.io/azure-load-balancer-internal: "true"。以下是一些流行云服务的设置:
微软 Azure
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
亚马逊 Web 服务
service.beta.kubernetes.io/aws-load-balancer-internal: "true"
阿里云
service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type: "intranet"
谷歌云平台
cloud.google.com/load-balancer-type: "Internal"
当您向您的服务添加此注解时,它应该是这样的:
...
metadata:
...
name: some-service
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
...
当您使用这些注解之一创建服务时,将创建一个内部暴露的服务,而不是在公共互联网上的服务。
提示
还有几个注解可以扩展负载均衡器的行为,包括用于使用预先存在的 IP 地址的注解。您的提供商的具体扩展应该在其网站上有文档记录。
高级细节
Kubernetes 是一个可扩展的系统。因此,有一些层次可以支持更高级的集成。理解像服务这样复杂的概念的具体实现细节可能有助于您进行故障排除或创建更高级的集成。本节稍微深入了解这些。
终端点
一些应用程序(以及系统本身)希望能够使用服务而不使用集群 IP。这可以通过另一种类型的对象——Endpoints 对象来实现。对于每个 Service 对象,Kubernetes 创建一个对应的 Endpoints 对象,其中包含该服务的 IP 地址:
$ kubectl describe endpoints alpaca-prod
Name: alpaca-prod
Namespace: default
Labels: app=alpaca
Subsets:
Addresses: 10.112.1.54,10.112.2.84,10.112.2.85
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
<unset> 8080 TCP
No events.
要使用服务,高级应用程序可以直接与 Kubernetes API 通信以查找端点并调用它们。Kubernetes API 甚至具有“观察”对象并在它们发生变化时立即得到通知的能力。通过这种方式,客户端可以在服务关联的 IP 地址发生变化时立即做出反应。
让我们演示一下这一点。在终端窗口中启动以下命令并让其保持运行:
$ kubectl get endpoints alpaca-prod --watch
它将输出当前端点的当前状态,然后“挂起”:
NAME ENDPOINTS AGE
alpaca-prod 10.112.1.54:8080,10.112.2.84:8080,10.112.2.85:8080 1m
现在打开另一个终端窗口,并删除并重新创建支持 alpaca-prod 的部署:
$ kubectl delete deployment alpaca-prod
$ kubectl create deployment alpaca-prod \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--port=8080
$ kubectl scale deployment alpaca-prod --replicas=3
如果您回顾观察到的端点输出,您会发现随着删除和重新创建这些 Pods,命令的输出反映了与服务关联的最新一组 IP 地址集。您的输出将类似于以下内容:
NAME ENDPOINTS AGE
alpaca-prod 10.112.1.54:8080,10.112.2.84:8080,10.112.2.85:8080 1m
alpaca-prod 10.112.1.54:8080,10.112.2.84:8080 1m
alpaca-prod <none> 1m
alpaca-prod 10.112.2.90:8080 1m
alpaca-prod 10.112.1.57:8080,10.112.2.90:8080 1m
alpaca-prod 10.112.0.28:8080,10.112.1.57:8080,10.112.2.90:8080 1m
如果您正在编写从头开始在 Kubernetes 上运行的新代码,那么 Endpoints 对象非常适合您。但是大多数项目并不处于这种位置!大多数现有系统都建立在不经常更改的常规 IP 地址上,这些系统可以使用 Endpoints 对象。
手动服务发现
Kubernetes 服务是建立在对 Pods 的标签选择器之上的。这意味着即使完全不使用 Service 对象,您也可以使用 Kubernetes API 进行基本的服务发现!让我们演示一下。
使用 kubectl(通过 API)我们可以轻松地查看分配给我们示例部署中每个 Pod 的 IP 地址:
$ kubectl get pods -o wide --show-labels
NAME ... IP ... LABELS
alpaca-prod-12334-87f8h ... 10.112.1.54 ... app=alpaca
alpaca-prod-12334-jssmh ... 10.112.2.84 ... app=alpaca
alpaca-prod-12334-tjp56 ... 10.112.2.85 ... app=alpaca
bandicoot-prod-5678-sbxzl ... 10.112.1.55 ... app=bandicoot
bandicoot-prod-5678-x0dh8 ... 10.112.2.86 ... app=bandicoot
这很棒,但是如果您有大量的 Pods 怎么办?您可能希望基于部署中应用的标签进行过滤。让我们仅对 alpaca 应用程序这样做:
$ kubectl get pods -o wide --selector=app=alpaca
NAME ... IP ...
alpaca-prod-3408831585-bpzdz ... 10.112.1.54 ...
alpaca-prod-3408831585-kncwt ... 10.112.2.84 ...
alpaca-prod-3408831585-l9fsq ... 10.112.2.85 ...
现在,您已经掌握了服务发现的基础知识!您始终可以使用标签来识别您感兴趣的一组 Pods,获取所有这些标签的 Pods,并获取其 IP 地址。但是保持正确的标签集以同步使用可能会有些棘手。这就是为什么创建 Service 对象的原因。
kube-proxy 和集群 IP
集群 IP 是稳定的虚拟 IP,它负载平衡服务中所有端点的流量。这项魔术由集群中每个节点上运行的组件 kube-proxy 完成(见 图 7-1)。

图 7-1. 配置和使用集群 IP
在 图 7-1 中,kube-proxy 通过 API 服务器监视集群中的新服务。然后,它在主机内核中编程一组 iptables 规则,以重写数据包的目的地,使其指向该服务的一个端点。如果服务的端点集合发生更改(由于 Pods 的出现和消失或由于失败的就绪检查),则重写 iptables 规则集。
API 服务器在创建服务时通常会分配集群 IP 地址。但是,在创建服务时,用户可以指定特定的集群 IP。一旦设置,集群 IP 就不能在不删除和重新创建服务对象的情况下进行修改。
注意
使用 kube-apiserver 二进制文件上的 --service-cluster-ip-range 标志配置 Kubernetes 服务地址范围。服务地址范围不应与分配给每个 Docker 桥接或 Kubernetes 节点的 IP 子网和范围重叠。此外,任何显式请求的集群 IP 必须来自该范围,而不是已经在使用中。
集群 IP 环境变量
尽管大多数用户应该使用 DNS 服务来查找集群 IP,但仍然可能在使用一些旧的机制。其中之一是在 Pod 启动时将一组环境变量注入其中。
要看到这个过程,请查看 kuard 实例的 bandicoot 控制台。在终端中输入以下命令:
$ BANDICOOT_POD=$(kubectl get pods -l app=bandicoot \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $BANDICOOT_POD 48858:8080
现在,请将浏览器指向 http://localhost:48858 ,查看此服务器的状态页面。展开“服务器环境”部分,并注意 alpaca 服务的环境变量集。状态页面应显示类似于 Table 7-1 的表格。
表 7-1. 服务环境变量
| Key | Value |
|---|---|
ALPACA_PROD_PORT |
tcp://10.115.245.13:8080 |
ALPACA_PROD_PORT_8080_TCP |
tcp://10.115.245.13:8080 |
ALPACA_PROD_PORT_8080_TCP_ADDR |
10.115.245.13 |
ALPACA_PROD_PORT_8080_TCP_PORT |
8080 |
ALPACA_PROD_PORT_8080_TCP_PROTO |
tcp |
ALPACA_PROD_SERVICE_HOST |
10.115.245.13 |
ALPACA_PROD_SERVICE_PORT |
8080 |
使用的两个主要环境变量是 ALPACA_PROD_SERVICE_HOST 和 ALPACA_PROD_SERVICE_PORT。其他环境变量是为了与(现在已弃用的)Docker 链接变量兼容而创建的。
环境变量方法的一个问题是它要求资源按特定顺序创建。服务必须在引用它们的 Pod 之前创建。这在部署构成较大应用程序的一组服务时可能会引入相当多的复杂性。此外,对许多用户来说,仅仅使用环境变量似乎有些奇怪。因此,DNS 可能是一个更好的选择。
与其他环境连接
尽管在自己的集群中进行服务发现非常好,但实际上,许多真实世界的应用程序实际上需要您集成更多在 Kubernetes 中部署的云原生应用程序与部署在更传统环境中的应用程序。此外,您可能需要将在云中部署的 Kubernetes 集群与部署在本地的基础设施集成。这是 Kubernetes 的一个仍在进行大量探索和解决方案开发的领域。
连接到集群外的资源
当你连接 Kubernetes 到集群外的传统资源时,你可以使用无选择器服务来声明一个 Kubernetes 服务,其手动分配的 IP 地址位于集群外部。这样,通过 DNS 的 Kubernetes 服务发现就能如预期地工作,但网络流量本身会流向外部资源。要创建无选择器服务,你需要从你的资源中移除 spec.selector 字段,同时保留 metadata 和 ports 部分不变。因为你的服务没有选择器,所以不会自动添加端点到服务中。这意味着你必须手动添加它们。通常你将添加的端点是一个固定的 IP 地址(例如,你的数据库服务器的 IP 地址),因此你只需添加一次。但如果支持服务的 IP 地址发生更改,你需要更新相应的端点资源。要创建或更新端点资源,你可以使用类似以下的端点:
apiVersion: v1
kind: Endpoints
metadata:
# This name must match the name of your service
name: my-database-server
subsets:
- addresses:
# Replace this IP with the real IP of your server
- ip: 1.2.3.4
ports:
# Replace this port with the port(s) you want to expose
- port: 1433
将外部资源连接到集群内部的服务
将外部资源连接到 Kubernetes 服务内部有一些技巧。如果你的云服务提供商支持,最简单的方法是创建一个“内部”负载均衡器,就像上面描述的那样,它位于你的虚拟私有网络中,并且可以将流量从一个固定的 IP 地址传递到集群中。然后,你可以使用传统的 DNS 来使这个 IP 地址对外部资源可用。如果没有可用的内部负载均衡器,你可以使用 NodePort 服务在集群节点的 IP 地址上公开服务。然后,你可以编程一个物理负载均衡器来为这些节点提供服务,或者使用基于 DNS 的负载均衡来在节点之间分发流量。
如果以上两种解决方案对你的用例都不适用,更复杂的选项包括在外部资源上运行完整的 kube-proxy 并编程该机器使用 Kubernetes 集群中的 DNS 服务器。这样的设置要更难正确配置,实际上应该只在本地环境中使用。还有许多开源项目(例如 HashiCorp 的 Consul)可以用来管理集群内部和集群外部资源之间的连接。这些选项需要对网络和 Kubernetes 的知识有深入了解,真的应该作为最后的选择考虑。
清理工作
运行以下命令来清理本章创建的所有对象:
$ kubectl delete services,deployments -l app
概要
Kubernetes 是一个动态系统,挑战传统的命名和通过网络连接服务的方法。Service 对象提供了一种灵活而强大的方式来同时在集群内部和集群外部公开服务。通过本章介绍的技术,你可以将服务互相连接并将它们暴露到集群外部。
虽然在 Kubernetes 中使用动态服务发现机制会引入一些新概念,可能一开始看起来比较复杂,但理解和适应这些技术是解锁 Kubernetes 强大功能的关键。一旦你的应用程序能够动态地找到服务并响应这些应用程序的动态部署,你就可以不再担心事物的运行位置和移动时间。以逻辑方式思考服务,并让 Kubernetes 处理容器放置的细节是解决问题关键的一部分。
当然,服务发现只是应用程序与 Kubernetes 配合工作的开始。第八章 讨论了 Ingress 网络,专注于第 7 层(HTTP)负载均衡和路由,而 第十五章 是关于服务网格,这是最近在云原生网络中开发的一种方法,除了服务发现和负载均衡之外,还提供许多额外的功能。
第八章:使用 Ingress 进行 HTTP 负载均衡
任何应用程序的关键部分是将网络流量发送和接收到该应用程序。正如在第七章中描述的那样,Kubernetes 具有一套能力,使得服务能够在集群外部暴露。对于许多用户和简单的用例,这些能力是足够的。
但是 Service 对象在 OSI 模型中操作于第 4 层。^(1) 这意味着它仅转发 TCP 和 UDP 连接,并且不检查这些连接的内部内容。因此,在集群上托管多个应用程序时,会使用许多不同的暴露服务。对于那些type: NodePort的服务,您将不得不为每个服务连接到唯一端口。而对于那些type: LoadBalancer的服务,您将为每个服务分配(通常昂贵或稀缺的)云资源。但是对于基于 HTTP(第 7 层)的服务,我们可以做得更好。
当在非 Kubernetes 环境中解决类似问题时,用户通常会转向“虚拟主机”这一概念。这是一种在单个 IP 地址上托管多个 HTTP 站点的机制。通常,用户使用负载均衡器或反向代理接受 HTTP(80)和 HTTPS(443)端口的传入连接。该程序然后解析 HTTP 连接,并基于Host头和请求的 URL 路径,将 HTTP 调用代理到其他程序。通过这种方式,负载均衡器或反向代理将流量引导到正确的“上游”服务器进行解码和连接引导。
Kubernetes 将其基于 HTTP 的负载均衡系统称为Ingress。Ingress 是 Kubernetes 本地的一种实现“虚拟主机”模式的方式,正如我们刚刚讨论的那样。该模式的更复杂部分之一是用户必须管理负载均衡器配置文件。在动态环境中,并且随着虚拟主机集合的扩展,这可能会非常复杂。Kubernetes Ingress 系统通过(a)标准化该配置,(b)将其移到标准 Kubernetes 对象,并且(c)将多个 Ingress 对象合并为负载均衡器的单一配置,来简化这一过程。
典型的软件基础实现看起来像 图 8-1 所示。Ingress 控制器是由两部分组成的软件系统。第一部分是 Ingress 代理,使用 type: LoadBalancer 的服务在集群外部暴露此代理,将请求发送给“上游”服务器。另一部分是 Ingress 和解器或操作员。Ingress 操作员负责读取和监视 Kubernetes API 中的 Ingress 对象,并重新配置 Ingress 代理以按照 Ingress 资源中指定的方式路由流量。有许多不同的 Ingress 实现。在某些实现中,这两个组件合并到一个容器中;在其他情况下,它们是分开部署在 Kubernetes 集群中的不同组件。在 图 8-1 中,我们介绍了一个 Ingress 控制器的示例。

图 8-1. 典型的软件 Ingress 控制器配置
Ingress 规范与 Ingress 控制器
虽然在概念上简单,但在实现级别上,Ingress 与 Kubernetes 中几乎所有其他常规资源对象都非常不同。具体来说,它分为一个常见的资源规范和一个控制器实现。没有一个内置到 Kubernetes 中的“标准”Ingress 控制器,因此用户必须安装众多可选的实现之一。
用户可以像创建和修改其他对象一样创建和修改 Ingress 对象。但默认情况下,并没有运行实际操作这些对象的代码。用户(或者他们正在使用的发行版)需要安装和管理一个外部控制器。这种方式下,控制器是可插拔的。
Ingress 最终形成这样的原因有几点。首先,没有一个单一的 HTTP 负载均衡器可以普遍使用。除了许多软件负载均衡器(包括开源和专有的)外,云提供商(例如 AWS 上的 ELB)和基于硬件的负载均衡器也提供了负载均衡能力。其次,Ingress 对象是在任何常见的可扩展能力被添加之前就被添加到 Kubernetes 中的(参见 第十七章)。随着 Ingress 的发展,它可能会演变以使用这些机制。
安装 Contour
虽然有许多可用的入口控制器,但在这里的示例中,我们使用一个称为 Contour 的入口控制器。这是一个专为配置开源(以及 CNCF 项目)负载均衡器 Envoy 而构建的控制器。Envoy 是通过 API 动态配置的。Contour 入口控制器负责将 Ingress 对象转换为 Envoy 可以理解的内容。
注意
Contour 项目由 Heptio 与实际客户合作创建,并用于生产环境,但现在是一个独立的开源项目。
您可以通过简单的一行命令安装 Contour:
$ kubectl apply -f https://projectcontour.io/quickstart/contour.yaml
注意,这需要由具有cluster-admin权限的用户执行。
大多数情况下,这一行代码是有效的。它创建了一个名为projectcontour的命名空间。在该命名空间内,它创建了一个包含两个副本的部署,并创建了一个外部可访问的type: LoadBalancer服务。此外,它通过一个服务账户设置了正确的权限,并安装了一个自定义资源定义(参见第十七章)以支持一些在“Ingress 的未来”中讨论的扩展功能。
因为它是全局安装,所以您需要确保您在安装的集群上具有广泛的管理员权限。安装完成后,您可以通过以下方式获取 Contour 的外部地址:
$ kubectl get -n projectcontour service envoy -o wide
NAME CLUSTER-IP EXTERNAL-IP PORT(S) ...
contour 10.106.53.14 a477...amazonaws.com 80:30274/TCP ...
查看EXTERNAL-IP列。这可以是 IP 地址(适用于 GCP 和 Azure)或主机名(适用于 AWS)。其他云和环境可能会有所不同。如果您的 Kubernetes 集群不支持type: LoadBalancer的服务,则必须更改用于安装 Contour 的 YAML,以使用type: NodePort并通过适合您配置的机制将流量路由到集群中的机器。
如果您使用minikube,可能不会列出任何EXTERNAL-IP。要解决此问题,您需要打开一个单独的终端窗口并运行minikube tunnel命令。这将配置网络路由,使每个type: LoadBalancer服务分配到唯一的 IP 地址。
配置 DNS
要使 Ingress 正常工作,您需要为负载均衡器的外部地址配置 DNS 条目。您可以将多个主机名映射到单个外部端点,并且 Ingress 控制器将根据主机名将传入的请求定向到适当的上游服务。
对于本章,我们假设您拥有一个名为example.com的域名。您需要配置两个 DNS 条目:alpaca.example.com和bandicoot.example.com。如果您有外部负载均衡器的 IP 地址,您将需要创建 A 记录。如果您有主机名,则需要配置 CNAME 记录。
ExternalDNS 项目是一个集群附加组件,您可以使用它来管理 DNS 记录。ExternalDNS 监视您的 Kubernetes 集群,并将 Kubernetes 服务资源的 IP 地址与外部 DNS 提供程序同步。ExternalDNS 支持各种 DNS 提供商,包括传统的域名注册商和公共云提供商。
配置本地 hosts 文件
如果您没有域名,或者正在使用诸如minikube之类的本地解决方案,可以通过编辑您的/etc/hosts文件添加 IP 地址来设置本地配置。您需要在工作站上拥有管理员/根权限。文件的位置可能因您的平台而异,并且使其生效可能需要额外的步骤。例如,在 Windows 上,文件通常位于C:\Windows\System32\drivers\etc\hosts,对于较新版本的 macOS,您需要在更改文件后运行sudo killall -HUP mDNSResponder。
编辑文件,添加以下类似的行:
<*ip-address*> alpaca.example.com bandicoot.example.com
对于<*ip-address*>,请填写 Contour 的外部 IP 地址。如果您只有主机名(例如来自 AWS),可以通过执行host -t a *<address>*获取一个 IP 地址(这可能会在将来更改)。
在完成后不要忘记撤消这些更改!
使用 Ingress
现在我们已经配置了 Ingress 控制器,让我们来测试它。首先,我们将通过执行以下命令创建一些上游(有时也称为“后端”)服务进行测试:
$ kubectl create deployment be-default \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--replicas=3 \
--port=8080
$ kubectl expose deployment be-default
$ kubectl create deployment alpaca \
--image=gcr.io/kuar-demo/kuard-amd64:green \
--replicas=3 \
--port=8080
$ kubectl expose deployment alpaca
$ kubectl create deployment bandicoot \
--image=gcr.io/kuar-demo/kuard-amd64:purple \
--replicas=3 \
--port=8080
$ kubectl expose deployment bandicoot
$ kubectl get services -o wide
NAME CLUSTER-IP ... PORT(S) ... SELECTOR
alpaca 10.115.245.13 ... 8080/TCP ... run=alpaca
bandicoot 10.115.242.3 ... 8080/TCP ... run=bandicoot
be-default 10.115.246.6 ... 8080/TCP ... run=be-default
kubernetes 10.115.240.1 ... 443/TCP ... <none>
最简单的用法
使用 Ingress 的最简单方式是让它盲目地将它看到的一切都传递给一个上游服务。在kubectl中,对于与 Ingress 一起工作的命令的支持有限,因此我们将从一个 YAML 文件开始(参见示例 8-1)。
示例 8-1. simple-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-ingress
spec:
defaultBackend:
service:
name: alpaca
port:
number: 8080
使用kubectl apply创建此 Ingress:
$ kubectl apply -f simple-ingress.yaml
ingress.extensions/simple-ingress created
您可以使用kubectl get和kubectl describe验证它是否设置正确:
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
simple-ingress * 80 13m
$ kubectl describe ingress simple-ingress
Name: simple-ingress
Namespace: default
Address:
Default backend: alpaca:8080
(172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080)
Rules:
Host Path Backends
---- ---- --------
* * alpaca:8080 (172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080)
Annotations:
...
Events: <none>
这样设置后,命中 Ingress 控制器的任何HTTP 请求都将转发到alpaca服务。您现在可以通过服务的任何原始 IP 地址/CNAME 访问kuard的alpaca实例;在这种情况下,要么是alpaca.example.com,要么是bandicoot.example.com。此时,与type: LoadBalancer的简单服务相比,没有太多附加价值。接下来的部分将尝试更复杂的配置。
使用主机名
当我们根据请求的属性引导流量时,事情开始变得有趣。最常见的示例是让 Ingress 系统查看 HTTP 主机头(设置为原始 URL 中的 DNS 域),并根据该头部引导流量。让我们为将流量引导到任何流向alpaca.example.com的流量添加另一个 Ingress 对象(请参见示例 8-2)。
示例 8-2. host-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-ingress
spec:
defaultBackend:
service:
name: be-default
port:
number: 8080
rules:
- host: alpaca.example.com
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: alpaca
port:
number: 8080
使用kubectl apply创建此 Ingress:
$ kubectl apply -f host-ingress.yaml
ingress.extensions/host-ingress created
我们可以通过以下方式验证设置是否正确:
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
host-ingress alpaca.example.com 80 54s
simple-ingress * 80 13m
$ kubectl describe ingress host-ingress
Name: host-ingress
Namespace: default
Address:
Default backend: be-default:8080 (<none>)
Rules:
Host Path Backends
---- ---- --------
alpaca.example.com
/ alpaca:8080 (<none>)
Annotations:
...
Events: <none>
这里有几件令人困惑的事情。首先,有一个对 default-http-backend 的引用。这是一种约定,只有一些 Ingress 控制器使用它来处理没有以其他方式处理的请求。这些控制器将这些请求发送到 kube-system 命名空间中名为 default-http-backend 的服务。这种约定在客户端中通过 kubectl 显示。接下来,alpaca 后端服务没有列出端点。这是 kubectl 中的一个错误,在 Kubernetes v1.14 中已修复。
无论如何,现在你应该能够通过 http://alpaca.example.com 访问 alpaca 服务。如果你通过其他方法到达服务端点,你应该会得到默认服务。
使用路径
下一个有趣的场景是根据 HTTP 请求中的主机名和路径来定向流量。我们可以通过在 paths 条目中指定路径来轻松实现这一点(参见 示例 8-3)。在这个例子中,我们将所有进入 http://bandicoot.example.com 的流量定向到 bandicoot 服务,但我们也将 http://bandicoot.example.com/a 发送到 alpaca 服务。这种情况可以用来在单个域的不同路径上托管多个服务。
示例 8-3. path-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-ingress
spec:
rules:
- host: bandicoot.example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: bandicoot
port:
number: 8080
- pathType: Prefix
path: "/a/"
backend:
service:
name: alpaca
port:
number: 8080
当在 Ingress 系统中列出同一主机上的多个路径时,最长前缀匹配。因此,在这个例子中,以 /a/ 开头的流量被转发到 alpaca 服务,而所有其他流量(以 / 开头)被定向到 bandicoot 服务。
当请求被代理到上游服务时,路径保持不变。这意味着对 bandicoot.example.com/a/ 的请求显示为配置为该请求主机名和路径的上游服务器。上游服务需要准备好在该子路径上提供流量服务。在这种情况下,kuard 具有用于测试的特殊代码,它在根路径 (/) 以及一组预定义的子路径 (/a/, /b/, 和 /c/) 上响应。
清理
清理工作,执行以下操作:
$ kubectl delete ingress host-ingress path-ingress simple-ingress
$ kubectl delete service alpaca bandicoot be-default
$ kubectl delete deployment alpaca bandicoot be-default
高级 Ingress 主题和常见问题
Ingress 支持一些其他花哨的功能。这些功能的支持程度取决于 Ingress 控制器的实现,两个控制器可能以稍微不同的方式实现某个功能。
许多扩展功能通过 Ingress 对象上的注释公开。要小心;这些注释很难验证,容易出错。许多这些注释适用于整个 Ingress 对象,因此可能比你想要的更一般。为了将注释范围缩小,你可以将单个 Ingress 对象拆分为多个 Ingress 对象。Ingress 控制器应该读取它们并将它们合并在一起。
运行多个 Ingress 控制器
存在多个 Ingress 控制器实现,您可能希望在单个集群上运行多个 Ingress 控制器。为了解决这种情况,存在 IngressClass 资源,以便 Ingress 资源可以请求特定的实现。创建 Ingress 资源时,使用spec.ingressClassName字段来指定特定的 Ingress 资源。
注意
在 Kubernetes 1.18 版本之前,不存在IngressClassName字段,而是使用kubernetes.io/ingress.class注释。尽管许多控制器仍然支持此注释,但建议用户不再使用此注释,因为它可能会被控制器弃用。
如果缺少spec.ingressClassName注释,则会使用默认的 Ingress 控制器。可以通过在正确的 IngressClass 资源上添加ingressclass.kubernetes.io/is-default-class注释来指定它。
多个 Ingress 对象
如果指定多个 Ingress 对象,则 Ingress 控制器应该读取它们所有并尝试将它们合并为一个连贯的配置。然而,如果指定重复和冲突的配置,则行为是未定义的。不同的 Ingress 控制器可能会有不同的行为。即使是单一实现也可能根据不明显的因素采取不同的行动。
Ingress 与命名空间
Ingress 与命名空间有一些不明显的交互方式。首先,由于安全方面的过多警告,Ingress 对象只能引用同一命名空间中的上游服务。这意味着您不能使用 Ingress 对象将子路径指向另一个命名空间中的服务。
然而,不同命名空间中的多个 Ingress 对象可以为相同的主机指定子路径。然后,这些 Ingress 对象被合并以生成 Ingress 控制器的最终配置。
这种跨命名空间的行为意味着在整个集群中协调 Ingress 是必要的。如果不仔细协调,一个命名空间中的 Ingress 对象可能会在其他命名空间中引起问题(和未定义的行为)。
通常,在 Ingress 控制器中没有限制哪些命名空间可以指定哪些主机名和路径。高级用户可以尝试使用自定义准入控制器来强制执行此策略。在“Ingress 的未来”中描述了 Ingress 的演进来解决这个问题。
路径重写
一些 Ingress 控制器实现支持可选的路径重写。这可以用来修改作为代理的 HTTP 请求中的路径。这通常通过 Ingress 对象上的注释指定,并适用于该对象指定的所有请求。例如,如果我们使用 NGINX Ingress 控制器,我们可以指定一个注释nginx.ingress.kubernetes.io/rewrite-target: /。这有时可以使上游服务在没有专门设计的情况下在子路径上工作。
有多个实现不仅实现了路径重写,还在指定路径时支持正则表达式。例如,NGINX 控制器允许使用正则表达式捕获路径的部分,然后在重写时使用捕获的内容。如何实现这一点(以及使用的正则表达式变体)取决于具体的实现。
路径重写并非万能解决方案,通常会导致错误。许多 Web 应用程序假定它们可以使用绝对路径在自身内部链接。在这种情况下,所讨论的应用程序可能托管在 /subpath 上,但请求却显示在 / 上。然后它可能将用户发送到 /app-path。然后就有了一个问题,即这是否是应用程序的“内部”链接(在这种情况下,它应该是 /subpath/app-path)还是指向其他应用程序的链接。因此,如果可能的话,最好避免对任何复杂应用程序使用子路径。
提供 TLS
在提供网站时,使用 TLS 和 HTTPS 进行安全提供变得越来越必要。Ingress 支持这一点(大多数 Ingress 控制器也支持)。
首先,用户需要使用他们的 TLS 证书和密钥指定一个 Secret,类似于 示例 8-4 中概述的内容。您还可以使用 kubectl create secret tls <*secret-name*> --cert <*certificate-pem-file*> --key <*private-key-pem-file*> 命令创建一个 Secret。
示例 8-4. tls-secret.yaml
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: null
name: tls-secret-name
type: kubernetes.io/tls
data:
tls.crt: <base64 encoded certificate>
tls.key: <base64 encoded private key>
一旦上传了证书,就可以在 Ingress 对象中引用它。这指定了一组证书以及应该为这些证书使用的主机名(参见 示例 8-5)。同样,如果多个 Ingress 对象为相同的主机名指定了证书,则行为是未定义的。
示例 8-5. tls-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
spec:
tls:
- hosts:
- alpaca.example.com
secretName: tls-secret-name
rules:
- host: alpaca.example.com
http:
paths:
- backend:
serviceName: alpaca
servicePort: 8080
上传和管理 TLS 证书可能会很困难。此外,证书通常会带来相当大的成本。为了帮助解决这个问题,有一个名为 “Let’s Encrypt” 的非营利组织运行一个免费的基于 API 的证书颁发机构。由于它是基于 API 的,因此可以设置一个 Kubernetes 集群,自动获取并安装 TLS 证书。设置可能有些棘手,但一旦运行,使用起来非常简单。缺失的部分是一个名为 cert-manager 的开源项目,由英国初创公司 Jetstack 创建,并加入了 CNCF。cert-manager.io 网站或 GitHub 仓库 上有关于如何安装 cert-manager 并入门的详细信息。
备用入口实现
有许多不同的 Ingress 控制器实现,每个都在基本的 Ingress 对象上构建具有独特功能。这是一个充满活力的生态系统。
首先,每个云提供商都有一个 Ingress 实现,用于暴露特定的基于云的 L7 负载均衡器。与配置在 Pod 中运行的软件负载均衡器不同,这些控制器接收 Ingress 对象,并通过 API 配置基于云的负载均衡器。这减少了集群的负载和操作员的管理负担,但通常会带来一定的成本。
最流行的通用 Ingress 控制器可能是开源的 NGINX Ingress 控制器。请注意,还有一个基于专有 NGINX Plus 的商业控制器。开源控制器基本上会读取 Ingress 对象,并将它们合并到一个 NGINX 配置文件中。然后,它向 NGINX 进程发出信号,以使用新配置重新启动(同时负责处理正在进行中的连接)。开源 NGINX 控制器具有大量通过 annotations 公开的功能和选项。
Emissary 和 Gloo 是另外两个基于 Envoy 的 Ingress 控制器,专注于成为 API 网关。
Traefik 是一个用 Go 实现的反向代理,也可以作为 Ingress 控制器运行。它具有一系列非常适合开发者的功能和仪表板。
这只是冰山一角。Ingress 生态系统非常活跃,有许多新项目和商业产品以独特的方式构建在谦逊的 Ingress 对象之上。
Ingress 的未来
正如你所看到的,Ingress 对象提供了一个非常有用的抽象来配置 L7 负载均衡器,但它还没有扩展到用户需要的所有功能,并且各种实现正在寻求提供。许多 Ingress 中的功能定义不清晰。实现可以以不同的方式展示这些功能,从而降低配置在不同实现之间的可移植性。
另一个问题是,容易配置错误的 Ingress。多个对象的组合方式为不同实现提供了解决冲突的机会。此外,这些对象在命名空间之间的合并方式破坏了命名空间隔离的理念。
Ingress 的创建是在服务网格的概念(例如 Istio 和 Linkerd 这样的项目)被广泛认知之前。Ingress 和服务网格的交集仍在定义中。服务网格在第十五章中有更详细的介绍。
Kubernetes 的 HTTP 负载均衡未来看起来将是 Gateway API,这是由专注于网络的 Kubernetes 特别兴趣小组(SIG)正在开发中。Gateway API 项目旨在为 Kubernetes 中的路由开发一个更现代化的 API。虽然它更专注于 HTTP 负载均衡,Gateway 也包括用于控制第四层(TCP)负载均衡的资源。Gateway API 目前仍在积极开发中,因此强烈建议大家继续使用目前在 Kubernetes 中存在的 Ingress 和 Service 资源。关于 Gateway API 的当前状态可以在网上找到。
摘要
Ingress 是 Kubernetes 中独特的系统。它只是一个模式,该模式的控制器实现必须单独安装和管理。但它也是一种将服务以实用和经济高效的方式暴露给用户的关键系统。随着 Kubernetes 的不断成熟,预计 Ingress 将变得越来越重要。
^(1) 开放系统互联模型(OSI 模型) 是描述不同网络层如何构建在一起的标准方式。TCP 和 UDP 被认为是第四层,而 HTTP 是第七层。
第九章:ReplicaSets
我们已经讨论了如何将单个容器作为 Pods 运行,但这些 Pods 本质上是一次性单例。通常情况下,出于多种原因,您希望在特定时间运行多个容器的副本:
冗余性
通过运行多个实例来容忍故障。
规模
通过运行多个实例提高请求处理能力。
分片
不同的副本可以并行处理计算的不同部分。
当然,你可以手动使用多个不同的 Pod 清单创建多个 Pod 的副本,但这样做既繁琐又容易出错。从逻辑上讲,管理一组副本 Pods 的用户将其视为一个单一实体来定义和管理——这正是 ReplicaSet 的作用。ReplicaSet 充当集群范围的 Pod 管理器,确保正确类型和数量的 Pods 随时运行。
由于 ReplicaSets 可以轻松创建和管理一组副本的 Pods,它们是常见应用部署模式和基础设施级自愈应用的基础模块。
将 ReplicaSet 理解为将 cookie 切割机和所需数量的 cookie 结合成一个单一的 API 对象的最简单方法。当我们定义一个 ReplicaSet 时,我们定义了要创建的 Pods 的规范(“cookie 切割机”)和所需数量的副本。此外,我们需要定义一种查找 ReplicaSet 应控制的 Pods 的方法。管理复制 Pods 的实际行为是协调循环的一个示例。这样的循环是 Kubernetes 设计和实现的大部分基础。
协调循环
协调循环背后的核心概念是“期望”状态与“观察到”的或“当前”的状态的概念。期望状态是你想要的状态。在 ReplicaSet 中,它是副本的期望数量和要复制的 Pod 的定义。例如,“期望的状态是有三个 kuard 服务器的 Pod 副本正在运行”。相反,当前状态是系统当前观察到的状态。例如,“当前只有两个 kuard Pod 正在运行”。
协调循环不断运行,观察世界的当前状态并采取行动,以尝试使观察到的状态与期望状态匹配。例如,在前面的例子中,协调循环将创建一个新的 kuard Pod,以使观察到的状态与期望的三个副本的状态匹配。
采用对和解调循环管理状态的方法有许多好处。它是一个本质上以目标驱动、自我修复的系统,但通常可以用几行代码轻松表达。例如,ReplicaSets 的解调循环是一个单一的循环,但它处理用户动作来扩展或缩减 ReplicaSet,以及节点故障或节点重新加入集群后的情况。
我们将在本书的其余部分中看到大量和解调循环相关的示例。
关联 Pods 和 ReplicaSets
解耦是 Kubernetes 的一个关键主题。特别重要的是,Kubernetes 的所有核心概念在彼此之间都是模块化的,并且它们可以互换和替换为其他组件。在这种精神下,ReplicaSet 和 Pod 之间的关系是松散耦合的。虽然 ReplicaSet 创建和管理 Pods,但它们不拥有它们创建的 Pods。ReplicaSet 使用标签查询来标识它们应该管理的一组 Pods。然后,它们使用您在 第五章 中直接使用的完全相同的 Pod API 来创建它们正在管理的 Pods。这种“从前门进入”的概念是 Kubernetes 中的另一个核心设计概念。在类似的解耦中,创建多个 Pods 的 ReplicaSets 和负载均衡到这些 Pods 的服务也是完全分开的、解耦的 API 对象。除了支持模块化外,解耦 Pods 和 ReplicaSets 还启用了几个重要的行为,将在接下来的几节中讨论。
采用现有的容器
尽管声明式配置很有价值,但有时以命令式方式构建某些东西会更容易。特别是在初期阶段,您可能只是部署一个带有容器镜像的单个 Pod,而不是由 ReplicaSet 进行管理。您甚至可以定义一个负载均衡器来为该单个 Pod 提供流量服务。
但是在某些时候,您可能希望将您的单例容器扩展为复制服务,并创建和管理一系列类似的容器。如果 ReplicaSets 拥有它们创建的 Pods,那么复制您的 Pod 的唯一方法将是删除它,并通过 ReplicaSet 重新启动它。这可能会造成干扰,因为在您的容器不运行的时候会有一段时间。然而,由于 ReplicaSets 与它们管理的 Pods 解耦,您可以简单地创建一个“采用”现有 Pod 的 ReplicaSet,并扩展这些容器的额外副本。通过这种方式,您可以从一个命令式的单一 Pod 无缝过渡到由 ReplicaSet 管理的复制 Pods 集合。
隔离容器
通常情况下,当服务器表现不佳时,Pod 级别的健康检查会自动重新启动该 Pod。但是如果您的健康检查不完整,一个 Pod 可能会表现不佳,但仍然是复制集的一部分。在这些情况下,简单地杀死 Pod 可以解决问题,但这样做会使开发人员只能依赖日志来调试问题。相反,您可以修改患病 Pod 上的标签集。这样做将使其与 ReplicaSet(和服务)解除关联,以便您可以调试该 Pod。ReplicaSet 控制器将注意到一个 Pod 缺失并创建一个新副本,但由于 Pod 仍在运行,开发人员可以进行交互式调试,这比仅仅依赖日志进行调试要有价值得多。
使用 ReplicaSets 进行设计
ReplicaSets 的设计用于表示体系结构中的单个可扩展微服务。它们的关键特性是 ReplicaSet 控制器创建的每个 Pod 都是完全同质的。通常,这些 Pod 然后由 Kubernetes 服务负载均衡器前端化,该负载均衡器在服务中分发流量到组成服务的各个 Pod 上。一般来说,ReplicaSets 设计用于无状态(或几乎无状态)服务。它们创建的元素是可互换的;当缩减 ReplicaSet 时,会选择一个任意的 Pod 进行删除。由于这种缩减操作,你的应用行为不应该发生改变。
注意
通常,你会看到应用程序使用 Deployment 对象,因为它允许你管理新版本的发布。ReplicaSets 在幕后支持 Deployments,了解它们的操作方式非常重要,以便在需要排除故障时进行调试。
ReplicaSet 规范
就像 Kubernetes 中的所有对象一样,ReplicaSets 是使用规范定义的。所有的 ReplicaSets 必须有一个唯一的名称(使用 metadata.name 字段定义),一个 spec 部分来描述集群中任何给定时间应该运行的 Pod(副本)数量,以及一个 Pod 模板,描述当定义的副本数未达到时应创建的 Pod。 示例 9-1 展示了一个最小化的 ReplicaSet 定义。注意规范中的 replicas、selector 和 template 部分,因为它们提供了关于 ReplicaSets 操作方式更深入的见解。
示例 9-1. kuard-rs.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
labels:
app: kuard
version: "2"
name: kuard
spec:
replicas: 1
selector:
matchLabels:
app: kuard
version: "2"
template:
metadata:
labels:
app: kuard
version: "2"
spec:
containers:
- name: kuard
image: "gcr.io/kuar-demo/kuard-amd64:green"
Pod 模板
正如前面提到的,当当前状态中的 Pod 数量少于期望状态中的 Pod 数量时,ReplicaSet 控制器将使用 ReplicaSet 规范中包含的模板创建新的 Pod。这些 Pod 的创建方式与您在前几章中从 YAML 文件创建 Pod 的方式完全相同,但不是使用文件,而是直接基于 Pod 模板创建和提交 Pod 清单到 API 服务器。以下是 ReplicaSet 中的一个 Pod 模板示例:
template:
metadata:
labels:
app: helloworld
version: v1
spec:
containers:
- name: helloworld
image: kelseyhightower/helloworld:v1
ports:
- containerPort: 80
标签
在任何合理大小的集群中,许多不同的 Pod 同时运行—那么 ReplicaSet 协调循环如何发现特定 ReplicaSet 的一组 Pod?ReplicaSets 使用一组 Pod 标签来监视集群状态,以过滤 Pod 列表并跟踪集群中运行的 Pods。初始创建时,ReplicaSet 从 Kubernetes API 获取一个 Pod 列表,并通过标签进行结果过滤。根据查询返回的 Pod 数量,ReplicaSet 删除或创建 Pods 以满足所需的副本数量。这些过滤标签在 ReplicaSet 的spec部分中定义,并且是理解 ReplicaSets 工作原理的关键。
注意
ReplicaSet 中的选择器spec应该是 Pod 模板中标签的一个合适的子集。
创建 ReplicaSet
ReplicaSets 是通过向 Kubernetes API 提交一个 ReplicaSet 对象来创建的。在本节中,我们将使用配置文件和kubectl apply命令创建一个 ReplicaSet。
在示例 9-1 中的 ReplicaSet 配置文件将确保gcr.io/kuar-demo/kuard-amd64:green容器的一个副本在任何给定时间内运行。使用kubectl apply命令将kuard ReplicaSet 提交到 Kubernetes API:
$ kubectl apply -f kuard-rs.yaml
replicaset "kuard" created
一旦接受了kuard ReplicaSet,ReplicaSet 控制器将检测到没有符合所需状态的kuard Pods 正在运行,并基于 Pod 模板的内容创建一个新的kuard Pod:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
kuard-yvzgd 1/1 Running 0 11s
检查 ReplicaSet
与 Pod 和其他 Kubernetes API 对象一样,如果您对 ReplicaSet 的更多详细信息感兴趣,可以使用describe命令提供关于其状态的更多信息。以下是使用describe获取我们之前创建的 ReplicaSet 详细信息的示例:
$ kubectl describe rs kuard
Name: kuard
Namespace: default
Selector: app=kuard,version=2
Labels: app=kuard
version=2
Annotations: <none>
Replicas: 1 current / 1 desired
Pods Status: 1 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
您可以查看 ReplicaSet 的标签选择器,以及它管理的所有副本的状态。
从 Pod 查找 ReplicaSet
有时您可能会想知道 Pod 是否由 ReplicaSet 管理,如果是,是哪一个。为了启用这种发现功能,ReplicaSet 控制器会向它创建的每个 Pod 添加一个ownerReferences部分。如果您运行以下命令,请查找ownerReferences部分:
$ kubectl get pods <*pod-name*> -o=jsonpath='{.metadata.ownerReferences[0].name}'
如果适用,这将列出管理此 Pod 的 ReplicaSet 的名称。
查找 ReplicaSet 的一组 Pods
您还可以确定由 ReplicaSet 管理的 Pod 集合。首先,使用kubectl describe命令获取标签集合。在前面的示例中,标签选择器是app=kuard,version=2。要查找与此选择器匹配的 Pods,使用--selector标志或简写-l:
$ kubectl get pods -l app=kuard,version=2
这与 ReplicaSet 执行以确定当前 Pod 数量完全相同的查询。
扩展 ReplicaSets
通过更新存储在 Kubernetes 中的 ReplicaSet 对象上的spec.replicas键,您可以将 ReplicaSets 进行缩放。当您扩展一个 ReplicaSet 时,它会使用 ReplicaSet 上定义的 Pod 模板向 Kubernetes API 提交新的 Pods。
使用 kubectl scale 进行命令式扩展
使用kubectl中的scale命令是实现这一点的最简单方法。例如,要扩展到四个副本,您可以运行:
$ kubectl scale replicasets kuard --replicas=4
尽管这样的命令式命令对于演示和快速响应紧急情况(如负载突然增加)非常有用,但同样重要的是,更新任何文本文件配置以匹配通过命令式scale命令设置的副本数。当您考虑以下情景时,其重要性就变得明显起来。
当 Alice 正在值班时,服务的负载突然增加。Alice 使用scale命令将响应请求的服务器数量增加到 10 个,并解决了这种情况。然而,Alice 忘记更新已经检入源代码控制的 ReplicaSet 配置。
几天后,Bob 正在准备每周的部署。Bob 编辑存储在版本控制中的 ReplicaSet 配置,以使用新的容器映像,但他没有注意到文件中当前副本的数量是 5,而不是 Alice 在响应增加负载时设置的 10 个。Bob 继续进行部署,这既更新了容器映像,又将副本数量减半。这导致立即超载,进而导致停机。
这个虚构的案例研究说明了确保任何命令式更改立即后跟源代码控制中的声明式更改的必要性。确实,如果需求不紧急,我们通常建议只进行如下部分所述的声明性更改。
使用 kubectl apply 进行声明性扩展
在声明性世界中,您通过编辑版本控制中的配置文件进行更改,然后将这些更改应用于集群。要扩展kuard ReplicaSet,请编辑kuard-rs.yaml配置文件,并将replicas计数设置为3:
...
spec:
replicas: 3
...
在多用户环境中,您可能会对此更改进行文档化的代码审查,并最终将更改检入版本控制。无论哪种方式,然后您可以使用kubectl apply命令将更新的kuard ReplicaSet 提交到 API 服务器:
$ kubectl apply -f kuard-rs.yaml
replicaset "kuard" configured
现在更新后的kuard ReplicaSet 已经就位,ReplicaSet 控制器将检测到期望的 Pod 数量已更改,并且需要采取措施来实现该期望状态。如果您在前一节中使用了命令式的scale命令,ReplicaSet 控制器将销毁一个 Pod,使数量变为三个。否则,它将使用在kuard ReplicaSet 上定义的 Pod 模板向 Kubernetes API 提交两个新的 Pod。无论如何,请使用kubectl get pods命令列出正在运行的kuard Pods。您应该会看到类似以下内容的输出,其中有三个处于运行状态的 Pod;两个由于最近启动,其年龄较小:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
kuard-3a2sb 1/1 Running 0 26s
kuard-wuq9v 1/1 Running 0 26s
kuard-yvzgd 1/1 Running 0 2m
自动缩放 ReplicaSet
尽管有时您希望显式控制 ReplicaSet 中副本的数量,但通常您只需具有足够多的副本即可。定义的具体内容取决于 ReplicaSet 中容器的需求。例如,对于像 NGINX 这样的 Web 服务器,您可能会因 CPU 使用率而进行扩展。对于内存缓存,您可能会根据内存消耗进行扩展。在某些情况下,您可能希望根据自定义应用程序指标进行扩展。Kubernetes 可以通过 Horizontal Pod Autoscaling(HPA)处理所有这些情况。
“Horizontal Pod Autoscaling” 这个名字有点拗口,您可能会想为什么不直接叫 “autoscaling”。Kubernetes 区分了 horizontal 缩放(涉及创建 Pod 的额外副本)和 vertical 缩放(涉及增加特定 Pod 所需的资源,例如增加 Pod 所需的 CPU)。许多解决方案还支持 cluster 自动缩放,即根据资源需求调整集群中机器的数量,但这超出了本章的范围。
注意
自动缩放需要在集群中存在 metrics-server。metrics-server 负责跟踪指标并提供一个 API 用于消耗 HPA 在制定缩放决策时使用的指标。大多数 Kubernetes 安装默认包含 metrics-server。您可以通过列出 kube-system 命名空间中的 Pods 来验证其存在:
$ kubectl get pods --namespace=kube-system
在列表中应该看到一个名称以 metrics-server 开头的 Pod。如果没有看到它,自动扩展将无法正常工作。
基于 CPU 使用率进行扩展是 Pod 自动缩放的最常见用例。您还可以基于内存使用情况进行扩展。基于 CPU 的自动缩放对于根据请求消耗 CPU 的请求式系统最为有用,而内存消耗相对静态的系统则不然。
要扩展 ReplicaSet,可以运行以下命令:
$ kubectl autoscale rs kuard --min=2 --max=5 --cpu-percent=80
该命令创建一个自动缩放器,其在 CPU 利用率达到 80% 时在两个到五个副本之间进行缩放。要查看、修改或删除此资源,可以使用标准的 kubectl 命令和 horizontalpodautoscalers 资源。输入 horizontalpodautoscalers 这个词相当长,但可以缩写为 hpa:
$ kubectl get hpa
警告
由于 Kubernetes 的解耦特性,HPA 与 ReplicaSet 之间没有直接的链接。尽管这对于模块化和组合是很好的,但也会导致一些反模式。特别是,在命令式或声明式管理副本数量的同时与自动缩放器结合使用是一个坏主意。如果您和自动缩放器同时尝试修改副本数量,很可能会发生冲突,导致意外行为。
删除 ReplicaSets
当不再需要 ReplicaSet 时,可以使用kubectl delete命令删除它。默认情况下,这也会删除由 ReplicaSet 管理的 Pods:
$ kubectl delete rs kuard
replicaset "kuard" deleted
运行kubectl get pods命令显示,所有由kuard ReplicaSet 创建的kuard Pods 也已被删除:
$ kubectl get pods
如果不想删除 ReplicaSet 管理的 Pods,可以将--cascade标志设置为false,以确保仅删除 ReplicaSet 对象而不是 Pods:
$ kubectl delete rs kuard --cascade=false
摘要
使用 ReplicaSet 组合 Pods 为构建具有自动故障转移功能的健壮应用程序提供了基础,并通过启用可扩展和合理的部署模式使部署这些应用程序变得轻松。无论是单个 Pod,都可以使用 ReplicaSets 进行关注。有些人甚至默认使用 ReplicaSets 而不是 Pods。典型的集群将拥有许多 ReplicaSets,因此在受影响的区域大量应用它们。
第十章:部署(Deployments)
到目前为止,您已经了解了如何将应用程序打包为容器、创建容器的复制集,并使用 Ingress 控制器来负载均衡流量到您的服务。您可以使用所有这些对象(Pods、ReplicaSets 和 Services)来构建应用程序的单个实例。但是,它们对于帮助您管理每日或每周发布新版本的节奏几乎没有帮助。实际上,Pods 和 ReplicaSets 都预期与不会更改的特定容器映像绑定。
Deployment 对象的存在是为了管理新版本的发布。Deployments 以超越任何特定版本的方式表示部署的应用程序。此外,Deployments 使您能够轻松地从代码的一个版本迁移到下一个版本。这个“滚动更新”过程是可指定的和谨慎的。它在升级单个 Pods 之间等待用户可配置的时间量。它还使用健康检查确保新版本的应用程序正在正确运行,并在发生太多故障时停止部署。
使用 Deployments,您可以简单而可靠地推出新的软件版本,无需停机或错误。由 Deployment 控制器在 Kubernetes 集群中运行来控制软件推出的实际机制。这意味着您可以让 Deployment 无人值守地进行,它仍然会正确和安全地操作。这使得将 Deployments 与许多持续交付工具和服务轻松集成成为可能。此外,从服务器端运行可以安全地执行来自网络连接质量较差或间歇性的地方的推出。想象一下,从手机上的地铁上推出软件的新版本。Deployments 使这成为可能且安全!
注意
当 Kubernetes 首次发布时,其功能的最受欢迎的演示之一是“滚动更新”,展示了如何使用单个命令无缝更新运行中的应用程序,而无需任何停机并且不会丢失请求。这个最初的演示基于 kubectl rolling-update 命令,尽管其功能大部分已被 Deployment 对象所吸收,但该命令仍然可在命令行工具中使用。
您的第一个部署(Your First Deployment)
与 Kubernetes 中的所有对象一样,Deployment 可以表示为声明性 YAML 对象,提供有关您要运行的内容的详细信息。在以下情况下,Deployment 正在请求一个 kuard 应用程序的单个实例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: kuard
labels:
run: kuard
spec:
selector:
matchLabels:
run: kuard
replicas: 1
template:
metadata:
labels:
run: kuard
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:blue
将此 YAML 文件保存为 kuard-deployment.yaml,然后可以使用以下命令创建它:
$ kubectl create -f kuard-deployment.yaml
让我们探讨部署(Deployments)的实际工作原理。就像我们学到的 ReplicaSets 管理 Pods 一样,Deployments 管理 ReplicaSets。与 Kubernetes 中的所有关系一样,这种关系是由标签和标签选择器定义的。您可以通过查看 Deployment 对象来查看标签选择器:
$ kubectl get deployments kuard \
-o jsonpath --template {.spec.selector.matchLabels}
{"run":"kuard"}
从中可以看出,部署正在管理一个带有标签run=kuard的副本集。您可以在副本集之间使用此标签选择器查询,以找到特定的副本集:
$ kubectl get replicasets --selector=run=kuard
NAME DESIRED CURRENT READY AGE
kuard-1128242161 1 1 1 13m
现在让我们看看部署和副本集之间的关系如何运作。我们可以使用命令式scale命令调整部署大小:
$ kubectl scale deployments kuard --replicas=2
deployment.apps/kuard scaled
现在如果我们再次列出该副本集,我们应该会看到:
$ kubectl get replicasets --selector=run=kuard
NAME DESIRED CURRENT READY AGE
kuard-1128242161 2 2 2 13m
扩展部署也已扩展了它控制的副本集。
现在让我们尝试相反的操作,扩展副本集:
$ kubectl scale replicasets kuard-1128242161 --replicas=1
replicaset.apps/kuard-1128242161 scaled
现在再次get该副本集:
$ kubectl get replicasets --selector=run=kuard
NAME DESIRED CURRENT READY AGE
kuard-1128242161 2 2 2 13m
这很奇怪。尽管将副本集扩展到一个副本,但其期望状态仍然是两个副本。出了什么问题?
请记住,Kubernetes 是一个在线的、自愈的系统。顶级部署对象正在管理此副本集。当您将副本数调整为一个时,它不再匹配部署的期望状态,部署的replicas设置为2。部署控制器注意到这一点,并采取行动确保观察到的状态与期望状态匹配,本例中重新调整副本数为两个。
如果您想直接管理该副本集,您需要删除部署。(记得设置--cascade为false,否则它将删除副本集和 Pod!)
创建部署
当然,正如在介绍中所述,您应该优先考虑在磁盘上使用 YAML 或 JSON 文件进行声明式管理您的 Kubernetes 配置状态。
作为起点,将此部署下载到一个 YAML 文件中:
$ kubectl get deployments kuard -o yaml > kuard-deployment.yaml
$ kubectl replace -f kuard-deployment.yaml --save-config
如果您查看文件,您会看到类似于以下内容(请注意,为了可读性,我们已删除了许多只读和默认字段)。注意注释、选择器和策略字段,因为它们提供了部署特定功能的见解:
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: null
generation: 1
labels:
run: kuard
name: kuard
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
run: kuard
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
run: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: IfNotPresent
name: kuard
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status: {}
注意
您还需要运行kubectl replace --save-config。这会添加一个注释,以便在未来应用更改时,kubectl将知道上次应用的配置,从而实现更智能的配置合并。如果您始终使用kubectl apply,则仅在使用kubectl create -f创建部署后首次需要执行此步骤。
部署规范与副本集规范具有非常相似的结构。有一个 Pod 模板,其中包含由部署管理的每个副本创建的多个容器。除了 Pod 规范之外,还有一个strategy对象:
...
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
...
strategy对象规定了新软件发布可以进行的不同方式。部署支持两种策略:Recreate和RollingUpdate。这些在本章后面详细讨论。
管理部署
与所有 Kubernetes 对象一样,您可以通过kubectl describe命令获取关于您的部署的详细信息。此命令提供了部署配置的概述,其中包括像选择器、副本和事件等有趣的字段:
$ kubectl describe deployments kuard
Name: kuard
Namespace: default
CreationTimestamp: Tue, 01 Jun 2021 21:19:46 -0700
Labels: run=kuard
Annotations: deployment.kubernetes.io/revision: 1
Selector: run=kuard
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 ...
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: run=kuard
Containers:
kuard:
Image: gcr.io/kuar-demo/kuard-amd64:blue
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
OldReplicaSets: <none>
NewReplicaSet: kuard-6d69d9fc5c (2/2 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 4m6s deployment-con... ...
Normal ScalingReplicaSet 113s (x2 over 3m20s) deployment-con... ...
在 describe 的输出中,有大量重要的信息。输出中最重要的两个信息是 OldReplicaSets 和 NewReplicaSet。这些字段指向此 Deployment 当前管理的 ReplicaSet 对象。如果 Deployment 正在进行中的部署,这两个字段都会设置为一个值。如果部署完成,OldReplicaSets 将设置为 <none>。
除了 describe 命令外,还有 kubectl rollout 命令用于处理 Deployments。稍后我们将详细介绍此命令,但现在知道你可以使用 kubectl rollout history 获取与特定 Deployment 关联的部署历史。如果当前有进行中的 Deployment,可以使用 kubectl rollout status 获取该部署的当前状态。
更新 Deployments
Deployments 是描述已部署应用程序的声明性对象。对 Deployment 的两种最常见操作是扩展和应用程序更新。
扩展 Deployment
尽管我们之前展示了如何使用 kubectl scale 命令命令式地扩展 Deployment,但最佳实践是通过 YAML 文件声明性地管理您的 Deployments,然后使用这些文件来更新您的 Deployment。要扩展 Deployment,您应该编辑您的 YAML 文件以增加副本数:
...
spec:
replicas: 3
...
保存并提交此更改后,您可以使用 kubectl apply 命令更新 Deployment:
$ kubectl apply -f kuard-deployment.yaml
这将更新 Deployment 的期望状态,导致 ReplicaSet 的大小增加,并最终创建由 Deployment 管理的新 Pod:
$ kubectl get deployments kuard
NAME READY UP-TO-DATE AVAILABLE AGE
kuard 3/3 3 3 10m
更新容器镜像
更新 Deployment 的另一个常见用例是在一个或多个容器中部署新软件版本。为此,您应该同样编辑 Deployment 的 YAML 文件,但这种情况下您更新的是容器镜像,而不是副本数量:
...
containers:
- image: gcr.io/kuar-demo/kuard-amd64:green
imagePullPolicy: Always
...
对 Deployment 的模板进行注释,记录有关更新的一些信息:
...
spec:
...
template:
metadata:
annotations:
kubernetes.io/change-cause: "Update to green kuard"
...
注意
确保将此注释添加到模板而不是 Deployment 本身,因为 kubectl apply 命令使用此字段在 Deployment 对象中。在进行简单扩展操作时,请不要更新 change-cause 注释。修改 change-cause 是模板的重大更改,并将触发新的部署。
同样,您可以使用 kubectl apply 更新 Deployment:
$ kubectl apply -f kuard-deployment.yaml
更新 Deployment 后,它将触发一个部署,您可以通过 kubectl rollout 命令监视该过程:
$ kubectl rollout status deployments kuard
deployment "kuard" successfully rolled out
您可以看到由 Deployment 管理的旧 ReplicaSet 和新 ReplicaSet,以及正在使用的镜像。旧 ReplicaSet 和新 ReplicaSet 都会保留下来,以便您可以进行回滚操作:
$ kubectl get replicasets -o wide
NAME DESIRED CURRENT READY ... IMAGE(S) ...
kuard-1128242161 0 0 0 ... gcr.io/kuar-demo/ ...
kuard-1128635377 3 3 3 ... gcr.io/kuar-demo/ ...
如果您正在进行部署并希望暂时暂停它(例如,如果您开始看到系统中的奇怪行为并希望进行调查),您可以使用 pause 命令:
$ kubectl rollout pause deployments kuard
deployment.apps/kuard paused
如果调查后认为发布可以安全进行,您可以使用resume命令从中断的地方重新开始:
$ kubectl rollout resume deployments kuard
deployment.apps/kuard resumed
发布历史记录
Kubernetes 部署维护发布历史记录,这对于了解部署的先前状态以及回滚到特定版本都很有用。
您可以通过运行以下命令查看部署历史记录:
$ kubectl rollout history deployment kuard
deployment.apps/kuard
REVISION CHANGE-CAUSE
1 <none>
2 Update to green kuard
修订历史记录按从旧到新的顺序给出。每次新发布时都会递增唯一的修订号。到目前为止,我们有两个:初始部署和将图像更新为kuard:green。
如果您对特定修订版本的详细信息感兴趣,可以添加--revision标志以查看该特定修订版本的详细信息:
$ kubectl rollout history deployment kuard --revision=2
deployment.apps/kuard with revision #2
Pod Template:
Labels: pod-template-hash=54b74ddcd4
run=kuard
Annotations: kubernetes.io/change-cause: Update to green kuard
Containers:
kuard:
Image: gcr.io/kuar-demo/kuard-amd64:green
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
让我们为此示例再做一个更新。通过修改容器版本号并更新change-cause注释,将kuard版本回退为blue。使用kubectl apply应用它。现在历史记录应该有三个条目:
$ kubectl rollout history deployment kuard
deployment.apps/kuard
REVISION CHANGE-CAUSE
1 <none>
2 Update to green kuard
3 Update to blue kuard
假设最新版本存在问题,并且您希望在调查时回滚。您可以简单地撤消上次发布:
$ kubectl rollout undo deployments kuard
deployment.apps/kuard rolled back
undo命令在发布的任何阶段都有效。您可以撤消部分完成和完全完成的发布。撤消发布实际上只是逆向执行的发布(例如从 v2 到 v1,而不是从 v1 到 v2),同样控制发布策略的所有相同策略也适用于撤消策略。您可以看到部署对象简单地调整了受管 ReplicaSet 中的期望副本计数:
$ kubectl get replicasets -o wide
NAME DESIRED CURRENT READY ... IMAGE(S) ...
kuard-1128242161 0 0 0 ... gcr.io/kuar-demo/ ...
kuard-1570155864 0 0 0 ... gcr.io/kuar-demo/ ...
kuard-2738859366 3 3 3 ... gcr.io/kuar-demo/ ...
注意
当使用声明文件控制生产系统时,尽可能确保签入的清单与实际在集群中运行的内容匹配。当您执行kubectl rollout undo时,您正在以未反映在源代码控制中的方式更新生产状态。
撤销发布的另一种(也许更可取的)方法是还原您的 YAML 文件并kubectl apply以前的版本。通过这种方式,您的“变更跟踪配置”更接近实际运行在集群中的情况。
让我们再次查看部署历史记录:
$ kubectl rollout history deployment kuard
deployment.apps/kuard
REVISION CHANGE-CAUSE
1 <none>
3 Update to blue kuard
4 Update to green kuard
缺失修订版本 2!事实证明,当您回滚到先前的修订版本时,部署简单地重用模板并重新编号,使其成为最新修订版本。之前的修订版本 2 现在成为修订版本 4。
我们之前看到您可以使用kubectl rollout undo命令回滚到部署的先前版本。此外,您还可以使用--to-revision标志回滚到历史记录中的特定修订版本:
$ kubectl rollout undo deployments kuard --to-revision=3
deployment.apps/kuard rolled back
$ kubectl rollout history deployment kuard
deployment.apps/kuard
REVISION CHANGE-CAUSE
1 <none>
4 Update to green kuard
5 Update to blue kuard
再次执行undo命令采用修订版本 3,将其应用并重新编号为修订版本 5。
指定修订版本0是指定上一个修订版本的简写方式。这样,kubectl rollout undo相当于kubectl rollout undo --to-revision=0。
默认情况下,一个部署的最后 10 个修订版本会附加到部署对象本身。建议如果您有长期保留的部署,您可以设置部署修订历史的最大历史大小。例如,如果您每天更新一次,可以将修订历史限制为 14,以保留两周的修订(如果您不希望在两周之外回滚)。
要实现这一点,请在部署规范中使用 revisionHistoryLimit 属性:
...
spec:
# We do daily rollouts, limit the revision history to two weeks of
# releases as we don't expect to roll back beyond that.
revisionHistoryLimit: 14
...
部署策略
当需要更改实施服务的软件版本时,Kubernetes 部署支持两种不同的滚动策略,Recreate 和 RollingUpdate。让我们依次查看每一种。
重建策略
Recreate 策略是这两种策略中更简单的一种。它只是更新其管理的 ReplicaSet 来使用新镜像,并终止与部署相关联的所有 Pod。ReplicaSet 注意到它不再有任何副本,并重新创建所有使用新镜像的 Pod。一旦 Pod 重新创建,它们就会运行新版本。
虽然这种策略快速而简单,但会导致工作负载停机。因此,Recreate 策略应仅用于可接受服务停机时间的测试部署。
滚动更新策略
RollingUpdate 策略通常是任何面向用户服务的首选策略。虽然比 Recreate 策略更慢,但也显著更复杂和更健壮。使用 RollingUpdate,您可以在服务仍在接收用户流量的情况下推出新版本,而无需任何停机时间。
如其名称所示,RollingUpdate 策略通过逐步更新一部分 Pod 来工作,直到所有 Pod 都运行新版本的软件。
管理多个版本的服务
重要的是,这意味着一段时间内,您的服务的新旧版本都将接收请求并提供流量服务。这对您如何构建软件具有重要的影响。特别是,非常重要的是,您的每个软件版本及其客户端都能够与稍旧和稍新的软件版本互换通信。
考虑以下情景:您正在部署前端软件的过程中;一半服务器运行版本 1,另一半运行版本 2。用户向您的服务发出初始请求并下载实现您 UI 的客户端 JavaScript 库。此请求由版本 1 服务器处理,因此用户收到版本 1 客户端库。此客户端库在用户浏览器中运行,并向您的服务发出后续 API 请求。这些 API 请求被路由到版本 2 服务器;因此,您的 JavaScript 客户端库的版本 1 正在与您的 API 服务器的版本 2 进行通信。如果您没有确保这些版本之间的兼容性,您的应用程序将无法正确运行。
起初,这可能看起来像是一个额外的负担。但事实上,您始终面临这个问题;只是您可能没有注意到。具体而言,用户可以在时间t之前发出请求,就在您启动更新之前。此请求由版本 1 服务器处理。在t_1时,您将服务更新到版本 2。在t_2时,运行在用户浏览器上的版本 1 客户端代码运行,并且请求了由版本 2 服务器操作的 API 端点。无论您如何更新软件,您都必须保持向后和向前兼容,以确保可靠的更新。RollingUpdate策略的本质只是使这一点更加清晰和明确。
这不仅适用于 JavaScript 客户端,它也适用于编译到其他服务并调用您服务的客户端库。仅仅因为您更新了,并不意味着它们已经更新了它们的客户端库。这种向后兼容性对于解耦您的服务与依赖于您服务的系统至关重要。如果您不规范化您的 API 并解耦自己,那么您将被迫仔细管理与调用您服务的所有其他系统的发布。这种紧密耦合使得非常难以产生必要的灵活性,以便每周甚至每小时、每天都能推出新软件。在图 10-1 中显示的解耦架构中,通过 API 合同和负载均衡器将前端与后端隔离开来,而在耦合架构中,则是通过编译到前端的厚客户端直接连接到后端。

图 10-1。解耦(左)和耦合(右)应用架构的图示
配置滚动更新
RollingUpdate是一种非常通用的策略;它可以用来更新各种设置中的各种应用程序。因此,滚动更新本身是可以配置的;您可以调整其行为以适应您的特定需求。有两个参数可以用来调整滚动更新的行为:maxUnavailable和maxSurge。
maxUnavailable参数设置在滚动更新期间可以不可用的最大 Pod 数量。它可以设置为绝对数(例如3,表示最多可以不可用三个 Pod),也可以设置为百分比(例如20%,表示最多可以不可用期望副本数的 20%)。一般来说,对大多数服务来说,使用百分比是一个好方法,因为该值是正确应用的,无论在部署中期望的副本数是多少。但是,在某些情况下,您可能希望使用绝对数(例如将最大不可用 Pod 限制为一个)。
在其核心,maxUnavailable参数帮助调整滚动更新的速度。例如,如果将maxUnavailable设置为50%,则滚动更新将立即将旧的 ReplicaSet 缩减到其原始大小的 50%。如果有四个副本,则将其缩减为两个副本。然后,滚动更新将通过将新的 ReplicaSet 扩展到两个副本来替换已删除的 Pod,总共四个副本(两个旧的,两个新的)。然后,它将旧的 ReplicaSet 缩减为零个副本,总大小为两个新副本。最后,它将新的 ReplicaSet 扩展到四个副本,完成升级。因此,将maxUnavailable设置为50%,升级在四个步骤中完成,但在某些时候服务容量只有 50%。
如果我们将maxUnavailable设置为25%会发生什么?在这种情况下,每个步骤仅执行一个副本,因此完成升级需要两倍的步骤,但在升级期间可用性只降到最低的 75%。这说明了maxUnavailable允许我们在速度和可用性之间进行权衡。
注意
细心的人会注意到,Recreate策略与将maxUnavailable设置为100%的RollingUpdate策略是相同的。
使用降低的容量来实现成功的升级在以下情况下非常有用:当您的服务具有周期性流量模式时(例如,在夜间流量较少)或者当您的资源有限,因此无法扩展到比当前最大副本数更大。
但是,有些情况下,您不希望低于 100%的容量,但愿意临时使用额外的资源来执行滚动升级。在这些情况下,您可以将maxUnavailable参数设置为0,而是使用maxSurge参数来控制升级。与maxUnavailable类似,maxSurge可以指定为具体的数值或百分比。
maxSurge 参数控制可以创建多少额外资源来实现部署。为了说明其工作原理,想象一个具有 10 个副本的服务。我们将 maxUnavailable 设置为 0,将 maxSurge 设置为 20%。部署首先会将新的 ReplicaSet 扩展 2 个副本,总共达到 12 个副本(120%)在服务中。然后,它会将旧的 ReplicaSet 缩减到 8 个副本,服务总数为 10 个(8 个旧的,2 个新的)。这个过程一直持续,直到部署完成。在任何时候,服务的容量保证至少为 100%,并且用于部署的最大额外资源限制为所有资源的额外 20%。
注意
将 maxSurge 设置为 100% 等同于蓝/绿部署。部署控制器首先将新版本扩展到旧版本的 100%。一旦新版本健康,它立即将旧版本缩减到 0%。
减缓部署以确保服务健康
阶段性部署的目的是确保部署结果是一个健康、稳定的服务运行新软件版本。为了做到这一点,部署控制器始终等待一个 Pod 报告其准备就绪,然后再继续更新下一个 Pod。
警告
部署控制器检查 Pod 的状态,这由其准备就绪检查确定。准备就绪检查是 Pod 的健康检查的一部分,在第五章 中详细描述。如果你希望使用部署来可靠地部署你的软件,必须 为 Pod 中的容器指定准备就绪健康检查。如果缺少这些检查,部署控制器在不知道 Pod 状态的情况下运行。
然而,仅仅注意到一个 Pod 已经准备就绪并不足以让你对 Pod 是否实际上表现正确有足够的信心。有些错误条件不会立即发生。例如,你可能有一个严重的内存泄漏需要几分钟才能显示出来,或者你可能有一个只有 1% 请求触发的 bug。在大多数真实场景中,你希望等待一段时间,以高度确信新版本在正确运行后再继续更新下一个 Pod。
对于部署,这个等待时间由 minReadySeconds 参数定义:
...
spec:
minReadySeconds: 60
...
将 minReadySeconds 设置为 60 表示部署必须在看到一个 Pod 变为健康后等待 60 秒,然后 再继续更新下一个 Pod。
除了等待 Pod 变为健康状态外,你还需要设置一个超时时间限制系统等待的时间。例如,假设你的服务的新版本有一个 bug 并立即陷入死锁。它将永远无法准备就绪,在没有超时的情况下,部署控制器将永远停滞你的部署。
在这种情况下的正确行为是超时滚动发布。这反过来将发布标记为失败。此失败状态可用于触发警报,指示操作员发布存在问题。
注意
乍一看,超时滚动发布似乎是一个不必要的复杂过程。然而,越来越多的事物,如发布,正被完全自动化的系统触发,几乎没有人类参与。在这种情况下,超时变得至关重要,它可以触发发布的自动回滚,或者创建一个触发人工干预的工单/事件。
为了设置超时期限,您将使用部署参数progressDeadlineSeconds:
...
spec:
progressDeadlineSeconds: 600
...
本示例将进度截止期限设置为 10 分钟。如果在滚动发布的任何特定阶段未能在 10 分钟内取得进展,则该部署将标记为失败,并且所有推进部署的尝试都将停止。
需要注意的是,此超时是以部署的进度而不是部署的整体长度来计算的。在此上下文中,进度被定义为部署创建或删除 Pod 的任何时间。发生这种情况时,超时时钟将重置为零。图 10-2 显示了部署的生命周期。

图 10-2. Kubernetes 部署生命周期
删除一个部署
如果您希望删除部署,可以使用以下命令:
$ kubectl delete deployments kuard
您还可以使用您之前创建的声明性 YAML 文件来执行此操作:
$ kubectl delete -f kuard-deployment.yaml
无论哪种情况,默认情况下删除部署将删除整个服务。这意味着不仅会删除部署,还会删除它管理的任何 ReplicaSets,以及 ReplicaSets 管理的任何 Pods。与 ReplicaSets 一样,如果这不是期望的行为,则可以使用--cascade=false标志仅删除部署对象。
监控部署
如果部署在指定时间内未能取得进展,则会超时。发生这种情况时,部署的状态将转换为失败状态。此状态可以从status.conditions数组中获取,其中将存在一个Type为Progressing且Status为False的Condition。处于这种状态的部署已失败,并且将不会进一步推进。要设置部署控制器在转换到此状态之前应等待多长时间,请使用spec.progressDeadlineSeconds字段。
总结
Kubernetes 的主要目标是使您能够轻松构建和部署可靠的分布式系统。这意味着不仅仅是一次性实例化应用程序,而是管理定期安排的新软件服务版本的滚动发布。部署是可靠发布和发布管理的关键组成部分。在下一章中,我们将介绍 DaemonSets,它们确保在 Kubernetes 集群中的一组节点上只运行一个 Pod 的副本。
第十一章:DaemonSets
部署(Deployments)和 ReplicaSet 通常用于创建具有多个副本以提高冗余性的服务(例如 Web 服务器)。但复制 Pod 集群的另一个原因是在集群内的每个节点上调度单个 Pod。一般来说,将 Pod 复制到每个节点的动机是为了在每个节点上部署某种代理或守护程序,而 Kubernetes 中实现这一目标的对象就是 DaemonSet。
DaemonSet 确保在 Kubernetes 集群的一组节点上运行 Pod 的副本。DaemonSet 用于部署系统守护程序,如日志收集器和监控代理,这些程序通常需要在每个节点上运行。DaemonSet 与 ReplicaSet 具有类似的功能;它们都创建预期长时间运行的 Pod,并确保集群的期望状态和观察状态匹配。
鉴于 DaemonSet 和 ReplicaSet 的相似性,了解何时使用其中之一是很重要的。当您的应用程序与节点完全解耦并且可以在给定节点上运行多个副本而无需特别考虑时,应使用 ReplicaSet。当必须在集群中的所有或一部分节点上运行应用程序的单个副本时,应使用 DaemonSet。
通常不应使用调度限制或其他参数来确保 Pod 不共同存在于同一节点上。如果发现自己希望每个节点只有一个 Pod,则 DaemonSet 是正确的 Kubernetes 资源。同样,如果发现自己正在构建一个用于服务用户流量的同质化复制服务,则 ReplicaSet 可能是正确的 Kubernetes 资源。
您可以使用标签在特定节点上运行 DaemonSet Pod;例如,您可能希望在暴露给边缘网络的节点上运行专门的入侵检测软件。
您还可以使用 DaemonSet 在基于云的集群中的节点上安装软件。对于许多云服务来说,升级或扩展集群可能会删除和/或重新创建新的虚拟机。这种动态的不可变基础设施方法可能会在您希望(或被中央 IT 要求)在每个节点上具有特定软件时造成问题。为了确保尽管升级和扩展事件,每台机器上都安装了特定软件,DaemonSet 是正确的方法。您甚至可以挂载主机文件系统并运行安装 RPM/DEB 包的脚本到主机操作系统上。通过这种方式,您可以拥有一个符合企业 IT 部门要求的云原生集群。
DaemonSet 调度器
默认情况下,DaemonSet 会在每个节点上创建一个 Pod 的副本,除非使用了节点选择器,该选择器将限制符合一组标签的节点为符合条件的节点。DaemonSet 在 Pod 创建时通过在 Pod 规范中指定 nodeName 字段来确定 Pod 将在哪个节点上运行。因此,DaemonSets 创建的 Pods 将被 Kubernetes 调度程序忽略。
像 ReplicaSets 一样,DaemonSets 由一个协调控制循环管理,该循环通过测量期望状态(所有节点上都存在一个 Pod)和观察状态(特定节点上是否存在该 Pod?)来工作。根据这些信息,DaemonSet 控制器在每个当前没有匹配 Pod 的节点上创建一个 Pod。
如果向集群添加了新节点,则 DaemonSet 控制器会注意到缺少一个 Pod 并将该 Pod 添加到新节点上。
注意
DaemonSets 和 ReplicaSets 很好地展示了解耦架构的价值。也许看起来正确的设计是 ReplicaSet 拥有它管理的 Pods,并且 Pods 是 ReplicaSet 的子资源。同样,由 DaemonSet 管理的 Pods 将是该 DaemonSet 的子资源。然而,这种封装会要求为处理 Pods 编写两次工具:一次为 DaemonSets,一次为 ReplicaSets。相反,Kubernetes 使用了一种解耦的方法,其中 Pods 是顶级对象。这意味着您在 ReplicaSets 上下文中学习的用于检查 Pods 的每个工具(例如 kubectl logs <*pod-name*>)同样适用于 DaemonSets 创建的 Pods。
创建 DaemonSets
DaemonSets 是通过向 Kubernetes API 服务器提交 DaemonSet 配置来创建的。示例 11-1 中的 DaemonSet 将在目标集群的每个节点上创建一个 fluentd 日志代理。
示例 11-1. fluentd.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
containers:
- name: fluentd
image: fluent/fluentd:v0.14.10
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
在给定 Kubernetes 命名空间中,DaemonSets 需要唯一的名称。每个 DaemonSet 必须包含一个 Pod 模板规范,该规范将用于根据需要创建 Pods。这是 ReplicaSets 和 DaemonSets 相似性的结束之处。与 ReplicaSets 不同,DaemonSets 默认会在集群中的每个节点上创建 Pods,除非使用节点选择器。
一旦您有了有效的 DaemonSet 配置,您可以使用 kubectl apply 命令将 DaemonSet 提交给 Kubernetes API。在本节中,我们将创建一个 DaemonSet 来确保 fluentd HTTP 服务器在我们集群中的每个节点上运行:
$ kubectl apply -f fluentd.yaml
daemonset.apps/fluentd created
一旦成功将 fluentd DaemonSet 提交到 Kubernetes API,您可以使用 kubectl describe 命令查询其当前状态:
$ kubectl describe daemonset fluentd
Name: fluentd
Selector: app=fluentd
Node-Selector: <none>
Labels: app=fluentd
Annotations: deprecated.daemonset.template.generation: 1
Desired Number of Nodes Scheduled: 3
Current Number of Nodes Scheduled: 3
Number of Nodes Scheduled with Up-to-date Pods: 3
Number of Nodes Scheduled with Available Pods: 3
Number of Nodes Misscheduled: 0
Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed
...
此输出表明 fluentd Pod 已成功部署到我们集群中的所有三个节点。我们可以使用 kubectl get pods 命令,并使用 -o 标志打印每个 fluentd Pod 分配到的节点来验证这一点:
$ kubectl get pods -l app=fluentd -o wide
NAME READY STATUS RESTARTS AGE IP NODE
fluentd-1q6c6 1/1 Running 0 13m 10.240.0.101 k0-default...
fluentd-mwi7h 1/1 Running 0 13m 10.240.0.80 k0-default...
fluentd-zr6l7 1/1 Running 0 13m 10.240.0.44 k0-default...
有了 fluentd DaemonSet 后,在集群中添加新节点将自动部署一个 fluentd Pod 到该节点:
$ kubectl get pods -l app=fluentd -o wide
NAME READY STATUS RESTARTS AGE IP NODE
fluentd-1q6c6 1/1 Running 0 13m 10.240.0.101 k0-default...
fluentd-mwi7h 1/1 Running 0 13m 10.240.0.80 k0-default...
fluentd-oipmq 1/1 Running 0 43s 10.240.0.96 k0-default...
fluentd-zr6l7 1/1 Running 0 13m 10.240.0.44 k0-default...
这正是管理日志守护程序和其他整个集群服务时所需的行为。我们不需要采取任何行动;这就是 Kubernetes DaemonSet 控制器将其观察到的状态与我们期望的状态进行对比的方式。
限制 DaemonSets 只能部署到特定节点
DaemonSets 最常见的用例是在 Kubernetes 集群中的每个节点上运行一个 Pod。但是,也有一些情况下,您只希望将 Pod 部署到节点的子集中。例如,可能有一些工作负载需要 GPU 或仅在集群中特定节点上可用的快速存储访问。在这些情况下,可以使用节点标签来标记满足工作负载需求的特定节点。
向节点添加标签
限制 DaemonSet 只能部署到特定节点的第一步是在节点子集上添加所需的标签集。可以使用 kubectl label 命令实现此操作。
以下命令向单个节点添加 ssd=true 标签:
$ kubectl label nodes k0-default-pool-35609c18-z7tb ssd=true
node/k0-default-pool-35609c18-z7tb labeled
与其他 Kubernetes 资源一样,如果没有标签选择器,则列出没有标签选择器的节点将返回集群中的所有节点:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k0-default-pool-35609c18-0xnl Ready agent 23m v1.21.1
k0-default-pool-35609c18-pol3 Ready agent 1d v1.21.1
k0-default-pool-35609c18-ydae Ready agent 1d v1.21.1
k0-default-pool-35609c18-z7tb Ready agent 1d v1.21.1
使用标签选择器,我们可以根据标签筛选节点。要仅列出具有设置为 true 的 ssd 标签的节点,请使用 kubectl get nodes 命令并带有 --selector 标志:
$ kubectl get nodes --selector ssd=true
NAME STATUS ROLES AGE VERSION
k0-default-pool-35609c18-z7tb Ready agent 1d v1.21.1
节点选择器
在给定的 Kubernetes 集群中创建 DaemonSet 时,可以将节点选择器作为 Pod 规范的一部分来限制 Pod 可以运行的节点。示例中的 DaemonSet 配置示例 11-2 限制 NGINX 仅在带有 ssd=true 标签设置的节点上运行。
示例 11-2. nginx-fast-storage.yaml
apiVersion: apps/v1
kind: "DaemonSet"
metadata:
labels:
app: nginx
ssd: "true"
name: nginx-fast-storage
spec:
selector:
matchLabels:
app: nginx
ssd: "true"
template:
metadata:
labels:
app: nginx
ssd: "true"
spec:
nodeSelector:
ssd: "true"
containers:
- name: nginx
image: nginx:1.10.0
让我们看看当我们向 Kubernetes API 提交 nginx-fast-storage DaemonSet 时会发生什么:
$ kubectl apply -f nginx-fast-storage.yaml
daemonset.apps/nginx-fast-storage created
由于只有一个带有 ssd=true 标签的节点,nginx-fast-storage Pod 将仅在该节点上运行:
$ kubectl get pods -l app=nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-fast-storage-7b90t 1/1 Running 0 44s 10.240.0.48 ...
将 ssd=true 标签添加到其他节点将导致 nginx-fast-storage Pod 部署在这些节点上。反之亦然:如果从节点中删除了所需的标签,则 DaemonSet 控制器将删除由该 DaemonSet 管理的 Pod。
警告
从具有 DaemonSet 节点选择器所需标签的节点上删除标签将导致该 DaemonSet 管理的 Pod 从节点中删除。
更新 DaemonSet
DaemonSets 适用于在整个集群中部署服务,但是如何进行升级呢?在 Kubernetes 1.6 之前,更新由 DaemonSet 管理的 Pod 的唯一方法是更新 DaemonSet,然后手动删除每个由 DaemonSet 管理的 Pod,以便使用新配置重新创建它们。随着 Kubernetes 1.6 的发布,DaemonSets 获得了类似于 Deployment 对象的功能,在集群内管理 ReplicaSet 的滚动更新。
可以使用与 Deployments 相同的 RollingUpdate 策略推出 DaemonSets。可以使用 spec.updateStrategy.type 字段配置更新策略,该字段的值应为 RollingUpdate。当 DaemonSet 具有 RollingUpdate 的更新策略时,对 DaemonSet 中的 spec.template 字段(或子字段)进行任何更改将启动滚动更新。
与 Deployments 的滚动更新类似(参见 第十章),RollingUpdate 策略逐步更新 DaemonSet 的成员,直到所有 Pod 都在新配置下运行。有两个参数控制 DaemonSet 的滚动更新:
spec.minReadySeconds
确定 Pod 必须“准备就绪”多长时间,然后滚动更新才能继续升级后续 Pod
spec.updateStrategy.rollingUpdate.maxUnavailable
指示可以同时更新多少个 Pod 的滚动更新
通常应设置 spec.minReadySeconds 为一个合理较长的值,例如 30-60 秒,以确保 Pod 在继续推出之前确实健康。
设置 spec.updateStrategy.rollingUpdate.maxUnavailable 更可能依赖于应用程序。将其设置为 1 是一种安全的通用策略,但完全推出可能需要一段时间(节点数 × minReadySeconds)。增加最大不可用性将加快推出速度,但会增加失败推出的“影响范围”。应用程序和集群环境的特性决定了速度与安全性之间的相对值。一个好的方法可能是将 maxUnavailable 设置为 1,并仅在用户或管理员投诉 DaemonSet 推出速度时才增加它。
一旦开始滚动更新,您可以使用 kubectl rollout 命令查看 DaemonSet 推出的当前状态。例如,kubectl rollout status daemonSets my-daemon-set 将显示名为 my-daemon-set 的 DaemonSet 的当前推出状态。
删除 DaemonSet
使用 kubectl delete 命令删除 DaemonSet 非常简单。只需确保提供要删除的 DaemonSet 的正确名称:
$ kubectl delete -f fluentd.yaml
警告
删除 DaemonSet 还将删除由该 DaemonSet 管理的所有 Pod。设置 --cascade 标志为 false,以确保仅删除 DaemonSet 而不是 Pod。
总结
DaemonSets 提供了在 Kubernetes 集群中每个节点上运行一组 Pod 的易用抽象,或者根据标签在节点子集上运行。DaemonSet 提供了自己的控制器和调度器,以确保关键服务如监控代理始终在集群中的正确节点上运行。
对于某些应用程序,你只需安排一定数量的副本;只要它们具有足够的资源和分布以可靠地运行,你并不关心它们运行在哪里。然而,有一类不同的应用程序,如代理和监控应用程序,需要在集群中的每台机器上都存在才能正常工作。这些守护集并不是传统的服务应用程序,而是为 Kubernetes 集群本身添加额外功能和特性。因为守护集是由控制器管理的主动声明对象,因此很容易声明你希望代理在每台机器上运行,而无需显式地将其放置在每台机器上。在自动缩放的 Kubernetes 集群中尤其有用,因为节点可能会不断地添加和移除而无需用户干预。在这种情况下,守护集会在自动缩放器将节点添加到集群时自动向每个节点添加适当的代理。
第十二章:作业
到目前为止,我们专注于长时间运行的进程,例如数据库和 Web 应用程序。这些类型的工作负载运行直到升级或服务不再需要为止。虽然长时间运行的进程占 Kubernetes 集群中运行工作负载的绝大多数,但通常需要运行短暂的一次性任务。作业对象专门用于处理这些类型的任务。
作业创建直到成功终止(例如,退出代码为 0)的 Pod。相比之下,常规 Pod 将会无论其退出代码如何都会持续重新启动。作业适用于仅需执行一次的任务,如数据库迁移或批处理作业。如果作为常规 Pod 运行,您的数据库迁移任务将会在循环中运行,每次退出后都会重新填充数据库。
在本章中,我们将探讨 Kubernetes 提供的最常见的作业模式。我们还将向您展示如何在实际场景中利用这些模式。
作业对象
作业对象负责根据作业规范中的模板创建和管理 Pod。这些 Pod 通常运行直到成功完成。作业对象协调并行运行多个 Pod 的数量。
如果 Pod 在成功终止之前失败,作业控制器将基于作业规范中的 Pod 模板创建一个新的 Pod。由于 Pod 必须被调度,如果调度程序找不到所需资源,可能会导致作业无法执行。此外,由于分布式系统的特性,在某些故障场景下,可能会为特定任务创建重复的 Pod 的小概率存在。
作业模式
作业旨在管理类似批处理的工作负载,其中工作项由一个或多个 Pod 处理。默认情况下,每个作业运行一个 Pod,直到成功终止为止。作业模式由作业的两个主要属性定义:作业完成数和并行运行的 Pod 数。在“运行一次直到完成”的模式下,completions和parallelism参数被设置为1。表 12-1 根据作业配置中completions和parallelism的组合突出显示了作业模式。
表 12-1. 作业模式
| 类型 | 用例 | 行为 | completions |
parallelism |
|---|---|---|---|---|
| 一次性任务 | 数据库迁移 | 一个 Pod 运行一次直到成功终止 | 1 | 1 |
| 并行固定完成 | 多个 Pod 并行处理一组工作 | 一个或多个 Pod 运行一次或多次直到达到固定完成数 | 1+ | 1+ |
| 工作队列:并行作业 | 多个 Pod 从集中工作队列处理 | 一个或多个 Pod 运行一次直到成功终止 | 1 | 2+ |
一次性任务
一次性作业提供了一种方法来运行一个单独的 Pod,直到成功终止。虽然这听起来像是一个简单的任务,但在执行时确实需要一些工作。首先,必须创建一个 Pod,并将其提交到 Kubernetes API。这是使用作业配置中定义的 Pod 模板完成的。一旦作业运行起来,必须监视支持作业的 Pod 是否成功终止。作业可能由于多种原因失败,包括应用程序错误,在运行时未捕获的异常或在作业有机会完成之前节点故障。在所有情况下,作业控制器负责重新创建 Pod,直到成功终止为止。
在 Kubernetes 中创建一次性作业的多种方法。最简单的方法是使用 kubectl 命令行工具:
$ kubectl run -i oneshot \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--restart=OnFailure \
--command /kuard \
-- --keygen-enable \
--keygen-exit-on-complete \
--keygen-num-to-gen 10
...
(ID 0) Workload starting
(ID 0 1/10) Item done: SHA256:nAsUsG54XoKRkJwyN+OShkUPKew3mwq7OCc
(ID 0 2/10) Item done: SHA256:HVKX1ANns6SgF/er1lyo+ZCdnB8geFGt0/8
(ID 0 3/10) Item done: SHA256:irjCLRov3mTT0P0JfsvUyhKRQ1TdGR8H1jg
(ID 0 4/10) Item done: SHA256:nbQAIVY/yrhmEGk3Ui2sAHuxb/o6mYO0qRk
(ID 0 5/10) Item done: SHA256:CCpBoXNlXOMQvR2v38yqimXGAa/w2Tym+aI
(ID 0 6/10) Item done: SHA256:wEY2TTIDz4ATjcr1iimxavCzZzNjRmbOQp8
(ID 0 7/10) Item done: SHA256:t3JSrCt7sQweBgqG5CrbMoBulwk4lfDWiTI
(ID 0 8/10) Item done: SHA256:E84/Vze7KKyjCh9OZh02MkXJGoty9PhaCec
(ID 0 9/10) Item done: SHA256:UOmYex79qqbI1MhcIfG4hDnGKonlsij2k3s
(ID 0 10/10) Item done: SHA256:WCR8wIGOFag84Bsa8f/9QHuKqF+0mEnCADY
(ID 0) Workload exiting
这里有一些需要注意的事项:
-
kubectl的-i选项表示这是一个交互式命令。kubectl将等待作业运行,并显示作业中的第一个(在本例中是唯一的)Pod 的日志输出。 -
--restart=OnFailure是告诉kubectl创建 Job 对象的选项。 -
--后面的所有选项都是容器映像的命令行参数。这些指令我们的测试服务器 (kuard) 生成十个 4,096 位的 SSH 密钥,然后退出。 -
您的输出可能与此不完全匹配。
kubectl使用-i选项经常会错过输出的前几行。
作业完成后,Job 对象和相关的 Pod 被保留,以便您可以检查日志输出。请注意,如果没有传递 -a 标志,此作业不会显示在 kubectl get jobs 中。没有此标志,kubectl 会隐藏已完成的作业。在继续之前删除作业:
$ kubectl delete pods oneshot
创建一次性作业的另一种选项是使用配置文件,如 示例 12-1 中所示。
示例 12-1. job-oneshot.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: oneshot
spec:
template:
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: Always
command:
- "/kuard"
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-num-to-gen=10"
restartPolicy: OnFailure
使用 kubectl apply 命令提交作业:
$ kubectl apply -f job-oneshot.yaml
job.batch/oneshot created
然后描述 oneshot 作业:
$ kubectl describe jobs oneshot
Name: oneshot
Namespace: default
Selector: controller-uid=a2ed65c4-cfda-43c8-bb4a-707c4ed29143
Labels: controller-uid=a2ed65c4-cfda-43c8-bb4a-707c4ed29143
job-name=oneshot
Annotations: <none>
Parallelism: 1
Completions: 1
Start Time: Wed, 02 Jun 2021 21:23:23 -0700
Completed At: Wed, 02 Jun 2021 21:23:51 -0700
Duration: 28s
Pods Statuses: 0 Running / 1 Succeeded / 0 Failed
Pod Template:
Labels: controller-uid=a2ed65c4-cfda-43c8-bb4a-707c4ed29143
job-name=oneshot
Events:
... Reason Message
... ------ -------
... SuccessfulCreate Created pod: oneshot-4kfdt
您可以通过查看所创建的 Pod 的日志来查看作业的结果:
$ kubectl logs oneshot-4kfdt
...
Serving on :8080
(ID 0) Workload starting
(ID 0 1/10) Item done: SHA256:+r6b4W81DbEjxMcD3LHjU+EIGnLEzbpxITKn8IqhkPI
(ID 0 2/10) Item done: SHA256:mzHewajaY1KA8VluSLOnNMk9fDE5zdn7vvBS5Ne8AxM
(ID 0 3/10) Item done: SHA256:TRtEQHfflJmwkqnNyGgQm/IvXNykSBIg8c03h0g3onE
(ID 0 4/10) Item done: SHA256:tSwPYH/J347il/mgqTxRRdeZcOazEtgZlA8A3/HWbro
(ID 0 5/10) Item done: SHA256:IP8XtguJ6GbWwLHqjKecVfdS96B17nnO21I/TNc1j9k
(ID 0 6/10) Item done: SHA256:ZfNxdQvuST/6ZzEVkyxdRG98p73c/5TM99SEbPeRWfc
(ID 0 7/10) Item done: SHA256:tH+CNl/IUl/HUuKdMsq2XEmDQ8oAvmhMO6Iwj8ZEOj0
(ID 0 8/10) Item done: SHA256:3GfsUaALVEHQcGNLBOu4Qd1zqqqJ8j738i5r+I5XwVI
(ID 0 9/10) Item done: SHA256:5wV4L/xEiHSJXwLUT2fHf0SCKM2g3XH3sVtNbgskCXw
(ID 0 10/10) Item done: SHA256:bPqqOonwSbjzLqe9ZuVRmZkz+DBjaNTZ9HwmQhbdWLI
(ID 0) Workload exiting
恭喜,您的作业已成功运行!
注意
注意,当创建 Job 对象时我们没有指定任何标签。与其他使用标签来识别一组 Pod 的控制器(例如 DaemonSet、ReplicaSet 和 Deployment)类似,如果一个 Pod 被多个对象重用,可能会导致意外行为。
因为作业有一个有限的开始和结束,用户通常会创建许多这样的作业。这使得选择唯一标签更加困难和更为关键。因此,Job 对象将自动选择一个唯一标签,并用它来标识它所创建的 Pod。在高级场景中(例如在不杀死它管理的 Pod 的情况下替换运行中的作业),用户可以选择关闭此自动行为,并手动指定标签和选择器。
我们刚才看到一个作业可以成功完成。但如果出现故障会发生什么呢?让我们试试看。修改我们的配置文件中 kuard 的参数,使其在生成三个密钥后以非零的退出码失败,如 示例 12-2 所示。
示例 12-2. job-oneshot-failure1.yaml
...
spec:
template:
spec:
containers:
...
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-exit-code=1"
- "--keygen-num-to-gen=3"
...
现在使用 kubectl apply -f job-oneshot-failure1.yaml 启动这个。让它运行一段时间,然后查看 Pod 的状态:
$ kubectl get pod -l job-name=oneshot
NAME READY STATUS RESTARTS AGE
oneshot-3ddk0 0/1 CrashLoopBackOff 4 3m
这里我们看到同一个 Pod 已经重新启动了四次。Kubernetes 的这个 Pod 处于 CrashLoopBackOff 状态。在某些情况下,可能会有某个地方的 bug 导致程序在启动后立即崩溃。在这种情况下,Kubernetes 会等待一段时间再重新启动 Pod,以避免出现崩溃循环,这会消耗节点上的资源。所有这些都由 kubelet 在节点本地处理,而不涉及作业本身。
终止该作业(kubectl delete jobs oneshot),让我们尝试其他方法。再次修改配置文件,并将 restartPolicy 从 OnFailure 改为 Never。使用 kubectl apply -f jobs-oneshot-failure2.yaml 启动它。
如果我们让这个运行一段时间,然后查看相关的 Pods,我们会发现一些有趣的事情:
$ kubectl get pod -l job-name=oneshot -a
NAME READY STATUS RESTARTS AGE
oneshot-0wm49 0/1 Error 0 1m
oneshot-6h9s2 0/1 Error 0 39s
oneshot-hkzw0 1/1 Running 0 6s
oneshot-k5swz 0/1 Error 0 28s
oneshot-m1rdw 0/1 Error 0 19s
oneshot-x157b 0/1 Error 0 57s
我们可以看到这里有多个 Pods 出现错误。通过设置 restartPolicy: Never,我们告诉 kubelet 在失败时不重新启动 Pod,而只是声明 Pod 已失败。然后 Job 对象会察觉到并创建一个新的 Pod 替代它。如果不小心的话,这可能会在集群中产生大量的“垃圾”。因此,我们建议您使用 restartPolicy: OnFailure,这样失败的 Pods 将在原地重新运行。您可以使用 kubectl delete jobs oneshot 清理这些。
到目前为止,我们已经看到程序因为退出并返回非零的退出码而失败。但工作器可能以其他方式失败。具体来说,它们可能会卡住并且无法继续向前进展。为了帮助处理这种情况,您可以在作业中使用活跃性探针。如果活跃性探针策略确定一个 Pod 已经死亡,它会为您重新启动或替换它。
并行性
生成密钥可能会很慢。让我们一起启动一堆工作器,以加快密钥生成速度。我们将使用 completions 和 parallelism 参数的组合。我们的目标是通过运行 10 次 kuard,每次运行生成 10 个密钥,共生成 100 个密钥。但我们不想淹没我们的集群,所以我们将一次限制在五个 Pods。
这意味着将 completions 设置为 10,parallelism 设置为 5。配置如 示例 12-3 所示。
示例 12-3. job-parallel.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: parallel
labels:
chapter: jobs
spec:
parallelism: 5
completions: 10
template:
metadata:
labels:
chapter: jobs
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: Always
command:
- "/kuard"
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-num-to-gen=10"
restartPolicy: OnFailure
启动它:
$ kubectl apply -f job-parallel.yaml
job.batch/parallel created
现在观察 Pods 的启动、执行和退出情况。直到总共有 10 个 Pods 完成为止。在这里,我们使用 --watch 标志让 kubectl 保持运行,并实时列出变化:
$ kubectl get pods -w
NAME READY STATUS RESTARTS AGE
parallel-55tlv 1/1 Running 0 5s
parallel-5s7s9 1/1 Running 0 5s
parallel-jp7bj 1/1 Running 0 5s
parallel-lssmn 1/1 Running 0 5s
parallel-qxcxp 1/1 Running 0 5s
NAME READY STATUS RESTARTS AGE
parallel-jp7bj 0/1 Completed 0 26s
parallel-tzp9n 0/1 Pending 0 0s
parallel-tzp9n 0/1 Pending 0 0s
parallel-tzp9n 0/1 ContainerCreating 0 1s
parallel-tzp9n 1/1 Running 0 1s
parallel-tzp9n 0/1 Completed 0 48s
parallel-x1kmr 0/1 Pending 0 0s
...
随意学习已完成的作业,并检查它们的日志,查看它们生成的密钥指纹。通过使用 kubectl delete job parallel 删除已完成的作业对象来清理。
工作队列
作业的一个常见用例是从工作队列处理工作。在这种情况下,某些任务创建多个工作项并将它们发布到工作队列。可以运行工作者作业来处理每个工作项,直到工作队列为空(参见 图 12-1)。

图 12-1. 并行作业
启动工作队列
我们首先启动一个集中式工作队列服务。kuard 内置了一个简单的基于内存的工作队列系统。我们将启动一个 kuard 实例来充当所有工作的协调者。
接下来,我们创建一个简单的 ReplicaSet 来管理一个单例工作队列守护程序。我们使用 ReplicaSet 来确保在机器故障时会创建一个新的 Pod,如 示例 12-4 所示。
示例 12-4. rs-queue.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
name: queue
spec:
replicas: 1
selector:
matchLabels:
app: work-queue
component: queue
chapter: jobs
template:
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
spec:
containers:
- name: queue
image: "gcr.io/kuar-demo/kuard-amd64:blue"
imagePullPolicy: Always
使用以下命令运行工作队列:
$ kubectl apply -f rs-queue.yaml
replicaset.apps/queue created
此时,工作队列守护程序应该已经启动和运行。让我们使用端口转发连接到它。在终端窗口中保持此命令运行:
$ kubectl port-forward rs/queue 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
你可以打开浏览器访问 http://localhost:8080 并查看 kuard 接口。切换到“MemQ Server”选项卡以监视当前情况。
将工作队列服务器放置好后,下一步是使用服务来暴露它。这将使生产者和消费者可以通过 DNS 找到工作队列,如 示例 12-5 所示。
示例 12-5. service-queue.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
name: queue
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: work-queue
component: queue
使用 kubectl 创建队列服务:
$ kubectl apply -f service-queue.yaml
service/queue created
加载队列
现在我们已经准备好将一堆工作项放入队列中了。为了简单起见,我们将使用 curl 驱动工作队列服务器的 API 并插入一堆工作项。curl 将通过我们之前设置的 kubectl port-forward 与工作队列通信,如 示例 12-6 所示。
示例 12-6. load-queue.sh
# Create a work queue called 'keygen'
curl -X PUT localhost:8080/memq/server/queues/keygen
# Create 100 work items and load up the queue.
for i in work-item-{0..99}; do
curl -X POST localhost:8080/memq/server/queues/keygen/enqueue \
-d "$i"
done
运行这些命令,你应该会在终端看到输出了 100 个 JSON 对象,并为每个工作项生成一个唯一的消息标识符。你可以通过 UI 中的“MemQ Server”选项卡确认队列的状态,或者直接通过工作队列 API 查询:
$ curl 127.0.0.1:8080/memq/server/stats
{
"kind": "stats",
"queues": [
{
"depth": 100,
"dequeued": 0,
"drained": 0,
"enqueued": 100,
"name": "keygen"
}
]
}
现在我们已经准备好启动一个任务来消耗工作队列直到为空。
创建消费者任务
这就是事情变得有趣的地方!kuard 也可以作为消费者模式运行。我们可以设置它从工作队列中获取工作项,创建一个密钥,然后在队列为空时退出,如 示例 12-7 所示。
示例 12-7. job-consumers.yaml
apiVersion: batch/v1
kind: Job
metadata:
labels:
app: message-queue
component: consumer
chapter: jobs
name: consumers
spec:
parallelism: 5
template:
metadata:
labels:
app: message-queue
component: consumer
chapter: jobs
spec:
containers:
- name: worker
image: "gcr.io/kuar-demo/kuard-amd64:blue"
imagePullPolicy: Always
command:
- "/kuard"
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-memq-server=http://queue:8080/memq/server"
- "--keygen-memq-queue=keygen"
restartPolicy: OnFailure
在这里,我们告诉作业并行启动五个 Pod。由于 completions 参数未设置,我们将作业放入工作池模式。一旦第一个 Pod 以零退出代码退出,作业将开始收尾,并且不会启动任何新的 Pod。这意味着在工作完成之前,没有任何工作者应该退出,他们都在结束工作的过程中。
现在,创建 consumers 作业:
$ kubectl apply -f job-consumers.yaml
job.batch/consumers created
然后,您可以查看支持作业的 Pod:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
queue-43s87 1/1 Running 0 5m
consumers-6wjxc 1/1 Running 0 2m
consumers-7l5mh 1/1 Running 0 2m
consumers-hvz42 1/1 Running 0 2m
consumers-pc8hr 1/1 Running 0 2m
consumers-w20cc 1/1 Running 0 2m
请注意,有五个 Pod 并行运行。这些 Pod 将继续运行,直到工作队列为空。您可以在工作队列服务器的 UI 上观察到这一过程。随着队列变空,消费者 Pod 将干净退出,consumers 作业将被视为完成。
清理
使用标签,我们可以清理本节中创建的所有内容:
$ kubectl delete rs,svc,job -l chapter=jobs
CronJobs
有时,您希望安排作业在特定间隔运行。为实现此目的,您可以在 Kubernetes 中声明一个 CronJob,负责在特定间隔创建一个新的 Job 对象。示例 12-8 是一个 CronJob 声明的示例:
示例 12-8. job-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: example-cron
spec:
# Run every fifth hour
schedule: "0 */5 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: batch-job
image: my-batch-image
restartPolicy: OnFailure
请注意 spec.schedule 字段,其中包含 CronJob 的间隔,采用标准的 cron 格式。
您可以将此文件保存为 job-cronjob.yaml,并使用 kubectl create -f cron-job.yaml 创建 CronJob。如果您对 CronJob 的当前状态感兴趣,可以使用 kubectl describe *<cron-job>* 获取详细信息。
摘要
在单个集群上,Kubernetes 可以处理长时间运行的工作负载,如 Web 应用程序和短暂的工作负载,如批处理作业。作业抽象允许您对批处理作业模式进行建模,从简单的一次性任务到处理许多项目直到工作耗尽的并行作业。
作业是一种低级原语,可直接用于简单的工作负载。但是,Kubernetes 是从头开始构建的,可以通过更高级别的对象进行扩展。作业也不例外;更高级别的编排系统可以轻松使用它们来承担更复杂的任务。
第十三章:ConfigMaps 和 Secrets
尽可能使容器镜像可重复使用是一个好的实践。同一个镜像应该能够用于开发、暂存和生产环境。如果同一个镜像足够通用,能够跨应用和服务使用,那就更好了。如果每个新环境都需要重新创建镜像,测试和版本控制就会变得更加风险和复杂。那么在运行时如何专门化使用该镜像呢?
这就是 ConfigMaps 和 Secrets 发挥作用的地方。ConfigMaps 用于为工作负载提供配置信息。这既可以是像字符串一样的细粒度信息,也可以是文件形式的复合值。Secrets 类似于 ConfigMaps,但专注于使敏感信息对工作负载可用。它们可以用于诸如凭据或 TLS 证书之类的内容。
ConfigMaps
将 ConfigMap 视为 Kubernetes 对象之一,用于定义一个小型文件系统。另一种方式是作为在定义容器环境或命令行时可使用的变量集合。需要注意的关键点是,ConfigMap 与 Pod 在运行前被合并。这意味着容器镜像和 Pod 定义可以通过更改使用的 ConfigMap 被许多工作负载重复使用。
创建 ConfigMaps
让我们马上创建一个 ConfigMap。像 Kubernetes 中的许多对象一样,您可以通过即时和命令式的方式创建它们,或者可以从磁盘上的清单创建它们。我们将从即时方法开始。
首先,假设我们在磁盘上有一个文件(称为 my-config.txt),我们希望将其提供给相关的 Pod 使用,如 示例 13-1 中所示。
示例 13-1. my-config.txt
# This is a sample config file that I might use to configure an application
parameter1 = value1
parameter2 = value2
接下来,让我们用这个文件创建一个 ConfigMap。我们还会在这里添加几个简单的键值对。这些在命令行上被称为字面值:
$ kubectl create configmap my-config \
--from-file=my-config.txt \
--from-literal=extra-param=extra-value \
--from-literal=another-param=another-value
刚刚创建的 ConfigMap 对象的等效 YAML 如下所示:
$ kubectl get configmaps my-config -o yaml
apiVersion: v1
data:
another-param: another-value
extra-param: extra-value
my-config.txt: |
# This is a sample config file that I might use to configure an application
parameter1 = value1
parameter2 = value2
kind: ConfigMap
metadata:
creationTimestamp: ...
name: my-config
namespace: default
resourceVersion: "13556"
selfLink: /api/v1/namespaces/default/configmaps/my-config
uid: 3641c553-f7de-11e6-98c9-06135271a273
如您所见,ConfigMap 只是一个存储在对象中的键值对。有趣的部分是当您尝试使用 ConfigMap 时。
使用 ConfigMap
使用 ConfigMap 的三种主要方式:
文件系统
您可以将 ConfigMap 挂载到 Pod 中。根据键名,为每个条目创建一个文件。该文件的内容设置为该值。
环境变量
ConfigMap 可以用于动态设置环境变量的值。
命令行参数
Kubernetes 支持基于 ConfigMap 值动态创建容器的命令行。
让我们为 kuard 创建一个清单,将所有这些内容汇总到一起,如 示例 13-2 中所示。
示例 13-2. kuard-config.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard-config
spec:
containers:
- name: test-container
image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: Always
command:
- "/kuard"
- "$(EXTRA_PARAM)"
env:
# An example of an environment variable used inside the container
- name: ANOTHER_PARAM
valueFrom:
configMapKeyRef:
name: my-config
key: another-param
# An example of an environment variable passed to the command to start
# the container (above).
- name: EXTRA_PARAM
valueFrom:
configMapKeyRef:
name: my-config
key: extra-param
volumeMounts:
# Mounting the ConfigMap as a set of files
- name: config-volume
mountPath: /config
volumes:
- name: config-volume
configMap:
name: my-config
restartPolicy: Never
对于文件系统方法,我们在 Pod 内部创建一个新的卷,并给它命名为 config-volume。然后我们将此卷定义为 ConfigMap 卷,并指向要挂载的 ConfigMap。我们必须指定将其挂载到 kuard 容器中的位置,使用 volumeMount。在这种情况下,我们将其挂载在 /config。
环境变量使用特殊的 valueFrom 成员进行指定。这引用了 ConfigMap 及其内部的数据键。命令行参数基于环境变量构建。Kubernetes 将使用特殊的 $(*<env-var-name>*) 语法执行正确的替换。
运行此 Pod,并让我们进行端口转发,以查看应用程序如何看待这个世界:
$ kubectl apply -f kuard-config.yaml
$ kubectl port-forward kuard-config 8080
现在将浏览器指向 http://localhost:8080。我们可以看看如何以三种方式将配置值注入到程序中。点击左侧的“Server Env”选项卡。这将显示应用程序启动时使用的命令行及其环境,如 图 13-1 所示。

图 13-1. 显示 kuard 的环境
在这里,我们可以看到我们添加了两个环境变量(ANOTHER_PARAM 和 EXTRA_PARAM),它们的值通过 ConfigMap 设置。我们还在 kuard 的命令行中添加了一个基于 EXTRA_PARAM 值的参数。
接下来,点击“文件系统浏览器”选项卡(图 13-2)。这允许您探索应用程序所见的文件系统。您应该看到一个名为 /config 的条目。这是基于我们的 ConfigMap 创建的卷。如果您进入其中,您将看到每个 ConfigMap 条目都创建了一个文件。您还将看到一些隐藏文件(以 .. 开头),这些文件用于在更新 ConfigMap 时进行新值的干净交换。

图 13-2. 通过 kuard 查看的 /config 目录
机密
虽然 ConfigMaps 对于大多数配置数据非常适用,但某些数据非常敏感。这包括密码、安全令牌或其他类型的私钥。总称为这类数据“Secrets”。Kubernetes 具有本地支持,用于安全地存储和处理此类数据。
Secrets 允许创建不捆绑敏感数据的容器映像。这使得容器可以在不同环境中保持可移植性。Secrets 通过在 Pod 清单和 Kubernetes API 中显式声明向 Pod 暴露。通过这种方式,Kubernetes Secrets API 提供了一种面向应用程序的机制,用于以易于审计的方式向应用程序公开敏感的配置信息,并利用本地操作系统隔离原语。
本节的其余部分将探讨如何创建和管理 Kubernetes Secrets,并提出将 Secrets 暴露给需要它们的 Pod 的最佳实践。
警告
默认情况下,Kubernetes Secrets 以明文形式存储在集群的 etcd 存储中。根据您的需求,这可能不足以提供足够的安全性。特别是,在您的集群中具有集群管理权限的任何人都可以读取集群中的所有 Secrets。
在最近的 Kubernetes 版本中,已添加了使用用户提供的密钥对 Secrets 进行加密的支持,通常集成到云密钥存储中。此外,大多数云密钥存储与 Kubernetes Secrets Store CSI Driver volumes 集成,使您可以完全跳过 Kubernetes Secrets,完全依赖于云提供商的密钥存储。所有这些选项都应为您提供足够的工具来创建符合您需求的安全配置文件。
创建 Secrets
使用 Kubernetes API 或 kubectl 命令行工具创建 Secrets。Secrets 作为一组键/值对持有一个或多个数据元素。
在本节中,我们将创建一个 Secret 来存储 kuard 应用程序的 TLS 密钥和证书,以满足前面列出的存储要求。
注意
kuard 容器镜像不捆绑 TLS 证书或密钥。这使得 kuard 容器可以跨环境移植,并通过公共 Docker 仓库分发。
创建 Secret 的第一步是获取要存储的原始数据。可以通过运行以下命令下载 kuard 应用程序的 TLS 密钥和证书:
$ curl -o kuard.crt https://storage.googleapis.com/kuar-demo/kuard.crt
$ curl -o kuard.key https://storage.googleapis.com/kuar-demo/kuard.key
警告
这些证书与世界共享,并没有提供实际的安全性。除了在这些示例中作为学习工具外,请不要使用它们。
使用本地存储的 kuard.crt 和 kuard.key 文件,我们已准备好创建 Secret。使用 create secret 命令创建名为 kuard-tls 的 Secret:
$ kubectl create secret generic kuard-tls \
--from-file=kuard.crt \
--from-file=kuard.key
kuard-tls Secret 已创建并包含两个数据元素。运行以下命令以获取详细信息:
$ kubectl describe secrets kuard-tls
Name: kuard-tls
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
kuard.crt: 1050 bytes
kuard.key: 1679 bytes
有了 kuard-tls Secret,我们可以通过使用 Secrets volume 从 Pod 中消费它。
消费 Secrets
应用程序可以使用 Kubernetes REST API 消费 Secrets,前提是它们知道如何直接调用该 API。然而,我们的目标是保持应用程序的可移植性。它们不仅应该在 Kubernetes 中运行良好,而且应该在其他平台上不经修改地运行。
不再通过 API 服务器访问 Secrets,我们可以使用Secrets volume。使用 Secrets volume 类型,可以向 Pods 公开 Secret 数据。Secrets volumes 由 kubelet 管理,并在 Pod 创建时创建。Secrets 存储在 tmpfs volume(也称为 RAM 磁盘)上,因此不会写入节点的磁盘。
Secret 的每个数据元素存储在卷挂载中指定的目标挂载点下的单独文件中。kuard-tls Secret 包含两个数据元素:kuard.crt 和 kuard.key。将 kuard-tls Secrets volume 挂载到 /tls 将生成以下文件:
/tls/kuard.crt
/tls/kuard.key
在 Example 13-3 中的 Pod 配置文件中演示了如何声明一个 Secrets 卷,这会在/tls下向kuard容器公开kuard-tls Secret。
Example 13-3. kuard-secret.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard-tls
spec:
containers:
- name: kuard-tls
image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: Always
volumeMounts:
- name: tls-certs
mountPath: "/tls"
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: kuard-tls
使用 kubectl 创建 kuard-tls Pod 并观察正在运行的 Pod 的日志输出:
$ kubectl apply -f kuard-secret.yaml
运行以下命令连接到 Pod:
$ kubectl port-forward kuard-tls 8443:8443
现在在浏览器中输入 https://localhost:8443。你会看到一些无效证书警告,因为这是为 kuard.example.com 自签名的证书。如果你忽略此警告,你会看到通过 HTTPS 托管的kuard服务器。使用“文件系统浏览器”选项卡在/tls目录中找到磁盘上的证书。
私有容器注册表
Secrets 的一个特殊用例是存储私有容器注册表的访问凭据。Kubernetes 支持使用存储在私有注册表上的镜像,但访问这些镜像需要凭据。私有镜像可以存储在一个或多个私有注册表中。这对于在集群的每个可能的节点上管理每个私有注册表的凭据是一个挑战。
Image pull Secrets 利用 Secrets API 自动分发私有注册表凭据。Image pull Secrets 存储方式与常规 Secrets 相同,但通过spec.imagePullSecrets Pod 规范字段消费。
使用kubectl create secret docker-registry来创建这种特殊类型的 Secret:
$ kubectl create secret docker-registry my-image-pull-secret \
--docker-username=*<username>* \
--docker-password=*<password>* \
--docker-email=*<email-address>*
通过在 Pod 配置文件中引用镜像 pull secret 来启用对私有仓库的访问,如 Example 13-4 中所示。
Example 13-4. kuard-secret-ips.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard-tls
spec:
containers:
- name: kuard-tls
image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: Always
volumeMounts:
- name: tls-certs
mountPath: "/tls"
readOnly: true
imagePullSecrets:
- name: my-image-pull-secret
volumes:
- name: tls-certs
secret:
secretName: kuard-tls
如果你反复从同一注册表中拉取,可以将 Secrets 添加到与每个 Pod 关联的默认服务账户中,以避免在创建每个 Pod 时都需要指定 Secrets。
命名约束
Secret 或 ConfigMap 中数据项的键名称定义为映射到有效环境变量名称。它们可以以点开头,然后是一个字母或数字,后跟包括点、破折号和下划线的字符。点不能重复,且点与下划线或破折号不能相邻。更正式地说,它们必须符合正则表达式 ^[.]?a-zAZ0-9*$。 Table 13-1 中提供了 ConfigMaps 和 Secrets 的一些有效和无效名称示例。
Table 13-1. ConfigMap 和 Secret 键示例
| 有效键名称 | 无效键名称 |
|---|---|
.auth_token |
Token..properties |
Key.pem |
auth file.json |
config_file |
_password.txt |
注意
当选择关键名称时,请记住这些键可能会通过卷挂载暴露给 Pod。选择一个在命令行或配置文件中指定时能够有意义的名称。将 TLS 密钥命名为key.pem比在配置应用程序以访问 Secrets 时使用tls-key更清晰。
ConfigMap 数据值是在清单中直接指定的简单 UTF-8 文本。Secret 数据值使用 base64 编码的任意数据。使用 base64 编码使得可以存储二进制数据。然而,这也使得在 YAML 文件中管理作为 base64 编码值存储的 Secrets 更加困难。请注意,ConfigMap 或 Secret 的最大大小为 1 MB。
管理 ConfigMaps 和 Secrets
ConfigMaps 和 Secrets 通过 Kubernetes API 进行管理。通常的 create、delete、get 和 describe 命令适用于操作这些对象。
列表
您可以使用 kubectl get secrets 命令列出当前命名空间中的所有 Secrets:
$ kubectl get secrets
NAME TYPE DATA AGE
default-token-f5jq2 kubernetes.io/service-account-token 3 1h
kuard-tls Opaque 2 20m
同样地,您可以列出命名空间中的所有 ConfigMaps:
$ kubectl get configmaps
NAME DATA AGE
my-config 3 1m
可以使用 kubectl describe 获取单个对象的更多详细信息:
$ kubectl describe configmap my-config
Name: my-config
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
another-param: 13 bytes
extra-param: 11 bytes
my-config.txt: 116 bytes
最后,您可以使用类似以下命令查看原始数据(包括 Secrets 中的值):kubectl get configmap my-config -o yaml 或 kubectl get secret kuard-tls -o yaml。
创建
创建 Secret 或 ConfigMap 的最简单方法是通过 kubectl create secret generic 或 kubectl create configmap。有多种方法可以指定放入 Secret 或 ConfigMap 中的数据项。这些可以结合在一个命令中:
--from-file=*<filename>*
使用文件加载具有与文件名相同的 Secret 数据键。
--from-file=*<key>*=*<filename>*
从明确指定了 Secret 数据键的文件加载。
--from-file=*<directory>*
加载指定目录中的所有文件,其中文件名是可接受的键名。
--from-literal=*<key>*=*<value>*
直接使用指定的键/值对。
更新
您可以更新 ConfigMap 或 Secret,并在运行的应用程序中反映出来。如果应用程序配置为重新读取配置值,则无需重新启动。接下来,我们将描述更新 ConfigMaps 或 Secrets 的三种方式。
从文件更新
如果您有 ConfigMap 或 Secret 的清单,可以直接编辑它,并使用 kubectl replace -f <*filename*> 替换为新版本。如果之前使用 kubectl apply 创建了资源,还可以使用 kubectl apply -f *<filename>*。
由于数据文件编码到这些对象的方式,更新配置可能有些繁琐;没有支持从外部文件加载数据的 kubectl 命令。数据必须直接存储在 YAML 文件中。
最常见的用例是当 ConfigMap 定义为目录或资源列表的一部分,并且所有内容都一起创建和更新。通常这些清单将被提交到源代码控制中。
警告
将 Secret YAML 文件提交到源代码控制通常是一个不好的主意,因为很容易无意中将这些文件推送到公共位置并泄露您的 Secrets。
重新创建和更新
如果你将输入存储到 ConfigMaps 或 Secrets 中作为独立的文件(而不是直接嵌入到 YAML 中),你可以使用 kubectl 重新创建清单,然后使用它来更新对象,看起来像这样:
$ kubectl create secret generic kuard-tls \
--from-file=kuard.crt --from-file=kuard.key \
--dry-run -o yaml | kubectl replace -f -
此命令行首先使用与现有 Secret 同名的新 Secret。如果我们仅止步于此,Kubernetes API 服务器将返回错误, complaining that we are trying to create a Secret that already exists. Instead, we tell kubectl not to actually send the data to the server but instead to dump the YAML that it would have sent to the API server to stdout. We then pipe that to kubectl replace and use -f - to tell it to read from stdin. In this way, we can update a Secret from files on disk without having to manually base64-encode data.
编辑当前版本
更新 ConfigMap 的最终方法是使用 kubectl edit 在编辑器中提供 ConfigMap 的一个版本,以便你可以进行微调(你也可以用 Secret 做同样的事情,但你将被困在自己管理值的 base64 编码中):
$ kubectl edit configmap my-config
你应该在编辑器中看到 ConfigMap 的定义。进行所需的更改,然后保存并关闭编辑器。对象的新版本将被推送到 Kubernetes API 服务器。
实时更新
使用 API 更新 ConfigMap 或 Secret 后,它将自动推送到使用该 ConfigMap 或 Secret 的所有卷。可能需要几秒钟,但文件列表和文件内容(由 kuard 看到)将使用这些新值进行更新。使用此实时更新功能,你可以更新应用程序的配置而无需重启它们。
目前没有内置的方法来在部署 ConfigMap 的新版本时向应用程序发出信号。这取决于应用程序(或某些辅助脚本)来查找要更改和重新加载的配置文件。
使用 kuard 中的文件浏览器(通过 kubectl port-forward 访问)是与动态更新 Secrets 和 ConfigMaps 互动的好方法。
摘要
ConfigMaps 和 Secrets 是提供应用程序动态配置的好方法。它们允许你只需创建一个容器镜像(和 Pod 定义),然后在不同的上下文中重复使用它。这可以包括从开发到分级再到生产时使用完全相同的镜像。它还可以包括在多个团队和服务之间使用单个镜像。将配置与应用程序代码分离将使你的应用程序更可靠和可重用。
第十四章:Kubernetes 的基于角色的访问控制
此时,您几乎会遇到的每个 Kubernetes 集群都已启用基于角色的访问控制(RBAC)。所以您很可能之前就遇到过 RBAC。也许最初您无法访问您的集群,直到使用某些神奇的命令添加了一个 RoleBinding 来映射一个用户到一个角色。即使您可能已经接触过 RBAC,您可能还没有很好地理解 Kubernetes 中的 RBAC,包括其用途和如何使用它。
基于角色的访问控制提供了一种机制,用于限制对 Kubernetes API 的访问和操作,以确保只有授权的用户能够访问。RBAC 是加固您部署应用程序的 Kubernetes 集群访问权限的关键组成部分,(可能更重要的是)防止一个人在错误的命名空间误以为正在销毁他们的测试集群时,意外地关闭生产环境。
注意
虽然 RBAC 在限制对 Kubernetes API 的访问方面非常有用,但重要的是要记住,任何能在 Kubernetes 集群内运行任意代码的人都可以有效地获得整个集群的 root 权限。有一些方法可以使此类攻击变得更加困难和昂贵,正确的 RBAC 设置就是这种防御的一部分。但如果您专注于敌对的多租户安全性,仅仅 RBAC 本身是足以保护您的。您必须隔离运行在集群中的 Pods,以提供有效的多租户安全性。通常,这是通过使用隔离容器或容器沙箱来完成的。
在我们深入了解 Kubernetes 中的 RBAC 的详细信息之前,了解 RBAC 作为一个概念以及身份验证和授权的高层理解是非常有价值的。
每个对 Kubernetes 的请求首先需要进行认证。认证提供了发出请求的调用者的身份。可以简单地说该请求未经身份验证,或者它可以深度集成到可插拔的身份验证提供程序(例如 Azure Active Directory)中,在该第三方系统中建立一个身份。有趣的是,Kubernetes 并没有内置的身份存储,而是专注于将其他身份源集成到自身中。
一旦用户经过身份验证,授权阶段确定他们是否被授权执行该请求。授权是用户的身份、资源(实际上是 HTTP 路径)以及用户试图执行的动作的组合。如果特定用户被授权在该资源上执行该操作,则允许该请求继续进行。否则,将返回 HTTP 403 错误。让我们深入了解这个过程。
基于角色的访问控制
要在 Kubernetes 中正确管理访问权限,理解身份、角色和角色绑定如何交互以控制谁可以使用哪些资源是至关重要的。刚开始时,RBAC 可能看起来像是一个难以理解的挑战,由一系列相互连接的抽象概念组成;但一旦理解了,您就可以确信自己能够有效地管理集群访问权限。
Kubernetes 中的身份
每个对 Kubernetes 的请求都与某个身份相关联。即使是没有身份的请求也与system:unauthenticated组相关联。Kubernetes 区分用户身份和服务账户身份。服务账户由 Kubernetes 自身创建和管理,通常与集群内运行的组件相关联。用户账户则是与集群的实际用户相关联的所有其他账户,通常包括在集群外运行的持续交付服务等自动化服务。
Kubernetes 为身份验证提供者使用通用接口。每个提供者提供一个用户名,以及可选的用户所属组集合。Kubernetes 支持多种身份验证提供者,包括:
-
HTTP 基本身份验证(已大部分废弃)
-
x509 客户端证书
-
主机上的静态令牌文件
-
云身份验证提供者,例如 Azure Active Directory 和 AWS 身份和访问管理(IAM)
-
身份验证 Webhook
尽管大多数托管的 Kubernetes 安装为您配置了身份验证,但如果您正在部署自己的身份验证,则需要适当地配置 Kubernetes API 服务器上的标志。
在您的集群中,应始终为不同的应用程序使用不同的身份。例如,您应该为生产前端使用一个身份,为生产后端使用另一个身份,并且所有生产身份应该与开发身份不同。您还应为不同的集群使用不同的身份。所有这些身份应该是不与用户共享的机器身份。您可以使用 Kubernetes 服务账户来实现这一点,或者您可以使用由您的身份系统提供的 Pod 身份提供者;例如,Azure Active Directory 提供了一个Pod 的开源身份提供者,其他流行的身份提供者也提供类似的解决方案。
理解 Kubernetes 中的角色和角色绑定
身份仅是 Kubernetes 授权的起点。一旦 Kubernetes 知道请求的身份,它就需要确定是否授权该用户执行该请求。为了实现这一点,它使用角色和角色绑定。
角色是一组抽象的能力。例如,appdev角色可能代表创建 Pod 和服务的能力。角色绑定是将角色分配给一个或多个身份的过程。因此,将appdev角色绑定到用户身份alice表示 Alice 具有创建 Pod 和服务的能力。
Kubernetes 中的角色和角色绑定
在 Kubernetes 中,有两对相关资源表示角色和角色绑定。一对作用域是命名空间级别(Role 和 RoleBinding),而另一对作用域是集群级别(ClusterRole 和 ClusterRoleBinding)。
让我们首先检查 Role 和 RoleBinding。Role 资源是命名空间化的,表示该单个命名空间中的功能。你不能将命名空间角色用于非命名空间资源(例如 CustomResourceDefinitions),并且将 RoleBinding 绑定到角色只在包含 Role 和 RoleBinding 的 Kubernetes 命名空间中提供授权。
作为一个具体的例子,这里有一个简单的角色,赋予一个身份创建和修改 Pods 和 Services 的能力:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-and-services
rules:
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
要将此角色绑定到用户 alice,我们需要创建一个如下所示的 RoleBinding。这个角色绑定还将组 mydevs 绑定到相同的角色:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: default
name: pods-and-services
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: alice
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: mydevs
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: pod-and-services
有时候你需要创建一个适用于整个集群的角色,或者你想要限制对集群级资源的访问。为了实现这一点,你可以使用 ClusterRole 和 ClusterRoleBinding 资源。它们在很大程度上与它们的命名空间对等物相同,但是作用域是集群级的。
Kubernetes 角色的动词
角色定义了对资源(例如 Pods)以及描述可以在该资源上执行的动作的动词。这些动词大致对应于 HTTP 方法。Kubernetes RBAC 中常用的动词列在 Table 14-1 中。
Table 14-1. Kubernetes RBAC 常见动词
| Verb | HTTP 方法 | 描述 |
|---|---|---|
create |
POST |
创建新资源。 |
delete |
DELETE |
删除现有资源。 |
get |
GET |
获取资源。 |
list |
GET |
列出资源集合。 |
patch |
PATCH |
通过部分更改修改现有资源。 |
update |
PUT |
通过完整对象修改现有资源。 |
watch |
GET |
监视资源的流式更新。 |
proxy |
GET |
通过流式 WebSocket 代理连接资源。 |
使用内建角色
设计自己的角色可能会很复杂且耗时。Kubernetes 有大量用于已知系统身份(例如调度程序)的内建集群角色,需要已知的一组功能。你可以通过运行以下命令查看这些角色:
$ kubectl get clusterroles
虽然大多数内建角色是为系统实用程序设计的,但其中四个是为通用最终用户设计的:
-
cluster-admin角色提供对整个集群的完全访问权限。 -
admin角色提供对完整命名空间的完全访问权限。 -
edit角色允许最终用户在命名空间中修改资源。 -
view角色允许对命名空间进行只读访问。
大多数集群已经设置了大量的 ClusterRole 绑定,你可以使用 kubectl get clusterrolebindings 查看这些绑定。
内建角色的自动协调
当 Kubernetes API 服务器启动时,它会自动安装一些默认的 ClusterRoles,这些 ClusterRoles 是在 API 服务器代码中定义的。这意味着,如果您修改任何内置的集群角色,这些修改是暂时的。每当 API 服务器重新启动(例如进行升级)时,您的更改将被覆盖。
为了防止这种情况发生,在进行其他任何修改之前,您需要向内置的 ClusterRole 资源添加 rbac.authorization.kubernetes.io/autoupdate 注解,并将其值设置为 false。如果将此注解设置为 false,则 API 服务器将不会覆盖已修改的 ClusterRole 资源。
警告
默认情况下,Kubernetes API 服务器安装一个集群角色,允许 system:unauthenticated 用户访问 API 服务器的 API 发现端点。对于任何暴露于敌对环境(例如公共互联网)的集群来说,这是一个不好的想法,并且已经至少发生过一个严重的安全漏洞通过这种暴露。如果您在公共互联网或其他敌对环境上运行 Kubernetes 服务,您应确保在您的 API 服务器上设置 --anonymous-auth=false 标志。
管理 RBAC 的技术
管理集群的 RBAC 可能会很复杂和令人沮丧。可能更令人担忧的是,错误配置的 RBAC 可能会导致安全问题。幸运的是,有几种工具和技术可以使 RBAC 管理变得更加容易。
使用 can-i 进行授权测试
第一个有用的工具是 kubectl 的 auth can-i 命令。此工具用于测试特定用户是否可以执行特定操作。您可以在配置集群时使用 can-i 验证配置设置,或者要求用户在提交错误或 bug 报告时使用该工具验证其访问权限。
在其最简单的用法中,can-i 命令接受一个动词和一个资源。例如,此命令将指示当前 kubectl 用户是否被授权创建 Pods:
$ kubectl auth can-i create pods
您还可以使用 --subresource 命令行标志测试子资源,如日志或端口转发:
$ kubectl auth can-i get pods --subresource=logs
在源代码控制中管理 RBAC
与 Kubernetes 中的所有资源一样,RBAC 资源使用 YAML 建模。鉴于这种基于文本的表示形式,将这些资源存储在版本控制中是有意义的,这允许进行问责、审计和回滚。
kubectl 命令行工具提供了一个 reconcile 命令,类似于 kubectl apply,它将与集群的当前状态协调一组角色和角色绑定。您可以运行:
$ kubectl auth reconcile -f some-rbac-config.yaml
如果您希望在应用更改之前查看它们,可以在命令中添加 --dry-run 标志以输出但不应用更改。
高级主题
一旦您掌握了基于角色的访问控制的基础知识,管理 Kubernetes 集群的访问就相对容易了。但是,当管理大量用户或角色时,您可以使用其他高级功能来扩展 RBAC 的管理能力。
聚合 ClusterRoles
有时您希望能够定义其他角色的组合角色。一种选择是简单地将一个 ClusterRole 的所有规则克隆到另一个 ClusterRole 中,但这很复杂且容易出错,因为对一个 ClusterRole 的更改不会自动反映在另一个 ClusterRole 中。相反,Kubernetes RBAC 支持使用聚合规则来将多个角色组合成一个新角色。此新角色结合了所有聚合角色的所有功能,并且任何对任何组成子角色的更改都将自动传播回聚合角色中。
与 Kubernetes 中的其他聚合或分组类似,要聚合的 ClusterRoles 是使用标签选择器指定的。在这种特定情况下,ClusterRole 资源中的aggregationRule字段包含一个clusterRoleSelector字段,后者是一个标签选择器。所有与此选择器匹配的 ClusterRole 资源都动态聚合到聚合 ClusterRole 资源的rules数组中。
管理 ClusterRole 资源的最佳实践是创建多个细粒度的集群角色,然后将它们聚合以形成更高级别或更广泛的集群角色。这就是内置集群角色的定义方式。例如,您可以看到内置的edit角色如下所示:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: edit
...
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.authorization.k8s.io/aggregate-to-edit: "true"
...
这意味着edit角色被定义为所有具有标签rbac.authorization.k8s.io/aggregate-to-edit设置为true的 ClusterRole 对象的集合。
使用组进行绑定
在管理不同组织中大量人员且具有相似集群访问权限时,通常最佳做法是使用组来管理定义访问权限的角色,而不是单独向特定身份添加绑定。当将组绑定到角色或 ClusterRole 时,属于该组的任何成员都可以访问该角色定义的资源和动作。因此,要使任何个人获得访问组角色的权限,需要将该个人添加到组中。
使用组是管理规模化访问的首选策略,原因有几点。首先,在任何大型组织中,对集群的访问是以某人所在团队为单位定义的,而不是他们的具体身份。例如,属于前端运营团队的人需要访问与前端相关的资源并具有编辑权限,而他们可能只需要对与后端相关的资源具有查看/读取权限。授予组权限使特定团队与其能力之间的关联清晰明了。当向个人授予角色时,很难清楚地了解每个团队所需的适当(即最小)权限,特别是当一个人可能属于多个团队时。
将角色绑定到组而不是个人的额外好处是简单性和一致性。当有人加入或离开团队时,只需简单地将其添加到或从组中移除,而不是必须删除多个不同的角色绑定。如果您不得不为其身份删除多个角色绑定,可能会导致权限过多或过少,从而造成不必要的访问或阻止其执行必要操作。此外,由于只需维护单一组角色绑定集,因此无需大量工作来确保所有团队成员拥有相同且一致的权限集。
注意
许多云服务提供商支持将其身份和访问管理平台集成到 Kubernetes RBAC 中,以便可以与 Kubernetes RBAC 一起使用这些平台的用户和组。
许多组系统支持“即时”访问(JIT),即人们仅在响应事件时(例如半夜的警报页面)临时添加到组中,而不是保持持久访问权限。这意味着您可以审计任何特定时间谁有访问权限,并确保即使是受损身份也不能访问您的生产基础设施。
最后,在许多情况下,这些同样的组用于管理对其他资源(从设施到文档和机器登录)的访问权限。因此,将这些相同的组用于 Kubernetes 访问控制大大简化了管理工作。
要将组绑定到 ClusterRole,使用 subject 绑定中的 Group 类型:
...
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: my-great-groups-name
...
在 Kubernetes 中,组由身份验证提供程序提供。Kubernetes 内部没有强烈的组概念,只是一个身份可以是一个或多个组的一部分,并且这些组可以通过绑定与 Role 或 ClusterRole 关联。
摘要
当您从一个小团队和小集群开始时,每个团队成员拥有等效的集群访问权限就足够了。但随着团队的增长和产品变得更加关键,限制对集群部分区域的访问变得至关重要。在设计良好的集群中,访问权限被限制在最少的人员和能力集上,以便高效地管理集群中的应用程序。
理解 Kubernetes 如何实现 RBAC 及其如何用于控制集群访问权限对于开发人员和集群管理员都非常重要。与构建测试基础设施一样,最佳实践是尽早设置 RBAC。从正确的基础开始要比后期尝试改装要容易得多。希望本章提供的信息为您在集群中添加 RBAC 提供了必要的基础。
第十五章:服务网格
或许仅次于容器,术语服务网格已经成为云原生开发的代名词。然而,就像容器一样,服务网格是一个广义术语,涵盖了各种开源项目以及商业产品。了解服务网格在云原生架构中的一般作用是很有用的。本章将向您展示什么是服务网格,不同的软件项目如何实现它们,以及最后(也是最重要的)在何时将服务网格纳入应用程序,相对于更简单的架构,是有意义的。
注意
在许多抽象的云原生架构图中,似乎服务网格对于云原生架构是必需的。但这在很大程度上并不正确。当考虑采用服务网格时,您必须平衡增加新组件(通常由第三方提供)到依赖列表中所带来的复杂性。在许多情况下,如果现有的 Kubernetes 资源能够满足应用程序的需求,简单依赖这些资源可能更容易和更可靠。
我们之前讨论过 Kubernetes 中的其他网络原语,比如 Services 和 Ingress。鉴于这些网络功能已经存在于 Kubernetes 核心中,为什么还需要向网络层注入额外的功能(以及复杂性)呢?归根结底,这取决于使用这些网络原语的软件应用程序的需求。
Kubernetes 核心中的网络仅意识到应用程序作为目标。Service 和 Ingress 资源都有标签选择器,用于将流量路由到特定的 Pod 集,但除此之外,这些资源在增加附加功能方面相对较少。作为 HTTP 负载均衡器,Ingress 稍微超越了这一点,但是在定义一个适合各种不同现有实现的通用 API 的挑战下,限制了 Ingress API 的功能。如何才能使一个真正的“云原生”HTTP 路由 API 与从裸金属网络设备到未考虑云原生开发的公共云 API 的代理兼容呢?
实际上,服务网格 API 在 Kubernetes 核心之外的发展正是这一挑战的结果。Ingress API 将 HTTP(S)流量从外部世界引入到云原生应用程序中。在 Kubernetes 中的云原生应用程序内部,解放了与现有基础设施兼容的需求,服务网格 API 提供了额外的云原生网络功能。那么这些功能是什么呢?大多数服务网格实现提供了三种一般性能力:网络加密和授权、流量塑形以及可观察性。接下来的部分将依次讨论每一个。
使用 Mutual TLS 进行加密和认证
在微服务架构中,Pod 之间网络流量的加密是安全的关键组成部分。由相互传输层安全性提供的加密,即mTLS,是服务网格的最流行用例之一。虽然开发人员可以自行实现这种加密,但证书处理和流量加密是复杂且难以正确实现的。将加密的实现留给各个开发团队会导致开发人员忘记完全添加加密,或者实现不当。当加密实施不当时,可能会对可靠性产生负面影响,并且在最坏的情况下根本提供不了真正的安全性。相比之下,在你的 Kubernetes 集群上安装服务网格会自动为集群中每个 Pod 之间的网络流量提供加密。服务网格为每个 Pod 添加了一个旁载容器,该容器透明地拦截所有网络通信。除了保护通信外,mTLS 还使用客户端证书为加密添加了身份,因此你的应用程序安全地知道每个网络客户端的身份。
交通整形
当你首次考虑应用程序设计时,通常会有一个清晰的图表,系统中每个微服务或层次都用一个框表示(例如,前端服务,用户偏好服务等)。在实际实施时,实际上通常会有多个实例在应用程序中运行。例如,在从服务版本 X 到版本 Y 的升级过程中,升级的中间阶段会同时运行两个不同版本的服务。虽然升级过程中间是一个临时状态,但是有很多时候你需要创建一个更长时间运行的实验,这种实验会持续一段时间。软件行业常用的模式是“狗粮化”你自己的软件,意思是新版本的软件会在其他地方发布之前先在内部试用。在狗粮化模式中,你可能会在一部分用户中运行服务版本 Y 一天到一周(或更长时间),然后再广泛地发布到所有用户中。
这类实验需要能够进行交通整形,即根据请求的特征将请求路由到不同的服务实现。在这个例子中,你可以创建一个实验,使公司内部用户的所有流量都发送到服务 Y,而来自世界其他地方的流量仍然发送到服务 X。
实验在各种场景下都很有用,包括开发中,程序员可以将有限的真实流量(通常为 1%或更少)发送到实验后端,或者可以运行 A/B 实验,其中 50%的用户获得一种体验,另外 50%的用户获得另一种体验,这样你可以建立哪种方法更有效的统计模型。实验是增加应用程序可靠性、灵活性和洞察力的极其有用的方法,但实际中往往难以实现,因此使用率不如可能的高。
服务网格通过将实验集成到网格本身来改变这一点。与其编写代码来实施您的实验,或者在新基础设施上部署应用程序的全新副本,您可以声明性地定义实验的参数(10%的流量发送到版本 Y,90%的流量发送到版本 X),服务网格会为您实现它。尽管作为开发者,您参与定义实验,但实施过程是透明和自动化的,这意味着会运行更多实验,并相应增加可靠性、灵活性和洞察力。
自省
如果您像大多数程序员一样,一旦编写了程序,就会在新的错误显现时反复调试它。发现代码中的错误是大多数开发者日常工作的重要部分。当应用程序分布在多个微服务之间时,调试变得更加困难。当一个请求跨越多个 Pod 时,将其组合成一个单一请求是很困难的。调试所需的信息必须从多个源头组合在一起,前提是首先收集到了相关信息。
自动内省是服务网格提供的另一个重要功能。因为它参与了 Pod 之间的所有通信,所以服务网格知道请求被路由到哪里,并且可以跟踪组装完整请求所需的信息。开发者不再看到大量请求发送到多个不同的微服务,而是可以看到一个单一的聚合请求,定义了他们整个应用程序的用户体验。此外,服务网格只需为整个集群实现一次。这意味着无论哪个团队开发了服务,相同的请求跟踪都能正常工作。监控数据在由集群级服务网格连接在一起的所有不同服务之间是完全一致的。
您真的需要服务网格吗?
这里描述的优势可能会让你急于在集群上安装服务网格。然而,在你这样做之前,值得考虑一下是否真的需要为你的应用程序安装服务网格。服务网格是一个将复杂性增加到你的应用程序设计中的分布式系统。服务网格深度集成到你的微服务核心通信中。当服务网格失败时,整个应用程序将停止工作。在采用服务网格之前,你必须确信自己能够在问题发生时解决问题。你还必须准备好监控服务网格的软件发布,以确保获取最新的安全性和错误修复,当然,当修复可用时,你也必须准备好在不影响应用程序的情况下部署新版本。这种额外的运营开销意味着对于许多小型应用程序来说,服务网格是一种不必要的复杂性。
如果你正在使用作为托管服务提供的 Kubernetes,它同时提供了服务网格,那么使用该网格会更加容易,因为云提供商将提供支持、调试和无缝的新版本发布服务网格。但即使是由云提供商提供的服务网格,对于开发人员来说也存在额外的学习复杂性。最终,在集群级别,每个应用程序或平台团队都需要权衡服务网格的成本与收益。为了最大化服务网格的好处,对于集群中的所有微服务同时采用它是有帮助的。
审视服务网格实现
云原生生态系统中有许多不同的服务网格项目和实现,但大多数共享许多相同的设计模式和技术。由于服务网格会透明地拦截来自你的应用 Pod 的网络流量,修改并通过集群重新路由它,所以服务网格的一部分需要存在于每个 Pod 中。强制开发人员向每个容器镜像添加某些内容会引入显著的摩擦,同时使得集中管理服务网格版本变得更加困难。因此,大多数服务网格实现在每个网格中的每个 Pod 添加了一个 sidecar 容器。由于 sidecar 位于与应用 Pod 相同的网络堆栈中,它可以使用诸如iptables或最近更多使用的eBPF等工具来审视和拦截来自你的应用容器的网络流量,并将其处理到服务网格中。
当然,要求每个开发人员在他们的 Pod 定义中添加一个容器镜像几乎和要求他们修改他们的容器镜像一样繁琐。为了解决这个问题,大多数服务网格实现依赖于一个变异接入控制器,以自动向特定集群中创建的所有 Pod 添加服务网格 sidecar。任何用于创建 Pod 的 REST API 请求首先会被路由到这个接入控制器。服务网格接入控制器通过添加 sidecar 来修改 Pod 定义。由于这个接入控制器是由集群管理员安装的,它会透明且一致地为整个集群实现服务网格。
但服务网格不仅仅是修改 Pod 网络。您还需要能够控制服务网格的行为;例如,通过定义用于实验的路由规则或用于网格中服务的访问限制。与 Kubernetes 中的其他所有内容一样,这些资源定义是通过 JSON 或 YAML 对象定义声明性地指定的,您可以使用 kubectl 或其他与 Kubernetes API 服务器通信的工具创建它们。服务网格实现利用 自定义资源定义(CRD)来向您的 Kubernetes 集群添加专门的资源,这些资源不是默认安装的一部分。在大多数情况下,特定的自定义资源与服务网格本身紧密相关。CNCF 正在进行一个持续的工作,定义一个标准的供应商中立的服务网格接口(SMI),许多不同的服务网格可以实现这个接口。
服务网格景观
服务网格景观最令人望而生畏的方面可能是选择哪种网格。到目前为止,没有一种网格被视为事实上的标准。尽管具体的统计数据难以获得,但最受欢迎的服务网格可能是 Istio 项目。除了 Istio,还有许多其他开源网格,包括 Linkerd、Consul Connect、Open Service Mesh 等。还有像 AWS App Mesh 这样的专有网格。我们预计云原生社区在未来几年会继续努力标准化这些接口。
开发人员或集群管理员该如何选择?事实上,最适合您的服务网格很可能是您的云服务提供商为您提供的那种。将服务网格的操作添加到集群操作员已经复杂的职责中通常是不必要的。最好让云服务提供商为您管理它。
如果这对您来说不是一个选项,请进行调查。不要被华丽的演示和功能承诺所吸引。服务网格深深植根于您的基础设施中,任何故障都可能严重影响您应用程序的可用性。此外,由于服务网格的 API 往往是特定实现的,一旦您花费时间围绕它开发应用程序,更改服务网格选择将变得困难。最终,您可能会发现适合您的网格是没有网格。
概要
服务网格包含强大的功能,可以为您的应用程序增加安全性和灵活性。同时,服务网格会增加集群操作的复杂性,并可能成为应用程序故障的另一个潜在源头。仔细考虑向基础架构添加服务网格的利弊。如果有选择的余地,请使用托管服务网格,让其他人负责操作细节,同时使您的应用程序能够访问服务网格的功能。
第十六章:集成存储解决方案与 Kubernetes
在许多情况下,将状态从应用程序中解耦,并构建你的微服务尽可能无状态,能够最大程度地提高系统的可靠性和可管理性。
然而,几乎每个有任何复杂性的系统在系统中都有状态,从数据库中的记录到为网页搜索引擎提供结果的索引碎片。总有一些时候,你必须将数据存储在某个地方。
将这些数据与容器和容器编排解决方案集成,通常是构建分布式系统中最复杂的方面。这种复杂性很大程度上源于向容器化架构的迁移也是向解耦、不可变和声明式应用程序开发的迁移。这些模式相对容易应用于无状态的 Web 应用程序,但即使是像 Cassandra 或 MongoDB 这样的“云原生”存储解决方案,也涉及一些手动或命令式的步骤来设置可靠的、复制的解决方案。
作为例子,考虑在 MongoDB 中设置一个 ReplicaSet,这涉及到部署 Mongo 守护进程,然后运行一个命令来识别 Mongo 集群中的领导者和参与者。当然,这些步骤可以通过脚本完成,但在容器化的世界中,很难看到如何将这些命令集成到部署中。同样,即使为一个容器化的副本集的单个容器获取 DNS 可解析的名称也是一项挑战。
额外的复杂性来自于数据引力的存在。大多数容器化系统并不是在真空中构建的;它们通常是从部署在虚拟机上的现有系统改编而来的,这些系统可能包括必须导入或迁移的数据。
最 最终,向云端演进通常意味着存储是一个外部化的云服务,在这种情况下,它永远不可能真正存在于 Kubernetes 集群内部。
本章涵盖了多种将存储集成到 Kubernetes 容器化微服务中的方法。首先,我们介绍如何将现有的外部存储解决方案(无论是云服务还是运行在虚拟机上的)导入到 Kubernetes 中。接下来,我们探讨如何在 Kubernetes 中运行可靠的单例服务,使你能够拥有一个与之前部署存储解决方案的虚拟机环境大致相同的环境。最后,我们介绍 StatefulSets,这是大多数人用来在 Kubernetes 中处理有状态工作负载的 Kubernetes 资源。
导入外部服务
在许多情况下,你在网络中运行着一台已有的机器,机器上运行着某种数据库。在这种情况下,你可能不希望立即将该数据库迁移到容器和 Kubernetes 中。也许它由不同的团队维护,或者你正在进行渐进式迁移,或者迁移数据的任务本身就是值得的麻烦。
不管停留在此的原因是什么,这个旧的服务器和服务都不会迁移到 Kubernetes—但在 Kubernetes 中代表这个服务器仍然是有价值的。这样做的好处是您可以利用 Kubernetes 提供的所有内置命名和服务发现原语。此外,这使您能够配置所有应用程序,使其看起来像在某台机器上运行的数据库实际上是一个 Kubernetes 服务。这意味着可以轻松地将其替换为作为瞬态容器运行的测试数据库。例如,在生产环境中,您可能依赖运行在某台机器上的旧数据库,但对于连续测试,您可以部署一个测试数据库作为临时容器。由于它每次测试运行时都会创建和销毁,数据持久性在连续测试案例中并不重要。将这两个数据库都表示为 Kubernetes 服务使您能够在测试和生产中保持相同的配置。测试和生产之间的高度一致性确保通过测试将导致在生产环境中成功部署。
要具体了解如何在开发和生产之间保持高度一致,请记住所有 Kubernetes 对象都部署到 命名空间 中。假设我们定义了 test 和 production 命名空间。可以使用类似以下对象导入测试服务:
kind: Service
metadata:
name: my-database
# note 'test' namespace here
namespace: test
...
生产服务看起来一样,只是使用了不同的命名空间:
kind: Service
metadata:
name: my-database
# note 'prod' namespace here
namespace: prod
...
当您将 Pod 部署到 test 命名空间并查找名为 my-database 的服务时,它将接收指向 my-database.test.svc.cluster.internal 的指针,这将指向测试数据库。相比之下,当部署在 prod 命名空间中的 Pod 查找相同名称 (my-database) 时,它将收到指向 my-database.prod.svc.cluster.internal 的指针,这是生产数据库。因此,相同的服务名称在两个不同的命名空间中解析为两个不同的服务。有关此工作原理的更多详细信息,请参阅第七章。
注意
所有以下技术都使用数据库或其他存储服务,但这些方法同样适用于没有在您的 Kubernetes 集群内运行的其他服务。
没有选择器的服务
当我们首次介绍服务时,我们详细讨论了标签查询及其如何用于识别特定服务的后端动态 Pod 集合。然而,对于外部服务,没有这样的标签查询。相反,通常有一个 DNS 名称,它指向运行数据库的特定服务器。在我们的示例中,假设此服务器命名为 database.company.com。要将此外部数据库服务导入 Kubernetes,我们首先创建一个服务,它没有 Pod 选择器,而是引用数据库服务器的 DNS 名称(示例 16-1)。
示例 16-1. dns-service.yaml
kind: Service
apiVersion: v1
metadata:
name: external-database
spec:
type: ExternalName
externalName: database.company.com
当创建典型的 Kubernetes 服务时,还会创建一个 IP 地址,并且 Kubernetes DNS 服务会填充一个 A 记录,指向该 IP 地址。当您创建 ExternalName 类型的服务时,Kubernetes DNS 服务会填充一个 CNAME 记录,指向您指定的外部名称(在本例中为 database.company.com)。当集群中的应用程序对主机名 external-database.svc.default.cluster 进行 DNS 查找时,DNS 协议将该名称别名为 database.company.com。然后,这将解析为您外部数据库服务器的 IP 地址。通过这种方式,Kubernetes 中的所有容器都认为它们正在与支持其他容器的服务通信,而实际上它们被重定向到外部数据库。
请注意,这并不仅限于您自己基础设施上运行的数据库。许多云数据库和其他服务在访问数据库时会提供一个 DNS 名称(例如,my-database.databases.cloudprovider.com)。您可以将此 DNS 名称用作 externalName。这将把由云提供的数据库导入到您的 Kubernetes 集群的命名空间中。
有时,您可能没有外部数据库服务的 DNS 地址,只有一个 IP 地址。在这种情况下,仍然可以将此服务导入为 Kubernetes 服务,但操作略有不同。首先,创建一个没有标签选择器的服务,但也不使用我们之前使用的 ExternalName 类型(示例 16-2)。
示例 16-2. external-ip-service.yaml
kind: Service
apiVersion: v1
metadata:
name: external-ip-database
Kubernetes 将为此服务分配一个虚拟 IP 地址,并为其填充一个 A 记录。然而,由于服务没有选择器,负载均衡器将不会填充任何端点以重定向流量。
鉴于这是一个外部服务,用户需负责手动使用 Endpoints 资源(示例 16-3)来填充端点。
示例 16-3. external-ip-endpoints.yaml
kind: Endpoints
apiVersion: v1
metadata:
name: external-ip-database
subsets:
- addresses:
- ip: 192.168.0.1
ports:
- port: 3306
如果您有多个 IP 地址以实现冗余,可以在 addresses 数组中重复它们。一旦填充了端点,负载均衡器将开始将流量从 Kubernetes 服务重定向到 IP 地址端点。
注意
由于用户已经承担了保持服务器 IP 地址更新的责任,您需要确保它永远不会更改,或者确保某些自动化流程更新 Endpoints 记录。
外部服务的限制:健康检查
Kubernetes 中的外部服务有一个重要的限制:它们不执行任何健康检查。用户需确保供给 Kubernetes 的端点或 DNS 名称对应的可靠性足够满足应用需求。
运行可靠的单例
在 Kubernetes 中运行存储解决方案的挑战通常在于,诸如 ReplicaSet 这样的基元期望每个容器都是相同且可替换的,但对于大多数存储解决方案来说,情况并非如此。解决这个问题的一个选项是使用 Kubernetes 基元,但不尝试复制存储。相反,只需运行一个单独的 Pod,其中运行数据库或其他存储解决方案即可。通过这种方式,运行 Kubernetes 中复制的存储的挑战不会发生,因为没有复制。
乍一看,这似乎与构建可靠的分布式系统的原则相悖,但总体而言,它并不比将数据库或存储基础设施运行在单个虚拟或物理机器上不可靠。实际上,如果正确结构化系统,您所牺牲的唯一东西就是在升级或机器故障时的潜在停机时间。尽管对于大规模或关键任务的系统而言,这可能是不可接受的,但对于许多较小规模的应用程序来说,这种有限的停机时间是减少复杂性的合理权衡。如果这对您不适用,请随意跳过本节,或按照前一节描述的导入现有服务的方式进行操作,或者继续阅读“使用 StatefulSets 实现 Kubernetes 本地存储”。对于其他人,我们将介绍如何构建可靠的数据存储单例。
运行 MySQL 单例
在这一节中,我们将描述如何在 Kubernetes 中将 MySQL 数据库的可靠单例实例作为 Pod 运行,并且如何将该单例暴露给集群中的其他应用程序。为此,我们将创建三个基本对象:
-
持久卷用于独立管理磁盘存储的生命周期,与运行中的 MySQL 应用程序的生命周期无关。
-
一个将运行 MySQL 应用程序的 MySQL Pod
-
一个服务,将此 Pod 暴露给集群中的其他容器
在第五章中,我们描述了持久卷:这些存储位置的生命周期与任何 Pod 或容器无关。持久卷在持久存储解决方案的情况下非常有用,其中数据库的磁盘表示应该在容器运行数据库应用程序崩溃或移动到不同机器时仍然存在。如果应用程序移动到不同的机器,卷应随之移动,并且数据应该得到保留。将数据存储分离为持久卷使这一切成为可能。
首先,我们将为我们的 MySQL 数据库创建一个持久卷供其使用。本示例使用 NFS 实现最大的可移植性,但 Kubernetes 支持许多不同的持久卷驱动程序类型。例如,有适用于所有主要公共云提供商以及许多私有云提供商的持久卷驱动程序。要使用这些解决方案,只需将 nfs 替换为适当的云提供商卷类型(例如 azure、awsElasticBlockStore 或 gcePersistentDisk)。在所有情况下,这些更改就足够了。Kubernetes 知道如何在相应的云提供商中创建适当的存储磁盘。这是 Kubernetes 简化可靠分布式系统开发的一个很好的例子。示例 16-4 展示了 PersistentVolume 对象。
示例 16-4. nfs-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: database
labels:
volume: my-volume
spec:
accessModes:
- ReadWriteMany
capacity:
storage: 1Gi
nfs:
server: 192.168.0.1
path: "/exports"
这定义了一个具有 1 GB 存储空间的 NFS PersistentVolume 对象。我们可以像往常一样创建这个持久卷:
$ kubectl apply -f nfs-volume.yaml
现在我们已经创建了一个持久卷,我们需要为我们的 Pod 声明该持久卷。我们使用 PersistentVolumeClaim 对象来实现这一点(示例 16-5)。
示例 16-5. nfs-volume-claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: database
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
selector:
matchLabels:
volume: my-volume
selector 字段使用标签来查找我们之前定义的匹配卷。
这种间接的方法可能看起来过于复杂,但它有其目的——它用来将我们的 Pod 定义与存储定义隔离开来。您可以直接在 Pod 规范中声明卷,但这会将该 Pod 规范锁定到特定的卷提供者(例如特定的公共或私有云)。通过使用卷声明,您可以保持您的 Pod 规范与云无关;简单地创建不同的卷,特定于云,并使用 PersistentVolumeClaim 将它们绑定在一起。此外,在许多情况下,持久卷控制器实际上会自动为您创建卷。关于此过程的更多详细信息,请参见下一节。
现在我们已经声明了我们的卷,我们可以使用 ReplicaSet 来构建我们的单例 Pod。也许使用 ReplicaSet 来管理单个 Pod 看起来有些奇怪,但这对于可靠性是必要的。请记住,一旦调度到一台机器上,一个裸 Pod 将永远绑定到该机器上。如果机器发生故障,则任何不受高级控制器(如 ReplicaSet)管理的 Pod 都将与该机器一起消失,并且不会在其他地方重新调度。因此,为了确保我们的数据库 Pod 在机器故障时重新调度,我们使用高级别的 ReplicaSet 控制器,其副本大小为 1,来管理我们的数据库(示例 16-6)。
示例 16-6. mysql-replicaset.yaml
apiVersion: extensions/v1
kind: ReplicaSet
metadata:
name: mysql
# Labels so that we can bind a Service to this Pod
labels:
app: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: database
image: mysql
resources:
requests:
cpu: 1
memory: 2Gi
env:
# Environment variables are not a best practice for security,
# but we're using them here for brevity in the example.
# See Chapter 11 for better options.
- name: MYSQL_ROOT_PASSWORD
value: some-password-here
livenessProbe:
tcpSocket:
port: 3306
ports:
- containerPort: 3306
volumeMounts:
- name: database
# /var/lib/mysql is where MySQL stores its databases
mountPath: "/var/lib/mysql"
volumes:
- name: database
persistentVolumeClaim:
claimName: database
一旦我们创建了 ReplicaSet,它将进而创建一个运行 MySQL 的 Pod,使用我们最初创建的持久磁盘。最后一步是将其公开为 Kubernetes 服务(示例 16-7)。
示例 16-7. mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
protocol: TCP
selector:
app: mysql
现在,我们在集群中运行了一个可靠的单例 MySQL 实例,并将其公开为名为mysql的服务的完整域名mysql.svc.default.cluster可访问。
类似的说明可以用于各种数据存储,并且如果您的需求简单,并且可以在面对机器故障或需要升级数据库软件时承受有限的停机时间,那么可靠的单例可能是您的应用程序存储的正确方法。
动态卷供应
许多集群还包括动态卷供应。通过动态卷供应,集群操作员创建一个或多个StorageClass对象。在 Kubernetes 中,StorageClass封装了特定类型存储的特征。一个集群可以安装多个不同的存储类。例如,您可能在网络上有一个 NFS 服务器的存储类和一个 iSCSI 块存储的存储类。存储类还可以封装不同的可靠性或性能级别。示例 16-8 展示了在 Microsoft Azure 平台上自动创建磁盘对象的默认存储类。
示例 16-8. storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: default
annotations:
storageclass.beta.kubernetes.io/is-default-class: "true"
labels:
kubernetes.io/cluster-service: "true"
provisioner: kubernetes.io/azure-disk
一旦为集群创建了存储类,您可以在持久卷声明中引用此存储类,而不是引用任何特定的持久卷。当动态供应程序看到这个存储声明时,它会使用适当的卷驱动程序来创建卷并将其绑定到您的持久卷声明上。
示例 16-9 展示了一个使用我们刚刚定义的default存储类来声明新创建的持久卷的 PersistentVolumeClaim 的示例。
示例 16-9. dynamic-volume-claim.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: my-claim
annotations:
volume.beta.kubernetes.io/storage-class: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
volume.beta.kubernetes.io/storage-class注释将此声明与我们创建的存储类关联起来。
注意
自动提供持久卷是一个很棒的功能,它显著简化了在 Kubernetes 中构建和管理有状态应用程序的过程。但是,这些持久卷的生命周期取决于 PersistentVolumeClaim 的回收策略,默认情况下将其绑定到创建卷的 Pod 的生命周期。
这意味着如果您删除 Pod(例如通过缩减规模或其他事件),那么卷也将被删除。虽然在某些情况下这可能是您希望的结果,但您需要小心确保不会意外删除您的持久卷。
持久卷非常适合需要存储的传统应用程序,但是如果您需要以 Kubernetes 本地方式开发高可用性、可扩展的存储,那么新发布的 StatefulSet 对象可以代替。我们将在下一节中描述如何使用 StatefulSets 部署 MongoDB。
使用 StatefulSets 的 Kubernetes 本地存储
当 Kubernetes 最初开发时,对于复制集中的所有副本都强调了同质性。在这种设计中,没有任何副本具有独立的身份或配置。这要求应用开发者为他们的应用程序确定一个可以建立这种身份的设计。
虽然这种方法为编排系统提供了大量隔离性,但也使得开发有状态应用程序变得相当困难。在社区的大量参与和对各种现有有状态应用程序的大量实验后,StatefulSets 在 Kubernetes 版本 1.5 中被引入。
StatefulSets 的特性
StatefulSets 是一组复制的 Pod,类似于 ReplicaSets。但与 ReplicaSet 不同,它们具有某些独特的特性:
-
每个副本都会获得一个带有唯一索引的持久主机名(例如
database-0,database-1等)。 -
每个副本按照从最低到最高索引的顺序创建,创建过程将暂停,直到前一个索引处的 Pod 健康且可用。这也适用于扩展。
-
当删除 StatefulSet 时,管理的每个副本 Pod 也将按照从最高到最低的顺序删除。这也适用于减少副本数量时的缩容。
原来这简单的要求集使得在 Kubernetes 上部署存储应用程序变得极为简便。例如,稳定的主机名组合(例如 database-0)和顺序约束意味着,除了第一个副本外,所有副本都可以可靠地引用 database-0 用于发现和建立复制共识。
使用 StatefulSets 手动复制的 MongoDB
在本节中,我们将部署一个复制的 MongoDB 集群。现在,复制设置本身将手动完成,以让你感受一下 StatefulSets 的工作方式。最终,我们也将自动化这个设置过程。
首先,我们将使用 StatefulSet 对象创建一个包含三个 MongoDB Pod 的复制集(示例 16-10)。
示例 16-10. mongo-simple.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: "mongo"
replicas: 3
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongodb
image: mongo:3.4.24
command:
- mongod
- --replSet
- rs0
ports:
- containerPort: 27017
name: peer
正如你所看到的,该定义与我们之前见过的 ReplicaSet 定义类似。唯一的变化在于 apiVersion 和 kind 字段。
创建 StatefulSet:
$ kubectl apply -f mongo-simple.yaml
一旦创建完成,ReplicaSet 和 StatefulSet 之间的区别就显而易见了。运行 kubectl get pods,你可能会看到如下情况:
NAME READY STATUS RESTARTS AGE
mongo-0 1/1 Running 0 1m
mongo-1 0/1 ContainerCreating 0 10s
这与使用 ReplicaSet 的情况有两个重要区别。首先,每个复制的 Pod 都有一个数字索引(0,1,…),而不是 ReplicaSet 控制器添加的随机后缀。其次,这些 Pod 是按顺序逐步创建的,而不是像 ReplicaSet 那样一次性创建。
创建 StatefulSet 之后,我们还需要创建一个“无头”服务来管理 StatefulSet 的 DNS 条目。在 Kubernetes 中,如果服务没有集群虚拟 IP 地址,则称为“无头”服务。由于 StatefulSets 中每个 Pod 都有唯一的标识,为复制服务创建负载均衡 IP 地址实际上并不合理。您可以使用服务规范中的clusterIP: None来创建无头服务(示例 16-11)。
示例 16-11. mongo-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
ports:
- port: 27017
name: peer
clusterIP: None
selector:
app: mongo
一旦创建了该服务,通常会填充四个 DNS 条目。像往常一样,会创建mongo.default.svc.cluster.local,但与标准服务不同的是,对此主机名进行 DNS 查找会提供 StatefulSet 中所有地址。此外,还会创建mongo-0.mongo.default.svc.cluster.local、mongo-1.mongo和mongo-2.mongo的条目。这些条目分别解析为 StatefulSet 中复制索引的特定 IP 地址。因此,使用 StatefulSets 可以为集合中的每个复制提供明确定义的持久名称。当您配置复制存储解决方案时,这通常非常有用。您可以通过在 Mongo 复制品之一中运行以下命令来查看这些 DNS 条目的效果:
$ kubectl run -it --rm --image busybox busybox ping mongo-1.mongo
接下来,我们将使用这些逐个 Pod 主机名手动设置 Mongo 复制。我们将选择mongo-0.mongo作为我们的初始主节点。在该 Pod 中运行mongo工具:
$ kubectl exec -it mongo-0 mongo
> rs.initiate( {
_id: "rs0",
members:[ { _id: 0, host: "mongo-0.mongo:27017" } ]
});
OK
这条命令告诉mongodb使用mongo-0.mongo作为主复制集启动 ReplicaSet rs0。
注意
rs0的名称是任意的。您可以使用任何您喜欢的名称,但在mongo-simple.yaml StatefulSet 定义中也需要进行更改。
一旦初始化了 Mongo ReplicaSet,您可以在mongo-0.mongo Pod 上的mongo工具中运行以下命令添加其余的副本:
> rs.add("mongo-1.mongo:27017");
> rs.add("mongo-2.mongo:27017");
正如您所看到的,我们正在使用特定于复制品的 DNS 名称将它们添加为 Mongo 集群中的副本。在此时,我们已经完成了。我们的复制 MongoDB 已经运行起来了。但实际上并没有像我们希望的那样自动化—在下一节中,我们将看到如何使用脚本来自动设置。
自动化 MongoDB 集群创建
为了自动化基于 StatefulSet 的 MongoDB 集群的部署,我们将向我们的 Pods 添加一个容器来执行初始化。为了配置此 Pod 而无需构建新的 Docker 镜像,我们将使用 ConfigMap 将脚本添加到现有的 MongoDB 镜像中。
我们将使用“初始化容器”来运行此脚本。“初始化容器”(或“init”容器)是在 Pod 启动时运行一次的专用容器。通常用于像这样需要做少量设置工作以便主应用程序运行的情况。在 Pod 定义中,有一个单独的initContainers列表,可以在其中定义 init 容器。这里提供了一个示例:
...
initContainers:
- name: init-mongo
image: mongo:3.4.24
command:
- bash
- /config/init.sh
volumeMounts:
- name: config
mountPath: /config
...
volumes:
- name: config
configMap:
name: "mongo-init"
注意,它正在挂载一个名为 mongo-init 的 ConfigMap 卷,该 ConfigMap 包含执行我们初始化的脚本。首先,脚本确定它是否正在 mongo-0 上运行。如果它在不同的 Mongo 副本上,则等待 ReplicaSet 存在,然后将自身注册为该 ReplicaSet 的成员。
示例 16-12 包含完整的 ConfigMap 对象。
示例 16-12. mongo-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mongo-init
data:
init.sh: |
#!/bin/bash
# Need to wait for the readiness health check to pass so that the
# Mongo names resolve. This is kind of wonky.
until ping -c 1 ${HOSTNAME}.mongo; do
echo "waiting for DNS (${HOSTNAME}.mongo)..."
sleep 2
done
until /usr/bin/mongo --eval 'printjson(db.serverStatus())'; do
echo "connecting to local mongo..."
sleep 2
done
echo "connected to local."
HOST=mongo-0.mongo:27017
until /usr/bin/mongo --host=${HOST} --eval 'printjson(db.serverStatus())'; do
echo "connecting to remote mongo..."
sleep 2
done
echo "connected to remote."
if [[ "${HOSTNAME}" != 'mongo-0' ]]; then
until /usr/bin/mongo --host=${HOST} --eval="printjson(rs.status())" \
| grep -v "no replset config has been received"; do
echo "waiting for replication set initialization"
sleep 2
done
echo "adding self to mongo-0"
/usr/bin/mongo --host=${HOST} \
--eval="printjson(rs.add('${HOSTNAME}.mongo'))"
fi
if [[ "${HOSTNAME}" == 'mongo-0' ]]; then
echo "initializing replica set"
/usr/bin/mongo --eval="printjson(rs.initiate(\
{'_id': 'rs0', 'members': [{'_id': 0, \
'host': 'mongo-0.mongo:27017'}]}))"
fi
echo "initialized"
这个脚本会立即退出,这在使用 initContainers 时非常重要。每个初始化容器都会等待前一个容器完成后再运行。主应用容器会等待所有初始化容器都完成。如果这个脚本没有退出,主 Mongo 服务器将永远无法启动。
综合起来,示例 16-13 是使用 ConfigMap 的完整 StatefulSet。
示例 16-13. mongo.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: "mongo"
replicas: 3
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongodb
image: mongo:3.4.24
command:
- mongod
- --replSet
- rs0
ports:
- containerPort: 27017
name: web
# This container initializes the MongoDB server, then sleeps.
- name: init-mongo
image: mongo:3.4.24
command:
- bash
- /config/init.sh
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
configMap:
name: "mongo-init"
给定所有这些文件,你可以创建一个 Mongo 集群:
$ kubectl apply -f mongo-config-map.yaml
$ kubectl apply -f mongo-service.yaml
$ kubectl apply -f mongo-simple.yaml
或者,如果你愿意,你可以将它们全部合并到一个单独的 YAML 文件中,其中各个对象由 --- 分隔。确保保持相同的顺序,因为 StatefulSet 定义依赖于存在 ConfigMap 定义。
持久卷和 StatefulSets
对于持久存储,你需要将持久卷挂载到 /data/db 目录中。在 Pod 模板中,你需要更新它以将持久卷声明挂载到该目录:
...
volumeMounts:
- name: database
mountPath: /data/db
尽管这种方法与我们看到的可靠的单例方法类似,因为 StatefulSet 复制了多个 Pod,你不能简单地引用一个持久卷声明。相反,你需要添加一个 持久卷声明模板。你可以将声明模板视为与 Pod 模板相同,但它不是创建 Pod,而是创建卷声明。你需要将以下内容添加到 StatefulSet 定义的底部:
volumeClaimTemplates:
- metadata:
name: database
annotations:
volume.alpha.kubernetes.io/storage-class: anything
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 100Gi
当你向 StatefulSet 定义添加一个卷声明模板时,每当 StatefulSet 控制器创建 StatefulSet 的一部分的 Pod 时,它将根据该模板创建一个持久卷声明作为该 Pod 的一部分。
注意
要使这些复制的持久卷正常工作,你需要设置持久卷的自动配置或预先填充一个持久卷对象集合供 StatefulSet 控制器使用。如果没有可以创建的声明,StatefulSet 控制器将无法创建相应的 Pod。
最后一点:就绪探针
在生产环境中配置我们的 MongoDB 集群的最后一步是为我们的 Mongo 服务容器添加活性检查。正如我们在“健康检查”中学到的那样,活性探针用于确定容器是否正常运行。
对于存活性检查,我们可以通过将以下内容添加到 StatefulSet 对象中的 Pod 模板来使用mongo工具本身:
...
livenessProbe:
exec:
command:
- /usr/bin/mongo
- --eval
- db.serverStatus()
initialDelaySeconds: 10
timeoutSeconds: 10
...
摘要
一旦我们结合了 StatefulSets、持久卷索赔和存活探测,我们就在 Kubernetes 上运行一个经过硬化、可扩展的云原生 MongoDB 安装。虽然这个示例涉及 MongoDB,但创建 StatefulSets 来管理其他存储解决方案的步骤非常类似,可以遵循相似的模式。
第十七章:扩展 Kubernetes
从一开始就清楚,Kubernetes 将不仅仅是其核心 API 集合;一旦应用程序在集群中进行编排,就会有无数其他有用的工具和实用程序可以作为 API 对象在 Kubernetes 集群中表示和部署。挑战在于如何在没有无限制扩展 API 的情况下,接纳这些对象和用例的爆炸式增长。
为了解决扩展用例和 API 扩展之间的张力,我们付出了大量努力,使 Kubernetes API 可扩展。这种可扩展性意味着集群运营者可以根据自己的需求定制他们的集群,添加适合的附加组件。这种可扩展性使人们能够自行增强其集群,使用社区开发的集群附加组件,甚至开发捆绑在集群插件生态系统中出售的扩展。可扩展性也催生了管理系统的全新模式,如运算符模式。
不论是构建自己的扩展还是从生态系统中消费运算符,理解如何扩展 Kubernetes API 服务器以及如何构建和交付扩展是解锁 Kubernetes 及其生态系统全部潜力的关键组成部分。随着越来越多的高级工具和平台使用这些可扩展机制构建在 Kubernetes 之上,了解它们的操作方式对于在现代 Kubernetes 集群中构建应用程序至关重要。
扩展 Kubernetes 的含义
总体而言,扩展 Kubernetes API 服务器通常是为集群添加新功能或限制和调整用户与其集群交互的方式。有一个丰富的插件生态系统,集群管理员可以使用这些插件为其集群添加服务和功能。值得注意的是,扩展集群是一项非常高权限的操作。这不应该授予任意用户或任意代码的能力,因为扩展集群需要集群管理员权限。即使是集群管理员在安装第三方工具时也应该小心谨慎。一些扩展,如准入控制器,可以用于查看集群中创建的所有对象,并且可以轻易地被用作窃取密钥或运行恶意代码的手段。此外,扩展集群会使其与原始的 Kubernetes 有所不同。在多个集群上运行时,构建工具以保持跨集群一致性体验非常重要,这包括安装的扩展。
可扩展点
Kubernetes 可以通过多种方式进行扩展,从自定义资源定义到容器网络接口插件。本章将重点介绍通过向 API 请求添加新资源类型或接入控制器来扩展 API 服务器。我们不会涵盖 CNI/CSI/CRI(容器网络接口/容器存储接口/容器运行时接口)扩展,因为它们更常用于 Kubernetes 集群提供者而非本书的目标读者——Kubernetes 终端用户。
除了接入控制器和 API 扩展之外,实际上还有许多方法可以在完全不修改 API 服务器的情况下“扩展”您的集群。这些包括安装自动日志记录和监控的 DaemonSet、扫描服务以查找跨站点脚本(XSS)漏洞的工具等。然而,在开始自行扩展集群之前,值得考虑的是在现有 Kubernetes API 的限制内可能实现的各种可能性。
要理解接入控制器和 CustomResourceDefinition 的作用,有助于回顾请求通过 Kubernetes API 服务器的流程,如 图 17-1 所示。

图 17-1. API 服务器请求流程
接入控制器是在 API 对象写入后端存储之前调用的。接入控制器可以拒绝或修改 API 请求。几个接入控制器内置于 Kubernetes API 服务器中;例如,限制范围接入控制器为没有设置默认限制的 Pod 设置默认限制。许多其他系统使用自定义接入控制器自动将 sidecar 容器注入到系统上创建的所有 Pod 中,以实现“自动化体验”。
另一种扩展形式,也可以与接入控制器结合使用,是自定义资源。使用自定义资源,可以向 Kubernetes API 添加全新的 API 对象。这些新的 API 对象可以添加到命名空间中,受到 RBAC 的限制,并且可以通过现有工具如 kubectl 和 Kubernetes API 访问。
后续章节将更详细地描述这些 Kubernetes 扩展点,并提供如何扩展集群的使用案例和实际示例。
创建自定义资源的第一步是创建 CustomResourceDefinition。该对象实际上是一个元资源,即定义另一个资源的资源。
作为一个具体的例子,考虑定义一个新资源来表示集群中的负载测试。当创建新的 LoadTest 资源时,将在 Kubernetes 集群中启动一个负载测试,并向服务驱动流量。
创建新资源的第一步是通过 CustomResourceDefinition 进行定义。一个示例定义如下:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: loadtests.beta.kuar.com
spec:
group: beta.kuar.com
versions:
- name: v1
served: true
storage: true
scope: Namespaced
names:
plural: loadtests
singular: loadtest
kind: LoadTest
shortNames:
- lt
您可以看到这是像任何其他对象一样的 Kubernetes 对象。它有一个metadata子对象,在该子对象中,资源被命名。然而,在自定义资源的情况下,名称是特殊的。它必须是格式*<resource-plural>*.*<api-group>*,以确保集群中每个资源定义都是唯一的,因为每个 CustomResourceDefinition 的名称必须匹配此模式,并且集群中没有两个对象可以具有相同的名称。因此,我们确保没有两个 CustomResourceDefinitions 定义相同的资源。
除了元数据外,CustomResourceDefinition 还有一个spec子对象。这是资源本身的定义位置。在该spec对象中,有一个apigroup字段,用于为资源提供 API 组。如前所述,它必须与 CustomResourceDefinition 名称的后缀匹配。此外,还有一个版本列表,包括版本名称(例如,v1,v2等),以及指示该版本是否由 API 服务器提供以及用于在 API 服务器的后端存储中存储数据的版本的字段。storage字段必须对于资源的单个版本为 true。还有一个scope字段,用于指示资源是否是有命名空间的(默认为有命名空间),以及一个names字段,允许为资源的单数、复数和kind值定义。它还允许定义方便的“短名称”,以供在kubectl和其他地方使用。
根据这个定义,您可以在 Kubernetes API 服务器中创建资源。但是首先,为了展示动态资源类型的真实本质,请尝试使用kubectl列出我们的loadtests资源:
$ kubectl get loadtests
您将看到当前没有定义这样的资源。现在使用loadtest-resource.yaml创建此资源:
$ kubectl create -f loadtest-resource.yaml
然后再次获取loadtests资源:
$ kubectl get loadtests
这次您会看到已定义了 LoadTest 资源类型,尽管尚未存在此资源类型的实例。让我们通过创建一个新的 LoadTest 资源来改变这种情况。
就像所有内置的 Kubernetes API 对象一样,您可以使用 YAML 或 JSON 来定义自定义资源(在本例中是我们的 LoadTest)。请参阅以下定义:
apiVersion: beta.kuar.com/v1
kind: LoadTest
metadata:
name: my-loadtest
spec:
service: my-service
scheme: https
requestsPerSecond: 1000
paths:
- /index.xhtml
- /login.xhtml
- /shares/my-shares/
有一件事情您会注意到,那就是我们从未在 CustomResourceDefinition 中定义自定义资源的模式。实际上,为自定义资源提供 OpenAPI 规范(以前称为 Swagger)是可能的,但对于简单的资源类型来说,这种复杂性通常不值得。如果您确实希望执行验证,可以注册一个验证入场控制器,如下节所述。
您现在可以使用loadtest.yaml文件创建资源,就像使用任何内置类型一样:
$ kubectl create -f loadtest.yaml
当您列出loadtests资源时,您将看到您新创建的资源:
$ kubectl get loadtests
这可能令人兴奋,但实际上它还没有做任何事情。当然,你可以使用这个简单的 CRUD(创建/读取/更新/删除)API 来操作 LoadTest 对象的数据,但是在我们定义的新 API 中,没有控制器存在于集群中以在定义 LoadTest 对象时做出反应并采取行动,因此没有实际的负载测试被创建。LoadTest 自定义资源仅仅是添加 LoadTests 到我们集群所需基础设施的一半。另一半是一段代码,将持续监视自定义资源,并根据需要创建、修改或删除 LoadTests 来实现 API。
就像 API 的用户一样,控制器与 API 服务器交互以列出 LoadTests 并监视可能发生的任何更改。控制器与 API 服务器之间的这种交互在图 17-2 中展示。

图 17-2. CustomResourceDefinition 交互
这样的控制器代码可能从简单到复杂不等。最简单的控制器运行一个 for 循环,重复轮询新的自定义对象,然后执行创建或删除实现这些自定义对象的资源(例如 LoadTest 工作 Pod)的操作。
然而,这种基于轮询的方法效率低下:轮询循环的周期增加了不必要的延迟,并且轮询的开销可能会给 API 服务器增加不必要的负载。更高效的方法是使用 API 服务器上的 watch API,它在更新发生时提供更新流,消除了轮询的延迟和开销。然而,在一个没有 bug 的方式中正确地使用这个 API 是复杂的。因此,如果您想使用 watch,强烈建议使用像 client-go library 中提供的 Informer 模式这样的受到良好支持的机制。
现在我们已经创建了一个自定义资源,并通过控制器实现了它,我们在集群中具备了一个新资源的基本功能。然而,作为一个良好运行的资源所需的许多部分还缺失。其中最重要的两个部分是验证和默认设置。验证是确保发送到 API 服务器的 LoadTest 对象格式正确,并能用于创建负载测试的过程,而默认设置则通过默认提供自动化、常用的数值,使我们的资源更易于使用。接下来我们将介绍如何为我们的自定义资源添加这些功能。
正如前面提到的,通过 OpenAPI 规范来添加验证的一个选项是为我们的对象定义一个 OpenAPI 规范。这对于基本验证(例如检查必填字段的存在或未知字段的缺失)是有用的。完整的 OpenAPI 教程超出了本书的范围,但是在线上有很多资源,包括 完整的 Kubernetes API 规范。
一般来说,API 架构实际上不足以验证 API 对象。例如,在我们的 loadtests 示例中,我们可能希望验证 LoadTest 对象具有有效的方案(例如 http 或 https)或者 requestsPerSecond 是一个非零正数。
为了完成这个任务,我们将使用一个验证型 admission 控制器。正如前面讨论的那样,admission 控制器在请求被处理之前拦截这些请求,并且可以在处理过程中拒绝或修改这些请求。admission 控制器可以通过动态 admission 控制系统添加到集群中。动态 admission 控制器是一个简单的 HTTP 应用程序。API 服务器可以通过 Kubernetes Service 对象或任意 URL 连接到 admission 控制器。这意味着 admission 控制器可以选择在集群外部运行,例如在云提供商的函数即服务(如 Azure Functions 或 AWS Lambda)中。
要安装我们的验证型 admission 控制器,我们需要将其指定为 Kubernetes ValidatingWebhookConfiguration。该对象指定了 admission 控制器运行的端点,以及应该在哪个资源(在本例中为 LoadTest)和操作(在本例中为 CREATE)上运行 admission 控制器。您可以在以下代码中查看验证型 admission 控制器的完整定义:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: kuar-validator
webhooks:
- name: validator.kuar.com
rules:
- apiGroups:
- "beta.kuar.com"
apiVersions:
- v1
operations:
- CREATE
resources:
- loadtests
clientConfig:
# Substitute the appropriate IP address for your webhook
url: https://192.168.1.233:8080
# This should be the base64-encoded CA certificate for your cluster,
# you can find it in your ${KUBECONFIG} file
caBundle: REPLACEME
对于安全性来说是件好事,但对于复杂性来说是件不好的事,被 Kubernetes API 服务器访问的 Webhook 只能通过 HTTPS 访问。因此,我们需要生成一个用于提供 Webhook 的证书。做到这一点的最简单方法是使用集群自身的证书颁发机构(CA)生成新证书。
首先,我们需要一个私钥和一个证书签名请求(CSR)。以下是一个生成这些内容的简单 Go 程序:
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"net/url"
"os"
)
func main() {
host := os.Args[1]
name := "server"
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
keyDer := x509.MarshalPKCS1PrivateKey(key)
keyBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: keyDer,
}
keyFile, err := os.Create(name + ".key")
if err != nil {
panic(err)
}
pem.Encode(keyFile, &keyBlock)
keyFile.Close()
commonName := "myuser"
emailAddress := "someone@myco.com"
org := "My Co, Inc."
orgUnit := "Widget Farmers"
city := "Seattle"
state := "WA"
country := "US"
subject := pkix.Name{
CommonName: commonName,
Country: []string{country},
Locality: []string{city},
Organization: []string{org},
OrganizationalUnit: []string{orgUnit},
Province: []string{state},
}
uri, err := url.ParseRequestURI(host)
if err != nil {
panic(err)
}
asn1, err := asn1.Marshal(subject.ToRDNSequence())
if err != nil {
panic(err)
}
csr := x509.CertificateRequest{
RawSubject: asn1,
EmailAddresses: []string{emailAddress},
SignatureAlgorithm: x509.SHA256WithRSA,
URIs: []*url.URL{uri},
}
bytes, err := x509.CreateCertificateRequest(rand.Reader, &csr, key)
if err != nil {
panic(err)
}
csrFile, err := os.Create(name + ".csr")
if err != nil {
panic(err)
}
pem.Encode(csrFile, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bytes})
csrFile.Close()
}
您可以使用以下命令运行此程序:
$ go run csr-gen.go *<URL-for-webhook>*
并且它会生成两个文件,server.csr 和 server-key.pem。
然后,您可以使用以下 YAML 为 Kubernetes API 服务器创建证书签名请求:
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
name: validating-controller.default
spec:
groups:
- system:authenticated
request: REPLACEME
usages:
usages:
- digital signature
- key encipherment
- key agreement
- server auth
您会注意到 request 字段的值是 REPLACEME;这需要用我们在前述代码中生成的 base64 编码的证书签名请求来替换:
$ perl -pi -e s/REPLACEME/$(base64 server.csr | tr -d '\n')/ \
admission-controller-csr.yaml
现在您的证书签名请求已经准备好,您可以将其发送到 API 服务器以获取证书:
$ kubectl create -f admission-controller-csr.yaml
接下来,您需要批准该请求:
$ kubectl certificate approve validating-controller.default
一旦批准,您可以下载新证书:
$ kubectl get csr validating-controller.default -o json | \
jq -r .status.certificate | base64 -d > server.crt
有了证书,您最终可以创建基于 SSL 的准入控制器(哦!)。当准入控制器代码收到请求时,它包含一个类型为 AdmissionReview 的对象,其中包含有关请求的元数据以及请求本身的主体。在我们的验证准入控制器中,我们仅注册了单个资源类型和单个操作(CREATE),因此我们不需要检查请求的元数据。相反,我们直接进入资源本身,并验证 requestsPerSecond 是否为正数以及 URL 方案是否有效。如果它们不是,则返回一个 JSON 主体,禁止请求。
实现准入控制器以提供默认值与刚才描述的步骤类似,但不使用 ValidatingWebhookConfiguration,而是使用 MutatingWebhookConfiguration,并需要提供一个 JSON Patch 对象来修改请求对象在存储之前。
下面是一个 TypeScript 片段,您可以将其添加到验证准入控制器中以添加默认值。如果 loadtest 中的 paths 字段长度为零,则为 /index.xhtml 添加一个路径:
if (needsPatch(loadtest)) {
const patch = [
{ 'op': 'add', 'path': '/spec/paths', 'value': ['/index.xhtml'] },
]
response['patch'] = Buffer.from(JSON.stringify(patch))
.toString('base64');
response['patchType'] = 'JSONPatch';
}
您可以通过简单地更改 YAML 对象中的 kind 字段并将文件保存为 mutating-controller.yaml,然后通过运行以下命令创建控制器:
$ kubectl create -f mutating-controller.yaml
到此为止,您已经看到了如何使用自定义资源和准入控制器扩展 Kubernetes API 服务器的完整示例。以下部分描述了一些各种扩展的常见模式。
自定义资源的模式
并非所有的自定义资源都相同。有许多原因可以扩展 Kubernetes API 的表面,接下来的部分将讨论一些您可能想考虑的一般模式。
仅数据
API 扩展的最简单模式是“仅数据”的概念。在这种模式下,您只是使用 API 服务器来存储和检索应用程序的信息。重要的是要注意,您不应该将 Kubernetes API 服务器用作应用程序数据存储的关键/值存储。 Kubernetes API 服务器并非设计用于您的应用程序的关键/值存储;相反,API 扩展应该是帮助您管理应用程序部署或运行时的控制或配置对象。例如,“仅数据”模式的一个用例可能是配置您的应用程序的金丝雀部署,例如,将所有流量的 10% 导向实验后端。尽管从理论上讲,这种配置信息也可以存储在 ConfigMap 中,但 ConfigMap 本质上是无类型的,有时使用更强类型的 API 扩展对象提供了更清晰和易用的操作。
只是数据的扩展不需要相应的控制器来激活它们,但它们可能有验证或变更控制器来确保它们的格式良好。例如,在金丝雀使用案例中,验证控制器可以确保金丝雀对象中的所有百分比总和为 100%。
编译器
稍微复杂一些的模式是“编译器”或“抽象”模式。在这种模式中,API 扩展对象代表一个更高级别的抽象,被“编译”成一组低级别的 Kubernetes 对象的组合。前面例子中的 LoadTest 扩展就是这种编译器抽象模式的一个示例。用户以高级概念(例如loadtest)使用该扩展,但它以 Kubernetes Pods 和服务的集合形式部署。为了实现这一点,编译后的抽象需要一个 API 控制器在集群中某处运行,以监视当前的 LoadTests 并创建“编译”的表示形式(以及删除不再存在的表示形式)。然而,与下文描述的运算符模式相比,编译的抽象没有在线健康维护;健康维护被委托给更低级别的对象(例如 Pods)。
运算符
虽然编译器扩展提供了易于使用的抽象,但使用“运算符”模式的扩展提供了对扩展创建的资源的在线、主动管理。这些扩展可能提供更高级别的抽象(例如数据库),被编译为较低级别的表示,但它们还提供在线功能,例如数据库的快照备份或在软件新版本可用时的升级通知。为实现这一点,控制器不仅监视扩展 API 以根据需要添加或删除内容,还监视扩展提供的应用程序(例如数据库)的运行状态,并采取行动来修复不健康的数据库,拍摄快照,或在发生故障时从快照中恢复。
运算符是 Kubernetes API 扩展中最复杂的模式之一,但也是最强大的,使用户能够轻松访问“自动驾驶”抽象,不仅负责部署,还包括健康检查和修复。
入门指南
开始扩展 Kubernetes API 可能是一个令人畏惧和耗费精力的经验。幸运的是,有大量的代码可以帮助您。Kubebuilder 项目 包含了一个代码库,旨在帮助您轻松构建可靠的 Kubernetes API 扩展。这是一个极好的资源,可以帮助您启动扩展。
概要
Kubernetes 的一个伟大的“超能力”之一是其生态系统,推动这一生态系统的其中一个最重要的因素是 Kubernetes API 的可扩展性。无论您是设计自己的扩展来定制您的集群,还是使用现成的扩展作为实用工具、集群服务或运营商,API 扩展都是使您的集群独一无二并为可靠应用程序的快速开发构建正确环境的关键。
第十八章:从常见编程语言访问 Kubernetes
尽管本书大部分内容都致力于使用声明性 YAML 配置,直接通过 kubectl 或者像 Helm 这样的工具,但有些情况下必须直接从编程语言与 Kubernetes API 进行交互。例如,Helm 工具 的作者们就需要用编程语言来编写该应用程序。更普遍地说,如果你需要编写额外的工具,比如 kubectl 插件,或者像 Kubernetes 运算符这样的更复杂的代码,这是很常见的情况。
Kubernetes 生态系统的大部分内容都是用 Go 编程语言编写的。因此,Go 语言拥有最丰富和最广泛的客户端。不过,对于大多数常见的编程语言(甚至一些不常见的语言),都有高质量的客户端。由于已经有了大量关于如何使用 Go 客户端的文档和示例,本章将涵盖使用 Python、Java 和 .NET 示例与 Kubernetes API 服务器进行交互的基础知识。
Kubernetes API:客户端视角
归根结底,Kubernetes API 服务器只是一个 HTTP(S) 服务器,每个客户端库都会以此方式来看待它,尽管每个客户端都有大量的附加逻辑来实现各种 API 调用,并且在 JSON 序列化和反序列化方面。因此,你可能会倾向于简单地使用普通的 HTTP 客户端来处理 Kubernetes API,但客户端库会将这些不同的 HTTP 调用封装成有意义的 API(例如 readNamespacedPod(...)),以使你的代码更易读,并提供有意义的类型化对象模型,从而促进静态类型检查,减少错误(例如 Deployment)。也许更重要的是,客户端库还实现了 Kubernetes 特定的功能,如从 kubeconfig 文件或 Pod 环境中加载授权信息。客户端还提供了 Kubernetes API 表面的非 RESTful 部分的实现,如端口转发、日志和监听功能。我们将在后续章节中描述这些高级功能。
OpenAPI 和生成的客户端库
Kubernetes API 中的资源和功能集合非常庞大。不同的 API 组中有许多不同的资源,以及每个资源上的许多不同操作。如果开发者需要手工编写所有这些 API 调用,那将是一个非常庞大(并且无疑令人枯燥)的任务。特别是考虑到客户端必须在各种编程语言中手工编写。相反,客户端采用了不同的方法,与 Kubernetes API 服务器进行交互的基础都是由一个类似于反向编译器的计算机程序生成的。API 客户端的代码生成器采用 Kubernetes API 的数据规范,并使用这些规范为特定语言生成客户端。
Kubernetes API 采用称为 OpenAPI 的格式表示,这是表示 RESTful API 的最常见模式。为了让你感受到 Kubernetes API 的规模,GitHub 上的 OpenAPI 规范 文件大小超过四兆字节。这是一个相当大的文本文件!所有官方的 Kubernetes 客户端库都是使用相同的核心代码生成逻辑生成的,你可以在 GitHub 上找到这些逻辑。虽然你不太可能需要自己生成客户端库,但了解生成这些库的过程仍然是有用的。特别是因为大部分客户端代码是生成的,因此无法直接在生成的客户端代码中进行更新和修复,因为下次生成 API 时会被覆盖。因此,当发现客户端中的错误时,需要修复 OpenAPI 规范(如果错误在规范本身中)或代码生成器(如果错误在生成的代码中)。尽管这个过程看起来过于复杂,但这是少数 Kubernetes 客户端作者能够跟上 Kubernetes API 广度的唯一方式。
但是 kubectl x 呢?
当你开始实现自己的逻辑与 Kubernetes API 交互时,可能不久就会想知道如何执行 kubectl x。大多数人在学习 Kubernetes 时都是从 kubectl 工具开始的,因此他们预期 kubectl 和 Kubernetes API 之间有一对一的映射关系。虽然某些命令在 Kubernetes API 中直接表示(例如 kubectl get pods),但大多数更复杂的功能实际上是通过多个具有复杂逻辑的 API 调用在 kubectl 工具中实现的。
自 Kubernetes 起初以来,客户端和服务器端功能之间的平衡一直是一个设计折衷。现在在 API 服务器上实现的许多功能最初是在 kubectl 中作为客户端实现的。例如,现在由 Deployment 资源在服务器上实现的发布功能,以前是在客户端上实现的。同样,直到最近,kubectl apply ... 只能在命令行工具内使用,但已迁移到服务器作为服务器端的 apply 功能,将在本章后面讨论。
尽管总体上朝向服务器端实现发展,仍然有一些显著功能留在客户端。这些功能必须在每个客户端库中重新实现。不同语言之间的与 kubectl 命令行工具的兼容性也各不相同。特别是 Java 客户端已经构建了一个模拟大部分 kubectl 功能的厚客户端。
如果在您的客户端库中找不到所需的功能,一个有用的技巧是在您的 kubectl 命令中添加 --v=10 标志。这将打开详细日志记录,包括发送到 Kubernetes API 服务器的所有 HTTP 请求和响应。您可以使用此日志来重构 kubectl 所做的大部分工作。如果您仍然需要深入挖掘,kubectl 的源代码也可以在 Kubernetes 仓库中找到。
编程 Kubernetes API
现在您对 Kubernetes API 如何工作以及客户端和服务器如何交互有了更深入的理解。在接下来的章节中,我们将介绍如何对 Kubernetes API 服务器进行认证并与资源进行交互。最后,我们将涉及从编写运算符到与 Pod 进行交互操作的高级主题。
安装客户端库
在开始使用 Kubernetes API 进行编程之前,您需要找到客户端库。我们将使用 Kubernetes 项目本身生产的官方客户端库,尽管也有许多作为独立项目开发的高质量客户端。这些客户端库都托管在 GitHub 上的 kubernetes-client 仓库下:
这些项目中的每一个都具有兼容性矩阵,显示客户端的哪些版本适用于 Kubernetes API 的哪些版本,并提供使用特定编程语言的软件包管理器(例如 npm)安装库的说明。^(1)
认证到 Kubernetes API
如果允许世界上任何人访问 Kubernetes API 并读取或写入其编排的资源,那么 Kubernetes API 服务器将不会很安全。因此,编程访问 Kubernetes API 的第一步是连接到 API 并进行身份验证。由于 API 服务器在其核心是一个 HTTP 服务器,所以这些身份验证方法是核心的 HTTP 身份验证方法。最初的 Kubernetes 实现使用基本的 HTTP 身份验证通过用户和密码组合进行身份验证,但是这种方法已被更现代的身份验证基础设施所取代。
如果您一直使用 kubectl 命令行工具与 Kubernetes 交互,可能未考虑过认证的实现细节。幸运的是,客户端库通常使连接到 API 变得简单。但是,了解 Kubernetes 认证的基本工作原理仍然有助于在出现问题时进行调试。
kubectl 工具和客户端获取认证信息的两种基本方式:来自 kubeconfig 文件和来自 Kubernetes 集群中 Pod 的上下文。
不在 Kubernetes 集群内运行的代码需要一个 kubeconfig 文件来提供认证所需的信息。默认情况下,客户端会在 *\({HOME}/.kube/config* 或 `\)KUBECONFIG环境变量指定的位置搜索此文件。如果存在KUBECONFIG` 变量,则优先于默认的主目录位置中的任何配置文件。kubeconfig 文件包含访问 Kubernetes API 服务器所需的所有信息。客户端都有易于使用的调用方式,可以从默认位置或代码中提供的 kubeconfig 文件创建客户端:
Python
config.load_kube_config()
Java
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
.NET
var config = KubernetesClientConfiguration.BuildDefaultConfig();
var client = new Kubernetes(config);
注意
许多云提供商的认证通过一个外部可执行文件完成,该文件知道如何为 Kubernetes 集群生成令牌。此可执行文件通常作为云提供商命令行工具的一部分安装。当您编写与 Kubernetes API 交互的代码时,需要确保在代码运行的上下文中也可执行此可执行文件以获取令牌。
在 Kubernetes 集群中 Pod 的上下文中,运行在 Pod 中的代码可以访问与该 Pod 关联的 Kubernetes 服务账户。包含相关令牌和证书授权的文件在创建 Pod 时由 Kubernetes 作为卷放置在 Pod 中。在 Kubernetes 集群中,API 服务器始终位于固定的 DNS 名称下,通常为 kubernetes。因为 Pod 中存在所有必要的数据,所以不需要 kubeconfig 文件,客户端可以从其上下文中合成其配置。客户端都有易于使用的调用方式来创建这样的“集群内”客户端:
Python
config.load_incluster_config()
Java
ApiClient client = ClientBuilder.cluster().build();
Configuration.setDefaultApiClient(client);
.NET
var config = KubernetesClientConfiguration.InClusterConfig()
var client = new Kubernetes(config);
注意
与 Pod 相关联的默认服务账户被授予了最低的角色(RBAC)。这意味着,默认情况下,运行在 Pod 中的代码对 Kubernetes API 的操作有限。如果你遇到授权错误,可能需要调整服务账户,选择一个特定于你的代码并具有集群中必要角色权限的账户。
访问 Kubernetes API
人们与 Kubernetes API 交互的最常见方式是通过基本操作,如创建、列出和删除资源。因为所有客户端都是从相同的 OpenAPI 规范生成的,它们都遵循相同的大致模式。在深入代码之前,还有几个关于 Kubernetes API 的细节是必须理解的。
在 Kubernetes 中,有命名空间和集群级别的资源区别。命名空间 资源存在于 Kubernetes 命名空间内;例如,Pod 或 Deployment 可能存在于 kube-system 命名空间中。集群级别 资源则只存在于整个集群中的一个实例。最明显的例子是 Namespace,但其他集群级别的资源还包括 CustomResourceDefinitions 和 ClusterRoleBindings。这种区别很重要,因为它在你访问资源时的函数调用中得以体现。例如,要在 Python 中列出 default 命名空间中的 Pods,你需要编写 api.list_namespaced_pods('default')。要列出命名空间,你需要编写 api.list_namespaces()。
第二个你需要理解的概念是API 组。在 Kubernetes 中,所有资源都被分组到不同的 API 集合中。尽管使用 kubectl 工具的用户可能不太会注意到这一点,但你可能在 Kubernetes 对象的 YAML 规范的 apiVersion 字段中看到过。当针对 Kubernetes API 进行编程时,这种分组变得很重要,因为通常每个 API 组都有其自己的客户端来与该组资源进行交互。例如,要创建一个用于与 Deployment 资源交互的客户端(该资源存在于 apps/v1 API 组和版本中),你需要创建一个 new AppsV1Api() 对象,它知道如何与 apps/v1 API 组和版本中的所有资源进行交互。如何为 API 组创建客户端的示例将在以下部分展示。
将所有内容汇总:在 Python、Java 和 .NET 中列出和创建 Pods
现在我们已经准备好实际编写一些代码了。首先创建一个客户端对象,然后使用它来列出“default”命名空间中的 Pods;以下是在 Python、Java 和 .NET 中实现的代码:
Python
config.load_kube_config()
api = client.CoreV1Api()
pod_list = api.list_namespaced_pod('default')
Java
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
CoreV1Api api = new CoreV1Api();
V1PodList list = api.listNamespacedPod("default");
.NET
var config = KubernetesClientConfiguration.BuildDefaultConfig();
var client = new Kubernetes(config);
var list = client.ListNamespacedPod("default");
一旦你弄清楚如何列出、读取和删除对象,下一个常见任务就是创建新对象。调用 API 来创建对象是相对容易理解的(例如,在 Python 中使用 create_namespaced_pod),但实际定义新 Pod 资源可能更加复杂。
下面是在 Python、Java 和 .NET 中创建 Pod 的方法:
Python
container = client.V1Container(
name="myapp",
image="my_cool_image:v1",
)
pod = client.V1Pod(
metadata = client.V1ObjectMeta(
name="myapp",
),
spec=client.V1PodSpec(containers=[container]),
)
Java
V1Pod pod =
new V1PodBuilder()
.withNewMetadata().withName("myapp").endMetadata()
.withNewSpec()
.addNewContainer()
.withName("myapp")
.withImage("my_cool_image:v1")
.endContainer()
.endSpec()
.build();
.NET
var pod = new V1Pod()
{
Metadata = new V1ObjectMeta{ Name = "myapp", },
Spec = new V1PodSpec
{
Containers = new[] {
new V1Container() {
Name = "myapp", Image = "my_cool_image:v1",
},
},
}
};
创建和修补对象
当你探索 Kubernetes 的客户端 API 时,你会注意到似乎有三种不同的方法来操作资源,分别是create、replace和patch。这三个动词代表着与资源交互的略微不同的语义:
Create
正如名称所示,这将创建一个新的资源。但是,如果资源已经存在,则会失败。
Replace
这将完全替换现有资源,而不查看现有资源。当使用replace时,必须指定完整的资源。
Patch
这修改了现有资源,未改变资源的未更改部分。在使用patch时,您使用特殊的 Patch 资源而不是发送要修改的资源(例如 Pod)。
注意
对资源进行修补可能会很复杂。在许多情况下,直接替换会更容易。然而,在某些情况下,特别是对于大型资源,修补资源在网络带宽和 API 服务器处理方面可能更高效。此外,多个操作者可以同时修补资源的不同部分,而无需担心写冲突,这减少了开销。
要修补 Kubernetes 资源,必须创建一个表示要对资源进行的更改的 Patch 对象。Kubernetes 支持三种此补丁的格式:JSON Patch、JSON Merge Patch 和策略性合并补丁。前两种补丁格式是其他地方使用的 RFC 标准,第三种是 Kubernetes 开发的补丁格式。每种补丁格式都有优缺点。在这些示例中,我们将使用 JSON Patch,因为它最简单易懂。
下面是如何将 Deployment 修补以增加副本数到三个的方法:
Python
deployment.spec.replicas = 3
api_response = api_instance.patch_namespaced_deployment(
name="my-deployment",
namespace="some-namespace",
body=deployment)
Java
// JSON-patch format
static String jsonPatch =
"[{\"op\":\"replace\",\"path\":\"/spec/replicas\",\"value\":3}]";
V1Deployment patched =
PatchUtils.patch(
V1Deployment.class,
() ->
api.patchNamespacedDeploymentCall(
"my-deployment",
"some-namespace",
new V1Patch(jsonPatchStr),
null,
null,
null,
null,
null),
V1Patch.PATCH_FORMAT_JSON_PATCH,
api.getApiClient());
.NET
var jsonPatch = @"
[{
""op"": ""replace"",
""path"": ""/spec/replicas"",
""value"": 3
}]";
client.PatchNamespacedPod(
new V1Patch(patchStr, V1Patch.PatchType.JsonPatch),
"my-deployment",
"some-namespace");
在这些代码示例中,Deployment 资源已被修补以将部署中的副本数设置为三。
监视 Kubernetes API 的变化
Kubernetes 中的资源是声明性的。它们代表系统的期望状态。为了使这个期望的状态成为现实,程序必须监视期望的状态以进行更改,并采取行动使世界的当前状态与期望的状态匹配。
由于这种模式,针对 Kubernetes API 编程时最常见的任务之一是监视资源的变化,然后根据这些变化采取某些操作。最简单的方法是通过轮询来实现。轮询 简单地在固定的间隔(如每 60 秒)调用上述列出资源的函数,并枚举代码感兴趣的所有资源。虽然这种代码编写起来很容易,但对客户端代码和 API 服务器都有很多缺点。轮询引入不必要的延迟,因为等待轮询周期导致在上一次轮询完成后发生的更改存在延迟。此外,轮询会导致 API 服务器负载加重,因为它反复返回未更改的资源。虽然许多简单的客户端开始使用轮询,但太多客户端轮询 API 服务器可能会使其超载并增加延迟。
为解决这个问题,Kubernetes API 还提供了 watch 或基于事件的语义。使用 watch 调用,您可以向 API 服务器注册对特定更改的兴趣,而不是重复轮询,API 服务器会在发生更改时发送通知。在实际操作中,客户端执行一个持续的 GET 到 HTTP API 服务器。支持这个 HTTP 请求的 TCP 连接在整个 watch 期间保持打开状态,服务器在每次更改时向该流写入响应(但不关闭流)。
从程序角度来看,watch 语义支持基于事件的编程,将重复轮询的 while 循环改为一组回调函数。以下是监视 Pod 变化的示例:
Python
config.load_kube_config()
api = client.CoreV1Api()
w = watch.Watch()
for event in w.stream(v1.list_namespaced_pods, "some-namespace"):
print(event)
Java
ApiClient client = Config.defaultClient();
CoreV1Api api = new CoreV1Api();
Watch<V1Namespace> watch =
Watch.createWatch(
client,
api.listNamespacedPodCall(
"some-namespace",
null,
null,
null,
null,
null,
Integer.MAX_VALUE,
null,
null,
60,
Boolean.TRUE);
new TypeToken<Watch.Response<V1Pod>>() {}.getType());
try {
for (Watch.Response<V1Pod> item : watch) {
System.out.printf(
"%s : %s%n", item.type, item.object.getMetadata().getName());
}
} finally {
watch.close();
}
.NET
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
var client = new Kubernetes(config);
var watch =
client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true);
using (watch.Watch<V1Pod, V1PodList>((type, item) =>
{
Console.WriteLine(item);
}
在这些示例中,与重复轮询循环不同,watch API 调用将每个资源的每个变化传递给用户提供的回调函数。这既减少了延迟,也减少了 Kubernetes API 服务器的负载。
与 Pod 交互
Kubernetes API 还提供了直接与运行在 Kubernetes Pod 中的应用程序进行交互的函数。kubectl 工具提供了许多与 Pod 交互的命令,包括 logs、exec 和 port-forward,也可以从自定义代码中使用这些命令。
注意
由于 logs、exec 和 port-forward API 在 RESTful 视角上是非标准的,它们在客户端库中需要定制逻辑,因此在不同的客户端之间可能不太一致。不幸的是,除了学习每种语言的实现之外,没有其他选择。
当获取 Pod 的日志时,您必须决定是读取 Pod 日志以获取其当前状态的快照,还是将其流式传输以在发生时接收新的日志。如果流式传输日志(相当于 kubectl logs -f ...),则会创建到 API 服务器的开放连接,并且新的日志行将像写入 Pod 一样写入到此流中。否则,您只会接收日志的当前内容。
这里是如何同时读取和流式传输日志的方法:
Python
config.load_kube_config()
api = client.CoreV1Api()
log = api_instance.read_namespaced_pod_log(
name="my-pod", namespace="some-namespace")
Java
V1Pod pod = ...; // some code to define or get a Pod here
PodLogs logs = new PodLogs();
InputStream is = logs.streamNamespacedPodLog(pod);
.NET
IKubernetes client = new Kubernetes(config);
var response = await client.ReadNamespacedPodLogWithHttpMessagesAsync(
"my-pod", "my-namespace", follow: true);
var stream = response.Body;
另一个常见的任务是在 Pod 中执行某个命令并获取运行该任务的输出。您可以在命令行上使用 kubectl exec ... 命令。在幕后,实现此功能的 API 正在创建到 API 服务器的 WebSocket 连接。WebSocket 可以在同一 HTTP 连接上同时存在多个数据流(在本例中是 stdin、stdout 和 stderr)。如果您从未使用过 WebSocket,不用担心;客户端库会处理与 WebSocket 的交互细节。
这里是如何在 Pod 中执行 ls /foo 命令的方法:
Python
cmd = [ 'ls', '/foo' ]
response = stream(
api_instance.connect_get_namespaced_pod_exec,
"my-pod",
"some-namespace",
command=cmd,
stderr=True,
stdin=False,
stdout=True,
tty=False)
Java
ApiClient client = Config.defaultClient();
Configuration.setDefaultApiClient(client);
Exec exec = new Exec();
final Process proc =
exec.exec("some-namespace",
"my-pod",
new String[] {"ls", "/foo"},
true,
true /*tty*/);
.NET
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
IKubernetes client = new Kubernetes(config);
var webSocket =
await client.WebSocketNamespacedPodExecAsync(
"my-pod", "some-namespace", "ls /foo", "my-container-name");
var demux = new StreamDemuxer(webSocket);
demux.Start();
var stream = demux.GetStream(1, 1);
除了在 Pod 中运行命令外,您还可以将网络连接从 Pod 端口转发到运行在本地机器上的代码。与 exec 类似,端口转发的流量通过 WebSocket 进行。由您的代码决定如何处理此端口转发的套接字。您可以简单地发送一个请求并作为字节字符串接收响应,或者您可以构建一个完整的代理服务器(类似于 kubectl port-forward),通过此代理处理任意请求。
无论您打算如何处理连接,这里是如何设置端口转发的方法:
Python
pf = portforward(
api_instance.connect_get_namespaced_pod_portforward,
'my-pod', 'some-namespace',
ports='8080',
)
Java
PortForward fwd = new PortForward();
List<Integer> ports = new ArrayList<>();
int localPort = 8080;
int targetPort = 8080;
ports.add(targetPort);
final PortForward.PortForwardResult result =
fwd.forward("some-namespace", "my-pod", ports);
.NET
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
IKubernetes client = new Kubernetes(config);
var webSocket = await client.WebSocketNamespacedPodPortForwardAsync(
"some-namespace", "my-pod", new int[] {8080}, "v4.channel.k8s.io");
var demux = new StreamDemuxer(webSocket, StreamType.PortForward);
demux.Start();
var stream = demux.GetStream((byte?)0, (byte?)0);
每个示例都在 Pod 中的端口 8080 上创建到程序端口 8080 的连接。该代码返回必要的字节流,通过这种端口转发通道进行通信。您可以使用这些流来发送和接收消息。
概要
Kubernetes API 提供了丰富且强大的功能,让您编写自定义代码。将您的应用程序编写成最适合任务或个人角色的语言,与尽可能多的 Kubernetes 用户分享编排 API 的功能。当您准备好超越对 kubectl 可执行文件的脚本调用时,Kubernetes 客户端库提供了一种方式,深入 API 构建操作员、监控代理、新用户界面或您的想象力可以梦想到的任何东西。
^(1) 为了简洁起见,我们没有包含 JavaScript 示例,但它也在积极开发中。
第十九章:安全保护 Kubernetes 应用程序
为你的工作负载提供一个安全平台对 Kubernetes 能够在生产环境中得到广泛应用至关重要。幸运的是,Kubernetes 集成了许多不同的安全焦点 API,可以帮助你构建一个安全的操作环境。挑战在于有许多不同的安全 API,并且你必须声明性地选择使用它们。使用这些安全焦点 API 可能会很麻烦和复杂,这使得难以达到你期望的安全目标。
在保护 Kubernetes 中的 Pod 时,理解以下两个概念非常重要:深度防御和最小权限原则。深度防御 是一个概念,它在包括 Kubernetes 在内的计算系统上使用多层安全控制。最小权限原则 意味着只允许你的工作负载访问操作所需的资源。这两个概念不是终点,而是不断应用于不断变化的计算系统环境中。
在本章中,我们将探讨能够逐步应用于帮助在 Pod 级别保护你的工作负载的安全焦点 Kubernetes API。
理解 SecurityContext
在保护 Pod 的核心是 SecurityContext,它是可能应用在 Pod 和容器规范级别的所有安全焦点字段的汇总。以下是由 SecurityContext 包含的一些示例安全控制:
-
用户权限和访问控制(例如,设置用户 ID 和组 ID)
-
只读根文件系统
-
允许特权升级
-
Seccomp、AppArmor 和 SELinux 的配置文件和标签分配
-
以特权或非特权方式运行
让我们看一个在 示例 19-1 中定义了 SecurityContext 的 Pod 示例。
示例 19-1. kuard-pod-securitycontext.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
ports:
- containerPort: 8080
name: http
protocol: TCP
你可以在这个示例中看到,Pod 和容器级别都有一个 SecurityContext。许多安全控制可以同时应用在这两个级别。如果同时在这两个级别应用,容器级别的配置会优先生效。让我们看看在这个示例中我们在 Pod 规范中定义的字段以及它们对保护你的工作负载的影响:
runAsNonRoot
Pod 或容器必须作为非根用户运行。如果以根用户身份运行,容器将无法启动。作为非根用户运行被认为是最佳实践,因为许多错误配置和漏洞都是由于容器运行时将容器进程误认为与主机根用户相同而发生的。这可以在 PodSecurityContext 和 SecurityContext 中设置。kuard 容器镜像被配置为以 Dockerfile 中定义的用户 "nobody" 运行。以非根用户身份运行您的容器始终是最佳实践;然而,如果您正在运行从其他来源下载的容器,而该容器未明确设置容器用户,则可能需要扩展原始 Dockerfile 来进行设置。这种方法并不总是有效,因为应用程序可能有其他需要考虑的要求。
runAsUser/runAsGroup
此设置将覆盖容器进程的用户和组。容器镜像可能已经在 Dockerfile 中配置了这一点。
fsgroup
配置 Kubernetes 在将卷挂载到 Pod 时更改所有文件的组。可以使用额外字段 fsGroupChangePolicy 来配置确切的行为。
allowPrivilegeEscalation
配置容器中的进程是否可以获得比其父进程更多的特权。这是一种常见的攻击向量,重要的是要显式地将其设置为 false。如果设置了 privileged: true,它将设置为 true 也很重要。
privileged
以特权方式运行容器,这将提升容器到与主机相同的权限。
readOnlyRootFilesystem
将容器根文件系统挂载为只读。这是一个常见的攻击向量,并且是一种最佳实践。工作负载需要写访问权限的任何数据或日志可以通过卷进行挂载。
这个示例中的字段并不是所有可用安全控制的完整列表;然而,在使用 SecurityContext 时,它们代表了一个很好的起点。我们将在本章后面的上下文中进一步介绍一些内容。
现在我们将通过将此示例保存到名为 kuard-pod-securitycontext.yaml 的文件中来创建 Pod。我们将演示如何将 SecurityContext 配置应用于正在运行的 Pod。使用以下命令创建 Pod:
$ kubectl create -f kuard-pod-securitycontext.yaml
pod/kuard created
现在我们将在 kuard 容器内启动一个 shell 并检查进程正在以哪个用户 ID 和组 ID 运行:
$ kubectl exec -it kuard -- ash
/ $ id
uid=1000 gid=3000 groups=2000
/ $ ps
PID USER TIME COMMAND
1 1000 0:00 /kuard
30 1000 0:00 ash
37 1000 0:00 ps
/ $ touch file
touch: file: Read-only file system
我们可以看到我们启动的 shell ash 正在以用户 ID(uid)1000、组 ID(gid)3000 运行,并且在组 2000 中。我们还可以看到 kuard 进程正如 Pod 规范中 SecurityContext 定义的那样以用户 1000 运行。我们还确认无法创建任何新文件,因为容器是只读的。如果您的工作负载仅应用以下更改,您已经迈出了良好的开端。
我们现在将介绍由 SecurityContext 覆盖的其他几个安全控件,这些控件可以更精细地控制您的工作负载的访问和特权。首先,我们将介绍操作系统级别的安全控制,然后介绍如何通过 SecurityContext 进行配置。需要注意的是,许多这些控件依赖于主机操作系统。这意味着它们可能仅适用于在 Linux 操作系统上运行的容器,而不适用于其他支持的 Kubernetes 操作系统,如 Windows。以下是由 SecurityContext 覆盖的核心操作系统控件列表:
Capabilities
允许添加或删除可能需要的特权组。例如,您的工作负载可能配置主机的网络配置。与其配置 Pod 以获取特权访问,即实际上是主机根访问权限,您可以添加特定的能力来配置主机的网络配置(NET_ADMIN 是特定的能力名称)。这遵循最小权限原则。
AppArmor
控制进程可以访问哪些文件。通过将container.apparmor.security.beta.kubernetes.io/<container_name>: <profile_ref>的注释添加到 Pod 规范中,可以将 AppArmor 配置文件应用于容器。<profile_ref>的可接受值包括runtime/default、localhost/<path to profile>和unconfined。默认值为unconfined,这明确设置了不应用任何配置文件。
Seccomp
Seccomp(安全计算)配置文件允许创建系统调用过滤器。这些过滤器允许允许或阻止特定的系统调用,从而限制在 Pod 中的进程向 Linux 内核暴露的表面积。
SELinux
定义文件和进程的访问控制。SELinux 运算符使用标签将其组合,以创建一个安全上下文(不要与 Kubernetes SecurityContext 混淆),用于限制对进程的访问。默认情况下,Kubernetes 为每个容器分配一个随机的 SELinux 上下文;但是,您可以选择通过 SecurityContext 设置一个上下文。
注意
AppArmor 和 Seccomp 都可以设置用于运行时默认配置文件。每个容器运行时都附带了经过精心策划的默认 AppArmor 和 Seccomp 配置文件,以减少攻击面积,通过删除已知的系统调用和文件访问攻击向量或不常用于应用程序的方式。
要演示这些安全控制是如何应用于 Pod 的,我们将使用一个名为 amicontained(“Am I contained”)的工具,由 Jess Frazelle 编写。将 Pod 的规范保存在 Example 19-2 中,保存为 amicontained-pod.yaml 文件。第一个 Pod 没有应用安全上下文,并将用于显示默认情况下对 Pod 应用的安全控制。请注意,由于不同的 Kubernetes 发行版和托管服务提供了不同的默认值,您的输出可能会有所不同。
Example 19-2. amicontained-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: amicontained
spec:
containers:
- image: r.j3ss.co/amicontained:v0.4.9
name: amicontained
command: [ "/bin/sh", "-c", "--" ]
args: [ "amicontained" ]
创建 amicontainer Pod:
$ kubectl apply -f amicontained-pod.yaml
pod/amicontained created
让我们查看 Pod 日志,以检查 amicontained 工具的输出:
$ kubectl logs amicontained
Container Runtime: kube
Has Namespaces:
pid: true
user: false
AppArmor Profile: docker-default (enforce)
Capabilities:
BOUNDING -> chown dac_override fowner fsetid kill setgid setuid
setpcap net_bind_service net_raw sys_chroot mknod audit_write
setfcap
Seccomp: disabled
Blocked Syscalls (21):
SYSLOG SETPGID SETSID VHANGUP PIVOT_ROOT ACCT SETTIMEOFDAY UMOUNT2
SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME INIT_MODULE
DELETE_MODULE LOOKUP_DCOOKIE KEXEC_LOAD FANOTIFY_INIT
OPEN_BY_HANDLE_AT FINIT_MODULE KEXEC_FILE_LOAD
Looking for Docker.sock
从上面的输出中,我们看到正在应用 AppArmor 运行时默认配置。我们还可以看到默认情况下允许的功能以及禁用的 seccomp。最后,我们看到默认情况下阻止了 21 个系统调用。现在我们有了一个基准,让我们将 seccomp、AppArmor 和功能安全控制应用于 Pod 规范。创建一个名为 amicontained-pod-securitycontext.yaml 的文件,内容来自 Example 19-3。
Example 19-3. amicontained-pod-securitycontext.yaml
apiVersion: v1
kind: Pod
metadata:
name: amicontained
annotations:
container.apparmor.security.beta.kubernetes.io/amicontained: "runtime/default"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- image: r.j3ss.co/amicontained:v0.4.9
name: amicontained
command: [ "/bin/sh", "-c", "--" ]
args: [ "amicontained" ]
securityContext:
capabilities:
add: ["SYS_TIME"]
drop: ["NET_BIND_SERVICE"]
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
首先,我们需要删除现有的 amicontained Pod:
$ kubectl delete pod amicontained
pod "amicontained" deleted
现在我们可以创建一个新的 Pod,并应用安全上下文。我们明确声明要应用运行时默认的 AppArmor 和 seccomp 配置文件。此外,我们还添加和删除了一个功能:
$ kubectl apply -f amicontained-pod-securitycontext.yaml
pod/amicontained created
让我们再次查看 Pod 日志,以检查 amicontained 工具的输出:
$ kubectl logs amicontained
Container Runtime: kube
Has Namespaces:
pid: true
user: false
AppArmor Profile: docker-default (enforce)
Capabilities:
BOUNDING -> chown dac_override fowner fsetid kill setgid setuid setpcap
net_raw sys_chroot sys_time mknod audit_write setfcap
Seccomp: filtering
Blocked Syscalls (67):
SYSLOG SETUID SETGID SETPGID SETSID SETREUID SETREGID SETGROUPS
SETRESUID SETRESGID USELIB USTAT SYSFS VHANGUP PIVOT_ROOT_SYSCTL ACCT
SETTIMEOFDAY MOUNT UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME
SETDOMAINNAME IOPL IOPERM CREATE_MODULE INIT_MODULE DELETE_MODULE
GET_KERNEL_SYMS QUERY_MODULE QUOTACTL NFSSERVCTL GETPMSG PUTPMSG
AFS_SYSCALL TUXCALL SECURITY LOOKUP_DCOOKIE VSERVER MBIND SET_MEMPOLICY
GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY KEYCTL MIGRATE_PAGES
FUTIMESAT UNSHARE MOVE_PAGES PERF_EVENT_OPEN FANOTIFY_INIT
NAME_TO_HANDLE_AT OPEN_BY_HANDLE_AT SETNS PROCESS_VM_READV
PROCESS_VM_WRITEV KCMP FINIT_MODULE KEXEC_FILE_LOAD BPF USERFAULTFD
PKEY_MPROTECT PKEY_ALLOC PKEY_FREE
Looking for Docker.sock
安全上下文挑战
如您所见,要使用 SecurityContext,需要理解很多内容,并且直接配置每个 Pod 的所有字段来应用基线安全控制并不容易。创建和管理 AppArmor、seccomp 和 SELinux 配置和上下文并非易事,且容易出错。出错的代价是破坏应用程序执行功能的能力。有几种工具可以生成运行中 Pod 的 seccomp 配置文件,并使用 SecurityContext 应用。其中一个项目是 Security Profiles Operator,它可以轻松生成和管理 Seccomp 配置文件。现在让我们看看其他安全 API,这些 API 可以确保在整个集群中一致地应用 SecurityContext。
Pod 安全
现在我们已经查看了 SecurityContext 作为管理应用于 Pod 和容器的安全控制的方法,接下来我们将介绍如何确保一组 SecurityContext 值在规模应用。Kubernetes 有一个现已弃用的 PodSecurityPolicy(PSP)API,它可以进行验证和变异。验证将不允许创建 Kubernetes 资源,除非它们具有特定的 SecurityContext。另一方面,变异将改变 Kubernetes 资源,并根据通过 PSP 应用的标准应用特定的 SecurityContext。鉴于 PSP 已弃用,并将在 Kubernetes v1.25 中删除,我们不会深入讨论它,而是会介绍其继任者 Pod Security。Pod Security 与其前身的主要区别之一是,Pod Security 仅执行验证而不执行变异。如果您想了解更多关于变异的信息,我们鼓励您查看第二十章。
什么是 Pod 安全?
Pod Security 允许您为 Pod 声明不同的安全配置文件。这些安全配置文件称为 Pod 安全标准,并应用于命名空间级别。Pod 安全标准是 Pod 规范中一组安全敏感字段(包括但不限于 SecurityContext)及其关联值的集合。有三种不同的标准,从受限到宽松不等。其理念是您可以将一般的安全姿态应用于给定命名空间中的所有 Pod。三种 Pod 安全标准如下:
基线
最常见的权限升级,同时支持更简易的入门。
受限
高度限制,涵盖安全最佳实践。可能导致工作负载中断。
特权
开放和无限制。
警告
截至 Kubernetes v1.23,Pod Security 目前是一个测试功能,可能会有所更改。
每个 Pod 安全标准定义了 Pod 规范中一组字段及其允许的值。以下是这些标准涵盖的一些字段:
-
spec.securityContext -
spec.containers[*].securityContext -
spec.containers[*].ports -
spec.volumes[*].hostPath
您可以在官方文档中查看每个 Pod 安全标准涵盖的完整字段列表。
每个标准都适用于命名空间,采用给定的模式。策略可以适用于三种模式。它们如下所示:
执行
任何违反策略的 Pod 将被拒绝。
警告
任何违反策略的 Pod 将被允许,并向用户显示警告消息。
审核
任何违反策略的 Pod 将在审计日志中生成审计消息。
应用 Pod 安全标准
通过标签将 Pod 安全标准应用于命名空间:
-
必需:
pod-security.kubernetes.io/<MODE>: <LEVEL> -
可选:
pod-security.kubernetes.io/<MODE>-version: <VERSION>(默认为最新)
在 示例 19-4 中的命名空间说明了如何同时使用多种模式来强制执行一个标准(例如本示例中的基线)并在另一个标准(受限)上进行审计和警告。使用多种模式可以让您以较低的安全姿态部署策略,并审计哪些工作负载违反了更严格的策略。然后,您可以在强制执行更严格的标准之前纠正策略违规。您还可以将模式固定到特定版本,例如 v1.22. 这允许策略标准随每个 Kubernetes 版本的发布而变化,并允许您固定到特定版本。在 示例 19-4 中,我们正在强制执行基线标准,并同时警告和审计受限标准。所有模式都固定到标准的 v1.22 版本。
示例 19-4. baseline-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: baseline-ns
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.22
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.22
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.22
首次部署策略可能是一项令人生畏的任务。幸运的是,Pod 安全性通过单个 dry-run 命令使得查看现有工作负载是否违反 Pod 安全标准变得简单:
$ kubectl label --dry-run=server --overwrite ns \
--all pod-security.kubernetes.io/enforce=baseline
Warning: kuard: privileged
namespace/default labeled
namespace/kube-node-lease labeled
namespace/kube-public labeled
Warning: kube-proxy-vxjwb: host namespaces, hostPath volumes, privileged
Warning: kube-proxy-zxqzz: host namespaces, hostPath volumes, privileged
Warning: kube-apiserver-kind-control-plane: host namespaces, hostPath volumes
Warning: etcd-kind-control-plane: host namespaces, hostPath volumes
Warning: kube-controller-manager-kind-control-plane: host namespaces, ...
Warning: kube-scheduler-kind-control-plane: host namespaces, hostPath volumes
namespace/kube-system labeled
namespace/local-path-storage labeled
此命令评估 Kubernetes 集群中所有 Pod 是否符合基线 Pod 安全标准,并将违规项作为警告消息输出。
让我们看看 Pod 安全性的实际操作。创建一个名为 baseline-ns.yaml 的文件,并使用 示例 19-5 中的内容。
示例 19-5. baseline-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: baseline-ns
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.22
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.22
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.22
$ kubectl apply -f baseline-ns.yaml
namespace/baseline-ns created
创建一个名为 kuard-pod.yaml 的文件,并使用 示例 19-6 中的内容。
示例 19-6. kuard-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
labels:
app: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP
使用以下命令创建 Pod 并查看输出:
$ kubectl apply -f kuard-pod.yaml --namespace baseline-ns
Warning: would violate "v1.22" version of "restricted" PodSecurity profile:
allowPrivilegeEscalation != false (container "kuard" must set
securityContext.allowPrivilegeEscalation=false), unrestricted capabilities
(container "kuard" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "kuard" must set securityContext.
runAsNonRoot=true), seccompProfile (pod or container "kuard" must set
securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
pod/kuard created
在此输出中,您可以看到 Pod 已成功创建;然而,它违反了受限的 Pod 安全标准,并且输出中提供了违规的详细信息,以便您进行纠正。我们还可以看到 API 服务器审计日志中的消息,因为我们配置了审计模式:
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"...
Pod 安全性是通过在命名空间级别应用策略来管理工作负载的安全姿态的一种绝佳方式,并且只有在不违反策略的情况下才允许创建 Pod。它灵活且提供不同的预构建策略,从宽松到受限,以及工具支持,可以轻松地推出策略更改,而无需担心破坏工作负载的风险。
服务账户管理
Service accounts 是 Kubernetes 资源,为运行在 Pod 内部的工作负载提供身份。RBAC 可以应用于 service accounts,以控制通过 Kubernetes API 身份可以访问的资源。请参阅 第十四章 了解更多信息。如果您的应用程序不需要访问 Kubernetes API,则应按最小权限原则禁用访问。默认情况下,Kubernetes 在每个命名空间中创建一个默认的 service account,并自动将其设置为所有 Pods 的 service account。该 service account 包含一个在每个 Pod 中自动挂载的令牌,用于访问 Kubernetes API。要禁用此行为,必须将 automountServiceAccountToken: false 添加到 service account 配置中。示例 19-7 演示了如何为默认 service account 进行此操作。这必须在每个命名空间中完成。
示例 19-7. service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
automountServiceAccountToken: false
在考虑 Pod 安全性时,人们经常忽略 service accounts;然而,它们允许直接访问 Kubernetes API,并且在没有足够的 RBAC 的情况下,可能允许攻击者访问 Kubernetes。重要的是要了解如何通过简单地更改 service account 令牌处理方式来限制访问。
基于角色的访问控制
在讨论保护 Pods 的章节中,我们不得不提到 Kubernetes 基于角色的访问控制(RBAC)。您可以在 第十四章 找到有关 RBAC 的所有信息,并可以应用于补充工作负载的安全策略。
RuntimeClass
Kubernetes 通过容器运行时接口(CRI)与节点操作系统上的容器运行时进行交互。该接口的创建和标准化使得容器运行时生态系统得以存在。这些容器运行时可能提供不同级别的隔离,包括基于实现方式的更强安全性保证。像 Kata Containers、Firecracker 和 gVisor 这样的项目基于不同的隔离机制,从嵌套虚拟化到更复杂的系统调用过滤。这些安全和隔离保证为 Kubernetes 管理员提供了灵活性,使用户可以根据其工作负载类型选择容器运行时。例如,如果您的工作负载需要更强的安全保证,则可以选择在使用不同容器运行时的 Pod 中运行。
RuntimeClass API 被引入以允许选择容器运行时。它允许用户从集群中支持的容器运行时列表中选择一个。图 19-1 展示了 RuntimeClass 的功能。
注意
不同的 RuntimeClasses 必须由集群管理员配置,并且可能需要在您的工作负载上配置特定的 nodeSelectors 或 tolerations 才能被调度到正确的节点。

图 19-1. RuntimeClass 流程图
您可以通过在 Pod 规范中指定runtimeClassName来使用 RuntimeClass。示例 19-8 是一个指定 RuntimeClass 的示例 Pod。
Example 19-8. kuard-pod-runtimeclass.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
labels:
app: kuard
spec:
runtimeClassName: firecracker
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP
RuntimeClass 允许用户选择不同的容器运行时,这些运行时可能具有不同的安全隔离。使用 RuntimeClass 可以帮助补充工作负载的整体安全性,特别是当工作负载处理敏感信息或运行不受信任的代码时。
Network Policy
Kubernetes 也有一个 Network Policy API,允许您为工作负载创建入口和出口网络策略。网络策略使用标签进行配置,这些标签允许您选择特定的 Pod,并定义它们如何与其他 Pod 和端点通信。例如,Ingress 这样的 Network Policy 实际上没有与之关联的 Kubernetes 控制器。这意味着您可以创建 Network Policy 资源,但如果您没有安装一个响应 Network Policy 资源创建的控制器,那么它们将不会被执行。Network Policy 资源由网络插件(如 Calico、Cilium 和 Weave Net)实现。
Network Policy 资源是有命名空间的,并且由podSelector、policyTypes、ingress和egress部分组成,其中唯一需要的字段是podSelector。如果podSelector字段为空,则该策略匹配命名空间中的所有 Pod。此字段还可以包含一个matchLabels部分,其功能与 Service 资源相同,允许您添加一组标签以匹配特定的一组 Pod。
使用 Network Policy 时有几个需要注意的特殊情况。如果一个 Pod 匹配任何 Network Policy 资源,则必须显式定义任何入口或出口通信,否则将被阻止。如果一个 Pod 匹配多个 Network Policy 资源,则策略是叠加的。如果一个 Pod 没有匹配任何 Network Policy,则允许流量。这个决定是有意为之,以便简化新工作负载的接入。然而,如果您确实希望默认情况下阻止所有流量,您可以为每个命名空间创建一个默认拒绝规则。示例 19-9 展示了一个可以应用于每个命名空间的默认拒绝规则。
Example 19-9. networkpolicy-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
让我们通过一组网络策略示例来演示如何使用它们来保护您的工作负载。首先,使用以下命令创建一个测试命名空间:
$ kubectl create ns kuard-networkpolicy
namespace/kuard-networkpolicy created
创建名为kuard-pod.yaml的文件,其中包含示例 19-10 的内容。
Example 19-10. kuard-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
labels:
app: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP
在kuard-networkpolicy命名空间中创建kuard Pod:
$ kubectl apply -f kuard-pod.yaml \
--namespace kuard-networkpolicy
pod/kuard created
将kuard Pod 公开为服务:
$ kubectl expose pod kuard --port=80 --target-port=8080 \
--namespace kuard-networkpolicy
pod/kuard created
现在我们可以使用kubectl run来启动一个 Pod 作为我们的源,并测试访问kuard Pod,而不应用任何 Network Policy:
$ kubectl run test-source --rm -ti --image busybox /bin/sh \
--namespace kuard-networkpolicy
If you don't see a command prompt, try pressing enter.
/ # wget -q kuard -O -
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><KUAR Demo></title>
...
我们可以成功从我们的测试源 Pod 连接到 kuard Pod。现在让我们应用默认拒绝策略并再次测试。创建一个名为 networkpolicy-default-deny.yaml 的文件,并包含 示例 19-11 的内容。
示例 19-11. networkpolicy-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
现在应用默认拒绝网络策略:
$ kubectl apply -f networkpolicy-default-deny.yaml \
--namespace kuard-networkpolicy
networkpolicy.networking.k8s.io/default-deny-ingress created
现在让我们测试从测试源 Pod 访问 kuard Pod:
$ kubectl run test-source --rm -ti --image busybox /bin/sh \
--namespace kuard-networkpolicy
If you don't see a command prompt, try pressing enter.
/ # wget -q --timeout=5 kuard -O -
wget: download timed out
由于默认拒绝的网络策略,我们无法再从测试源 Pod 访问 kuard Pod。创建一个允许测试源访问 kuard Pod 的网络策略。创建一个名为 networkpolicy-kuard-allow-test-source.yaml 的文件,并包含 示例 19-12 的内容。
示例 19-12. networkpolicy-kuard-allow-test-source.yaml
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: access-kuard
spec:
podSelector:
matchLabels:
app: kuard
ingress:
- from:
- podSelector:
matchLabels:
run: test-source
应用网络策略:
$ kubectl apply \
-f code/chapter-security/networkpolicy-kuard-allow-test-source.yaml \
--namespace kuard-networkpolicy
networkpolicy.networking.k8s.io/access-kuard created
再次验证测试源 Pod 确实可以访问 kuard Pod:
$ kubectl run test-source --rm -ti --image busybox /bin/sh \
--namespace kuard-networkpolicy
If you don't see a command prompt, try pressing enter.
/ # wget -q kuard -O -
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><KUAR Demo></title>
...
通过运行以下命令清理命名空间:
$ kubectl delete namespace kuard-networkpolicy
namespace "kuard-networkpolicy" deleted
应用网络策略为您的工作负载提供额外的安全层,并继续构建深度防御和最小特权原则的概念。
服务网格
服务网格还可以用于增强工作负载的安全性姿态。服务网格提供访问策略,允许基于服务配置协议感知策略。例如,您的访问策略可能声明 ServiceA 通过 HTTPS 在 443 端口连接到 ServiceB。此外,服务网格通常在所有服务间通信上实施双向 TLS,这意味着通信不仅加密,而且还验证了服务身份。如果您想了解更多关于服务网格如何用于保护工作负载的信息,请查看 第十五章。
安全基准工具
有几个开源工具允许您针对 Kubernetes 集群运行一系列安全基准测试,以确定您的配置是否符合预定义的安全基线。其中一种工具称为 kube-bench。kube-bench 可用于运行 CIS 基准。像 kube-bench 运行 CIS 基准并不专门关注 Pod 安全性;然而,它确实可以暴露任何集群配置错误并帮助确定修复措施。您可以使用以下命令运行 kube-bench:
$ kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench...
job.batch/kube-bench created
然后,您可以通过 Pod 日志查看基准输出和修复措施:
$ kubectl logs job/kube-bench
[INFO] 4 Worker Node Security Configuration
[INFO] 4.1 Worker Node Configuration Files
[PASS] 4.1.1 Ensure that the kubelet service file permissions are set to 644...
[PASS] 4.1.2 Ensure that the kubelet service file ownership is set to root ...
[PASS] 4.1.3 If proxy kubeconfig file exists ensure permissions are set to ...
[PASS] 4.1.4 Ensure that the proxy kubeconfig file ownership is set to root ...
[PASS] 4.1.5 Ensure that the --kubeconfig kubelet.conf file permissions are ...
[PASS] 4.1.6 Ensure that the --kubeconfig kubelet.conf file ownership is set...
[PASS] 4.1.7 Ensure that the certificate authorities file permissions are ...
[PASS] 4.1.8 Ensure that the client certificate authorities file ownership ...
[PASS] 4.1.9 Ensure that the kubelet --config configuration file has permiss...
[PASS] 4.1.10 Ensure that the kubelet --config configuration file ownership ...
[INFO] 4.2 Kubelet
[PASS] 4.2.1 Ensure that the anonymous-auth argument is set to false (Automated)
[PASS] 4.2.2 Ensure that the --authorization-mode argument is not set to ...
[PASS] 4.2.3 Ensure that the --client-ca-file argument is set as appropriate...
[PASS] 4.2.4 Ensure that the --read-only-port argument is set to 0 (Manual)
[PASS] 4.2.5 Ensure that the --streaming-connection-idle-timeout argument is...
[FAIL] 4.2.6 Ensure that the --protect-kernel-defaults argument is set to ...
[PASS] 4.2.7 Ensure that the --make-iptables-util-chains argument is set to ...
[PASS] 4.2.8 Ensure that the --hostname-override argument is not set (Manual)
[WARN] 4.2.9 Ensure that the --event-qps argument is set to 0 or a level ...
[WARN] 4.2.10 Ensure that the --tls-cert-file and --tls-private-key-file arg...
[PASS] 4.2.11 Ensure that the --rotate-certificates argument is not set to ...
[PASS] 4.2.12 Verify that the RotateKubeletServerCertificate argument is set...
[WARN] 4.2.13 Ensure that the Kubelet only makes use of Strong Cryptographic...
== Remediations node ==
4.2.6 If using a Kubelet config file, edit the file to set protectKernel...
If using command line arguments, edit the kubelet service file
/etc/systemd/system/kubelet.service.d/10-kubeadm.conf on each worker node and
set the below parameter in KUBELET_SYSTEM_PODS_ARGS variable.
--protect-kernel-defaults=true
Based on your system, restart the kubelet service. For example:
systemctl daemon-reload
systemctl restart kubelet.service
4.2.9 If using a Kubelet config file, edit the file to set eventRecordQPS...
If using command line arguments, edit the kubelet service file
/etc/systemd/system/kubelet.service.d/10-kubeadm.conf on each worker node and
set the below parameter in KUBELET_SYSTEM_PODS_ARGS variable.
Based on your system, restart the kubelet service. For example:
systemctl daemon-reload
systemctl restart kubelet.service
...
使用像 kube-bench 这样的工具与 CIS 基准可以帮助确定您的 Kubernetes 集群是否符合安全基线,并在需要时提供修复措施。
镜像安全性
另一个 Pod 安全的重要部分是保证 Pod 中的代码和应用程序安全。确保应用程序代码的安全是一个复杂的主题,超出了本章的范围;然而,容器镜像安全的基础包括确保你的容器镜像仓库对已知的代码漏洞进行静态扫描。此外,你应该有一个工具进行运行时扫描,以识别镜像启动后发现的漏洞,并查找潜在的恶意活动,如入侵。开源和专有公司提供了许多扫描工具。除了安全扫描外,专注于最小化容器镜像的内容,以删除不必要的依赖,可以减少扫描时的干扰。最后,镜像安全是投资于持续交付的另一个重要理由,这样当发现漏洞时,可以快速打补丁并重新部署镜像。
总结
在本章中,我们涵盖了许多不同的面向安全的 API 和资源,可以用来提高你的工作负载的安全性姿态。通过实践深度防御和最小权限原则,你可以逐步提高 Kubernetes 集群的基线安全性。开始实践更好的安全措施永远不会太晚,本章提供了你需要的一切,以确保你对 Kubernetes 提供的安全控制有所了解。
第二十章:Kubernetes 集群的政策和治理
本书中,我们介绍了许多不同的 Kubernetes 资源类型,每种都有特定的用途。在 Kubernetes 集群中的资源数量从单一微服务应用的几个,迅速增加到完整分布式应用的几百甚至几千个,所需管理的资源数目也随之增加。在生产集群的上下文中,管理成千上万的资源所面临的挑战是显而易见的。
在本章中,我们介绍了政策和治理的概念。政策 是一组约束和条件,规定 Kubernetes 资源的配置方式。治理 提供了验证和执行所有部署到 Kubernetes 集群的资源的组织政策的能力,例如确保所有资源使用当前最佳实践,符合安全政策,或遵守公司惯例。无论你的情况如何,你的工具都需要灵活和可扩展,以便集群上定义的所有资源都符合你组织定义的政策。
为什么政策和治理很重要
Kubernetes 中有许多不同类型的政策。例如,NetworkPolicy 允许你指定一个 Pod 可以连接到的网络服务和端点。PodSecurityPolicy 使你能够对 Pod 的安全元素进行细粒度控制。两者都可以用于配置网络或容器运行时。
然而,你可能希望在 Kubernetes 资源甚至被创建之前就强制执行一个政策。这就是政策和治理解决的问题。此时,你可能会想到,“这不是基于角色的访问控制(RBAC)所做的事情吗?”然而,正如你将在本章中看到的,RBAC 的粒度不足以限制资源中特定字段的设置。
这里是一些集群管理员常常配置的政策的常见例子:
-
所有容器 必须 只来自特定的容器注册表。
-
所有 Pod 必须 带有部门名称和联系信息的标签。
-
所有 Pod 必须 设置 CPU 和内存资源限制。
-
所有 Ingress 主机名 必须 在集群中唯一。
-
某个服务 不得 对外开放。
-
容器 不得 监听特权端口。
群集 集群管理员也可能想要审计集群上的现有资源,进行干运行政策评估,或基于一组条件修改资源——例如,如果不存在的话,对一个 Pod 应用标签。
集群管理员能够定义政策并进行合规性审计,而不干扰开发人员将应用部署到 Kubernetes 的能力,这一点非常重要。如果开发人员创建了不合规的资源,你需要一个系统,确保他们得到反馈和修正,以使他们的工作符合规定。
让我们看看如何利用 Kubernetes 核心的扩展组件实现政策和治理。
入驻流程
要理解策略和治理如何确保资源在创建前符合规范,必须首先了解请求通过 Kubernetes API 服务器的流程。图 20-1 描绘了 API 请求通过 API 服务器的流程。在这里,我们将重点介绍变异准入、验证准入和 Webhook。

图 20-1. API 请求通过 Kubernetes API 服务器的流程
准入控制器在 API 请求通过 Kubernetes API 服务器时内联操作,并用于在资源保存到存储之前对 API 请求资源进行修改或验证。变异准入控制器允许修改资源;验证准入控制器则不允许。有许多不同类型的准入控制器;本章重点介绍准入 Webhook,这些 Webhook 是动态配置的。它们允许集群管理员配置一个端点,API 服务器可以向其发送请求进行评估,通过创建 MutatingWebhookConfiguration 或 ValidatingWebhookConfiguration 资源。准入 Webhook 将以“admit”或“deny”的指令响应,告知 API 服务器是否将资源保存到存储。
策略与治理使用 Gatekeeper
让我们深入了解如何配置策略并确保 Kubernetes 资源符合规范。Kubernetes 项目未提供任何启用策略和治理的控制器,但有开源解决方案。在这里,我们将关注一个名为Gatekeeper的开源生态系统项目。
Gatekeeper 是一个 Kubernetes 本地策略控制器,根据定义的策略评估资源,并决定是否允许创建或修改 Kubernetes 资源。这些评估在 API 请求通过 Kubernetes API 服务器时在服务器端进行,这意味着每个集群具有单一的处理点。在服务器端处理策略评估意味着您可以在现有的 Kubernetes 集群上安装 Gatekeeper,而无需更改开发人员的工具、工作流程或持续交付流水线。
Gatekeeper 使用自定义资源定义(CRDs)定义了一组新的专门用于配置它的 Kubernetes 资源,这使得集群管理员可以使用熟悉的工具如kubectl来操作 Gatekeeper。此外,它为用户提供实时、有意义的反馈,说明为何资源被拒绝以及如何修复问题。这些 Gatekeeper 特定的自定义资源可以存储在源控制中,并使用 GitOps 工作流进行管理。
Gatekeeper 还执行资源变异(根据定义的条件修改资源)和审计。它高度可配置,并提供对要评估的资源及其命名空间的精细控制。
什么是开放策略代理(Open Policy Agent)?
Gatekeeper 的核心是 Open Policy Agent,一个可扩展的云原生开源策略引擎,允许策略在不同应用程序之间可移植。Open Policy Agent (OPA) 负责执行所有策略评估并返回允许或拒绝结果。这使得 Gatekeeper 能够访问一系列策略工具,例如 Conftest,它允许您编写策略测试并在部署之前在持续集成流水线中实施它们。
Open Policy Agent 专门使用名为 Rego 的本机查询语言来管理所有策略。Gatekeeper 的核心原则之一是将 Rego 的内部工作与集群管理员抽象化,并通过 Kubernetes CRD 提供结构化 API,以创建和应用策略。这使您能够在组织和社区之间共享参数化的策略。Gatekeeper 项目专门为此目的维护一个策略库(本章后续讨论)。
安装 Gatekeeper
在开始配置策略之前,您需要安装 Gatekeeper。Gatekeeper 组件作为 Pod 运行在 gatekeeper-system 命名空间中,并配置 webhook 入场控制器。
警告
在不理解如何安全创建和禁用策略之前,请不要在 Kubernetes 集群上安装 Gatekeeper。在安装 Gatekeeper 之前,请查看安装 YAML 文件,以确保您对其创建的资源感到满意。
您可以使用 Helm 软件包管理器安装 Gatekeeper:
$ helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
$ helm install gatekeeper/gatekeeper --name-template=gatekeeper \
--namespace gatekeeper-system --create-
注意
Gatekeeper 的安装需要 cluster-admin 权限,并且具体版本。请参阅 Gatekeeper 的官方文档获取最新发布信息。
安装完成后,请确认 Gatekeeper 是否已启动:
$ kubectl get pods -n gatekeeper-system
NAME READY STATUS RESTARTS AGE
gatekeeper-audit-54c9759898-ljwp8 1/1 Running 0 1m
gatekeeper-controller-manager-6bcc7f8fb5-4nbkt 1/1 Running 0 1m
gatekeeper-controller-manager-6bcc7f8fb5-d85rn 1/1 Running 0 1m
gatekeeper-controller-manager-6bcc7f8fb5-f8m8j 1/1 Running 0 1m
您还可以使用此命令查看 webhook 的配置方式:
$ kubectl get validatingwebhookconfiguration -o yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
labels:
gatekeeper.sh/system: "yes"
name: gatekeeper-validating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: gatekeeper-webhook-service
namespace: gatekeeper-system
path: /v1/admit
failurePolicy: Ignore
matchPolicy: Exact
name: validation.gatekeeper.sh
namespaceSelector:
matchExpressions:
- key: admission.gatekeeper.sh/ignore
operator: DoesNotExist
rules:
- apiGroups:
- '*'
apiVersions:
- '*'
operations:
- CREATE
- UPDATE
resources:
- '*'
sideEffects: None
timeoutSeconds: 3
...
在上述输出的 rules 部分下,我们看到所有资源都将发送到名为 gatekeeper-webhook-service 的服务,该服务作为 gatekeeper-system 命名空间中的服务运行。仅有标签不是 admission.gatekeeper.sh/ignore 的命名空间中的资源将被用于策略评估。最后,failurePolicy 设置为 Ignore,这意味着这是一个 失败开放配置:如果 Gatekeeper 服务在配置的三秒超时内未响应,则请求将被允许通过。
配置策略
现在 Gatekeeper 已安装,您可以开始配置策略。我们将首先介绍一个典型的示例,并演示集群管理员创建策略的过程。然后我们将查看开发人员在创建符合和不符合规范的资源时的体验。然后我们将进一步扩展每个步骤以获得更深入的理解,并指导您完成创建样例策略的过程,声明容器镜像只能来自一个特定的注册表。此示例基于Gatekeeper 策略库。
首先,您需要配置我们需要创建自定义资源的策略,称为约束模板。这通常由集群管理员完成。在示例 20-1 中的约束模板需要您提供容器库列表作为参数,允许 Kubernetes 资源使用。
示例 20-1. allowedrepos-constraint-template.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
annotations:
description: Requires container images to begin with a repo string from a
specified list.
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = starts...
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed...
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = starts...
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed...)
}
使用以下命令创建约束模板:
$ kubectl apply -f allowedrepos-constraint-template.yaml
constrainttemplate.templates.gatekeeper.sh/k8sallowedrepos created
现在,您可以创建一个约束资源来实施策略(再次扮演集群管理员的角色)。示例 20-2 中的约束允许在default命名空间中具有gcr.io/kuar-demo/前缀的所有容器。enforcementAction设置为“deny”:任何不符合规范的资源都将被拒绝。
示例 20-2. allowedrepos-constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: repo-is-kuar-demo
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "default"
parameters:
repos:
- "gcr.io/kuar-demo/"
$ kubectl create -f allowedrepos-constraint.yaml
k8sallowedrepos.constraints.gatekeeper.sh/repo-is-kuar-demo created
下一步是创建一些 Pod 来测试策略是否有效。示例 20-3 创建一个 Pod,使用一个符合我们在上一步定义的约束的容器镜像,gcr.io/kuar-demo/kuard-amd64:blue。工作负载资源的创建通常由负责操作服务的开发人员或持续交付流水线执行。
示例 20-3. compliant-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:blue
name: kuard
ports:
- containerPort: 8080
name: http
protocol: TCP
$ kubectl apply -f compliant-pod.yaml
pod/kuard created
如果我们创建一个不符合规范的 Pod 会发生什么?示例 20-4 创建一个 Pod,使用一个不符合我们在上一步定义的约束的容器镜像,nginx。工作负载资源的创建通常由开发人员或负责操作服务的持续交付流水线执行。请注意示例 20-4 中的输出。
示例 20-4. noncompliant-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-noncompliant
spec:
containers:
- name: nginx
image: nginx
$ kubectl apply -f noncompliant-pod.yaml
Error from server ([repo-is-kuar-demo] container <nginx> has an invalid image
repo <nginx>, allowed repos are ["gcr.io/kuar-demo/"]): error when creating
"noncompliant-pod.yaml": admission webhook "validation.gatekeeper.sh" denied
the request: [repo-is-kuar-demo] container <nginx> has an invalid image
repo <nginx>, allowed repos are ["gcr.io/kuar-demo/"]
示例 20-4 显示向用户返回错误,并详细说明为什么未创建资源以及如何解决问题。集群管理员可以在约束模板中配置错误消息。
注意
如果您的约束范围是 Pod,并且您创建生成 Pod 的资源(例如 ReplicaSets),Gatekeeper 将返回一个错误。然而,它不会返回给您,而是返回给尝试创建 Pod 的控制器。要查看这些错误消息,请查看相关资源的事件日志。
理解约束模板
现在我们已经演示了一个经典示例,请仔细查看示例 20-1 中的约束模板,该模板列出了允许在 Kubernetes 资源中使用的容器存储库列表。
此约束模板具有作为 Gatekeeper 专用自定义资源一部分的apiVersion和kind。在spec部分下,您将看到名称K8sAllowedRepos:请记住此名称,因为在创建约束时,您将使用它作为约束类型。您还将看到一个模式,该模式定义了供集群管理员配置的字符串数组。这通过提供允许的容器注册表列表来完成。它还包含原始的 Rego 策略定义(在target部分下)。此策略评估容器和 initContainers,以确保容器存储库名称以约束提供的值开头。如果违反策略,则在msg部分定义的消息将发送回用户。
创建约束
要实例化策略,您必须创建一个约束,提供模板所需的参数。可能会有许多与特定约束模板种类匹配的约束。让我们仔细查看我们在示例 20-2 中使用的约束,该约束仅允许来源于gcr.io/kuar-demo/的容器镜像。
注意,这些限制是基于“K8sAllowedRepos”类型的约束,这是作为约束模板的一部分定义的。它还定义了一个enforcementAction为“deny”,意味着不符合规范的资源将被拒绝。 enforcementAction还接受“dryrun”和“warn”: “dryrun”使用审计功能来测试策略并验证其影响; “warn”将警告发送回用户并附带消息,但允许他们创建或更新。 match部分定义了此约束的范围,即默认命名空间中的所有 Pod。最后,parameters部分是必需的,以满足约束模板(字符串数组)。以下演示了当enforcementAction设置为“warn”时用户体验:
$ kubectl apply -f noncompliant-pod.yaml
Warning: [repo-is-kuar-demo] container <nginx> has an invalid image repo...
pod/nginx-noncompliant created
警告
约束仅在资源创建和更新事件上执行。如果您已经在集群上运行工作负载,则 Gatekeeper 不会重新评估它们,直到发生创建或更新事件。
这里有一个现实世界的例子来演示:假设您创建了一个策略,仅允许来自特定仓库的容器。所有在集群上已运行的工作负载将继续运行。如果您将工作负载 Deployment 从 1 扩展到 2,ReplicaSet 将尝试创建另一个 Pod。如果该 Pod 没有来自允许的存储库的容器,则将被拒绝。在将enforcementAction设置为“deny”之前,将其设置为“dryrun”并进行审计以确认任何策略违规都是已知的,这一点非常重要。
审计
能够对新资源实施策略只是策略和治理故事的一部分。策略经常会随时间变化,您还可以使用 Gatekeeper 确认当前部署的一切是否仍然符合规定。此外,您可能已经拥有一个充满服务的集群,并希望安装 Gatekeeper 以使这些资源符合规定。Gatekeeper 的审计功能允许集群管理员获取集群中当前非符合规定的资源列表。
要演示审计的工作原理,让我们看一个例子。我们将更新repo-is-kuar-demo的约束,使enforcementAction动作为“dryrun”(如示例 20-5 所示)。这将允许用户创建不符合规定的资源。然后,我们将使用审计确定哪些资源不符合规定。
示例 20-5. allowedrepos-constraint-dryrun.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: repo-is-kuar-demo
spec:
enforcementAction: dryrun
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "default"
parameters:
repos:
- "gcr.io/kuar-demo/"
通过运行以下命令更新约束:
$ kubectl apply -f allowedrepos-constraint-dryrun.yaml
k8sallowedrepos.constraints.gatekeeper.sh/repo-is-kuar-demo configured
使用以下命令创建不符合规定的 Pod:
$ kubectl apply -f noncompliant-pod.yaml
pod/nginx-noncompliant created
要对给定约束的非符合资源列表进行审计,请运行kubectl get constraint命令,并指定要将输出格式设置为 YAML,如下所示:
$ kubectl get constraint repo-is-kuar-demo -o yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
...
spec:
enforcementAction: dryrun
match:
kinds:
- apiGroups:
- ""
kinds:
- Pod
namespaces:
- default
parameters:
repos:
- gcr.io/kuar-demo/
status:
auditTimestamp: "2021-07-14T20:05:38Z"
...
totalViolations: 1
violations:
- enforcementAction: dryrun
kind: Pod
message: container <nginx> has an invalid image repo <nginx>, allowed repos
are ["gcr.io/kuar-demo/"]
name: nginx-noncompliant
namespace: default
在status部分下,您可以看到auditTimestamp,这是上次运行审计的时间。totalViolations列出了违反此约束的资源数量。violations部分列出了违规情况。我们可以看到 nginx-noncompliant Pod 存在违规,并显示了详细的消息。
注意
使用enforcementAction约束为“dryrun”与审计是确认您的策略产生预期影响的强大方式。它还创建了一个将资源带入符合规定的工作流程。
Mutation
到目前为止,我们已经讨论了如何使用约束来验证资源是否符合规定。那么如何修改资源使其符合规定呢?这通过 Gatekeeper 中的 Mutation 功能来处理。在本章的前面部分,我们讨论了两种不同类型的入场网钩,即 Mutation 和 Validation。默认情况下,Gatekeeper 仅部署为验证入场网钩,但可以配置为操作为 Mutation 入场网钩。
注意
Gatekeeper 中的 Mutation 功能处于 beta 状态,可能会发生变化。我们分享它们以展示 Gatekeeper 即将推出的功能。本章中的安装步骤不涵盖启用 Mutation。请参考 Gatekeeper 项目以获取有关启用 Mutation的更多信息。
让我们通过一个示例来展示 Mutation 的威力。在本示例中,我们将在所有 Pod 上将imagePullPolicy设置为“Always”。我们将假设 Gatekeeper 已正确配置以支持 Mutation。示例 20-6 定义了一个 Mutation 分配,该分配匹配除“system”命名空间之外的所有 Pod,并将imagePullPolicy的值设置为“Always”。
示例 20-6. imagepullpolicyalways-mutation.yaml
apiVersion: mutations.gatekeeper.sh/v1alpha1
kind: Assign
metadata:
name: demo-image-pull-policy
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
kinds:
- apiGroups: ["*"]
kinds: ["Pod"]
excludedNamespaces: ["system"]
location: "spec.containers[name:*].imagePullPolicy"
parameters:
assign:
value: Always
创建 Mutation 分配:
$ kubectl apply -f imagepullpolicyalways-mutation.yaml
assign.mutations.gatekeeper.sh/demo-image-pull-policy created
现在创建一个 Pod。此 Pod 未显式设置 imagePullPolicy,因此默认情况下此字段设置为“IfNotPresent”。但是,我们期望 Gatekeeper 将此字段变更为“Always”:
$ kubectl apply -f compliant-pod.yaml
pod/kuard created
通过运行以下内容验证 imagePullPolicy 是否已成功变更为“Always”:
$ kubectl get pods kuard -o=jsonpath="{.spec.containers[0].imagePullPolicy}"
Always
注
变更准入发生在验证准入之前,因此创建验证所期望的变更的约束,应用于特定资源。
使用以下命令删除 Pod:
$ kubectl delete -f compliant-pod.yaml
pod/kuard deleted
使用以下命令删除变更分配:
$ kubectl delete -f imagepullpolicyalways-mutation.yaml
assign.mutations.gatekeeper.sh/demo-image-pull-policy deleted
与验证不同,变更提供了一种自动修复非符合资源的方式,代表群集管理员操作。
数据复制
在编写约束时,您可能希望比较一个字段的值与另一个资源中字段的值。可能需要执行此操作的具体示例是确保整个群集中入口主机名唯一。默认情况下,Gatekeeper 只能评估当前资源内的字段:如果需要跨资源比较来满足策略,则必须进行配置。Gatekeeper 可配置为将特定资源缓存到 Open Policy Agent 中,以允许跨资源比较。示例 20-7 中的资源配置 Gatekeeper 以缓存 Namespace 和 Pod 资源。
示例 20-7. config-sync.yaml
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: "gatekeeper-system"
spec:
sync:
syncOnly:
- group: ""
version: "v1"
kind: "Namespace"
- group: ""
version: "v1"
kind: "Pod"
注
您应仅缓存执行策略评估所需的特定资源。在 OPA 中缓存数百或数千个资源将需要更多内存,并可能具有安全性影响。
示例 20-8 中的约束模板演示了如何在 Rego 部分中比较某些内容(在本例中为唯一的入口主机名)。具体而言,“data.inventory” 指的是缓存资源,而不是从 Kubernetes API 服务器发送到评估的“input”资源,作为准入流程的一部分。此示例基于Gatekeeper 策略库。
示例 20-8. uniqueingresshost-constraint-template.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8suniqueingresshost
annotations:
description: Requires all Ingress hosts to be unique.
spec:
crd:
spec:
names:
kind: K8sUniqueIngressHost
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8suniqueingresshost
identical(obj, review) {
obj.metadata.namespace == review.object.metadata.namespace
obj.metadata.name == review.object.metadata.name
}
violation[{"msg": msg}] {
input.review.kind.kind == "Ingress"
re_match("^(extensions|networking.k8s.io)$", input.review.kind.group)
host := input.review.object.spec.rules[_].host
other := data.inventory.namespace[ns][otherapiversion]["Ingress"][name]
re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion)
other.spec.rules[_].host == host
not identical(other, input.review)
msg := sprintf("ingress host conflicts with an existing ingress <%v>"...
}
数据复制是一个强大的工具,允许您跨 Kubernetes 资源进行比较。我们建议仅在需要此功能的策略下配置它。如果使用它,请仅将其限定于相关资源。
指标
Gatekeeper 以 Prometheus 格式发出指标,以实现持续资源符合性监控。您可以查看有关 Gatekeeper 总体健康状况的简单指标,例如约束数、约束模板数以及发送给 Gatekeeper 的请求数。
此外,还提供有关策略符合性和治理的详细信息:
-
审计违规总数
-
按
enforcementAction分类的约束数 -
审计持续时间
注
完全自动化策略和治理过程是理想目标,因此强烈建议您从外部监控系统监视 Gatekeeper,并根据资源符合性设置警报。
策略库
Gatekeeper 项目的核心理念之一是创建可在组织之间共享的可重用策略库。能够共享策略可以减少模板化的策略工作,使集群管理员能够专注于应用策略而不是编写它们。Gatekeeper 项目拥有一个很棒的 策略库。它包含一个通用库,其中包含最常见的策略,以及一个 pod-security-policy 库,该库模拟了 PodSecurityPolicy API 作为 Gatekeeper 策略的能力。这个库的好处在于它持续扩展并且是开源的,因此请随意贡献您编写的任何策略。
摘要
在本章中,您了解了策略和治理的重要性,特别是在越来越多的资源部署到 Kubernetes 上时。我们介绍了 Gatekeeper 项目,这是一个基于 Open Policy Agent 构建的 Kubernetes 本地策略控制器,并向您展示了如何使用它来满足您的策略和治理需求。从编写策略到审核,您现在具备了满足合规需求的技能。
第二十一章:多集群应用部署
本书共有二十章,应该明白 Kubernetes 可以是一个复杂的主题,当然,希望如果您已经读到这里,它比起初要清晰些。考虑到在单个 Kubernetes 集群中构建和运行应用程序的复杂性,为什么要增加设计和部署应用程序到多个集群的复杂性呢?
事实上,现实世界的需求意味着大多数应用程序需要进行多集群应用部署。这有许多原因,很可能您的应用程序至少符合其中一个要求。
第一个要求是冗余和弹性的需求。无论是在云端还是本地,单个数据中心通常是一个单一故障域。无论是猎人使用光纤电缆作为靶子的练习,还是冰风暴导致的停电,或者仅仅是软件发布的失败,部署到单个位置的任何应用程序都可能完全失败,使用户无法获得任何补救措施。在许多情况下,单个 Kubernetes 集群都与单个位置绑定,因此是一个单一故障域。
在某些情况下,特别是在云环境中,Kubernetes 集群被设计为区域性。区域性集群跨越多个独立区域,因此对于之前描述的基础设施问题具有弹性。因此很容易认为这样的区域性集群足以保证弹性,除了 Kubernetes 本身可能成为单点故障的事实。任何单个 Kubernetes 集群都与特定版本的 Kubernetes 绑定(例如,1.21.3),升级集群可能会导致应用程序出现问题。有时 Kubernetes 会弃用 API 或更改这些 API 的行为。这些变更不频繁,Kubernetes 社区会提前进行沟通确保变更顺利进行。此外,尽管经过大量测试,但偶尔仍会在发布版本中引入错误。尽管任何一个问题影响您的应用程序的可能性不大,但在大多数应用程序的生命周期(几年)内,您的应用程序很可能会受到某种程度的影响。对于大多数应用程序来说,这是不可接受的风险。
除了弹性要求外,多集群部署的另一个强大驱动因素是对区域关联的业务或应用需求。例如,游戏服务器需要靠近玩家以减少网络延迟并提高游戏体验。其他应用可能受到法律或监管要求的限制,要求数据位于特定的地理区域内。由于任何 Kubernetes 集群都与特定位置绑定,这些应用部署到特定地理位置的需求意味着应用必须跨越多个集群。
最后,尽管在单个集群中有多种隔离用户的方法(例如命名空间、RBAC、节点池——为不同能力或工作负载组织的 Kubernetes 节点集合),但 Kubernetes 集群仍然基本上是一个单一的合作空间。对于一些团队和产品来说,不同团队甚至会因为意外影响他们的应用的风险不值得,他们宁愿承担管理多个集群的复杂性。
到了这一点,你可以看到,无论你的应用程序如何,很可能现在或在不久的将来,你的应用程序都需要跨多个集群。本章的其余部分将帮助你理解如何实现这一点。
在你开始之前
在考虑迁移到多集群部署之前,确保单个集群部署具备正确的基础架构至关重要。每个人对他们的设置都有一份必须做的清单,但是这些捷径和问题在多集群部署中被放大。同样地,在拥有 10 个集群时,修复基础设施中的问题会变得十分困难。此外,如果增加额外的集群需要付出显著的额外工作,你可能会抗拒增加额外的集群,尽管(因为前面提到的种种原因)这对你的应用来说是正确的做法。
当我们说“基础架构”时,我们指的是什么?正确的自动化是最重要的部分。重要的是,这不仅包括部署应用程序的自动化,还包括创建和管理集群本身的自动化。当你只有一个集群时,它从定义上来说是一致的。然而,当你增加集群时,你增加了集群中各个组件版本不一致的可能性。你可能会拥有不同版本的 Kubernetes、不同版本的监控和日志代理,甚至是容器运行时的基本差异。所有这些变化都会使你的生活更加困难。基础设施的差异使得你的系统变得“更加奇怪”。在一个集群中获得的知识并不能转移到其他集群,有时因为这种差异性,问题似乎会在某些地方随机发生。保持稳定基础的一个最重要的部分就是确保所有集群的一致性。
实现这种一致性的唯一途径是自动化。你可能会想,“我总是以这种方式创建集群”,但经验告诉我们,这并不正确。下一章将详细讨论基础设施即代码对于管理你的应用程序的价值,但同样适用于管理你的集群。不要使用 GUI 或 CLI 工具来创建你的集群。起初通过源代码控制和 CI/CD 推送所有更改可能看起来很麻烦,但稳定的基础会带来显著的回报。
部署到您的集群中的基础组件也是如此。这些组件包括监控、日志记录和安全扫描器,在部署任何应用程序之前都必须存在。这些工具还需要使用 Helm 等基础设施即代码工具进行管理,并使用自动化进行部署。
超越集群形状的内容,还有其他必要的一致性方面。首先是为所有集群使用单一身份系统。虽然 Kubernetes 支持简单的基于证书的身份验证,但我们强烈建议使用与全球身份提供者(如 Azure Active Directory 或任何其他支持 OpenID Connect 的身份提供者)集成。确保每个人访问所有集群时都使用相同的身份是维护安全最佳实践并避免危险行为(如共享证书)的关键部分。此外,大多数这些身份提供者还提供额外的安全控制,如双因素身份验证,可以增强集群的安全性。
就像身份验证一样,确保集群的一致访问控制也非常关键。在大多数云平台中,这意味着使用基于云的 RBAC,在这种情况下,RBAC 角色和绑定存储在中央云位置,而不是在集群本身。在单一位置定义 RBAC 可以防止诸如在某个集群中留下权限或未能向某个单一集群添加权限等错误。不幸的是,如果要为本地集群定义 RBAC,则情况比身份验证要复杂得多。有一些解决方案(例如,用于 Kubernetes 的 Azure Arc)可以为本地集群提供 RBAC,但如果您的环境中没有此类服务,则在源代码控制中定义 RBAC 并使用基础设施即代码将规则应用于所有集群可以确保跨整个部署中应用一致的特权。
同样地,当您考虑为集群定义策略时,定义这些策略并在单一位置上具有用于查看所有集群符合状态的单一仪表板非常关键。与 RBAC 一样,此类全局服务通常通过您的云提供商提供,但对于本地部署,选项有限。同样可以使用基础设施即代码工具来帮助填补此差距,并确保您可以在单一位置定义您的策略。
就像设置正确的单元测试和构建基础设施对应用程序开发至关重要一样,为管理多个 Kubernetes 集群设置正确的基础设施为在广泛的基础设施群中稳定部署应用程序奠定了基础。在接下来的部分中,我们将讨论如何构建您的应用程序以在多集群环境中成功运行。
从顶部开始使用负载平衡方法
一旦开始考虑将应用部署到多个位置,就变得必要考虑用户如何访问它。通常通过域名来实现这一点(例如,my.company.com)。虽然我们将花费大量时间讨论如何构建你的应用程序以在多个位置运行,但更重要的起点是如何实施访问。这是因为显然使人们能够使用你的应用程序是至关重要的,但也因为设计人们如何访问你的应用程序可以提高你在意外负载或故障情况下快速响应和重新路由流量的能力。
访问你的应用程序始于一个域名。这意味着你的多集群负载均衡策略的开始是一个 DNS 查询。这个 DNS 查询是你负载均衡策略的第一个选择。在许多传统的负载均衡方法中,这个 DNS 查询被用于将流量路由到特定的位置。这通常被称为“GeoDNS”。在 GeoDNS 中,DNS 查询返回的 IP 地址与客户端的物理位置相关联。IP 地址通常是离客户端最近的区域集群。
虽然 GeoDNS 在许多应用中仍然流行,并且对于本地应用可能是唯一可行的方法,但它有一些缺点。第一个缺点是 DNS 在互联网的各个地方都有缓存,虽然你可以设置 DNS 查询的生存时间(TTL),但在追求更高性能时,许多地方会忽略这个 TTL。在稳定状态下运行时,这种缓存并不是什么大问题,因为 DNS 通常是相当稳定的,无论 TTL 是多少。然而,当你需要将流量从一个集群移动到另一个集群时,比如响应特定数据中心的故障时,这就成为一个非常大的问题。在这种紧急情况下,DNS 查询被缓存的事实可能会显著延长停机的持续时间和影响。此外,由于 GeoDNS 根据客户端的 IP 地址猜测你的物理位置,当许多不同的客户端从同一防火墙 IP 地址出口其流量时,尽管它们位于许多不同的地理位置,GeoDNS 经常会感到困惑并猜测错误的位置。
用于选择你的集群的另一种替代方法是一种称为任播的负载均衡技术。使用任播网络,单个静态 IP 地址通过核心路由协议从互联网的多个位置进行广告。传统上我们认为 IP 地址映射到单个机器,但是使用任播网络,IP 地址实际上是一个虚拟 IP 地址,根据你的网络位置被路由到不同的位置。你的流量根据网络性能的距离而不是地理距离被路由到“最近”的位置。任播网络通常能产生更好的结果,但并不总是在所有环境中都可用。
在设计负载平衡时的最后一个考虑因素是负载平衡是在 TCP 层还是 HTTP 层进行的。到目前为止,我们只讨论了 TCP 级别的负载平衡,但对于基于 Web 的应用程序来说,在 HTTP 层进行负载平衡有显著的好处。如果您正在编写基于 HTTP 的应用程序(大多数应用程序都是这样的),那么使用全局 HTTP 感知负载均衡器使您能够了解更多客户端通信的细节。例如,您可以根据浏览器中设置的 cookie 做出负载平衡决策。此外,了解协议的负载均衡器可以做出更智能的路由决策,因为它看到每个 HTTP 请求,而不仅仅是通过 TCP 连接的字节流。
无论您选择哪种方法,最终您的服务位置都将从全局 DNS 端点映射到代表服务入口点的一组区域 IP 地址。这些 IP 地址通常是您在本书前几章中了解到的 Kubernetes 服务或入口资源的 IP 地址。一旦用户流量到达该端点,它将根据您的应用程序设计流经您的集群。
构建多集群应用程序
一旦您解决了负载平衡问题,设计多集群应用程序的下一个挑战是考虑状态。理想情况下,您的应用程序不需要状态,或者所有状态都是只读的。在这种情况下,您几乎不需要做任何事情来支持多个集群部署。您的应用程序可以单独部署到每个集群中,顶部添加一个负载均衡器,您的多集群部署就完成了。不幸的是,对于大多数应用程序来说,存在必须以一致方式在应用程序副本之间管理的状态。如果您没有正确处理状态,您的用户将得到一个令人困惑且有缺陷的体验。
要理解复制状态如何影响用户体验,让我们以一个简单的零售商店为例。很明显,如果您只在多个集群中的一个中存储客户订单,当他们的请求移到不同的区域时,客户可能会无法看到他们的订单,无论是因为负载平衡还是因为他们实际上移动了地理位置。因此,用户的状态需要在各个区域之间复制。复制方法也可能影响客户体验,尽管这可能不那么明显。复制数据和客户体验的挑战被这个问题简洁地概括为:“我能读取我自己的写入吗?”看起来答案显而易见应该是“是的”,但实现这一点比看起来更难。例如,考虑一个在他们的电脑上下订单,然后立即在他们的手机上查看订单的客户。他们可能从完全不同的网络访问您的应用程序,因此可能会登陆到完全不同的集群上。用户关于他们能否查看刚刚下的订单的期望是一致性的一个例子。
一致性决定了您如何考虑复制数据。我们假设我们希望我们的数据是一致的;也就是说,无论我们从哪里读取数据,我们都能读取到相同的数据。但是,时间是一个复杂的因素:我们的数据必须多快才能保持一致?当数据不一致时,我们会得到任何错误指示吗?一致性有两种基本模型:强一致性保证了写操作直到成功复制之前都不会成功,而最终一致性则是写操作总是立即成功,并且只有在以后的某个时间点才保证成功复制。一些系统还允许客户端根据请求选择它们的一致性需求。例如,Azure Cosmos DB 实现了有界一致性,在这种最终一致性系统中对过期数据的一些保证。Google Cloud Spanner 使客户端能够指定他们愿意容忍旧数据读取,以换取更好的性能。
看起来每个人都会选择强一致性,因为它显然更容易理解,因为数据在任何地方都是相同的。但是强一致性是有代价的。在写入时保证复制需要更多的工作,当无法复制时,许多写入将失败。强一致性更昂贵,相对于最终一致性可以支持更少的并发事务。最终一致性更便宜,并且可以支持更高的写入负载,但对于应用程序开发者来说更为复杂,可能会向最终用户暴露一些边缘条件。许多存储系统仅支持单一并发模型。那些支持多个并发模型的存储系统要求在创建存储系统时指定。您选择的并发模型对应用程序的设计有重大影响,并且难以更改。因此,在为多个环境设计应用程序之前,选择一致性模型是一个重要的第一步。
注意
部署和管理复制的有状态存储是一项复杂的任务,需要一个专门的团队具有领域专业知识来设置、维护和监控。您应该强烈考虑使用基于云的存储来进行复制数据存储,这样负担将由云提供商庞大的团队深度承担,而不是您自己的团队。在本地环境中,您还可以将存储的支持转移到专注于运行您选择的存储解决方案的公司。只有在大规模时,投资于构建自己的团队来管理存储才有意义。
一旦确定了存储层,下一步是构建应用程序设计。
复制的孤立体:最简单的跨区域模型
将您的应用程序简单复制到多个集群和多个区域的最简单方法只是将您的应用程序复制到每个区域。您的应用程序的每个实例都是一个完全相同的克隆,无论在哪个集群中运行,它看起来完全相同。由于顶部有一个负载均衡器分布客户请求,并且您已经在需要状态的地方实现了数据复制,您的应用程序不需要太多更改来支持这种模型。根据您选择的数据一致性模型,您需要处理的事实是数据在区域之间可能不会被快速复制,但是,特别是如果您选择强一致性,这不需要进行主要的应用程序重构。
当您以这种方式设计应用程序时,每个区域都是其自身的孤立体。它需要的所有数据都存在于该区域内,一旦请求进入该区域,它将完全由运行在那个集群中的容器提供服务。这在减少复杂性方面具有显著的好处,但像通常情况一样,这也是以效率为代价。
要理解分离式方法如何影响效率,请考虑一个分布在世界各地大量地理区域的应用程序,以便为其用户提供非常低的延迟。世界的现实是,某些地理区域有大量人口,而某些地区则人口较少。如果应用程序中每个簇的每个分离式都完全相同,那么每个分离式都必须大小适应最大的地理区域的需求。这样做的结果是,地区簇中应用程序的大多数副本都被大量超量配置,因此应用程序的成本效率较低。这种多余成本的明显解决方案是减少在较小地理区域使用的资源大小。虽然调整应用程序大小可能看起来很容易,但由于瓶颈或其他要求(例如,至少保持三个副本),这并不总是可行。
特别是将现有应用程序从单一集群转移到多集群时,复制的分离式设计是最简单的方法,但值得理解的是,它会带来成本,这些成本可能最初可以承受,但最终会要求重新设计您的应用程序。
分片:区域数据
当您的应用程序扩展时,您可能会遇到的一个痛点是,采用区域分离方法全球复制所有数据变得越来越昂贵,也越来越浪费。虽然为了可靠性复制数据是一件好事,但不太可能所有应用程序数据都需要在部署应用程序的每个集群中共同存在。大多数用户只会从少数几个地理区域访问您的应用程序。
此外,随着您的应用程序在全球范围内的增长,您可能会遇到关于数据本地性的法规和其他法律要求。根据用户的国籍或其他考虑因素,可能会存在关于存储用户数据位置的外部限制。这些要求的结合意味着最终您将需要考虑区域数据分片。在不同区域分片您的数据意味着您的应用程序在所有集群中并非所有数据都存在,这(显然)会影响您的应用程序设计。
以此作为例子,想象一下我们的应用程序部署到了六个区域集群(A、B、C、D、E、F)。我们将应用程序的数据集拆分为三个子集或片段(1、2、3)。
我们的数据片段部署可能如下所示:
| A | B | C | D | E | F | |
|---|---|---|---|---|---|---|
| 1 | ✓ | - | - | ✓ | - | - |
| 2 | - | ✓ | - | - | ✓ | - |
| 3 | - | - | ✓ | - | - | ✓ |
每个分片在两个区域中都有冗余性,但每个区域集群只能提供三分之一的数据。这意味着每当您需要访问数据时,您必须为服务添加额外的路由层。路由层负责确定请求是否需要发送到本地或跨区域数据分片。
尽管将这种数据路由作为链接到主应用程序的客户端库的一部分实现可能很诱人,但我们强烈建议将数据路由作为单独的微服务构建。引入新的微服务可能会增加复杂性,但实际上它引入了一个简化事物的抽象层。而不是您应用程序中的每个服务都担心数据路由,您只需一个服务封装这些关注点,而其他服务只需访问数据服务。将应用程序分解为独立的微服务提供了在多集群环境中显著灵活性。
更好的灵活性:微服务路由
当我们讨论多集群应用程序开发的区域隔离方法时,我们举了一个例子,说明它可能会降低部署的多集群应用程序的成本效率。但灵活性也会受到其他影响。在创建这种隔离时,您正在以更大规模创建容器和 Kubernetes 试图打破的单块体。此外,您正在强迫应用程序内的每个微服务同时扩展到相同数量的区域。
如果您的应用程序小而集中,这可能有道理,但随着服务变得越来越大,特别是当它们可能开始在多个应用程序之间共享时,单片式的多集群方法开始显著影响您的灵活性。如果集群是部署单位,并且所有的 CI/CD 都与该集群相关联,那么即使这种匹配不合适,您也会强迫每个团队遵循相同的部署流程和时间表。
以具体示例来说,假设您有一个部署到三十个集群的非常大的应用程序,以及正在开发中的一个小型新应用程序。强迫小团队立即达到大型应用程序的规模是不合理的,但如果在应用设计上过于死板,这可能正是会发生的事情。
更好的方法是将应用程序中的每个微服务在应用程序设计上视为一个面向公共的服务。虽然可能永远不会真正期望其成为公共服务,但它应该像前面部分所描述的那样拥有自己的全局负载均衡器,并且应该管理其自身的数据复制服务。在所有意图和目的上,不同的微服务应该彼此独立。当一个服务调用另一个服务时,其负载在与外部负载相同的方式下平衡。有了这种抽象,每个团队可以独立扩展和部署他们的多集群服务,就像他们在单个集群内做的那样。
当然,为应用程序中的每个微服务都这样做可能会给您的团队带来重大负担,并且可能会通过每个服务的负载均衡器维护以及可能的跨区域网络流量增加成本。像软件设计中的一切一样,复杂性与性能之间存在权衡,您需要确定适合您的应用程序在哪些地方增加服务边界的隔离,并且在何处将服务组合成复制的隔离空间是有意义的。就像单集群上下文中的微服务一样,这种设计很可能随着应用程序的变化和增长而变化和适应。期望(并设计)这种流动性将有助于确保您的应用程序可以适应而无需进行大规模的重构。
概要
尽管将您的应用程序部署到多个集群会增加复杂性,但现实世界中的需求和用户期望使这种复杂性对于大多数您构建的应用程序都是必要的。从头开始设计您的应用程序和基础设施以支持多集群应用程序部署将极大地增强您的应用程序的可靠性,并显著降低应用程序增长时重构的概率。多集群部署的最重要部分之一是管理应用程序的配置和部署到集群。无论您的应用程序是区域性的还是多集群的,下一章将帮助确保您能够快速和可靠地部署它。
第二十二章:组织您的应用程序
在本书中,我们描述了构建在 Kubernetes 之上的应用程序的各种组件。我们描述了如何将程序封装为容器,将这些容器放置在 Pod 中,使用 ReplicaSets 复制这些 Pod,并使用 Deployments 进行部署。我们甚至描述了如何部署有状态和真实世界的应用程序,将这些对象收集到一个单一的分布式系统中。但是,我们并没有讨论如何实际操作这样的应用程序。您如何布置、共享、管理和更新构成您应用程序的各种配置?这就是本章的主题。
引导我们的原则
在深入探讨如何构建您的应用程序结构的具体细节之前,考虑一下驱动这种结构的目标是值得的。显然,可靠性和敏捷性是在 Kubernetes 中开发云原生应用程序的一般目标,但这与您如何设计应用程序的维护和部署有什么关系?以下部分描述了可以指导您设计最适合这些目标的结构的三个原则。这些原则是:
-
将文件系统视为真实性的源头
-
进行代码审查以确保变更的质量
-
使用功能标志来分阶段部署和回滚
文件系统作为真相的源头
当您开始探索 Kubernetes 时,就像我们在本书的开头所做的那样,通常会以命令式的方式与其进行交互。您会运行诸如 kubectl run 或 kubectl edit 的命令来创建和修改运行在集群中的 Pod 或其他对象。即使在我们开始探索如何编写和使用 YAML 文件时,这也是以一种临时的方式呈现的,好像文件本身只是修改集群状态道路上的一个途径。实际上,在一个真正生产化的应用程序中,相反的情况应该是真实的。
与其将集群的状态——存储在 etcd 中的数据——视为真实性的源头,不如将 YAML 对象的文件系统视为应用程序的真实性的最佳源头。部署到您的 Kubernetes 集群中的 API 对象然后是文件系统中存储的真实性的反映。
这是正确观点的众多理由。首要的是,它在很大程度上使您能够将您的集群视为不可变基础设施。随着我们进入云原生架构,我们越来越习惯于将我们的应用程序及其容器视为不可变基础设施,但将集群视为这样的基础设施则较少见。然而,将我们的应用程序迁移到不可变基础设施的相同理由也适用于我们的集群。如果您的集群是通过随意应用从互联网下载的随机 YAML 文件制造的雪花,那么它与通过命令式 bash 脚本构建的虚拟机一样危险。
此外,通过文件系统管理集群状态使得与多个团队成员协作变得非常容易。源代码控制系统被广泛理解,并且可以轻松地使多人同时编辑集群状态,同时使冲突(以及解决这些冲突)对每个人都清晰可见。
注意
所有部署到 Kubernetes 的应用程序都应首先在文件系统中的文件中进行描述,这绝对是一个第一原则。然后,实际的 API 对象只是这个文件系统投射到特定集群的一部分。
代码审查的作用
不久以前,对应用程序源代码进行代码审查还是一个新颖的想法。但现在很明显,在将代码提交到应用程序之前,多人审查代码是生产高质量、可靠代码的最佳实践。
因此令人惊讶的是,对于用于部署这些应用程序的配置来说,同样的情况相对较少。代码审查的所有理由都直接适用于应用程序配置。但是当你考虑它时,审查这些配置对于可靠部署服务同样至关重要是显而易见的。根据我们的经验,大多数服务中断是自我造成的,由于意外后果、拼写错误或其他简单错误。确保至少有两个人查看任何配置更改,显著降低了此类错误发生的概率。
注意
我们应用程序布局的第二个原则是,它必须促进对合并到代表我们集群真实源的文件集的每个更改的审查。
功能门
一旦你的应用源代码和部署配置文件都在源代码控制中,其中一个最常见的问题是这些仓库如何相互关联。你应该将应用程序源代码和配置文件放在同一个仓库中吗?对于小项目来说这可能行得通,但在大项目中通常更合理地将它们分开。即使是同一组人负责构建和部署应用程序,构建者和部署者的视角有足够大的不同,以至于这种关注点的分离是有道理的。
如果是这种情况,那么如何在源代码控制中开发新功能并将这些功能部署到生产环境中呢?这就是功能门控的重要作用所在。
思想是,当开发某个新功能时,该开发完全在功能标志或门后进行。这个门看起来像这样:
if (featureFlags.myFlag) {
// Feature implementation goes here
}
这种方法有许多好处。首先,它允许团队在功能准备好发货之前长时间提交到生产分支。这使得功能开发能够与仓库的HEAD更紧密地对齐,因此可以避免长期分支的可怕合并冲突。
在功能标志背后工作还意味着启用功能只需进行配置更改即可激活标志。这使得在生产环境中明确了哪些内容发生了变化,并且如果功能激活导致问题,回滚功能激活也非常简单。
使用功能标志既简化了调试,又确保禁用功能不需要二进制回滚到较旧版本的代码,从而删除所有由新版本带来的错误修复和其他改进。
注意
应用程序布局的第三个原则是代码默认存储在源代码控制中,通过功能标志关闭,并且只能通过代码审查的配置文件更改来激活。
在源代码控制中管理您的应用程序
现在我们已确定文件系统应该代表集群的真实来源,下一个重要问题是如何实际布置文件系统中的文件。显然,文件系统包含层次目录,并且源代码控制系统添加了标签和分支等概念,因此本节描述如何将它们结合在一起以表示和管理您的应用程序。
文件系统布局
本节描述如何为单个集群设置应用程序实例的布局。在后续章节中,我们将描述如何为多个实例参数化此布局。在开始时正确地组织这一点非常重要。就像修改源代码控制中包的布局一样,修改部署配置后进行的复杂且昂贵的重构可能永远都不会完成。
组织应用程序的第一个基数是语义组件或层(例如前端或批处理工作队列)。尽管在早期可能看起来有些过度,因为一个团队管理所有这些组件,但这为团队扩展奠定了基础——最终,不同的团队(或子团队)可能负责每个组件。
因此,对于一个使用两个服务的前端的应用程序,文件系统可能如下所示:
frontend/
service-1/
service-2/
在每个目录中,存储每个应用程序的配置。这些是直接表示集群当前状态的 YAML 文件。通常将服务名称和对象类型包含在同一个文件中非常有用。
注意
虽然 Kubernetes 允许你在同一个文件中创建多个对象的 YAML 文件,但这通常是一个反模式。将多个对象分组放在同一个文件中的唯一好理由是它们在概念上是相同的。在决定将什么内容放入单个 YAML 文件时,考虑类似于定义类或结构体的设计原则。如果将这些对象组合在一起不形成一个单一的概念,它们可能不应该在同一个文件中。
因此,延伸我们之前的例子,文件系统可能如下所示:
frontend/
frontend-deployment.yaml
frontend-service.yaml
frontend-ingress.yaml
service-1/
service-1-deployment.yaml
service-1-service.yaml
service-1-configmap.yaml
...
管理周期版本
那么如何管理版本?能够回顾和查看应用程序之前的部署情况非常有用。同样,能够在保持稳定发布配置的同时将配置向前迭代也非常有用。
因此,能够同时存储和维护配置的多个修订版本非常方便。我们在这里列出的文件和版本控制系统有两种不同的方法可以使用。第一种是使用标签、分支和源控制功能。这种方法很方便,因为它与人们在源代码控制中管理修订版本的方式相匹配,并且导致更简化的目录结构。另一种选择是在文件系统内克隆配置,并使用不同修订版本的目录。这样可以非常直观地同时查看配置。
这些方法在管理不同的发布版本方面具有相同的能力,因此最终是在两者之间的美学选择。我们将讨论这两种方法,让您或您的团队决定哪种更合适。
使用分支和标签进行版本控制
当您使用分支和标签管理配置修订版本时,目录结构不会与前一节中的示例不同。当您准备发布时,您会在配置源控制系统中放置一个源控制标签(例如git tag v1.0)。该标签代表了该版本使用的配置,源控制的HEAD继续向前迭代。
更新发布配置略微复杂,但方法模拟了源控制中的操作。首先,您将更改提交到仓库的HEAD。然后,您在v1.0标签处创建一个名为v1的新分支。您将所需的更改挑选到发布分支上(git cherry-pick *<edit>*),最后,您使用v1.1标签标记此分支以指示一个新的点发布。此方法在图 22-1 中有所说明。

图 22-1. 挑选工作流程
注
在将修复内容挑选到发布分支时,一个常见的错误是只将更改挑选到最新的发布中。最好将其挑选到所有活动的发布中,以防需要回滚版本但仍需要此修复。
版本控制与目录
使用文件系统功能的另一种选择是使用源控制功能。在这种方法中,每个版本化的部署都存在于其自己的目录中。例如,您的应用程序文件系统可能如下所示:
frontend/
v1/
frontend-deployment.yaml
frontend-service.yaml
current/
frontend-deployment.yaml
frontend-service.yaml
service-1/
v1/
service-1-deployment.yaml
service-1-service.yaml
v2/
service-1-deployment.yaml
service-1-service.yaml
current/
service-1-deployment.yaml
service-1-service.yaml
...
因此,每个修订版本存在于与发布关联的目录内的并行目录结构中。所有部署都来自HEAD,而不是特定的修订版本或标签。您将向当前目录中的文件添加新配置。
在创建新版本时,您可以复制当前目录以创建与新版本关联的新目录。
当您对发布进行错误修复时,您的拉取请求必须修改所有相关发布目录中的 YAML 文件。这比之前描述的挑选方法稍好,因为在单个更改请求中明确指出正在更新所有相关版本,而不是需要每个版本都进行挑选。
为开发、测试和部署结构化您的应用程序
除了为周期性发布节奏构建应用程序外,您还希望为敏捷开发、质量测试和安全部署构建应用程序。这使得开发人员能够快速地对分布式应用程序进行更改和测试,并安全地将这些更改推向客户。
目标
关于开发和测试,您的应用程序有两个目标。首先,每个开发人员都应能轻松地为应用程序开发新功能。在大多数情况下,开发人员仅在一个组件上工作,但该组件与集群中的所有其他微服务都是相互连接的。因此,为了促进开发,开发人员能够在自己的环境中使用所有服务是至关重要的。
另一个目标是为了在部署前轻松准确地为应用程序进行结构化测试。这对于快速推出功能同时保持高可靠性至关重要。
发布进程
要实现这两个目标,将开发阶段与前述的发布版本阶段相关联是非常重要的。发布的阶段包括:
HEAD
配置的最前沿;最新的更改。
开发
主要是稳定的,但尚未准备部署。适合开发人员用于构建功能。
预发布
测试的开始,除非发现问题,否则不太可能更改。
金丝雀发布
面向用户的第一个真正发布版本,用于测试真实流量中的问题,并让用户有机会测试即将推出的内容。
发布
当前的生产发布版本。
引入开发标签
无论您是使用文件系统还是版本控制来构建发布版本,建模开发阶段的正确方式是通过源代码控制标签。这是因为开发必须跟踪稳定性,仅略微滞后于HEAD。
要引入开发阶段,您需要向源代码控制系统添加一个新的development标签,并使用自动化流程将此标签向前推进。定期,您将通过自动化集成测试测试HEAD。如果这些测试通过,则将development标签向前推进到HEAD。因此,开发人员可以在部署其自己的环境时跟踪最新的更改,但同时可以确保已部署的配置至少通过了有限的冒烟测试。这种方法在图 22-2 中有所体现。

图 22-2. 开发标签工作流程
将各个阶段映射到修订版
或许诱人的是为每个阶段引入一组新的配置,但实际上,每个版本和阶段的每种组合都会造成混乱,这将非常难以理解。相反,正确的做法是引入一个将修订版与阶段进行映射的方法。
无论您是使用文件系统还是源控制修订版来表示不同的配置版本,都可以轻松实现从阶段到修订版的映射。在文件系统情况下,可以使用符号链接将阶段名称映射到修订版:
frontend/
canary/ -> v2/
release/ -> v1/
v1/
frontend-deployment.yaml
...
对于版本控制而言,它只是与适当版本相同修订版的附加标签。
在任一情况下,版本控制都是使用先前描述的过程进行的,并且根据需要将阶段移动到新版本。实际上,这意味着存在两个同时进行的过程:第一个用于生成新的发布版本,第二个用于将发布版本合格化为应用程序生命周期中特定阶段的版本。
使用模板参数化您的应用程序
一旦您拥有环境和阶段的笛卡尔乘积,保持它们完全相同变得不切实际或不可能。然而,努力使环境尽可能相似是很重要的。在不同环境之间的变化和漂移会产生雪花和难以理解的系统。如果您的演示环境与发布环境不同,您真的能相信您在演示环境中运行的负载测试来验证发布吗?为了确保您的环境保持尽可能相似,使用参数化环境非常有用。参数化环境使用模板来处理大部分配置,但是混入一小部分参数以生成最终配置。这种方式,大部分配置包含在共享模板中,而参数化范围有限,并且在一个小的参数文件中维护,便于可视化不同环境之间的差异。
使用 Helm 和模板进行参数化
有各种不同的语言用于创建参数化配置。通常它们将文件分为模板文件,其中包含大部分配置,以及参数文件,可以与模板结合以生成完整的配置。除了参数外,大多数模板语言允许参数具有默认值,如果未指定值,则使用默认值。
以下示例展示了如何使用Helm,这是 Kubernetes 的包管理器,来参数化配置。尽管各种语言的信徒可能会说些不同,但所有参数化语言在很大程度上是相等的,与编程语言一样,你偏爱哪一种很大程度上是个人或团队风格的问题。因此,这里描述的 Helm 模式适用于您选择的任何模板语言。
Helm 模板语言使用“mustache”语法:
metadata:
name: {{ .Release.Name }}-deployment
这表明Release.Name应该用部署的名称替换。
要为此值传递参数,您可以使用名为values.yaml的文件,其内容如下:
Release:
Name: my-release
参数替换后,结果如下:
metadata:
name: my-release-deployment
参数化的文件系统布局
现在你了解如何为你的配置参数化了,那么如何将其应用到文件系统布局中呢?不要将每个部署生命周期阶段都视为指向某个版本的指针,而是将每个部署生命周期阶段视为参数文件和指向特定版本的组合。例如,在基于目录的布局中,可能如下所示:
frontend/
staging/
templates -> ../v2
staging-parameters.yaml
production/
templates -> ../v1
production-parameters.yaml
v1/
frontend-deployment.yaml
frontend-service.yaml
v2/
frontend-deployment.yaml
frontend-service.yaml
...
使用版本控制执行此操作看起来类似,只是每个生命周期阶段的参数保留在配置目录树的根目录下:
frontend/
staging-parameters.yaml
templates/
frontend-deployment.YAML
...
在全球范围内部署您的应用程序
现在,您的应用程序有多个版本在多个部署阶段中运行,配置结构化的最后一步是在全球范围内部署您的应用程序。但不要认为这些方法仅适用于大型应用程序。您可以使用它们从两个不同的区域扩展到全球范围内的十个或数百个区域。在云中,整个区域可能会失败,因此部署到多个区域(以及管理该部署)是满足要求用户需求的唯一方法。
全球部署架构
通常情况下,每个 Kubernetes 集群旨在位于单个区域,并包含应用程序的单个完整部署。因此,应用程序的全球部署由多个不同的 Kubernetes 集群组成,每个集群都有其自己的应用程序配置。描述如何实际构建全球应用程序,特别是涉及数据复制等复杂主题,超出了本章的范围,但我们将描述如何在文件系统中安排应用程序配置。
特定区域的配置在概念上与部署生命周期中的阶段相同。因此,将多个区域添加到配置中等同于添加新的生命周期阶段。例如,不是:
-
开发
-
演练
-
金丝雀
-
生产
您可能会有:
-
开发
-
演练
-
金丝雀
-
东部美国
-
西部美国
-
欧洲
-
亚洲
配置文件系统中的建模看起来像:
frontend/
staging/
templates -> ../v3/
parameters.yaml
eastus/
templates -> ../v1/
parameters.yaml
westus/
templates -> ../v2/
parameters.yaml
...
如果你使用版本控制和标签,文件系统将如下所示:
frontend/
staging-parameters.yaml
eastus-parameters.yaml
westus-parameters.yaml
templates/
frontend-deployment.yaml
...
使用这种结构,您将为每个区域引入一个新标签,并使用该标签下的文件内容部署到该区域。
实施全球部署
现在您已经为世界各地的每个区域配置了配置,问题变成了如何更新这些不同的区域。使用多个区域的主要目标之一是确保非常高的可靠性和正常运行时间。虽然人们可能会认为云和数据中心的宕机是停机的主要原因,但事实上,宕机通常是由新版本的软件发布引起的。因此,构建高可用系统的关键是限制您可能进行的任何更改的影响,或者说“爆炸半径”。因此,在跨多个区域推出版本时,逐个区域谨慎移动、验证并确保信心,显得十分合理。
在全球范围内推出软件通常看起来更像是一个工作流程,而不是单一的声明性更新:您首先将版本更新到最新版本,并在所有区域中进行逐步推进,直到在所有地方都推出为止。但是,您应该如何结构化各个区域,以及在各区域之间进行验证之间应该等待多长时间呢?
注意
您可以使用诸如GitHub Actions之类的工具自动化部署工作流程。它们提供了一种声明性语法来定义您的工作流程,并且也存储在源代码控制中。
要确定在各个区域之间推出的时间间隔,考虑软件的“烟雾平均时间”。这是一个新版本在推出到一个区域后,平均需要多长时间才能发现问题(如果存在问题)。显然,每个问题都是独特的,可能需要不同的时间才能被发现,这就是为什么您要了解平均时间。在规模化管理软件时,这是一种概率而非确定性的业务,因此您希望等待一个使错误概率低到足够让您放心继续向下一个区域推进的时间。例如,两到三倍的平均烟雾时间可能是一个合理的起点,但这取决于您的应用程序,因此会有很大的变化。
要确定各地区的顺序,重要的是考虑各个地区的特点。例如,您可能会有高流量地区和低流量地区。根据您的应用程序,某些功能在一个地理区域比另一个地方更受欢迎。在制定发布时间表时应考虑所有这些特征。您可能希望首先在低流量地区进行推出。这样可以确保您发现的早期问题仅限于影响不大的地区。尽管这不是一个硬性规则,但早期问题通常最为严重,因为它们会在您首次推出的地区迅速显现出来。因此,减少此类问题对客户的影响是有意义的。接下来,推出到高流量地区。一旦您通过低流量地区成功验证了发布的正确性,就要验证其在大规模上的正确性。唯一的方法是将其推出到单个高流量地区。当您成功推出到低流量和高流量地区时,您可以相信您的应用程序可以安全地在所有地方推出。然而,如果存在区域性差异,您可能还想在更广泛地推出发布之前在各种地理区域逐渐测试。
制定发布时间表时,重要的是无论发布的大小如何,都要完全遵循它。许多停机事件是因为人们加速发布,要么是为了解决其他问题,要么是因为他们认为是“安全”的。
全球部署的仪表板和监控
当您在小规模开发时,这可能是一个奇怪的概念,但在中等或大规模时,您可能会遇到的一个重要问题是,不同版本的应用程序部署到不同的地区。这可能由于各种原因发生(例如,因为发布失败、被中止或在特定地区出现问题),如果您不仔细追踪,您可能会迅速遇到在全球各地部署不同版本的难以管理的问题。此外,随着客户询问他们正在经历的错误的修复情况,一个常见的问题将是:“它已经部署了吗?”
因此,开发仪表板是至关重要的,它可以让您一眼看出各个地区运行的哪个版本,以及警报功能,当您的应用程序部署了太多版本时将触发警报。最佳实践是将活跃版本数量限制在不超过三个:一个用于测试,一个正在推出,一个正在被推出的版本所替代。如果活跃版本超过这个数量,将会带来麻烦。
总结
本章提供了关于如何通过软件版本、部署阶段和全球各地区来管理 Kubernetes 应用程序的指导。它强调了组织应用程序基础的原则:依赖文件系统进行组织、使用代码审查来确保质量变更,并依赖于功能标志或门控,以便逐步添加和删除功能。
和其他所有内容一样,本章中的示例应视为灵感,而非绝对真理。阅读指南,找到最适合您应用程序特定情况的方法组合。但请记住,在部署应用程序时,您正在设定一个可能需要多年维护的过程。
附录. 搭建你自己的 Kubernetes 集群
Kubernetes 通常通过公共云计算的虚拟世界来体验,你只需在网页浏览器或终端中操作,距离你的集群非常近。但在裸机上物理搭建一个 Kubernetes 集群可能会带来非常丰富的体验。同样地,看到你随意断开某个节点的电源或网络,然后观察 Kubernetes 如何恢复你的应用程序,你会深信其实用性。
自己搭建集群可能看起来既具有挑战性又昂贵,但幸运的是实际上都不是。购买低成本的片上系统计算机板的能力,以及社区为了让 Kubernetes 更易安装而做出的大量工作,意味着你可以在几个小时内建立一个小型 Kubernetes 集群。
在接下来的说明中,我们专注于搭建一组 Raspberry Pi 机器的集群,但稍作调整,同样的说明也可以用于各种不同的单板机器或者你周围的任何其他计算机。
零件清单
搭建集群的第一步是准备好所有的零件。在这里的所有示例中,我们假设是一个四节点的集群。你也可以建立一个三节点或者甚至一百节点的集群,但四节点是一个相当好的选择。首先,你需要购买(或者搜刮到)搭建集群所需的各种零件。
这里是购物清单,列出了一些写作时的大致价格:
-
四台 Raspberry Pi 4 机器,每台至少配备 2 GB 内存— $180
-
四张至少 8 GB 的 SDHC 存储卡(务必购买高质量的!)— $30–50
-
四根 12 英寸 Cat. 6 以太网线— $10
-
四根 12 英寸 USB-A 到 USB-C 电缆— $10
-
一个 5 端口 10/100 快速以太网交换机— $10
-
一个 5 端口 USB 充电器— $25
-
一个能容纳四个 Pi 的 Raspberry Pi 堆叠盒子— $40(或者自己动手建造)
-
一个用于给以太网交换机供电的 USB 到插口插头(可选)— $5
整个集群的总成本约为$300,如果你建立一个三节点集群并且跳过盒子和交换机的 USB 电源电缆,你可以将成本降低到$200(尽管盒子和电缆确实能使整个集群更加整洁)。
关于存储卡的另一个注意事项:不要省钱在这里。低端存储卡表现不可预测,会使你的集群非常不稳定。如果你想节省一些钱,可以购买较小但高质量的存储卡。在线购买高质量的 8 GB 存储卡大约每张约$7。
一旦你准备好了所有零件,你就可以继续搭建集群了。
注意
这些说明还假设你有一个能够刷写 SDHC 卡的设备。如果没有,你需要购买一个 USB 存储卡读写器。
刷写镜像
默认的 Ubuntu 20.04 镜像支持 Raspberry Pi 4,也是许多 Kubernetes 集群常用的操作系统。安装它的最简单方法是使用由Raspberry Pi 项目提供的 Raspberry Pi Imager:
使用映像工具将 Ubuntu 20.04 映像写入每张存储卡。在映像工具中,Ubuntu 可能不是默认的映像选择,但您可以选择它作为选项。
首次启动
首先要做的是仅启动 API 服务器节点。组装您的集群,并决定哪一个将是 API 服务器节点。插入存储卡,将板子插入 HDMI 输出,并将键盘插入 USB 端口。
接下来,连接电源以启动板子。
在提示符下使用用户名ubuntu和密码ubuntu登录。
警告
您的 Raspberry Pi(或任何新设备)的第一件事是更改默认密码。每种安装的默认密码都是众所周知的,这使得那些有意恶作剧的人可以访问系统的默认登录。这会让互联网对所有人都不安全。请更改默认密码!
为您集群中的每个节点重复这些步骤。
设置网络
下一步是在 API 服务器上设置网络。为 Kubernetes 集群设置网络可能会很复杂。在以下示例中,我们正在设置一个网络,其中一台机器通过无线网络连接到互联网;这台机器还通过有线以太网连接到集群网络,并提供 DHCP 服务器以向集群中的其余节点提供网络地址。此网络的示例如下图所示:

决定哪个板子将托管 API 服务器和etcd。通常最容易记住这一点的方法是将其设置为堆栈中的顶部或底部节点,但也可以使用某种标签。
要做到这一点,请编辑文件/etc/netplan/50-cloud-init.yaml。如果不存在此文件,您可以创建它。文件的内容应如下所示:
network:
version: 2
ethernets:
eth0:
dhcp4: false
dhcp6: false
addresses:
- '10.0.0.1/24'
optional: true
wifis:
wlan0:
access-points:
<your-ssid-here>:
password: '<your-password-here>'
dhcp4: true
optional: true
这将使主要以太网接口具有静态分配的地址 10.0.0.1,并设置 WiFi 接口以连接到您的本地 WiFi。然后运行sudo netplan apply来应用这些新变更。
重新启动机器以获取 10.0.0.1 地址。您可以通过运行ip addr并查看eth0接口的地址来验证设置是否正确。还要验证与互联网的连接是否正常。
接下来,我们将在此 API 服务器上安装 DHCP,以便为工作节点分配地址。运行:
$ apt-get install isc-dhcp-server
然后按以下方式配置 DHCP 服务器(/etc/dhcp/dhcpd.conf):
# Set a domain name, can basically be anything
option domain-name "cluster.home";
# Use Google DNS by default, you can substitute ISP-supplied values here
option domain-name-servers 8.8.8.8, 8.8.4.4;
# We'll use 10.0.0.X for our subnet
subnet 10.0.0.0 netmask 255.255.255.0 {
range 10.0.0.1 10.0.0.10;
option subnet-mask 255.255.255.0;
option broadcast-address 10.0.0.255;
option routers 10.0.0.1;
}
default-lease-time 600;
max-lease-time 7200;
authoritative;
您可能还需要编辑/etc/default/isc-dhcp-server以将INTERFACES环境变量设置为eth0。使用sudo systemctl restart isc-dhcp-server重新启动 DHCP 服务器。现在您的机器应该正在分配 IP 地址。您可以通过通过以太网连接第二台机器到交换机来测试这一点。第二台机器应该从 DHCP 服务器获得 10.0.0.2 地址。
记得编辑 /etc/hostname 文件,将此机器重命名为 node-1。为了帮助 Kubernetes 进行网络设置,还需要设置 iptables,使其能够看到桥接网络流量。在 /etc/modules-load.d/k8s.conf 创建一个文件,只包含 br_netfilter。这将在您的内核中加载 br_netfilter 模块。
接下来,您需要为网络桥接和地址转换(NAT)启用一些 systemctl 设置,以使 Kubernetes 网络正常工作,并且您的节点能够访问公共互联网。创建名为 /etc/sysctl.d/k8s.conf 的文件,并添加以下内容:
net.ipv4.ip_forward=1
net.bridge.bridge-nf-call-ip6tables=1
net.bridge.bridge-nf-call-iptables=1
然后编辑 /etc/rc.local 文件(或等效文件),添加 iptables 规则以从 eth0 转发到 wlan0(以及反向):
iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
iptables -A FORWARD -i wlan0 -o eth0 -m state \
--state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT
到这一步,基本的网络设置应该已经完成。插入并启动其余两个板(应该看到它们被分配了地址 10.0.0.3 和 10.0.0.4)。在每台机器上编辑 /etc/hostname 文件,分别命名为 node-2 和 node-3。
首先查看 /var/lib/dhcp/dhcpd.leases 验证,然后 SSH 到节点(记得首先更改默认密码)。确认节点能够连接到外部互联网。
安装容器运行时
在安装 Kubernetes 之前,您需要先安装一个容器运行时。有几种可能的运行时可供选择,但最广泛采用的是来自 Docker 的 containerd。containerd 由标准的 Ubuntu 软件包管理器提供,但其版本有点滞后。虽然需要多做一些工作,但我们建议从 Docker 项目自身安装它。
第一步是将 Docker 设置为系统上安装软件包的仓库:
# Add some prerequisites
sudo apt-get install ca-certificates curl gnupg lsb-release
# Install Docker's signing key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor \
-o /usr/share/keyrings/docker-archive-keyring.gpg
最后一步,创建文件 /etc/apt/sources.list.d/docker.list,内容如下:
deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
https://download.docker.com/linux/ubuntu focal stable
现在您已经安装了 Docker 软件包仓库,可以通过运行以下命令安装 containerd.io。重要的是要安装 containerd.io,而不是 containerd,以获取 Docker 软件包,而不是默认的 Ubuntu 软件包:
sudo apt-get update; sudo apt-get install containerd.io
到这一步,containerd 已经安装完毕,但您需要配置它,因为软件包提供的配置不能满足 Kubernetes 的需求:
containerd config default > config.toml
sudo mv config.toml /etc/containerd/config.toml
# Restart to pick up the config
sudo systemctl restart containerd
现在您已经安装了容器运行时,可以继续安装 Kubernetes 自身了。
安装 Kubernetes
此时,所有节点应该已经启动,并具有 IP 地址,并且能够访问互联网。现在是在所有节点上安装 Kubernetes 的时候了。使用 SSH,在所有节点上运行以下命令安装 kubelet 和 kubeadm 工具。
首先,添加软件包的加密密钥:
# curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| sudo apt-key add -
然后将该仓库添加到您的仓库列表中:
# echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" \
| sudo tee /etc/apt/sources.list.d/kubernetes.list
最后,更新并安装 Kubernetes 工具。这也会更新系统上的所有软件包,以确保一切正常:
# sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni
配置集群
在运行 DHCP 和连接到互联网的 API 服务器节点上运行:
$ sudo kubeadm init --pod-network-cidr 10.244.0.0/16 \
--apiserver-advertise-address 10.0.0.1 \
--apiserver-cert-extra-sans kubernetes.cluster.home
请注意,您正在广告您的内部 IP 地址,而不是外部地址。
最终,这将打印出一个命令,用于将节点加入到您的集群中。它看起来会像这样:
$ kubeadm join --token=*<token>* 10.0.0.1
SSH 到集群中每个工作节点,并运行该命令。
当所有这些都完成后,您应该能够运行此命令并查看您的工作集群:
$ kubectl get nodes
设置集群网络
您已经设置好了节点级网络,但仍然需要设置 Pod 到 Pod 的网络。由于集群中的所有节点都在同一物理以太网网络上运行,因此您可以简单地在主机内核中设置正确的路由规则。
管理此操作的最简单方法是使用由 CoreOS 创建并现在由 Flannel 项目 支持的 Flannel 工具。Flannel 支持多种不同的路由模式;我们将使用 host-gw 模式。您可以从 Flannel 项目页面 下载一个示例配置文件。
$ curl https://oreil.ly/kube-flannelyml \
> kube-flannel.yaml
Flannel 提供的默认配置使用 vxlan 模式。要修复此问题,请在您喜欢的编辑器中打开该配置文件;将 vxlan 替换为 host-gw。
您还可以使用 sed 工具来完成此操作:
$ curl https://oreil.ly/kube-flannelyml \
| sed "s/vxlan/host-gw/g" \
> kube-flannel.yaml
一旦您更新了 kube-flannel.yaml 文件,您就可以使用以下命令创建 Flannel 网络设置:
$ kubectl apply -f kube-flannel.yaml
这将创建两个对象,一个用于配置 Flannel 的 ConfigMap 和一个运行实际 Flannel 守护程序的 DaemonSet。您可以使用以下命令检查它们:
$ kubectl describe --namespace=kube-system configmaps/kube-flannel-cfg
$ kubectl describe --namespace=kube-system daemonsets/kube-flannel-ds
摘要
此时,您应该已经在您的树莓派上运行一个工作的 Kubernetes 集群。这对于探索 Kubernetes 非常有用。安排一些任务,打开 UI,并尝试通过重新启动机器或断开网络来破坏您的集群。


浙公网安备 33010602011771号