Kubernetes-算子框架之书-全-
Kubernetes 算子框架之书(全)
原文:
annas-archive.org/md5/01ffe17989fc9e0e9aa7a6dc7f487d53
译者:飞龙
前言
Kubernetes 作为分布式计算标准平台的出现,彻底改变了企业应用程序开发的格局。组织和开发人员现在可以轻松地编写和部署具有云原生特点的应用程序,并扩展这些部署以满足他们和用户的需求。然而,随着规模的扩大,复杂性和维护负担也在增加。此外,分布式工作负载的性质使得应用程序暴露于更多的潜在故障点,这些故障可能代价高昂且修复耗时。虽然 Kubernetes 本身是一个强大的平台,但它也存在自身的挑战。
Operator 框架的开发旨在通过定义一个标准流程来自动化 Kubernetes 集群中的运维任务,从而解决这些痛点。Kubernetes 管理员和开发人员现在可以利用一整套 API、库、管理应用程序和命令行工具,快速创建能够自动创建和管理应用程序(甚至是核心集群组件)的控制器。这些控制器被称为 Operators,它们响应 Kubernetes 集群中自然波动的状态,确保任何偏离期望管理静态状态的情况都能得到调整。
本书是一本针对任何对 Operators 感兴趣但不熟悉其使用的 Kubernetes 用户的 Operator 框架入门书,旨在提供设计、构建和使用 Operators 的实际经验。为此,本书不仅仅是一个写 Operator 代码的技术教程(尽管它会逐步讲解如何用 Go 编写一个示例 Operator)。它还包括了无形的设计考虑因素和维护工作流,提供了一种全面的方法,指导你理解 Operator 的使用场景和开发,帮助你构建和维护自己的 Operators。
本书适用对象
本书的目标读者是任何考虑使用 Operator 框架进行自己开发的人,包括工程师、项目经理、架构师以及业余开发者。本书内容假设读者对基本的 Kubernetes 概念(如 Pods、ReplicaSets 和 Deployments)有所了解。不过,对于 Operators 或 Operator 框架的先前经验并不要求。
本书涵盖的内容
第一章,介绍 Operator 框架,简要介绍了描述 Operator 框架的基本概念和术语。
第二章,理解 Operators 如何与 Kubernetes 交互,提供了 Operators 在 Kubernetes 集群中功能的示例描述,包括不仅仅是技术上的交互,还有不同用户交互的描述。
第三章,设计操作符 – CRD、API 和目标对账,讨论了在设计新操作符时需要考虑的高层次因素。
第四章,使用 Operator SDK 开发操作符,提供了使用 Operator SDK 工具包创建一个示例操作符项目的技术性讲解。
第五章,开发操作符 – 高级功能,在上一章的示例操作符项目基础上,添加了更复杂的功能。
第六章,构建和部署你的操作符,演示了如何手动编译和安装一个操作符到 Kubernetes 集群中。
第七章,使用操作符生命周期管理器安装和运行操作符,介绍了操作符生命周期管理器,帮助自动化操作符在集群中的部署。
第八章,为操作符的持续维护做准备,提供了促进操作符项目积极维护的考虑因素,包括如何发布新版本和与上游 Kubernetes 发布标准对齐。
第九章,深入探讨常见问题与未来趋势,提供了前几章内容的精炼总结,分解为小的 FAQ 风格的部分。
第十章,可选操作符案例研究 – Prometheus 操作符,提供了操作符框架概念在实际操作符管理应用程序中的应用示范。
第十一章,核心操作符案例研究 – Etcd 操作符,提供了操作符框架概念在核心集群组件管理中的应用实例。
为了充分利用本书
假设你至少对基本的 Kubernetes 概念和术语有一定的基础理解,因为操作符框架在很大程度上基于这些概念来实现其目的。包括基本的应用程序部署以及使用命令行工具如kubectl
与 Kubernetes 集群交互的熟悉度。虽然不一定需要直接的实践经验,但有这些背景会有所帮助。
此外,完成本书中的所有示例任务需要管理员权限来访问 Kubernetes 集群(例如,在第六章中,构建和部署你的 Operator)。需要 Kubernetes 集群的章节提供了一些创建临时集群和基本设置步骤的选项,但为了集中精力讲解主要内容,这些部分故意不涉及关于集群设置的详细说明。强烈建议为所有示例使用临时集群,以避免意外损害敏感工作负载。
如果你正在使用本书的数字版本,我们建议你自己输入代码,或者从本书的 GitHub 仓库访问代码(链接在下一节提供)。这样可以帮助你避免由于复制和粘贴代码而导致的潜在错误。
下载示例代码文件
你可以从 GitHub 上下载本书的示例代码文件,链接:github.com/PacktPublishing/The-Kubernetes-Operator-Framework-Book
。如果代码有更新,将会在 GitHub 仓库中同步更新。
我们还提供其他代码包,来自我们丰富的书籍和视频目录,详情请访问github.com/PacktPublishing/
。快来看看吧!
《代码实战》
本书的《代码实战》视频可以在bit.ly/3m5dlYa
观看。
下载彩色图片
我们还提供了一个 PDF 文件,里面包含了本书中使用的截图和图表的彩色图片。你可以在这里下载:static.packt-cdn.com/downloads/9781803232850_ColorImages.pdf
。
使用的约定
本书中使用了多种文本约定。
代码文本
: 表示文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 账号。例如:“这需要额外的资源,如 ClusterRoles
和 RoleBindings
,以确保 Prometheus Pod 有权限从集群及其应用程序中抓取度量数据。”
代码块将按以下方式显示:
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: sample
spec:
replicas: 2
当我们希望特别强调代码块的某一部分时,相关的行或项目将以粗体显示:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: web-service-monitor
labels:
app: web
spec:
selector:
matchLabels:
serviceLabel: webapp
任何命令行输入或输出将按以下方式书写:
$ export BUNDLE_IMG=docker.io/sample/nginx-bundle:v0.0.2
$ make bundle-build bundle-push
$ operator-sdk run bundle docker.io/same/nginx-bundle:v0.0.2
粗体:表示一个新术语、重要的词语或你在屏幕上看到的词语。例如,菜单或对话框中的文字会以粗体显示。示例:“点击Grafana Operator图块会打开该特定 Operator 的信息页面。”
提示或重要说明
以这种方式显示。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件联系我们:customercare@packtpub.com,并在邮件主题中注明书名。
勘误表:尽管我们已尽一切努力确保内容的准确性,但错误难免发生。如果您在本书中发现了错误,我们将非常感激您向我们报告。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上发现任何形式的我们作品的非法复制品,我们将非常感激您提供该位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并附上相关资料的链接。
如果您有兴趣成为作者:如果您在某个主题领域具有专业知识,并且有兴趣撰写或参与编写书籍,请访问authors.packtpub.com。
分享您的想法
阅读完《Kubernetes 操作框架书》后,我们非常期待听到您的想法!请点击这里直接前往亚马逊评论页面并分享您的反馈。
您的评价对我们和技术社区都非常重要,能够帮助我们确保提供优质的内容。
第一部分:Operator 及 Operator 框架的基本知识
在本节中,您将对 Kubernetes Operator 的历史和目的有一个基本的了解。Operator 框架的基本概念将被介绍,您将学习 Operator 如何在 Kubernetes 集群中运作。这将为更复杂的概念奠定基础,后续会介绍这些内容。
本节包含以下章节:
-
第一章,介绍 Operator 框架
-
第二章,理解 Operator 如何与 Kubernetes 交互
第一章:第一章:介绍操作符框架
管理 Kubernetes 集群很难。这部分是因为任何微服务架构本质上都基于许多小组件的交互,每个组件都有可能成为故障点。当然,这种系统设计也有很多好处,比如由于责任分离,能够优雅地处理错误。然而,诊断和解决这些错误需要大量的工程资源,并且需要对应用程序的设计有深入的了解。这是项目团队迁移到 Kubernetes 平台时的一个主要痛点。
操作符框架被引入 Kubernetes 生态系统,以解决这些问题。本章将介绍一些基本的主题,提供操作符框架的概览。目的是简要介绍操作符框架、它所解决的问题、解决方法以及它为用户提供的工具和模式。通过这些内容,我们可以总结出使用操作符来帮助管理 Kubernetes 集群的目标和好处。以下是这些话题的概述:
-
没有操作符的集群管理
-
介绍操作符框架
-
使用操作符软件开发工具包(SDK)进行开发
-
使用操作符生命周期管理器(OLM)管理操作符
-
在
OperatorHub.io
上分发操作符 -
使用能力模型定义操作符功能
-
使用操作符来管理应用
技术要求
本章没有任何技术要求,因为我们只会讨论一些通用话题。在后续章节中,我们将深入探讨这些话题,并提供相关的技术前提,帮助你更好地跟随。
本章的《代码实战》视频可以通过以下链接观看:bit.ly/3GKJfmE
没有操作符的集群管理
Kubernetes 是一个强大的微服务容器编排平台,提供了许多不同的控制器、资源和设计模式,几乎可以覆盖任何用例,并且它在不断发展。因此,设计部署到 Kubernetes 上的应用程序可能非常复杂。
在设计一个使用微服务的应用时,有许多概念需要熟悉。在 Kubernetes 中,这些概念主要是核心平台中包含的本地应用程序编程接口(API)资源对象。在本书中,我们将假设读者对常见的 Kubernetes 资源及其功能有基本的了解。
这些对象包括 Pods、Replicas、Deployments、Services、Volumes 等。任何基于微服务的云应用在 Kubernetes 上的编排,都依赖于将这些不同的概念整合在一起,形成一个协调一致的整体。这种编排是造成复杂性的原因,许多应用开发者在管理时都会遇到困难。
在示例应用中进行演示
以一个简单的 web 应用程序为例,它接受、处理并存储用户输入(如留言板或聊天服务器)。这种应用程序的良好容器化设计应该是,设置一个 Pod 来向用户展示前端界面,另一个后台 Pod 接受用户输入并将其发送到数据库进行存储。
当然,你将需要一个运行数据库软件的 Pod,以及一个由数据库 Pod 挂载的持久卷。这三个 Pods 将通过服务相互通信,并且它们还需要共享一些公共环境变量(例如数据库访问凭证和用于调整不同应用设置的环境变量)。
这里是一个示例应用程序的图示。它包含三个 Pods(前端、后端和数据库),以及一个持久卷:
图 1.1 – 带有三个 Pod 和一个持久卷的简单应用程序图示
这只是一个小例子,但已经可以明显看出,即使是一个简单的应用程序,也很快会涉及多个活动组件之间的繁琐协调。从理论上讲,只要每个独立组件没有发生故障,这些离散的组件就会继续协同工作。但如果应用程序的分布式设计中某个地方发生故障时,该怎么办呢?假设应用程序的有效状态会始终保持不变是非常不明智的。
对集群状态变化的反应
集群状态变化的原因有很多。有些可能甚至不被技术上视为故障,但它们仍然是运行中的应用程序必须注意的变化。例如,如果你的数据库访问凭证发生更改,那么该更新需要传递到所有与其交互的 Pods。或者,如果应用程序中有一个新功能,需要巧妙地进行发布,并更新正在运行的工作负载的设置。这需要手动操作(更重要的是,需要时间),以及对应用程序架构的深入理解。
在发生意外故障时,时间和精力变得更加关键。这正是 Operator 框架自动处理的问题。如果构成此应用程序的其中一个 Pod 遇到异常,或者应用程序的性能开始下降,这些场景需要干预。这意味着人工工程师不仅必须了解部署的细节,还必须随时待命以确保系统正常运行。
还有其他组件可以帮助管理员监控应用程序的健康状况和性能,例如指标聚合服务器。然而,这些组件本质上是额外的应用程序,也必须定期监控以确保它们正常工作,因此将它们添加到集群中可能会重新引入手动管理应用程序的相同问题。
介绍 Operator Framework
Kubernetes Operators 的概念是在 2016 年由 CoreOS 在一篇博客文章中提出的。CoreOS 创建了他们自己的容器原生 Linux 操作系统,专为云架构的需求进行了优化。2018 年,Red Hat 收购了该公司,虽然 CoreOS 操作系统的官方支持在 2020 年结束,但他们的 Operator Framework 依然蓬勃发展。
Operator 的主要思想是自动化那些通常由人工完成的集群和应用程序管理任务。这个角色可以看作是对支持工程师或 开发运维 (DevOps) 团队的自动化扩展。
即使大多数 Kubernetes 用户从未使用过 Operator Framework,他们也已经熟悉 Operators 的一些设计模式。这是因为 Operators 看起来是一个复杂的话题,但归根结底,它们在功能上与已经默认自动化大多数 Kubernetes 集群的许多核心组件没有太大区别。这些组件被称为控制器,本质上,任何 Operator 都只是一个控制器。
探索 Kubernetes 控制器
Kubernetes 本身由许多默认的控制器组成。这些控制器保持集群的期望状态,状态由用户和管理员设置。Deployments、ReplicaSets 和 Endpoints 只是一些由其各自控制器管理的集群资源的例子。每个这些资源都涉及管理员声明期望的集群状态,随后由控制器来维持这个状态。如果出现任何偏差,控制器必须采取行动来解决其控制的内容。
这些控制器通过监控集群的当前状态并将其与期望状态进行比较来工作。一个例子是 ReplicaSet,它指定需要维护一个 Pod 的三个副本。如果其中一个副本失败,ReplicaSet 会迅速识别出当前只有两个副本在运行。然后,它会创建一个新的 Pod 以使集群恢复到稳定状态。
此外,这些核心控制器由 Kube Controller Manager 集中管理,后者也是一种控制器。它监控控制器的状态,并在控制器发生故障时尝试从错误中恢复,或者如果无法自动恢复,则报告错误并进行人工干预。因此,也有可能存在管理其他控制器的控制器。
同样,Kubernetes Operators 将操作控制器的开发交给用户。这为管理员提供了灵活性,可以编写一个控制器来管理 Kubernetes 集群或自定义应用程序的任何方面。通过定义更具体的逻辑,开发人员可以将 Kubernetes 的主要优势扩展到自己应用程序的独特需求中。
按照 Operator Framework 的指南编写的 Operator 设计得非常类似于本地控制器。它们通过监控集群的当前状态并采取措施将其与期望状态调和来实现这一点。具体来说,Operator 是为特定的工作负载或组件量身定制的。然后,Operator 知道如何与该组件以不同的方式进行交互。
了解 Operator 的关键术语
由 Operator 管理的组件是其操作数。操作数是任何由 Operator 调和状态的应用程序或工作负载。Operator 可以管理多个操作数,但大多数 Operator 管理的操作数—通常最多只有一个。关键的区别是,Operator 存在的目的是管理操作数,其中 Operator 是系统架构设计中的元应用。
操作数几乎可以是任何类型的工作负载。虽然一些 Operator 管理应用程序部署,但许多 Operator 部署了额外的、可选的集群组件,提供如数据库备份和恢复等元功能。某些 Operator 甚至将核心的本地 Kubernetes 组件作为操作数,例如 etcd
。因此,Operator 不一定管理你自己的工作负载,它们可以帮助集群的任何部分。
无论 Operator 管理的是什么,它都必须为集群管理员提供一种与之交互并配置其应用程序设置的方式。Operator 通过自定义资源暴露其配置选项。
自定义资源作为 API 对象创建,遵循匹配的CustomResourceDefinition(CRD)的约束。CRD 本身是一种本地 Kubernetes 对象,允许用户和管理员将自己的资源对象扩展到 Kubernetes 平台上,超出核心 API 定义的内容。换句话说,虽然 Pod 是 Kubernetes 中内置的本地 API 对象,但 CRD 允许集群管理员定义 MyOperator 作为另一个 API 对象,并以与本地对象相同的方式与之交互。
将所有内容整合在一起
Operator Framework 力求定义一个完整的 Operator 开发和分发生态系统。这个生态系统由三个支柱组成,涵盖了 Operator 的编码、部署和发布。它们分别是 Operator SDK、OLM 和 OperatorHub。
这三个支柱是使 Operator 框架如此成功的关键因素。它们将框架从仅仅是开发模式转变为一个涵盖整个 Operator 生命周期的循环过程。这有助于支持 Operator 开发者和用户之间的契约,为他们的软件提供一致的行业标准。
Operator 的生命周期始于开发阶段。为了帮助这一过程,Operator SDK 存在以指导开发者创建 Operator 的第一步。从技术上讲,Operator 并不一定要使用 Operator SDK 编写,但 Operator SDK 提供了开发模式,显著减少了启动和维护 Operator 源代码所需的工作量。
虽然编码和开发当然是创建 Operator 的重要部分,但任何项目的时间表并不会在代码编译后结束。Operator 框架社区认识到,项目的一致生态系统必须在初始开发阶段之外提供指导。项目需要一致的安装方法,并且随着软件的演变,需要发布和分发新版本。OLM 和 OperatorHub 帮助用户在他们的集群中安装和管理 Operator,并在社区中分享他们的 Operator。
最后,Operator 框架提供了称为能力模型的 Operator 功能规模。能力模型为开发者提供了一种通过回答可量化问题对其 Operator 的功能能力进行分类的方法。Operator 的分类,连同能力模型,向用户提供了关于他们可以从 Operator 中期待什么的信息。
这三个支柱共同奠定了 Operator 框架的基础,并形成了区分 Operator 概念的设计模式和社区标准。连同能力模型一起,这一标准化框架导致了 Kubernetes 中 Operator 的广泛采用。
到目前为止,我们已经讨论了 Operator 框架核心概念的简要介绍。与没有 Operator 管理的 Kubernetes 应用相比,Operator 框架的支柱解决了应用开发者遇到的问题。对 Operator 框架核心支柱的理解将为我们深入探讨每个支柱奠定基础。
使用 Operator SDK 进行开发
Operator 框架的第一个支柱是 Operator SDK。与任何其他软件开发工具包一样,Operator SDK 以代码形式提供打包功能和设计模式。这些包括预定义的 API、抽象化的常见函数、代码生成器和项目搭建工具,以便从头开始轻松启动 Operator 项目。
Operator SDK 主要是用 Go 编写的,但其工具链允许使用 Go 代码、Ansible 或 Helm 编写 Operators。这使得开发人员可以从头开始编写自己的 Operators,通过自己编写 CRD 和调和逻辑,或者根据需求利用 Ansible 和 Helm 提供的自动化部署工具生成他们的 API 和调和逻辑。
开发人员通过 operator-sdk
命令行二进制文件与 Operator SDK 进行交互。该二进制文件可以通过 Homebrew 在 Mac 上获取,也可以直接从 Operator Framework 的 GitHub 仓库(github.com/operator-framework/operator-sdk
)作为发布版本获取,您也可以从源代码编译它。
无论您是计划使用 operator-sdk init
和 operator-sdk create api
开发一个 Operator,第一条命令会用标准的 Go 代码、依赖项、hack 脚本,甚至为编译项目提供 Dockerfile
和 Makefile
,初始化一个项目的源目录。
为您的 Operator 创建 API 是必需的,以便定义 CRD,供 Operator 部署到 Kubernetes 集群后与之交互。这是因为 CRD 是由 Go 代码编写的 API 类型定义所支持的。CRD 是从这些代码定义生成的,Operator 内置了逻辑来转换 CRD 和 Go 表示的对象之间的关系。实质上,CRD 是用户与 Operators 交互的方式,而 Go 代码是 Operator 理解设置的方式。CRD 还提供了结构化验证模式的好处,用于自动验证输入。
Operator SDK 二进制文件具有标志来指定 API 的名称和版本。然后,它根据最佳实践的标准定义生成 Go 代码和相应的 YAML Ain't Markup Language(YAML)文件。然而,您可以自由修改 API 的定义,以任何您选择的方式进行修改。
如果我们要为一个应用程序初始化一个基本的 Operator,就像本章开头首次展示的那样,步骤相对简单。它们将如下所示:
$ mkdir sample-app
$ cd sample-app/
$ operator-sdk init --domain mysite.com --repo github.com/sample/simple-app
$ operator-sdk create api --group myapp --version v1alpha1 --kind WebApp --resource –controller
$ ls
total 112K
drwxr-xr-x 15 mdame staff 480 Nov 15 17:00 .
drwxr-xr-x+ 270 mdame staff 8.5K Nov 15 16:48 ..
drwx------ 3 mdame staff 96 Nov 15 17:00 api
drwxr-xr-x 3 mdame staff 96 Nov 15 17:00 bin
drwx------ 10 mdame staff 320 Nov 15 17:00 config
drwx------ 4 mdame staff 128 Nov 15 17:00 controllers
drwx------ 3 mdame staff 96 Nov 15 16:50 hack
-rw------- 1 mdame staff 129 Nov 15 16:50 .dockerignore
-rw------- 1 mdame staff 367 Nov 15 16:50 .gitignore
-rw------- 1 mdame staff 776 Nov 15 16:50 Dockerfile
-rw------- 1 mdame staff 8.7K Nov 15 16:51 Makefile
-rw------- 1 mdame staff 422 Nov 15 17:00 PROJECT
-rw------- 1 mdame staff 218 Nov 15 17:00 go.mod
-rw-r--r-- 1 mdame staff 76K Nov 15 16:51 go.sum
-rw------- 1 mdame staff 3.1K Nov 15 17:00 main.go
之后,您将根据选择的方法开发 Operator 的逻辑。如果选择直接编写 Go 代码,首先会修改项目树中的 *.go
文件。对于 Ansible 和 Helm 部署,您将开始着手编写项目的 Ansible 角色或 Helm 图表。
最后,Operator SDK 二进制文件提供了一套与 OLM 交互的命令。这些命令包括在运行的集群中安装 OLM 的能力,还可以在 OLM 内安装和管理特定的 Operators。
使用 OLM 管理 Operators
OLM 是 Operator Framework 的第二个支柱。它的目的是简化在 Kubernetes 集群中部署和管理 Operators。它是一个在 Kubernetes 集群中运行的组件,提供了几个命令和功能,用于与 Operators 进行交互。
OLM 主要用于操作员的安装和升级——这包括获取和安装操作员的任何依赖项。用户通过操作员 SDK 二进制文件提供的命令、Kubernetes 命令行工具(kubectl
)和声明式 YAML 与 OLM 进行交互。
要开始使用,OLM 可以通过以下命令在集群中初始化:
$ operator-sdk olm install
除了安装操作员,OLM 还可以使当前安装的操作员对集群中的用户可见。这为集群用户提供了一个已安装操作员的目录。此外,通过管理集群中所有已知的操作员,OLM 可以监控可能导致集群不稳定的冲突的操作员 API 和设置。
一旦操作员的 Go 代码被编译成镜像,它就可以准备安装到运行 OLM 的集群中。技术上讲,OLM 并不是在任何集群中运行操作员的必需条件。例如,完全可以像部署任何其他基于容器的应用程序一样,在集群中手动部署一个操作员。然而,由于之前提到的优势和安全措施(包括它的安装操作员的能力以及对其他已安装操作员的感知),强烈建议使用 OLM 来管理集群中的操作员。
在开发操作员时,镜像会被编译成一个包,这个包通过 OLM 进行安装。这个包由几个 YAML 文件组成,这些文件描述了操作员、其 CRD 和依赖项。OLM 知道如何处理这种标准化格式的包,以便在集群中正确地管理操作员。
编译操作员的代码并进行部署可以通过以下命令来完成。下文代码片段中的第一个命令构建描述操作员的 YAML 清单包。然后,它将这些信息传递给 OLM,以便在你的集群中运行操作员:
$ make bundle ...
$ operator-sdk run bundle ...
后续章节将详细演示如何使用这些命令以及它们的作用,但总体思路是,这些命令首先将操作员的 Go 代码编译成镜像,并转换为 OLM 能够理解的可部署格式。但 OLM 并不是操作员框架中唯一使用操作员包的部分——很多相同的信息也被 OperatorHub 用来提供关于操作员的信息。
一旦操作员被编译成镜像,OperatorHub 就作为一个平台,用于将这些镜像分享和分发给其他用户。
在 OperatorHub.io 上分发操作员
操作员框架的最终核心组件是OperatorHub.io
。作为一个重要的开源项目,操作员框架生态系统建立在项目的开放共享和分发上。因此,OperatorHub 推动了作为 Kubernetes 概念的操作员的发展。
OperatorHub 是一个由 Kubernetes 社区发布和管理的操作员开源目录。它作为一个自由可用的操作员的中央索引,每个操作员由开发者和组织贡献。你可以在下面的截图中查看 OperatorHub.io
首页的概览:
图 1.2 – OperatorHub.io 首页截图,展示了一些最受欢迎的操作员
提交操作员到 OperatorHub 进行索引的过程已经标准化,以确保操作员与 OLM 的一致性和兼容性。新的操作员会通过自动化工具审查,确保符合操作员的标准定义。这个过程主要通过提供 OperatorHub 后端的开源 GitHub 仓库进行处理。然而,OperatorHub 并不提供关于操作员持续维护的任何帮助,这也是为什么操作员开发者需要共享他们自己的开源仓库链接以及用户可以报告错误和贡献代码的联系方式。
准备操作员提交到 OperatorHub 涉及生成其包和相关清单。提交过程主要依赖于操作员的 集群服务版本 (CSV) 文件。CSV 是一个 YAML 文件,提供关于操作员的大部分元数据给 OLM 和 OperatorHub。它包括一般信息,如操作员的名称、版本和关键词。然而,它也定义了安装要求(例如 基于角色的访问控制 (RBAC) 权限)、CRD、API 和操作员拥有的附加集群资源对象。
操作员 CSV 的特定部分包括以下内容:
-
操作员的名称和版本号,以及操作员的描述和以 Base64 编码的图像格式显示的图标
-
操作员的注解
-
操作员维护者的联系信息以及其代码所在的开源仓库
-
操作员应该如何安装到集群中
-
操作员 CRD 的示例配置
-
操作员运行所需的 CRD 和其他资源及依赖项
由于涵盖了大量信息,操作员 CSV 通常非常长,需要一定时间来正确准备。然而,一个定义清晰的 CSV 有助于操作员接触到更广泛的受众。有关操作员 CSV 的详细信息将在后续章节中介绍。
使用能力模型定义操作员功能
运维框架定义了一个能力模型 (operatorframework.io/operator-capabilities/
),该模型根据运维人员的功能和设计对其进行分类。此模型有助于根据运维人员的成熟度将其拆分,同时描述了运维人员与 OLM 的互操作性程度以及用户在使用该运维人员时可以预期的功能。
能力模型分为五个层次,运维人员可以在这些层次中的任何一个级别发布,并随着其成长,随着功能的增加,可能会从一个级别逐渐发展到下一个级别。此外,各个级别是累进的,每个级别通常包括下面所有级别的功能。
运维人员的当前级别是 CSV 的一部分,该级别会在其 OperatorHub 列表中显示。该级别基于一定程度的主观判断标准,并且纯粹是一个信息性指标。
每个级别都有定义其功能的特定功能。这些功能被拆分为 基础安装、无缝升级、完整生命周期、深度洞察 和 自动驾驶。能力模型的具体级别在这里列出:
- Level I—基础安装:此级别代表运维人员能力中最基础的部分。在 Level I 中,运维人员仅能在集群中安装其 Operand,并向集群管理员报告工作负载的状态。这意味着它可以设置应用程序所需的基本资源,并报告这些资源何时可以供集群使用。
在 Level I 中,运维人员还允许对 Operand 进行简单配置。此配置通过运维人员的自定义资源指定。运维人员负责将配置规范与运行中的 Operand 工作负载进行协调。然而,如果 Operand 进入失败状态,无论是由于配置错误还是外部影响,它可能无法作出响应。
回到本章开头的示例 Web 应用程序,对于该应用程序,Level I 级别的运维人员只会处理工作负载的基本设置,其他无所涉及。这适用于需要在多个集群上快速设置的简单应用程序,或者是需要用户自行安装并共享的应用程序。
- Level II—无缝升级:Level II 级别的运维人员提供基本安装功能,并增加了关于升级的附加功能。这包括 Operand 的升级以及运维人员本身的升级。
升级是任何应用程序的重要部分。随着错误修复的实现和更多功能的添加,能够平滑地在版本之间过渡有助于确保应用程序的正常运行。处理自身升级的运维人员可以在升级自身时升级其 Operand,或者通过修改运维人员的自定义资源手动升级其 Operand。
为了实现无缝升级,操作员还必须能够升级其操作数的旧版本(这些旧版本可能是因为它们由操作员的早期版本管理)。这种向后兼容性对于升级到新版本和处理回滚至关重要(例如,如果新版本引入了一个显而易见的错误,无法等待修补程序发布)。
我们的示例 Web 应用操作员也可以提供相同的一组功能。这意味着,如果发布了应用程序的新版本,操作员可以处理升级已部署的应用程序实例到新版本。或者,如果对操作员本身进行了更改,则可以管理自己的升级(并且稍后升级应用程序,无论操作员与操作数之间的版本差异如何)。
- III 级—完整生命周期:III 级操作员提供至少一项操作数生命周期管理功能。能够在操作数生命周期内提供管理意味着操作员不仅仅是以设定并遗忘的方式被动地操作工作负载。在 III 级,操作员积极地参与操作数的持续功能。
与操作数生命周期管理相关的功能包括以下内容:
-
能够创建和/或恢复操作数的备份。
-
支持更复杂的配置选项和多步骤工作流。
-
灾难恢复(DR)的故障转移和故障回退机制。当操作员遇到错误(无论是在自身还是在操作数中)时,它需要能够将流程重新路由到备份过程(故障转移)或将系统回滚到最后已知的正常状态(故障回退)。
-
管理集群化操作数的能力,特别是支持向操作数添加或移除成员。操作员应能够考虑多个副本的操作数的法定人数。
-
同样,支持使用只读功能的工作实例扩展操作数。
实现这些功能之一或多个功能的操作员可以被认为至少是 III 级操作员。简单的 Web 应用操作员可以利用其中的一些功能,例如灾难恢复(DR)和扩展。随着用户基础的增长和资源需求的增加,管理员可以指示操作员通过增加副本 Pod 来扩展应用程序,以应对增加的负载。
如果在此过程中某些 Pod 失败,操作员应足够智能,知道将故障转移到另一个 Pod 或完全不同的集群区域。或者,如果发布了一个新的 Web 应用版本,并且该版本引入了意外的错误,操作员可以识别先前的成功版本,并在管理员发现错误时提供将操作数工作负载降级的方式。
- Level IV—深度见解:虽然之前的级别主要关注操作员与应用工作负载的功能性交互,但 Level IV 强调监控和度量。这意味着操作员能够提供可衡量的见解,展示自身及其 Operand 的状态。
从开发角度来看,见解可能相对于功能和 bug 修复显得不那么重要,但它们对于应用程序的成功至关重要。关于应用程序性能的量化报告可以推动持续开发,并突出需要改进的地方。拥有一个可衡量的系统来推动这些工作,可以科学地证明或反驳哪些变化有实际效果。
操作员最常以度量的形式提供他们的见解。这些度量通常与度量聚合服务器兼容,如 Prometheus。(有趣的是,Red Hat 发布了一个 Prometheus 操作员,它是一个 Level IV 操作员。这个操作员可以在 OperatorHub 上找到,网址是 operatorhub.io/operator/prometheus
。)
然而,操作员也可以通过其他方式提供见解。这些方式包括警报和 Kubernetes 事件。事件是内建的集群资源对象,由 Kubernetes 核心对象和控制器使用。
Level IV 操作员报告的另一个关键见解是操作员和 Operand 的性能。这些见解有助于管理员了解集群的健康状况。
我们的简单 web 应用操作员可以提供关于 Operand 性能的见解。对应用程序的请求会提供有关当前和历史负载的信息。此外,由于操作员此时可以识别失败状态,它可以在应用程序不健康时触发警报。许多警报可能表明存在可靠性问题,从而引起管理员的关注。
- Level V—自动驾驶:Level V 是针对操作员的最复杂级别。它包括提供最高能力的操作员,除了之前四个级别中的所有功能外,这个级别还具备其他特性。这个级别被称为自动驾驶,因为定义它的特性侧重于能够几乎完全自主运行。这些能力包括自动扩展、自动修复、自动调优和异常检测。
自动扩展是操作员根据需求检测需要对应用程序进行扩展或缩减的能力。通过测量当前负载和性能,操作员可以确定是否需要更多或更少的资源来满足当前的使用需求。高级操作员甚至可以根据当前和过去的数据预测扩展的需求。
自动修复操作员可以对报告不健康状态的应用程序做出反应,并努力修正它们(或者至少防止其恶化)。当操作对象报告错误时,操作员应采取反应措施来修复故障。此外,操作员还可以利用当前指标主动防止操作对象进入故障状态。
自动调优意味着操作员可以动态地修改操作对象,以达到最佳性能。这涉及到自动调节操作对象的设置,甚至可能包括将工作负载转移到完全不同、更适合的节点上等复杂操作。
最后,异常检测是操作员识别操作对象中亚优化或异常行为的能力。通过衡量性能,操作员可以了解应用程序当前和历史的功能水平。这些数据可以与手动定义的最低预期进行比较,或者用来动态地通知操作员该预期。
所有这些功能都在很大程度上依赖于使用指标来自动通知操作员需要对其自身或操作对象采取行动。因此,级别 V 操作员是级别 IV 的自然进展,后者是操作员暴露高级指标的层级。
在级别 V,简单的 Web 应用程序操作员将管理应用程序的大多数方面。它可以了解当前的请求数量,因此能够按需扩展应用程序的副本。如果这种扩展开始导致错误(例如,过多的并发数据库调用),它可以识别失败的 Pod 数量,并防止进一步的扩展。它还会尝试修改 Web 应用程序的参数(例如请求超时),以帮助纠正问题并允许自动扩展继续。当负载高峰期过去后,操作员会自动将应用程序缩减到基线服务水平。
级别 I 和 II(基本安装和无缝升级)可以与操作员 SDK 的三个方面一起使用:Helm、Ansible 和 Go。然而,级别 III 及以上(全生命周期、深度洞察和自动驾驶)仅能通过 Ansible 和 Go 实现。这是因为这些更高层次的功能需要比单独使用 Helm 图表更复杂的逻辑。
现在我们已经解释了操作员框架的三个主要支柱:操作员 SDK、OLM 和 OperatorHub。我们了解了它们如何为操作员的开发和使用提供不同的有益功能。我们还学习了能力模型,它作为操作员可能拥有的不同功能层级的参考。在下一部分,我们将应用这些知识来处理一个示例应用程序。
使用操作员来管理应用程序
显然,操作员的工作不仅仅是调和集群状态。操作员框架是一个全面的平台,供 Kubernetes 开发者和用户解决独特问题,这也是 Kubernetes 如此灵活的原因。
集群管理员在操作员框架中的第一步通常是使用操作员 SDK,如果没有现成的操作员能够满足需求,就自己开发一个操作员,或者使用 OperatorHub(如果有合适的操作员)。
操作员框架总结
在从零开始开发操作员时,有三种开发方法可供选择:Go、Ansible 或 Helm。然而,单独使用 Ansible 或 Helm 最终会将操作员的功能限制到最基本的水平。
如果开发者希望分享他们的操作员,他们需要将其打包成标准的操作员清单包,以便提交到 OperatorHub。经过审核后,他们的操作员将公开提供,供其他用户下载并安装到自己的集群中。
OLM 使用户在集群中启动操作员变得更加简单。这些操作员可以来自 OperatorHub,也可以从零开始编写。不管哪种方式,OLM 都让操作员的安装、升级和管理变得更加轻松。它还提供了多操作员工作时的多个稳定性优势。你可以通过下面的图示看到三者之间的关系:
图 1.3 – 操作员 SDK、OperatorHub 和 OLM 之间的关系
这些支柱中的每一个都提供了不同的功能,帮助操作员的开发。它们共同构成了操作员框架的基础。利用这些支柱是操作员与普通 Kubernetes 控制器之间的关键区别。总结来说,虽然每个操作员本质上都是一个控制器,但并非每个控制器都是操作员。
应用操作员功能
回顾本章中的第一个示例,分析了一个简单应用,包含三个 Pod 和一个持久卷,没有操作员管理。这个应用依赖于乐观的正常运行时间和面向未来的设计,以便持续运行。在实际部署中,这些理念不幸是不可行的。设计会不断演化,变化,无法预见的故障也会导致应用宕机。但操作员如何帮助这个应用在不可预测的环境中持续运行呢?
通过定义单一声明性配置,这个运算符可以在一个位置控制应用程序部署的各种设置。这就是运算符建立在 CRD 上的原因。这些自定义对象使开发人员和用户可以像操作原生 Kubernetes 对象一样轻松地与他们的运算符进行交互。因此,编写一个运算符来管理我们简单的 Web 应用程序的第一步将是定义一个带有我们认为需要的所有设置的 CRD 的基本代码结构。一旦我们完成了这一点,我们应用程序的新图表将如下所示:
图 1.4 – 在新的应用布局中,集群管理员只与运算符进行交互;然后运算符管理工作负载。
这显示了 Operand 部署的细节已经从需要手动管理员控制中抽象出来,CRD 的优点在于随着我们应用程序的增长,可以在后续版本的运算符中添加更多设置。一些初始设置的示例包括:
-
数据库访问信息
-
应用程序行为设置
-
日志级别
编写我们的运算符代码时,我们还希望为诸如指标、错误处理和报告等事务编写逻辑。运算符还可以开始双向与操作数通信。这意味着它不仅可以安装和更新操作数,还可以接收操作数关于其状态的反馈,并进行相应的报告。
概要
在本章中,我们介绍了运算符框架的基本概念。这些包括运算符 SDK、OLM 和 OperatorHub。除了运算符框架的开发和分发支柱外,能力模型还提供了衡量运算符功能的额外工具。在本书的整个过程中,我们将深入探讨这些组件,以深入理解它们的实际工作方式。
我们从分析手动管理应用程序和集群时出现的一些问题开始。这是通过一个简单的基于几个 Pod 和持久卷的通用 Web 应用程序的视角来完成的。管理此类应用程序的主要困难包括调试应用程序所需的时间和资源。这在云应用程序中尤为重要,高可用性(HA)和持续的正常运行时间是首要任务。
接下来,我们将看看运算符框架的每个支柱如何解决应用管理的最大困难。这些支柱从运算符 SDK 开始,它简化了运算符的开发。这使得开发人员能够快速开始迭代自动对账逻辑,以快速编写他们的运算符。它还提供了与 OLM 交互的命令,这是框架的下一个支柱。
OLM 的存在是为了帮助管理员在集群中安装和管理操作员。它提供了依赖关系管理,并通知管理员 API 冲突,以促进集群的稳定性。它还充当已安装操作员的本地目录,对于集群中的用户非常有用。
接下来,我们考察了 OperatorHub 及其在更广泛的开源 Kubernetes 社区中的作用。作为一个自由可用操作员的公开索引,OperatorHub 旨在促进操作员的采纳和维护。它使用与 OLM 相同的清单,向用户提供每个操作员的标准化元数据集。
最后,能力模型根据操作员提供的功能总结了操作员的成熟度。这对用户很有帮助,同时也为开发人员规划操作员功能提供了便捷的路线图。
为了总结这些组件,我们回顾了第一部分中呈现的原始应用示例。我们展示了,通过使用操作员来管理应用程序,集群管理员无需过于关注应用程序的架构细节即可保持其运行。相反,这些信息和控制已通过操作员的界面进行了抽象。
考虑到这一切,我们将继续下一章,深入探讨每个主题。我们还将通过详细示例构建我们自己的示例操作员。在下一章中,我们将开始研究基于与 Kubernetes 集群交互设计操作员的关键概念。
第二章:第二章:理解操作符如何与 Kubernetes 交互
现在我们已经了解了操作符做什么(以及为什么要做这些事情),我们可以开始探索如何执行这些任务。在确定了操作符的使用场景后,接下来的步骤就是制定它的技术设计。虽然这还是在实际编码之前的阶段,但它仍然是开发过程中的一个重要部分。这是几乎所有软件项目的标准做法,在本章中,我们将把它框架化并应用于 Kubernetes 的背景中。
在规划阶段,有几个因素需要考虑,并且需要回答一些问题,这些问题将有助于指导操作符的设计。这些因素既有技术性的,也有有机性的,因为你的操作符不仅需要与集群中的 Kubernetes 资源进行交互,还需要与工程师和管理员等人力资源进行互动。
本章将解释一些应该考虑纳入操作符设计的关键因素。首先,我们将介绍一些 Kubernetes 的原生组件和资源,这些资源是许多操作符交互的对象。通过查看操作符如何使用这些资源以及使用它们的用例,我们可以开始探索一些功能性操作符设计的模式。接下来,我们将研究如何在设计操作符时考虑其独特的用户群体,以及如何使设计对用户有利。最后,我们将讨论一些最佳实践,帮助操作符应对未来的持续发展。
在本章中,我们将涵盖以下主题:
-
与 Kubernetes 集群资源进行交互
-
确定你的用户和维护者
-
为你的操作符设计有益的功能
-
为操作符的演进变化进行规划
本章的目标是帮助你了解操作符的早期设计过程。若操作符的设计规划不当,可能会导致在操作符生命周期内需要进行大量的更改和更新。这会给工程资源带来压力,也可能会让用户感到困惑和沮丧。因此,本章的大部分内容将集中在非技术性细节上。然而,我们确实需要了解一些基本的技术交互,以便为我们的用户进行设计调整。在本章结束时,你可能会发现自己目前无法解决所有在此描述的预设计问题。这没关系——重要的是要理解在进入开发阶段时你必须意识到的某些概念。
与 Kubernetes 集群资源进行交互
在你决定如何从用户体验角度设计 Operator 之前,理解 Operator 的技术能力是非常重要的。了解 Operator 代码库的具体能力,有助于指导整个设计过程,明确哪些是实际可行的。否则,仅根据用户需求定义 Operator 的范围而忽视可行性,可能会导致承诺过多而功能和可用性不足。
任何 Operator 的可能性本质上受到 Kubernetes 平台底层特性的限制。该平台由不同的原生集群资源组成,其中一些资源你可能已经熟悉。本节将介绍 Operator 常用的资源,并解释如何以及为何使用它们。当你开发 Operator 时,这些资源通常会通过 Kubernetes 客户端库进行访问,这些库允许任何应用与集群资源进行交互。
Pods、ReplicaSets 和 Deployments
或许 Kubernetes 集群架构中最基本的单元就是 Pod。这些对象代表一个或多个正在运行的容器。从底层来看,Pod 本质上是容器镜像的定义,Kubelet 可以利用这些定义来指导容器运行时在哪里以及如何运行特定容器。应用程序以 Pods 形式部署在 Kubernetes 上,Kubernetes 本身由许多系统 Pods 组成,Operators 也作为 Pods 部署。
虽然 Pods 对 Kubernetes 集群至关重要,但它们通常过于原子化,难以手动管理。而且通常需要运行多个副本的应用程序,这时 ReplicaSets 就派上用场了。ReplicaSets 的作用是为某个 Pod 模板定义多个副本。
然而,ReplicaSets 的能力也有限。它们仅维护集群中 Pod 的副本数量。Deployments 通过包含 ReplicaSets 并定义更多控制功能,如发布策略和回滚修订管理,进一步增强了这一功能。
从 Operator 开发者的角度来看,Deployments 通常是最重要的交互资源。Deployments 的高级机制为应用程序的运行时提供了灵活性。这些机制可以从用户角度进行抽象或限制,因为用户将通过 Operator 的 CRD 与之交互(而不是直接使用 Deployment)。然而,灵活性依然存在,可以稍后将其添加或自动编程到 Operator 的调和逻辑中。
下图展示了应用工作负载 Pod 在 ReplicaSets 和 Deployments 中的封装,如何仍然可以通过 Operator 进行管理。在此示例中,Operator 只关注 Deployment,但该 Deployment 的状态反映了其中实际工作负载的健康状况:
图 2.1 – 一个操作符管理着一个包含两个 ReplicaSets(每个包含若干 Pods)的部署
操作符通常由部署安装和管理。部署提供了一个良好的所有者引用,用于所有构成操作符的资源组件的垃圾回收。这对于升级和卸载操作符非常有用。
尽管操作符直接管理某些 Pods 或 ReplicaSets 并不罕见,但确实存在权衡问题。首先,这样的操作符设计会导致应用程序架构更简单,但以牺牲使用部署(Deployments)时的能力和便利性为代价。因此,当你决定是否让操作符直接管理 Pods 或部署时,考虑应用程序本身的意图和设计需求非常重要。
自定义资源定义
正如我们在第一章中提到的,引入操作符框架,大多数操作符依赖于自定义资源定义(CRDs)。CRD 是本地 Kubernetes 资源,允许用户在运行时扩展 Kubernetes 平台的资源定义。它们就像 Kubernetes API 的插件。当在集群中安装 CRD 后,它会向 API 服务器提供如何处理和验证该自定义类型对象的信息。
操作符使用 CRD 的最常见用例是提供其配置对象作为其中之一。一旦 Kubernetes API 知道如何处理这个 CRD,操作符或用户将从 YAML 创建操作符对象(就像创建 Pod 或部署一样)。
用户可以仅与操作符资源进行交互,而不必手动调整操作符的部署设置。这允许开发人员展示精心设计的前端用户体验,并抽象化应用程序的 Kubernetes 内部实现,所有这些都可以在一个界面中进行,就像其他任何本地的 Kubernetes 资源对象一样。
但是,操作符不仅仅可以在 CRD 中打包它们的配置。根据所管理的应用程序,操作符还可以安装和监控应用程序所需的自定义资源。由于 CRD 的多种使用场景,应用程序提供和依赖其自定义资源对象并不罕见。一个应用程序操作符应该了解这些资源,并能够安装和管理它们。
CRD 是操作员开发的核心部分,因为如果没有 CRD,剩下的只有核心 Kubernetes 资源(如 Pods、ReplicaSets 和 Deployments),这些资源本身虽然能完成很多工作,但没有内在的灵活性,可以根据特定需求进行定制。操作员的自定义资源对象提供了一个用户界面,可以与集群中的其他 Kubernetes 对象无缝集成。此外,Kubernetes API 客户端和 Operator SDK 提供了代码工具,便于你与这些自定义资源交互,就像与任何其他集群资源一样。
ServiceAccount、角色和 RoleBinding(RBAC)
访问策略往往被忽视,但它们对于确保你和你用户集群的稳定性和安全性至关重要。就像应用程序管理其他集群组件一样,操作员也需要一定的 RBAC 策略来完成其在所需访问权限内的工作。
操作员需要 RBAC 策略来管理其操作对象。为 Kubernetes 中的任何应用定义 RBAC 策略时,首先需要定义一个角色。角色定义了应用或用户可以访问的 API 对象类型,以及允许与这些对象一起使用的操作动词。以下是一个角色的示例:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: my-operators-namespace
name: my-operator-role
rules:
- apiGroups:
- operator.sample.com
resources:
- "*"
verbs:
- "*"
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- watch
- list
这个示例创建了一个角色,允许操作员对指定命名空间中的 Pods 执行获取、观察和列出操作。它还允许操作员访问 operator.sample.com
API 组下的所有资源。这个例子用来说明操作员如何访问其 CRD。由于操作员与 CRD 之间没有任何固有的绑定,它仍然需要 RBAC 权限来访问该对象,就像访问任何其他 API 对象一样。在此情况下,CRD 会在该 API 组下创建。
角色可以限定在单一命名空间内,或者可以通过ClusterRoles被限定为集群角色。无论哪种方式,角色都会绑定到一个ServiceAccount,并通过RoleBinding(或ClusterRoleBinding)进行绑定。ServiceAccount 会在 Pods 上进行指定,以便完整地识别 Pod 在集群内具有的基于 RBAC 的访问权限:
图 2.2 – 集群角色与命名空间角色的对比图
角色和 ClusterRole 之间的区别取决于操作员在集群中需要的访问范围。具体来说,这主要取决于操作员、其操作对象(Operand)及其依赖项安装所在的命名空间。
命名空间
所有 Kubernetes 应用程序都在命名空间内运行。命名空间是一个逻辑分区,用于存放应用程序资源,它将各个组件隔离开来,并允许一定程度的访问控制。操作员也被部署在命名空间内,但有时它们还需要访问其他命名空间。
Operator 的命名空间覆盖范围被称为其作用域。Operator 可以是命名空间作用域(namespace-scoped),也可以是集群作用域(cluster-scoped)。当 Operator 是集群作用域时,它可以观察并与多个命名空间中的资源进行交互。相反,命名空间作用域的 Operator 仅安装在一个命名空间中,并管理该命名空间内的资源。开发者为其 Operator 选择的作用域取决于该 Operator 将要管理的资源。
如果 Operator 必须能够管理在任何命名空间中创建的资源,则应该选择集群作用域。这可以是管理在与 Operator 本身分开的命名空间中的操作数(Operand)的 Operator,或者它可能仅仅需要管理存在于多个命名空间中的操作数(例如一个资源供应器,用户请求部署开发环境)。
另一方面,命名空间作用域的 Operator 有一些优势。具体而言,将 Operator 限制在单个命名空间内便于调试和故障隔离。它还允许灵活的安装(包括在多个命名空间中安装多个相同的 Operator)。
Operator 的作用域最终由其 CRD 和访问角色来定义。集群作用域的自定义资源在任何特定命名空间之外创建。这对于那些在集群中只会有一个实例的 Operator 很有用(例如,管理核心 Kubernetes 组件的 Operator,如 API 服务器)。Operator 所需的访问角色通常会遵循其 CRD 的作用域。
总结一下,我们在这里介绍的资源(Pods、ReplicaSets、Deployments、CRDs、RBAC 和命名空间)只是 Operator 可能依赖的资源中的一小部分。然而,它们是最常见的,也是你在设计 Operator 集群交互时应该首先考虑的资源。在下一节中,我们将讨论 Operator 交互的另一面:它如何与人类交互。
识别用户和维护者
Operator 与 Kubernetes 交互的另一种方式是通过其用户。尽管 Operator 的存在是为了自动化许多人类所需的集群交互,但 Kubernetes 集群中的有机元素依然存在。用户仍然必须以某种方式与 Operator 进行交互,而不同用户希望和需要如何进行这种交互,会影响 Operator 的设计。
因此,识别你的 Operator 目标用户类型非常重要;它有几种不同的类别。每个类别对 Operator 的需求和看法都有所不同。因此,通过识别 Operator 的目标用户,你可以确保 Operator 设计得足够吸引最广泛的用户群体,并满足最多的使用场景。
对于大多数操作器,用户与之交互的类型可以根据这些用户在集群中的访问级别以及他们在整个应用程序中的角色分为几组。这些用户组通常如下:
-
集群管理员:负责集群维护和稳定性的管理员用户。
-
集群用户:具有集群内部访问权限的个人用户,例如团队中的工程师。
-
最终用户/客户:公共或企业产品的集群外部用户。这些用户不会直接与集群互动,但他们使用运行在集群上的应用程序。
这些类型的用户具有相似的动机和使用场景,但也存在一些明显的差异。逐个考虑每个群体有助于突显这些比较。
集群管理员
集群管理员通常是操作器的使用者。他们是对集群架构拥有最多访问权限和知识的用户。因此,他们需要最大的权限和灵活性。
首先,集群管理员的工作是确保集群的稳定性。这就是为什么他们需要尽可能多的工具和控制权限来根据需要修改集群设置的原因。但正如俗话所说,权力越大,责任越大。在这种情况下,操作器可以为管理员提供极大的权限,以根据他们的需要运行集群。但如果操作器没有按预期工作,这种权力可能会适得其反,甚至损害集群。
因此,打算由集群管理员使用的操作器开发者可能会考虑将其功能限制在已知可以支持的定义集内。这可以帮助他们设计操作器,使其不会变得过于强大,从而暴露出可能会无意中损害集群的设置。
然而,如果管理员发现他们的集群突然着火或严重故障,他们将需要尽快修复这个问题。如果这是由于操作器正在管理的组件(或操作器本身)引起的,那么管理员将需要直接访问操作器、操作对象或两者。在这种情况下,受限的功能集可能会延迟恢复时间,从而影响集群的稳定性恢复。
为集群管理员开发操作器的一个好处是,他们对自己的集群非常了解,并且拥有很高的内部访问权限。这提供了一定的信任,使得可以创建更强大的功能。此外,拥有统一的用户群体可以限制操作器需要支持的不同工作流程。
以下图表展示了一个内部集群管理员与操作器互动来管理其集群中操作对象的简单布局:
图 2.3 – 单一集群管理员通过 CRD 直接管理 Operator
最终,由 Operator 的开发人员来决定为集群管理员提供什么功能(如果集群管理员是目标用户的话)。根据 Operator 管理的应用程序,限制管理员通过 Operator 直接访问 Operand 可能是合理且安全的。但是,如果 Operand 部署可能会出错并需要手动干预,那么应提供紧急访问权限。
集群用户
集群的用户与集群进行交互,但不具备管理员的访问和控制权限。例如,这些可能是正在共享集群上工作的开发人员,他们需要按需部署和销毁某些资源。为开发资源提供服务涉及到用户从 Operator 请求这些资源。
集群用户对底层集群组件的控制要求较少。他们也不太可能接触到集群的某些部分,从而可能破坏集群。这两个因素为开发一个有助于限制这些用户支持的功能范围的 Operator 提供了保证。此外,这些用户如果需要更多的功能,可以通过集群管理员获得个人访问权限。
集群用户也可能通过内部应用程序与 Operator 进行交互。对于组织来说,在内部网络中部署定制的应用程序并不罕见。使用这样的内部应用程序可以为集群用户提供更友好的前端界面,同时也可以将集群用户的访问权限限制在底层 Operator 的实际能力范围内。
下图展示了一个多位内部用户使用 Operator 在集群中配置资源的设置。其中一些用户直接与 Operator 交互,而其他用户必须通过内部应用程序与 Operator 交互(Operator 甚至可能在管理这个前端应用程序)。在这种模式下,可以为使用 Operator 提供的内部工具的跨组织特权层级定义规则:
图 2.4 – 内部集群用户请求资源,既可以直接通过 Operator,也可以通过中介的内部应用程序
将 Operator 向内部集群用户开放的选项是非常灵活的。这些用户对于集群稳定性的信任和投入程度足够高,因此能有效降低疏忽或恶意行为的风险。然而,将应用程序管理控制权限暴露给更广泛的受众仍然存在一定的风险。因此,在设计 Operator 的功能时,必须考虑这一点。
最终用户和客户
产品的终端用户也可以从操作员中受益。这些用户可能甚至不知道他们正在与操作员互动,因为产品的架构设计通常对普通用户来说是不明显的。但了解产品用户期望其功能的方式仍然很重要,尤其是在我们设计产品关键组件的情况下。
这些终端用户可能不会直接与操作员互动。这意味着您的用户和客户自己能够访问 Kubernetes 集群,这对于安全性或可用性来说并不理想。应用程序的客户受益于交互式前端,无论是网站还是移动应用。但这个前端仅仅是与后端进行交互的工具,而后端可能由许多不同的组件组成,包括操作员。
在这种情况下,您的用户可能就是您自己——也就是说,您(或您的组织)将开发操作员,同时也可能开发依赖于该操作员的前端应用程序。在这种情况下,跨项目的协作对于阐明每个团队的需求和期望至关重要。在这种场景下,操作员将从优雅的 API 设计中受益最多,这种设计能更轻松地与其他程序进行通信,而非与人工用户互动:
图 2.5 – 外部终端用户通过外部应用程序与操作员互动
一个应用程序的终端用户通常与操作员的互动是完全隔离的。然而,这并不意味着一个应用程序及其资源不能仍然由操作员进行管理。在这种情况下,操作员仍然是维护集群状态的重要且有用的工具,即使这些终端用户不需要知道他们正在与操作员互动。
终端用户是我们将讨论的与操作员进行功能性互动的最终用户类型。然而,在设计操作员时,我们还必须考虑其他人:那些维护操作员及其代码的人。
维护者
最后一类将与您的操作员互动的用户与前三类有所不同。这些是项目的维护者,他们处理代码以解决问题并实施新特性。在任何软件项目中,维护者的角色都很重要,但在像 Kubernetes 这样的开源生态系统中,还有其他额外的考虑因素。
如果您的操作员源代码将开放并接受任何人的贡献,那么任命受信任的负责人来审查代码更改将是至关重要的。这对于任何开源项目都是如此,但对于操作员而言,审查人员熟悉任何操作员所依赖的核心 Kubernetes 概念将极为有利。
从企业角度来看,投资建立一个可靠的工程师团队来构建和维护 Operator,为你的维护者提供了一个长期的动力,促使他们继续开发 Operator 的源代码。这也在 Operator 的维护者与其主要利益相关者之间建立了信任关系。由于 Kubernetes 平台的不断变化,Operator 代码库的持续维护是必要的。熟悉 Kubernetes 社区的维护者是使用 Operator 的团队中一个重要的补充。
这些只是你的 Operator 可能与之互动的一些用户类型。这个列表并不详尽无遗,但目的是激发你对用户群体的思考。确定 Operator 的用户类型有助于缩小 Operator 需要哪些功能的范围。它也为确定哪些功能是必要的提供了一个起点。在接下来的部分,我们将探讨一些有助于设计对用户最有利功能的思路。
设计对你的 Operator 有益的功能
一旦你确定了 Operator 的目标用户群,下一步就是定义 Operator 的功能。列出 Operator 必须解决的问题将帮助你更好地理解项目的目标。它还将突出这些目标是否为用户提供了切实、可衡量的收益。
确定什么样的功能真正有益是很难界定的。“有帮助”的确切定义因案例而异。一些有益的 Operator 以新颖且直观的方式解决了广泛的难题。另一些则解决了更小众的问题,这些问题可能只影响小范围的用户社区,但消除这些问题的影响却是显著的。然而,描述有益功能时,从什么是不有用的方面来阐述会稍微容易一些。
首先,有用的 Operator 不是冗余的。在具体的示例中,冗余可能意味着很多不同的事情。但从根本上讲,Operator 设计者应该努力追求的是不重复的本质。最基本的例子是,不应该在没有充分理由的情况下重新编写一个已经存在的 Operator。研究你提议的 Operator 所涉及的领域可以防止这种情况发生。研究其他人已经采取的方法也能揭示出在过程中需要避免的潜在陷阱。
Operator 在 Kubernetes 概念上也可能是冗余的。由于 Operator 本质上是与集群进行交互并将一些交互暴露给用户,因此它是 Kubernetes 的扩展。因此,在为 Operator 思考功能时,很容易不自觉地去“重新发明轮子”。这种功能性冗余可能与我们接下来要讨论的另一个问题相关,即开发人员试图解决不存在的问题。
接下来的问题涉及有益的任务,这些任务并非假设性的。软件开发的世界从许多充满激情的工程师中受益,他们急于解决遇到的问题。对贡献的热情甚至可以延伸到解决潜在问题,即便这些问题尚未在实践中出现。这种对贡献的热情当然是一个很好的心态,绝不应当被压制。当然,确实存在许多类型的问题和 bug 可以被发现并且应该被修复——希望是在生产环境中没有人遇到它们之前。但有时,针对纯粹理论性问题也会提出真实的解决方案。
这些假设性的使用场景可能在没有实际证据表明用户需要这样的解决方案时被提出。这可能是个棘手的问题,因为有时,使用场景看起来足够明显,似乎不需要任何明确的需求。但当每个新特性都需要投入资源来实现,并且将维护人员绑定于支持它时,密切关注“沉默”可以揭示出不必要的工作。
虽然可能会有许多突破性的特性创意,但验证特性提案是否有真实需求非常重要。这种类型的研究还可以揭示想法中的冗余,进而提出已经存在的、更有效的替代解决方案。
当然,有无限多的标准可以用来定义特性是否有用。然而,在 Kubernetes Operator 的上下文中,这些只是一些起步的初步想法。通过从它的益处角度考虑你的 Operator,并问自己“这有什么用?”你可以避免进行过度或不必要的更改,这些更改日后可能需要维护。
这种前瞻性思维将对你的 Operator 在发展和演进过程中非常有用。像大多数软件一样,Operators 随着时间的推移容易发生变化,尤其是它们依赖于上游的 Kubernetes 代码库。在接下来的部分中,我们将学习如何为这些变化做好准备,并在变化到来时进行应对。
为你的 Operator 的变更做好规划。
在任何软件项目的生命周期中,代码库都会发生变化。这些变化包括修复 bug、重构、添加新特性以及删除旧特性。这对 Kubernetes、其子项目以及建立在该平台上的项目(如 Operators)都适用。
尽管无法预测你的 Operator 将来会发展成什么样子,但在设计阶段有一些想法可以帮助你顺利过渡并应对之后的新发展。具体如下:
-
从小开始。
-
有效迭代。
-
优雅地弃用。
当你维护一个 Operator 时,这些决策在实践中会带来显著的好处。然而,这些并不是绝对的,也并非严格限定于 Kubernetes Operator。你可以将它们视为开发任何软件项目的一般建议。这里,我们将从编写 Operator 的角度来审视这些建议。
从小做起。
在规划 Operator 的原始设计时,考虑 Operator 随时间变化的可能性可以避免未来的挑战。以增长为导向的设计还可以帮助限制项目的初始范围,使其仅涵盖必要的目的。这使得能够为开发一个强大的首个产品分配足够的资源,并且该产品能够在时间上高效迭代。
虽然看起来开发 Operator 的最佳目标是从尽可能多的功能开始,但实际上,往往相反。毕竟,Operator 的一个目的就是在用户和底层集群功能之间创建一个抽象层。暴露过多的选项可能会让你失去自动化的好处,因为这会要求用户理解更多的本地 Kubernetes 集群功能。这也让你的用户在 Kubernetes 平台发生变化时更加脆弱,这些变化本不该直接影响他们。
在决定是否为 Operator 添加某个功能时,需批判性地思考该功能的重要性,以及实现它的风险和成本。这与避免冗余和解决假设性问题的主题相关,这些内容我们在上一节中已经探讨过。同时,这也与即将讨论的“弃用”主题相关。
最终,大多数时候,缺乏功能比发布一个复杂且令人困惑的 Operator 更好。每新增一个功能都会带来维护成本,并且可能在后期引入漏洞。这些都是从小做起并根据用户反馈进行开发的最大原因之一。
高效迭代
一旦你拥有了一组最小功能,最终就需要进行改进。幸运的是,添加新功能比移除旧功能要容易得多。
你应该积极寻求用户对所需功能的反馈。这可以通过多种方式实现,从在 Operator 及其相关项目周围保持活跃的在线存在,到在 Operator 的代码中实施详细的使用度量。此外,了解更广泛的 Kubernetes 项目社区也能帮助你关注潜在的底层平台变化,从而支持你的 Operator。
在你添加功能的同时,继续监控它们的使用情况,以衡量它们的效果。牢记最初设计 Operator 时的指导方针,例如其用户群体和功能提示,也能确保每个新功能的效果与最初设计的功能集一样有效。
优雅地弃用
一种对用户非常不便的变化是弃用。许多大型软件项目的部分功能不可避免地会被替换,或者官方维护者会停止对其的支持。通过最小化的 Operator 设计,你可以最大限度地减少用户体验被更改的可能性。
从小范围的设计开始,并在此基础上进行深思熟虑的有效迭代,将有助于减少删除功能的需求。不幸的是,某些时候这一点可能是不可避免的。在这种情况下,重要的是提前给予用户足够的通知,以便他们有足够的时间过渡到替代方案(如果适用的话)。
Kubernetes 社区已经定义了废弃政策,这将在第八章《为你的 Operator 准备持续维护》中详细介绍。尽管如此,这些指南作为废弃处理的良好模板仍然很有价值。Kubernetes 用户对废弃的时间线和流程已经非常熟悉,因此,如果你遵循这些流程,你的用户将受益于他们已经熟悉的方式。
就像所有美好的事物都必须走到尽头一样,一些优秀的功能最终也必须被淘汰。幸运的是,对于 Kubernetes 社区来说,这并不是一个陌生的概念,它提供了一个成熟的废弃处理模板。作为开源社区的责任成员,你和你的用户将从一种尊重且体贴的方式中受益,这种方式有助于移除和更改项目中的某些方面。
在设计 Operator 时,牢记这些实践将有助于限制项目的范围,从而让资源能够专注于构建一个稳定的工具,避免随时间剧烈变化。用户和维护者都能从这种稳定性中受益,用户能够在不改变工作流程的情况下,无缝使用不同版本的 Operator,而维护者则可以投入长期精力,发展项目并将相关知识传授给他人。
概述
本章重点讨论了 Operator 与 Kubernetes 集群互动的不同方式。除了 Operator 代码库与集群原生资源之间的字面技术交互外,我们还探讨了一些在设计 Operator 时值得考虑的其他交互。这些交互包括 Operator 的用户以及 Operator 随时间变化的生命周期。
Kubernetes 集群包含多种不同类型的原生资源。这些资源是所有部署在 Kubernetes 上的应用程序的基本构建块。从这一点来看,Operator 与任何其他应用程序没有不同,它们必须能够原生地使用这些资源。本章重点介绍了几种 Operator 资源的分解,包括 Pods、Deployments、CRDs 和 RBAC 策略,以便你了解如何定义 Operator 如何使用这些资源。
人类与 Operator 的互动是设计时最重要的概念之一。Operator 的设计目的是通过自动化各种任务为人类服务,但像任何自动化人工劳动的工具一样,它们仍然需要人为的输入来操作,并为人类生成输出。因此,我们讨论了为不同类型的用户构建 Operator 时需要考虑的各种用户类型及其独特的需求和期望。
最后,我们介绍了一些关于功能设计的好方法。为了帮助你成功搭建一个 Operator,我们讨论了制定初步设计的想法,以便为用户提供实际的好处。然后,我们提出了一些你在 Operator 演化过程中需要记住的概念。
在下一章,我们将把这些知识应用到实际的 Operator 设计中。我们将从构建一个示例 Operator 开始,首先设计它的 CRD、API 和协调循环。在这里,我们将开始构建一个实际的 Operator,并在本书的剩余部分进行编码和部署。
第二部分:设计与开发 Operator
在本节中,您将学习如何创建自己的 Operator,从一般的架构最佳实践到具体的技术代码示例。
本节包括以下章节:
-
第三章,设计 Operator —— CRD、API 与目标协调
-
第四章,使用 Operator SDK 开发 Operator
-
第五章,开发 Operator —— 高级功能
-
第六章,构建与部署您的 Operator
第三章:第三章:设计一个操作符 – CRD、API 和目标协调
前几章的内容帮助我们理解了操作符框架的基础知识。在第一章《介绍操作符框架》中,我们讲解了操作符框架的概念支柱及其所服务的目的。接着,在第二章《理解操作符如何与 Kubernetes 交互》中,我们讨论了在Kubernetes和操作符框架背景下的某些软件设计原则。这些章节共同奠定了对操作符及其开发的基本理解。现在,我们将通过实例应用这些知识,开始设计我们自己的操作符。
我们将从定义一个简单的问题开始,该问题是我们的操作符将要解决的。在这种情况下,就是管理一个仅包含单个 Pod 的应用程序的基本部署。在接下来的几章中,我们将通过具体的代码示例为该操作符添加功能,但在开始编写我们的示例操作符之前,我们必须首先完成设计过程。通过具体的例子构建我们已讨论的通用定义和步骤,将为我们将早期的课程内容转化为实际应用提供上下文。
该过程将包括几个不同的步骤,以便阐明我们操作符的核心方面。这些步骤将包括绘制应用程序编程接口(API)、自定义资源定义(CRD)以及使操作符正常工作的协调逻辑。在此过程中,这些步骤将与之前讨论的内容以及行业标准的操作符最佳实践相联系。我们将把这个过程分解为以下几个步骤:
-
描述问题
-
设计 API 和 CRD
-
与其他必需资源的协作
-
设计目标协调循环
-
处理升级和降级
-
使用故障报告
除了一些适用的YAML 不是标记语言(YAML)代码片段外,我们还不会开始编写任何实际的代码。然而,我们将使用一些伪代码来更好地展示一旦我们用操作符软件开发工具包(SDK)初始化项目后,实际代码将如何工作,具体内容将在第四章《使用操作符 SDK 开发操作符》中进行讲解。
描述问题
许多软件项目都可以通过以下格式的用户故事来定义:作为一个[用户],我希望[动作],以便[原因]。 我们在这里也会这样做,具体如下:
作为集群管理员,我希望使用操作符来管理我的 nginx 应用程序,以便自动管理它的健康状况和监控。
对于我们的用例(设计一个 Operator),我们现在不关心具体的应用程序。因此,我们的应用程序将只是一个基本的 nginx 示例 Pod。我们将假设这代表任何具有基本输入/输出(I/O)需求的单 Pod 应用程序。虽然这看起来有些抽象,但重点将放在围绕应用程序构建 Operator 上。
我们从这个用户故事中确定的第一件事是,我们将为集群管理员构建这个 Operator。从上一章中,我们知道这意味着 Operator 的用户比大多数最终用户更了解集群架构,并且他们需要对 Operand 拥有更高层次的直接控制。我们还可以假设,大多数集群管理员会更愿意直接与 Operator 交互,而不是通过中介前端应用程序。
这个用户故事的第二部分明确了 Operator 的功能目标。具体来说,这个 Operator 将管理单 Pod 应用程序的部署。在这种情况下,管理是一个模糊的术语,我们将假设它意味着创建并维护运行应用程序所需的 Kubernetes 资源。这些资源至少包括一个 Deployment。我们需要通过 Operator 暴露这个 Deployment 的一些选项,例如 nginx 的容器端口。
最后,用户故事提供了我们运行 Operator 的动机。集群管理员希望 Operator 管理应用程序的健康状况和监控。应用程序健康可能意味着许多不同的事情,但通常来说,这归结为保持应用程序的高可用性,并尽可能从任何崩溃中恢复。监控应用程序也可以通过多种方式进行,通常以度量指标的形式呈现。
因此,从以上所有信息中,我们已经确定我们需要一个非常基础的 Operator,能够执行以下操作:
-
部署一个应用程序
-
如果应用程序失败,保持其运行
-
报告应用程序的健康状态
这些是 Operator 可以提供的一些最简单的功能。在后面的章节中,我们将对这些请求进行更多扩展。但为了从一个坚实的基础开始,以便以后迭代,这将是我们的最小可行产品(MVP)。因此,从现在开始,这将是我们在引用示例时所参考的基本 Operator 设计。
基于这些标准,我们可以尝试按照 第一章,介绍运算符框架 中涵盖的能力模型来定义我们的运算符(回顾一下,能力模型定义了运算符功能的五个级别,从 基本安装 到 自动驾驶)。我们知道,运算符将能够安装操作数,并管理任何额外所需的资源。我们希望它还能报告操作数的状态,并通过其 CRD 提供配置。这些都是 Level I 运算符的标准。此外,如果我们的运算符能够处理升级,那么它将符合 Level II 运算符的标准。
这是初始运算符设计的良好起点。有了我们试图解决的问题的全貌,我们现在可以开始头脑风暴,考虑我们将如何解决它。为此,我们可以从设计我们的运算符在集群 API 中的表示开始。
设计 API 和 CRD
正如我们在 第一章,介绍运算符框架,和 第二章,理解运算符如何与 Kubernetes 交互 中所介绍的,CRD 的使用是运算符的一个定义特征,用于创建用户交互的对象。这个对象为控制运算符创建了一个接口。通过这种方式,自定义资源(CR)对象就成为了运算符主要功能的窗口。
就像任何良好的窗户一样,运算符的 CRD 必须设计得很好。它必须足够清晰,以便暴露运算符的细节,同时又足够安全,以防止恶劣天气和入室盗窃。就像一个窗户一样,CRD 的设计应该遵循本地的建筑规范,以确保其按照环境的预期标准建造。在我们的案例中,这些建筑规范就是 Kubernetes API 的惯例。
遵循 Kubernetes API 设计惯例
尽管 CRD 是任何人都可以创建的自定义对象,仍然有一些最佳实践需要牢记。这是因为 CRD 存在于 Kubernetes API 中,该 API 定义了其惯例,因此在与 API 交互时有一定的期望。这些惯例在 Kubernetes 社区的文档中有所记录:github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
。然而,这份文档非常详尽,涵盖了所有种类 API 对象的要求,而不仅限于运算符的 CRD。然而,有几个关键要素对我们的目的是相关的,如下所述(注意:本章节稍后将更详细地讨论一些字段):
-
所有 API 对象必须包含两个字段:
kind
和apiVersion
。这两个字段使得 Kubernetes API 客户端能够解码对象。kind
表示对象类型的名称——例如,MyOperator
——而apiVersion
是该对象的 API 版本。例如,您的 Operator 可能会提供v1alpha1
、v1beta1
和v1
版本的 API。 -
API 对象还应包含以下字段(尽管它们不是必需的):
-
resourceVersion
和generation
,这两个字段有助于跟踪对象的变化。然而,这些字段的用途不同。resourceVersion
字段是一个内部引用,每次对象被修改时都会递增,用于帮助并发控制。例如,当尝试更新一个 Operand 部署时,您将进行两次客户端调用:Get()
和Update()
。在调用Update()
时,API 可以检测到对象的resourceVersion
字段是否已在服务器上发生变化(这表明其他控制器在我们更新之前已修改了该对象),如果是,则拒绝更新。相比之下,generation
用于跟踪对象的相关更新。例如,最近推出新版本的 Deployment 将会更新其generation
字段。这些值可用于引用旧的 generation,或者确保新的 generation 是预期的 generation 编号(即 current+1)。 -
creationTimestamp
和deletionTimestamp
,这两个字段有助于参考对象的年龄。例如,通过creationTimestamp
,您可以轻松根据 CRD 创建时间引用 Operator 部署的年龄。类似地,deletionTimestamp
表示该对象已向 API 服务器发送了删除请求。 -
labels
和annotations
,它们的作用相似,但在语义上有所不同。将labels
应用于一个对象,可以通过 API 轻松筛选出符合条件的对象,从而实现组织管理。另一方面,annotations
提供了关于对象的元数据。
-
-
API 对象应包含
spec
和status
字段。我们将在本章稍后部分更详细地讨论status
(在 使用故障报告 部分),但目前,需要牢记一些关于它的约定,如下所示:-
在对象的
status
字段中报告的条件应当能够清晰自解释,无需额外的上下文来理解它们。 -
条件应遵循与其他字段相同的 API 兼容性规则。为了保持向后兼容,条件定义一旦确定不应再更改。
-
条件可以报告
True
或False
作为其正常操作状态。对于哪个应作为标准模式,并没有固定的指导方针;开发者应根据条件定义中的可读性来做出考虑。例如,Ready=true
条件的含义与NotReady=false
相同,但前者更容易理解。 -
条件应表示集群的当前已知状态,而不是报告状态之间的过渡。正如我们在 设计目标协调循环 部分中所涵盖的,许多 Kubernetes 控制器是使用级别触发设计编写的(意味着它们基于集群的当前状态而不是仅仅基于传入事件进行操作)。因此,Operator 基于其当前状态报告条件,有助于保持这种相互设计假设,即可以随时在内存中构建集群的当前状态。然而,对于长期过渡阶段,如果需要,可以使用
Unknown
条件。
-
-
API 对象中的子对象应该表示为列表,而不是映射;例如,我们的 nginx 部署可能需要通过 Operator CRD 指定多个不同的命名端口。这意味着它们应该表示为列表,列表中的每个项目都有
name
和port
字段,而不是每个条目的name
作为映射中的键。 -
可选字段应该实现为指针值,以便轻松区分零值和未设置值。
-
带有单位的字段的约定是将单位包括在字段名称中,例如
restartTimeoutSeconds
。
这些只是许多 API 约定中的一部分,但它们在设计 Operator 的 CRD 和 API 时非常重要。遵循 API 设计的指导方针,确保 Kubernetes 生态系统中的其他组件(包括平台本身)可以对你的 Operator 做出适当的假设。牢记这些指导方针,我们可以进入设计我们自己 CRD 模式的下一步。
理解 CRD 模式
我们已经讨论了 CRD 及其对 Operator 的重要性,但到目前为止,我们还没有深入探讨 CRD 的组成方式。现在,我们已经知道了我们的示例 Operator 要解决的问题,我们可以开始查看我们希望通过 CRD 暴露的选项,并了解这些选项对用户的表现形式。
首先,最好查看一个示例 CRD,并检查每个部分以了解其目的,如下所示:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myoperator.operator.example.com
spec:
group: operator.example.com
names:
kind: MyOperator
listKind: MyOperatorList
plural: myoperators
singular: myoperator
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
...
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
前两个字段,apiVersion
和 kind
,定义了这是一个 CRD。即使我们试图定义我们自己自定义对象的蓝图,这个蓝图首先必须存在于 CustomResourceDefinition
对象中。从这里,API 服务器会知道如何解析 CRD 数据,以创建我们 CR 的实例。
接下来,metadata.Name
字段定义了我们 CRD 的名称(不是从 CRD 创建的自定义对象)。更具体地说,这是蓝图的名称,而不是从蓝图创建的对象。例如,我们可以使用 kubectl get crd/myoperator.operator.example.com
来检索这个 CRD 设计。
在 spec
中,CRD 开始实际定义我们要创建的 CR 对象。group
定义了一个自定义 API 组,新对象将属于该组。这里使用唯一名称有助于避免与集群中其他对象和 API 的冲突。
names
部分定义了我们对象的不同引用方式。在这里,只有 kind
和 plural
是必需的(因为其他的可以从这两个推断出)。就像集群中任何其他类型的对象都可以通过其 kind
或 plural
形式进行访问(例如,kubectl get pods
),我们的 CR 也将通过类似的命令进行访问,如 kubectl edit myoperator/foo
。尽管大多数操作员(并且应该)在集群中只拥有一个 CR 对象,这些字段仍然是必需的。
接下来,scope
定义了自定义对象的范围,可以是命名空间范围或集群范围。这两者之间的区别在 第一章 中已经详细介绍过,引入操作员框架。此字段的可选值为 Cluster
和 Namespaced
。
Versions
提供了我们 CR 可用的不同 API 版本的列表。随着操作员的演进以及新功能的添加或移除,您将需要引入新的操作员 CR 版本。为了向后兼容和支持,您应该继续发布旧版本的资源,以便为用户提供一个过渡期,之后该版本可以安全地弃用。这就是为什么此字段提供版本列表的原因。API 服务器知道每个版本,并能够有效地处理在该列表中创建和使用的任何版本的对象。
列表中的每个版本都包含关于对象本身的架构信息,用于唯一标识该版本在openAPIV3Schema
中的结构。在这个示例中,openAPIV3Schema
部分故意被省略了。我们之所以这样做,是因为该部分通常非常长且复杂。然而,在 Kubernetes 的最新版本中,这个部分是必须的,以便为 CRD 提供结构化架构。
结构化架构是基于OpenAPI 版本 3 (V3) 验证的对象架构。OpenAPI 定义了每个字段的验证规则,这些规则可以在创建或更新对象时用于验证字段数据。这些验证规则包括字段的数据类型,以及其他信息,如允许的字符串模式和枚举值。CRD 的结构化架构要求确保对象的表示在存储时是一致和可靠的。
由于 OpenAPI 验证架构的复杂性,不建议手写它们。建议使用像Kubebuilder(Operator SDK 使用的工具)这样的生成工具。可以直接在 Go 类型上定义全面且灵活的验证规则,使用各种 Kubebuilder 标记,这些标记的完整参考文档可以在book.kubebuilder.io/reference/generating-crd.html
找到。
个别版本定义的下一个部分是served
和storage
,它们决定了该版本是否通过 REST API 提供服务,以及是否是应该用作存储表示的版本。每个 CRD 只能设置一个版本作为存储版本。
最后的部分是subresources
和status
,它们是相关的,因为它们定义了一个status
字段,该字段用于报告 Operator 当前状态的信息。我们将在使用故障报告部分更详细地介绍该字段及其用途。
现在,我们已经了解了 CRD 的结构,并且知道一个 CRD 应该是什么样的,我们可以为我们的示例 nginx Operator 设计一个 CRD。
示例 Operator CRD
从前面的需求分析中,我们知道我们的 Operator 最初只会在集群中部署一个 nginx 实例。我们现在也知道,Operator 的 CRD 将提供一个spec
字段,里面包含各种控制操作数部署的选项。但我们应该暴露什么样的设置呢?我们的操作数比较简单,所以我们从定义一些基本选项开始,用来配置一个简单的 nginx Pod,如下所示:
-
port
—这是我们希望在集群内暴露 nginx Pod 的端口号。由于 nginx 是一个 Web 服务器,这将允许我们修改可访问的端口,而无需直接操作 nginx Pod,因为 Operator 将为我们安全地处理这个变更。 -
replicas
—这个字段有点多余,因为 Kubernetes 部署的副本数可以通过部署本身进行调整。但为了将操作数的控制抽象到 Operator 的用户界面(UI)背后,我们提供了这个选项。这样,管理员(或其他应用程序)可以在 Operator 的处理和报告下扩展操作数。 -
forceRedeploy
—这个字段很有趣,因为它实际上对操作数(Operand)来说是无操作(no-op),即它的功能没有任何改变。然而,在 Operator 的 CRD 中包括一个可以设置为任意值的字段,可以让我们指示 Operator 触发操作数的重新部署,而不需要修改任何实际的设置。这个功能对于卡住的部署(Deployment)非常有用,手动干预通常可以解决这个问题。
之所以可行,是因为操作员监视集群中相关资源的变化,其中之一就是它自己的 CRD(更多内容请参见设计目标调和循环部分)。这种监视是必要的,以便操作员知道何时更新操作数。因此,包含一个无操作字段就足够让操作员知道重新部署操作数,而无需进行任何实际更改。
这三项设置将构成我们操作员 CRD spec
的基础。有了这个,我们知道作为集群中的对象,CR 对象将如下所示:
apiVersion: v1alpha1
kind: NginxOperator
metadata:
name: instance
spec:
port: 80
replicas: 1
status:
...
请注意,这里展示的是 CR 本身,而不是 CRD,如前面示例所示。我们在这里使用通用的 name: instance
值,因为我们可能一次只会在一个命名空间中运行一个操作员实例。我们也没有在这里包含 forceRedeploy
字段,因为该字段是可选的。
如果我们正确地定义了 CRD,可以通过 kubectl get -o yaml nginxoperator/instance
命令来检索此对象。幸运的是,Operator SDK 和 Kubebuilder 将帮助我们生成该内容。
与其他必需资源的协作
除了 CRD,我们的操作员还将负责管理许多其他集群资源。目前,这是作为我们的操作数将要创建的 nginx Deployment,以及用于操作员的 ServiceAccount、Role 和 RoleBinding。我们需要理解的是,操作员将如何知道这些资源的定义。
在某处,这些资源需要作为 Kubernetes 集群对象书写。就像你手动创建 Deployment 一样(例如,使用 kubectl create -f
),所需资源的定义可以通过几种不同的方式与操作员代码打包。这可以通过模板轻松完成,如果你使用 Helm 或 Ansible 创建操作员,但是对于用 Go 编写的操作员,我们需要考虑我们的选择。
将这些资源打包以便操作员可以创建它们的一种方法是直接在操作员的代码中定义它们。所有 Kubernetes 对象都基于相应的 Go 类型定义,因此我们有能力通过将资源声明为变量,直接在操作员中创建 Deployments(或任何资源)。以下是一个示例:
…
import appsv1 "k8s.io/api/apps/v1"
…
nginxDeployment := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
apiVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "nginx-deploy",
Namespace: "nginx-ns",
},
Spec: appsv1.DeploymentSpec{
Replicas: 1
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app":"nginx"},
},
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
ObjectMeta: metav1.ObjectMeta{
Name: "nginx-pod",
Namespace: "nginx-ns",
Labels: map[string]string{"app":"nginx"},
},
Containers: []v1.Container{
{
Name: "nginx",
Image: "nginx:latest",
Ports: []v1.ContainerPort{{ContainerPort: int32(80)}},
},
},
},
},
},
}
以这种方式在代码中定义对象的便利性对开发非常有帮助。这种方法提供了透明的定义,Kubernetes API 客户端可以清楚地访问并立即使用它。然而,这也有一些缺点。首先,这种方式并不十分人性化。用户通常会与以 YAML 或 JavaScript 对象表示法(JSON)表示的 Kubernetes 对象进行交互,而这些表示法中每个字段的类型定义并不存在。这些信息对于大多数用户来说是不必要且多余的。因此,任何希望清晰查看资源定义或修改它们的用户可能会在操作员的代码中迷失。
幸运的是,直接将资源定义为 Go 类型还有另一种选择。有一个非常有用的包叫做go-bindata
(可在github.com/go-bindata/go-bindata找到),它将声明性 YAML 文件编译成 Go 二进制文件,使得它们可以通过代码访问。Go 的新版本(1.16+)现在也包含了 go:embed
编译指令,可以在不使用像 go-bindata
这样的外部工具的情况下实现这一点。因此,我们可以像这样简化前面的部署定义:
kind: Deployment
apiVersion: apps/v1
metadata:
name: nginx-deploy
namespace: nginx-ns
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
这种方式对普通用户来说更加可读。它也更容易维护,您可以在操作员的代码库中为不同版本的各种资源提供命名目录。这对代码组织很有好处,同时也简化了您对持续集成(CI)检查这些类型定义有效性的选项。
我们将在第四章,使用 Operator SDK 开发 Operator 中更详细地讲解如何使用 go-bindata
和 go:embed
,但现在,我们已经知道如何将额外的资源打包,以便在操作员中使用。这是一个关键的设计考虑因素,既有利于我们的用户,也有利于维护人员。
设计目标协调循环
现在,我们已经通过设计一个 CRD 来表示操作员的 UI,并列出了它将管理的操作数资源,接下来可以进入操作员的核心逻辑。这个逻辑嵌入在主协调循环中。
如前几章所述,操作员的工作原理是根据将集群的当前状态与用户设置的期望状态进行协调。它们通过定期检查当前状态来实现这一点。这些检查通常由与操作数相关的某些事件触发。例如,操作员将监视其目标操作数命名空间中的 Pod,并对 Pod 的创建或删除做出反应。由操作员开发人员来定义哪些事件对操作员来说是重要的。
基于层级与基于边缘的事件触发
当事件触发操作员的协调循环时,逻辑不会接收到整个事件的上下文。相反,操作员必须重新评估集群的整个状态,以执行其协调逻辑。这被称为基于层级的触发。这种设计的替代方案是基于边缘的触发。在基于边缘的系统中,操作员逻辑只会在事件本身上起作用。
这两种系统设计的权衡在于效率与可靠性之间的选择。基于边缘的系统更高效,因为它们不需要重新评估整个状态,只能对相关信息做出反应。然而,基于边缘的设计可能会遭遇不一致和不可靠的数据问题——例如,如果事件丢失。
另一方面,基于级别的系统始终意识到系统的整个状态。这使得它们更适合用于大规模的分布式系统,如 Kubernetes 集群。虽然这些术语最初来源于与电子电路相关的概念,但它们在上下文中与软件设计也有很好的关系。更多信息请访问venkateshabbarapu.blogspot.com/2013/03/edge-triggered-vs-level-triggered.html
。
理解这些设计选择之间的差异使我们能够思考对账逻辑如何运作。通过采用基于级别的触发方法,我们可以确保操作员不会丢失任何信息或错过任何事件,因为它内存中的集群状态表示最终总会追赶到现实。然而,我们必须考虑实现基于级别设计的要求。具体来说,操作员每次触发事件对账时,必须具备构建整个相关集群状态所需的信息。
设计对账逻辑
对账循环是操作员的核心功能。当操作员接收到事件时,正是这个函数被调用,并且这是编写操作员主要逻辑的地方。此外,这个循环理想情况下应该设计为管理一个 CRD,而不是让单个控制循环承担多个责任。
当使用 Operator SDK 来搭建 Operator 项目时,对账循环的函数签名将是这样的:
func (r *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)
这个函数是一个Controller
对象的方法(这个名字可以是任意的;在本示例中我们使用Controller
,但它也可以是FooOperator
)。这个对象将在操作员启动时实例化。然后它接受两个参数:context.Context
和ctrl.Request
。最后,它返回一个ctrl.Result
参数,如果适用的话,还会返回一个error
参数。我们将在第四章《使用 Operator SDK 开发 Operator》中详细讲解这些类型及其具体作用,但目前,请理解,操作员的核心对账循环是基于非常有限的关于触发对账事件的信息构建的。请注意,操作员的 CRD 和集群状态信息并不会传递给这个循环;也没有其他任何信息。
因为操作员是基于级别驱动的,所以Reconcile
函数应该构建集群本身的状态。在伪代码中,这通常看起来像这样:
func Reconcile:
// Get the Operator's CRD, if it doesn't exist then return
// an error so the user knows to create it
operatorCrd, error = getMyCRD()
if error != nil {
return error
}
// Get the related resources for the Operator (ie, the
// Operand's Deployment). If they don't exist, create them
resources, error = getRelatedResources()
if error == ResourcesNotFound {
createRelatedResources()
}
// Check that the related resources relevant values match
// what is set in the Operator's CRD. If they don't match,
// update the resource with the specified values.
if resources.Spec != operatorCrd.Spec {
updateRelatedResources(operatorCrd.Spec)
}
这将是我们操作员对账循环的基本布局。将这个过程分解成步骤,过程是这样的:
-
首先,检查是否存在现有的操作符 CRD 对象。正如我们所知道的,操作符 CRD 包含了操作符如何工作的配置设置。最佳实践是操作符不应管理自己的 CRD,因此如果集群中没有 CRD,则应立即返回错误。该错误将显示在操作符的 Pod 日志中,提示用户应该创建 CRD 对象。
-
其次,检查集群中是否存在相关资源。对于我们的当前用例,这将是操作数部署。如果部署不存在,那么创建它将是操作符的职责。
-
最后,如果相关资源已经存在于集群中,则需要检查它们是否根据操作符 CRD 中的设置进行配置。如果没有,那么就需要使用预定的值更新集群中的资源。虽然我们可以每次直接更新(因为我们知道期望的状态,而无需查看当前状态),但最好先检查差异。这有助于减少不必要的 API 调用,避免盲目更新。不必要的更新还会增加更新热循环的风险,导致操作符对资源的更新创建事件,触发处理该对象的调和循环。
这三个步骤在很大程度上依赖于通过标准 API 客户端访问 Kubernetes API。操作符 SDK 提供了帮助简化实例化这些客户端并将其传递给操作符控制循环的函数。
处理升级和降级
作为操作符开发者,我们关注的是两个主要应用程序的版本控制:操作数(Operand)和操作符本身。无缝升级也是 II 级操作符的核心功能,我们已经决定将其作为初始操作符设计的目标。因此,我们必须确保我们的操作符能够处理自身以及 nginx 操作数的升级。就我们的用例而言,升级操作数相对简单。我们可以直接拉取新的镜像标签并更新操作数部署。然而,如果操作数发生了显著变化,那么操作符可能也需要更新,以便能够正确管理新的操作数版本。
操作符升级通常是在需要将操作符代码、API 或者两者的更改交付给用户时发生的。操作符生命周期管理器(OLM)使得从用户角度来看,升级到更新版本的操作符变得容易。操作符的ClusterServiceVersion(CSV)允许开发者为维护者定义特定的升级路径,提供有关替代旧版本的新版本的具体信息。这将在第七章《使用操作符生命周期管理器安装和运行操作符》中详细介绍,届时我们将为操作符编写一个 CSV 文件。
也可能出现操作员的 CRD 以不兼容的方式发生变化的情况(例如,某个旧字段的弃用)。在这种情况下,操作员的 API 版本应该增加(例如,从v1alpha1
到v1alpha2
或v1beta1
)。新版本还应该与现有版本的 CRD 一起发布。这就是为什么 CRD 的versions
字段是一个版本定义列表,并且通过同时支持两个版本,它允许用户从一个版本过渡到下一个版本的原因。
然而请记住,在这些版本列表中,可能只有一个是指定的存储版本。同时,永远发布每个先前的 API 版本也是过度的(最终,旧版本需要在适当的弃用时间线之后完全移除)。当是时候永久移除对弃用的 API 版本的支持时,存储版本可能也需要更新。这可能会对仍然在集群中安装旧版本操作员 CRD 作为存储版本的用户造成问题。kube-storage-version-migrator
工具(github.com/kubernetes-sigs/kube-storage-version-migrator
)通过为集群中现有对象提供迁移过程来帮助解决这个问题。存储版本可以通过Migration
对象迁移,例如:
apiVersion: migration.k8s.io/v1alpha1
kind: StorageVersionMigration
metadata:
name: nginx-operator-storage-version-migration
spec:
resource:
group: operator.example.com
resource: nginxoperators
version: v1alpha2
当创建这个对象时,kube-storage-version-migrator
会看到它,并将集群中存储的任何现有对象更新到指定版本。此操作只需要执行一次,甚至可以通过将此对象打包为操作员的附加资源来实现自动化。未来版本的 Kubernetes 将完全自动化这个过程(见 KEP-2855,github.com/kubernetes/enhancements/pull/2856
)。
提前为成功的版本过渡做好准备,将有助于未来操作员的维护。然而,并不是所有的事情都能顺利进行,无法为每个可能的场景做好准备。这就是为什么操作员需要具有足够的错误报告和处理机制的重要原因。
使用故障报告
说到故障,我们需要担心两件事:操作对象中的故障,以及操作员本身的故障。有时,这两者可能甚至是相关的(例如,操作对象以操作员无法解决的意外方式失败)。当发生任何错误时,操作员的一个重要任务是将这些错误报告给用户。
当操作员的协调循环中发生错误时,操作员必须决定接下来该怎么做。在使用 Operator SDK 的实现中,协调逻辑能够识别错误发生的时刻,并尝试重新执行循环。如果错误持续导致协调失败,循环可以进行指数级的退避,在每次尝试之间等待更长时间,希望造成失败的某些条件能够得到解决。然而,当操作员达到这种状态时,错误仍然应该以某种方式暴露给用户。
错误报告可以通过几种方式轻松完成。报告故障的主要方法有日志记录、状态更新和事件。每种方法都有不同的优点,但一个复杂的操作员设计会优雅地利用这三者的结合。
使用日志报告错误
报告错误最简单的方法是基本日志记录。这是大多数软件项目向用户报告信息的方式,不仅仅是 Kubernetes 操作员。因为日志输出相对容易实现,并且对于大多数用户来说,直观易懂。特别是考虑到 kubectl logs pod/my-pod
等日志库的可用性,这个理由更加成立。然而,单纯依赖日志来报告重大错误也有一些缺点。
首先,Kubernetes Pod 日志不是持久化的。当 Pod 崩溃并退出时,只有在失败的 Pod 被集群的 垃圾回收(GC)过程清理掉之前,日志才会存在。这使得调试故障变得特别困难,因为用户与时间赛跑。此外,如果操作员正努力修复问题,用户也会与他们自己的自动化系统赛跑,而自动化系统应该是帮助他们而不是阻碍他们。
其次,日志可能包含大量信息需要解析。除了你自己可能写的相关操作员日志外,操作员通常会依赖许多库和依赖项,它们会将自己的信息注入到日志输出中。这可能会形成一个混乱的日志堆积,用户需要花费精力去整理。尽管有像 grep
这样的工具可以让你相对容易地搜索大量文本,但用户可能并不总是知道首先要搜索哪些文本。这可能会在调试问题时造成严重的延误。
日志有助于追踪操作员的过程步骤或进行低级调试。然而,它们并不擅长将故障立即引起用户的注意。Pod 日志的有效期很短,而且常常被无关的日志所淹没。此外,日志本身通常不会提供太多可供调试的可读上下文。这就是为什么需要注意的重要故障,最好通过状态更新和事件来处理。
使用状态更新报告错误
正如本章前面在讨论 Kubernetes API 约定和 CRD 设计时提到的,Operator CRD 应该提供两个重要字段:spec
和 status
。spec
表示集群的期望状态并接受用户输入,而 status
用于报告集群的当前状态,并应仅作为输出形式更新。
通过利用 status
字段来报告 Operator 及其 Operand 的健康状况,你可以轻松地以易读的格式突出显示重要的状态信息。该格式基于条件类型,由 Kubernetes API 机制提供。
条件报告其名称以及一个布尔值,指示条件当前是否存在。例如,Operator 可以报告 OperandReady=false
条件,表示 Operand 不健康。条件中还有一个名为 Reason
的字段,允许开发者提供当前状态的更易读的解释。从 Kubernetes 1.23
开始,Condition
的 Type
字段最大长度为 316
个字符,Reason
字段最大可达 1,024
个字符。
Kubernetes API 客户端提供了报告条件的函数,如 metav1.SetStatusCondition(conditions *[]metav1.Condition, newCondition metav1.Condition)
。这些函数(以及 Condition
类型本身)存在于 k8s.io/apimachinery/pkg/apis/meta/v1
包下。
在 Operator 的 CRD status
字段中,条件类似如下所示:
status:
conditions:
- type: Ready
status: "True"
lastProbeTime: null
lastTransitionTime: 2018-01-01T00:00:00Z
对于我们的 nginx 部署 Operator,我们将首先报告一个名为Ready
的条件。我们将在 Operator 启动成功时将此条件的状态设置为True
,如果 Operator 在执行协调循环时失败,将其更改为False
(同时在Reason
字段中提供更详细的失败说明)。我们可能会发现更多合适的条件,可以在以后添加,但考虑到 Operator 的初步简洁性,这应该已足够。
使用条件有助于显示 Operator 及其管理资源的当前状态,但这些条件仅在 Operator 的 CRD 的status
部分显示。然而,我们可以将它们与事件结合使用,使错误报告在整个集群中可用。
使用事件报告错误
Kubernetes 事件是一个原生 API 对象,像 Pods 或集群中的其他对象一样。事件会被聚合,并在使用kubectl describe
描述 Pod 时显示出来。它们也可以通过kubectl get events
独立地进行监控和过滤。它们在 Kubernetes API 中的可用性使得其他应用程序(如告警系统)也能理解它们。
下面是列出 Pod 事件的示例,在这里我们看到五个不同的事件:
-
发生三次的
Warning
事件,表明 Pod 无法调度。 -
当 Pod 成功调度后,记录一个
Normal
事件。 -
另外有三个
Normal
事件,作为 Pod 的容器镜像被拉取、创建并成功启动。
你可以在以下代码片段中看到这些事件:
$ kubectl describe pod/coredns-558bd4d5db-6mqc2 -n kube-system
…
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 6m36s (x3 over 6m52s) default-scheduler 0/1 nodes are available: 1 node(s) had taint {node.kubernetes.io/not-ready: }, that the pod didn't tolerate.
Normal Scheduled 6m31s default-scheduler Successfully assigned kube-system/coredns-558bd4d5db-6mqc2 to kind-control-plane
Normal Pulled 6m30s kubelet, kind-control-plane Container image "k8s.gcr.io/coredns/coredns:v1.8.0" already present on machine
Normal Created 6m29s kubelet, kind-control-plane Created container coredns
Normal Started 6m29s kubelet, kind-control-plane Started container coredns
由于事件对象设计更加复杂,事件可以传递比条件更多的信息。虽然事件包括Reason
和Message
字段(分别与条件的Type
和Reason
字段类似),但它们还包括诸如Count
(显示该事件发生的次数)、ReportingController
(显示事件的源控制器)和Type
(可用于筛选不同严重程度的事件)等信息。
Type
字段目前可以用来将集群事件分类为Normal
或Warning
。这意味着,类似于条件可以报告成功状态,事件也可以用来表明某些功能已成功完成(例如启动或升级)。
为了让 Pod 将事件报告给集群,代码需要实现一个EventRecorder
对象。这个对象应该在整个控制器中传递,并将事件广播到集群中。Operator SDK 和 Kubernetes 客户端提供了模板代码,帮助正确设置这个功能。
除了报告事件之外,你的 Operator 还会对集群中的事件作出反应。这回到 Operator 基于事件触发的设计核心基础。设计哪些事件类型需要被 Operator 响应的代码模式是存在的,你可以在其中加入逻辑来过滤特定事件。这个内容将在后续章节中详细讨论。
如你所见,一个复杂的错误报告系统利用日志、状态和事件来提供应用程序状态的完整图景。每种方法都有其独特的好处,它们一起编织了一幅美丽的调试图景,帮助管理员追踪故障并解决问题。
总结
本章概述了我们希望构建的 Operator 的详细信息。从描述问题(在本例中是一个管理 nginx Pod 的简单 Operator)开始,为可用的解决方案提供了坚实的基础。这个步骤甚至提供了足够的信息,以设定这个 Operator 的功能级别目标(Level II – 无缝升级)。
下一步是概述 Operator CRD 的结构。为此,我们首先注意到 Kubernetes API 中一些相关的约定,这些约定有助于确保 Operator 符合 Kubernetes 对象的预期标准。然后,我们拆解了 CRD 的结构,并解释了每个部分如何与对应的 CR 对象相关联。最后,我们草拟了一个示例,展示了 Operator 的 CR 在集群中的样子,以便更具体地了解用户的期望。
在设计 CRD 后,我们也考虑了如何管理其他资源。对于用 Go 编写的 Operator,将额外资源(如 RoleBinding 和 ServiceAccount 定义)打包成 YAML 文件是合适的做法。这些文件可以通过 go-bindata
和 go:embed
编译到 Operator 二进制文件中。
设计的下一步是目标对账循环。它包含了 Operator 的核心逻辑,也是使 Operator 成为有用且功能强大的应用的关键。这一过程始于理解水平触发和边缘触发事件处理的区别,以及为什么 Operator 更适合基于水平触发。接着,我们讨论了 Operator 对账循环的基本步骤。
最后的两个部分讨论了升级、降级和错误报告的话题。在升级和降级部分,我们介绍了同时发布和支持多个 API 版本的使用场景,以及偶尔需要在现有安装中迁移存储版本的必要性。关于错误报告的部分则聚焦于应用程序向用户暴露健康信息的三种主要方式:日志记录、状态条件和事件。
在下一章中,我们将以我们已决定的内容作为初始设计,并将其转化为实际代码。这将涉及使用 Operator SDK 初始化项目,生成将成为 Operator 的 CRD 的 API,并编写目标对账逻辑。实质上,我们将把本章中的知识应用到 Operator 开发的实际操作中。
第四章:第四章:使用 Operator SDK 开发 Operator
在完成了Operator的设计大纲之后,现在可以开始实际的开发工作了。这意味着编写并编译代码,将其部署到实际运行的Kubernetes 集群上。本章将使用Operator SDK来初始化一个模板化的 Operator 项目框架。从这里开始,将通过教程演示开发其余基本 Operator 的技术步骤。本指南将遵循在第三章中已规划的 Operator 设计,设计一个 Operator – CRD、API 和目标调和,该章聚焦于开发一个二级 Operator 来部署和升级一个简单的Nginx Pod。
本章作为教程,将遵循从头开始使用Go构建 Operator 的过程。首先是初始化模板化项目代码,然后按步骤定义 Operator API,并生成相应的CustomResourceDefinition(CRD)。接下来,我们将看到如何实现构成 Operator 核心功能的简单调和逻辑。最后,还会介绍一些基本的故障排除和常见问题。使用 Operator SDK 开发 Operator 的步骤将分为以下几个部分:
-
设置你的项目
-
定义 API
-
添加资源清单
-
编写控制循环
-
故障排除
这些章节大致遵循官方 Operator SDK Go 文档中推荐的设计模式(sdk.operatorframework.io/docs/building-operators/golang/
),因此我们选择遵循这种方法。在本章结束时,我们将拥有一个符合设计中描述的二级功能的 Operator,该设计已在第三章中概述,设计一个 Operator – CRD、API 和目标调和。该功能包括操作数(在此为 Nginx)的基本部署,以及 Operator 和操作数的无缝升级。在后续章节中,本指南将以此为基础,构建更多复杂的功能,使该示例 Operator 从较低级别逐步向更高级别发展,符合能力模型。
技术要求
本章中的引导步骤需要以下技术前提条件才能跟进:
-
go
版本 1.16+ -
本地安装
operator-sdk
二进制文件
可以通过直接从发布版本安装operator-sdk
二进制文件,或者使用 Homebrew(适用于 macOS)进行安装,或从 GitHub 编译安装,网址为 github.com/operator-framework/operator-sdk
。如果选择从 GitHub 安装 Operator SDK,还需要git
,不过建议还是使用git
,以便利用版本控制管理项目。
本章的《代码实战》视频可以在以下链接观看:bit.ly/3N7yMDY
设置项目
启动一个新的 Operator 项目的第一步是初始化一个空的项目结构。首先,通过 mkdir nginx-operator
创建一个空的项目目录并进入该目录。现在,使用以下命令初始化一个模板项目结构:
operator-sdk init --domain example.com --repo github.com/example/nginx-operator
注意
该命令首次运行时可能需要几分钟才能完成。
该命令设置了许多不同的文件和文件夹,这些文件和文件夹将被填充上我们正在构建的 Operator 的自定义 API 和逻辑。曾经空无一物的项目目录现在应该包含以下文件:
~/nginx-operator$ ls
total 112K
drwxr-xr-x 12 mdame staff 384 Dec 22 21:07 .
drwxr-xr-x+ 282 mdame staff 8.9K Dec 22 21:06 ..
drwx------ 8 mdame staff 256 Dec 22 21:07 config
drwx------ 3 mdame staff 96 Dec 22 21:06 hack
-rw------- 1 mdame staff 129 Dec 22 21:06 .dockerignore
-rw------- 1 mdame staff 367 Dec 22 21:06 .gitignore
-rw------- 1 mdame staff 776 Dec 22 21:06 Dockerfile
-rw------- 1 mdame staff 8.7K Dec 22 21:07 Makefile
-rw------- 1 mdame staff 228 Dec 22 21:07 PROJECT
-rw------- 1 mdame staff 157 Dec 22 21:07 go.mod
-rw-r--r-- 1 mdame staff 76K Dec 22 21:07 go.sum
-rw------- 1 mdame staff 2.8K Dec 22 21:06 main.go
这些文件的用途如下:
-
config
– 一个存放 Operator 资源 YAML 定义的目录。 -
hack
– 一个目录,许多项目都用它来存放各种hack
脚本。这些脚本可以用于多种用途,但通常用于生成或验证更改(通常作为持续集成过程的一部分,确保在合并之前代码已正确生成)。 -
.dockerignore
/.gitignore
– 用于声明在 Docker 构建和 Git 操作中应忽略的文件列表。 -
Dockerfile
– 容器镜像构建定义。 -
Makefile
– Operator 构建定义。 -
PROJECT
– Kubebuilder 使用的文件,用于存储项目配置文件信息 (book.kubebuilder.io/reference/project-config.html
)。 -
go.mod
/go.sum
–go mod
的依赖管理列表(已经填充了各种 Kubernetes 依赖项)。 -
main.go
– Operator 主功能代码的入口文件。
在初始化了这个模板项目结构后,就可以开始构建 Operator 逻辑了。尽管这个空项目可以编译,但它除了启动一个包含 Readyz
和 Healthz
端点的空控制器外没有其他功能。为了让它做更多的事情,首先,Operator 必须有一个定义好的 API。
定义 API
Operator 的 API 将定义它在 Kubernetes 集群中的表现方式。API 会直接转换为生成的 CRD,描述用户与 Operator 交互时使用的自定义资源对象的蓝图。因此,在编写其他逻辑之前,创建此 API 是必要的第一步。没有它,Operator 的逻辑代码就无法从自定义资源中读取值。
构建 Operator API 需要通过编写 Go 结构体来表示对象。这个结构体的基本框架可以通过 Operator SDK 使用以下命令来生成:
operator-sdk create api --group operator --version v1alpha1 --kind NginxOperator --resource --controller
该命令执行以下操作:
-
在名为
api/
的新目录中创建 API 类型 -
将这些类型定义为属于 API 组
operator.example.com
(因为我们是在example.com
域下初始化的项目)。 -
创建名为
v1alpha1
的 API 初始版本 -
将这些类型命名为我们的 Operator,
NginxOperator
。 -
在一个名为
controllers/
的新目录下实例化模板控制器代码(我们将在 编写控制循环 部分进一步操作此目录)。 -
更新
main.go
,添加启动新控制器的模板代码。
目前,我们只关心位于 api/v1alpha1/nginxoperator_types.go
下的 API 类型。该目录中还有另外两个文件(groupversion_info.go
和 zz_generated.deepcopy.go
),通常不需要修改。实际上,zz_generated.
前缀是用作标准,表示这些是生成的文件,应该避免手动修改。groupversion_info.go
文件用于定义该 API 的包变量,指导客户端如何处理其中的对象。
查看 nginxoperator_types.go
,里面已经有一些空的结构体,并且有填写额外字段的说明。该文件中最重要的三种类型是 NginxOperator
、NginxOperatorSpec
和 NginxOperatorStatus
:
// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec 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 NginxOperator. Edit nginxoperator_types.go to remove/update
Foo string `json:"foo,omitempty"`
}
// NginxOperatorStatus defines the observed state of NginxOperator
type NginxOperatorStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// NginxOperator is the Schema for the nginxoperators API
type NginxOperator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec NginxOperatorSpec `json:"spec,omitempty"`
Status NginxOperatorStatus `json:"status,omitempty"`
}
如 第三章 所述,设计 Operator – CRD、API 和目标对账,所有 Kubernetes API 对象都应包含 Spec
和 Status
字段,Operator 也不例外。因此,NginxOperatorSpec
和 NginxOperatorStatus
就是这些字段,分别用于接受用户输入和报告 Operator 当前状态。NginxOperator
代表主要对象,它们之间的关系是层级性的。
图 4.1 – NginxOperator 字段与逻辑之间的关系。
回想一下 第三章 中定义的问题,设计 Operator – CRD、API 和目标对账,该 Operator 需要接受以下输入:
-
port
,定义了要在 Nginx Pod 上暴露的端口号。 -
replicas
,定义了 Pod 副本的数量,以便通过 Operator 实现该部署的扩展。 -
forceRedploy
,这是一个Nginx
操作数。
为了实现这些字段,我们需要更新前面的代码,通过以下方式修改 NginxOperatorSpec
以包含这些新字段。我们为整数类型的字段使用指针,这样我们的 Operator 可以区分零值和未设置值,未设置值将回退为使用默认值:
// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec struct {
// Port is the port number to expose on the Nginx Pod
Port *int32 `json:"port,omitempty"`
// Replicas is the number of deployment replicas to scale
Replicas *int32 `json:"replicas,omitempty"`
// ForceRedploy is any string, modifying this field
// instructs the Operator to redeploy the Operand
ForceRedploy string `json:"forceRedploy,omitempty"`
}
(请注意,我们还移除了由 Operator SDK 生成的示例 Foo
字段。)
重新生成代码。
一旦修改了 Operator 类型,有时需要从项目根目录运行 make generate
。这将更新生成的文件,例如前面提到的 zz_generated.deepcopy.go
。即使它不总是产生任何变化,养成在每次修改 API 时定期运行此命令的习惯是一个好做法。更好的做法是,在 Operator 的代码库中添加预提交的持续集成检查,以确保任何传入的代码都包含这些生成的更改。这样的自动化检查可以通过运行 make generate
然后执行简单的 git diff
命令来评估是否有任何变化。如果有变化,检查应失败并指导开发人员重新生成代码。
对于所有这三个新字段,我们还添加了以`json:"...,omitempty"`
形式表示的 JSON 标签。这些标签的第一部分定义了该字段在以 JSON 或 YAML 表示时的显示方式(例如,当通过 kubectl
与对象交互时)。omitempty
指定如果该字段为空,则不应在 JSON 输出中显示。这对于隐藏可选字段非常有用,以便在查看集群中的对象时提供简洁的输出(否则,空字段将显示为 nil 或空字符串)。
我们将最初将这三个字段都设置为可选,默认值在 Operator 中定义。然而,删除 omitempty
并添加更多 Kubebuilder 标签后,它们也可以被指定为必填字段,例如:
// Port is the port number to expose on the Nginx Pod
// +kubebuilder:default=8080
// +kubebuilder:validation:Required
Port int `json:"port"`
使用这些设置,任何尝试在不包含 port
字段的情况下修改 NginxOperator
对象的操作都将导致 API 服务器返回错误。在当前版本的 Kubebuilder 中,默认假设任何未被标记为 omitempty
的字段都是必填字段。然而,也有方法可以全局切换这个默认行为(通过在 API 顶层应用 // +kubebuilder:validation:Optional
标记)。因此,每次更改字段的要求时,最好明确更新该字段的具体要求值。
定义了 API 类型后,现在可以生成一个等效的 CRD 清单,之后将用于在 Kubernetes 集群中创建与这些类型匹配的对象。
添加资源清单
对于 Operator 相关资源,重要的是将它们打包成易于部署和维护的形式。这包括 Operator 的 CRD,但也包括其他资源,如 ClusterRoles 以及与这些角色匹配的 ServiceAccount。然而,第一步是根据前一部分定义的 Go 类型生成一个 CRD,使用以下内容:
$ make manifests
此命令生成一个基于我们刚才定义的 API 的 CRD。该 CRD 被放置在 config/crd/bases/operator.example.com_nginxoperators.yaml
下。该 CRD 如下所示:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
creationTimestamp: null
name: nginxoperators.operator.example.comspec:
group: operator.example.com
names:
kind: NginxOperator
listKind: NginxOperatorList
plural: nginxoperators
singular: nginxoperator
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: NginxOperator is the Schema for the
nginxoperators 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: NginxOperatorSpec defines the desired
state of NginxOperator
properties:
forceRedploy:
description: ForceRedploy is any string,
modifying this field instructs
the Operator to redeploy the
Operand
type: string
port:
description: Port is the port number to expose
on the Nginx Pod
type: integer
replicas:
description: Replicas is the number of
deployment replicas to scale
type: integer
type: object
status:
description: NginxOperatorStatus defines the
observed state of NginxOperator
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
(在此输出中,我们添加了额外的格式,以更清晰地表示较长的字符串,如字段描述,并突出显示了添加到 API 中的三个字段。)
由于 Operator 的基本结构,CRD 相对简单,但这也证明了 OpenAPI 验证架构的固有复杂性。这种复杂性强调了 CRD 清单应该始终通过生成方式创建,而不是手动编辑。
定制生成的清单
默认的生成清单命令会创建一个复杂的验证架构,通常不应手动编辑。不过,make manifests
的底层命令实际上是调用一个额外的工具,手动使用 controller-gen
是以非默认方式生成文件和代码的可接受方法。例如,controller-gen schemapatch
命令将只重新生成 CRD 的 OpenAPI 验证架构。如果你希望手动修改 CRD 的其他部分(例如附加的注释或标签),这将非常有用,因为这些修改会被完全重生成的操作覆盖。可以通过从之前提到的仓库安装 controller-gen
并使用 controller-gen -h
运行它来查看完整的命令列表。
make manifests
命令还会创建一个相应的 基于角色的访问控制 (RBAC) 角色,可以将该角色绑定到 Operator 的 ServiceAccount,以便为 Operator 提供访问其自定义对象的权限:
config/rbac/role.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- operator.example.com
resources:
- nginxoperators
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- operator.example.com
resources:
- nginxoperators/finalizers
verbs:
- update
- apiGroups:
- operator.example.com
resources:
- nginxoperators/status
verbs:
- get
- patch
- update
该角色授予对集群中所有 nginxoperator
对象的完整访问权限,包括创建、删除、获取、列出、修补、更新和监视。通常,不建议 Operator 管理其自定义资源对象的生命周期(例如,config
对象的创建最好由用户手动操作),因此一些动词,如 create
和 delete
,在这里并非严格必要。不过,我们暂时保留它们。
其他清单和二进制数据
需要创建的其他资源清单包括 Operator 的 ClusterRole
和 Nginx 部署定义。ClusterRole
可以通过代码中的 Kubebuilder 标签方便地生成,这将在稍后的 编写控制循环 部分中完成。在此之前,应定义部署,以便控制循环能够访问它。
定义内存资源(例如部署)的一种方法是通过在代码中创建它们。许多项目采用这种方法,包括在 github.com/operator-framework/operator-sdk
上提供的官方示例项目。对于这个 Nginx 部署,方法将涉及创建一个类似于以下的函数:
func nginxDeployment() *appsv1.Deployment {
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "nginxDeployment",
Namespace: "nginxDeploymentNS",
},
Spec: appsv1.DeploymentSpec{
Replicas: &pointer.Int32Ptr(1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app":"nginx"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app":"nginx"},, },
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "nginx:latest",
Name: "nginx",
Command: []string{"nginx" Ports: []corev1.ContainerPort{{
ContainerPort: 8080,
Name: "nginx", }}, }},
},
}, },
}
return dep
}
前面的函数返回一个静态的 Deployment Go 结构体,预填充了默认值,如 Deployment 名称和暴露的端口。然后可以根据 Operator CRD 中设置的规范修改该对象,再通过 Kubernetes API 客户端更新集群中的 Deployment(例如,更新副本数)。
正如在 第三章 中讨论的,设计一个操作符 – CRD、API 和目标协调,这种方法易于编写代码,因为资源结构体可以直接作为 Go 类型使用。然而,从可维护性和可用性角度来看,仍然有更好的选择。这就是 go-bindata 和 go:embed 等工具的作用。
使用 go-bindata 和 go:embed 访问资源
go-bindata 项目可以在 GitHub 上找到,网址是 github.com/go-bindata/go-bindata
。它通过将任意文件转换为 Go 代码,随后将其编译到主程序中并在内存中使用。使用 go-bindata 的好处在于,项目资源可以更简洁地管理,并以更易读的格式(如 YAML)维护,从而提供与原生 Kubernetes 资源创建的相似性。自 Go 1.16 起,语言已包含自己的编译器指令 go:embed
,本质上执行相同的功能;然而,为了方便尚未更新到 Go 1.16 或希望避免在开发和生产环境中依赖编译器特定指令的用户,我们将提供两种方法的示例。
无论哪种方法,第一步是将资源清单创建在一个目录中,例如 assets/nginx_deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: "nginx-deployment"
namespace: "nginx-operator-ns"
labels:
app: "nginx"
spec:
replicas: 1
selector:
matchLabels:
app: "nginx"
template:
metadata:
labels:
app: "nginx"
spec:
containers:
- name: "nginx"
image: "nginx:latest"
command: ["nginx"]
这种结构比原生 Go 类型更容易使用,因为它让我们无需为每个嵌入的类型(如 map[string]string
用于标签和 Pod 命令)定义类型。它还可以被持续集成检查轻松解析,以确保它保持有效的结构。
接下来的两个小节将演示实现 go-bindata
或 go:embed
的基本概念。这些示例将展示如何为每种方法添加基础概念。然而,我们最终将在 简化资源嵌入 小节中重构大部分代码;因此,您可以选择在达到重构小节之前不编写任何这些代码。
Go 1.15 及更早版本 – go-bindata
对于较旧版本的 Go,您必须从 GitHub 安装 go-bindata
包来生成您的文件:
$ go get -u github.com/go-bindata/go-bindata/...
然后可以使用以下命令创建包含清单的生成代码:
$ go-bindata -o assets/assets.go assets/...
该命令将在assets/
目录下创建一个assets.go
文件,其中包含生成的函数以及assets/
目录中文件的内存表示。请注意,将资产文件与生成的代码放在不同目录下可能更方便,因为重新运行go-bindata
命令时,除非排除,否则现在会包含assets.go
文件本身的表示,方法如下:
$ go-bindata -o assets/assets.go -ignore=\assets\/assets\.go assets/…
每次对底层资产进行修改时,都需要重新生成此文件。这样可以确保更改被应用到编译后的资产包中,该包包括对文件的访问功能。
生成资产后,可以通过导入新的assets
包并使用Asset()
函数在代码中访问它们,方法如下:
import "github.com/sample/nginx-operator/assets"
...
asset, err := assets.Asset("nginx_deployment.yaml")
if err != nil {
// process object not found error
}
对于较新的 Go 版本(1.16 及以上),编译资源清单为资产变得更加简单。
Go 1.16 及更新版本 — go:embed
go:embed
标记作为编译指令在 Go 1.16 中被加入,用于提供原生资源嵌入,而无需像 go-bindata 这样的外部工具。要使用这种方法,首先在名为assets/
的新目录下创建与 go-bindata 设置类似的资源清单文件。
接下来,需要修改 Operator 项目的main.go
文件,导入embed
包,并声明资产清单作为变量,如下所示(以下所有代码仅显示需要进行的更改):
package main
import (
...
"embed"
...
)
//go:embed assets/nginx_deployment.yaml
var deployment embed.FS
请注意//go:embed
注释,它告诉编译器将assets/nginx_deployment.yaml
的内容作为文件系统数据存储在deployment
变量中。
然后可以通过利用 Kubernetes API 架构声明将数据读取并转换为 Deployment Go 结构体,如下所示:
import (
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
...
var (
appsScheme = runtime.NewScheme()
appsCodecs = serializer.NewCodecFactory(appsScheme)
)
...
func main() {
if err := appsv1.AddToScheme(appsScheme); err != nil {
panic(err)
}
deploymentBytes, err := deployment.ReadFile("assets/nginx_deployment.yaml")
if err != nil {
panic(err)
}
deploymentObject, err := runtime.Decode(appsCodecs.UniversalDecoder(appsv1.SchemeGroupVersion), deploymentBytes)
if err != nil {
panic(err)
}
dep := deploymentObject.(*appsv1.Deployment)
...
}
这段代码做了几件事:
-
它导入了相关的 Kubernetes API 包,这些包定义了 Deployment API 对象的架构:
import ( appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" )
-
它初始化一个
Scheme
和一组编解码器,供 API 的 UniversalDecoder 使用,以便知道如何将文件的[]byte
数据表示转换为 Go 结构体:var ( appsScheme = runtime.NewScheme() appsCodecs = serializer.NewCodecFactory(appsScheme) )
-
它使用我们之前声明的
deployment
变量(作为设置embed
指令的一部分)来读取assets/nginx_deployment.yaml
文件(如高亮所示):deploymentBytes, err := deployment.ReadFile("assets/nginx_deployment.yaml") if err != nil { panic(err) }
-
它将
deployment.ReadFile()
返回的[]byte
数据解码为一个可以转换为 Go 类型的 Deployment 对象:deploymentObject, err := runtime.Decode(appsCodecs.UniversalDecoder(appsv1.SchemeGroupVersion), deploymentBytes) if err != nil { panic(err) }
-
它将对象数据转换为
*appsv1.Deployment
的内存表示:dep := deploymentObject.(*appsv1.Deployment)
从这一点开始,我们需要找到一种方法将 Deployment 对象传递给我们的 Nginx Operator Controller。这可以通过修改NginxOperatorReconciler
类型来实现,添加一个字段以保存*appsv1.Deployment
类型。然而,对于 Operator 将要管理的各种资源类型来说,这并不方便。为了简化这一点,并更好地组织项目结构,我们可以将资源嵌入代码移到一个独立的包中。
简化资源嵌入
前面的示例展示了将 YAML 文件嵌入 Go 代码中的基本步骤。然而,对于我们的示例 Nginx Operator,这可以更好地组织成一个独立的包。为此,我们将保留现有的 assets/
目录(作为一个可导入的 Go 模块路径,用于加载和处理文件的辅助函数),并在其下创建一个新的 manifests/
目录(用来存放实际的清单文件)。新的文件结构如下所示:
./nginx-operator/
| - assets/
| - - assets.go
| - - manifests/
| - - - nginx_deployment.yaml
assets.go
文件将包括前面示例中的 API 架构初始化和封装对象转换功能,代码如下:
package assets
import (
"embed"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
var (
//go:embed manifests/*
manifests embed.FS
appsScheme = runtime.NewScheme()
appsCodecs = serializer.NewCodecFactory(appsScheme)
)
func init() {
if err := appsv1.AddToScheme(appsScheme); err != nil {
panic(err)
}
}
func GetDeploymentFromFile(name string) *appsv1.Deployment {
deploymentBytes, err := manifests.ReadFile(name)
if err != nil {
panic(err)
}
deploymentObject, err := runtime.Decode(
appsCodecs.UniversalDecoder(appsv1.SchemeGroupVersion),
deploymentBytes,
)
if err != nil {
panic(err)
}
return deploymentObject.(*appsv1.Deployment)
}
这个新文件相比之前共享的实现做了一些修改:
-
现在,整个
manifests/
目录作为一个文件系统变量被嵌入。这将使得在该目录中读取其他资源变得更加容易,而不需要为每个资源在这个包中声明新的变量。 -
主要逻辑已经被封装到一个新的函数
GetDeploymentFromFile()
中。我们的控制循环可以像下面这样调用并使用这个函数:import "github.com/sample/nginx-operator/assets" ... nginxDeployment := assets.GetDeploymentFromFile("manifests/nginx_deployment.yaml")
我们可以向这个目录添加其他清单文件,以便 Operator 管理它们(例如,额外的 Operand 依赖项)。但目前,我们已经有足够的内容开始编写控制循环了。
编写控制循环
在建立了内存中资源清单表示策略后,现在编写 Operator 的控制循环变得更容易了。正如前面章节所描述的,这个控制循环包含了一个核心的状态协调功能调用,该功能由某些相关的集群事件触发。这个功能不会持续运行,而是 Operator 的主线程会不断观察集群,等待事件触发,从而启动状态协调函数的调用。
空的 Reconcile()
函数已经由 Operator SDK 在 controllers/nginxoperator_controller.go
中预先生成:
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// your logic here
return ctrl.Result{}, nil
}
现在,这个函数什么都不做,只是返回一个空的 ctrl.Result
和一个空的 error
,这表示成功执行,并指示框架的其余部分无需重试这个协调运行。如果该函数返回非 nil
的 error
或非空的 ctrl.Result
结构,控制器则会重新排队该协调尝试,等待再次尝试。这些情况会出现在我们根据注释填写控制器逻辑时。
因为 Operator SDK 使用已经可以访问的 Kubernetes 客户端实例化此控制器,所以我们可以使用诸如Get()
之类的函数来检索集群资源。首先要做的是尝试访问现有的 Nginx Operator 资源对象。如果没有找到,我们应该记录一条消息并终止调和尝试。如果在检索对象时遇到任何其他错误,我们将返回错误,这样该尝试将被重新排队并再次尝试。此方法可以处理其他故障,例如网络问题或与 API 服务器的连接暂时中断。通过这些更改,新的Reconcile()
函数如下所示:
controllers/nginxoperator_controller.go:
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
operatorCR := &operatorv1alpha1.NginxOperator{}
err := r.Get(ctx, req.NamespacedName, operatorCR)
if err != nil && errors.IsNotFound(err) {
logger.Info("Operator resource object not found.")
return ctrl.Result{}, nil
} else if err != nil {
logger.Error(err, "Error getting operator resource object")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
这种错误处理模式在 Kubernetes 项目中很常见,包括官方 Operator SDK 文档中的示例。与其立即返回错误,这段代码实际上忽略了 Operator 对象简单地没有找到的情况。errors.IsNotFound()
检查来自k8s.io/apimachinery/pkg/api/errors
包,该包提供了多个帮助函数,用于处理特定 Kubernetes 错误的标准方法。使用这种模式有助于减少用户的日志噪声,并忽略删除了 Operator 资源(这仍会触发调和尝试)的事件。如果未找到 Operator 对象,开发人员可以进一步采取措施,将其作为采取其他步骤的信号(例如删除依赖于 Operator 部署的其他资源)。
此外,请注意此代码使用req.NamespacedName
来获取 Operator 配置对象的Name
和Namespace
。这符合 Operator 框架文档中列出的最佳实践之一(sdk.operatorframework.io/docs/best-practices/best-practices/
):
Operator 不应对其部署的命名空间做任何假设,也不应使用硬编码的资源名称来预期资源已经存在。
在这种情况下,req
参数包括触发调和尝试的对象事件的名称。跨资源使用一致的名称使我们能够在每次调用Reconcile()
时重用req.NamespacedName
字段,而不管触发调和的对象是什么。换句话说,如果 Deployment 与 Operator 自定义资源对象具有相同的名称和命名空间,我们可以始终消除硬编码资源名称假设的使用。
成功找到 Operator 资源对象后,控制器可以获取 Operator spec
中每个设置的值,并根据需要进行更新。与我们刚刚为 Operator 资源所做的类似,然而,我们必须首先检查 Deployment 是否存在。为此,我们将遵循类似的模式,利用 errors.IsNotFound()
来检查是否存在 Nginx Deployment。不过,在这种情况下,如果没有找到 Deployment,函数不会直接返回,而是控制器将从嵌入的 Deployment YAML 文件中创建一个新的 Deployment:
controllers/nginxoperator_controller.go:
deployment := &appsv1.Deployment{}
err = r.Get(ctx, req.NamespacedName, deployment)
if err != nil && errors.IsNotFound(err) {
deployment.Namespace = req.Namespace
deployment.Name = req.Name
deploymentManifest := assets.GetDeploymentFromFile("manifests/nginx_deployment.yaml")
deploymentManifest.Spec.Replicas = operatorCR.Spec.Replicas
deploymentManifest.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = *operatorCR.Spec.Port
err = r.Create(ctx, deploymentManifest)
if err != nil {
logger.Error(err, "Error creating Nginx deployment.")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
} else if err != nil {
logger.Error(err, "Error getting existing Nginx deployment.")
return ctrl.Result{}, err
}
在这段代码中,我们通过之前创建的 assets.GetDeploymentFromFile()
函数加载嵌入的默认清单文件。我们还修改了该清单声明,以包含来自当前 Operator 资源对象的值。
这种方法的替代方案是创建一个具有默认值的 Deployment,然后让函数立即返回 ctrl.Result{Requeue: true}
。这将触发另一次协调尝试,在该尝试中,应该能够找到 Deployment 并根据 Operator 资源设置进行更新。这里的权衡是立即创建一个新对象,而无需另一个协调周期,换取的是操作不够原子和某些代码的重复(因为在找到现有的 Deployment 时,我们仍然需要以下代码来应用 Operator 资源设置)。为了消除这些重复代码,我们可以像下面这样修改前面的部分:
controllers/nginxoperator_controller.go:
deployment := &appsv1.Deployment{}
create := false
err = r.Get(ctx, req.NamespacedName, deployment)
if err != nil && errors.IsNotFound(err) {
create = true
deployment = assets.GetDeploymentFromFile("manifests/nginx_deployment.yaml")
} else if err != nil {
logger.Error(err, "Error getting existing Nginx deployment.")
return ctrl.Result{}, err
}
if operatorCR.Spec.Replicas != nil {
deployment.Spec.Replicas = operatorCR.Spec.Replicas
}
if operatorCR.Spec.Port != nil {
deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = *operatorCR.Spec.Port
}
由于 Operator 自定义资源中的 Replicas
和 Port
字段是可选的(并且是指针),我们应该使用 nil
检查来查看是否设置了任何值。否则,Deployment 将默认使用其 manifest
文件中定义的值。
现在,我们始终确保无论是现有的 Deployment 还是新创建的,都正在修改 Deployment 对象,以包括 Operator 设置。然后,是否调用 Create()
还是 Update()
的决定将在稍后根据 create
布尔值来做出:
controllers/nginxoperator_controller.go:
if create {
err = r.Create(ctx, deployment)
} else {
err = r.Update(ctx, deployment)
}
return ctrl.Result{}, err
如果任何调用返回错误,控制器会通过脚手架框架代码返回并记录该错误。如果创建或更新 Deployment 的调用成功,则 err
将是 nil
,并且协调调用将成功完成。我们的完整 Reconcile()
函数现在看起来像这样:
controllers/nginxoperator_controller.go:
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
operatorCR := &operatorv1alpha1.NginxOperator{}
err := r.Get(ctx, req.NamespacedName, operatorCR)
if err != nil && errors.IsNotFound(err) {
logger.Info("Operator resource object not found.")
return ctrl.Result{}, nil
} else if err != nil {
logger.Error(err, "Error getting operator resource object")
return ctrl.Result{}, err
}
deployment := &appsv1.Deployment{}
create := false
err = r.Get(ctx, req.NamespacedName, deployment)
if err != nil && errors.IsNotFound(err) {
create = true
deployment = assets.GetDeploymentFromFile("assets/nginx_deployment.yaml")
} else if err != nil {
logger.Error(err, "Error getting existing Nginx deployment.")
return ctrl.Result{}, err
}
deployment.Namespace = req.Namespace
deployment.Name = req.Name
if operatorCR.Spec.Replicas != nil {
deployment.Spec.Replicas = operatorCR.Spec.Replicas
}
if operatorCR.Spec.Port != nil { deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = *operatorCR.Spec.Port
}
ctrl.SetControllerReference(operatorCR, deployment, r.Scheme)
if create {
err = r.Create(ctx, deployment)
} else {
err = r.Update(ctx, deployment)
}
return ctrl.Result{}, err
}
此外,我们添加了对 ctrl.SetControllerReference()
的调用,以表明 Nginx Operator 资源对象应该列为 Nginx Deployment 的 OwnerReference
(一个 API 字段,用来标识哪个对象“拥有”指定的对象),这有助于垃圾回收。
最后,我们需要确保操作符实际上具有获取、创建和更新 Deployment 的集群权限。为此,我们需要更新操作符的 RBAC 角色。这可以通过在Reconcile()
函数上使用 Kubebuilder 标记自动完成,从而帮助保持权限的组织性,并明确标识所需的使用权限。已经生成了用于访问操作符自定义资源的 Kubebuilder 标记,但现在我们可以为 Deployment 添加额外的标记:
controllers/nginxoperator_controller.go:
//+kubebuilder:rbac:groups=operator.example.com,resources=nginxoperators,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=operator.example.com,resources=nginxoperators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=operator.example.com,resources=nginxoperators/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
现在,运行make manifests
应该会在操作符的ClusterRole
(config/rbac/role.yaml
)中生成这个新部分:
rules:
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
此时,我们已经有了一个基本的控制循环,将指定的操作符设置与集群的当前状态进行协调。但是,什么事件会触发这个循环运行呢?这是在SetupWithManager()
中设置的:
controllers/nginxoperator_controller.go:
// SetupWithManager sets up the controller with the Manager.
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&operatorv1alpha1.NginxOperator{}).
Complete(r)
}
这段代码是为了观察集群中NginxOperator
对象的变化,但我们还需要它来观察 Deployment 对象的变化(因为操作符正在管理一个 Deployment)。可以通过如下方式修改函数来实现:
// SetupWithManager sets up the controller with the Manager.
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&operatorv1alpha1.NginxOperator{}).
Owns(&appsv1.Deployment{}).
Complete(r)
}
通过添加对Owns(&appsv1.Deployment{})
的调用,控制器管理器现在知道也要在集群中 Deployment 对象发生变化时触发Reconcile()
调用。由于我们已将 Deployment 列为NginxOperator
对象的拥有者,因此控制器现在会将对 Deployment 对象的任何更改视为NginxOperator
对象的相关事件。通过后续调用Owns()
,可以将多种类型的对象链入这个监视列表。
我们现在有一个执行某些操作的操作符。当构建并部署后,这个控制器将监视集群中任何NginxOperator
自定义资源的变化,并做出响应。这意味着,当用户第一次创建操作符自定义资源对象时,操作符会看到现在存在一个配置对象,并根据现有的值创建一个 Deployment。它还会监视 Deployment 的变化。例如,如果 Nginx Deployment 被意外删除,操作符将通过根据自定义资源中的现有设置创建一个新的 Deployment 来响应。
故障排除
本章中概述的步骤涉及使用几种不同的工具和库,它们有不同的依赖要求。可以理解的是,这可能会导致错误,特别是在不同的开发环境中。尽管相关软件的作者在必要时采取措施生成有用的错误消息,但有时无法通过自动响应提供明确的解决方案。这就是快速发展的软件开发所带来的不幸局面。
然而,幸运的是,开源软件的优势提供了许多资源和志愿者,帮助支持和调试出现的问题。本节将重点介绍这些资源,作为解决技术问题的指南。所有这些工具都提供文档和使用指南,但其中许多也有社区资源,用户可以在这些资源中向维护者和其他用户寻求澄清和帮助。
一般的 Kubernetes 资源
Operator SDK 基于多个 Kubernetes 库构建,因此了解一些用于构建 Operators 的 Kubernetes 包非常有帮助。通过这样做,有时能更容易找到问题的根本原因。
Kubernetes 的参考文档位于 kubernetes.io/docs/home/
。然而,这个首页部分主要面向使用文档。关于 Kubernetes API 的支持(包括 API 客户端、标准和对象引用),API 参考部分更为相关,涵盖了本章所讨论的主题。该部分位于 kubernetes.io/docs/reference/
。
整个 Kubernetes 源代码可以在 GitHub 上通过不同的组织访问,例如 github.com/kubernetes
(大部分项目代码)和 github.com/kubernetes-sigs
(子项目,例如 Kubebuilder)。例如,Go 客户端库,它在 Operator SDK 框架下被用来提供诸如 r.Get()
的资源功能,托管地址为 github.com/kubernetes/client-go/
。
熟悉托管 Operator Framework 所依赖的不同代码库的 GitHub 仓库,为与这些项目的维护者沟通提供了极好的资源。在 GitHub 上搜索 Issues 通常可以立即缓解问题(或至少提供当前问题状态的见解)。
为了更快的响应和更广泛的受众,Kubernetes 社区在 Slack 消息平台上非常活跃。官方的 Kubernetes Slack 服务器对任何人开放,网址为 slack.k8s.io。针对开发者处理一般 Kubernetes 问题的有用频道包括以下几个:
-
#kubernetes-novice – 这个频道是为新的 Kubernetes 用户和开发者准备的。
-
#kubernetes-contributors – 这个频道更多关注 Kubernetes 本身的开发,但仍然会涵盖一些相关话题,例如 API 客户端。
-
#kubernetes-novice
主要集中在使用方面,而非开发方面,但针对更具体的问题。 -
SIG-APIMachinery
负责 Kubernetes Go 客户端的所有权,我们在本章中通过扩展使用了这个客户端。在这里,你会找到在 API 主题方面最有经验的贡献者。
对于本章中的主题,这些资源适用于与 Kubernetes API 相关的问题,包括使用 make generate
等命令生成的客户端工具。
Operator SDK 资源
Operator SDK 还提供了丰富的文档,包括示例 Operator 开发教程。在本章中,使用 Go 开发 Operator 的步骤与在 sdk.operatorframework.io/docs/building-operators/golang/
上的 Operator SDK Go 文档中概述的步骤类似。
与其他 Kubernetes 项目类似,Operator SDK 也可以在 GitHub 上找到,地址是 github.com/operator-framework/operator-sdk/
。这是一个很好的资源,包含示例、问题追踪,以及项目更新和持续工作的通知。
在 slack.k8s.io 上有几个与 Operator 相关的频道,包括 #operator-sdk-dev(专门用于讨论与 Operator SDK 相关的内容)和 #kubernetes-operators,该频道用于关于 Operators 的一般讨论。
这些资源对于解决与 operator-sdk
二进制文件相关的问题,或与 SDK 的代码库和模式提供的模式相关的问题非常有帮助。
Kubebuilder 资源
Kubebuilder 是 Operator SDK 用于生成清单和一些控制器代码的工具。它包括在本章中运行的命令,例如 make manifests
,因此,对于大多数与 CRD 相关的问题,或从代码标记中生成它们的问题(例如 //+kubebuilder…
),这是一个很好的起点。
Kubebuilder 的一个优秀参考资料是《Kubebuilder 书籍》,可以在 book.kubebuilder.io/
找到。这是 Kubebuilder 的核心文档,包含了生成代码的所有可用注释标记的详细信息。它的代码库也可以在 GitHub 上找到,地址是 github.com/kubernetes-sigs/kubebuilder
,其中的一些子工具(如 controller-gen)可以在 github.com/kubernetes-sigs/controller-tools
找到。
最后,Kubernetes Slack 服务器上有 #kubebuilder 频道,供大家进行互动讨论和寻求帮助。
总结
本章遵循我们在第三章中概述的设计,设计一个 Operator – CRD、API 和目标协调,生成实现 Level I Operator(基本安装)最低要求的功能代码。在 Operator 生命周期管理器的支持下(将在后续章节中展示)以及良好的后续 API 设计,本 Operator 还将支持自我升级及其 Operand 的升级,从而使其符合 Level II 的标准。
按照 Operator SDK 文档方法推荐的步骤,创建基于 Go 的 Operator 的过程是逐步进行的,以实现基本功能。在本章中,这一模式意味着首先设计 Operator 的 API 类型,然后使用诸如 Kubebuilder 等工具将其生成到 CRD 中。此时,可以开始考虑其他资源清单,例如 Operand 部署,以及它们如何在内存中表示。此指南采用了将这些附加资源直接嵌入 Go 二进制文件的方法,利用内建的 Go 编译器指令使语言本身支持这一操作。
最后,核心控制器代码被填充完成。这使得 Operator 成为一个控制器,并且这个控制循环用于根据用户通过 Operator 的 CRD 输入的内容,将集群的期望状态与实际状态进行协调。通过对事件触发器和添加 RBAC 权限的一些额外调整,这段代码开始观察 Deployments,这是管理 Operand 所必需的。
在下一章中,我们将在此基础功能上构建,添加更多高级代码。这将使我们的 Operator 超越 Level II,因为我们将加入指标和领导者选举等功能,从而创建一个更复杂的控制器,能够进行更深入的洞察和错误处理。
第五章:第五章:开发一个 Operator —— 高级功能
尽管具有基本安装和升级功能的 Operator 集群相比于非 Operator 基础的 Kubernetes 集群有了显著的改进,但仍有更多的工作可以提高集群管理和用户体验。高级功能可以帮助用户实现更复杂的自动化,指导故障恢复,并通过度量和状态更新等功能来支持数据驱动的部署决策。
这些是 能力模型 中更高级的 Operator 的一些基本功能(如 第一章 Operator 框架介绍 所述)。因此,本章将首先解释实现高级功能的成本与收益(相对于所需的努力),然后在接下来的章节中演示如何添加这些功能:
-
理解高级功能的需求
-
报告状态条件
-
实现度量报告
-
实现领导者选举
-
添加健康检查
方便的是,实现这些功能所需的代码不需要对现有 Operator 代码进行重大重构。实际上,能力模型的层次结构和 Operator SDK 提供的开发模式鼓励这种迭代式构建。因此,本章的目标是基于 第四章 使用 Operator SDK 开发 Operator 中的基本 Operator 代码,构建一个能够提供我们刚才列出功能的更复杂的 Operator。
技术要求
本章中展示的示例是在 第四章 使用 Operator SDK 开发 Operator 中开始的项目代码基础上构建的。因此,建议从该章节(及其前提条件)开始,该章节涵盖了项目初始化和基本的 Operator 功能。但这不是必需的,本章中的各个部分通常可以应用于任何 Operator SDK 项目。也就是说,任何由 Operator SDK 初始化的项目都可以按照以下步骤进行操作,你不需要专门实现前面章节中的所有代码。
鉴于此,本章的要求如下:
-
任何现有的 Operator SDK 项目
-
Go 1.16+
本章的《代码实战》视频可以通过以下链接观看:bit.ly/3zbsvD0
理解高级功能的需求
在构建并准备好部署一个基本且功能完整的操作符后,您可能会问:我还需要做什么?事实上,现在您的操作数(operand)可以安装,并且其健康状况由操作符管理,可能不需要做更多事情了。这对于一个操作符来说是完全可以接受的功能水平。实际上,最好从一个简单的操作符开始,随着开发资源的增加逐步迭代(回想一下在第三章中讨论的内容,设计操作符 – CRD、API 和目标协调)。
关键是,在开发您的操作符时,停在这里并不丢人。能力模型(Capability Model)定义低级操作符是有原因的(换句话说,如果只能安装操作数的操作符不可接受,那么为什么要定义第一级操作符呢?)。
然而,能力模型(Capability Model)确实也定义了更高层次的操作符(Operator),其背后有原因。例如,很容易想象,在用户与您的操作符交互的过程中,他们可能希望查看更多有关其在生产环境中表现的详细信息。这是为操作符添加自定义指标的一个好例子。或者,也可能存在一个常见的故障状态,难以调试(在这种情况下,状态条件可以帮助以更高效的方式揭示更多关于故障的信息)。
以下几个部分只是一些最常见的附加功能,它们有助于将操作符提升到更高的功能水平。部分内容也在操作符 SDK 文档中的高级主题(sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/
)中有更详细的介绍。当然,列出您可以为操作符添加的每个可能的功能是不现实的。但希望这些例子能为您的开发提供一个良好的起点。
报告状态条件
状态条件在第三章中讨论过,设计操作符 – CRD、API 和目标协调,作为一种高效地将操作符健康状况的可读信息传达给管理员的方式。通过直接在操作符的自定义资源定义(CRD)中呈现,它们提供的信息更容易突出显示,并在更集中的调试起始点中查看。这样,它们相较于错误日志具有优势,后者可能包含大量无关信息,且往往缺乏直接的上下文,难以追溯到根本原因。
在 Operator 中实现条件变得更容易,因为 Condition
类型是在 Kubernetes 1.19 版本左右通过 KEP-1623(github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1623-standardize-conditions
)实现的。该类型现在是 Kubernetes API 的一部分,位于 k8s.io/apimachinery/pkg/api/meta
模块中。这使得开发者能够以一致的方式理解在 Kubernetes 中如何报告条件,并且享有 Kubernetes API 支持的兼容性保证。
Operator 框架也基于 Kubernetes 类型实现了条件,既在 Operator SDK 中,也在 OLM 创建的 OperatorCondition
资源中。本节将涵盖这两种方法。
Operator CRD 状态
根据在第三章中涵盖的 Kubernetes API 约定,设计 Operator - CRD、API 和目标协调,对象(包括自定义资源)应包含 spec
和 status
字段。在 Operator 的情况下,我们已经使用 spec
作为配置 Operator 参数的输入。然而,我们尚未修改 status
字段。现在,我们将通过在 api/v1alpha1/nginxoperator_types.go
中添加条件列表作为新字段来修改它:
// NginxOperatorStatus defines the observed state of NginxOperator
type NginxOperatorStatus struct {
// Conditions is the list of status condition updates
Conditions []metav1.Condition `json:"conditions"`
}
然后,我们将运行 make generate
(以更新生成的客户端代码)和 make manifests
(以使用新字段更新 Operator 的 CRD),或者直接运行 make
(该命令会运行所有生成器,尽管我们现在不需要其中的一些)。新字段现在已在 CRD 中反映:
properties:
conditions:
description: Conditions is the list of the most recent status condition
updates
items:
description: "Condition contains details for one aspect
of the current state of this API Resource.
--- This struct is intended for direct use
as an array at the field path
.status.conditions."
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the
condition transitioned from one status
to another. This should be when the
underlying condition changed. If that
is not known, then using the time when
the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message
indicating details about the
transition. This may be an empty
string.
maxLength: 32768
type: string
...
status:
description: status of the condition, one of True,
False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in
foo.example.com/CamelCase. --- Many
.condition.type values are consistent
across resources like Available, but
because arbitrary conditions can be
useful (see .node.status.conditions),
the ability to deconflict is important.
The regex it matches is
(dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^(a-z0-9?(\.a-z0-9?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
required:
- conditions
type: object
请注意,这也会导入来自 Kubernetes API 的 Condition
类型的所有嵌入式验证要求。
现在 Operator 的 CRD 已经有了一个用于报告最新状态条件的字段,代码可以更新以实现这些条件。为此,我们可以使用 SetStatusCondition()
辅助函数,该函数可在 k8s.io/apimachinery/pkg/api/meta
模块中使用。对于本示例,我们将从一个名为 OperatorDegraded
的单个条件开始,默认值为 False
,表示 Operator 正在成功地协调集群中的更改。然而,如果 Operator 确实遇到错误,我们将更新此条件为 True
,并附带一个指示错误的消息。这将涉及对 controllers/nginxoperator_controller.go
中的 Reconcile()
函数进行一些重构,以便与以下内容匹配:
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
operatorCR := &operatorv1alpha1.NginxOperator{}
err := r.Get(ctx, req.NamespacedName, operatorCR)
if err != nil && errors.IsNotFound(err) {
logger.Info("Operator resource object not found.")
return ctrl.Result{}, nil
} else if err != nil {
logger.Error(err, "Error getting operator resource object")
meta.SetStatusCondition(&operatorCR.Status.Conditions, metav1.Condition{
Type: "OperatorDegraded",
Status: metav1.ConditionTrue,
Reason: "OperatorResourceNotAvailable",
LastTransitionTime: metav1.NewTime(time.Now()),
Message: fmt.Sprintf("unable to get operator custom resource: %s", err.Error()),
})
return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, operatorCR)})
}
上述代码现在将在控制器最初无法访问 Operator 的 CRD 时尝试报告降级状态条件。代码继续如下:
deployment := &appsv1.Deployment{}
create := false
err = r.Get(ctx, req.NamespacedName, deployment)
if err != nil && errors.IsNotFound(err) {
create = true
deployment = assets.GetDeploymentFromFile("assets/nginx_deployment.yaml")
} else if err != nil {
logger.Error(err, "Error getting existing Nginx deployment.")
meta.SetStatusCondition(&operatorCR.Status.Conditions, metav1.Condition{
Type: "OperatorDegraded",
Status: metav1.ConditionTrue,
Reason: "OperandDeploymentNotAvailable",
LastTransitionTime: metav1.NewTime(time.Now()),
Message: fmt.Sprintf("unable to get operand deployment: %s", err.Error()),
})
return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, operatorCR)})
}
这段代码的工作方式类似于之前的代码,但现在如果 nginx 操作数的 Deployment 清单不可用,它将报告降级状态:
deployment.Namespace = req.Namespace
deployment.Name = req.Name
if operatorCR.Spec.Replicas != nil {
deployment.Spec.Replicas = operatorCR.Spec.Replicas
}
if operatorCR.Spec.Port != nil {
deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = *operatorCR.Spec.Port
}
ctrl.SetControllerReference(operatorCR, deployment, r.Scheme)
if create {
err = r.Create(ctx, deployment)
} else {
err = r.Update(ctx, deployment)
}
if err != nil {
meta.SetStatusCondition(&operatorCR.Status.Conditions, metav1.Condition{
Type: "OperatorDegraded",
Status: metav1.ConditionTrue,
Reason: "OperandDeploymentFailed",
LastTransitionTime: metav1.NewTime(time.Now()),
Message: fmt.Sprintf("unable to update operand deployment: %s", err.Error()),
})
return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, operatorCR)})
}
如果更新操作数部署的尝试失败,则此块将报告操作员处于降级状态。如果控制器能够成功通过此点,则无需报告降级。因此,下一块代码将通过更新操作员的 CRD 来结束函数,显示没有降级条件。
meta.SetStatusCondition(&operatorCR.Status.Conditions, metav1.Condition{
Type: "OperatorDegraded",
Status: metav1.ConditionFalse,
Reason: "OperatorSucceeded",
LastTransitionTime: metav1.NewTime(time.Now()),
Message: "operator successfully reconciling",
})
return ctrl.Result{}, utilerrors.NewAggregate([]error{err, r.Status().Update(ctx, operatorCR)})
}
在这段代码中,我们添加了四个SetStatusCondition()
调用,其中类型为"OperatorDegraded"
的条件会更新为当前时间和简短的原因:
-
如果操作员无法访问其自定义资源(除非是简单的
IsNotFound
错误),则将条件设置为True
,并注明原因OperatorResourceNotAvailable
。 -
如果无法从嵌入的 YAML 文件中获取 nginx 部署清单,则将条件更新为
True
,并注明原因OperandDeploymentNotAvailable
。 -
如果成功找到部署清单,但创建或更新操作失败,则将条件设置为
True
,并注明原因OperandDeploymentFailed
。 -
最后,如果
Reconcile()
函数执行完成且没有严重错误,则将OperatorDegraded
条件设置为False
。成功与失败的指标
请注意,正如在第三章《设计操作员——CRD、API 和目标协调》中讨论的那样,
False
条件表示成功运行。我们也可以反转这个逻辑,将条件命名为OperatorSucceeded
,在这种情况下,默认条件为True
,任何失败都会将条件更改为False
。这样做仍然符合 Kubernetes 的约定,因此,最终的决定由开发者根据他们希望传达的意图来做出。
在这个示例中,我们为每个Reason
更新使用了字符串字面量。在实际应用中,通常会在操作员的 API 中定义各种Reason
常量,以便保持一致性和可重用性。例如,我们可以在api/v1alpha1/nginxoperator_types.go
中定义以下内容,并通过常量名称来使用它们:
const (
ReasonCRNotAvailable = "OperatorResourceNotAvailable"
ReasonDeploymentNotAvailable = "OperandDeploymentNotAvailable"
ReasonOperandDeploymentFailed = "OperandDeploymentFailed"
ReasonSucceeded = "OperatorSucceeded"
)
条件原因的命名方案由开发者决定,唯一的 Kubernetes 条件要求是必须使用驼峰命名法(CamelCase)。因此,不同的条件类型和原因可以根据具体操作员的需求进行命名。当前唯一存在的标准条件名称是Upgradeable
,该条件由 OLM 使用。我们将在下一节中展示如何使用该条件。
实现这些条件后,我们将在与我们的操作员自定义资源交互时看到以下输出:
$ kubectl describe nginxoperators/cluster
Name: cluster
Namespace: nginx-operator-system
API Version: operator.example.com/v1alpha1
Kind: NginxOperator
Metadata:
Creation Timestamp: 2022-01-20T21:47:32Z
Generation: 1
...
Spec:
Replicas: 1
Status:
Conditions:
Last Transition Time: 2022-01-20T21:47:32Z
Message: operator successfully reconciling
Reason: OperatorSucceeded
Status: False
Type: OperatorDegraded
在下一节中,我们将展示如何将操作员条件直接报告给 OLM。
使用 OLM OperatorCondition
我们已经讨论了 OLM 如何管理当前已安装的 Operator 列表,包括升级和降级。在 第七章,《使用 Operator 生命周期管理器安装和运行 Operators》中,我们将展示 OLM 在一些功能中的实际应用。但是,目前我们可以在我们的 Operator 中实现条件报告,以便 OLM 能够了解可能会阻止 Operator 升级的某些状态。结合 Operator 特有的状态条件(如在其 CRD 中报告的状态),这可以帮助 OLM 获取有关 Operator 当前状态的关键信息。
要读取 Operator 条件,OLM 会创建一个名为 OperatorCondition
的自定义资源。OLM 会自动为它管理的每个 Operator 创建该对象的实例,来自一个 CRD。一个示例 OperatorCondition
对象如下所示:
apiVersion: operators.coreos.com/v1
kind: OperatorCondition
metadata:
name: sample-operator
namespace: operator-ns
status:
conditions:
- type: Upgradeable
status: False
reason: "OperatorBusy"
message: "Operator is currently busy with a critical task"
lastTransitionTime: "2022-01-19T12:00:00Z"
该对象使用的是与之前相同的 Kubernetes API 中的 Condition
类型(当实现 Operator CRD 中的状态条件时)。这意味着它也包括相同的字段,如 type
、status
、reason
和 message
,这些字段可以像之前一样更新。不同之处在于,现在将 type
设置为 Upgradeable
会指示 OLM 阻止任何升级 Operator 的尝试。
另一个不同之处在于,Operator 需要将此状态更改报告给不同的 CRD(而不是其自身)。为此,可以使用 github.com/operator-framework/operator-lib
上提供的库,其中包括更新 OperatorCondition
CRD 的辅助函数。有关使用此库的详细信息,请参考 Operator Framework 增强功能库,地址为 github.com/operator-framework/enhancements/blob/master/enhancements/operator-conditions-lib.md
。一种实现方法是通过修改 controllers/nginxoperator_controller.go
中的 Reconcile()
函数,如下所示:
import (
...
apiv2 "github.com/operator-framework/api/pkg/operators/v2"
"github.com/operator-framework/operator-lib/conditions"
)
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
...
condition, err := conditions.InClusterFactory{r.Client}.
NewCondition(apiv2.ConditionType(apiv2.Upgradeable))
if err != nil {
return ctrl.Result{}, err
}
err = condition.Set(ctx, metav1.ConditionTrue,
conditions.WithReason("OperatorUpgradeable"),
conditions.WithMessage("The operator is upgradeable"))
if err != nil {
return ctrl.Result{}, err
}
...
}
这段代码导入了两个新模块:Operator Framework V2 API 和 operator-lib/conditions
库。它接着实例化了一个新的 Factory
对象,该对象使用了已经可用的 Kubernetes 客户端。然后,工厂可以使用 NewCondition()
创建新的 Condition
对象,该函数接受 ConditionType
(实质上是一个字符串),并基于此类型创建一个条件。
在这个示例中,Condition
是使用 apiv2.Upgradeable
类型创建的,这是 Operator Framework 为 Upgradeable
状态定义的常量,该状态被 OLM 理解。
接下来,condition.Set()
函数更新 OLM 为我们的 Operator 创建的 OperatorCondition
CRD 对象。具体来说,它将新创建的条件以及传递给它的状态(在此情况下为 True
)添加到(或更新)条件列表中。此外,还有两个可以选择传递给 Set()
的函数(WithReason()
和 WithMessage()
),它们分别用于设置条件的原因和消息。
使用这些辅助函数可以大大简化从 OLM 为我们的 Operator 创建的 OperatorCondition
CRD 对象中检索和更新的工作。此外,OLM 还会采取措施确保 Operator 无法删除 CRD 或修改对象状态以外的任何内容。然而,OperatorCondition
CRD spec
中仍有一些其他字段可以由管理员修改。具体来说,overrides
字段允许用户手动绕过自动报告的条件更新,这些更新本应阻止升级。该字段的示例用法如下:
apiVersion: operators.coreos.com/v1
kind: OperatorCondition
metadata:
name: sample-operator
namespace: operator-ns
spec:
overrides:
- type: Upgradeable
status: True
reason: "OperatorIsStable"
message: "Forcing an upgrade to bypass bug state"
status:
conditions:
- type: Upgradeable
status: False
reason: "OperatorBusy"
message: "Operator is currently busy with a critical task"
lastTransitionTime: "2022-01-19T12:00:00Z"
像这样使用 overrides
可以在遇到已知问题或错误时非常有用,这些问题或错误不应阻止 Operator 升级。
实现指标报告
指标是任何 Kubernetes 集群中至关重要的一部分。指标工具可以提供集群中几乎所有可衡量数据的详细洞察。这也是为什么指标是将 Operator 提升到能力模型第四级的关键因素。事实上,大多数原生 Kubernetes 控制器已经报告了关于自身的指标,例如 schedule_attempts_total
,它报告调度器尝试将 Pods 调度到节点上的次数。
Kubernetes 监控架构的原始设计(github.com/kubernetes/design-proposals-archive/blob/main/instrumentation/monitoring_architecture.md
)定义了像 schedule_attempts_total
这样的指标作为 服务指标。服务指标的替代品是 核心指标,即通常可以从所有组件中获取的指标。核心指标目前包括关于 CPU 和内存使用情况的信息,并由 Kubernetes metrics-server 应用程序抓取(github.com/kubernetes-sigs/metrics-server
)。
另一方面,服务指标公开了在各个组件的代码中定义的特定应用程序数据。所有这些数据都可以通过 Prometheus(prometheus.io
)或 OpenTelemetry(opentelemetry.io
)等工具抓取和聚合,并通过前端可视化应用程序(如 Grafana(grafana.com
))呈现。
在 Kubernetes 中,度量的整个概念远远超出了仅与 Operator 的实现相关的部分。虽然这意味着有很多信息超出了本章节的范围,但有大量社区和文档资源可以覆盖此主题的基础知识。因此,本节将专注于针对新的服务度量的相关 Operator 实现步骤,假设读者已经具备了关于度量的基本了解。
添加自定义服务度量
当项目初始化时,operator-sdk
生成的样板代码已经包括了暴露 Operator Pod 中度量端点所需的代码和依赖项。默认情况下,这是位于端口8080
上的/metrics
路径。这消除了需要精心编写 HTTP 处理程序代码的麻烦,并允许我们将精力集中在实现度量本身上,如 Prometheus 文档中所述(prometheus.io/docs/guides/go-application/#adding-your-own-metrics
)。
Kubebuilder 度量
内置的度量处理程序代码是 Operator SDK 的另一个方面,实际上是由 Kubebuilder 提供的。此实现依赖于从sigs.k8s.io/controller-runtime
导入的度量库。该库包括如全局度量注册表等功能,已经对核心 Operator 代码可用。该库易于接入,以便从 Operator 的代码库中的任何位置注册新度量并更新它们。关于度量及其使用的更多信息,可以在 Kubebuilder 文档中找到,地址是book.kubebuilder.io/reference/metrics.html
。
构建此功能的controller-runtime
库已经包含有关 Operator 控制器代码的若干指标,每个指标以controller_runtime_
为前缀。这些指标包括:
-
controller_runtime_reconcile_errors_total
– 一个计数器指标,显示Reconcile()
函数返回非 nil 错误的累计次数 -
controller_runtime_reconcile_time_seconds_bucket
– 一个显示单个同步尝试延迟的直方图 -
controller_runtime_reconcile_total
– 一个计数器,每次尝试调用Reconcile()
时都会增加
在这个示例中,我们将重新创建最后一个指标controller_runtime_reconcile_total
,作为报告我们的 Operator 在集群中尝试同步其状态的次数的一种方式。
RED 指标
在定义任何应用程序中的指标类型时,可能性几乎是无限的。这可能会让开发人员感到决策疲劳,因为看似没有好的起点。那么,Operator 应该暴露哪些最重要的指标呢?Operator SDK 文档建议遵循 RED 方法,该方法概述了每个服务在 Kubernetes 集群中应该暴露的三种关键类型的指标:
-
hotloop
条件或次优请求管道。 -
controller_runtime_reconcile_errors_total
)。当与 Rate 指标相关联时,这可以帮助调试常见的故障,这些故障正在降低 Operator 的性能。 -
controller_runtime_reconcile_time_seconds
)。此信息可以指示性能差或其他降低集群健康状况的情况。
这些基础指标可以为定义 服务级目标 (SLOs) 提供基础,以确保你的 Operator 在应用程序的预期标准范围内运行。
为了开始将自定义指标添加到我们的 Operator,最好将指标定义组织到它们自己的包中,方法是创建一个新的文件 controllers/metrics/metrics.go
。这个新模块将保存我们新指标的声明,并将其自动注册到来自 sigs.k8s.io/controller-runtime/pkg/metrics
的全局注册表中。在以下代码中,我们定义了这个文件并实例化了一个新的自定义指标:
controllers/metrics/metrics.go:
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
var (
ReconcilesTotal = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "reconciles_total",
Help: "Number of total reconciliation attempts",
},
)
)
func init() {
metrics.Registry.MustRegister(ReconcilesTotal)
}
这个文件依赖于 Prometheus 客户端库来定义一个新的计数器(一个简单的递增指标),我们将其存储在 ReconcilesTotal
公共变量中。该指标的实际名称是 reconciles_total
,这是暴露给 Prometheus 的名称。
指标命名最佳实践
在实际环境中,最好在指标名称前添加特定于导出该指标的应用程序的前缀。这是推荐的最佳实践之一,建议将 _total
用于这个累计指标。熟悉这些实践非常有帮助,不仅有助于你开发自己的指标,还能帮助你了解在与其他指标交互时需要期待什么。
通过这个文件的 init()
函数自动将新指标注册到全局注册表中,我们现在可以在 Operator 的代码中的任何地方更新这个指标。由于这是衡量控制器进行的所有协调尝试的总数,所以在 controllers/nginxoperator_controller.go
中的 Reconcile()
函数声明开始时更新它是合理的。这可以通过以下两行新代码来实现:
controllers/nginxoperator_controller.go:
import (
...
"github.com/sample/nginx-operator/controllers/metrics"
...
)
func (r *NginxOperatorReconciler) Reconcile(...) (...) {
metrics.ReconcilesTotal.Inc()
...
}
所需要做的就是导入刚刚创建的新metrics
包,并调用metrics.ReconcilesTotal.Inc()
。该函数不返回任何内容,因此无需添加任何错误处理或状态更新。我们还希望将其放在函数的第一行,因为目标是对每次调用Reconcile()
(无论调用是否成功)进行递增。
更新的指标值会通过由 kubebuilder 初始化的指标端点自动报告,因此可以通过正确配置的 Prometheus 实例查看,如下所示:
图 5.1 – Prometheus UI 中 reconciles_total 指标图表的截图
与内置的controller_runtime_reconcile_total
指标相比,我们看到值是相同的:
图 5.2 – 比较自定义 reconciles_total 指标与内置 controller_runtime_reconcile_total 指标的截图
我们将在第六章中详细介绍如何安装和配置 Prometheus 来捕获这个自定义指标,构建和部署您的 Operator。
实现领导者选举
领导者选举是任何分布式计算系统中的一个重要概念,不仅仅是 Kubernetes(也不仅仅是针对 Operators)。高可用性应用程序通常会部署多个副本的工作负载 Pods,以支持用户期望的正常运行时间保证。在只能有一个工作负载 Pod 在集群中执行工作的情况下,该副本被称为领导者。其余的副本将等待,虽然在运行但没有做任何重要的工作,直到当前的领导者不可用或放弃其领导者身份。然后这些 Pods 会自行决定谁应该成为新的领导者。
启用正确的领导者选举可以大大提升应用程序的正常运行时间。这可以包括在一个副本失败时进行优雅的故障切换处理,或者帮助在滚动升级期间保持应用程序的可访问性。
Operator SDK 使得为 Operators 实现领导者选举变得简单。通过operator-sdk
生成的样板代码会在我们的 Operator 中创建一个标志--leader-elect
,默认值为false
,表示禁用领导者选举。该标志会传递给控制器初始化函数ctrl.NewManager()
中的LeaderElection
选项值:
main.go:
func main() {
...
var enableLeaderElection bool
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
...
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
...
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "df4c7b26.example.com",
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
LeaderElectionID
字段表示 Operator 用于持有领导者锁的资源名称。
该设置(使用ctrl.NewManager()
中的LeaderElection
选项)将操作员设置为使用两种可能的领导选举策略中的第一个,即带租约的领导。另一种可能的领导选举策略是终身领导。每种策略都有一些优点和权衡,如 Operator SDK 文档中所描述的(sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#leader-election
)。
-
带租约的领导 – 默认的领导选举策略,其中当前领导者定期尝试续期其领导者身份(称为其租约)。如果无法续期,领导者会将其职位交给新的领导者。此策略通过在必要时快速切换领导者来提高可用性。然而,该策略可能导致所谓的脑裂场景,其中多个副本认为自己是领导者。
-
在调用
ctrl.NewManager()
时,RenewDeadline
和LeaseDuration
字段用于为集群中最快和最慢节点之间的时钟偏差比率添加大致的容忍度。这是因为LeaseDuration
是当前非领导者在尝试获取领导权之前将等待的时间,而RenewDeadline
是控制平面等待刷新当前领导权的时间。因此,举个例子,将LeaseDuration
设置为RenewDeadline
的某个倍数,将在集群内为该时钟偏差比率添加容忍度。例如,假设当前领导者运行在节点 A 上,该节点的时间保持准确。如果备份副本运行在节点 B 上,该节点的速度较慢并且时间比节点 A 慢一半,那么节点 A 和节点 B 之间存在 2:1 的时钟偏差比率。
在这种情况下,如果领导者的
LeaseDuration
为 1 小时并因某种原因失败,则可能需要 2 小时才能让节点 B 注意到租约已过期并尝试获取新租约。然而,如果
RenewDeadline
为 30 分钟,原始领导者将在该时间范围内无法续约其租约。这允许运行在较慢节点上的副本仅按其认为的 30 分钟进行操作,但实际时间已超过LeaseDuration
的 1 小时。这是领导选举库中的一个隐蔽细节,但对于可能受此时钟偏差影响的集群操作员而言,值得注意。有关此主题的更多讨论,可以参考原始 GitHub 拉取请求,内容为将领导选举添加到 Kubernetes 客户端,链接为github.com/kubernetes/kubernetes/pull/16830
。
为了使我们的操作员可配置,以便用户可以在带租约的领导选举策略和终身领导选举策略之间进行选择,我们可以利用现有的--leader-elect
标志来启用或禁用对leader.Become()
函数的调用:
import (
"github.com/operator-framework/operator-lib/leader"
...
)
var (
...
setupLog = ctrl.Log.WithName("setup")
)
func main() {
...
var enableLeaderElection bool
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
...
if !enableLeaderElection {
err := leader.Become(context.TODO(), "nginx-lock")
if err != nil {
setupLog.Error(err, "unable to acquire leader lock")
os.Exit(1)
}
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
...
LeaderElection: enableLeaderElection,
LeaderElectionID: "df4c7b26.example.com",
})
现在,我们的 Operator 将始终使用某种方式的领导者选举。leader.Become()
函数是一个阻塞调用,如果无法获取领导者锁,它将阻止 Operator 的运行。它将每秒尝试一次。如果成功,它将创建一个 ConfigMap 作为锁(在我们的例子中,这个 ConfigMap 将命名为 nginx-lock)。
添加健康检查
健康检查(也称为存活性和就绪性探针)是任何 Pod 使其当前功能状态可以被集群中其他组件发现的一种方式。通常是通过在容器中暴露一个端点来完成的(传统上/healthz
用于存活性检查,/readyz
用于就绪性检查)。其他组件(如 kubelet)可以访问该端点,以确定 Pod 是否健康并准备好服务请求。该主题在 Kubernetes 文档中有详细的说明,详见kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
。
main.go
中由 Operator SDK 初始化的代码默认包含了healthz
和readyz
检查的设置:
main.go
import (
...
"sigs.k8s.io/controller-runtime/pkg/healthz"
)
func main() {
...
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "df4c7b26.example.com",
})
...
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
这段代码设置了两个端点,在主控制器启动之前立即开始提供服务。这是有意义的,因为此时,所有其他启动代码已经运行完毕。在尝试创建客户端连接之前,开始宣传你的 Operator 为健康和准备好服务请求是没有意义的。
这两个函数,mgr.AddHealthzCheck
和mgr.AddReadyzCheck
,每个都接受两个参数:一个字符串(用于检查的名称)和一个返回检查状态的函数。该函数必须具有以下签名:
func(*http.Request) error
该签名显示该函数接受一个 HTTP 请求(因为该检查正在提供一个端点,该端点将被 kubelet 等组件查询),并返回一个错误(如果检查失败)。在这段代码中,Operator 已经填充的healthz.Ping
函数是一个简单的nil
错误(表示成功)。这虽然没有太多洞察力,但它在代码中的位置提供了至少报告 Operator 已成功通过大部分初始化过程的最小信息。
然而,在前面的函数签名之后,可以实现自定义健康检查。只需再次调用mgr.AddHealthzCheck
或mgr.AddReadyzCheck
(取决于检查类型),并将新函数传递给每个调用,即可添加这些附加检查。当查询/healthz
或/readyz
端点时,这些检查会按顺序运行(没有保证的顺序),如果有任何检查失败,则返回HTTP 500
状态。通过这种方式,可以开发具有更复杂逻辑的存活性和就绪性检查,这些逻辑是针对你的 Operator 独特的(例如,在报告就绪性之前,依赖组件需要可访问)。
总结
本章突出了一些功能选项,超出了在第四章,使用 Operator SDK 开发操作符 中建立的最低限度。这个列表显然不会详尽列举每种高级功能的可能性,但旨在展示一些添加到操作符中最常见的附加功能。此时,一些特性开发模式应该开始变得清晰起来(例如,启动和初始化代码通常放在 main.go
中,而与核心逻辑相关的功能可以很好地与 nginxoperator_controller.go
中的控制器代码或其自己的包中适配)。
本章的工作展示了从低级功能逐步提升到能力模型中更高级别的操作符所需的一些步骤。例如,度量指标是四级(深入洞察)操作符的关键方面,因此,这是期望用户使用的最高功能操作符之一。此外,领导选举可以帮助建立故障转移过程(在达到三级 – 全生命周期 时非常有帮助)。添加这样的功能有助于构建一个有用且功能丰富的操作符,提高应用程序性能,进而提升用户体验。
在下一章中,我们将开始编译在第四章,使用 Operator SDK 开发操作符,以及第五章,开发操作符 – 高级功能 中构建的代码。然后,我们将演示如何在本地 Kubernetes 集群中部署此代码并运行我们的 nginx 操作符(不使用 OLM)。这将是一个有用的开发过程,因为在测试环境中绕过 OLM 可以简化和加快我们迭代和测试新变更的能力。
第六章:第六章:构建和部署您的 Operator
到目前为止,我们已经编写了大量代码,以便开发由基础模板 Operator SDK 项目提供的 make
命令,用于构建容器镜像并手动将该镜像部署到正在运行的 Kubernetes 集群中。此外,本章将继续跟进这些步骤,提供迭代开发的指导步骤,其中 Operator 中的更改将被编译并推送到集群中。最后,我们将提供故障排除资源和在此过程中可能遇到的问题的解决建议。这些部分将分为以下内容:
-
构建容器镜像
-
在测试集群中进行部署
-
推送和测试变更
-
故障排除
请注意,在本章过程中,将使用本地构建命令手动运行 Operator。对于本地开发和非生产环境中的测试,这是非常有用的,因为它快速且不依赖于额外的组件,最大限度地减少了部署概念验证测试用例所需的时间和资源。在实际环境中,最好使用Operator 生命周期管理器来安装和管理 Operator。这个过程将在第七章中更详细地介绍,标题为 使用 Operator 生命周期管理器安装和运行 Operator。现在,我们将继续在测试集群中进行本地部署。
技术要求
本章将依赖前几章的代码来构建容器镜像,并将该镜像部署到 Kubernetes 集群中。因此,本章的技术要求需要访问一个集群和像 Docker 这样的容器管理工具。然而,使用前几章的代码并非强制要求,因为所解释的命令和过程适用于任何 operator-sdk
项目。因此,本章的最低推荐要求如下:
-
互联网连接(用于拉取 Docker 基础镜像并将构建好的容器镜像推送到公共注册表)。
-
访问一个运行中的 Kubernetes 集群。这可以是任何集群,尽管推荐使用像 Kubernetes in Docker (kind) (
kind.sigs.k8s.io/
) 或 minikube (minikube.sigs.k8s.io/docs/
) 这样的工具,以便在需要时可以轻松销毁并重新创建集群。 -
计算机上安装了最新版本的
kubectl
(kubernetes.io/docs/tasks/tools/#kubectl
),用于与 Kubernetes 集群进行交互。 -
本地安装了 Docker,并且在
Makefile
文件中生成的 Operator SDK 项目中假设docker
二进制文件会在本地可用。因此,需要额外的本地设置(例如,将docker
命令别名为buildah
),本章不涉及这部分内容,但这是必须的。
本章介绍了前面几个项目,其中一些需要额外的设置。此外,部分项目(例如 kind)在本教程中仅用于创建一个标准的测试环境以供跟随。在这些情况下,如果您对其他工具更为熟悉,可以按需选择。对于本章中介绍的每项技术,若有需要帮助的地方,本章末尾的 故障排除 部分提供了更多资源。然而,本章中所选的技术使用案例相对简单,旨在指导您最大限度地减少技术问题的风险。
注意
使用没有任何访问凭证的公共注册表会使您的 Operator 镜像对互联网上的任何人开放。虽然对于像这样的教程来说可能没问题,但对于生产镜像,您可能希望进一步了解如何保护您的镜像注册表(这超出了本书的范围)。
本章的《Code in Action》视频可以在以下网址观看:bit.ly/3NdVZ7s
构建容器镜像
Kubernetes 是一个容器编排平台,意味着它被设计用来运行已经构建成容器的应用程序。即使是 Kubernetes 的核心系统组件,如 API 服务器和调度器,也以容器的形式运行。因此,Kubernetes 开发的 Operators 也必须作为容器来构建和部署,这一点应该不足为奇。
对于这个过程,了解容器操作的基础知识是有帮助的。然而,幸运的是,Operator SDK 抽象化了许多配置和命令行操作,通过简单的 Makefile
目标进行处理。这些是构建宏,帮助自动化编译二进制文件和容器镜像的过程(以及将这些镜像推送到注册表并在集群中部署它们)。
要查看 Operator SDK 提供的所有可用目标列表,请在项目中运行 make help
命令:
$ make help
Usage:
make <target>
General
help Display this help.
Development
manifests Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
generate Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
fmt Run go fmt against code.
vet Run go vet against code.
test Run tests.
Build
build Build manager binary.
run Run a controller from your host.
docker-build Build docker image with the manager.
docker-push Push docker image with the manager.
Deployment
install Install CRDs into the K8s cluster specified in ~/.kube/config.
uninstall Uninstall CRDs from the K8s cluster specified in ~/.kube/config.
deploy Deploy controller to the K8s cluster specified in ~/.kube/config.
undeploy Undeploy controller from the K8s cluster specified in ~/.kube/config.
controller-gen Download controller-gen locally if necessary.
kustomize Download kustomize locally if necessary.
envtest Download envtest-setup locally if necessary.
bundle Generate bundle manifests and metadata, then validate generated files.
bundle-build Build the bundle image.
bundle-push Push the bundle image.
opm Download opm locally if necessary.
catalog-build Build a catalog image.
catalog-push Push a catalog image.
其中一些命令,如 make manifests
和 make generate
,在前面的章节中已经使用过,用于初始化项目并生成 Operator 的 API 和 Build
头文件。具体来说,make build
和 make docker-build
,前者负责编译 Operator 的本地二进制文件,后者则构建容器镜像。
本地构建 Operator
首先,让我们来看看 make build
。从 Makefile
中,定义非常简单:
build: generate fmt vet ## Build manager binary.
go build -o bin/manager main.go
这个目标主要涉及运行 go build
,这是标准命令,用于编译任何 make generate
、make fmt
和 make vet
目标,这些目标确保 Operator 生成的 API 代码是最新的,并且项目源代码中的 Go 代码符合语言的风格标准。这是一个附加的便利,因此像这样的 Makefile
目标在开发中非常有用。
运行 make build
会生成编译 Go 程序时预期的标准输出:
$ make build
/home/nginx-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go
当编译成功时,输出不会超过前面的代码。完成后,bin/manager
下将会有一个新的可执行文件,这是编译后的 Operator 代码。可以手动运行该文件(或使用 make run
),并连接到任何可访问的 Kubernetes 集群,但在构建成容器镜像之前,它不会真正部署到集群中。这正是 make docker-build
的作用。
使用 Docker 构建 Operator 镜像
make docker-build
的定义比本地的 build
目标稍微有趣一些:
docker-build: test ## Build docker image with the manager.
docker build -t ${IMG} .
这基本上就是调用 docker build
(并添加了一个依赖项,用于测试运行项目中定义的任何单元测试,同时确保所有生成的 CRD 清单和 API 代码都是最新的)。
docker build
命令将指示本地 Docker 守护进程根据 Operator 项目目录根目录中的 Dockerfile 构建一个容器镜像。此文件最初由项目首次创建时的 operator-sdk init
命令生成。我们做了一些微小的修改(将在此处解释),因此该文件现在看起来如下:
Dockerfile:
# Build the manager binary
FROM golang:1.17 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY assets/ assets
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
请注意,关于 Dockerfile 如何工作的详细细节是容器构建中更高级的话题。并不需要深入理解这些细节(这也是使用 Operator SDK 生成该文件的一个好处!),但我们会在此处做一个总结。大致的步骤如下:
-
设置 Operator 构建的基础镜像为 Go 1.17。
-
将 Go 模块依赖文件复制到新镜像中。
-
下载模块依赖。
-
将主 Operator 代码,包括
main.go
、api/
、controllers/
和assets/
,复制到新镜像中。注意
在此项目中,我们修改了默认的 Dockerfile 来复制
assets/
目录。当它由operator-sdk
生成时,默认只会复制main.go
以及api/
和controllers/
目录。由于我们为 nginx Operator 编写的教程包括在assets/
下添加一个新的顶级包,因此我们需要确保该目录也被包含在 Operator 镜像中。这作为一个示例,展示了修改项目的默认 Dockerfile 是完全可以的(不过建议使用版本控制或其他方式进行备份)。或者,
assets
包本可以在controllers/
文件夹下创建,这样就不需要对 Dockerfile 做任何更新(因为它会被包括在现有的COPY controllers/ controllers/
行中)。有关更多信息,请参见本章的 故障排除 部分。 -
在镜像内编译 Operator 二进制文件。这与在本地构建 Operator(如前所示)相同,只不过它将被打包进容器中。
-
将运算符的二进制文件定义为构建容器的主要命令。
使用上述 Dockerfile(包括包含 COPY assets/ assets/
的更改),运行 make docker-build
将会成功完成。但在此之前,请首先注意,这个命令包含了一个我们尚未讨论的变量:${IMG}
。
Makefile
命令使用这个 IMG
变量来定义编译后镜像的标签。该变量在 Makefile
中较早定义,默认值为 controller:latest
:
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
了解这一点很有帮助,因为如果不更新此变量,我们为运算符构建的镜像将简单地命名为 controller
。为了构建一个带有引用我们实际容器仓库的标签的镜像(例如,docker.io/myregistry
),可以像这样调用 build
命令:
$ IMG=docker.io/sample/nginx-operator:v0.1 make docker-build
...
docker build -t docker.io/sample/nginx-operator:v0.1 .
[+] Building 99.1s (18/18) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> [builder 1/10] FROM docker.io/library/golang:1.17 21.0s
=> [builder 2/10] WORKDIR /workspace 2.3s
=> [builder 3/10] COPY go.mod go.mod 0.0s
=> [builder 4/10] COPY go.sum go.sum 0.0s
=> [builder 5/10] RUN go mod download 31.3s
=> [builder 6/10] COPY main.go main.go 0.0s
=> [builder 7/10] COPY api/ api/ 0.0s
=> [builder 8/10] COPY controllers/ controllers/ 0.0s
=> [builder 9/10] COPY assets/ assets 0.0s
=> [builder 10/10] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 42.5s
=> [stage-1 2/3] COPY --from=builder /workspace/manager .
=> exporting to image 0.2s
=> => exporting layers 0.2s
=> => writing image sha256:dd6546d...b5ba118bdba4 0.0s
=> => naming to docker.io/sample/nginx-operator:v0.1
部分输出已被省略,但需要注意的重要部分是 builder
步骤,这些步骤已被包含。它们遵循项目 Dockerfile 中定义的步骤。
成功构建容器镜像后,新的镜像现在应该已经存在于您的本地机器上。您可以通过运行 docker images
来确认这一点:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
sample/nginx-operator v0.1 dd6546d2afb0 45 hours ago 48.9MB
在下一节中,我们将把这个镜像推送到公共镜像仓库,并在运行中的 Kubernetes 集群中部署运算符。
在测试集群中部署
现在,运算符已经构建成容器镜像,可以作为容器部署到集群中。要做到这一点,首先需要确保你有访问运行中集群的权限,并且有一个公共镜像仓库。要将镜像托管在仓库中,您可以在 Docker Hub 上获取一个免费的个人账户 (hub.docker.com
)。
在本教程中,我们将使用通过 kind 创建的本地 Kubernetes 集群,该集群在 Docker 容器中运行 Kubernetes 集群,而不是直接在本地机器上运行,您可以在 kind.sigs.k8s.io/
上访问该项目。然而,这里描述的步骤与任何运行最新版本平台的 Kubernetes 集群无关。例如,如果您更喜欢使用像 minikube 这样的开发环境(或已经有其他集群可用),那么可以跳过本节中展示的 kind 设置。该节中的其余步骤将适用于任何 Kubernetes 集群。
要使用 kind 启动本地集群,请确保在机器上已安装 Docker 和 kind,然后运行 kind create cluster
:
$ kind create cluster
Creating cluster "kind" ...
Ensuring node image (kindest/node:v1.21.1)
Preparing nodes
Writing configuration
Starting control-plane
Installing CNI
Installing StorageClass
Set kubectl context to "kind-kind"
You can now use your cluster with
kubectl cluster-info --context kind-kind
Have a nice day!
请注意,kind create cluster
可能需要一些时间才能完成。这会在 Docker 中引导一个功能正常的 Kubernetes 集群。您可以通过运行任何 kubectl
命令来确认集群是否可访问,例如,运行 kubectl cluster-info
:
$ kubectl cluster-info
Kubernetes master is running at https://127.0.0.1:56384
CoreDNS is running at https://127.0.0.1:56384/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
集群运行后,接下来是通过将镜像推送到公共镜像仓库来使运算符的镜像可访问。首先,确保您能够访问您的镜像仓库。对于 Docker Hub,这意味着运行 docker login
并输入您的用户名和密码。
登录后,你可以使用提供的Makefile
中的make docker-push
目标来推送镜像(这相当于手动运行docker push
):
$ IMG=docker.io/sample/nginx-operator:v0.1 make docker-push docker push docker.io/sample/nginx-operator:v0.1
The push refers to repository [docker.io/sample/nginx-operator]
18452d09c8a6: Pushed 5b1fa8e3e100: Layer already exists
v0.1: digest: sha256:5315a7092bd7d5af1dbb454c05c15c54131 bd3ab78809ad1f3816f05dd467930 size: 739
(此命令可能需要一些时间来运行,你的输出可能与示例略有不同。)
注意,我们仍然将IMG
变量传递给了此命令。为了避免这样做,你可以修改Makefile
来更改变量的默认定义(该定义在构建容器镜像部分已展示),或将镜像名称作为环境变量导出,如下所示:
$ export IMG=docker.io/sample/nginx-operator:v0.1
$ make docker-push
docker push docker.io/sample/nginx-operator:v0.1
The push refers to repository [docker.io/sample/nginx-operator]
18452d09c8a6: Pushed 5b1fa8e3e100: Layer already exists
v0.1: digest: sha256:5315a7092bd7d5af1dbb454c05c15c54131bd 3ab78809ad1f3816f05dd467930 size: 739
现在,镜像已公开可用。你可以通过运行docker pull <image>
手动确认镜像是否可以访问,但这不是必须的。
避免使用 Docker Hub
从技术上讲,你不必使用像 Docker Hub 这样的公共注册表来使镜像对集群可访问。还有其他方式将镜像导入到集群中,例如,kind 提供了kind load docker-image
命令,可以将镜像手动加载到集群的内部注册表中(有关更多信息,请参阅kind.sigs.k8s.io/docs/user/quick-start/#loading-an-image-into-your-cluster
)。不过,在本教程中,我们选择了公共注册表方式,因为这是一个常见的方法(特别是对于公开发布供他人使用的开源 Operator)且与运行的具体集群无关。
在 Operator 镜像可访问(且公共镜像名称已在环境变量中定义或如前所述在Makefile
中修改)之后,现在只需运行make deploy
命令即可在集群中运行 Operator:
$ make deploy
/Users/sample/nginx-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
cd config/manager && /Users/sample/nginx-operator/bin/kustomize edit set image controller=docker.io/sample/nginx-operator:v0.1
/Users/sample/nginx-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/nginx-operator-system created
customresourcedefinition.apiextensions.k8s.io/nginxoperators.operator.example.com created
serviceaccount/nginx-operator-controller-manager created
role.rbac.authorization.k8s.io/nginx-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/nginx-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/nginx-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/nginx-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/nginx-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/nginx-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/nginx-operator-proxy-rolebinding created
configmap/nginx-operator-manager-config created
service/nginx-operator-controller-manager-metrics-service created
deployment.apps/nginx-operator-controller-manager created
现在,你将在集群中看到一个与 Operator 名称匹配的新命名空间:
$ kubectl get namespaces
NAME STATUS AGE
default Active 31m
kube-node-lease Active 31m
kube-public Active 31m
kube-system Active 31m
local-path-storage Active 31m
nginx-operator-system Active 54s
使用kubectl get all
深入探索此命名空间将显示其中包含了 Operator 的Deployment、ReplicaSet、Service和Pod(部分输出已省略以简洁呈现):
$ kubectl get all -n nginx-operator-system
NAME READY STATUS pod/nginx-operator-controller-manager-6f5f66795d-945pb 2/2 Running
NAME TYPE service/nginx-operator-controller-manager-metrics-service ClusterIP
NAME READY UP-TO-DATE deployment.apps/nginx-operator-controller-manager 1/1 1
NAME DESIRED replicaset.apps/nginx-operator-controller-manager-6f5f66795d 1
但是操作数 nginx Pod 在哪里呢?回想一下,我们设计 Operator 时,如果找不到其 CRD 实例,它将什么也不做。为了解决这个问题,你可以像下面这样创建第一个 CRD 对象(符合第四章中定义的 API,使用 Operator SDK 开发 Operator):
sample-cr.yaml:
apiVersion: operator.example.com/v1alpha1
kind: NginxOperator
metadata:
name: cluster
namespace: nginx-operator-system
spec:
replicas: 1
创建此文件并将其保存为任何名称(在此例中,sample-cr.yaml
即可)。然后,运行kubectl create -f sample-cr.yaml
来在 nginx Operator 的命名空间中创建自定义资源对象。现在,运行kubectl get pods
将显示新创建的 nginx Pod(命名为cluster-xxx
):
$ kubectl get pods -n nginx-operator-system
NAME READY STATUS cluster-7855777498-rcwdj 1/1 Running
nginx-operator-controller-manager-6f5f66795d-hzb8n 2/2 Running
您可以使用 kubectl edit nginxoperators/cluster -n nginx-operator-system
修改刚刚创建的自定义资源对象。此命令(kubectl edit
)将打开本地文本编辑器,您可以直接修改对象的 spec
字段。例如,要更改操作数副本的数量,可以运行前面的命令并设置 spec.replicas: 2
,如下所示:
$ kubectl edit nginxoperators/cluster -n nginx-operator-system
apiVersion: operator.example.com/v1alpha1
kind: NginxOperator
metadata:
creationTimestamp: "2022-02-05T18:28:47Z"
generation: 1
name: cluster
namespace: nginx-operator-system
resourceVersion: "7116"
uid: 66994aa7-e81b-4b18-8404-2976be3db1a7
spec:
replicas: 2
status:
conditions:
- lastTransitionTime: "2022-02-05T18:28:47Z"
message: operator successfully reconciling
reason: OperatorSucceeded
status: "False"
type: OperatorDegraded
请注意,使用kubectl edit
时,Operator 中的其他字段(如status
)也可见。虽然无法直接修改这些字段,但这是一个很好的地方,可以指出我们的 Operator 条件已成功报告在 CRD 的 status
部分。通过 OperatorDegraded: False
条件类型和状态可以看出这一点。
但是,请注意,这个条件最初可能会让用户感到困惑,因为它似乎表明 OperatorSucceeded
是 False
。但经过进一步检查后,可以看出 OperatorSucceeded
实际上是 OperatorDegraded
为 False
的原因。换句话说,Operator 并未 降级,因为 Operator 成功 了。这个例子被特意选择出来,强调在实现具有信息量且易于理解的状态条件时必须小心。
保存对 CRD 对象的更改后,再次运行 kubectl get
pods 现在会显示一个新的 nginx Pod:
$ kubectl get pods -n nginx-operator-system
NAME READY STATUS cluster-7855777498-rcwdj 1/1 Running
cluster-7855777498-kzs25 1/1 Running
nginx-operator-controller-manager-6f5f66795d-hzb8n 2/2 Running
类似地,将 spec.replicas
字段更改为 0
将删除所有 nginx Pods:
$ kubectl get pods -n nginx-operator-system
NAME READY STATUS cluster-7855777498-rcwdj 1/1 Terminating
cluster-7855777498-kzs25 1/1 Terminating
nginx-operator-controller-manager-6f5f66795d-hzb8n 2/2 Running
这就完成了 Operator 的基本部署步骤。以下步骤总结了我们迄今为止所做的工作:
-
为 Operator 构建了容器镜像
-
将镜像推送到公共注册表
-
使用
make deploy
在本地集群中启动 Operator(在此过程中,将镜像从公共注册表拉取到集群中) -
手动创建了 Operator 的 CRD 对象实例
-
使用
kubectl edit
修改了集群中的 CRD 对象
然而,仍然需要一些工作来启用指标(这也是在第五章《开发 Operator – 高级功能》中的一个重要部分,并且是实现 能力模型 中更高级功能的关键)。在下一部分中,我们将展示如何修改我们的 Operator 部署并重新部署到集群中。
推送并测试更改
在开发过程中(无论是任何项目,不仅仅是 Kubernetes Operator),可能需要对代码或其他项目文件(如资源清单)进行更改并测试这些更改。在此示例中,我们不会更改任何代码。相反,我们将重新部署 Operator,并创建正确的指标资源以使指标可见,这一点我们在 第五章《开发 Operator – 高级功能》中实现了。
安装和配置 kube-prometheus
没有工具来抓取和展示指标,指标是没有多大用处的。这正是 Prometheus 的作用,它理解我们所实现的自定义指标语言。还有许多其他工具可以解析 Prometheus 指标。在本教程中,我们将使用 kube-prometheus (github.com/prometheus-operator/kube-prometheus
) 来安装一个完整的端到端监控栈到我们的集群中。kube-prometheus 提供了许多额外的功能,虽然我们在本书中不会专门探讨它们,但它是安装监控的一个非常方便和强大的库。在您的环境中,您可以选择其他选项,比如直接安装 Prometheus,或者使用来自 github.com/prometheus-operator/prometheus-operator
的 Prometheus Operator(由 kube-prometheus 提供)。
要开始,按照 github.com/prometheus-operator/kube-prometheus
中的步骤进行操作,安装 kube-prometheus 到我们的 Operator 项目中。请注意链接中的 安装 和 编译 部分的前提条件。具体来说,以下工具是必需的:
-
jb
-
gojsontoyaml
-
jsonnet
当 kube-prometheus 成功安装到项目中后,我们将拥有一个新的子目录(my-kube-prometheus
,如 kube-prometheus 文档中所述),该目录包含以下文件:
$ ls
total 20K
drwxr-xr-x 8 ... .
drwxr-xr-x 21 ... ..
drwxr-xr-x 74 ... manifests
drwxr-xr-x 19 ... vendor
-rwxr-xr-x 1 ... build.sh
-rw-r--r-- 1 ... 05 example.jsonnet
-rw-r--r-- 1 ... jsonnetfile.json
-rw-r--r-- 1 ... 04 jsonnetfile.lock.json
现在,我们将修改 example.jsonnet
来包含我们的 Operator 命名空间。这意味着要修改文件中的 values+::
块,添加一个包含命名空间列表(在我们的例子中,只有 nginx-operator-system
命名空间)的 prometheus+
对象:
my-kube-prometheus/example.jsonnet:
local kp =
(import 'kube-prometheus/main.libsonnet') +
...
{
values+:: {
common+: {
namespace: 'monitoring',
},
prometheus+: {
namespaces+: ['nginx-operator-system'],
},
},
};
...
接下来,使用 build.sh
通过运行以下命令来编译新的清单:
$ ./build.sh example.jsonnet
现在,我们可以通过以下命令(在 my-kube-prometheus
目录内)将 kube-prometheus
清单应用到集群中:
$ kubectl apply --server-side -f manifests/setup
$ kubectl apply -f manifests/
完成后,您可以通过运行以下命令打开本地代理到您的集群和 Prometheus 服务,从而访问 Prometheus 仪表板:
$ kubectl --namespace monitoring port-forward svc/prometheus-k8s 9090
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
(请注意,这个命令将保持运行状态,直到您手动结束它,例如按 Ctrl + C。)您可以通过在 Web 浏览器中导航到 http://localhost:9090
来查看仪表板。然而,如果您尝试搜索我们的 Operator 的指标(记得它被命名为 reconciles_total
),您会发现它不可用。这是因为我们需要使用一个额外的清单重新部署 Operator,而这个清单在默认情况下不会被创建。
使用指标重新部署 Operator
Prometheus 通过之前创建的配置知道从我们的 Operator 命名空间抓取指标。然而,它仍然需要知道命名空间内具体要查询的端点。这是名为ServiceMonitor
的对象的作用(pkg.go.dev/github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1#ServiceMonitor
)。这个对象在运行make deploy
时并不会由 Operator SDK 默认创建,因此我们需要修改config/default/kustomization.yaml
。(该文件位置相对于项目根目录,而不是我们之前在安装 kube-prometheus 时创建的my-kube-prometheus
目录)。
在此文件中,只需找到标记为[PROMETHEUS]
的行,并通过删除前面的井号或哈希符号(#
)来取消注释。当前只有一行,如下所示:
config/default/kustomization.yaml:
...
bases:
- ../crd
- ../rbac
- ../manager
# ...
# ...
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. - ../prometheus
这是Kustomize的默认配置文件(kustomize.io/
),Kustomize 是一个 Kubernetes 模板项目,Operator SDK 使用它来生成和部署项目清单。
此时,你可以运行make undeploy
来移除当前的 Operator 安装,然后再次运行make deploy
重新创建它。几秒钟后,reconciles_total
指标应该会出现在 Prometheus 仪表盘中。以下截图展示了此指标在 Prometheus 仪表盘搜索栏中的样子:
图 6.1 – Prometheus 仪表盘截图
主要收获
虽然本节似乎专注于设置指标,但实际上它涵盖了一些与开发驱动的 Operator 项目重新部署相关的重要步骤。具体来说,我们涵盖了以下内容:
-
将
kube-prometheus
作为库安装到我们的项目中,并配置它以抓取我们 Operator 的命名空间 -
修改 Kustomize 配置文件,以便在我们的 Operator 部署中包含新的依赖项
-
使用
make undeploy
来移除现有的 Operator 部署
从技术上讲,我们本可以直接运行make deploy
,而无需先卸载项目。Kubernetes 资源清单的幂等性意味着只有新资源会被创建。然而,在需要完全移除现有项目的情况下,了解make undeploy
非常有用。
故障排除
本章介绍了几个新工具和概念,这些内容在前面的章节中尚未涉及。它们包括 Docker、kind、kubectl、Make 和 kube-prometheus。你可能在使用这些工具时遇到了一些问题,因此本节旨在提供一些参考链接,帮助解决常见问题。本章中使用的许多底层工具并非 Operator 框架所独有,幸运的是,这意味着有大量的资源可供解决你可能遇到的问题。
Makefile 问题
Make (www.gnu.org/software/make/
) 是一个非常流行的工具,用于自动化生成和编译无数的软件项目。它已经在第四章《使用 Operator SDK 开发 Operator》和第五章《开发 Operator - 高级功能》中使用,用于生成我们的项目所使用的 API 和清单。在本章中,它被更广泛地应用于自动化构建和部署的许多命令。
然而,本书中使用的make ...
命令是运行底层工具的简写。因此,当遇到任何make
命令的错误时,第一步的调试步骤是检查Makefile
,看看该命令实际上在运行什么。如果发生这种情况,你很可能会发现,实际上你遇到的是 Docker、Go 或 Kubernetes 的问题。
这些命令在operator-sdk
初始化项目时已经预先提供,但你可以根据需要修改提供的Makefile
,以定制你的项目。
kind
在本章中,我们使用 kind 来部署一个测试的 Kubernetes 集群。使用 kind 提供了一种非常快速的方式来创建(和销毁)本地 Kubernetes 集群。它还提供了一个可配置的设置,可以相对容易地更改默认集群(例如,启动一个包含额外节点的集群)。
kind 的官方网站是kind.sigs.k8s.io/
。该网站提供了大量文档和不同集群设置的示例配置。此外,kind 的代码库可以在 GitHub 上找到,网址是github.com/kubernetes-sigs/kind
。kind 的维护者和用户也活跃在官方 Kubernetes Slack 服务器(slack.k8s.io)的#kind频道。这两个链接都是提问或搜索答案的绝佳资源。
Docker
如果你正在使用 Kubernetes,可能已经熟悉 Docker(www.docker.com/
)。它只是构建和管理容器镜像的多个选项之一,这些镜像对于在 Kubernetes 上部署应用程序至关重要。将操作符代码转换为可部署镜像的关键步骤是 docker build
命令(在运行 make docker-build
时调用)。该命令遵循 Dockerfile 中定义的 build
步骤。有关 Dockerfile 语法的更多信息,请参见 Docker 文档:docs.docker.com/engine/reference/builder/
。
在按照本章步骤构建容器镜像时,你可能会遇到一些特定于本教程的独特问题,接下来将解释这些问题。
docker build 在构建资产包时因缺少必要模块而失败
在运行 make docker-build
时,你可能会遇到以下错误(或类似的错误):
$ make docker-build
...
=> ERROR [builder 9/9] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 2.1s
------
> [builder 9/9] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go:
#15 2.081 controllers/nginxoperator_controller.go:37:2: no required module provides package github.com/sample/nginx-operator/assets; to add it:
#15 2.081 go get github.com/sample/nginx-operator/assets
------
executor failed running [/bin/sh -c CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go]: exit code: 1
make: *** [docker-build] Error 1
这个错误实际上源自 Dockerfile 最后一步的 go build
命令。在本教程的上下文中,Go 构建失败是因为它无法找到 assets
包(该包是在 第四章 使用 Operator SDK 开发操作符 中创建的,用于组织和访问操作对象的部署清单)。
要解决此问题,确保已修改 Dockerfile,包含 COPY assets/ assets/
指令(请参见本章 构建容器镜像 部分中的示例)。另外,你也可以重构操作符代码,将 assets/
目录嵌入现有的 controllers/
目录中,而无需修改 Dockerfile。原因是 controllers/
和 api/
目录已经被复制到 builder
镜像中(但将嵌入式清单存储在 API 目录中在语义上并不合理,因为它们不是 API)。
docker push 因访问被拒绝而失败
make docker-push
命令可能会因以下错误而失败:
$ make docker-push
docker push controller:latest
The push refers to repository [docker.io/library/controller]
18452d09c8a6: Preparing
5b1fa8e3e100: Preparing
denied: requested access to the resource is denied
make: *** [docker-push] Error 1
这个具体的错误(包括 docker push controller:latest
行)意味着该命令的 IMG
变量配置错误。回想一下,在本章 构建容器镜像 部分,曾讨论过该变量作为为构建镜像打标签的方式。有几种设置该值的方式,可以作为环境变量或通过修改 Makefile
。
无论你选择如何更新此变量,都必须检查该值是否已传播到 Makefile
中的 docker-push
目标。否则,Docker 会尝试将其推送到用于库镜像的通用注册表。你无法访问该注册表,因此,Docker 会返回此处显示的 access denied
错误。
如果错误信息包含了正确的公共注册表和你的 IMG
变量值(例如,第二行是 docker push docker.io/yourregistry/yourimagename
),那么很可能是简单的身份验证错误。运行 docker login
确保你已登录到 Docker Hub 账户。
Operator 部署成功,但因 ImagePull 错误无法启动
如果你运行 make deploy
,命令通常会成功完成(除非你对生成的清单文件进行了重大修改)。然而,在集群中查看 Operator(例如,使用 kubectl get all -n nginx-operator-system
)时,可能会看到 Operator 的 Pod 启动失败,并显示以下信息:
$ kubectl get all -n nginx-operator-system
NAME READY STATUS RESTARTS AGE
pod/nginx-operator… 1/2 ImagePullBackOff 0 34s
这可能与之前描述的错误类似。在 Kubernetes 中,ImagePullBackOff
错误表示由于某些原因,Pod 无法拉取它需要运行的容器镜像。通常,这可能是身份验证错误(例如,注册表可能是私有的)或镜像未找到。确保你已经使用正确的 IMG
环境变量构建并推送了 Operator 镜像,如其他故障排除章节所述。如果仍然看到错误,检查一下是否已通过 Docker Hub Web UI 登录,确保你的镜像注册表没有设置为私有。
Operator 部署成功,但遇到另一个错误导致无法启动
在 Kubernetes 中,Pod 启动失败的原因有很多。可能是 Operator 代码中存在逻辑错误,或者集群中的系统配置出现问题。不幸的是,解决这个问题没有一个适合所有情况的通用解决方案。不过,还是有一些工具可以帮助收集更多的信息。使用 kubectl
来检查 Pod 是诊断错误的最常见方法。例如,kubectl describe pod/<podname>
将打印出事件和状态更新,帮助解释失败的原因。或者,kubectl logs pod/<podname>
会打印出 Pod 的日志输出(这对于诊断运行时错误非常有帮助,通常这些错误需要通过修改代码来解决)。所有 kubectl
命令都可以通过运行 kubectl -h
获取文档。
指标
作为开发具有丰富功能和调试能力的 Operator 的一部分,本章和前几章专门介绍了 Prometheus 指标的实现。Prometheus (prometheus.io/
) 是一个开源监控平台,在 Kubernetes 生态系统中被广泛使用。因此,网上有许多资源可以解决各种问题(许多问题并非专门针对 Operator 框架)。这些社区资源的文档可以在 prometheus.io/community/
找到。
Operator 指标在 Prometheus 中未显示
在集群中部署 Prometheus,并按照本教程中的kube-prometheus
步骤进行操作后,我们在操作符代码中定义的自定义度量应该会在几秒钟后出现在 Prometheus 仪表板中。如果一段时间后,仍然无法看到自定义度量,请确保您已按照安装和配置 kube-prometheus部分的说明,正确配置 Prometheus,以便它从操作符的命名空间中抓取新的度量。
此外,请确保在部署操作符之前,已经取消注释config/default/kustomization.yaml
中的- ../prometheus
行,如带度量重新部署操作符部分所述。此步骤确保ServiceMonitor
对象(它告知 Prometheus 抓取哪个端点的度量)在命名空间中创建。
操作符部署失败,提示没有匹配的ServiceMonitor
类型
在运行make deploy
时,可能会出现以下错误,并显示其他一些行,说明创建了哪些资源:
$ make deploy
...
serviceaccount/nginx-operator-controller-manager created
role.rbac.authorization.k8s.io/nginx-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/nginx-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/nginx-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/nginx-operator-proxy-role created
...
error: unable to recognize "STDIN": no matches for kind "ServiceMonitor" in version "monitoring.coreos.com/v1"
make: *** [deploy] Error 1
在这种情况下,操作符实际上并没有失败部署。然而,由于无法在 Kubernetes API 中找到对象的定义,它确实未能创建ServiceMonitor
对象。这很可能是因为在尝试部署带有度量的操作符之前,没有在集群中安装 Prometheus。具体来说,ServiceMonitor
是由 Prometheus 提供的 CRD。因此,在本教程中,在安装kube-prometheus
之前部署带有度量的操作符将导致报告度量时出现失败。为了解决这个问题,请确保在部署带有度量的操作符之前,已经按照安装 Prometheus 的步骤进行操作。
其他错误
上述问题只是跟随本教程时可能出现的一些技术问题。不幸的是,本章无法涵盖所有场景。然而,操作符框架社区及其资源提供了解决许多不同类型问题的方案。通过这些资源,以及第四章中故障排除部分的资料,您很可能能解决遇到的任何难题。
总结
本章的主要目标是将我们在整本书中构建的操作符代码进行编译,并将其部署到集群中。为此,我们按照适用于本地开发环境的步骤进行操作。这些步骤包括将操作符构建为本地二进制文件,并构建容器镜像以便在临时测试集群中部署(该集群是使用 kind 创建的)。这个轻量级的过程有助于开发和快速测试,但缺乏发布操作符时在生产环境中部署所需的完整工作流优势。
在下一章,我们将探讨操作符框架的最终支柱:OperatorHub 和 Operator Lifecycle Manager。学习如何准备和提交一个操作符到 OperatorHub,将是将任何操作符提供给公众使用的关键部分。与此同时,Operator Lifecycle Manager 是一个更为优雅的解决方案,用于部署操作符(无论是公开在 OperatorHub 上,还是私下部署)。与手动使用 Make 部署相比,这些过程更适合将你的操作符与全世界分享。
第三部分:为公共使用部署和分发操作员
本部分将展示如何打包操作员并发布供他人使用,通过案例研究为操作员框架贡献到开源生态系统。最后,书中的内容将通过常见问题解答的形式总结,并将这些教训应用到应用程序和系统操作员的真实世界示例中。
本部分包含以下章节:
-
第七章,使用操作员生命周期管理器安装和运行操作员
-
第八章,为操作员的持续维护做准备
-
第九章,深入探讨常见问题解答与未来趋势
-
第十章,可选操作员案例研究 – Prometheus 操作员
-
第十一章,核心操作员案例研究 – Etcd 操作员
第七章:第七章:使用操作符生命周期管理器安装和运行操作符
到目前为止,前几章中涉及的操作符开发工作大多是自包含的。也就是说,迄今为止,开发和部署过程主要集中在本地环境中,且期望与我们编写的操作符交互的外部服务相对较少。虽然这些过程对操作符的早期设计有用(在某些方面甚至是必需的),但大多数操作符(实际上,大多数软件项目)最终都会暴露给外部世界。本章将专注于操作符生命周期中的这一阶段,其中操作符将被呈现并由外部用户使用。
在第一章,《操作符框架简介》中,介绍了操作符框架的三大支柱。书中的几章已经讨论了第一大支柱(操作符 SDK),但其余的支柱尚未详细探讨。这些支柱是操作符生命周期管理器(OLM)和OperatorHub。这两个操作符框架的组成部分是操作符从实验性本地原型到发布可安装产品开发过程中的关键过渡元素。在本章中,我们将通过以下几个部分,讲解如何从开发中的操作符过渡到用户可访问的操作符:
-
理解 OLM
-
运行你的操作符
-
与 OperatorHub 合作
-
故障排除
通过将操作符打包,使其能够通过 OLM 安装和管理,并将该操作符发布到 OperatorHub,我们将利用操作符框架中用户期望的标准部署工作流。虽然这些步骤并非必要,因为我们已经展示过,可以在没有 OLM 或 OperatorHub 的情况下手动构建和部署操作符,但本章的目标是介绍操作符框架的这两大支柱,演示如何将操作符转变为一个丰富的社区项目。
技术要求
本章将继续使用在第四章,《使用操作符 SDK 开发操作符》和第五章,《开发操作符 - 高级功能》中编写的 nginx 操作符。假设已经可以访问公共 Docker 仓库(在第六章,《构建与部署操作符》中使用过),以及一个正在运行的Kubernetes集群。因此,本章的技术要求是在前几章的大部分要求基础上建立的,包含以下内容:
-
访问 Kubernetes 集群。建议使用如kind或minikube等工具创建一个可丢弃的集群(参见第六章,构建和部署你的 Operator)。
-
在本地系统上有可用的
kubectl
二进制文件,用于与 Kubernetes 集群进行交互。 -
在本地系统上有可用的
operator-sdk
二进制文件,用于部署 OLM 并构建 Operator 清单。 -
已安装并运行 Docker 以构建 Operator 捆绑映像。
-
拥有 GitHub 账户,并熟悉 GitHub 的 fork 和 pull 请求流程,以便向 OperatorHub 提交新的 Operator(仅限演示)。
本章的《代码实战》视频可以通过以下链接查看:bit.ly/3PPItsB
了解 OLM
OLM 在第一章,Operator 框架简介中被介绍,作为一个用于在集群中安装和管理 Operator 的工具。它的功能包括提供对已安装 Operator 的升级控制,并使这些 Operator 对集群用户可见。它还通过强制执行 Operator 依赖关系,防止来自不同 Operator 的 API 冲突,从而帮助维护集群稳定性。这只是一个简要概述,但这些功能使得 OLM 成为一个强大的工具,适用于生产环境中部署 Operator。你可以在 Operator 框架文档中找到更多关于 OLM 功能的细节:olm.operatorframework.io/docs/#features-provided-by-olm
。
尽管这可能让 OLM 看起来像是一个复杂的组件,但它实际上不过是一组可以像安装其他组件或应用程序(包括 Operator 本身)一样在集群中安装的资源清单。这些资源包括各种Pods(由 Deployments 管理)、CustomResourceDefinitions(CRDs)、namespaces、ServiceAccounts和RoleBindings。
此外,Operator SDK 命令行工具提供了简单的命令,方便在 Kubernetes 集群中安装和与 OLM 进行交互。
因此,在使用 OLM 安装 Operator 之前,我们必须首先安装 OLM 本身。本节将展示安装 OLM 所需的步骤,还将演示一些通过命令行与 OLM 交互的附加命令,这将在稍后安装和管理我们自己的 Operator 时非常有用。
在 Kubernetes 集群中安装 OLM。
要安装 OLM,首先确保你拥有正在运行的 Kubernetes 集群的管理员权限。尽管使用 OLM 来管理 Operators 是生产集群中的一种可接受的做法,但强烈建议在跟随本章时使用一个一次性集群(可以使用如 kind 这样的工具创建)。这样,如果需要,你可以轻松且低成本地销毁并重新构建集群。如果你已经有一个来自上一章的集群运行,可能还需要关闭该集群,重新开始(使用 kind 时,执行kind delete cluster
命令即可)。
接下来,调用operator-sdk
二进制文件,通过以下命令在你的集群中安装 OLM:
$ operator-sdk olm install
INFO[0000] Fetching CRDs for version "latest"
INFO[0000] Fetching resources for resolved version "latest"
...
INFO[0027] Deployment "olm/packageserver" successfully rolled out INFO[0028] Successfully installed OLM version "latest"
该命令可能需要一些时间才能完成,但在此过程中,你将看到operator-sdk
获取 OLM 的各种资源清单并将其安装到 Kubernetes 集群中。安装完成后,它还会打印出最终安装的资源列表。许多资源是集群范围的(例如 OLM 特定的 CRD),或者安装在新创建的olm
命名空间中。你可以通过以下命令使用kubectl
检查该命名空间,以查看这些资源:
$ kubectl get all -n olm
NAME READY STATUS RESTARTS AGE
pod/catalog-operator-5c4997c789-xr986 1/1 Running 0 4m35s
pod/olm-operator-6d46969488-nsrcl 1/1 Running 0 4m35s
pod/operatorhubio-catalog-h97sx 1/1 Running 0 4m27s
pod/packageserver-69649dc65b-qppvg 1/1 Running 0 4m26s
pod/packageserver-69649dc65b-xc2fr 1/1 Running 0 4m26s
NAME TYPE CLUSTER-IP EXTERNAL-IP
service/operatorhubio-catalog ClusterIP 10.96.253.116 <none> service/packageserver-service ClusterIP 10.96.208.29 <none>
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/catalog-operator 1/1 1 1 4m35s
deployment.apps/olm-operator 1/1 1 1 4m35s
deployment.apps/packageserver 2/2 2 2 4m26s
NAME DESIRED CURRENT READY replicaset.apps/catalog-operator-5c4997c789 1 1 1 replicaset.apps/olm-operator-6d46969488 1 1 1 replicaset.apps/packageserver-69649dc65b 2 2 2
值得注意的是,在这个命名空间中有五个 Pod,执行 OLM 的核心功能。这些 Pod 共同工作,提供 OLM 的统一功能,包括追踪 Operator 订阅和监视指示 Operator 安装的自定义资源。
与 OLM 交互
除了operator-sdk olm install
(顾名思义,它在集群中安装 OLM),operator-sdk
二进制文件还提供了两个 OLM 特定的命令:olm uninstall
和olm status
。前者会将 OLM 及其所有依赖的清单从集群中移除,而后者提供当前集群中 OLM 资源的状态信息。对于健康的 OLM 安装,输出应如下所示:
$ operator-sdk olm status
INFO[0000] Fetching CRDs for version "v0.20.0"
INFO[0000] Fetching resources for resolved version "v0.20.0"
INFO[0002] Successfully got OLM status for version "v0.20.0"
NAME NAMESPACE KIND STATUS
operatorgroups.operators.coreos.com CustomResourceDefinition Installed
operatorconditions.operators.coreos.com CustomResourceDefinition Installed
olmconfigs.operators.coreos.com CustomResourceDefinition Installed
installplans.operators.coreos.com CustomResourceDefinition Installed
clusterserviceversions.operators.coreos.com CustomResourceDefinition Installed
olm-operator-binding-olm ClusterRoleBinding Installed
operatorhubio-catalog olm CatalogSource Installed
olm-operators olm OperatorGroup Installed
aggregate-olm-view ClusterRole Installed
catalog-operator olm Deployment Installed
cluster OLMConfig Installed
operators.operators.coreos.com CustomResourceDefinition Installed
olm-operator olm Deployment Installed
subscriptions.operators.coreos.com CustomResourceDefinition Installed
aggregate-olm-edit ClusterRole Installed
olm Namespace Installed
global-operators operators OperatorGroup Installed
operators Namespace Installed
packageserver olm ClusterServiceVersion Installed
olm-operator-serviceaccount olm ServiceAccount Installed
catalogsources.operators.coreos.com CustomResourceDefinition Installed
system:controller:operator-lifecycle-manager ClusterRole Installed
然而,如果 OLM 表现异常或集群中的 Operator 出现问题,可以使用此命令进行故障排除。例如,你可以运行kubectl delete crd/operatorgroups.operators.coreos.com
(删除 OLM 安装的OperatorGroups
CRD)。之后,运行operator-sdk olm status
将显示错误no matches for kind "OperatorGroup" in version "operators.coreos.com/v1
,并且在global-operators
和olm-operators
条目旁边,会指示该 CRD 在集群中缺失。
这个错误可以通过使用operator-sdk olm uninstall
卸载 OLM 并重新安装来修复。请注意,卸载 OLM 不会卸载它在集群中管理的任何 Operators。这是有意为之,目的是防止数据丢失,但也意味着任何想要从集群中移除 Operators 的操作,不能仅通过卸载 OLM 来完成。
除了安装和检查 OLM 本身的健康状况外,另一种与 OLM 交互的方式是安装和管理操作符。但首先,操作符必须以 OLM 能够理解的方式进行准备。这就叫做操作符的包,我们将在下一节展示如何生成它。
运行您的操作符
在第六章中,构建和部署您的操作符,我们展示了通过本地编译或构建 Docker 镜像以便在 Kubernetes 集群中运行来手动构建和运行操作符的方法。但是,这些方法都与 OLM 不完全兼容,因此为了提供一个 OLM 可以安装的操作符,操作符必须准备一个包含 OLM 理解的格式的操作符元数据的包。然后,您可以将此包传递给 OLM,OLM 将处理其余的安装和生命周期管理。
生成操作符的包
一个操作符的包由多个清单文件组成,这些文件描述了操作符并提供附加的元数据,例如其依赖关系和 API。一旦创建,这些清单可以编译成一个包镜像,这是一个可部署的容器镜像,OLM 会用它在集群中安装操作符。
生成包清单的最简单方法是运行make bundle
。此命令会要求您提供一些有关操作符的元数据,并将这些输入编译成输出资源清单。
注意
make bundle
会基于在第六章中使用的相同IMG
环境变量,生成某些字段中的容器镜像名称,构建和部署您的操作符。确保在生成包时此环境变量仍然被设置,或者在调用make bundle
命令时,它已被传递给该命令。
以下代码块显示了make bundle
的输出。在此例中,我们将为我们的 nginx 操作符填写提示信息,使用公司名称MyCompany
,以及一些附加的关键字和操作符维护者的联系信息:
$ make bundle
/Users/mdame/nginx-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
operator-sdk generate kustomize manifests -q
Display name for the operator (required):
> Nginx Operator
Description for the operator (required):
> Operator for managing a basic Nginx deployment
Provider's name for the operator (required):
> MyCompany
Any relevant URL for the provider name (optional):
> http://mycompany.example
Comma-separated list of keywords for your operator (required):
> nginx,tutorial
Comma-separated list of maintainers and their emails (e.g. 'name1:email1, name2:email2') (required):
> Mike Dame:mike@mycompany.example
cd config/manager && /Users/mdame/nginx-operator/bin/kustomize edit set image controller=controller:latest
/Users/mdame/nginx-operator/bin/kustomize build config/manifests | operator-sdk generate bundle -q --overwrite --version 0.0.1
INFO[0000] Creating bundle.Dockerfile
INFO[0000] Creating bundle/metadata/annotations.yaml
INFO[0000] Bundle metadata generated suceessfully
operator-sdk bundle validate ./bundle
INFO[0000] All validation tests have completed successfully
在此步骤中,生成器将依次请求以下输入:
-
操作符的显示名称
: 这是在诸如 OperatorHub 之类的资源上显示操作符时使用的名称。因此,它应该易读且清晰,并且大小写正确。例如,我们选择了Nginx Operator
。 -
操作符描述
: 该字段提供操作符的描述及其功能。与显示名称类似,此项是供用户查看的。因此,它应该清晰且详尽地描述操作符的功能。 -
操作符的提供者名称
: 这是操作符的提供者或开发者的名称。对于单个开发者,它可以简单地是您的名字。或者,对于更大的组织,它可以是公司或部门名称。 -
提供者名称的相关 URL
:这是开发者提供外部 URL 的机会,用于获取有关开发者的更多信息。这可以是个人博客、GitHub 账户或公司网站。 -
为您的操作员提供的以逗号分隔的关键词列表
:这是一个关键词列表,可以帮助用户对您的操作员进行分类并进行搜索。对于此示例,我们选择了nginx,tutorial
,但您也可以提供不同的列表,例如deployment,nginx,high availability,metrics
。这将进一步展现我们为该操作员开发的关键功能。请注意,列表是以逗号分隔的,因此high availability
是一个关键词。 -
以逗号分隔的维护者及其电子邮件列表
:最后,您可以在此部分提供操作员维护者的联系信息,这样用户就可以找到支持或报告错误的联系人。然而,出于开发者的隐私考虑,提供公司地址而非个人联系信息可能会更为合适。
这些字段对应于操作员的 集群服务版本 (CSV) 文件中的匹配字段(CSV 在第一章,《操作员框架简介》中简要描述,稍后在本章中会在《与 OperatorHub 协作》部分详细解释)。有关这些字段如何在操作员框架中使用的更多信息,您可以参考 sdk.operatorframework.io/docs/olm-integration/generation/#csv-fields
中的操作员框架文档。
CSV 是在运行 make bundle
后,在项目中创建的多个新文件之一。这些新文件大多数位于名为 bundle/
的新目录下。在项目根目录下,还有一个名为 bundle.Dockerfile
的新文件,用于将清单编译成 bundle 镜像。
浏览 bundle 文件
由 make bundle
生成的文件包含有关操作员的元数据,可供 OLM 用于安装和管理操作员,且 OperatorHub 会提供有关操作员及其依赖关系和功能的信息。在 bundle/
目录下,有三个子目录,包含以下文件:
-
tests/
:这些是用于运行 scorecard 测试的配置文件,scorecard 测试是一系列用于验证操作员包的测试(请参见sdk.operatorframework.io/docs/testing-operators/scorecard
)。 -
metadata/
:该目录包含一个annotations.yaml
文件,向 OLM 提供关于操作员版本和依赖关系的信息。此文件中的注解必须与bundle.Dockerfile
中指定的标签相同(稍后将详细介绍该文件),并且通常不应修改。 -
manifests/
:此目录包含您的操作员所需的各种清单文件,包括操作员的 CRD 和与度量相关的资源(如果适用)。然而,最值得注意的是 CSV,它包含操作员的大部分元数据。
操作员的 CSV 是这些文件中最有趣的部分,因为它包含了 OLM 用于处理操作员创建的许多信息,以及 OperatorHub,用于向用户展示有关操作员的重要信息。为我们的 nginx 操作员创建的 CSV 文件名为nginx-operator.clusterserviceversion.yaml
,并包含以下部分:
-
元数据,包括一个样本自定义资源对象(由用户创建以配置操作员)及其能力级别:
apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: annotations: alm-examples: |- [ { "apiVersion": "operator.example.com/v1alpha1", "kind": "NginxOperator", "metadata": { "name": "nginxoperator-sample" }, "spec": null } ] capabilities: Basic Install operators.operatorframework.io/builder: operator-sdk-v1.17.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 name: nginx-operator.v0.0.1 namespace: placeholder
-
一个规格字段,包含操作员的描述、显示名称、显示图标(如果提供)和相关的 CRD:
spec: apiservicedefinitions: {} customresourcedefinitions: owned: - description: NginxOperator is the Schema for the nginxoperators API displayName: Nginx Operator kind: NginxOperator name: nginxoperators.operator.example.com version: v1alpha1 description: Operator for managing a basic Nginx deployment displayName: Nginx Operator icon: - base64data: "" mediatype: ""
-
安装说明,包括操作员 Pod 的集群权限和部署规范(这里为了简洁省略)。
-
操作员的安装模式,显示它支持哪些命名空间安装策略:
installModes: - supported: false type: OwnNamespace - supported: false type: SingleNamespace - supported: false type: MultiNamespace - supported: true type: AllNamespaces
-
关键词、维护者信息、提供者 URL 以及版本(在运行
make bundle
时提供):keywords: - nginx - tutorial links: - name: Nginx Operator url: https://nginx-operator.domain maintainers: - email: mike@mycompany.example name: Mike Dame maturity: alpha provider: name: MyCompany url: http://mycompany.example version: 0.0.1
综合这些信息,可以将它们打包在一起,为 OLM 提供足够的数据,以便在集群中部署和管理操作员。该包被称为捆绑镜像。
构建捆绑镜像
一旦捆绑清单文件生成完毕,可以通过调用make bundle-build
来构建捆绑镜像。此命令基于之前通过make bundle
生成的bundle.Dockerfile
文件构建一个 Docker 容器。该Dockerfile
文件包含以下指令:
$ cat bundle.Dockerfile
FROM scratch
# Core bundle labels.
LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1
LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/
LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/
LABEL operators.operatorframework.io.bundle.package.v1=nginx-operator
LABEL operators.operatorframework.io.bundle.channels.v1=alpha
LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.17.0
LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1
LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v3
# Labels for testing.
LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1
LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/
# Copy files to locations specified by labels.
COPY bundle/manifests /manifests/
COPY bundle/metadata /metadata/
COPY bundle/tests/scorecard /tests/scorecard/
类似于在第六章中用于编译操作员镜像的主Dockerfile
文件,构建和部署您的操作员,此 Dockerfile 构建过程中的关键步骤之一是将bundle/
目录中的必要捆绑文件复制到其自己的镜像中(如前面的代码块所示)。它还会为生成的镜像打上关于操作员、其版本和构建工具的各种元数据标签。
运行make bundle-build
将生成以下构建日志:
$ make bundle-build
docker build -f bundle.Dockerfile -t example.com/nginx-operator-bundle:v0.0.1 .
[+] Building 0.4s (7/7) FINISHED
=> [internal] load build definition from bundle.Dockerfile
=> => transferring dockerfile: 966B
=> [internal] load .dockerignore
=> => transferring context: 35B
=> [internal] load build context
=> => transferring context: 16.73kB
=> [1/3] COPY bundle/manifests /manifests/
=> [2/3] COPY bundle/metadata /metadata/
=> [3/3] COPY bundle/tests/scorecard /tests/scorecard/
=> exporting to image
=> => exporting layers
=> => writing image
sha256:6b4bf32edd5d15461d112aa746a9fd4154fefdb1f9cfc49b 56be52548ac66921
=> => naming to example.com/nginx-operator-bundle:v0.0.1
但是,请注意,新容器镜像的名称为example.com/nginx-operator-bundle
,您可以通过运行docker images
来确认:
$ docker images
REPOSITORY TAG IMAGE ID CREATED example.com/nginx-operator-bundle v0.0.1 6b4bf32edd 29 seconds ago
使用这个通用名称是因为make bundle-build
依赖于与之前手动构建操作员镜像时使用的IMG
变量不同的环境变量(并生成捆绑清单文件)。要设置自定义的捆绑镜像名称,可以给生成的镜像打标签,或重新运行make bundle-build
并设置BUNDLE_IMG
变量。示例如下:
$ BUNDLE_IMG=docker.io/myregistry/nginx-bundle:v0.0.1 make bundle-build
这将生成名为docker.io/myregistry/nginx-bundle:v0.0.1
的捆绑镜像。
推送捆绑镜像
回想一下,在第六章中,构建和部署您的操作员,不仅需要构建操作员的容器镜像,还需要将其推送到一个公开可访问的注册表。这使得镜像可以在我们的 Kubernetes 集群中使用。同样,包镜像也必须可以被集群(以及 OLM)访问。因此,我们必须将包镜像推送到一个注册表,以便 OLM 能够将其拉取到集群中。
操作员 SDK 通过make bundle-push
命令使这一步变得容易:
$ make bundle-push
/Library/Developer/CommandLineTools/usr/bin/make docker-push IMG=docker.io/mdame/nginx-bundle:v0.0.1
docker push docker.io/mdame/nginx-bundle:v0.0.1
The push refers to repository [docker.io/mdame/nginx-bundle]
79c3f933fff3: Pushed
93e60c892495: Pushed
dd3276fbf1b2: Pushed
v0.0.1: digest: sha256:f6938300b1b8b5a2ce127273e2e48443 ad3ef2e558cbcf260d9b03dd00d2f230 size: 939
该命令只是调用docker push
,但它继承了之前命令中设置和使用的环境变量(例如,BUNDLE_IMG
)。这种便捷性有助于减少出错的机会,避免将错误的镜像名称推送到错误的注册表。
使用 OLM 部署操作员包
通过构建并推送到一个可访问的注册表的包镜像,可以简单地使用operator-sdk run bundle
命令从其包中部署操作员。例如,我们现在可以通过运行以下命令部署上一节中的 nginx 操作员包:
$ operator-sdk run bundle docker.io/mdame/nginx-bundle:v0.0.1
INFO[0013] Successfully created registry pod: docker-io-mdame-nginx-bundle-v0-0-1
INFO[0013] Created CatalogSource: nginx-operator-catalog
INFO[0013] OperatorGroup "operator-sdk-og" created
INFO[0013] Created Subscription: nginx-operator-v0-0-1-sub
INFO[0016] Approved InstallPlan install-44bh9 for the Subscription: nginx-operator-v0-0-1-sub
INFO[0016] Waiting for ClusterServiceVersion "default/nginx-operator.v0.0.1" to reach 'Succeeded' phase
INFO[0016] Waiting for ClusterServiceVersion "default/nginx-operator.v0.0.1" to appear
INFO[0023] Found ClusterServiceVersion "default/nginx-operator.v0.0.1" phase: Pending
INFO[0026] Found ClusterServiceVersion "default/nginx-operator.v0.0.1" phase: Installing
INFO[0046] Found ClusterServiceVersion "default/nginx-operator.v0.0.1" phase: Succeeded
INFO[0046] OLM has successfully installed "nginx-operator.v0.0.1"
注意
这个命令可能需要几分钟才能成功。然而,如果操作员的ClusterServiceVersion
对象安装失败,请仔细检查是否按照第六章中详细说明的步骤在集群中安装了kube-prometheus。如果操作员包已经构建并包含了对 Prometheus 资源的引用,但这些资源在集群中不存在,就可能导致操作员的安装失败。
该命令创建了 OLM 安装 nginx 操作员所需的资源,这些资源仅包括操作员包中的信息,如CatalogSource
、OperatorGroup
、Subscription
和InstallPlan
。
然后,操作员可以通过operator-sdk cleanup <packageName>
命令卸载,其中<packageName>
在操作员的PROJECT
文件中定义为projectName
:
$ operator-sdk cleanup nginx-operator
INFO[0000] subscription "nginx-operator-v0-0-1-sub" deleted
INFO[0000] customresourcedefinition "nginxoperators.operator.example.com" deleted
INFO[0000] clusterserviceversion "nginx-operator.v0.0.1" deleted
INFO[0000] catalogsource "nginx-operator-catalog" deleted
INFO[0000] operatorgroup "operator-sdk-og" deleted
INFO[0000] Operator "nginx-operator" uninstalled
这标志着手动使用 OLM 构建和部署操作员的正常开发工作流程的结束。然而,仍然有另一个来源可以用来在集群中安装操作员。这个来源就是OperatorHub,它是下一节的重点。
使用 OperatorHub
任何成功的开源项目都需要一个专门的用户和开发者社区来帮助项目的生态系统繁荣。操作员框架也不例外,在这个社区的核心是operatorhub.io/
的操作员目录。事实上,这种集中化正是 OperatorHub 的目标,正如在operatorhub.io/about
中所述:
尽管有多种方法可以实现与 Kubernetes 相同级别的集成,但一直缺少一个中央位置,供用户查找由社区构建的各种优秀操作员。OperatorHub.io 旨在成为这个中央位置。
OperatorHub 由Red Hat、AWS、Google Cloud和Microsoft于 2019 年共同推出,一直在推动 Kubernetes 操作员的增长和采纳。截至写作时,OperatorHub 索引包含了超过 200 个操作员(这一数字仍在增长)。该平台仅由一个公开的 GitHub 仓库和许多志愿者维护者支持,OperatorHub 开放式的目录管理和操作员接受机制也支持了 Kubernetes 的核心理念,允许任何组织的任何人将自己的操作员贡献到目录中,并让所有人都能访问。
简而言之,OperatorHub 使得推广自己的操作员变得简单,同时也可以方便地找到并安装由其他提供者开发的操作员。在本节中,我们将演示如何通过操作 OperatorHub 网站和后端来实现这两个目标。
从 OperatorHub 安装操作员
在自己的 Kubernetes 集群中从 OperatorHub 安装操作员非常简单,可以使用operatorhub.io/
上的目录。你可以通过浏览所有可用操作员的列表来开始,或者通过 OperatorHub 主页上的搜索框进行搜索。你还可以通过类别来缩小搜索范围(可用类别包括AI/机器学习、大数据、云提供商和监控)。
作为一个任意的示例,Grafana Operator 可以在Monitoring(监控)类别下找到。Grafana 是一个分析和监控可视化平台,提供丰富的、有洞察力的工具来查看应用程序的健康状况。以下是Grafana Operator及其他在 OperatorHub 的Monitoring类别中可用的操作员截图:
图 7.1 – OperatorHub 监控类别截图
点击Grafana Operator图标将打开该操作员的详细信息页面。该页面包括操作员当前在功能模型中的功能级别、发布过的操作员版本、以及操作员提供者和维护者的信息。以下是Grafana Operator在 OperatorHub 上的信息页面截图:
图 7.2 – Grafana Operator 信息页面
本页面还提供了此操作员的安装说明,可以通过点击该操作员的install
命令来查看。以下是 Grafana Operator 安装说明的截图:
图 7.3 – Grafana Operator 安装说明
在终端中运行此命令会产生以下输出:
$ kubectl create -f https://operatorhub.io/install/grafana-operator.yaml
namespace/my-grafana-operator created
operatorgroup.operators.coreos.com/operatorgroup created
subscription.operators.coreos.com/my-grafana-operator created
接下来,新的命名空间 my-grafana-operator
已经创建,并具备了该 Operator 所需的资源:
$ kubectl get all -n my-grafana-operator
NAME READY STATUS pod/grafana-operator-controller-manager-b95954bdd-sqwzr 2/2 Running
NAME TYPE service/grafana-operator-controller-manager-metrics-service ClusterIP
NAME READY UP-TO-DATE deployment.apps/grafana-operator-controller-manager 1/1 1 1
NAME DESIRED replicaset.apps/grafana-operator-controller-manager-b95954bdd 1 1
此外,此命令还为该 Operator 创建了 OperatorGroup
对象和 Subscription
对象。这些资源类型是由 OLM 安装在集群中的 CRD,由各个 Operator 实现,用来表示它们的安装。这些对象的作用详细信息可以在 OperatorHub 文档中找到,链接为 operatorhub.io/how-to-install-an-operator
,但总的来说,它们定义了用户(即你)安装该 Operator 的意图,并告知 OLM Operator 的元数据在 OperatorHub 上的位置。OLM 使用这些信息来创建新 Operator 所需的 Deployment、Service 及其他资源。
一旦 Operator 被安装,通常由用户创建该 Operator 的配置 CRD 对象。由于有这么多不同的 CRD,这可能会让人感到困惑。然而,许多 CRD(如 OperatorGroup
和 Subscription
)是由 OLM 等工具自动安装和管理的,不需要手动干预。通常,用户只需要关心特定 Operator 配置的 CRD 对象(例如我们为 nginx Operator 创建的那个)。大多数 Operator 的 README
文件和 OperatorHub 描述中会包含示例 CRD 和开始使用每个 Operator 的步骤(这也是一个很好的做法,适用于你自己的 Operator)。
说到你自己的 Operator,贡献到 OperatorHub 目录几乎和从中安装 Operators 一样简单。在接下来的章节中,我们将看看这些 Operators 如何进入 OperatorHub,以及你的 Operator 如何也能做到这一点。
将自己的 Operator 提交到 OperatorHub
虽然公开发布任何 Operator 不是必需的,但许多提供商选择这样做,既是为了社区的利益,也是为了自身用户的利益。如果你开发的 Operator 被用来管理你提供给用户的应用程序,那么该 Operator 的公开可用性可以提高该应用程序的知名度,并增强你在开源社区中的声誉。提供一个免费的 Operator 向你的用户表明,你致力于提供一个稳定的产品,并且用户无需投入过多工程时间。
Operator SDK 项目需要什么?
Operator SDK,像许多 Kubernetes 项目一样,是在Apache 2.0 许可证下发布的。这为项目的商业使用、分发和私人使用(以及其他使用场景)提供了宽松的许可。有关 Operator SDK 许可证的更多信息,请访问github.com/operator-framework/operator-sdk/blob/master/LICENSE
。
因为我们在本书中开发的 nginx Operator 仅作为教程使用(并不适用于公开使用),所以我们无法演示提交到 OperatorHub 的过程。然而,提交 Operator 到 OperatorHub 的一般过程已在operatorhub.io/contribute
中列出。大致而言,这涉及以下步骤:
-
开发一个已准备好发布的 Operator。
-
生成 Operator 的包,包括其 CSV 和相关的 CRD。
-
创建一个针对 GitHub 上 OperatorHub 仓库的拉取请求(PR),并附上你的 Operator 元数据,
github.com/k8s-operatorhub/community-operators
。
如果你一直在按照本书的步骤进行操作,那么你已经熟悉了前两个步骤。然而,第三步是提交到 OperatorHub 最关键的一部分,因为 GitHub 仓库代表了在 OperatorHub 上列出的所有 Operator 的目录。因此,若没有必要的 PR 修改将你的 Operator 信息合并到这个仓库中,它将不会出现在 OperatorHub 上。
哪个是 OperatorHub 仓库?
一些仍然可用的过时文档提到了两个不同的 OperatorHub 仓库位置,community-operators
和 upstream-community-operators
,这些位置最初是现已归档的 OperatorHub 仓库的子目录,地址为 github.com/operator-framework/community-operators
。前者是 Red Hat 最初用于发布 OperatorHub 时的遗留物(具体来说,它指的是一个位置,预留给在 Red Hat 的 OpenShift Kubernetes 发行版中集成版本的 OperatorHub 上列出的 Operators)。这个 OpenShift 特定的 Operator 索引现已与之前提到的社区仓库脱钩。对于有兴趣贡献 OpenShift 目录的开发者,已有相关文档,但本章将专注于社区版 OperatorHub,它是平台无关的。
通过 GitHub 提交 Operator 的步骤如下(这些步骤假定你已经熟悉 GitHub 以及相关的分叉/PR 流程):
-
将 OperatorHub 仓库 (
github.com/k8s-operatorhub/community-operators
) fork 到你自己的 GitHub 账户中。这允许你将仓库的本地副本克隆到你的机器上,并对其进行更改,稍后这些更改将通过 PR 拉取到上游目录中。 -
在
operators/
目录下为你的 Operator 创建一个新文件夹。它必须具有一个独特的名称,与其他所有 Operators 不重复(例如,我们可以创建operators/nginx-operator
)。 -
在该目录中创建一个名为
ci.yaml
的新文件。此文件定义了版本控制语义以及允许更改你 Operator 的审阅者(更多信息请见k8s-operatorhub.github.io/community-operators/operator-ci-yaml/
)。一个简单的ci.yaml
文件如下所示:reviewers: - myGithubUsername - yourTeammateUsername
-
在你的 Operator 文件夹中为每个你希望发布的版本创建一个目录(例如,
operators/nginx-operator/0.0.1
)。 -
将你 Operator 项目中
bundle
目录的内容复制到新版本文件夹中。
同时,将在你 Operator 项目根目录生成的 bundle.Dockerfile
复制到版本文件夹中。
-
将更改提交并推送到你在 GitHub 上的 forked OperatorHub 仓库的新分支。
-
返回上游 OperatorHub 仓库的 PR 页面(
github.com/k8s-operatorhub/community-operators/pulls
),然后点击 New pull request。选择你的 fork 和分支以合并到上游仓库。 -
阅读 PR 模板描述,确保你已遵循所有列出的步骤。这些前置步骤有助于加速你 Operator PR 的审查和批准过程,包括以下内容:
-
审查社区贡献指南
-
在本地集群中测试你的 Operator
-
验证你的 Operator 元数据是否符合 OperatorHub 的标准
-
确保你的 Operator 描述和版本控制方案足以满足用户需求
-
一旦你审查了 PR 模板中的预提交检查,提交你的请求。此时,自动化检查将验证你的 Operator 元数据,以确保它通过提交质量阈值(并在 GitHub 评论中报告任何问题)。如果你需要对提交进行更改以使其通过这些检查,你可以简单地将更改推送到你 fork 的 OperatorHub 仓库分支。
一旦你的 PR 通过预提交检查,它应自动将你的更改合并到上游仓库。此后不久,你的 Operator 将在 operatorhub.io/
上可见,供全球用户安装!
故障排除
尽管本章介绍了一些新概念,包括 OLM 和 OperatorHub,但本书之前故障排除章节中列出的大多数资源仍然适用。
OLM 支持
OLM 及其相关资源通常与 Operator SDK 开发相关。因此,关于这一主题的合理帮助可以在 #operator-sdk-dev
Slack 渠道找到,网址为 slack.k8s.io。OLM 的相关代码也可以在 GitHub 上报告问题,地址是 github.com/operator-framework/operator-lifecycle-manager
。集成 Operator 与 OLM 的文档可以作为主要资源访问,网址为 sdk.operatorframework.io/docs/olm-integration/
。
OperatorHub 支持
OperatorHub 也可以在 GitHub 上访问,地址是本章所示的目录仓库 (github.com/k8s-operatorhub/community-operators
)。对于前端 operatorhub.io/
网站的具体问题,相关代码位于 github.com/k8s-operatorhub/operatorhub.io
。该仓库提供了关于提交到 OperatorHub 所需的所有元数据和打包文件的详细文档(以及提交流程本身),可访问 k8s-operatorhub.github.io/community-operators/
。
OperatorHub 还提供了验证工具和工具,用于在向仓库创建 PR 之前预览你 Operator 的提交。预览工具可通过 operatorhub.io/preview
访问。在该工具中提交生成的 CSV 文件将显示你提交到 OperatorHub 后,Operator 的预览效果:
图 7.4 – OperatorHub 上 Nginx Operator 预览的截图
预览一个 Operator 的展示可以是一个非常有用的手动步骤,帮助测试为该 Operator 准备的所有元数据是否能够按照你期望的方式展示给新用户。由于 CRD 和 CSV 定义可能比较混乱,很容易失去追踪,因此预览能够提供早期的视觉确认,确保一切设置正确。同时,它还验证了元数据的语法有效性。
总结
本章总结了 Operator 的主要开发和发布过程。如果你从一开始就跟随本书开发自己的 Operator,那么恭喜你!你的 Operator 现在已经发布,并通过 OperatorHub 的影响力可供新用户访问。从本书的早期章节开始,我们展示了设计 Operator 的步骤,使用 Go 开发其基本和高级功能,构建并部署到本地进行测试,最终打包并发布以供公开分发。然而,很少有 Operator 项目的生命周期会在此时结束。
大多数操作符最终可能需要演变、改变其提供的功能,并发布新版本。以一致和可预测的方式进行这些操作,有利于你的用户和维护人员,通过建立预期的发布标准。这些标准包括弃用政策和新版本发布的时间表。在下一章中,我们将解释一些 Kubernetes 项目中已有的最佳实践,并引发关于如何为新操作符的持续维护和开发做好准备的前瞻性思考。
第八章:第八章:为你的 Operator 做持续维护的准备
在本书中,我们展示了创建新的 Kubernetes Operator 的步骤。我们涵盖了从构思、设计、编码、部署到最终发布的全过程。但是,极少有软件项目在初始发布后结束生命周期,Operator 也不例外。事实上,对于许多 Operator,大部分工作最终会在发布后很长时间内完成。因此,通过了解用户和 Operator 社区的期望,为将来维护你的 Operator 做准备是很有价值的。
作为一个基于 Kubernetes 的项目,依赖 Kubernetes 及其子项目中已建立的约定对你自己持续的开发工作非常有帮助。虽然你可以自由制定未来发布的指导方针,但由于是为 Kubernetes 构建的,你的 Operator 很可能至少会依赖于某些库或 Kubernetes 核心平台的某些方面。因此,了解 Kubernetes 的现行政策可以帮助你在准备应对上游平台的变化时对齐自己的开发实践,因为这些变化几乎是必然的,你将不得不做出反应。这就是为什么本章将在接下来的部分中重点介绍这些约定。
-
发布你 Operator 的新版本
-
为弃用和向后兼容性做规划
-
遵守 Kubernetes 对变更的标准
-
与 Kubernetes 发布时间表对齐
-
与 Kubernetes 社区合作
Kubernetes 社区制定的程序为你自己的开发实践提供了一个优秀的模板,并为用户提供了一套熟悉的政策。当然,没有任何要求要求 Operator 严格遵循这些指南,但本章的目标是以一种方式解释它们,提供与你的项目相关的先例。
技术要求
对于本章,唯一的技术工作将集中在 发布你 Operator 的新版本 部分,我们将在这一部分基于前面章节中的 nginx Operator 代码进行扩展,新增代码并在 Kubernetes 集群中运行这些代码。因此,本章的要求包括以下内容:
-
operator-sdk
二进制文件 -
Go
1.16+ -
Docker
-
访问正在运行的 Kubernetes 集群
本章的 Code in Action 视频可以在以下链接观看:bit.ly/3aiaokl
发布你 Operator 的新版本
现在你的 Operator 已经发布,真正有趣的部分才刚刚开始。是时候开始考虑下一次发布了!和任何软件项目一样,你的 Operator 会随着时间推移而不断发展,推出新特性并适应上游 Kubernetes 的变化。关于发布软件的文献极其丰富,提供了何时以及如何发布软件更新的建议。大部分信息超出了本书的范围。相反,我们将解释使用 Operator SDK、Operator 生命周期管理器(OLM)和 OperatorHub 发布新版本 Operator 所需的技术步骤。从那时起,你的发布方式和时机完全由你的组织决定(不过你可能希望稍后在本章的 与 Kubernetes 发布时间表对齐 部分中了解其他 Kubernetes 项目的发布方式)。
为你的 Operator 添加 API 版本
虽然有许多因素可能影响你决定发布新版本 Operator(例如修复 Bug 或仅仅是跟随定期发布计划),但 Operator 中常见的一种更改是更新 Operator 的配置 API。回想一下,这是转换成 Operator 的 CustomResourceDefinition(CRD)的 API。因此,在某些情况下,可能需要更新与 Operator 一起发布的 API,以便向用户指示更改(请参见 遵循 Kubernetes 标准进行变更 部分)。
为此,我们需要创建一个新的 API 版本,并将该版本包含在 Operator 的 CRD 中(有关从技术角度深入了解此操作的更多信息,请参见 Kubernetes 关于 CRD 版本控制的文档,了解此操作的详细信息:kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/
)。
在 第四章 中,使用 Operator SDK 开发 Operator,我们通过以下命令初始化了我们 Operator API 的第一个版本:
operator-sdk create api --group operator --version v1alpha1 --kind NginxOperator --resource --controller
这创建了 NginxOperator
API 类型,并将其版本设置为 v1alpha1
。然后,我们在 api/v1alpha1/nginxoperator_types.go
中填写了 API 并生成了相应的 CRD,这为 Operator 部署到集群后提供了使用接口。
如果需要对该 API 进行某些不兼容的更改,并且需要生成新版本,可以以类似的方式生成该版本。例如,假设我们希望允许由 Operator 管理的 nginx 部署暴露多个端口,比如一个用于 HTTP,另一个用于 HTTPS 请求。我们可以通过将现有 nginx Operator 的 CRD 中的port
字段更改为ports
字段,来定义一个v1.ContainerPorts
列表(这是 Kubernetes 原生的 API 类型,允许为容器命名多个端口)。这个新类型暴露了额外的信息,如Name
和HostPort
,但它也包含了与原始port
字段相同的int32
值,用于定义单个ContainerPort
。我们可以从controllers/nginxoperator_controller.go
中取以下一行作为示例:
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if operatorCR.Spec.Port != nil {
deployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = *operatorCR.Spec.Port
}
这可以简化为以下内容:
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if len(operatorCR.Spec.Ports) > 0 {
deployment.Spec.Template.Spec.Containers[0].Ports = operatorCR.Spec.Ports
}
为了展示这对 Operator 类型意味着什么,我们将以现有的NginxOperatorSpec
类型为例,展示v1alpha1
版本:
// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec struct {
// Port is the port number to expose on the Nginx Pod
Port *int32 `json:"port,omitempty"`
// Replicas is the number of deployment replicas to scale
Replicas *int32 `json:"replicas,omitempty"`
// ForceRedploy is any string, modifying this field instructs
// the Operator to redeploy the Operand
ForceRedploy string `json:"forceRedploy,omitempty"`
}
现在,我们将其更改为v1alpha2
中新定义的NginxOperatorSpec
类型,像下面这样:
// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec struct {
// Ports defines the ContainerPorts exposed on the Nginx Pod
Ports []v1.ContainerPort `json:"ports,omitempty""`
// Replicas is the number of deployment replicas to scale
Replicas *int32 `json:"replicas,omitempty"`
// ForceRedploy is any string, modifying this field instructs
// the Operator to redeploy the Operand
ForceRedploy string `json:"forceRedploy,omitempty"`
}
为了保持用户的功能性,重要的是以一种确保 Operator 在退役策略要求的时间内支持两个版本的方式来引入新版本。
生成新的 API 目录
第一步是生成新的 API 文件。新的 API 版本通过operator-sdk
命令生成,像我们生成v1alpha1
时一样:
$ operator-sdk create api --group operator --version v1alpha2 --kind NginxOperator --resource
Create Controller [y/n]
n
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1alpha2/nginxoperator_types.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
/Users/mdame/nginx-operator/bin/controller-genobject:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
请注意,这次省略了--controller
标志(我们选择了n
代表Create Controller [y/n]
),因为该 Operator 的控制器已经存在(controllers/nginxoperator_controller.go
),因此我们不需要生成另一个控制器。
相反,现有的控制器需要手动更新,移除对v1alpha1
的引用并将其替换为v1alpha2
。这个步骤也可以通过诸如sed
之类的工具来自动化,但在自动化代码更新时,请务必仔细检查任何更改。
当版本生成时,它会创建一个新的api/v1alpha2
文件夹,其中还包含一个nginxoperator_types.go
文件。将现有的类型定义从api/v1alpha1/nginxoperator_types.go
复制到此文件,并将port
字段更改为ports
,如前面的代码所示。新文件应如下所示(注意对Ports
的高亮更改):
api/v1alpha2/nginxoperator_types.go:
package v1alpha2
import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
ReasonCRNotAvailable = "OperatorResourceNotAvailable"
ReasonDeploymentNotAvailable = "OperandDeploymentNotAvailable"
ReasonOperandDeploymentFailed = "OperandDeploymentFailed"
ReasonSucceeded = "OperatorSucceeded"
)
// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Ports defines the ContainerPorts exposed on the Nginx Pod
Ports []v1.ContainerPort `json:"ports,omitempty""`
// Replicas is the number of deployment replicas to scale
Replicas *int32 `json:"replicas,omitempty"`
// ForceRedploy is any string, modifying this field instructs
// the Operator to redeploy the Operand
ForceRedploy string `json:"forceRedploy,omitempty"`
}
// NginxOperatorStatus defines the observed state of NginxOperator
type NginxOperatorStatus struct {
// Conditions is the list of the most recent status condition updates
Conditions []metav1.Condition `json:"conditions"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion
// NginxOperator is the Schema for the nginxoperators API
type NginxOperator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec NginxOperatorSpec `json:"spec,omitempty"`
Status NginxOperatorStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// NginxOperatorList contains a list of NginxOperator
type NginxOperatorList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []NginxOperator `json:"items"`
}
func init() {
SchemeBuilder.Register(&NginxOperator{}, &NginxOperatorList{})
}
更新 Operator 的 CRD
接下来,需要更新 Operator 的 CRD,以包括v1alpha1
和v1alpha2
的定义。首先,需要将一个版本定义为etcd
。当 Operator 只有一个版本时,不需要指定这个(那时唯一可用的版本就是该版本)。然而,现在,API 服务器需要知道如何存储该对象。通过在NginxOperator
结构体中添加另一个kubebuilder
标记(//+kubebuilder:storageversion
)来实现这一点:
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion
// NginxOperator is the Schema for the nginxoperators API
type NginxOperator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec NginxOperatorSpec `json:"spec,omitempty"`
Status NginxOperatorStatus `json:"status,omitempty"`
}
这指示 CRD 生成器将v1alpha2
标记为存储版本。现在,运行make manifests
将生成新的 CRD 更改:
$ make manifests
$ git status
On branch master
Changes not staged for commit:
modified: config/crd/bases/operator.example.com_nginxoperators.yaml
现在,Operator 的 CRD 应该在versions
下包括一个新的v1alpha2
规范定义:
config/crd/bases/operator.example.com_nginxoperators.yaml:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
creationTimestamp: null
name: nginxoperators.operator.example.com
spec:
group: operator.example.com
names:
kind: NginxOperator
listKind: NginxOperatorList
plural: nginxoperators
singular: nginxoperator
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
...
served: true
storage: false
subresources:
status: {}
- name: v1alpha2
schema:
openAPIV3Schema:
...
served: true
storage: true
subresources:
status: {}
实现 API 转换
最后,API 服务器需要知道如何在这两个不兼容的版本之间进行转换。具体来说,v1alpha1
端口的int32
值需要转换为v1alpha2
中ports
列表中的ContainerPort
值。为了这个示例,我们将定义以下行为:
-
v1alpha1
到v1alpha2
:int32(port)
变为ports[0].ContainerPort
。 -
v1alpha2
到v1alpha1
:ports[0].ContainerPort
变为int32(port)
。
换句话说,如果我们收到一个端口列表并需要转换为单个端口(v1alpha2
到v1alpha1
),我们将取列表中的第一个值并使用它。反之(v1alpha1
到v1alpha2
),我们将取单个port
值并将其作为新端口列表中的第一个(也是唯一)值。
为了定义这些转换规则,我们将实现来自sigs.k8s.io/controller-runtime/pkg/conversion
的Convertible
和Hub
接口:
sigs.k8s.io/controller-runtime/pkg/conversion/conversion.go:
package conversion
import "k8s.io/apimachinery/pkg/runtime"
// Convertible defines capability of a type to convertible i.e. it can be converted to/from a hub type.
type Convertible interface {
runtime.Object
ConvertTo(dst Hub) error
ConvertFrom(src Hub) error
}
// Hub marks that a given type is the hub type for conversion. This means that
// all conversions will first convert to the hub type, then convert from the hub
// type to the destination type. All types besides the hub type should implement
// Convertible.
type Hub interface {
runtime.Object
Hub()
}
这些将通过operator-sdk
暴露给 API 服务器,operator-sdk
是 kubebuilder 命令的包装器,因此在 Operator 中实现转换 webhook 的步骤与任何其他控制器相同,如 kubebuilder 文档中所示)。该过程通过Hub
类型定义一个版本,Convertible
的 spoke 类型通过该版本进行转换。
首先创建一个新文件api/v1alpha2/nginxoperator_conversion.go
,将v1alpha2
定义为 Hub 版本:
api/v1alpha2/nginxoperator_conversion.go:
package v1alpha2
// Hub defines v1alpha2 as the hub version
func (*NginxOperator) Hub() {}
接下来,创建另一个文件api/v1alpha1/nginxoperator_conversion.go
(注意这是在v1alpha1
目录中)。此文件将实现ConvertTo()
和ConvertFrom()
函数,用于将v1alpha1
与v1alpha2
之间进行转换:
api/v1alpha1/nginxoperator_conversion.go:
package v1alpha1
import (
"github.com/sample/nginx-operator/api/v1alpha2"
v1 "k8s.io/api/core/v1"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/conversion"
)
// ConvertTo converts v1alpha1 to v1alpha2
func (src *NginxOperator) ConvertTo(dst conversion.Hub) error {
return nil
}
// ConvertFrom converts v1alpha2 to v1alpha1
func (dst *NginxOperator) ConvertFrom(src conversion.Hub) error {
return nil
}
然后,填写这些函数以执行实际的转换。对于Replicas
和ForceRedeploy
等字段,转换是 1:1 的(同样重要的是要复制Metadata
和Status.Conditions
)。但是,对于Port
/Ports
,我们需要添加之前定义的逻辑。这样,ConvertTo()
看起来像以下这样:
// ConvertTo converts v1alpha1 to v1alpha2
func (src *NginxOperator) ConvertTo(dst conversion.Hub) error {
objV1alpha2 := dst.(*v1alpha2.NginxOperator)
objV1alpha2.ObjectMeta = src.ObjectMeta
objV1alpha2.Status.Conditions = src.Status.Conditions
if src.Spec.Replicas != nil {
objV1alpha2.Spec.Replicas = src.Spec.Replicas
}
if len(src.Spec.ForceRedploy) > 0 {
objV1alpha2.Spec.ForceRedploy = src.Spec.ForceRedploy
}
if src.Spec.Port != nil {
objV1alpha2.Spec.Ports = make([]v1.ContainerPort, 0, 1)
objV1alpha2.Spec.Ports = append(objV1alpha2.Spec.Ports,
v1.ContainerPort{ContainerPort: *src.Spec.Port})
}
return nil
}
而ConvertFrom()
类似,但方向相反:
// ConvertFrom converts v1alpha2 to v1alpha1
func (dst *NginxOperator) ConvertFrom(src conversion.Hub) error {
objV1alpha2 := src.(*v1alpha2.NginxOperator)
dst.ObjectMeta = objV1alpha2.ObjectMeta
dst.Status.Conditions = objV1alpha2.Status.Conditions
if objV1alpha2.Spec.Replicas != nil {
dst.Spec.Replicas = objV1alpha2.Spec.Replicas
}
if len(objV1alpha2.Spec.ForceRedploy) > 0 {
dst.Spec.ForceRedploy = objV1alpha2.Spec.ForceRedploy
}
if len(objV1alpha2.Spec.Ports) > 0 {
dst.Spec.Port = pointer.Int32(objV1alpha2.Spec.Ports[0].ContainerPort)
}
return nil
}
现在,我们可以通过使用operator-sdk create webhook
生成 webhook 逻辑和端点,使用以下命令:
$ operator-sdk create webhook --conversion --version v1alpha2 --kind NginxOperator --group operator --force
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1alpha2/nginxoperator_webhook.go
Webhook server has been set up for you.
You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.
Update dependencies:
$ go mod tidy
Running make:
$ make generate
/Users/mdame/nginx-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new Webhook and generate the manifests with:
$ make manifests
你可以忽略消息中提到的You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types
,因为我们已经实现了这些(生成器简单假设它会在这些接口实现之前运行)。到目前为止,所有工作已经完成,下一步只是确保 webhook 在 Operator 安装到集群时被正确部署。
更新项目的 manifests 以部署 webhook
就像启用度量资源的 manifests 需要取消注释以便与 Operator 一起部署一样(第五章,开发 Operator – 高级功能),与 webhook 相关的资源也需要如此处理。
为此,首先修改config/crd/kustomization.yaml
,取消注释patches/webhook_in_nginxoperators.yaml
和patches/cainject_in_nginxoperators.yaml
行,以便将这两个补丁文件包含到部署中:
config/crd/kustomization.yaml:
resources:
- bases/operator.example.com_nginxoperators.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
- patches/webhook_in_nginxoperators.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD
- patches/cainjection_in_nginxoperators.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml
现在,修改其中一个文件patches/webhook_in_nginxoperators.yaml
,将两个 CRD 版本作为conversionReviewVersions
添加到 Operator 的 CRD 中:
config/crd/patches/webhook_in_nginxoperators.yaml:
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: nginxoperators.operator.example.com
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1
- v1alpha1
- v1alpha2
接下来,对config/default/kustomization.yaml
进行以下更改,以取消注释以下行:
-
- ../webhook
-
- ../certmanager
-
- manager_webhook_patch.yaml
-
所有在
vars
部分中带有[CERTMANAGER]
标签的变量。
最终文件将如下所示(取消注释的行高亮显示,部分内容为简洁起见被省略):
config/default/kustomization.yaml:
...
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
- ../prometheus
...
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
fieldref:
fieldpath: metadata.namespace
- name: CERTIFICATE_NAME
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
- name: SERVICE_NAMESPACE # namespace of the service
objref:
kind: Service
version: v1
name: webhook-service
fieldref:
fieldpath: metadata.namespace
- name: SERVICE_NAME
objref:
kind: Service
version: v1
name: webhook-service
最后,在config/webhook/kustomization.yaml
中注释掉manifests.yaml
行(该文件在我们的使用案例中不存在,尝试在没有取消注释这一行的情况下进行部署将导致错误)。下面的代码片段显示了应该注释掉的行:
config/webhook/kustomization.yaml:
resources:
#- manifests.yaml
- service.yaml
通过这些更改,Operator 可以使用前面章节中的operator-sdk
和make
命令重新构建并部署。
部署和测试新 API 版本
为了确认 API 服务器现在能够理解并在不同版本的 Operator CRD 之间进行转换,将其安装到集群中。请注意,现在你的 Operator 依赖于cert-manager存在于集群中,所以一定要先安装它(安装指南可在cert-manager.io/docs/installation/
找到)。
记住,你需要更新controllers/nginxoperator_controller.go
,将v1alpha1
的引用替换为v1alpha2
,并将Ports
检查(在Reconcile()
中)更改为以下内容:
controllers/nginxoperator_controller.go:
func (*NginxOperatorReconciler) Reconcile(…) {
if len(operatorCR.Spec.Ports) > 0 {
deployment.Spec.Template.Spec.Containers[0].Ports = operatorCR.Spec.Ports
}
}
忘记这样做将导致在创建或检索 Operator CRD 时发生错误(编译时不会显示该错误)。这是因为 v1alpha1
API 类型仍然被定义且有效(所以 Operator 代码能够正常编译),但新的客户端和协调代码会期望以 v1alpha2
格式检索该对象。
要部署 nginx Operator,请在运行 make deploy
之前构建并推送新的容器镜像:
$ export IMG=docker.io/mdame/nginx-operator:v0.0.2
$ make docker-build docker-push
$ make deploy
接下来,创建一个简单的 NginxOperator
对象。为了演示 API 转换,将其创建为 v1alpha1
版本,并使用旧的 port
字段:
sample-cr.yaml:
apiVersion: operator.example.com/v1alpha1
kind: NginxOperator
metadata:
name: cluster
namespace: nginx-operator-system
spec:
replicas: 1
port: 8080
接下来,使用 kubectl 创建自定义资源对象:
$ kubectl apply -f sample-cr.yaml
现在,使用 kubectl get
查看该对象时,将显示为 v1alpha2
,因为它已经被自动转换并存储为此版本:
$ kubectl get -o yaml nginxoperators/cluster -n nginx-operator-system
apiVersion: operator.example.com/v1alpha2
kind: NginxOperator
metadata:
...
name: cluster
namespace: nginx-operator-system
resourceVersion: "9032"
uid: c22f6e2f-58a5-4b27-be6e-90fd231833e2
spec:
ports:
- containerPort: 8080
protocol: TCP
replicas: 1
...
您可以选择通过以下命令以 v1alpha1
的形式查看该对象,这将指示 API 服务器调用 Operator 的 webhook,并使用我们编写的函数将其转换回来:
$ kubectl get -o yaml nginxoperators.v1alpha1.operator.example.com/cluster -n nginx-operator-system
apiVersion: operator.example.com/v1alpha1
kind: NginxOperator
metadata:
name: cluster
namespace: nginx-operator-system
resourceVersion: "9032"
uid: c22f6e2f-58a5-4b27-be6e-90fd231833e2
spec:
port: 8080
replicas: 1
对用户来说,这意味着他们可以继续使用现有的 API,从而在您引入新版本的同时提供宝贵的过渡时间。请注意,如果他们已经在使用 Operator,且您引入了新的存储版本,他们可能需要使用 kube-storage-version-migrator(github.com/kubernetes-sigs/kube-storage-version-migrator
)将现有的存储版本迁移到新版本。您可以为他们提供迁移文件(甚至将其自动化到 Operator 中,因为迁移本质上是 Kubernetes 资源)以简化这个过程。
引入了新的 API 版本并完成了转换后,您的 Operator 现在可以打包成一个新的 bundle,以便通过 OLM 来管理部署。这意味着需要将 Operator 的 CSV 更新为新版本。
更新 Operator 的 CSV 版本
更新 Operator 在其 CSV 中的版本为 OLM、OperatorHub 和用户提供有关他们正在运行的 Operator 版本的信息。它还指示 OLM 哪些版本会替代其他版本用于集群内升级。这使得开发者能够定义特定的升级 alpha
和 beta
版本,这类似于其他软件项目中的版本渠道,允许用户订阅不同的发布节奏。Operator SDK 文档在 GitHub 上详细讲解了这个过程,但为了完成这一部分内容,并不需要理解这些细节(github.com/operator-framework/operator-lifecycle-manager/blob/b43ecc4/doc/design/how-to-update-operators.md
)。不过,在这一部分中,我们将介绍更新 CSV 版本这一简单任务,且只涉及单一渠道的更新。
提升 Operator 的 CSV 版本的第一步是更新将被当前版本替换的版本。换句话说,v0.0.2
将替换 v0.0.1
,因此 v0.0.2
的 CSV 必须指示它正在替换 v0.0.1
。
通过修改 config/manifests/bases
中的基础 CSV,向其 spec 中添加 replaces
字段来完成此操作,示例如下:
config/manifests/bases/nginx-operator.clusterserviceversion.yaml:
apiVersion: operators.coreos.com/v1alpha1
kind: ClusterServiceVersion
metadata:
annotations:
alm-examples: '[]'
capabilities: Basic Install
name: nginx-operator.v0.0.0
namespace: placeholder
spec:
...
replaces: nginx-operator.v0.0.1
接下来,更新项目中的 Makefile 文件中的 VERSION
变量(你也可以像我们之前使用的其他环境变量那样,在 shell 中将这个变量导出为新版本,但手动更新它可以清楚地指示版本,并确保在任何机器上构建时都会传播正确的版本):
Makefile:
# VERSION defines the project version for the bundle.
# Update this value when you upgrade the version of your project.
# To re-generate a bundle for another specific version without changing the standard setup, you can:
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= 0.0.2
现在,可以像在第七章,使用 Operator 生命周期管理器安装和运行 Operators中所述的常规 make bundle 命令一样,构建新的 CSV:
$ make bundle IMG=docker.io/sample/nginx-operator:v0.0.2
这将更新 bundle/manifests/nginx-operator.clusterserviceversion.yaml
中列出的 Operator 版本(这是打包到 Operator 包中的主要 CSV)。如果你按照前面一节的步骤添加了新的 API 版本,它还会将有关该新版本(以及转换 Webhook)的信息添加到 CSV 和随包一起打包的示例 CRD 中。此外,它还将在包中生成一个新的 Service 清单,用于公开两个 API 版本的转换端点。
然后,可以像以前一样使用 OLM 构建、推送和运行该包镜像:
$ export BUNDLE_IMG=docker.io/sample/nginx-bundle:v0.0.2
$ make bundle-build bundle-push
$ operator-sdk run bundle docker.io/same/nginx-bundle:v0.0.2
在构建并正常运行新的包镜像后,发布新版本的最后一步是将其发布到 OperatorHub,以便用户查找。
在 OperatorHub 上发布新版本
在发布新版本的 Operator 后,如果你选择将 Operator 发布到 OperatorHub,那么发布到 OperatorHub 的包也需要更新。幸运的是,这个过程并不复杂。事实上,它基本上与发布初始版本的过程相同(如在第七章,使用 Operator 生命周期管理器安装和运行 Operators中所述),你需要创建一个包含 Operator 包的文件夹,并将该文件夹作为拉取请求提交到 GitHub 上的社区 Operator 仓库(github.com/k8s-operatorhub/community-operators
)。
要发布新版本的 Operator,只需在社区 Operator 项目中你的 Operator 目录下创建一个新的文件夹,文件夹名称使用版本号。例如,如果第一个版本是nginx-operator/0.0.1
,那么这个版本应该是nginx-operator/0.0.2
。
然后,就像你的第一个版本一样,只需将 Operator bundle
目录的内容(在生成新版本的包之后)复制到新版本文件夹中。提交并推送更改到你自己 GitHub 仓库的分支,并向主仓库提交一个新的拉取请求。
当你的拉取请求通过自动化检查后,它应该被合并,新的 Operator 版本应很快出现在 OperatorHub 上。
图 8.1 – Grafana Operator 在 OperatorHub 上的版本和频道列表截图
现在,你已经完成了发布新版本的操作。通过引入新的 API,确保新 API 在现有版本之间是可转换的,更新你的 Operator 包,并将更新后的包发布到 OperatorHub,你现在应该向用户宣布新的 Operator 版本已上线。在接下来的章节中,我们将讨论如何通过提前规划以最小化需要新版本的 API 破坏性更改,并遵循 Kubernetes 标准来确保未来的版本发布顺利进行。
为废弃和向后兼容做规划
在上一节中,我们讨论了发布新版本 Operator 所需的工作。虽然捆绑和发布新版本的过程在所需的工作量上相对简单,但实现新 API 版本并不是一项微不足道的任务。因此,这样做应该仅在必要时进行,以最小化工程资源的使用和对用户的干扰。
当然,偶尔会不可避免地需要引入不兼容的更改,例如在废弃的情况下。有些废弃甚至可能来自上游,这是你无法直接控制的(请参阅遵守 Kubernetes 标准的变更部分)。然而,通过仔细规划,这种更改的频率通常可以得到控制。在本节中,我们将讨论如何规划废弃并支持向后兼容,而不会对你的工程师或用户造成过大的压力。
重温 Operator 设计
在第二章,《理解 Operator 如何与 Kubernetes 交互》一章中的为你的 Operator 规划变更部分,概述了各种设计方法,确立了为未来演进而规划 Operator 设计的良好实践。这些建议的指南(实际上可以应用于许多软件项目)是从小做起,有效迭代,并优雅地废弃。
现在,在从头构建 Operator(如我们的 nginx Operator)之后,回顾这些指南并检查它们如何具体应用于我们的设计将会很有帮助。
从小开始
nginx Operator 的设计最初非常简单。这个 Operator 旨在提供一个非常基本的功能:管理 nginx Pod 的部署。对于配置选项,它暴露了三个字段来控制 Pod 上容器端口、部署的副本数以及一个额外的字段来触发强制重部署。作为一个最小可行产品(MVP),这非常有助于让我们的 Operator 起步。尽管该设计故意保持简约,以便展示一个合理规模的示例,但它依然体现了一个思维方式,即防止 Operator CRD 在一开始就暴露太多的配置选项。发布过多的配置选项可能会让用户感到困惑,从而导致用户无法完全理解每个选项的作用,这会给产品的稳定性带来潜在风险。同时,这也增加了团队的支持负担。
记住,Operator 的第一次发布实际上很可能只占其生命周期的一小部分。在未来的发布中将有足够的时间添加更多的功能,但随着 CRD 在 API 定义中的不断扩展以及新功能的加入,现有功能的可行性可能会被稀释。当然,这并不是说你永远不应该向 Operator 或其 CRD 中添加新功能。但当这个时机到来时,重要的是要谨慎行事,这也是有效迭代的核心。
有效地迭代
在本章中,我们介绍了对 Operator API 的变更,其中一个字段的类型被转换成了完全不同的嵌套结构体。这实际上移除了旧的字段,对于任何已经依赖该字段的用户来说,这将是一个破坏性的变更(事实上,我们确实技术上移除了旧字段,但稍后会详细解释)。像这样的变更的好处需要与其负面影响权衡,这些负面影响包括对用户的干扰(如果适用的话)以及对自己团队的持续支持。
在我们的案例中,我们选择将原来的 int32
字段(名为 port
)替换为一个 v1.ContainerPort
对象列表(名为 ports
)。这增加了 CRD 的复杂性(因为 v1.ContainerPort
包含其他嵌套字段)。然而,这也使我们的 CRD 转向依赖 Kubernetes 上游的一个原生、稳定的 API 类型。这一点,加上能够配置多个端口和新增字段的功能,为 Operator 的用户和开发者提供了可用性和稳定性的好处(并不是说 int32
很可能不稳定,但这个总体思路依然适用)。
然而,这次变更确实需要移除现有的 port
字段。这是用户最终必须响应的变化,但通过优雅的废弃,可以使过渡变得更加平滑。
优雅地废弃
在向我们的 Operator 的 CRD 添加新的 ports
字段时,经过深思熟虑决定移除现有的单一 port
字段,这完全可以接受;实际上,保留它会显得多余。然而,事实是,依赖旧字段的用户必须过渡到新字段,否则在旧配置迁移到新配置时会面临数据丢失的风险。虽然这种变化对于单个整数值来说可能看起来微不足道,但其影响范围在更复杂的 Operator 中会显得更加明显。
这就是我们为何为 Operator 添加了一个转换 webhook,以自动将旧字段转换为新字段。如果新的 ports
字段不是旧字段的超集,实施这个 webhook 就不会这么简单。选择一个兼容性较强的现有类型使得这个过渡对开发人员的实施和用户的理解变得更加容易。像这样的设计决策大大有助于减少在不断增长的代码库中产生的摩擦。
然而,我们的转换并不完全完美。虽然从 v1alpha1
到 v1alpha2
的过渡顺利进行,但反向转换只能保留一个端口值(即列表中的第一个)。这可能适用于实际的使用场景,因为大多数用户更有可能升级到新版本,而不是降级到废弃的版本,但从支持角度来看,这种丢失数据的转换可能会在未来带来麻烦。对此有一些方法可以解决,这与下一节讨论的 Kubernetes 如何平滑地实现更改相关。
遵循 Kubernetes 对更改的标准
Kubernetes 为所有核心项目定义了一套标准的废弃(以及其他破坏性更改)政策,所有核心项目必须遵守。这些政策可以在kubernetes.io/docs/reference/using-api/deprecation-policy/
找到。对于 Operator 开发而言,并不需要阅读并理解整个政策,我们将在此处重点介绍一些相关部分。它主要定义了废弃 Kubernetes API 部分的标准,其中许多相同(或相似)的准则也适用于其他类型的废弃(例如,面向用户的功能,这些功能不是 API 的直接一部分)。它通过列出明确的规则来废弃更改,其中一些我们将在本节中讨论。
作为第三方组件,您的 Operator 并没有义务遵循 Kubernetes 的废弃政策。但在实际操作中,在您 Operator 所构建的生态系统的约束条件下工作是有益的。这些包括提供一个模板来为用户设定期望,以及一套指导方针来规划您自己的持续开发。而且,即使您选择不遵循这些政策,理解它们如何在上游被执行仍然至关重要,以便为您最终需要继承的废弃和变化做好准备。
本节开始时提供的完整废弃政策详细说明了管理每个相关 Kubernetes 组件的标准。因此,废弃的一些细节与 Operator 开发并不直接相关。然而,某些元素,比如与 API 字段支持和移除相关的内容,确实适用于 Operators(如果您选择遵循它们)。
移除 API
Kubernetes 的废弃政策明确禁止从当前 API 版本中移除 API 元素。事实上,这也是整个政策中的第一条规则。
规则 #1
API 元素只能通过增加 API 组版本的方式来移除。
这意味着禁止从现有 API 中移除任何字段或对象。移除操作只能通过引入一个新版本的 API,并删除该元素来完成。
这与我们的 nginx Operator 相关,在我们将 API 版本从 v1alpha1
升级到 v1alpha2
时,移除了 v1alpha1
中存在的 port
字段。遵循这一规则可确保当前使用某个 API 版本的用户,在更新到新版本时,其工作流不会突然中断。API 版本之间的区分明确指出了某种程度的不兼容性。
反过来说,这条规则允许在不增加现有 API 版本的情况下添加 API 元素。这是因为当前 API 版本中的新元素不会破坏任何现有用例,因为就像消费者只是把这个字段留空一样(与移除现有字段不同,移除字段可能导致数据丢失,因为非空条目会被删除)。这与我们的用例直接相关,因为它提供了无缝转换的能力。
API 转换
Kubernetes 废弃政策的第二条规则如下。
规则 #2
API 对象必须能够在给定的版本之间进行往返转换而不会丢失信息,除非某些 REST 资源在某些版本中不存在。
这意味着在 Kubernetes 的同一版本中(或者在本例中,指的是您的 Operator),任何两个 API 版本必须能够在保持所有数据字段的同时,相互转换。
在我们的 nginx Operator 中,我们没有遵循这一规则(因为定义多个端口的v1alpha2
CRD 对象不能将它们全部转换为单一的port
值)。这没问题,因为作为第三方项目,我们不受上游 Kubernetes 政策的约束。然而,从实际角度来看,支持这种无损转换对我们和我们的用户会非常有用。可以通过向v1alpha2
和v1alpha1
都添加ports
字段来实现。然后,在转换到v1alpha1
时,我们的转换逻辑可以将额外的端口保存在新字段中。现有代码如果只知道v1alpha1
中的单个port
字段,可能无法使用这些额外的端口,但重要的是在转换过程中数据得以保存。或者,我们可以简单地将额外的ports
值作为注释存储在 CRD 对象的元数据中,在转换时从中读取。
API 生命周期
你选择支持 API 版本的时间完全取决于你的组织与用户之间的协议。Kubernetes 对支持时间表的标准根据 API 的稳定性级别有所不同。稳定性有三个级别:alpha、beta 和正式发布(GA)。
在我们的 nginx Operator 中,API 当前处于 alpha 阶段。从技术角度来看,这意味着没有对任何时间段的支持保证。然而,在将 API 升级到更稳定版本的过程中,最佳实践是以该 API 已经处于下一个稳定级别的假设来操作。对于 beta 版本的 API,这一时间线通常是 9 个月或三次发布中的较长者(参见与 Kubernetes 发布时间线对齐部分)。被升级为 GA 的 API 不能被删除,但可以标记为废弃。其目标是,GA 版本的 API 可以被认为在 Kubernetes 的该主版本生命周期内是稳定的。
删除、转换和生命周期是与我们 Operator 开发相关的 Kubernetes 废弃政策的三个关键点。更多细节可以参考本节顶部提供的链接。你也可以根据发布的kubernetes.io/docs/reference/using-api/deprecation-guide/
上的计划时间表,跟踪即将到来的上游 API 废弃,适用于每个版本。
尽管本节提到 Kubernetes 发布作为时间单位,但我们没有明确说明具体的时间长度。在下一节中,我们将详细解释 Kubernetes 发布的分解方式,以及它如何与您的 Operator 开发相关。
与 Kubernetes 发布时间线对齐
每个新版本的 Kubernetes 都是由来自不同公司和时区的许多人的辛勤工作和奉献推动的。因此,发布新版本的过程是组件领域和特别兴趣小组(SIGs)之间协调努力的产物,以确保及时、稳定的发布。虽然偶尔会遇到障碍和延迟,但发布时间表在很大程度上是一个定义良好的协作努力,力求提供可预测的更新,以便 Kubernetes 生态系统的用户和下游产品依赖。
在开发 Operator 时,您或您的组织可能会有类似的组织化发布努力。您还希望为用户提供可靠和及时的更新时间表。尽管您自己的发布时间表可能与 Kubernetes 的时间表不同,但了解上游发布工作的方式仍然具有重要意义。例如,它使您能够计划围绕特定日期进行,例如 beta API 升级为 GA 或者完全新功能的发布,这些都是您可以在自己的产品中利用并传递给用户的。另一方面,它还概述了废弃的 API 在被完全移除之前的剩余时间。作为产品供应商,您可以依靠此时间表指导您的发布计划。
出于这些原因,将您的 Operator 发布时间表与 Kubernetes 的更新保持一致是一项有价值的工作,这也是我们在本节中将更详细地解释该时间表的原因。
Kubernetes 发布概述
Kubernetes 发布周期约为 15 周。截至 Kubernetes 1.22 版本,这已经定义了每年三个版本的目标。当然,三个 15 周的发布并不能覆盖整个 52 周的一年。这是因为发布周期允许多次工作中断,包括假期、年底旅行以及 KubeCon 等事件或会议。此时间表由 SIG Release 团队在社区的参与下决定,您可以在kubernetes.io/blog/2021/07/20/new-kubernetes-release-cadence/
的博客文章中详细了解这一决定的更多细节。
在单个发布周期中,有几个关键日期标志着该发布中的重要进展更新。这些包括功能冻结、代码冻结、文档更新和博客文章的截止日期,以及发布候选版(RC)版本的发布。每个发布的确切时间表都发布在 SIG Release GitHub 仓库的github.com/kubernetes/sig-release
中。例如,Kubernetes 1.24 发布周期如下所示,并突出显示了一些关键日期。
图 8.2 – Kubernetes 1.24 发布周期日历
每个日期在单个版本的推进中都起着重要作用。在接下来的章节中,我们将详细解释每个日期的含义,以及它们如何与你的 Operator 发布周期相关联。很多这些信息也记录在 SIG Release 仓库中,地址为 github.com/kubernetes/sig-release/blob/master/releases/release_phases.md
。
发布开始
这个日期是相当直观的,表示下一个版本的 Kubernetes 发布周期的正式开始日期。这为周期中的所有其他步骤提供了参考日期。请注意,这与上一个版本的发布时间不同,因为版本之间存在缓冲期,以及发布后流程,如 回顾。因此,在这个日期之前,当前版本可能已经在进行中的工作(请参见 GA 发布/代码解冻 小节)。然而,就像任何比赛需要一个起跑线一样,每个发布也需要一个日期来标志其正式开始。
增强功能冻结
Kubernetes 中的新功能有多种形式。尽管所有平台的变更都是有价值的,不论大小,但某些工作需要更多的努力,并且工作范围更广。例子包括显著面向用户的变更或涉及多个组件和 SIG 协作的架构设计。这些特性需要额外的管理,以确保它们的成功推出。在这一阶段,特性可能会被视为 增强功能 或 Kubernetes 增强提案 (KEP)。
本书中已经提到过几个单独的 KEP。例如,在 第五章,《开发 Operator – 高级功能》中,我们提到了 Condition
类型,用于组件报告其状态(包括我们的 nginx Operator)。所有此类 KEP 都会在 Kubernetes Enhancements GitHub 仓库中跟踪,地址为 github.com/kubernetes/enhancements
。
KEP 过程和仓库的完整细节超出了本书的范围,但基本知识是,KEP 代表大规模的设计变更,这些变更可能会影响 API 的用户或消费者。作为一个 Operator 的开发者,Operator 本身是一个可能会使用一个或多个上游 API(例如 Condition
字段)的组件,因此了解即将发生的这类大规模变更的状态非常重要,因为它们可能会直接影响你。提供这些信息就是 KEP 过程的目标。
在一个发布周期中,增强功能冻结阶段标志着所有为该发布提出的 KEP 必须在此时被接受并承诺实施其变更,或者推迟到未来的发布版本。这是决定该发布是否推进的关键Go/No-Go日期。尽管许多进行中的 KEP 此时可以继续,但也可能有合理的理由解释某些变更无法在此日期之前承诺到发布版本中。在这些情况下,增强功能的开发者可以在接下来的异常请求征集期间请求异常。
对于 Operator 开发者来说,增强功能冻结截止日期是你在自己的开发周期中需要牢记的一个重要日期,因为虽然 KEP 通常用于向 Kubernetes 引入新特性,但它们的定义之一也是概述其他功能的移除(比如被替代的功能)。了解您依赖的任何功能是否被正式列为移除有助于评估您需要多紧急地对移除做出反应。另一方面,如果一个 KEP 错过了增强功能冻结的截止日期,那么可以放心,任何计划移除的相关功能将在至少另一个发布周期内继续得到支持,除非该计划获得异常批准。
异常请求征集
如果在发布过程中,某个功能还没有准备好提交到增强功能冻结(或代码冻结)中,参与该增强功能开发的贡献者和 SIG 可以请求延长冻结的截止日期。如果获得批准,这将允许这些贡献者在准备好发布的过程中获得合理的延期。
寻求异常的增强功能需要满足一定的标准,以确保它们不会影响平台的稳定性或延迟发布。因此,发布经理根据异常请求的范围、请求的估算延期时间以及请求在发布周期中提交的时间来评估每个异常请求。
异常请求和批准通常通过参与的 SIG 的邮件列表、Slack 频道以及 GitHub 上的具体 KEP 问题讨论页面进行沟通。因此,监控这些沟通渠道对于了解你的 Operator 所实现或依赖的功能至关重要。即使一个新特性错过了增强功能冻结(Enhancements Freeze)日期,也不意味着在官方拒绝异常请求之前,它就不会被实现。对于计划移除的特性也是如此,如果获得了异常批准的话。了解与您的 Operator 功能相关的上游 SIG 在增强功能冻结(或代码冻结)后是否会请求任何异常,是判断您在 Operator 开发周期中需要承诺的内容的一个重要信号。
代码冻结
代码冻结是发布周期中的一个时刻,在此时,所有的代码更改必须完成并合并到 Kubernetes 代码库中(除了那些被授予例外的功能)。这意味着,除非是对平台稳定性至关重要的更改,否则不再接受任何更改。
作为下游 Operator 的开发者,这与你自己的项目时间表相关,因为这是你可以开始将库更新到最新 Kubernetes 版本的时机,并且可以合理预期其稳定性。可以通过将依赖项更新到上游 Kubernetes 库的最新 RC 版本来完成此操作。
新版本的第一个 RC 通常会在代码冻结日期后不久发布。这个时机使得开发者能够更新依赖项,并有额外的缓冲时间来捕捉任何需要更新或修改的内容,以确保与新版本的 Kubernetes 兼容,并在最终发布之前进行调整。利用这个时机是非常有益的,有助于最小化在 Kubernetes 版本发布后发布更新的 Operator 的延迟。由于 Kubernetes 的规模和波动性,建议定期升级任何上游依赖项。否则,可能会导致技术债务累积,并最终与平台的新版本不兼容。
测试冻结
虽然代码冻结定义了一个严格的期限,要求所有增强功能的实现必须完全合并,但测试冻结提供了额外的缓冲时间以扩展测试覆盖范围。这为在发布之前,一旦功能合并后,改进测试用例提供了机会。在此日期之后,允许对任何测试的唯一更改是修复或移除那些持续失败的测试。这个日期可能对你自己的发布周期没有一致的影响,但它是监控关键增强功能进展时需要关注的一个重要信号。
GA 发布/代码解冻
最后,大家期待的时刻到了。如果在发布过程中发现一些问题导致延迟,这是 Kubernetes 发布最新版本的日期。对于开发者而言,这意味着 Kubernetes 中的代码(在github.com/kubernetes/kubernetes
上)将会更新一个新的 Git 标签,这使得你可以轻松地在代码中的某个确定点引用发布版本(例如,在更新 Operator 的依赖项时,更新其go.mod
文件)。此外,支持 Kubernetes 及其子项目的客户端库也会更新,包括以下内容:
-
github.com/kubernetes/api
– Kubernetes 平台使用的核心 API 类型(作为k8s.io/api
导入) -
github.com/kubernetes/apimachinery
– 用于实现代码中 API 类型编码和解码的库(k8s.io/apimachinery
) -
github.com/kubernetes/client-go
– Operators 和其他程序用于通过编程方式与 Kubernetes 资源交互的 Go 客户端 (k8s.io/client-go
)
这些只是一些额外的依赖项,在 GA 发布当天,它们会通过新的 Git 标签进行更新。这些依赖项实际上是核心 Kubernetes 仓库 (k8s.io/kubernetes
) 的一部分,但它们通过自动化机器人同步到符号化的标准位置,以便更好地进行依赖管理。这有时会导致核心 Kubernetes 发布标签和库更新之间出现短暂的延迟(通常开发者更关注的是库的更新)。
Kubernetes 作为依赖项
核心 Kubernetes 仓库 k8s.io/kubernetes
(或 github.com/kubernetes/kubernetes
)包含了核心平台组件所需的所有代码。因此,直接从这里导入代码可能会很有诱惑力。然而,由于其复杂性,不建议直接将 k8s.io/kubernetes
中的代码导入到你的项目中,因为这可能会导致依赖问题,这些问题使用 Go 模块很难解决,并且会将多余的传递依赖项引入你的项目中。相反,最好依赖于组件库(例如之前列出的那些库),这些库旨在被导入到外部项目中。
新版本正式发布后,Kubernetes 项目进入 master
(或 main
)分支,此时意味着你正在与 N+1
版本的代码进行交互(其中 N
是当前的 Kubernetes 版本)。
回顾
当尘埃落定后,SIG Release 团队会花时间回顾此次发布过程,进行 回顾。回顾的目的是开会讨论在发布过程中出现的任何障碍或问题,并提出解决方案以避免未来再次发生。此外,任何突出的成功也会被识别并予以表扬。这个过程是无责备的,最终目标是减少未来发布中的摩擦。虽然回顾的具体细节可能与开发你的 Operator 关系不大(回顾的重点通常更多放在发布基础设施和驱动发布的过程上,而非具体的功能更改),但它可以是了解未来发布周期变化的一种非常有价值的方式。
上述所有日期构成了 Kubernetes 发布周期中最重要的信号。了解这些信息有助于通知你的团队关于当前你所依赖的功能的状态,并且这些信息可以通过你对用户的承诺传递出去。保持对最新 Kubernetes 发布的关注至关重要,这样可以避免技术债务不断积累,因为 Kubernetes 项目在它所提供的支持限制内持续发展。
此外,了解自己在当前发布周期中的位置,也为你在合适的时机贡献 Kubernetes 提供了机会。在下一部分中,我们将讨论你可以如何做到这一点,以及它如何为你带来好处。
与 Kubernetes 社区合作
本章重点讨论了与 Operator 开发相关的 Kubernetes 标准、政策和时间表。虽然这些标准看似固定、具有约束力,但实际上它们是通过一个开放、公平的过程创建的灵活框架。这个过程由来自世界各地的不同公司贡献者组织,始终欢迎新的声音。
作为 Kubernetes 平台的开发者,你在组织 Kubernetes 上游的社区中有着固有的份额。因此,影响到你的改进或关注点,很可能也会影响到其他人。这就是为什么不仅是可以,而且鼓励你在开发自己的 Operator 时,积极参与上游开发的原因。如果没有别的,参与其中也有助于你的自身发展,因为开放的流程让你可以帮助指导上游工作,确保它按你认为合适的方式进行。
参与社区活动非常简单,只需发送一条消息或加入视频通话即可。在其他章节中提到的各种 GitHub 仓库和 Slack 频道是一个提供支持的好地方,你也可以在这些地方提供帮助。Kubernetes 的 Slack 服务器是slack.k8s.io,加入并贡献是免费的。你可能还想关注各种 SIG 会议,了解你感兴趣的主题,所有会议都可以在github.com/kubernetes/community
上的 Kubernetes Community 仓库中找到。这些仓库包括了所有 SIG 和社区会议的链接和时间表。
小结
曾经,在一次采访中,传奇橄榄球四分卫汤姆·布雷迪被问到哪一枚冠军戒指是他最喜欢的,他回答道:“下一个。”来自一个被许多人认为是自己领域中最成功的人,这个回答展示了他对持续追求成就的强大承诺(即便带着一点点自负)。作为软件开发者,这种同样的激情驱动着每次新版本发布时无休止的改进循环。
本章介绍了 Operator 开发周期中比第一次发布更重要的部分:下一个版本。发布新版本的软件并非只有 Operator 才需要做的事情,但惯用的流程和 Kubernetes 上游标准确实为 Operator 项目创建了独特的要求。通过探索创建和发布新版本所需的技术步骤,以及那些指导发布的更抽象的政策和时间表,我们确保你了解了一些建议,帮助你保持 Operator 的持续更新。
本书的技术教程部分到此结束。虽然有许多话题和细节遗憾地未能包含在这些章节中,但我们已经解释了构建一个操作符所需的所有基础概念,遵循操作符框架。 在下一章中,我们将以常见问题解答的形式总结这些概念,快速回顾我们所涵盖的内容。
第九章:第九章:深入探讨常见问题与未来趋势
Operator 框架涵盖了许多不同的主题,其中许多主题已在本书中讨论过。在本章中,我们不会讨论任何新主题。相反,我们将以简短、易于消化的 FAQ 风格标题回顾从书籍开始以来所有主要内容。这些常见问题的目的是提供一个简短的参考和复习,帮助你回顾整个书中涵盖的内容。这将是一个很好的快速提醒,尤其是在你准备面试、认证考试或仅仅是想记住某个点的概述时。本节常见问题的框架将包括以下部分:
-
关于 Operator 框架的常见问题
-
关于 Operator 设计、自定义资源定义(CRDs)和 API 的常见问题
-
关于 Operator SDK 和编码控制器逻辑的常见问题
-
关于 OperatorHub 和 Operator 生命周期管理器的常见问题
-
Operator 框架的未来趋势
这些部分大致按书中的顺序排列,因此按顺序阅读本章将帮助你回忆并巩固你对这些主题的理解,正如它们最初呈现的那样。
关于 Operator 框架的常见问题
这些主题包括 Operator 框架概述、其基本组件以及 Operator 设计的一般词汇。来自本节的主题摘自 第一章,介绍 Operator 框架。
什么是 Operator?
Operator 是一种 Kubernetes 控制器。Operator 旨在自动化管理 Kubernetes 应用程序和集群组件。它们通过不断地工作,将集群的当前状态与用户或管理员定义的期望状态进行协调。
Operator 对 Kubernetes 集群提供了什么好处?
Operator 为开发者提供了一种惯用的方式,将自动化的集群和应用程序管理逻辑编码到控制器中。Operator 还提供了将这种自动化的设置暴露给非开发者用户(例如集群管理员或客户)的方法。这种自动化释放了工程和 DevOps 资源,能够处理许多任务。
Operator 与其他 Kubernetes 控制器有何不同?
Operator 与任何其他 Kubernetes 控制器非常相似。Kubernetes 中的一些内置控制器示例包括 调度器、API 服务器和控制器管理器(它本身管理其他控制器)。这些原生控制器都负责自动执行核心集群功能,例如将 Pod 放置到节点上并维护 Deployment 副本数。这些都属于 Operator 所表现的持续状态协调模式的一部分。
然而,尽管功能上相似,Operator 的定义通过概念性和语义性约定将其与标准控制器区分开来。这些包括构成 Operator Framework 的开发库、工具、部署方法和分发渠道。
什么是 Operator Framework?
Operator Framework 是一组开发和部署工具以及模式,它定义并支持构建 Operator 的标准流程。广义来说,这包括代码库和脚手架工具(Operator SDK),用于安装、运行和升级 Operator 的组件(Operator 生命周期管理器 (OLM),以及在 Kubernetes 社区中分发 Operator 的集中式索引(OperatorHub)。
什么是 Operand?
Operand 是由 Operator 管理的组件或资源。
Operator Framework 的主要组成部分是什么?
Operator Framework 的主要组成部分如下:
-
Operator SDK – 一套用于从头开始构建 Operator 的常用库和命令行工具。这包括如 Kubebuilder 这样的工具的封装,用于生成在 Kubernetes 控制器中使用的代码。
-
OLM – 一个设计用来在 Kubernetes 集群中安装、运行和升级(或降级)Operator 的组件。Operator 开发者编写(或生成)描述 Operator 相关元数据的文件,使 OLM 可以自动化 Operator 的部署。OLM 还充当已安装 Operator 的集群内目录,并确保已安装的 Operator 之间没有 API 冲突。
-
OperatorHub – 一个集中式索引,包含由开源 GitHub 存储库支持的免费可用的 Operator。开发者可以将 Operator 提交到
operatorhub.io/
,以便用户可以进行索引和搜索。
现在,让我们来讨论可以用来编写 Operator 的编程语言。
Operator 可以用哪些编程语言编写?
从技术上讲,Operator 可以用任何支持与 Kubernetes 集群交互所需的客户端和 API 调用的语言编写。但 Operator SDK 支持在 operator-sdk
命令行工具中编写 Operator,但这些 Operator 最终在能力上是有限的。在本书中,我们介绍了用 Go 编写 Operator 所需的代码,Go 提供了更多的功能,这是由 Operator 能力模型定义的。
什么是 Operator 能力模型?
能力模型是衡量 Operator 提供的功能并告知用户该功能级别的标准。它定义了五个递增的功能级别:
-
I 级 – 基本安装:能够安装 Operand 的 Operator,如果适用,还会暴露该安装的配置选项。
-
II 级 – 无缝升级:能够在不中断功能的情况下升级自身及其 Operand 的 Operator。
-
III 级 – 完整生命周期:能够处理 Operand 备份创建和/或恢复、故障恢复的故障切换场景、更复杂的配置选项以及自动扩展 Operands 的 Operator。
-
IV 级 – 深度洞察:报告自己或其 Operand 的指标的 Operator。
-
V 级 – 自动驾驶:处理复杂自动化任务的 Operator,包括自动扩展(根据需要创建更多 Operand 副本或删除副本)、自动修复(根据自动报告如指标或警报,检测并恢复故障场景,无需干预)、自动调优(将 Operand Pods 重新分配到更合适的节点)、或异常检测(检测 Operand 性能与常规应用健康不符的情况)。
这些是第一章中涵盖的一些最基础的主题。本书接下来的章节在这些基础上进一步探讨了 Operator 设计概念。
关于 Operator 设计、CRD 和 API 的常见问题解答。
这些问题涵盖了有关 Operator 设计的信息,包括开发 Operator 的方法以及 Operator 如何在 Kubernetes 集群中运行。本节中的主题在第二章中介绍过,理解 Operator 如何与 Kubernetes 交互,以及第三章,设计 Operator – CRD、API 和目标调解。
Operator 如何与 Kubernetes 交互?
Operator 通过事件触发的持续监控集群状态与 Kubernetes 交互,在此过程中,Operator 尝试将当前状态与用户指定的期望状态进行调解。从技术角度看,它通过一组标准的 Kubernetes 客户端库来完成这一过程,这些库允许它列出、获取、观察、创建和更新 Kubernetes 资源。
Operator 作用于哪些集群资源?
Operator 可以作用于通过 Kubernetes API 可访问的任何资源(并且该 Operator 具有集群权限访问)。这包括原生 Kubernetes 资源(例如 Pods、ReplicaSets、Deployments、Volumes 和 Services)以及第三方 API 或 CRD 提供的自定义资源(CRs)。
什么是 CRD?
CRD(自定义资源定义)是 Kubernetes 原生 API 类型,允许开发者通过 CR 类型扩展 Kubernetes API,这些 CR 类型在外观和行为上与原生 Kubernetes API 资源完全一致。运维开发者可以创建一个 CRD 来定义其 Operator 的 API 类型(例如,customresourcedefinitions/MyOperator
),然后该 CRD 提供一个模板,用于创建符合该类型定义的 CR 对象(例如,MyOperators/foo
)。
CRD 与 CR 对象有什么不同?
CR 对象是基于 CRD 模板的对象的单独表示。从编程的角度来看,它是抽象类型和该类型的对象实例化之间的区别。CR 对象是用户用来设置 Operator 配置的 API 对象,例如通过 kubectl get MyOperators/foo
等命令进行交互。
Operator 在 Kubernetes 的哪些命名空间中运行?
Operator 可以是 namespaced
或 cluster-scoped
。命名空间 Operator 运行在单个命名空间内,这允许在集群中安装多个相同的 Operator。集群范围 Operator 运行在集群级别,管理多个命名空间中的资源。Operator 的范围主要由其 CRD 中定义的命名空间范围以及分配给 Operator 服务的 基于角色的访问控制 (RBAC) 策略决定。
用户如何与 Operator 进行交互?
用户首先通过安装 Operator 与其进行交互。安装方式可以是通过像 OperatorHub 这样的索引,或者直接从你所在组织的网站或 GitHub 页面安装。通常,Operator 可以通过一个 kubectl create
命令进行安装,尤其是在通过 OLM 安装时。
一旦安装,用户将主要通过创建一个 CR 对象与 Operator 进行交互,该对象是其 CRD 的表示。该 CR 对象将公开 API 字段,用于调整与 Operator 相关的各种设置。
如何在 Operator 生命周期的早期进行变更规划?
与许多软件项目一样,深思熟虑的设计可以使项目随着时间的推移更容易增长。在 Operator 的背景下,这意味着要及早考虑 Operator(更重要的是其 Operand)如何随着时间变化而变化。上游 API 和第三方依赖的支持周期可能与贵组织的支持周期不同,因此尽量减少这些依赖的暴露对于减少后续的工作非常有帮助。为此,从小规模开始设计 Operator,并根据需要扩展其功能可能会更有帮助。这正是能力模型背后的理念,每一层都有效地构建在上一层的基础上。
Operator 的 API 如何与其 CRD 相关联?
Operator 提供的 API 是其 CRD 的代码定义。当使用 Operator SDK 在 Go 中编写 API 时,该 API 会通过 SDK 提供的工具生成 CRD。
Operator API 的约定是什么?
Operator API 的约定通常遵循与本地 API 对象相同的上游 Kubernetes 约定。其中最重要的一点是,Operator 对象包含两个字段:spec
和 status
,这为 Operator 执行集群状态协调循环提供了基础。spec
是 Operator 对象的用户输入部分,而 status
报告 Operator 当前的运行状态。
什么是结构化 CRD 架构?
结构化 CRD 架构是强制集群内存中已知字段的对象定义。Kubernetes 要求 CRD 定义结构化架构,这可以通过 Operator SDK 提供的工具生成。它们提供了安全优势,且通常比较复杂,因为建议使用自动生成而非手动编写。
什么是 OpenAPI v3 验证?
OpenAPI v3 验证是一种在创建或修改对象时提供字段类型和格式验证的格式。字段验证在 Go 代码中为 CRD 的 API 类型定义。这些验证以注释的形式存在(例如,//+kubebuilder:validation…
)。当生成 Operator CRD 时,这些注释会被生成到验证架构中。
什么是 Kubebuilder?
Kubebuilder 是一个开源项目,提供了一个生成 Kubernetes API 和控制器的工具。Operator SDK 中的许多命令都是底层 Kubebuilder 命令的封装。了解这一点对于调试和排查 Operator SDK 问题时非常有帮助。
什么是对账循环?
对账循环,或称控制循环,是 Operator 的主要逻辑功能。从概念上讲,它是 Operator 执行的一系列持续检查,用以确保实际的集群状态与期望的状态相匹配。实际上,这通常不是一个持续循环,而是一个由事件触发的函数调用。
Operator 的对账循环的主要功能是什么?
Operator 的对账循环是其核心逻辑,在此期间,Operator 会评估集群的当前状态,将其与期望状态进行比较,并在必要时执行所需的操作,以使集群的状态与期望状态一致。这可能意味着更新部署或调整工作负载约束以应对状态变化。
事件触发的两种类型是什么?
事件触发通常分为两类:基于水平触发和基于边缘触发。Operator 设计遵循基于水平触发的方法,在这种方法中,触发事件并不包含集群状态的全部上下文。相反,Operator 只需从事件中接收到足够的信息,以理解相关的集群状态。通过每次重建这些信息,Operator 确保由于延迟或丢失的事件而不会丢失任何状态信息。这与基于边缘触发的事件(仅由事件的传入动作激活对账)形成对比,后者可能导致信息丢失,不适合像 Kubernetes 这样的大型分布式系统。这些术语源自电子电路设计。
什么是 ClusterServiceVersion(CSV)?
ClusterServiceVersion
是由 Operator Framework 提供的 CRD,包含描述单个 Operator 版本的元数据。ClusterServiceVersion
CRD 由 OLM 提供,OLM 是 Operator CSV 的主要消费者。OperatorHub 也使用 Operator CSV 向用户展示有关 Operator 的信息。
Operator 如何处理升级和降级?
Operator 版本由其发布版本、镜像标签和 CSV 元数据定义。CSV 尤其提供了升级通道的概念,允许开发者定义升级和降级版本的订阅路径。OLM 然后通过这些元数据了解如何在版本间过渡已安装的 Operators。Operator API 版本在 Operator 的 CRD 中定义,可以包含有关多个版本的信息。这使得开发者能够发布一个支持多个 API 版本的 Operator 版本,从而允许用户在单个版本中进行版本间的过渡。
Operator 如何报告故障?
Operator 有多种方式报告问题。这些方式包括标准的运行时日志、度量和遥测、状态条件以及 Kubernetes 事件。
什么是状态条件?
状态条件是 Operator 使用上游 Kubernetes API 类型(v1.Condition
)的一种能力,能够通过 Operator 的 CRD 中的 status
字段快速向用户通报各种失败(或成功)状态。
什么是 Kubernetes 事件?
事件是 Kubernetes API 对象,可以被本地 Kubernetes 工具(如 kubectl
)聚合、监控和过滤。与状态条件的定义相比,它们提供更丰富的信息,允许向用户报告更先进的信息。
关于 Operator SDK 和编写控制器逻辑的常见问题
本节的主题聚焦于使用 Operator SDK 开发 Operator 的技术内容。这包括生成 Operator 项目的初始模板代码、填充自定义的调解逻辑,并通过更多高级功能扩展代码。这些主题在 第四章,“使用 Operator SDK 开发 Operator” 和 第五章,“开发 Operator —— 高级功能”中介绍。
什么是 Operator SDK?
Operator SDK 是一个软件开发工具包,提供了代码库和工具,用于快速搭建和构建 Operator。它主要通过 operator-sdk
二进制文件使用,提供了初始化项目、创建模板 API 和控制器、生成代码以及在集群中构建和部署 Operators 的命令。
如何使用 operator-sdk 架构一个模板 Operator 项目?
创建 Operator SDK 项目的第一个命令是 operator-sdk init
。该命令接受额外的标志以提供一些项目信息(例如项目的代码库位置),这些信息将在创建 Operator 的其他部分(如 API 和控制器)时填充变量。
一个 boilerplate Operator 项目包含哪些内容?
一个 boilerplate Operator 项目(即仅通过 operator-sdk init
创建且未做其他更改的项目)只包含一个包含一些基本标准代码的 main.go
文件,一个用于构建容器镜像的 Dockerfile
文件,一个提供更多构建和部署 Operator 命令的 Makefile
文件,以及一些附加的配置文件和依赖目录。
如何使用 operator-sdk 创建 API?
operator-sdk create api
命令初始化一个模板 API 文件结构,供开发者填写。它接受额外的标志来定义 API 版本和 Operator 的资源名称,甚至可以创建 Operator 的 boilerplate 控制器来处理 API 对象。
使用 operator-sdk 创建的基本 Operator API 是什么样子的?
operator-sdk create api
创建的空模板 API 包含一个 Operator config
对象的基本定义。该对象由上游元数据类型组成(包含如 namespace
和 name
之类的字段),以及两个子对象,分别表示 Operator CRD 的 spec
和 status
字段。
operator-sdk 还生成了哪些其他代码?
除了供开发者修改的 boilerplate 模板代码外,operator-sdk
命令还生成了 deepcopy
和其他 Kubernetes 客户端使用的代码,这些代码不应修改。因此,定期重新运行 operator-sdk
提供的代码生成器非常重要,以确保这些自动生成的代码保持最新。
Kubebuilder 标记的作用是什么?
Kubebuilder 标记是特殊格式的代码注释,放置在 API 对象的字段、类型和包上。它们定义了字段验证设置(如类型、长度和模式)以及其他控制 Operator CRD 生成的选项。它们允许在靠近相关代码的地方配置这些选项,从而使其非常清晰。
Operator SDK 如何生成 Operator 资源清单?
Operator SDK 生成相关的资源清单(包括 Operator 的 CRD 以及其他必需资源,如 make manifests
命令)。该命令在基本 Operator 项目的标准 Makefile
文件中定义,并调用 controller-gen
二进制文件(Kubebuilder 工具集的一部分)来进行生成。
你还可以如何自定义生成的 Operator 清单?
生成的操作员清单可以根据需要进行自定义,超出 controller-gen
工具的默认操作。这个程序是生成资源文件的基础组件,手动运行它可以访问额外的命令和标志。
什么是 go-bindata 和 go:embed?
go-bindata
和 go:embed
是两种将原始文件编译成 Go 代码的方式。前者是可以导入项目作为库的包,而后者是 Go 编译器的原生指令。两者都是使与操作员相关的资源(例如 Operand 部署 YAML 文件)在代码中可访问并可供其他用户读取的有用选项。
控制/协调循环的基本结构是什么?
对于大多数操作员,控制循环的基本结构遵循以下步骤:
-
检索操作员/集群的期望配置。
-
评估集群的当前状态,结合操作员可用的信息,并将其与期望配置进行比较。
-
如有必要,采取行动将当前集群状态过渡到期望状态。
在每个步骤中,操作员还应实现错误检查和状态报告分支,以便向用户报告任何故障(例如,如果操作员无法找到自己的配置,显然不应该继续)。
控制循环功能如何访问操作员配置设置?
主要控制循环功能(在 Operator SDK 项目中为 Reconcile()
)通过其 CR 对象访问操作员的配置设置。这要求用户在集群中创建了 CR 对象的实例(如 CRD 所定义的)。由于 CR 对象可以通过 Kubernetes API 访问,操作员可以使用 Kubernetes 客户端和诸如 Get()
之类的功能来检索其配置。在 Operator SDK 中,这些客户端会自动填充并传递给 Reconcile()
函数,随时可以使用。
状态条件报告了哪些信息?
v1.Condition
对象包含描述状态的字段:type
(状态的简短名称),status
(表示状态的布尔值),以及 reason
(提供更多信息的较长描述)。它还包含关于状态最后转换时间戳的时间戳信息。
指标的两种基本类型是什么?
指标大致分为两类:服务指标和核心指标。服务指标是为特定组件(或服务)定义的自定义指标,而核心指标是所有服务发布的通用指标(例如 CPU 和内存使用情况)。
如何收集指标?
核心指标可以使用 metrics-server
组件进行收集(github.com/kubernetes-sigs/metrics-server
)。服务指标可以通过任何指标聚合工具收集,例如 Prometheus。
什么是 RED 指标?
速率、错误和持续时间(RED)是一个描述定义新指标最佳实践的缩写。建议服务的三种关键指标应包括速率、错误和持续时间。即分别报告每个周期的请求数量、失败的尝试数量和请求的延迟时间。
什么是领导者选举?
领导者选举是指运行多个副本的应用程序,其中一个副本(领导者)同时工作。这为应用程序提供了高可用性,因为如果一个副本失败,其他副本可以准备好替代它。这个概念适用于 Operator,因为在分布式系统中,可能需要确保 Operator 的正常运行时间。
领导者选举的两种主要策略是什么?
领导者选举可以实现为领导者与租约(leader-with-lease)或终身领导者(leader-for-life)。领导者与租约的方法是 Operator SDK 项目的默认策略,在这种方法中,当前的领导者会定期尝试续期其状态。这允许领导者的快速切换,若有需要时。终身领导者方法中,领导者只有在被删除时才会放弃其身份。这使得恢复变得较慢,但更加明确。
什么是健康检查和准备检查?
健康检查和准备检查是看门狗端点,允许应用程序指示何时处于健康状态(即运行顺畅)和准备就绪状态(即积极准备好接受请求)。Operator SDK 提供了一个基本的健康检查和准备检查,作为一个模板项目,但这些检查可以轻松扩展以适应自定义逻辑。
关于 OperatorHub 和 OLM 的常见问题
这些问题与 Operator 的构建、发布和部署有关。涵盖的主题包括使用 OLM 安装 Operator 和将 Operator 提交到 OperatorHub。这些主题来自于 第六章《构建与部署您的 Operator》和 第七章《使用 Operator 生命周期管理器安装和运行 Operator》。
有哪些不同的方法可以编译一个 Operator?
与许多云原生应用程序一样,Operator 可以被编译为本地二进制文件或构建为适用于直接部署到 Kubernetes 集群中的容器镜像。Operator SDK 提供了执行这两种操作的命令。
一个基础的 Operator SDK 项目是如何构建容器镜像的?
Operator SDK 提供了 Makefile
目标,可以通过 make docker-build
构建 Docker 镜像。默认情况下,这会将主 Operator 源代码(特别是主控制器和 API)以及其 assets
目录复制到 Docker 镜像中。
如何在 Kubernetes 集群中部署 Operator?
Operator 可以在 Docker 镜像构建完成后手动部署,类似于其他应用程序在 Kubernetes 集群中的部署方式。Operator SDK 通过 make deploy
命令简化了这个过程。Operator 也可以通过 OLM 安装。
什么是 OLM?
OLM 是 Operator Framework 提供的一个组件,用于管理 Operator 在集群中的安装、运行以及升级/降级。
使用 OLM 运行 Operator 有什么好处?
OLM 提供了一个方便的工具来安装 Operators(与手动部署 Operator 镜像等方法不同),并且可以监控集群中 Operator 的状态。它可以确保 Operator 不会与其他 Operator 发生冲突。它还可以处理集群中 Operator 的升级和降级。最后,它使集群中已安装的 Operator 列表对用户可用。
如何在集群中安装 OLM?
可以通过运行 operator-sdk olm install
来使用 operator-sdk
二进制文件安装 OLM。
operator-sdk olm status
命令显示什么内容?
运行 operator-sdk olm status
(在集群中安装 OLM 后)会通过列出 OLM 所需的资源(包括它安装的 CRD、RoleBindings、Deployment 和命名空间)来显示 OLM 的健康状态。
什么是 Operator 包?
包是用于打包 Operator 清单和 ClusterServiceVersion(CSV)的格式。
如何生成包?
使用 Operator SDK,可以通过运行 make bundle
来生成包。此命令是交互式的,会要求提供有关 Operator 和您的组织的信息,然后将这些信息编译到包中的 Operator 元数据中。
什么是包镜像?
包镜像是一个容器镜像,保存来自 Operator 包的信息。该镜像用于根据基础元数据在集群中部署 Operator。
如何构建包镜像?
可以通过运行 make bundle-build
使用 Operator SDK 构建包镜像。这将构建包含包信息的基本 Docker 镜像。
如何使用 OLM 部署包?
operator-sdk run bundle
命令使用 OLM 将包镜像部署到集群中。该命令需要一个额外的参数,即容器注册表中包镜像的位置,例如 operator-sdk run bundle docker.io/myreg/myoperator-bundle:v0.0.1
。
什么是 OperatorHub?
OperatorHub 是一个集中管理的索引,收录了来自各种开发者和组织发布的 Operator。它的网址是 operatorhub.io
。每个 Operator 都提供关于 Operator 和开发者的信息,以及安装指南、支持资源和源代码链接。它通过解析 Operator 包中的信息(主要来自 Operator CSV)来实现这一点。
如何从 OperatorHub 安装 Operator?
每个 OperatorHub 上的 Operator 页面都包括一个安装说明的链接。这些说明包含一系列简单的命令,通常使用 kubectl create
:
图 9.1 – OperatorHub 安装说明截图
如何将 Operator 提交到 OperatorHub?
在 OperatorHub 上列出一个 Operator 涉及向 GitHub 仓库提交 pull request (PR),该仓库作为支持 OperatorHub 的后端索引。要求包括提交一个有效的 CSV 文件,并按照目录结构排列,这样就可以直接从 Operator SDK 包生成命令的输出中复制。自动化测试会检查是否满足所有提交要求,然后合并提交的 PR,Operator 很快就会列出在 operatorhub.io
上。
Operator 框架中的未来趋势
本节涉及的是你自己 Operator 的未来维护,以及与上游 Kubernetes 社区的持续工作对齐,以及它如何与第三方 Operator 开发相关。这些内容来自 第八章,为 Operator 的持续维护做准备。
如何发布 Operator 的新版本?
发布 Operator 的新版本主要取决于你组织的发布方法,包括时间安排和交付基础设施。然而,Operator 框架提供了一些方式来标注 Operator 版本,例如在 Operator 的 CSV 文件中的版本字段(该字段会显示在 OperatorHub 上)、新的 API 版本和发布通道。
何时适合添加新 API 版本?
添加新 API 版本最常见的时机是在引入对现有 API 的破坏性更改时。增加 API 版本也可以作为其稳定性水平的一个指示(例如,将 v1alpha1
API 升级为 v1beta1
或 v1
)。最重要的是,在用新版本替换旧版本时,遵循你所选择的支持时间表(或者,如果你选择采用 Kubernetes 时间表作为模板,则遵循 Kubernetes 时间表)。
如何添加新 API 版本?
可以通过 operator-sdk create api
命令添加新的 API。这个命令会像在项目中创建 Operator 初始 API 时一样,生成空的 Go 文件。一旦这些文件填充了新的 API 类型,生成的相应代码、CRD 和其他清单可以通过 make generate
和 make manifests
命令来更新。
什么是 API 转换?
API 转换指的是 Operator 能够在同一 API 的不兼容版本之间转换 API 对象。Operator 中的转换代码允许它在同一版本中支持多个 API 版本。通常,开发者会编写手动转换逻辑,确保两个 API 之间的不兼容字段能够相互转换(双向转换)而不丢失任何数据。这么做的最大好处是允许用户无缝地从废弃的 API 过渡到更新的版本。
如何在两个 API 版本之间转换?
在设计新的 API 时,考虑如何准确地将旧版本中的现有信息转换到新版本并反向转换。然后,你可以通过在 Operator 的代码中实现转换 webhook 来在两个版本的 API 之间进行转换。
什么是转换 webhook?
转换 webhook 是一个在 Operator Pod 中暴露的端点,它接收来自 API 服务器的请求,用于在不同版本之间对 API 对象进行编码和解码。
如何将转换 webhook 添加到 Operator 中?
转换 webhook 由两个必须为正在转换的 API 对象类型实现的 Go 接口组成。这两个接口是 Convertible
和 Hub
,来自 sigs.k8s.io/controller-runtime/pkg/conversion
。最重要的是,Convertible
接口要求实现两个函数,ConvertTo()
和 ConvertFrom()
,开发者在这两个函数中手动编写逻辑,以便在两个对象之间转换字段。然后,可以使用 operator-sdk create webhook
命令创建暴露相关端点的 webhook 清单。这些清单通过在项目的 Kustomization
文件中取消注释 webhook 引用来启用。
什么是 kube-storage-version-migrator?
kube-storage-version-migrator 是一个工具,允许 Kubernetes 用户手动将现有的存储 API 对象转换为新的 API 版本。这对于帮助 Operator 版本之间的更新非常有用,尤其是当新 API 改变了 Operator CRD 对象的存储版本时。
如何更新 Operator 的 CSV?
Operator 的 CSV 包含有关 Operator 的信息,包括当前版本。为了提高 Operator 的版本,首先需要更新 CSV,使用 replaces
字段指示 Operator 的先前版本。换句话说,这告诉像 OLM 这样的工具,哪个版本是新版本的前一个版本,以便它能够升级到正确的版本。然后,需要在 Operator 项目的 Makefile
中更新 VERSION
变量为新版本号(例如 0.0.2
)。使用 make bundle
和 make bundle-build
命令生成新的 CSV。
什么是升级渠道?
升级渠道是一种提供不同分发升级路径的方式。例如,Operator 的 CSV 可以定义 alpha
和 stable
渠道。用户可以选择订阅其中一个渠道,以便按照他们期望的节奏和稳定性级别获取更新版本。
如何在 OperatorHub 上发布新版本?
OperatorHub 在 GitHub 上托管 Operator 的捆绑信息。每个 Operator 都有一个独特的目录,里面包含该 Operator 的每个不同版本作为子目录。这些子目录中包含该版本的捆绑信息(例如其 CSV)。
什么是 Kubernetes 弃用政策?
Kubernetes 废弃政策定义了核心 Kubernetes 组件的 API 版本支持指南。这为下游和第三方开发者(以及用户)提供了基于 API 稳定性支持的保证时间表。它也是其他不严格遵守该政策但希望与上游 Kubernetes 政策保持一致的项目的良好模板。
在 Kubernetes 废弃政策中,如何移除 API 元素?
在 Kubernetes 废弃政策中,API 元素只能通过增加 API 版本来移除。换句话说,如果一个 Operator 遵循 Kubernetes 废弃政策,则不能从当前 API 中移除 CRD 字段。相反,它必须创建一个不包含该字段的新 API 版本。
API 版本通常支持多长时间?
Kubernetes 废弃政策中 API 版本的支持时间表取决于其稳定性等级(alpha、beta 或 GA)。Alpha API 没有保证的时间表,可以随时更改。Beta API 支持 9 个月或三个发布版本(以较长者为准)。已经毕业的 GA API 不能被移除,但可以标记为废弃。
Kubernetes 发布周期多长?
Kubernetes 小版本发布周期大约为 15 周,并包含多个里程碑日期,包括增强冻结(Enhancements Freeze)、代码冻结(Code Freeze)和 Retrospective。
什么是增强冻结(Enhancements Freeze)?
增强冻结(Enhancements Freeze)是发布周期中的一个节点,此时 Kubernetes Enhancements(KEPs)必须是可实现的,并承诺加入当前发布,或者正式推迟到至少下一个发布。
什么是代码冻结(Code Freeze)?
在代码冻结时,所有重要的代码更改必须合并到当前发布版本中,否则需要获得合理的延期例外。
什么是 Retrospective?
Retrospective 是一系列会议,这些会议在 Kubernetes 版本发布前后举行,社区成员在这些会议中反思发布中成功的领域,并指出可以改进的流程。
Kubernetes 社区标准如何应用于 Operator 开发?
Operator 开发者没有义务遵守 Kubernetes 社区标准或参与上游开发。然而,了解 Kubernetes 项目的运作对 Operator 开发者非常有帮助,因为它提供了功能支持的指南,而 Operator 很多时候依赖于某些 Kubernetes 特性。这些标准还为 Operator 自身的开发流程提供了模板,可以呈现给 Operator 的用户。
总结
显然,在讨论操作符框架时,有太多的主题无法在一个简短的章节中涵盖所有内容。因此,本章的目标是回顾已经讨论过的最重要的要点,以简洁的方式解释它们,从头到尾。为了深入了解这些主题中的任何一个,已提供了它们原始章节作为资源。此外,本书中列出的众多精彩参考资料也可以为你提供支持,并进一步阅读所有这些主题。
在接下来的章节中,我们将研究在开源社区中开发的实际操作符。通过这样做,我们有机会将前面讨论的每个主题与具体示例联系起来,并比较本书中提供的示例与实际操作符之间的相似性和差异。
第十章:第十章:可选 Operator 案例研究——Prometheus Operator
本书的重点是介绍、讨论和展示使用 Operator Framework 开发 Kubernetes Operator 的主要流程。为此,构建了一个具有管理 nginx 部署基本功能的示例 Operator。这个示例旨在作为 Operator 开发的教程,避免用过多的功能或复杂的背景知识使读者感到困扰,从而理解用例。希望它能很好地实现这一目的。
但是,这个 nginx Operator 的简单性可能使得 Operator Framework 中的一些步骤看起来显得多余。从学习示例 Operator 到理解现实世界用例的应用,这是一个很大的跨越。因此,在本章中,我们将研究 Prometheus Operator(prometheus-operator.dev/
),该 Operator 用于管理 Prometheus 监控服务的单个部署(这个服务用于收集本书早期使用 nginx Operator 获取的指标)。在本章中,我们将其称为可选 Operator,因为它管理的 Operand 是一个应用级别的组件,并不对集群的运行至关重要(与下章不同,下章将讨论如何通过 Operator 管理核心的集群级别组件)。关于 Prometheus Operator 的内容将在以下几个部分讨论:
-
一个现实世界的用例
-
Operator 设计
-
Operator 的分发与开发
-
更新与维护
当然,虽然 Prometheus Operator 和本书中示例的 nginx Operator(严格遵循 Operator Framework 模板)之间有许多相似之处,但同样重要的是要突出它们的差异。部分差异将在全章中提到,展示即使在 Operator Framework 内部,也没有一种统一的方式来开发 Operator。这正是像这样的开源软件的魅力所在:它的模式和差异促进了一个多元化的社区,孕育了各种各样的项目。
一个现实世界的用例
Prometheus(github.com/prometheus/prometheus
)是一个用于通过收集应用程序导出的指标并以时间序列方式存储这些数据,从而监控应用程序和集群的工具。在第五章《开发 Operator - 高级功能》中,我们在 nginx Operator 中实现了基本的 Prometheus 指标,暴露了关于 Operator 总体调和尝试的汇总信息。这只是一个依赖 Prometheus 进行监控的潜在应用架构设计的小例子。
Prometheus 概述
除了抓取和聚合度量标准,Prometheus 还定义了一个数据模型,用于创建不同类型的度量标准并在应用程序中实现它们。这个模型通过 Prometheus 提供的多种语言客户端来进行仪表化,包括 Ruby、Python、Java 和 Go。这些客户端使得应用程序开发者能够轻松地以与 Prometheus 服务器 API 兼容的格式导出度量标准(就像我们在示例 nginx Operator 中所做的那样)。
除了计数器度量类型(用于汇总 nginx Operator 中的 reconciles_total
度量标准),Prometheus 提供的其他度量类型包括 Gauge、Histogram 和 Summary。每种度量标准都可以通过标签的形式导出附加属性,从而为它们报告的数据提供额外的维度。
此外,Prometheus 允许用户使用其自身的查询语言 PromQL 来搜索度量标准。这种语言的功能,再加上度量标准本身灵活且广泛的实现可能性,帮助 Prometheus 成长为云原生应用程序(不仅仅是 Kubernetes)中领先的度量收集工具之一(如果不是的话)。
在本书的早期章节中,我们简要讨论了如何使用 Prometheus 客户端创建新的度量标准,并通过 PromQL (第五章,开发操作员 – 高级功能) 来检索这些度量标准,同时还构建了示例 Operator。这些主题虽然重要,但与 Prometheus Operator 关系不大(无论如何,它们在此处简要描述,以提供实际使用案例的完整背景)。Prometheus Operator 解决的更相关的方面是 Prometheus 作为操作数的安装和配置。
安装和运行 Prometheus
在 第六章,构建和部署你的 Operator 中,我们展示了一种通过在 nginx Operator 项目中引用 kube-prometheus
库来安装 Prometheus 到集群中的方法。kube-prometheus 的优点是它安装了一个完整的监控堆栈,包括 Grafana 等组件,但也包括 Prometheus Operator 本身。那么,在集群中安装 Prometheus 到底意味着什么?通过使用 kube-prometheus(进而使用 Prometheus Operator),我们节省了哪些步骤?为了回答这些问题,首先让我们退一步,了解 Prometheus 的工作原理。
Prometheus 实例的核心是 Prometheus 服务器,它作为一个单一的二进制文件运行,获取指标并将其提供给 Web UI、通知服务或长期存储。类似于 Operator(或任何旨在部署到 Kubernetes 的应用程序),这个二进制文件必须编译并打包成容器镜像。如 Prometheus 文档中所描述,预编译的二进制文件可以直接下载(作为可执行文件或容器镜像),或者可以从源代码构建(github.com/prometheus/prometheus#install
)。这对于本地运行来说足够方便,但对于部署到 Kubernetes 集群(特别是生产环境)来说,还需要进一步的设置。
首先,通常不允许在没有某种配置形式的情况下将容器直接部署到集群中。Kubernetes 对象如 Deployments 会将容器封装在一个受管理和可配置的表示中,暴露诸如副本数和发布策略等选项。因此,手动在 Kubernetes 集群中安装 Prometheus 将需要自己定义 Kubernetes Deployment。
一旦它在集群中运行,Prometheus 就需要访问暴露指标的应用程序。这需要额外的资源,如 ClusterRoles
和 RoleBindings
,以确保 Prometheus Pod 拥有从集群及其应用程序抓取指标的权限。这些 RBAC 权限必须通过 ServiceAccount
实例绑定到 Prometheus Pod 上。然后,用户访问 Web UI 需要一个 Service 来使前端在 Web 浏览器中可达。该 Service 只能通过 Ingress 对象暴露到集群外部。
对于初始安装来说,这已经是很多步骤了。然而,手动管理该安装也需要持续的关注和对每个资源及其角色的系统性理解。虽然这肯定是可行的,但有一个 Operator 来处理这些资源可以释放工程师的时间,并通过抽象复杂的清单声明来实现更好的扩展。
然而,正如本书中所讨论的,许多(如果不是大多数)Operator 不仅仅是安装它们的 Operand。通常,它们会继续管理已安装应用程序的生命周期,包括允许你更改正在运行的 Operand 的配置。Prometheus Operator 也为 Prometheus 做了这件事。
配置 Prometheus
作为一个功能齐全的应用程序,Prometheus 提供了一套丰富的配置选项,以适应不同的场景。这些配置在官方 Prometheus 文档中有详细说明,网址为 prometheus.io/docs/prometheus/latest/configuration/configuration/
。在这些设置中,有两组用于配置 Prometheus 的选项:
-
命令行标志:它们控制影响 Prometheus 服务器本身的设置,例如持久存储访问和日志记录设置。
-
YAML 配置:它通过命令行标志(
--config.file
或--web.config.file
)传递给 Prometheus,并提供对 Prometheus 监控行为的声明式控制;例如,度量抓取设置。
这种设置类型的分离是一个很好的设计,通常用于 Kubernetes 应用程序、Operators 以及非 Kubernetes 软件中。它的好处是能清晰地将运行时应用设置与行为选项解耦,并且这种区分对用户而言是显而易见的。然而,从管理员的角度来看,这会创建两个需要单独跟踪的关注区域。
命令行标志
可以通过运行 prometheus -h
来查看 Prometheus 二进制文件的所有命令行标志。共有几十个选项,但它们大致可以分为以下几类:
-
Web
-
存储
-
规则
-
查询
-
日志记录
这些类别中的每一项都有最多 10 个(或更多)独立设置,控制 Prometheus 服务器的不同方面。此外,还有 --enable-feature
标志,它接受一个用逗号分隔的功能列表来启用(例如,--enable-feature=agent,exemplar-storage,expand-internal-labels,memory-snapshot-on-shutdown
启用这四个额外的功能门控)。
在 Kubernetes 部署清单中,这些标志将控制在 spec.template.spec.containers.command
(或 .args
)字段中。例如,以下是一个简单的 Prometheus 部署 YAML 文件,它传递了一个配置文件并启用了前述功能:
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
labels:
app: prometheus
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: docker.io/prom/prometheus:latest
command: ["prometheus"]
args:
- --config=/etc/prom/config-file.yaml
- --enable-feature=agent,exemplar-storage,expand-internal-labels,memory-snapshot-on-shutdown
当然,配置文件还需要挂载到 Prometheus Pod 中,以便能够访问,如下代码所示。这展示了前述部署 YAML 文件,修改后添加了一个 VolumeMount
实例,使得配置文件像本地文件一样能够被 Prometheus Pod 访问(新代码已高亮显示):
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
labels:
app: prometheus
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: docker.io/prom/prometheus:latest
command: ["prometheus"]
args:
- --config=/etc/prom/config-file.yaml
- --enable-feature=agent,exemplar-storage,expand-internal-labels,memory-snapshot-on-shutdown
volumeMounts:
- name: prom-config
mountPath: /etc/prom
volumes:
- name: prom-config
configMap:
name: prometheus-cfg
该配置文件(挂载为 /etc/prom/config-file.yaml
)需要作为一个独立的 ConfigMap 创建。这将引出 Prometheus 配置文件所控制的第二组选项。
YAML 配置设置
Prometheus 的 YAML 配置格式暴露了控制 Prometheus 一般抓取(度量收集)行为的设置。在可用选项中,包括平台特定的 服务发现 (SD) 控制,这些控制适用于各个云服务提供商,包括 Azure、Amazon EC2 和 Google Compute Engine 实例。还有用于重新标记度量数据的选项,启用度量数据的远程读取和写入功能,以及配置 AlertManager 通知、追踪和示例等功能。最后,配置还提供了 TLS 和 OAuth 设置,以确保度量数据抓取的安全性。
所有这些选项已经为 Prometheus 配置提供了复杂的可能性。即使是 Prometheus 提供的示例配置文件,也有近 400 行!(不过,它是用来演示多种不同类型的指标设置。例如,看看github.com/prometheus/prometheus/blob/release-2.34/config/testdata/conf.good.yml
。)这可能会让人感觉不知所措,尤其是当你只想要一个简单的指标解决方案时(就像许多用户一样)。因此,我们将主要聚焦于 Prometheus 配置文件中的基本scrape_config
部分。这是配置文件的主要部分,告诉 Prometheus 在哪里以及如何找到它感兴趣的指标。
该指令通过定义一系列job
实例来执行。每个job
提供关于某些指标目标的信息,并指示 Prometheus 如何从匹配这些标准的目标中发现新的指标。例如,kubernetes_sd_config
设置(与抓取 Kubernetes 应用程序相关:prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config
)可以控制对节点、Pod、服务、端点和 Ingress 对象的指标收集。
总结手动配置 Prometheus 的相关问题
本章并不打算介绍如何运行 Prometheus。相反,前面的部分旨在通过具体示例,展示运行任何复杂应用程序时可能出现的潜在复杂性,以及当该应用程序部署到像 Kubernetes 这样的平台时,这些复杂性如何成倍增加,因为 Kubernetes 本身也需要额外的维护开销。
总结来说,之前发现的问题可以分为几个类别,接下来我们将讨论这些问题。
过多的平台知识
从一开始(在集群内部安装 Prometheus 时),就需要比实际运行应用程序本身了解更多关于平台和部署资源的知识。从 ClusterRoles 和 RoleBindings,到甚至只是 Deployment 清单声明,管理员必须理解 Kubernetes 安装架构,才能开始运行 Prometheus。
这很糟糕,因为它分散了工程时间,原本这些时间可以用来做其他事情。然而,这也创造了一个不稳定的环境,在这个环境中,这种架构知识可能只在安装时学到一次,随后迅速被遗忘,或者至少没有像其他与应用程序相关的资源那样得到很好的文档记录。在灾难发生时,这会耗费宝贵的恢复时间,因为这些知识必须重新获得。
复杂的配置
一旦 Prometheus 被安装到集群内,不可变的服务器设置必须通过标志传递,而各个指标抓取任务必须在 YAML 文件中配置。对于这两个步骤,众多可用的设置和每个设置的灵活选项导致整体配置复杂。对于指标任务来说,随着更多服务添加到集群并需要抓取的指标增加,这种复杂性可能会随着时间的推移而增加。这个配置必须得到维护,任何更改都需要小心进行,以确保它们能够有效地推广。
需要重启才能启用更改
说到变化,无论是命令行标志还是配置文件设置,都不会立即生效。必须重新启动 Prometheus 应用程序才能使更改生效。对于命令行标志的更改,这不是大问题,因为显然需要停止当前正在运行的副本(通常,更改 Kubernetes Deployment 清单会触发一个新的副本来应用这些更改)。
但对于配置文件设置来说,这一点就不那么显而易见,可能会导致令人沮丧的困惑,因为看起来更改没有生效。这看起来可能是个小错误,但它是一个过于容易犯的错误,因此不建议在生产环境中冒这个风险。更糟糕的是,它可能会导致用户在意识到错误之前做出多次更改,导致新的 Deployment 在最终重启时应用了多个不小心的更改。
这些只是运行没有 Operator 的应用程序时可能遇到的几个简单问题的示例。在接下来的部分中,我们将更详细地讨论 Prometheus Operator 如何特别处理这些问题,目的是呈现一组可以在为你的应用程序构建 Operator 时考虑的抽象解决方案。
操作符设计
Prometheus Operator 旨在缓解之前提到的关于在 Kubernetes 集群中运行 Prometheus 实例时所涉及的复杂性问题。它通过将 Prometheus 可用的各种配置选项抽象为自定义资源定义(CRD),由 Operator 的控制器进行调和,以确保集群中的 Prometheus 安装与所期望的状态一致,无论该状态是什么(以及如何变化)。
当然,与之前教程中的 nginx Operator 示例相比,Prometheus Operator 管理的是一个更为复杂的应用程序,具有更多可能的状态,需要能够调和这些状态。但总体方法仍然相同,因此我们可以通过本书中展示的相同开发步骤来评估这个 Operator。
CRDs 和 APIs
正如我们多次讨论过的,CRD(自定义资源定义)是构建许多 Operator 的主要对象。因为它们允许开发人员定义可以被 Operator 消费的自定义 API 类型。通常,用户通过 CRD 与 Operator 进行交互,通过与其 Operator 相关的 CRD 设置他们期望的集群状态。
本书主要聚焦于 Operator 提供单一配置 CRD 的概念(在示例中,这就是 NginxOperators
对象),但实际情况是,Operators 可以根据其功能提供多个不同的 CRD。这正是 Prometheus Operator 所做的。事实上,它提供了八个不同的 CRD(详见 github.com/prometheus-operator/prometheus-operator/blob/v0.55.1/Documentation/design.md
)。它提供的 CRD 完整列表定义了以下对象类型:
-
Prometheus
-
Alertmanager
-
ThanosRuler
-
ServiceMonitor
-
PodMonitor
-
Probe
-
PrometheusRule
-
AlertmanagerConfig
接下来我们将更详细地讨论这些对象类型。一般而言,这些 CRD 的目的大致可以分为几类:
-
操作对象部署管理
-
监控配置设置
-
附加配置对象
为了保持本章的内容聚焦,我们将深入探讨前两个 CRD 组,如前所述。(第三组,称为附加配置对象,包括 Probe
、PrometheusRule
和 AlertmanagerConfig
类型,这些涉及到更高级的监控设置,超出了理解 Operator 用例的范围。)
操作对象部署管理
前三个 CRD,Prometheus
、Alertmanager
和 ThanosRuler
,允许用户控制 Operand 部署的设置。做个比较,我们的示例 NginxOperator
CRD 控制了一个 nginx 实例的 Kubernetes 部署,暴露了如 port
和 replicas
等选项,直接影响该部署的配置。这些 Prometheus Operator CRD 也起到了相同的作用,只不过是针对三种不同类型的 Operand 部署。(从技术上讲,Prometheus Operator 将这些 Operand 作为 StatefulSets 运行,这是一种 Kubernetes 对象类型,而不是 Deployments,但相同的原则适用。)
这些与 Operand 相关的 CRD 被定义在 Operator 的代码中,位于 pkg/apis/monitoring/v1/types.go
(请注意,pkg/api/<version>
模式与我们 Operator SDK 代码路径中使用的模式类似)。具体谈论 Prometheus
对象的顶层定义,它与我们的 NginxOperator
CRD 完全相同:
prometheus-operator/pkg/apis/monitoring/v1/types.go:
type Prometheus struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PrometheusSpec `json:"spec"`
Status *PrometheusStatus `json:"status,omitempty"`
}
仅凭 TypeMeta
、ObjectMeta
、Spec
和 Status
字段,这个定义看起来非常直接。然而,仔细查看 PrometheusSpec
对象,所提供的配置选项数量变得更加明显:
prometheus-operator/pkg/apis/monitoring/v1/types.go:
type PrometheusSpec struct {
CommonPrometheusFields `json:",inline"`
Retention string `json:"retention,omitempty"`
DisableCompaction bool
WALCompression *bool
Rules Rules
PrometheusRulesExcludedFromEnforce []PrometheusRuleExcludeConfig
Query *QuerySpec
RuleSelector *metav1.LabelSelector
RuleNamespaceSelector *metav1.LabelSelector
Alerting *AlertingSpec
RemoteRead []RemoteReadSpec
AdditionalAlertRelabelConfigs *v1.SecretKeySelector
AdditionalAlertManagerConfigs *v1.SecretKeySelector
Thanos *ThanosSpec
QueryLogFile string
AllowOverlappingBlocks bool
}
对于本章的目的,了解每个选项的作用并不是必要的。但众多的字段展示了一个 Operator 的 CRD 可以有多大,这突出了对 Operator API 进行仔细管理的必要性。可用选项的列表通过嵌入的CommonPrometheusFields
类型进一步扩展,提供了对 Prometheus 副本数、Operand Pods 的 ServiceAccount 设置以及与 Prometheus 部署相关的其他设置的控制。
然而,从用户的角度来看,他们在集群中创建的 Prometheus
自定义资源对象可能看起来要简单得多。这是因为它类型定义中的所有字段都标记了 omitempty
JSON 标签(为了清晰起见,它们已从前面的代码块中的所有字段中移除,除了一个字段)。这表示在 Kubebuilder CRD 生成器中,这些字段是可选的,如果没有设置,它们就不会显示。因此,一个示例 Prometheus
对象可能会像下面这样简单:
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: sample
spec:
replicas: 2
总的来说,Prometheus
CRD 提供了一个集中位置,用于控制前述两类设置,如配置 Prometheus部分中所讨论的那样。也就是说,它将命令行标志选项和配置文件选项暴露在同一个地方(还包括 Kubernetes 特定的部署设置,如副本数量)。它通过控制监控选项的 CRD 来进一步解开这些设置,接下来我们将讨论这一点。
监控配置设置
虽然Prometheus
CRD 允许用户配置 Prometheus 指标服务本身的设置,但ServiceMonitor
和PodMonitor
CRD 实际上转化为 Prometheus job
配置 YAML 设置,正如在配置 Prometheus部分中所描述的那样。在这一部分,我们将讨论如何通过 ServiceMonitor
配置 Prometheus,以便从特定的 Service 中抓取指标(同样的基本概念适用于 PodMonitor,它直接从 Pods 中抓取指标)。
为了演示这一转换,以下的 ServiceMonitor
对象将用于让 Prometheus Operator 配置 Prometheus,使其从带有 serviceLabel: webapp
标签的 Service 端点抓取指标:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: web-service-monitor
labels:
app: web
spec:
selector:
matchLabels:
serviceLabel: webapp
endpoints:
- port: http
更具体地说,这个对象被分为两个部分,这两个部分是大多数 Kubernetes 对象共有的:metadata
和 spec
。每个部分都扮演着重要角色:
-
metadata
字段定义了描述该ServiceMonitor
对象的标签。这些标签必须传递给 Prometheus Operator(如Operand 部署管理部分所述,在Prometheus
对象中),以告知它操作员应该监视哪些ServiceMonitor
对象。 -
spec
字段定义了一个selector
字段,用于根据那些服务的标签指定哪些应用服务需要抓取指标。在这里,Prometheus 最终会知道要从标有serviceLabel: webapp
的服务抓取应用指标。它将通过查询每个服务的端点上命名为http
的端口来收集这些指标。
为了收集这些服务发现信息(并最终在 Prometheus YAML 配置中处理它),Prometheus Operator 必须被设置为监视具有app: web
标签的ServiceMonitors
。为此,可以创建类似于以下内容的Prometheus
CRD 对象:
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus
spec:
serviceAccountName: prometheus
serviceMonitorSelector:
matchLabels:
app: web
通过这个Prometheus
对象,Prometheus Operator 会监视这些ServiceMonitor
对象的实例,并自动生成等效的 Prometheus YAML 配置文件。对于之前提到的ServiceMonitor
对象,Prometheus 配置文件看起来类似于以下内容(请注意,这段代码仅作为示例,目的是强调 Prometheus 配置的复杂性,并不需要深入理解它):
global:
evaluation_interval: 30s
scrape_interval: 30s
external_labels:
prometheus: default/prometheus
prometheus_replica: $(POD_NAME)
scrape_configs:
- job_name: serviceMonitor/default/web-service-monitor/0
honor_labels: false
kubernetes_sd_configs:
- role: endpoints
namespaces:
names:
- default
relabel_configs:
- source_labels:
- job
target_label: __tmp_prometheus_job_name
- action: keep
source_labels:
- __meta_kubernetes_service_label_serviceLabel
- __meta_kubernetes_service_labelpresent_serviceLabel
regex: (webapp);true
- action: keep
source_labels:
- __meta_kubernetes_endpoint_port_name
regex: http
- source_labels:
- __meta_kubernetes_endpoint_address_target_kind
- __meta_kubernetes_endpoint_address_target_name
separator: ;
regex: Node;(.*)
replacement: ${1}
target_label: node
- source_labels:
- __meta_kubernetes_endpoint_address_target_kind
- __meta_kubernetes_endpoint_address_target_name
separator: ;
regex: Pod;(.*)
replacement: ${1}
target_label: pod
- source_labels:
- __meta_kubernetes_namespace
target_label: namespace
- source_labels:
- __meta_kubernetes_service_name
target_label: service
- source_labels:
- __meta_kubernetes_pod_name
target_label: pod
- source_labels:
- __meta_kubernetes_pod_container_name
target_label: container
- source_labels:
- __meta_kubernetes_service_name
target_label: job
replacement: ${1}
- target_label: endpoint
replacement: http
- source_labels:
- __address__
target_label: __tmp_hash
modulus: 1
action: hashmod
- source_labels:
- __tmp_hash
regex: $(SHARD)
action: keep
metric_relabel_configs: []
当然,这个完整的 YAML 配置非常长,手动创建(更别提维护)需要付出相当大的努力。为了本次讨论的目的,不需要详细解释完整的配置。它主要用来强调操作员将如此复杂的配置抽象为相对简单的 CRD 所做的工作。
正是Prometheus
和ServiceMonitor
等 CRD 之间的关系,以合理的方式实现了这种抽象。例如,发送一个包含监控服务设置的大型Prometheus
CRD 是非常容易的。这也通过只要求操作员监控一种类型的 CRD 变化,从而简化了操作员的代码。
但解耦这些设置使得每个 CRD 对象保持可管理和可读性。此外,它还提供了对 Operand 设置修改的细粒度访问控制(换句话说,可以授予特定团队在其自己的命名空间内创建ServiceMonitor
对象的能力)。这种临时配置设计使得集群租户能够控制自己项目的使用。
在对 Prometheus Operator 使用的 CRD 有了这样的总体了解之后,我们将更详细地探讨操作员如何从技术角度对这些对象进行对账。
对账逻辑
要更好地理解所有 Prometheus 操作符 CRD 的作用,了解操作符如何配置 Prometheus 操作数很有帮助。在幕后,Prometheus 操作符通过一个 secret(即 Kubernetes 对象,用于包含任意敏感数据)管理 Prometheus Pod 的配置。该 Secret 被挂载到 Prometheus Pod 中,就好像它是一个文件,并且通过 --config.file
标志传递给 Prometheus 二进制文件。
操作符知道更新此 secret(并重新部署 Prometheus 操作数,在此过程中重新加载配置文件),因为它监视集群中的 Prometheus
CRD(以及其他 CRD,如 ServiceMonitor
和 PodMonitor
)进行更改。
使用 Prometheus 重新加载配置更改
在技术上,Prometheus 操作符在不需要重新部署整个操作数的情况下在更改时重新加载 Prometheus 配置。它通过一个 sidecar 容器实现,运行在 Prometheus 服务器上的 /-/reload
端点。虽然 Prometheus 支持通过这种方式进行运行时配置重新加载,但许多应用程序不支持。因此,为了演示目的,本章节忽略了这个技术细节,重点是操作符的功能和更常见的用例。
一旦操作符被授予适当的 RBAC 权限,它就能够监视这些 CRD 对象。这是因为即使它在自己的项目中定义这些对象,Kubernetes 认证服务也不知道这一点。对于集群而言,操作符只是另一个运行任意应用程序的 Pod,因此它需要权限来列出、监视、获取或执行对任何类型的 API 对象的任何其他操作。
在 nginx 操作符示例中,访问我们操作符的 CRD 对象的 RBAC 规则是使用 Kubebuilder 标记自动生成的。相反,Prometheus 操作符为其用户提供了包含适当权限定义的示例 YAML 文件。
Prometheus 操作符为其支持的三种不同操作数(即 Prometheus、Alertmanager 和 Thanos)创建了三个单独的控制器。通过 Operator SDK,可以通过对每个需要自己调节逻辑的 CRD 运行 operator-sdk create api --controller
来实现相同的设计。
每个控制器都会监视相关对象的添加、更新和删除,以进行调解。对于 Prometheus 控制器,这些包括 Prometheus
、ServiceMonitor
和 PodMonitor
CRD。但是它还会监视诸如 Secrets 和 StatefulSets 的更改,因为正如前文所述,这些对象用于部署 Prometheus 操作数。因此,通过监视这些对象,它可以确保操作数对象本身保持在适当的设置,并且可以从任何偏离(例如,意外手动更改当前保存 Prometheus 配置 YAML 的 Secret)中恢复。
主要的控制器逻辑是通过名为sync()
的函数实现的,这等同于操作员 SDK 自动创建的Reconcile()
函数。sync()
函数遵循与我们示例 nginx 操作员相同的一般结构。接下来将详细介绍 Prometheus sync()
函数中的一些相关代码片段。
首先,函数获取Prometheus
CRD 对象,这是 Prometheus Operand 部署存在所必需的。如果找不到该对象,控制器将返回错误。如果找到,则创建一个副本进行处理:
func (c *Operator) sync(ctx context.Context, key string) error {
pobj, err := c.promInfs.Get(key)
if apierrors.IsNotFound(err) {
c.metrics.ForgetObject(key)
return nil
}
if err != nil {
return err
}
p := pobj.(*monitoringv1.Prometheus)
p = p.DeepCopy()
接下来,它解析Prometheus
对象(并收集任何相关的ServiceMonitor
对象或PodMonitor
对象进行解析)以生成 YAML 配置 Secret。这是在一个帮助函数中完成的,该函数还会检查是否已有 Secret,并在没有时创建一个:
if err := c.createOrUpdateConfigurationSecret(…); err != nil {
return errors.Wrap(err, "creating config failed")
}
最后,它创建了 Prometheus StatefulSet,该 StatefulSet 运行 Operand 部署。与生成配置 Secret 类似,这一部分也使用帮助函数来检查是否存在现有的 StatefulSet,然后决定是更新它还是创建一个新的 StatefulSet:
ssetClient := c.kclient.AppsV1().StatefulSets(p.Namespace)
…
obj, err := c.ssetInfs.Get(…)
exists := !apierrors.IsNotFound(err)
if err != nil && !apierrors.IsNotFound(err) {
return errors.Wrap(err, "retrieving statefulset failed")
}
…
sset, err := makeStatefulSet(ssetName)
if err != nil {
return errors.Wrap(err, "making statefulset failed")
}
…
if !exists {
level.Debug(logger).Log("msg", "no current statefulset found")
level.Debug(logger).Log("msg", "creating statefulset")
if _, err := ssetClient.Create(ctx, sset, metav1.CreateOptions{}); err != nil {
return errors.Wrap(err, "creating statefulset failed")
}
return nil
}
…
level.Debug(logger).Log("msg", "updating current statefulset")
err = k8sutil.UpdateStatefulSet(ctx, ssetClient, sset)
这相当于示例中的 nginx 操作员创建 Kubernetes 部署对象的方式。然而,Prometheus 操作员并不像我们最终使用的文件嵌入库那样,而是将 StatefulSet 对象构建在内存中。简单来说,这对于这个应用是合理的,因为 StatefulSet 的定义中有很多内容依赖于由代码中的逻辑设置的可变选项。因此,维持一个嵌入的文件来表示这个对象并没有太多优势。
在整个调和循环中,操作员广泛使用结构化日志和度量指标来向用户报告其健康状况。尽管它不像我们的 Nginx 操作员那样报告任何Condition
更新,但它确实报告了在Prometheus
CRD 的PrometheusStatus
字段中自定义定义的其他字段:
pkg/apis/monitoring/v1/types.go:
type PrometheusStatus struct {
Paused bool `json:"paused"`
Replicas int32 `json:"replicas"`
UpdatedReplicas int32 `json:"updatedReplicas"`
AvailableReplicas int32 `json:"availableReplicas"`
UnavailableReplicas int32 `json:"unavailableReplicas"`
}
这是一个很好的示例,证明了操作员 CRD 可以提供特定于应用的健康信息,而不仅仅依赖现有模式和上游 API 类型来传递详细的状态报告。结合多个Prometheus
CRD 对象可以被创建,每个对象代表一个新的 Prometheus 部署,单个 Operand 部署的状态信息得以分离。
这只是 Prometheus 操作员调和逻辑的一个高级概述,许多具体的实现细节被省略,以便对比本书中关于操作员设计的相关概念。
操作员分发和开发
The Prometheus Operator 托管在 GitHub 上,网址为 github.com/prometheus-operator/prometheus-operator
,其大部分文档也在此处维护。它也通过 OperatorHub 分发,网址为 operatorhub.io/operator/prometheus
。
图 10.1 – OperatorHub.io 上的 Prometheus Operator 页面
如第六章《构建与部署您的 Operator》一节中讨论的那样,运行 Operator 有许多不同的方式。从本地构建到容器部署,每种方式都为不同的开发用例提供了优势。然后,在第七章《使用 Operator 生命周期管理器安装和运行 Operators》中,解释了 OperatorHub 的功能,作为分发索引和与Operator 生命周期管理器(OLM)结合使用时的安装方法。
实际上,Prometheus Operator 通过不同的分发和安装选项体现了这一范围。在其 GitHub 仓库中,Prometheus Operator 的维护者提供了一个单独的 bundle.yaml
文件,允许好奇的用户通过简单的 kubectl create
命令快速安装运行 Operator 所需的所有资源。
请注意,虽然这与为 OperatorHub 和 OLM 打包 Operator 时创建的捆绑包功能相似,但从技术上讲,它并不完全相同。因为它不包含ClusterServiceVersion(CSV)或其他 OLM 可用于管理 Prometheus Operator 安装的元数据。
然而,Prometheus Operator 确实在 OperatorHub 上提供了这些信息。相关的 CSV 文件以及 Operator 的 CRD 文件托管在其 GitHub 仓库中,网址为 github.com/k8s-operatorhub/community-operators/tree/main/operators/prometheus
。此目录遵循第七章《使用 Operator 生命周期管理器安装和运行 Operators》中描述的相同结构。每个 Prometheus Operator 捆绑包的新版本都保存在其自己编号的目录中。
图 10.2 – OperatorHub 上的 Prometheus Operator 版本目录
各个版本包含每个 Operator 使用的 CRD 的 YAML 定义,以及提供 Operator 及其资源元数据的 CSV 文件。
为了展示此 CSV 的使用场景,我们可以简要查看一些相关部分,如以下代码所示。首先,它描述了 Operator 的基本信息,包括其能力级别(在此情况下,Prometheus Operator 是一个 IV 级别的 Operator,提供如其自身及 Operand 的指标等深度洞察):
prometheusoperator.0.47.0.clusterserviceversion.yaml:
apiVersion: operators.coreos.com/v1alpha1
kind: ClusterServiceVersion
metadata:
annotations:
capabilities: Deep Insights
categories: Monitoring
certified: "false"
containerImage: quay.io/prometheus -operator/prometheus -operator:v0.47.0
createdAt: "2021-04-15T23:43:00Z"
description: Manage the full lifecycle of configuring and managing Prometheus and Alertmanager servers.
Repository: https://github.com/prometheus -operator/prometheus -operator
support: Red Hat, Inc.
name: prometheusoperator.0.47.0
namespace: placeholder
接下来,它嵌入了各种 CRDs 及其字段描述。以下是 Prometheus
CRD 描述的摘录:
spec:
customresourcedefinitions:
owned:
- description: A running Prometheus instance
displayName: Prometheus
kind: Prometheus
name: prometheuses.monitoring.coreos.com
resources:
- kind: StatefulSet
version: v1beta2
- kind: Pod
version: v1
- kind: ConfigMap
version: v1
- kind: Service
version: v1
specDescriptors:
- description: Desired number of Pods for the cluster
displayName: Size
path: replicas
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podCount
CSV 接下来定义了 Operator 的部署。这直接映射到 Kubernetes 的 Deployment 对象,它将运行 Operator Pods:
install:
spec:
deployments:
- name: prometheus -operator
spec:
replicas: 1
selector:
matchLabels:
k8s-app: prometheus -operator
template:
metadata:
labels:
k8s-app: prometheus -operator
spec:
containers:
- args:
- --prometheus-instance-namespaces=$(NAMESPACES)
- --prometheus-config-reloader=quay.io/prometheus -operator/prometheus -config-reloader:v0.47.0
最后,CSV 提供了 Operator 监控集群中相关资源所需的 RBAC 权限。此外,它还创建了实际 Prometheus Pods 所需的 RBAC 权限,这些权限与 Operator 所需的权限不同。这是因为 Operator 和其 Operand 在集群中是独立的实体,而 Prometheus 服务器本身需要访问不同的资源以收集指标(这与 Prometheus Operator 需要访问其 CRDs 不同)。
以下是用于访问其 CRDs 的 RBAC 权限,verbs
下的通配符(*
)访问表示 Operator 可以对这些对象执行任何 API 操作(如 get
、create
、delete
等):
permissions:
- rules:
- apiGroups:
- monitoring.coreos.com
resources:
- alertmanagers
- alertmanagers/finalizers
- alertmanagerconfigs
- prometheuses
- prometheuses/finalizers
- thanosrulers
- thanosrulers/finalizers
- servicemonitors
- podmonitors
- probes
- prometheusrules
verbs:
- '*'
serviceAccountName: prometheus -operator
CSV 最后包含了维护者的联系信息,以及指向文档的链接和此版本的版本号。
提供多样的发行渠道,在这种情况下,GitHub 和 OperatorHub,有明显的好处——使得 Operator 能够触及更广泛的用户群体。但这些用户群体往往不完全由 Operator 的分发位置定义,更多是由 Operator 预期的使用方式决定。换句话说,从 OperatorHub 安装的用户更有可能在生产环境中评估(或实际运行)该 Operator(使用完整的 OLM 栈),而不是从 GitHub 安装 Operator 的用户。在后者的情况下,这些安装可能更多是实验性的,可能来自希望为项目做出贡献的用户。
在你的发行版选择中适配不同的 Operator 使用场景,不仅有助于项目的增长,还有助于其健康发展。回想一下在第二章中,理解 Operator 如何与 Kubernetes 交互,我们识别了几种潜在的用户类型,如集群用户和管理员。虽然从理论上讲,Operator 的功能可能只适用于一种类型的用户,但该 Operator 的安装和运行方式对于不同的用户类型(包括开发人员)可能有所不同。了解这些用户并为他们提供使用路径,可以提高 Operator 功能的覆盖面,增加识别 bug 和潜在功能的机会。
与本书中的许多其他话题一样,这些概念并不特定于 Operator 设计。但在讨论 Operator 时,值得注意的是,这些概念是如何在这里应用的。同样,尽管维护软件和提供更新的话题并不严格属于 Operators 的专属,但在接下来的章节中,我们仍会从这个 Operator 的角度来审视它们。
更新与维护
Prometheus Operator 的维护者社区非常活跃。到目前为止,已经有超过 400 名贡献者参与其中(github.com/prometheus-operator/prometheus-operator/graphs/contributors
),其代码库通过持续的维护保持着活力。这使得 Prometheus Operator 能够在其 GitHub Releases 页面上定期发布版本(github.com/prometheus-operator/prometheus-operator/releases
)。与任何应用程序一样,定期发布更新能够通过展示项目拥有者对项目支持的积极投入,来增强潜在用户的信任。在 Kubernetes 项目中,像 Operators 这样的项目尤为重要,因为底层 Kubernetes 平台的发展变化较快且波动性大。否则,根据当前 Kubernetes 的弃用政策,Operator 可能在不到一年的时间内就会在新集群中变得不可用(见 第八章,为你的 Operator 做好持续维护准备)。
实际上,在大多数情况下,Operator 项目所使用的主要依赖项不会频繁地引入需要手动更新以保持与现有用户兼容的破坏性更改。相反,大多数更新只是版本号的提升,带来了安全性、性能以及边际情况优化的改进。为了自动化这一过程,Prometheus Operator 使用 GitHub 的 Dependabot,它会自动创建拉取请求以更新任何带有新版本的依赖项(docs.github.com/en/code-security/dependabot
)。
像 Dependabot 这样的自动化依赖管理工具是确保你的 Operator 与其依赖项保持最新的一种有效方式,从而保证与用户所做的最新环境更新兼容。然而,根据你的实际情况,你可能仍然选择手动更新你的 Operator(例如,如果你对接了一个不同的发布节奏,上游的修补发布对你自己的发布并不重要)。
除了依赖更新,大多数 Operator 还会发布自己的更新;例如,发布新的 API 版本(如第八章中的发布新版本的操作员部分所涵盖的内容,为持续维护做准备)。以 Prometheus Operator 为例,从 API 版本v1alpha1
到v1
的过渡也涉及从 Kubernetes AlertManager
CRD 从v1alpha1
到v1beta1
的迁移,利用转换 webhook 在两者之间进行转换(该提案已在github.com/prometheus-operator/prometheus-operator/issues/4677
中追踪并记录)。
最后,Prometheus Operator 维护自己的 Slack 频道以供社区支持和讨论。因为该 Operator 是一个与 Prometheus 本身没有直接关联的第三方开源项目,公开宣传合适的支持渠道不仅有助于用户找到正确的联系方式,也尊重了 Prometheus 维护者的责任范围。通过这种方式,发布一个管理你不拥有的 Operand 的 Operator 是完全可以接受的。然而,如果没有明确说明这一点,模糊操作员和 Operand 之间的区别可能会对用户和该 Operand 的所有者造成干扰。
摘要
在本章中,我们以 Prometheus Operator 为例,应用了本书中涉及的许多概念。这个 Operator 是一个很好的例子,因为除了通过管理一个流行的应用程序来满足常见需求外,它实际上是最早的 Operator 之一(其第一个版本 v0.1.1 发布于 2016 年 12 月)。这比正式化的 Operator Framework 还要早,开发者今天可以从中受益,解释了它缺少 Operator SDK 库等特性,但也展示了早期开发决策在 Operator Framework 设计中的影响。
在本章开始时,我们简要概述了 Prometheus 本身。这为我们提供了关于 Prometheus Operator 使用案例的基础理解,特别是在 Prometheus 的安装和配置方面。这为理解 Prometheus Operator 如何缓解这些痛点奠定了基础。通过检查它使用的 CRD 以及它们是如何被调和的,我们展示了 Prometheus Operator 如何将底层功能从用户中抽象出来,并与书中早些章节(以及用于构建那些章节中的示例的简单 Nginx 操作员)作对比。最后,我们探讨了 Prometheus Operator 的更无形的方面,比如它的分发和维护,展示了流行的操作员如何应用 Operator Framework 中的这些概念。
在下一章中,我们将跟随一个类似的案例研究,但针对的是不同的操作员,即 etcd 操作员。
第十一章:第十一章:核心 Operator 案例研究 – Etcd Operator
在上一章(以及本书的大部分内容)中,我们讨论了 Operator 作为管理部署在 Kubernetes 上的应用程序的工具。对于大多数用例来说,这是 Operator 的主要功能。换句话说,Operator 的作用是自动化由组织开发的应用程序。这些应用程序是提供给用户的产品,自动化它们有助于顺利交付并保持用户的满意。除此之外,Kubernetes 本身只是基础架构的一部分。作为这一部分,通常假设 Kubernetes 无需像 Operator 所提供的额外自动化功能。毕竟,本书前几章的一个关键点就是,Operator 在功能上与构成 Kubernetes 控制平面的本地控制器套件并没有太大区别。
然而,在某些情况下,可以使用 Operator 来管理 Kubernetes 核心的某些方面。尽管这种情况比应用程序 Operator 少见,但讨论一些核心 Operator 的实例有助于展示 Operator 框架所提供的广泛能力。从这些例子入手,我们将重点探讨一个(尽管不如前面的案例深入),即 etcd Operator。最后,我们将解释一些与集群稳定性和升级相关的概念,这些概念在为 Kubernetes 开发 Operator 时非常重要。以下几个章节将帮助我们完成这一过程:
-
核心 Operator – 扩展 Kubernetes 平台
-
etcd Operator 设计
-
稳定性与安全性
-
升级 Kubernetes
核心 Operator – 扩展 Kubernetes 平台
在 Operator 框架中,没有区别对待管理面向用户的应用程序和基础设施的 Operator 与管理核心 Kubernetes 组件的 Operator。唯一的区别仅在于 Operator 设计和开发的概念如何应用到稍有不同的类问题上。不过,组成 Kubernetes 安装的各种 Pod 和控制循环可以被视为与它们部署和管理的工作负载 Pod 没有区别。
不谈太多存在主义,这种简化桥接了Kubernetes 开发和Kubernetes 的开发之间的概念差距,使得后者看起来更加容易接触。这一理念为系统管理员和 DevOps 专家提供了更大的控制和灵活性,使他们能够更加有效地管理他们所编排的云架构。
接下来,我们将查看一些扩展 Kubernetes 的高层次 Operator 实例。我们不会深入技术细节(如其 API 或调和逻辑),但我们将简要地了解这些例子,以理解它们的使用场景,并展示一些不同方式,说明 Operator 如何直接管理 Kubernetes 系统进程。我们将查看的 Operator 如下:
-
RBAC 管理器 (
github.com/FairwindsOps/rbac-manager
) -
Kube 调度器操作员 (
github.com/openshift/cluster-kube-scheduler-operator
) -
etcd 操作员 (
github.com/coreos/etcd-operator
)
在概述之后,我们将深入探讨 etcd 操作员的技术细节,以便提供与本书中第十章,“可选操作员案例研究 - Prometheus 操作员”相似的设计理念理解。
RBAC 管理器
基于角色的访问控制(RBAC)策略是 Kubernetes 身份验证和授权的基石。Kubernetes 中的 RBAC 设置由三种类型的对象组成:
-
Roles(或ClusterRoles,视作用域而定),定义了用户或服务允许访问的权限级别
-
ServiceAccounts,是 Pod 的身份认证授权对象
-
RoleBindings(或ClusterRoleBindings),将 ServiceAccounts 映射到 Roles(或 ClusterRoles)
这三种类型的 Kubernetes API 对象在第二章,“理解操作员如何与 Kubernetes 交互”中已有解释。它们之间的关系可以通过下图进行概述(该图在该章节中也有使用):
图 11.1 – RBAC 对象关系的示意图
这些对象为集群中的 RBAC 策略设计提供了灵活性和控制力。然而,它们可能会变得令人困惑,并且在管理时变得繁琐,尤其是在大规模集群中,涉及不同服务的不同访问权限时。例如,如果用户在不同命名空间中需要不同的授权权限,管理员需要为每个命名空间创建独立的 RoleBindings,并进行适当的授权。然后,如果该用户离职或更换职位,管理员就需要跟踪这些 RoleBindings,确保它们能够得到相应更新。虽然这种方法灵活,但对于大组织而言,它的扩展性较差。
RBAC 管理器通过在原生 Kubernetes RBAC 策略对象之上提供一层抽象,解决了这些问题。该抽象由一个CustomResourceDefinition (CRD)表示,允许管理员在一个位置有效地创建和管理一个用户的多个 RoleBindings(使用略微简化的语法)。
RBAC 管理器简化的授权方法的效果是,将 RoleBindings 的管理从集群管理员的手动职责中移除。这可能只是前面描述的关系型 RBAC 对象链中的一个对象,但它是最具重复性和细致性的,在大规模集群中难以追踪。这是因为其他对象,如 Roles/ClusterRoles 和 ServiceAccounts,基本上会与用户、服务和访问级别一一对应。但用户与访问级别的交集意味着这可能是一个多对多的关系,通过 RoleBindings 维持。
即使是一个简单的设置,其潜在复杂性也通过以下图示表现出来,其中四个用户拥有不同级别的访问权限(假设有 读取、写入、查看 和 编辑 等角色)。在该图中,每个箭头代表一个必须手动维护的 RoleBinding:
图 11.2 – 不同用户与 RBAC 级别的基本 RoleBinding 映射
RBAC 管理器通过在用户和角色定义之间插入其 CRD,简化了相同的设置,创建了一个单一的访问点来管理任何用户的权限。此外,UserB 和 UserC 可以共享一个 RBAC 管理器 CRD,因为他们拥有相同的角色。这在以下图示中展示:
图 11.3 – 一个 RBAC 管理器 CRD 管理 RoleBindings 的示意图
在这个设置中,CRD 和角色之间的每个单独箭头(每个仍代表一个 RoleBinding)由 RBAC 管理员操作器管理。这有一个优点,即减少了管理员需要协调的单个对象关系的数量。它还提供了操作器的状态对账功能,其中任何基础角色的更新或删除都会被操作器对账,以匹配操作器的 CRD 对象中声明的集群期望状态。这种行为是操作器不仅帮助创建和管理复杂系统,而且确保其持续稳定性的一个很好例子。
RBAC 管理器是一个操作器,它的唯一功能是在集群中管理原生 Kubernetes 对象。接下来,我们将讨论 Kube 调度器操作器,它进一步管理集群中的关键组件——调度器。
Kube 调度器操作器
Kube Scheduler 是 Kubernetes 集群中的主要控制平面组件之一。它负责将新创建的 Pods 分配到节点上,并尽力以最优的方式进行分配。这个任务对 Kubernetes 作为云平台的功能至关重要,因为如果无法将 Pods 安排到节点上,那么 Pods 就无法在任何地方运行其应用程序代码。尽管可以手动将 Pods 部署到特定节点,但调度器进行的自动化评估和分配显然更具可扩展性。
此外,最优的 Pod 安排对于不同的组织(有时甚至是同一组织的不同集群)来说,定义可能截然不同。例如,一些管理员可能希望将工作负载 Pods 均匀分布在节点之间,以保持平均资源消耗相对较低,并防止某些节点过载。但其他系统管理员可能希望采取完全相反的做法,将尽可能多的 Pods 压缩到尽可能少的节点上,以最小化基础设施成本并最大化效率。为了满足这些不同的需求,调度器提供了一个配置 API,允许用户自定义其行为。
调度器所提供的功能性和灵活性非常有用,但操作集群中如此重要的部分可能存在风险。这是因为如果调度器失败,那么其他 Pods 将无法被调度(这包括一些系统 Pods)。此外,调度器复杂的配置语法也增加了这一风险的可能性。因此,许多 Kubernetes 用户避免对调度器进行自定义。
为了解决这些问题,OpenShift(Red Hat 的 Kubernetes 发行版)提供了 Kube Scheduler Operator(事实上,OpenShift 在很大程度上依赖于核心的 Operator,这一点在www.redhat.com/en/blog/why-operators-are-essential-kubernetes
中有更详细的讨论)。该 Operator 使用专门为 OpenShift Operators 开发的 Operator 库构建,而非 Operator SDK。这使得 Kube Scheduler Operator 能够以与 OpenShift 内置其他功能一致的方式管理关键调度器 Pods 的健康和稳定性。虽然大多数 Operator 开发者不需要编写自己的开发库,但这个例子表明,在某些特定用例中,如果你有 Operator SDK 不支持的独特需求,完全可以这样做。
Kube Scheduler Operator 遵循了 Operator 框架的其他设计方面,例如使用 CRD 作为用户与 Operator 逻辑之间的主要接口。这个 Operator 使用了两个 CRD。一个用于配置 Operator 特定的设置,并通过 Conditions 报告 Operator 的健康状态,而另一个则保存调度器配置选项,用于控制调度器如何将 Pods 分配到节点。通过第二个 CRD,该 Operator 进一步预定义了常见用例的调度器配置集,将底层 Operand 设置完全抽象为易于理解的选项供用户选择。
Kube Scheduler Operator 作为系统 Operator,管理 Kubernetes 集群中的核心组件,是一项重要任务。它的功能在于将 Pods 放置到合适的节点上,并且它从故障中恢复的能力有助于维持集群健康。在接下来的章节中,我们将看一下另一个执行类似管理任务的 Operator——etcd。
etcd Operator
etcd (etcd.io/
) 是 Kubernetes 集群背后的主要键值存储。它是持久化存储集群中 API 对象的默认选项,因其可扩展的分布式设计而广受偏爱,适用于高性能云计算。
etcd Operator 旨在管理集群中的 etcd 组件。尽管它不再积极维护,但其 GitHub 存储库仍以存档状态存在,为未来的开发者提供历史参考。在本章中,etcd Operator 的保存状态为核心 Operator 的设计提供了一个永久且不变的参考。
Kubernetes 集群中的 etcd 集群可以通过 etcd Operator 进行各种功能的管理。这些功能包括创建 etcd 实例、调整 etcd 的联邦安装大小、从故障中恢复、在不中断运行时间的情况下升级 etcd,以及执行 etcd 实例的备份(以及从这些备份中恢复)。这一系列功能使 etcd Operator 在能力模型中被归类为三级 Operator。如果你还记得第一章,“引入 Operator 框架”,三级 Operator 被称为全生命周期 Operator,表示它们能够管理超出简单安装的操作,并支持高级管理操作,如升级和备份。
在 Kubernetes 集群中手动安装和管理 etcd 对于大多数用户来说是一个相当高级的任务。大多数 Kubernetes 开发者理所当然地认为持久数据存储是可用的,假设他们的所有对象和集群状态信息始终可用。但如果 etcd 进程失败,可能会对整个 Kubernetes 集群产生灾难性的影响。
类似于任何其他数据库,集群中的 etcd 组件负责存储集群中所有存在的对象。未能这样做可能会导致甚至是基本的集群功能停止。这样的故障可能是由于 bug、不兼容的 API,或者甚至在尝试修改 etcd 安装时(例如,扩展以提供更高的可用性)输入格式错误所导致的。因此,集群的顺利运行依赖于对 etcd 中数据的高效和准确访问。
etcd Operator 旨在通过自动化创建、调整大小、升级、备份和恢复 etcd 集群所需的操作命令,简化 etcd 的管理,使用 Operator 的各种 CRD 来完成这些操作。在接下来的部分,我们将更详细地探讨 Operator 使用的 CRD,以及这些 CRD 是如何被协调以确保集群中 etcd 的当前状态与管理员的期望状态一致的。
etcd Operator 设计
与大多数其他 Operator 一样,etcd Operator 是以 CRD 作为其用户交互的核心接口构建的。了解 Operator 提供的 CRD 是了解 Operator 工作原理的一个好方法,因此我们将从检查 etcd Operator 的 CRD 开始。
CRDs
etcd Operator 使用的三个 CRD 是EtcdCluster
、EtcdBackup
和EtcdRestore
。第一个 CRD,EtcdCluster
,控制 etcd 安装的基本设置,例如要部署的操作数副本数量和应安装的 etcd 版本。基于这个 CRD 的一个示例对象如下所示:
simple-etcd-cr.yaml:
apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdCluster
metadata:
name: example
spec:
size: 3
version: 3.5.3
在这个示例中,如果在集群中创建这个对象(kubectl create -f simple-etcd-cr.yaml
),它将指示 etcd Operator 创建三个 etcd 版本 3.5.3 的副本。除了这些选项,EtcdCluster
CRD 还提供了配置设置,用于指定从特定的仓库拉取 etcd 容器镜像、操作数 Pod 设置(例如affinity
和nodeSelector
设置),以及 TLS 配置。
前面提到的另外两个 CRD,EtcdBackup
和EtcdRestore
,协同工作,允许用户声明性地触发 etcd 数据在集群中的备份和随后的恢复。例如,可以通过创建以下自定义资源对象将 etcd 备份到Google Cloud Storage(GCS)存储桶:
etcd-gcs-backup.yaml:
apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdBackup
metadata:
name: backup-etcd-to-gcs
spec:
etcdEndpoints:
- https://etcd-cluster-client:2379
storageType: GCS
gcs:
path: gcsbucket/etcd
gcpSecret: <gcp-secret>
这指示 Operator 备份集群端点etcd-cluster-client:2379
上的 etcd 数据,并将其发送到名为gcsbucket/etcd
的 GCS 存储桶,使用<gcp-secret>
进行身份验证。稍后可以通过创建以下EtcdRestore
对象来恢复该数据:
etcd-gcs-restore.yaml:
apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdRestore
metadata:
name: restore-etcd-from-gcs
spec:
etcdCluster:
name: sample-etcd-cluster
backupStorageType: GCS
gcs:
path: gcsbucket/etcd
gcpSecret: <gcp-secret>
这些 CRD 使得备份 etcd 数据和恢复备份变得更加容易,通过将繁重的工作抽象并自动化到 Operator 控制器逻辑中,但它们也确保这些操作能够成功执行。通过消除备份过程中对人工干预的需求,它们也消除了在收集和传输 etcd 数据到备份位置时出现人为错误的可能性。如果 Operator 在执行备份时遇到错误,这些信息会通过该备份的自定义资源对象的 Status
部分报告出来。
协调逻辑
在 etcd Operator 的设计中,每个 CRD 类型都有自己的协调控制器。这是一个良好的 Operator 设计,符合 Operator Framework 文档中的最佳实践。类似于 第十章 中的 Prometheus Operator,可选操作员案例研究–Prometheus Operator,每个控制器监控涉及其协调的 CRD 的集群事件。这些事件随后触发一个基于级别的协调循环,该循环会创建(或修改)etcd 集群、执行正在运行的 etcd 集群的备份,或从先前的 etcd 集群恢复已备份的数据。
这部分协调逻辑涉及报告 Operand etcd Pods 的状态。在接收到事件并在随后的协调周期中,如果 Operator 检测到 etcd 集群状态发生变化,它会通过匹配的自定义资源对象的 Status
字段报告这一变化。该类型具有以下子字段:
type ClusterStatus struct {
// Phase is the cluster running phase
Phase ClusterPhase `json:"phase"`
Reason string `json:"reason,omitempty"`
// ControlPaused indicates the operator pauses the control of the cluster.
ControlPaused bool `json:"controlPaused,omitempty"`
// Condition keeps track of all cluster conditions, if they exist.
Conditions []ClusterCondition `json:"conditions,omitempty"`
// Size is the current size of the cluster
Size int `json:"size"`
// ServiceName is the LB service for accessing etcd nodes.
ServiceName string `json:"serviceName,omitempty"`
// ClientPort is the port for etcd client to access.
// It's the same on client LB service and etcd nodes.
ClientPort int `json:"clientPort,omitempty"`
// Members are the etcd members in the cluster
Members MembersStatus `json:"members"`
// CurrentVersion is the current cluster version
CurrentVersion string `json:"currentVersion"`
// TargetVersion is the version the cluster upgrading to.
// If the cluster is not upgrading, TargetVersion is empty.
TargetVersion string `json:"targetVersion"`
}
请注意,etcd Operator 实现了自己的 ClusterCondition
类型(而不是在第五章中使用的 Condition
类型,开发 Operator – 高级功能)。这是因为 etch Operator 的活跃维护在本地 Condition
类型合并到上游 Kubernetes 之前不久被归档。不过,我们在这里提到这一点,是为了举例说明在发布周期中,了解上游 Kubernetes 增强提案(KEP)及其状态对第三方 Operator 开发的影响(参见 第八章,为您的 Operator 的持续维护做准备)。
除了协调 etcd 操作数的期望状态并报告其状态之外,etcd Operator 还具有从故障状态恢复的协调逻辑。
故障恢复
etcd Operator 收集 etcd Operands 的状态信息,并根据大多数 Operand Pods 的可用性(称为法定人数),在必要时尝试恢复 etcd 集群。在极端情况下,如果一个 etcd 集群没有运行中的 Operand Pods,Operator 必须做出有根据的决定,判断这是一个完全失败的 etcd 集群,还是一个尚未初始化的集群。在这种情况下,Operator 总是选择将其视为失败的集群,并尝试从备份中恢复该集群(如果有备份可用)。这不仅是 Operator 最简单的选择,也是最安全的选择(参见下面的稳定性和安全性部分),因为另一种选择(假设没有可用性只是未初始化)可能会导致忽视故障状态,而这些状态本应被恢复。
除了从其 Operands 的故障中恢复外,etcd Operator 还具有自我恢复的协调逻辑。其恢复路径的一部分取决于 etcd Operator 在集群中注册自己的 CRD。这一点很有趣,因为它与 Operator SDK 文档中推荐的最佳实践相矛盾。但通过将 CRD 的创建与 Operator 绑定(而不是让管理员来创建它——例如使用 kubectl create -f
),Operator 可以通过该 CRD 的存在来确定自己是作为新安装运行,还是曾经在集群中运行过。
这适用的原因是,当任何 Pod 重新启动时,包括 Operator Pods,它们对其前一个实例没有任何固有的知识。从该 Pod 的角度来看,它是以全新的状态开始运行的。所以,如果 etcd Operator 启动并发现其 CRD 已经在 Kubernetes 集群中注册,那么该 CRD 就充当了一个金丝雀指示器,告知 Operator 应该开始重建任何现有 Operand Pods 的状态。
故障恢复是 Operator 提供的最有用的功能之一,因为自动化的错误处理使得集群日常操作中的小故障能够快速且优雅地被吸收。在 etcd Operator 的情况下,它在处理故障和管理自身 CRD 时做出有根据的决定,形成一个清晰定义其恢复程序的支持契约。这样做大大有助于集群的整体稳定性,而这正是下一部分的重点。
稳定性和安全性
Kubernetes 集群中的应用程序和组件偶尔会面临意外故障,例如网络超时或由于不可预见的错误导致的代码崩溃。Operator 的工作之一就是监控这些自发的故障,并尝试从中恢复。但当然,调整系统时的人工错误也是故障的另一个来源。因此,任何与 Kubernetes 核心系统组件的交互或修改都带有固有的风险。这种风险会加剧,因为对一个组件的手动调整可能包含错误(即使是微小的错误),而这些错误会引发多米诺效应,导致依赖它的其他组件开始对原始错误作出反应。
Operator 的主要目标可能是为生产环境提供稳定性和安全性。在这里,稳定性指的是 Operand 程序的持续高效运行,而安全性则是指 Operator 能够清理和验证对该程序的任何输入或修改。可以将 Operator 想象成一辆车,其目的是让其引擎在道路上平稳运行,同时允许驾驶员在合理的参数内控制它。在 Kubernetes 中,针对系统组件的 Operator 提供了一些优秀的设计示例,在这些设计中,特别注重提供一种安全的设计,以确保系统稳定运行。
我们之前提到的 Kube Scheduler Operator 通过对其提供的可用配置选项采取一种明确的方式来确保集群的稳定性。换句话说,可能的调度设置总数是有限制的(通过预定义的选项集合),仅限于那些已被测试并且已知对集群风险最小的安排。然后,这些设置的更改会由 Operator 中的自动化代码以预定的方式推送。通过不仅自动化更改,还限制可用更改,这大大减少了用户在更新集群时出错的机会。以下图示展示了这种抽象,在这个图示中,用户只需要与 CRD 进行交互,而预定义设置中的所有字段则作为黑盒选项被隐藏起来:
图 11.4 – Kube Scheduler Operator 配置抽象图示
etcd Operator 也有类似的安全措施。例如,卸载 Operator 并不会自动删除与其相关的自定义资源对象。虽然有些 Operator 可能会将这种垃圾回收作为一种功能来简化卸载过程,但 etcd Operator 的开发人员有意选择不这么做,以防止意外删除正在运行的 etcd 集群。
etcd Operator 为实现操作稳定性采取的另一种方法是将其 Operand Pods 分布开来。如前所述,etcd Operator 允许用户为其部署的 etcd Operand 实例配置单独的 Pod 设置,如 nodeSelectors
。然而,有一个有趣的字段值得注意,那就是它提供了一个简单的布尔选项,称为 antiAffinity
。这个值可以在 EtcdCluster
CRD 中设置,如下所示:
spec:
size: 3
pod:
antiAffinity: true
启用这个值相当于用 antiAffinity
字段标记 Operand etcd Pods 自己。其效果是,单独的 Pods 不会调度到同一节点。这有助于确保 Pods 以高度可用的方式分布,从而确保如果一个节点发生故障,不会导致大量 etcd Pods 一同宕机。
这行对 Operand Pods 的有效更改大致如下所示:
apiVersion: v1
kind: Pod
metadata:
name: etcd-cluster-pod
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
etcd_cluster: <etcd-resource-name>
topologyKey: kubernetes.io/hostname
将这部分打包成一行不仅节省了时间,还能避免用户可能犯的错误。例如,以下代码片段看起来与之前的非常相似。然而,它具有完全相反的效果,要求所有相似的 etcd Pods 都调度到同一个节点:
apiVersion: v1
kind: Pod
metadata:
name: etcd-cluster-pod
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
etcd_cluster: <etcd-resource-name>
topologyKey: kubernetes.io/hostname
如果这个错误没有被发现,而托管所有 etcd Pods 的节点发生故障,可能会导致整个集群无法正常运行。
尽管最终antiAffinity
设置已被弃用,转而允许用户提供完整的 Pod Affinity 块(如前所示),但它的存在提供了一个 Operators 可以提供的安全配置示例。将其替换为完整 Affinity 块的选项,在安全性和灵活性之间做出了权衡,这是 Operator 开发者在自己实现中必须平衡的一个微妙尺度。
这些只是 Operators 将复杂操作打包成面向用户的抽象设置,以提供安全界面和稳定集群管理的几种方式的例子。接下来,我们将探讨 Operators 如何在可能动荡的升级期间保持这种稳定性。
升级 Kubernetes
在软件中,升级通常是不可避免的事实。很少有单个软件能够在不升级代码到新版本的情况下持续运行。在第八章中,我们讨论了如何为 Operator 准备和发布新版本。在那一章中,我们还解释了 Kubernetes 发布周期的不同阶段以及它们如何影响 Operator 开发。对于那些深度嵌入 Kubernetes 平台组件的 Operators,这一点尤为重要。
对于系统操作员来说,上游 Kubernetes 代码库的波动性变化往往不仅仅是简单的功能更新。例如,当 kube-scheduler
被重构以接受一种完全不同的配置格式(称为调度器框架,和 Operator 框架没有关系——更多详情请见kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/
——尽管在此并不技术相关)时,Kube Scheduler Operator 的 CRD 所处理的预定义设置配置文件需要完全重写以适应该变化。系统操作员绝对必须关注 Kubernetes 版本间这种变化,因为在许多情况下,他们自己就是执行升级到新版本的人。
etcd Operator 有处理其操作数(Operand)etcd 升级的逻辑。这增加了一些复杂性,因为从技术上讲,etcd 并不是运行集群的 Kubernetes 负载的一部分。因此,虽然上游的 Kubernetes 开发者必须协调他们的发布以支持特定版本的 etcd(因为 etcd 开发者在进行他们自己的发布工作),etcd Operator 的开发者则需要对这两者的依赖变化作出反应。
这样,操作数(Operand)也是操作员(Operator)的一个依赖,它带有必须遵守的自身约束。例如,在执行升级时,etcd Operator 首先验证所需的新版本是否被 etcd 支持的升级策略所允许。这表明,etcd 每次最多只能升级一个小版本(例如,从 3.4 升级到 3.5)。如果指定的版本超出了这个范围,etcd Operator 可以判断不应继续升级并终止该过程。
如果允许继续升级,etcd Operator 会使用滚动升级策略更新每一个操作数的 Pod。这是一种常见的应用版本升级方式,甚至 Kubernetes 本身也是如此,其中在可用的 Pod(或在 Kubernetes 的情况下是节点)中,一次只升级一个实例。这可以实现最小的停机时间,并且如果在升级某个实例时遇到错误,它还允许回滚(恢复到之前的工作版本)。这对任何操作员来说都是至关重要的稳定性优势,因为它提供了在稳定环境中诊断错误的机会。etcd Operator 使用其自身的备份和恢复工作流来处理这些类型的回滚操作。
升级 Kubernetes(与升级任何软件类似)可能是一个繁琐的过程,这也是为什么集群管理员通常会拖延升级,直到最后一刻。遗憾的是,这只会加剧问题,因为每跳过一个版本,就会引入更多的不兼容更改。但引入操作符来帮助处理这些更改,可以使升级过程更加顺利,或者至少帮助收集信息以诊断失败的升级。虽然大多数操作符开发者不会为系统级的 Kubernetes 组件编写操作符,但那些已经编写的操作符所带来的经验,将为如何处理即便是最关键任务场景下的升级提供借鉴。
总结
在本章中,我们最后一次回顾了负责 Kubernetes 集群中一些最关键任务的操作符示例。这些核心操作符(或系统操作符)自动管理 Kubernetes 管理员最复杂且最精细的工作流,同时平衡功能性与对操作对象的重要性的关注。通过这些类型的操作符,我们可以汲取关于操作符框架能力范围的教训。
本章的目的并不是将这些示例作为教程,或暗示许多开发者将编写自己的操作符来管理核心的 Kubernetes 组件。相反,它们作为极端案例,展示了操作符框架中的概念是如何应用到边缘问题集的。理解任何问题的边缘案例是形成对整个问题深刻理解的最佳途径。
我们通过从简要概述三个不同系统的操作符开始本章内容。接着,我们深入探讨了 etcd 操作符的技术细节,理解它如何利用 CRD 和协调逻辑来管理 Kubernetes 的数据备份存储。最后,我们通过探索如何像 etcd 操作符这样提供稳定和安全接口的操作符,结束了本章内容,即使在 Kubernetes 版本升级时也是如此。
至此,我们结束了对操作符框架的介绍。Kubernetes 操作符是一个广泛的主题,包含许多技术细节可供探索,且已有大量优秀文献由资深作者撰写。要简洁地整理出所有关于操作符的信息是非常困难的,因此在本书中,我们仅尝试以新颖且有趣的方式覆盖最重要的主题。希望这本书能为你提供深入的见解,并帮助你建立对操作符框架的理解,同时在构建自己的操作符时能应用这些经验。祝你编码愉快!
订阅我们的在线数字图书馆,您可以完全访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并推动职业生涯。如需更多信息,请访问我们的网站。
第十二章:为什么要订阅?
-
用来自超过 4,000 名行业专业人士的实用电子书和视频减少学习时间,更多时间用于编码
-
提升您的学习体验,使用专门为您打造的技能计划
-
每月获得一本免费的电子书或视频
-
完全可搜索,便于快速访问关键信息
-
复制、粘贴、打印和收藏内容
您知道 Packt 提供所有出版书籍的电子书版本,包括 PDF 和 ePub 格式文件吗?您可以在 packt.com 升级到电子书版本,作为纸质书籍客户,您有权获得电子书的折扣。更多详情,请通过 customercare@packtpub.com 与我们联系。
在 www.packt.com,您还可以阅读一系列免费的技术文章,注册各种免费的新闻通讯,并获得 Packt 图书和电子书的独家折扣和优惠。
其他您可能喜欢的书籍
如果您喜欢这本书,您可能对 Packt 出版的其他书籍感兴趣:
Kubernetes 圣经
Nassim Kebbani, Piotr Tylenda, Russ McKendrick
ISBN: 9781838827694
-
使用 Kubernetes 管理容器化应用程序
-
了解 Kubernetes 架构及各个组件的职责
-
在 Amazon Elastic Kubernetes Service、Google Kubernetes Engine 和 Microsoft Azure Kubernetes Service 上设置 Kubernetes
-
使用 Helm charts 部署如 Prometheus 和 Elasticsearch 等云应用程序
-
探索 Pod 调度和自动扩展集群的高级技术
-
了解 Kubernetes 中流量路由的可能方法
学习 DevOps - 第二版
Mikael Krief
ISBN: 9781801818964
-
了解基础设施即代码模式和实践的基础知识
-
了解 Git 命令和 Git 流程概述
-
安装并编写 Packer、Terraform 和 Ansible 代码,基于 Azure 示例进行云基础设施的配置和管理
-
使用 Vagrant 创建本地开发环境
-
使用 Docker 和 Kubernetes 容器化应用程序
-
采用 DevSecOps 进行合规性测试和保护 DevOps 基础设施
-
使用 Jenkins、Azure Pipelines 和 GitLab CI 构建 DevOps CI/CD 管道
-
探索蓝绿部署和开源项目的 DevOps 实践
Packt 正在寻找像您这样的作者
如果您有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并立即申请。我们已经与成千上万的开发者和技术专业人士合作,帮助他们与全球技术社区分享他们的见解。您可以提交一般申请,申请我们正在招聘作者的具体热门话题,或者提交您自己的想法。
分享您的想法
现在您已经完成了《Kubernetes 操作员框架书》,我们很想听听您的想法!如果您是在 Amazon 上购买的这本书,请 点击这里直接跳转到 Amazon 评价页面 为此书分享您的反馈或在您购买书籍的网站上留下评论。
您的评价对我们和技术社区非常重要,将帮助我们确保提供优质的内容。