本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
使用准入控制器在執行時檢測容器漂移

插圖作者:Munire Aireti
在 Box,我們使用 Kubernetes (K8s) 來管理數百個微服務,這些微服務使 Box 能夠以 PB 級的規模傳輸資料。在部署過程中,我們執行 kube-applier 作為 GitOps 工作流的一部分,採用宣告式配置和自動化部署。開發人員將他們的 K8s 應用清單宣告到一個 Git 倉庫中,在任何變更被合併並應用到我們的 K8s 叢集之前,都需要透過程式碼審查和自動檢查。然而,透過 kubectl exec
和其他類似命令,開發人員能夠直接與執行中的容器互動,並改變它們已部署的狀態。這種互動可能會規避我們在 CI/CD 管道中強制執行的變更控制和程式碼審查流程。此外,它還允許這些受影響的容器在生產環境中長期接收流量。
為了解決這個問題,我們開發了自己的 K8s 元件,名為 kube-exec-controller 及其對應的 kubectl 外掛。它們協同工作,檢測並終止可能發生突變的容器(由互動式 kubectl 命令引起),並將互動事件直接暴露給目標 Pod,以提高可見性。
針對互動式 kubectl 命令的准入控制
一旦請求傳送到 K8s,API 伺服器需要對其進行身份驗證和授權才能繼續。此外,K8s 還有一個獨立的保護層,稱為准入控制器,它可以在物件持久化到 etcd 之前攔截請求。API 伺服器二進位制檔案中編譯了各種預定義的准入控制器(例如,ResourceQuota 用於強制執行每個名稱空間的硬性資源使用限制)。此外,還有兩個動態准入控制器,名為MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook,分別用於改變或驗證 K8s 請求。我們採用的是後者,用於檢測由互動式 kubectl 命令引起的執行時容器漂移。整個過程可以分為三個步驟,詳述如下。
1. 准入互動式 kubectl 命令請求
首先,我們需要啟用一個驗證性 webhook,將符合條件的請求傳送到 kube-exec-controller。為了新增專門適用於互動式 kubectl 命令的新驗證機制,我們將 webhook 的規則配置為資源為 [pods/exec, pods/attach]
,操作為 CONNECT
。這些規則告訴叢集的 API 伺服器,所有 exec
和 attach
請求都應受到我們的准入控制 webhook 的約束。在我們配置的 ValidatingAdmissionWebhook 中,我們在 clientConfig
節下指定了一個 service
引用(也可以用指定 webhook 位置的 url
替代)和 caBundle
,以允許驗證其 X.509 證書。
以下是我們的 ValidatingWebhookConfiguration 物件的一個簡短示例
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: example-validating-webhook-config
webhooks:
- name: validate-pod-interaction.example.com
sideEffects: None
rules:
- apiGroups: ["*"]
apiVersions: ["*"]
operations: ["CONNECT"]
resources: ["pods/exec", "pods/attach"]
failurePolicy: Fail
clientConfig:
service:
# reference to kube-exec-controller service deployed inside the K8s cluster
name: example-service
namespace: kube-exec-controller
path: "/admit-pod-interaction"
caBundle: "{{VALUE}}" # PEM encoded CA bundle to validate kube-exec-controller's certificate
admissionReviewVersions: ["v1", "v1beta1"]
2. 為目標 Pod 標記可能發生突變的容器
一旦 kubectl exec
請求進入,kube-exec-controller 會在內部做一個記錄,為相關的 Pod 新增標籤。新增的標籤意味著我們不僅可以查詢所有受影響的 Pod,還能啟用安全機制來檢索先前識別的 Pod,以防控制器服務本身被重啟。
准入控制過程不能在其准入響應中直接修改目標物件。這是因為 pods/exec
請求是針對 Pod API 的一個子資源,而該子資源的 API kind 是 PodExecOptions
。因此,在 kube-exec-controller 中有一個獨立的程序來非同步地修補標籤。准入控制總是允許 exec
請求,然後作為 K8s API 的客戶端為目標 Pod 新增標籤並記錄相關事件。開發人員可以使用 kubectl
或類似工具檢查他們的 Pod 是否受到影響。例如
$ kubectl get pod --show-labels
NAME READY STATUS RESTARTS AGE LABELS
test-pod 1/1 Running 0 2s box.com/podInitialInteractionTimestamp=1632524400,box.com/podInteractorUsername=username-1,box.com/podTTLDuration=1h0m0s
$ kubectl describe pod test-pod
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning PodInteraction 5s admission-controller-service Pod was interacted with 'kubectl exec' command by user 'username-1' initially at time 2021-09-24 16:00:00 -0800 PST
Warning PodInteraction 5s admission-controller-service Pod will be evicted at time 2021-09-24 17:00:00 -0800 PST (in about 1h0m0s).
3. 在預定義的時間段後驅逐目標 Pod
正如你在上面的事件訊息中所看到的,受影響的 Pod 不會立即被驅逐。有時,開發人員可能必須進入他們正在執行的容器來除錯一些線上問題。因此,我們根據 Pod 執行的叢集環境定義了一個受影響 Pod 的生存時間(TTL)。特別地,我們在開發叢集中允許更長的時間,因為在積極開發過程中執行 kubectl exec
或其他互動式命令更為常見。
對於我們的生產叢集,我們指定一個較低的時間限制,以避免受影響的 Pod 持續提供流量。kube-exec-controller 內部為每個匹配的 Pod 根據關聯的 TTL 設定並跟蹤一個計時器。一旦計時器到期,控制器會使用 K8s API 驅逐該 Pod。採用驅逐(而不是刪除)是為了確保服務可用性,因為叢集會遵守任何已配置的PodDisruptionBudget (PDB)。例如,如果使用者在其 PDB 中將 x 個 Pod 定義為關鍵,當目標工作負載執行的 Pod 少於 x 個時,驅逐(由 kube-exec-controller 請求)將不會繼續。
下面是上述整個工作流程的序列圖
一個新的 kubectl 外掛以提供更好的使用者體驗
我們的准入控制器元件在解決我們平臺上的容器漂移問題方面表現出色。它還能夠將所有相關事件提交給受影響的目標 Pod。然而,K8s 叢集不會長時間保留事件(預設保留期為一小時)。我們需要為開發人員提供其他方式來獲取他們的 Pod 互動活動。一個 kubectl 外掛 是我們暴露這些資訊的完美選擇。我們將我們的外掛命名為 kubectl pi
(pod-interaction
的縮寫),並提供兩個子命令:get
和 extend
。
當呼叫 get
子命令時,外掛會檢查我們准入控制器附加的元資料,並將其轉換為人類可讀的資訊。以下是執行 kubectl pi get
的一個示例輸出
$ kubectl pi get test-pod
POD-NAME INTERACTOR POD-TTL EXTENSION EXTENSION-REQUESTER EVICTION-TIME
test-pod username-1 1h0m0s / / 2021-09-24 17:00:00 -0800 PST
該外掛還可以用於延長已標記為將來驅逐的 Pod 的 TTL。這在開發人員需要額外時間來除錯正在發生的問題時非常有用。為此,開發人員使用 kubectl pi extend
子命令,外掛會為給定的 Pod 修補相關的*註解*。這些*註解*包括延長的持續時間和請求延長的使用者名稱,以保證透明性(顯示在 kubectl pi get
命令返回的表格中)。
相應地,在 kube-exec-controller 中定義了另一個 webhook,它會准入有效的註解更新。一旦被准入,這些更新會根據請求重置目標 Pod 的驅逐計時器。開發人員請求延長的一個示例如下
$ kubectl pi extend test-pod --duration=30m
Successfully extended the termination time of pod/test-pod with a duration=30m
$ kubectl pi get test-pod
POD-NAME INTERACTOR POD-TTL EXTENSION EXTENSION-REQUESTER EVICTION-TIME
test-pod username-1 1h0m0s 30m username-2 2021-09-24 17:30:00 -0800 PST
未來的改進
儘管我們的准入控制器服務在處理對 Pod 的互動式請求方面表現出色,但它也可能在實際命令是空操作(no-op)的情況下驅逐 Pod。例如,開發人員有時執行 kubectl exec
僅僅是為了檢查儲存在主機上的服務日誌。然而,儘管容器的狀態完全沒有改變,目標 Pod 仍然會被重啟。這裡的改進之一可以是增加區分傳遞給互動式請求的命令的能力,這樣空操作命令就不應該總是強制驅逐 Pod。然而,當開發人員進入正在執行的容器的 shell 並在 shell 內部執行命令時,這就變得具有挑戰性,因為這些命令將不再對我們的准入控制器服務可見。
另一個值得指出的地方是 K8s *標籤*和*註解*的選擇。在我們的設計中,我們決定將所有不可變的元資料作為*標籤*附加,以便在我們的准入控制中更好地強制執行不變性。然而,其中一些元資料可能更適合作為*註解*。例如,我們有一個鍵為 box.com/podInitialInteractionTimestamp
的標籤,用於在 kube-exec-controller 程式碼中列出所有受影響的 Pod,儘管其值不太可能被查詢。作為一個在 K8s 世界中更理想的設計,在我們的案例中,使用單個*標籤*進行識別,而將其他元資料作為*註解*應用,可能會更可取。
總結
藉助准入控制器的強大功能,我們能夠透過在執行時檢測可能發生突變的容器來保護我們的 K8s 叢集,並在不影響服務可用性的情況下驅逐它們的 Pod。我們還利用 kubectl 外掛來提供驅逐時間的靈活性,從而為服務所有者帶來更好、更自主的體驗。我們自豪地宣佈,我們已經將整個專案開源,供社群在他們自己的 K8s 叢集中利用。我們非常歡迎並感謝任何貢獻。你可以在 GitHub 上找到這個專案:https://github.com/box/kube-exec-controller
特別感謝 Ayush Sobti 和 Ethan Goldblum 在此專案上的技術指導。