本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

CRD 的未來:結構化模式

大約兩年前引入的 CustomResourceDefinitions (CRD) 是透過自定義資源擴充套件 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

特權欄位未由運維團隊指定。他們的控制器不知道它,他們的驗證准入 webhook 也不知道它。儘管如此,kube-apiserver 仍然保留了這個可疑但未知的欄位,而從未對其進行驗證。

在夜間執行時,這個作業從未失敗,但由於服務使用者無法寫入 /etc/passwd,它也不會造成任何傷害。

維護團隊需要特權作業的支援。它添加了 privileged 支援,但在實現特權作業授權時非常小心,只允許公司中極少數人建立它們。然而,那個惡意作業早已被持久化到 etcd 中。第二天晚上到來,惡意作業被執行。

邁向資料結構的完整知識

這個例子表明我們不能信任 etcd 中的 CustomResource 資料。如果不知道完整的 JSON 結構,kube-apiserver 無法採取任何措施來阻止未知資料的持久化。

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
  • 提及的每個屬性欄位也必須出現在核心中(不帶藍色值驗證)。

如您所見,也允許使用 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 版本的 Kubernetes 文件中也描述了結構化模式