使用 shell-operator 實現(xiàn) Operator

在本文我們將介紹簡化 Kubernetes Operator 創(chuàng)建的方法,并展示如何使用 shell-operator 輕松實現(xiàn)自己的 Operator。本文基于我們在 KubeCon Europe 2020上的最新演講,這是此演講的完整視頻[1]
Kubernetes API 和控制器
我們可以將 Kubernetes API 看成包含每種對象文件夾的文件服務(wù)器,這些資源對象通過服務(wù)器上的 YAML 文件來表示。APIServer 有一個基本的 HTTP API,使我們可以對這些對象執(zhí)行三件事。我們可以:
根據(jù)資源類型和名稱獲取資源 更改資源 watch 資源
換句話說,我們可以將 Kubernetes 看作基本上是具有三種通用方法的YAML 文件服務(wù)器(當(dāng)然還有其他方法,我們現(xiàn)在可以先忽略它們)。

但是,服務(wù)端本身只能存儲信息,為了使其正常工作,我們需要一個控制器 - Kubernetes 中第二重要的基礎(chǔ)工具。
通常,有兩種類型的控制器,第一種類型從 Kubernetes 讀取信息,使用某種邏輯對其進行處理,然后將其寫回到 Kubernetes。第二種類型也從 Kubernetes 讀取數(shù)據(jù),但是與第一種類型不同,它改變了某些外部資源的狀態(tài)。
我們先看看用戶創(chuàng)建 Kubernetes Deployment 時會發(fā)生什么:
Deployment 控制器( kube-controller-manager的一部分)獲取對應(yīng)的資源信息并創(chuàng)建一個 ReplicaSet。然后,ReplicaSet 使用對應(yīng)的信息來創(chuàng)建兩個 Pod 副本,但是還沒有調(diào)度這些 Pod。 然后才是調(diào)度程序調(diào)度 Pod 并將調(diào)度結(jié)果的節(jié)點信息更新回YAML。 最后 Kubelets watch 到 Pod 數(shù)據(jù)后去啟動對應(yīng)的容器。
然后以相反的順序重復(fù)所有操作:kubelet 檢查容器,計算容器的狀態(tài),然后將其發(fā)送回去。ReplicaSet 控制器 接收它并更新副本集的狀態(tài)。Deployment 控制器也發(fā)生了同樣的事情,用戶最終獲得了當(dāng)前狀態(tài)。

Shell-operator
事實上 Kubernetes 完全就是各種控制器一起運行實現(xiàn)的(Operator 也是控制器)。為了能夠輕松創(chuàng)建一個控制器呢,我們引入了一個工具 shell-operator[2],它可以讓系統(tǒng)管理員使用他們習(xí)慣的方法來創(chuàng)建 Operator。

簡單的示例:復(fù)制 Secrets
讓我們看一個簡單的例子,假設(shè)我們有一個 Kubernetes 集群。其中有一個默認(rèn)的名稱空間,其中包含一些 Secret(mysecret)資源對象。此外,集群中還有其他名稱空間。這些名稱空間中有幾個具有額外的特定標(biāo)簽。我們的目標(biāo)是將 Secret 復(fù)制到帶有此標(biāo)簽的名稱空間中。
新的命名空間可以出現(xiàn)在集群中,并且其中一些可能帶有此標(biāo)簽,這一事實使任務(wù)變得復(fù)雜。另一方面,如果標(biāo)簽被刪除,則 Secret 也必須被刪除。Secret 本身也可以更改,在這種情況下,新的 Secret 必須傳播到所有帶標(biāo)簽的命名空間中去。如果 Secret 在某個命名空間中被意外刪除,則 Operator 必須立即將其還原。
現(xiàn)在我們已經(jīng)了解了需要實現(xiàn)的需求,接下來我們來使用 shell-oprerator 來真正實現(xiàn)它。
運行原理
與其他 Kubernetes 工作負(fù)載類似,shell-operator 部署在 Pod中。在 Pod 中有一個 /hooks 的一個子目錄,其中存儲了可執(zhí)行文件,它們可以用 Bash、Python、Ruby等編寫的,我們稱這些可執(zhí)行文件為hooks。

Shell-opeator 訂閱 Kubernetes?事件并執(zhí)行這些鉤子來響應(yīng)我們感興趣的事件。

但是,shell-operator 如何知道何時執(zhí)行鉤子呢?事實上每個鉤子都有兩個階段。在啟動過程中,shell-operator 使用-config參數(shù)運行每個鉤子。一旦配置階段結(jié)束,鉤子將以“正常”方式執(zhí)行:響應(yīng)附加給它們的事件。在這種情況下,鉤子會獲取綁定上下文。
使用 Bash 實現(xiàn)
現(xiàn)在,如果我們使用 Bash,我們需要實現(xiàn)兩個函數(shù)(強烈建議使用shell_lib[3] 庫,因為它大大簡化了 Bash 中鉤子的編寫):
第一個用于配置階段,并且應(yīng)該輸出綁定上下文; 第二個包含鉤子的核心邏輯。
#!/bin/bash
source?/shell_lib.sh
function?__config__()?{
??cat?<????configVersion:?v1
????#?BINDING?CONFIGURATION
EOF
}
function?__main__()?{
??#THE?LOGIC
}
hook::run?"$@"
下一步是確定我們感興趣的對象,在我們的示例中,我們需要跟蹤:
變更的 Secret 源對象; 集群中的所有命名空間,以查看帶有標(biāo)簽的命名空間; 目標(biāo) Secret,以驗證它們是否已和源 Secret 同步了。
訂閱源 Secret
綁定配置非常簡單,這里我們的mysecret對default 命名空間中的 Secrets 感興趣。

function?__config__()?{
??cat?<????configVersion:?v1
????kubernetes:
????-?name:?src_secret
??????apiVersion:?v1
??????kind:?Secret
??????nameSelector:
????????matchNames:
????????-?mysecret
??????namespace:
????????nameSelector:
??????????matchNames:?["default"]
??????group:?main
EOF
結(jié)果會根據(jù)源 Secret(src_secret)中的更新執(zhí)行該鉤子,它將獲得以下綁定上下文:

可以看到該綁定上下文具有其名稱和完整的對象信息。
處理命名空間
接下來我們需要訂閱命名空間,這是所需的綁定配置:
-?name:?namespaces
??group:?main
??apiVersion:?v1
??kind:?Namespace
??jqFilter:?|
????{
??????namespace:?.metadata.name,
??????hasLabel:?(
???????.metadata.labels?//?{}?|???
?????????contains({"secret":?"yes"})
??????)
????}
??group:?main
??keepFullObjectsInMemory:?false
可以看到的在配置中有一個新的字段,叫做 jqFilter。顧名思義,jqFilter 就是過濾掉所有不必要的信息,并提供一個新的 JSON 對象,其中包含我們感興趣的字段。以這種方式配置的鉤子會收到以下綁定上下文:

它由集群中每個命名空間的 filterResults 數(shù)組組成,布爾變量hasLabel顯示相關(guān)的命名空間是否具有mysecret標(biāo)簽,keepFullObjectsInMemory: false選擇器表示將刪除內(nèi)存中的完整對象。
追蹤目的 Secret
我們訂閱所有具有 managed-secret: "yes"注釋的 Secrets?(這些就是是我們的dst_secrets):
-?name:?dst_secrets
??apiVersion:?v1
??kind:?Secret
??labelSelector:
????matchLabels:
??????managed-secret:?"yes"
??jqFilter:?|
????{
??????"namespace":
????????.metadata.namespace,
??????"resourceVersion":
????????.metadata.annotations.resourceVersion
????}
??group:?main
??keepFullObjectsInMemory:?false
在這種情況下,jqFilter過濾掉除命名空間名稱和resourceVersion參數(shù)之外的所有信息。創(chuàng)建此目標(biāo) Secret 時,我們將該參數(shù)傳遞給注釋。
以這種方式配置的鉤子在執(zhí)行時將獲得上述三個綁定上下文,你可以將它們視為集群的某種快照。

我們可以使用所有這些信息來設(shè)計一種最基本的算法,它遍歷所有命名空間,如果當(dāng)前命名空間 hasLabel是true,則進行迭代:
比較源和目標(biāo) Secret 如果它們相同,則什么都不做 如果它們不同 - 執(zhí)行 kubectl replace或者create操作。
如果當(dāng)前命名空間 hasLabel是false,則:
確保命名空間中沒有 Secret 如果目標(biāo) Secret 存在 - 執(zhí)行 kubectl delete如果目標(biāo) Secret 不存在,則不執(zhí)行任何操作。

在我們的示例倉儲庫中[4],可以找到上述算法的完整 Bash 實現(xiàn)。
35 行 YAML 和相同數(shù)量的 Bash 組成了一個簡單的 Kubernetes 控制器!Shell-operator 的工作是將它們?nèi)拷壎ㄔ谝黄稹?/p>
顯然,使用 Shell-operator 并不是只能復(fù)制 Secrets,我們還會用更多示例來了解它的用法。
示例1:更新 ConfigMap
比如現(xiàn)在我們有一個具有三個 Pod 的 Deployment,這些 Pods 使用ConfigMap 來存儲一些配置,當(dāng)這些 Pod 啟動時,ConfigMap 處于某種狀態(tài)(我們將其稱為版本1:v.1),我們所有的 Pod 都具有相同的 v.1 版本的 ConfigMap。
現(xiàn)在,假設(shè) ConfigMap 更改為另一個版本 v.2,在這種情況下,我們的Pod 仍將使用 ConfigMap 的早期版本 v.1。

在這種情況下我們通常怎么做呢?是的,我們可以在 Pod 的模板中添加一些內(nèi)容。因此,讓我們將 checksum 注解添加到 Deployment 定義的模板部分:

現(xiàn)在,我們所有的 Pod 都有 checksum,并且與 Deployment 的 checksum 相同。接下來,我們應(yīng)該更新注釋來響應(yīng) ConfigMap 的更改。這就是 shell-operator 可能派上用場的時候,我們只需要編寫一個鉤子即可訂閱 ConfigMap 并更新 checksum。
當(dāng)用戶修改 ConfigMap 時,shell-operator 會 watch 到變更并更新 checksum。然后,Kubernetes 會殺死 Pod,創(chuàng)建一個新 Pod,等到準(zhǔn)備就緒后再進行下一個 Pod。因此,我們的 Deployment 可以完美同步并與更新的 ConfigMap 一起運行。

示例2:使用 CRD
我們知道 Kubernetes 允許我們創(chuàng)建自定義類型的對象。例如,我們可以創(chuàng)建一個名為 MysqlDatabase 的資源,假設(shè)這種類型只有兩個元數(shù)據(jù)參數(shù):name和namespace。
apiVersion:?example.com/v1alpha1
kind:?MysqlDatabase
metadata:
??name:?foo
??namespace:?bar
因此,我們可以在 Kubernetes 集群中創(chuàng)建 MySQL 數(shù)據(jù)庫,在這種情況下,可以使用 shell-operator 來 watch MysqlDatabase這類資源,將它們連接到 MySQL 數(shù)據(jù)庫服務(wù)器,并同步所需狀態(tài)和 watch 到的狀態(tài)。
示例3:監(jiān)控集群網(wǎng)絡(luò)
如您所知,ping 是監(jiān)視網(wǎng)絡(luò)的最簡單方法,當(dāng)然我們也可以使用 shell-operator 來實現(xiàn)。
首先,我們需要訂閱節(jié)點,shell-operator 需要每個節(jié)點的名稱和 IP 地址,以循環(huán)瀏覽節(jié)點列表并 ping 它們中的每一個。
configVersion:?v1
kubernetes:
-?name:?nodes
??apiVersion:?v1
??kind:?Node
??jqFilter:?|
????{
??????name:?.metadata.name,
??????ip:?(
???????.status.addresses[]?|???
????????select(.type?==?"InternalIP")?|
????????.address
??????)
????}
??group:?main
??keepFullObjectsInMemory:?false
??executeHookOnEvent:?[]
schedule:
-?name:?every_minute
??group:?main
??crontab:?"*?*?*?*?*"
該executeHookOnEvent: []參數(shù)可防止響應(yīng)任何事件而調(diào)用該鉤子(更新、添加或刪除節(jié)點時將不執(zhí)行掛鉤)。但是,它將根據(jù) schedule 字段每分鐘運行一次(并更新節(jié)點列表)。
我們?nèi)绾未_定丟包之類的問題?讓我們看一下如下所示代碼:
function?__main__()?{
??for?i?in?$(seq?0?"$(context::jq?-r?'(.snapshots.nodes?|?length)?-?1')");?do
????node_name="$(context::jq?-r?'.snapshots.nodes['"$i"'].filterResult.name')"
????node_ip="$(context::jq?-r?'.snapshots.nodes['"$i"'].filterResult.ip')"
????packets_lost=0
????if?!?ping?-c?1?"$node_ip"?-t?1?;?then
??????packets_lost=1
????fi
????cat?>>?"$METRICS_PATH"?<??????{
????????"name":?"node_packets_lost",
????????"add":?$packets_lost,
????????"labels":?{
??????????"node":?"$node_name"
????????}
??????}
END
??done
}
我們遍歷節(jié)點列表,獲取節(jié)點名稱和 IP 地址,對節(jié)點執(zhí)行 ping 操作,然后將結(jié)果寫入 Prometheus 指標(biāo)端點。Shell-operator 可以通過將指標(biāo)寫入存儲在 $METRICS_PATH 環(huán)境變量中指定路徑下的文件中來將指標(biāo)暴露到 Prometheus。
這樣我們就使用最少的代碼[5]在群集中實現(xiàn)了基本網(wǎng)絡(luò)監(jiān)視的方式。
排隊機制
如果不討論 shell-operator 必不可少的排隊機制,那么將是不完整的。想象一下,shell-operator 響應(yīng)集群中的某些事件而執(zhí)行了一個鉤子。
如果集群中發(fā)生了另一個事件,將會怎樣? shell-operator 會運行該鉤子的另一個實例嗎? 例如,如果集群中同時發(fā)生五個事件,該怎么辦? shell-operator 會并行運行它們嗎? 消耗的資源(如內(nèi)存和CPU)又如何呢?
幸運的是,shell-operator 具有內(nèi)置的排隊機制,所有事件都放入隊列并順序處理。
假設(shè)我們有兩個鉤子,第一個事件轉(zhuǎn)到第一個鉤子,處理完成后,隊列前進。接下來的三個事件是另一個鉤子,它們從隊列中彈出并作為批處理傳遞給鉤子。因此,該鉤子接收事件數(shù)組?-更準(zhǔn)確地說是綁定上下文數(shù)組。
另一種選擇是將這些事件合并為一個較大的事件,綁定配置的group參數(shù)對此負(fù)責(zé)。


您要做的就是將queue字段插入綁定配置中,如果queue省略該名稱,則鉤子在default隊列中運行,這種排隊機制可以整體解決所有資源管理問題。
總結(jié)
在本文中,我們解釋了什么是 shell-operator,展示了如何快速簡單地創(chuàng)建它的 Kubernetes Operator,并提供了使用它的一些示例。
有關(guān)我們工具的詳細信息以及快速入門指南,請參考其 GitHub 存儲庫。另外也可以看看我們的其他項目,例如,addon-operator[6] ,它可以綁定 Helm Charts,對其進行升級,監(jiān)視各種 Chart 參數(shù)/值(以及控制 Helm Chart 的安裝)并根據(jù)集群事件進行更新。

原文鏈接:https://medium.com/flant-com/meet-the-shell-operator-kubecon-36c14ba2f8fe
此外后臺回復(fù) shell 可以獲取 shell-operator 完整的 PDF 文檔。
參考資料
shell-operator 演講視頻: https://www.youtube.com/watch?v=we0s4ETUBLc
[2]shell-operator: https://github.com/flant/shell-operator
[3]shell_lib: https://github.com/flant/shell-operator/blob/master/shell_lib.sh
[4]示例倉儲庫: https://github.com/flant/examples/tree/master/2020/08-kubecon
[5]集群網(wǎng)絡(luò)監(jiān)控代碼: https://github.com/flant/examples/blob/master/2020/08-kubecon/container/ping_exporter.sh
[6]addon-operator: https://github.com/flant/addon-operator
K8S進階訓(xùn)練營,點擊下方圖片了解詳情

