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

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

特權欄位未由運營團隊指定。他們的控制器不知道它,他們的驗證 admission 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)在轉換和 admission 請求之後
    (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 版本的文件中進行了描述。