深刻理解Kubernetes Operator

深刻理解Kubernetes Operator

本文要點:linux

  • Kubernetes API 爲全部雲資源提供了單個集成點,以此來促進雲原生技術的採用。git

  • 有一些框架和庫能夠用來簡化 Operator 的編寫。支持多種語言,其中 Go 生態系統是最爲成熟的。sql

  • 你能夠爲非自有的軟件建立 Operator。DevOps 團隊可能會經過這種方式來管理數據庫或其餘外部產品。數據庫

  • 難點不在於 Operator 自己,而是要學會理解它的行爲。

多年來,Operator 一直是 Kubernetes 生態系統的重要組成部分。經過將管理界面移動到 Kubneretes API 中,帶來了「單層玻璃」的體驗。對於但願簡化 kuberentes 原生應用程序的開發人員或者但願下降現有系統複雜性的 DevOps 工程師來講,Operator 多是一個很是有吸引力的選擇。但如何從頭開始建立一個 Operator 呢?編程

深刻理解 Operator

Operator 是什麼?

現在,Operator 無處不在。數據庫、雲原生項目、任何須要在 Kubernetes 上部署或維護的複雜項目都用到了 Operator。CoreOS 在 2016 年首次引入了 Operator,將運維關注點轉移到軟件系統中。Operator 自動執行操做,例如,Operator 能夠部署數據庫實例、升級數據庫版本或執行備份。而後,這些系統能夠被測試,響應速度比人類工程師更快。json

Operator 還經過使用自定義資源定義對 Kubenretes API 進行了擴展,將工具配置轉移到了 API 中。這意味着 Kubenretes 自己就變成了「單層玻璃」。DevOps 工程師能夠利用圍繞 Kubernetes API 資源而構建的工具生態系統來管理和監控他們部署的應用程序:api

  • 使用 Kubernetes 內置的基於角色的訪問控制 (RBAC) 來修改受權和身份驗證。緩存

  • 使用「git ops」對生產變動進行可複製的部署和代碼審查。安全

  • 使用基於開放策略代理 (OPA) 的安全工具在自定義資源上應用策略。服務器

  • 使用 Helm、Kustomize、ksonnet 和 Terraform 等工具簡化部署描述。

這種方法還能夠確保生產、測試和開發環境之間的一致性。若是每一個集羣都是 Kubernetes 集羣,則可使用 Operator 在每一個集羣中部署相同的配置。

爲何要使用 Operator?

使用 Operator 有不少理由。一般狀況下,要麼是開發團隊爲他們的產品建立 Operator,要麼是 DevOps 團隊但願對第三方軟件管理進行自動化。不管哪一種方式,都應該從肯定 Operator 應該負責哪些東西開始。

最基本的 Operator 用於部署,使用 kubectl apply 就能夠建立一個用於響應 API 資源的數據庫,但這比內置的 Kubernetes 資源 (如 StatefulSets 或 Deployments) 好不了多少。複雜的 Operator 將提供更大的價值。若是你想要對數據庫進行伸縮該怎麼辦?

若是是 StatefulSet,你能夠執行 kubectl scale statefulset my-db --replicas 3,這樣就能夠獲得 3 個實例。但若是這些實例須要不一樣的配置呢?是否須要指定一個實例爲主實例,其餘實例爲副本?若是在添加新副本以前須要執行設置步驟,那該怎麼辦?在這種狀況下,可使用 Operator。

更高級的 Operator 能夠處理其餘一些特性,如響應負載的自動伸縮、備份和恢復、與 Prometheus 等度量系統的集成,甚至能夠進行故障檢測和自動調優。任何具備傳統「運行手冊」文檔的操做均可以被自動化、測試和依賴,並自動作出響應。

被管理的系統甚至不須要部署在 Kubernetes 上也能從 Operator 中獲益。例如,主要的雲服務提供商(如 Amazon Web Services、微軟 Azure 和谷歌雲)提供 Kubenretes Operator 來管理其餘雲資源,如對象存儲。用戶能夠經過配置 Kubernetes 應用程序的方式來配置雲資源。運維團隊可能對其餘資源也採起一樣的方法,使用 Operator 來管理任何東西——從第三方軟件服務到硬件。

Operator 示例

在本文中,咱們將重點關注 etcd-cluster-operator。這是我和一些同事共同開發的 Operator,用於管理 Kubernetes 內部的 etcd。本文不是專門介紹 Operator 或 etcd 自己,因此我不會太過詳細介紹 etcd 的細節,只要可以讓你瞭解 etcd 的用途便可。

簡單地說,etcd 是一個分佈式鍵值數據存儲。它有能力管理本身的穩定性,只要:

  • 每一個 etcd 實例都有一個用於計算、網絡和存儲的獨立故障域。

  • 每一個 etcd 實例都有一個惟一的網絡名稱。

  • 每一個 etcd 實例均可以鏈接到其餘實例。

  • 每一個 etcd 實例都知道其餘實例的存在。

此外:

  • etcd 集羣的增加或縮小須要使用 etcd 管理 API 進行特定的操做,在添加或刪除實例以前聲明集羣要發生的變化。

  • 可使用 etcd 管理 API 上的「快照」端點進行備份。經過 gRPC 調用它,你將獲得一個備份文件。

  • 使用 etcdctl 工具操做備份文件和 etcd 主機上的數據目錄來實現恢復。這在真實的機器上很容易,但在 Kubernetes 上須要作一些協調。

正如你所看到的,這比 Kubernetes StatefulSet 能作更多的事情,因此咱們使用 Operator。咱們不會深刻討論 etcd-cluster-operator 的機制,但在本文的其他部分,咱們都將引用這個 Operator 示例。

Operator 剖析

Operator 由兩部分組成:一個或多個 Kubernetes 自定義資源定義 (CRD),它們描述了一種新的資源,包括應該具備哪些字段。CRD 可能會有多個,例如 etcd-cluster-operator 同時使用 EtcdCluster 和 EtcdPeer 來封裝不一樣的概念。

一個運行中的軟件,讀取自定義資源並做出響應。

一般,Operator 被包含並部署在 Kubernetes 集羣中,一般使用一個簡單的 Deployment 資源。理論上,只要 Operator 可以與集羣的 Kubernetes API 通訊,它就能夠在任何地方運行。可是,在集羣中運行 Operator 一般更容易。一般狀況下會使用自定義 Namespace 將 Operator 與其餘資源分隔開來。

若是咱們使用這種方法來運行 Operator,還須要作一些事情:

  • 一個容器鏡像,其中包含 Operator 可執行文件。

  • 一個 Namespace。

  • Operator 的 ServiceAccount,授予讀取自定義資源的權限,並配置它要管理的資源 (例如 Pod)。

  • 用於 Operator 容器的 Deployment。

  • ClusterRoleBinding 和 ClusterRole 資源,綁定到 ServiceAccount。

  • Webhook 配置。

稍後咱們將詳細討論權限模型和 Webhook。

軟件和工具

第一個問題是編程語言和生態系統。從理論上講,幾乎任何可以進行 HTTP 調用的語言均可以使用 Operator。假設 Operator 部署在與資源相同的集羣中,那麼只須要在集羣容器中運行它便可。一般是 linux/x86_64,這也是 etcd-cluster-operator 的目標平臺,但 Operator 也能夠被編譯成 arm64 或其餘架構,甚至是 Windows 容器。

Go 語言擁有最成熟的工具。用於構建 Kubernetes 控制器的框架 controller-runtime 能夠做爲一個獨立的工具。此外,Kubebuilder 和 Operator SDK 等項目都構建在控制器運行時之上,目的是提供一種流線化的開發體驗。

除了 Go 語言,其餘語言 (如 Java、Rust、Python 和其餘語言) 一般會提供用於鏈接 Kubernetes API 或者專門用於構建 Operator 的工具。這些工具的成熟度和支持水平各有差異。

另外一種選擇是經過 HTTP 直接與 Kubernetes API 交互。這種方式所需的工做量最大,好處是團隊可使用他們最熟悉的編程語言。

最終,這種選擇取決於負責構建和維護 Operator 的團隊。若是團隊已經習慣使用 Go,那麼 Go 生態系統豐富的工具顯然是最佳的選擇。若是團隊尚未使用 Go,那麼就須要作出權衡,要麼在學習和培訓更成熟的生態系統工具方面付出代價,要麼選擇不成熟但團隊熟悉其底層語言的生態系統。

對於 etcd-cluster-operator 來講,開發團隊已經很是精通 Go,所以 Go 對咱們來講是一個很明智的選擇。咱們還選擇使用 Kubebuilder 而不是 Operator SDK,但這只是由於咱們對它比較熟悉。咱們的目標平臺是 linux/x86_64,但若是須要的話,也能夠以其餘平臺爲目標。

自定義資源和目標狀態

咱們爲咱們的 etcd Operator 建立了一個叫做 EtcdCluster 的自定義資源定義。安裝好 CRD 後,用戶就能夠建立 EtcdCluster 資源。EtcdCluster 資源描述了 etcd 集羣的需求,並給出了它的配置。

PlainTextapiVersion:etcd.improbable.io/v1alpha1kind:EtcdClustermetadata: name: my-first-etcd-clusterspec: replicas: 3 version: 3.2.28

apiVersion 指定這是哪一個版本的 API,在本例中是 v1alpha1。kind 聲明這是一個 EtcdCluster。與其餘類型的資源同樣,咱們有一個 metadata,它必須包含一個 name,也可能包含一個 namespace、labels、annotations 和其餘標準項。這樣咱們就能夠像對待 Kubernetes 中的其餘資源同樣對待 EtcdCluster。例如,咱們可使用一個標籤來標識哪一個團隊負責哪個集羣,而後經過 kubectl get etcdcluster -l team=foo 搜索這些集羣,就像使用其餘標準資源同樣。

spec 字段包含了有關這個 etcd 集羣的運維信息。還有不少其餘字段,但這裏咱們只介紹最基本的字段。version 字段描述要部署的 etcd 版本,replicas 字段描述有多少個實例。

還有一個 status 字段 (在示例中不可見),運維人員用這個字段來描述集羣的當前狀態。spec 和 status 是 Kubernetes API 提供的標準字段,能夠很好地與其餘資源和工具集成。

由於咱們使用了 Kubebuilder,因此能夠藉助工具生成這些自定義資源定義。咱們寫了一個 Go 結構體,定義了 spec 和 status 字段:

type EtcdClusterSpec struct {
    Version     string               `json:"version"`
    Replicas    *int32               `json:"replicas"`
    Storage     *EtcdPeerStorage     `json:"storage,omitempty"`
    PodTemplate *EtcdPodTemplateSpec `json:"podTemplate,omitempty"`
}

基於這個 Go 結構體(和一個相似的 status 結構體),Kubebuilder 會生成咱們的自定義資源定義,咱們只須要編寫代碼處理調解邏輯便可。

其餘語言提供的支持可能有所不一樣。若是你使用的是專爲 Operator 設計的框架,那麼可能會生成這個,例如 Rust 庫 kube-derive 的生成方式就跟這個差很少。若是有團隊直接使用 Kubernetes API,那麼他們就必須分別編寫 CRD 和用於解析數據的代碼。

調解循環

如今咱們已經有了描述 etcd 集羣的方式,能夠構建 Operator 來管理集羣資源。Operator 能夠以任何方式運行,而幾乎全部 Operator 均可以使用控制器模式。

控制器是一種簡單的程序循環,一般被稱爲「調解循環」,它能夠執行如下邏輯:

  1. 觀察指望的狀態。

  2. 觀察所管理資源的當前狀態。

  3. 採起行動,使託管的資源處在指望的狀態。

對於 Kubernetes 中的 Operator,目標狀態就是資源(示例中是 EtcdCluster 的 spec 字段指定的值)。咱們的託管資源能夠是集羣內部或外部的任何資源。在咱們的示例中,咱們將建立其餘 Kubneretes 資源,如 ReplicaSets、PersistentVolumeClaims 和 Services。

對於 etcd,咱們直接鏈接到 etcd 進程,使用管理 API 來獲取它的狀態。這種「非 kubernetes」的訪問方式須要當心一點,由於它可能會受到網絡中斷的影響,因此對於這種狀況,並不必定是由於服務被關閉了。咱們不能將沒法鏈接到 etcd 做爲 etcd 沒有在運行的信號 (若是咱們這麼認爲了,那麼重啓 etcd 實例只會加劇網絡中斷的發生)。

一般,在與非 Kubernetes API 服務通訊時,最重要的是要考慮可用性或一致性。對於 etcd 來講,若是咱們得到響應,那它們必定是一致的,但其餘系統可能不是這樣。關鍵要避免因爲信息過期而致使錯誤操做,從而使中斷變得更糟。

控制器的特性

對於控制器來講,最簡單的就是定時運行調解循環,好比每 30 秒一次。這樣作是能夠的,但有不少缺點。例如,它必須可以檢測上一次循環是否還在運行,這樣就不會同時運行兩個循環。此外,這意味着每 30 秒會對 Kubernetes 進行一次完整的掃描來得到相關的資源,而後,對於 EtcdCluster 的每一個實例,須要運行調解函數來得到相關 Pod 和其餘資源。這種方式給 Kubernetes API 形成大量的負載。

這也致使出現了一種很是「程序性」的方法,由於在下一次協調以前可能須要很長時間才能儘量快地執行每一個循環。例如,一次性建立多個資源。這可能會致使一種很是複雜的狀態,運維人員須要進行不少檢查才能知道要作什麼,並且頗有可能會出錯。

爲了解決這個問題,控制器提供了一些特性:

  • Kubernetes API 監聽。

  • API 緩存。

  • 批量更新。

全部這些均可以有效地減小要執行的任務,由於運行單個循環的成本和須要等待的時間都減小了,協調邏輯的複雜性也就下降了。

API 監聽

Kubernetes API 支持「監聽」,而不是定時掃描。API 使用者能夠對感興趣的資源或資源類別進行註冊,並在匹配的資源發生變動時收到通知。由於請求負載減小了,因此 Operator 大部分時間處於空閒狀態,並且幾乎能夠當即對變動作出響應。Operator 框架一般會爲你處理監聽所需的註冊和管理操做。

這種設計的另外一個結果是你還須要監聽你所建立的資源。例如,若是咱們建立了 Pods,那麼也必須監聽咱們建立的 Pod。若是它們被刪除或修改,致使與咱們想要的狀態不一致,咱們就能夠收到通知,並糾正它們。

咱們如今能夠進一步簡化調解程序。例如,爲了響應 EtcdCluster,Operator 但願建立一個 Service 和一些 EtcdPeer 資源。它不是一次性建立好它們,而是先建立 Service,而後退出。但由於咱們關注了本身的 Services,咱們會收到通知,並當即從新進行調解。

這樣咱們就能夠建立對等資源了。不然,咱們將建立大量的資源,而後爲每一個資源從新調解一次,這可能會觸發更多的從新調解。這種設計有助於保持調解器循環的簡單,由於只須要執行一個操做就退出,開發人員不須要處理複雜的狀態。

這樣作的一個主要後果是可能會錯過更新。網絡中斷、Pod 重啓和其餘問題在某些狀況下可能致使錯過事件。爲了解決這個問題,關鍵在於 Operator 的運行方式應該「基於條件」而不是「基於邊緣」。

這些術語來自信號控制軟件,是指基於信號電壓作出響應。在軟件領域,當咱們說「基於邊緣」時,意思是「對事件作出反應」,當咱們說「基於條件」時,意思是「對觀察到的狀態作出反應」。

例如,若是一個資源被刪除,咱們能夠觀察到刪除事件並選擇從新建立。可是,若是咱們錯過了刪除事件,就可能永遠不會嘗試從新建立。或者,更糟糕的是,咱們認爲它還在,致使後續出現問題。相反,「基於條件」的方法將觸發器簡單地視爲應該從新進行調解。它將再次觀察外部狀態,丟棄觸發它的變動。

API 緩存

控制器的另外一個主要特性是緩存請求。若是咱們請求 Pods,而且會在 2 秒後再次觸發,那麼咱們可能會爲第二個請求保留緩存結果。這減小了 API 服務器的負載,但也給開發人員帶來了一些須要注意的問題。

因爲資源請求可能過時,咱們必須處理這個問題。資源建立沒有被緩存,所以可能出現這種狀況:

  • 調解 EtcdCluster 資源

  • 搜索 Service,沒有找到。

  • 建立 Service 並退出。

  • 對建立的 Service 作出響應。

  • 搜索 Service,緩存過時,找不到。

  • 建立 Service。

咱們錯誤地建立了一個相同的 Service。Kubernetes API 將會處理這個問題,並給出一個錯誤,說明 Service 已經存在。所以,咱們必須處理這個問題。通常來講,最好的作法是在之後的某個時間進行從新調解。在 Kubebuilder 中,只是簡單地在 reconcile 函數中返回一個錯誤就會致使這種狀況發生,但不一樣的框架可能會有所不一樣。當稍後從新運行時,緩存最終會保持一致,並可能發生下一階段的調解。

這樣作的一個反作用是全部資源都必須有肯定的名稱。不然,若是咱們建立了一個重複的資源,可能會使用不一樣的名稱,致使真正的資源重複。

批量更新

在某些狀況下,咱們可能會同時進行不少個調解。例如,若是咱們正在監聽大量的 Pod 資源,其中有很資源同時處於中止狀態 (例如,因爲節點故障、管理員操做錯誤,等等),那麼咱們但願獲得屢次通知。然而,在第一次調解觸發並觀察到集羣狀態時,全部的 Pod 都已經消失了,那麼後續的調解就是沒有必要的。

若是數量很小,這就不是一個問題。但在較大的集羣中,當一次處理數百或數千個更新時,這樣作有可能會致使調解循環慢得像爬行同樣,由於它一次性重複 100 次相同的操做,甚至會致使隊列超載,並最終致使 Operator 崩潰。

由於咱們的調解函數是「基於條件」的,因此咱們能夠對其加以優化來解決這個問題。當咱們將特定資源的更新操做放入隊列時,若是隊列中已經有該資源的更新操做,那麼就將其刪除。在從隊列讀取數據以前先等待一下,咱們就能夠有效地進行「批量」操做。所以,若是 200 個 Pod 同時中止,咱們可能只須要進行一次調解,具體取決於 Operator 及其隊列的配置狀況。

權 限

訪問 Kubernetes API 必須提供憑證。在集羣中,這是由 ServiceAccount 負責處理的。咱們可使用 ClusterRole 和 ClusterRoleBinding 資源將權限與 ServiceAccount 關聯起來。對於 Operator 來講,這很關鍵。Operator 必須擁有權限來 get、list 和 watch 它在整個集羣中管理的資源。此外,對於它建立的任何資源,都須要權限。例如,Pods、StatefulSets、Services 等。

Kubebuilder 和 Operator SDK 等框架能夠爲你提供這些權限。例如,Kubebuilder 採用了註解爲每一個控制器分配權限。若是多個控制器合併爲一個二進制文件 (就像咱們對 etcd-cluster-operator 所作的那樣),那麼權限也將合併在一塊兒。

//+kubebuilder:rbac:groups=etcd.improbable.io,resources=etcdpeers,verbs=get;list;watch
//+kubebuilder:rbac:groups=etcd.improbable.io,resources=etcdpeers/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps,resources=replicasets,verbs=list;get;create;watch
//+kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=list;get;create;watch;delete

這是 EtcdPeer 資源的調解器權限。能夠看到,咱們 get、list 和 watch 本身的資源,而且能夠 update 和 patch 狀態子資源。咱們能夠只更新狀態,將信息顯示給其餘用戶。最後,咱們對所管理的資源具備普遍的權限,能夠根據須要建立和刪除它們。

驗證和默認值

雖然自定義資源自己提供了必定級別的驗證和默認值,但更復雜的檢查操做須要由 Operator 來執行。最簡單的方法是在 Operator 讀取資源時執行這些操做,不管是 watch 返回的,仍是手動讀取後。可是,這意味着默認值將永遠不會被應用到 Kubernetes 中,這種行爲會讓管理員感到困惑。

更好的方法是使用驗證和可變的 Webhook 配置。這些資源告訴 Kubernetes,當一個資源被建立、更新或者在持久化以前被刪除時,必須使用 Webhook。

例如,可變 Webhook 能夠用來設置默認值。在 Kubebuilder 中,咱們提供了一些額外的配置來建立 MutatingWebhookConfiguration,Kubebuilder 負責提供 API 端點。咱們只須要在 spec 結構體中設置 Default 值。而後,當資源被建立時,Webhook 在持久化資源以前被調用,就會應用默認值。

不過,咱們仍然要在讀取資源時應用默認值。Operator 不能假設已經知道平臺是否啓用了 Webhook。即便啓用了,也可能配置錯誤,或者由於網絡中斷致使 Webhook 被跳過,或者資源可能在配置 Webhook 以前就已經被應用過了。全部這些問題都意味着,雖然 Webhook 提供了更好的用戶體驗,但 Operator 代碼不能徹底依賴它們,必須再次應用默認值。

測 試

任何一個單獨的邏輯單元均可以使用編程語言的常規工具進行單元測試,可是,在進行集成測試時會出現一些特定的問題。咱們可能會把 API 服務器當成能夠被 mock 的數據庫。但在真實的系統中,API 服務器會執行大量的驗證和默認操做。這意味着測試和現實之間的行爲多是不同的。

通常來講,主要有兩種方式:

第一種方法,下載測試工具並執行 kube-apiserver etcd 可執行文件,建立一個真正的 API 服務器。固然,雖然你能夠建立一個 ReplicaSet,但缺乏了能夠建立 Pods 的 Kubernetes 組件,因此咱們看不到有東西真正在運行。

第二種方法更加全面一些,它使用一個真正的 Kubernetes 集羣,能夠運行 Pods,並能準確作出響應。經過使用 kind,這種集成測試變得更加容易。kind 是「Kubernetes in Docker」的縮寫,它能夠在任何能夠運行 Docker 容器的地方運行一個完整的 Kubernetes 集羣。它提供了一個 API 服務器,能夠運行 Pods,並運行 Kubernetes 全部主要的組件。所以,使用了 kind 的測試能夠在筆記本電腦上或 CI 中運行,並提供近乎完美的 Kubernetes 體驗。

總 結

在這篇文章中,咱們談到了不少想法:

  • 將 Operator 做爲 Pods 部署在集羣中。

  • 能夠支持任何一種編程語言,因此請選擇最適合團隊的那一種。不過,Go 語言擁有最成熟的生態系統。

  • 當心使用非 kubernetes 資源,特別是在網絡中斷或上游 API 發生故障時,它們可能會致使更嚴重的中斷。

  • 在每一個調解週期中執行一個操做,而後退出,並容許 Operator 從新將其放入隊列。

  • 使用「基於條件」的方法,忽略觸發調解的事件的內容。

  • 爲新資源使用肯定性的命名。

  • 爲你的服務賬戶提供最小權限。

  • 在 Webhook 和代碼中應用默認值。

  • 使用 kind 進行集成測試。

有了這些工具,你就能夠構建 Operator 來簡化部署,並減輕運維團隊的負擔,不管是你所擁有的應用程序,仍是你本身開發的應用程序。

原文連接:Kubernetes Operators in Depth
https://www.infoq.com/articles/kubernetes-operators-in-depth/

做者 | James Laverack譯者 | 王者文章轉自| InfoQ

相關文章
相關標籤/搜索