The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

Kubernetes 101:釐清 kubectl create 與 kubectl apply 的差異

kubectl 是 Kubernetes 的命令列工具 (CLI),主要用來幫助你管理 Kubernetes 叢集、部署應用程式、檢視與管理各種叢集中的各項資源與紀錄。而當我們想要建立資源時,經常會使用 kubectl createkubectl apply 來建立資源,如果單純要建立資源,這兩組命令的差異甚小,但為什麼要分兩個呢?真的是他字面上(建立/套用)的意思嗎?本篇文章我們就來探討這個問題。

kubectl create vs. kubectl apply

命令式(Imperative)與宣告式(Declarative)

我們在建立 Kubernetes 資源的時候,基本上分成兩種策略,而理解這兩者的差異,對未來要實現 GitOpsIaC 有著至關重要的影響,應該要徹底理解。

  1. 命令的方式建立資源(Imperative)

    為了一套系統建立多項不同的資源時,你可以透過一行一行的命令來建立資源,因為資源建立與啟動可能有其順序性,所以透過這種方式建立,你必須精準的理解資源建立的過程有什麼相依性。

    你必須知道要「做什麼」與「怎麼做」才能將系統完整建立起來,就如同我們「手動」建立資源一樣,只是這一連串的命令可以寫成 Shell Script 自動執行。

  2. 宣告的方式建立資源(Declarative)

    為了一套系統建立多項不同的資源時,你可以透過宣告一份資源的定義檔(YAML/JSON),直接將定義檔提供給平台或叢集,由叢集或平台自行決定該如何部署資源,而部署的順序也可以由系統自行分析與判斷。

    所以你只需要知道「要什麼」就好,剩下的由系統幫你完成。

兩種策略都可以完成服務的部署,在理解這兩者的差異後,你喜歡哪一種?

事實上,我們在實務上最常見的其實是 Imperative (命令式) 的作法。為什麼呢?因為一般人在建立資源時,比較缺乏資源的規劃,或是在資源建立後,經常會想到什麼就去改動什麼,系統必須遵循 IT 人員的「命令」去改變狀態。例如:我們需要一台 VM 就可能會先手動建立一台虛擬機器,然後再安裝軟體,然後再進行系統設定,這種一步一步操作的行為,就屬於 Imperative 的這種部署策略。

但是,你不覺得用「宣告」的方式很棒嗎?我直接說我「想要什麼」就好,就跟寫程式一樣,寫好程式後就編譯起來,理論上程式現在能跑,下次也肯定能跑啊。你只需要定義好資源的規格,剩下的都可以不用管!沒錯,理想很完美沒錯,但現實真的很骨感,因為想用「宣告」的方式來部署資源有一定的學習門檻,有些時候還需要學習一些「資源定義的專屬語言」才能開始定義,所以跟 Imperative 的方式相比,透過 Declarative 的方式可能會有更高的進入門檻。

兩個策略只能二選一嗎?也不是,實務上我蠻常看到兩種方式混用的。你可以將基本的服務定義透過 Declarative 的方式定義好,建立好資源之後,再透過 Imperative 的方式去微調。例如:因為運算資源不足的關係,我們可能會臨時「命令」叢集要調高 replicas 的數量,這種透過「命令」的方式手動調整叢集的作法,就屬於 Imperative (命令式) 的行為。抑或是因為服務運作異常,我們可能會「命令」叢集立刻砍掉某個 Pod 來讓服務重啟,這個行為當然也算 Imperative 方法。

不過,這種混合式的策略雖然可行,行為也合理,但它卻有一個嚴重的缺陷,那就是會有組態漂移(Configuration Drift)的問題。

組態漂移 (Configuration Drift)

我們想要透過宣告式(Declarative)的方式來管理服務,就是希望不要把心力與時間花在「做什麼」與「怎麼做」這件事情上,清楚的定義我們「要什麼」就好,更能幫助我們釐清架構,擁有更清晰的思路,不被繁瑣的細節所影響,讓我們可以更好的掌握 Infra 架構,做到 IaC (Infrastructure as Code) (基礎建設即代碼) 的成果,也就是把「基礎建設」當成「代碼」一樣來維護,大幅降低管理人員的認知負荷(Cognitive load)。

組態漂移通常意味著你對服務的「定義」與「現況」不同,導致服務容易失控,也不容易重建出完全一樣的環境。當你的叢集現況與定義差別甚大的時候,你就無法再透過 Declarative (宣告式) 的方式來管理資源了,也意味著你將失去對基礎建設的掌控,而 IaC 機制也會失效。

kubectl create 與 kubectl apply

我們常用的 kubectl create 命令,其實就屬於 Imperative 的作法,因為你明確的告知 Kubernetes 要「建立」一個資源,他不會紀錄建立資源的最後狀態,真的就是幫你建立資源而已。

我們常用的 kubectl apply 命令,其實就屬於 Declarative 的作法,因為你不用明確的告訴 Kubernetes 要如何建立一個資源,也不用管現在叢集中有沒有這個資源,他就是很單純的幫你建立起你想要的資源而已。如果目前沒有資源,Kubernetes 還會在你建立資源時,同時幫你建立一份快照(Snapshot),紀錄在資源的 .metadata.annotations.kubectl.kubernetes.io/last-applied-configuration 底下,你日後如果對 YAML 檔案進行更新,他就會去比對先前的版本與最近一次的套用版本,藉此計算出差異之處,並套用差異更新。

以下我用兩個例子來說明兩個命令之間的細微差異:

  1. 混用 kubectl createkubectl apply 的狀況

    建立資源 ( nginx.yaml )

    $ kubectl create -f nginx.yaml
    pod/nginx created
    

    這個新建的資源,其 metadata 的內容如下:

    apiVersion: v1
    kind: Pod
    metadata:
      annotations:
        cni.projectcalico.org/containerID: 0234111f7347e3ebdf5737dc2c81ccd376b12e552047b1d8899f692dd5f5fee5
        cni.projectcalico.org/podIP: 10.1.254.71/32
        cni.projectcalico.org/podIPs: 10.1.254.71/32
      creationTimestamp: "2022-10-20T16:00:28Z"
      labels:
        app: nginx
      name: nginx
      namespace: default
      resourceVersion: "162118"
      uid: 670a2341-d8fb-43c2-b991-768a3781f1b4
    

    套用資源

    $ kubectl apply -f nginx.yaml
    Warning: resource pods/nginx is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
    pod/nginx configured
    

    這裡將會出現一個警告訊息:

    Warning: resource pods/nginx is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.

    中文翻譯是這樣的:

    警告:資源 pods/nginx 缺少 kubectl apply 所需的 kubectl.kubernetes.io/last-applied-configuration 標注(annotation)。kubectl apply 只能用於由 kubectl create --save-configkubectl apply宣告式建立的資源。遺失的標注將會自動補上。

    我們可以用 kubectl get pod nginx -o yaml 命令查看一下這個資源的 metadata 內容:

    metadata:
      annotations:
        cni.projectcalico.org/containerID: 6f8ca18ef90deb9974e0675d26d659cc3846125828b21ef0e0dbaeae33b40b64
        cni.projectcalico.org/podIP: 10.1.254.72/32
        cni.projectcalico.org/podIPs: 10.1.254.72/32
        kubectl.kubernetes.io/last-applied-configuration: |
          {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"app":"nginx"},"name":"nginx","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"nginx","ports":[{"containerPort":80,"name":"http"}],"resources":{"limits":{"cpu":"200m","memory":"500Mi"},"requests":{"cpu":"100m","memory":"200Mi"}}}],"restartPolicy":"Always"}}
      creationTimestamp: "2022-10-20T16:02:47Z"
      labels:
        app: nginx
      name: nginx
      namespace: default
      resourceVersion: "162367"
      uid: 396b1d88-79f2-45a0-9036-d5073ffb8982
    

    他確實多了一個 kubectl.kubernetes.io/last-applied-configuration 標注,我們把內容展開排版如下:

    {
      "apiVersion": "v1",
      "kind": "Pod",
      "metadata": {
        "annotations": {},
        "labels": { "app": "nginx" },
        "name": "nginx",
        "namespace": "default"
      },
      "spec": {
        "containers": [
          {
            "image": "nginx",
            "name": "nginx",
            "ports": [{ "containerPort": 80, "name": "http" }],
            "resources": {
              "limits": { "cpu": "200m", "memory": "500Mi" },
              "requests": { "cpu": "100m", "memory": "200Mi" }
            }
          }
        ],
        "restartPolicy": "Always"
      }
    }
    

    說穿了,這份資料就是上一次套用資源定義的快照而已!

    由於 kubectl.kubernetes.io/last-applied-configuration 已經被自動補上,所以如果你再執行一次相同命令,就不會出現警告了!

    $ kubectl apply -f nginx.yaml
    pod/nginx configured
    

    刪除 nginx 資源

    $ kubectl delete -f nginx.yaml
    pod "nginx" deleted
    
  2. 使用 kubectl create --save-configkubectl apply 的狀況

    建立資源

    kubectl create --save-config -f nginx.yaml
    

    這裡的 --save-config 就是要建立 kubectl.kubernetes.io/last-applied-configuration 標注的意思

    metadata:
      annotations:
        cni.projectcalico.org/containerID: c78723ab3f170dc84859e2a66a139d6f3b88a6785be22e14812a8f40cc8fc5dd
        cni.projectcalico.org/podIP: 10.1.254.73/32
        cni.projectcalico.org/podIPs: 10.1.254.73/32
        kubectl.kubernetes.io/last-applied-configuration: |
          {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"app":"nginx"},"name":"nginx","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"nginx","ports":[{"containerPort":80,"name":"http"}],"resources":{"limits":{"cpu":"200m","memory":"500Mi"},"requests":{"cpu":"100m","memory":"200Mi"}}}],"restartPolicy":"Always"}}
      creationTimestamp: "2022-10-20T16:14:30Z"
      labels:
        app: nginx
      name: nginx
      namespace: default
      resourceVersion: "163219"
      uid: 3e5f3d0b-b279-46e7-9918-be93f0d6e983
    

    注意: 建立資源時,使用 kubectl create --save-config -f nginx.yamlkubectl apply -f nginx.yaml 並無二致,我自己比較習慣使用 kubectl apply 的命令,畢竟指令比較短,也比較好輸入。

    如果你重複使用一次 kubectl create 命令,就會得到以下錯誤訊息:

    $ kubectl create --save-config -f nginx.yaml
    Error from server (AlreadyExists): error when creating "nginx.yaml": pods "nginx" already exists
    

    比較正確的作法,就是日後都以 kubectl apply 來更新資源:

    $ kubectl apply -f nginx.yaml
    pod/nginx unchanged
    

    刪除 nginx 資源

    $ kubectl delete -f nginx.yaml
    pod "nginx" deleted
    

所以我應該使用 kubectl apply 嗎?

其實你只要手邊有 YAML 檔,應該都可以使用 kubectl apply 來建立資源,畢竟使用 Kubernetes 的人應該都會事先定義 YAML 檔,而且日後要更新資源也應該都會先修改 YAML 才套用更新為主,因此 kubectl create 的使用頻率應該是相當低才對。如果要用,也請記得加上 --save-config 參數。

難道 kubectl create 就真的一無是處嗎?也不是喔!因為我們在建立 Kubernetes 資源時,資源中有許多「預設值」會在建立時由 Kubernetes 自動產生,而這些預設值,在使用 kubectl apply 套用更新時,有些欄位(field)是無法套用的!簡單來說,你可以用 kubectl apply 來建立資源,但大多數欄位無法透過 kubectl apply 來套用更新,以下我用個簡短的例子說明:

注意: Kubernetes 在建立資源的過程有可能被 Admission Controllers 修改過,因此最終建立的資源不見得會跟你原始的 YAML 定義檔相同。

  1. 建立資源

    kubectl create --save-config -f nginx.yaml
    
  2. 備份資源到 nginx-dump.yaml (擁有完整的資源定義)

    kubectl get pod nginx -o yaml > nginx-dump.yaml
    
  3. 重建資源

    kubectl delete -f nginx.yaml
    kubectl create -f nginx-dump.yaml
    
  4. 更新資源

    kubectl apply -f nginx-dump.yaml
    

    這個步驟會發生錯誤,因為 kubectl apply 並不適用於更新「所有欄位」的資訊:

    Error from server (Conflict): error when applying patch:
    {"metadata":{"annotations":{"cni.projectcalico.org/containerID":"588fd79cad358cda17c776592aac4478c67777582cdb48a8c237f95a4f8a29f6","cni.projectcalico.org/podIP":"10.1.254.84/32","cni.projectcalico.org/podIPs":"10.1.254.84/32"},"creationTimestamp":"2022-10-20T16:47:03Z","resourceVersion":"165934","uid":"b182f40a-d990-43fd-b521-a91d4737c759"},"status":{"$setElementOrder/conditions":[{"type":"Initialized"},{"type":"Ready"},{"type":"ContainersReady"},{"type":"PodScheduled"}],"$setElementOrder/podIPs":[{"ip":"10.1.254.84"}],"conditions":[{"lastTransitionTime":"2022-10-20T16:47:03Z","type":"Initialized"},{"lastTransitionTime":"2022-10-20T16:47:06Z","type":"Ready"},{"lastTransitionTime":"2022-10-20T16:47:06Z","type":"ContainersReady"},{"lastTransitionTime":"2022-10-20T16:47:03Z","type":"PodScheduled"}],"containerStatuses":[{"containerID":"containerd://c5691b8efe12a031d38fb1af39d053088f452bb4ded4eca889f23c61de6507f1","image":"docker.io/library/nginx:latest","imageID":"docker.io/library/nginx@sha256:5ffb682b98b0362b66754387e86b0cd31a5cb7123e49e7f6f6617690900d20b2","lastState":{},"name":"nginx","ready":true,"restartCount":0,"started":true,"state":{"running":{"startedAt":"2022-10-20T16:47:05Z"}}}],"podIP":"10.1.254.84","podIPs":[{"ip":"10.1.254.84"}],"startTime":"2022-10-20T16:47:03Z"}}
    to:
    Resource: "/v1, Resource=pods", GroupVersionKind: "/v1, Kind=Pod"
    Name: "nginx", Namespace: "default"
    for: "nginx-dump.yaml": Operation cannot be fulfilled on pods "nginx": the object has been modified; please apply your changes to the latest version and try again
    

看到這裡,你應該可以很清楚的知道 kubectl createkubectl apply 的使用時機了,以後應該就不會用錯囉! 👍

相關連結