Operator 是一種使用 CRD 的控制器,它把特定應用的維運知識封裝成演算法化、自動化的形式。這個模式把上一章的控制器模式進一步擴展,帶來更高的彈性與表現力。
問題#
「控制器」讓我們能以簡單、解耦的方式擴充 Kubernetes 平台。但對更進階的使用案例,單純的自訂控制器不夠強大——它們只能監看與管理 Kubernetes 內建資源。有時我們想為平台加入全新的概念,而這需要額外的領域物件。
舉例來說,假設我們選了 Prometheus 作為監控方案,希望以定義良好的方式把它加入 Kubernetes。如果能像定義其他 Kubernetes 資源那樣,用一個 Prometheus 資源來描述監控配置與所有部署細節,豈不美妙?更進一步,能不能有資源用來描述「我們要監控哪些服務」(例如用標籤選擇器)?
這正是 CustomResourceDefinition(CRD)大顯身手的場合:它們透過為叢集加入自訂資源、並讓這些資源用起來就像原生資源,來擴充 Kubernetes API。自訂資源,加上作用於這些資源之上的控制器,就構成了 Operator 模式。
Zelinskie(Jimmy Zelinskie)這段話大概最能刻畫 Operator 的特徵:
「Operator 是一種懂兩個領域的 Kubernetes 控制器:Kubernetes,以及別的東西。結合這兩方面的知識,它能自動化那些通常需要「同時懂這兩個領域的人類維運者」才能完成的任務。」
解法#
我們已經知道如何對預設 Kubernetes 資源的狀態變化做出高效反應——那是 Operator 模式的一半。另一半是:用 CRD 資源在 Kubernetes 上表達自訂資源。
CustomResourceDefinition#
有了 CRD,我們就能擴充 Kubernetes 來管理自己領域的概念。自訂資源與其他資源一樣,透過 Kubernetes API 管理,最終存放在後端儲存 Etcd 中(CRD 的歷史前身是 ThirdPartyResource)。
前述情境正是 CoreOS Prometheus operator 用這些自訂資源實作的,讓 Prometheus 得以無縫整合進 Kubernetes:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: prometheuses.monitoring.coreos.com # 名稱
spec:
group: monitoring.coreos.com # 所屬的 API group
names:
kind: Prometheus # 用來識別此資源實例的 kind
plural: prometheuses # 複數形的命名規則,用於指定此類物件的清單
scope: Namespaced # 範疇:叢集層級建立,或限於命名空間
version: v1 # CRD 的版本
validation:
openAPIV3Schema: .... # 供驗證用的 OpenAPI V3 綱要OpenAPI V3 綱要可讓 Kubernetes 驗證自訂資源。簡單的使用案例可以省略,但正式等級的 CRD 應該提供綱要,好讓組態錯誤能被及早偵測。
Kubernetes 還允許透過 spec 的 subresources 欄位為 CRD 指定兩種子資源:
scale:讓 CRD 指定自己如何管理複本數。可宣告「期望複本數所在的 JSON path」、「實際執行複本數所在的 path」,以及一個可選的「標籤選擇器 path」用來找出自訂資源實例的副本。標籤選擇器通常是可選的,但若你想搭配 HorizontalPodAutoscaler 使用此自訂資源,它就是必要的(見「彈性擴縮」)。status:設定此屬性後,會多出一個只允許改變 status 的 API 呼叫。這個呼叫可以單獨做安全控管,允許從控制器外部更新狀態。反之,當我們整份更新自訂資源時,status區段會被忽略——這與標準 Kubernetes 資源一致。
kind: CustomResourceDefinition
# ...
spec:
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas # 指向「宣告的複本數」的 JSON path
statusReplicasPath: .status.replicas # 指向「活躍複本數」的 JSON path
labelSelectorPath: .status.labelSelector # 指向「用來查詢活躍複本數之標籤選擇器」的 JSON path定義好 CRD 之後,就能輕鬆建立這樣的資源:
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus
spec:
serviceMonitorSelector:
matchLabels:
team: frontend
resources:
requests:
memory: 400Mimetadata: 區段的格式與驗證規則跟其他 Kubernetes 資源相同;spec: 包含 CRD 特定的內容,Kubernetes 會依 CRD 中給定的驗證規則加以驗證。
控制器與 Operator 的分類#
依 Operator 的行動性質,CRD 大致可分為:
- 安裝型 CRD(Installation CRDs):用來在 Kubernetes 平台上安裝與操作應用。典型例子是 Prometheus CRD,我們用它來安裝與管理 Prometheus 本身。
- 應用型 CRD(Application CRDs):用來表達應用特定的領域概念,讓應用能與 Kubernetes 深度整合。例如 Prometheus operator 用
ServiceMonitorCRD 來註冊「要被 Prometheus 伺服器抓取的特定 Kubernetes Service」,並據此調整 Prometheus 伺服器組態。
一個 Operator 可以同時作用於不同種類的 CRD(Prometheus operator 就是如此)。這兩類 CRD 的界線是模糊的。
在我們的分類中,Operator「is-a」使用 CRD 的控制器——Operator 具備控制器的所有特徵,再加上一點別的。但即使這個區分也有些模糊,中間存在各種變體。
其中一種變體是:用 ConfigMap 取代 CRD 的控制器。當預設 Kubernetes 資源不夠用、但建立 CRD 又不可行時,這個做法就很合理。此時 ConfigMap 是絕佳的中間地帶,讓領域邏輯封裝在 ConfigMap 的內容中。
| 用 CRD | 用純 ConfigMap | |
|---|---|---|
| 權限需求 | 註冊 CRD 需要 cluster-admin 權限 | 不需要(某些叢集配置下根本無法註冊 CRD,例如 OpenShift Online 這類公有叢集) |
| 工具支援 | 有 kubectl get 等工具支援 | 沒有 |
| 驗證 | API Server 層級驗證 | 無 |
| API 版本控管 | 支援 | 不支援 |
status 欄位 | 可自由定義狀態模型 | 幾乎無法左右其形態 |
| 權限模型 | 可依 CRD 種類做細緻的 RBAC 調校 | 同一命名空間內所有 ConfigMap 共用同一組權限 |
即便如此,把 CRD 換成純 ConfigMap 作為領域特定組態時,你仍然可以沿用 Observe-Analyze-Act 的概念。
從實作角度看,控制器只操作原生 Kubernetes 物件、還是管理自訂資源,是有差別的:前者所有型別在你選用的 Kubernetes 客戶端函式庫中都現成可用;CRD 則沒有開箱即用的型別資訊,你可以採用無綱要(schemaless)的方式管理 CRD 資源,或自行定義自訂型別(可能基於 CRD 定義中的 OpenAPI 綱要)。對具型別 CRD 的支援程度,因客戶端函式庫與框架而異。

圖 23-1:控制器與 Operator 的光譜——從較簡單的資源定義選項到更進階者,兩者的界線就在「是否使用自訂資源」
延伸:API 聚合層——比 CRD 更進階的擴充鉤子
當 Kubernetes 託管的 CRD 不足以表達問題領域時,你可以用**自己的聚合層(aggregation layer)**擴充 Kubernetes API:把一個自行實作的 APIService 資源,作為新的 URL 路徑加入 Kubernetes API。
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1alpha1.sample-api.k8spatterns.io
spec:
group: sample-api.k8spattterns.io
service:
name: custom-api-server
version: v1alpha1除了 Service 與 Pod 的實作之外,還需要一些額外的安全組態,設定 Pod 執行所用的 ServiceAccount。
設定完成後,所有送往 https://<api server ip>/apis/sample-api.k8spatterns.io/v1alpha1/namespaces/<ns>/... 的請求,都會被導向我們的自訂 Service 實作。如何處理這些請求(包括持久化經此 API 管理的資源)就完全由這個自訂 Service 自己負責——這與 CRD 的情況不同,後者由 Kubernetes 完整管理自訂資源。
有了自訂 API Server,你的自由度大得多,能做的遠不只監看資源生命週期事件。但你也得實作多得多的邏輯,因此對典型使用案例而言,處理純 CRD 的 operator 往往已經夠好。
可參考官方文件、完整的 sample-apiserver,以及有助於實作 API Server 聚合的 apiserver-builder 函式庫。
Operator 的開發與部署框架#
撰寫本書時(2019 年),Operator 開發是 Kubernetes 中活躍演進的領域,已有數套工具組與框架。三個主要專案為:
Operator Framework(CoreOS)
為開發 Go 語言的 operator 提供廣泛支援,包含數個子元件:
- Operator SDK:提供存取 Kubernetes 叢集的高階 API,以及啟動 operator 專案的鷹架。
- Operator Lifecycle Manager(OLM):管理 operator 及其 CRD 的發布與更新,可以把它想成一種「operator 的 operator」。
- Operator Metering:為 operator 提供報表功能。
OLM 是一個在背景執行的叢集服務,以具備安裝 CRD 權限的 service account 執行。它註冊了一個專屬的 CRD
ClusterServiceVersion(CSV),讓我們能指定 operator 的 Deployment 以及與該 operator 關聯的 CRD 定義。CSV 一經建立,OLM 的一部分就等待該 CRD 與其所有相依 CRD 完成註冊;條件滿足後便部署 CSV 中指定的 operator。OLM 的另一部分則可代表非特權使用者註冊這些 CRD——這是讓一般叢集使用者安裝自己 operator 的優雅做法。
Kubebuilder
由 SIG API Machinery 主導的專案,文件完備。與 Operator SDK 類似,支援 Go 專案鷹架與單一專案內多個 CRD 的管理。
與 Operator Framework 的細微差異在於:Kubebuilder 直接與 Kubernetes API 打交道,而 Operator SDK 在標準 API 之上加了一些抽象,較易使用(但少了一些花俏功能)。Kubebuilder 對 operator 安裝與生命週期管理的支援不如 OLM 精巧;兩個專案有大量重疊,最終可能以某種方式匯流。
Metacontroller(Google Cloud Platform)
與前兩者非常不同:它用 API 擴充 Kubernetes,把「撰寫自訂控制器」的共通部分封裝起來。它的行為類似 Kubernetes Controller Manager,執行多個控制器,但這些控制器不是寫死的,而是透過 Metacontroller 專屬的 CRD 動態定義。換言之,它是一個委派式控制器,會呼叫出去、由外部服務提供真正的控制器邏輯。
另一種描述方式是「宣告式行為」:CRD 讓我們能在 Kubernetes API 中儲存新型別,而 Metacontroller 讓我們能以宣告方式定義標準或自訂資源的行為。
透過 Metacontroller 定義控制器時,我們只需提供一個「只含自身控制器商業邏輯」的函式。Metacontroller 處理所有與 Kubernetes API 的互動、代我們執行調和迴圈,並透過 webhook 呼叫我們的函式——webhook 帶著描述 CRD 事件的明確定義負載被呼叫;函式回傳值即是「應代表我們的控制器函式建立(或刪除)的 Kubernetes 資源定義」。
這種委派讓我們能用任何懂 HTTP 與 JSON 的語言撰寫函式,完全不依賴 Kubernetes API 或其客戶端函式庫。函式可以託管在 Kubernetes 上、FaaS 供應商上,或其他任何地方。
若你的使用案例是用簡單的自動化或編排來擴充與客製 Kubernetes、且不需要額外功能,就值得看看 Metacontroller——尤其當你想用 Go 以外的語言實作商業邏輯時。
範例:ConfigWatcher Operator#
延續上一章的控制器範例,我們引入一個 ConfigWatcher 型別的 CRD。它的實例指定「要監看哪個 ConfigMap」以及「該 ConfigMap 改變時要重啟哪些 Pod」。
這個做法的兩個進步:
- 移除了 ConfigMap 對 Pod 的依賴,我們不必再修改 ConfigMap 本身去加上觸發用的註解。
- 上一章基於註解的簡單做法只能把一個 ConfigMap 連到單一應用;有了 CRD,ConfigMap 與 Pod 的任意組合都成為可能。
kind: ConfigWatcher
apiVersion: k8spatterns.io/v1
metadata:
name: webapp-config-watcher
spec:
configMap: webapp-config # 要監看的 ConfigMap 引用
podSelector: # 用來決定要重啟哪些 Pod 的標籤選擇器
app: webapp對應的 CRD 定義:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: configwatchers.k8spatterns.io
spec:
scope: Namespaced # 綁定到命名空間
group: k8spatterns.io # 專屬的 API group
version: v1 # 初始版本
names:
kind: ConfigWatcher # 此 CRD 獨一無二的 kind
singular: configwatcher # 供 kubectl 等工具使用的資源標籤
plural: configwatchers
validation:
openAPIV3Schema: # 此 CRD 的 OpenAPI V3 綱要規格
properties:
spec:
properties:
configMap:
type: string
description: "Name of the ConfigMap"
podSelector:
type: object
description: "Label selector for Pods"
additionalProperties:
type: string要讓 operator 能管理這型別的自訂資源,必須為它的 Deployment 附上具適當權限的 ServiceAccount。我們引入一個專屬 Role,稍後透過 RoleBinding 綁到該 ServiceAccount:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: config-watcher-crd
rules:
- apiGroups:
- k8spatterns.io
resources:
- configwatchers
- configwatchers/finalizers
verbs: [get, list, create, update, delete, deletecollection, watch]接著調整上一章控制器腳本的事件迴圈。ConfigMap 更新時,不再檢查特定註解,而是查詢所有 ConfigWatcher 資源,看被修改的 ConfigMap 是否出現在某個 configMap: 值中:
# 針對指定命名空間,開啟監看 ConfigMap 變更的 watch 串流
curl -Ns $base/api/v1/${ns}/configmaps?watch=true | \
while read -r event
do
type=$(echo "$event" | jq -r '.type')
if [ $type = "MODIFIED" ]; then # 只檢查 MODIFIED 事件
watch_url="$base/apis/k8spatterns.io/v1/${ns}/configwatchers"
config_map=$(echo "$event" | jq -r '.object.metadata.name')
# 取得所有已安裝的 ConfigWatcher 自訂資源清單
watcher_list=$(curl -s $watch_url | jq -r '.items[]')
# 從清單中取出所有引用此 ConfigMap 的 ConfigWatcher
watchers=$(echo $watcher_list | \
jq -r "select(.spec.configMap == \"$config_map\") | .metadata.name")
# 對每個找到的 ConfigWatcher,依其選擇器刪除所設定的 Pod
for watcher in watchers; do
label_selector=$(extract_label_selector $watcher)
delete_pods_with_selector "$label_selector"
done
fi
done為求清晰,此處省略了計算標籤選擇器與刪除 Pod 的邏輯。與上一章相比,測試用的範例網頁應用唯一的差別是:應用組態使用的是一個沒有註解的 ConfigMap。
真實世界的 Operator 範例
我們這個基於 shell script 的 operator 雖然可用,但仍相當簡單,未涵蓋邊角與錯誤情況。野外有許多更有趣的正式等級範例:
- Awesome Operators 收錄了一份基於本章概念的真實 operator 清單。
- Prometheus operator(Go):管理 Prometheus 安裝。
- Etcd Operator(Go):管理 Etcd 鍵值儲存,自動化備份與還原資料庫等維運任務。
- Strimzi Operator(Java):管理 Apache Kafka 這類複雜訊息系統在 Kubernetes 上的運行,是 Java operator 的絕佳範例。
- JVM Operator Toolkit:為 Java 及 Groovy、Kotlin 等 JVM 語言撰寫 operator 提供基礎,並附有一組範例。
討論#
許多情況下,一個操作標準資源的單純控制器就已經夠好——它的優點是註冊時不需要 cluster-admin 權限,但在安全與驗證方面有其侷限。
Operator 適合用來模擬「與 Kubernetes 宣告式、反應式控制器處理資源的方式契合」的自訂領域邏輯。更具體地說,出現以下任一情況時,就值得為你的應用領域考慮 Operator 搭配 CRD:
- 你想與
kubectl等既有 Kubernetes 工具緊密整合。 - 你正在做綠地專案,可以從頭設計應用。
- 你能從資源路徑、API group、API 版本控管,尤其是命名空間這些 Kubernetes 概念中獲益。
- 你想要現成的良好客戶端支援來存取 API,包含 watch、認證、基於角色的授權,以及中繼資料選擇器。
若你的使用案例符合上述條件,但需要在「自訂資源如何實作與持久化」上有更大彈性,可以考慮自訂 API Server。
但也別把 Kubernetes 擴充點當成解決一切的金鎚。若你的使用案例不是宣告式的、要管理的資料不契合 Kubernetes 資源模型、或你並不需要與平台緊密整合,那麼寫一個獨立的 API、再用傳統的 Service 或 Ingress 物件把它曝露出去,多半是更好的選擇。
更多資訊#
- Operator Example
- Operator Framework
- OpenAPI V3
- Kubebuilder
- Kubernetes Client Libraries
- Metacontroller
- JVM Operator Toolkit
- Extend the Kubernetes API with CustomResourceDefinitions
- Awesome Operators in the Wild
- Custom Resources Versus API Server Aggregations
- Comparison of Kubebuilder, Operator Framework, and Metacontroller
- TPR Is Dead! Kubernetes 1.7 Turns on CRD
- Code Generation for Custom Resources
- A Sample Operator in Go
- Prometheus Operator
- Etcd Operator
- Memhog Operator