Watch 連線可能因為多種原因而中斷,包括網路問題、API 伺服器重啟或超時。因此,實作可靠的錯誤處理和重連機制是必要的:
func watchWithRetry(clientset *kubernetes.Clientset, namespace string) {
for {
timeoutSeconds := int64(300)
listOptions := metav1.ListOptions{
Watch: true,
TimeoutSeconds: &timeoutSeconds,
}
watcher, err := clientset.CoreV1().Pods(namespace).Watch(context.Background(), listOptions)
if err != nil {
log.Printf("Error creating watcher: %v", err)
time.Sleep(5 * time.Second)
continue
}
log.Println("Watch connection established")
for event := range watcher.ResultChan() {
// 處理事件...
}
log.Println("Watch connection closed, reconnecting...")
time.Sleep(5 * time.Second)
}
}
這段程式碼實作了一個帶有重試機制的 Watch 函式。當 Watch 連線因任何原因中斷時(包括正常超時),程式會等待 5 秒後嘗試重新建立連線。這種模式確保了即使在網路不穩定或 API 伺服器重啟的情況下,客戶端也能持續接收資源變更事件。外層的無限迴圈確保了 Watch 操作的永續性,而內層迴圈則處理單次 Watch 連線中的事件流。
理解 Kubernetes 中的資源版本
在 Kubernetes 中,每個資源物件都有一個 resourceVersion
欄位,用於跟蹤該資源的變更歷史。這個版本號在 Watch 操作和樂觀並發控制中扮演著關鍵角色。
資源版本的作用
資源版本主要有以下幾個作用:
- 變更追蹤:允許客戶端追蹤資源的變更歷史
- 樂觀並發控制:防止客戶端覆寫其他客戶端的變更
- 增量同步:支援客戶端從特定版本開始接收變更事件
在 Watch 操作中使用資源版本
在啟動 Watch 操作時,可以指定 resourceVersion
引數來控制從哪個版本開始接收事件:
// 首先取得資源列表及其最新的 resourceVersion
podList, err := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return err
}
resourceVersion := podList.ResourceVersion
// 使用取得的 resourceVersion 啟動 Watch 操作
listOptions := metav1.ListOptions{
Watch: true,
ResourceVersion: resourceVersion,
TimeoutSeconds: &timeoutSeconds,
}
watcher, err := clientset.CoreV1().Pods(namespace).Watch(context.Background(), listOptions)
這段程式碼展示瞭如何在 Watch 操作中使用資源版本。首先,程式取得 Pod 列表及其最新的 resourceVersion
,然後使用這個版本號啟動 Watch 操作。這確保了 Watch 操作只會接收從該版本之後發生的變更事件,避免了處理已知的歷史事件。這種模式在實作客戶端快取同步時特別有用,可以確保不會錯過任何變更。
資源版本的特殊值
Kubernetes API 支援幾個特殊的資源版本值:
- 空字元串:從伺服器當前狀態開始 Watch,不保證不會錯過事件
- “0”:從最早可用的資源版本開始 Watch(通常用於完全重建客戶端快取)
- 特定版本:從指定的版本開始 Watch,如果該版本太舊,可能會回傳錯誤
// 從最早可用的資源版本開始 Watch(完全重建快取)
listOptions := metav1.ListOptions{
Watch: true,
ResourceVersion: "0",
TimeoutSeconds: &timeoutSeconds,
}
這個例子展示瞭如何使用特殊的資源版本值 “0” 來從最早可用的資源版本開始 Watch。這通常用於客戶端需要完全重建其本地快取的情況,例如在客戶端重啟後。然而,需要注意的是,這可能會導致客戶端接收大量的歷史事件,特別是在大型叢集中,因此應謹慎使用。
處理資源版本過期的情況
Kubernetes API 伺服器不會無限期地儲存所有資源版本的歷史記錄。如果客戶端請求的資源版本已經過期,API 伺服器會回傳 “Gone” 錯誤(HTTP 410)。在這種情況下,客戶端需要重新取得資源列表並從新的資源版本開始 Watch:
func watchWithResourceVersionHandling(clientset *kubernetes.Clientset, namespace string) {
var resourceVersion string
for {
listOptions := metav1.ListOptions{
ResourceVersion: resourceVersion,
Watch: true,
TimeoutSeconds: &timeoutSeconds,
}
watcher, err := clientset.CoreV1().Pods(namespace).Watch(context.Background(), listOptions)
if err != nil {
if errors.IsGone(err) {
// 資源版本過期,重新取得資源列表
podList, listErr := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
if listErr != nil {
log.Printf("Error listing pods: %v", listErr)
time.Sleep(5 * time.Second)
continue
}
resourceVersion = podList.ResourceVersion
log.Printf("Resource version expired, restarting with new version: %s", resourceVersion)
continue
}
log.Printf("Error creating watcher: %v", err)
time.Sleep(5 * time.Second)
continue
}
for event := range watcher.ResultChan() {
// 處理事件...
// 更新資源版本
if obj, ok := event.Object.(metav1.Object); ok {
resourceVersion = obj.GetResourceVersion()
}
}
log.Println("Watch connection closed, reconnecting...")
}
}
這段程式碼展示瞭如何處理資源版本過期的情況。當 Watch 請求回傳 “Gone” 錯誤時,程式會重新取得資源列表並使用新的資源版本重啟 Watch 操作。此外,程式會在處理每個事件時更新本地儲存的資源版本,確保在連線中斷時能夠從最新的已知版本重新開始。這種方法確保了客戶端能夠在資源版本過期的情況下優雅地還原,而不會丟失事件。
理解 Kubernetes 中的變更通知機制
Kubernetes 的變更通知機制是其宣告式 API 的核心部分,它允許控制器和其他元件對資源變更做出反應。這種機制主要透過 Watch 操作和資源版本跟蹤來實作。
變更通知的工作原理
當資源物件發生變更時,Kubernetes API 伺服器會生成一個事件並將其傳送給所有正在 Watch 該資源的客戶端。這些事件包含了變更的型別(新增、修改、刪除)和變更後的資源狀態。
// 實作一個簡單的控制器,監聽 Pod 變更並做出反應
func runSimpleController(clientset *kubernetes.Clientset, namespace string) {
for {
watcher, err := clientset.CoreV1().Pods(namespace).Watch(context.Background(), metav1.ListOptions{})
if err != nil {
log.Printf("Error creating watcher: %v", err)
time.Sleep(5 * time.Second)
continue
}
for event := range watcher.ResultChan() {
pod, ok := event.Object.(*v1.Pod)
if !ok {
continue
}
switch event.Type {
case watch.Added:
handlePodCreation(clientset, pod)
case watch.Modified:
handlePodUpdate(clientset, pod)
case watch.Deleted:
handlePodDeletion(clientset, pod)
}
}
}
}
func handlePodCreation(clientset *kubernetes.Clientset, pod *v1.Pod) {
log.Printf("Pod created: %s, applying initial configuration...", pod.Name)
// 執行建立後的處理邏輯...
}
func handlePodUpdate(clientset *kubernetes.Clientset, pod *v1.Pod) {
log.Printf("Pod updated: %s, checking for required actions...", pod.Name)
// 執行更新後的處理邏輯...
}
func handlePodDeletion(clientset *kubernetes.Clientset, pod *v1.Pod) {
log.Printf("Pod deleted: %s, cleaning up resources...", pod.Name)
// 執行刪除後的處理邏輯...
}
這段程式碼實作了一個簡單的控制器模式,透過 Watch 操作監聽 Pod 資源的變更,並根據不同的事件型別執行相應的處理邏輯。這種模式是 Kubernetes 控制器的基本工作方式,允許系統元件對資源狀態的變化做出反應,從而實作宣告式 API 的自動協調功能。控制器會持續執行,在 Watch 連線中斷時自動重新建立連線,確保不會錯過重要的資源變更事件。
變更通知的效率最佳化
在大規模叢集中,變更通知機制可能會產生大量的事件,因此最佳化事件處理的效率非常重要:
- 使用工作佇列:將事件放入工作佇列中,由工作執行緒處理,避免阻塞 Watch 迴圈
- 事件去重:對同一資源的多次快速變更進行去重,只處理最新狀態
- 選擇性處理:根據資源的標籤或狀態選擇性地處理事件
// 使用工作佇列處理事件
type Controller struct {
clientset kubernetes.Interface
queue workqueue.RateLimitingInterface
informer cache.SharedIndexInformer
}
func NewController(clientset kubernetes.Interface) *Controller {
queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
informer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return clientset.CoreV1().Pods("").List(context.Background(), options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return clientset.CoreV1().Pods("").Watch(context.Background(), options)
},
},
&v1.Pod{},
0, // 不重新同步
cache.Indexers{},
)
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err == nil {
queue.Add(key)
}
},
UpdateFunc: func(old, new interface{}) {
key, err := cache.MetaNamespaceKeyFunc(new)
if err == nil {
queue.Add(key)
}
},
DeleteFunc: func(obj interface{}) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err == nil {
queue.Add(key)
}
},
})
return &Controller{
clientset: clientset,
queue: queue,
informer: informer,
}
}
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) {
defer c.queue.ShutDown()
// 啟動 informer
go c.informer.Run(stopCh)
// 等待 informer 的快取同步
if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
log.Println("Timed out waiting for caches to sync")
return
}
// 啟動工作執行緒
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
<-stopCh
}
func (c *Controller) runWorker() {
for c.processNextItem() {
}
}
func (c *Controller) processNextItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.processItem(key.(string))
if err == nil {
c.queue.Forget(key)
} else if c.queue.NumRequeues(key) < 5 {
log.Printf("Error processing %s: %v", key, err)
c.queue.AddRateLimited(key)
} else {
log.Printf("Giving up processing %s: %v", key, err)
c.queue.Forget(key)
}
return true
}
func (c *Controller) processItem(key string) error {
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return err
}
obj, exists, err := c.informer.GetIndexer().GetByKey(key)
if err != nil {
return err
}
if !exists {
// 資源已刪除
return c.handleDeletion(namespace, name)
}
// 資源存在,處理它
return c.handlePod(obj.(*v1.Pod))
}
func (c *Controller) handlePod(pod *v1.Pod) error {
// 處理 Pod 邏輯...
return nil
}
func (c *Controller) handleDeletion(namespace, name string) error {
// 處理刪除邏輯...
return nil
}
這段程式碼展示了一個更完整的控制器實作,使用了 Kubernetes 客戶端函式庫中的 Informer 和工作佇列機制。Informer 負責監聽資源變更並維護本地快取,而工作佇列則用於非同步處理事件,避免阻塞 Watch 迴圈。
這種實作有幾個關鍵優勢:
- 效率:使用工作佇列可以平行處理多個事件,提高處理效率
- 可靠性:包含了重試機制,確保暫時性錯誤不會導致事件丟失
- 資源最佳化:透過 Informer 的本地快取減少了對 API 伺服器的請求
- 去重:工作佇列會自動對同一資源的多次變更進行去重,只處理最新狀態
這種模式是 Kubernetes 控制器的標準實作方式,被廣泛用於各種自定義控制器和操作者模式應用中。
Kubernetes 的變更通知機制是其自動化和宣告式特性的基礎。透過理解和正確使用這一機制,開發者可以構建強大的自定義控制器和操作者,擴充套件 Kubernetes 的功能並自動化複雜的工作流程。同時,合理設定超時引數和實作可靠的重連機制,可以確保系統在各種情況下都能穩定執行。
在實際應用中,玄貓建議根據具體需求和環境條件調整超時設定和重試策略,並始終考慮系統的可靠性、效率和資源消耗之間的平衡。透過精心設計的變更監聽和處理機制,可以構建出既高效又可靠的 Kubernetes 應用。
Kubernetes 准入控制機制的深層解析
在 Kubernetes 的安全架構中,准入控制(Admission Control)扮演著至關重要的角色。當我在設計企業級 Kubernetes 安全架構時,發現准入控制是最容易被忽視,卻也是最能提升系統安全性的環節之一。准入控制允許我們在資源被持久化到 etcd 之前,對請求進行驗證或修改,這為實施安全策略提供了絕佳的切入點。
MutatingWebhook 與 ValidatingWebhook 的關鍵差異
從表面上看,MutatingWebhookConfiguration
和 ValidatingWebhookConfiguration
這兩種資源定義幾乎相同,主要差異僅在於 kind
和 reinvocationPolicy
欄位。然而,在後端實作上存在一個根本性的區別:
- MutatingWebhook:允許准入 webhook 回傳修改後的請求物件
- ValidatingWebhook:僅能驗證請求,不能修改物件內容
值得注意的是,即使定義了 MutatingWebhookConfiguration
,你也可以選擇只進行驗證而不修改物件。在設計准入控制策略時,我通常建議遵循最小許可權原則,只在必要時使用變更許可權。
自我保護機制
你可能會思考一個有趣的問題:「如果我定義一個 Webhook 設定,讓它處理自己的資源型別,會發生什麼?」
幸運的是,Kubernetes 設計者已經考慮到這個問題。無論是 ValidatingAdmissionWebhooks
還是 MutatingAdmissionWebhooks
,都不會被用於處理 ValidatingWebhookConfiguration
和 MutatingWebhookConfiguration
物件的准入請求。這是一個重要的安全設計,防止叢集因錯誤設定而陷入無法還原的狀態。
准入控制最佳實踐
經過多年的 Kubernetes 安全架構設計,我總結了以下准入控制的最佳實踐,希望能幫助你更有效地利用這一強大機制。
准入外掛順序不再重要
在早期版本的 Kubernetes 中,准入外掛的順序與處理順序直接相關。然而,在目前支援的 Kubernetes 版本中,透過 --enable-admission-plugins
指定的准入外掛順序已不再重要。
不過,對於准入 webhook 來說,順序仍然扮演著一定角色。請求的准入或拒絕遵循邏輯 AND 操作,這意味著如果任何一個准入 webhook 拒絕請求,整個請求都會被拒絕,並向使用者回傳錯誤。
更重要的是,變更型准入控制器(Mutating Admission Controllers)總是在驗證型准入控制器(Validating Admission Controllers)之前執行。這很合理:你不會想先驗證物件,然後再對其進行修改。下圖展示了一個透過准入 webhook 的請求流程:
客戶端請求 → API Server → 認證 → 授權 → 變更型准入控制器 → 驗證型准入控制器 → etcd
避免修改相同欄位
設定多個變更型准入 webhook 時會面臨一個挑戰:無法控制請求透過多個變更型准入 webhook 的順序。因此,避免讓多個變更型准入控制器修改相同欄位非常重要,否則可能導致不一致的行為。
當我在設計複雜的准入控制系統時,通常會建議設定驗證型准入 webhook 來確認資源清單在變更後的最終狀態符合預期,因為驗證型 webhook 保證在變更型 webhook 之後執行。
變更型准入 webhook 必須是冪等的
這意味著它們必須能夠處理並准入已經被處理過甚至可能已經被修改過的物件。在實作變更型 webhook 時,我總是確保無論物件被處理多少次,最終結果都是一致的。
失敗策略:開放失敗還是關閉失敗
你可能注意到在 webhook 設定資源中有一個 failurePolicy
欄位。這個欄位定義了當准入 webhook 遇到存取題或未識別的錯誤時,API 伺服器應該如何處理。可以設定為 Ignore
(忽略)或 Fail
(失敗):
- Ignore:本質上是開放失敗,請求處理將繼續
- Fail:拒絕整個請求,關閉失敗
這兩種策略都有其影響需要考慮。忽略關鍵的准入 webhook 可能導致業務依賴的策略未被應用,而使用者卻不知情。一個潛在的解決方案是當 API 伺服器記錄無法存取准入 webhook 時發出警示。
而設定為 Fail
則可能更具破壞性,如果准入 webhook 出現問題,所有請求都會被拒絕。為了防止這種情況,可以限定規則範圍,確保只有特定資源請求才會傳送到准入 webhook。作為原則,我從不設定適用於叢集中所有資源的規則。
准入 webhook 必須快速回應
如果你編寫了自己的准入 webhook,請記住使用者/系統請求可能會直接受到 webhook 決策和回應時間的影響。所有準入 webhook 呼叫都設定了 30 秒的超時,超時後 failurePolicy
將生效。
即使你的准入 webhook 只需要幾秒鐘來做出准入/拒絕決定,也會嚴重影響使用者使用叢集的體驗。我建議避免複雜的邏輯或依賴外部系統(如資料函式庫)來處理准入/拒絕邏輯。
限定準入 webhook 的範圍
透過 NamespaceSelector
欄位,你可以限定準入 webhook 操作的名稱空間。此欄位預設為空,比對所有內容,但可以透過 matchLabels
欄位比對名稱空間標籤。我建議始終使用此欄位,因為它允許每個名稱空間明確選擇加入。
在單獨的名稱空間中佈署並使用 NamespaceSelector
當自行託管 webhook 准入控制器時,將其佈署到單獨的名稱空間,並使用 NamespaceSelector
欄位排除該名稱空間中佈署的資源,避免被處理。
不要觸碰 kube-system 名稱空間
kube-system
名稱空間是所有 Kubernetes 叢集通用的保留名稱空間,所有系統級服務都在這裡執行。我建議永遠不要對此名稱空間中的資源執行准入 webhook,可以透過 NamespaceSelector
欄位實作,只需不比對 kube-system
名稱空間即可。對於叢集操作所需的任何系統級名稱空間,也應考慮這樣做。
使用 RBAC 鎖定準入 webhook 設定
瞭解了准入 webhook 設定中的所有欄位後,你可能已經想到了一種破壞叢集存取的簡單方法。不言而喻,MutatingWebhookConfiguration
和 ValidatingWebhookConfiguration
的建立是叢集上的根級操作,必須使用 RBAC 適當地鎖定。否則可能導致叢集損壞,甚至更糟的是,對應用工作負載的注入攻擊。
不要傳送敏感資料
准入 webhook 本質上是接受 AdmissionRequests
並輸出 AdmissionResponses
的不透明盒子。它們如何儲存和操作請求對使用者來說是不透明的。思考你傳送到准入 webhook 的請求負載很重要。對於 Kubernetes 金鑰或 ConfigMap,它們可能包含敏感資訊,需要對資訊的儲存和分享提供強有力的保證。與准入 webhook 分享這些資源可能會洩露敏感資訊,這就是為什麼你應該將資源規則範圍限定為驗證和/或變更所需的最小資源。
Kubernetes 授權機制
在 Kubernetes 中,授權(Authorization)是在認證之後但在准入之前執行的。授權主要解決這個問題:「這個使用者能否對這些資源執行這些操作?」
授權模組
授權模組負責授予或拒絕存取許可權。它們根據明確定義的策略決定是否授予存取許可權;否則,所有請求都將被隱式拒絕。
Kubernetes 內建提供以下授權模組:
- 根據屬性的存取控制(ABAC):允許透過本地檔案設定授權策略
- 根據角色的存取控制(RBAC):允許透過 Kubernetes API 設定授權策略
授權模組是 Kubernetes 安全架構中的關鍵元件,它們在 API 請求流程中位於認證之後、准入控制之前。這些模組根據預先定義的策略決定是否允許特定使用者執行特定操作。
ABAC 和 RBAC 是兩種主要的授權模式,各有優缺點:
- ABAC 透過本地檔案定義策略,靈活但管理複雜
- RBAC 透過 Kubernetes API 定義策略,更易於管理和稽核
在現代 Kubernetes 佈署中,RBAC 已成為主流授權機制,因為它提供了更好的可管理性和安全性。
准入控制和授權是 Kubernetes 安全架構中的兩個關鍵環節。准入控制允許我們在資源持久化前對請求進行驗證或修改,而授權則決定使用者是否有權執行特定操作。
在設計 Kubernetes 安全策略時,我建議遵循以下原則:
- 理解變更型和驗證型准入控制器的差異,並根據最小許可權原則選擇適當的型別
- 避免多個變更型准入控制器修改相同欄位
- 確保變更型准入 webhook 是冪等的
- 謹慎選擇失敗策略,並考慮其潛在影響
- 限定準入 webhook 的範圍,特別是避免處理系統名稱空間
- 使用 RBAC 嚴格控制准入 webhook 設定的存取許可權
- 避免向准入 webhook 傳送敏感資料
透過正確實施這些最佳實踐,你可以顯著提高 Kubernetes 環境的安全性,同時確保系統的可用性和可靠性。准入控制和授權機制提供了強大的工具,幫助我們在複雜的雲原生環境中實施精細的安全策略。
Kubernetes 授權機制:確保資源存取安全
在 Kubernetes 的安全架構中,授權機制扮演著至關重要的角色。授權決定了使用者或服務帳號能對哪些資源執行哪些操作。Kubernetes 提供了多種授權模組,每種都有其特定的使用場景和優勢。
授權模組概覽
Kubernetes 支援多種授權模組,可透過 API 伺服器的 --authorization-mode
引數進行設定:
- ABAC (Attribute-Based Access Control) - 根據屬性的存取控制
- RBAC (Role-Based Access Control) - 根據角色的存取控制
- Webhook - 透過遠端 REST 端點處理授權請求
- Node - 專門用於授權 kubelet 請求的模組
與准入控制器不同,授權模組採用「任一允許即透過」的策略。只要有一個授權模組允許請求,該請求就能繼續處理。只有當所有模組都拒絕請求時,才會向使用者回傳錯誤。
ABAC 授權例項
以下是使用 ABAC 授權模組的政策定義範例,該政策授予使用者 Mary 對 kube-system 名稱空間中的 pod 唯讀存取權:
apiVersion: abac.authorization.kubernetes.io/v1beta1
kind: Policy
spec:
user: mary
resource: pods
readonly: true
namespace: kube-system
如果 Mary 嘗試存取 demo-app 名稱空間中的 pod,請求將被拒絕,因為她沒有該名稱空間的存取許可權。
授權 API 與除錯工具
Kubernetes 提供了 authorization.k8s.io
API 群組,用於暴露 API 伺服器授權功能給外部服務。這組 API 非常適合用於除錯:
- SelfSubjectAccessReview - 檢查當前使用者的存取許可權
- SubjectAccessReview - 檢查任何使用者的存取許可權
- LocalSubjectAccessReview - 名稱空間特定的 SubjectAccessReview
- SelfSubjectRulesReview - 回傳使用者在特定名稱空間中可執行的操作列表
這些 API 可以透過建立資源的方式進行查詢。例如,使用 SelfSubjectAccessReview 測試是否有許可權:
$ cat << EOF | kubectl create -f - -o yaml
apiVersion: authorization.k8s.io/v1
kind: SelfSubjectAccessReview
spec:
resourceAttributes:
verb: get
resource: pods
namespace: demo-app
EOF
Kubernetes 還提供了更簡便的 kubectl auth can-i
命令,它實際上是查詢相同的 API:
$ kubectl auth can-i get pods --namespace demo-app
yes
# 以管理員身份檢查其他使用者的許可權
$ kubectl auth can-i get pods --namespace demo-app --as mary
yes
Webhook 授權
Webhook 授權模組允許叢集管理員設定外部 REST 端點來處理授權流程。這個端點需要在控制平面主機的檔案系統上設定,並透過 API 伺服器的 --authorization-webhook-config-file=SOME_FILENAME
引數指定。
設定後,API 伺服器會將 SubjectAccessReview 物件作為請求主體傳送到授權 webhook 應用程式,該應用程式處理並回傳帶有完整狀態列位的物件。
授權最佳實踐
在設定叢集的授權模組時,應考慮以下最佳實踐:
避免在多控制平面叢集中使用 ABAC
由於 ABAC 政策需要放置在每個控制平面主機的檔案系統上並保持同步,因此不建議在多控制平面叢集中使用 ABAC。Webhook 模組也有類別似問題,因為其設定根據檔案。此外,這些政策的變更需要重啟 API 伺服器才能生效,這在單控制平面叢集中會導致控制平面中斷,在多控制平面叢集中則可能導致設定不一致。考慮到這些因素,玄貓建議僅使用 RBAC 模組進行使用者授權,因為其規則直接設定並儲存在 Kubernetes 中。
避免使用 webhook 模組
雖然 webhook 模組功能強大,但潛在風險很高。由於每個請求都要經過授權流程,如果 webhook 服務失敗,對叢集影響將是災難性的。因此,除非完全瞭解並能接受 webhook 服務不可用時的叢集故障模式,否則不建議使用外部授權模組。
GitOps 與佈署策略
GitOps 是一種在 Kubernetes 上佈署和管理應用程式的方法,它將 Git 作為單一事實來源,用於管理 Kubernetes 資源。這種方法讓開發者和維運人員能夠透過提取請求來加速和簡化應用佈署和維運任務。
GitOps 的核心概念
GitOps 由 Weaveworks 團隊推廣,其理念根據他們在生產環境中執行 Kubernetes 的經驗。GitOps 將軟體開發生命週期的概念應用到維運中,使 Git 儲存函式庫成為事實來源,而叢集則與設定的 Git 儲存函式庫同步。
例如,當更新 Kubernetes Deployment 清單時,這些設定變更會自動反映在 Git 中的叢集狀態。這種方法使維護多個一致的叢集變得更容易,避免了整個機群中的設定偏差。GitOps 允許以宣告式方式描述多個環境的叢集,並驅動維護叢集的狀態。
GitOps 工作流程
GitOps 工作流程的核心是 Git 儲存函式庫,它包含應用程式碼和 Kubernetes 清單。當開發者提交程式碼變更時,GitOps 代理(如 Flux)會監視儲存函式庫並同步任何新變更到 Kubernetes 叢集。
OpenGitOps 專案定義了 GitOps 的四個核心原則:
- 宣告式設定 - 所有設定都以宣告式 YAML 檔案儲存在 Git 中,提供單一事實來源
- 版本化設定 - 所有設定都儲存在 Git 中,所有變更都被追蹤和版本化,便於稽核和回復
- 不可變設定 - 所有設定都是不可變的,一旦變更就不能修改,確保叢集狀態一致
- 持續狀態調和 - 叢集狀態與 Git 中定義的狀態持續調和,確保叢集處於一致狀態
GitOps 的優勢
相較於傳統佈署工作流程,GitOps 提供了多項優勢:
- 宣告式設定 - 所有設定都以宣告式 YAML 檔案儲存在 Git 中,便於稽核和追蹤變更
- 版本控制 - Git 儲存函式庫支援不可變性和版本歷史,便於追蹤和比較變更
- 持續調和 - 叢集狀態與 Git 中定義的狀態持續調和,便於回復和保持一致性
- 安全性 - 使用 Git 管理應用程式佈署提供了完整的變更稽核日誌,增強環境安全性
GitOps 儲存函式庫結構
設計 GitOps 儲存函式庫結構時,有多種策略可供選擇,每種都有其優缺點:
- 單一大型儲存函式庫 - 所有 Kubernetes 清單和應用程式碼儲存在單一儲存函式庫中
- 每團隊一個儲存函式庫 - 每個團隊有自己的儲存函式庫,Kubernetes 清單儲存在同一儲存函式庫中
- 每應用程式一個儲存函式庫 - 每個應用程式有自己的儲存函式庫,Kubernetes 清單儲存在同一儲存函式庫中
- 每環境一個分支 - 每個環境在同一儲存函式庫中有自己的分支
通常,應根據組織和團隊結構決定哪種結構最適合。從每團隊一個儲存函式庫開始是一個很好的起點,它提供了明確的關注點分離和簡單的儲存函式函式倉管理。
機密管理
在實施 GitOps 工作流程時,機密管理是一個常見挑戰。有多種管理機密的方法,最佳方法取決於組織需求:
- 直接儲存在 Git 中 - 最簡單但不推薦,因為機密以明文儲存
- 烘焙到容器映像中 - 比明文儲存好一些,但每次機密輪換都需要重建映像
- 使用 Kubernetes Secrets - Kubernetes 原生方式,但實際上只是 base64 編碼,不是真正加密
- 使用 Sealed Secrets - Bitnami 的專案,使用非對稱加密加密機密,只有叢集控制器能解密
- 儲存在機密管理工具中 - 將機密儲存在安全位置,如 HashiCorp Vault、Azure Keyvault、Google KMS 等
推薦使用 Sealed Secrets 或外部機密管理解決方案來管理 GitOps 中的機密。
使用 Flux 設定 GitOps
Flux 是一個 Kubernetes 運算元,它監視 Git 儲存函式庫的變更並自動將這些變更應用到叢集。以下是設定 Flux 的基本步驟:
- 安裝 Flux CLI
- 匯出 GitHub 令牌和使用者名稱
- 檢查叢集是否可以安裝 Flux
- 引導 Flux
- 建立指向應用程式儲存函式庫的 Git 儲存函式庫清單
- 設定 Flux 佈署應用程式
- 推播變更到儲存函式庫
設定完成後,對主分支中的 Kubernetes 清單所做的任何變更都會自動反映在叢集中。
GitOps 工具
實施 GitOps 時,有多種工具可供選擇:
- Flux - Kubernetes 運算元,監視 Git 儲存函式庫變更並自動應用到叢集,CNCF 畢業專案
- ArgoCD - 開放原始碼 GitOps 持續交付工具,CNCF 畢業專案
- Codefresh - 可用於實施 GitOps 的 CI/CD 平台,提供 ArgoCD 即服務
- Harness - 可用於實施 GitOps 的 CI/CD 平台,導向企業客戶
GitOps 最佳實踐
實施 GitOps 時,考慮以下最佳實踐:
- 從小型應用程式開始,然後擴充套件到使用 GitOps 模型管理所有內容
- 評估符合需求的工具,或從 Flux 或 ArgoCD 等經過驗證的開放原始碼工具開始
- 避免使用分支作為儲存函式庫佈局,這是最複雜與容易出錯的儲存函式庫佈局
- 從每環境一個資料夾開始,這提供了靈活性並允許使用 Kustomize 或 Helm 進行範本化
- 使用 Sealed Secrets 或外部機密提供者管理叢集中的機密
- 記住 GitOps 是一個過程而非工具,現有工具集可能符合需求
Kubernetes 安全架構
Kubernetes 是一個強大的雲原生應用程式協調平台,但在 API 和工具的表面下隱藏著一個龐大、複雜的分散式系統,需要特定知識來保護。保護 Kubernetes 是一個複雜的主題,但如果忽視瞭解和實施安全最佳實踐,風險將非常高。
安全 Kubernetes 的一個好方法是遵循「縱深防禦」策略,在每一層使用多種安全措施來保護 Kubernetes 和工作負載。此外,遵循最小許可權原則,即使用者和工作負載只能存取執行其功能所絕對需要的內容。
叢集安全
由於 Kubernetes 控制平面透過一組 API 暴露,保護叢集的第一步是規範和限制誰可以存取叢集以及他們可以執行哪些操作。
etcd 存取
Kubernetes 的預設儲存系統是 etcd。必須確保只有 Kubernetes API 伺服器可以使用強大的憑證存取 etcd,與這些憑證不分享。還必須確保只有 API 伺服器可以透過網路防火牆存取 etcd。直接存取 etcd 會繞過所有後續安全措施,因此這是一個非常重要的安全層。
身份驗證
Kubernetes 提供了多種身份驗證方法,從承載令牌和憑證到 OpenID Connect (OIDC) 和輕量級目錄存取協定 (LDAP) 整合。選擇適合業務需求的身份驗證模型非常重要。安全挑戰通常出現在使用者使用 kubectl 等工具驗證 Kubernetes 所需的 Kubeconfig 檔案的建立、分發和儲存中。使用身份驗證提供者允許檢索臨時動態令牌,而不是使用可能被惡意行為者輕易檢索的靜態令牌或憑證。
授權是強制執行誰可以對哪些資源執行哪些操作的強大工具。主要工具是根據角色的存取控制 (RBAC)。Kubernetes 預設設定合理,但應考慮將團隊成員資格等屬性以及名稱空間作為擴充套件 RBAC 資源數量的方式,以支援不斷增長的工作負載和使用者數量。同樣重要的是使用 RBAC 鎖定服務帳號,確認需要存取 Kubernetes API 的工作負載只能存取執行其功能所需的最小操作。
TLS
預設情況下,Kubernetes 啟用了 TLS 安全的 API 端點。但是,不同的工具和平台可能啟用 HTTP 明文通訊,這會開啟攻擊向量,因為流量將不安全。安全儲存和控制對 Kubernetes 使用的任何憑證和金鑰的存取非常重要,並建立計劃在丟失或洩露時輪換它們。憑證的短生命週期有助於降低安全風險。
Kubelet 和雲端中繼資料存取
Kubelet 是在每個節點上執行的元件,負責管理節點和在其上執行的 pod。不幸的是,Kubelet 預設啟用了未經身份驗證的 API。Kubelet API 非常強大,因此應啟用身份驗證和授權。如果在雲端提供商上執行,節點可能有權存取可用於暴露 Kubernetes 佈建憑證的雲端中繼資料 API。建議使用網路策略鎖定對中繼資料端點的存取。
機密
Kubernetes 機密預設不加密。這意味著惡意行為者可能夠從其他向量讀取這些機密。幸運的是,有幾種不同的解決方案可以幫助解決這個問題。Kubernetes API 伺服器提供了設定加密提供者的能力,該提供者與設定案一起使用,在儲存到 etcd 之前加密特定的 Kubernetes 資源。加密提供者通常是雲端機密儲存服務。當前加密提供者實作的唯一挑戰是無法加密所有內容,與設定繁瑣與容易出錯。Kubernetes 社群構建的另一個解決方案是 csi-secret-store,它允許透過臨時 RAMDISK 檔案系統將機密直接掛載到 pod 中。使用 csi-secret-store 可以繞過使用 Kubernetes 機密的需求,而是直接從另一個受信任的機密儲存中存取它們。
日誌記錄和稽核
Kubernetes 預設設定了豐富的日誌記錄。此外,在 API 伺服器上啟用稽核日誌也很重要,這將啟用所有安全特定事件的時間順序日誌,並可透過稽核策略進行設定。啟用稽核只是解決方案的一部分;還必須確保稽核日誌被傳送到聚合點,並設定觸發器,如果檢測到可疑事件,則向安全團隊發出警示。
叢集安全態勢工具
實施 Kubernetes 安全可能具有挑戰性。好訊息是有開放原始碼工具可以掃描 Kubernetes 叢集,檢測安全風險,並標記常見的錯誤設定。此外,它們可以掃描叢集上的所有資源並提供最佳實踐。像 Kubescape 這樣的工具執行快速,並根據嚴重性提供輸出。建議定期在所有叢集上執行這些工具,以確定叢集的安全態勢和佈署到其中的資源。
叢集安全最佳實踐
以下是叢集層安全最佳實踐的清單:
- 鎖定 etcd 存取並將存取憑證和憑證儲存在安全位置
- 停用不安全和未經身份驗證的 API 端點
- 使用提供臨時動態令牌而非靜態設定令牌的身份驗證提供者
- 確保使用者和服務遵循最小許可權原則
- 定期輪換基礎設施憑證
- 使用金鑰和憑證加密靜態和傳輸中的敏感資料
- 在佈署到叢集之前掃描容器映像是否存在漏洞和惡意軟體
- 啟用稽核日誌和監控以檢測和回應可疑活動
- 使用 Kubescape 等安全掃描工具基準測試 Kubernetes 叢集和工作負載的安全態勢
工作負載容器安全
Kubernetes 提供了許多以安全為中心的 API,這使得透過與佈署工作負載相同的工具進行設定變得簡單。
Pod 安全准入
Pod 安全准入是工作負載安全故事的關鍵部分,它允許設定和管理 pod 設定的所有安全敏感元件,並在名稱空間或叢集級別應用開箱即用的最佳實踐。
Seccomp、AppArmor 和 SELinux
Linux 提供了幾種不同的安全機制,可以與 Kubernetes 一起使用,以提高在 Kubernetes 上執行的工作負載的安全態勢:
- Seccomp 允許建立系統呼叫過濾設定檔案,用於限制來自容器的系統呼叫。Kubernetes 社群建立了一個名為安全設定檔案運算元的工具,簡化了 Seccomp 設定檔案的管理開銷。
- AppArmor 和 SELinux 是 Linux 核心安全模組,允許對每個容器的強制存取控制進行精細設定。這些允許叢集管理員對容器可以執行的操作進行精細控制。
准入控制器
准入控制器是保護工作負載的關鍵部分。Kubernetes 附帶了一組整合的准入控制器,所有與安全相關的准入控制器預設都已啟用。例如,NodeRestriction 准入控制器限制 Kubelet 的許可權,使其只能修改分配給該特定節點的 pod。
授權機制與安全架構
在 Kubernetes 的安全體系中,授權機制是決定誰能對系統資源進行何種操作的關鍵環節。與許多人誤解的不同,授權與身份驗證是兩個截然不同的概念 - 身份驗證確認「你是誰」,而授權則決定「你能做什麼」。
Kubernetes 支援多種授權模組同時運作,這種設計非常靈活。當一個請求進入 API 伺服器時,它會依序透過所有啟用的授權模組,只要有一個模組允許該請求,整個授權過程就會透過。這與准入控制器的「全部必須透過」邏輯形成鮮明對比。
ABAC 模式雖然靈活,但在多控制平面環境中維護極為困難,因為它需要在每個控制平面節點上同步更新檔案,與每次更改都需要重啟 API 伺服器。這就是為什麼在生產環境中,RBAC 已成為主流選擇 - 它直接將授權規則儲存在 Kubernetes 資源中,無需重啟即可更新。
Webhook 授權模組雖然功能強大,但引入了外部依賴,如果外部授權服務失敗,可能導致整個叢集無法正常工作。這種風險在生產環境中通常是不可接受的。
GitOps 的安全優勢
GitOps 模式不僅提升了佈署效率,也顯著增強了系統安全性。透過將 Git 作為唯一事實來源,每一個系統變更都必須透過 Git 提交,這自動建立了完整的稽核軌跡。任何未經授權的直接變更都會被 GitOps 控制器自動修正,確保系統始終與 Git 儲存函式庫中定義的狀態一致。
在傳統佈署模式中,開發人員可能需要直接存取生產叢集來佈署變更,這增加了安全風險。而在 GitOps 模式下,開發人員只需提交程式碼到 Git 儲存函式庫,無需直接存取生產環境。這種職責分離大降低了安全風險。
機密管理是 GitOps 中的一個關鍵挑戰。Sealed Secrets 和外部機密管理工具提供了安全儲存機密的方法,同時保持 GitOps 工作流程的完整性。這些工具確保機密在 Git 中安全儲存,只有在叢集中才能被解密和使用。
縱深防禦策略
Kubernetes 安全不是單一層面的問題,而是需要採用縱深防禦策略。這意味著在多個層面實施安全控制,即使一層被突破,其他層仍能提供保護。
從基礎設施層面看,保護 etcd 和 API 伺服器是首要任務。etcd 儲存了叢集的所有狀態,直接存取它會繞過所有安全控制。同樣,API 伺服器是所有操作的入口點,必須確保其安全設定。
在身份驗證和授權層面,使用動態令牌而非靜態憑證,並實施最小許可權原則至關重要。每個使用者和服務帳號只應擁有完成其任務所需的最小許可權。
在工作負載層面,Pod 安全准入、Seccomp 和 AppArmor 等機制提供了容器級別的安全控制。這些機制限制了容器可以執行的操作,減少了潛在的攻擊面。
最後,持續監控和稽核是安全策略的關鍵組成部分。啟用稽核日誌並將其集中收集,可以幫助檢測和回應安全事件。定期使用安全掃描工具評估叢集的安全態勢,也是維護安全環境的重要實踐。
Kubernetes 的安全是一個多層次、多導向的挑戰,需要從叢集基礎設施到工作負載容器的全方位防護。透過實施適當的授權機制、採用 GitOps 佈署模式、保護關鍵元件如 etcd 和 API 伺服器,以及利用 Pod 安全准入和 Linux 安全機制,可以顯著提高 Kubernetes 環境的安全性。
縱深防禦策略和最小許可權原則是 Kubernetes 安全的根本。每一層安全控制都應該被視為整體防禦策略的一部分,而非獨立存在。定期使用安全掃描工具評估叢集的安全態勢,並根據最新的安全最佳實踐更新安全策略,是維護安全 Kubernetes 環境的關鍵。
隨著組織對 Kubernetes 的依賴日益增加,投資於理解和實施這些安全最佳實踐變得越來越重要。安全不是一次性的工作,而是一個持續的過程,需要不斷學習、評估和改進。透過遵循本文中概述的最佳實踐,組織可以建立一個強大的安全基礎,保護其 Kubernetes 環境免受潛在威脅。
混沌測試、負載測試與實驗:開發更穩健的Kubernetes應用
在Kubernetes環境中,確保應用程式的穩定性、效能和使用者經驗是每位工程師的核心任務。本文將探討三種強大的測試方法:混沌測試、負載測試和實驗,這些方法能幫助你建立更穩健、高效與符合使用者需求的應用系統。
混沌測試:在控制環境中擁抱失敗
混沌測試,顧名思義,是在應用程式中引入「混沌」元素,測試系統在非理想條件下的反應能力。但這裡的「混沌」究竟指什麼?簡單來說,它是指那些不常見但並非完全意外的邊緣情況。
混沌測試的目標
混沌測試的核心目標是在應用環境中引入極端條件,並觀察應用如何應對這些情況,特別是它如何失敗。這聽起來可能有些反直覺——我們為什麼要刻意讓系統失敗?答案很簡單:在測試環境中觀察失敗遠比在生產環境中讓使用者遭遇失敗要好得多。
混沌測試讓我們有機會在問題影響真實使用者前發現並修復它們。當然,我們引入的錯誤應該是現實中可能發生的,而非完全不可能的極端情況。每個應用都有不同的彈性需求——手機遊戲的容錯要求顯然低於飛機或汽車控制系統。
混沌測試的前提條件
在開始混沌測試前,你需要了解:
- 環境條件:你的應用可能遇到的環境條件,包括錯誤頻率和型別。
- 風險評估:識別應用中的風險點,決定在哪裡引入錯誤以及頻率。
- 監控能力:高品質的監控系統,能夠觀察應用行為並確定混沌的影響。
測試應用通訊的混沌
測試應用通訊混沌的一個有效方法是在客戶端和服務之間放置代理。以ToxiProxy為例,我們可以這樣實作:
- 將現有服務重新命名(例如將
backend
改為backend-real
) - 建立ToxiProxy佈署,設定為監聽相同連線埠並轉發到實際服務
- 建立新服務指向ToxiProxy佈署
然後,我們可以開始增加混沌:
kubectl exec $SomeToxiProxyPod -- toxiproxy-cli toxic add -t latency -a latency=2000 backend
這會為所有透過代理的流量增加2000毫秒的延遲。
測試應用執行的混沌
除了通訊問題,我們還可以測試基礎設施不穩定的情況。最簡單的方法是刪除Pod:
NAMESPACE="some-namespace"
LABEL=k8s-app=my-app
PODS=$(kubectl get pods --selector=${LABEL} -n ${NAMESPACE} --no-headers | awk '{print $1}')
for x in $PODS; do
if [ $[ $RANDOM % 10 ] == 0 ]; then
kubectl delete pods -n $NAMESPACE $x;
fi;
done
這個指令碼會隨機刪除約10%的Pod。你可以擴充套件到刪除整個名稱空間中的Pod,甚至模擬節點故障。
模糊測試:安全與彈性的保障
模糊測試(Fuzz Testing)與混沌測試類別似,但它專注於引入技術上合法但極端的輸入,而非環境故障。例如,傳送包含重複欄位或特長資料的JSON請求。這有助於測試應用對隨機極端或惡意輸入的彈性,特別是在安全測試中非常有價值。
負載測試:瞭解應用在壓力下的表現
負載測試用於確定應用在負載下的行為。它使用負載測試工具生成等同於實際生產使用的流量,可以是人工生成的流量或從實際生產流量記錄並重放。
負載測試的目標
負載測試的核心目標是瞭解應用在負載下的行為。在開發過程中,應用通常只接受少量使用者的偶爾流量,這足以測試正確性,但無法瞭解在實際負載下的表現。
負載測試有兩個基本用途:
- 估計當前容量:瞭解系統能處理的最大負載
- 防止效能倒退:確保新版本能維持與舊版本相同的負載能力
預測性負載測試則用於在問題發生前預測它們。例如,如果你的應用流量每月增長10%,你可以執行110%當前峰值流量的測試,模擬下個月的情況。
負載測試的前提條件
成功的負載測試需要:
- 應用可觀察性:能夠驗證應用執行正常,並在失敗時提供足夠資訊
- 生成真實負載的能力:如果負載測試不能模擬真實使用者行為,其價值有限
生成真實流量
為應用生成真實流量模式的方法取決於應用型別。對於某些主要是讀取操作的網站,可能只需重複存取不同頁面。但對於涉及讀寫操作的應用,唯一的方法可能是記錄真實流量並重放。
然而,這種方法有幾個挑戰:
- 隱私和安全:請求可能包含私人資訊和安全令牌
- 時效性:請求可能與時間相關,幾週後重放可能行為不同
- 資料修改:重放修改儲存的請求必須在生產儲存的副本上執行
因此,更好的方法是建立服務使用模型,生成具有真實特性的合成負載。
使用負載測試調整應用
負載測試還可用於最佳化應用的資源利用率。對於任何服務,多個變數可能影響系統效能,如Pod數量、核心數和記憶體。
乍看之下,相同的總核心數(例如5個Pod各3核與3個Pod各5核)應該表現相同,但實際上往往不是。例如,使用垃圾收集的語言(如Java、.NET或Go)在不同核心設定下會有不同的垃圾收集器調整。
同樣,更多記憶體通常意味著更多內容可以保留在快取中,但這種好處有漸近限制。
透過設定不同設定的實驗集並執行負載測試,你可以識別系統效能的行為模式,選擇最高效的設定。
實驗:改進使用者經驗的科學方法
與混沌測試和負載測試不同,實驗不是用來發現服務架構和執行中的問題,而是識別改進使用者經驗的方法。實驗是對服務的長期變更,通常在使用者經驗方面,其中一小部分使用者(例如1%的流量)接收略有不同的體驗。
實驗的目標
當我們構建服務時,我們的目標是提供有用、易用與令使用者滿意的體驗。但如何知道我們是否達到了這個目標?瞭解使用者如何體驗我們的服務可能很困難。
傳統方法包括調查,詢問使用者對當前服務的感受。但這很難用來預測未來變更的影響。實驗的主要目標是在對使用者經驗產生最小影響的情況下學習。
實驗的前提條件
每個好的實驗都始於一個好的假設:我們認為某個變更會對使用者經驗產生什麼影響。
我們還需要能夠測量使用者經驗。這可以透過調查(如滿意度評分或淨推薦值)或與使用者行為相關的被動指標(如網站停留時間或點選頁面數)。
設定實驗
設定實驗有兩種方法:
在單一服務中包含多種體驗:在程式碼中包含實驗和對照版本,使用請求屬性(如HTTP頭、Cookie或查詢引數)在它們之間切換。
佈署服務的多個副本:佈署控制版本和實驗版本,使用服務網格將小部分流量路由到實驗版本。
第一種方法最簡單,但有兩個缺點:如果實驗程式碼不穩定,可能影響生產流量;與更改與服務完整發布繫結,更新實驗較慢。
第二種方法雖然更複雜,但更靈活和穩健。因為它需要完全新的程式碼佈署,設定成本較高,但因為它不影響除實驗流量外的任何內容,你可以隨時輕鬆佈署實驗的新版本。
此外,服務網格可以測量請求是否成功,如果實驗程式碼開始失敗,可以快速移除,最小化使用者影響。
在本文中,我們探討了三種不同的方法來瞭解你的服務,使其更具彈性、更高效與更有用。混沌測試幫助我們在控制環境中發現系統的弱點;負載測試確保我們的系統能夠承受預期的流量壓力;而實驗則幫助我們瞭解使用者對變更的反應,從而做出更好的決策。
就像單元測試是軟體開發過程的關鍵部分一樣,使用混沌、負載和實驗測試你的服務是服務設計和營運的關鍵部分。透過這些方法,你可以在問題影響使用者前發現並解決它們,確保你的系統不僅功能正確,還能在各種條件下穩定執行,並持續改進以滿足使用者需求。