1. 程式人生 > >基於 Docker、Kubernetes 實現高效可靠的規模化 CI/CD 流水線的搭建

基於 Docker、Kubernetes 實現高效可靠的規模化 CI/CD 流水線的搭建

640.gif?wxfrom=5&wx_lazy=1

本文來自作者 邸富傑 在 GitChat 上分享,閱讀原文」檢視交流實錄

文末高能

編輯 | 庫克

高效可靠的 CI/CD 流水線是一個IT組織實現軟體服務快速交付的基礎,現如今大量企業採用 jenkins 叢集來搭建其交付流水線。然而,如何管理大量 Jenkins Slave 的差異化?

如何簡單快速實現 Jenkins 能力的橫向擴充套件?如何實現流水線的高可用?如何有效利用閒置的 Jenkins Slave 資源?

上述這些問題一直困攏著叢集管理員,近兩年隨著虛擬化技術突飛猛進的發展,Docker, Kubernetes 等現代化工具徹底顛覆了交付團隊的交付流程,同時也為 CI/CD 流程水線的搭建與管理提供了全新思路。

Setup

目前流行的CI工具很多,鑑於本文討論CI/CD流水線在企業中的應用,考慮到企業不會有意願將原始碼的訪問權開放給第三方,像 Travis CI 等這些基於 SaaS 的 CI 工具自然被 pass,由於 Jenkins 在CI 領域的主導性地位,所以本文的CI工具只涉及 Jenkins。

另外,本文預設您對Jenkins, Docker, Kubernetes 等工具有一些基礎的瞭解。

持續交付流水線簡介

簡單介紹一下持續交付流水線,如下圖所示,簡單來說流水線工作流程是這樣的,當代碼庫有程式碼變更的時候持續整合伺服器(Jenkins)會監聽到程式碼變更並自動觸發第一個階段 — 持續整合階段,這個階段做的事情是自動構建,單元測試,靜態程式碼分析以及生成報告。

如果所有步驟都如預期成功通過的話會自動進入下一個階段 — 自動化測試階段,在跑自動化測試套件之前,首先要把成功通過第一階段的產出物(artifact)。

如果選擇 java 做為開發語言的話就是生成的 war 包,ruby 的話就是 gem 檔案,部署到測試伺服器上,部署完成之後觸發自動化測試套件,包括驗收測試,容量測試,效能測試等會自動執行。

如果所有測試用例都順利通過(有些公司還需要做一些手工測試如探索性測試等)的話,那麼這個版本就會被標記成一個可釋出的侯選版本。一旦業務需要,就會通過一鍵部署的方式將相應的侯選版本釋出到生產環境上去。

如果你想更多的瞭解持續交付相關的知識請閱讀 David Farley and Jez Humble 的名著《Continues Delivery》。

0?wx_fmt=png

實踐與痛點

上述這個過程實現起來需要這樣做,首先需要把上圖中各個階段的工作指令碼化,說具體一點就是需要寫一個構建指令碼來完成編譯,單元測試,靜態程式碼分析,生成報告等步驟,從而完成持續整合階段的工作。

然後是自動化測試指令碼,這個指令碼可以觸發自動化測試套件並生成相關報告,最後需要寫一個部署指令碼,用於將持續整合階段的產出物部署到測試環境上(當然最後的釋出階段也會重用這個指令碼)。

這裡需要注意的是要確保部署都是可重複的,重複部署同一個產出物N次的效果與只部署一次的效果相同,也就是大家常說的冪等。

接下來就輪到 Jenkins 出場了,首先我們需要配置一下 Jenkins 來監聽程式碼庫的變更,這就意味著只要有程式碼遷入就會觸發相對應的流水線。

然後我們用 Jenkins job 或 pipeline 將這幾個階段的指令碼串聯起來(下圖是以job為例),這樣一個簡單的 CI/CD 流水線就搭建完成了。

0?wx_fmt=png

如上圖所示,每一個 Job 負責執行某一階段的指令碼,可以簡單類比為 Job1 執行持續整合階段的指令碼將原始碼從程式碼庫中遷出,編譯,單元測試,靜態程式碼分析以及生成報告。

Job2 首先將持續整合階段的產出物部署到測試環境並執行自動化測試套件,生成報告並標記產出物,Job3 用於按需釋出。

每個 Job 所執行的指令碼依賴的語言或執行環境會有所不同,可以通過label方式選擇到相應的Slave上執行。

但在企業級大規模的應用 CI/CD 流水線時,由於企業中多團隊多產品的存在,不同的團隊會根據產品自身的特點來選擇不同的技術實現方式,這就意味著產品實現語言會有很多種。

比如 java, C#, ruby, python, nodejs … 那麼相對應的 CI/CD 流水線就需要提供所有語言的編譯環境並安裝相關的依賴包,當然為了減少依賴,避免衝突以及更好的管理這些編譯環境聰明的管理員會選擇讓每一個slave只能執行特定程式語言的 Job, 如下圖所示:

0?wx_fmt=png

上圖這個 Jenkins 叢集 Maser 只用來排程和收集 log,所有的 Job 都會由 Master 根據不同的 label 導流到對應的 Slave 上執行。

這種叢集的實現方式是我們在VM時代不得已的選擇,雖然能解決大部分問題,但也帶來了很多困擾。

  • 單點依賴
    Jenkins Master 成為單點,一旦 Jenkins Master down 機,那將是災難性的,整個 CI/CD 流水線都將處於不可用的狀態。

  • 不易維護

    大量差異化的Jenkins Slave管理起來難度很大,由於差異化的存在,維護升級幾乎都需要手動完成,人力成本投入很高。

  • 舉個例子,我們發現流水線對 Java7 這個 Slave 發出的請求量比較大,經常出現排隊現像。為了緩解這種情況,管理員需要增加一個可以編譯Java7應用的Slave,那麼管理員需要怎麼做呢?

    首先需要準備一臺VM,然後安裝 java7 以及所有依賴的軟體包,最後配置 Slave 相關資訊label等並將新安裝好的 Slave 註冊到 Master,基本上都需要人為干預,擴充套件起來非常不方便。

  • 資源浪費

    每一臺 Jenkins Slave Server 都是一臺實實在在執行的 VM,當 Slave Sever 空閒時也不能將它所佔用的資源釋放,因為隨時可能需要這個 Slave完成相關的 Job。

解決痛點

下面我們來看一下虛擬化技術帶來了的福音,下圖是一個基於 Kubernetes, Docker 搭建起來的 Jenkins 叢集,為了避免混淆我略去了 Kubernetes 叢集中的 Master node。

我們看到 Jenkins Mater 以 Docker container 的形式執行在 Kubernetes 一個 Node 上並將所有 Jenkins 相關資料儲存到一個 volume 中,Jenkins Slave 也以 Docker container 的形式執行在各個Node中,之所以用虛線來表現 Slave 是因為 Slave 不是一直存在的,它會被動態的按需建立並自動刪除。

簡單介紹一下這種動態建立註冊 Slave 的方式,它的工作流程是,當 Jenkins Master 收到一個 build 的請求時,會用按照 label 的要求動態的建立一個執行在 Docker container 中的 Jenkins Slave 並註冊到 Master 上。

然後執行相應的 Job,當 Job 執行完成後這個 Slave 會被登出,所在的 Docker container 也會被自動刪除。

0?wx_fmt=png

這種基於 Docker,Kubernetes 搭建的 CI/CD 流水線給 Jenkins 叢集帶來了諸多益處:

  • 高可用

    Jenkins Master 被部署在 Kubernetes 叢集上,一旦 container 執行異常意外退出,那麼 Kubernetes 會自動用相同的 Docker image 幫我們從新起動一個新的 Jenkins。

    並將 volume attach 給新建立的 Docker container,從而保證不會丟失任何資料,實現了 Jenkins 叢集的高可用性。

  • 自動伸縮

    由於每一次執行 Job 時,Jenkins Master 都會動態建立一個 Jenkins slave,Job 完成之後 Slave 會被登出所在的 Docker container 也會被自動刪除,所佔用的資源就會被自動釋放。

    也就是說當同時請求的 Job 數量越多,生成的 Slave container 就會越多,佔用的資源也就越多,反之亦然,而且這種動態伸縮是完全不需要人為干預的。

  • 完全隔離

    由於每一次執行 Job 都是在一個全新的 Jenkins slave 中執行,避免了同時執行的 Job 與 Job 之間發生衝突的可能性。

  • 容易維護

    對比之前每一個 Jenkins Slave 是一臺固定 VM 的做法,以這種方式搭建的叢集維護的不再是固定的 VM 而是建立動態 Slave 所需要的 Docker image。

    我們可以很容易通過 Docker File 來 build 適用於我們自已的 Docker image 並將它們儲存在私有的 Docker registry 中,非常易於維護。

  • 容易擴充套件

    當我們發現 Jenkins 的 Queue 中存在大量等待執行的 Job 是因為 kubernetes 叢集的資源不足時,能夠很容易的初始化一個 kubernetes node 並將它新增到叢集中來,實現橫向擴充套件非常的方便。

簡單實現

下面我們來完成一個簡單的實現:

  1. 首先你需要安裝一個 Kubernetes cluster,請參考https://kubernetes.io/docs/setup/
    Kubernetes 安裝完成之後,首先需要用下面兩條命令和檔案部署一個 Jenkins 到 Kubernetes 叢集:

    0?wx_fmt=png

    tip: 標紅的部分很重要,開放 8080 埠是用來訪問 Jenkins web portal 用的;而動態建立的 Jenkins slave 會預設通過 50000(可修改)埠與 master 建立連線。

    檢查 jenkins 安裝執行情況:

    0?wx_fmt=png

  2. 檢視 Jenkins log,用管理員密碼登入 Jenkins 並安裝 Kubernetes plugin。

  3. 配置 Kubernetes cloud

    Manage Jenkins/Configure System/ Add a new cloud/ Kubernetes:

    0?wx_fmt=png

    Add pod template/Kubernetes Pod Template:

    0?wx_fmt=png

    點選 Save 之後大功告成。

牛刀小試

下面我們來測試一下這個動態註冊 Slave的Jenkins 叢集是否工作正常,首先登入 Jenkins 建立一個簡單的 free style job,指定這個Job只能在Label為“jnlp”的 agent 上執行。

點選 build now,你會發現奇蹟發生了,原來沒有註冊任何 Slave 的 Jenkins 動態的建立一個 Slave 並註冊到 Master 上,然後執行相應的 Job,當 Job 執行結束後這個 Slave 被自動清除了。

0?wx_fmt=png

PS:這只是一個簡單的實現,在企業的實踐中,我們需要不同的build環境,需要我們基於 jenkinsci/jnlp-slave 這個 Image 構建我們自己的 Jenkins slave Image 並儲存到私有的 Registry 中,相對應的 Kubernetes 需要從私有 Registry 拉取 Image。

參考文獻:

  • https://kubernetes.io/

  • https://www.docker.com/

  • https://github.com/jenkinsci/kubernetes-plugin

  • https://kumorilabs.com/blog/k8s-6-integrating-jenkins-kubernetes/

近期熱文

福利

0?wx_fmt=png

閱讀原文」瞭解更多知識