想列印k8s資源YAML結果搞懂了Client-Side & Server-Side Apply

YOYOFx發表於2023-05-09

前言

由於檢視k8s資源YAML時常看到沉長的YAML與手寫的格式,相差甚遠不利於閱讀,經過探索官方文件,才理解什麼是Client-Side & Server-Side Apply。

先看一下我用client-go在生成Deployment的YAML格式,核心程式碼如下:

k8sDeployment, _ := clientSet.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
k8sDeployment.Kind = "Deployment"
k8sDeployment.APIVersion = "apps/v1"
k8sDeployment.SetManagedFields(nil)  // 不顯示管理欄位,至於什麼是管理欄位? 請繼續閱讀後面的內容.
runtimeWorkload = k8sDeployment
yamlPrinter := printers.YAMLPrinter{}
buffers := bytes.NewBufferString("")
yamlErr := yamlPrinter.PrintObj(runtimeWorkload, buffers)
YAML := buffers.String()

效果:

之前一直有這樣一個困擾,例如這樣的一個Pod資源,可以看到像kubectl.kubernetes.io/last-applied-configuration annotationmanagedFields 這兩個欄位,並不是我們手寫YAML中存在的,他們是什麼呢?

用一個例子展現:

kubectl get pods demo -o yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"run":"demo"},"name":"demo","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"demo","resources":{}}],"dnsPolicy":"ClusterFirst","restartPolicy":"Always"},"status":{}}
  creationTimestamp: "2022-05-28T07:28:51Z"
  labels:
    run: demo
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:kubectl.kubernetes.io/last-applied-configuration: {}
        f:labels:
          .: {}
          f:run: {}
....

由這兩個欄位,引出本文的兩位主角,Client-Side Apply(以下簡稱CSA)和Server-Side Apply(以下簡稱SSA)

  1. kubectl.kubernetes.io/last-applied-configuration是使用kubectl apply進行Client-Side Apply時,由kubectl自行填充的。
  2. managedFields 則是由kubectl apply的增強功能 Server-Side Apply 的引入而新增。

Client-Side Apply

kubectl apply是一種宣告示的K8S物件管理方式,是我們最常用的應用部署,升級方式之一。

需要特別指出的是,kubectl apply宣告的僅僅是它關心的欄位的狀態,而不是整個物件的真實狀態。apply表達的意思是:此次提交管理的欄位應該和apply的配置檔案一致,其它欄位並不關心。

比如首次部署時,K8S會將replicas值設定為預設1,隨後由HPA控制器擴容到合適的副本數。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  # replicas: 1 不要設定replicas
  selector:
    matchLabels:
      app: nginx
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:latest
        name: nginx
        resources: {}

當升級應用時(修改映象版本),修改配置檔案中的image欄位,再次執行kubectl apply。此時kubectl apply只會影響映象版本 ,而不會影響HPA控制器設定的副本數。在這個例子中,replicas欄位不是kubectl apply管理的欄位,因此更新映象時不會被刪除,避免了每次應用升級時,副本數都會被重置。

在上述例子中,為了能識別出replicas不是kubectl管理的欄位,kubectl需要一個標識,用來追蹤物件中哪些欄位是由kubectl apply管理的,而這個標識就是last-applied-configuration。
該annotation是在kubectl apply時,由kubectl客戶端自行填充——每次執行kubectl apply時(未啟用SSA),kubectl會將本次apply的配置檔案全量的記錄在last-applied-configurationannotation中,用於追蹤哪些欄位由kubectl apply管理。

CSA工作工作機制:

Apply一個資源物件時,如果該物件不存在,則建立它(同時寫入last-applied-configuration)。如果物件已經存在,則kubectl需要根據以下三個狀態:

  • 當前配置檔案所表示的物件在叢集中的真實狀態。(修改物件前先Get一次)當前apply的配置。以及上次apply的配置。 (在last-applied-configuration裡)。
  • 當前apply的配置。
  • 以及上次apply的配置。 (在last-applied-configuration裡)

計算patch

透過patch方式進行更新(而不是將配置檔案全量的傳送到服務端)。patch報文的計算方法如下:

  1. 計算需要被刪除的欄位。如果欄位存在在last-applied-configuration中,但配置檔案中沒有,將刪除它們。
  2. 計算需要修改或新增的欄位。如果配置檔案中的欄位與真實狀態不一致,則新增或修改它們。
  3. 對於那些last-applied-configuration中不存在的欄位,不要修改它們(例如上述示例中的replicas欄位)。

CSA的合併策略

詳細的patch計算示例可參考K8S文件中給出的詳細示例

由此可見,last-applied-configuration體現的是一種ownership的關係,表示哪些欄位是由kubectl管理,它是kubectl apply時,計算patch報文的依據。

Server-Side Apply

Server-Side Apply 是另一種宣告式的物件管理方式,和CSA的作用是基本一致的。SSA始於從1.14開始釋出alpha版本,到1.16beta,到1.18beta2,終於在1.22升級為GA。
它協助使用者、控制器透過宣告式配置的方式管理他們的資源。 客戶端可以傳送完整描述的目標(A fully specified intent), 宣告式地建立和修改物件。

顧名思義,SSA將物件合併的邏輯轉移到了服務端(APIServer),客戶端只需提交完整的配置檔案,剩下的工作交給服務端處理。 在kubectl中使用SSA,只需在kubectl apply時加上--server-side引數即可,例如這樣:

kubectl apply --server-side=true -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-server-side-apply
data:
  a: "a"
  b: "b"
EOF

部署成功後,檢視物件會發現該物件中不再存在之前CSA中的last-applied-configuration,取而代之的是metadata.managedFields。檢視上面apply建立的資源:

apiVersion: v1
data:
  a: a
  b: b
kind: ConfigMap
metadata:
  creationTimestamp: "2022-12-04T07:59:24Z"
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:data:
        f:a: {}
        f:b: {}
    manager: kubectl
    operation: Apply
    time: "2022-12-04T07:59:24Z"
  name: test-server-side-apply
  namespace: default
  resourceVersion: "1304750"
  uid: d265df3d-b9e9-4d0f-91c2-e654f850d25a

欄位管理(field management)機制追蹤物件欄位的變化。 當一個欄位值改變時,其所有權從當前管理器(manager)轉移到施加變更的管理器。 當嘗試將新配置應用到一個物件時,如果欄位有不同的值,且由其他管理器管理, 將會引發衝突。 衝突引發警告訊號:此操作可能抹掉其他協作者的修改。 衝突可以被刻意忽略,這種情況下,值將會被改寫,所有權也會發生轉移。

SSA的合併策略

在介紹SSA的合併策略前,我們先了解一下CSA的合併策略。CSA的合併規則是基於Kubernetes的strategic merge patch方式,不同的欄位型別分別有各自不同的合併策略,規則比較複雜。這也導致了CSA容易產生更多的Bug。SSA針對這個問題做了最佳化,相較於CSA,SSA定義了更加規範和準確的合併規則。 這裡抄錄文件中的一段表格加以說明:

SSA的優點

簡化客戶端邏輯,CSA是一個很重的客戶端邏輯,裡面有複雜的物件合併操作,這意味著apply這項操作和kubectl是深度繫結的,使用其他客戶端或者在控制器(Controller)中難以使用apply方式來配置物件。而SSA將這些合併的邏輯轉移到了服務端,提供單一的API,客戶端實現方式得以簡化。這讓apply的能力得以整合到client-go中,讓應用可以透過client-go來使用apply的能力。

更細粒度的欄位所有權管理,減少錯誤覆蓋配置的可能性

相比於last-applied-configuration,SSA使用managedFields來管理每個欄位的ownership,這是一種更細粒度的欄位管理方式。這使得多個管理者之間能更好的協作,且其自帶衝突檢測,能很大程度避免錯誤覆蓋配置的發生。

當使用SSA時,dry-run的邏輯也放在服務端執行。相比CSA,服務端dry-run可以真實的經過validating/mutating admission webhooks的校驗,從而獲取最準確的返回結果。這是CSA無法實現的。

總之CSA和SSA是兩種不同實現的宣告示管理Kubernetes物件的方式。SSA的出現是為了解決了CSA中存在的一些挑戰與問題,如apply邏輯和kubectl深度繫結、strategic merge patch複雜多bug等等。SSA發展至今已是Kubernetes中的一個關鍵特性,相信其最終的目標將會是完全取代CSA,成為Kubernetes中唯一的apply方式。