原文位於 https://github.com/huazhihao/kubespy/blob/master/implement-a-k8s-debug-plugin-in-bash.md
背景
Kubernetes調試的最大痛點是精簡過的容器鏡像里沒有日常的調試工具。背后的原因是精簡容器鏡像本身就是容器技術的最佳實踐之一。nginx的容器鏡像甚至不包含ps和curl這種最基礎的工具。這種完全服務於生產環境的策略無異於過早優化,但受制於immutable infrastructure的基本思想和CI/CD實際操作的雙重制約,你無法在生產環境發布一個和開發環境不同的容器鏡像。這使得這一過早優化的結果更加災難化。解決這個問題的關鍵在於,能否在不侵入式的修改容器鏡像的情況下,向目標容器里加載需要的調試工具。例如,類似於istio之類的解決方案可以向目標pod插入一個sidecar容器。當然這里的權限要求是高於sidecar容器的,因為pod中的各個容器雖然共享network,但pid和ipc是不共享的。此外,sidecar容器是無法被加入一個已經創建出來的pod,而我們希望工具容器可以在運行時被動態插入,因為問題的產生是隨機的,你不能完全預測需要加載哪些工具。
Kubernetes社區很早有相關的issue和proposal但並沒有最后最終被upstream接受。
目前官方給出最接近的方案是Ephemeral Containers和shareProcessNamespace。前者允許你在運行時在一個pod里插入一個短生命周期的容器(無法擁有livenessProbe, readinessProbe),而后者允許你共享目標pod內容器的network,pid,ipc等cgroups namespace(注意,此namespace非Kubernetes的namespace)甚至修改其中的環境。目前Ephemeral Containers
還在v1.17 alpha階段。而且shareProcessNamespace
這個spec要求在創建pod的時候就必須顯式啟用,否則運行時無法修改。
kubespy (https://github.com/huazhihao/kubespy)是一個完全用bash實現的Kubernetes調試工具,它不但完美的解決了上面提到的如何在運行時向目標容器加載工具的問題,而且並不依賴任何最新版本的Kubernetes的特性。這篇文章稍后會介紹如何使用kubespy
來動態調試,以及kubespy
是如何通過kubectl,docker以及chrootl來構建這條調試鏈的,最后會簡單分析一下關鍵的代碼實現。
安裝
你可以直接從代碼安裝,因為kubespy
是完全bash實現的,所以可以直接拷貝文件來執行。
$ curl -so kubectl-spy https://raw.githubusercontent.com/huazhihao/kubespy/master/kubespy
$ sudo install kubectl-spy /usr/local/bin/
你也可以從krew
來安裝。krew
是一個kubernetes-sigs
孵化中的kubectl
插件包管理工具,帶有准官方性質。
$ kubectl krew install spy
使用
安裝過后,kubespy
可以成為一個kubectl
的子命令被執行。你可以指定目標pod為參數。如果目標pod有多個容器,你可以通過-c
指定具體的容器。你也可以指定加載的工具容器的鏡像,默認是busybox:latest
$ kubectl spy POD [-c CONTAINER] [--spy-image SPY_IMAGE]
你可以通過以下這個demo來快速體驗如何調試一個鏡像為nginx的pod。nginx鏡像不包含ps或者任何網絡工具。而加載的工具容器的鏡像為busybox,不但可以訪問原容器的文件系統和進程樹,甚至可以殺進程,修改文件。當然發http請求更是沒有問題。
工作原理
kubespy
的工作原理大致可以用以下這個流程圖來展示。
local machine: kubectl spy [1]
|
v
master node: kube-apiserver [2]
|
v
worker node: kubelet [3]
|
v
spy pod (eg. busybox) [4]
| (chroot)
v
docker runtime [5]
| (run)
v
spy container [6]
| (join docker namespace: pid/net/ipc)
v
application pod (eg. nginx) [7]
概要的看,kubespy
是通過以下這些步驟構建調試連的,上圖的步驟數字可以與下文對應
[1] `kubespy`作為`kubectl`的插件被執行,可以向`master node`上的`kube-apiserver`發出api請求,會先取得目標容器的關鍵信息,如其所在的`worker node`和pid/net/ipc 等cgroups namespace
[2] `kube-apiserver`將具體的命令分發給目標容器所在的`worker node`上的agent`kubelet`執行
[3] `kubelet`創建一個`busybox`作為`spy pod`
[4] `spy pod`mount了`worker node`的根目錄,並通過`chroot`取得了worker node的控制權
[5] `spy pod`控制了docker cli創建了工具容器(`spy container `)
[6] 工具容器被加入目標容器的pid/net/ipc等cgroups namespace
[7] 用戶通過`kubectl`被attach到工具容器的tty里,可以對目標容器進行調試甚至是修改
關鍵代碼
如何取得目標容器的pid/net/ipc等cgroups namespace
if [[ "${co}" == "" ]]; then cid=$(kubectl get pod "${po}" -o='jsonpath={.status.containerStatuses[0].containerID}' | sed 's/docker:\/\///') else cid=$(kubectl get pod "${po}" -o='jsonpath={.status.containerStatuses[?(@.name=="'"${co}"'")].containerID}' | sed 's/docker:\/\///') fi
根據kubectl
的convention,如果用戶未指定容器,我們默認以目標pod的第一個容器作為目標容器。kubelet
在為kubernetes集群創建容器時,會相應的創建以containerID命名的pid/net/ipc等cgroups namespace。cgroups namespace是docker實現user space隔離的基礎原理,可以參考https://docs.docker.com/engine/docker-overview/#namespaces 進行了解。
如何獲得目標容器所在worker node以及其docker cli的控制權
"volumes": [ { "name": "node", "hostPath": { "path": "/" } } ]
spy pod
會將worker node
的根目錄作為volume來mount在/host
{
"name": "spy", "image": "busybox", "command": [ "/bin/chroot", "/host"], "args": [ "docker", "run", "-it", "--network=container:'"${cid}"'", "--pid=container:'"${cid}"'", "--ipc=container:'"${cid}"'", "'"${ep}"'" ], "stdin": true, "stdinOnce": true, "tty": true, "volumeMounts": [ { "mountPath": "/host", "name": "node" } ] }
然后在通過busybox
里的chroot,將worker node
的根目錄作為自己的根目錄。而docker cli也將被直接暴露出來。
在獲取了目標容器的pid/net/ipc等cgroups namespace之后,即可直接創建工具容器共享目標容器的cgroups namespace。此時,目標容器和目標容器在進程樹,網絡空間,內部進程通訊等,都是沒有任何隔離的。
如何將用戶的terminal帶入目標容器中
kubectl run -it
,chroot
和docker run -it
都是可以attach到目標的tty中的,這些命令鏈接起來,像一系列跳板,把用戶的terminal一層層帶入下一個,最終帶入目標容器中。
小結
kubespy (https://github.com/huazhihao/kubespy) 用bash實現對kubernetes集群中的pod通過動態加載工具容器來調試,彌補了目前kubernetes版本上功能的缺失,並展示了一些對kubernetes本身有深度的技巧。