操作员(Operator)是一个控制器,它使用 CRD 将特定应用的操作知识以算法和自动化的形式封装起来。操作员模式使我们能够扩展上一章的控制器模式,使其具有更多的灵活性和表现力。

问题描述

你在第 22 章控制器(Controller)中学习了如何以简单的、解耦的方式扩展 Kubernetes 平台。然而,对于扩展用例来说,普通的自定义控制器还不够强大,因为它们只限于监视和管理 Kubernetes 的内在资源。有时我们想在 Kubernetes 平台上添加新的概念,这就需要额外的领域对象。比方说,我们选择了 Prometheus 作为我们的监控方案,我们想把它作为一个监控设施以一种明确的方式添加到 Kubernetes 中。如果我们有一个描述我们的监控设置和所有部署细节的 Prometheus 资源,类似于我们定义其他 Kubernetes 资源的方式,那岂不妙哉?此外,我们是否可以有资源来描述我们要监控哪些服务(例如,用标签选择器)?

这些情况正是 CustomResourceDefinition(CRD)非常有用的用例。它们允许扩展 Kubernetes API,将自定义资源添加到 Kubernetes 集群中,并像使用本地资源一样使用它们。自定义资源与作用于这些资源的 Controller 一起构成了 Operator 模式。

Jimmy Zelinskie 的这句话大概最能描述操作者的特点:Operator 是一个 Kubernetes 控制器,了解两个领域: Kubernetes 和其他东西。通过结合这两个领域的知识,它可以自动完成那些通常需要懂这两个领域的人类操作员的任务。

解决方案

正如你在第 22 章 Controller 中所看到的,我们可以有效地对默认 Kubernetes 资源的状态变化做出反应。现在你已经理解了 Operator 模式的一半,我们现在来看看另一半 — 使用 CRD 资源在 Kubernetes 上表示自定义资源。

自定义资源定义(CRD)

:::info 通过 CRD,我们可以在 Kubernetes 平台上扩展 Kubernetes 来管理我们的领域概念。自定义资源和其他资源一样,通过 Kubernetes API 进行管理,并最终存储在后端存储 etcd 中。历史上,CRD 的前身是 ThirdPartyResources。 :::

前面的方案实际上是由 CoreOS Prometheus 操作者用这些新的自定义资源实现的,以实现 Prometheus 与 Kubernetes 的无缝集成。Prometheus CRD 的定义如例 23-1,其中也解释了 CRD 的大部分可用字段:

  1. # 例 23-1 自定义资源定义
  2. ---
  3. apiVersion: apiextensions.k8s.io/v1beta1
  4. kind: CustomResourceDefinition
  5. metadata:
  6. name: prometheuses.monitoring.coreos.com
  7. spec:
  8. # 所属的 API 组
  9. group: monitoring.coreos.com
  10. names:
  11. # 用于识别该资源实例的种类
  12. kind: Prometheus
  13. # 用于创建复数形式的命名规则,用于指定这些对象的列表
  14. plural: prometheuses
  15. # 范围:资源是可以在整个集群范围内创建,还是特定于一个名称空间?
  16. scope: Namespaced
  17. version: v1
  18. # 用于验证的 OpenAPI V3 模式
  19. validation:
  20. openAPIV3Schema: ....

还可以指定一个 OpenAPI V3 模式,以允许 Kubernetes 验证一个自定义资源。对于简单的用例,这个属性可以省略,但对于生产级的 CRD,应该提供 API 格式定义,这样可以及早发现配置错误。

此外,Kubernetes 允许我们通过 spec 字段子资源为 CRD 指定两种可能的子资源:

  • scale:通过该属性,CRD 可以指定如何管理其副本数。该字段可用于声明 JSON 路径,在该路径中指定该自定义资源的所需复制数:到持有实际运行的复制数的属性的路径,以及到可用于查找自定义资源实例副本的标签选择器的可选路径。这个标签选择器通常是可选的,但如果您想将此自定义资源与第 24 章 “弹性伸缩” 中解释的 HorizontalPodAutoscaler 一起使用,则需要这个标签选择器。
  • status:当我们设置这个属性时,一个新的 API 调用就可以使用,它只允许你改变状态。这个 API 调用可以单独被保护,并允许从控制器外部进行状态更新。另一方面,当我们将一个自定义资源作为一个整体进行更新时,状态部分会像标准的 Kubernetes 资源一样被忽略。

例子 23-2 显示了一个潜在的子资源路径,因为也是用于一个普通的 Pod:

# 例 23-2 CustomResourceDefinition 的子资源定义

---
kind: CustomResourceDefinition 
# ...
spec:
    subresources: 
      status: {} 
    scale:
        # 通往已声明的副本数量的 JSON 路径
            specReplicasPath: .spec.replicas
      # 活动副本数量的 JSON 路径
            statusReplicasPath: .status.replicas 
      # 用于查询活动副本数量的标签选择器的 JSON 路径
      labelSelectorPath: .status.labelSelector

一旦我们定义了一个 CRD,我们就可以很容易地创建这样一个资源,如例 23-3 所示:

# 例 23-3 一个 Prometheus 自定义资源

---
apiVersion: monitoring.coreos.com/v1 
kind: Prometheus
metadata:
    name: prometheus 
spec:
    serviceMonitorSelector: 
      matchLabels:
            team: frontend 
  resources:
        requests: 
        memory: 400Mi

metadata 部分的格式和验证规则与其他任何 Kubernetes资源相同。 spec 包含 CRD 特定的内容,Kubernetes 根据 CRD 给出的验证规则进行验证。

如果没有一个活跃的组件来对它们进行操作,单单是自定义资源是没有什么用处的。为了赋予它们一些意义,我们又需要我们著名的 Controller,它观察这些资源的生命周期,并根据资源中的声明采取行动。

Controller 和 Operator 分类

在深入写我们的 Operator 之前,我们先来看看 Controller、Operator,尤其是 CRD 的几种分类。根据 Operator 的动作,大致有以下几种分类:

  • 安装类 CRD:适用于在 Kubernetes 平台上安装和操作应用程序。典型的例子是 Prometheus CRD,我们可以用它来安装和管理 Prometheus 本身。
  • 应用类 CRD:而这些则是用来表示特定应用的领域概念。这种 CRD 可以让应用与 Kubernetes 深度集成,这就涉及到将 Kubernetes 与特定应用领域行为相结合。例如,ServiceMonitor CRD 被 Prometheus 操作者用来注册特定的 Kubernetes 服务,以便被 Prometheus 服务器刮取。Prometheus Operator 负责相应地调整 Prometheus 的服务器配置。

:::info 请注意,一个 Operator 可以对不同类型的 CRD 采取行动,就像 Prometheus Operator 在本例中所做的那样。这两类 CRD 之间的界限是模糊的。 :::

在我们对 Controller 和 Operator 的分类中,Operator 是指使用 CRD 的 Controller。然而,即使是这种区分也有点模糊,因为两者之间存在着差异。

其中一个例子是控制器,它使用 ConfigMap 作为 CRD 的一种替代。在默认的 Kubernetes 资源不够用,但创建 CRD 也不可行的场景下,这种方法是有意义的。在这种情况下,ConfigMap 是一个很好的中间地带,可以将领域逻辑封装在 ConfigMap 的内容中。使用普通 ConfigMap 的一个优点是,注册 CRD 时不需要拥有集群管理员权限。在某些集群设置中,你只是不可能注册这样的 CRD(例如,像在 OpenShift Online 等公共集群上运行时)。

:::info 然而,当你用一个普通的 ConfigMap 代替 CRD 作为你的特定域配置时,你仍然可以使用 “观察 - 分析 - 行动”的概念。缺点是,你不能像 kubectl 获取 CRD 那样获得必要的工具支持;你在 API Server 层面没有验证,也不支持 API 版本化。另外,你对 ConfigMap 的状态:字段的建模方式影响不大,而对于 CRD,你可以自由地定义你的状态模型。 :::

:::tips CRD 的另一个优点是,你有一个基于 CRD 种类的细粒度的权限模型,你可以单独对其进行调整。当你所有的域配置都封装在 ConfigMap 中时,这种 RBAC 安全是不可能的,因为一个命名空间中的所有 ConfigMap 都共享相同的权限设置。 :::

从实现的角度来看,我们是通过限制控制器的使用范围来实现控制器,还是通过自定义的方式来实现控制器,这一点很重要。在前一种情况下,我们已经在我们选择的 Kubernetes 客户端库中拥有了所有的类型。对于 CRD 的情况,我们没有开箱即用的类型信息,我们可以使用无模式(schemaless)的方法来管理 CRD 资源,或者自己定义自定义类型,可以基于 CRD 定义中包含的 OpenAPI 模式。对类型化 CRD 的支持因所使用的客户端库和框架而异。

图 23-1 显示了我们的 Controller 和 Operator 分类,从简单的资源定义选项开始到更高级的,Controller 和 Operator 之间的边界是自定义资源的使用。
image.png
图 23-1 Controller 和 Operator 的光谱

:::tips 对于 Operator 来说,甚至还有更高级的 Kubernetes 扩展钩子选项。当 Kubernetes 管理的 CRD 不足以代表一个问题域时,你可以用自己的聚合层来扩展 Kubernetes API。我们可以将一个自定义实现的 APIService 资源作为新的 URL 路径添加到 Kubernetes API 中。 :::

要将一个名称为 custom-api-server 并由 Pod 支持的给定 Service 与你的服务连接起来,你可以使用像例 23-4 所示的资源。

# 例 23-4 使用自定义的 APIService 进行 API 聚合

---
apiVersion: apiregistration.k8s.io/v1beta1 
kind: APIService
metadata:
    name: v1alpha1.sample-api.k8spatterns.io 
spec:
    group: sample-api.k8spattterns.io 
  service:
        name: custom-api-server 
  version: v1alpha1

除了服务和 Pod 的实现,我们还需要一些额外的安全配置,用于设置 Pod 运行的 ServiceAccount,一旦设置好,每次向 API Server 发出的请求https://<api server ip>/apis/sample-api.k8spatterns.io/v1alpha1/namespaces/<ns>/... 的请求都会被指向我们的自定义 Service 的实现来处理这些请求,包括持久化通过这个 API 管理的资源。这种方法与前面的 CRD 案例不同,在 CRD 案例中,Kubernetes 自己完全管理自定义资源。

:::info 有了自定义 API Server,你就有了更多的自由度,这就可以超越观察资源生命周期事件的范围。另一方面,你也必须实现更多的逻辑,所以对于典型的用例,处理普通 CRD 的 Operator 通常已经足够好了。 :::

对 API Server 功能的详细探讨超出了本章的范围。官方文档以及一个完整的示例 sample-apiserver 有更详细的信息。此外,你还可以使用apiserver-builder 库,它可以帮助实现 API Server 聚合。

现在,让我们看看如何用 CRD 来开发和部署我们的 Operator。

Operator 开发和部署

在写这篇文章的时候(2019年),操作员开发是 Kubernetes 的一个积极发展的领域,有几个工具箱和框架可以用来编写 Operator。协助创建操作员的三个主要项目如下。

  • CoreOS Operator Framework
  • Kubebuilder 是在 Kubernetes 自己的 SIG API Machinaery 下开发的
  • 谷歌云平台的 Metacontroller

接下来我们会非常简单地触及这些项目,但要注意,这些项目都是相当年轻的,可能会随着时间的推移而改变,甚至被合并。

Operator Framwork

Operator Framework 为开发基于 Go 的 Operator 提供了广泛的支持。它提供了几个子组件。

  • Operator SDK 提供了一个访问 Kubernetes 集群的高级 API,以及一个启动 Operator 项目的脚手架。
  • Operator 生命周期管理器(OLM)管理 Operator 及其 CRD 的发布和更新。您可以把它看作是一种 “operator operator”。
  • Operator Metering 可以为 Operator 提供使用报告。

我们在这里不会详细介绍 Operator SDK,它仍在不断发展,但 Operator 生命周期管理器(OLM)在使用 Operator 时提供了特别有价值的帮助。CRD 的一个问题是,这些资源只能在集群范围内注册,因此需要集群管理员的权限。虽然普通的 Kubernetes 用户通常可以管理他们已授予访问权限的命名空间的所有方面,但他们不能在没有与集群管理员交互的情况下只使用 Operator。

:::info 为了简化这种交互,OLM 是一个在后台运行的集群服务,在服务账户下有安装 CRD 的权限。一个名为 ClusterServiceVersion (CSV)的专用 CRD 与 OLM 一起注册,允许我们指定 Operator 的部署以及与该 Operator 相关的 CRD 定义的引用。一旦我们创建了这样的 CSV,OLM 的一部分就会等待该 CRD 及其所有附属 CRD 被注册。如果是这样,OLM 就会部署 CSV 中指定的 Operator 。OLM 的另一部分可用于代表非特权用户注册这些 CRD。这种方法是一种优雅的方式,可以让普通群集用户安装他们的 Operator。 :::

Kubebuilder

Kubebuilder是 SIG API Machinery 的一个项目,它有完整的文档。和 Operator SDK 一样,它支持 Go 项目的脚手架和在一个项目中管理多个 CRD。

:::info 与 Operator Framework 有一点不同,Kubebuilder 直接与 Kubernetes API 合作,而 Operator SDK 在标准 API 上增加了一些额外的抽象,使其更容易使用(但缺乏一些功能)。 :::

对安装和管理 Operator 的生命周期的支持不像 Operator 框架的 OLM 那样复杂。然而,这两个项目有明显的重叠,它们最终可能会以某种方式融合。

Metacontroller

:::info Metacontroller 与其他两个 Operator 构建框架有很大的不同,因为它用 API 扩展了Kubernetes,封装了编写自定义控制器的常见部分。它的作用类似于 Kubernetes Controller Manager,通过运行多个控制器,这些控制器不是硬编码的,而是通过 Metacontroller 特定的 CRD 动态定义的。换句话说,它是一个委托控制器,调用提供实际 Controller 逻辑的服务。 :::

另一种描述 Metacontroller 的方式是声明式行为。虽然 CRD 允许我们在 Kubernetes API 中存储新的类型,但 Metacontroller 使我们可以轻松地声明性地定义标准或自定义资源的行为。

当我们通过 Metacontroller 定义一个控制器时,我们必须提供一个只包含我们的控制器特定业务逻辑的函数。Metacontroller 处理所有与Kubernetes API 的交互,代表我们运行一个调节循环,并通过 Webhook 调用我们的函数。Webhook 被调用时,会有一个定义良好的有效载荷,描述 CRD 事件。当函数返回值时,我们返回一个代表我们控制器函数应该创建(或删除)的 Kubernetes 资源的定义。

这种授权允许我们用任何能够理解 HTTP 和 JSON 的语言来编写函数,并且这些函数对 Kubernetes API 或其客户端库没有任何依赖性。这些函数可以托管在 Kubernetes 上,或者外部的 Functions-as-a-Service(FaaS)提供商,或者其他地方。

我们不能在此赘述太多细节,但如果你的用例涉及通过简单的自动化或编排来扩展和耦合 Kubernetes,并且你不需要任何额外的功能,你应该看看 Metacontroller,特别是当你想用 Go 以外的语言实现你的业务逻辑时。一些控制器的例子将演示如何仅通过使用 Metacontroller 来实现 StatefulSet、蓝绿部署、Indexed Job 和 “每个 Pod 一个 Service”。

Operator 示例

让我们来看看一个具体的 Operator 示例。我们扩展了第 22 章 Controller 中的例子,引入了 ConfigWatcher 类型的 CRD。这个 CRD 的实例指定了要监视的 ConfigMap 的引用,以及如果 ConfigMap 发生变化时要重新启动的 Pod。通过这种方法,我们消除了 ConfigMap 对 Pod 的依赖性,因为我们不必修改 ConfigMap 本身来添加触发注释。此外,通过我们在 Controller 示例中的基于注释的简单方法,我们也可以只将 ConfigMap 连接到单个应用程序。有了 CRD,ConfigMap 和 Pod 的任意组合是可能的。

这个 ConfigWatcher 自定义资源看起来就像例 23-5 中的那样:

# 例 23-5 一个简单的 ConfigWatcher 资源

---
kind: ConfigWatcher 
apiVersion: k8spatterns.io/v1
metadata:
    name: webapp-config-watcher
spec:
    # 参照 ConfigMap 进行观察
    configMap: webapp-config 
  # 标签选择器确定要重新启动的 Pod
  podSelector:
        app: webapp

在这个定义中,属性 configMap 引用了要监视的 ConfigMap 的名称。字段 podSelector 是一个标签及其值的集合,它标识了要重启的 Pod。

我们可以用 CRD 定义这个自定义资源的类型,如例 23-6 所示:

# 例 23-6 ConfigWatcher 的自定义资源定义

---
apiVersion: apiextensions.k8s.io/v1beta1 
kind: CustomResourceDefinition 
metadata:
    name: configwatchers.k8spatterns.io 
spec:
    # 连接到命名空间
    scope: Namespaced 
  # 特定的 API 组
  group: k8spatterns.io 
  # 初始版本
  version: v1
    names:
      # 此 CRD 特定的类别
        kind: ConfigWatcher 
    # kubectl 等工具中使用的资源标签
    singular: configwatcher 
    plural: configwatchers
    validation: 
      # 此 CRD 的 OpenAPI V3 模式规范
      openAPIV3Schema:
            properties: 
          spec:
                    properties: 
              configMap:
                            type: string
                            description: "Name of the ConfigMap" 
            podSelector:
                            type: object
                            description: "Label selector for Pods" 
                 additionalProperties:
                                type: string

为了让我们的 Operator 能够管理这种类型的自定义资源,我们需要将具有适当权限的 ServiceAccount 附加到我们 Operator 的部署中。为了完成这项任务,我们引入了一个专用的 Role,稍后在 RoleBinding 中使用该 Role 将其附加到例 23-7 中的 ServiceAccount 上。

# 例 23-7 允许访问自定义资源的角色定义

---
apiVersion: rbac.authorization.k8s.io/v1 
kind: Role
metadata:
    name: config-watcher-crd 
rules:
- apiGroups:
    - k8spatterns.io
    resources:
    - configwatchers
    - configwatchers/finalizers
    verbs: [ get, list, create, update, delete, deletecollection, watch ]

有了这些 CRD,我们现在可以像例 23-5 那样定义自定义资源。为了了解这些资源的意义,我们必须实现一个控制器,它可以评估
这些资源,并在 ConfigMap 改变时触发 Pod 重启。

我们在这里对例 22-2 中的 Controller 脚本进行扩展,并调整 Controller 脚本中的事件循环。

在 ConfigMap 更新的情况下,我们不检查特定的注解,而是对所有 ConfigWatcher 种类的资源进行查询,并检查修改后的 ConfigMap 是否包含为 configMap: 值。例 23-8 显示了调和循环。请参阅我们的 Git 仓库,了解完整的示例,其中还包括安装该操作符的详细说明。

# 例 23-8 WatchConfig 控制器调和循环

# 启动一个观察流,以观察给定命名空间的 ConfigMap 变化
curl -Ns $base/api/v1/${ns}/configmaps?watch=true | \ 
while read -r event
do
    type=$(echo "$event" | jq -r '.type')

  # 只检查是否有已修改的事件
  if [ $type = "MODIFIED" ]; then 

    watch_url="$base/apis/k8spatterns.io/v1/${ns}/configwatchers"
    config_map=$(echo "$event" | jq -r '.object.metadata.name')

        # 获取所有已安装的 ConfigWatcher 自定义资源的列表
    watcher_list=$(curl -s $watch_url | jq -r '.items[]')

        # 从该列表中提取引用该 ConfigMap 的所有 ConfigWatcher 元素
    watchers=$(echo $watcher_list | \
               jq -r "select(.spec.configMap == \"$config_map\") | .metadata.name")

        # 每找到一个 ConfigWatcher,通过选择器删除配置的 Pod
    # 为了清晰起见,这里省略了计算标签选择器以及删除 Pod 的逻辑
    for watcher in watchers; do 
      label_selector=$(extract_label_selector $watcher) 
      delete_pods_with_selector "$label_selector"
    done 
     fi
done

至于控制器示例,可以使用我们的示例 Git 仓库中提供的示例 Web 应用程序来测试该控制器。该 Deployment 的唯一不同之处在于,我们使用了一个未注释的 ConfigMap 来进行应用配置。

虽然我们的 Operator 功能相当强大,但也很明显,我们基于 Shell 脚本的操作员仍然相当简单,并没有涵盖边缘或错误情况。你可以在野外找到更多有趣的、生产级的例子。

Awesome Operators 有一个不错的真实世界的 Operator 列表,这些 Operator 都是基于本章所涉及的概念。我们已经看到了 Prometheus Operator 如何管理 Prometheus 安装。另一个基于 Go 的 Operator 是 etcd Operator,用于管理 etcd 键值存储和自动化操作任务,如数据库的备份和恢复。

如果您正在寻找一个用 Java 编程语言编写的 Operator,Strimzi Operator 是一个很好的例子,它可以在 Kubernetes 上管理像 Apache Kafka 这样的复杂消息系统。另一个好的起点是基于是 JVM Operator 工具包,它为在 Java 和基于 JVM 的语言(如 Groovy 或 Kotlin)中创建 Operator 提供了基础,还附带了一套实例。

一些讨论

虽然我们已经看到了如何扩展 Kubernetes 平台,但 Operator 仍然不是银弹。在使用 Operator 之前,你应该仔细观察你的用例,以确定它是否适合 Kubernetes 范式。

:::tips 在很多情况下,一个普通的 Controller 使用标准资源工作就足够了。这种方法的优点是,它不需要任何集群管理员权限来注册 CRD,但在安全或验证方面有其局限性。Operator 很适合对自定义的领域逻辑进行建模,它能很好地配合 Kubernetes 用反应式控制器处理资源的声明式方式。 :::

更具体地说,考虑在以下任何一种情况下为您的应用域使用带有 CRD 的 Operator。

  • 你希望紧密集成到已有的 Kubernetes 工具中,比如 kubectl
  • 您正处于一个绿地项目,您可以从头开始设计应用。
  • 你可以从 Kubernetes 的概念中受益,比如资源路径、API 组、API 验证,特别是命名空间。
  • 你希望已经有良好的客户端支持,通过 Watch、认证、基于角色的授权和元数据的选择器来访问 API。

如果你的自定义用例符合这些标准,但你需要更灵活地实现和持久化自定义资源,可以考虑使用自定义 API Server。但是,你也不应该将 Kubernetes 扩展点视为万能的金锤。

如果你的用例不是声明式的,如果要管理的数据不适合 Kubernetes 资源模型,或者你不需要与平台紧密集成,你可能最好编写你的独立 API,并用一个经典的 Service 或 Ingress 对象来暴露它。

Kubernetes 文档本身也有一章关于何时使用 Controller、Operator、API 聚合或自定义 API 实现的建议。

参考资料