1. 程式人生 > >Bitnami:一個真實的Kubernetes案例

Bitnami:一個真實的Kubernetes案例

Kubernetes叢集已經配置好,“Hello world”程式已經能正常執行。那麼,接下來呢?

環顧目前網際網路上的內容,我們意識到有非常多很棒的Kubernetes案例釋出在文件或者部落格中,但這些內容被故意簡單化,目的是為了更好地闡述各自的觀點。不過,一旦你入了門,你會突然發現,幾乎沒有真實且可操作的案例或建議來學習。因此, 我們釋出了github.com/bitnami/kube-manifests,它包含了我們內部用來管理Kubernetes叢集的配置、工具、以及工作流程(workflow)。

我們的解決方案

Bitnami的全球分散式SRE團隊負責3個內部Kubernetes叢集,這3個叢集都執行在私有的AWS基礎設施上。

  • dev:開發者做產品試驗,或者執行alpha階段的軟體。
  • int:內部服務,比如bugtracker和CI/CD。
  • web:外部可訪問的服務,比如網站

這些Kubernetes叢集的建立和維護都使用的kops工具,並且我們會盡量同步更新Kubernetes的版本。這3個叢集的使用目的有些略微區別,這導致AWS防火牆和Kubernetes RBAC規則也會相應變化,不過除了這方面有區別,在其它方面,如叢集的基礎架構都是廣泛類似的。

管理這些服務的配置帶來了一些高層次的挑戰:

  • 如何協調不同人員、不同時區的改動?
  • 我們如何確保不同的服務能夠使用相同的部署模式/規則
  • 如何確保一個通用的服務能夠被一致地部署在多個叢集中

(服務和部署這兩個詞在Kubernetes中有特定的含義,因此我將使用“Service”(大寫)來表示Kubernetes的Service資源,而“service”(小寫)來表示常見的一般含義。)

關於叢集功能的簡單介紹

Git專案中包含的jsonnet配置是直接從我們當前的活躍程式碼倉庫中直接拿出來的,當然,我們會對它們進行一些小小的清理。我們刪除了RBAC規則,以及Bitnami產品軟體的配置。檢視檔案,你會發現關於遺留系統以及正在進行的實驗的註釋。

很多的自動化工作流都是基於執行在叢集上的Jenkins。

所有的容器日誌都通過一個經典的elasticsearch stack來收集。

我們的Ingress資源通過nginx-ingress和內部的AWS ELBs實現。我們使用letsencrypt自動化產生SSL證書,並且在kube-cert-manager中選擇了DNS challenges,這是因為我們的ELBs不能夠接收外部請求。我們還擁有一個DNS wildcard *.k.dev.bitnami.net 指向Ingress ELBs。這些配置使得我們的開發者可以只建立一個合適的Ingress規則,接下來系統會為他們自動生成一個域名、SSL證書、以及HTTP到HTTPS的重定向。

我們的容器和遺留的VM service使用prometheus來監控,並且能夠通過新增適當的註釋和標籤來獲得自配置的功能。重要的是,之前提到的jenkins、elasticsearch以及nginx都已經配置好,能夠輸出prometheus的度量,因此service能夠直接獲得HTTP級別的請求/狀態碼等統計資訊,而不需要做額外的工作。

巨集圖

根據以往的經驗和教訓,我們選擇了一種基礎架構即程式碼(Infrastructure as Code)的方式。這種方式會盡可能地通過版本控制系統(git)中的檔案來描述叢集的配置,然後使用我們熟悉的程式碼工作流,如評審和單元測試來管理我們的基礎架構。

架構

將我們理想的環境存編寫在git中是非常棒的,因為我們的團隊能夠討論基礎架構的版本控制,並且在開發、評審、測試、更新、回滾這些過程中使用統一的版本。所有的這些好處降低了團隊溝通的複雜度,從每個團隊員工之間的交流O(!n)降低到了團隊與中央倉庫的互動O(n)。

配置:模板無處不在!

Kubernets原生支援將所有資訊寫在Json(或者等同的YAML)中。

這裡有一個YAML的例子。(沒關係,我只是想表達一個高層次的觀點,因此可以隨意跳過這些細節):

apiVersion: v1

kind: Service

metadata:

labels:

name: proxy

name: proxy

namespace: webcache

spec:

ports:

– port: 80

targetPort: proxy

selector:

name: proxy

type: ClusterIP

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

labels:

name: proxy

name: proxy

namespace: webcache

spec:

minReadySeconds: 30

replicas: 1

revisionHistoryLimit: 10

strategy:

rollingUpdate:

maxSurge: 1

maxUnavailable: 0

type: RollingUpdate

template:

metadata:

labels:

name: proxy

spec:

containers:

– name: squid

# skipped, for clarity

Kubernets的資源定義擁有很多的重複性模板,一個相同的值重複出現在幾乎相關的資源中。我們想要一個工具,能夠表達出模式的繼承功能,不僅支援Kubernetes資源定義,並且在我們自己的基礎架構中的資源定義也能夠使用。在調研多個不同選擇後,我們選擇了jsonnet,原因如下:

  • 宣告式的,無副作用(side-effect-free)語言在複雜的情況系統中能夠減少意外的發生。
  • 能夠原生地生成JSON,而不僅僅是一個特定領域專用的語言,只能夠將幾個特殊的字元片段拼接起來。
  • 支援多個輸入檔案,並且擁有一個強大的合併功能,它使得我們能夠構建通用模板的庫。
  • 當我們在Google工作時,也遇到類似的問題,那時我們使用了一個類似的語言。
  • 缺點:小眾,並且沒有被預安裝在主流的linux發行版中。

為了演示效果,我們這裡使用了kube.libsonnet庫,之前提到的配置檔案重新用jsonnet語法編寫後,如下圖所示。值得注意的是,無聊繁瑣的工作已經被庫自動處理了,開發者只需關注高層次的內容,如Service和Deployment之間的關係,以及任何關於基礎模板的異常(比如要顯示的設定namespace)。

{

namespace: “webcache”,

squid_service: kube.Service(“proxy”) {

metadata+: { namespace: $.namespace },

target_pod: $.squid.spec.template,

port: 80,

},

squid: kube.Deployment(“proxy”) {

metadata+: { namespace: $.namespace },

spec+: {

template+: {

spec+: {

containers_+: {

squid: kube.Container(“squid”) {

// skipped, for clarity

},

},

},

},

},

},  // Kubernetes structures are deep :)

}

我不想在這裡重新介紹jsonnet的語法,你可以直接參考jsonnet的教程。唯一我想要強調的就是,jsonnet擁有一個強大的合併功能,這個功能除了發生在其他表示式被展開之前外,其他方面很像python的dict.update。這個合併操作超級有用,它使用“+”標識,你可以在上面的配置檔案中看到很多這個符號。

不過,例外情況總是存在的,每一個Kubernetes配置選項都對應了一個合法的用例場景。因此,我們選擇傳遞整個Kubernetes資源物件(添加了一些可選的幫助選項使得它更接近原生的jsonnet)而不是一些簡化過的中間結構。這意味著我們可以直接對接標準的Kubernetes文件,而不是自定義,並且它允許任何Kubernetes選項在模板棧(template stack)的任何一層被覆寫。

表達相似性

我們的相似性主要指的是:

  • 執行在多個叢集的通用模組
  • 橫跨多個模組的通用部署模式

部署模式

因此,大多數的模板有3層:基礎層->特殊軟體層->特殊部署層。我們的目錄結構也符合這個分層結構:

  • lib/ 基礎庫,主要負責通用部署模式
  • common/ 基於基礎庫,描述的是軟體棧中耦合更緊密的資源宣告
  • $cluster_name/ 每個名稱空間對應一個檔案,它們整合了common/中的棧,並且覆寫叢集特有的選項。

補充一點,不像其他簡單的工作流,我們使用了顯式的Kubernetes名稱空間宣告。這是因為我們的團隊管理了很多不同的軟體棧,因此我們不能依賴某些神奇的外部環境來保證名稱空間的正確性。

工具以及工作流

下面列舉的工具是經過深思熟慮後的選擇,這證明了當前Kubernetes架構的強大力量。當基礎設施出現了問題,能夠重新從第一步將它拼接回來是極好的。

  1. 開發者在他們最喜歡的編輯器中編輯jsonnet檔案。在實際場景中,為了測試,總是會有一個“編輯、執行jsonnnet、推送到dev叢集,檢視結果”的迴圈過程。Kubecfg.sh指令碼使得這個過程變得簡單。我們關於dev叢集的宗旨就是“善待開發者,任何改動都能夠被重新恢復到git中的任一個階段”
  2. Jsonnet檔案被擴充套件成正常的json檔案,這個過程使用了一個簡單的jsonnet-in-docker指令碼,它會被Makefile觸發。我們發現JSON檔案能夠讓作者和評審者清楚的知道改動會影響什麼內容,尤其是當你修改了基礎模板時。
  3. 不同的單元測試會對jsonnet和JSON檔案進行測試。我們的jsonnet程式碼包含了許多的assert語句,這些都會在JSON生成的過程中被驗證。我們還額外地檢查了jsonnet程式碼風格的一致性,因此生成的JSON檔案非常準確,符合Kubernetes的jsonschema,並且所有的資源都有一個顯式的名稱空間。重要的是,在任何時間這些檢查都是安全的,因此我們可以在github pull請求的時候執行這些檢查。
  4. 團隊成員能夠通過常規的github程式碼評審來檢視改動。他們知道多個自動化測試已經通過了,因此他們能夠專注於高層次的內容正確性而不是語法正確性。當覺得改動能夠接受時,只需要點選同意,就能夠完成合並了。
  5. 合併之後,jenkins自動對每個叢集執行deplo.sh指令碼來部署這些改動。已有的Deployment檢查和首航(rollout)策略都能防止災難性的改動被通過。此外,我們還有持續性的監控來報告任何錯誤的發生。重要的是,首航策略足夠緩慢,因此監控系統能夠給我們足夠的時間來應對以及凍結這次崩潰的首航,這些可以通過常規的kubectl rollout pause和undo命令來完成。我們擁有過去的所有歷史記錄,因此我們能夠通過回滾git中錯誤的改動來完成恢復。

教訓以及未來的工作

這套系統執行地非常好,不過,Jsonnet是一個實實在在的程式語言(儘管很小),在你草率的選擇它之前,我建議你通讀一遍jsonnet的教程。

我們的基礎模板目前直接表達出資源名字,這使得在同一個Kubernetes名稱空間中,同一個棧很難擁有一個以上的例項。我們可以很簡單地在jsonnet庫中解決這個問題,但我們還沒做這件事情。

總之,這是一個工具箱,一個流程,而不是一個打包好的產品。由此帶來的好處就是你能夠修改這些工具來適配你的真實場景;缺點就是你不能夠通過兩個簡單的命令來獲得這個功能。

好訊息是在Kubernets sigapps組中大家進行了活躍的討論,並且我們一直在積累經驗。我對未來的工具和配置的提升感到非常激動。

文章來自微信公眾號:Docker