Kubernetes-最佳实践第二版-全-
Kubernetes 最佳实践第二版(全)
原文:
zh.annas-archive.org/md5/b4e7a24d06a7cc27b05c122fe3fa466d译者:飞龙
前言
谁应该阅读本书
Kubernetes 是云原生开发的事实标准。它是一个强大的工具,可以使您的下一个应用程序更易于开发,部署更快速,运行更可靠。然而,要发挥 Kubernetes 的潜力,需要正确使用它。本书适用于那些正在将真实应用程序部署到 Kubernetes 上,并有兴趣了解他们可以应用到基于 Kubernetes 构建的应用程序的模式和实践的任何人。
重要的是,本书不是 Kubernetes 的入门。我们假设您对 Kubernetes API 和工具有基本的了解,并且知道如何创建和与 Kubernetes 集群交互。如果您想学习 Kubernetes,有许多很好的资源可以帮助您,比如 Kubernetes: Up and Running(O'Reilly),可以为您提供介绍。
相反,本书是一个资源,适用于任何想要深入了解如何在 Kubernetes 上部署特定应用程序和工作负载的人。无论您是准备在 Kubernetes 上部署第一个应用程序,还是已经使用 Kubernetes 多年,本书都应该对您有所帮助。
为什么写这本书
我们四人中,都有帮助各种用户将其应用程序部署到 Kubernetes 上的重要经验。通过这些经验,我们看到了人们在哪些地方遇到困难,并帮助他们找到成功的方法。在撰写本书时,我们试图记录这些经验,以便更多的人通过阅读我们从这些实际经验中学到的教训来学习。我们希望通过将我们的经验写下来,能够扩展我们的知识,并使您能够成功地独立部署和管理您的应用程序在 Kubernetes 上。
浏览本书
尽管您可能会一口气读完这本书的每一页,但这并不是我们设计您使用它的方式。相反,我们设计本书为独立的章节集合。每一章节都提供了完成特定任务的完整概述,这些任务您可能需要在 Kubernetes 上完成。我们期望人们深入研究本书以了解特定主题或兴趣,然后让本书独自放置,只有在出现新主题时才返回。
尽管这本书采用独立的方法,但一些主题贯穿全书。有几章关于在 Kubernetes 上开发应用程序。第二章涵盖开发者工作流程。第五章讨论持续集成和测试。第十五章涵盖在 Kubernetes 上构建高级平台,而第十六章讨论管理状态和有状态应用程序。除了开发应用程序外,还有几章关于在 Kubernetes 中操作服务。第一章涵盖基本服务的设置,第三章涵盖监控和指标。第四章涵盖配置管理,而第六章涵盖版本控制和发布。第七章涵盖在全球范围内部署您的应用程序。
本书还有几章关于集群管理,包括第八章关于资源管理,第九章关于网络,第十章关于 Pod 安全,第十一章关于策略和治理,第十二章关于多集群管理,以及第十七章关于准入控制和授权。最后,一些章节是真正独立的;这些章节涵盖机器学习(第十四章)和与外部服务集成(第十三章)。
尽管在真实世界中尝试实际主题之前阅读所有章节可能会有所帮助,但我们主要希望您将本书视为实践中这些主题的指南。
本版新增内容
我们希望通过增加四个新章节来补充第一版,这些章节涵盖随着 Kubernetes 的成熟而不断发展的工具和模式的最佳实践。这些新章节分别是第十八章关于 GitOps,第十九章关于安全,第二十章关于混沌测试,以及第二十一章关于实施运算符。
本书中使用的约定
本书中使用以下排版约定:
斜体
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
常量宽度
用于程序清单,以及段落内引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
常数宽度粗体
显示用户应按字面输入的命令或其他文本。
常数宽度斜体
显示应由用户提供值或由上下文确定值的文本。
小贴士
这个元素表示小贴士或建议。
注意
这个元素表示一般注意。
警告
这个元素表示警告或注意。
使用代码示例
补充材料(代码示例、练习等)可在https://oreil.ly/KBPsample下载。
如果您有技术问题或使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
这本书旨在帮助您完成工作。一般情况下,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您重复使用了大量代码,否则无需征得我们的许可。例如,编写一个使用本书多个代码片段的程序不需要许可。销售或分发 O’Reilly 书籍中的示例代码需要许可。引用本书并引用示例代码回答问题无需许可。将本书大量示例代码整合到产品文档中需要许可。
我们赞赏,但通常不需要,署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Kubernetes Best Practices by Brendan Burns, Eddie Villalba, Dave Strebel, and Lachlan Evenson (O’Reilly). Copyright 2024 Brendan Burns, Eddie Villalba, Dave Strebel, and Lachlan Evenson, 978-1-098-14216-2.”
如果您觉得您对代码示例的使用超出了合理使用或上述许可范围,请随时联系我们,邮箱为permissions@oreilly.com。
O’Reilly 在线学习
注意
超过 40 年来,O’Reilly Media为公司成功提供技术和商业培训、知识和见解。
我们独特的专家和创新者网络通过书籍、文章、会议和我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问实时培训课程、深入学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。有关更多信息,请访问http://oreilly.com。
如何联系我们
请将关于本书的评论和问题发送给出版商:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
加利福尼亚州塞巴斯托波尔 95472
-
800-889-8969(美国或加拿大)
-
707-829-7019(国际或本地)
-
707-829-0104(传真)
-
support@oreilly.com
我们有一个网页专门用来列出这本书的勘误、示例和任何额外信息。您可以访问这个页面:https://oreil.ly/kubernetes-best-practices2。
想了解关于我们的书籍和课程的最新消息,请访问:https://oreilly.com。
在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media
关注我们的 Twitter:https://twitter.com/oreillymedia
观看我们的 YouTube 频道:https://youtube.com/oreillymedia
致谢
Brendan 想感谢他的美好家庭,Robin、Julia 和 Ethan,对他所做的一切的爱与支持;Kubernetes 社区,没有他们,这一切都不可能;以及他那些了不起的合著者,没有他们这本书也不会存在。
Dave 想感谢他美丽的妻子 Jen 和他们的三个孩子 Max、Maddie 和 Mason,对他们的支持。他还想感谢 Kubernetes 社区多年来提供的所有建议和帮助。最后,他感谢他的合著者们让这个冒险成为现实。
Lachlan 想感谢他的妻子和三个孩子,对他的爱与支持。他还想感谢多年来在 Kubernetes 社区中教导他的所有优秀个体。他还想特别感谢 Joseph Sandoval 的指导。最后,他感谢他那些出色的合著者让这本书成为可能。
Eddie 想感谢他的妻子 Sandra,在写作过程中她始终给予的支持、爱和鼓励。他还想感谢他的女儿 Giavanna,因为她给了他留下遗产的动力,这样她可以为她的爸爸感到自豪。最后,他感谢 Kubernetes 社区和他的合著者们,他们一直是他在云原生旅程中的路标。
我们都要感谢 Virginia Wilson 在撰写手稿和帮助我们整合所有想法方面的工作,以及 Jill Leonard 在第二版中的指导。最后,我们要感谢 Bridget Kromhout、Bilgin Ibryam、Roland Huß、Justin Domingus、Jess Males 和 Jonathan Johnson 对完成的关注。
第一章:设置基本服务
本章描述了在 Kubernetes 中设置简单多层应用程序的步骤。我们将详细介绍的示例由两个层组成:一个简单的 Web 应用程序和一个数据库。尽管这可能不是最复杂的应用程序,但这是学习在 Kubernetes 中管理应用程序时的一个很好的起点。
应用程序概述
我们示例中将使用的应用程序非常简单直接。它是一个带有以下细节的简单日志服务:
-
它使用 NGINX 运行一个独立的静态文件服务器。
-
它在/api路径上有一个 RESTful 应用程序编程接口(API)https://some-host-name.io/api。
-
它在主 URL 上有一个文件服务器,https://some-host-name.io。
-
它使用Let’s Encrypt 服务来管理安全套接字层(SSL)。
图 1-1 展示了该应用程序的图表。如果您一时不理解所有部件,不要担心;它们将在本章节中详细解释。我们将逐步构建这个应用程序,首先使用 YAML 配置文件,然后使用 Helm 图表。

图 1-1. 作为部署在 Kubernetes 中的日志服务的图表
管理配置文件
在深入研究如何在 Kubernetes 中构建此应用程序的细节之前,讨论如何管理配置本身是值得的。在 Kubernetes 中,一切都是声明性的。这意味着您在集群中编写应用程序的期望状态(通常是 YAML 或 JSON 文件),这些声明的期望状态定义了应用程序的所有部分。这种声明性方法远比命令式方法更可取,后者的集群状态是对集群一系列更改的总和。如果集群以命令式方式配置,那么理解和复制集群如何达到该状态是困难的,这使得理解或解决应用程序问题变得具有挑战性。
在声明应用程序状态时,人们通常更喜欢 YAML 而不是 JSON,尽管 Kubernetes 支持两者。这是因为 YAML 比 JSON 略微不那么冗长,更适合人类编辑。然而,值得注意的是,YAML 对缩进很敏感;在 Kubernetes 配置中经常会出现由于 YAML 中不正确的缩进而导致的错误。如果应用程序的行为不如预期,检查缩进是开始故障排除的一个好方法。大多数编辑器都支持 JSON 和 YAML 的语法高亮显示。在处理这些文件时,安装这样的工具以便更容易找到配置中的作者和文件错误是一个好主意。还有一个优秀的 Visual Studio Code 扩展,支持 Kubernetes 文件的更丰富的错误检查。
由于这些 YAML 文件中包含的声明性状态作为应用程序的真相来源,正确管理这种状态对应用程序的成功至关重要。在修改应用程序的期望状态时,您希望能够管理更改,验证其正确性,审计谁进行了更改,并在失败时可能回滚。幸运的是,在软件工程的背景下,我们已经开发了管理声明性状态以及审计和回滚所需工具。换句话说,围绕版本控制和代码审查的最佳实践直接适用于管理应用程序的声明性状态的任务。
如今,大多数人将他们的 Kubernetes 配置存储在 Git 中。尽管版本控制系统的具体细节并不重要,但 Kubernetes 生态系统中的许多工具都期望在 Git 存储库中找到文件。对于代码审查,存在更多的异构性;尽管显然 GitHub 非常受欢迎,但其他人使用本地代码审查工具或服务。无论您如何为应用程序配置实施代码审查,都应该像对待源代码控制一样认真和专注。
在为应用程序布置文件系统时,值得使用文件系统提供的目录组织来组织您的组件。通常,一个单独的目录用于包含一个应用服务。构成应用服务的定义在团队之间的大小可能有所不同,但通常是由 8-12 人团队开发的服务。在该目录中,子目录用于应用程序的子组件。
对于我们的应用程序,我们将文件布置如下:
journal/
frontend/
redis/
fileserver/
在每个目录中都有定义服务所需的具体 YAML 文件。随着我们开始将应用程序部署到多个不同的区域或集群时,您将看到这种文件布局会变得更加复杂。
使用部署创建复制服务
要描述我们的应用程序,我们将从前端开始向下工作。期刊的前端应用程序是一个使用 TypeScript 实现的 Node.js 应用程序。完整的应用程序太大,无法包含在书中,因此我们将其托管在我们的 GitHub 上。您也可以在那里找到未来示例的代码,因此值得收藏。该应用程序在端口 8080 上公开一个 HTTP 服务,用于服务 /api/* 路径的请求,并使用 Redis 后端来添加、删除或返回当前的期刊条目。如果您计划在本地计算机上使用后续的 YAML 示例,请使用 Dockerfile 将此应用程序构建为容器映像,并将其推送到您自己的映像存储库。然后,您将需要在代码中包含您的容器映像名称,而不是使用我们的示例文件名。
图像管理的最佳实践
虽然总体上,构建和维护容器镜像超出了本书的范围,但确定一些通用的最佳实践以进行镜像构建和命名仍然是值得的。总体上,镜像构建过程可能容易受到“供应链攻击”的影响。在这种攻击中,恶意用户将代码或二进制文件注入到来自受信任源的某个依赖项中,然后构建到您的应用程序中。因为存在这种攻击的风险,因此在构建镜像时,基于仅知名和可信的镜像提供者是至关重要的。或者,您可以从头开始构建所有镜像。对于某些语言(例如 Go),构建静态二进制文件很容易,但对于像 Python、JavaScript 或 Ruby 这样的解释性语言来说,则复杂得多。
关于镜像的其他最佳实践与命名有关。尽管镜像注册表中容器镜像的版本理论上是可变的,但应将版本标签视为不可变。特别是,使用语义版本和构建镜像的提交的 SHA 散列的组合作为命名镜像的良好实践(例如,v1.0.1-bfeda01f)。如果不指定镜像版本,则默认使用 latest。尽管这在开发中可能很方便,但在生产环境中使用 latest 是一个不好的做法,因为每次构建新镜像时 latest 明显在变化。
创建一个复制的应用程序
我们的前端应用程序是无状态的;它完全依赖于 Redis 后端来维护其状态。因此,我们可以随意复制它,而不会影响流量。虽然我们的应用程序不太可能支持大规模使用,但最好仍然至少运行两个副本,以便能够处理意外崩溃或无需停机即可推出新版本的应用程序。
在 Kubernetes 中,ReplicaSet 资源直接管理复制容器化应用程序的特定版本。由于随着代码修改,所有应用程序的版本都会随时间变化,直接使用 ReplicaSet 并不是最佳实践。相反,应使用 Deployment 资源。Deployment 结合了 ReplicaSet 的复制功能与版本控制以及执行分阶段部署的能力。通过使用 Deployment,您可以利用 Kubernetes 的内置工具从一个应用程序版本迁移到下一个版本。
我们应用程序的 Kubernetes Deployment 资源如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
# All pods in the Deployment will have this label
app: frontend
name: frontend
namespace: default
spec:
# We should always have at least two replicas for reliability
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: my-repo/journal-server:v1-abcde
imagePullPolicy: IfNotPresent
name: frontend
# TODO: Figure out what the actual resource needs are
resources:
request:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
在这个 Deployment 中需要注意的几个事项。首先是我们使用 Labels 来标识 Deployment、ReplicaSets 和 Deployment 创建的 pod。我们为所有这些资源添加了 app: frontend 标签,以便可以在单个请求中检查特定层的所有资源。随着我们添加其他资源,您将看到我们将遵循同样的实践。
另外,我们在 YAML 中的许多地方添加了注释。虽然这些注释不会出现在存储在服务器上的 Kubernetes 资源中,就像代码中的注释一样,它们用于指导第一次查看此配置的人员。
你还应该注意,在我们的 Deployment 中为容器指定了请求和限制资源请求,我们已将请求设置为限制。在运行应用程序时,请求是保证在运行的主机机器上的预留。限制是容器被允许的最大资源使用量。当您刚开始时,将请求设置为限制将导致应用程序的行为最可预测。这种可预测性是以资源利用率为代价的。因为将请求设置为限制会防止您的应用程序过度调度或消耗过多的空闲资源,所以除非您非常仔细地调整请求和限制,否则您将无法实现最大利用率。随着您对 Kubernetes 资源模型的理解越来越深入,您可能会考虑独立地修改应用程序的请求和限制,但总体而言,大多数用户发现从可预测性中获得的稳定性值得降低利用率。
通常情况下,正如我们的评论所示,很难知道这些资源限制的正确值。首先高估估计值,然后使用监控来调整到正确的值是一个相当不错的方法。然而,如果您正在启动一个新的服务,请记住,第一次看到大规模流量时,您的资源需求可能会显著增加。此外,有些语言,特别是垃圾回收语言,会愉快地消耗所有可用内存,这可能会使确定内存正确最小值变得困难。在这种情况下,可能需要进行某种形式的二分搜索,但请记住要在测试环境中执行此操作,以免影响生产环境!
现在我们已经定义了 Deployment 资源,我们将其提交到版本控制,并将其部署到 Kubernetes 中。
git add frontend/deployment.yaml
git commit -m "Added deployment" frontend/deployment.yaml
kubectl apply -f frontend/deployment.yaml
确保您的集群内容与源控件的内容完全匹配也是最佳实践。确保这一点的最佳模式是采用 GitOps 方法,并仅从源控件的特定分支中部署到生产环境,使用持续集成/持续交付(CI/CD)自动化。通过这种方式,您可以确保源控件和生产环境匹配。尽管对于一个简单的应用程序来说,完整的 CI/CD 管道可能看起来过于复杂,但自动化本身独立于它提供的可靠性,通常都是花时间设置的值得。而将 CI/CD 管道应用于现有的命令式部署的应用程序则非常困难。
我们将在后续章节回顾此应用程序描述的 YAML,以检查其他元素,如 ConfigMap 和 secret 卷,以及 Pod 的服务质量。
设置用于 HTTP 流量的外部 Ingress
现在我们的应用程序容器已经部署,但目前任何人都无法访问该应用程序。默认情况下,集群资源仅在集群内部可用。为了将我们的应用程序暴露给外部世界,我们需要创建一个服务和负载均衡器,以提供外部 IP 地址并将流量引导到我们的容器。为了进行外部暴露,我们将使用两个 Kubernetes 资源。第一个是负载均衡传输控制协议(TCP)或用户数据报协议(UDP)流量的服务。在我们的情况下,我们使用的是 TCP 协议。第二个是 Ingress 资源,它提供基于 HTTP 路径和主机的 HTTP(S)负载均衡请求智能路由。对于像这样简单的应用程序,您可能会想知道为什么选择使用更复杂的 Ingress,但正如您将在后续章节中看到的,即使是这样简单的应用程序也将为来自两个不同服务的 HTTP 请求提供服务。此外,在边缘部署 Ingress 使得未来扩展我们的服务变得更加灵活。
注意
Ingress 资源是 Kubernetes 中较早的资源之一,多年来已经提出了许多关于它如何模拟对微服务的 HTTP 访问的问题。这导致开发了适用于 Kubernetes 的 Gateway API。Gateway API 被设计为 Kubernetes 的扩展,需要在您的集群中安装额外的组件。如果发现 Ingress 不能满足您的需求,请考虑转向 Gateway API。
在定义 Ingress 资源之前,需要为 Ingress 指向的 Kubernetes 服务创建一个 Kubernetes 服务。我们将使用标签来将服务指向我们在上一节中创建的 Pod。与部署相比,服务的定义要简单得多,如下所示:
apiVersion: v1
kind: Service
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: frontend
type: ClusterIP
在定义了服务之后,您可以定义一个 Ingress 资源。与服务资源不同,Ingress 需要在集群中运行一个 Ingress 控制器容器。您可以选择多种不同的实现方式,可以是您的云提供商提供的,也可以使用开源服务器来实现。如果选择安装开源 Ingress 提供程序,建议使用Helm 软件包管理器来安装和维护它。流行的选择有nginx或haproxy Ingress 提供程序:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 8080
有了我们创建的 Ingress 资源,我们的应用程序已经准备好为全球的 Web 浏览器提供流量服务。接下来,我们将看看如何设置应用程序以便进行简单的配置和定制。
使用 ConfigMaps 配置应用程序
每个应用程序都需要一定程度的配置。这可能包括每页显示的日志条目数、特定背景的颜色、特殊假期显示,或者其他类型的配置。通常,将这些配置信息与应用程序本身分开是一种最佳实践。
有几个原因需要进行这种分离。首先,你可能希望根据不同的设置配置相同的应用程序二进制文件。在欧洲,你可能希望推出复活节特别活动,而在中国,你可能希望展示中国新年的特别活动。除了这种环境专业化外,分离还有敏捷性的原因。通常一个二进制发布包含多个不同的新功能;如果通过代码打开这些功能,修改活动功能的唯一方法是构建和发布新的二进制文件,这可能是一个昂贵和缓慢的过程。
使用配置来激活一组功能意味着你可以快速(甚至动态地)根据用户需求或应用程序代码失败来激活和停用功能。功能可以按功能单元进行推出和回滚。这种灵活性确保你即使需要回滚以解决性能或正确性问题,也能持续推进大多数功能。
在 Kubernetes 中,这种配置被称为 ConfigMap 资源。ConfigMap 包含多个键值对,表示配置信息或文件。这些配置信息可以通过文件或环境变量的方式提供给 Pod 中的容器。假设你想要配置你的在线日志应用程序,以显示每页可配置的日志条目数量。为了实现这一点,你可以定义一个如下的 ConfigMap:
kubectl create configmap frontend-config --from-literal=journalEntries=10
要配置你的应用程序,你需要在应用程序本身中将配置信息公开为一个环境变量。为了实现这一点,你可以将以下内容添加到之前定义的 Deployment 的 container 资源中:
...
# The containers array in the PodTemplate inside the Deployment
containers:
- name: frontend
...
env:
- name: JOURNAL_ENTRIES
valueFrom:
configMapKeyRef:
name: frontend-config
key: journalEntries
...
尽管这演示了如何使用 ConfigMap 配置你的应用程序,但在实际的部署环境中,你至少每周需要对这些配置进行定期更改。可能会诱惑你简单地通过改变 ConfigMap 自身来进行这些更改,但这并不是一个最佳实践,原因如下:首先,改变配置实际上并不会触发对现有 Pod 的更新。配置只有在 Pod 重新启动时才会应用。因此,部署并不是基于健康状况进行的,可能是临时或随机的。另一个原因是,ConfigMap 的唯一版本控制在你的版本控制中,而执行回滚可能非常困难。
更好的方法是在 ConfigMap 的名称中放入版本号。不要称其为frontend-config,而是称其为frontend-config-v1。当您想要进行更改时,不要直接更新 ConfigMap,而是创建一个新的v2 ConfigMap,然后更新 Deployment 资源以使用该配置。当您执行此操作时,将自动触发 Deployment 的滚动升级,使用适当的健康检查和更改之间的暂停。此外,如果您需要回滚,v1配置仍然保存在集群中,回滚只需再次更新 Deployment 即可。
使用秘密管理身份验证
到目前为止,我们并没有真正讨论我们的前端连接的 Redis 服务。但在任何实际应用程序中,我们需要保护服务之间的连接。部分原因是确保用户及其数据的安全,此外,防止诸如将开发前端与生产数据库连接的错误也是至关重要的。
Redis 数据库使用简单密码进行身份验证。或许方便的想法是将此密码存储在应用程序的源代码中,或者在镜像的文件中,但这些都是因为各种原因不好的做法。首先,您已经泄露了您的秘密(密码),使其进入了一个您并没有考虑访问控制的环境。如果您将密码放入源代码控制中,您就是将源代码访问权限与所有秘密的访问权限对齐。这不是最佳行动,因为可以访问您的源代码的用户群体可能比真正应该访问您的 Redis 实例的用户群体要广泛。同样,能够访问您的容器镜像的人不一定应该访问您的生产数据库。
除了访问控制的考虑之外,避免将秘密绑定到源代码和/或镜像的另一个原因是参数化。您希望能够在各种环境(例如开发、金丝雀和生产)中使用相同的源代码和镜像。如果秘密紧密绑定在源代码或镜像中,则需要为每个环境使用不同的镜像(或不同的代码)。
在前一节看到 ConfigMaps 后,您可能立即认为密码可以存储为配置,然后作为特定于应用程序的配置填充到应用程序中。您绝对正确,认为配置与应用程序的分离与秘密与应用程序的分离是相同的。但事实是,秘密本身是一个重要的概念。您可能希望以不同于配置的方式处理秘密的访问控制、处理和更新。更重要的是,您希望您的开发人员在访问秘密时与访问配置时有不同的思考方式。因此,Kubernetes 为管理秘密数据提供了内置的 Secret 资源。
你可以如下创建 Redis 数据库的秘密密码:
kubectl create secret generic redis-passwd --from-literal=passwd=${RANDOM}
显然,你可能希望为你的密码使用比随机数更复杂的内容。此外,你可能想要使用秘密/密钥管理服务,可以是你的云提供商如 Microsoft Azure Key Vault,也可以是开源项目如 HashiCorp 的 Vault。当使用密钥管理服务时,它们通常与 Kubernetes Secrets 有更紧密的集成。
在 Kubernetes 中将 Redis 密码存储为一个 Secret 后,你需要在将应用程序部署到 Kubernetes 后,将该 Secret 绑定 到运行中的应用程序。为此,可以使用 Kubernetes Volume。Volume 实际上是一个文件或目录,可以挂载到运行容器中的用户指定位置。对于 Secrets,Volume 被创建为 tmpfs 内存支持的文件系统,然后挂载到容器中。这样即使机器被物理攻击(在云中不太可能,但在数据中心可能),对攻击者来说获取 Secrets 将更加困难。
注意
Kubernetes 中的 Secrets 默认以明文形式存储。如果你希望以加密方式存储 Secrets,可以集成密钥提供者,以获取一个由 Kubernetes 使用的密钥,用于加密集群中的所有 Secrets。请注意,虽然这样可以保护密钥免受对 etcd 数据库的直接攻击,但仍需确保通过 Kubernetes API 服务器的访问得到适当的安全保护。
要将秘密 Volume 添加到部署中,你需要在部署的 YAML 文件中指定两个新条目。第一个是为 Pod 添加 Volume 的 volume 条目:
...
volumes:
- name: passwd-volume
secret:
secretName: redis-passwd
Container Storage Interface (CSI) 驱动程序使你能够使用位于 Kubernetes 集群外部的密钥管理系统(KMS)。这通常是大型或受管制组织内部合规性和安全性的要求。如果使用这些 CSI 驱动程序之一,你的 Volume 将会如下所示:
...
volumes:
- name: passwd-volume
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "azure-sync"
...
无论你使用哪种方法,在 Pod 中定义了 Volume 后,你需要将其挂载到特定的容器中。你可以通过容器描述中的 volumeMounts 字段来实现这一点:
...
volumeMounts:
- name: passwd-volume
readOnly: true
mountPath: "/etc/redis-passwd"
...
这将秘密 Volume 挂载到 redis-passwd 目录,以便客户端代码访问。将这一切组合起来,你将得到完整的部署如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: my-repo/journal-server:v1-abcde
imagePullPolicy: IfNotPresent
name: frontend
volumeMounts:
- name: passwd-volume
readOnly: true
mountPath: "/etc/redis-passwd"
resources:
requests:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
volumes:
- name: passwd-volume
secret:
secretName: redis-passwd
到目前为止,我们已经配置了客户端应用程序,使其可以使用秘密进行身份验证以连接到 Redis 服务。配置 Redis 使用这个密码类似;我们将其挂载到 Redis Pod 并从文件中加载密码。
部署一个简单的有状态数据库
尽管在概念上部署有状态应用类似于部署像我们的前端这样的客户端,但状态带来了更多的复杂性。首先,在 Kubernetes 中,Pod 可能因为节点健康问题、升级或重新平衡等原因而重新调度。当发生这种情况时,Pod 可能会迁移到另一台机器上。如果 Redis 实例的数据位于任何特定的机器上或容器内部,那么当容器迁移或重新启动时,该数据将会丢失。为了防止这种情况发生,在 Kubernetes 中运行有状态工作负载时,使用远程 PersistentVolume 管理应用程序关联的状态非常重要。
在 Kubernetes 中,有各种实现持久卷(PersistentVolumes),但它们都有共同的特点。像前面描述的密钥卷一样,它们与一个 Pod 相关联,并被挂载到容器的特定位置。与密钥不同的是,持久卷通常是通过某种网络协议挂载的远程存储,可以是基于文件的,例如网络文件系统(NFS)或服务器消息块(SMB),也可以是基于块的(iSCSI、云磁盘等)。一般来说,对于像数据库这样的应用,基于块的磁盘更可取,因为它们提供更好的性能,但如果性能不是首要考虑因素,有时基于文件的磁盘提供了更大的灵活性。
注意
管理一般状态是复杂的,Kubernetes 也不例外。如果在支持有状态服务的环境中运行(例如 MySQL 作为服务、Redis 作为服务),通常最好使用这些有状态服务。最初,有状态软件即服务(SaaS)的成本溢价可能看起来很高,但考虑到状态的所有操作要求(备份、数据本地性、冗余等)以及在 Kubernetes 集群中存在状态会使应用程序难以在集群之间移动,大多数情况下,存储 SaaS 是值得额外花费的。在本地环境中,如果没有存储 SaaS 可用,那么有一个专门的团队为整个组织提供存储作为服务绝对比允许每个团队自行构建要好。
要部署我们的 Redis 服务,我们使用 StatefulSet 资源。StatefulSet 在初始 Kubernetes 发布后作为 ReplicaSet 资源的补充添加,它提供了稍微更强的保证,如一致的名称(没有随机哈希!)和定义的扩展和缩减顺序。当部署单实例时,这些可能不太重要,但当您想要部署复制状态时,这些属性非常方便。
要为我们的 Redis 获取一个 PersistentVolume,我们使用 PersistentVolumeClaim。您可以将声明视为“资源请求”。我们的 Redis 抽象地声明它需要 50 GB 的存储空间,Kubernetes 集群决定如何提供适当的 PersistentVolume。有两个原因。第一个是为了我们可以编写一个在不同云和本地部署之间可移植的 StatefulSet,磁盘细节可能不同。另一个原因是,虽然许多 PersistentVolume 类型只能挂载到单个 pod,我们可以使用 Volume claim 编写一个模板,可以复制,并且每个 pod 仍然分配有自己特定的 PersistentVolume。
以下示例显示了一个具有 PersistentVolumes 的 Redis StatefulSet:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis"
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:5-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
这部署了您的 Redis 服务的单个实例,但假设您想要复制 Redis 集群以扩展读取和提高故障恢复能力。为此,您显然需要将副本数增加到三个,但您还需要确保两个新的副本连接到 Redis 的写主。我们将在下一节中看到如何进行此连接。
当您为 Redis StatefulSet 创建无头服务时,它会创建一个 DNS 条目redis-0.redis;这是第一个副本的 IP 地址。您可以使用此条目在所有容器中启动一个简单的脚本:
#!/bin/sh
PASSWORD=$(cat /etc/redis-passwd/passwd)
if [[ "${HOSTNAME}" == "redis-0" ]]; then
redis-server --requirepass ${PASSWORD}
else
redis-server --slaveof redis-0.redis 6379 --masterauth ${PASSWORD}
--requirepass ${PASSWORD}
fi
您可以将此脚本创建为 ConfigMap:
kubectl create configmap redis-config --from-file=./launch.sh
然后,您将此 ConfigMap 添加到您的 StatefulSet,并将其用作容器的命令。让我们还添加章节前面创建的用于身份验证的密码。
完整的三副本 Redis 如下所示:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis"
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:5-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
- name: script
mountPath: /script/launch.sh
subPath: launch.sh
- name: passwd-volume
mountPath: /etc/redis-passwd
command:
- sh
- -c
- /script/launch.sh
volumes:
- name: script
configMap:
name: redis-config
defaultMode: 0777
- name: passwd-volume
secret:
secretName: redis-passwd
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
现在您的 Redis 已经集群化以实现容错。如果任何一个三个 Redis 副本因任何原因失败,您的应用程序可以继续运行,直到第三个副本被恢复。
使用服务创建 TCP 负载均衡器
现在我们已经部署了有状态的 Redis 服务,我们需要将其提供给我们的前端。为此,我们创建了两个不同的 Kubernetes 服务。第一个是用于从 Redis 读取数据的服务。因为 Redis 将数据复制到 StatefulSet 的所有三个成员中,我们不关心我们的请求去哪里读取。因此,我们使用了一个基本的读取服务:
apiVersion: v1
kind: Service
metadata:
labels:
app: redis
name: redis
namespace: default
spec:
ports:
- port: 6379
protocol: TCP
targetPort: 6379
selector:
app: redis
sessionAffinity: None
type: ClusterIP
要启用写入,您需要定位 Redis 主(副本#0)。为此,创建一个headless服务。无头服务没有集群 IP 地址;相反,它为 StatefulSet 中的每个 pod 编程了一个 DNS 条目。这意味着我们可以通过redis-0.redis DNS 名称访问我们的主节点:
apiVersion: v1
kind: Service
metadata:
labels:
app: redis-write
name: redis-write
spec:
clusterIP: None
ports:
- port: 6379
selector:
app: redis
因此,当我们希望连接 Redis 进行写入或事务读/写对时,我们可以构建一个单独的写客户端,连接到redis-0.redis-write服务器。
使用 Ingress 将流量路由到静态文件服务器
我们应用的最后一个组件是静态文件服务器。静态文件服务器负责提供 HTML、CSS、JavaScript 和图像文件。将静态文件服务与之前描述的 API 服务前端分离,既更高效又更专注。我们可以轻松地使用像 NGINX 这样的高性能静态文件服务器来提供文件,同时允许我们的开发团队专注于实现 API 所需的代码。
幸运的是,Ingress 资源使得这种微型微服务架构变得非常简单。就像前端一样,我们可以使用一个 Deployment 资源来描述一个复制的 NGINX 服务器。让我们将静态图像构建到 NGINX 容器中,并将它们部署到每个副本中。Deployment 资源如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: fileserver
name: fileserver
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: fileserver
template:
metadata:
labels:
app: fileserver
spec:
containers:
# This image is intended as an example, replace it with your own
# static files image.
- image: my-repo/static-files:v1-abcde
imagePullPolicy: Always
name: fileserver
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
resources:
requests:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
dnsPolicy: ClusterFirst
restartPolicy: Always
现在已经有一个复制的静态网页服务器正在运行,你也将创建一个服务资源作为负载均衡器:
apiVersion: v1
kind: Service
metadata:
labels:
app: fileserver
name: fileserver
namespace: default
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: fileserver
sessionAffinity: None
type: ClusterIP
现在你已经为静态文件服务器创建了一个服务,扩展 Ingress 资源以包含新路径。重要的是要注意,必须将/路径放在/api路径之后,否则它将包含/api并将 API 请求指向静态文件服务器。新的 Ingress 如下所示:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: fileserver
port:
number: 8080
# NOTE: this should come after /api or else it will hijack requests
- path: /
pathType: Prefix
backend:
service:
name: fileserver
port:
number: 80
现在你已经为文件服务器设置了一个 Ingress 资源,除了之前设置的 API 的 Ingress 之外,应用的用户界面已经准备好使用了。大多数现代应用程序将静态文件(通常是 HTML 和 JavaScript)与使用 Java、.NET 或 Go 等服务器端编程语言实现的动态 API 服务器结合在一起,更加高效。
使用 Helm 通过参数化你的应用
到目前为止,我们讨论的一切都集中在将服务的单个实例部署到单个集群上。然而,在现实中,几乎每个服务和每个服务团队都需要部署到多个环境(即使它们共享一个集群)。即使你是一个单独开发者,只工作于一个应用程序,你可能也想要至少有一个开发版本和一个生产版本的应用程序,以便你可以迭代和开发而不会影响生产用户。考虑到集成测试和 CI/CD,即使是一个服务和一小撮开发人员,你可能也想要部署至少三种不同的环境,如果考虑处理数据中心级别的故障,可能还会更多。让我们探讨一些部署选项。
对于许多团队来说,最初的失败模式是简单地将文件从一个集群复制到另一个集群。不要只有一个frontend/目录,而是有一个frontend-production/和frontend-development/的目录对。虽然这是一个可行的选择,但也是危险的,因为现在你需要确保这些文件保持彼此同步。如果它们本来就应该完全相同,那么这可能很容易,但由于你将开发新功能,开发和生产之间可能会有一些偏差是预期的。关键是这种偏差既是有意的,又容易管理。
另一个实现这一点的选项是使用分支和版本控制,生产和开发分支从一个中央仓库分开,并且分支之间的差异清晰可见。对一些团队来说,这可能是一个可行的选择,但当你想要同时将软件部署到不同环境(例如,一个部署到多个不同云区域的 CI/CD 系统)时,分支之间的移动机制是具有挑战性的。
因此,大多数人最终会使用一个模板系统。模板系统结合了模板,这些模板形成了应用程序配置的中心骨干,以及专门化模板以特定环境配置。通过这种方式,你可以拥有一个通常共享的配置,同时根据需要进行有意义(和易于理解)的定制。对于 Kubernetes,有各种模板系统,但迄今为止最流行的是Helm。
在 Helm 中,一个应用程序被打包成一组称为chart的文件(在容器和 Kubernetes 的世界中充满了航海笑话)。
一个 chart 以chart.yaml文件开始,该文件定义了 chart 本身的元数据:
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for our frontend journal server.
name: frontend
version: 0.1.0
这个文件放在图表目录的根目录下(例如frontend/)。在这个目录中,有一个templates目录,其中放置了模板。模板基本上是前面示例中的一个 YAML 文件,其中一些值在文件中被替换为参数引用。例如,想象一下,你想要对前端的副本数量进行参数化。以前,部署中有这样的内容:
...
spec:
replicas: 2
...
在模板文件(frontend-deployment.tmpl)中,它看起来像这样:
...
spec:
replicas: {{ .replicaCount }}
...
这意味着当你部署 chart 时,你将用适当的参数替换副本的值。这些参数本身在一个values.yaml文件中定义。每个应用程序应该部署的环境都会有一个 values 文件。对于这个简单的 chart 来说,values 文件看起来像这样:
replicaCount: 2
把这些全部结合起来,你可以使用helm工具来部署这个图表,如下所示:
helm install path/to/chart --values path/to/environment/values.yaml
这将使你的应用程序参数化并部署到 Kubernetes 中。随着时间的推移,这些参数化将增长,涵盖应用程序的各种环境。
部署服务最佳实践
Kubernetes 是一个功能强大的系统,看起来可能很复杂。但如果您遵循以下最佳实践,为应用程序的成功设置基本环境可能会很简单:
-
大多数服务应该部署为部署资源。部署可以创建相同的副本以实现冗余和扩展。
-
可以使用服务来暴露部署,服务实际上是一个负载均衡器。服务可以在集群内(默认)或外部公开。如果要公开一个 HTTP 应用程序,可以使用 Ingress 控制器来添加诸如请求路由和 SSL 等功能。
-
最终,您将希望参数化您的应用程序,以使其在不同环境中的配置更具重用性。像Helm这样的打包工具是进行这种参数化的最佳选择。
摘要
本章构建的应用程序很简单,但几乎包含了构建更大更复杂应用所需的所有概念。理解各部分如何组合以及如何使用基础的 Kubernetes 组件是成功使用 Kubernetes 的关键。
通过版本控制、代码审查和持续交付你的服务来奠定正确的基础,确保无论构建什么,都能够稳固地构建起来。当我们在接下来的章节中深入讨论更高级的主题时,请记住这些基础信息。
第二章:开发工作流程
Kubernetes 的设计初衷是可靠地运行软件。它通过面向应用的 API、自愈特性以及诸如部署(Deployments)之类的有用工具简化了应用程序的部署和管理,实现了零停机滚动升级。虽然所有这些工具都很有用,但它们并没有多少帮助来简化为 Kubernetes 开发应用程序的过程。这就是开发工作流程发挥作用的地方。尽管许多集群设计用于运行生产应用程序,因此很少被开发人员工作流程访问,但关键是要支持开发工作流程以面向 Kubernetes,通常这意味着需要一个或部分用于开发的集群。建立这样一个集群来便捷地开发 Kubernetes 应用程序对确保 Kubernetes 成功非常关键。如果你的集群中没有代码正在构建,那么集群本身并不能完成多少工作。
目标
在我们描述如何构建开发集群的最佳实践之前,值得说明我们对这些集群的目标。显然,最终目标是使开发人员能够在 Kubernetes 上快速轻松地构建应用程序,但在实践中,这究竟意味着什么,又如何在开发集群的实际特性中体现出来呢?
要回答这个问题,让我们首先识别开发人员与集群交互的各个阶段。
第一阶段是入职。这是新开发人员加入团队的时候。这一阶段包括为用户提供集群登录权限,并让他们了解他们的第一个部署。这一阶段的目标是在最短的时间内让开发人员尝试水。你应该为这个过程设定一个关键绩效指标(KPI)目标。一个合理的目标是,用户可以在半小时内从零到当前应用程序的 HEAD 运行。每当有新成员加入团队时,都要测试你们对这个目标的执行情况。
第二阶段是开发。这是开发人员的日常活动。这一阶段的目标是确保快速迭代和调试。开发人员需要能够快速重复地将代码推送到集群,并且需要能够轻松地测试和调试代码,以确保其正常运行。这一阶段的关键绩效指标较难测量,但可以通过测量将拉取请求(PR)或更改在集群中运行的时间,或者通过用户感知生产力的调查来估算。你还可以通过团队的整体生产力来衡量这一指标。
第三阶段是测试。这个阶段与开发交织在一起,用于在提交和合并之前验证代码的有效性。这个阶段的目标有两个。首先,开发者应该能够在提交 PR 之前在他们的环境中运行所有的测试。其次,所有的测试应该在代码合并到仓库之前自动运行。除了这些目标之外,你还应该为测试运行时间设置一个关键绩效指标(KPI)。随着项目变得越来越复杂,测试需要的时间自然会变长。在这种情况下,识别一组更小的烟雾测试,供开发者在提交 PR 前进行初步验证,可能会很有价值。你还应该在测试的不稳定性周围设定非常严格的 KPI。不稳定的测试偶尔会失败。在任何相对活跃的项目中,每千次运行超过一个失败的不稳定性率都会导致开发者之间的摩擦。你需要确保你的集群环境不会产生不稳定的测试。有时不稳定的测试是由于代码问题引起的,但也可能是由于开发环境中的干扰(例如资源耗尽和嘈杂的邻居)引起的。你应该通过测量测试的不稳定性并迅速采取措施来修复,确保你的开发环境没有这些问题。
构建开发集群
当人们开始考虑在 Kubernetes 上进行开发时,首先要做出的选择之一是是构建一个单一的大型开发集群,还是每个开发者一个集群。请注意,这个选择只在动态创建集群容易的环境中才有意义,比如公共云。在物理环境中,可能只有一个大型集群是唯一的选择。
如果你有选择的余地,你应该考虑每个选项的利弊。如果你选择每个用户一个开发集群,这种方法的重大缺点是成本更高、效率更低,而且你将需要管理大量不同的开发集群。额外的成本来自于每个集群可能严重闲置的事实。此外,由于开发者创建了不同的集群,跟踪和回收不再使用的资源变得更加困难。集群每用户的方法的优势在于简单性:每个开发者可以自主管理他们自己的集群,并且由于隔离性,不同开发者之间更难互相干扰。
另一方面,单个开发集群的效率显著提高;你可能能够以三分之一的价格(甚至更少)维持相同数量的开发人员在共享集群上工作。此外,你可以更轻松地安装共享集群服务,例如监控和日志记录,这显著提高了开发人员友好型集群的生产效率。共享开发集群的不利之处在于用户管理的过程以及开发人员之间的潜在干扰。因为目前添加新用户和命名空间到 Kubernetes 集群的流程并不是特别流畅,你需要启动一个新的开发者入职流程。尽管 Kubernetes 资源管理和基于角色的访问控制(RBAC)可以减少两位开发者发生冲突的可能性,但仍然存在用户可能因为消耗过多资源而使开发集群“砖化”,导致其他应用程序和开发者无法调度的情况。此外,你仍需确保开发者不会泄漏和忘记他们创建的资源。然而,这比每位开发者各自创建自己的集群的方法要容易一些。
尽管两种方法都可行,但一般我们建议为所有开发者使用单个大集群。尽管开发者之间的干扰存在挑战,但可以通过管理来解决,而最终的成本效益和轻松为集群添加组织范围的功能,大大超过了干扰的风险。但你需要投资于开发者入职流程、资源管理和垃圾收集的流程。我们建议首先尝试单个大集群作为首选。随着组织的增长(或者如果已经很大),你可能会考虑为每个团队或小组(10 至 20 人)设置一个集群,而不是为数百名用户设置一个巨大的集群。这样可以更容易地进行计费和管理。转向多个集群可能会使保持一致性变得更加复杂,但类似于舰队管理的工具可以更轻松地管理多个集群组。
为多个开发者设置共享集群
在设置大集群时,主要目标是确保多个用户可以同时使用集群而不会相互干扰。区分不同开发者的明显方法是使用 Kubernetes 命名空间。命名空间可以作为服务部署的作用域,以便一个用户的前端服务不会干扰另一个用户的前端服务。命名空间还是 RBAC 的作用域,确保一个开发者不会意外删除另一个开发者的工作。因此,在共享集群中使用命名空间作为开发者的工作空间是有意义的。关于用户入职流程、创建和保护命名空间的具体流程将在以下章节中详细描述。
用户入职流程
在将用户分配到命名空间之前,您必须将该用户加入到 Kubernetes 集群本身。为了实现这一点,有两种选择。您可以使用基于证书的认证来为用户创建一个新的证书,并提供给他们一个kubeconfig文件,以便他们登录;或者您可以配置您的集群以使用外部身份系统(例如,Microsoft Entra ID 或 AWS 身份和访问管理 [IAM])来访问集群。
通常来说,使用外部身份系统是一种最佳实践,因为它不需要您维护两个不同的身份来源。此外,大多数外部系统使用短暂的令牌而不是长期的证书,因此意外泄露令牌会产生有时间限制的安全影响。如果可能的话,您应该限制开发人员通过外部身份提供者证明他们的身份。
不幸的是,在某些情况下这是不可能的,您需要使用证书。幸运的是,您可以使用 Kubernetes 证书 API 来创建和管理这些证书。以下是向现有集群添加新用户的流程。
首先,您需要生成一个证书签名请求来生成一个新的证书。以下是一个简单的 Go 程序来执行此操作:
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"os"
)
func main() {
name := os.Args[1]
user := os.Args[2]
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.pem")
if err != nil {
panic(err)
}
pem.Encode(keyFile, &keyBlock)
keyFile.Close()
commonName := user
// You may want to update these too
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},
}
asn1, err := asn1.Marshal(subject.ToRDNSequence())
if err != nil {
panic(err)
}
csr := x509.CertificateRequest{
RawSubject: asn1,
EmailAddresses: []string{emailAddress},
SignatureAlgorithm: x509.SHA256WithRSA,
}
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 client <user-name>;
这会创建名为client-key.pem和client.csr的文件。然后,您可以运行以下脚本来创建和下载一个新的证书:
#!/bin/bash
csr_name="my-client-csr"
name="${1:-my-user}"
csr="${2}"
cat <<EOF | kubectl create -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: ${csr_name}
spec:
groups:
- system:authenticated
request: $(cat ${csr} | base64 | tr -d '\n')
usages:
- key encipherment
- client auth
EOF
echo
echo "Approving signing request."
kubectl certificate approve ${csr_name}
echo
echo "Downloading certificate."
kubectl get csr ${csr_name} -o jsonpath='{.status.certificate}' \
| base64 --decode > $(basename ${csr} .csr).crt
echo
echo "Cleaning up"
kubectl delete csr ${csr_name}
echo
echo "Add the following to the 'users' list in your kubeconfig file:"
echo "- name: ${name}"
echo " user:"
echo " client-certificate: ${PWD}/$(basename ${csr} .csr).crt"
echo " client-key: ${PWD}/$(basename ${csr} .csr)-key.pem"
echo
echo "Next you may want to add a role-binding for this user."
此脚本将打印出最终信息,您可以将其添加到kubeconfig文件中以启用该用户。当然,该用户没有访问权限,因此您需要为用户应用 Kubernetes RBAC 来授予他们访问命名空间的权限。
创建和保护命名空间
创建命名空间的第一步实际上只是创建它。您可以使用kubectl create namespace my-namespace来完成这个操作。
但事实上,当您创建命名空间时,您希望将一堆元数据附加到该命名空间,例如部署到命名空间的组件的团队联系信息。一般来说,这是以注释的形式存在的;您可以使用某些模板生成 YAML 文件,例如Jinja或其他工具,或者您可以创建然后注释命名空间。执行此操作的简单脚本如下所示:
ns='my-namespace'
team='some team'
kubectl create namespace ${ns}
kubectl annotate namespace ${ns} team=${team}
当命名空间被创建时,您希望通过确保您可以将对命名空间的访问权限授予特定用户来保护它。为了做到这一点,您可以在该命名空间的上下文中将角色绑定到用户。您可以通过在命名空间本身内创建一个RoleBinding对象来完成这个操作。RoleBinding可能看起来像这样:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: example
namespace: my-namespace
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: edit
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: myuser
要创建它,只需运行 kubectl create -f role-binding.yaml。请注意,只要更新绑定中的命名空间指向正确的命名空间,就可以重复使用此绑定。如果确保用户没有其他角色绑定,可以确保此命名空间是用户可以访问的集群的唯一部分。一个合理的做法是还给整个集群授予读取权限;这样开发者就可以看到其他人正在做的事情,以防干扰他们的工作。但是,在授予此类读取访问权限时要小心,因为它将包括对集群中秘密资源的访问。通常在开发集群中这是可以接受的,因为每个人都在同一个组织中,并且秘密资源仅用于开发;然而,如果这是一个问题,那么可以创建一个更精细的角色,消除读取秘密的能力。
如果你想限制特定命名空间消耗的资源,以控制成本或确保资源在开发者间公平分配,可以使用 ResourceQuota 资源为命名空间设置资源总量限制。例如,以下配额限制命名空间内的 pods 的请求和限制为 10 个核心和 100 GB 内存:
apiVersion: v1
kind: ResourceQuota
metadata:
name: limit-compute
namespace: my-namespace
spec:
hard:
# These look a little odd because they're not nested
# but they refer to the requests and limit fields in
# a Pod
requests.cpu: "10"
requests.memory: 100Gi
limits.cpu: 10
limits.memory: 100Gi
管理命名空间
现在你已经了解了如何为新用户注册并创建用作工作空间的命名空间,接下来的问题是如何分配开发者到命名空间。和许多事情一样,并没有单一完美的答案;而是有两种方法。第一种是在注册流程中为每个用户提供其自己的命名空间。这很有用,因为一旦用户注册成功,他们就有了一个专门的工作空间,在这里可以开发和管理他们的应用程序。然而,将开发者的命名空间设置得太持久会鼓励开发者在完成后将东西留在命名空间中,清理和计费各个资源也会变得更加复杂。另一种方法是临时创建和分配有限生存期(TTL)的命名空间。这确保开发者将集群中的资源视为短暂的,并且可以轻松地围绕命名空间的整体删除构建自动化流程,当 TTL 到期时删除整个命名空间。
在有界 TTL 模型中,当开发者想要开始一个新项目时,他们会使用一个工具为项目分配一个新的命名空间。当他们创建命名空间时,它有一些与命名空间相关联的元数据,用于管理和记账。显然,这些元数据包括命名空间的 TTL,但也包括分配给它的开发者、应该分配给命名空间的资源(如 CPU 和内存)、团队及命名空间的用途。这些元数据确保你可以追踪资源使用情况,并在适当的时候删除命名空间。
开发按需分配命名空间的工具可能看起来是一个挑战,但是开发简单的工具相对来说是比较容易的。例如,你可以通过一个简单的脚本来实现分配新命名空间,并提示输入相关的元数据以附加到命名空间。
如果你想更深入地与 Kubernetes 集成,你可以使用自定义资源定义(CRDs)来使用户能够动态地使用 kubectl 工具创建和分配新的命名空间。如果你有时间和意愿,这绝对是一个好的实践,因为它使命名空间管理成为声明式的,还能够启用 Kubernetes RBAC。
当你有工具来启用命名空间的分配时,你还需要添加工具来在其 TTL 到期时收回命名空间。你可以通过一个简单的脚本来实现这一点,该脚本检查命名空间并删除那些 TTL 已过期的命名空间。
你可以将这个脚本构建成一个容器,并使用 ScheduledJob 每小时运行一次。这些组合工具可以确保开发者可以根据需要轻松分配独立资源来进行项目开发,但是这些资源也会在适当的时间间隔内被收回,以确保你不会浪费资源,并且旧资源不会妨碍新的开发。
集群级服务
除了使用工具来分配和管理命名空间之外,还有一些有用的集群级服务,在你的开发集群中启用它们是一个好主意。首先是将日志聚合到一个中央日志服务(LaaS)系统。对于开发者来说,理解他们应用的运行情况最简单的方式之一是将一些内容写入标准输出。虽然你可以通过 kubectl logs 访问这些日志,但该日志长度有限且不易搜索。如果你将这些日志自动发送到 LaaS 系统,如云服务或 Elasticsearch 集群,开发者可以轻松搜索相关信息,并在他们的服务中跨多个容器聚合日志信息。
启用开发者工作流
现在我们已成功设置了共享集群,并可以将新应用程序开发者加入到集群本身,我们需要确实让他们开始开发他们的应用程序。请记住,我们正在衡量的 KPI 之一是从加入到集群到在集群中运行初始应用程序的时间。很明显,通过刚刚描述的加入脚本,我们可以快速为用户在集群中进行身份验证并分配命名空间,但如何开始应用程序呢?不幸的是,尽管有一些技术可以帮助这个过程,但通常需要更多的约定而不是自动化来使初始应用程序运行起来。在以下各节中,我们描述了一种实现这一目标的方法;这绝不是唯一的方法或唯一的解决方案。您可以选择按原样应用此方法,或者受到这些想法的启发以制定您自己的解决方案。
初始设置
部署应用程序的主要挑战之一是安装所有依赖项。在许多情况下,特别是在现代微服务架构中,要想在其中一个微服务上开始开发,通常需要部署多个依赖项,无论是数据库还是其他微服务。虽然应用程序本身的部署相对简单,但识别和部署所有依赖项以构建完整应用程序的任务常常是试错和不完整或过时说明的结合,这是一个令人沮丧的过程。
要解决这个问题,通常有必要引入一种约定来描述和安装依赖项。这可以被视为类似于npm install的等效物,后者安装所有所需的 JavaScript 依赖项。最终,可能会有类似于npm的工具,为基于 Kubernetes 的应用程序提供此服务,但在那之前,依赖于团队内部的约定是最佳实践。
一种约定的选项是在所有项目仓库的根目录中创建一个setup.sh脚本。此脚本的责任是在特定命名空间内创建所有依赖项,以确保正确创建所有应用程序的依赖项。例如,设置脚本可能如下所示:
kubectl create my-service/database-stateful-set-yaml
kubectl create my-service/middle-tier.yaml
kubectl create my-service/configs.yaml
您可以通过将以下内容添加到您的package.json来将此脚本集成到 npm 中:
{
...
"scripts": {
"setup": "./setup.sh",
...
}
}
使用这个设置,新开发者只需运行npm run setup,就可以安装集群依赖项。显然,这种特定的集成是针对 Node.js/npm 的。在其他编程语言中,最好使用特定于语言的工具集成。例如,在 Java 中,可能会与Maven 的 pom.xml文件集成。
对于更通用的工作流程,GitHub 和 Visual Studio Code 最近都已经标准化了“devcontainers”,这些容器由存储在仓库的 .devcontainer/ 文件夹中的 Dockerfile 描述。构建时,它们会为该仓库的开发提供一个完整的环境。
启用活跃开发
在设置了开发者工作空间及所需依赖项后,下一步是让开发者能够快速迭代他们的应用程序。这其中的第一个前提是能够构建并推送一个容器镜像。假设您已经设置了这一点;如果没有,您可以在其他在线资源和书籍中了解如何操作。
在构建并推送容器镜像之后,任务是将其部署到集群中。与传统的部署不同,在开发者迭代的情况下,维护可用性实际上并不是一个问题。因此,部署新代码的最简单方式是简单地删除与前一次部署相关联的 Deployment 对象,然后创建一个指向新构建镜像的新 Deployment。也可以直接更新现有的 Deployment,但这会触发 Deployment 资源中的部署逻辑。虽然可以配置 Deployment 快速部署代码,但这样做会引入开发环境和生产环境之间的差异,可能会带来危险或不稳定性。例如,假设您意外地将部署的开发配置推送到生产环境中,则会突然在生产环境中部署新版本,而没有适当的测试和部署阶段之间的延迟。由于存在这种风险,并且有替代方案,最佳实践是删除并重新创建 Deployment。
就像安装依赖项一样,为执行此部署编写脚本也是一个好习惯。例如,deploy.sh 脚本可能如下所示:
kubectl delete -f ./my-service/deployment.yaml
perl -pi -e 's/${old_version}/${new_version}/' ./my-service/deployment.yaml
kubectl create -f ./my-service/deployment.yaml
与之前一样,您可以将其集成到现有的编程语言工具中,以便(例如)开发者只需运行 npm run deploy 就可以将他们的新代码部署到集群中。
在构建此自动化过程时,通常有必要将其集成到持续集成和交付(CI/CD)工具中,例如 GitHub Actions、Azure DevOps 或 Jenkins。与 CI/CD 工具的集成可以更轻松地实现进一步的自动化,例如在合并开发者的 PR 后自动部署。
启用测试和调试
用户成功部署其应用的开发版本后,需要对其进行测试,并在有问题时调试应用程序。在 Kubernetes 中进行开发时,这也可能是一个障碍,因为并不总是清楚如何与集群交互。kubectl命令行是一个多功能工具箱,可以通过其中的工具(例如kubectl logs、kubectl exec和kubectl port-forward)来实现这一点,但学习如何使用所有不同的选项并熟悉工具可能需要相当多的经验。此外,由于该工具在终端中运行,通常需要组合多个窗口同时检查应用程序的源代码和运行的应用程序本身。
为了简化测试和调试体验,Kubernetes 工具正在越来越多地集成到开发环境中,例如 Visual Studio(VS)Code 用于 Kubernetes 的开源扩展。该扩展可以免费从 VS Code 市场轻松安装。安装后,它会自动发现您在kubeconfig文件中已有的任何集群,并提供一个树形视图导航窗格,让您可以一目了然地查看集群内容。
除了能够一目了然地查看集群状态之外,集成还允许开发者以直观、可发现的方式使用通过kubectl可用的工具。从树形视图中,如果右键单击 Kubernetes pod,您可以立即使用端口转发将网络连接从 pod 直接引导到本地计算机。同样,您还可以访问 pod 的日志,甚至在运行中的容器内获取终端。
这些命令与典型的用户界面期望(例如,右键显示上下文菜单)的集成,以及这些体验与应用程序代码本身的集成,使具有最少 Kubernetes 经验的开发者能够迅速在开发集群中提高生产力。
当然,这个 VS Code 扩展并不是 Kubernetes 和开发环境之间唯一的集成方式;根据您的编程环境和风格(如vi、emacs等),还有其他几种可以安装的集成方式。
建立开发环境最佳实践
在 Kubernetes 上设置开发工作流程对于生产力至关重要,对于积极、快乐的开发团队也是至关重要。遵循这些最佳实践将有助于确保开发者能够快速上手:
-
考虑开发者体验的三个阶段:入职、开发和测试。确保您构建的开发环境支持这三个阶段。
-
在构建开发集群时,您可以选择一个大型集群或每个开发者一个集群。每种方式都有其利弊,但通常来说,一个单一的大型集群是一个更好的选择。
-
当您向集群添加用户时,请使用他们自己的身份和对他们自己命名空间的访问权限。使用资源限制来限制他们可以使用集群的量。
-
当管理命名空间时,请考虑如何收割旧的、未使用的资源。开发者常常不注意删除未使用的东西。可以利用自动化来清理这些资源。
-
考虑设置所有用户都可以使用的集群级服务,如日志和监控。有时候,像数据库这样的集群级依赖项也可以通过类似 Helm 图表的模板来为所有用户设置。
概要
我们已经到了一个阶段,在这个阶段中,在云环境中创建 Kubernetes 集群已经是一个相对简单的练习,但是使开发者能够有效地使用这样的集群却明显不那么明显和简单。在考虑如何使开发者成功地在 Kubernetes 上构建应用程序时,重要的是考虑关于入门、迭代、测试和调试应用程序的关键目标。同样,值得投资于一些专门用于用户入门、命名空间供应以及基本日志聚合等集群服务的基本工具。将开发集群和代码库视为标准化和应用最佳实践的机会,将确保您拥有快乐和高效的开发者成功地构建代码并部署到生产 Kubernetes 集群中。
第三章:Kubernetes 中的监控和日志记录
在本章中,我们将讨论在 Kubernetes 中监控和日志记录的最佳实践。我们将深入探讨不同监控模式的细节,收集重要的度量指标,并从这些原始度量指标构建仪表板。然后,我们以实现 Kubernetes 集群监控的示例结束。
度量与日志
您首先需要理解日志收集和度量收集之间的区别。它们是互补的,但提供不同的用途:
度量
一系列在一段时间内测量的数字。
日志
日志记录了程序运行时发生的所有事件,包括任何错误、警告或显著事件。
在应用性能不佳时,您可能需要同时使用度量和日志记录的示例是,我们第一次发现问题可能是由于托管应用程序的 Pod 上高延迟的警报,但度量可能无法明确问题。然后我们可以查看日志以调查应用程序发出的错误。
监控技术
封闭盒监控侧重于从应用程序外部进行监控,传统上用于监控诸如 CPU、内存、存储等组件的系统。封闭盒监控仍然可以用于基础设施级别的监控,但缺乏对应用程序操作方式的洞察和上下文。例如,为了测试集群是否健康,我们可以调度一个 Pod,如果成功,我们知道调度器和服务发现在我们集群内部是健康的,因此可以假设集群组件是健康的。
开放盒监控侧重于在应用程序状态上下文中的详细信息,例如总 HTTP 请求、500 错误的数量、请求的延迟等。通过开放盒监控,我们可以开始理解系统状态的原因。它允许我们提出问题:“为什么磁盘填满了?”而不仅仅是陈述:“磁盘填满了。”
监控模式
您可能会审视监控并说:“这有多难?我们一直在监控我们的系统。”监控的概念并不新鲜,我们有许多工具可以帮助我们了解系统的运行情况。但像 Kubernetes 这样的平台更具动态性和短暂性,因此您需要改变对如何监控这些环境的思考方式。例如,当监控虚拟机(VM)时,您期望该 VM 在线 24/7,并且其所有状态得到保留。在 Kubernetes 中,Pod 可能非常动态和短暂,因此您需要有能够处理这种动态和瞬态性质的监控系统。
在监控分布式系统时,有两种监控模式值得关注。由 Brendan Gregg 推广的USE方法,关注以下内容:
-
U—利用率
-
S—饱和度
-
E—错误
该方法侧重于基础设施监控,因为在应用级监控中使用它存在限制。USE 方法被描述为“对于每个资源,检查利用率、饱和度和错误率”。该方法可以帮助您快速识别系统的资源约束和错误率。例如,要检查集群中节点的网络健康状况,您将希望监控利用率、饱和度和错误率,以便轻松识别任何网络瓶颈或网络堆栈中的错误。USE 方法是更大工具箱中的一种工具,不是您监控系统时唯一使用的方法。
另一种监控方法称为RED方法,由 Tom Wilkie 推广。RED 方法专注于以下内容:
-
R—Rate
-
E—错误
-
D—持续时间
这一理念源于 Google 的四个黄金信号:
延迟
提供请求所需的时间
流量
您的系统承受了多少需求
错误
请求失败的速率
饱和度
您的服务被多少程度利用
例如,您可以使用此方法来监控在 Kubernetes 中运行的前端服务,以计算以下内容:
-
我的前端服务正在处理多少个请求?
-
服务用户收到多少个 500 错误?
-
服务是否被请求过度利用?
正如您从上一个例子中看到的,这种方法更关注用户对服务的体验。
USE 和 RED 方法是互补的,因为 USE 方法侧重于基础设施组件,而 RED 方法侧重于监控应用程序的最终用户体验。
Kubernetes 指标概述
现在我们知道了不同的监控技术和模式,让我们看看您应该在 Kubernetes 集群中监控哪些组件。Kubernetes 集群包括控制平面组件和节点组件。控制平面组件包括 API 服务器、etcd、调度器和控制器管理器。节点包括 kubelet、容器运行时、kube-proxy、kube-dns 和 pods。您需要监控所有这些组件,以确保集群和应用程序的健康。
Kubernetes 通过多种方式公开这些指标,因此让我们看看您可以用来收集集群内指标的不同组件。
cAdvisor
容器监视器,或者说 cAdvisor,是一个收集运行在节点上容器资源和指标的开源项目。cAdvisor 内置于运行在集群每个节点上的 Kubernetes kubelet 中。它通过 Linux 控制组(cgroup)树收集内存和 CPU 指标。如果您对 cgroups 不熟悉,它是 Linux 内核的一个功能,允许隔离 CPU、磁盘 I/O 或网络 I/O 资源。cAdvisor 还将通过 Linux 内核内置的 statfs 收集磁盘指标。这些是您不必过于担心的实现细节,但您应了解这些指标如何公开以及您可以收集的信息类型。您应将 cAdvisor 视为所有容器指标的真实来源。
Metrics Server
Kubernetes metrics server 和 Metrics Server API 取代了已弃用的 Heapster。Heapster 在实现数据汇聚方面存在一些架构上的劣势,导致核心 Heapster 代码库中出现了许多供应商解决方案。通过在 Kubernetes 中实现资源和自定义指标 API 作为聚合 API,解决了这个问题,允许在不改变 API 的情况下切换实现。
在 Metrics Server API 和 metrics server 中有两个方面需要理解。
首先,资源指标 API 的典范实现是 metrics server。metrics server 收集诸如 CPU 和内存之类的资源指标。它从 kubelet 的 API 收集这些指标,然后将它们存储在内存中。Kubernetes 在调度器、水平 Pod 自动伸缩器(HPA)和垂直 Pod 自动伸缩器(VPA)中使用这些资源指标。
第二,自定义指标 API 允许监控系统收集任意指标。这使得监控解决方案可以构建自定义适配器,从而允许扩展核心资源指标之外的功能。例如,Prometheus 构建了第一个自定义指标适配器,允许您基于自定义指标使用 HPA。这打开了基于您的使用情况进行更好扩展的可能性,因为现在您可以引入队列大小等指标,并根据可能是 Kubernetes 外部的指标进行扩展。
现在有了标准化的 Metrics API,这开启了许多超越传统 CPU 和内存指标的可能性。
kube-state-metrics
kube-state-metrics 是一个 Kubernetes 插件,用于监控存储在 Kubernetes 中的对象。在 cAdvisor 和 Metrics Server 用于提供详细的资源使用情况指标时,kube-state-metrics 专注于识别部署到集群的 Kubernetes 对象的条件。
下面是 kube-state-metrics 可以为您解答的一些问题:
-
Pod
-
集群部署了多少个 Pod?
-
有多少个 Pod 处于挂起状态?
-
是否有足够的资源来处理 Pod 的请求?
-
-
部署
-
有多少个 Pod 处于运行状态与期望状态?
-
有多少个副本可用?
-
哪些部署已更新?
-
-
节点
-
我的节点状态如何?
-
我的集群中可分配的 CPU 核心有哪些?
-
是否有任何不可调度的节点?
-
-
任务
-
一个任务何时开始?
-
一个任务何时完成?
-
多少个任务失败了?
-
截至本文撰写时,kube-state-metrics 跟踪许多对象类型。这些类型始终在扩展,您可以在 GitHub 仓库 中找到文档。
我应该监控哪些指标?
简单的答案是“一切”,但如果您尝试监视过多,可能会产生噪音,过滤掉您需要洞察的真实信号。当我们考虑 Kubernetes 中的监控时,我们希望采用一种分层方法,考虑以下内容:
-
物理或虚拟节点
-
集群组件
-
集群附加组件
-
最终用户应用程序
使用这种分层监控方法可以更轻松地识别监控系统中的正确信号。它使您能够更有针对性地解决问题。例如,如果您的 Pod 进入挂起状态,您可以从节点资源利用开始,如果一切正常,您可以针对集群级组件。
下面是您系统中希望关注的指标:
-
节点
-
CPU 利用率
-
内存利用率
-
网络利用率
-
磁盘利用率
-
-
集群组件
- etcd 延迟
-
集群附加组件
-
集群自动缩放器
-
Ingress 控制器
-
-
应用程序
-
容器内存利用率和饱和度
-
容器 CPU 利用率
-
容器网络利用率和错误率
-
特定应用程序框架的指标
-
监控工具
许多监控工具可以与 Kubernetes 集成,并且每天都有更多工具加入,扩展其功能集以更好地与 Kubernetes 集成。以下是几个流行的与 Kubernetes 集成的工具:
Prometheus
Prometheus 是一个最初在 SoundCloud 构建的开源系统监控和警报工具包。自 2012 年成立以来,许多公司和组织都采用了 Prometheus,并且该项目拥有非常活跃的开发者和用户社区。现在它是一个独立的开源项目,独立于任何公司的维护。为了强调这一点,并澄清项目的治理结构,Prometheus 在 2016 年作为第二个托管项目加入了云原生计算基金会(CNCF),紧随 Kubernetes 之后。
InfluxDB
InfluxDB 是一个设计用于处理高写入和查询负载的时间序列数据库。它是 TICK(Telegraf、InfluxDB、Chronograf 和 Kapacitor)堆栈的重要组成部分。InfluxDB 旨在作为任何涉及大量时间戳数据的用例的后端存储使用,包括 DevOps 监控、应用程序指标、IoT 传感器数据和实时分析。
Datadog
Datadog 提供云规模应用程序的监控服务,通过基于 SaaS 的数据分析平台监控服务器、数据库、工具和服务。
Sysdig
Sysdig 监控是一个商业工具,为容器本地应用程序提供 Docker 监控和 Kubernetes 监控。Sysdig 还允许您收集、关联和查询具有直接 Kubernetes 集成的 Prometheus 指标。
云提供商工具
所有主要的云提供商都为其不同的解决方案提供监控工具。这些工具通常集成到云提供商的生态系统中,并为监控您的 Kubernetes 集群提供一个良好的起点。以下是一些云提供商工具的例子:
GCP Stackdriver
Stackdriver Kubernetes 引擎监视旨在监视 Google Kubernetes 引擎(GKE)集群。它管理监视和日志服务,并且其界面提供了专为 GKE 集群定制的仪表板。Stackdriver 监视提供了对云驱动应用程序性能、可用性和总体健康的可见性。它从 Google 云平台(GCP)、亚马逊网络服务(AWS)、托管的正常运行时间探针和应用程序仪表化中收集指标、事件和元数据。
Microsoft Azure 容器监视器
Azure 容器监视器是一个旨在监视部署到 Azure 容器实例或托管在 Azure Kubernetes 服务上的托管 Kubernetes 集群的性能的功能。在运行生产集群时,特别是在规模化和多应用程序情况下,监视您的容器至关重要。Azure 容器监视器通过从控制器、节点和 Kubernetes 中通过 Metrics API 可用的容器收集内存和处理器指标来提供性能可见性。还收集容器日志。在从 Kubernetes 集群启用监视后,指标和日志将通过 Linux 版本的 Log Analytics 代理自动收集。
AWS 容器洞察
如果您使用亚马逊弹性容器服务(ECS)、亚马逊弹性 Kubernetes 服务或其他基于亚马逊 EC2 的 Kubernetes 平台,您可以使用 CloudWatch 容器洞察收集、聚合和总结容器化应用程序和微服务的指标和日志。这些指标包括 CPU、内存、磁盘和网络等资源的利用率。容器洞察还提供诊断信息,例如容器重启失败,帮助您隔离问题并快速解决。
在考虑实施监控指标工具时,一个重要的方面是查看指标存储方式。提供带有键/值对的时间序列数据库的工具将为您的指标提供更高程度的属性。
提示
始终评估您已经拥有的监控工具,因为采用新的监控工具会有学习曲线和操作实施成本。许多监控工具现在已经集成到 Kubernetes 中,因此评估您今天拥有的工具及其是否能满足您的要求是非常重要的。
使用 Prometheus 监视 Kubernetes
在本节中,我们关注使用 Prometheus 监控指标,它与 Kubernetes 标签、服务发现和元数据有很好的集成。本章节实施的高级概念也适用于其他监控系统。
Prometheus 是由 CNCF 托管的开源项目。它最初在 SoundCloud 开发,其许多概念基于 Google 的内部监控系统 Borgmon。它实现了一个多维数据模型,使用键值对的方式工作,类似于 Kubernetes 的标签系统。Prometheus 以人类可读的格式暴露指标,如下面的例子所示:
# HELP node_cpu_seconds_total Seconds the CPU is spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="0",mode="idle"} 5144.64
node_cpu_seconds_total{cpu="0",mode="iowait"} 117.98
Prometheus 使用拉模型来收集指标,在这种模型中,它会获取一个指标端点以收集和摄取指标到 Prometheus 服务器中。像 Kubernetes 这样的系统已经以 Prometheus 格式暴露了它们的指标,使得收集指标变得简单。许多其他的 Kubernetes 生态系统项目(如 NGINX、Traefik、Istio、Linkerd 等)也以 Prometheus 格式暴露它们的指标。Prometheus 还可以使用导出器,允许您获取服务发出的指标并将其转换为 Prometheus 格式的指标。
Prometheus 具有非常简化的架构,如图 3-1 所示。

图 3-1. Prometheus 架构
提示
您可以在集群内部或集群外部安装 Prometheus。从“实用集群”监控您的集群是一个好的实践,以避免生产问题也影响您的监控系统。像Thanos这样的工具为 Prometheus 提供了高可用性,并允许将指标导出到外部存储系统。
对 Prometheus 架构的深入讨论超出了本书的范围,您应参考专门的图书。Prometheus: Up & Running(O'Reilly)是一个很好的深入学习的起点。
因此,让我们深入了解并在我们的 Kubernetes 集群上设置 Prometheus。有许多不同的部署方式可以部署 Prometheus,部署方式将取决于您的具体实现。我们将使用 Helm 安装 Prometheus Operator:
Prometheus 服务器
拉取和存储从系统收集的指标。
Prometheus Operator
使 Prometheus 配置与 Kubernetes 原生化,并管理和操作 Prometheus 和 Alertmanager 集群。允许您通过本机 Kubernetes 资源定义创建、销毁和配置 Prometheus 资源。
Node Exporter
从 Kubernetes 集群中的节点导出主机指标。
kube-state-metrics
收集特定于 Kubernetes 的指标。
Alertmanager
允许您配置并将警报转发到外部系统。
Grafana
为 Prometheus 提供仪表板能力的可视化。
首先,我们将开始设置 minikube 以部署 Prometheus。我们使用 Mac,所以我们将使用brew来安装 minikube。你也可以从minikube 网站安装 minikube。
brew install minikube
现在我们将安装 kube-prometheus-stack(前身为 Prometheus Operator),并准备好我们的集群以开始监控 Kubernetes API 服务器的变化。
创建一个用于监控的命名空间:
kubectl create ns monitoring
添加 prometheus-community Helm chart 仓库:
helm repo add prometheus-community
https://prometheus-community.github.io/helm-charts
添加 Helm Stable chart 仓库:
helm repo add stable https://charts.helm.sh/stable
更新 chart 仓库:
helm repo update
安装 kube-prometheus-stack chart:
helm install --namespace monitoring prometheus
prometheus-community/kube-prometheus-stack
让我们检查确保所有的 Pod 都在运行:
kubectl get pods -n monitoring
如果安装正确,您应该看到以下 Pod:
kubectl get pods -n monitoring
NAME READY STATUS RESTARTS AGE
alertmanager-prometheus-kube-prometheus-alertm... 2/2 Running 1 79s
prometheus-grafana-6f7cf9b968-xtnzj 3/3 Running 0 97s
prometheus-kube-prometheus-operator-7bdb94567b... 1/1 Running 0 97s
prometheus-kube-state-metrics-6bdd65d76-s5r5j 1/1 Running 0 97s
prometheus-prometheus-kube-prometheus-promethe... 2/2 Running 0 78s
prometheus-prometheus-node-exporter-dgrlf 1/1 Running 0 98s
现在我们将创建一个到包含在 kube-prometheus-stack 中的 Grafana 实例的隧道。这将允许我们从本地机器连接到 Grafana:
这将在我们的本地主机上创建到端口 3000 的隧道。现在我们可以打开 Web 浏览器,并连接到http://127.0.0.1:3000上的 Grafana。
在本章前面我们已经讨论了使用 USE 方法,所以让我们收集一些有关 CPU 利用率和饱和度的节点指标。Kube-prometheus-stack 提供了这些我们想要追踪的常见 USE 方法指标的可视化。你安装的 kube-prometheus-stack 的一个很棒的功能是它带有预构建的 Grafana 仪表板,你可以使用它们。
现在我们将创建一个到包含在 kube-prometheus-stack 中的 Grafana 实例的隧道。这将允许我们从本地机器连接到 Grafana:
kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80
将您的 Web 浏览器指向http://localhost:3000,并使用以下凭据登录:
-
用户名:admin
-
密码:prom-operator
在 Grafana 仪表板下,你会找到一个名为 Kubernetes / USE Method / Cluster 的仪表板。这个仪表板为你提供了对 Kubernetes 集群利用率和饱和度的良好概述,这是 USE 方法的核心。Figure 3-2 展示了仪表板的一个示例。

图 3-2. 一个 Grafana 仪表板
接下来花点时间探索不同的仪表板和可以在 Grafana 中可视化的指标。
提示
避免创建太多的仪表板(即“图形墙”),因为这可能会让工程师在故障排除情况下难以理解。你可能认为在仪表板中有更多信息意味着更好的监控,但大多数情况下,这会让用户在看仪表板时更加困惑。将你的仪表板设计重点放在结果和解决时间上。
日志概述
到目前为止,我们已经讨论了很多关于指标和 Kubernetes 的内容,但为了全面了解您的环境,您还需要从部署到集群的 Kubernetes 和应用程序中收集和集中日志。在日志记录方面,可能很容易说,“让我们记录所有事情”,但这会导致两个问题:
-
噪音太大,无法快速找到问题。
-
日志可能消耗大量资源,并且成本高昂。
对于应该记录的具体内容并没有明确的答案,因为调试日志变成了一种必要的恶。随着时间的推移,您将开始更好地了解您的环境,并学会从日志系统中消除噪音。此外,为了解决不断增加的日志存储量,您需要实施保留和归档策略。从最终用户的体验来看,保留 30 到 45 天的历史日志是一个不错的选择。这样可以用于调查长时间内出现的问题,同时减少存储日志所需的资源量。如果出于合规原因需要长期存储,您将需要将日志存档到成本更低的资源上。
在 Kubernetes 集群中,有多个组件需要记录日志。以下是应收集指标的组件列表:
-
节点日志
-
Kubernetes 控制平面日志
-
API 服务器
-
控制管理器
-
调度器
-
-
Kubernetes 审计日志
-
应用程序容器日志
对于节点日志,您希望收集发生在关键节点服务上的事件。例如,您将希望收集运行在节点上的 Docker 守护程序的日志。健康的 Docker 守护程序对于在节点上运行容器至关重要。收集这些日志将帮助您诊断可能遇到的任何 Docker 守护程序问题,并为您提供有关守护程序底层问题的信息。此外,还有其他关键服务的日志,您也会希望从底层节点记录。
Kubernetes 控制平面由多个组件组成,您需要收集这些日志以深入了解其中的潜在问题。Kubernetes 控制平面对于集群的健康至关重要,您将希望聚合它存储在主机上的日志,包括 /var/log/kube-APIserver.log、/var/log/kube-scheduler.log 和 /var/log/kube-controller-manager.log。控制管理器负责创建用户定义的对象。例如,作为用户,您使用类型为 LoadBalancer 的 Kubernetes 服务,它仅处于待定状态;Kubernetes 事件可能不会提供足够的细节来诊断问题。如果您将日志收集到集中系统中,将更详细地了解底层问题,并更快地进行调查。
您可以将 Kubernetes 审计日志视为安全监控,因为它们可以帮助您了解系统内谁做了什么。这些日志可能会非常嘈杂,因此您需要为您的环境进行调整。在初始化时,这些日志可能会在日志系统中造成巨大的峰值,因此请确保遵循 Kubernetes 文档中关于审计日志监控的指导。
应用程序容器日志能让你深入了解应用程序实际发出的日志。你可以通过多种方式将这些日志转发到中央存储库。第一种推荐的方式是将所有应用程序日志发送到 STDOUT,因为这样可以统一应用程序日志记录的方式,监控的守护程序集可以直接从 Docker 守护程序中获取这些日志。另一种方式是使用 sidecar 模式,在 Kubernetes Pod 中应用程序容器旁边运行一个日志转发容器。如果你的应用程序将日志记录到文件系统,可能需要使用这种模式。
注意
对于管理 Kubernetes 审计日志,有许多选项和配置。这些审计日志可能非常嘈杂,并且记录所有操作可能很昂贵。你应该考虑查看 审计日志文档,以便为你的环境调整这些日志。
日志工具
与收集度量指标一样,有许多工具用于从运行在集群中的 Kubernetes 和应用程序中收集日志。你可能已经为此拥有工具,但要注意工具如何实现日志记录。该工具应具有作为 Kubernetes DaemonSet 运行的能力,并且对于不将日志发送到 STDOUT 的应用程序,应该有一个作为 sidecar 运行的解决方案。使用现有工具的一个优势是你已经对该工具的操作有了一定的了解。
一些与 Kubernetes 集成比较流行的工具包括:
-
Loki
-
Elastic Stack
-
Datadog
-
Sumo Logic
-
Sysdig
-
云服务提供商的服务(如 GCP Stackdriver、Azure Monitor for containers 和 Amazon CloudWatch)
在寻找集中日志工具时,托管解决方案可以提供很多价值,因为它们可以减少很多运营成本。在第 N 天使用自己托管的日志解决方案看起来很不错,但随着环境的增长,维护这个解决方案可能会非常耗时。
使用 Loki-Stack 进行日志记录
为了本书的目的,我们在集群中使用 Loki-Stack 和 prom-tail 进行日志记录。实施 Loki-Stack 可以是一个很好的起步方式,但是在某个时候,你可能会问自己,“管理自己的日志平台真的值得吗?”通常情况下,这并不值得,因为自托管的日志解决方案一开始很棒,但随着时间推移变得过于复杂。随着环境的扩展,自托管的日志解决方案变得操作上更加复杂。并没有一个正确答案,所以要评估你的业务需求是否需要你托管自己的解决方案。此外,还有一个由 Grafana 提供的托管 Loki 解决方案,所以如果你选择不自己托管,你总是可以轻松地转移。
我们将使用以下内容进行日志堆栈:
-
Loki
-
prom-tail
-
Grafana
使用以下步骤将 Loki-Stack 通过 Helm 部署到你的 Kubernetes 集群。
添加 Loki-Stack Helm 仓库:
helm repo add grafana https://grafana.github.io/helm-charts
更新 Helm 仓库:
helm repo update
helm upgrade --install loki --namespace=monitoring grafana/loki-stack
这将部署 Loki 与 prom-tail,使我们能够将日志转发到 Loki 并使用 Grafana 可视化日志。
您应该看到以下 Pod 部署到您的集群中:
kubectl get pods -n monitoring
NAME READY STATUS RESTARTS AGE
loki-0 1/1 Running 0 93s
loki-promtail-x7nw8 1/1 Running 0 93s
当所有 Pod 都处于“Running”状态后,请通过端口转发连接到我们的本地 Grafana:
kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80
现在,请将您的 Web 浏览器指向 http://localhost:3000,并使用以下凭据登录:
-
用户名:admin
-
密码:prom-operator
在 Grafana 配置下,您会找到数据源,如 图 3-3 所示。然后,我们将 Loki 添加为一个 数据源。

图 3-3. Grafana 数据源
然后,我们将添加一个新的数据源,并将 Loki 添加为数据源(见图 3-4)。

图 3-4. Loki 数据源
在 Loki 设置页面(见图 3-5),用 http://loki:3100 填写 URL,然后点击保存并测试按钮。

图 3-5. Loki 配置
在 Grafana 中,您可以对日志执行即席查询,并可以构建仪表板,以便为您提供环境概述。
要探索 Loki-Stack 收集的日志,我们可以在 Grafana 中使用 Explore 功能,如 图 3-6 所示。这将允许我们对已收集的日志运行查询。

图 3-6. 探索 Loki 日志
对于标签过滤器,您将需要以下过滤器:
namespace = kube-system
前往 Loki 和 Grafana 中可视化的不同日志,花些时间进行探索。
警报
警报是一把双刃剑,您需要在应该报警的事件与仅需监控的事件之间保持平衡。过多的报警会导致警报疲劳,重要事件将会在所有噪音中丢失。一个例子是每当 Pod 失败时生成一个警报。您可能会问:“为什么我不想监视 Pod 失败?”好吧,Kubernetes 的美妙之处在于它提供了功能来自动检查容器的健康状态并自动重启容器。您真正想要关注的是影响您的服务级别目标(SLO)的事件。SLO 是一些具体可测的特性,例如可用性、吞吐量、频率和响应时间,这些特性是您与服务最终用户协商达成的。设定 SLO 可以为最终用户明确系统行为的期望。没有 SLO,用户可能会形成他们对服务的不切实际期望。在像 Kubernetes 这样的系统中,警报需要一种全新的方法,需要专注于最终用户如何体验服务。例如,如果您前端服务的 SLO 是 20 毫秒的响应时间,而您看到的延迟高于平均水平,您应该对此问题进行警报。
你需要决定哪些警报是有效的,并需要干预。在典型的监控中,你可能习惯于对高 CPU 使用率、内存使用率或无响应的进程发出警报。这些看起来可能是好的警报,但它们可能并不表示需要立即采取行动并通知值班工程师的问题。需要通知值班工程师的警报应该是需要立即人工关注,并且影响应用程序用户体验的问题。如果你曾经经历过“问题自行解决”的情况,那么这很可能是警报无需联系值班工程师的一个很好的指示。
处理不需要立即行动的警报的一种方法是专注于自动化处理原因的修复。例如,当磁盘空间不足时,你可以自动删除日志以释放磁盘空间。此外,在应用部署中利用 Kubernetes 的liveness probes可以帮助自动处理应用程序中无响应的进程问题。
在建立警报时,你还需要考虑警报阈值;如果设置的阈值太短,那么警报可能会产生很多误报。通常建议至少设置一个五分钟的阈值,以帮助消除误报。制定标准阈值可以帮助定义一个标准,并避免管理许多不同的阈值。例如,你可能想要遵循一个特定的模式,如 5 分钟、10 分钟、30 分钟、1 小时等。
在建立用于警报的通知时,你需要确保通知中提供相关信息。例如,你可以提供一个指南链接,该指南提供解决问题的故障排除或其他有用信息。你还应该在通知中包含有关数据中心、区域、应用程序所有者和受影响系统的信息。提供所有这些信息将使工程师能够快速形成关于问题的理论。
你还需要建立通知渠道来路由触发的警报。在思考“触发警报时应该通知谁?”时,你应确保通知不仅仅发送到分发列表或团队邮箱。如果警报发送到较大的群组,通常会因为用户认为这些是噪音而被过滤掉。你应将通知路由到将负责问题的用户。
在警报中,你永远无法在第一天就做到完美,甚至可以说可能永远不会完美。你只需要确保逐步改进警报以预防警报疲劳,这可能会导致员工疲劳和系统问题的多种问题。
注意
若要进一步了解如何处理警报和管理系统,请阅读基于 Rob Ewaschuk 作为 Google 站点可靠性工程师(SRE)的观察所著的《关于警报的我的哲学》。
监控、日志记录和警报的最佳实践
以下是关于监控、日志记录和警报您应该采纳的最佳实践。
监控
-
监控节点和所有 Kubernetes 组件的利用率、饱和度和错误率,监控应用程序的速率、错误和持续时间。
-
使用封闭箱监控来监控系统的症状,而不是系统的预测健康状态。
-
使用开箱即用的监控工具来检查系统及其内部情况,并进行仪表化。
-
实施基于时间序列的指标以获得高精度的度量,同时还能洞察您应用程序行为。
-
利用像 Prometheus 这样提供高维度关键标签的监控系统;这将更好地显示受影响问题的信号。
-
使用平均指标来可视化小计和基于事实数据的指标。利用总计指标来可视化特定指标的分布。
日志记录
-
您应该结合使用日志记录和指标监控来全面了解环境运行情况。
-
要谨慎存储超过 30 至 45 天的日志;如果需要,使用更便宜的资源进行长期存档。
-
限制在 Sidecar 模式中使用日志转发器的使用,因为它们会消耗更多资源。建议使用 DaemonSet 来进行日志转发,并将日志发送到 STDOUT。
警报
-
要小心警报疲劳,因为它可能导致人员和流程产生不良行为。
-
始终努力改进警报功能,并接受它不会始终完美。
-
警报用于影响您的 SLO 和客户的症状,而不是不需要立即人工干预的短暂问题。
总结
在本章中,我们讨论了用于通过度量和日志收集监控系统的模式、技术和工具。从本章中最重要的部分可以得出的结论是,您需要重新考虑如何进行监控,并从一开始就这样做。我们经常看到这种实施是事后进行的,这可能会导致对系统理解上的非常不利影响。监控的目的在于更好地了解系统,并能够提供更好的弹性,从而为应用程序提供更好的最终用户体验。监控分布式应用程序和像 Kubernetes 这样的分布式系统需要大量工作,因此您必须在旅程的开始时做好准备。
第四章:配置、秘密和 RBAC
容器的可组合性使我们作为运维人员能够在运行时将配置数据引入容器中。这使得我们能够将应用程序的功能与其运行环境分离。通过容器运行时允许的约定,可以通过环境变量或在运行时将外部卷挂载到容器中,有效地改变应用程序的配置。作为开发人员,重要的是要考虑这种行为的动态性,并允许使用环境变量或从应用程序运行时用户可用路径读取配置数据。
将敏感数据(如秘密)移入原生 Kubernetes API 对象时,理解 Kubernetes 如何安全访问 API 非常重要。Kubernetes 中最常实现的安全方法是基于角色的访问控制(RBAC),以实施针对特定用户或组的可执行操作的精细权限结构。本章介绍了一些关于 RBAC 的最佳实践,并提供了一个小的入门指南。
通过 ConfigMaps 和 Secrets 进行配置
Kubernetes 允许您通过 ConfigMaps 或秘密资源原生地为我们的应用程序提供配置信息。两者之间的主要区别在于 pod 存储接收信息的方式以及数据存储在 etcd 数据存储中的方式。
ConfigMaps
应用程序通常通过一些机制(如命令行参数、环境变量或系统可用的文件)消耗配置信息是非常普遍的。容器允许开发人员将此配置信息与应用程序解耦,从而实现真正的应用程序可移植性。ConfigMap API 允许注入提供的配置信息。ConfigMaps 非常适应应用程序的需求,并可以提供键/值对或复杂的批量数据,如 JSON、XML 或专有配置数据。
ConfigMaps 不仅为 pod 提供配置信息,还可以为更复杂的系统服务(如控制器、CRD、运算符等)提供信息。正如前面提到的,ConfigMap API 更适用于不是真正敏感的字符串数据。如果您的应用程序需要更敏感的数据,那么 Secrets API 更合适。
要使应用程序使用 ConfigMap 数据,可以将其注入为挂载到 pod 中的卷或环境变量的形式。
Secrets
使用配置映射的原因和属性之多适用于秘密。 主要区别在于秘密的基本性质。 秘密数据应以一种可以轻松隐藏并在环境配置为这样的情况下可能加密的方式存储和处理。 秘密数据表示为 base64 编码的信息,重要的是要理解这不是加密。 一旦秘密注入到 pod 中,pod 本身就可以看到明文的秘密数据。
秘密数据意味着小量数据,默认在 Kubernetes 中限制为 1 MB 的 base64 编码数据,因此由于编码的开销,确保实际数据约为 750 KB。 Kubernetes 中有三种类型的秘密:
generic
这通常只是从文件、目录或使用 --from-literal= 参数从字符串字面值创建的常规键/值对,如下所示:
kubectl create secret generic mysecret --from-literal=key1=$3cr3t1
--from-literal=key2=@3cr3t2
docker-registry
这是由 kubelet 在传递 pod 模板时使用的,如果有 imagePullsecret,则提供所需的凭据以进行私有 Docker 注册表的身份验证:
kubectl create secret docker-registry registryKey --docker-server
myreg.azurecr.io --docker-username myreg --docker-password
$up3r$3cr3tP@ssw0rd --docker-email ignore@dummy.com
tls
这将从有效的公共/私有密钥对创建一个传输层安全 (TLS) 秘密。 只要证书处于有效的 PEM 格式中,密钥对将被编码为秘密,并可以传递给 pod 用于 SSL/TLS 需求:
kubectl create secret tls www-tls --key=./path_to_key/wwwtls.key
--cert=./path_to_crt/wwwtls.crt
秘密也仅在需要秘密的 pod 所在的节点上挂载到 tmpfs,并在需要秘密的 pod 被删除时删除。 这可以防止任何秘密遗留在节点磁盘上。 尽管这可能看起来很安全,但重要的是要知道,默认情况下,秘密以明文形式存储在 Kubernetes 的 etcd 数据存储中,系统管理员或云服务提供商必须努力确保 etcd 环境的安全性,包括 etcd 节点之间的 mTLS 和启用 etcd 数据的加密。 更近期的 Kubernetes 版本使用 etcd3,并具有启用 etcd 原生加密的能力; 但是,这是一个必须在 API 服务器配置中手动配置的过程,通过指定提供程序和适当的密钥介质来正确加密 etcd 中保存的秘密数据。 从 Kubernetes v1.10 开始 (在 v1.12 中已升级为 beta),我们有 KMS 提供程序,它承诺通过使用第三方 KMS 系统来保持适当的密钥过程,从而提供更安全的密钥过程。
配置映射和密钥 API 的常见最佳实践
使用配置映射或秘密时出现的大多数问题源于对对象更新后数据处理方式的错误假设。 通过理解路规并添加一些技巧,以使遵守这些规则更容易,可以避免麻烦:
- 为了支持应用程序的动态变更而无需重新部署 Pod 的新版本,请将 ConfigMaps/Secrets 作为卷挂载,并配置应用程序使用文件监视器检测更改的文件数据并根据需要重新配置自身。以下代码展示了一个将 ConfigMap 和 Secret 文件作为卷挂载的 Deployment 示例:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-http-config
namespace: myapp-prod
data:
config: |
http {
server {
location / {
root /data/html;
}
location /images/ {
root /data;
}
}
}
apiVersion: v1
kind: Secret
metadata:
name: myapp-api-key
type: Opaque
data:
myapikey: YWRtd5thSaW4=
apiVersion: apps/v1
kind: Deployment
metadata:
name: mywebapp
namespace: myapp-prod
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
name: nginx-config
- mountPath: /usr/var/nginx/html/keys
name: api-key
volumes:
- name: nginx-config
configMap:
name: nginx-http-config
items:
- key: config
path: nginx.conf
- name: api-key
secret:
name: myapp-api-key
secretname: myapikey
注意
使用 volumeMounts 时需要考虑几个事项。首先,一旦创建 ConfigMap/Secret,请将其作为 Pod 规范中的卷添加。然后将该卷挂载到容器的文件系统中。ConfigMap/Secret 中的每个属性名称将成为挂载目录中的一个新文件,并且每个文件的内容将是 ConfigMap/Secret 中指定的值。其次,避免使用 volumeMounts.subPath 属性来挂载 ConfigMaps/Secrets。这将阻止在更新 ConfigMap/Secret 时动态更新卷中的数据。
-
ConfigMaps/Secrets 必须在将要使用它们的 Pod 的命名空间中存在,然后才能部署 Pod。如果 ConfigMap/Secret 不存在,可以使用可选标志来防止 Pod 无法启动。
-
使用准入控制器来确保特定的配置数据或防止未设置特定配置值的部署。例如,如果您要求所有生产 Java 工作负载在生产环境中具有特定的 JVM 属性集。
-
如果您正在使用 Helm 将应用发布到您的环境中,可以使用生命周期钩子确保在应用 Deployment 之前部署 ConfigMap/Secret 模板。
-
有些应用程序要求将它们的配置作为单个文件(如 JSON 或 YAML 文件)应用。ConfigMap/Secret 允许通过使用
|符号来传递整个原始数据块,如此示例所示:
apiVersion: v1
kind: ConfigMap
metadata:
name: config-file
data:
config: |
{
"iotDevice": {
"name": "remoteValve",
"username": "CC:22:3D:E3:CE:30",
"port": 51826,
"pin": "031-45-154"
}
}
-
如果应用程序使用系统环境变量来确定其配置,您可以使用 ConfigMap 数据的注入来创建环境变量映射到 Pod 中。有两种主要方法可以实现这一点:使用
envFrom将 ConfigMap 中的每个键值对作为一系列环境变量挂载到 Pod 中,然后使用configMapRef或secretRef;或者使用configMapKeyRef或secretKeyRef分配单独的键及其相应的值。 -
如果您使用
configMapKeyRef或secretKeyRef方法,请注意,如果实际键不存在,这将阻止 Pod 启动。 -
如果您使用
envFrom将 ConfigMap/Secret 中的所有键值对加载到 Pod 中,并且有些键被视为无效的环境值将会被跳过;但是,Pod 将被允许启动。Pod 的事件将包含一个原因为InvalidVariableNames的事件,并包含有关跳过的键的适当消息。以下代码展示了一个带有 ConfigMap 和 Secret 引用作为环境变量的 Deployment 示例:
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
data:
mysqldb: myappdb1
user: mysqluser1
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
data:
rootpassword: YWRtJasdhaW4=
userpassword: MWYyZDigKJGUyfgKJBmU2N2Rm
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-db-deploy
spec:
selector:
matchLabels:
app: myapp-db
template:
metadata:
labels:
app: myapp-db
spec:
containers:
- name: myapp-db-instance
image: mysql
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: rootpassword
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: userpassword
- name: MYSQL_USER
valueFrom:
configMapKeyRef:
name: mysql-config
key: user
- name: MYSQL_DB
valueFrom:
configMapKeyRef:
name: mysql-config
key: mysqldb
- 如果需要向容器传递命令行参数,可以使用
$(ENV_KEY)插值语法来源环境变量数据:
[...]
spec:
containers:
- name: load-gen
image: busybox
command: ["/bin/sh"]
args: ["-c", "while true; do curl $(WEB_UI_URL); sleep 10;done"]
ports:
- containerPort: 8080
env:
- name: WEB_UI_URL
valueFrom:
configMapKeyRef:
name: load-gen-config
key: url
-
在将 ConfigMap/Secret 数据作为环境变量消耗时,非常重要的是要理解,对 ConfigMap/Secret 中数据的更新 不会 在 Pod 中更新,需要重新启动 Pod。这可以通过删除 Pod 并让 ReplicaSet 控制器创建新的 Pod,或通过触发 Deployment 更新来完成,后者将遵循 Deployment 规范中声明的适当应用更新策略。
-
假设所有对 ConfigMap/Secret 的更改都需要更新整个 Deployment,这样可以确保即使使用环境变量或卷,代码也会采用新的配置数据。为了简化此过程,您可以使用 CI/CD 流水线更新 ConfigMap/Secret 的
name属性,并同时更新 Deployment 中的引用,这将通过常规的 Kubernetes 更新策略触发 Deployment 更新。我们将在以下示例代码中探讨这一点。如果您使用 Helm 发布应用代码到 Kubernetes,您可以利用 Deployment 模板中的注释来检查 ConfigMap/Secret 的sha256校验和。当 ConfigMap/Secret 中的数据发生变化时,这将触发 Helm 使用helm upgrade命令更新 Deployment:
apiVersion: apps/v1
kind: Deployment
[...]
spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml")
. | sha256sum }}
[...]
专用于 Secrets 的最佳实践
由于 Secrets API 中敏感数据的特性,自然会有更具体的最佳实践,主要围绕数据本身的安全性:
- 如果您的工作负载不需要直接访问 Kubernetes API,最佳实践是阻止自动挂载服务账户(默认或操作员创建的)的 API 凭据。这将减少对 API 服务器的 API 调用,因为使用监视功能来更新 API 凭据数据,以便在凭据过期时更新。在非常大的集群或具有大量 Pod 的集群中,这将减少对控制平面的调用,从而减少可能导致性能下降的原因之一。可以在 ServiceAccount 或 Pod Spec 本身定义这一行为:
apiVersion: v1
kind: ServiceAccount
metadata:
name: app1-svcacct
automountServiceAccountToken: false
[...]
apiVersion: v1
kind: Pod
metadata:
name: app1-pod
spec:
serviceAccountName: app1-svcacct
automountServiceAccountToken: false
[...]
-
Secrets API 的原始规范概述了一种可插拔的架构,允许根据需求配置实际的秘密存储。诸如 HashiCorp Vault、Aqua Security、Twistlock、AWS Secrets Manager、Google Cloud KMS 或 Azure Key Vault 等解决方案允许使用高级别的加密和审计功能的外部存储系统来存储秘密数据,这比 Kubernetes 本地提供的功能更强大。Linux 基金会项目 ExternalSecrets Operator 提供了一种本地方式来提供这种功能。
-
将
imagePullSecrets分配给一个serviceaccount,该 pod 将使用它来自动挂载密钥,而无需在pod.spec中声明它。您可以对应用程序所在命名空间的默认服务帐户进行修补,并直接添加imagePullSecrets到其中。这将自动将其添加到命名空间中的所有 pods:
Create the docker-registry secret first
kubectl create secret docker-registry registryKey --docker-server
myreg.azurecr.io --docker-username myreg --docker-password $up3r$3cr3tP@ssw0rd
--docker-email ignore@dummy.com
patch the default serviceaccount for the namespace you wish to configure
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name":
"registryKey"}]}'
- 使用 CI/CD 能力从安全保管库或加密存储中获取密钥,使用硬件安全模块(HSM)在发布流程中。这允许分离职责。安全管理团队可以创建和加密密钥,而开发人员只需引用预期的密钥名称。这也是确保更动态应用程序交付流程的首选 DevOps 流程。
RBAC
在大型分布式环境中工作时,通常需要某种安全机制来防止对关键系统的未经授权访问。在计算机系统中,有许多关于如何限制资源访问的策略,但大多数都经历相同的阶段。通过类比常见的经历,如飞往外国的旅行者经历,可以帮助解释类似 Kubernetes 系统中发生的过程。我们可以利用护照、旅行签证和海关或边境警卫的共同旅行经验来展示这一过程:
护照(主体验证)
通常,您需要由某个政府机构颁发的护照,该机构将提供一些关于您身份的验证。这相当于 Kubernetes 中的用户帐户。Kubernetes 依赖外部授权机构来验证用户;然而,服务账户是 Kubernetes 直接管理的一种账户类型。
签证或旅行政策(授权)
各国将通过正式的短期协议如签证来接受持有其他国家护照的旅行者。签证还会概述访客可以做什么以及他们可以在访问国家停留多长时间,这取决于具体的签证类型。这相当于 Kubernetes 中的授权。Kubernetes 拥有不同的授权方法,但 RBAC 是最常用的一种。这允许对不同的 API 功能具有非常精细的访问控制。
边境巡逻或海关(准入控制)
进入外国时,通常有一个权威机构将检查必要的文件,包括护照和签证,并且在许多情况下检查进入该国家的物品,以确保其符合该国的法律。在 Kubernetes 中,这相当于准入控制器。准入控制器可以根据定义的规则和政策允许、拒绝或更改对 API 的请求。Kubernetes 拥有许多内置的准入控制器,如 PodSecurity、ResourceQuota 和 ServiceAccount 控制器。Kubernetes 还允许通过使用验证或变异准入控制器来实现动态控制器。
本节的重点是这三个领域中最不为人所理解和最被避免的:RBAC。在我们概述一些最佳实践之前,我们必须首先介绍 Kubernetes RBAC 的入门知识。
RBAC 入门
Kubernetes 中的 RBAC 过程有三个主要组件需要定义:主题、规则和角色绑定。
主题
第一个组件是主题,实际上正在检查访问权限的项目。主题通常是用户、服务账户或组。如前所述,用户以及组由使用的授权模块在 Kubernetes 之外处理。我们可以将这些分类为基本认证、x.509 客户端证书或持有令牌。最常见的实现使用 x.509 客户端证书或类似 Azure Active Directory(Azure AD)、Salesforce 或 Google 等 OpenID Connect 系统的持有令牌。
注意
Kubernetes 中的服务账户与用户账户不同,它们是命名空间绑定的,并且在 Kubernetes 内部存储;它们旨在代表进程,而不是人员,并由本地 Kubernetes 控制器管理。
规则
简单来说,这是可以在 API 中对特定对象(资源)或对象组执行的实际操作列表。动词与典型的创建、读取、更新和删除(CRUD)类型的操作对齐,但在 Kubernetes 中具有一些额外的功能,如watch、list和exec。对象与不同的 API 组件对齐,并按类别进行分组。例如,Pod 对象是核心 API 的一部分,并且可以通过apiGroup: ""引用,而部署则属于 app API 组。这是 RBAC 过程的真正力量,也可能是在创建适当的 RBAC 控制时让人感到恐惧和困惑的原因。
角色
角色允许定义规则定义的范围。Kubernetes 有两种类型的角色,role和clusterRole,它们的区别在于role特定于命名空间,而clusterRole是跨所有命名空间的集群范围角色。一个具有命名空间范围的角色定义示例如下:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-viewer
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["pods"]
verbs: ["get", "watch", "list"]
RoleBindings
RoleBinding 允许将用户或组等主题映射到特定角色。绑定也有两种模式:roleBinding,它是特定于命名空间的;clusterRoleBinding,它是跨整个集群的。这里有一个命名空间范围内的示例 RoleBinding:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: noc-helpdesk-view
namespace: default
subjects:
- kind: User
name: helpdeskuser@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role #this must be Role or ClusterRole
name: pod-viewer # this must match the name of the Role or ClusterRole
# to bind to
apiGroup: rbac.authorization.k8s.io
RBAC 最佳实践
RBAC 是运行安全、可靠和稳定的 Kubernetes 环境的关键组件。RBAC 背后的概念可能很复杂;然而,遵循一些最佳实践可以减少一些主要障碍:
-
开发用于在 Kubernetes 中运行的应用程序几乎从不需要与它们关联的 RBAC 角色和 RoleBinding。只有当应用程序代码直接与 Kubernetes API 交互时,应用程序才需要 RBAC 配置。
-
如果应用程序确实需要直接访问 Kubernetes API,也许是根据添加到服务的端点来更改配置,或者需要列出特定命名空间中的所有 Pod,最佳实践是创建一个新的服务帐户,然后在 Pod 规范中指定它。然后,创建一个角色,该角色具有完成其目标所需的最少特权。
-
使用支持身份管理和如有需要的双因素认证的 OpenID Connect 服务。这将允许更高级别的身份验证。将用户组映射到具有完成工作所需的最少特权的角色。
-
除了上述实践,您应该使用即时(JIT)访问系统,允许站点可靠性工程师(SRE)、操作员以及可能需要在短时间内拥有升级特权以完成非常特定任务的人员。或者,这些用户应该有更受审计程度更高的不同身份来进行登录,并且这些帐户应该由用户帐户或绑定到角色的组分配更高的权限。
-
应为将应用程序部署到 Kubernetes 集群的 CI/CD 工具使用特定的服务帐户。这确保了集群内的审计性,并且能够理解谁可能已经在集群中部署或删除了任何对象。
-
如果您仍在使用 Helm v2 来部署应用程序,默认服务帐户是部署到
kube-system的 Tiller。最好将 Tiller 部署到每个命名空间,并为该命名空间专门指定一个用于 Tiller 的服务帐户。在调用 Helm install/upgrade 命令的 CI/CD 工具中,作为预步骤,使用服务帐户和部署的特定命名空间初始化 Helm 客户端。每个命名空间可以使用相同的服务帐户名称,但命名空间应特定。建议迁移到 Helm v3,因为其核心原则之一是不再需要在集群中运行 Tiller。新架构完全基于客户端,并使用调用 Helm 命令的用户的 RBAC 访问权限。这与客户端基础工具对 Kubernetes API 的首选方法一致。 -
限制任何需要在Secrets API上执行
watch和list操作的应用程序。这基本上允许应用程序或部署 Pod 的人查看该命名空间中的秘密。如果应用程序需要访问特定秘密的 Secrets API,则限制其仅使用get操作,而不是直接分配的那些秘密之外的其他秘密。
摘要
为了云原生交付应用程序的原则是另一个话题,但普遍认为,严格将配置与代码分离是成功的关键原则。通过适用于非敏感数据的本地对象,如 ConfigMap API,以及敏感数据的 Secrets API,Kubernetes 现在可以以声明性方法管理这一过程。随着越来越多的关键数据在 Kubernetes API 中原生表示和存储,通过适当的门控安全流程(如 RBAC 和集成认证系统),保护对这些 API 的访问至关重要。
正如你将在本书的其余部分中看到的那样,这些原则渗透到将服务正确部署到 Kubernetes 平台的每个方面,以构建一个稳定、可靠、安全和健壮的系统。
第五章:持续集成、测试和部署
本章中,我们将看看如何将持续集成/持续部署(CI/CD)流水线集成到 Kubernetes 中以交付应用程序的关键概念。构建一个良好集成的流水线将使您能够自信地将应用程序交付到生产环境,因此在这里我们将探讨在您的环境中实现 CI/CD 所需的方法、工具和流程。CI/CD 的目标是实现全自动化的流程,从开发者提交代码到将新代码推送到生产环境中去。您希望避免在部署到 Kubernetes 的应用程序中手动进行更新,因为这样很容易出错。在 Kubernetes 中手动管理应用程序更新会导致配置漂移和脆弱的部署更新,整体而言会失去交付应用程序的灵活性。
本章我们将涵盖以下主题:
-
版本控制
-
持续集成
-
测试
-
容器构建
-
容器镜像标记
-
持续部署
-
部署策略
-
在生产环境中进行测试
-
混乱测试
我们还通过一个示例的 CI/CD 流水线,其中包括以下任务:
-
将代码更改推送到 Git 仓库
-
运行应用程序代码的构建
-
对代码运行测试
-
在成功测试后构建容器镜像
-
将容器镜像推送到容器注册表
-
将应用程序部署到 Kubernetes
-
对已部署应用程序运行测试
-
对部署进行滚动升级
版本控制
每个 CI/CD 流水线都从版本控制开始,这维护应用程序和配置代码更改的运行历史记录。Git 已成为行业标准的源代码管理平台,每个 Git 仓库都将包含一个主分支。主分支包含您的生产代码。您将有其他用于功能和开发工作的分支,最终将合并到主分支。有许多设置分支策略的方法,设置将非常依赖于组织结构和职责分离。我们发现,包含应用程序代码和配置代码(如 Kubernetes 清单或 Helm 图表)有助于促进良好的 DevOps 沟通和协作原则。在单一仓库中让应用程序开发人员和运维工程师共同合作建立了团队交付应用程序到生产环境的信心。
持续集成
CI 是将代码更改持续集成到版本控制存储库的过程。与较少频繁地提交大型更改不同,您会更频繁地提交较小的更改。每次向存储库提交代码更改时,都会启动一次构建。这使您能够更快地反馈可能导致应用程序出现问题的原因。许多解决方案提供 CI,其中 Jenkins 是更受欢迎的工具之一。此时,您可能会问:“为什么我需要了解应用程序的构建过程;难道这不是应用程序开发人员的角色吗?” 传统上可能是这样,但随着公司向 DevOps 文化的转变,运维团队更接近应用程序代码和软件开发工作流。
测试
在流水线中运行测试的目标是快速提供针对破坏构建的代码更改的反馈循环。您使用的编程语言将决定您使用的测试框架。例如,Go 应用程序可以使用go test来运行一套针对您的代码库的单元测试。拥有广泛的测试套件有助于避免将糟糕的代码交付到生产环境。您将希望确保如果流水线中的测试失败,则在测试套件运行后构建失败。如果您的代码库存在失败的测试,则不应构建容器镜像并将其推送到注册表中。
也许你会问:“创建测试不是开发人员的工作吗?” 当你开始自动化基础设施和应用程序的交付到生产环境时,你需要考虑对代码库中所有部分运行自动化测试。例如,在第二章中,我们讨论了使用 Helm 为 Kubernetes 打包应用程序。Helm 包括一个名为helm lint的工具,它针对图表运行一系列测试,以检查提供的图表是否存在潜在问题。在端到端的流水线中需要运行许多不同的测试。有些是开发人员的责任,比如应用程序的单元测试,但其他测试如烟雾测试则是团队共同努力的结果。测试代码库及其交付到生产环境是团队的工作,需要端到端实施。
容器构建
在构建图像时,应优化图像的大小。图像越小,拉取和部署图像的时间就越短,同时也增加了图像的安全性。优化图像大小有多种方法,但某些方法确实存在权衡。以下策略将帮助您构建应用程序可能的最小图像:
多阶段构建
这些允许您移除不需要的依赖项,以使您的应用程序运行。例如,对于 Golang,我们不需要用于构建静态二进制文件的所有构建工具,因此多阶段构建允许您在单个 Dockerfile 中运行构建步骤,并且最终镜像仅包含运行应用程序所需的静态二进制文件。
Distroless 基础镜像
这些从镜像中移除所有不必要的二进制文件和 shell。这样做可以减小镜像的大小并增加安全性。Distroless 镜像的权衡在于没有 shell,因此无法附加调试器到镜像上。您可能认为这很好,但调试应用程序可能会很麻烦。Distroless 镜像不包含包管理器、shell 或其他典型的操作系统包,因此可能无法访问您在典型操作系统中习惯使用的调试工具。
优化基础镜像
这些镜像专注于清理操作系统层中的垃圾,并提供精简的镜像。例如,Alpine 提供一个从仅 10 MB 开始的基础镜像,并允许您在本地开发时附加本地调试器。其他发行版通常也提供优化的基础镜像,例如 Debian 的 Slim 镜像。这可能对您是一个不错的选择,因为其优化的镜像既提供了您在开发中期望的功能,同时又优化了镜像大小并降低了安全风险。
优化您的镜像非常重要,但用户常常忽视。您可能因公司对企业中可用操作系统的标准存在障碍,但应对此进行抗拒,以便最大化容器的价值。
我们发现刚开始使用 Kubernetes 的公司通常通过使用当前的操作系统获得成功,然后选择更优化的镜像,如 Debian Slim。在操作化和针对容器环境进行开发成熟之后,您将会对 Distroless 镜像感到满意。
容器镜像标记
CI 流水线中的另一步是构建容器镜像,以便您拥有一个部署到环境中的镜像工件。拥有一个镜像标记策略非常重要,这样您可以轻松识别已部署到环境中的版本化镜像。我们强调一件最重要的事情:不要使用“latest”作为镜像标记。将其用作镜像标记不是一个版本,将导致无法识别哪个代码更改属于已部署的镜像。在 CI 流水线中构建的每个镜像都应具有唯一的标记。
我们发现在 CI 流水线中标记镜像时有多种有效策略。以下策略可帮助您轻松识别代码更改及其关联的构建:
构建 ID
当 CI 构建启动时,它会关联一个构建 ID。使用标签的这一部分可让您引用哪个构建组装了该镜像。
构建系统-构建 ID
此标签与 BuildID 相同,但为那些使用多个构建系统的用户添加了构建系统。
Git 哈希
在新代码提交时,将生成一个 Git 哈希,并使用该哈希作为标签,以便轻松地引用生成图像的提交。
githash-buildID
这使您能够引用生成图像的代码提交和 BuildID。唯一需要注意的是,标签可能会有点长。
持续部署
CD 是指通过已成功通过 CI 流水线的变更无需人工干预即可部署到生产环境的过程。容器为将变更部署到生产环境提供了巨大的优势。容器映像变成了一个不可变的对象,可以通过开发、暂存和生产环境推广使用。例如,我们一直面临的一个主要问题是如何保持一致的环境。几乎每个人都曾经经历过在暂存环境中运行正常的部署,在推广到生产环境时却出现了问题。这是由于存在配置漂移,每个环境中的库和组件版本不同。Kubernetes 为我们提供了一种声明性的方式来描述我们的部署对象,从而可以进行版本控制并进行一致的部署。
需要记住的一件事是,在专注于 CD 之前,您需要建立一个可靠的 CI 流水线。如果在流水线的早期没有强大的测试套件来及早发现问题,您最终将向所有环境部署糟糕的代码。
部署策略
现在我们已经了解了 CD 的原理,让我们看看可以使用的不同部署策略。Kubernetes 提供多种策略来部署应用程序的新版本。虽然它具有内置机制来提供滚动更新,但您还可以利用更高级的策略。在这里,我们将研究以下策略来提供应用程序的更新:
-
滚动更新
-
蓝/绿部署
-
金丝雀部署
滚动更新内建于 Kubernetes 中,允许您触发当前运行的应用程序的更新,而无需停机。例如,如果您有一个当前正在运行 frontend:v1 的前端应用,并将部署更新为 frontend:v2,Kubernetes 将以滚动方式更新副本到 frontend:v2。图 5-1 展示了一个滚动更新。

图 5-1. 一个 Kubernetes 滚动更新
一个 Deployment 对象还允许您配置要更新的最大副本数以及在部署过程中不可用的最大 Pod 数。以下清单是指定滚动更新策略的示例:
kind: Deployment
apiVersion: apps/v1
metadata:
name: frontend
labels:
app: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: brendanburns/frontend:v1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Maximum amount of replicas to update at one time
maxUnavailable: 1 # Maximum amount of replicas unavailable during rollout
使用此策略时需要注意滚动更新,因为可能会导致连接丢失。为了解决此问题,可以使用 就绪探针 和 preStop 生命周期钩子。就绪探针确保部署的新版本已准备好接受流量,而 preStop 钩子可以确保当前部署的应用程序上的连接已经被排空。生命周期钩子在容器退出之前被调用,是同步的,因此必须在最终终止信号给出之前完成。以下示例实现了就绪探针和生命周期钩子:
kind: Deployment
apiVersion: apps/v1
metadata:
name: frontend
labels:
app: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: brendanburns/frontend:v1
livenessProbe:
# ...
readinessProbe:
httpGet:
path: /readiness # probe endpoint
port: 8888
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
strategy:
# ...
在此示例中,preStop 生命周期钩子将优雅地退出 NGINX,而 SIGTERM 则进行不优雅且快速的退出。
滚动更新的另一个问题是在转换期间同时运行两个应用程序版本。您的数据库架构需要支持应用程序的两个版本。您还可以使用特性标志策略,其中您的架构指示新应用程序版本创建的新列。完成滚动更新后,可以移除旧列。
我们还在我们的部署清单中定义了就绪性探针和活性探针。就绪性探针将确保您的应用程序在成为服务端点之前已准备好提供流量。活性探针确保您的应用程序健康运行,并在其失败活性探针时重新启动 Pod。只有在 Pod 因错误退出时,Kubernetes 才会自动重新启动失败的 Pod。例如,活性探针可以检查其端点,如果我们从中无法退出的死锁,则重新启动它。
蓝/绿部署 允许您可预测地发布应用程序。通过蓝/绿部署,您可以控制何时将流量转移到新环境,因此可以很好地控制应用程序新版本的发布。使用蓝/绿部署,您需要具备同时部署现有和新环境的能力。这些类型的部署具有许多优势,例如轻松切换回先前的应用程序版本。然而,使用此部署策略时需要考虑一些事项:
-
使用此部署选项可能使数据库迁移变得困难,因为您需要考虑飞行中的事务和架构更新兼容性。
-
存在意外删除两个环境的风险。
-
您需要为两个环境提供额外的容量。
-
混合部署存在协调问题,其中旧版应用程序无法处理部署。
图 5-2 描述了一个蓝/绿部署。

图 5-2. 蓝/绿部署
金丝雀部署与蓝绿部署非常相似,但它可以更好地控制将流量转移到新版本。大多数现代化的入口实现都允许您将一定比例的流量释放到新版本,但您也可以实现服务网格技术,比如 Istio、Linkerd 或 HashiCorp Consul,这些技术提供了多种功能,有助于实施这种部署策略。
金丝雀部署允许您仅针对用户的一部分测试新功能。例如,您可能要推出应用程序的新版本,并且只想针对您用户群的 10%进行部署测试。这样可以大大减少坏部署或功能中断对用户的影响。如果部署或新功能没有错误,您可以开始将更大比例的流量转移到新版本的应用程序上。还有更高级的技术可以与金丝雀部署结合使用,比如只向特定用户区域发布或只针对具有特定配置文件的用户发布。这些类型的发布通常称为 A/B 或暗发布,因为用户并不知道他们正在测试新功能的部署。
使用金丝雀部署时,您会遇到与蓝绿部署相同的一些考虑因素,但也有一些额外的考虑因素。您必须具备以下能力:
-
将流量转移到一部分用户的能力
-
对稳态有坚实的理解,以便与新版本进行比较
-
用于理解新版本是否处于“良好”或“不良”状态的指标
图 5-3 提供了金丝雀部署的示例。

图 5-3. 金丝雀部署
注意
金丝雀发布也面临着同时运行多个应用程序版本的问题。您的数据库架构需要同时支持这两个版本的应用程序。在使用这些策略时,您需要关注如何处理依赖服务以及同时运行多个版本的问题。这包括具有强大的 API 契约,并确保您的数据服务支持同时部署的多个版本。
在生产环境中进行测试
在生产环境中进行测试有助于建立对应用程序弹性、可扩展性和用户体验的信心。这带来了一个警告:在生产环境中测试并不是没有挑战和风险,但为了确保系统的可靠性,这是值得付出的努力。在实施时,您需要首先处理一些重要方面。您需要确保有一套深入的可观察策略,以便能够识别在生产环境中测试的影响。如果无法观察影响应用程序最终用户体验的指标,您将无法清楚地了解在努力提高系统弹性时应专注于什么。此外,您还需要实施高度自动化,以能够自动从注入到系统中的故障中恢复。
当您在生产环境中运行实验时,需要实施许多工具来减少风险并有效地测试您的系统。我们在本章中讨论了一些工具,但还有一些新工具,如分布式跟踪、仪器化、混沌工程和流量阴影。总结一下,这些是我们已经提到的工具:
-
金丝雀部署
-
蓝/绿部署
-
流量转移
-
功能标志
混沌工程是由 Netflix 开发的。它是将实验部署到实时生产系统中,以发现这些系统中的弱点的实践。混沌工程允许您通过观察在控制实验期间的系统行为来了解其行为。以下是在进行“游戏日”实验之前要实施的步骤:
-
建立假设并了解您的稳态。
-
具有可能影响系统的各种真实事件的不同程度。
-
建立一个对照组和实验组,以与稳态进行比较。
-
进行实验以测试假设。
在运行实验时,极为重要的是要最小化“影响范围”,以确保可能出现的问题最小化。此外,当您建立实验时,还需确保专注于自动化,因为运行实验可能会很费力。
到这一点,你可能会问,“为什么我不只是在测试阶段测试呢?”我们发现在测试阶段会有一些固有问题,比如以下问题:
-
资源的非相同部署。
-
来自生产环境的配置漂移。
-
流量和用户行为倾向于是合成生成的。
-
生成的请求数量不模仿真实工作负载。
-
在测试阶段没有实施监控。
-
部署的数据服务包含与生产环境不同的数据和负载。
我们无法过分强调这一点:确保您对生产环境的监控有足够的信心,因为这种做法往往会使没有足够可观察性的用户失败。此外,从较小的实验开始首先了解您的实验及其影响将有助于建立信心。
设置管道并执行混沌实验
进程的第一步是分叉一个 GitHub 存储库,以便您可以拥有自己的存储库在整章中使用。您将需要使用 GitHub 界面分叉示例应用程序存储库。
设置 CI
现在您已经了解了 CI,您将设置代码的构建,我们之前克隆的。
例如,我们使用托管的drone.io。您需要注册一个免费帐户。使用 GitHub 凭据登录(这将注册您的存储库在 Drone 中并允许您同步存储库)。登录到 Drone 后,在您的分支存储库上选择激活。您需要做的第一件事是将一些机密添加到设置中,以便您可以将应用程序推送到您的 Docker Hub 注册表,并将其部署到您的 Kubernetes 集群。
在 Drone 中的存储库下,单击“设置”,然后添加以下机密(见图 5-4):
-
docker_username -
docker_password -
kubernetes_server -
kubernetes_cert -
kubernetes_token

图 5-4. Drone 机密配置
Docker 用户名和密码将是您注册在 Docker Hub 上使用的内容。以下步骤展示了如何创建 Kubernetes 服务账户和证书以及检索令牌。
对于 Kubernetes 服务器,您将需要一个公开可用的 Kubernetes API 端点。
注意
您需要在您的 Kubernetes 集群上拥有 cluster-admin 特权才能执行本节中的步骤。
您可以通过以下命令检索您的 API 端点:
kubectl cluster-info
您应该看到类似以下的内容:Kubernetes 主服务器正在运行于 https://kbp.centralus.azmk8s.io:443。您将把这个存储在kubernetes_server秘密中。
现在让我们创建一个服务账户,Drone 将用其连接到集群。使用以下命令创建serviceaccount:
kubectl create serviceaccount drone
接下来,请使用以下命令为serviceaccount创建clusterrolebinding:
kubectl create clusterrolebinding drone-admin \
--clusterrole=cluster-admin \
--serviceaccount=default:drone
现在检索您的serviceaccount令牌:
TOKENNAME=`kubectl -n default get serviceaccount/drone
-o jsonpath='{.secrets[0].name}'`
TOKEN=`kubectl -n default get secret $TOKENNAME -o jsonpath='{.data.token}' |
base64 -d`
echo $TOKEN
要将令牌的输出存储在kubernetes_token秘密中。
您还需要用户证书以进行集群身份验证,因此请使用以下命令并粘贴ca.crt作为kubernetes_cert秘密:
kubectl get secret $TOKENNAME -o yaml | grep 'ca.crt:'
现在,在 Drone 管道中构建您的应用程序,然后将其推送到 Docker Hub。
第一步是 构建步骤,它将构建你的 Node.js 前端。Drone 利用容器镜像来运行其步骤,这为你提供了很大的灵活性。在构建步骤中,使用 Docker Hub 上的 Node.js 镜像:
pipeline:
build:
image: node
commands:
- cd frontend
- npm i redis --save
当构建完成后,你将希望对其进行测试,因此我们包含了一个 测试步骤,它将针对新构建的应用程序运行 npm:
test:
image: node
commands:
- cd frontend
- npm i redis --save
- npm test
现在你已经成功构建并测试了你的应用程序,接下来将进行 发布步骤,创建一个应用程序的容器镜像并将其推送到 Docker Hub。
在 .drone.yml 文件中,进行以下代码更改:
repo: <your-registry>/frontend
publish:
image: plugins/docker
dockerfile: ./frontend/Dockerfile
context: ./frontend
repo: dstrebel/frontend
tags: [latest, v2]
secrets: [ docker_username, docker_password ]
Docker 构建步骤完成后,它将把镜像推送到你的 Docker 注册表。
设置 CD
对于管道中的部署步骤,你将会将应用程序推送到你的 Kubernetes 集群。你将使用存储库中前端应用文件夹下的部署清单:
kubectl:
image: dstrebel/drone-kubectl-helm
secrets: [ kubernetes_server, kubernetes_cert, kubernetes_token ]
kubectl: "apply -f ./frontend/deployment.yaml"
在管道完成部署后,你将看到在你的集群中运行的 pod。运行以下命令以确认 pod 是否在运行:
kubectl get pods
你还可以添加一个测试步骤,通过在你的 Drone 管道中添加以下步骤来检索部署的状态:
test-deployment:
image: dstrebel/drone-kubectl-helm
secrets: [ kubernetes_server, kubernetes_cert, kubernetes_token ]
kubectl: "get deployment frontend"
执行滚动升级
让我们通过更改前端代码中的一行来演示滚动升级。在 server.js 文件中更改以下行,然后提交更改:
console.log('api server is running.');
你将看到部署正在进行中,并且现有 pod 正在进行滚动更新。在滚动更新完成后,你将部署应用程序的新版本。
一个简单的混沌实验
Kubernetes 生态系统中的各种工具可以帮助你在环境中执行混沌实验。它们从复杂的托管混沌服务解决方案到简单的混沌实验工具,用于杀死你环境中的 pod。以下是一些成功的工具:
Gremlin
提供运行混沌实验的先进特性的托管混沌服务
PowerfulSeal
提供高级混沌场景的开源项目
Chaos Toolkit
旨在为各种形式的混沌工程工具提供免费、开放和社区驱动的工具包和 API 的开源项目
KubeMonkey
提供基本的容错测试以测试集群中的 pod 的开源工具
让我们设置一个快速的混沌实验,通过自动终止 pod 来测试你的应用程序的弹性。对于这个实验,我们将使用 Chaos Toolkit:
pip install -U chaostoolkit
pip install chaostoolkit-kubernetes
export FRONTEND_URL="http://$(kubectl get svc frontend
-o jsonpath="{.status.loadBalancer.ingress[*].ip}"):8080/api/"
chaos run experiment.json
CI/CD 的最佳实践
你的 CI/CD 管道在第一天可能不会完美无缺,但考虑以下一些最佳实践来逐步改进管道:
-
在 CI 中,专注于自动化和提供快速构建。优化构建速度将为开发人员提供快速反馈,以判断他们的更改是否导致构建失败。
-
焦点放在管道中提供可靠的测试上。这将为开发人员快速反馈他们代码中的问题。开发人员的反馈循环越快,他们在工作流程中的生产力就会越高。
-
在选择 CI/CD 工具时,请确保这些工具允许您将流水线定义为代码。这将使您能够将流水线与应用程序代码一同进行版本控制。
-
确保优化您的镜像,以减小镜像的大小,并在生产环境中运行镜像时减少攻击面。多阶段 Docker 构建允许您移除应用程序运行时不需要的软件包。例如,您可能需要 Maven 来构建应用程序,但在实际运行镜像时不需要它。
-
避免使用“latest”作为镜像标签,而是使用可以引用到 buildID 或 Git 提交的 tag。
-
如果您对持续交付(CD)不熟悉,请从使用 Kubernetes 的滚动更新开始。它们易于使用,并将帮助您逐步熟悉部署过程。随着您对持续交付更加熟悉和自信,可以考虑使用蓝绿部署和金丝雀部署策略。
-
在持续交付过程中,请确保测试客户端连接和数据库架构升级在您的应用程序中的处理方式。
-
在生产环境中进行测试将帮助您在应用程序中建立可靠性,并确保您具备良好的监控能力。在生产环境测试时,也要从小规模开始,并限制实验的影响范围。
概要
在本章中,我们讨论了为应用程序构建 CI/CD 流水线的各个阶段,这将帮助您以可靠的方式交付软件并增强信心。CI/CD 流水线有助于减少风险,并增加将应用程序交付到 Kubernetes 的吞吐量。我们还讨论了可用于应用程序交付的不同部署策略。
第六章:版本控制、发布和部署
传统的单体应用程序的主要问题之一是随着时间推移,它们开始变得过于庞大和笨拙,无法按照业务需要的速度进行正确升级、版本控制或修改。许多人认为,这是导致更敏捷开发实践和微服务架构出现的关键因素之一。能够快速迭代新代码、解决新问题或在它们成为主要问题之前修复隐藏问题,以及实现零停机升级的承诺,都是开发团队努力实现的目标。实际上,通过适当的流程和程序可以解决这些问题,无论是什么类型的系统,但这通常会以更高的技术和人力成本来维护。
在设计系统时,隔离性和组合性是重要的变量。采用容器作为应用代码的运行时可以实现这一点,但仍然需要高水平的人工自动化或系统管理来保持大型系统的可靠水平。随着时间的推移,系统变得更加脆弱,引入了更多脆弱性,并且系统工程师开始构建复杂的自动化流程,以实现复杂的发布、升级和故障检测机制。诸如 Apache Mesos、HashiCorp Nomad 以及甚至专门的基于容器的编排器,如 Kubernetes 和 Docker Swarm,已经将这些流程进化为更原始的组件直接集成到它们的运行时中。现在,系统工程师可以解决更复杂的系统问题,因为基础已经提升到包括将应用程序版本控制、发布和部署到系统中。
版本控制
本节并非关于软件版本控制及其历史的入门介绍;关于这个主题有无数的文章和计算机科学课程书籍可供参考。最重要的是选择一种模式并坚持下去。大多数软件公司和开发者已经达成共识,某种形式的语义化版本控制是最有用的,特别是在微服务架构中,写作某个微服务的团队会依赖于构成系统的其他微服务的 API 兼容性。
对于那些不熟悉语义化版本控制的人来说,基本的概念是,它遵循一个由主版本、次版本和修订版本组成的三部分版本号,通常以点号表示,比如 1(主).2(次).3(修订)。修订版本表示包含了修复 bug 或者没有 API 变更的非常小的改动。次版本表示有可能有新的 API 变更,但与之前的版本兼容。对于与其他微服务进行交互但未参与开发的开发者来说,这是一个关键属性。知道我将我的服务编写成与另一个微服务的 1.4.7 版本进行通信,而后者最近升级到了 1.5.7,就意味着除非我想利用任何新的 API 特性,否则我可能不需要更改我的代码。主版本是代码的重大变更增量。在大多数情况下,同一代码的主版本之间的 API 不再兼容。此过程有许多细微的修改,包括使用“4”版本来指示软件在其开发生命周期中的阶段,例如 alpha 代码的 1.4.7.0 版本和发布的 1.4.7.3 版本。最重要的是,系统内部保持一致性。
发布版本
事实上,Kubernetes 实际上没有发布控制器,因此没有发布的本地概念。这通常添加到部署的metadata.labels规范中和/或pod.spec.template.metadata.label规范中。何时包含其中一个非常重要,并且基于 CD 用于更新部署变更的方式,它可能会产生不同的影响。当引入用于 Kubernetes 的 Helm 时,其主要概念之一是引入了发布的概念,以区分集群中相同 Helm 图表的运行实例。这个概念可以很容易地在没有 Helm 的情况下复制,但是 Helm 本身可以跟踪发布及其历史记录,因此许多 CD 工具将 Helm 集成到其流水线中,作为实际的发布服务。再次强调的关键是在集群系统状态中使用版本控制的一致性。
如果对某些名称的定义达成机构一致意见,发布名称可能非常有用。通常会使用诸如stable或canary之类的标签,这有助于在添加诸如服务网格等工具以进行细粒度路由决策时进行一些操作控制。驱动多个面向不同受众的变更的大型组织也会采用环形架构,可以表示为ring-0、ring-1等。
这个主题需要稍微深入了解 Kubernetes 声明性模型中标签的具体细节。标签本身非常自由形式,可以是任何遵循 API 语法规则的键/值对。关键不是内容本身,而是每个控制器如何处理标签,标签的更改以及标签的选择器匹配。Jobs、Deployments、ReplicaSets 和 DaemonSets 支持通过直接映射或基于集合的表达式来基于标签对 pod 进行选择器匹配。重要的是要理解,在创建后标签选择器是不可变的,这意味着如果添加了新的选择器,并且 pod 的标签有相应的匹配,会创建一个新的 ReplicaSet,而不是对现有 ReplicaSet 进行升级。在处理讨论的扩展时,理解这一点非常重要。
Rollouts
在 Kubernetes 引入 Deployment 控制器之前,控制应用程序如何由 Kubernetes 控制器进程进行滚动更新的唯一机制是在特定要更新的 replicaController 上使用命令行界面(CLI)命令 kubectl rolling-update。这对于声明式 CD 模型来说非常困难,因为这不是原始清单状态的一部分。必须确保清单正确更新,适当版本化,以防意外回滚系统,并在不再需要时进行存档。Deployment 控制器增加了使用特定策略自动化此更新过程的能力,然后允许系统根据 Deployment 的 spec.template 的更改来读取声明性的新状态。这一点经常被 Kubernetes 的新用户误解,当他们更改 Deployment 元数据字段中的标签,重新应用清单时,并未触发任何更新,从而导致 frustration。Deployment 控制器能够确定规范的更改,并将根据规范定义的策略采取行动来更新 Deployment。Kubernetes Deployments 支持两种策略,rollingUpdate 和 recreate,前者是默认值。
如果指定了滚动更新,Deployment 将创建一个新的 ReplicaSet 来扩展到所需的副本数量,并且旧的 ReplicaSet 将根据 maxUnavailble 和 maxSurge 的特定值缩减到零。实质上,这两个值将防止 Kubernetes 删除旧的 pod,直到足够数量的新 pod 上线,并且 Kubernetes 不会创建新的 pod,直到一定数量的旧 pod 被删除。好处在于,Deployment 控制器将保留更新的历史记录,并且通过 CLI,可以将 Deployment 回滚到先前的版本。
recreate 策略是对某些工作负载有效的策略,能够处理 ReplicaSet 中的 Pod 完全宕机而无需降级服务。在这种策略中,部署控制器将使用新的配置创建一个新的 ReplicaSet,并在将新的 Pod 上线之前删除先前的 ReplicaSet。在排队系统后面的服务就是可以处理此类中断的示例,因为消息将在等待新的 Pod 上线时排队,一旦新的 Pod 上线,消息处理将恢复。
将所有内容整合在一起
在单个服务的部署中,版本控制、发布和推出管理影响几个关键领域。让我们来看一个部署的例子,然后分析与最佳实践相关的特定关注领域:
# Web Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: gb-web-deploy
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
release number: 34e57f01
spec:
strategy:
type: rollingUpdate
rollingUpdate:
maxUnavailbale: 3
maxSurge: 2
selector:
matchLabels:
app: gb-web
ver: 1.5.8
matchExpressions:
- {key: environment, operator: In, values: [production]}
template:
metadata:
labels:
app: gb-web
ver: 1.5.8
environment: production
spec:
containers:
- name: gb-web-cont
image: evillgenius/gb-web:v1.5.5
env:
- name: GB_DB_HOST
value: gb-mysql
- name: GB_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
---
# DB Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: gb-mysql
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
release number: 34e57f01
spec:
selector:
matchLabels:
app: gb-db
tier: backend
strategy:
type: Recreate
template:
metadata:
labels:
app: gb-db
tier: backend
ver: 1.5.9
environment: production
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
# DB Backup Job
apiVersion: batch/v1
kind: Job
metadata:
name: db-backup
labels:
app: guest-book
appver: 1.6.9
environment: production
release: guest-book-stable
release number: 34e57f01
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook": pre-delete
"helm.sh/hook": pre-rollback
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
metadata:
labels:
app: gb-db-backup
tier: backend
ver: 1.6.1
environment: production
spec:
containers:
- name: mysqldump
image: evillgenius/mysqldump:v1
env:
- name: DB_NAME
value: gbdb1
- name: GB_DB_HOST
value: gb-mysql
- name: GB_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
volumeMounts:
- mountPath: /mysqldump
name: mysqldump
volumes:
- name: mysqldump
hostPath:
path: /home/bck/mysqldump
restartPolicy: Never
backoffLimit: 3
初次检查时,可能会感觉有些奇怪。一个部署如何具有版本标签,而部署使用的容器镜像却具有不同的版本标签?如果一个更改而另一个不更改会发生什么?在这个例子中发布意味着什么,如果更改会对系统有什么影响?如果更改了某个标签,何时会触发对我的部署的更新?我们可以通过查看版本控制、发布和推出的一些最佳实践来找到这些问题的答案。
版本控制、发布和推出的最佳实践
有效的 CI/CD 和提供减少或零停机时间的部署能力取决于使用一致的版本控制和发布管理实践。以下最佳实践可以帮助定义一致的参数,以协助 DevOps 团队提供平稳的软件部署:
-
对应用程序使用语义化版本控制,其版本与组成整个应用程序的容器和 Pod 的版本不同。这允许容器具有独立的生命周期,而组成应用程序的容器和整个应用程序可以有不同的版本。起初可能会有些混乱,但如果采用原则性的分层方法来处理一个变化另一个的情况,您可以轻松跟踪它。在前面的例子中,容器本身目前使用的是
v1.5.5,然而 Pod 的规范是1.5.8,这可能意味着对 Pod 规范进行了更改,比如新的 ConfigMaps、额外的 Secrets 或更新的副本值,但所使用的具体容器版本并没有更改。整个应用程序,包括其所有服务的整个应用程序客户端,目前处于1.6.9,这可能意味着运维团队在这一过程中进行了超出这个具体服务之外的更改,比如组成整个应用程序的其他服务。 -
在部署元数据中使用一个发布和发布版本/编号标签来跟踪从 CI/CD 管道中的发布。发布名称和发布编号应与 CI/CD 工具记录中的实际发布协调。这样可以通过 CI/CD 过程追踪到集群,并更容易识别回滚标识。在前面的示例中,发布编号直接来自创建清单的 CD 管道的发布 ID。
-
如果正在使用 Helm 打包服务以部署到 Kubernetes,特别注意将需要一起回滚或升级的服务捆绑到同一个 Helm 图中。Helm 允许轻松回滚应用程序的所有组件,将状态恢复到升级之前的状态。因为 Helm 实际上在传递扁平化的 YAML 配置之前会处理模板和所有 Helm 指令,使用生命周期钩子可以确保特定模板的正确应用顺序。操作员可以使用适当的 Helm 生命周期钩子来确保升级和回滚的正确执行。前面关于
Job规范的示例使用 Helm 生命周期钩子来确保在回滚、升级或删除 Helm 发布之前运行数据库备份。它还确保在作业成功运行后删除Job,在 Kubernetes 的 TTL 控制器退出 alpha 版本之前,这需要手动清理。 -
对于组织的运营节奏,达成一个有意义的发布命名约定是很重要的。对于大多数情况来说,简单的
stable、canary和alpha状态是相当足够的。
摘要
Kubernetes 已经允许公司(无论大小)采用更复杂的敏捷开发流程。能够自动化许多通常需要大量人力和技术资本的复杂流程,现在已经被普及,即使是初创公司也能相对容易地利用这种云模式。Kubernetes 的真正声明性质在于在规划正确使用标签和使用本地 Kubernetes 控制器功能时表现得非常出色。通过在部署到 Kubernetes 的应用程序的声明属性中正确识别操作和开发状态,组织可以将工具和自动化联系起来,更轻松地管理升级、发布和回滚能力的复杂流程。
第七章:全球应用分发与分级
到目前为止,本书中我们已经看到了许多不同的实践方法,用于构建、开发和部署应用程序,但是在部署和管理具有全球影响力的应用程序时,会出现一整套不同的考虑因素。
应用程序可能需要扩展到全球部署的原因有很多。第一个也是最明显的原因就是规模。您的应用程序可能非常成功或者具有使命关键性,因此需要全球部署以提供用户所需的容量。此类应用程序的示例包括公共云提供商的全球 API 网关、具有全球足迹的大规模物联网产品、非常成功的社交网络等。
尽管我们中相对较少的人会构建需要全球规模的系统,但更多的应用程序需要全球足迹来减少延迟。即使使用容器和 Kubernetes,也无法摆脱光速。为了最小化客户端与我们应用程序之间的延迟,有时需要将我们的应用程序分布到世界各地,以尽量减少应用程序与其用户之间的物理距离。
最后,全球分布的更常见原因是地理位置的需求。出于带宽(例如,远程感知平台)或数据隐私(例如,地理限制)的原因,有时需要在特定位置部署应用程序,以使应用程序的部署或成功成为可能。随着越来越多的国家和地区实施数据隐私和主权法律法规,将您的应用程序部署在特定位置以服务于居住在该地区的用户正在成为一种常见的商业必要性。
在所有这些情况下,您的应用程序不再仅仅存在于少数几个生产集群中。相反,它分布在数十到数百个不同的地理位置。管理这些位置以及推出一个全球可靠的服务的需求是一个重大挑战。本章涵盖了成功实现这一目标的方法和实践。
分发您的镜像
在你考虑将你的应用程序在全球范围内运行之前,你需要确保这个镜像可以在全球各地的集群中使用。首先要考虑的是你的镜像仓库是否支持自动地理复制。许多由云提供商提供的镜像仓库会自动将你的镜像分发到全球,并将请求定位到离拉取镜像的集群最近的存储位置。许多云平台允许你决定在哪些地方复制镜像;例如,你可能知道某些你不会出现的地方。一个例子是Microsoft Azure 容器注册表,但其他提供类似服务的也有很多。如果你使用支持地理复制的云提供的注册表,将你的镜像分发到全球就变得很简单。你只需将镜像推送到注册表,选择地理复制的区域,剩下的就由注册表来处理了。
如果你没有使用云注册表,或者你的提供商不支持镜像的自动地理分发,你就需要自己解决这个问题。一个选项是使用位于特定位置的注册表。对这种方法有几个担忧。镜像拉取延迟通常决定了你在集群中启动容器的速度。这反过来可以决定在机器故障时你能多快地做出响应,考虑到通常情况下,在机器故障时你需要将容器镜像下载到新机器上。
关于单一注册表的另一个问题是它可能是单点故障。如果注册表位于单一区域或单一数据中心,那么在该数据中心发生大规模事故时,注册表可能会下线。如果你的注册表下线了,你的 CI/CD 管道将停止工作,你将无法部署新的代码。这显然会对开发者的生产力和应用程序的运行产生重大影响。此外,单一注册表可能会更加昂贵,因为每次启动新容器时都会使用大量带宽,尽管容器镜像通常很小,但带宽消耗会累积。尽管存在这些负面因素,对于只在少数全球区域运行的小规模应用程序来说,单一注册表解决方案可能是合适的答案。它确实比全面复制镜像更容易设置。
如果您无法使用云提供的地理复制,并且需要复制您的镜像,您就需要自行制定镜像复制的解决方案。要实施这样的服务,您有两个选择。第一个选择是为每个镜像注册表使用地理名称(例如,us.my-registry.io,eu.my-registry.io等)。这种方法的优点是设置和管理简单。每个注册表都是完全独立的,您可以在 CI/CD 流程结束时简单地推送到所有注册表。缺点是每个集群将需要稍微不同的配置来从最近的地理位置拉取镜像。然而,鉴于您的应用配置可能会在地理上存在差异,这种缺点相对容易管理,并且可能已经存在于您的环境中。
第二个选项是使用网络配置将您的镜像拉取连接到特定的仓库。在这种方法中,您仍然将镜像推送到多个注册表,但不是为每个注册表提供唯一的名称,而是为它们都提供一个单一的 DNS 终端点(例如,my-registry.io)。您可以使用地理感知 DNS(GeoDNS),它会对来自不同地理区域的 DNS 请求响应不同的 IP 地址,或者如果您拥有正确的网络基础设施,可以使用组播 IP 地址。在组播中,所有的注册表共享相同的 IP 地址,但会在多个物理位置向互联网广播,依靠最短路径网络路由将流量发送到提供最近镜像注册表的服务器。这两种网络配置都很难正确实施。最佳答案无疑是使用基于云的注册表,即使您是在本地服务器上进行拉取。如果您确实想要运行自己的注册表(并承担相应的运营负担),我们强烈建议您使用前一段讨论过的区域服务器方法,除非您已经具备复制服务的网络经验。接下来的部分将描述如何对您的部署进行参数化,例如在不同区域使用不同的注册表。
参数化您的部署
当您在所有地方复制了您的镜像后,您需要对不同的全球位置进行部署参数化。每当您部署到各种不同的地区时,这些地区的应用配置都可能有所不同。例如,如果您没有地理复制的注册表,您可能需要为不同的区域调整镜像名称。然而,即使您有地理复制的镜像,不同的地理位置可能会对您的应用产生不同的负载,因此大小(例如副本数)以及其他配置可能会在区域之间有所不同。以一种不会增加过多负担的方式管理这种复杂性,是成功管理全球应用的关键。
首先要考虑的是如何在磁盘上组织不同的配置。一种常见的实现方法是为每个全局区域使用不同的目录。在这些目录中,也许会诱惑你简单地将相同的配置复制到每个目录中,但这样做肯定会导致配置之间的漂移和变化,某些区域被修改而其他区域被遗忘。相反,采用基于模板的方法,使大部分配置保留在一个单一模板中,该模板被所有区域共享,然后将参数应用于该模板以生成特定于区域的模板。Helm 是用于这种模板化的常用工具(详细信息请参见第一章)。
在全球范围内负载均衡流量
现在,您的应用程序已经在全球运行,下一步是确定如何将流量引导到该应用程序。通常情况下,您希望利用地理位置的近距离来确保对服务的低延迟访问。但您还希望在发生故障或任何其他服务故障源的情况下,在地理区域之间进行故障切换。正确设置流量平衡到各个区域部署是建立高性能和可靠系统的关键。
让我们从假设您有一个要用于服务的单个主机名开始,例如myapp.myco.com。您需要做出的第一个决定是是否要使用域名系统(DNS)协议来实现负载均衡跨您的区域终端点。如果您使用 DNS 进行负载平衡,当用户对myapp.myco.com进行 DNS 查询时返回的 IP 地址基于用户访问服务的位置以及您服务的当前可用性。另一种选择是多播 IP 地址,其中相同的 IP 地址从互联网上的多个位置进行广告。当用户查找myapp.myco.com时,DNS 始终返回这个固定 IP 地址,但实际数据包的路由因连接在网络中的位置而异。
在全球范围内可靠地推出软件
当你将应用程序模板化,为每个区域准备适当的配置后,下一个重要问题是如何在全球范围内部署这些配置。可能会有诱惑,同时在全球范围内部署你的应用程序,以便你可以高效快速地迭代应用程序,但这种做法虽然敏捷,却很容易导致全球性故障。你在世界范围内意外推出的任何错误都会立即影响到所有地区的所有用户。相比之下,对于大多数生产应用程序而言,更为谨慎的全球软件推出策略更为合适。当结合全球负载均衡等因素时,这些方法可以在面对重大应用程序故障时保持高可用性。
提示
总的来说,在处理全球推出的问题时,目标是尽快推出软件,同时迅速检测问题,最好是在影响到多个用户之前就能发现。
让我们假设在进行全球推出时,你的应用程序已经通过了基本功能和负载测试。在某个特定镜像(或镜像)被认证为全球推出之前,它应该经过足够的测试,你相信应用程序正在正确运行。重要的是要注意,这并不意味着你的应用程序确实正在正确运行。尽管测试可以发现许多问题,但在现实世界中,应用程序问题通常是在推出到生产流量时首次注意到的。这是因为生产流量的真实性质通常很难完美模拟。例如,你可能只测试英语输入,而在现实世界中,你会看到多种语言的输入。或者你的测试输入集可能对应用程序接收的真实世界数据不够全面。当然,每当你在生产环境中看到未经测试就发生的故障时,这是需要扩展和扩大你的测试的一个强烈指示。尽管如此,仍然有许多问题是在生产推出期间被捕捉到的。
当你考虑到这一点时,你每次推出新的区域都是发现新问题的机会。而且由于该区域是生产区域,它也是一个潜在的故障停机,你需要对此做出反应。这些因素共同为你如何应对区域性推出奠定了基础。
注
在本讨论中,我们谈论的是将软件推广到一个地理区域,但这种渐进式推广只是渐进式暴露控制的一种形式。推出功能的另一种方法是使用特征标志来进行渐进式暴露。使用特征标志时,新功能首先通过遵循接下来描述的地理推广的发布进行推广;然而,默认情况下,该功能的标志被设为“关闭”。一旦发布到所有地区,标志就会逐渐打开,例如,首先激活 10%的用户的功能,然后是 20%,依此类推,直到功能完全推出。有许多配置系统可用于进行基于标志的实验和渐进式推广。将标志与地理发布结合使用是发布新功能的一种非常稳定的方式,同时可以快速响应故障。
预发布验证
在您甚至考虑将软件的特定版本在全球范围内推广之前,非常重要的是在某种合成测试环境中验证该软件。如果您的持续交付流水线设置正确,特定发布构建之前的所有代码将经过某种形式的单元测试,可能还有有限的集成测试。然而,即使有这些测试,也很重要考虑发布之前的另外两种测试。第一种是完整的集成测试。这意味着您将整个堆栈组装成您应用程序的全面部署,但没有任何真实世界的流量。这个完整的堆栈通常会包括一个生产数据的副本或者在与您真实生产数据相同规模和尺度的模拟数据。如果在现实世界中,您的应用程序数据为 500 GB,那么在预生产测试中,您的数据集大致应该是相同的大小(甚至可能是确实相同的数据集)。
一般来说,建立完整的集成测试环境是一个重大挑战。通常,生产数据仅在生产环境中存在,生成相同规模和尺度的合成数据集非常困难。由于这种复杂性,设置一个真实的集成测试数据集是一个非常值得在应用程序开发早期就做的任务的绝佳示例。如果早期设置了数据集的合成副本,当数据集本身很小的时候,您的集成测试数据会与生产数据的增长速度保持一致。这通常比在已经达到规模时尝试复制生产数据要容易得多。
不幸的是,许多人直到已经处于大规模并且任务困难时才意识到他们需要数据的副本。在这种情况下,可能可以在您的生产数据存储前部署一个读写偏转层。显然,您不希望您的集成测试写入生产数据,但通常可以在您的生产数据存储前设置一个代理,从生产中读取但将写入存储在一个侧表中,后续读取时也会查询该侧表。
当然,如果您将生产数据用于测试和开发,非常重要的是要非常小心该数据的安全性。许多数据泄漏事件与开发人员意外地将他们的生产用户数据放置在不安全的位置有关。
无论您如何设置集成测试环境,目标都是相同的:验证在给定一系列测试输入和交互时,您的应用程序是否按预期行为。有多种方法来定义和执行这些测试——从最手动的方式,即测试工作表和人工操作(不建议,因为容易出错),到模拟浏览器和用户交互(例如点击等)的测试。在中间阶段是探测 RESTful API 的测试,但不一定测试构建在这些 API 之上的 Web UI。无论您如何定义集成测试,目标都应该是相同的:一个自动化测试套件,验证您的应用程序对完整一组真实世界输入的正确行为。对于简单的应用程序,可能可以在合并前进行此验证,但对于大多数大规模现实世界的应用程序,需要一个完整的集成环境。
集成测试将验证您的应用程序的正确操作,但您还应该对应用程序进行负载测试。证明应用程序的行为正确是一回事;证明它能够经受住真实世界的负载是另一回事。在任何相当高规模的系统中,例如请求延迟增加 20%这样的性能显著回退,对应用程序的用户体验有重大影响,并且除了让用户感到沮丧外,还可能导致应用程序完全失败。因此,确保在生产环境中不发生这种性能退化非常关键。
像集成测试一样,识别正确的应用程序负载测试方式可能是一个复杂的命题;毕竟,这需要您以合成和可重现的方式生成类似于生产流量的负载。其中一个最简单的方法就是简单地回放来自真实生产系统流量的日志。这样做可以是执行负载测试的一种很好的方式,因为其特性与应用程序在部署时将会经历的情况相匹配。然而,使用回放并非总是万无一失。例如,如果您的日志已经过时,而您的应用程序或数据集已发生变化,那么旧的回放日志上的性能可能与新鲜流量上的性能不同。此外,如果您有实际的依赖关系没有进行模拟,那么当发送到依赖关系时,旧的流量可能是无效的(例如,数据可能不再存在)。
就像生产数据一样,保护记录的任何真实请求的安全性至关重要。就像生产数据库一样,生产请求通常包含私人信息或安全凭据(或两者都有!),确保任何记录的安全性与实际用户请求同等重要。
因为与保存、保护和管理这些测试数据相关的挑战,许多系统,甚至是关键系统,在长时间内开发而没有进行负载测试。就像对生产数据建模一样,这是一个明显的例子,如果你早点开始,就会更容易维护。如果在应用程序只有少数依赖关系时建立负载测试,并在调整应用程序时改进和迭代负载测试,你将比试图在现有的大型应用程序上进行负载测试要容易得多。
假设您已经创建了一个负载测试,下一个问题是在负载测试应用程序时要关注的指标。显而易见的是每秒请求和请求延迟,因为这些显然是用户关注的指标。
在测量延迟时,重要的是要意识到这实际上是一个分布,并且您需要测量平均延迟以及异常百分位数(如第 90 和第 99 百分位数),因为它们代表了您应用程序的“最差”用户体验。如果只看平均值,则可能隐藏具有非常长延迟的问题,但如果有 10%的用户体验不佳,那么这可能会对您产品的成功产生显著影响。
另外,值得关注的是应用程序在负载测试下的资源使用情况(CPU、内存、网络、磁盘)。虽然这些指标并不直接影响用户体验,但你的应用程序资源使用发生较大变化时,应在预生产测试中识别并理解。如果你的应用程序突然消耗了两倍的内存,即使通过了负载测试,你也需要调查,因为这样显著的资源增长最终会影响应用程序的质量和可用性。根据情况,你可能会继续将发布推向生产,但同时,你需要理解为什么应用程序的资源占用量在变化。
金丝雀区域
当你的应用程序看起来运行正常时,第一步应该是一个金丝雀区域。金丝雀区域是一个接收来自希望验证你发布的真实流量的人和团队的部署。这些可以是依赖于你服务的内部团队,也可以是使用你服务的外部客户。金丝雀的存在是为了给团队一些关于即将推出的变化的早期警告。无论你的集成和负载测试有多好,总有可能有一些未被你的测试覆盖但对某些用户或客户至关重要的错误会漏过。在这种情况下,在每个人都知道失败可能性更高的空间中捕捉到这些问题要好得多。这就是金丝雀区域。
注意
金丝雀也是你的团队或公司在生产之前进行自我测试或早期测试的好地方。一个很好的最佳实践是设置一个 HTTP 重定向器,使得公司内部的请求重定向到运行在金丝雀环境中的产品实例。这样你团队的每个人在发布进入外部用户之前都变成了端到端的测试人员。
金丝雀在监控、规模、功能等方面必须像生产区域一样对待。然而,由于它是发布过程中的第一站,它也是最有可能看到出现故障发布的位置。这没问题;事实上,这正是目的所在。你的客户将会有意使用金丝雀来进行低风险的用例(例如开发或内部用户),以便他们能够早期了解到你可能作为发布的一部分推出的任何破坏性变化。
因为金丝雀的目标是在发布后获得早期反馈,所以在金丝雀区域保留发布几天是个好主意。这样能让广泛的客户群体在您移动到其他区域之前就能访问它。这段时间是必要的,因为有时候 bug 是概率性的(例如,影响到 1% 的请求),或者它只在一个需要时间才能显现的边缘案例中出现。它甚至可能不严重到触发自动警报,但可能存在业务逻辑问题,只有通过客户互动才能看到。
辨识区域类型
当您开始考虑在全球范围内推广您的软件时,重要的是考虑不同区域的不同特征。在将软件推向生产区域后,您需要进行集成测试以及初始的金丝雀测试。这意味着您找到的任何后续问题将是在这两种设置中都没有显现的问题。思考一下您的不同区域。有些区域的流量比其他区域多吗?有些区域的访问方式不同吗?一个例子是,发展中国家的流量更可能来自移动 Web 浏览器。因此,与更多发展中国家地理上接近的区域可能比您的测试或金丝雀区域有更多的移动流量。
另一个例子可能是输入语言。世界非英语区域的区域可能会发送更多的 Unicode 字符,这可能在字符串或字符处理中显现出 bug。如果您正在构建一个基于 API 的服务,某些 API 在某些区域可能比其他区域更受欢迎。所有这些都是您的应用程序可能存在的差异的示例,这些差异可能与您的金丝雀流量不同。每个这些差异都是生产事故的可能来源。建立一个您认为重要的不同特征的表格。识别这些特征将帮助您规划全球推广。
构建全球推广
识别了您的各个区域的特征后,您希望制定一个向所有区域推出的计划。显然,您希望尽量减少生产中断的影响,因此一个很好的起始区域是一个看起来大部分像您的金丝雀区域并且用户流量较轻的区域。这样的区域几乎不太可能出现问题,但如果出现问题,影响也较小,因为该区域接收的流量较少。
在第一个生产区域成功推出后,你需要决定在转移到下一个区域之前等待多长时间。等待的原因不是为了人为延迟你的发布;相反,是为了等待足够长的时间以便发现问题。这个时间到问题发现的间隔是一个衡量标准,通常是在推出完成后多长时间内,你的监控看到问题的迹象。显然,如果一个推出包含问题,那么在推出完成的那一刻,问题就存在于你的基础设施中。但即使它存在,也可能需要一些时间才能显现。例如,内存泄漏可能需要一个小时或更长时间,才能在监控中清晰地看到泄漏内存的影响或影响用户。时间到问题发现的间隔是一个概率分布,指示你应该等待多长时间才能有很强的概率表明你的发布正在正确运行。一般来说,一个不错的经验法则是将过去问题显现的平均时间加倍。
如果在过去六个月中,每次故障平均需要一个小时才能出现,那么在区域推出之间等待两个小时可以让你有很大的概率成功。如果你想根据应用程序历史推导出更丰富(和更有意义)的统计数据,你可以更加准确地估计这个时间到问题发现的间隔。
在成功向类似金丝雀的低流量区域推出后,现在是时候向类似金丝雀的高流量区域推出了。这是一个输入数据看起来与你的金丝雀相似,但接收大量流量的区域。因为你已经成功向流量较低的类似区域推出,此时你所测试的唯一事情就是你的应用程序扩展的能力。如果你安全地执行了这次推出,你就可以对你的发布质量有很强的信心。
在向接收类似金丝雀数据的高流量区域推出后,你应该对其他潜在的流量差异遵循相同的模式。例如,接下来你可能会向亚洲或欧洲的低流量区域推出。此时,加速推出可能很诱人,但关键是只向代表任何输入或负载变化的单个区域推出。在你确信已经测试了应用程序生产输入的所有潜在变化后,你可以开始并行化发布以加快速度,并且有很强的信心它正在正确运行,你的推出可以成功完成。
当出现问题时
到目前为止,我们已经看到了设置软件系统全球发布的各个要素,并了解了如何构建这种发布方式以尽量减少出错的机会。但是,当确实出现问题时,你该怎么办呢?所有应急响应人员都知道,在危机的热情和恐慌中,你的大脑处于极度紧张状态,即使是最简单的流程也更难记住。加上这种压力,你知道一旦发生故障,公司中从 CEO 到基层的每个人都会急切地等待“一切清楚”的信号,你就能明白,犯错误是多么容易。此外,在这种情况下,一个简单的错误,比如在恢复过程中忘记某个步骤,或者推出一个“修复”版本实际上有更多问题,都会使糟糕的情况恶化十倍。
出于所有这些原因,当发布出现问题时,能够迅速、冷静和正确地响应是至关重要的。为了确保所有必要的事情都做到位,并按正确的顺序进行,有一个清晰的任务检查列表是值得的,按照执行顺序组织每个步骤的预期输出。记录下每一步,无论看起来多么显而易见。在紧要关头,即使是最显而易见和简单的步骤也可能被遗忘并意外地跳过。
急救人员确保在高压情况下作出正确响应的方式是在紧急情况之外练习这种响应。同样的练习也适用于你在处理发布问题时可能采取的所有活动。你首先要确定应对问题和执行回滚所需的所有步骤,并进行练习。理想情况下,首要响应是“止血”,将用户流量从受影响区域转移到尚未进行发布且系统正常运行的区域。这是你应该练习的第一件事情。你能成功地引导流量远离一个区域吗?需要多长时间?
第一次尝试使用基于 DNS 的流量负载均衡器来移动流量时,您会意识到我们的计算机如何缓存 DNS 条目,以及这需要多长时间。使用基于 DNS 的流量整形器完全消除某一区域的流量可能需要将近一天的时间。无论您第一次尝试排放流量的情况如何,都要做好记录。哪些方面运作良好?哪些方面运作不佳?根据这些数据,设定一个排放流量所需时间的目标,例如,在不到 10 分钟内排放掉 99%的流量。持续练习,直到达到这个目标。您可能需要进行架构更改才能实现这一目标。您可能需要添加自动化,以确保不再需要人工复制和粘贴命令。无论需要做出哪些变更,实践将确保您在应对事件时更为从容,并且您将了解到系统设计需要改进的地方。
对您可能在系统上采取的每个操作都应采用相同的实践。实践全面的数据恢复。实践将您的系统全球回滚到以前的版本。设定时间目标。记录任何可能犯的错误,并添加验证和自动化来消除错误的可能性。在实践中达到您的事件反应目标将使您确信,在真实事件中您将能够做出正确反应。但就像每个紧急响应人员都继续训练和学习一样,您也需要建立定期实践的节奏,以确保团队中的每个人都熟悉正确的应对措施,并且(也许更重要的是)随着系统的变化,您的响应保持最新。
全球推出最佳实践
在全球范围内推出您的软件,特别是如果您以前从未这样做过,可能是一个重大挑战。以下是我们多年生产经验基础上的一些最佳实践,用于管理关键任务软件的全球部署:
-
将每个镜像分布到全球各地。成功的推出取决于发布的位(二进制文件、镜像等)是否靠近它们将被使用的地方。这也确保在网络减速或不规则性存在时推出的可靠性。地理分布应成为您的自动化发布流水线的一部分,以确保一致性。
-
尽可能将您的测试尽量向左移动,通过尽可能广泛的集成和重放测试来测试您的应用程序。您希望仅通过您强烈相信正确的发布才开始推出。
-
在金丝雀区域开始发布,这是一个预生产环境,在这里其他团队或大客户可以在您开始更大规模的推出之前验证他们对您服务的使用。
-
辨识您部署的不同地区的特征。每一个差异可能都会导致故障和完全或部分的停机。尽量先向风险较低的地区推出。
-
记录并实践对可能遇到的任何问题或流程(例如回滚)的响应。试图在紧急情况下记住该做什么往往会导致遗漏某些步骤,使问题更加严重。
总结
今天看起来可能不太可能,但我们大多数人在职业生涯中最终会运行一个全球规模的系统。本章描述了如何逐步构建和迭代您的系统,使其成为真正的全球设计。还讨论了如何设置您的部署以确保系统在更新期间最小化停机时间。最后,我们讨论了设置和实践必要的流程和程序,以在出现问题时做出反应(请注意,我们没有说“如果”而是“当”出现问题时)。
第八章:资源管理
在本章中,我们专注于管理和优化 Kubernetes 资源的最佳实践。我们讨论工作负载调度、集群管理、Pod 资源管理、命名空间管理和应用程序扩展。我们还深入探讨了 Kubernetes 通过亲和性、反亲和性、污点、容忍度和节点选择器提供的一些高级调度技术。
我们将向您展示如何实施资源限制、资源请求、Pod 服务质量、PodDisruptionBudget、LimitRanger 和反亲和性策略。
Kubernetes 调度器
Kubernetes 调度器是托管在控制平面中的主要组件之一。调度器允许 Kubernetes 对部署到集群中的 Pod 进行放置决策。它处理基于集群约束和用户指定约束的资源优化。它使用基于谓词和优先级的评分算法。
谓词
Kubernetes 用于进行调度决策的第一个函数是谓词函数,它确定可以在哪些节点上调度 Pod。它意味着一个硬约束,因此返回一个 true 或 false 的值。例如,当一个 Pod 请求 4 GB 的内存,而一个节点无法满足此要求时。该节点将返回一个 false 值,并将从可用节点中移除以供 Pod 调度。另一个例子是,如果节点设置为不可调度,则将从调度决策中移除。
调度器根据限制性和复杂性的顺序检查谓词。截至目前,调度器检查以下谓词:
CheckNodeConditionPred,
CheckNodeUnschedulablePred,
GeneralPred,
HostNamePred,
PodFitsHostPortsPred,
MatchNodeSelectorPred,
PodFitsResourcesPred,
NoDiskConflictPred,
PodToleratesNodeTaintsPred,
PodToleratesNodeNoExecuteTaintsPred,
CheckNodeLabelPresencePred,
CheckServiceAffinityPred,
MaxEBSVolumeCountPred,
MaxGCEPDVolumeCountPred,
MaxCSIVolumeCountPred,
MaxAzureDiskVolumeCountPred,
MaxCinderVolumeCountPred,
CheckVolumeBindingPred,
NoVolumeZoneConflictPred,
CheckNodeMemoryPressurePred,
CheckNodePIDPressurePred,
CheckNodeDiskPressurePred,
MatchInterPodAffinityPred
优先级
而谓词指示一个 true 或 false 的值并且排除一个节点进行调度,优先级值根据相对值对所有有效节点进行排名。以下优先级为节点评分:
EqualPriority
MostRequestedPriority
RequestedToCapacityRatioPriority
SelectorSpreadPriority
ServiceSpreadingPriority
InterPodAffinityPriority
LeastRequestedPriority
BalancedResourceAllocation
NodePreferAvoidPodsPriority
NodeAffinityPriority
TaintTolerationPriority
ImageLocalityPriority
ResourceLimitsPriority
分数将被相加,然后节点将被赋予最终分数以指示其优先级。例如,如果一个 Pod 需要 600 毫核,而有两个节点,一个可用 900 毫核,另一个可用 1,800 毫核,那么可用 1,800 毫核的节点将具有更高的优先级。
如果节点返回相同的优先级,则调度器将使用 selectHost() 函数,以轮询方式选择一个节点。
高级调度技术
对于大多数情况,Kubernetes 会为您优化地安排 Pod 的调度。它考虑的因素包括仅将 Pod 放置在具有足够资源的节点上。它还试图将来自同一 ReplicaSet 的 Pod 分散到不同的节点以增加可用性,并平衡资源利用率。当这不够好时,Kubernetes 为您提供了影响资源调度的灵活性。例如,您可能希望跨可用性区域安排 Pod 以减少因区域性故障而导致应用程序停机时间。您还可能希望将 Pod 放置在特定主机以获得性能优势。
Pod 亲和性和反亲和性
Pod 亲和性和反亲和性允许您设置规则以相对于其他 Pod 放置 Pod。这些规则允许您修改调度行为并覆盖调度器的放置决策。
例如,反亲和性规则允许您跨多个数据中心区域分散来自 ReplicaSet 的 Pod。它通过利用设置在 Pod 上的键标签来实现这一点。设置键/值对指示调度器在同一节点上调度 Pod(亲和性)或防止 Pod 在同一节点上调度(反亲和性)。
以下是设置 Pod 反亲和性规则的示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: frontend
replicas: 4
template:
metadata:
labels:
app: frontend
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- frontend
topologyKey: "kubernetes.io/hostname"
containers:
- name: nginx
image: nginx:alpine
这个 NGINX 部署清单有四个副本,并且选择标签为app=frontend。部署中配置了一个 PodAntiAffinity 段,确保调度器不会在同一节点上放置副本。这样做可以确保如果一个节点故障,仍然有足够的 NGINX 副本从其缓存中提供数据。
nodeSelector
nodeSelector 是将 Pod 调度到特定节点的最简单方法。它使用带有键/值对的标签选择器来做出调度决策。例如,您可能希望将 Pod 调度到具有专用硬件(例如 GPU)的特定节点。您可能会问,“我不能用节点污点做到这一点吗?”答案是,是的,您可以。区别在于,当您希望请求一个启用 GPU 的节点时,您使用 nodeSelector,而污点则保留节点仅用于 GPU 工作负载。您可以同时使用节点污点和 nodeSelector 来仅将节点保留给 GPU 工作负载,并使用 nodeSelector 自动选择具有 GPU 的节点。
以下是标记节点和在 Pod 规范中使用 nodeSelector 的示例:
kubectl label node <node_name> disktype=ssd
现在,让我们创建一个带有disktype: ssd节点选择器键/值的 Pod 规范:
apiVersion: v1
kind: Pod
metadata:
name: redis
labels:
env: prod
spec:
containers:
- name: frontend
image: nginx:alpine
imagePullPolicy: IfNotPresent
nodeSelector:
disktype: ssd
使用 nodeSelector 将 Pod 调度到只有标签disktype=ssd的节点上:
污点和容忍
污点用于节点上排斥调度 pod。但这不是反亲和性的作用吗?是的,但污点采用了与 pod 反亲和性不同的方法,并且用于不同的用例。例如,您可能有需要特定性能配置文件的 pod,并且不希望将任何其他 pod 调度到特定节点上。污点与容忍结合使用,允许您覆盖带有污点的节点。这两者的结合为您提供了对反亲和性规则的精细控制。
通常情况下,您将使用污点和容忍来处理以下用例:
-
专用节点硬件
-
专用节点资源
-
避免降级节点
多种污点类型影响调度和运行容器:
NoSchedule
阻止在节点上进行调度的硬污点
PreferNoSchedule
仅在无法在其他节点上调度 pod 时才进行调度
NoExecute
驱逐已在节点上运行的 pod
NodeCondition
如果满足特定条件,则对节点进行污点处理
图 8-1 展示了一个被taint为gpu=true:NoSchedule的节点的示例。Pod Spec 1 具有一个gpu的容忍键,因此将被调度到带污点的节点上。Pod Spec 2 具有一个no-gpu的容忍键,因此不会被调度到该节点上。

图 8-1. Kubernetes 污点和容忍
当由于带污点的节点而无法调度 pod 时,您将看到以下类似的错误消息:
Warning: FailedScheduling 10s (x10 over 2m) default-scheduler
0/2 nodes are available: 2 node(s) had taints that the pod did not tolerate.
现在我们已经看到如何手动添加污点以影响调度,还有一个强大的概念是taint-based eviction,它允许驱逐正在运行的 pod。例如,如果由于坏的磁盘驱动器而使节点变得不健康,基于污点的驱逐可以将 pod 重新调度到集群中的另一个健康节点上。
Pod 资源管理
在 Kubernetes 中管理应用程序的一个最重要方面是适当管理 pod 资源。管理 pod 资源包括管理 CPU 和内存,以优化您的 Kubernetes 集群的整体利用率。您可以在容器级别和命名空间级别管理这些资源。还有其他资源,如网络和存储,但 Kubernetes 尚无法设置这些资源的请求和限制。
为了调度程序能够优化资源并做出智能的放置决策,它需要了解应用程序的需求。例如,如果一个容器(应用程序)需要至少 2 GB 的内存来运行,我们需要在 pod 规范中定义这一点,以便调度程序知道该容器在所调度的主机上需要 2 GB 的内存。
资源请求
Kubernetes 资源请求定义了一个容器需要调度X数量的 CPU 或内存。如果您在 pod 规范中指定容器需要 8 GB 的资源请求,而所有节点都有 7.5 GB 的内存,那么该 pod 将无法调度。如果 pod 无法调度,它将进入挂起状态,直到所需资源可用。所以让我们看看在我们的集群中是如何工作的。
要确定集群中可用的空闲资源,请使用 kubectl top:
kubectl top nodes
输出应该像这样(您的集群的内存大小可能不同):
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
aks-nodepool1-14849087-0 524m 27% 7500Mi 33%
aks-nodepool1-14849087-1 468m 24% 3505Mi 27%
aks-nodepool1-14849087-2 406m 21% 3051Mi 24%
aks-nodepool1-14849087-3 441m 22% 2812Mi 22%
正如这个例子所示,主机可用的最大内存量是 7,500 Mi,所以让我们安排一个请求 8,000 Mi 内存的 pod:
apiVersion: v1
kind: Pod
metadata:
name: memory-request
spec:
containers:
- name: memory-request
image: polinux/stress
resources:
requests:
memory: "8000Mi"
注意,pod 将保持挂起状态,如果查看 pod 的事件,您会看到没有可用的节点来调度 pod:
kubectl describe pods memory-request
事件的输出应该像这样:
Events:
Type Reason Age From Message
Warning FailedSch... 27s (x2 over 27s) default-sched... 0/3 nodes are
available: 3
Insufficient memory
资源限制和 pod 服务质量
Kubernetes 资源限制定义了 pod 可以获得的最大 CPU 或内存。当您为 CPU 和内存指定限制时,每个达到指定限制时都会采取不同的操作。对于 CPU 限制,容器会被限制使用超过指定限制的资源。对于内存限制,如果 pod 达到其限制,它将被重新启动。该 pod 可能会在集群中的同一主机或不同主机上重新启动。
为容器指定限制是一种最佳实践,以确保应用程序在集群中分配到公平的资源份额:
apiVersion: v1
kind: Pod
metadata:
name: cpu-demo
namespace: cpu-example
spec:
containers:
- name: frontend
image: nginx:alpine
resources:
limits:
cpu: "1"
requests:
cpu: "0.5"
apiVersion: v1
kind: Pod
metadata:
name: qos-demo
namespace: qos-example
spec:
containers:
- name: qos-demo-ctr
image: nginx:alpine
resources:
limits:
memory: "200Mi"
cpu: "700m"
requests:
memory: "200Mi"
cpu: "700m"
当一个 pod 被创建时,它被分配以下其中之一的服务质量 (QoS) 类:
-
保证
-
突发
-
最佳努力
当 CPU 和内存都有匹配的请求和限制时,pod 被分配保证的 QoS。当限制设置高于请求时,即被分配突发的 QoS,这意味着容器保证了其请求,但也可以突发到容器设置的限制。当 pod 中的容器没有设置请求或限制时,被分配最佳努力的 QoS。
图 8-2 描述了如何为 pod 分配 QoS。

图 8-2. Kubernetes QoS
注意
使用保证的 QoS,如果您的 pod 中有多个容器,您将需要为每个容器设置内存请求和限制,同时还需要为每个容器设置 CPU 请求和限制。如果所有容器的请求和限制都没有设置,它们将不会被分配保证的 QoS。
PodDisruptionBudgets
Kubernetes 在某个时间点可能需要从主机中驱逐 pod。有两种类型的驱逐:自愿和非自愿中断。非自愿中断可能由硬件故障、网络分区、内核崩溃或节点资源不足引起。自愿驱逐可能由于对集群执行维护、集群自动缩放器释放节点或更新 pod 模板而引起。为了最小化对应用程序的影响,您可以设置PodDisruptionBudget以确保 pod 需要被驱逐时应用程序的正常运行时间。PodDisruptionBudget允许您在自愿驱逐事件中设置最小可用和最大不可用 pod 的策略。例如,当在节点上执行维护时,自愿驱逐的一个示例就是排空节点。
例如,您可以指定您的应用程序中最多有 20% 的 pod 可以在任何给定时间内处于停机状态。您还可以根据X个必须始终可用的副本来指定此策略。
最小可用
在下面的示例中,我们设置了一个PodDisruptionBudget来处理应用程序前端的最小可用为 5:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: frontend-pdb
spec:
minAvailable: 5
selector:
matchLabels:
app: frontend
在此示例中,PodDisruptionBudget指定前端应用程序始终必须有五个复制 pod 在任何时候可用。在这种情况下,可以驱逐尽可能多的 pod,只要五个 pod 可用即可。
最大不可用
在下一个示例中,我们设置了一个PodDisruptionBudget来处理前端应用程序的最大不可用为 20%:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: frontend-pdb
spec:
maxUnavailable: 20%
selector:
matchLabels:
app: frontend
在此示例中,PodDisruptionBudget指定在任何给定时间内不得超过 20% 的复制 pod 不可用。在这种情况下,驱逐期间可以驱逐多达 20% 的 pod。
在设计 Kubernetes 集群时,重要的是要考虑集群资源的大小,以便能够处理一定数量的失败节点。例如,如果您有一个四节点集群,并且一个节点失败了,那么您将失去四分之一的集群容量。
注意
当将PodDisruptionBudget指定为百分比时,可能无法与特定数量的 pod 相关联。例如,如果您的应用程序有七个 pod,并且您将maxAvailable指定为50%,那么不清楚是三个还是四个 pod。在这种情况下,Kubernetes 会四舍五入到最接近的整数,因此maxAvailable将是四个 pod。
通过使用命名空间管理资源
命名空间在 Kubernetes 中为您提供了资源的良好逻辑分离。这允许您为每个命名空间设置资源配额、基于角色的访问控制(RBAC)和命名空间的网络策略。它为您提供了软多租户功能,可以在集群中分隔工作负载,而无需为团队或应用程序专用特定的基础设施。这使您可以在保持逻辑分离的同时充分利用集群资源。
例如,你可以为每个团队创建一个命名空间,并为每个团队分配资源配额,例如 CPU 和内存。
在设计如何配置命名空间时,应考虑如何控制对特定应用程序集的访问。如果有多个团队将使用单个集群,则通常最好为每个团队分配一个命名空间。如果集群专用于单个团队,则为部署到集群的每个服务分配一个命名空间可能是有意义的。没有单一的解决方案;你的团队组织和责任将驱动设计。
部署 Kubernetes 集群后,你会在集群中看到以下命名空间:
kube-system
Kubernetes 内部组件已部署在这里,例如 coredns、kube-proxy 和 metrics-server。
default
这是在资源对象中未指定命名空间时使用的默认命名空间。
kube-public
用于匿名和未认证内容,保留用于系统使用。
你应避免使用默认命名空间,因为用户不必在特定资源约束内部署应用程序,这可能导致资源争用。你还应避免将 kube-system 命名空间用于你的应用程序,因为它用于 Kubernetes 内部组件。
在使用命名空间时,使用 kubectl 时需要使用 --namespace 标志,或者简写为 -n:
kubectl create ns team-1
kubectl get pods --namespace team-1
你还可以将 kubectl 上下文设置为特定的命名空间,这样你就不需要在每个命令中添加 --namespace 标志了。你可以使用以下命令设置你的命名空间上下文:
kubectl config set-context my-context --namespace=team-1
提示
当处理多个命名空间和集群时,设置不同的命名空间和集群上下文可能会很麻烦。我们发现使用 kubens 和 kubectx 可以帮助轻松切换这些不同的命名空间和上下文。
ResourceQuota
当多个团队或应用共享单个集群时,设置 ResourceQuota 对你的命名空间非常重要。ResourceQuota 允许你将集群分割成逻辑单元,以确保没有单个命名空间能在集群中占用超过其份额的资源。以下资源可以为它们设置配额:
-
计算资源:
-
requests.cpu: CPU 请求的总和不得超过此数量。 -
limits.cpu: CPU 限制的总和不得超过此数量。 -
requests.memory: 内存请求的总和不得超过此数量。 -
limit.memory: 内存限制的总和不得超过此数量。
-
-
存储资源:
-
requests.storage: 存储请求的总和不得超过此值。 -
persistentvolumeclaims: 命名空间中可以存在的 PersistentVolume claim 的总数。 -
storageclass.request: 与指定存储类相关联的卷声明不得超过此值。 -
storageclass.pvc:命名空间中可以存在的持久卷声明的总数
-
-
对象计数配额(仅作为示例设置):
-
计数/pvc
-
计数/services
-
计数/deployments
-
计数/replicasets
-
从这个列表中可以看出,Kubernetes 允许您对每个命名空间的资源配额进行精细化控制。这使您能够更有效地管理多租户集群中的资源使用。
现在让我们看看这些配额是如何通过在命名空间上设置一个配额来实际工作的。将以下 YAML 文件应用到team-1命名空间:
apiVersion: v1
kind: ResourceQuota
metadata:
name: mem-cpu-demo
namespace: team-1
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
persistentvolumeclaims: "5"
requests.storage: "10Gi
kubectl apply quota.yaml -n team-1
此示例为team-1命名空间设置了 CPU、内存和存储的配额。
现在让我们尝试部署一个应用程序,看看资源配额如何影响部署:
kubectl run nginx-quotatest --image=nginx --restart=Never --replicas=1 --port=80
--requests='cpu=500m,memory=4Gi' --limits='cpu=500m,memory=4Gi' -n team-1
由于内存配额超过了 2 Gi 内存,此部署将失败并显示以下错误:
Error from server (Forbidden): pods "nginx-quotatest" is forbidden:
exceeded quota: mem-cpu-demo
正如这个示例展示的那样,设置资源配额可以根据你为命名空间设置的策略拒绝资源的部署。
LimitRange
我们已经讨论过在容器级别设置request和limits,但是如果用户忘记在 pod 规范中设置这些内容会发生什么?Kubernetes 提供了一个准入控制器,允许在规范中没有指定这些内容时自动设置它们。
首先,创建一个用于配额和LimitRange工作的命名空间:
kubectl create ns team-1
在命名空间中应用LimitRange以应用limits中的defaultRequest:
apiVersion: v1
kind: LimitRange
metadata:
name: team-1-limit-range
spec:
limits:
- default:
memory: 512Mi
defaultRequest:
memory: 256Mi
type: Container
将此保存为limitranger.yaml,然后运行kubectl apply:
kubectl apply -f limitranger.yaml -n team-1
验证LimitRange是否应用了默认的限制和请求:
kubectl run team-1-pod --image=nginx -n team-1
接下来,让我们描述一下 pod,看看在其上设置了什么请求和限制:
kubectl describe pod team-1-pod -n team-1
您应该看到在 pod 规范上设置了以下请求和限制:
Limits:
memory: 512Mi
Requests:
memory: 256Mi
当使用ResourceQuota时,使用LimitRange非常重要,因为如果在规范中未设置请求或限制,部署将被拒绝。
集群扩展
在部署集群时,您需要做的第一个决定之一是确定您将在集群中使用的实例大小。这在将工作负载混合在单个集群中时尤为重要,更多地是一门艺术而非科学。您首先需要确定集群的良好起点;追求 CPU 和内存的良好平衡是一个选项。在确定了集群的合理大小后,您可以使用几个 Kubernetes 核心原语来管理集群的扩展。
手动扩展
Kubernetes 使得扩展您的集群变得简单,特别是如果您正在使用像 Kops 或托管的 Kubernetes 服务这样的工具。手动扩展您的集群通常只需选择一个新的节点数,服务将会将新节点添加到您的集群中。
这些工具还允许您创建节点池,这使得您可以向已运行的集群添加新的实例类型。在单个集群中运行混合工作负载时,这变得非常有用。例如,一个工作负载可能更多地依赖于 CPU,而其他工作负载可能是内存驱动的应用程序。节点池允许您在单个集群中混合多个实例类型。
但也许您不想手动执行此操作,希望它自动扩展。在集群自动扩展时,您需要考虑一些事项,我们发现大多数用户最好是在需要资源时主动手动扩展其节点。如果您的工作负载高度可变,集群自动扩展将非常有用。
集群自动扩展
Kubernetes 提供了一个 Cluster Autoscaler 插件,允许您设置集群的最小可用节点数,以及集群可以扩展到的最大节点数。Cluster Autoscaler 根据 pod 进入挂起状态时做出扩展决策。例如,如果 Kubernetes 调度器尝试调度一个需要 4,000 Mib 内存的 pod,而集群只有 2,000 Mib 可用,那么该 pod 将进入挂起状态。在 pod 挂起后,Cluster Autoscaler 将向集群添加一个节点。一旦新节点添加到集群中,挂起的 pod 就会被调度到该节点。Cluster Autoscaler 的缺点在于,只有在 pod 进入挂起状态之前才会添加新节点,因此当调度时,您的工作负载可能需要等待新节点上线。截至 Kubernetes v1.15,Cluster Autoscaler 不支持基于自定义指标的扩展。
当不再需要资源时,Cluster Autoscaler 还可以减少集群的大小。当资源不再需要时,它将排空节点并将 pod 重新调度到集群中的新节点。您将需要使用 PodDisruptionBudget 来确保在执行排空操作以从集群中移除节点时不会对应用程序产生负面影响。
应用程序扩展
Kubernetes 提供了多种方式来扩展集群中的应用程序。您可以通过手动更改部署中的副本数量来扩展应用程序。您也可以更改 ReplicaSet 或复制控制器,但我们不建议通过这些实现来管理您的应用程序。对于静态的工作负载或者您知道工作负载峰值时段的情况,手动扩展是完全可以接受的,但对于经历突发峰值或不是静态的工作负载,手动扩展并不理想。幸运的是,Kubernetes 还提供了 Horizontal Pod Autoscaler(HPA),可以自动为您扩展工作负载。
让我们首先看看如何通过应用以下部署清单来手动扩展部署:
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 3
selector:
matchlables:
app: frontend
template:
metadata:
name: frontend
labels:
app: frontend
spec:
containers:
- image: nginx:alpine
name: frontend
resources:
requests:
cpu: 100m
本示例部署了我们的前端服务的三个副本。然后,我们可以使用 kubectl scale 命令来扩展此部署:
kubectl scale deployment frontend --replicas 5
这导致我们的前端服务产生了五个副本。这很棒,但让我们看看如何根据指标添加一些智能化,并自动扩展应用程序。
使用 HPA 进行扩展
Kubernetes HPA 允许根据 CPU、内存或自定义指标扩展您的部署。它对部署执行监视,并从 Kubernetes metrics-server 中拉取指标。它还允许您设置可用的最小和最大 pod 数量。例如,您可以定义一个 HPA 策略,将最小 pod 数量设置为 3,最大 pod 数量设置为 10,并且在部署达到 80% CPU 使用率时进行扩展。设置最小和最大值非常关键,因为您不希望由于应用程序错误或问题,HPA 将副本无限扩展。
HPA 具有以下同步指标、升级和降级副本的默认设置:
horizontal-pod-autoscaler-sync-period
指标同步的默认设置为 30 秒
horizontal-pod-autoscaler-upscale-delay
两次扩展操作之间的默认间隔为三分钟
horizontal-pod-autoscaler-downscale-delay
两次降级操作之间的默认间隔为五分钟
您可以通过使用它们的相关标志来更改默认值,但在这样做时需要小心。如果您的工作负载非常变化,值得尝试不同的设置以优化特定用例。
接下来,让我们为你在上一个练习中部署的前端应用设置一个 HPA 策略。
首先,在端口 80 上公开部署:
kubectl expose deployment frontend --port 80
然后,设置自动缩放策略:
kubectl autoscale deployment frontend --cpu-percent=50 --min=1 --max=10
这将策略设置为从最小 1 个副本扩展到最多 10 个副本,并且在 CPU 负载达到 50% 时会触发扩展操作。
让我们生成一些负载,以便我们可以看到部署的自动扩展:
kubectl run -i --tty load-generator --image=busybox /bin/sh
Hit enter for command prompt
while true; do wget -q -O- http://frontend.default.svc.cluster.local; done
kubectl get hpa
您可能需要等待几分钟以便看到副本自动扩展。
HPA 与自定义指标
在第四章中,我们介绍了在 Kubernetes 中监视我们系统的指标服务器扮演的角色。使用指标服务器 API,我们还可以支持使用自定义指标扩展我们的应用程序。自定义指标 API 和指标聚合器允许第三方提供商插入和扩展这些指标,而 HPA 则可以根据这些外部指标进行扩展。例如,您可以基于外部存储队列上收集的指标,而不仅仅是基本的 CPU 和内存指标进行扩展。通过利用自定义指标进行自动缩放,您可以根据应用程序特定的指标或外部服务指标进行扩展。
垂直 Pod 自动缩放器
垂直 Pod 自动缩放器(VPA)与 HPA 不同,它不会扩展副本;相反,它会自动扩展请求。在本章前面,我们讨论了在我们的 Pod 上设置请求以及如何保证给定容器的 X 资源量。VPA 使您无需手动调整这些请求,并为您自动缩放 Pod 请求。对于不能因其架构而进行扩展的工作负载,这对于自动缩放资源非常有效。例如,MySQL 数据库与无状态 Web 前端的扩展方式不同。对于 MySQL,您可能希望根据工作负载自动扩展主节点。
VPA 比 HPA 更复杂,由三个组件组成:
推荐者
监控当前和过去的资源消耗,并为容器的 CPU 和内存请求提供推荐值。
更新者
检查哪些 Pod 设置了正确的资源,如果没有,则杀死它们,以便它们可以由其控制器重新创建并更新请求。
准入插件
在新的 Pod 上设置正确的资源请求。
垂直扩展有两个目标:
-
通过自动化配置资源需求来降低维护成本。
-
提高集群资源的利用率,同时最大限度地减少容器内存耗尽或 CPU 被饥饿的风险。
资源管理最佳实践
-
利用 Pod 反亲和性来在多个可用性区域间分布工作负载,确保应用程序的高可用性。
-
如果使用了专用硬件(如支持 GPU 的节点),请通过使用污点,仅安排需要 GPU 的工作负载到这些节点。
-
使用
NodeCondition的污点来主动避免节点的故障或降级。 -
将 nodeSelectors 应用于您的 Pod 规范,以便将 Pod 调度到集群中部署的专用硬件。
-
在进入生产环境之前,尝试不同的节点大小以找到成本和性能的良好平衡。
-
如果您部署了具有不同性能特征的混合工作负载,请利用节点池在单个集群中拥有混合节点类型。
-
确保为部署到集群中的所有 Pod 设置内存和 CPU 限制。
-
使用
ResourceQuota来确保集群中多个团队或应用程序得到公平分配的资源份额。 -
实施
LimitRange来为未设置限制或请求的 Pod 规范设置默认限制和请求。 -
在了解 Kubernetes 上的工作负载配置文件之前,从手动集群扩展开始。您可以使用自动缩放,但需要考虑节点启动时间和集群缩减的其他问题。
-
对于变量且具有意外使用量高峰的工作负载,使用 HPA。
摘要
在本章中,我们讨论了如何优化管理 Kubernetes 和应用资源。Kubernetes 提供了许多内置功能来管理资源,您可以利用这些功能来维护可靠、高效利用的集群。最初可能会对集群和 Pod 的大小感到困惑,但通过在生产环境中监控应用程序,您可以发现优化资源的方法。
第九章:网络、网络安全和服务网格
Kubernetes 有效地是跨连接系统集群的分布式系统管理器。这立即将系统连接的方式如何与其它系统通信置于至关重要的位置,而网络是这一切的关键。了解 Kubernetes 如何促进其管理的分布式服务之间的通信对于有效地应用服务间通信非常重要。
本章重点介绍 Kubernetes 在网络上的原则,并围绕在不同情况下应用这些概念的最佳实践展开讨论。任何关于网络的讨论通常都会带来安全性的讨论。传统的网络安全边界模型在 Kubernetes 中的分布式系统的新世界中并未消失,但它们的实施方式和提供的功能略有变化。Kubernetes 带来了一种原生的网络安全策略 API,听起来令人毛骨悚然地类似于旧时防火墙规则。
本章的最后一节深入探讨了服务网格的新世界,术语“恐怖”是开玩笑用的,但在 Kubernetes 的服务网格技术中确实是相当不可预测的领域。
Kubernetes 网络原则
理解 Kubernetes 如何利用底层网络来促进服务之间的通信对于有效规划应用程序架构至关重要。通常,网络主题开始让大多数人头痛起来。我们将保持简单,因为这更多是最佳实践指导,而不是容器网络的课程。幸运的是,Kubernetes 已经为网络制定了一些规则,为我们提供了一个起点。这些规则概述了不同组件之间预期的通信行为。让我们更详细地看看每个规则:
同一 Pod 中容器之间的通信
同一 Pod 中的所有容器共享相同的网络空间。这有效地允许容器之间通过 localhost 进行通信。这也意味着同一 Pod 中的容器需要暴露不同的端口。通过 Linux 命名空间和 Docker 网络的能力,通过在每个 Pod 中运行一个什么也不做的暂停容器来托管 Pod 的网络,从而使这些容器可以位于相同的本地网络。图 9-1 展示了容器 A 如何直接使用 localhost 和容器正在监听的端口号与容器 B 进行通信。

图 9-1 两个容器之间的 Pod 内通信
Pod 与 Pod 的通信
所有的 pod 都需要在没有任何网络地址转换(NAT)的情况下相互通信。这意味着接收 pod 看到的 pod IP 地址就是发送 pod 的实际 IP 地址。这是通过使用不同的网络插件来处理的,具体在本章后面会详细讨论。这条规则适用于同一节点上的 pod,以及同一集群中不同节点上的 pod。这也扩展到节点能够直接与 pod 进行通信,无需涉及 NAT。这允许基于主机的代理或系统守护程序根据需要与 pod 进行通信。图 9-2 是表示同一节点内 pod 和集群中不同节点上 pod 之间通信过程的图示。

图 9-2. Pod 对 pod 的节点内和节点间通信
服务对 pod 的通信
Kubernetes 中的服务代表了在每个节点上找到的持久 IP 地址和端口,将所有流量转发到映射到服务的端点。在 Kubernetes 的不同迭代中,启用这一功能的方法有所改变,但主要方法有通过使用 iptables 或更新的 IP Virtual Server(IPVS)。一些云提供商和更高级的实现允许基于新的 eBPF 数据平面。今天大多数实现使用 iptables 实现在每个节点上启用伪 Layer 4 负载均衡器。图 9-3 是显示服务如何通过标签选择器与 pod 关联的视觉表示。

图 9-3. 服务对 pod 的通信
网络插件
早期,特别兴趣组(SIG)引导网络标准向更具可插拔架构的方向发展,为 Kubernetes 工作负载打开了许多第三方网络项目的大门,在许多情况下,这些项目为 Kubernetes 工作负载注入了增值能力。这些网络插件分为两种类型。最基本的称为 Kubenet,是 Kubernetes 本地提供的默认插件。第二种类型的插件遵循容器网络接口(CNI)规范,这是容器的通用插件网络解决方案。
Kubenet
Kubenet 是 Kubernetes 中随箱即出的最基本的网络插件。它是最简单的插件,为连接到其上的 pod 提供了一个 Linux 桥接器 cbr0,这是一个虚拟以太网对。然后,pod 从分布在集群节点上的无类域间路由(CIDR)范围获取 IP 地址。还有一个 IP 伪装标志,应设置为允许发送到 pod CIDR 范围外 IP 的流量进行伪装。这遵守了 pod 对 pod 通信的规则,因为只有发送到 pod CIDR 范围外的流量才会经过网络地址转换(NAT)。当数据包离开一个节点去往另一个节点时,会放置某种路由来促进将流量正确转发到相应的节点的过程。
Kubenet 最佳实践
-
Kubenet 允许简单的网络堆栈,并且不会在已经拥挤的网络上消耗宝贵的 IP 地址。这对扩展到本地数据中心的云网络尤其重要。
-
确保 Pod CIDR 范围足够大,以处理集群的潜在大小和每个集群中的 Pod。kubelet 中设置的默认每节点 Pod 数为 110,但您可以进行调整。
-
确保根据路由规则进行适当的规划,以便流量能够正确找到节点中的 Pod。在云提供商中,这通常是自动化的,但在本地或边缘情况下将需要自动化和可靠的网络管理。
CNI 插件
CNI 插件通过规范设定了基本要求。这些规范规定了 CNI 提供的接口和最低 API 操作,以及它如何与集群中使用的容器运行时接口。网络管理组件由 CNI 定义,但它们都必须包括某种类型的 IP 地址管理,并最少允许将容器添加到网络并删除容器。最初源自rkt网络提案的完整原始规范可在GitHub 上获得。
核心 CNI 项目提供了库,您可以使用这些库编写插件,提供基本要求,并调用其他插件执行各种功能。这种适应性导致了许多 CNI 插件,您可以在云提供商的容器网络中使用,如微软 Azure 本机 CNI 和亚马逊 Web 服务(AWS)VPC CNI 插件,以及来自传统网络提供商的插件,如 Nuage CNI、Juniper Networks Contrail/Tunsten Fabric 和 VMware NSX。
CNI 最佳实践
网络是运行良好的 Kubernetes 环境的关键组成部分。Kubernetes 内的虚拟组件与物理网络环境之间的交互应经过精心设计,以确保可靠的应用程序通信:
-
评估完成基础设施的整体网络目标所需的功能集。一些 CNI 插件提供本地高可用性、多云连接、Kubernetes 网络策略支持以及其他各种功能。
-
如果通过公共云提供商运行集群,请验证是否支持云提供商的 SDN 中不是本地的任何 CNI 插件。
-
验证任何网络安全工具、网络可观察性工具和管理工具是否与所选 CNI 插件兼容。如果不兼容,研究可以替换现有工具的替代工具。当转移到诸如 Kubernetes 之类的大规模分布式系统时,不要失去可观察性或安全性能力是非常重要的。您可以将 Weaveworks Weave Scope、Dynatrace 和 Sysdig 等工具添加到任何 Kubernetes 环境中,每个工具都提供其独特的优势。如果您在云提供商的托管服务中运行,例如 Azure AKS、Google GCE 或 AWS EKS,请寻找像 Azure Container Insights 和 Network Watcher、Google Logging 和 Monitoring、以及 AWS CloudWatch 等本地工具。无论您使用哪种工具,它都应提供对网络堆栈和由 Google SRE 团队和 Rob Ewashuck 流行的四个黄金信号(延迟、流量、错误和饱和度)的洞察。
-
如果您正在使用不提供与 SDN 空间分开的覆盖网络的 CNI,请确保您有适当的网络地址空间来处理节点 IP、Pod IP、内部负载均衡器以及集群升级和扩展过程中的开销。
Kubernetes 中的服务
当 Pod 部署到 Kubernetes 集群中时,由于 Kubernetes 网络的基本规则及用于促进这些规则的网络插件,Pod 只能直接与同一集群中的其他 Pod 进行通信。某些 CNI 插件在与节点相同的网络空间上为 Pod 提供 IP,因此技术上,一旦知道了 Pod 的 IP,就可以直接从集群外访问它。然而,由于 Kubernetes 中 Pod 的瞬时性质,这并不是访问由 Pod 提供的服务的有效方式。想象一下,您有一个需要访问运行在 Kubernetes Pod 中的 API 的函数或系统。一段时间内,这可能会毫无问题地运行,但在某个时刻可能会出现自愿或非自愿的中断,导致该 Pod 消失。Kubernetes 可能会创建一个新的 Pod 来替换原来的 Pod,并分配新的名称和 IP 地址,因此自然需要某种机制来找到替换的 Pod。这就是服务 API 出马的地方。
服务 API 允许在 Kubernetes 集群内分配持久的 IP 和端口,并自动映射到服务的正确 Pod 端点。这一魔法通过 Linux 节点上的 iptables 或 IPVS 实现,以创建将分配的服务 IP 和端口映射到端点或 Pod 实际 IP 的映射。负责管理这一过程的控制器称为kube-proxy服务,它实际上在集群中的每个节点上运行。它负责在每个节点上操作 iptables 规则。
当定义服务对象时,需要定义服务的类型。服务类型将决定端点是仅在集群内部暴露还是在集群外部暴露。我们将在以下部分简要讨论四种基本的服务类型。
ClusterIP 服务类型
如果在规范中未声明服务类型,则 ClusterIP 是默认的服务类型。ClusterIP 意味着服务将被分配一个指定服务 CIDR 范围内的 IP。此 IP 与服务对象一样持久,因此它为后端 Pod 提供 IP、端口和协议映射,使用选择器字段;然而,正如我们将看到的,有时您可以没有选择器的情况。服务的声明还为服务提供了一个域名系统(DNS)名称。这在集群内部促进了服务发现,并允许工作负载通过基于服务名称的 DNS 查找轻松与集群中的其他服务通信。例如,如果您有如下示例中所示的服务定义,并且需要通过 HTTP 调用从集群内的另一个 Pod 访问该服务,则调用可以简单地使用 http://web1-svc(如果客户端与服务在同一命名空间中):
apiVersion: v1
kind: Service
metadata:
name: web1-svc
spec:
selector:
app: web1
ports:
- port: 80
targetPort: 8081
如果需要在其他命名空间中查找服务,则 DNS 模式将是 *<service_name>.<namespace_name>*.svc.cluster.local。
如果在服务定义中没有给出选择器,则可以通过使用端点 API 定义为服务显式定义服务的端点,而不是依赖于选择器属性从符合选择器匹配的 Pod 中自动更新端点。在某些场景中,这可能非常有用,例如您有一个特定的数据库用于测试,而不是集群中的数据库,但稍后将该服务更改为 Kubernetes 部署的数据库。这有时被称为 无头服务,因为它不像其他服务一样由 kube-proxy 管理,但您可以直接管理端点,如 图 9-4 所示。

图 9-4. ClusterIP-Pod 和服务可视化
NodePort 服务类型
NodePort 服务类型为集群中每个节点分配一个高级端口到每个节点的服务 IP 和端口。高级 NodePort 位于 30,000 到 32,767 范围内,并且可以静态分配或在服务规范中明确定义。NodePorts 通常用于本地集群或不提供自动负载均衡配置的定制解决方案。要从集群外部直接访问服务,请使用 NodeIP:NodePort,如 图 9-5 所示。

图 9-5. NodePort-Pod、服务和主机网络可视化
外部名称服务类型
ExternalName 服务类型在实践中很少使用,但对于将集群持久性 DNS 名称传递给外部 DNS 命名服务可能有所帮助。一个常见的例子是来自云提供商的外部数据库服务,云提供商提供唯一 DNS,例如 mymongodb.documents.azure.com。从技术上讲,可以非常容易地通过在 Pod 规范中使用 Environment 变量来添加它,如 第六章 中所讨论的那样。然而,使用更通用的集群名称,如 prod-mongodb,可能更有利,因为这样只需更改服务规范即可改变它指向的实际数据库,而无需因为 Environment 变量的更改而重新启动 Pod:
kind: Service
apiVersion: v1
metadata:
name: prod-mongodb
namespace: prod
spec:
type: ExternalName
externalName: mymongodb.documents.azure.com
Service Type LoadBalancer
LoadBalancer 是一个非常特殊的服务类型,因为它可以与云提供商和其他可编程云基础设施服务自动化集成。LoadBalancer 类型是确保部署由 Kubernetes 集群的基础设施提供商提供的负载均衡机制的单一方法。这意味着在大多数情况下,LoadBalancer 在 AWS、Azure、GCE、OpenStack 等环境中的工作方式大致相同。这个入口通常会创建一个公共面向外部的负载均衡服务;然而,每个云提供商都有一些特定的注释,以启用其他功能,如仅内部的负载均衡器、AWS ELB 配置参数等。您还可以定义要使用的实际负载均衡器 IP 和在服务规范中允许的源范围,如接下来的代码示例和在 图 9-6 中的可视化表示:
kind: Service
apiVersion: v1
metadata:
name: web-svc
spec:
type: LoadBalancer
selector:
app: web
ports:
- protocol: TCP
port: 80
targetPort: 8081
loadBalancerIP: 13.12.21.31
loadBalancerSourceRanges:
- "142.43.0.0/16"

图 9-6. 负载均衡器- Pod、服务、节点和云提供商网络可视化
Ingress 和 Ingress 控制器
尽管在 Kubernetes 中技术上不是一个服务类型,但 Ingress 规范对于 Kubernetes 中的工作负载入口非常重要。服务根据服务 API 的定义,允许基本的第 3/4 层负载均衡。事实上,许多部署在 Kubernetes 中的无状态服务需要高级别的流量管理,并且通常需要应用级别的控制,尤其是 HTTP 协议管理。
入口 API 基本上是一个 HTTP 级别的路由器,允许基于主机和路径的规则将请求定向到特定的后端服务。想象一个托管在 www.evillgenius.com 上的网站,并且该站点上托管有两个不同路径,/registration 和 /labaccess,由 Kubernetes 中托管的两个不同服务 reg-svc 和 labaccess-svc 提供。您可以定义一个入口规则,以确保将对 www.evillgenius.com/registration 的请求转发到 reg-svc 服务和正确的端点 pod,同样地,将对 www.evillgenius.com/labaccess 的请求转发到 labaccess-svc 服务的正确端点。入口 API 还允许基于主机的路由,以允许在单个入口上使用不同的主机。另一个功能是声明一个 Kubernetes 密钥,其中包含用于在端口 443 上终止传输层安全性(TLS)的证书信息。当未指定路径时,通常会有一个默认后端可以用于提供比标准 404 错误更好的用户体验。
关于特定的 TLS 和默认后端配置的详细信息实际上由称为入口控制器的东西处理。入口控制器与入口 API 解耦,允许运维人员部署他们选择的入口控制器,如 NGINX、Traefik、HAProxy 等。入口控制器就像任何 Kubernetes 控制器一样,但它不是系统的一部分,而是一个理解 Kubernetes 入口 API 的第三方控制器,用于动态配置。入口控制器的最常见实现是 NGINX,因为它部分由 Kubernetes 项目维护;然而,也有许多开源和商业入口控制器的例子:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: labs-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
tls:
- hosts:
- www.evillgenius.com
secretName: secret-tls
rules:
- host: www.evillgenius.com
http:
paths:
- path: /registration
pathType: ImplementationSpecific
backend:
service:
name: reg-svc
port:
number: 8088
- path: /labaccess
pathType: ImplementationSpecific
backend:
service:
name: labaccess-svc
port:
number: 8089
网关 API
入口 API 在它处于 beta 阶段和升级到 v1 之后的几年里遇到了一些挑战。这些挑战导致了其他网络服务通过使用自定义资源定义和控制器来创建自己的 API,填补了入口 API 存在的一些空白。入口 API 遇到的一些最常见挑战包括:
-
定义的表达能力不足,因为它代表特定入口实现的能力的最低公分母。
-
架构中普遍存在的扩展性不足。供应商已经使用了无数的注解来暴露特定的实现能力;然而,这也有一些局限性。
-
使用特定供应商的注解删除了 API 所承诺的部分可移植性。用于在基于 NGINX 的入口控制器中公开功能的注解可能与基于 Kong 的控制器实现有所不同或表达不同。
-
当前的入口 API 没有正式的多租户处理方式,DevOps 团队必须创建非常严格的控制措施,以防止入口定义之间的路径冲突,这可能会影响同一集群中的其他租户。
网关 API 自 2019 年推出以来,目前由 Kubernetes 项目下的 SIG Network 团队管理。网关 API 并不打算取代 Ingress API,因为它主要用于以声明性语法暴露 HTTP 应用程序。此 API 提供了一个更通用的代理多种协议的 API,并适合更加基于角色的管理流程,因为它更紧密地模拟环境中的基础设施组件。
基于角色的范式,如图 9-7 所示,对于解决现有入口 API 的一些缺陷非常重要。独立的组件允许基础设施提供者(例如云提供商和代理 ISV)定义基础设施,平台操作员通过策略定义可以使用什么基础设施。开发人员可以根据所给的约束条件考虑如何公开他们的服务。图 9-8 展示了网关 API 结构如何将基础设施服务和功能抽象出来,使开发人员可以专注于其特定的服务需求。

图 9-7. 网关 API 结构

图 9-8. 网关 API 结构,续
这一规范非常有前景,许多主要的代理和服务网格提供商,以及云提供商,已经开始将网关 API 集成到其堆栈中。Google 的 GKE、Acnodeal EPIC、Contour、Apache APISIX 等已经开始提供有限的预览或 alpha 支持。截至目前,API 本身在 GatewayClass、Gateway 和 HTTPRoute 资源上处于 beta 阶段,其他资源则处于 alpha 支持阶段。与 Ingress API 不同,这是一个自定义资源,可以添加到任何集群中,因此不遵循 Kubernetes 的 alpha 或 beta 发布流程。
服务和入口控制器的最佳实践
创建一个复杂的虚拟网络环境,其中应用程序彼此相互连接,需要仔细规划。有效地管理应用程序不同服务之间以及与外部世界的通信方式,需要随着应用程序变化而持续关注。以下是一些管理最佳实践:
-
限制需要从集群外部访问的服务数量。理想情况下,大多数服务将是 ClusterIP,只有外部服务才会暴露给集群外部。
-
如果需要暴露的服务主要是基于 HTTP/HTTPS 的服务,最好使用 Ingress API 和 Ingress 控制器来路由流量到支持 TLS 终止的后端服务。根据使用的 Ingress 控制器类型,诸如速率限制、头部重写、OAuth 认证、可观察性以及其他服务等功能可以提供,而无需将它们构建到应用程序中。
-
选择一个具有所需安全入口功能的 Ingress 控制器来服务您的基于 Web 的工作负载。在企业中标准化一个控制器并在全企业范围内使用它,因为许多特定的配置注解在不同实现之间会有所不同,并阻止部署代码在企业 Kubernetes 实施之间的可移植性。
-
评估特定云服务提供商的 Ingress 控制器选项,以将入口的基础设施管理和负载移出集群,但仍允许通过 Kubernetes API 进行配置。
-
当主要外部提供 API 时,评估特定于 API 的 Ingress 控制器,如 Kong 或 Ambassador,这些控制器对于基于 API 的工作负载具有更精细的调优能力。尽管 NGINX、Traefik 等可能提供了一些 API 调优,但不如专用的 API 代理系统精细。
-
在 Kubernetes 中将 Ingress 控制器部署为基于 Pod 的工作负载时,确保部署设计具有高可用性和聚合性能吞吐量。使用指标观测性来正确地调整 Ingress 的规模,但要包括足够的余量以防止工作负载扩展时客户端中断。
网络安全策略
Kubernetes 内置的 NetworkPolicy API 允许定义与工作负载相关的网络级入口和出口访问控制。网络策略允许您控制一组 Pod 如何与彼此以及其他端点通信。如果您希望深入了解 NetworkPolicy 规范,可能会感到困惑,特别是因为它被定义为 Kubernetes API,但需要一个支持 NetworkPolicy API 的网络插件。
网络策略具有简单的 YAML 结构,看起来可能复杂,但如果将其视为简单的东西流量防火墙,可能会帮助您更好地理解。每个策略规范都有 podSelector、ingress、egress 和 policyType 字段。唯一必需的字段是 podSelector,其遵循与任何 Kubernetes 选择器相同的约定,并具有 matchLabels。您可以创建多个 NetworkPolicy 定义,这些定义可以针对相同的 pods,其效果是累加的。因为 NetworkPolicy 对象是命名空间对象,如果没有为 podSelector 给出选择器,那么命名空间中的所有 pods 都属于策略的范围。如果定义了任何 ingress 或 egress 规则,则这会创建一个允许列表,指定可以从 pod 进入或离开的内容。这里有一个重要的区别:如果一个 pod 因为选择器匹配而落入策略的范围内,所有流量(除非在 ingress 或 egress 规则中明确定义)都会被阻止。这个微小而微妙的细节意味着,如果一个 pod 因为选择器匹配而不落入任何策略,那么所有的 ingress 和 egress 都允许到该 pod。这是有意为之,以便在 Kubernetes 中轻松部署新的工作负载而无需任何阻碍。
ingress 和 egress 字段基本上是基于源或目的地的规则列表,可以是特定的 CIDR 范围,podSelector 或 namespaceSelector。如果将 ingress 字段留空,则等同于拒绝所有入站流量。类似地,如果将 egress 字段留空,则等同于拒绝所有出站流量。还支持端口和协议列表,以进一步限制允许的通信类型。
policyTypes 字段指定策略对象关联的网络策略规则类型。如果该字段不存在,则只会查看 ingress 和 egress 列表字段。再次区分的是,您必须在 policyTypes 中明确调用出 egress,并且还必须为此策略定义一个 egress 规则列表才能使其工作。Ingress 是默认的,不需要显式定义。
让我们以部署到单个命名空间的三层应用程序的原型示例为例,其中各层被标记为 tier: "web"、tier: "db" 和 tier: "api"。如果您希望确保流量正确限制到每个层级,请创建如下的 NetworkPolicy 清单。
默认拒绝规则:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
Web 层网络策略:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: webaccess
spec:
podSelector:
matchLabels:
tier: "web"
policyTypes:
- Ingress
ingress:
- {}
API 层网络策略:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-access
spec:
podSelector:
matchLabels:
tier: "api"
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: "web"
数据库层网络策略:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-db-access
spec:
podSelector:
matchLabels:
tier: "db"
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: "api"
网络策略最佳实践
在企业系统中保护网络流量曾经是复杂网络规则集的物理硬件设备的领域。现在,通过 Kubernetes 网络策略,可以采用更具应用中心化的方法来分段和控制托管在 Kubernetes 中的应用程序的流量。一些常见的最佳实践适用于任何使用的策略插件:
-
从流量入口到 pod 开始缓慢进行,并侧重于此。使用入口和出口规则会使网络跟踪变得非常复杂。一旦流量按预期流动,可以开始查看出口规则,以进一步控制流向敏感工作负载。规范也偏向于入口,因为即使在入口规则列表中没有输入任何内容,也会默认许多选项。
-
确保所使用的网络插件要么具有自己的接口与 NetworkPolicy API 连接,要么支持其他众所周知的插件。例如插件包括 Calico、Cilium、Kube-router、Romana 和 Weave Net。
-
如果网络团队习惯于使用“默认拒绝”策略,请为集群中每个包含要保护的工作负载的命名空间创建以下网络策略。这样即使删除了另一个网络策略,也不会意外“暴露”任何 pod:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- 如果需要从互联网访问 pod,请使用标签明确应用允许入口的网络策略。在实际 IP 不是来自互联网而是负载均衡器、防火墙或其他网络设备的内部 IP 的情况下,要注意整个流程。例如,为允许所有(包括外部)来源的 pod 执行
allow-internet=true标签的流量,执行以下操作:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: internet-access
spec:
podSelector:
matchLabels:
allow-internet: "true"
policyTypes:
- Ingress
ingress:
- {}
- 尽量将应用工作负载对齐到单个命名空间,以便更容易创建规则,因为规则本身是命名空间特定的。如果需要跨命名空间通信,请尽可能明确,并可能使用特定标签来识别流模式:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: namespace-foo-2-namespace-bar
namespace: bar
spec:
podSelector:
matchLabels:
app: bar-app
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
networking/namespace: foo
podSelector:
matchLabels:
app: foo-app
- 在测试命名空间中设置较少限制的策略(如果有的话),以便有时间调查所需的正确流量模式。
服务网格
很容易想象一个单一集群托管数百个服务,这些服务在数千个端点之间进行负载平衡,彼此通信,访问外部资源,并且可能被外部来源访问。在试图管理、安全地保护、观察和跟踪所有这些服务之间的连接时,这可能非常令人畏惧,特别是由于端点在整个系统中的动态性质。服务网格的概念(不仅限于 Kubernetes)允许控制这些服务如何通过专用数据平面和控制平面连接和安全保护。服务网格通常具有不同的功能,但通常都提供以下一些功能:
-
使用可能是细粒度流量整形策略的负载均衡,这些策略分布在整个网格中。
-
发现网格成员服务,这可能包括集群内或另一个集群中的服务,或者是网格成员的外部系统。
-
观察流量和服务的可观察性,包括使用像 Jaeger 或 Zipkin 这样遵循 OpenTracing 标准的跨分布式服务的追踪系统。
-
网格中流量的安全性使用相互认证来保障。在某些情况下,不仅仅是 Pod 与 Pod 或东西向流量被保护,还提供了一个 Ingress 控制器,提供南北向的安全性和控制。
-
提供弹性、健康和故障预防能力,允许诸如熔断器、重试、期限等模式。
这里的关键是所有这些功能都集成到参与网格的应用程序中,几乎不需要或根本不需要应用程序更改。所有这些令人惊叹的功能如何免费获得?通常是通过 Sidecar 代理完成的。今天大多数可用的服务网格在每个成员 pod 中注入一个数据平面的代理,以便由控制平面组件在整个网格中同步策略和安全性。这隐藏了容纳工作负载的容器的网络细节,并将其留给代理处理分布式网络的复杂性。从应用程序的角度来看,它仅通过 localhost 与其代理进行通信。在许多情况下,控制平面和数据平面可能是不同的技术,但彼此互补。
在许多情况下,首先想到的服务网格是 Istio,这是一个由 Google、Lyft 和 IBM 合作的项目,使用 Envoy 作为其数据平面代理,并使用专有的控制平面组件 Mixer、Pilot、Galley 和 Citadel。其他服务网格提供不同级别的功能,比如使用 Rust 构建自己的数据平面代理的 Linkerd2。HashiCorp 最近在 Consul 中增加了更多面向 Kubernetes 的服务网格能力,允许您选择 Consul 自己的代理或 Envoy,并为其服务网格提供商业支持。
Kubernetes 中的服务网格主题是一个流动的话题,如果不是在许多社交媒体技术圈中过于情绪化,那么详细解释每个网格在这里没有价值。如果不提到由 Microsoft、Linkerd、HashiCorp、Solo.io、Kinvolk 和 Weaveworks 领导的有希望的努力,我们将不会尽责。服务网格接口(SMI)希望为所有服务网格期望的基本功能集设定一个标准接口。截至本文撰写时,该规范涵盖流量策略,如身份和传输级加密、捕获网格内服务之间关键指标的流量遥测以及允许在不同服务之间进行流量转移和加权的流量管理。该项目希望消除服务网格的某些变化性,同时允许服务网格供应商扩展和构建增值功能以区分自己的产品。
服务网格最佳实践
服务网格社区每天都在不断增长,随着更多企业帮助定义他们的需求,服务网格生态系统将发生显著变化。本文所述的最佳实践基于服务网格今天试图解决的常见问题:
-
评估服务网格提供的关键功能的重要性,并确定哪些当前提供的方案在人力技术债务和基础设施资源债务方面提供了最重要的功能。如果确实需要的只是某些 Pod 之间的互联 TLS,也许找到一个集成了这种功能的 CNI 插件会更容易。
-
跨系统网格的需求,如多云或混合场景,是否是一个关键需求?并非所有服务网格都提供此功能,即使提供,也往往会在环境中引入脆弱性。
-
许多服务网格方案都是基于开源社区的项目,如果管理环境的团队对服务网格还不熟悉,商业支持的方案可能是一个更好的选择。一些公司开始提供基于 Istio 的商业支持和托管服务网格,这可能非常有帮助,因为几乎普遍认为 Istio 是一个复杂的系统需要管理。
总结
除了应用程序管理外,Kubernetes 提供的最重要的功能之一是能够链接应用程序的不同部分。在本章中,我们详细介绍了 Kubernetes 的工作原理,包括如何通过 CNI 插件为 Pod 分配 IP 地址,如何将这些 IP 地址分组形成服务,以及如何通过 Ingress 资源实现更多的应用程序或第 7 层路由(这些资源反过来使用服务)。您还看到了如何使用网络策略限制流量和保护网络安全,最后,看到了服务网格技术如何改变人们连接和监控服务之间连接方式的方式。除了设置应用程序以可靠地运行和部署外,为应用程序设置网络是成功使用 Kubernetes 的关键部分。了解 Kubernetes 如何处理网络以及这如何与您的应用程序最佳交集是其最终成功的关键。
第十章:Pod 和容器安全
当涉及通过 Kubernetes API 实现 Pod 安全时,您有两个主要选项可供选择:Pod 安全准入和 RuntimeClass。在本章中,我们将审查每个 API 的目的和用途,并提供其最佳实践。
Pod 安全准入控制器
此全集群资源创建一个统一的地方,用于定义和管理 Pod 规范中的所有安全敏感字段。在创建 Pod 安全准入资源之前,集群管理员和/或用户使用的是 PodSecurityPolicy,这是一个复杂的设置,可能难以正确设置。在 PodSecurityPolicy 之前,用户需要独立地为其工作负载中的每个 Pod 或部署定义个别的 SecurityContext 设置,或者在集群上启用定制的准入控制器来强制执行一些 Pod 安全性方面的规则。
注意
Pod 安全准入控制器从 Kubernetes 1.22 开始取代了 Beta 版 PodSecurityPolicy API。PodSecurityPolicy 在 Kubernetes 1.25 中被移除。Pod 安全准入提供了一个简化的 API 来保护 Pod,但它不提供与 PodSecurityPolicy 的完全功能对等。要实现完全的策略功能对等,您需要安装一个更完整的策略解决方案,如 Gatekeeper 项目。
Pod 安全准入是为了解决这种复杂性,并且让集群管理员能够相对轻松地保护其集群上的 Pod。虽然比其他解决方案复杂度低,但 Pod 安全准入也有显著的局限性,即它具有在命名空间级别应用的粗粒度权限。尽管您可以豁免特定用户或运行时类别免受策略强制执行,但您无法在命名空间内为不同的 Pod 或用户启用不同级别的安全性。
由于这些限制,许多企业或运行多租户集群的管理员可能需要实施类似 Gatekeeper 项目的策略解决方案。但对于许多较小的单租户集群来说,Pod 安全准入控制可能更为合适。
启用 Pod 安全准入
如果您的集群是 Kubernetes 1.22 或更新版本,那么可能已经启用了 Pod 安全准入。您可以使用 kubectl version 命令检查集群的版本。如果您正在运行较旧版本的 Kubernetes,我们建议更新,因为这些较旧版本已不再由 Kubernetes 项目积极支持,这将使您面临未修补的安全漏洞风险。
警告
在现有集群上启用 Pod 安全准入控制时需要谨慎,因为如果一开始没有充分的准备,它有可能会阻塞工作负载。考虑从 warn 和 audit 强制模式开始,以确保您的策略按预期工作。
Pod 安全级别
Pod 安全准入控制器通过实施三种不同的策略级别简化了安全配置,供管理员选择。每个安全级别包含一组不同的规则,用于限制 Pod 配置。安全级别的详细信息可以在Kubernetes 文档中找到。
三种 Pod 安全标准级别为:
privileged
事实上没有任何限制。它与未启用 Pod 安全的 Kubernetes 集群的默认行为相匹配。
baseline
防止已知的特权升级和其他安全问题。
restricted
Pod 安全的当前社区最佳实践。
在开始使用策略时,可能会急于立即对所有命名空间强制执行restricted级别,但需要注意,集群中的现有配置可能会中断,并且社区解决方案或其他第三方提供的软件可能无法正常工作。
除了安全级别外,Pod 安全准入控制器还提供了三种策略激活级别。enforce级别会主动阻止不符合安全级别的 Pod 创建。warn级别会向用户发出警告,指出其 Pod 违反了策略,但不会阻止创建。audit级别记录策略违规情况,但不会向用户提供反馈。
最后,每个安全级别都与特定的 Kubernetes 版本(例如,v1.25)相对应。需要注意的是,虽然安全级别与 Kubernetes 版本关联,但在其他 Kubernetes 版本中也可用:你可以在 Kubernetes 1.26 集群中使用v1.25安全级别。这些版本遵循与任何其他 Kubernetes 组件相同的三版本弃用策略。还有一个latest版本,跟踪最新的策略。但是,与使用容器镜像中的latest相同,这是不鼓励的,因为当集群升级时,你的安全策略将发生变化,这意味着通过意外采用新策略可能会破坏集群。相反,在集群升级后逐步升级安全策略是最佳实践。
注意
需要注意的是,warn级别仅在支持警告的工具(如kubectl)中提供警告。如果使用其他工具进行部署,特别是 CI/CD 自动化工具,则可能不会向用户显示警告。在这种情况下,你可能需要结合某种检查配置的 linter,以及 Pod 安全审计。
使用命名空间标签激活 Pod 安全时
Pod 安全的激活是通过为命名空间添加标签来完成的。你可以在命名空间的 YAML 中添加标签,如下例所示。我们将从一个简单的配置开始,仅在基线安全级别下审核现有用法:
...
metadata:
labels:
# Start with enforce and warn unrestricted so as not to
# interfere with existing users
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/enforce-version: v1.25
pod-security.kubernetes.io/warn: privileged
pod-security.kubernetes.io/warn-version: v1.25
# Turn on baseline auditing
pod-security.kubernetes.io/audit: baseline
pod-security.kubernetes.io/audit-version: v1.25
应用此配置到所有命名空间后,您将开始在集群审计日志中看到审计信息。这将让您了解集群的合规水平。如果您的集群严重不合规,您可能需要识别各种工作负载的所有者,并与他们合作,使其工作负载达到合规性。由于执行是按命名空间进行的,您可以逐个团队进行工作,并在其工作负载符合合规性时转向执行。
最终,您的最终安全姿态取决于您的团队及其工作负载,因此很难确定单一的最佳 Pod 安全配置实践。然而,对于大多数用户来说,将audit设置为restricted,warn设置为enforce是一个不错的起点。这将使您能够查看潜在的漏洞配置,同时启用执行以防止最严重的违规行为。
工作负载隔离和 RuntimeClass
容器运行时仍然被普遍认为是一个不安全的工作负载隔离边界。目前最常见的运行时是否会被认为是安全的,尚无明确的路径。行业内对 Kubernetes 的动力和兴趣促使了不同容器运行时的发展,它们提供不同级别的隔离。有些基于熟悉和值得信赖的技术堆栈,而另一些则是解决问题的全新尝试。像 Kata 容器、gVisor 和 Firecracker 这样的开源项目宣称具有更强的工作负载隔离能力。这些特定项目要么基于嵌套虚拟化(在虚拟机内运行超轻量级虚拟机),要么基于系统调用过滤和服务。最近还对 WebAssembly 虚拟机提供的沙箱产生了兴趣,它最初是为浏览器而构建的,但在服务器端的使用越来越多。其中,containerd 项目,作为最受欢迎的容器运行时之一,现在支持基于 WebAssembly(WASM)的容器。此外,可能需要 RuntimeClass 来选择基于特定硬件能力的容器运行时,例如与 GPU 交互用于人工智能和机器学习工作负载。
引入这些提供不同工作负载隔离的容器运行时允许用户在同一集群中根据其隔离保证选择不同的运行时。例如,您可以在同一集群中使用不同的容器运行时运行可信和不可信的工作负载。
RuntimeClass 被引入 Kubernetes 作为一个 API,允许选择容器运行时。当集群由集群管理员配置时,它用于表示集群上支持的容器运行时之一。作为 Kubernetes 用户,您可以通过在 Pod 规范中使用 RuntimeClassName 来为您的工作负载定义特定的运行时类别。在底层实现方式是,RuntimeClass 指定了一个RuntimeHandler,该 Handler 被传递给容器运行时接口(CRI)来实现。然后可以使用节点标签或节点污点与节点选择器或容忍性结合使用,以确保工作负载落在能够支持所需 RuntimeClass 的节点上。Figure 10-1 演示了 kubelet 在启动 Pod 时如何使用 RuntimeClass。

图 10-1。RuntimeClass流程图
使用 RuntimeClass
如果集群管理员设置了不同的 RuntimeClasses,则可以通过在 Pod 规范中指定runtimeClassName来使用它们;例如:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
runtimeClassName: firecracker
运行时实现
以下是一些开源容器运行时实现,它们提供不同级别的安全性和隔离性供您参考。此列表旨在作为指南,并非详尽无遗:
一个 API 外观,专注于简单性,健壮性和可移植性的容器运行时。
一个基于 Open Container Initiative(OCI)的轻量级容器运行时的专用实现,用于 Kubernetes。
基于基于内核的虚拟机(KVM)构建,这种虚拟化技术允许您在非虚拟化环境中非常快速地启动微型 VM,同时利用传统 VM 的安全性和隔离性。
一个兼容 OCI 的沙箱运行时,使用新的用户空间内核来运行容器,提供低开销和安全、隔离的容器运行时。
通过运行轻量级 VM 感觉和操作像容器的轻量级 VM 来提供类似 VM 的安全性和隔离性的安全容器运行时。
工作负载隔离和 RuntimeClass 最佳实践
以下最佳实践将帮助您避免常见的工作负载隔离和 RuntimeClass 陷阱:
-
通过 RuntimeClass 实现不同的工作负载隔离环境会使您的操作环境变得复杂。这意味着鉴于它们提供的隔离性质,工作负载可能在不同的容器运行时之间不可移植。理解不同运行时支持功能矩阵可能很复杂,并且会导致用户体验不佳。如果可能的话,我们建议使用单一运行时的单独集群,以避免混淆。
-
工作负载隔离并不意味着安全的多租户。即使您可能已经实施了安全的容器运行时,这并不意味着 Kubernetes 集群和 API 以相同方式已经得到保护。您必须考虑 Kubernetes 端到端的总表面积。仅仅因为您有一个隔离的工作负载,并不意味着它不能通过 Kubernetes API 被不良行为者修改。
-
不同运行时的工具集不一致。您可能有用户依赖于容器运行时的工具进行调试和内省。拥有不同的运行时意味着您可能无法再运行
docker ps来列出运行中的容器。这会在故障排除时导致混淆和复杂化。
其他 Pod 和容器安全考虑
除了 Pod 安全准入控制和工作负载隔离之外,以下是一些处理 Pod 和容器安全性时您可能考虑的其他工具。
准入控制器
之前讨论的 Pod 安全准入控制是由 Pod 安全准入控制器提供支持的,但在云原生生态系统中还有许多其他准入控制器可供选择。如果您认为 Pod 安全准入控制器过于限制,许多其他选项提供了更复杂的策略解决方案。有关准入控制的更多信息,请参阅第十七章。
入侵和异常检测工具集
我们已经涵盖了安全策略和容器运行时,但当您希望在容器运行时内部检查和执行策略时会发生什么?有一些开源工具可以实现这一点及更多功能。它们通过监听和过滤 Linux 系统调用或利用伯克利数据包过滤器(BPF)来运行。其中一个工具是Falco,这是一个云原生计算基金会(CNCF)项目,作为 DaemonSet 安装,允许您在执行过程中配置和执行策略。Falco 只是一种方法。我们鼓励您探索这个领域的工具,看看哪种适合您。
概要
在本章中,我们深入探讨了 Pod 安全准入控制和 RuntimeClass API,您可以使用这些工具为您的工作负载配置粒度级别的安全性。我们还查看了一些开源生态系统工具,可以用于监视和执行容器运行时内的策略。我们为您提供了详尽的概述,以便您做出关于提供最适合您工作负载需求的安全级别的知情决策。
第十一章:你的集群的政策与治理
你是否曾想过如何确保集群上运行的所有容器仅来自已批准的容器注册表?或者,也许安全团队要求你强制执行一项政策,即服务永远不暴露在互联网上。这些正是集群政策与治理旨在解决的挑战。随着 Kubernetes 的成熟和越来越多企业的采用,如何将政策与治理应用于 Kubernetes 资源的问题日益频繁。在本章中,我们分享了你可以采取的方法和工具,以确保你的集群符合定义的政策,无论你是在初创企业还是大企业工作。
为什么政策与治理至关重要
无论你是在高度受管制的环境中操作(例如医疗或金融服务),还是仅仅想确保你对集群上运行的内容保持控制,你都需要一种实施公司特定政策的方法。一旦定义了你的政策,你就需要确定如何实施它,并维护符合这些政策的集群。这些政策可能需要符合法规合规性,或者仅仅是强制执行最佳实践。无论原因如何,你必须确保在实施这些政策时不牺牲开发者的灵活性和自助服务。
这项政策有何不同?
在 Kubernetes 中,政策无处不在。无论是网络策略还是 Pod 安全,我们都明白何时以及如何使用政策。我们相信 Kubernetes 资源规范中声明的任何内容都会按照政策定义进行实施。网络策略和 Pod 安全都是在运行时实施的。然而,政策限制 Kubernetes 资源规范中的字段值。这是政策与治理的工作。与在运行时实施政策不同,当我们在治理的背景下谈论政策时,我们的意思(或者至少我们试图达到的目标)是限制在 Kubernetes 资源中配置字段的方式。只有在通过政策评估时符合的 Kubernetes 资源规范才允许提交到集群状态。
云原生政策引擎
为了能够评估哪些资源符合规定,我们需要一个灵活到可以满足各种需求的策略引擎。开放策略代理 (OPA) 是一个开源、灵活、轻量级的策略引擎,在云原生生态系统中越来越受欢迎。在生态系统中引入 OPA 后,出现了许多不同的 Kubernetes 管理工具实现。社区正在支持的一个这样的 Kubernetes 策略和治理项目称为 Gatekeeper。在本章的其余部分,我们使用 Gatekeeper 作为规范示例,展示如何为您的集群实现策略和治理。尽管生态系统中还有其他策略和治理工具的实现,它们都致力于通过允许只提交符合 Kubernetes 资源规范的资源来提供相同的用户体验(UX)。
Gatekeeper 简介
Gatekeeper 是一个开源的、可定制的 Kubernetes 准入 webhook,用于集群策略和治理。Gatekeeper 利用 OPA 约束框架来强制执行基于自定义资源定义(CRD)的策略。使用 CRD 允许集成的 Kubernetes 体验,将策略编写与实现分离。策略模板称为 约束模板,可以在集群间共享和重用。Gatekeeper 支持资源验证和审计功能。Gatekeeper 的一个很大的优点是它的可移植性,这意味着您可以将其实现在任何 Kubernetes 集群上,如果您已经使用 OPA,可能可以将该策略迁移到 Gatekeeper 上。
注意
Gatekeeper 是一个成熟的开源项目。请访问官方的上游存储库获取最新稳定版本。
示例策略
在深入了解如何配置 Gatekeeper 之前,保持解决问题的关注至关重要。虽然每个组织/团队都需要根据其需求优化其策略,但一些普遍适用的策略可作为最佳实践。让我们看一些解决常见合规问题的策略作为背景:
-
服务不得在互联网上公开。
-
仅允许来自受信任的容器注册表的容器。
-
所有容器必须设置资源限制。
-
Ingress 主机名不得重叠。
-
Ingress 必须仅使用 HTTPS。
Gatekeeper 术语
Gatekeeper 采用了与 OPA 相同的大部分术语。了解这些术语对您理解 Gatekeeper 如何运作至关重要。Gatekeeper 使用 OPA 约束框架,引入了三个新术语:
-
约束
-
Rego
-
约束模板
约束
最好的理解约束的方式是将其视为应用于 Kubernetes 资源规范特定字段和值的限制。这实际上只是在说策略的长远方式。当定义约束时,您实际上是在声明您不希望允许这样做。这种方法的含义是资源在没有发出拒绝的约束时会被隐式地允许。这是一个重要的细微差别,因为它不是允许您希望的 Kubernetes 资源规范字段和值,而是只拒绝您不希望的。这种架构决策非常适合 Kubernetes 资源规范,因为它们是不断变化的。
Rego
Rego 是一种 OPA 本机查询语言。 Rego 查询是对 OPA 存储的数据的断言。 Gatekeeper 在约束模板中存储 rego。
约束模板
将其视为一个策略模板。它是可移植和可重用的。约束模板由类型化参数和用于重用的目标 rego 组成。
定义约束模板
约束模板是一个自定义资源定义(CRD),提供了模板化策略的手段,以便进行共享或重用。此外,可以验证策略的参数。让我们在上文示例的背景下看一个约束模板,来自上游Gatekeeper 策略库。在以下示例中,我们分享了一个约束模板,提供策略“仅允许来自可信容器注册表的容器”:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
annotations:
metadata.gatekeeper.sh/title: "Allowed Repositories"
metadata.gatekeeper.sh/version: 1.0.0
description: >-
Requires container images to begin with a string from the specified list.
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
# Schema for the `parameters` field
openAPIV3Schema:
type: object
properties:
repos:
description: The list of prefixes a container image is allowed to
have.
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 = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>,
allowed repos are %v",
[container.name, container.image, input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ;
good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("initContainer <%v> has an invalid image repo <%v>,
allowed repos are %v",
[container.name, container.image, input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.ephemeralContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ;
good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("ephemeralContainer <%v> has an invalid image repo <%v>,
allowed repos are %v",
[container.name, container.image, input.parameters.repos])
}
约束模板由三个主要组成部分组成:
Kubernetes 必需的 CRD 元数据
名称是最重要的部分。最佳做法是使其描述性足够强,以便轻松识别策略的目的。我们稍后引用此名称。
输入参数的模式
根据验证字段指示,此部分定义了输入参数及其关联类型。在本例中,我们有一个名为repos的单一参数,它是一个字符串数组。
策略定义
根据target字段指示,此部分包含模板化的 rego(在 OPA 中定义策略的语言)。使用约束模板允许重复使用模板化的 rego,这意味着通用策略可以共享。如果规则匹配,则违反了约束。
定义约束
要使用先前的约束模板,我们必须创建约束资源。约束资源的目的是为我们之前创建的约束模板提供必要的参数。您可以看到以下示例中定义的资源的kind是K8sAllowedRepos,它映射到先前部分定义的约束模板:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: prod-repo-is-openpolicyagent
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "production"
parameters:
repos:
- "openpolicyagent/"
约束包含两个主要部分:
Kubernetes 元数据
请注意,此约束属于kind K8sAllowedRepos,与约束模板的名称匹配。
规范
match 字段定义了策略的意图范围。在此示例中,我们仅匹配生产命名空间中的 Pod。
参数定义策略的意图。请注意,它们与前一节约束模板架构的类型匹配。在本例中,我们只允许以 openpolicyagent/ 开头的容器镜像。
约束具有以下操作特征:
-
逻辑 AND
- 当多个策略验证相同字段时,如果一个违反,则整个请求被拒绝
-
允许早期错误检测的模式验证
-
选择标准
-
可以使用标签选择器
-
仅约束特定种类
-
仅在特定命名空间约束
-
数据复制
在某些情况下,您可能希望将当前资源与集群中的其他资源进行比较,例如,“Ingress 主机名不得重叠”的情况。为了评估规则,OPA 需要将所有其他 Ingress 资源缓存到其缓存中。Gatekeeper 使用 config 资源来管理在 OPA 缓存中缓存哪些数据,以执行诸如前述的评估。此外,config 资源还用于审计功能,稍后我们会详细探讨。
以下示例 config 资源缓存 v1 服务、Pod 和命名空间:
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: gatekeeper-system
spec:
sync:
syncOnly:
- kind: Service
version: v1
- kind: Pod
version: v1
- kind: Namespace
version: v1
UX
Gatekeeper 允许实时反馈给集群用户资源违反了定义的策略。如果我们考虑前面章节的例子,我们只允许从以 openpolicyagent/ 开头的仓库获取容器。
让我们尝试创建以下资源;根据当前策略,它是不合规的:
apiVersion: v1
kind: Pod
metadata:
name: opa
namespace: production
spec:
containers:
- name: opa
image: quay.io/opa:0.9.2
这会给您定义在约束模板中的违规消息:
$ kubectl create -f bad_resources/opa_wrong_repo.yaml
Error from server (Forbidden): error when creating "STDIN": admission webhook
"validation.gatekeeper.sh" denied the request: [repo-is-openpolicyagent]
container <opa> has an invalid image repo <quay.io/opa:0.9.2>, allowed
repos are ["openpolicyagent/"]
使用执法行动和审计
到目前为止,我们仅讨论了如何定义策略并将其作为请求接受过程的一部分执行。约束包括配置 enforcementAction 的能力,默认设置为 deny。除了 deny 外,enforcementAction 还允许接受 warn 和 dryrun 的值。当我们考虑部署策略时,并不总是情况下你正在应用到一个已有资源的集群或命名空间。因此,了解如何在已部署工作负载的集群上部署策略,并有信心能够识别和修复策略违规,是非常重要的。enforcementAction 字段允许您定义其行为。当设置为 deny 时,违反策略的资源将不会被创建,并且错误消息将被记录到审计日志并返回给用户。如果设置为 warn,资源将会被创建;但是,会记录警告消息到审计日志并返回给用户。最后,如果设置为 dryrun,资源将被创建,并且违反策略的资源将在审计日志中可用。
无论您决定使用何种 enforcementAction,Gatekeeper 都会定期评估资源,根据任何配置的策略提供审计日志。这有助于检测根据策略配置错误的资源,并允许进行修复。审计结果存储在约束的状态字段中,通过简单使用 kubectl 即可找到。要使用审计,必须复制要审计的资源。有关更多详细信息,请参阅 “数据复制”。
让我们看看您在前一节中定义的名为 prod-repo-is-openpolicyagent 的约束。在这种情况下,假设我们已经在生产命名空间中运行了一个名为 nginx 的 Pod,并且我们希望使用审计来检查其符合策略:
$ kubectl get k8sallowedrepos
NAME ENFORCEMENT-ACTION TOTAL-VIOLATIONS
prod-repo-is-openpolicyagent deny 1
$ kubectl get k8sallowedrepos prod-repo-is-openpolicyagent -o yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: ...
creationTimestamp: "..."
generation: 1
name: prod-repo-is-openpolicyagent
resourceVersion: "..."
uid: ...
spec:
match:
kinds:
- apiGroups:
- ""
kinds:
- Pod
namespaces:
- production
parameters:
repos:
- openpolicyagent/
status:
auditTimestamp: "2022-11-27T23:37:42Z"
totalViolations: 1
violations:
- enforcementAction: deny
group: ""
kind: Pod
message: container <nginx> has an invalid image repo <nginx>, allowed repos
are ["openpolicyagent/"]
name: nginx
namespace: production
version: v1
检查时,您可以看到审计最后运行的时间在 auditTimestamp 字段中。我们还看到所有违反此约束的资源,本例中仅有 nginx Pod,在 violations 中,以及 enforcementAction。
变异
除了资源验证外,Gatekeeper 还允许您配置变异策略。变异策略允许您在准入时修改 Kubernetes 资源。通常情况下,在准入时修改资源并不被认为是最佳实践。Gatekeeper “神奇地”修改资源是云原生反模式,与 Kubernetes 的声明性本质相违背。这里简单提到变异策略,旨在提供指导,以避免除非您的用例绝对需要,并且已经尽力遵循其他最佳实践。有关如何实施 Kubernetes 资源的声明式最佳实践的更多详细信息,请参阅 第十八章。
测试策略
随着 GitOps 理念的广泛采纳,将策略和评估作为本地测试或 CI/CD 流水线的一部分已成为必备。Gatekeeper 配备了一个 gator CLI,使您能够获取约束模板和约束,并进行本地评估。这是一个很好的工具,用于构建新策略,对资源进行测试,并在部署到生产集群之前解决任何问题。Gatekeeper 文档 提供了使用 gator CLI 进行策略测试的实用指南。
熟悉 Gatekeeper
如果您希望进一步探索 Gatekeeper,请注意,该存储库附带了出色的演示内容,带您详细了解构建银行合规性政策的示例。我们强烈建议您参与这一演示,以深入了解 Gatekeeper 的操作方式。您可以在 此 Git 存储库 中找到演示。Gatekeeper 还维护着一个 公共库,其中包含一些政策,您可以通过 ArtifactHub 提供的简易安装指南将其应用到您的集群上。
政策与治理最佳实践
在实施集群上的策略和治理时,您应考虑以下最佳实践:
-
如果您想强制执行 Pod 中的特定字段,需要确定要检查和强制执行的 Kubernetes 资源规范。例如,让我们考虑 Deployments 的情况。Deployments 管理 ReplicaSets,ReplicaSets 管理 Pods。我们可以在所有三个级别强制执行,但最佳选择是在运行时之前的最低交接点,即 Pod。然而,这个决定有其影响。例如,当我们尝试部署不符合规范的 Pod 时,如在 “UX” 中所见,将不会显示用户友好的错误消息。这是因为用户并未创建不符合规范的资源,而是 ReplicaSet 在创建。这种经历意味着用户需要通过运行
kubectl describe来确定资源是否符合规范,尽管这可能看起来很繁琐,但与其他 Kubernetes 特性(如 Pod 安全性)的行为是一致的。 -
约束可以根据以下标准应用于 Kubernetes 资源:种类、命名空间和标签选择器。我们强烈建议尽可能将约束范围限定在您希望应用的资源上。这样可以确保随着集群资源的增长,政策行为保持一致,并且不需要评估不需要的资源,避免将不需要评估的资源传递给 OPA,这可能导致其他效率低下的情况发生。
-
在已部署资源的集群上,利用
warn和dryrun结合审核,在将enforcementAction设置为deny之前,修复违反策略的资源。 -
不要使用变异策略;而是考虑其他声明性方法,包括 GitOps。
-
不推荐在可能涉及敏感数据的情况下同步和执行,例如 Kubernetes 密钥。考虑到 OPA 可能将其保存在缓存中(如果配置为复制该数据),并且资源将被传递给 Gatekeeper,这会留下潜在的攻击面。
-
如果定义了许多约束,拒绝约束意味着整个请求都将被拒绝。无法使此功能成为逻辑 OR。
摘要
在本章中,我们讨论了为什么策略和治理很重要,并介绍了一个基于 OPA 构建的项目,它是一个云原生生态系统策略引擎,提供了一种 Kubernetes 本地方法来处理策略和治理。现在,当安全团队询问“我们的集群是否符合我们定义的策略?”时,您应该已经准备好并自信了。
第十二章:管理多个集群
在本章中,我们讨论了管理多个 Kubernetes 集群的最佳实践。我们深入探讨了多集群管理与联邦之间的区别、管理多个集群的工具以及管理多个集群的操作模式的细节。
你可能会想为什么需要多个 Kubernetes 集群。Kubernetes 被设计为将许多工作负载整合到一个单一集群中,是吗?这是正确的,但有些场景可能需要多个集群,比如跨区域的工作负载、爆炸半径的考虑、合规性和特定的工作负载。
我们讨论了这些场景,并探讨了在 Kubernetes 中管理多个集群的工具和技术。
为什么需要多个集群?
当采用 Kubernetes 时,你可能会拥有多个集群,甚至可能从一开始就分别部署生产环境、暂存环境、用户验收测试(UAT)或开发环境。Kubernetes 提供了一些多租户特性,如命名空间,这是将集群分割成较小逻辑结构的一种方式。命名空间允许你定义基于角色的访问控制(RBAC)、配额、Pod 安全策略和网络策略,以实现工作负载的分离。这是分隔多个团队和项目的好方法,但在决定使用多集群架构还是单一集群架构时,需要考虑其他问题:
-
爆炸半径
-
合规性
-
安全性
-
硬多租户
-
基于区域的工作负载
-
特定的工作负载
在思考架构时,爆炸半径应当置于首位。这是我们在设计多集群架构时看到的主要问题之一。在微服务架构中,我们采用断路器、重试、隔舱和速率限制来限制系统的损害范围。你应当在基础架构层面设计相同的机制,多集群可以帮助防止由软件问题导致的级联故障影响。例如,如果你有一个集群为 500 个应用提供服务,而且遇到平台问题,将会导致这 500 个应用全部受到影响。如果这些 500 个应用分布在五个集群中,当你遇到平台层面的问题时,只会影响到 20%的应用。然而,这样做的不利之处在于你需要管理五个集群,而且聚合比率不如单一集群好。Dan Woods 撰写了一篇出色的文章,详细描述了在生产 Kubernetes 环境中的实际级联故障。这是一个很好的例子,说明为什么你应当考虑在较大的环境中采用多集群架构。
合规性是多集群设计的另一个关注点,因为对于 PCI(付款卡行业)、HIPAA(健康保险便携性和责任)和其他工作负载,有特殊的考虑因素。 Kubernetes 确实提供了一些多租户功能,但这些工作负载如果与通用目的工作负载隔离,可能更容易管理。这些符合规定的工作负载可能具有关于安全加固、非共享组件或专用工作负载需求的具体要求。与将集群视为如此专业化的处理相比,将这些工作负载分开要容易得多。
在大型 Kubernetes 集群中,安全性可能变得难以管理。当您开始将更多团队引入 Kubernetes 集群时,每个团队可能具有不同的安全需求,满足这些需求在大型多租户集群中可能变得非常困难。即使在单个集群中管理 RBAC、网络策略和 Pod 安全策略也可能在规模化时变得困难。对网络策略的小改动可能会无意中为集群的其他用户打开安全风险。通过多个集群,您可以限制由于配置错误而带来的安全影响。如果您决定较大的 Kubernetes 集群符合您的要求,那么请确保您有非常好的操作过程来进行安全更改,并且了解对 RBAC、网络策略和 Pod 安全策略进行更改的影响范围。
Kubernetes 并不提供硬多租户,因为它与集群中所有工作负载共享相同的 API 边界。使用命名空间可以实现良好的软多租户,但不足以保护集群内的恶意工作负载。对于许多用户来说,硬多租户并不是必需的;他们信任将在集群内运行的工作负载。如果您是云提供商、托管基于软件即服务(SaaS)的软件或托管不受信任的工作负载和不受信任的用户控制,则通常需要硬多租户。
注意
Kubernetes 项目确实通过虚拟集群来解决硬多租户问题,超出了本书的范围。在项目的 GitHub 页面上可以找到更多信息。
当运行需要从区域端点提供流量的工作负载时,您的设计将包括基于每个区域的多个集群。当您拥有全球分布的应用程序时,在那时运行多个集群就成为必要条件。当您有需要区域分布的工作负载时,使用多个集群的集群联合是一个非常好的用例,我们将在本章后面进一步探讨此问题。
专业工作负载,例如高性能计算(HPC)、机器学习(ML)和网格计算,也需要在多集群架构中加以考虑。这些类型的专业工作负载可能需要特定类型的硬件,具有独特的性能配置文件,并且集群的用户也可能是专业化的。我们发现这种用例在设计决策中不太常见,因为拥有多个 Kubernetes 节点池可以帮助解决专用硬件和性能配置文件问题。当您需要一个非常大的集群用于 HPC 或机器学习工作负载时,您应该考虑专门为这些工作负载分配集群。
使用多集群,您可以免费获得隔离,但也有设计方面的问题需要在开始时解决。
多集群设计的关注点
在选择多集群设计时,会遇到一些挑战。其中一些挑战可能会阻止您尝试多集群设计,因为这种设计可能会使架构过于复杂化。我们发现用户常遇到的一些常见挑战包括:
-
数据复制
-
服务发现
-
网络路由
-
运营管理
-
持续部署
数据复制和一致性一直是在跨地理区域和多个集群部署工作负载的关键。在运行这些服务时,您需要决定何处运行以及开发复制策略。大多数数据库都有内置工具来执行复制,但您需要设计应用程序以处理复制策略。对于 NoSQL 类型的数据库服务来说,这可能更容易,因为它们可以处理跨多个实例的扩展,但您仍然需要确保您的应用程序可以处理跨地理区域的最终一致性,或者至少处理区域之间的延迟。一些云服务,例如 Google Cloud Spanner 和 Microsoft Azure CosmosDB,提供了建立在多个地理区域处理数据复杂性的数据库服务。
每个 Kubernetes 集群都部署自己的服务发现注册表,并且注册表在多个集群之间不同步。这使得应用程序难以轻松识别和发现彼此。诸如 HashiCorp 的 Consul 之类的工具可以透明地同步来自多个集群甚至在 Kubernetes 之外的服务。其他工具如 Istio、Linkerd 和 Cilium 正在构建多集群架构,以扩展集群之间的服务发现。
Kubernetes 使得在集群内部进行网络工作变得非常简单,因为它是一个扁平网络,并且避免使用网络地址转换(NAT)。如果需要在集群内外路由流量,则这变得更加复杂。对集群的入口实施为将入口与集群的 1:1 映射,因为它不支持具有 Ingress 资源的多集群拓扑。您还需要考虑集群间出口流量的路由。当应用程序位于单个集群内时,这很容易,但在引入多集群时,您需要考虑服务的额外跳跃的延迟。对于具有紧密耦合依赖关系的应用程序,应考虑在同一集群内运行这些服务,以消除延迟和额外的复杂性。
管理多集群的最大开销之一是运营管理。不再是管理和保持一两个一致的集群,你可能需要在你的环境中管理多个集群。管理多集群的最重要的方面之一是确保你有良好的自动化实践,因为这将有助于减少运营负担。在自动化你的集群时,你需要考虑基础设施部署和管理集群的附加功能。对于管理基础设施来说,使用像 HashiCorp 的 Terraform 这样的工具可以帮助部署和管理跨多个集群的一致状态。
使用像 Terraform 这样的基础设施即代码(IaC)工具将使您获得部署集群的可重复方法的好处。另一方面,您还需要能够一致地管理集群的附加功能,例如监视、日志记录、入口、安全性和其他工具。安全性是运营管理的另一个重要方面,您必须能够在集群间维护安全策略、RBAC 和网络策略。本章后面我们将更深入地探讨使用自动化来维护一致的集群。
随着多个集群和持续交付(CD),现在您需要处理多个 Kubernetes API 端点与单个 API 端点之间的差异。这可能会在应用程序分发中带来挑战。您可以轻松地管理多个流水线,但假设您有一百个不同的流水线需要管理,则应用程序分发将变得非常困难。考虑到这一点,您需要考虑不同的方法来管理这种情况。我们稍后在本章中会探讨帮助管理此问题的解决方案。
管理多集群部署
在管理多集群部署时,您希望采取的第一步之一是使用像 Terraform 这样的 IaC 工具设置部署。其他部署工具,如 kubespray、kops 或其他特定于云提供商的工具,都是有效的选择,但更重要的是使用一个允许您为可重复性进行源代码控制的工具。
自动化是成功管理环境中多个集群的关键。您可能无法在第一天就自动化所有内容,但您应该优先考虑自动化集群部署和操作的所有方面。
一个有趣的项目是Kubernetes Cluster API,这是一个将声明式、Kubernetes 风格的 API 引入集群创建、配置和管理的 Kubernetes 项目。它在核心 Kubernetes 之上提供了可选的附加功能。Cluster API 通过一个共同的 API 提供了集群级配置,这将使您能够轻松自动化和构建围绕集群自动化的工具。Cluster API 目前仍处于早期阶段,但这是一个值得关注的项目。
部署和管理模式
Kubernetes 操作员作为基础设施即代码概念的实现引入。使用它们允许您在 Kubernetes 集群中抽象应用程序和服务的部署。例如,假设您想要在监视 Kubernetes 集群时标准化为 Prometheus。您需要为每个集群和团队创建和管理各种对象(部署、服务、入口等)。您还需要维护 Prometheus 的基本配置,如版本、持久性、保留策略和副本。可以想象,在大量集群和团队中维护这样的解决方案可能会很困难。
您可以安装prometheus-operator而不是处理如此多的对象和配置。这扩展了 Kubernetes API,暴露了多个名为Prometheus、ServiceMonitor、PrometheusRule和AlertManager的新对象种类,允许您仅使用几个对象指定 Prometheus 部署的所有细节。您可以使用kubectl工具管理这些对象,就像管理任何其他 Kubernetes API 对象一样。
图 12-1 展示了prometheus-operator的架构。

图 12-1. prometheus-operator 架构
利用Operator模式来自动化关键操作任务可以帮助提高整体集群管理能力。Operator 模式由 CoreOS 团队在 2016 年引入,使用 etcd 操作员和prometheus-operator。Operator 模式建立在两个概念之上:
-
自定义资源定义
-
自定义控制器
自定义资源定义(CRDs)是允许您根据您定义的自己的 API 扩展 Kubernetes API 的对象。
自定义控制器基于核心 Kubernetes 资源和控制器的概念构建。自定义控制器允许您通过监视来自 Kubernetes API 对象(如命名空间、部署、Pod 或您自己的 CRD)的事件来构建自己的逻辑。通过自定义控制器,您可以以声明性方式构建您的 CRD。如果考虑到 Kubernetes 部署控制器如何在协调循环中始终维护部署对象的状态来维护其声明状态,这也为您的 CRD 带来了控制器的同样优势。
在使用操作员模式时,您可以将操作工具中需要执行的操作自动化到多集群中。让我们以Elasticsearch 操作员为例。Elasticsearch 操作员可以执行以下操作:
-
主、客户端和数据节点的副本
-
高可用部署的区域
-
主节点和数据节点的卷大小
-
调整集群大小
-
Elasticsearch 集群备份的快照
如您所见,操作员为您管理 Elasticsearch 时提供了自动化,例如自动快照备份和调整集群大小。这种方法的美妙之处在于您通过熟悉的 Kubernetes 对象来管理一切。
考虑如何在您的环境中利用不同的操作员,如prometheus-operator,以及如何构建您自己的自定义操作员来卸载常见的运维任务。
管理集群的 GitOps 方法
GitOps被 Weaveworks 团队推广开来,其理念和基础是基于他们在生产中运行 Kubernetes 的经验。GitOps 将软件开发生命周期的概念应用到运维中。通过 GitOps,您的 Git 仓库成为了事实来源,而您的集群则与配置好的 Git 仓库同步。例如,如果您更新了一个 Kubernetes 部署清单,这些配置变更会自动反映在集群状态中。
通过使用这种方法,您可以更容易地维护一致的多集群,并避免整个设备组中的配置漂移。GitOps 允许您声明性地描述多个环境的集群,并努力维护集群的状态。GitOps 的实践可以应用于应用程序交付和运维,但在本章中,我们专注于使用它来管理集群和运维工具。
Weaveworks Flux 是最早支持 GitOps 方法的工具之一,也是我们在本章剩余部分将使用的工具。在云原生生态系统中发布了许多新工具值得关注,例如来自 Intuit 团队的 Argo CD,它也被广泛采用了 GitOps 方法。
我们将深入探讨在第十八章中利用 GitOps 模型的方法,但以下内容快速概述了利用 GitOps 管理集群的好处。
图 12-2 呈现了 GitOps 工作流程的表示。

图 12-2. GitOps 工作流程
因此,让我们在您的集群中设置 Flux,并将一个存储库同步到集群中:
git clone https://github.com/weaveworks/flux
cd flux
现在您需要更改部署清单,以配置它使用您从第五章分叉的存储库。 修改部署文件中的以下行以匹配您的分叉 GitHub 存储库:
vim deploy/flux-deployment.yaml
使用您的 Git 存储库修改以下行:
--git-url=git@github.com:weaveworks/flux-get-started
(ex. --git-url=git@github.com:your_repo/kbp )
现在,继续在您的集群中部署 Flux:
kubectl apply -f deploy
当 Flux 安装时,它会创建一个 SSH 密钥,以便可以使用 Git 存储库进行身份验证。 使用 Flux 命令行工具检索 SSH 密钥,以便您可以配置访问您的分叉存储库; 首先,您需要安装fluxctl。
对于 macOS:
brew install fluxctl
对于 Linux Snap 软件包:
snap install fluxctl
对于所有其他软件包,您可以在这里找到最新的二进制文件:
fluxctl identity
打开 GitHub,导航到您的分叉版本,转到“设置”>“部署密钥”,点击“添加部署密钥”,给它一个标题,选择“允许写访问”复选框,粘贴 Flux 公钥,然后点击“添加密钥”。 更多关于如何管理部署密钥的信息,请参见GitHub 文档。
现在,如果您查看 Flux 日志,您应该会看到它正在与您的 GitHub 存储库同步:
kubectl -n default logs deployment/flux -f
在您看到它与您的 GitHub 存储库同步后,您应该会看到创建了 Elasticsearch、Prometheus、Redis 和前端 Pods:
kubectl get pods -w
通过这个例子完成后,您应该能够看到如何轻松地将您的 GitHub 存储库状态与 Kubernetes 集群同步。 这使得管理集群中的多个操作工具变得更加容易,因为多个集群可以与单个存储库同步,并且您避免了拥有雪花集群的情况。
多集群管理工具
在处理多个集群时,使用kubectl可能会立即变得令人困惑,因为您需要设置不同的上下文来管理不同的集群。 处理多个集群时,您需要立即安装的两个工具是kubectx和kubens,它们允许您轻松在多个上下文和命名空间之间切换。
当您需要一个完整的多集群管理工具时,Kubernetes 生态系统中有一些工具可以用来管理多个集群。 以下是一些较受欢迎工具的摘要:
Rancher 在一个集中管理的用户界面中集中管理多个 Kubernetes 集群。它监控、管理、备份和恢复跨本地、云和托管 Kubernetes 设置中的 Kubernetes 集群。它还提供了控制跨多个集群部署的应用程序和运维工具。
开放集群管理 (OCM)
OCM 是一个专注于 Kubernetes 应用程序的多集群和多云场景的社区驱动项目。它提供集群注册、工作负载分发以及策略和工作负载的动态放置。
Gardener 采用了不同的多集群管理方法,它利用 Kubernetes 原语为最终用户提供 Kubernetes 即服务。它支持所有主要的云供应商,并由 SAP 的工作人员开发。该解决方案面向构建 Kubernetes 即服务的用户。
Kubernetes 联邦
Kubernetes 在 Kubernetes 1.3 中首次引入了 Federation v1,后来由于 Federation v2 的出现而被弃用。Federation v1 的初衷是帮助将应用程序分发到多个集群。Federation v1 是利用 Kubernetes API 构建的,并且在设计中大量依赖于 Kubernetes 注解,这在设计上带来了一些问题。设计紧密耦合于核心 Kubernetes API,使得 Federation v1 相当单片化。当时的设计决策可能不是坏选择,但它们是基于当时可用的原语构建的。Kubernetes CRD 的引入允许以不同的方式思考 Federation 的设计。
管理多个集群的最佳实践
在管理多个 Kubernetes 集群时,请考虑以下最佳实践:
-
限制您的集群爆炸半径,以确保级联故障不会对应用程序造成更大的影响。
-
如果您拥有 PCI、HIPPA 或 HiTrust 等监管问题,请考虑利用多集群来简化将这些工作负载与普通工作负载混合的复杂性。
-
如果硬多租户是业务需求,则应将工作负载部署到专用集群。
-
如果您的应用程序需要多个区域,请利用全局负载均衡器来管理集群之间的流量。
-
您可以将专业的工作负载(例如 HPC)拆分为其自己的独立集群,以确保满足工作负载的专门需求。
-
如果您部署的工作负载将分布在多个区域数据中心,请首先确保有工作负载的数据复制策略。跨区域的多个集群可以很容易,但是跨区域复制数据可能会很复杂,因此确保有处理异步和同步工作负载的可靠策略。
-
利用 Kubernetes 操作者,如
prometheus-operator或 Elasticsearch 操作者来处理自动化运维任务。 -
当设计您的多集群策略时,还需考虑如何在集群之间实现服务发现和网络互联。像 HashiCorp 的 Consul 或 Istio 这样的服务网格工具可以帮助跨集群进行网络互联。
-
确保您的持续交付策略能够处理区域或多集群之间的多次部署。
-
调查利用 GitOps 方法来管理多个集群操作组件,以确保所有集群在整个集群中保持一致性。GitOps 方法并不适用于每个环境,但您至少应该调查一下,以减轻多集群环境的操作负担。
摘要
在本章中,我们讨论了管理多个 Kubernetes 集群的不同策略。在开始时考虑您的需求及这些需求是否符合多集群拓扑结构是非常重要的。首先要考虑的情况是,您是否真正需要硬多租户性能,因为这将自动要求采用多集群策略。如果不需要,考虑您的合规需求以及是否有操作能力来承担多集群架构的额外开销。最后,如果您选择更多、更小的集群,请确保自动化它们的交付和管理,以减少操作负担。
第十三章:将外部服务与 Kubernetes 集成
在本书的许多章节中,我们讨论了如何在 Kubernetes 中构建、部署和管理服务。然而,事实是系统并不孤立存在,我们构建的大多数服务都需要与 Kubernetes 集群之外的系统和服务进行交互。这可能是因为我们正在构建新服务,这些服务由运行在虚拟或物理机器上的遗留基础设施访问。此外,这也可能是因为我们构建的服务需要访问运行在本地数据中心物理基础设施上的现有数据库或其他服务。最后,您可能有多个 Kubernetes 集群,这些服务需要互联互通。因此,能够公开、共享并构建跨越 Kubernetes 集群边界的服务是构建现实应用程序的重要组成部分。
将服务导入 Kubernetes
将 Kubernetes 与外部服务连接的最常见模式是通过 Kubernetes 服务来消费存在于 Kubernetes 集群外部的服务。通常情况下,这是因为 Kubernetes 被用于新应用开发或作为与传统资源(如本地数据库)接口的服务。在许多现有应用程序中,应用程序的某些部分比其他部分更容易移动。例如,出于数据治理、合规性或业务连续性的原因,具有关键数据的数据库可能需要保留在本地。同时,在 Kubernetes 中为这些传统数据库构建新接口也有显著的好处。如果每次迁移到 Kubernetes 都需要整个应用程序的搬迁,那么许多应用程序可能会永远停留在其传统实现上。相反,本章展示了如何将新应用程序的云原生开发与现有服务(如可能运行在传统虚拟机、裸机服务器甚至大型机上的数据库)集成。
当我们考虑将外部服务从 Kubernetes 中访问时,第一个挑战就是确保网络能够正常工作。使网络操作起来的具体细节与数据库的位置以及 Kubernetes 集群的位置有关。因此,它们超出了本书的范围,但通常情况下,基于云的 Kubernetes 提供商可以将集群部署到用户提供的虚拟网络(VNET)中,然后这些虚拟网络可以与本地网络进行互联。
当您在 Kubernetes 集群中的 Pod 与本地资源之间建立了网络连接之后,下一个挑战是使外部服务看起来和感觉像是 Kubernetes 服务的一部分。在 Kubernetes 中,服务发现通过域名系统(DNS)查找进行,因此,要使我们的外部数据库感觉像是 Kubernetes 的一个本地部分,我们需要在相同的 DNS 中使数据库可发现。接下来我们将详细讨论如何做到这一点。
用于稳定 IP 地址的无选择器服务
实现这一点的第一种方法是使用无选择器的 Kubernetes 服务。当您创建一个没有选择器的 Kubernetes 服务时,不存在与虚构服务选择器匹配的 Pod,因此不会执行负载平衡。相反,您可以将这种无选择器服务编程为具有您想要添加到 Kubernetes 集群的外部资源的特定 IP 地址的端点。这样,当 Kubernetes Pod 查找 your-database 时,内置的 Kubernetes DNS 服务器将把它翻译为您外部服务的服务 IP 地址。以下是一个用于外部数据库的无选择器服务示例:
apiVersion: v1
kind: Service
metadata:
name: my-external-database
spec:
ports:
- protocol: TCP
port: 3306
targetPort: 3306
当服务存在时,您需要更新其端点以包含提供 24.1.2.3 的数据库 IP 地址。
apiVersion: v1
kind: Endpoints
metadata:
# Important! This name has to match the Service.
name: my-external-database
subsets:
- addresses:
- ip: 24.1.2.3
ports:
- port: 3306
图 13-1 描述了如何在 Kubernetes 中集成服务。正如您所见,Pod 会像对待任何其他 Kubernetes 服务一样在集群 DNS 服务器中查找服务。但是,与其给出 Kubernetes 集群中另一个 Pod 的 IP 地址不同,它会给出一个对应于 Kubernetes 集群外部资源的 IP 地址。通过这种方式,开发人员甚至可能不知道服务是在集群外部实现的。

图 13-1. 服务集成
用于稳定 DNS 名称的基于 CNAME 的服务
前面的例子假定您试图将外部资源集成到 Kubernetes 集群中,并具有稳定的 IP 地址。尽管物理本地资源通常如此,这在网络拓扑结构可能不总是正确。在虚拟机(VM)IP 地址更动态的云环境中,这通常不是真的。或者,服务可能有多个副本坐落在单一基于 DNS 的负载均衡器后面。在这些情况下,您尝试桥接到集群中的外部服务没有稳定 IP 地址,但它确实有一个稳定的 DNS 名称。
对于这些情况,您可以定义基于 CNAME 的 Kubernetes 服务。如果您对 DNS 记录不熟悉,CNAME 或 规范名称 记录指示特定的 DNS 地址应翻译为不同的 规范 DNS 名称。例如,对于包含 bar.com 的 foo.com 的 CNAME 记录表示,任何查询 foo.com 的人应执行递归查找 bar.com 以获取正确的 IP 地址。您可以使用 Kubernetes 服务在 Kubernetes DNS 服务器中定义 CNAME 记录。例如,如果您有一个外部数据库,其 DNS 名称为 database.myco.com,您可以创建一个名为 myco-database 的 CNAME 服务。这样的服务如下所示:
kind: Service
apiVersion: v1
metadata:
name: myco-database
spec:
type: ExternalName
externalName: database.myco.com
使用这种方式定义的服务,任何对 myco-database 的查询都将递归解析为 database.myco.com。当然,为使此功能生效,您的外部资源的 DNS 名称 也 需要能够从 Kubernetes DNS 服务器解析。如果 DNS 名称是全球可访问的(例如来自知名的 DNS 服务提供商),这将自动生效。然而,如果外部服务的 DNS 位于公司内部的 DNS 服务器中(例如仅服务内部流量的 DNS 服务器),Kubernetes 集群可能默认不知道如何解析对此公司 DNS 服务器的查询。
要将集群的 DNS 服务器设置为与备用 DNS 解析器通信,您需要调整其配置。这可以通过更新 Kubernetes ConfigMap 中的 DNS 服务器配置文件来完成。
CNAME 记录是一种有用的方式,可将具有稳定 DNS 名称的外部服务映射到在集群内可发现的名称。一开始,将一个众所周知的 DNS 地址重新映射到集群本地 DNS 地址似乎有些违反直觉,但是所有服务看起来和感觉相同的一致性通常值得增加的少量复杂性。此外,因为 CNAME 服务与所有 Kubernetes 服务一样,是按命名空间定义的,您可以使用命名空间将相同的服务名称(例如database)映射到不同的外部服务(例如canary或production)。
基于活动控制器的方法
在一组有限的情况下,以上两种在 Kubernetes 内部暴露外部服务的方法都不可行。一般情况下,这是因为要暴露到 Kubernetes 集群内部的服务既没有稳定的 DNS 地址,也没有单一的稳定 IP 地址。在这种情况下,将外部服务暴露到 Kubernetes 集群内部变得更加复杂,但并非不可能。
要实现这一点,您需要对 Kubernetes 服务在底层如何工作有一定的了解。Kubernetes 服务由两种不同的资源组成:您无疑熟悉的 Service 资源以及代表组成服务的 IP 地址的 Endpoints 资源。在正常操作中,Kubernetes 控制器管理器根据服务中的选择器填充服务的端点。但是,如果创建一个无选择器的服务,就像第一个稳定 IP 方法一样,服务的 Endpoints 资源将不会被填充,因为没有选择 pod。在这种情况下,您需要提供控制循环来创建和填充正确的 Endpoints 资源。您需要动态查询基础设施,获取要集成到 Kubernetes 之外的服务的 IP 地址,然后使用这些 IP 地址填充您的服务端点。完成这些步骤后,Kubernetes 的机制将接管并正确地配置 DNS 服务器和 kube-proxy,以实现对外部服务的负载均衡。图 13-2 展示了这在实践中的完整工作方式。

图 13-2. 一个外部服务
从 Kubernetes 导出服务
在前一节中,我们探讨了如何将现有服务导入到 Kubernetes 中,但您可能还需要将服务从 Kubernetes 导出到现有环境。这可能是因为您有一个需要访问您正在开发的云原生基础设施中的新 API 的传统内部客户管理的遗留应用程序。或者,您可能正在构建基于微服务的新 API,但由于内部政策或监管要求,您需要与现有的传统 Web 应用程序防火墙(WAF)进行接口。无论原因是什么,能够将 Kubernetes 集群中的服务暴露给其他内部应用程序是许多应用程序的关键设计要求。
这可能是一个挑战,因为在许多 Kubernetes 安装中,pod 的 IP 地址在集群外部通常不是可路由的地址。通过像 flannel 这样的工具或其他网络提供者,建立了 Kubernetes 集群内的路由,以便在 pod 之间以及节点与 pod 之间进行通信,但是通常不会将同样的路由扩展到同一网络中的任意机器。在许多情况下,分配给 pod 的 IP 范围与企业网络的 IP 空间不同,并且路由是不可能的。此外,在云对本地连接的情况下,pod 的 IP 地址并不总是通过 VPN 或网络对等关系广告返回到本地网络。因此,设置传统应用程序与 Kubernetes pod 之间的路由是实现基于 Kubernetes 的服务导出的关键任务。
使用内部负载均衡器导出服务
从 Kubernetes 导出的最简单方法是使用内置的Service对象。如果您之前有 Kubernetes 的使用经验,毫无疑问您已经看到过如何连接云基础负载均衡器以将外部流量引导到集群中的一组 pod。但是,您可能没有意识到大多数云服务提供的是 内部 负载均衡器。内部负载均衡器提供了相同的功能,将虚拟 IP 地址映射到一组 pod,但该虚拟 IP 地址来自内部 IP 地址空间(例如10.0.0.0/24),因此仅从虚拟网络内可路由。您可以通过向服务负载均衡器的元数据字段添加特定于云的注释来激活内部负载均衡器。例如,在 Microsoft Azure 中,您需要添加service.beta.kubernetes.io/azure-load-balancer-internal: "true"注释。在 Amazon Web Services (AWS) 中,注释为service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0。您可以按照以下方式将注释放置在服务资源的metadata字段中:
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
# Replace this as needed in other environments
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
...
当您通过内部负载均衡器导出服务时,您会获得一个稳定的、可路由的 IP 地址,该地址在集群外的虚拟网络上可见。然后,您可以直接使用该 IP 地址或设置内部 DNS 解析来为导出的服务提供发现功能。
在 NodePorts 上导出服务
不幸的是,在本地安装中,无法使用基于云的内部负载均衡器。在这种情况下,使用基于 NodePort 的服务通常是一个不错的解决方案。类型为 NodePort 的服务在集群中的每个节点上导出一个监听器,该监听器将流量从节点的 IP 地址和选定的端口转发到您定义的服务中,如图 13-3 所示。

图 13-3. 基于 NodePort 的服务
下面是一个 NodePort 服务的 YAML 文件示例:
apiVersion: v1
kind: Service
metadata:
name: my-node-port-service
spec:
type: NodePort
...
在创建了类型为 NodePort 的服务之后,Kubernetes 会自动为该服务选择一个端口;您可以从服务中查看spec.ports[*].nodePort字段获取该端口。如果您希望自己选择端口,可以在创建服务时指定,但是 NodePort 必须在集群配置的范围内。该范围的默认值为端口在30000到30999之间。
当服务在此端口上暴露时,Kubernetes 的工作就完成了。要将其导出到集群外的现有应用程序中,你(或你的网络管理员)需要使其可发现。根据应用程序的配置方式,你可能能够为应用程序提供 ${node}:${port} 对的列表,并且应用程序将执行客户端负载平衡。或者,你可能需要在网络中配置一个物理或虚拟负载均衡器,以将流量从虚拟 IP 地址引导到这些 ${node}:${port} 后端列表。此配置的具体细节将根据你的环境而有所不同。
集成外部机器和 Kubernetes
如果前面的解决方案对你都不合适,可能是因为你希望更紧密地集成动态服务发现,那么将 Kubernetes 服务暴露给外部应用程序的最终选择是直接将运行应用程序的机器集成到 Kubernetes 集群的服务发现和网络机制中。这比前面的任何方法都要更具侵入性和复杂性,你应该只在应用程序必要时使用它(这应该是很少的情况)。在某些托管的 Kubernetes 环境中,甚至可能不可行。
当将外部机器集成到集群中用于网络时,你需要确保 pod 网络路由和基于 DNS 的服务发现都能正常工作。做到这一点的最简单方法是在要加入集群的机器上运行 kubelet,但在集群中禁用调度。将 kubelet 节点加入集群超出了本书的范围,但有许多其他书籍或在线资源描述了如何实现这一点。加入节点后,你需要立即使用 kubectl cordon ... 命令将其标记为不可调度,以防止在其上调度任何额外的工作。这种遮挡不会阻止 DaemonSet 将 pod 定位到节点上,因此 KubeProxy 和网络路由的 pod 将定位在该机器上,并使基于 Kubernetes 的服务可被运行在该机器上的任何应用程序发现。
我们刚刚描述的方法对节点的侵入性很大,因为它需要安装 Docker 或其他容器运行时。因此,在许多环境中可能不可行。一个更轻量但更复杂的方法是在机器上仅作为进程运行 kube-proxy,并调整机器的 DNS 服务器。假设你可以设置 pod 路由以正确工作,运行 kube-proxy 将建立机器级网络,使 Kubernetes 服务的虚拟 IP 地址重新映射到组成该服务的 pod 上。如果还将机器的 DNS 更改为指向 Kubernetes 集群 DNS 服务器,那么你将有效地在不属于 Kubernetes 集群的机器上启用 Kubernetes 发现。
这两种方法都很复杂和先进,您不应该轻易采用。如果您发现自己在考虑这种服务发现集成的水平,不妨问问自己是否将要连接到集群的服务实际上可能更容易直接将服务带入集群中。我们在第十六章中有所涉及。
在 Kubernetes 之间共享服务
前面的章节已经描述了如何将 Kubernetes 应用连接到外部服务,以及如何连接外部服务到 Kubernetes 应用,但另一个重要的用例是连接 Kubernetes 集群之间的服务。这可能是为了在不同区域的 Kubernetes 集群之间实现东西向故障转移,或者可能是为了将不同团队运行的服务链接在一起。实现这种交互的过程实际上是前面章节描述的设计的结合体。
首先,您需要暴露第一个 Kubernetes 集群中的服务以启用网络流量。假设您在支持内部负载均衡器的云环境中,并且收到了内部负载均衡器的虚拟 IP 地址为 10.1.10.1. 接下来,您需要将此虚拟 IP 地址集成到第二个 Kubernetes 集群中以启用服务发现。您可以通过与将外部应用程序导入 Kubernetes 相同的方式实现此目的(我们在“将服务导入 Kubernetes”中已经讨论过这一点)。您创建一个无选择器服务,并将其 IP 地址设置为 10.1.10.1. 通过这两个步骤,您已经实现了在两个 Kubernetes 集群内的服务发现和连接集成。
这些步骤相当手动化,虽然对于一小部分静态服务集合来说可能可以接受,但如果你希望在集群之间实现更紧密或自动的服务集成,编写一个在两个集群中都运行的集群守护程序以执行集成操作就是合理的。该守护程序将监视第一个集群中带有特定注解的服务,比如myco.com/exported-service;所有带有此注解的服务将通过无选择器服务导入到第二个集群中。同样,该守护程序还会进行垃圾回收并删除已导出到第二个集群但在第一个集群中不再存在的任何服务。如果在您的每个区域集群中设置了这样的守护程序,您就可以在环境中的所有集群之间实现动态的东西向连接。
Kubernetes 项目中最近还进行了多集群服务 API 的定义工作。这项工作是实验性的,可以在 GitHub 上的 Multi-Cluster Service 项目中找到。在撰写本文时,该项目的实验性质意味着它可能不适合生产用例,但它展示了 Kubernetes 生态系统中多集群服务管理的未来方向。随着它从 alpha 版本到 beta 版本再到一般可用版本的推进,这种服务共享的实现将使构建跨集群微服务应用程序变得更加容易。即使在今天,像 Microsoft Azure 中的 Fleet 集群管理器等工具也开始根据用户需求实现这些多集群服务 API。
第三方工具
到目前为止,本章已经描述了各种导入、导出和连接 Kubernetes 集群及某些外部资源的方法。如果您之前有服务网格技术的经验,这些概念可能会非常熟悉。事实上,有许多第三方工具和项目可以用来将服务与 Kubernetes 以及任意应用程序和机器进行互联。通常,这些工具提供了很多功能,但在操作上也比之前描述的方法复杂得多。然而,如果您发现自己越来越多地构建网络互联性,应该探索服务网格的领域,这个领域正在快速迭代和发展。几乎所有这些第三方工具都有开源组件,但它们也提供商业支持,可以减少运行额外基础设施的操作开销。
集群与外部服务连接的最佳实践
-
在集群和本地环境之间建立网络连接。网络配置可能因不同的站点、云和集群配置而有所不同,但首先确保 Pod 能够与本地环境中的机器进行通信,反之亦然。
-
要访问集群外的服务,您可以使用无选择器服务,并直接编程输入您想要通信的机器(例如数据库)的 IP 地址。如果您没有固定的 IP 地址,您可以使用 CNAME 服务重定向到 DNS 名称。如果既没有 DNS 名称也没有固定服务,您可能需要编写一个动态操作程序,定期将外部服务 IP 地址与 Kubernetes 服务端点同步。
-
要从 Kubernetes 导出服务,请使用内部负载均衡器或 NodePort 服务。内部负载均衡器通常更易于在公共云环境中使用,可以绑定到 Kubernetes 服务本身。当这些负载均衡器不可用时,NodePort 服务可以在集群中的所有机器上公开服务。
-
通过这两种方法的组合,您可以在 Kubernetes 集群之间建立连接,将服务暴露给外部,然后在另一个 Kubernetes 集群中作为无选择器服务消费。
摘要
在现实世界中,并非每个应用都是云原生的。构建生产就绪的应用程序通常涉及将现有系统与新应用程序连接起来。本章描述了如何将 Kubernetes 与传统应用程序集成,以及如何集成跨多个不同 Kubernetes 集群运行的不同服务。除非您有幸能够构建全新的东西,云原生开发始终需要遗留集成。本章介绍的技术将帮助您实现这一点。
第十四章:在 Kubernetes 中运行机器学习
微服务时代、分布式系统和云计算为机器学习模型和工具的民主化提供了完美的环境条件。规模化基础设施现在已经成为商品化,围绕机器学习生态系统的工具正在成熟。Kubernetes 是其中之一,作为一个平台,它在开发者、数据科学家和更广泛的开源社区中变得越来越流行,成为促进机器学习工作流和生命周期的理想环境。像GPT-4和DALL·E这样的大型机器学习模型已经将机器学习推上了舞台,而像OpenAI这样的组织也公开表明它们使用 Kubernetes 来支持这些模型。在本章中,我们将探讨为什么 Kubernetes 是一个优秀的机器学习平台,并为集群管理员和数据科学家提供关于在 Kubernetes 上运行机器学习工作负载时如何获取最大收益的最佳实践。具体而言,我们专注于深度学习而不是传统机器学习,因为深度学习已经迅速成为像 Kubernetes 这样平台上创新的领域。
Kubernetes 为什么是机器学习的绝佳选择?
Kubernetes 快速成为深度学习的创新之地。工具和库(如TensorFlow)的结合使得这项技术更加易于大众使用。那么,是什么让 Kubernetes 成为运行深度学习工作负载的绝佳选择呢?让我们看看 Kubernetes 提供了哪些功能:
普遍存在
Kubernetes 无处不在。所有主要的公共云都支持它,还有专门为私有云和基础设施提供的发行版。将生态系统工具基于 Kubernetes 这样的平台,让用户可以在任何地方运行他们的深度学习工作负载。
可扩展
深度学习工作流通常需要大量的计算资源来高效训练机器学习模型。Kubernetes 自带本地自动缩放功能,使得数据科学家能够轻松实现和微调他们需要的规模级别来训练模型。
可扩展
高效地训练机器学习模型通常需要访问专门的硬件。Kubernetes 允许集群管理员快速简便地向调度器公开新类型的硬件,而无需修改 Kubernetes 源代码。它还允许自定义资源和控制器无缝集成到 Kubernetes API 中,以支持特定的工作流程,如超参数调整。
自助式
数据科学家可以利用 Kubernetes 实现按需自助式的机器学习工作流程,无需专门了解 Kubernetes 本身。
可移植
机器学习模型可以在任何地方运行,只要工具基于 Kubernetes API。这使得机器学习工作负载可以跨 Kubernetes 提供者可移植。
机器学习工作流程
要有效地理解深度学习的需求,您必须了解完整的机器学习工作流程。图 14-1 表示了一个简化的工作流程。

图 14-1. 机器学习开发工作流程
如您所见,工作流程包括以下阶段:
数据集准备
此阶段包括用于训练模型的数据集的存储、索引、编目和元数据。对于本书的目的,我们只考虑存储方面。数据集的大小各不相同,从几百兆字节到数百 TB,甚至 PB 不等,并且需要将其提供给模型以进行训练。您必须考虑提供适当属性以满足这些需求的存储。通常需要大规模的块和对象存储,并且必须通过 Kubernetes 本地存储抽象或直接可访问的 API 进行访问。
模型开发
在这个阶段,数据科学家编写、分享并协作机器学习算法。像 JupyterHub 这样的开源工具易于在 Kubernetes 上安装,因为它们通常像任何其他工作负载一样运行。
训练
要使模型使用数据集学习执行其设计的任务,必须对其进行训练。训练过程的结果通常是训练模型状态的检查点。训练过程是利用 Kubernetes 所有功能的部分。调度、访问专用硬件、数据集卷管理、扩展和网络将同时运行以完成此任务。我们将在下一节中详细介绍训练阶段的具体内容。
服务
这是将训练好的模型对客户端的服务请求进行访问,以便根据客户端提供的数据进行推断的过程。例如,如果您有一个经过训练以检测狗和猫的图像识别模型,客户端可能会提交一张狗的图片,模型应该能够以一定的准确性确定它是否是一只狗。
Kubernetes 集群管理员的机器学习
在运行机器学习工作负载之前,有几个主题需要考虑。本节专门针对集群管理员。作为负责数据科学家团队的集群管理员,您将面临的最大挑战是理解术语。随着时间的推移,您必须熟悉许多新术语,但请放心,您可以做到。让我们看看准备集群以运行机器学习工作负载时需要解决的主要问题领域。
在 Kubernetes 上训练模型
在 Kubernetes 上训练机器学习模型需要常规的 CPU 和图形处理单元(GPU)。通常情况下,你应用的资源越多,训练完成的速度就越快。在大多数情况下,可以在具备所需资源的单台机器上完成模型训练。许多云服务提供商提供多 GPU 虚拟机(VM)类型,因此我们建议在考虑分布式训练之前,将 VM 垂直扩展到四到八个 GPU。数据科学家在训练模型时使用一种称为 超参数调整 的技术。超参数是在训练过程开始之前就设定好的参数。超参数调整是寻找模型训练的最佳超参数组合的过程。该技术涉及使用不同的超参数集合运行多个相同的训练作业。
在 Kubernetes 上训练你的第一个模型
在这个示例中,你将使用 MNIST 数据集来训练一个图像分类模型。MNIST 数据集是公开可用的,常用于图像分类。
要训练模型,你需要 GPU。让我们确认你的 Kubernetes 集群中是否有可用的 GPU。以下命令显示了 Kubernetes 集群中有多少个 GPU 可用。从输出中可以看出,这个集群有四个 GPU 可用:
$ kubectl get nodes -o yaml | grep -i nvidia.com/gpu
nvidia.com/gpu: "1"
nvidia.com/gpu: "1"
nvidia.com/gpu: "1"
nvidia.com/gpu: "1"
鉴于训练是批量工作负载,你将会在 Kubernetes 中使用 Job 类型来运行你的训练。你将进行 500 步的训练,并使用单个 GPU。创建一个名为 mnist-demo.yaml 的文件,使用以下清单,并将其保存到你的文件系统中:
apiVersion: batch/v1
kind: Job
metadata:
labels:
app: mnist-demo
name: mnist-demo
spec:
template:
metadata:
labels:
app: mnist-demo
spec:
containers:
- name: mnist-demo
image: lachlanevenson/tf-mnist:gpu
args: ["--max_steps", "500"]
imagePullPolicy: IfNotPresent
resources:
limits:
nvidia.com/gpu: 1
restartPolicy: OnFailure
现在,在你的 Kubernetes 集群上创建这个资源:
$ kubectl create -f mnist-demo.yaml
job.batch/mnist-demo created
检查刚刚创建的作业的状态:
$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
mnist-demo 1/1 31s 49s
如果查看 Pods,你应该可以看到训练作业正在运行:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
mnist-demo-8lqrn 1/1 Running 0 63s
查看 Pod 日志,你可以看到训练正在进行:
$ $ kubectl logs mnist-demo-8lqrn
2023-02-10 23:14:42.007518: I
tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports
instructions that this TensorFlow binary was not compiled to
use: SSE4.1 SSE4.2 AVX AVX2 FMA
2023-02-10 23:14:42.205555: I
tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with
properties:
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: 0001:00:00.0
totalMemory: 11.17GiB freeMemory: 11.12GiB
2023-02-10 23:14:42.205596: I
tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow
device (/device:GPU:0) -> (device: 0, name: Tesla K80, pci bus
id: 0001:00:00.0, compute capability: 3.7)
2023-02-10 23:14:46.848342: I
tensorflow/stream_executor/dso_loader.cc:139] successfully opened CUDA library
libcupti.so.8.0 locally
Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Extracting /tmp/tensorflow/input_data/train-images-idx3-ubyte.gz
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Extracting /tmp/tensorflow/input_data/train-labels-idx1-ubyte.gz
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Extracting /tmp/tensorflow/input_data/t10k-images-idx3-ubyte.gz
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting /tmp/tensorflow/input_data/t10k-labels-idx1-ubyte.gz
Accuracy at step 0: 0.0886
Accuracy at step 10: 0.7094
Accuracy at step 20: 0.8354
Accuracy at step 30: 0.8667
Accuracy at step 40: 0.8833
Accuracy at step 50: 0.8902
Accuracy at step 60: 0.897
Accuracy at step 70: 0.9062
Accuracy at step 80: 0.9057
Accuracy at step 90: 0.906
Adding run metadata for 99
Accuracy at step 100: 0.9163
Accuracy at step 110: 0.9203
Accuracy at step 120: 0.9168
Accuracy at step 130: 0.9215
Accuracy at step 140: 0.9241
Accuracy at step 150: 0.9251
Accuracy at step 160: 0.9286
Accuracy at step 170: 0.9288
Accuracy at step 180: 0.9274
Accuracy at step 190: 0.9337
Adding run metadata for 199
Accuracy at step 200: 0.9361
Accuracy at step 210: 0.9369
Accuracy at step 220: 0.9365
Accuracy at step 230: 0.9328
Accuracy at step 240: 0.9409
Accuracy at step 250: 0.9428
Accuracy at step 260: 0.9408
Accuracy at step 270: 0.9432
Accuracy at step 280: 0.9438
Accuracy at step 290: 0.9433
Adding run metadata for 299
Accuracy at step 300: 0.9446
Accuracy at step 310: 0.9466
Accuracy at step 320: 0.9468
Accuracy at step 330: 0.9463
Accuracy at step 340: 0.9464
Accuracy at step 350: 0.9489
Accuracy at step 360: 0.9506
Accuracy at step 370: 0.9489
Accuracy at step 380: 0.9484
Accuracy at step 390: 0.9494
Adding run metadata for 399
Accuracy at step 400: 0.9513
Accuracy at step 410: 0.9474
Accuracy at step 420: 0.9499
Accuracy at step 430: 0.9462
Accuracy at step 440: 0.952
Accuracy at step 450: 0.952
Accuracy at step 460: 0.9487
Accuracy at step 470: 0.9569
Accuracy at step 480: 0.9547
Accuracy at step 490: 0.9516
Adding run metadata for 499
最后,通过查看作业状态,你可以看到训练已经完成:
$ kubectl get jobs
NAME COMPLETIONS DURATION AGE
mnist-demo 1/1 31s 2m19s
要清理训练作业,只需运行以下命令:
$ kubectl delete -f mnist-demo.yaml
job.batch "mnist-demo" deleted
恭喜!你刚刚在 Kubernetes 上运行了你的第一个模型训练作业。
Kubernetes 上的分布式训练
分布式训练目前还处于初期阶段,优化起来相当困难。如果运行一个需要八个 GPU 的训练作业,与每台有四个 GPU 的两台机器相比,几乎总是在单台有八个 GPU 的机器上进行训练速度更快。唯一应当采用分布式训练的时候是当模型无法适应当前最大的单台机器时。如果确实需要运行分布式训练,理解架构是非常重要的。图 14-2 展示了分布式 TensorFlow 的架构,你可以看到模型和参数是如何分布的。

图 14-2. 分布式 TensorFlow 架构
资源约束
机器学习工作负载在集群的所有方面都需求非常具体的配置。训练阶段肯定是最资源密集的。还值得注意的是,正如我们刚提到的,机器学习算法的训练几乎总是批量式工作负载。具体来说,它将有一个开始时间和一个完成时间。训练运行的完成时间取决于您能多快满足模型训练的资源需求。这意味着扩展几乎肯定是更快完成训练工作的一种方式,但扩展也有其自身的一系列瓶颈。
专用硬件
训练和服务模型几乎总是在专用硬件上更为高效。这类专用硬件的典型示例包括通用 GPU。Kubernetes 允许您通过设备插件访问 GPU,这些插件使 GPU 资源对 Kubernetes 调度器可见,从而可以进行调度。设备插件框架促进了这一能力,这意味着厂商无需修改核心 Kubernetes 代码来实现他们的特定设备。这些设备插件通常作为 DaemonSets 在每个节点上运行,负责向 Kubernetes API 广告这些特定资源。让我们看看用于 Kubernetes 的NVIDIA 设备插件,它使得访问 NVIDIA GPU 成为可能。一旦它们运行起来,您可以创建一个 Pod 如下,Kubernetes 将确保它被调度到拥有这些资源的节点上:
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: digits-container
image: nvidia/digits:6.0
resources:
limits:
nvidia.com/gpu: 2 # requesting 2 GPUs
设备插件不仅限于 GPU;您可以在需要专用硬件的任何地方使用它们,例如可编程门阵列(FPGA)或 InfiniBand。
调度的特殊性
需要注意的是,Kubernetes 无法对其没有知识的资源做出决策。在训练时,您可能会注意到 GPU 的利用率未达到最大。因此,您没有达到希望看到的利用率水平。让我们考虑之前的例子;它仅公开了 GPU 核心的数量,但省略了每个核心可运行的线程数量。它也没有公开 GPU 核心所在的总线,因此需要访问彼此或相同内存的作业可能会共同放置在相同的 Kubernetes 节点上。所有这些考虑可能会在未来由设备插件解决,但目前可能会让您想知道为什么无法在刚购买的强大 GPU 上获得 100% 的利用率。还值得一提的是,您无法请求 GPU 的分数(例如 0.1),这意味着即使特定的 GPU 支持同时运行多个线程,您也无法利用该容量。
库、驱动程序和内核模块
要访问专用硬件,通常需要专为其设计的库、驱动程序和内核模块。您需要确保这些内容被挂载到容器运行时中,以便工具能够在容器中运行时使用。您可能会问:“为什么不直接将这些内容添加到容器映像本身?”答案很简单:这些工具需要与底层主机上的版本匹配,并且必须针对特定系统进行适当配置。像NVIDIA Docker这样的容器运行时消除了将主机卷映射到每个容器中的负担。如果没有专为其设计的容器运行时,您可能可以构建一个准入 Webhook,提供相同的功能。还要考虑的重要因素是,您可能需要特权容器才能访问某些专用硬件,这会影响集群安全配置文件。安装相关库、驱动程序和内核模块也可能通过 Kubernetes 设备插件来实现。许多设备插件在每台机器上运行检查,以确认所有安装已完成,然后将可调度的 GPU 资源广告到 Kubernetes 调度程序。
存储
存储是机器学习工作流程中最关键的方面之一。您需要考虑存储,因为它直接影响机器学习工作流程的以下部分:
-
训练期间数据集的存储和在节点之间的分发
-
检查点和模型保存
训练期间数据集的存储和在节点之间的分发
在训练过程中,每个节点必须能够检索数据集。存储需求是只读的,通常来说,磁盘越快越好。提供存储的磁盘类型几乎完全取决于数据集的大小。数百兆或几千兆字节大小的数据集可能非常适合块存储,但几百至几千兆字节大小的数据集可能更适合对象存储。根据存放数据集的磁盘的大小和位置,可能会对网络性能造成影响。
检查点和模型保存
当模型正在训练时会创建检查点,并且保存模型使您可以将其用于服务。在这两种情况下,您需要每个节点连接的存储来存储这些数据。数据通常存储在单个目录下,每个节点都在向特定的检查点或保存文件写入。大多数工具期望检查点和保存数据位于单个位置,并且需要ReadWriteMany。ReadWriteMany简单地意味着该卷可以被多个节点挂载为读写。在使用 Kubernetes PersistentVolumes 时,您需要确定适合您需求的最佳存储平台。Kubernetes 文档保留了一个列表,列出支持ReadWriteMany的卷插件。
网络
机器学习工作流的训练阶段对网络有很大影响(特别是在运行分布式训练时)。如果我们考虑 TensorFlow 的分布式架构,两个离散阶段会产生大量网络流量:从每个参数服务器到每个节点的变量分布,以及从每个节点到参数服务器的梯度应用(参见图 14-2)。这种交换所需的时间直接影响模型训练所需的时间。所以,这是一个简单的游戏:越快越好(当然,合理范围内)。如今大多数公共云和服务器支持 1-Gbps、10-Gbps 甚至 40-Gbps 的网络接口卡,通常网络带宽只在较低带宽下成为问题。如果需要高网络带宽,可以考虑 InfiniBand。
虽然原始网络带宽往往是限制因素,但在某些情况下,问题是如何将数据首先从内核传输到线上。一些开源项目利用远程直接内存访问(RDMA)进一步加速网络流量,而无需修改节点或应用程序代码。RDMA 允许网络中的计算机在不使用任何计算机的处理器、缓存或操作系统的情况下在主内存中交换数据。
专用协议
当在 Kubernetes 上使用机器学习时,您可以考虑其他专用协议,这些协议通常是特定供应商的,但它们都致力于通过消除架构中快速成为瓶颈的区域来解决分布式训练扩展问题。例如,参数服务器。这些协议通常允许多节点上的 GPU 直接交换信息,而无需涉及节点 CPU 和操作系统。以下是您可能希望了解的一些协议,以更有效地扩展您的分布式训练:
消息传递接口(MPI)
用于在分布式进程之间传输数据的标准便携式 API
NVIDIA 集体通信库(NCCL)
一个具有拓扑感知的多 GPU 通信基元库
数据科学家的关注点
在本章的前面,我们分享了您需要考虑的一些内容,以便能够在 Kubernetes 集群上运行机器学习工作负载。但数据科学家如何?在这里,我们涵盖了一些流行工具,这些工具使数据科学家能够在不必成为 Kubernetes 专家的情况下利用 Kubernetes 进行机器学习:
一个针对 Kubernetes 的机器学习工具包,它是 Kubernetes 原生的,并配备了完成机器学习工作流所需的几个工具。诸如 Jupyter Notebooks、流水线和 Kubernetes 本地控制器等工具,使数据科学家能够简单轻松地充分利用 Kubernetes 作为机器学习平台。
一种管理机器学习工作流程的工具,支持许多流行的库,并在任何 Kubernetes 集群上运行。Polyaxon 提供商业和开源版本。
一个成熟的企业级数据科学平台,具备丰富的数据集准备、生命周期管理和版本控制工具,以及构建机器学习管道的能力。Pachyderm 提供一种商业解决方案,您可以部署到任何 Kubernetes 集群上。
Kubernetes 上的机器学习最佳实践
要实现机器学习工作负载的最佳性能,考虑以下最佳实践:
智能调度和自动缩放
考虑到机器学习工作流程的大多数阶段本质上是批处理的,我们建议您使用集群自动缩放器。启用 GPU 的硬件成本高昂,当它们不被使用时,您肯定不希望为其付费。我们建议通过特定时间批量运行作业,使用 taints 和 tolerations 或特定时间的集群自动缩放器。这样,集群可以根据机器学习工作负载的需求进行伸缩,而不是早了一步。关于 taints 和 tolerations,上游约定是将带有扩展资源的节点 taint。例如,具有 NVIDIA GPU 的节点应按以下方式 tainted:Key: nvidia.com/gpu, Effect: NoSchedule。使用此方法意味着您还可以利用 ExtendedResourceToleration Admission Controller,该控制器将为请求扩展资源的 Pod 自动添加适当的 tolerations,从而用户无需手动添加它们。
实际上,模型训练是一个微妙的平衡
允许某个区域运行更快通常会导致其他区域的瓶颈。这是一个持续观察和调整的努力。作为一个通用规则,我们建议您尝试让 GPU 成为瓶颈,因为它是最昂贵的资源。保持 GPU 的饱和。要时刻准备好寻找瓶颈,并设置监控以跟踪 GPU、CPU、网络和存储的利用率。
混合工作负载集群
用于日常业务服务的集群也可能用于机器学习。考虑到机器学习工作负载的高性能要求,我们建议使用一个专用的节点池,专门用于接受机器学习工作负载。这将有助于保护集群的其他部分免受运行在机器学习节点池上的机器学习工作负载的任何影响。此外,您应考虑多个支持 GPU 的节点池,每个节点池具有不同的性能特征以适应不同的工作负载类型。我们还建议在机器学习节点池上启用节点自动缩放。仅在您充分了解机器学习工作负载对集群性能影响之后,才使用混合模式集群。
实现分布式训练的线性扩展
这是分布式模型训练的圣杯。不幸的是,大多数库在分布式环境下并不呈线性扩展。虽然有很多工作正在进行中以改善扩展性,但理解成本是非常重要的,因为这不是简单地投入更多硬件来解决问题。根据我们的经验,几乎总是模型本身而不是支持它的基础设施成为瓶颈的源头。然而,在指责模型之前,审查 GPU、CPU、网络和存储的利用率是非常重要的。开源工具如Horovod致力于改进分布式训练框架,并提供更好的模型扩展能力。
总结
在本章中,我们涵盖了大量内容,希望能够深入理解为什么 Kubernetes 是一个出色的机器学习平台,特别是深度学习,并且在部署首个机器学习工作负载之前需要考虑的因素。如果您按照本章的建议去实施,您将能够为这些特殊工作负载构建和维护一个良好的 Kubernetes 集群。
第十五章:在 Kubernetes 之上构建更高级别的应用模式
Kubernetes 是一个复杂的系统并不是秘密。尽管它简化了分布式应用的部署和操作,但它并没有简化这些系统的开发。事实上,当为开发者添加新的概念和工件时,它增加了一层复杂性,以简化操作为代价。因此,在许多环境中,开发更高级别的抽象以提供更多开发者友好的原语是有意义的。此外,在许多大公司中,标准化应用配置和部署方式也是有意义的,以便每个人都遵循相同的运维最佳实践。通过开发更高级别的抽象,开发者可以自动遵守这些原则。然而,开发这些抽象可能会隐藏开发者需要了解的重要细节,并可能引入封闭的园区。这限制或复杂化了某些应用程序的开发或现有解决方案的集成。在云端的发展过程中,基础设施的灵活性和平台的功能之间的张力一直是不断存在的。设计适当的高级别抽象使我们能够在这个分歧中走出理想的路径。
开发更高级别抽象的方法
当考虑如何在 Kubernetes 之上开发更高级别的原语时,有两种基本方法。第一种是将 Kubernetes 包装为一个实现细节。采用这种方法时,消费您平台的开发者应该大部分时间意识不到他们正在运行在 Kubernetes 之上;相反,他们应该将自己视为您提供的平台的消费者,因此 Kubernetes 是一个实现细节。
第二个选择是利用 Kubernetes 本身构建的可扩展性能力。Kubernetes 服务器 API 非常灵活,您可以动态地向 Kubernetes API 添加任意新资源。采用这种方法,您的新的高级别资源与内置的 Kubernetes 对象并存,并且用户可以使用内置工具与所有 Kubernetes 资源(包括内置的和扩展的资源)进行交互。这种扩展模型使得 Kubernetes 仍然是开发者的核心,但通过添加降低了复杂性并使其更易于使用。
如何选择适当的方法?这取决于您正在构建的抽象层的目标。如果您正在构建一个完全隔离的集成环境,您有强烈信心用户不需要“打破玻璃”逃脱,并且易用性是一个重要特征,第一个选项是一个很好的选择。构建机器学习流水线就是一个很好的例子。该领域相对来说是被理解的。您的用户可能不熟悉 Kubernetes。让这些数据科学家能够快速完成工作并专注于他们的领域而不是分布式系统是主要目标。因此,在 Kubernetes 顶部构建完整的抽象层是最合理的选择。
另一方面,当构建更高级别的开发者抽象时——例如,一个简化的部署 Java 应用程序的方法——扩展 Kubernetes 而不是包装它是一个更好的选择,有两个原因。首先,应用开发的领域非常广泛。对于您来说,难以预测开发人员的所有需求和用例,特别是随着应用程序和业务随时间的迭代和变化。另一个原因是确保您能继续利用 Kubernetes 生态系统中的工具。有无数的云原生工具用于监控、持续交付等等。扩展而不是替换 Kubernetes API 确保您可以继续使用这些工具和新开发的工具。此外,当选择扩展而不是混淆 Kubernetes API 时,相对容易找到具有 Kubernetes 行业经验的人。在只存在于您环境中的定制应用程序平台上构建应用程序的经验在定义上是罕见的。
扩展 Kubernetes
因为您可能在 Kubernetes 之上构建的每个层都是独特的,描述如何构建这样一个层级以扩展 Kubernetes 超出了本书的范围。但是,扩展 Kubernetes 的工具和技术对于您可能在 Kubernetes 之上进行的任何构建都是通用的,因此我们将花时间来覆盖它们。
扩展 Kubernetes 集群
完整的如何扩展 Kubernetes 集群的操作指南是一个庞大的主题,更详尽的内容可以在其他书籍中找到,比如 管理 Kubernetes 和 Kubernetes: Up and Running(O’Reilly)。与在此重复相同材料不同,本节专注于提供如何使用 Kubernetes 的扩展性的理解。
扩展 Kubernetes 集群涉及理解 Kubernetes 资源的接触点。有三种相关的技术解决方案。首先是sidecar。Sidecar 容器(显示在图 15-1 中)在服务网格的背景下得到了推广。这些容器与主应用容器并行运行,提供与主应用分离并经常由独立团队维护的额外功能。例如,在服务网格中,sidecar 可以为容器化应用提供透明的互相认证的传输层安全性(mTLS)。你可以使用 sidecar 来为用户定义的应用程序添加功能。

图 15-1. sidecar 设计
在行业内,sidecar 方法变得越来越流行,并且许多项目使用它来在开发者的容器旁提供服务。一个很好的例子是Dapr(分布式应用运行时)项目。Dapr 是 CNCF 内的一个开源项目,为应用程序实现了一个 sidecar,提供诸如加密、键/值存储、发布/订阅队列等许多功能,具有非常简单一致的 API。像 Dapr 这样的 sidecar 可以作为你在 Kubernetes 之上开发的平台的模块化构建块使用。
当然,这项工作的整体目标是使开发者的生活更轻松,但如果我们要求他们学习并知道如何使用 sidecar,实际上会使问题变得更糟。幸运的是,用于扩展 Kubernetes 的额外工具简化了事务。特别是,Kubernetes 具有admission controllers。Admission controllers 是拦截器,它们在将 Kubernetes API 请求存储(或“接受”)到集群的后备存储之前读取这些请求。你可以使用这些 admission controllers 来验证或修改 API 对象。在 sidecar 的上下文中,你可以使用它们自动向集群中创建的所有 pod 添加 sidecar,以便开发者无需了解 sidecar 即可获得其好处。图 15-2 展示了 admission controllers 如何与 Kubernetes API 交互。

图 15-2. Admission controllers
准入控制器的效用不仅限于添加 sidecar。您还可以使用它们验证开发人员提交给 Kubernetes 的对象。例如,您可以实现一个 Kubernetes 的 linter(分析代码的工具),确保开发人员提交符合 Kubernetes 最佳实践的 pod 和其他资源。开发人员常见的一个错误是未为其应用程序保留资源。对于这些情况,基于准入控制器的 linter 可以拦截此类请求并将其拒绝。当然,您还应该留下一种逃生方式(例如,特殊的注释),以便高级用户可以选择退出适当的 lint 规则。我们将在本章后面讨论逃生通道的重要性。
到目前为止,我们只讨论了增强现有应用程序的方法以及确保开发人员遵循最佳实践的方式,但我们并没有真正涉及如何添加更高级别的抽象。这就是自定义资源定义(CRDs)发挥作用的地方。CRDs 是一种动态向现有 Kubernetes 集群添加新资源的方式。例如,使用 CRDs,您可以向 Kubernetes 集群添加一个新的 ReplicatedService 资源。当开发人员创建 ReplicatedService 的实例时,它会转到 Kubernetes 并创建相应的 Deployment 和 Service 资源。因此,ReplicatedService 是一个常见模式的方便开发者抽象。通常,CRDs 由部署到集群本身的控制循环来实现对这些新资源类型的管理。
扩展 Kubernetes 用户体验
向集群添加新资源是提供新功能的好方法,但要真正利用它们,通常需要扩展 Kubernetes 用户体验(UX)。默认情况下,Kubernetes 工具不了解自定义资源和其他扩展,因此对待它们的方式非常普遍且不够用户友好。扩展 Kubernetes 命令行可以提供增强的用户体验。
通常用于访问 Kubernetes 的工具是 kubectl 命令行工具。幸运的是,它也被构建为可扩展的。kubectl 插件是具有像 kubectl-foo 这样的名称的二进制文件,其中 foo 是插件的名称。当您在命令行上调用 kubectl foo ... 时,该调用将路由到插件二进制文件的调用。使用 kubectl 插件,您可以定义深度理解您已添加到集群的新资源的新体验。您可以自由实现适合的体验,同时利用 kubectl 工具的熟悉性。这尤其宝贵,因为这意味着您无需教开发人员使用新的工具集。同样,您可以逐步引入 Kubernetes 本地概念,随着开发人员增加其 Kubernetes 知识。
如果您希望为基于 Kubernetes 的平台构建图形界面,有几种工具可以帮助。特别是开源项目Headlamp 项目,它是一个库,可轻松构建基于 Web、移动或桌面的应用程序,用于与 Kubernetes 基础设施交互。使用类似 Headlamp 的工具可以快速创建定制的开发者体验,完全符合您的平台及其需求。
使容器化开发更加简单
在开发人员甚至能够将应用程序部署到 Kubernetes 之前,必须首先将该应用程序打包为容器。虽然对于熟悉云原生生态系统的人来说构建容器已经是信手拈来,但对于许多人来说,这是一个艰巨的任务,甚至阻碍了现代应用程序开发的开始。
幸运的是,有几种开源工具可以帮助加快您的开发。像Draft和Skaffold这样的工具将为特定语言或开发环境自动生成 Dockerfile。
如果开发人员熟悉来自 Cloud Foundry 或其他平台的构建包概念,还有像Paketo这样的工具,提供易于使用和经过验证的容器镜像,用于构建流行语言的应用程序以及命令行工具,以便轻松入门。
开发“推送即部署”体验
许多 PaaS 产品最受欢迎的功能之一是“推送即部署”,这意味着将代码推送到 Git 存储库只需一次,即可将应用程序部署到云环境中。尽管这以前是大规模托管 PaaS 解决方案的领域,但现在可以使用 GitHub Actions、Azure DevOps 或其他持续构建工具轻松构建类似的体验。
通过正确设计的流水线,一旦开发人员将代码推送到其 Git 存储库中,就会自动进行测试、构建、打包成容器镜像并推送到容器注册表中。
一旦容器镜像的新版本出现在容器注册表中,只需使用另一个 Git 提交结合 GitOps 就可以将该镜像推送到正在运行的应用程序。
结合 GitHub Actions 和 GitOps 可以使您的开发人员实现快速部署,同时也保持云原生生态系统以及基础设施即代码(IaC)等理念的真实性。
构建平台时的设计考虑
无数的平台已被建立,以提高开发人员的生产力。有机会观察这些平台成功与失败的各个方面,您可以开发出一套共同的模式和考虑事项,从他人的经验中汲取教训。遵循这些设计指南可以帮助确保您构建的平台成功,而不是成为一个必须最终放弃的“遗留”死胡同。
支持导出到容器镜像
当你建立一个平台时,许多设计通过允许用户仅提供代码(例如,Function as a Service [FaaS] 中的一个函数)或本地包(例如 Java 中的一个 JAR 文件),而不是完整的容器镜像,来提供简单性。这种方法非常吸引人,因为它让用户可以在他们熟悉的工具和开发经验范围内操作。平台会为他们处理应用程序的容器化。
然而,这种方法的问题出现在开发者遇到所提供的编程环境的限制时。也许是因为他们需要特定版本的语言运行时来解决 bug。或者可能是因为他们需要打包额外的资源或可执行文件,这些不在你自动容器化应用程序的结构中。
无论原因是什么,碰到这个障碍对开发者来说都是一个令人沮丧的时刻,因为这时他们突然需要学习如何打包他们的应用程序,而他们真正想做的只是稍微扩展它以修复 bug 或交付新功能。
然而,情况不一定非得如此。如果你支持将你的平台的编程环境导出到一个通用容器中,使用你平台的开发者就不需要从头学习关于容器的所有知识。相反,他们有一个完整的工作容器镜像,代表了他们当前的应用程序(即包含他们的函数和节点运行时的容器镜像)。有了这个起点,他们可以进行必要的微调,使容器镜像适应他们的需求。这种逐步降级和增量学习显著地平滑了从高级平台到更低级基础设施的过渡路径。它还增加了平台的通用效用,因为使用它不会为开发者引入陡峭的学习曲线。
支持现有的服务和服务发现机制
另一个平台的常见情况是它们会发展并与其他系统互联。许多开发者可能对你的平台非常满意和高效,但是任何真实世界的应用程序都会涉及到你构建的平台、更低级别的 Kubernetes 应用程序以及其他平台。与传统数据库或为 Kubernetes 构建的开源应用程序的连接始终是足够大的应用程序的一部分。
由于这种互联的需求,任何你构建的平台都必须使用并公开核心 Kubernetes 的服务和服务发现基元。不要为了改善平台体验而重复造轮子,因为这样做会创建一个无法与更广泛世界互动的封闭环境。
如果将平台中定义的应用程序公开为 Kubernetes 服务,集群中的任何位置的应用程序都可以使用您的应用程序,无论它们是否在您的更高级平台中运行。同样,如果使用 Kubernetes DNS 服务器进行服务发现,您将能够从您的高级应用程序平台连接到集群中运行的其他应用程序,即使它们未在您的更高级平台中定义。也许建立更好或更易于使用的东西是很诱人的,但跨不同平台的互联性是任何足够年龄和复杂性的应用程序的常见设计模式。您将始终后悔决定建立一个封闭的园地。
构建应用平台的最佳实践
尽管 Kubernetes 提供了强大的工具来操作软件,但它在帮助开发人员构建应用程序方面的功能较少。通常需要在 Kubernetes 之上构建平台,以提高开发效率和/或使 Kubernetes 更易于使用。在构建这些平台时,应牢记以下最佳实践:
-
使用准入控制器来限制和修改对集群的 API 调用。准入控制器可以验证(并拒绝无效的)Kubernetes 资源。变异准入控制器可以自动修改 API 资源,以添加新的 sidecar 或其他用户甚至不需要知道的变更。
-
使用
kubectl插件来扩展 Kubernetes 用户体验,通过向现有命令行工具添加新工具。在极少数情况下,可能需要使用专门构建的工具。 -
在构建基于 Kubernetes 的平台时,仔细考虑平台的用户及其需求的演变。使事情简单易用显然是一个良好的目标,但如果这也导致用户陷入困境,无法成功地在您的平台之外重写所有内容,最终将是令人沮丧(和失败的)经历。
摘要
Kubernetes 是简化软件部署和操作的绝佳工具;不幸的是,它并非总是最友好或最高效的开发环境。因此,一个常见的任务是在 Kubernetes 之上构建一个更高级的平台,使其更易接近和被普通开发者使用。本章描述了设计这样一个更高级系统的几种方法,并提供了 Kubernetes 中可用的核心可扩展性基础设施的摘要。它总结了从我们观察到的其他构建在 Kubernetes 之上的平台中得出的教训和设计原则,希望它们能指导您平台的设计。
第十六章:管理状态和有状态应用程序
在容器编排的早期阶段,目标工作负载通常是无状态应用程序,当需要时使用外部系统来存储状态。当时的想法是容器非常短暂,保持状态一致性的后备存储编排在最佳情况下也是困难的。随着时间的推移,基于容器的保持状态的工作负载成为了现实,并且在某些情况下,这种需求可能更高效。随着越来越多的组织寻求云计算能力,并且 Kubernetes 成为应用程序的事实标准容器运行时,阻碍因素成为数据量和对数据的高效访问,有时称为“数据引力”。Kubernetes 在多次迭代中进行了适应。现在,它不仅允许将存储卷挂载到 Pod 中,还允许 Kubernetes 直接管理这些卷。这是与需要存储的工作负载编排中的一个重要组成部分。
如果能够将外部卷挂载到容器就足够了,那么在 Kubernetes 中运行的规模化有状态应用程序的示例将更多。现实是,在有状态应用程序的大计划中,卷挂载只是一个简单的组成部分。大多数需要在节点故障后维护状态的应用程序是复杂的数据状态引擎,如关系数据库系统、分布式键/值存储和复杂文档管理系统。这类应用程序需要更多协调,包括集群应用程序成员之间的通信方式、成员的识别方式以及成员出现或消失的顺序。
本章重点介绍了管理状态的最佳实践,从简单模式,比如将文件保存到网络共享,到复杂的数据管理系统,如 MongoDB、MySQL 或 Kafka。还有一个关于名为操作员的新模式的小节,该模式不仅引入了 Kubernetes 的基本组件,还允许将业务或应用逻辑添加为自定义控制器,有助于更轻松地操作复杂的数据管理系统。
卷和卷挂载
并非每个需要维护状态的工作负载都需要成为复杂的数据库或高吞吐量数据队列服务。通常,将应用程序移动到容器化工作负载中的应用程序希望某些目录存在,并且能够读取和写入这些目录中的相关信息。可以从第四章中注入数据到可以由 Pod 中的容器读取的卷中,但是从 ConfigMaps 或秘密中挂载的数据通常是只读的,本节关注提供容器可以写入并能够在容器或甚至更好,Pod 故障后幸存的卷。
每个主要的容器运行时,如 Docker、rkt、CRI-O,甚至 Singularity,都允许将卷挂载到映射到外部存储系统的容器中。在其最简单形式下,外部存储可以是内存位置、容器主机上的路径,或外部文件系统,如 NFS、Glusterfs、CIFS 或 Ceph。为什么会需要这样做呢?一个有用的例子是,一个传统应用程序被编写为将应用程序特定信息记录到本地文件系统。许多可能的解决方案,如更新应用程序代码以将日志输出到侧车容器的stdout或stderr,可以通过共享的 pod 卷流式传输日志数据到外部源。有些方法会采用基础设施方法,通过使用可以读取容器应用程序日志和主机日志的卷的主机基于日志工具来进行卷挂载,使用 Kubernetes hostPath 挂载,如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-webserver
spec:
replicas: 3
selector:
matchLabels:
app: nginx-webserver
template:
metadata:
labels:
app: nginx-webserver
spec:
containers:
- name: nginx-webserver
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: hostvol
mountPath: /usr/share/nginx/html
volumes:
- name: hostvol
hostPath:
path: /home/webcontent
卷最佳实践
-
尽量限制将卷用于需要多个容器共享数据的 pod,如适配器或大使类型模式。对于这些共享模式,请使用
emptyDir。 -
当需要通过基于节点的代理或服务访问数据时,请使用
hostDir。 -
尝试识别任何将其关键应用程序日志和事件写入本地磁盘的服务,并在可能的情况下将其更改为
stdout或stderr,让真正的 Kubernetes 感知日志聚合系统流式传输日志,而不是依赖卷映射。
Kubernetes 存储
到目前为止,我们走过的示例展示了将卷基本映射到 pod 中的容器中,这只是基本的容器引擎能力。真正的关键是允许 Kubernetes 管理支持卷挂载的存储。这允许更动态的场景,其中 pod 可以根据需要存活和死亡,支持 pod 的存储将相应地过渡到 pod 可能生活的任何地方。Kubernetes 使用两个不同的 API 管理 pod 的存储,即 PersistentVolume 和 PersistentVolumeClaim。
持久卷
最好将 PersistentVolume 视为支持挂载到 pod 的任何卷的磁盘。PersistentVolume 将具有声明策略,定义卷的生命周期范围,独立于使用卷的 pod 的生命周期。Kubernetes 可以使用动态或静态定义的卷。要允许动态创建卷,必须在 Kubernetes 中定义一个 StorageClass。可以在集群中创建不同类型和类别的 PersistentVolumes,只有当 PersistentVolumeClaim 匹配 PersistentVolume 时,它才会真正分配给一个 pod。卷本身由卷插件支持。直接在 Kubernetes 中支持多种插件,并且每种插件都有不同的配置参数可供调整:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv001
labels:
tier: "silver"
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /tmp
server: 172.17.0.2
PersistentVolumeClaims
PersistentVolumeClaims 是 Kubernetes 中为存储定义资源需求的一种方式,Pods 将引用这些声明,如果存在与声明请求匹配的 persistentVolume,则将为该特定 Pod 分配该卷。至少需要定义存储请求大小和访问模式,但也可以定义特定的 StorageClass。选择器也可以用来确保分配符合特定条件的 PersistentVolumes。在以下示例中,具有键 tier 和值 "silver" 的标签:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
storageClass: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
selector:
matchLabels:
tier: "silver"
此声明将与之前创建的 PersistentVolume 匹配,因为 StorageClass 名称、选择器匹配、大小和访问模式都相等。
Kubernetes 将匹配 PersistentVolume 与声明并将它们绑定在一起。要使用该卷,pod.spec 应通过名称引用声明,如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-webserver
spec:
replicas: 3
selector:
matchLabels:
app: nginx-webserver
template:
metadata:
labels:
app: nginx-webserver
spec:
containers:
- name: nginx-webserver
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: hostvol
mountPath: /usr/share/nginx/html
volumes:
- name: hostvol
persistentVolumeClaim:
claimName: my-pvc
StorageClasses
管理员可以选择创建 StorageClass 对象,而不是手动预先定义 PersistentVolumes,这些对象定义了要使用的卷插件。他们还可以创建所有该类 PersistentVolumes 将使用的任何特定挂载选项和参数。然后可以根据 StorageClass 的参数和选项动态创建 PersistentVolume,并定义声明要使用的特定 StorageClass,Kubernetes 将基于 StorageClass 参数和选项动态创建 PersistentVolume:
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: nfs
provisioner: cluster.local/nfs-client-provisioner
parameters:
archiveOnDelete: True
Kubernetes 还允许操作员使用 DefaultStorageClass 准入插件创建默认存储类。如果已在 API 服务器上启用此选项,则可以定义默认 StorageClass,并且任何未明确定义 StorageClass 的 PersistentVolumeClaims 将分配给默认类。某些云提供商将包括默认存储类,以映射到其实例允许的最便宜的存储。
容器存储接口和 FlexVolume
大多数卷插件今天需要等待 Kubernetes 代码库的直接代码添加。然而,容器存储接口 (CSI) 和 FlexVolume(通常称为“Out-of-Tree”卷插件)使存储供应商能够创建自定义存储插件,而无需等待这些直接代码添加。
CSI 和 FlexVolume 插件作为运营商在 Kubernetes 集群上部署的扩展,并且可以在需要时由存储供应商进行更新以公开新功能。
CSI 在 GitHub 上表明其目标是:
定义一个行业标准的容器存储接口,使存储供应商(SP)可以开发一次插件并在多个容器编排系统(CO)中运行。
FlexVolume 接口一直是添加存储提供程序附加功能的传统方法。它确实需要在将使用它的集群的所有节点上安装特定驱动程序。这基本上成为集群主机上安装的可执行文件。这个最后的组件是使用 FlexVolumes 的主要缺点,特别是在托管服务提供商中,因为访问节点是不受欢迎的,而访问控制平面几乎是不可能的。CSI 插件通过暴露相同的功能并像部署 pod 到集群一样容易使用解决了这个问题。
Kubernetes 存储最佳实践
云原生应用程序设计原则尽可能强制无状态应用程序设计;然而,基于容器的服务的不断增长使得数据存储持久性成为必要。这些关于 Kubernetes 存储的最佳实践将有助于设计一种有效的方法来提供应用程序设计所需的存储实现:
-
如果可能的话,启用 DefaultStorageClass 准入插件,并定义一个默认的存储类。通常,需要 PersistentVolumes 的应用程序的 Helm charts 默认使用
default存储类,这使得应用程序可以在不太多修改的情况下安装。 -
在设计集群架构时(不论是在本地还是在云提供商),考虑计算和数据层之间的区域和连接。您将希望为节点和 PersistentVolumes 使用适当的标签,并使用亲和性使数据和工作负载尽可能接近。你最不希望的情况是,位于 A 区域的节点上的 pod 尝试挂载附加到 B 区域节点上的卷。
-
非常仔细地考虑哪些工作负载需要在磁盘上维护状态。这是否可以通过外部服务来处理,比如数据库系统?或者,您的实例是否可以在云提供商中运行,通过与当前使用的 API 一致的托管服务,比如作为服务的 MongoDB 或 MySQL?
-
确定修改应用程序代码以实现更无状态化需要付出多大的努力。
-
虽然 Kubernetes 将跟踪和挂载卷随着工作负载的调度,但它尚未处理存储在这些卷中的数据的冗余和备份。CSI 规范已添加了一个 API,供供应商插入本地快照技术,如果存储后端支持的话。
-
验证卷将保存的数据的适当生命周期。默认情况下,为动态配置的 PersistentVolumes 设置回收策略,这将在删除 pod 时从后备存储提供程序中删除卷。应设置敏感数据或可用于取证分析的数据以回收。
有状态应用程序
与流行观念相反,Kubernetes 从其初始阶段就支持有状态应用程序,从 MySQL、Kafka 和 Cassandra 到其他技术。然而,这些先驱时期充满了复杂性,并且通常只适用于小型工作负载,需要大量工作才能使缩放和耐久性等功能正常运行。
要完全理解关键差异,你必须了解典型 ReplicaSet 如何调度和管理 Pods,以及这如何对传统有状态应用程序有害:
-
ReplicaSet 中的 Pods 在调度时被分配随机名称并进行扩展。
-
ReplicaSet 中的 Pods 随意缩减。
-
ReplicaSet 中的 Pods 不会通过其名称或 IP 地址直接调用,而是通过与 Service 的关联。
-
ReplicaSet 中的 Pods 可以随时重新启动并移动到另一个节点。
-
ReplicaSet 中具有映射 PersistentVolume 的 Pods 仅通过声明链接,但如果需要重新调度,任何具有新名称的新 Pod 都可以接管该声明。
那些对集群数据管理系统只有粗浅了解的人可以立即看到 ReplicaSet 基础 Pods 的这些特性存在问题。想象一下,一个拥有当前可写副本的数据库的 Pod 突然被删除!肯定会造成纯粹的混乱。
Kubernetes 中的大多数新手都认为 StatefulSet 应用程序自动是数据库应用程序,因此等同起来。事实并非如此。Kubernetes 不知道它正在部署的应用程序类型。它不知道你的数据库系统需要领导选举过程,它是否能够处理集合成员之间的数据复制,或者它是否是数据库系统。这就是 StatefulSets 发挥作用的地方。
StatefulSets
StatefulSets 的作用是更容易运行期望更可靠节点/Pod 行为的应用系统。如果我们回顾一下 ReplicaSet 中典型 Pods 特性的列表,StatefulSets 几乎完全相反。最初在 Kubernetes 版本 1.3 中提出的 PetSets(现在的 StatefulSets)是为了满足诸如复杂数据管理系统等有状态应用程序的关键调度和管理需求:
-
StatefulSet 中的 Pods 将会被扩展并分配顺序名称。随着集合的扩展,Pods 将得到序数名称,默认情况下,新的 Pod 必须完全在线(通过其健康检查和/或准备就绪探针)才能添加下一个 Pod。
-
StatefulSet 中的 Pods 按相反的顺序缩减。
-
StatefulSet 中的 Pods 可以通过头部无服务的方式按名称单独访问。
-
StatefulSet 中需要挂载卷的 Pods 必须使用定义好的 PersistentVolume 模板。StatefulSet 中的 Pods 所声明的卷在删除 StatefulSet 时不会被删除。
StatefulSet 规范与 Deployment 非常相似,除了 Service 声明和 PersistentVolume 模板之外。应首先创建无头 Service,它定义了将逐个为 pod 寻址的 Service。无头 Service 与常规 Service 相同,但不执行正常的负载均衡:
apiVersion: v1
kind: Service
metadata:
name: mongo
labels:
name: mongo
spec:
ports:
- port: 27017
targetPort: 27017
clusterIP: None #This creates the headless Service
selector:
role: mongo
StatefulSet 定义看起来也和 Deployment 几乎一模一样,只有少许不同之处:
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: "mongo"
replicas: 3
template:
metadata:
labels:
role: mongo
environment: test
spec:
terminationGracePeriodSeconds: 10
containers:
- name: mongo
image: mongo:3.4
command:
- mongod
- "--replSet"
- rs0
- "--bind_ip"
- 0.0.0.0
- "--smallfiles"
- "--noprealloc"
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-persistent-storage
mountPath: /data/db
- name: mongo-sidecar
image: cvallance/mongo-k8s-sidecar
env:
- name: MONGO_SIDECAR_POD_LABELS
value: "role=mongo,environment=test"
volumeClaimTemplates:
- metadata:
name: mongo-persistent-storage
annotations:
volume.beta.kubernetes.io/storage-class: "fast"
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 2Gi
运算符
StatefulSets 在将复杂有状态数据系统引入 Kubernetes 中作为可行工作负载方面起了重要作用。正如前面所述,唯一真正的问题是 Kubernetes 实际上并不了解在 StatefulSet 中运行的工作负载。所有其他复杂操作,如备份、故障转移、领导者注册、新副本注册和升级,都是需要定期执行的操作,在作为 StatefulSets 运行时需要仔细考虑。
在 Kubernetes 的早期阶段,CoreOS 的可靠性工程师(SREs)创建了一种称为运算符的新型云原生软件,用于 Kubernetes。最初的目的是将运行特定应用程序的领域特定知识封装到一个控制器中,从而扩展 Kubernetes 的能力。想象一下在 StatefulSet 控制器的基础上构建能够部署、扩展、升级、备份和运行 Cassandra 或 Kafka 等操作的控制器。最早创建的一些运算符是为 etcd 和 Prometheus 创建的,后者使用时间序列数据库长期保存指标。通过运算符可以正确创建、备份和恢复配置 Prometheus 或 etcd 实例,并且它们是新的由 Kubernetes 管理的对象,就像 pod 或 Deployment 一样。
直到最近,运算符一直是由 SRE 或软件供应商为其特定应用程序创建的一次性工具。在 2018 年中期,Red Hat 创建了 Operator Framework,这是一套工具,包括 SDK 生命周期管理器和未来模块,将支持计量、市场和注册表类型功能。运算符不仅适用于有状态应用程序,而且由于其自定义控制逻辑,它们确实更适合复杂数据服务和有状态系统。
运算符不仅成为扩展 Kubernetes API 的标准方式,还将最佳实践和操作监控引入到 Kubernetes 中复杂系统流程中。发现已发布的 Kubernetes 生态系统运算符的好地方是OperatorHub。他们维护一个更新的经过策划的运算符列表。
如果你有兴趣了解运算符的工作原理,第二十一章是这版新增的内容,会为你提供开发运算符和使用的最佳实践基础。此外,可以查阅《Kubernetes Operators》(O'Reilly)由 Jason Dobies 和 Joshua Wood 撰写,深入了解构建运算符的详细步骤。
StatefulSet 和运营商最佳实践
需要状态和可能复杂管理和配置操作的大型分布式应用程序受益于 Kubernetes StatefulSets 和 Operators。运营商仍在发展中,但它们得到了社区的大力支持,因此这些最佳实践基于发布时的当前能力:
-
使用 StatefulSets 应谨慎,因为状态型应用通常需要更深入的管理,目前编排器无法很好地管理(请阅读“运营商”,可能会对 Kubernetes 的这一不足提供未来的解决方案)。
-
StatefulSet 的无头 Service 不会自动创建,必须在部署时创建,以正确地将 pod 地址作为单独的节点处理。
-
当应用程序需要顺序命名和可靠的扩展时,并不总是意味着需要分配 PersistentVolumes。
-
如果集群中的节点变得无响应,则 StatefulSet 的任何 pod 都不会自动删除;相反,在优雅期限后,它们将进入
Terminating或Unknown状态。清除此 pod 的唯一方法是从集群中删除节点对象,kubelet 开始重新工作并直接删除 pod,或者运营商强制删除 pod。强制删除应该是最后的选择,并且应该小心确保删除 pod 的节点不会再次上线,因为现在集群中将有两个具有相同名称的 pod。您可以使用kubectl delete pod nginx-0 --grace-period=0 --force强制删除该 pod。 -
即使强制删除 pod 后,它可能仍然处于
Unknown状态,因此对 API 服务器的修补将删除该条目,并导致 StatefulSet 控制器创建已删除 pod 的新实例:kubectl patch pod nginx-0 -p '{"metadata":{"finalizers":null}}'。 -
如果正在运行具有某种类型的领导者选举或数据复制确认过程的复杂数据系统,请使用
preStop hook来适当关闭任何连接,强制进行领导者选举或在使用优雅关闭进程删除 pod 前验证数据同步。 -
当需要有状态数据的应用程序是复杂的数据管理系统时,请查看是否存在运营商来帮助管理应用程序更复杂的生命周期组件。如果应用程序是内部构建的,则值得调查是否将应用程序打包为运营商以增加应用程序的可管理性。参见CoreOS 运营商 SDK 作为示例。
概要
大多数组织希望将其无状态应用程序容器化,而将有状态应用程序保持不变。随着越来越多的云原生应用在云服务提供商的 Kubernetes 平台上运行,数据引力成为一个问题。有状态应用程序需要更多的尽职调查,但通过引入 StatefulSets 和 Operators,将它们运行在集群中的现实性已经加快。将卷映射到容器中允许 Operators 将存储子系统的具体细节与任何应用程序开发分离开来。在 Kubernetes 中管理诸如数据库系统之类的有状态应用程序仍然是一个复杂的分布式系统,需要使用 Pods、ReplicaSets、Deployments 和 StatefulSets 等原生 Kubernetes 基元进行仔细的编排。使用具有特定应用程序知识的 Operators 作为 Kubernetes 本地 API 可能有助于将这些系统提升到基于生产的集群中。
第十七章:准入控制和授权
控制访问 Kubernetes API 对于确保您的集群不仅安全,还可以用作向所有用户、工作负载和集群组件传达策略和治理的手段至关重要。在本章中,我们分享如何使用准入控制器和授权模块来启用特定功能,以及如何定制它们以满足您的特定需求。
在我们深入讨论准入控制和授权之前,让我们回顾一下 API 请求通过 API 服务器的流程。图 17-1 提供了关于准入控制和授权在该流程中发生位置和方式的见解。它描述了通过 Kubernetes API 服务器的端到端请求流程,直到接受对象保存到存储为止。按照 API 请求从左到右通过 API 服务器,特别注意准入控制和授权的顺序。我们将在本章介绍这些的最佳实践。

图 17-1。Kubernetes API 请求流程
准入控制
您是否曾想过在定义一个不存在的命名空间中的资源时如何自动创建命名空间?也许您想知道如何选择默认的存储类?这些变更由名为准入控制器的功能支持。在本节中,我们将探讨如何使用准入控制器代表用户在服务器端实现 Kubernetes 最佳实践,并如何利用准入控制来管理 Kubernetes 集群的使用方式。
它们是什么?
准入控制器位于 Kubernetes API 服务器请求流程的路径中,并在认证和授权阶段后接收请求。它们用于在将请求对象保存到存储之前验证或变更(或两者兼有)请求对象。验证和变更准入控制器的区别在于,变更准入控制器可以修改它们接受的请求对象,而验证准入控制器则不能。
为什么它们很重要?
鉴于准入控制器位于所有 API 服务器请求的路径上,您可以以多种不同的方式使用它们。最常见的是,准入控制器的使用可以分为以下三类:
策略和治理
准入控制器允许执行策略以满足业务需求;例如:
-
仅当在
dev命名空间中时,才能使用内部云负载均衡器。 -
Pod 中的所有容器必须具有资源限制。
-
向所有资源添加预定义的标准标签或注释,使它们可以被现有工具发现。
-
所有 Ingress 资源只能使用 HTTPS。有关如何在此上下文中使用准入 Web 钩子的详细信息,请参阅第十一章。
安全性
您可以使用准入控制器强制执行整个集群中的一致安全姿态。一个典型的例子是 Pod 安全准入控制器,它根据 pod 规范中定义的安全敏感字段的配置确定是否应该允许 pod。例如,它可以拒绝特权容器或从主机文件系统使用特定路径。您可以使用准入 Webhook 强制执行更精细或自定义的安全规则。
资源管理
准入控制器允许您为您的集群用户验证并提供最佳实践,例如:
-
确保所有入口完全限定域名(FQDN)位于特定后缀内。
-
确保入口 FQDN 不重叠。
-
Pod 中的所有容器必须具有资源限制。
准入控制器类型
准入控制器分为两类:标准 和 动态。标准准入控制器已编译到 API 服务器中,并随每个 Kubernetes 发布作为插件提供;它们在启动 API 服务器时需要配置。另一方面,动态控制器可以在运行时配置,并且是在核心 Kubernetes 代码库之外开发的。唯一类型的动态准入控制是准入 Webhook,它通过 HTTP 回调接收准入请求。
默认情况下,推荐启用准入控制器。您可以使用以下标志在 Kubernetes API 服务器上启用额外的准入控制器:
--enable-admission-plugins
在当前版本的 Kubernetes 中,默认启用以下准入控制器:
CertificateApproval, CertificateSigning, CertificateSubjectRestriction,
DefaultIngressClass, DefaultStorageClass, DefaultTolerationSeconds,
LimitRanger, MutatingAdmissionWebhook, NamespaceLifecycle,
PersistentVolumeClaimResize, PodSecurity, Priority, ResourceQuota,
RuntimeClass, ServiceAccount, StorageObjectInUseProtection,
TaintNodesByCondition,
ValidatingAdmissionWebhook
您可以在 Kubernetes 文档 中找到 Kubernetes 准入控制器及其功能的列表。
您可能已经注意到建议启用的准入控制器列表中包含以下内容:“MutatingAdmissionWebhook, ValidatingAdmissionWebhook”。这些标准准入控制器本身不实施任何准入逻辑;而是用于配置在集群中运行的 Webhook 端点以转发准入请求对象。
配置准入 Webhook
如前所述,准入 Webhook 的主要优势之一是它们是动态可配置的。重要的是,您理解如何有效地配置准入 Webhook,因为在一致性和失败模式方面存在重要的影响和权衡。
接下来的代码片段是一个 ValidatingWebhookConfiguration 资源清单。此清单用于定义一个验证准入 Webhook。代码片段提供了每个字段功能的详细描述:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: ## Resource name
webhooks:
- name: ## Admission webhook name, which will be shown to the user when
## any admission reviews are denied
clientConfig:
service:
namespace: ## The namespace where the admission
## webhook pod resides
name: ## The service name that is used to connect to the admission
## webhook
path: ## The webhook URL
caBundle: ## The PEM encoded CA bundle which will be used to validate the
## webhook's server certificate
rules: ## Describes what operations on what resources/subresources the API
## server must send to this webhook
- operations:
- ## The specific operation that triggers the API server to send to this
## webhook (e.g., create, update, delete, connect)
apiGroups:
- ""
apiVersions:
- "*"
resources:
- ## Specific resources by name (e.g., deployments, services, ingresses)
failurePolicy: ## Defines how to handle access issues or unrecognized errors,
## and must be Ignore or Fail
admissionReviewVersions: ["v1"] ## Specify what versions of AdmissionReview
## objects are accepted
sideEffects: ## Signal whether the webhook may out-of-band changes that need
## to be handled
timeoutSeconds: 5 ## How long the API server should wait for a response
## before treating the request as a failure
为了完整起见,让我们看一下一个 MutatingWebhookConfiguration 资源清单。此清单定义了一个变更准入 Webhook。代码片段提供了每个字段功能的详细描述:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: ## Resource name
webhooks:
- name: ## Admission webhook name, which will be shown to the user when any
## admission reviews are denied
clientConfig:
service:
namespace: ## The namespace where the admission webhook pod resides
name: ## The service name that is used to connect to the admission
## webhook
path: ## The webhook URL
caBundle: ## The PEM encoded CA bundle which will be used to validate the
## webhook's server certificate
rules: ## Describes what operations on what resources/subresources the API
## server must send to this webhook
- operations:
- ## The specific operation that triggers the API server to send to this
## webhook (e.g., create, update, delete, connect)
apiGroups:
- ""
apiVersions:
- "*"
resources:
- ## Specific resources by name (e.g., deployments, services, ingresses)
failurePolicy: ## Defines how to handle access issues or unrecognized errors,
## and must be Ignore or Fail
admissionReviewVersions: ["v1"] ## Specify what versions of AdmissionReview
## objects are accepted
sideEffects: ## Signal whether the webhook may out-of-band changes that need
## to be handled
reinvocationPolicy: ## Control whether mutating webhooks are reinvoked if
## another mutation to an object occurs
timeoutSeconds: 5 ## How long the API server should wait for a response
## before treating the request as a failure
你可能已经注意到这两个资源是相同的,除了kind和reinvocationPolicy字段之外。然而,在后端有一个区别:MutatingWebhookConfiguration 允许准入 Webhook 返回修改后的请求对象,而 ValidatingWebhookConfiguration 不允许。不过,定义一个 MutatingWebhookConfiguration 并简单验证是可以接受的;这里涉及到安全考虑,你应该考虑遵循最小权限原则。
注意
你可能会想:“如果我定义一个 ValidatingWebhookConfiguration 或 MutatingWebhookConfiguration,并且在规则对象下的资源字段为 ValidatingWebhookConfiguration 或 MutatingWebhookConfiguration,会发生什么?” 好消息是,对于 ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration 对象的准入请求,永远不会调用 ValidatingAdmissionWebhooks 或 MutatingAdmissionWebhooks。这有其充分的理由:你不希望意外地使集群处于不可恢复的状态。
准入控制最佳实践
现在我们已经介绍了准入控制器的强大功能,以下是我们的最佳实践,帮助你充分利用它们。
准入插件的顺序无关紧要
在 Kubernetes 的早期版本中,准入插件的顺序对处理顺序具有特定的影响,因此很重要。在当前支持的 Kubernetes 版本中,通过 --enable-admission-plugins 作为 API 服务器标志指定的准入插件顺序不再重要。然而,在准入 Webhook 的情况下,顺序确实发挥了一定作用,因此了解请求流程非常重要。请求的准入或拒绝操作采用逻辑与运算,这意味着如果任何一个准入 Webhook 拒绝请求,整个请求将被拒绝,并向用户返回错误。还需注意的是,变更准入控制器始终在验证准入控制器之前运行。如果仔细思考一下,这是合理的:你可能不希望验证随后将要修改的对象。图 17-2 展示了通过准入 Webhook 的请求流程;你将看到变更准入控制器在验证准入控制器之前运行的情况。

图 17-2. 通过准入 Webhook 的 API 请求流程
不要修改相同的字段
配置多个变异入场网钩也带来了挑战。没有办法通过多个变异入场网钩来排序请求流程,因此重要的是不要让变异入场控制器修改相同的字段,因为这可能导致行为不一致。在您拥有多个变异入场网钩的情况下,我们通常建议配置验证入场网钩来确认最终的资源清单在变异后是否符合您的预期,因为它确保在变异网钩之后运行。
变异入场网钩必须是幂等的
这意味着它们必须能够处理和接纳已经被处理过并且可能已经被修改过的对象。
失败开/失败关闭
您可能会记得在变异和验证网钩配置资源中看到failurePolicy字段。该字段定义了在入场网钩遇到访问问题或遇到未识别错误时,API 服务器应该如何处理。您可以将此字段设置为Ignore或Fail。Ignore本质上是失败打开,意味着请求的处理将继续,而Fail则拒绝整个请求。这看起来可能很明显,但两种情况的影响都需要考虑。忽略关键的入场网钩可能导致业务依赖的策略未应用到用户不知情的资源上。
保护措施之一是在 API 服务器记录无法访问特定入场网钩时发出警报。如果入场网钩遇到问题,Fail可能会更加破坏性,因为它将拒绝所有请求。为了防止这种情况发生,您可以限定规则以确保只有特定的资源请求设置为入场网钩。作为一个原则,您不应该有适用于集群中所有资源的任何规则。
入场网钩必须快速响应
如果您编写了自己的入场网钩,重要的是要记住用户/系统请求可能直接受到您的入场网钩做出决策并响应的时间影响。所有入场网钩调用都配置有 30 秒的超时时间,超时后将采取failurePolicy。即使您的入场网钩需要几秒钟来做出批准/拒绝的决定,也可能严重影响与集群的用户体验。避免使用复杂的逻辑或依赖外部系统(如数据库)来处理批准/拒绝逻辑。
验证入场网钩
一个可选字段允许您通过NamespaceSelector字段来限定入场网钩操作的命名空间。此字段默认为空,匹配所有内容,但可以通过matchLabels字段匹配命名空间标签。我们建议您始终使用此字段,因为它允许对每个命名空间进行显式选择。
始终使用 NamespaceSelector 在单独的命名空间部署
当自托管 Webhook 入场控制器时,部署 Webhook 入场控制器到单独的命名空间,并使用 NamespaceSelector 字段排除部署到该命名空间的资源的处理。
不要触碰 kube-system 命名空间
kube-system 命名空间是所有 Kubernetes 集群中共有的保留命名空间。所有系统级服务都在这里运行。我们建议绝对不要针对此命名空间中的资源运行入场 Webhook,并且可以通过使用 NamespaceSelector 字段并简单地不匹配 kube-system 命名空间来实现此目的。您还应考虑对于任何对集群操作必需的系统级命名空间执行此操作。
使用 RBAC 严格限制入场 Webhook 配置
现在您已经了解了入场 Webhook 配置中的所有字段,您可能已经想到了一种非常简单的方式来破坏对集群的访问。毫无疑问,创建 MutatingWebhookConfiguration 和 ValidatingWebhookConfiguration 都是集群的根级操作,并且必须适当地使用 RBAC 进行锁定。如果未这样做,可能会导致集群故障,甚至更糟糕的是,对您的应用工作负载进行注入攻击。
不要发送敏感数据
入场 Webhook 本质上是接受 AdmissionRequests 并输出 AdmissionResponses 的不透明盒子。它们如何存储和操作请求对用户来说是不透明的。重要的是要考虑发送到入场 Webhook 的请求有效负载。在 Kubernetes Secrets 或 ConfigMaps 的情况下,它们可能包含敏感信息,并且需要对存储和共享这些信息的方式提供强有力的保证。与入场 Webhook 共享这些资源可能会泄露敏感信息,因此应将资源规则限定于验证和/或变异所需的最小资源。
授权
我们经常在以下问题的背景下考虑授权:“此用户是否能够对这些资源执行这些操作?”在 Kubernetes 中,每个请求的授权在认证之后但入场之前执行。在本节中,我们探讨了如何配置不同的授权模块,并更好地了解如何创建适当的策略以满足集群的需求。图 17-3 展示了授权在请求流程中的位置。

图 17-3. API 请求流经授权模块
授权模块
授权模块负责授予或拒绝访问权限。它们根据必须明确定义的策略决定是否授予访问权限;否则,所有请求将被隐式拒绝。
Kubernetes 默认提供以下授权模块:
基于属性的访问控制(ABAC)
允许通过本地文件配置授权策略
RBAC
允许通过 Kubernetes API 配置授权策略(更多细节请参阅第四章)
Webhook
允许通过远程 REST 端点处理请求的授权
Node
专门的授权模块,用于授权来自 kubelet 的请求
这些模块由集群管理员通过 API 服务器上的--authorization-mode标志进行配置。可以配置多个模块,并按顺序检查。与准入控制器不同,如果单个授权模块允许请求,则请求可以继续进行。只有在所有模块拒绝请求的情况下,才会向用户返回错误。
ABAC
让我们在使用 ABAC 授权模块的情况下看一个策略定义的例子。以下内容授予用户 Mary 对kube-system命名空间中一个 Pod 的只读访问权限:
apiVersion: abac.authorization.kubernetes.io/v1beta1
kind: Policy
spec:
user: mary
resource: pods
readonly: true
namespace: kube-system
如果 Mary 尝试发起以下请求,则会被拒绝,因为 Mary 没有权限获取demo-app命名空间中的 Pod:
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
spec:
resourceAttributes:
verb: get
resource: pods
namespace: demo-app
此示例引入了一个新的 API 组authorization.k8s.io。这一系列 API 公开了 API 服务器的授权给外部服务,并包括以下用于调试的 API:
SelfSubjectAccessReview
当前用户的访问审查
SubjectAccessReview
Like SelfSubjectAccessReview but for any user
LocalSubjectAccessReview
类似于 SubjectAccessReview,但特定于命名空间
SelfSubjectRulesReview
返回用户在给定命名空间中可以执行的操作列表
这一点很酷,您可以按照通常的方式创建资源来查询这些 API。让我们以前面的例子为例,并使用 SelfSubjectAccessReview 来测试这一点。输出中的状态字段指示此请求被允许:
$ cat << EOF | kubectl create -f - -o yaml
apiVersion: authorization.k8s.io/v1
kind: SelfSubjectAccessReview
spec:
resourceAttributes:
verb: get
resource: pods
namespace: demo-app
EOF
apiVersion: authorization.k8s.io/v1
kind: SelfSubjectAccessReview
metadata:
creationTimestamp: null
spec:
resourceAttributes:
namespace: kube-system
resource: pods
verb: get
status:
allowed: true
实际上,Kubernetes 提供了内置于kubectl中的工具,使这一过程更加简单。kubectl auth can-i命令通过查询与前述示例相同的 API 来运行:
$ kubectl auth can-i get pods --namespace demo-app
yes
使用管理员凭据,您也可以以其他用户身份运行相同的命令来检查操作:
$ kubectl auth can-i get pods --namespace demo-app --as mary
yes
RBAC
Kubernetes 基于角色的访问控制在第四章中有详细介绍。
Webhook
使用 Webhook 授权模块允许集群管理员配置外部 REST 端点以委托授权流程。这将在集群外运行,并通过 URL 访问。REST 端点的配置信息存储在控制平面主机文件系统中的文件中,并通过--authorization-webhook-config-file=SOME_FILENAME选项配置到 API 服务器上。配置完成后,API 服务器将 SubjectAccessReview 对象作为请求体的一部分发送到授权 Webhook 应用程序,后者会处理并返回带有完成状态字段的对象。
授权最佳实践
在更改配置在集群上配置的授权模块之前,请考虑以下最佳实践:
不要在多个控制平面集群上使用 ABAC
考虑到 ABAC 策略需要放置在每个控制平面主机的文件系统上,并保持同步,我们通常建议不要在多个控制平面集群中使用 ABAC。同样的情况也适用于 webhook 模块,因为配置是基于文件和相应的标志的存在。此外,对这些文件中的策略进行更改需要重启 API 服务器才能生效,这实际上是单个控制平面集群中的控制平面故障,或多个控制平面集群中不一致的配置。鉴于这些细节,我们建议仅针对用户授权使用 RBAC 模块,因为规则是在 Kubernetes 自身中配置和存储的。
不要使用 webhook 模块
Webhook 模块虽然功能强大,但潜在风险很高。考虑到每个请求都要经过授权过程,如果 webhook 服务出现故障,对集群将是灾难性的。因此,我们通常建议不使用外部授权模块,除非您完全审核并对集群在 webhook 服务不可达或不可用时的故障模式感到满意和放心。
总结
在本章中,我们涵盖了准入和授权的基础主题,并介绍了最佳实践。通过确定最佳准入和授权配置,您可以自定义所需的控制和策略,以维护您集群的生命周期。
第十八章:GitOps 与部署
在本章中,我们将讨论 GitOps 以及如何使用它来在 Kubernetes 上部署和管理应用程序。我们将深入探讨设置 GitOps 工作流程的最佳实践以及如何利用不同的工具来实现这一目标。
GitOps 是一种用于进行 Kubernetes 应用部署的方法。它通过利用 Git 作为 Kubernetes 资源的唯一真实来源来实现。将 Git 置于部署流水线的中心,开发人员和运维人员可以发起拉取请求,加速并简化 Kubernetes 应用的部署和操作任务。这样一来,您可以利用相同的实践来管理 Kubernetes 资源,就像管理应用代码一样。开发人员会非常熟悉这种工作流程,因为他们可以利用处理应用代码时使用的相同工具。
在本章中,我们涵盖以下主题:
-
什么是 GitOps?
-
为什么要使用 GitOps?
-
比较 GitOps 与其他部署方法
-
模式和最佳实践
-
GitOps 工具链
我们还会经历一个示例 GitOps 工作流程,包括以下任务:
-
使用 Flux 设置 GitOps 代理
-
将 Flux 代理连接到 Git 仓库
-
同步资源到 Kubernetes 集群
-
将应用程序部署到集群
什么是 GitOps?
GitOps 由 Weaveworks 的人员推广,并基于他们在生产中运行 Kubernetes 的经验构建了这些思想和基础。GitOps 将软件开发生命周期的概念应用到运维中。通过 GitOps,您的 Git 仓库成为了真实来源,而您的集群则与配置的 Git 仓库同步。例如,如果您更新了一个 Kubernetes 部署清单,这些配置更改会自动反映在 Git 中的集群状态中。
使用这种方法,您可以更轻松地维护一致的多集群,并避免在整个集群中出现配置漂移。GitOps 允许您以声明方式描述多个环境的集群,并驱动维护该集群的状态。GitOps 的实践可以适用于应用交付和操作,并为开发人员提供了一个共同的工具链。
Weaveworks Flux 是最早支持 GitOps 方法的工具之一,也是我们在本章中将使用的工具。许多新的工具已经发布到云原生生态系统中,如 Intuit 公司的 Argo CD,也被广泛应用于 GitOps 方法。稍后我们将更深入地探讨 GitOps 可用的工具。
Figure 18-1 提供了一个 GitOps 工作流程的表示。我们有一个 Git 仓库,其中包含我们应用程序的应用代码和 Kubernetes 清单。Flux 代理已配置为监视该仓库的任何更改。当开发人员提交代码更改时,Flux 代理将同步任何新变更到 Kubernetes 集群中。

图 18-1. GitOps 工作流程
在构建您的 GitOps 工作流程时,您应考虑 OpenGitOps 项目 定义的四个核心原则:
声明性配置
所有配置都存储在 Git 中,以声明性的 YAML 文件形式。这样可以为集群配置提供一个唯一的真实数据来源。
配置版本化
所有配置都存储在 Git 中,并跟踪和版本化所有更改。这允许轻松审计更改和回滚。
不可变的配置
所有配置都是不可变的。这意味着一旦进行更改,就无法修改。这允许集群保持一致的状态。
持续状态对比
集群状态与 Git 中定义的状态持续对比。这允许集群保持一致的状态。
为什么选择 GitOps?
GitOps 是管理您的 Kubernetes 集群的一种优秀方式,它可以用于部署应用程序到您的集群以及管理集群和应用程序配置。在我们讨论所有这些好处之前,让我们首先看一下我们如何传统地在 Kubernetes 上部署和配置应用程序。
图 18-2 展示了传统的部署工作流程。我们有一个开发人员正在为一个应用程序开发新功能。开发人员将会对应用程序代码进行更改,然后构建一个新的容器映像。接下来,开发人员将新的容器映像推送到容器注册表。然后,开发人员将更新 Kubernetes 清单以使用新的容器映像,并将更改应用到集群中。这是一个非常手动的过程,可能非常耗时。某些步骤可以通过工具自动化,但随着应用程序和集群数量的增长,这可能变得复杂。

图 18-2. 传统部署工作流程
这种工作流程可能非常容易出错,并且很难追踪问题的源头。回滚更改也可能很困难,因为您需要手动恢复 Kubernetes 清单的更改。这也可能导致配置漂移,因为用户可能直接对 Kubernetes 中的资源进行更改。管理环境的安全访问也可能变得复杂,因为需要多个流水线和用户访问权限。审计从更改到部署的每一次互动也可能会因为多个流水线而变得困难。
我们可以通过 GitOps 提供的以下好处解决这些问题:
声明性配置
所有配置都以声明性 YAML 文件形式存储在 Git 中。这不仅允许有一个真实数据来源,还能通过 Git 历史轻松审计更改。开发人员习惯于使用 Git 进行工作,因此他们对这种工作流程非常熟悉。
版本控制
Git 仓库支持不可变性和版本历史。例如,使用 Git 管理前述配置将为你提供一个单一的源,从中驱动应用程序的一切。这使你能够轻松追踪任何时间的更改。它允许你查看 Git 历史中找到的所有更改并比较这些更改。
持续对比
集群状态持续与 Git 中定义的状态对比。它还允许简单的回滚,因为你可以简单地在 Git 中恢复更改。系统可以自动同步 Git 中的相同状态到你的集群中。这使得集群能够保持一致的状态。
安全性
当你使用 Git 管理部署到 Kubernetes 的应用程序时,你获得了集群所有更改的完整审计日志。所有更改都在 Git 仓库中进行,GitOps 代理可以自动调解对 Kubernetes 资源的任何直接更改。这提供了更改记录的完整追踪,可以了解到是谁做了什么更改。它支持一致的操作,并增强了环境的安全性。
尽管你可能有一个非常自动化的 CI/CD 管道,但你的工作流程可能仍有一些手动步骤。GitOps 旨在通过自动化工作流并提供开发者中心的工作流来解决这些挑战。
GitOps 仓库结构
关于 GitOps 的首要问题之一是如何结构化你的 Git 仓库。有许多不同的方法来结构化你的 Git 仓库,但每种方法都有其利弊。
结构化 Git 仓库的四种常见策略是:
单一的 monorepo
所有的 Kubernetes 清单和应用程序代码都存储在单个仓库中。这是简单的方法,但随着公司规模的扩大,变得更加困难。这种方法也不允许关注点分离,因为所有团队的源代码和 Kubernetes 清单都存放在同一个仓库中。对于小公司来说可能效果不错,但随着公司的扩展,你很快就会发现这种方法不再适用。以下是此类仓库布局结构的示例:
├── app-x
│ ├── common
│ └── deploy
│ └── manifest
├── app-y
│ ├── prod
│ └── staging
├── app-z
└── ops-team
├── flux
├── ingress
└── prometheus
每个团队一个仓库
每个团队有自己的仓库,并且 Kubernetes 清单存储在同一个仓库中。这种方法允许更好地组织和关注点分离,但随着应用程序组合随时间增长,管理起来会变得更加困难。以下是此类仓库布局结构的示例:
├── ops-team
│ ├── elk
│ ├── flux
│ └── prometheus
├── team-x
│ └── app-x
│ └── deploy
│ └── manifest
└── team-y
├── prod
└── staging
每个应用程序一个仓库
每个应用程序都有自己的仓库,并且 Kubernetes 清单存储在同一个仓库中。这种方法允许更好地组织和关注点分离,因为可以将其锁定为团队的只读访问权限。使用这种结构的缺点是无法在一个地方看到所有内容。以下是此类仓库布局结构的示例:
── ops-team-repo
│ ├── elk
│ ├── flux
│ └── prometheus
── team-x-repo
│ └── app-x
│ └── deploy
│ └── manifest
每个环境一个分支
每个环境在同一存储库中有自己的分支。这种方法允许您通过简单的 Git 合并来推广环境。通过简单的 Git 合并推广可能会导致环境之间的意外更改和合并冲突。这种方法的缺点是通常会有大量分支,而且难以管理。这种方法也不适合使用 Kustomize 和 Helm 等模板工具。以下是这种类型的存储库布局结构的示例:
-- main
-- staging
-- QA
-- dev
通常,您会希望评估您的组织和团队布局,以决定哪种结构最适合您。从每个团队的存储库开始是一个很好的起点,因为它是一个良好的折中点,提供了明确的关注点分离和简单的存储库管理。
管理秘密
在实施 GitOps 工作流程时,秘密管理是一个常见的挑战。有许多不同的方法来管理秘密,最佳方法将取决于您的组织。接下来我们将深入探讨在 GitOps 方式下管理秘密的五种常见方法:
直接在 Git 中存储秘密
这种方法是最简单的,但不推荐使用。这种方法的问题在于,您正在将纯文本秘密存储在可能是公共的存储库中。即使您的存储库是内部和私有的,您仍然在以纯文本形式存储秘密。多个用户可能有权访问此存储库,然后将访问权限扩展到秘密。
将秘密嵌入容器映像
这种方法比在 Git 中以纯文本存储秘密稍好一些。这种方法的问题在于,将秘密嵌入映像将要求您每次旋转秘密时重新构建映像。它也无法解决安全问题,因为多个用户可能能够拉取并运行映像。由于安全问题,这种方法也不推荐使用。
使用 Kubernetes Secrets
这种方法直接在 Kubernetes 中可用,提供了一个简单的入门方式。这种方法的问题在于 Kubernetes Secrets 并不真正保密。这里的意思是 Kubernetes Secrets 看起来被加密了,但实际上只是 base64 编码。由于安全问题,这种方法也不推荐使用。
使用 Sealed Secrets
Sealed Secrets 是 Bitnami 的一个项目。它有两个组件:一个集群控制器和一个客户端工具称为 kubeseal。kubeseal 实用程序使用非对称加密来加密只有控制器可以解密的秘密。这些秘密然后可以加密存储在 Git 中,只能由您集群中的控制器解密。这是在 GitOps 方式下管理秘密的推荐方法。
将秘密存储在秘密管理工具中
这种方法允许您将密钥存储在安全位置,并从您的集群访问它们。这些密钥可以存储在像 HashiCorp Vault、Azure Keyvault、Google KMS 等外部密钥管理解决方案中。这种方法允许您使用可能已经存在的现有解决方案,并继续使用相同的工作流程。这也是以 GitOps 方式管理密钥的推荐方法之一。
虽然有许多不同的方法来管理密钥,但最佳方法将取决于您的组织。正如我们讨论的那样,Sealed Secrets 和外部密钥管理是管理密钥的推荐方法。
设置 Flux
Flux 是一个 Kubernetes 运算符,它监视您的 Git 仓库的变化,并自动将这些变化应用到您的集群中。Flux 是在您的集群中实施 GitOps 的成熟工具,并且它是我们将在本章的其余部分中使用的工具。
首先,我们将开始配置 minikube 以部署 Flux。您可以从 minikube 网站 安装 minikube。我们正在使用 Mac,所以我们将使用 brew 安装 minikube:
brew install minikube
现在我们将安装 Flux 并准备我们的集群以同步到 Git 仓库。我们将使用 flux CLI 安装 Flux。你可以从 flux 网站 安装 flux CLI。
安装 Flux CLI:
brew install fluxcd/tap/flux
导出您的 GitHub 令牌:
export GITHUB_TOKEN=<your-token>
export GITHUB_USER=<your-username>
检查您的集群是否可以安装 Flux:
flux check --pre
引导 Flux:
flux bootstrap github \
--owner=$GITHUB_USER \
--repository=kbp-flux \
--branch=main \
--path=./clusters/prod \
--personal
前面的 bootstrap 命令将在您的 GitHub 账户中创建一个名为 kbp-flux 的 Git 仓库。它还将创建一个 main 分支和一个 clusters/prod 目录。clusters/prod 目录将包含将部署到您的集群中的 Flux 组件。clusters/prod 目录还将包含一个 gotk-components.yaml 文件,该文件将用于将 Flux 组件部署到您的集群中。这还将把 Flux 组件安装到 flux-system 命名空间中。
现在让我们检查 flux-system 命名空间,看看 Flux 组件是否已部署:
kubectl get pods -n flux-system
您应该看到以下输出:
NAME READY STATUS RESTARTS AGE
helm-controller-8664d9dcfc-4gd2h 1/1 Running 0 6m30s
kustomize-controller-9888f965-ld5g6 1/1 Running 0 6m30s
notification-controller-b6d8458c7-vjb86 1/1 Running 0 6m30s
source-controller-5b68b64c65-pj2tn 1/1 Running 0 6m30s
现在让我们将它创建的仓库克隆到我们的本地机器上:
git clone https://github.com/$GITHUB_USER/kbp-flux
接下来,我们将向我们的仓库添加一个 Flux 配置,并在 GitHub 上使用一个公共仓库。我们将使用 Weaveworks 的 Stefan Prodan 创建的示例应用程序。
让我们创建一个指向应用程序仓库主分支的 Git 仓库清单:
flux create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
--branch=master \
--interval=30s \
--export > ./clusters/prod/podinfo-source.yaml
然后我们将配置 Flux 来部署该应用程序,并向应用程序应用 Kustomize 配置:
flux create kustomization podinfo \
--target-namespace=default \
--source=podinfo \
--path="./kustomize" \
--prune=true \
--interval=5m \
--export > ./clusters/prod/podinfo-kustomization.yaml
现在我们将推送更改到我们的仓库:
git add -A && git commit -m "Add podinfo Kustomization"
git push
我们可以使用 Flux CLI 看到这些变化正在应用中:
flux get kustomizations
您应该看到以下输出:
flux get kustomizations --watch
NAME REVISION SUSPENDED READY MESSAGE
flux-system main@sha1:9c3fb6f1 False True Applied revision: main@sh...
podinfo master@sha1:1abc44f0 False True Applied revision: master@...
我们可以看到资源已经部署到我们的集群中:
kubectl get pods -n default
主分支中对 podinfo Kubernetes 清单所做的任何更改现在都会自动反映在您的集群中。
我们现在在我们的集群中设置了 Flux,并将其引导到一个 Git 仓库,并配置 Flux 来部署一个应用程序。现在我们可以开始使用 Flux 来管理我们的集群。
这是如何设置 Flux 的一个非常基本的示例,如果您想深入了解 Flux,请查阅 Flux 文档。
GitOps 工具
许多不同的工具可以用来在您的集群中实施 GitOps。在本节中,我们将介绍一些最受欢迎的工具。
在评估用于 GitOps 的工具时,您应考虑易用性、企业特性和可扩展性。以下是可用于在您的集群中实施 GitOps 的开源和商业工具:
Flux
Flux 是一个 Kubernetes 操作器,用于监视您的 Git 仓库的变更,并自动将这些变更应用到您的集群中。Flux 是在您的集群中实施 GitOps 的成熟工具。Weaveworks 也提供了 Flux 的托管版本。Flux 目前是 CNCF 的毕业项目。
ArgoCD
Argo CD 是一个开源的 GitOps 连续交付工具。它监控您的集群和存储在 Git 仓库中声明性定义的基础设施,并解决两者之间的差异,从而自动化应用部署。ArgoCD 目前是 CNCF 的毕业项目。
Codefresh
Codefresh 是一个可以用于在您的集群中实施 GitOps 的 CI/CD 平台。Codefresh 提供了一个托管平台,提供 ArgoCD 作为服务。
Harness
Harness 是一个可以用于在您的集群中实施 GitOps 的 CI/CD 平台。Harness 是一个成熟的工具,适用于在您的集群中实施 GitOps,并提供托管版本。Harness 面向企业客户,提供了完整的持续交付功能套件。
GitOps 最佳实践
使用 Kubernetes 进行 GitOps 时,请考虑以下最佳实践:
-
从一个小应用开始,然后扩展您的努力来管理所有东西的 GitOps 模型。这将使您对 GitOps 实现建立信心。
-
评估符合您需求的工具,或者从像 Flux 或 ArgoCD 这样经过验证的 OSS 工具开始。
-
避免为您的仓库布局使用分支,因为这是最复杂和容易出错的仓库布局。
-
从每个环境一个文件夹开始,这提供了灵活性,并允许您使用 Kustomize 或 Helm 等模板工具。
-
利用 Sealed Secrets 或外部机密提供者来管理集群中的机密信息。
-
记住,GitOps 是一种过程,而不是一种工具,您现有的工具集可能已经满足您的需求。
总结
在本章中,我们介绍了什么是 GitOps,以及如何使用它来管理您的 Kubernetes 集群。我们还介绍了一些可以用来在您的集群中实施 GitOps 的工具。当您考虑是否适合使用 GitOps 时,应考虑您试图解决的问题和您的需求。如果 GitOps 能够帮助您解决这些问题,那么从 Flux 或 ArgoCD 这样的工具开始是个不错的选择。
第十九章:安全
Kubernetes 是一个强大的平台,用于编排云原生应用程序。然而,在我们熟悉和喜爱的 API 和工具之下,隐藏着一个庞大而复杂的分布式系统,需要特定的知识来确保安全。保护 Kubernetes 是一个复杂的话题,实际上需要一本专门的书来详细讲解;然而,如果您忽视了理解和实施安全最佳实践,可能会面临重大风险,我们在这里简要介绍。如果您没有正确地保护 Kubernetes 集群和工作负载,可能会导致您的数据和资源暴露给黑客、恶意软件和未经授权的访问。我们必须覆盖一些主要的安全领域,并提供最佳实践以帮助您走上正轨。
考虑到 Kubernetes 的复杂性,我们建议将问题分解为逻辑层次,在每个层次可以专注于具体的工具。处理安全问题的一个很好的方法是遵循“深度防御”策略。这要求在每个层次使用多重安全措施来保护 Kubernetes 和您的工作负载。此外,牢记最小特权原则,即用户和工作负载应仅具有执行其功能所需的访问权限。这些理论听起来都很好,但在实践中是什么样子呢?本章提供了一种将安全问题分层处理的方法,帮助您关注可用的解决方案和工具,以及集群安全、容器安全和代码安全。
许多安全最佳实践在其他章节中已经详细介绍,包括第 4 至第十一章。我们鼓励您查看这些章节,因为我们在这里不会再次以相同的详细程度涵盖这些特定主题,而是专注于我们未涵盖的领域。特别是,本章将重点放在层次结构上;深入挖掘它们,涵盖安全领域,并为每个层次提供最佳实践。
集群安全
鉴于 Kubernetes 控制平面通过一组 API 开放,保护集群的第一步是规范和限制谁可以访问集群以及他们可以执行的操作。接下来,我们将介绍 Kubernetes 控制平面的不同部分以及如何保护它们。
etcd 访问
Kubernetes 的默认存储系统是 etcd。您必须确保只有 Kubernetes API 服务器通过使用强密码来访问 etcd,并且不共享这些凭据。您还必须确保只有 API 服务器通过使用网络防火墙才能访问 etcd。直接访问 etcd 将绕过您所采取的所有后续安全措施,因此这是一个非常重要的安全层。
认证
Kubernetes 提供多种不同的认证方法,从 bearer token 和证书到 OpenID Connect(OIDC)和 Lightweight Directory Access Protocol(LDAP)集成。选择适合你业务需求的正确认证模型非常重要。安全挑战通常出现在创建、分发和存储用户需要使用 kubectl 来认证 Kubernetes 的 Kubeconfig 文件过程中。使用认证提供者允许检索临时动态令牌,而不是使用容易被恶意角色获取的静态令牌或证书。有关存储在 Kubeconfig 文件中的恶意代码实例的论文已经被撰写,因此控制它们的创建和分发非常重要。
授权
我们在第十七章中讨论了授权;然而,在 Kubernetes 安全的上下文中,授权是一个强大的工具,用于强制执行谁可以在哪些资源上执行什么操作。你可以使用基于角色的访问控制(RBAC)作为主要工具。幸运的是,Kubernetes 默认情况下提供了合理的默认设置;但是,你需要考虑整合团队成员身份以及命名空间,作为扩展 RBAC 资源的方法,以支持不断增长的工作负载和用户数量。使用 RBAC 锁定服务账户也非常重要,以确保需要访问 Kubernetes API 的工作负载只能执行其功能所需的最低操作。
TLS
默认情况下,Kubernetes 使用 TLS 加密的 API 端点。然而,不同的工具和平台可能启用了 HTTP 明文通信,这会导致安全漏洞,因为流量是不安全的。安全存储和控制访问 Kubernetes 使用的证书和密钥是非常重要的,并创建一个计划来轮换它们,以防它们丢失或被 compromise。在证书上设置较短的生命周期有助于降低安全风险。
Kubelet 和云元数据访问
Kubelet 是在每个节点上运行的组件,负责管理节点和运行在其上的 pod。不幸的是,Kubelet 默认启用了未经身份验证的 API。Kubelet API 非常强大,因此应该启用认证和授权。你的 Kubernetes 提供商可能已经为你处理了这些问题;然而,如果你自己搭建 Kubernetes 集群,应该仔细检查。除了 Kubelet API 外,如果在云提供商上运行,节点可能有访问云元数据 API 的权限,该 API 可用于暴露 Kubernetes 的配置凭证。建议使用网络策略锁定对元数据端点的访问。
Secrets
Kubernetes 秘密默认情况下未加密并非秘密。这意味着恶意行为者可以通过其他途径在静止状态下读取这些秘密。幸运的是,有几种不同的解决方案可以帮助解决这个问题。Kubernetes API 服务器提供配置加密提供程序的能力,该提供程序与配置文件合作,用于在存储在 etcd 之前加密特定的 Kubernetes 资源。加密提供程序通常是云秘密存储服务。当前加密提供程序实现的唯一挑战是没有办法加密所有内容,且配置繁琐且容易出错。Kubernetes 社区构建的另一个解决方案是csi secret store,它允许将秘密直接挂载到通过临时 RAMDISK 文件系统的 Pod 中。使用csi-secret-store使您可以绕过使用 Kubernetes 秘密,而是直接从其他受信任的秘密存储中访问它们。
日志记录和审计
Kubernetes 默认配置了丰富的日志记录。此外,还重要的是在 API 服务器上启用审计日志记录,这将记录所有安全事件的时间顺序日志,并可以通过审计策略进行配置。启用审计只是解决方案的一部分;您还必须确保将审计日志发送到聚合点,并配置触发器,一旦检测到异常事件,即向安全团队发出警报。
集群安全姿态工具
实施 Kubernetes 安全可能具有挑战性。好消息是,有开源工具可以扫描您的 Kubernetes 集群,检测安全风险,并标记常见的配置错误。此外,它们可以扫描集群上的所有资源并提供最佳实践。像Kubescape这样的工具运行快速,并根据严重性提供输出。建议定期在所有集群上运行这些工具,以确定集群及其部署资源的安全姿态。
集群安全最佳实践
现在我们已经涵盖了集群层面的最大安全领域,这里有一个方便的安全最佳实践清单供您参考:
-
限制 etcd 访问并将访问凭据和证书存储在安全位置。
-
禁用不安全和未经身份验证的 API 端点。
-
使用提供临时动态令牌而不是静态配置令牌的身份验证提供者在 Kubeconfig 中进行配置。
-
确保用户和服务遵循最小权限原则。
-
定期轮换基础设施凭据。
-
使用密钥和证书对静止和传输中的敏感数据进行加密。
-
在部署到集群之前,扫描容器镜像以检测漏洞和恶意软件。
-
启用审计日志记录和监控以检测和响应可疑活动。
-
使用安全扫描工具如 Kubescape 来基准您的 Kubernetes 集群和工作负载的安全姿态。
工作负载容器安全性
现在我们已经讨论了集群安全的核心组件,我们将看看工作负载层面的安全机制。Kubernetes 提供了许多专注于安全的 API,通过相同的工具可以简化配置和部署工作负载。
Pod 安全准入
Pod 安全准入是您工作负载安全性的重要组成部分,允许您配置和管理 pod 配置中所有安全敏感组件,并在命名空间或集群级别应用开箱即用的最佳实践。专门有关容器和 pod 安全的详细内容,请查阅第十章。
Seccomp、AppArmor 和 SELinux
Linux 提供了几种不同的安全机制,可以与 Kubernetes 结合使用,增强运行在 Kubernetes 上的工作负载的安全性。Seccomp 允许创建系统调用过滤配置文件,用于限制容器发出的系统调用。不幸的是,Kubernetes 社区对 Seccomp 配置的讨论不足,或者根本没有进行配置,或者配置错误,允许容器访问可能用于恶意目的的系统调用。Kubernetes 社区开发了一个称为安全配置操作器的优秀工具,简化了配置 Seccomp 配置文件的管理开销。从安全角度来看,Seccomp 是一个低成本的配置项,强烈建议您至少启用 Seccomp 的默认配置文件。
AppArmor 和 SELinux 是 Linux 内核安全模块,允许对每个容器进行精细化的强制访问控制配置。这些模块允许集群管理员对容器的操作权限进行细粒度控制。结合 Pod 安全准入和这些 Linux 安全机制,您可以控制容器对操作系统的访问级别。
准入控制器
准入控制器是确保工作负载安全的关键部分。Kubernetes 提供了一组集成的准入控制器,并且所有与安全相关的准入控制器都默认启用。例如,NodeRestriction 准入控制器限制了 Kubelet 只能修改分配给特定节点的 pod 的权限。准入控制器是一个重要主题,建议您查看第十七章获取更多详细信息。
操作器
运算符是使用 Kubernetes API 提供自定义资源来支持特定工作负载的控制器,这些工作负载需要应用程序特定的知识。如果您想了解更多关于运算符模式的信息,请参考第二十一章,我们在其中详细介绍了如何实现运算符。在安全的背景下,不幸的是,许多运算符具有非常宽松的 RBAC 配置,以便于使用。许多运算符授予集群管理员或等效权限,这可能作为攻击向量。此外,尽管较少见,这些运算符可能直接暴露其他 API,这可能提供特权升级的路径。
网络策略
Kubernetes 提供了网络策略资源;但是,您需要确保您的网络提供商在运行时实现了该资源。有关网络安全的更多详细信息,请参考第九章。Kubernetes 网络策略可以对允许进入或退出服务或命名空间的网络流量进行细粒度控制,适用于集群内外的资源。网络策略还允许集群管理员创建集群范围或命名空间特定的策略,并将应用程序特定的网络策略委托给应用程序开发人员。网络策略仅涵盖 IP 地址和 TCP/UDP 端口,而不涉及特定的 HTTP 流量或端点路由访问控制。如果您需要应用程序特定的访问策略,服务网格提供了高级别的访问策略,这些策略不是 Kubernetes 集成 API 的一部分。
运行时安全
大多数 Kubernetes 集群默认使用诸如containerd或CRI-O等容器运行时,它们在底层使用 Linux cgroups 提供轻量级沙箱以支持容器运行时。对于一些安全敏感的工作负载,这些安全保证可能不足够。存在一系列不同的容器运行时,包括Kata containers和gvisor,它们提供不同的安全配置以满足工作负载的需求。Kubernetes 支持在同一集群中使用多个容器运行时,使用 pod 规范中的 RuntimeClass 字段。请参考第十章以获取有关 RuntimeClass 的更多详细信息。如果您仍然需要更高级别的安全性,则机密容器可能也是需要考虑的选项。机密容器利用受信执行环境,这是在 CPU 上运行工作负载的安全区域。
类似 Kubernetes 控制平面上的审计日志,您还应该投资于容器运行时内部的审计日志记录。像 Falco 这样的工具提供了一种启用容器运行时内应用程序可以执行的审计日志记录和策略的方式。了解容器运行时的情况使您能够尽可能接近源头地监视和捕获恶意行为。
工作负载容器安全最佳实践
Kubernetes 为您提供了丰富的安全工具集,可以帮助您几乎无法理解。这里是一些快速提高集群上运行负载安全姿态的最佳实践的简短列表:
-
使用 Node 和 RBAC 授权器以及 NodeRestriction 准入插件的组合。
-
使用强身份验证和授权机制保护集群控制平面。
-
检查操作员 API 权限,并确保它们遵循最小特权原则。
-
应用最小特权原则来限制用户、pod 和服务账号的访问和权限。
-
实施网络策略来限制 pod 和命名空间之间的流量。
-
确保启用推荐的基于安全的准入控制器集。
-
使用 Seccomp、AppArmor 和 SELinux 来减少容器运行时能够访问的 Linux 内核攻击面。
-
确保动态 Webhook 准入控制器的安全配置,仅限于它们需要验证/变更的资源,并遵循最小特权 RBAC。
-
在您的集群上提供不同的容器运行时沙盒,并使用
RuntimeClass允许应用程序开发人员选择与安全要求匹配的运行时。 -
使用准入控制器验证应用程序工作负载的安全最佳实践。
代码安全性
良好的安全性在代码进入 Kubernetes 之前就开始了。我们将介绍一些不同的工具和技术,您可以引入来进一步改善安全姿态。
非根和无分发容器
在构建容器时有两个快速的胜利点可以提高安全姿态。通过在容器构建文件中指定非根用户来配置应用程序进程不以 root 用户身份运行。Kubernetes 允许在 pod 规范的 securityContext 部分设置此项。这可用作故障安全机制;但优先考虑在容器构建文件中配置。此外,许多基础容器预安装了常用软件包,这些软件包可能未被使用并可能引入漏洞。像 distroless 和 scratch 容器提供了可能的最小基础容器映像,再次减少了攻击面。
容器漏洞扫描
许多开源工具提供容器镜像的漏洞扫描。这些工具,如Trivy,易于使用,并可以提供容器镜像中漏洞的基线。然后根据这些结果决定是否部署容器。然而,这些工具可能会产生很多噪音并提供不一致的结果。许多容器仓库提供商提供集成的漏洞扫描,某些准入控制器会根据镜像中存在的漏洞来允许或拒绝工作负载的部署。
代码仓库安全
源代码仓库是改善安全性的另一个良好场所,幸运的是有工具和指导可帮助改善此层的安全姿态。
软件工件的供应链级别,或 SLSA,是一个基于递增级别的控制清单框架,可帮助提高软件安全性和完整性。许多开源项目正在采纳 SLSA 以改善软件安全性。这些级别定义明确,实施后可以提升源代码的安全姿态。
OpenSSF 评分卡 提供一组自动化工具,为您可能使用或考虑作为依赖项的开源仓库提供安全姿态的 0 到 10 分评分。综合评分提供了一目了然的视图,可用于评估开源项目的信任度。许多知名开源项目正在采用此评分卡。
代码安全最佳实践
良好的安全性始于容器部署到 Kubernetes 集群之前。代码仓库也是实施安全措施来构建深入安全策略的好地方。以下是一些最佳实践,可帮助您在代码安全性前线上快速取得成功:
-
检查操作者 API 权限,并确保其遵循最小权限原则。
-
配置容器构建文件以非根用户身份运行应用程序进程。
-
使用像 scratch 和 distroless 这样的容器基础镜像。
-
对容器进行漏洞扫描,并根据这些漏洞实施策略,决定是否允许部署容器。
-
查看您依赖的开源项目的 OpenSSF 评分卡。
-
实施 SLSA Level 1 以为您的软件提供基线级别的透明度和完整性。
概述
在本章中我们覆盖了大量内容。理解如何全面保护 Kubernetes 是非常重要的,这样你可以将问题分解成更小的部分来实施。安全是一个旅程而不是一个目的地。它将一直是一个动态的目标,通过遵循这些最佳实践,您可以提高 Kubernetes 集群的安全性并减少数据泄露或妥协的风险。
第二十章:混沌测试、负载测试和实验
本章涵盖了在你的 Kubernetes 集群中测试应用程序的三种不同方法:混沌测试、负载测试和实验。所有这些工具都可以帮助你构建更有用、更具弹性和更高性能的应用程序。它们还可以为你的应用程序提供洞察,并帮助你更好地理解你的用户,并在广泛推出变更之前预测其影响。这些洞察力使你能够做出更好的决策,并识别未来改进的领域。接下来的几节将描述每种测试类型的详细信息、其目标以及开始每项测试之前所需的先决条件。
混沌测试
混沌测试,顾名思义,是测试你的应用程序对世界混乱的响应能力。但是混沌究竟意味着什么呢?广义上来说,对于一个应用程序而言,混沌意味着引入不寻常但并非完全意外的边缘条件,观察应用程序如何响应。这使你能够了解你的应用程序是否能够对这些边缘条件具有弹性,这些条件可能在应用程序开发过程中之前从未发生过,但在应用程序运行期间某个时刻可能会发生。通常情况下,我们的应用程序开发都在理想化的条件下进行。不幸的是,长时间暴露在真实世界中时,这些理想化条件会面临在初步开发阶段不存在的错误和故障。这些错误包括通信错误、网络断开、存储问题以及应用程序崩溃和失败。混沌测试就是在测试环境中人为地引入这些错误,观察你的应用程序如何处理它们的艺术。
混沌测试的目标
混沌测试的目标是将极端条件引入你的应用程序环境,并观察你的应用程序在这些条件下的行为,特别是它如何失败。以这种方式进行测试,似乎不寻常,因为我们预期并希望观察到失败。虽然总体上我们尽量避免应用程序的失败,但是在测试环境中观察这些失败要好得多,因为此时不会影响到客户或用户。我们希望通过混沌测试观察到失败,因为它们为我们在影响到用户或客户之前修复这些问题提供了机会。
当然,目标是在我们的应用程序中引入一个现实的错误水平,以查看它们的行为。引入一个在实践中预计不会发生的错误水平,虽然有趣,但不是时间或资源的好用途。过高的错误水平可以帮助我们加固应对极端环境的应用程序,但如果这样的极端情况从未发生,加固应用程序的努力就是浪费的。当然,每个应用程序都有其所需的不同程度的可变性和弹性水平。对于移动游戏所期望的弹性水平远低于对飞机或汽车所期望的弹性水平。了解应用程序的弹性要求和预期环境对于高质量的混沌测试是至关重要的先决条件。
混沌测试的先决条件
要构建一个有用的混沌测试,理解应用程序可能遇到的环境条件至关重要。这包括错误的预期频率以及可能发生的错误类型。例如,你的存储是否已经具备了弹性?如果你正在构建一个使用云支持的无状态应用程序,你可能不需要测试应用程序的硬盘故障,但你可能希望在与云存储解决方案的通信中引入混沌。
在开始混沌测试之前,要考虑你的应用程序中存在的风险,并确定希望引入错误的位置及其频率。在考虑频率时,请记住我们并不试图测试平均情况。平均情况已经在您现有的集成测试中得到很好的代表。相反,我们希望模拟可能仅在一年一次或十年一次发生的环境。你需要充分了解你的应用程序,以描述什么是合理的。
在了解你的应用程序方面,混沌测试的另一个重要先决条件是对你的应用程序的正确性和行为进行高质量的监控。引入混沌到你的环境中是一回事,但要使这种混沌有用,你还需要能够以足够详细的方式观察你的应用程序的运行情况,以确定混沌的影响,并识别你的应用程序需要加固的地方,以能够处理混沌。总的来说,这种监控对于任何生产应用程序都是必需的。除了在弹性方面的核心贡献之外,混沌测试还可以是一个测试,看看你的监控和日志记录是否足以处理真实的故障。
混沌测试你的应用程序通信
将混沌注入到应用程序通信中的最简单方法之一是在每个客户端和您的服务之间放置代理。该代理处理客户端和服务器之间的所有网络流量,并注入额外的延迟、断开连接或其他错误等随机故障。有几种不同的开源选项可以用作这种代理,但其中最受欢迎的之一是ToxiProxy,由 Shopify 创建。将 ToxiProxy 添加到系统中的最简单方法是在集群中的每个实际服务前有效地运行一个 ToxiProxy 层。
为此,您首先需要将要添加混沌的每个服务重命名。以更详细地查看这一点,假设您有一个名为backend的服务,该服务在端口 8080 上提供流量。您可以更新名为backend的 Kubernetes 服务为backend-real。然后,您可以使用 ToxiProxy 命令行工具创建配置如下的新 ToxiProxy Pods 部署:
toxiproxy-cli create -l 0.0.0.0:8080 -u backend-real:8080 backend
当您为 ToxiProxy 的这个 Pod 部署构建 Pod 定义时,您可以将此命令作为 PostStart 生命周期钩子运行。此命令配置 ToxiProxy 在 Pod 内的 8080 端口上监听,然后将流量转发到您的实际后端服务,该服务具有 DNS 名称backend-real。
接下来,您可以创建一个名为backend的新服务来替换您重命名的服务,并将此服务指向您刚刚创建的 ToxiProxy Pods 的部署。这样,您应用程序中与backend通信的任何客户端都会自动开始与混沌代理进行通信。
最后,您可以使用 ToxiProxy 命令行工具向应用程序添加混沌,例如发出以下命令:
kubectl exec $SomeToxiProxyPod -- toxiproxy-cli toxic add -t latency
-a latency=2000 backend
这将向通过此代理的所有流量添加 2000 毫秒的延迟。如果您在代理部署中创建多个 Pods,则需要为每个 Pod 运行此命令,或者使用脚本或代码进行自动化。
测试应用程序操作的混沌测试
除了在通信不稳定时测试应用程序的操作外,还可以考虑在基础设施运行不稳定或过载的情况下测试应用程序的运行情况。
开始基础设施故障的最简单方法是简单地删除 Pods。从单个 Deployment 开始,您可以根据其标签选择器删除 Deployment 中的随机 Pods,使用一个简单的 bash 脚本:
NAMESPACE="some-namespace"
LABEL=k8s-app=my-app
PODS=$(kubectl get pods --selector=${LABEL} -n ${NAMESPACE} --no-headers | awk
'{print $1}')
for x in $PODS; do
if [ $[ $RANDOM % 10 ] == 0 ]; then
kubectl delete pods -n $NAMESPACE $x;
fi;
done
当然,如果您更愿意拥有更完整的功能,您可以使用各种Kubernetes 客户端编写代码,甚至使用现有的开源工具如Chaos Mesh。
一旦您完成了应用程序中所有微服务部署的移动,您可以继续一次性删除不同服务中的 Pods。这会模拟更广泛的故障。您可以扩展先前的脚本,根据以下方式随机删除特定命名空间内 Deployment 的 Pods:
NAMESPACE="some-namespace"
PODS=$(kubectl get pods -n ${NAMESPACE} --no-headers | awk '{print $1}')
for x in $PODS; do
if [ $[ $RANDOM % 10 ] == 0 ]; then
kubectl delete pods -n $NAMESPACE $x;
fi;
done
最后,您可以通过导致集群中整个节点失败来模拟您基础设施中的完全故障。有多种方法可以实现这一点。如果您在基于云的 Kubernetes 上运行,您可以使用云 VM API 来关闭或重新启动集群中的机器。如果您在物理基础设施上运行,您可以直接拔掉特定机器的电源插头,或者登录并运行命令来重新启动它。在物理和虚拟硬件上,您还可以通过运行 sudo sh -c 'echo c > /proc/sysrq-trigger' 来使您的内核发生紧急情况。
下面是一个简单的脚本,它将在 Kubernetes 集群中大约随机使 10% 的机器发生紧急情况:
NODES=$(kubectl get nodes -o jsonpath='{.items[*].status.addresses[0].address}')
for x in $NODES; do
if [ $[ $RANDOM % 10 ] == 0 ]; then
ssh $x sudo sh -c 'echo c > /proc/sysrq-trigger'
fi
done
对应用程序进行模糊测试以确保安全性和弹性
与混沌测试精神相同的最后一种测试类型是模糊测试。模糊测试与混沌测试类似,因为它引入了随机性和混乱到您的应用程序中,但不是引入故障,而是专注于引入在某种方式上技术上合法但极端的输入。例如,您可以向端点发送一个合法的 JSON 请求,但包含重复字段或特别长或包含随机值的数据。模糊测试的目标是测试您的应用程序对随机极端或恶意输入的弹性。模糊测试最常用于安全测试的背景中,因为随机输入可能导致执行意外代码路径并引入漏洞或崩溃。模糊测试可以帮助您确保您的应用程序对来自恶意或错误输入的混沌具有弹性,除了环境中的故障。模糊测试可以在集群服务级别和单元测试级别都添加。
总结
混沌测试是在应用程序运行时引入意外但不是不可能的条件的艺术,并观察发生了什么。在任何故障影响实际应用程序使用之前,将潜在错误和故障引入环境有助于您在它们变得关键之前识别问题区域。
负载测试
负载测试用于确定您的应用程序在负载下的行为。使用负载测试工具生成等同于您的应用程序真实生产使用的实际应用流量的真实应用流量。这种流量可以是人工生成的,也可以是从实际生产流量中记录并重放的流量。负载测试可用于识别未来可能成为问题的领域,或者确保新代码和功能不会引起退步。
负载测试的目标
负载测试的核心目标是了解你的应用在负载下的行为。当你构建一个应用时,通常只有少数用户偶尔访问。这些流量足以理解应用的正确性,但无法帮助我们了解应用在实际负载下的表现。因此,为了了解你的应用在生产环境中的运行情况,负载测试是必要的。
负载测试的两个基本用途是估算当前容量和预防回归。预防回归是利用负载测试确保新版本软件可以与之前版本承受相同负载的使用。每次我们发布软件的新版本时,都会有新的代码和配置(如果没有,那发布的意义何在?)。虽然这些代码变更引入了新功能并修复了错误,但它们也可能引入性能退化:新版本无法像旧版本那样提供相同水平的负载。当然,有时这些性能退化是已知且预期的;例如,新功能可能使计算变得更复杂,因此更慢,但即使在这种情况下,负载测试也是必要的,以确定基础设施(例如 pod 数量、它们需要的资源)需要如何扩展以支持生产流量。
与回归预防相比,后者用于捕获新引入到应用程序中的问题,预测性负载测试用于在问题发生之前预测。对于许多服务来说,服务的使用是稳定增长的。每个月都会有更多的用户和对你的服务的请求。总体而言,这是一件好事,但是要保持这些用户的满意意味着不断改进基础设施以跟上新的负载。预测性负载测试利用应用程序的历史增长趋势,并使用它们来测试你的应用程序,仿佛它在未来运行一样。例如,如果你的应用程序流量每个月增长 10%,你可以在当前峰值流量的 110%处运行预测性负载测试,模拟你的应用程序在下个月的工作情况。虽然扩展你的应用程序可能只需添加更多的副本和资源,但通常应用程序中的基本瓶颈需要重新架构。预测性负载测试允许你预见未来并进行这些更改,而不会因负载增加而导致用户面临的紧急故障。
预测性负载测试也可以用于预测应用在发布前的行为。与使用历史信息不同,你可以使用对发布时使用情况的预测来确保发布成功而不是灾难。
负载测试的先决条件
负载测试用于确保您的应用程序在承受重大负载时仍能正常运行。此外,与混沌测试类似,负载测试也可能因负载而在您的应用程序中引入故障条件。因此,负载测试与应用程序的可观察性具有相同的先决条件。要成功使用负载测试,您需要验证应用程序是否正常运行,并且有足够的信息来了解故障发生的位置和原因(如果有的话)。
除了应用程序的核心可观察性之外,负载测试的另一个关键先决条件是能够为您的测试生成真实的负载。如果您的负载测试不能紧密模拟真实世界用户的行为,那么它将毫无用处。具体来说,想象一下,如果您的负载测试不断重复向单个用户发出请求,那么在许多应用程序中,这样的流量将导致不现实的缓存命中率,并且您的负载测试将似乎展示出一种在更真实的流量下是不可能实现的处理大量负载的能力。
生成真实流量
为了为您的应用程序生成真实世界的流量模式,方法会因应用程序的类型而异。例如,对于某些更多只读操作的网站,比如新闻网站,仅仅使用某种概率分布重复访问每个不同页面可能就足够了。但是对于许多应用程序,特别是涉及读写操作的应用程序,生成真实的负载测试的唯一方法是记录真实世界的流量并回放。其中一种最简单的方法是将每个 HTTP 请求的完整细节写入文件,然后稍后重新发送这些请求到服务器。
不幸的是,这种方法可能会带来复杂性。将所有请求记录到应用程序可能涉及用户隐私和安全的首要后果。在许多情况下,向应用程序发出的请求同时包含私人信息和安全令牌。如果将所有这些信息记录到文件以供回放,则必须非常小心地处理这些文件,以确保尊重用户的隐私和安全性。
记录并回放实际用户请求的另一个挑战与请求本身的及时性有关。例如,如果请求包含了关于最新新闻事件的搜索查询,这些请求在几周(或几个月)后的行为将会非常不同。旧新闻相关的消息会大大减少。及时性还会影响应用程序的正确行为。请求通常包含安全令牌,如果您在正确处理安全性,这些令牌的生命周期很短。这意味着验证时记录的令牌可能无法正确工作。
最后,当请求向后端存储系统写入数据时,修改存储的请求必须在生产存储基础设施的副本或快照中执行。如果在设置这一点时不小心,可能会导致客户数据的重大问题。
出于所有这些原因,简单地记录和回放请求虽然简单,但不是最佳实践。相反,更有用的使用请求的方式是建立服务使用方式的模型。有多少读请求?针对哪些资源?有多少写入操作?使用这个模型,您可以生成具有现实特征的合成负载。
测试您的应用程序负载
一旦生成了用于驱动负载测试的请求,将这种负载应用到您的服务就是一件简单的事情。不幸的是,实际应用中很少会这么简单。在大多数实际应用中,涉及到数据库和其他存储系统。为了正确模拟承受负载的应用程序,您还需要写入存储系统,但不能写入生产数据存储,因为这是人为负载。因此,要正确进行应用程序负载测试,您需要能够启动一个真实的应用程序副本及其所有依赖关系。
一旦您的应用克隆运行起来,发送所有请求就成了问题。事实证明,大规模负载测试也是分布式系统问题。您将希望使用大量不同的 Pod 来向您的应用程序发送负载。这样可以通过负载均衡器均匀分发请求,并使得发送超过单个 Pod 网络支持负载成为可能。您需要做出的选择之一是是否将这些负载测试 Pod 运行在与您的应用程序相同的集群中,还是在单独的集群中运行。在同一集群内运行 Pod 最大化了您可以发送到应用程序的负载,但它确实会对从互联网上引入流量到您的应用程序的边缘负载均衡器进行测试。根据您希望测试的应用程序的哪些部分,您可能希望在集群内运行负载,集群外运行,或者两者兼而有之。
在 Kubernetes 中运行分布式负载测试的两个流行工具是JMeter和Locust。两者都提供了描述要发送到您的服务的负载的方式,并允许您部署分布式负载测试机器人到 Kubernetes 中。
使用负载测试调整您的应用程序
除了使用负载测试来防止性能回归和预测未来的性能问题外,负载测试还可以用于优化您的应用程序的资源利用率。对于任何给定的服务,可以调整多个变量,并且可以影响系统性能。对于本讨论的目的,我们考虑了三个变量:Pod 的数量,核心数和内存。
起初,似乎一个应用在相同数量的副本乘以核心数的情况下表现相同。也就是说,一个每个有三个核心的五个 pod 的应用与一个每个有五个核心的三个 pod 的应用表现相同。在某些情况下,这是正确的,但在许多情况下,情况并非如此;服务的具体细节和其瓶颈的位置通常会导致行为上的差异,这些差异很难预料。例如,使用 Java、dotnet 或 Go 等语言构建的应用程序提供垃圾收集:如果只有一两个核心,应用程序会对垃圾收集器进行显着不同的调整,而如果有多个核心,则会有所不同。
同样适用于内存。更多的内存意味着可以将更多内容保留在缓存中,这通常会带来更好的性能,但这种好处有一个渐近限制。您不能简单地为服务抛出更多内存,并期望它继续提高性能。
通常,了解你的应用在不同配置下的行为方式的唯一方法是实际进行实验。要做到这一点,你可以设置一组实验性配置,其中包含不同的 pod、核心和内存值,并对每个配置运行负载测试。通过这些实验的数据,你通常可以识别出行为模式,这些模式可以洞察系统性能的特定细节,并且你可以使用结果来选择最高效的服务配置。
总结
性能是构建令用户满意的应用程序的关键部分。负载测试确保您不会引入影响性能并导致用户体验不佳的退化。负载测试还可以作为一台时光机,让您可以想象应用程序在未来的行为,并对架构进行更改以支持额外的增长。负载测试还可以帮助您理解和优化资源使用,降低成本并提高效率。
实验
与混沌测试和负载测试相比,实验不是用于发现服务架构和操作中的问题,而是用于识别改进用户如何使用您的服务的方式。实验是对您的服务的长期更改,通常涉及用户体验,在这种更改中,一小部分用户(例如,所有流量的 1%)将获得稍有不同的体验。通过检查控制组(没有更改的组)和实验组(经历了不同体验的组)之间的差异,您可以理解更改的影响,并决定是否继续进行实验或广泛推广更改。
实验的目标
当我们构建一个服务时,我们构建它是有一个目标的。那个目标往往是为用户或客户提供一个有用、易于使用和令人愉悦的东西。但我们怎么知道我们是否实现了这个目标呢?我们很容易看到我们的网站在混乱环境中会崩溃,或者在负载量较小时会失败,但了解用户体验我们服务的方式可能会比较棘手。
几种传统的了解用户体验的方法包括调查,通过这些调查您可以询问用户对当前服务的感受。虽然这在理解我们服务的当前表现方面很有用,但要使用调查来预测未来变化的影响则更加困难。就像性能回归一样,最好在变更完全部署之前就知道影响。这是任何实验的主要目标:以最小影响学习我们用户体验。
实验的先决条件
就像我们小时候在科学展览会上一样,每一个好的实验都始于一个好的假设,这也是我们服务实验的一个自然前提。我们考虑要进行某种变化,我们需要猜测它对用户体验的影响。
当然,为了理解用户体验的影响,我们还需要能够衡量用户体验。这些数据可以通过之前提到的调查来获取,通过这些调查可以收集到像满意度(“请您评价我们一到五分”)或净推荐值(“您多有可能向朋友推荐此服务?”)等指标。或者可以从与用户行为相关的被动指标中获取(“他们在我们网站上花费了多少时间?”或“他们点击了多少个页面?”等)。
一旦您有了假设和衡量用户体验的方法,您就可以开始实验了。
实验设置
有两种不同的方式可以设置实验。您采取的方法取决于正在测试的具体内容。您可以在单个服务中包含多种可能的体验,或者部署两个服务副本并使用服务网格来在它们之间分配流量。
第一种方法是将代码的两个版本都提交到您的发布二进制文件中,并使用服务收到请求的某些属性来在实验组和对照组之间进行切换。您可以使用 HTTP 头部、cookie 或查询参数来让用户明确选择参与实验。或者您可以使用请求的特征,如源 IP,随机选择用户进行实验。例如,您可以选择 IP 地址以某个数字结尾的用户进行实验。
实施实验的常见方式是使用显式功能标志,用户通过提供打开实验的查询参数或 Cookie 来选择参与实验。这是允许特定客户尝试新功能或演示新功能而不广泛发布的好方法。功能标志还可以在不稳定情况下快速启用或禁用功能。许多开源项目,例如 Flagger,可以用于实施功能标志。
将实验放置在与控制代码相同的二进制文件中的好处在于,将其推广到生产环境是最简单的,但这种简单性也带来了两个缺点。第一个是,如果实验代码不稳定并且崩溃,它也可能影响您的生产流量。另一个是,因为任何更改都与服务的完整发布相关联,因此更新实验或推出新实验的速度要慢得多。
实验的第二种方法是部署两个(或更多)不同版本的服务。在这种方法中,您有控制生产服务接收大部分流量,以及一个单独的实验部署服务,只接收部分流量。您可以使用服务网格(在 第九章 中描述)将小部分流量路由到此实验部署,而不是生产部署。尽管这种方法在实施上更复杂,但比将实验代码包含在生产二进制文件中要更灵活和更健壮。因为它需要完全新的代码部署,设置实验的前期成本增加了,但因为它只影响实验流量,所以您可以随时轻松地部署新版本的实验(甚至多个版本的实验),而不影响大部分流量。
此外,由于服务网格可以衡量请求是否成功,如果实验代码开始失败,可以迅速将其从使用中移除,并最小化用户影响。当然,检测这些失败可能是一个挑战。您需要确保实验基础设施独立监控,而不是与标准生产监控混为一谈;否则,实验失败可能会在当前生产基础设施处理的成功请求中丢失。理想情况下,Pod 的名称或部署应提供足够的上下文,以确定监控信号是来自生产还是实验。
通常来说,使用单独的部署以及某种流量路由器如服务网格是进行实验的最佳实践,但这需要大量的基础设施来设置。对于你的初步实验,或者如果你是一个已经相当敏捷的小团队,可能直接提交实验性代码是最简便的实验和迭代路径。
总结
实验能让你在将更改推广到广泛用户群之前了解这些变化对用户体验的影响。实验在帮助我们迅速了解可行的变更及如何更新我们的服务以更好地为用户服务方面起到关键作用。实验使得改进我们的服务变得更加容易、快速和安全。
混沌测试、负载测试和实验总结
在本章中,我们涵盖了多种学习服务更加弹性、性能更好和更有用的不同方式。正如使用单元测试来测试你的代码是软件开发过程的关键部分一样,使用混沌、负载和实验来测试你的服务是服务设计和运营的关键部分。
第二十一章:实施操作员
Kubernetes 的一个关键原则是它能够通过系统的操作员扩展到核心 API 之外。许多人(包括本作者)认为这种可扩展性是 Kubernetes 在市场上占主导地位的推动因素。随着开发人员开始创建能够在 Kubernetes 上运行的应用程序,操作员开发了辅助应用程序,这些应用程序知道如何调用 Kubernetes API 并自动化大部分维持应用程序稳定所需的常规工作。许多这些应用程序是 bash 脚本或在集群中运行的辅助容器。
2016 年,由 CoreOS(现为 Red Hat)领导的关键 Kubernetes 贡献者小组提出了操作员模式,以便更轻松地开发和实施 Kubernetes 应用程序。操作员模式概述了一种打包、部署和维护与 Kubernetes API 和客户端工具(如kubectl)集成的应用程序的方式。通过操作员,应用程序开发人员可以本地创建能够在 Kubernetes 中运行的应用程序,与现有的 Kubernetes 流程集成,并嵌入机构知识。这种知识不仅限于部署应用程序,还允许在复杂系统中实现平滑升级、跨不同服务的对账、自定义缩放过程以及嵌入可观察性,这推动了框架在 Kubernetes 生态系统中的接受。
本章的目标不是教你如何编写操作员。关于这个主题有许多资源可供参考,涵盖比单独一章更深入的内容。这里的目标是介绍概念,并解释何时以及为何将操作员实施到你的环境中,同时分享一些需要计划的关键考虑因素。
操作员的关键组成部分
操作员框架本身是一个开源工具包,具有明确定义的软件开发工具包(SDK)、生命周期管理和发布工具。一些项目围绕操作员模式的概念构建,使社区开发更加简便。Kubernetes 社区的 API Machinery SIG 成员赞助开发了 kubebuilder,提供了与操作员的两个主要组件(自定义资源定义(CRDs)和控制器)配合使用的基础 SDK。作为社区的一部分,由 Google 赞助,kubebuilder 正被定位为所有操作员和其他项目(如 KUDO、KubeOps 和 Kopf)的基础 SDK。本章的示例将基于 kubebuilder 语法进行讨论,但在现有的许多操作员 SDK 中,概念非常相似。
自定义资源定义
在实际操作中,仅使用本机 Kubernetes 资源定义复杂应用程序依赖和资源通常具有挑战性。平台工程师通常必须构建复杂的 yaml 模板,并使用渲染流水线和额外的资源如 jobs 和 init containers 来管理运行大型应用程序所需的大部分定制。但是,自定义资源定义允许开发人员扩展 Kubernetes API 以提供新的资源类型,从而更好地声明应用程序的资源需求。
Kubernetes 允许使用 CustomResourceDefinition 接口动态注册新资源,并将根据您指定的版本自动注册新的 RESTful 资源路径。与许多内置于 Kubernetes 中的资源不同,CRD 可以独立维护并根据需要进行更新。CRD 将在 spec 字段下定义资源的规范,并将使用 spec.scope 定义从 CRD 创建的自定义资源是命名空间范围的还是集群范围的。在我们看到 CRD 及其自定义资源实现之前,了解 Kubernetes API 的命名规范是很重要的。
Kubernetes API 对象、资源、版本、组和类型
Kubernetes 中的对象是在系统中持久保存以表示集群状态的实际实体。对象本身是集群内标准 CRUD 操作的操作对象。实质上,对象将是在状态中的整个资源定义,如 Pod 或 PersistentVolume。
Kubernetes 资源是 API 中表示特定类型对象集合的端点。因此,一个 Pod 资源将包含一组 Pod 对象。可以在集群中轻松看到这一点:
kubectl api-versions
NAME SHORTNAMES APIVERSION NAMESPACED KIND
bindings v1 true Binding
componentstatu... cs v1 false ComponentS...
configmaps cm v1 true ConfigMap
edited for space
mutatingwebhoo... admissionregistration... false MutatingWe...
validatingwebh... admissionregistration... false Validating...
customresource... crd,crds apiextensions.k8s.io/... false CustomReso...
apiservices apiregistration.k8s.i... false APIService
controllerrevi... apps/v1 true Controller...
daemonsets ds apps/v1 true DaemonSet
deployments deploy apps/v1 true Deployment
replicasets rs apps/v1 true ReplicaSet
statefulsets sts apps/v1 true StatefulSet
组将具有类似关注点的对象聚合在一起。此组合结合版本控制允许单独管理同一组内的对象并根据需要进行更新。该组在对象的 apiVersion 字段中以 RESTful 路径表示。在 Kubernetes 中,核心组(也称为遗留组)将位于 /api/REST 路径下。通常在 Pod 规范或 Deployment yaml 中的 apiVersion 字段中可以看到基本路径被移除,例如:
kind: Deployment
apiVersion: apps/v1
metadata:
name: sample
spec:
selector:
matchLabels:
就像任何良好的 API 一样,Kubernetes 的 API 是经过版本化处理的,并支持使用不同的 API 路径来支持多个版本。在 Kubernetes 版本控制方面有指导原则,自定义资源的版本化应使用相同的指导原则。API 也可以根据其支持或稳定性划分为不同级别,因此您通常会看到 Alpha、Beta 或 Stable API。例如,在集群中,您可能会看到相同组的 v1 和 v1beta1 版本:
kubectl api-versions
---- excerpt
autoscaling/v1
autoscaling/v2
autoscaling/v2beta1
autoscaling/v2beta2
通常,Kind 和 Resource 在同一语境中使用;然而,资源是 Kind 的具体实现。通常存在直接的 Kind 到 Resource 的关系,例如在定义kind: Pod规范时,将在集群中创建一个 Pod 资源。偶尔也会有一对多的关系,例如Scale kind,可以由不同的资源如Deployment或ReplicaSet返回。这称为子资源。
将这些原则结合起来,我们可以开始为客户资源建模我们的 API。在本章的其余部分,将使用 kubebuilder 生成的片段,但实际代码并不重要,只是部分表述。重点将放在讨论的概念及其与实现 Operator 时的最佳实践之间的关系。
创建我们的 API
可以手动创建 YAML 格式的客户资源定义;然而,kubebuilder 和其他 Operator SDK 会根据提供的代码自动为您生成 API 定义。在 kubebuilder 中,您可以在项目初始化后创建 API 的脚手架和所需的 Go 代码。要初始化项目,一旦满足 kubebuilder 及其先决条件,您可以从新目录运行init命令,其中将包含您的项目文件:
$ kubebuilder init --domain platform.evillgenius.com
--repo platform.evillgenius.com/platformapp --project-name=pe-app
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.14.1
go: downloading sigs.k8s.io/controller-runtime v0.14.1
go: downloading k8s.io/apimachinery v0.26.0
....................................................... removed for brevity ...
Update dependencies:
$ go mod tidy
go: downloading github.com/go-logr/zapr v1.2.3
go: downloading go.uber.org/zap v1.24.0
go: downloading github.com/onsi/ginkgo/v2 v2.6.0
go: downloading github.com/onsi/gomega v1.24.1
go: downloading gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f
go: downloading github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e
Next: define a resource with:
$ kubebuilder create api
这将创建一些基本文件和占位符样板代码:
$ tree
.
├── config
│ ├── default
│ │ ├── kustomization.yaml
│ │ ├── manager_auth_proxy_patch.yaml
│ │ └── manager_config_patch.yaml
│ ├── manager
│ │ ├── kustomization.yaml
│ │ └── manager.yaml
│ ├── prometheus
│ │ ├── kustomization.yaml
│ │ └── monitor.yaml
│ └── rbac
│ ├── auth_proxy_client_clusterrole.yaml
│ ├── auth_proxy_role_binding.yaml
│ ├── auth_proxy_role.yaml
│ ├── auth_proxy_service.yaml
│ ├── kustomization.yaml
│ ├── leader_election_role_binding.yaml
│ ├── leader_election_role.yaml
│ ├── role_binding.yaml
│ └── service_account.yaml
├── Dockerfile
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
├── main.go
├── Makefile
├── PROJECT
└── README.md
完成后,您可以通过运行以下命令创建 API 定义的脚手架:
$ kubebuilder create api --group egplatform --version v1alpha1 --kind EGApp
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1alpha1/egapp_types.go
controllers/egapp_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /home/eddiejv/dev/projects/operators/platformapp/bin
test -s /home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen
&& /home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen
--version | grep -q v0.11.1 || \
GOBIN=/home/eddiejv/dev/projects/operators/platformapp/bin
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.1
go: downloading sigs.k8s.io/controller-tools v0.11.1
go: downloading github.com/spf13/cobra v1.6.1
go: downloading github.com/gobuffalo/flect v0.3.0
go: downloading golang.org/x/tools v0.4.0
go: downloading k8s.io/utils v0.0.0-20221107191617-1a15be271d1d
go: downloading github.com/mattn/go-colorable v0.1.9
/home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen
object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
这将添加一个 API、bin 和 controllers 目录,并更新其他目录以包含更多样板代码。主要工作的两个文件是 api/
要开始修改您的 API 以映射到您在 CRD 中希望表达的资源,您需要在 api/
type EGAppSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Foo is an example field of EGApp. Edit egapp_types.go to remove/update
Foo string `json:"foo,omitempty"`
}
// EGAppStatus defines the observed state of EGApp
// +kubebuilder:subresource:status
type EGAppStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
注意
如果您不是 Go 程序员,不用担心,因为还有其他项目如 Java Operator SDK 和 Kopf,可以帮助您用 Java 或 Python 构建 Operator。Operator Framework SDK 还支持从 Ansible 或 Helm 创建 Operator。
继续上面的示例,我们希望向规范添加特定字段,并且还有一个状态。要更新规范,可以添加如下信息:
type EGAppSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// AppId is the unique AppId match to internal catalog systems
AppId string `json:"appId,omitempty"`
// +kubebuilder:validation:Enum=java;python;go
Framework string `json:"framework"`
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Enum=lowMem;highMem;highCPU;balanced
// +kubebuilder:default="lowMem"
InstanceType string `json:"instanceType"`
// +kubebuilder:validation:Enum=dev;stage;prod
Environment string `json:"environment"`
// +kubebuilder:validation:Optional
// +kubebuilder:default:=1
ReplicaCount int32 `json:"replicaCount"`
}
// EGAppStatus defines the observed state of EGApp
// +kubebuilder:subresource:status
type EGAppStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Pods []string `json:"pods"`
}
这里重要的是,我们将定义应用程序中的信息映射到数据类型中,并给它们提供 JSON 表示。看起来像是注释的 // +kubebuilder: 行是 kubebuilder 使用的标记注释,根据提供的信息生成代码。例如,我们声明给 kubebuilder 生成所有必要的代码,以确保 Framework 字段根据 Java、Python 或 Go 的三个可能字符串进行验证。这就是为什么在 kubebuilder create api 命令的末尾指出,对 API 所做的任何更改都需要 make generate 来更新所有其他所需的生成代码,以及 make manifests 来更新所有 yaml 清单的样板代码。这将为您提供一个起始的 CRD,看起来类似于这样:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: egapps.egplatform.platform.evillgenius.com
spec:
group: egplatform.platform.evillgenius.com
names:
kind: EGApp
listKind: EGAppList
plural: egapps
singular: egapp
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: EGApp is the Schema for the egapps API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this
representation of an object. Servers should convert recognized
schemas to the latest internal value, and may reject unrecognized
values. More info: https://git.k8s.io/community/contributors/
devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource
this object represents. Servers may infer this from the endpoint
the client submits requests to. Cannot be updated. In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/
sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: EGAppSpec defines the desired state of EGApp
properties:
appId:
description: Foo is an example field of EGApp. Edit
egapp_types.go to remove/update
type: string
environment:
enum:
- dev
- stage
- prod
type: string
framework:
enum:
- java
- python
- go
type: string
instanceType:
default: lowMem
enum:
- lowMem
- highMem
- highCPU
- balanced
type: string
replicaCount:
default: 1
format: int32
type: integer
required:
- environment
- framework
type: object
status:
description: EGAppStatus defines the observed state of EGApp
properties:
pods:
items:
type: string
type: array
required:
- pods
type: object
type: object
served: true
storage: true
subresources:
status: {}
你会注意到,kubebuilder 还添加了 OpenAPI 验证信息,因此 CR 可以根据 CRD 的要求进行验证。 Kubebuilder 还允许通过 webhook 使用逻辑创建额外的验证器。通过实现 Defaulter 和/或 Validator 接口,kubebuilder 提供了代码生成来创建 webhook 服务器,并将其注册到控制器管理器中。使用 kubebuilder CLI 可以轻松地再次生成此脚手架:
$ kubebuilder create webhook --group egplatform --version v1alpha1 --kind EGApp
--defaulting --programmatic-validation
我们可以使用 kubebuilder 轻松部署这个自定义资源:
$ make install
test -s /home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen &&
/home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen --version |
grep -q v0.11.1 || \
GOBIN=/home/eddiejv/dev/projects/operators/platformapp/bin go install
sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.1/home/eddiejv/dev/
projects/operators/platformapp/bin/controller-gen rbac:roleName=manager-role
crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/eddiejv/dev/projects/operators/platformapp/bin/kustomize build config/crd
| kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/
egapps.egplatform.platform.evillgenius.com created
然后我们可以从 kubectl 命令中看到,egapp 资源现在已安装在集群中,并且我们可以看到资源本身的结构:
$ kubectl explain egapp --recursive
KIND: EGApp
VERSION: egplatform.platform.evillgenius.com/v1alpha1
DESCRIPTION:
EGApp is the Schema for the egapps API
FIELDS:
apiVersion <string>
kind <string>
metadata <Object>
annotations <map[string]string>
creationTimestamp <string>
deletionGracePeriodSeconds <integer>
deletionTimestamp <string>
finalizers <[]string>
generateName <string>
generation <integer>
labels <map[string]string>
managedFields <[]Object>
apiVersion <string>
fieldsType <string>
fieldsV1 <map[string]>
manager <string>
operation <string>
subresource <string>
time <string>
name <string>
namespace <string>
ownerReferences <[]Object>
apiVersion <string>
blockOwnerDeletion <boolean>
controller <boolean>
kind <string>
name <string>
uid <string>
resourceVersion <string>
selfLink <string>
uid <string>
spec <Object>
appId <string>
environment <string>
framework <string>
instanceType <string>
replicaCount <integer>
status <Object>
pods <[]string>
现在 API 已经在集群中创建并安装了,但它实际上什么也做不了。在这个阶段,如果我们创建一个 yaml 文件并将其部署到命名空间中,它将在 etcd 中创建一个条目,并按照 yaml 中指定的信息进行设置,但在创建控制器之前,什么也不会发生。现在让我们探索一下控制器是如何工作的。
Controller Reconciliation
当创建 API 时,控制器代码也会一并创建,并具有开始构建所需的调和逻辑所需的大部分样板代码。请注意,这里代码本身并不重要,但理解幕后发生的事情对于理解 Operator 能够做什么至关重要。控制器代码位于 controllers/Reconcile 方法是应该添加逻辑的地方。在我们深入了解之前,需要制定一个调和计划,并理解各个阶段。这在 Figure 21-1 中展示。

Figure 21-1. 操作员概览
Operator 是监视与 Operator 模式关注的资源类型相关事件的服务。当事件满足条件(在 Operator 模式中称为谓词)时,Operator 开始将期望状态调和到运行状态的过程。在调和过程中,实现任何决定如何处理状态更改的逻辑;无论具体发生了什么变化,调和周期都会处理。这被称为基于级别的触发,虽然效率较低,但非常适合像 Kubernetes 这样的复杂分布式系统。
在 Operator 中,开发者将会在Reconcile方法中编写 图 21-2 中所代表的逻辑:
-
是否存在自定义资源的实例?
-
如果需要,进行一些验证。
-
如果有效,则检查状态是否需要更改并进行更改。
如果正在删除资源,则在此处还实现了处理清理的逻辑。
如果您的 CR 实现了其他资源,而这些资源并非直接属于它,您应该实现一个Finalizer来处理这些资源的清理工作,并阻止 CR 在最终器完成其处理之前被删除。例如,这经常用于在云服务提供商上创建资源的 CR 或具有某些回收策略的 PersistentVolumes,在 PV 被视为已删除之前需要执行该策略。

图 21-2. 调和逻辑
资源验证
设计高效 Operator 的一个重要方面是验证所请求资源的有效性。正如前述的,有几种方法可以根据 API 规范验证资源;然而,重要的是在流程中构建冗余,以确保一致的行为。验证的第一层应该是在 CRD 规范中定义的 OpenAPI 验证。该验证将阻止 CR 作为资源传递到 etcd 服务器,并导致有害的后果。第二层验证应该是一个验证接入控制器实现,通过 webhook 请求检查资源是否符合 API 规范。这再次阻止资源传递到 API 服务器。在调和循环代码中添加一些验证逻辑也是一种有效的策略,但重要的是要理解,这将是针对集群状态中已存在的资源进行验证,因此需要适当的错误处理。通常,这作为一个IsValid方法来实现,该方法调用与验证 Webhook 实现相同的验证逻辑。
控制器实现
如果我们继续使用到目前为止的示例,我们的控制器的逻辑将如下所示:
// +kubebuilder:rbac:groups=egplatform.platform.evillgenius.com,
resources=egapps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=egplatform.platform.evillgenius.com,
resources=egapps/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=egplatform.platform.evillgenius.com,
resources=egapps/finalizers,verbs=update
// Reconcile is part of the main Kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the EGApp object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile
func (r *EGAppReconciler) Reconcile(ctx context.Context, req ctrl.Request)
(ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
logger := log.Log.WithValues("EGApp", req.NamespacedName)
logger.Info("EGApp Reconcile started...")
// fetch the EGApp CR instance
egApp := &egplatformv1alpha1.EGApp{}
err := r.Get(ctx, req.NamespacedName, egApp)
if err != nil {
if errors.IsNotFound(err) {
logger.Info("EGApp resource not found. Object must be deleted")
return ctrl.Result{}, nil
}
logger.Error(err, "Failed to get EGApp")
return ctrl.Result{}, nil
}
// check if the deployment already exists, if not create a new one
found := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: egApp.Name, Namespace:
egApp.Namespace}, found)
if err != nil {
dep := r.deploymentForEGApp(egApp)
logger.Info("Creating a new deployment", "Deployment.Namespace",
dep.Namespace, "Deployment.Name", dep.Name)
err = r.Create(ctx, dep)
if err != nil {
logger.Error(err, "Failed to create new deployment",
"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
} else if err != nil {
logger.Error(err, "Failed to get deployment")
return ctrl.Result{}, nil
}
// This point, we have the deployment object created
// Ensure the deployment size is same as the spec
replicas := egApp.Spec.ReplicaCount
if *found.Spec.Replicas != replicas {
found.Spec.Replicas = &replicas
err = r.Update(ctx, found)
if err != nil {
logger.Error(err, "Failed to update Deployment",
"Deployment.Namespace", found.Namespace, "Deployment.Name",
found.Name)
return ctrl.Result{}, err
}
// Spec updated return and requeue
// Requeue for any reason other than an error
return ctrl.Result{Requeue: true}, nil
}
// Update the egApp status with pod names
// List the pods for this egApp's deployment
podList := &corev1.PodList{}
listOpts := []client.ListOption{
client.InNamespace(egApp.Namespace),
client.MatchingLabels(egApp.GetLabels()),
}
if err = r.List(ctx, podList, listOpts...); err != nil {
logger.Error(err, "Failed to list pods", "egApp.Namespace",
egApp.Namespace, "egApp.Name", egApp.Name)
return ctrl.Result{}, err
}
podNames := getPodNames(podList.Items)
// Update status.Pods if needed
if !reflect.DeepEqual(podNames, egApp.Status.Pods) {
egApp.Status.Pods = podNames
err := r.Status().Update(ctx, egApp)
if err != nil {
logger.Error(err, "Failed to update egApp status")
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func (r *EGAppReconciler) deploymentForEGApp(m *egplatformv1alpha1.EGApp)
*appsv1.Deployment {
ls := m.GetLabels()
replicas := m.Spec.ReplicaCount
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: m.Name,
Namespace: m.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: ls,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: ls,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "gcr.io/kuar-demo/kuard-amd64:1", // hard-coded
here, make this dynamic
Name: m.Spec.AppId,
Ports: []corev1.ContainerPort{{
ContainerPort: 8080,
Name: "http",
}},
}},
},
},
},
}
ctrl.SetControllerReference(m, deploy, r.Scheme)
return deploy
}
// Utility function to iterate over pods and return the names slice
func getPodNames(pods []corev1.Pod) []string {
var podNames []string
for _, pod := range pods {
podNames = append(podNames, pod.Name)
}
return podNames
}
// SetupWithManager sets up the controller with the Manager.
func (r *EGAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&egplatformv1alpha1.EGApp{}).
Complete(r)
}
这些主要步骤通过协调进程实现。参考的两个重要点是:
-
自定义资源实际上创建了部署。如果未找到特定实例,则使用 CR 规范中的值创建部署所需的数据。
ctrl.SetControllerReference(m, deploy, r.scheme)是在此处通过 CR 获取部署所有权的位置。这允许资源在被删除时清理其所有权下的任何部署。 -
更新资源的状态,并列出与部署相关联的一组 pod。此更新通过在代码行
err := r.Status().Update(ctx, egApp)上创建的 CR 子资源status.pods完成。这非常重要,因为它将不会在未增加ResourceGeneration元数据字段的事件上更新我们的资源状态。通过在观察期间实现一个谓词,可以确保在无操作情况下不会重复整个循环。
一旦实现了控制器逻辑,可以使用 kubebuilder 在本地运行代码以测试其是否与先前部署到集群的 CR 规范匹配,并且在准备好投入运行时,还可以将其打包为容器并部署到集群中。
具体内容如下:
$ make run
test -s /home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen &&
/home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen --version
| grep -q v0.11.1 || \
GOBIN=/home/eddiejv/dev/projects/operators/platformapp/bin go install
sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.1
/home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen
rbac:roleName=manager-role crd webhook paths="./..."
output:crd:artifacts:config=config/crd/bases
/home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen
object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
2023-02-24T11:07:21-06:00 INFO controller-runtime.metrics Metrics server is
starting to listen {"addr": ":8080"}
2023-02-24T11:07:21-06:00 INFO setup starting manager
2023-02-24T11:07:21-06:00 INFO Starting server {"path": "/metrics", "kind":
"metrics", "addr": "[::]:8080"}
2023-02-24T11:07:21-06:00 INFO Starting server {"kind": "health probe",
"addr": "[::]:8081"}
2023-02-24T11:07:21-06:00 INFO Starting EventSource {"controller": "egapp",
"controllerGroup": "egplatform.platform.evillgenius.com", "controllerKind":
"EGApp", "source": "kind source: *v1alpha1.EGApp"}
2023-02-24T11:07:21-06:00 INFO Starting Controller {"controller": "egapp",
"controllerGroup": "egplatform.platform.evillgenius.com", "controllerKind":
"EGApp"}
2023-02-24T11:07:21-06:00 INFO Starting workers {"controller": "egapp",
"controllerGroup": "egplatform.platform.evillgenius.com", "controllerKind":
"EGApp", "worker count": 1}
日志显示控制器已启动并正在侦听事件。然后使用以下命令将 CR 部署到集群中:
apiVersion: egplatform.platform.evillgenius.com/v1alpha1
kind: EGApp
metadata:
labels:
app.Kubernetes.io/name: egapp
app.Kubernetes.io/instance: egapp-sample
app.Kubernetes.io/part-of: pe-app
app.Kubernetes.io/managed-by: kustomize
app.Kubernetes.io/created-by: pe-app
name: egapp-sample
spec:
appId: egapp-sample
framework: go
instanceType: lowMem
environment: dev
replicaCount: 2
控制器日志显示协调循环开始,并因为部署不存在而创建了部署:
2023-02-24T11:12:46-06:00 INFO EGApp Reconcile started... {"EGApp":
"default/egapp-sample"}
2023-02-24T11:12:46-06:00 INFO Creating a new deployment {"EGApp":
"default/egapp-sample", "Deployment.Namespace": "default", "Deployment.Name":
"egapp-sample"}
然后在实例上调用 kubectl delete,控制器开始另一个协调循环并删除对象:
2023-02-24T11:21:39-06:00 INFO EGApp Reconcile started... {"EGApp":
"default/egapp-sample"}
2023-02-24T11:21:39-06:00 INFO EGApp resource not found. Object must
be deleted {"EGApp": "default/egapp-sample"}
在控制器和 API 本身的上下文中可以完成更多工作,例如实现围绕清理的复杂逻辑,如调用备份、在节点之间重新平衡工作负载、使用自定义逻辑进行扩展等。这是工程师对系统行为、部署方式以及在问题发生时如何响应的深入了解的体现。运算符模式的好处就在于这个代码示例中。
运算符生命周期
开发运算符并非易事,但不必回答应用程序的所有运行问题。开发应侧重解决重要障碍,然后通过版本迭代以逐步增强运算符的能力。CoreOS 和 RedHat 团队合作制定了称为运算符能力级别的完整能力谱,概述了运算符在不同级别成熟时应解决的主要问题。这些问题包括:
基本安装
自动应用程序供应和配置管理
无缝升级
支持补丁和次要版本升级
完整生命周期
应用程序生命周期、存储生命周期(备份、故障恢复)
深入洞察
指标、警报、日志处理和工作负载分析
自动驾驶
横向/纵向扩展、自动配置调优、异常检测、调度调优
这是规划运算符生命周期的坚实框架。重点是,运算符应像任何软件一样具有定义的生命周期、产品管理、弃用策略和清晰一致的版本控制。
版本升级
在我们的示例中,我们从v1alphav1作为 CRD 支持的声明版本开始。在运算符的生命周期中,可能需要支持多个版本,这取决于 API 的阶段和稳定性。
引入新版本时,应谨慎遵循一套流程,以确保现有资源不会出现问题。自定义资源对象将需要能够由 CRD 的所有定义版本提供服务。这意味着服务的版本与状态中存储的版本可能不匹配。应在自定义资源对象上实施转换过程,以便在存储的版本和提供的版本之间进行转换。当转换涉及模式更改或自定义逻辑时,可以使用转换 Webhook 来执行所需的更新。当不需要模式或自定义逻辑时,默认使用 None 转换策略,因为只会更改 apiVersion 字段。
您将在 CRD 中添加一个转换策略字段,并将其指向监听特定资源的 Webhook。这看起来像这样:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
...
spec:
...
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: egapp-conversion
name: egapp
path: /egapp-conversion
port: 8081
caBundle: "Hf8j0Kw...<base64-encoded PEM bundle>...tLS0K"
...
运算符最佳实践
开发和持续维护运算符并不是一件小事,应该非常谨慎地考虑和规划。许多时候,使用简单的范式(如 Helm 图表、Kustomize 存储库或 Terraform 模块)来打包应用程序更容易。当需要包含特殊的调和逻辑来维护应用程序或减轻用户的负担时,运算符模式可能是有意义的。如果决定构建一个运算符,则应遵循最佳实践指南:
-
不要过载运算符来管理多个应用程序,每个控制它控制的 CRD。
-
如果您的运算符管理多个 CRD,则运算符也应具有多个控制器。保持简单:每个 CRD 一个控制器。
-
运算符不应特定于它们监视资源的命名空间,也不应特定于它们将部署的命名空间。
-
运算符应使用语义化版本,作为 Kubernetes API 的扩展,它们还应遵循与版本更改相关的Kubernetes API 版本控制指南。
-
CRD 应遵循 OpenAPI 规范以允许使用已知模式。大多数基于 Operator SDK 的工具将提供一种基于 OpenAPI 规范创建样板 CRD 的方法,以便更容易开发它们。
-
与任何 Kubernetes 服务一样,操作者本身应遵循良好的安全指南,如以非 root 用户运行,最小权限 RBAC 和可观察性。指标和日志应该是系统外部的。操作者应该被仪表化,以便能够看到操作者的健康状态和任何已知的服务水平指标(SLIs)。可以利用诸如 Prometheus、DataDog、Cloud Operations 和 OpenTelemetry 的度量输出。
-
操作者不会安装其他操作者,也不应注册自己的 CRD,因为这些资源对集群是全局的,并且需要为操作者提升的特权。
-
在接受请求之前,检查所有 CRD 的有效性。可以根据已知的模式(如 OpenAPI 验证模式)或通过可以验证 CRD 的准入控制器来验证 CRD。这些方法将防止资源浪费空间在 etcd 上,因为它将不会被提交到 API。还应该在对账循环中增加一些验证逻辑,作为最后一次努力来验证和清理资源。
-
当操作者不再需要时,应自动清理。删除后正确清理资源非常重要,不仅适用于操作者直接创建的资源,还适用于可能已为操作者的应用程序需求创建的任何外部资源(例如,用于与 pod 上附加的存储相关的 PV、集群外的外部资源等)。
-
仔细考虑操作者的生命周期以及在向现有版本的用户升级路径引入破坏性更改时的时机。实施转换 Webhook,允许转换到特定版本和从特定版本转换,以确保在将资源从 vX 转换为 vY 并再次转换回 vX 时不会丢失信息。
-
在由操作者管理的资源上写入状态信息时需要慎重。客户资源是用户了解资源状态的唯一接口。通过控制器向资源写入清晰简明的状态,用户可以轻松使用现有的 Kubernetes 客户端工具查询和操作状态。状态应作为子资源实现,并应使用谓词,以免在更新后未增加主资源的
ResourceGeneration元数据字段后触发对账循环。
摘要
自动化部署和应用的“第二天”运维的承诺,已经使得运算符市场不再仅仅是实验性质,而成为 Kubernetes 生态系统中的关键特性。当组织需要支持复杂应用时,应使用运算符,但在存在更简单机制时要小心创建它们。利用软件供应商创建的现有运算符来帮助支持和维护它们,许多可以在 operatorhub.io 找到。虽然运算符模式可以是一个非常强大的工具,但需要承诺,以确保它不会制造更多问题而非解决问题。所有警告都不在话下,如果您正在基于 Kubernetes 构建大型应用平台,那么运算符模式应该是减少运维工作的重要工具。
第二十二章:结论
Kubernetes 的主要优势在于其模块化和通用性。几乎您可能想要部署的任何类型的应用程序都可以适应 Kubernetes,无论您需要对系统进行什么样的调整或优化,通常都是可能的。
当然,这种模块化和通用性是有成本的,这个成本是相当数量的复杂性。了解 Kubernetes 的 API 和组件如何工作对于成功发挥 Kubernetes 的能力,使您的应用程序开发、管理和部署更加简单和可靠至关重要。
同样,了解如何将 Kubernetes 连接到各种外部系统,如本地数据库和持续交付系统,对于在现实世界中高效使用 Kubernetes 至关重要。
在本书中,我们努力提供来自具体实际经验的见解,涵盖您可能会遇到的特定主题,无论您是 Kubernetes 的新手还是经验丰富的管理员。无论您面临一个新领域,努力成为专家,还是仅需了解其他人如何解决熟悉问题的方法,我们的目标是本书中的章节使您能够从我们的经验中学习。我们希望您在 Kubernetes 的旅程中能随时查阅本书,并快速翻到相关章节,轻松获得一些最佳实践。
遵循这些最佳实践,您可以借助我们的结合经验帮助避免常见陷阱,优化性能和安全性,并迅速增强信心,充分利用 Kubernetes 的潜力。谢谢您,我们期待在现实世界中见到您!


浙公网安备 33010602011771号