﻿# Kubernetes 101：釐清 kubectl create 與 kubectl apply 的差異

[kubectl](https://web.archive.org/web/20230315023812/https://www.containiq.com/post/kubectl-cheat-sheet) 是 Kubernetes 的命令列工具 (CLI)，主要用來幫助你管理 Kubernetes 叢集、部署應用程式、檢視與管理各種叢集中的各項資源與紀錄。而當我們想要建立資源時，經常會使用 `kubectl create` 或 `kubectl apply` 來建立資源，如果單純要建立資源，這兩組命令的差異甚小，但為什麼要分兩個呢？真的是他字面上(建立/套用)的意思嗎？本篇文章我們就來探討這個問題。

![kubectl create vs. kubectl apply](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/65e1ed8ea38a9233ccdd-197440718-12701d81-b759-4dd6-81f3-2b9fcc7d3508.jpg)

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

我們在建立 Kubernetes 資源的時候，基本上分成兩種策略，而理解這兩者的差異，對未來要實現 [GitOps](https://www.gitops.tech/) 或 [IaC](https://www.trendmicro.com/zh_tw/what-is/cloud-security/infrastructure-as-code.html) 有著至關重要的影響，應該要徹底理解。

1.  以**命令**的方式建立資源(Imperative)
    
    為了一套系統建立多項不同的資源時，你可以透過一行一行的命令來建立資源，因為資源建立與啟動可能有其順序性，所以透過這種方式建立，你必須精準的理解資源建立的過程有什麼相依性。
    
    你必須知道要「做什麼」與「怎麼做」才能將系統完整建立起來，就如同我們「手動」建立資源一樣，只是這一連串的命令可以寫成 Shell Script 自動執行。
    
2.  以**宣告**的方式建立資源(Declarative)
    
    為了一套系統建立多項不同的資源時，你可以透過宣告一份資源的定義檔(YAML/JSON)，直接將定義檔提供給平台或叢集，由叢集或平台自行決定該如何部署資源，而部署的順序也可以由系統自行分析與判斷。
    
    所以你只需要知道「要什麼」就好，剩下的由系統幫你完成。
    

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

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

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

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

不過，這種混合式的策略雖然可行，行為也合理，但它卻有一個嚴重的缺陷，那就是會有**組態漂移**([Configuration Drift](https://blog.netwrix.com/2022/06/30/what-is-configuration-drift/))的問題。

### 組態漂移 (Configuration Drift)

我們想要透過**宣告式**(Declarative)的方式來管理服務，就是希望不要把心力與時間花在「做什麼」與「怎麼做」這件事情上，清楚的定義我們「要什麼」就好，更能幫助我們釐清架構，擁有更清晰的思路，不被繁瑣的細節所影響，讓我們可以更好的掌握 Infra 架構，做到 **IaC** (Infrastructure as Code) (基礎建設即代碼) 的成果，也就是把「基礎建設」當成「代碼」一樣來維護，大幅降低管理人員的[認知負荷](https://zh.wikipedia.org/wiki/%E8%AA%8D%E7%9F%A5%E8%B2%A0%E8%8D%B7)([Cognitive load](https://en.wikipedia.org/wiki/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 create` 與 `kubectl apply` 的狀況
    
    建立資源 ( [nginx.yaml](https://gist.github.com/doggy8088/174f4a1612306714d2f9653e612aebab) )
    
    ```bash
    $ 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
    ```
    
    套用資源
    
    ```bash
    $ 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-config` 或 `kubectl 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` 標注，我們把內容展開排版如下：
    
    ```json
    {
      "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` 已經被自動補上，所以如果你再執行一次相同命令，就不會出現警告了！
    
    ```bash
    $ kubectl apply -f nginx.yaml
    pod/nginx configured
    ```
    
    刪除 `nginx` 資源
    
    ```bash
    $ kubectl delete -f nginx.yaml
    pod "nginx" deleted
    ```
    
2.  使用 `kubectl create --save-config` 或 `kubectl apply` 的狀況
    
    建立資源
    
    ```bash
    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.yaml` 與 `kubectl apply -f nginx.yaml` 並無二致，我自己比較習慣使用 `kubectl apply` 的命令，畢竟指令比較短，也比較好輸入。
    
    如果你重複使用一次 `kubectl create` 命令，就會得到以下錯誤訊息：
    
    ```bash
    $ kubectl create --save-config -f nginx.yaml
    Error from server (AlreadyExists): error when creating "nginx.yaml": pods "nginx" already exists
    ```
    
    比較正確的作法，就是日後都以 `kubectl apply` 來更新資源：
    
    ```bash
    $ kubectl apply -f nginx.yaml
    pod/nginx unchanged
    ```
    
    刪除 `nginx` 資源
    
    ```bash
    $ 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](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) 修改過，因此最終建立的資源不見得會跟你原始的 YAML 定義檔相同。

1.  建立資源
    
    ```bash
    kubectl create --save-config -f nginx.yaml
    ```
    
2.  備份資源到 `nginx-dump.yaml` (擁有完整的資源定義)
    
    ```bash
    kubectl get pod nginx -o yaml > nginx-dump.yaml
    ```
    
3.  重建資源
    
    ```bash
    kubectl delete -f nginx.yaml
    kubectl create -f nginx-dump.yaml
    ```
    
4.  更新資源
    
    ```bash
    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 create` 與 `kubectl apply` 的使用時機了，以後應該就不會用錯囉！ 👍

### 相關連結

-   Kubectl Apply vs. Create
-   [何謂基礎架構程式碼 (IaC)？](https://www.trendmicro.com/zh_tw/what-is/cloud-security/infrastructure-as-code.html)
-   [Understanding and Preventing Configuration Drift](https://blog.netwrix.com/2022/06/30/what-is-configuration-drift/)
