深刻理解Kubernetes Operator

深刻理解Kubernetes Operator

本文要点:linux

  • Kubernetes API 为全部云资源提供了单个集成点,以此来促进云原生技术的采用。git

  • 有一些框架和库能够用来简化 Operator 的编写。支持多种语言,其中 Go 生态系统是最为成熟的。sql

  • 你能够为非自有的软件建立 Operator。DevOps 团队可能会经过这种方式来管理数据库或其余外部产品。数据库

  • 难点不在于 Operator 自己,而是要学会理解它的行为。

多年来,Operator 一直是 Kubernetes 生态系统的重要组成部分。经过将管理界面移动到 Kubneretes API 中,带来了“单层玻璃”的体验。对于但愿简化 kuberentes 原生应用程序的开发人员或者但愿下降现有系统复杂性的 DevOps 工程师来讲,Operator 多是一个很是有吸引力的选择。但如何从头开始建立一个 Operator 呢?编程

深刻理解 Operator

Operator 是什么?

现在,Operator 无处不在。数据库、云原生项目、任何须要在 Kubernetes 上部署或维护的复杂项目都用到了 Operator。CoreOS 在 2016 年首次引入了 Operator,将运维关注点转移到软件系统中。Operator 自动执行操做,例如,Operator 能够部署数据库实例、升级数据库版本或执行备份。而后,这些系统能够被测试,响应速度比人类工程师更快。json

Operator 还经过使用自定义资源定义对 Kubenretes API 进行了扩展,将工具配置转移到了 API 中。这意味着 Kubenretes 自己就变成了“单层玻璃”。DevOps 工程师能够利用围绕 Kubernetes API 资源而构建的工具生态系统来管理和监控他们部署的应用程序:api

  • 使用 Kubernetes 内置的基于角色的访问控制 (RBAC) 来修改受权和身份验证。缓存

  • 使用“git ops”对生产变动进行可复制的部署和代码审查。安全

  • 使用基于开放策略代理 (OPA) 的安全工具在自定义资源上应用策略。服务器

  • 使用 Helm、Kustomize、ksonnet 和 Terraform 等工具简化部署描述。

这种方法还能够确保生产、测试和开发环境之间的一致性。若是每一个集群都是 Kubernetes 集群,则可使用 Operator 在每一个集群中部署相同的配置。

为何要使用 Operator?

使用 Operator 有不少理由。一般状况下,要么是开发团队为他们的产品建立 Operator,要么是 DevOps 团队但愿对第三方软件管理进行自动化。不管哪一种方式,都应该从肯定 Operator 应该负责哪些东西开始。

最基本的 Operator 用于部署,使用 kubectl apply 就能够建立一个用于响应 API 资源的数据库,但这比内置的 Kubernetes 资源 (如 StatefulSets 或 Deployments) 好不了多少。复杂的 Operator 将提供更大的价值。若是你想要对数据库进行伸缩该怎么办?

若是是 StatefulSet,你能够执行 kubectl scale statefulset my-db --replicas 3,这样就能够获得 3 个实例。但若是这些实例须要不一样的配置呢?是否须要指定一个实例为主实例,其余实例为副本?若是在添加新副本以前须要执行设置步骤,那该怎么办?在这种状况下,可使用 Operator。

更高级的 Operator 能够处理其余一些特性,如响应负载的自动伸缩、备份和恢复、与 Prometheus 等度量系统的集成,甚至能够进行故障检测和自动调优。任何具备传统“运行手册”文档的操做均可以被自动化、测试和依赖,并自动作出响应。

被管理的系统甚至不须要部署在 Kubernetes 上也能从 Operator 中获益。例如,主要的云服务提供商(如 Amazon Web Services、微软 Azure 和谷歌云)提供 Kubenretes Operator 来管理其余云资源,如对象存储。用户能够经过配置 Kubernetes 应用程序的方式来配置云资源。运维团队可能对其余资源也采起一样的方法,使用 Operator 来管理任何东西——从第三方软件服务到硬件。

Operator 示例

在本文中,咱们将重点关注 etcd-cluster-operator。这是我和一些同事共同开发的 Operator,用于管理 Kubernetes 内部的 etcd。本文不是专门介绍 Operator 或 etcd 自己,因此我不会太过详细介绍 etcd 的细节,只要可以让你了解 etcd 的用途便可。

简单地说,etcd 是一个分布式键值数据存储。它有能力管理本身的稳定性,只要:

  • 每一个 etcd 实例都有一个用于计算、网络和存储的独立故障域。

  • 每一个 etcd 实例都有一个惟一的网络名称。

  • 每一个 etcd 实例均可以链接到其余实例。

  • 每一个 etcd 实例都知道其余实例的存在。

此外:

  • etcd 集群的增加或缩小须要使用 etcd 管理 API 进行特定的操做,在添加或删除实例以前声明集群要发生的变化。

  • 可使用 etcd 管理 API 上的“快照”端点进行备份。经过 gRPC 调用它,你将获得一个备份文件。

  • 使用 etcdctl 工具操做备份文件和 etcd 主机上的数据目录来实现恢复。这在真实的机器上很容易,但在 Kubernetes 上须要作一些协调。

正如你所看到的,这比 Kubernetes StatefulSet 能作更多的事情,因此咱们使用 Operator。咱们不会深刻讨论 etcd-cluster-operator 的机制,但在本文的其他部分,咱们都将引用这个 Operator 示例。

Operator 剖析

Operator 由两部分组成:一个或多个 Kubernetes 自定义资源定义 (CRD),它们描述了一种新的资源,包括应该具备哪些字段。CRD 可能会有多个,例如 etcd-cluster-operator 同时使用 EtcdCluster 和 EtcdPeer 来封装不一样的概念。

一个运行中的软件,读取自定义资源并做出响应。

一般,Operator 被包含并部署在 Kubernetes 集群中,一般使用一个简单的 Deployment 资源。理论上,只要 Operator 可以与集群的 Kubernetes API 通讯,它就能够在任何地方运行。可是,在集群中运行 Operator 一般更容易。一般状况下会使用自定义 Namespace 将 Operator 与其余资源分隔开来。

若是咱们使用这种方法来运行 Operator,还须要作一些事情:

  • 一个容器镜像,其中包含 Operator 可执行文件。

  • 一个 Namespace。

  • Operator 的 ServiceAccount,授予读取自定义资源的权限,并配置它要管理的资源 (例如 Pod)。

  • 用于 Operator 容器的 Deployment。

  • ClusterRoleBinding 和 ClusterRole 资源,绑定到 ServiceAccount。

  • Webhook 配置。

稍后咱们将详细讨论权限模型和 Webhook。

软件和工具

第一个问题是编程语言和生态系统。从理论上讲,几乎任何可以进行 HTTP 调用的语言均可以使用 Operator。假设 Operator 部署在与资源相同的集群中,那么只须要在集群容器中运行它便可。一般是 linux/x86_64,这也是 etcd-cluster-operator 的目标平台,但 Operator 也能够被编译成 arm64 或其余架构,甚至是 Windows 容器。

Go 语言拥有最成熟的工具。用于构建 Kubernetes 控制器的框架 controller-runtime 能够做为一个独立的工具。此外,Kubebuilder 和 Operator SDK 等项目都构建在控制器运行时之上,目的是提供一种流线化的开发体验。

除了 Go 语言,其余语言 (如 Java、Rust、Python 和其余语言) 一般会提供用于链接 Kubernetes API 或者专门用于构建 Operator 的工具。这些工具的成熟度和支持水平各有差异。

另外一种选择是经过 HTTP 直接与 Kubernetes API 交互。这种方式所需的工做量最大,好处是团队可使用他们最熟悉的编程语言。

最终,这种选择取决于负责构建和维护 Operator 的团队。若是团队已经习惯使用 Go,那么 Go 生态系统丰富的工具显然是最佳的选择。若是团队尚未使用 Go,那么就须要作出权衡,要么在学习和培训更成熟的生态系统工具方面付出代价,要么选择不成熟但团队熟悉其底层语言的生态系统。

对于 etcd-cluster-operator 来讲,开发团队已经很是精通 Go,所以 Go 对咱们来讲是一个很明智的选择。咱们还选择使用 Kubebuilder 而不是 Operator SDK,但这只是由于咱们对它比较熟悉。咱们的目标平台是 linux/x86_64,但若是须要的话,也能够以其余平台为目标。

自定义资源和目标状态

咱们为咱们的 etcd Operator 建立了一个叫做 EtcdCluster 的自定义资源定义。安装好 CRD 后,用户就能够建立 EtcdCluster 资源。EtcdCluster 资源描述了 etcd 集群的需求,并给出了它的配置。

PlainTextapiVersion:etcd.improbable.io/v1alpha1kind:EtcdClustermetadata: name: my-first-etcd-clusterspec: replicas: 3 version: 3.2.28

apiVersion 指定这是哪一个版本的 API,在本例中是 v1alpha1。kind 声明这是一个 EtcdCluster。与其余类型的资源同样,咱们有一个 metadata,它必须包含一个 name,也可能包含一个 namespace、labels、annotations 和其余标准项。这样咱们就能够像对待 Kubernetes 中的其余资源同样对待 EtcdCluster。例如,咱们可使用一个标签来标识哪一个团队负责哪个集群,而后经过 kubectl get etcdcluster -l team=foo 搜索这些集群,就像使用其余标准资源同样。

spec 字段包含了有关这个 etcd 集群的运维信息。还有不少其余字段,但这里咱们只介绍最基本的字段。version 字段描述要部署的 etcd 版本,replicas 字段描述有多少个实例。

还有一个 status 字段 (在示例中不可见),运维人员用这个字段来描述集群的当前状态。spec 和 status 是 Kubernetes API 提供的标准字段,能够很好地与其余资源和工具集成。

由于咱们使用了 Kubebuilder,因此能够借助工具生成这些自定义资源定义。咱们写了一个 Go 结构体,定义了 spec 和 status 字段:

type EtcdClusterSpec struct {
    Version     string               `json:"version"`
    Replicas    *int32               `json:"replicas"`
    Storage     *EtcdPeerStorage     `json:"storage,omitempty"`
    PodTemplate *EtcdPodTemplateSpec `json:"podTemplate,omitempty"`
}

基于这个 Go 结构体(和一个相似的 status 结构体),Kubebuilder 会生成咱们的自定义资源定义,咱们只须要编写代码处理调解逻辑便可。

其余语言提供的支持可能有所不一样。若是你使用的是专为 Operator 设计的框架,那么可能会生成这个,例如 Rust 库 kube-derive 的生成方式就跟这个差很少。若是有团队直接使用 Kubernetes API,那么他们就必须分别编写 CRD 和用于解析数据的代码。

调解循环

如今咱们已经有了描述 etcd 集群的方式,能够构建 Operator 来管理集群资源。Operator 能够以任何方式运行,而几乎全部 Operator 均可以使用控制器模式。

控制器是一种简单的程序循环,一般被称为“调解循环”,它能够执行如下逻辑:

  1. 观察指望的状态。

  2. 观察所管理资源的当前状态。

  3. 采起行动,使托管的资源处在指望的状态。

对于 Kubernetes 中的 Operator,目标状态就是资源(示例中是 EtcdCluster 的 spec 字段指定的值)。咱们的托管资源能够是集群内部或外部的任何资源。在咱们的示例中,咱们将建立其余 Kubneretes 资源,如 ReplicaSets、PersistentVolumeClaims 和 Services。

对于 etcd,咱们直接链接到 etcd 进程,使用管理 API 来获取它的状态。这种“非 kubernetes”的访问方式须要当心一点,由于它可能会受到网络中断的影响,因此对于这种状况,并不必定是由于服务被关闭了。咱们不能将没法链接到 etcd 做为 etcd 没有在运行的信号 (若是咱们这么认为了,那么重启 etcd 实例只会加剧网络中断的发生)。

一般,在与非 Kubernetes API 服务通讯时,最重要的是要考虑可用性或一致性。对于 etcd 来讲,若是咱们得到响应,那它们必定是一致的,但其余系统可能不是这样。关键要避免因为信息过期而致使错误操做,从而使中断变得更糟。

控制器的特性

对于控制器来讲,最简单的就是定时运行调解循环,好比每 30 秒一次。这样作是能够的,但有不少缺点。例如,它必须可以检测上一次循环是否还在运行,这样就不会同时运行两个循环。此外,这意味着每 30 秒会对 Kubernetes 进行一次完整的扫描来得到相关的资源,而后,对于 EtcdCluster 的每一个实例,须要运行调解函数来得到相关 Pod 和其余资源。这种方式给 Kubernetes API 形成大量的负载。

这也致使出现了一种很是“程序性”的方法,由于在下一次协调以前可能须要很长时间才能尽量快地执行每一个循环。例如,一次性建立多个资源。这可能会致使一种很是复杂的状态,运维人员须要进行不少检查才能知道要作什么,并且颇有可能会出错。

为了解决这个问题,控制器提供了一些特性:

  • Kubernetes API 监听。

  • API 缓存。

  • 批量更新。

全部这些均可以有效地减小要执行的任务,由于运行单个循环的成本和须要等待的时间都减小了,协调逻辑的复杂性也就下降了。

API 监听

Kubernetes API 支持“监听”,而不是定时扫描。API 使用者能够对感兴趣的资源或资源类别进行注册,并在匹配的资源发生变动时收到通知。由于请求负载减小了,因此 Operator 大部分时间处于空闲状态,并且几乎能够当即对变动作出响应。Operator 框架一般会为你处理监听所需的注册和管理操做。

这种设计的另外一个结果是你还须要监听你所建立的资源。例如,若是咱们建立了 Pods,那么也必须监听咱们建立的 Pod。若是它们被删除或修改,致使与咱们想要的状态不一致,咱们就能够收到通知,并纠正它们。

咱们如今能够进一步简化调解程序。例如,为了响应 EtcdCluster,Operator 但愿建立一个 Service 和一些 EtcdPeer 资源。它不是一次性建立好它们,而是先建立 Service,而后退出。但由于咱们关注了本身的 Services,咱们会收到通知,并当即从新进行调解。

这样咱们就能够建立对等资源了。不然,咱们将建立大量的资源,而后为每一个资源从新调解一次,这可能会触发更多的从新调解。这种设计有助于保持调解器循环的简单,由于只须要执行一个操做就退出,开发人员不须要处理复杂的状态。

这样作的一个主要后果是可能会错过更新。网络中断、Pod 重启和其余问题在某些状况下可能致使错过事件。为了解决这个问题,关键在于 Operator 的运行方式应该“基于条件”而不是“基于边缘”。

这些术语来自信号控制软件,是指基于信号电压作出响应。在软件领域,当咱们说“基于边缘”时,意思是“对事件作出反应”,当咱们说“基于条件”时,意思是“对观察到的状态作出反应”。

例如,若是一个资源被删除,咱们能够观察到删除事件并选择从新建立。可是,若是咱们错过了删除事件,就可能永远不会尝试从新建立。或者,更糟糕的是,咱们认为它还在,致使后续出现问题。相反,“基于条件”的方法将触发器简单地视为应该从新进行调解。它将再次观察外部状态,丢弃触发它的变动。

API 缓存

控制器的另外一个主要特性是缓存请求。若是咱们请求 Pods,而且会在 2 秒后再次触发,那么咱们可能会为第二个请求保留缓存结果。这减小了 API 服务器的负载,但也给开发人员带来了一些须要注意的问题。

因为资源请求可能过时,咱们必须处理这个问题。资源建立没有被缓存,所以可能出现这种状况:

  • 调解 EtcdCluster 资源

  • 搜索 Service,没有找到。

  • 建立 Service 并退出。

  • 对建立的 Service 作出响应。

  • 搜索 Service,缓存过时,找不到。

  • 建立 Service。

咱们错误地建立了一个相同的 Service。Kubernetes API 将会处理这个问题,并给出一个错误,说明 Service 已经存在。所以,咱们必须处理这个问题。通常来讲,最好的作法是在之后的某个时间进行从新调解。在 Kubebuilder 中,只是简单地在 reconcile 函数中返回一个错误就会致使这种状况发生,但不一样的框架可能会有所不一样。当稍后从新运行时,缓存最终会保持一致,并可能发生下一阶段的调解。

这样作的一个反作用是全部资源都必须有肯定的名称。不然,若是咱们建立了一个重复的资源,可能会使用不一样的名称,致使真正的资源重复。

批量更新

在某些状况下,咱们可能会同时进行不少个调解。例如,若是咱们正在监听大量的 Pod 资源,其中有很资源同时处于中止状态 (例如,因为节点故障、管理员操做错误,等等),那么咱们但愿获得屡次通知。然而,在第一次调解触发并观察到集群状态时,全部的 Pod 都已经消失了,那么后续的调解就是没有必要的。

若是数量很小,这就不是一个问题。但在较大的集群中,当一次处理数百或数千个更新时,这样作有可能会致使调解循环慢得像爬行同样,由于它一次性重复 100 次相同的操做,甚至会致使队列超载,并最终致使 Operator 崩溃。

由于咱们的调解函数是“基于条件”的,因此咱们能够对其加以优化来解决这个问题。当咱们将特定资源的更新操做放入队列时,若是队列中已经有该资源的更新操做,那么就将其删除。在从队列读取数据以前先等待一下,咱们就能够有效地进行“批量”操做。所以,若是 200 个 Pod 同时中止,咱们可能只须要进行一次调解,具体取决于 Operator 及其队列的配置状况。

权 限

访问 Kubernetes API 必须提供凭证。在集群中,这是由 ServiceAccount 负责处理的。咱们可使用 ClusterRole 和 ClusterRoleBinding 资源将权限与 ServiceAccount 关联起来。对于 Operator 来讲,这很关键。Operator 必须拥有权限来 get、list 和 watch 它在整个集群中管理的资源。此外,对于它建立的任何资源,都须要权限。例如,Pods、StatefulSets、Services 等。

Kubebuilder 和 Operator SDK 等框架能够为你提供这些权限。例如,Kubebuilder 采用了注解为每一个控制器分配权限。若是多个控制器合并为一个二进制文件 (就像咱们对 etcd-cluster-operator 所作的那样),那么权限也将合并在一块儿。

//+kubebuilder:rbac:groups=etcd.improbable.io,resources=etcdpeers,verbs=get;list;watch
//+kubebuilder:rbac:groups=etcd.improbable.io,resources=etcdpeers/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps,resources=replicasets,verbs=list;get;create;watch
//+kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=list;get;create;watch;delete

这是 EtcdPeer 资源的调解器权限。能够看到,咱们 get、list 和 watch 本身的资源,而且能够 update 和 patch 状态子资源。咱们能够只更新状态,将信息显示给其余用户。最后,咱们对所管理的资源具备普遍的权限,能够根据须要建立和删除它们。

验证和默认值

虽然自定义资源自己提供了必定级别的验证和默认值,但更复杂的检查操做须要由 Operator 来执行。最简单的方法是在 Operator 读取资源时执行这些操做,不管是 watch 返回的,仍是手动读取后。可是,这意味着默认值将永远不会被应用到 Kubernetes 中,这种行为会让管理员感到困惑。

更好的方法是使用验证和可变的 Webhook 配置。这些资源告诉 Kubernetes,当一个资源被建立、更新或者在持久化以前被删除时,必须使用 Webhook。

例如,可变 Webhook 能够用来设置默认值。在 Kubebuilder 中,咱们提供了一些额外的配置来建立 MutatingWebhookConfiguration,Kubebuilder 负责提供 API 端点。咱们只须要在 spec 结构体中设置 Default 值。而后,当资源被建立时,Webhook 在持久化资源以前被调用,就会应用默认值。

不过,咱们仍然要在读取资源时应用默认值。Operator 不能假设已经知道平台是否启用了 Webhook。即便启用了,也可能配置错误,或者由于网络中断致使 Webhook 被跳过,或者资源可能在配置 Webhook 以前就已经被应用过了。全部这些问题都意味着,虽然 Webhook 提供了更好的用户体验,但 Operator 代码不能彻底依赖它们,必须再次应用默认值。

测 试

任何一个单独的逻辑单元均可以使用编程语言的常规工具进行单元测试,可是,在进行集成测试时会出现一些特定的问题。咱们可能会把 API 服务器当成能够被 mock 的数据库。但在真实的系统中,API 服务器会执行大量的验证和默认操做。这意味着测试和现实之间的行为多是不同的。

通常来讲,主要有两种方式:

第一种方法,下载测试工具并执行 kube-apiserver etcd 可执行文件,建立一个真正的 API 服务器。固然,虽然你能够建立一个 ReplicaSet,但缺乏了能够建立 Pods 的 Kubernetes 组件,因此咱们看不到有东西真正在运行。

第二种方法更加全面一些,它使用一个真正的 Kubernetes 集群,能够运行 Pods,并能准确作出响应。经过使用 kind,这种集成测试变得更加容易。kind 是“Kubernetes in Docker”的缩写,它能够在任何能够运行 Docker 容器的地方运行一个完整的 Kubernetes 集群。它提供了一个 API 服务器,能够运行 Pods,并运行 Kubernetes 全部主要的组件。所以,使用了 kind 的测试能够在笔记本电脑上或 CI 中运行,并提供近乎完美的 Kubernetes 体验。

总 结

在这篇文章中,咱们谈到了不少想法:

  • 将 Operator 做为 Pods 部署在集群中。

  • 能够支持任何一种编程语言,因此请选择最适合团队的那一种。不过,Go 语言拥有最成熟的生态系统。

  • 当心使用非 kubernetes 资源,特别是在网络中断或上游 API 发生故障时,它们可能会致使更严重的中断。

  • 在每一个调解周期中执行一个操做,而后退出,并容许 Operator 从新将其放入队列。

  • 使用“基于条件”的方法,忽略触发调解的事件的内容。

  • 为新资源使用肯定性的命名。

  • 为你的服务账户提供最小权限。

  • 在 Webhook 和代码中应用默认值。

  • 使用 kind 进行集成测试。

有了这些工具,你就能够构建 Operator 来简化部署,并减轻运维团队的负担,不管是你所拥有的应用程序,仍是你本身开发的应用程序。

原文连接:Kubernetes Operators in Depth
https://www.infoq.com/articles/kubernetes-operators-in-depth/

做者 | James Laverack译者 | 王者文章转自| InfoQ

相关文章
相关标签/搜索