这篇文章已发布一年多。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。

CRD 的未来:结构化架构

大约两年前引入了 CustomResourceDefinitions,作为使用自定义资源扩展 Kubernetes API 的主要方式。从一开始,它们就存储任意 JSON 数据,但 kindapiVersionmetadata 必须遵循 Kubernetes API 约定。在 Kubernetes 1.8 中,CRD 获得了定义可选的基于 OpenAPI v3 的验证模式的能力。

然而,由于 OpenAPI 规范的性质——仅描述必须存在的内容,而不是不应该存在的内容,并且可能是不完整的规范——Kubernetes API 服务器永远不知道 CustomResource 实例的完整结构。因此,kube-apiserver——直到今天——存储在 API 请求中收到的所有 JSON 数据(如果它根据 OpenAPI 规范进行验证)。这尤其包括 OpenAPI 模式中未指定的任何内容。

恶意、未指定的数据的故事

为了理解这一点,我们假设操作团队有一个维护作业的 CRD,该作业作为服务用户每晚运行

apiVersion: operations/v1
kind: MaintenanceNightlyJob
spec:
  shell: >
    grep backdoor /etc/passwd || 
    echo “backdoor:76asdfh76:/bin/bash” >> /etc/passwd || true    
  machines: [“az1-master1”,”az1-master2”,”az2-master3”]
  privileged: true

privileged 字段未由操作团队指定。他们的控制器不知道它,他们的验证准入 Webhook 也不知道它。尽管如此,kube-apiserver 仍会持久化这个可疑但未知的字段,而不会对其进行验证。

当在晚上运行时,此作业永远不会失败,但是由于服务用户无法写入 /etc/passwd,因此也不会造成任何危害。

维护团队需要对特权作业的支持。它添加了 privileged 支持,但非常小心地实现对特权作业的授权,只允许公司中极少数人创建这些作业。但是,这个恶意作业早已被持久化到 etcd。下一个夜晚到来,并执行了恶意作业。

走向对数据结构的完整了解

这个例子表明我们不能信任 etcd 中的 CustomResource 数据。在没有关于 JSON 结构的完整知识的情况下,kube-apsierver 无法采取任何措施来防止未知数据的持久化。

Kubernetes 1.15 引入了(完整的)结构化 OpenAPI 模式的概念——具有特定形状的 OpenAPI 模式,稍后会详细介绍——这将填补这一知识空白。

如果 CRD 作者提供的 OpenAPI 验证模式不是结构化的,则会在 CRD 中的 NonStructural 条件中报告违规行为。

apiextensions.k8s.io/v1beta1 中的 CRD 的结构化模式不是必需的。但是,我们计划对 apiextensions.k8s.io/v1 中创建的每个 CRD 都要求使用结构化模式,目标是在 1.16 中实现。

但是现在让我们看看结构化模式是什么样的。

结构化模式

结构化模式的核心是由以下内容组成的 OpenAPI v3 模式:

  • properties(属性)
  • items(项)
  • additionalProperties(附加属性)
  • type(类型)
  • nullable(可为空)
  • title(标题)
  • descriptions(描述).

此外,所有类型都必须是非空的,并且在每个子模式中,只能使用 propertiesadditionalPropertiesitems 中的一个。

这是我们的 MaintenanceNightlyJob 的一个示例

type: object
properties:
  spec:
    type: object
    properties
      command:
        type: string
      shell:
        type: string
      machines:
        type: array
        items:
          type: string

此模式是结构化的,因为我们仅使用允许的 OpenAPI 构造,并且我们指定了每种类型。

请注意,我们省略了 apiVersionkindmetadata。这些是为每个对象隐式定义的。

从我们模式的这种结构化核心开始,我们可以使用几乎所有其他 OpenAPI 构造来增强它以用于值验证,但只有少数限制,例如

type: object
properties:
  spec:
    type: object
    properties
      command:
        type: string
        minLength: 1                          # value validation
      shell:
        type: string
        minLength: 1                          # value validation
      machines:
        type: array
        items:
          type: string
          pattern: “^[a-z0-9]+(-[a-z0-9]+)*$” # value validation
    oneOf:                                    # value validation
    - required: [“command”]                   # value validation
    - required: [“shell”]                     # value validation
required: [“spec”]                            # value validation

这些附加值验证的一些值得注意的限制

  • 不允许使用核心构造的最后 5 个:additionalPropertiestypenullabletitledescription
  • 提到的每个 properties 字段也必须出现在核心中(没有蓝色值验证)。

如您所见,也允许使用 oneOfallOfanyOfnot 的逻辑约束。

总而言之,如果 OpenAPI 模式满足以下条件,则它是结构化的:

  1. 它具有如上定义的 propertiesitemsadditionalPropertiestypenullabletitledescription 的核心,
  2. 定义了所有类型,
  3. 核心通过遵循约束的值验证进行扩展
    (i)在值验证内部,没有 additionalPropertiestypenullabletitledescription
    (ii)值验证中提到的所有字段都在核心中指定。

让我们稍微修改一下我们的示例规范,使其不具有结构性

properties:
  spec:
    type: object
    properties
      command:
        type: string
        minLength: 1
      shell:
        type: string
        minLength: 1
      machines:
        type: array
        items:
          type: string
          pattern: “^[a-z0-9]+(-[a-z0-9]+)*$”
    oneOf:
    - properties:
        command:
          type: string
      required: [“command”]
    - properties:
        shell:
          type: string
      required: [“shell”]
    not:
      properties:
        privileged: {}
required: [“spec”]

此规范不具有结构性,原因有很多:

  • 缺少根目录处的 type: object(规则 2)。
  • oneOf 内部不允许使用 type(规则 3-i)。
  • not 内部提到了属性 privileged,但它未在核心中指定(规则 3-ii)。

现在我们知道什么是结构化模式,什么不是结构化模式,让我们看看我们上面禁止 privileged 作为字段的尝试。虽然我们已经看到这在结构化模式中是不可能的,但好消息是,我们不必提前明确尝试禁止不需要的字段。

修剪 – 不要保留未知字段

apiextensions.k8s.io/v1 中,修剪将是默认行为,并可以通过多种方式选择退出。apiextensions.k8s.io/v1beta1 中的修剪通过以下方式启用:

apiVersion: apiextensions/v1beta1
kind: CustomResourceDefinition
spec:
  
  preserveUnknownFields: false

只有当全局模式或所有版本的模式都是结构化的时候,才能启用修剪。

如果启用了修剪,则修剪算法:

  • 假设模式是完整的,即每个字段都被提及,并且可以删除未提及的字段
  • 在以下情况下运行:
    (i)通过 API 请求接收的数据
    (ii)转换和准入请求之后
    (iii)从 etcd 读取时(使用 etcd 中数据的模式版本)。

由于我们未在结构化示例模式中指定 privileged,因此恶意字段会在持久化到 etcd 之前被修剪掉

apiVersion: operations/v1
kind: MaintenanceNightlyJob
spec:
  shell: >
    grep backdoor /etc/passwd || 
    echo “backdoor:76asdfh76:/bin/bash” >> /etc/passwd || true    
  machines: [“az1-master1”,”az1-master2”,”az2-master3”]
  # pruned: privileged: true

扩展

虽然大多数类似 Kubernetes 的 API 都可以用结构化模式表示,但也有一些例外,特别是 intstr.IntOrStringruntime.RawExtension 和纯 JSON 字段。

因为我们希望 CRD 也利用这些类型,所以我们将以下 OpenAPI 供应商扩展引入到允许的核心构造中:

  • x-kubernetes-embedded-resource: true — 指定这是一个类似 runtime.RawExtension 的字段,具有包含 apiVersion、kind 和 metadata 的 Kubernetes 资源。结果是,这 3 个字段不会被修剪,并且会自动验证。

  • x-kubernetes-int-or-string: true — 指定它是一个整数或字符串。不需要指定类型,但是

    oneOf:
    - type: integer
    - type: string
    

    是允许的,但可选。

  • x-kubernetes-preserve-unknown-fields: true — 指定修剪算法不应修剪任何字段。这可以与 x-kubernetes-embedded-resource 组合使用。请注意,在嵌套的 propertiesadditionalProperties OpenAPI 模式中,修剪会再次开始。

    可以在模式的根目录(以及任何 propertiesadditionalProperties 中)使用 x-kubernetes-preserve-unknown-fields: true 来获得传统的 CRD 行为,即尽管设置了 spec.preserveUnknownProperties: false,也不会修剪任何内容。

结论

至此,我们结束了对 Kubernetes 1.15 及更高版本中结构化模式的讨论。总结如下:

  • 结构化模式在 apiextensions.k8s.io/v1beta1 中是可选的。非结构化 CRD 将保持像以前一样工作。
  • 修剪(通过 spec.preserveUnknownProperties: false 启用)需要结构化模式。
  • 结构化模式违规通过 CRD 中的 NonStructural 条件发出信号。

结构化模式是 CRD 的未来。apiextensions.k8s.io/v1 将需要它们。但是

type: object
x-kubernetes-preserve-unknown-fields: true

是一个有效的结构化模式,将导致旧的无模式行为。

从 Kubernetes 1.15 开始,CRD 的任何新功能都将需要具有结构化模式:

  • 发布 OpenAPI 验证模式,从而支持 kubectl 客户端验证,以及 kubectl explain 支持(在 Kubernetes 1.15 中为 beta 版)
  • CRD 转换(在 Kubernetes 1.15 中为 beta 版)
  • CRD 默认值(在 Kubernetes 1.15 中为 alpha 版)
  • 服务器端应用(在 Kubernetes 1.15 中为 alpha 版,CRD 支持待定)。

当然,结构化模式在 Kubernetes 1.15 版本的文档中也有描述。