kubectl 是 Kubernetes 的命令列工具 (CLI),主要用來幫助你管理 Kubernetes 叢集、部署應用程式、檢視與管理各種叢集中的各項資源與紀錄。而當我們想要建立資源時,經常會使用 kubectl create
或 kubectl apply
來建立資源,如果單純要建立資源,這兩組命令的差異甚小,但為什麼要分兩個呢?真的是他字面上(建立/套用)的意思嗎?本篇文章我們就來探討這個問題。
命令式(Imperative)與宣告式(Declarative)
我們在建立 Kubernetes 資源的時候,基本上分成兩種策略,而理解這兩者的差異,對未來要實現 GitOps 或 IaC 有著至關重要的影響,應該要徹底理解。
-
以命令的方式建立資源(Imperative)
為了一套系統建立多項不同的資源時,你可以透過一行一行的命令來建立資源,因為資源建立與啟動可能有其順序性,所以透過這種方式建立,你必須精準的理解資源建立的過程有什麼相依性。
你必須知道要「做什麼」與「怎麼做」才能將系統完整建立起來,就如同我們「手動」建立資源一樣,只是這一連串的命令可以寫成 Shell Script 自動執行。
-
以宣告的方式建立資源(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 檔案進行更新,他就會去比對先前的版本與最近一次的套用版本,藉此計算出差異之處,並套用差異更新。
以下我用兩個例子來說明兩個命令之間的細微差異:
-
混用 kubectl create
與 kubectl 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-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
標注,我們把內容展開排版如下:
{
"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
-
使用 kubectl create --save-config
或 kubectl 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.yaml
與 kubectl 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 定義檔相同。
-
建立資源
kubectl create --save-config -f nginx.yaml
-
備份資源到 nginx-dump.yaml
(擁有完整的資源定義)
kubectl get pod nginx -o yaml > nginx-dump.yaml
-
重建資源
kubectl delete -f nginx.yaml
kubectl create -f nginx-dump.yaml
-
更新資源
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
的使用時機了,以後應該就不會用錯囉! 👍
相關連結