今天我們介紹的主題是 “企業級容器 CI/CD 的進階之路”,也就是我們 KubeSphere DevOps 的成長之路。
我這里介紹將分為以下幾部分,我們 KubeSphere DevOps 主要分為兩個重要功能,一是 DevOps 流水線,二是 S2I/B2I。
首先,我們介紹我們為什么使用 Jenkins 作為我們流水線的底層引擎,當我們選擇好 Jenkins 后,我們如何把它跟 KubeSphere 融合在一起,打造我們 KubeSphere CI/CD 流水線。同時,我們后面使用復雜的方案,可能有些用戶不太接受,我們提出更加簡單易用的 S2I/B2I。我們在上線后有很多用戶,這些用戶給我們做了很多反饋,我們踩了很多的坑。這里講講我們具體優化的點,以及我們未來的簡單規劃。
1、為什么我們選擇 Jenkins
首先,為什么我們選擇 Jenkins 作為我們 CI/CD 流水線?
先看看企業里的 CI/CD 流程。KubeSphere 最初的目標用戶是比較大型的用戶,像大型的銀行業、保險業,面對的用戶 DevOps 流程很復雜。我們可以看到在圖中,這里涉及很多人員、流程復雜、審批等一系列的需求。同時,在大型企業當中已經有一些 CI/CD 工具時,已經有 CI/CD 自己的流程,我們希望通過 KubeSphere 的流水線把傳統用上的工具融合進來,同時也能為它提供容器平臺之上,借助 KubeSphere 助力業務快速上線的能力。
在這些客戶當中,一些企業已經在使用一些 DevOps 工具做測試、進度管理等,但是他不知道如何在容器上做發布,我們希望能夠通過 KubeSphere 的 DevOps 功能對整個流程進行把控。
于是我們開始做技術選型。在做技術選型時,這是我從國外的網站 StackShare 拿到的一份數據,我選了一個我認為比較主流的 CI/CD 引擎。這里是市場份額的數據,可以明顯看到 Jenkins 的用戶、相關問題、投票都非常多。比如 Drone.io 和 GitLab CI 是幾百,到了 Jenkins 是幾萬。
那用戶為什么會選擇 Jenkins 呢?
這也是我們選擇 Jenkins 作為我們流水線引擎非常重要依據,這里可以看到 Drone.io 和 GitLab CI,用戶喜歡他們的點是安裝簡單、配置簡單,和 Docker 結合很簡單等一系列的東西。當我們看 Jenkins 時,首先是開源和可以私有化部署。
更重要的是使用 Jenkins 我們可以和許多 CI/CD 工具很容易結合在一起,完成打包、部署一系列的流程。同時 Jenkins 有很多插件、文檔,他們有成熟且活躍的社區,我們可以借助社區的力量,來完成我們的 DevOps 功能。
這是使用 Jenkins 的代表企業,可以從代表企業看出,我們不了解這些企業具體是做什么的,只有 Jenkins 的代表企業我看的比較眼熟,比如 Facebook、Netflix、ebay 等這種比較出名的國外大型企業,說明他們企業內部的復雜流水,使用 Jenkins 是可以滿足的。
于是我們選擇了 Jenkins 作為我們 CI/CD 底層流水線引擎。這個方案是用戶喜歡的方案,已經有很多用戶在使用。它有活躍、成熟的社區,我們可以很容易借助社區的力量完成我們的產品。同時很容易跟企業已有的 CI/CD 工具進行集成。它有非常可靠的插件體系,我們想完成 Jenkins 和我們 KubeSphere 進行融合,很有可能會使用插件擴展的功能,如果它沒有可靠的擴展開發的體系,可能我們集成起來會比較困難。
當我們選擇 Jenkins 作為我們 CI/CD 的引擎后,下一步是如何將 Jenkins 融入到我們 KubeSphere CI/CD 流水線里。
2、打造 KubeSphere CI/CD Pipeline
KubeSphere CI/CD 流水線早期時有一系列的需求,我簡單列出來。首先是多租戶隔離的需求,最初一些大企業用戶有很多部門、分公司,他們希望用我們 KubeSphere 作為容器平臺,最后是多租戶管理,方便他們進行管理。我們是一個容器平臺我選擇 K8s 作為我們底層容器的引擎。我們希望在 K8s 上,Jenkins 可以充分地釋放、利用 K8s 自己的能力,并且我們要把前面提到的企業中復雜的開發、測試、構建、部署一整套流程。同時我們要支持多種方式的流水線,我和代碼倉庫綁定還是不綁定,滿足企業的復雜需求。有一些用戶沒用過 Jenkins,他搭建流水線比較困難,我們希望通過圖形化編輯的手段簡化他們流水線搭建的過程,使用起來更加簡單。
首先,我介紹一下我們如何實現多租戶。在多租戶里主要分為兩部分,一是認證部分,二是鑒權部分。在認證部分比較簡單,我們 KubeSphere 本身內置 Ldap 進行用戶的存儲。Jenkins 本身有對接 Ldap 方案的,我們讓 Jenkins 直接對接 Ldap 里。這時候我們完成用戶的打通,這時候會有一個問題,用戶打通后,Requset 無法打通。KubeSphere 不知道 Ldap 里的用戶的密碼是什么,因此在 KubeSphere 想要以用戶身份對 Jenkins 做操作時也比較困難了。
如何完成認證?
在認證里,我當時做了一些調研,我發現 Jenkins 擴展果然很豐富,左邊是 Jenkins 一張官方的圖,介紹 Jenkins 鑒權過程是怎樣的。他分為 Web UI 和 Rest API 兩部分,在 Rest API 里面有一個叫 Basic Header 驗證器,我看到這個驗證器有真正的密碼驗證器、API Token 驗證器,我想這是不是可擴展的密碼,我研究了一下發現它真的可以擴展,我們自己寫了一個 KubeSphere Token 的驗證器,幫用戶使用 KubeSphere Token 請求 Rest API 時,這個請求最后會轉發到我們 KubeSphere IAM 請求驗證的過程。
這時候我們的認證完成了,我們的用戶是打通的,并且我們可以使用統一的 API Token 進行驗證,訪問 Jenkins 的 API。
接下來是另一部分鑒權。
這一部分里,我先介紹 Jenkins 的 API 和 K8s API 的區別。上面是 K8s API 獲取的信息,下面是 Jenkins 的 API。K8s 的 API 我們可以非常容易發現它非常結構化,它有自己的動作、API group,它具體請求的 Resource 是什么,它的名稱是什么。
我們再來看 Jenkins 的 API,Jenkins 的 API 看起來是一堆不知道是什么的東西,Jenkins 在設計之初不是基于 API 的服務,這導致它的一些差距。我們可以看 K8s 的界面方式,K8s 的方式是基于 Rest API 的路徑和動作進行鑒權的。但在 Jenkins 當中,從一些代碼里內嵌檢查語句,看這個人是不是有管理權限或者讀權限等。這說明我們很難借助 K8s 的能力把它鑒權。
前面提到 K8s 的鑒權有很多問題,我們后面進行改造。最初我們希望借助 K8s 的能力,我們在想 DevOps 能否借助 Jenkins 的能力先完成鑒權的方法。于是我們使用了另一個方案,這可以理解為是暫時的方案。最后我們會通過 KubeSphere 內置鑒權可擴展來完成這部分工作。
這時候用戶在請求 KubeSphere API 時分為兩部分,一部分是和權限操作相關的。比如我要為 DevOps 工程添加用戶,分類、角色等。另一部分是非權限相關的,當 K8s API 收到請求后,權限 API 轉發為 API Server,我們會在 API Server 里同步往 Jenkins 寫入一些權限規則,滿足權限的匹配。當你們訪問其他 API 時可以直接發到 Jenkins,借助原有 Jenkins 的鑒權能力和 API 的能力,使用這種方式有一個優點,用戶仍然可以使用 Jenkins 的頁面。我們的鑒權、認證是可以打通的,并且我們不需要對 Jenkins 的 API 進行大量的包裝,因為它不規則,我們包裝起來非常困難。我們在之后的版本中也對 API 進行了包裝。這時候我們完成了認證和鑒權的部分,可以滿足多租戶的需求。
關于如何充分釋放 K8s 能力,最初我們使用 Jenkins 設計中的 Kubernetes Plugin 的方案,我們做了配置上的調整,我們默認全部使用 Kubernetes 的 Agent,自動動態創建,這樣比較節省資源,也可以充分釋放 K8s 的能力。
這時候有一個問題,用戶寫 Jenkinsfile 需要 Agent 的定義。這對不熟悉 K8s 的人來說是非常困難的,他要先學會怎么做 Docker 鏡像,再學會怎么寫 K8s yaml 文件,再運行。于是我們內置了一些用戶可能比較常用的 Agent 類型,比如 Maven Agent、NodeJs Agent 同時支持用戶自己擴展 Agent 類型,滿足他自己獨特的需求。通過這種方式,我們充分釋放 K8s 的能力。
另一部分是 In SCM 流水線的實現方式,我們的流水線一般分為兩種,一種是通過代碼操作綁定的,另一種是通過代碼操作不綁定的。我調研了很多方案,很多廠商做的和代碼分支綁定,我個人認為是比較假的方案。我綁定了這個流水線到某一個代碼倉庫的某一個分支上,我不支持自己發現分支,發現它的 PR,自動進行 CI/CD。我實際上只能為一個分支執行流水線的過程。
這會有一些問題,我們在開發過程中很容易創建新的分支、新的 PR,這時候 DevOps 是沒有運行的。只有將提交代碼到 Master 分支或者其他的已經設置了流水線的分支上才會,不利于企業 DevOps 的發展。
于是我們當時調研想著借助 Jenkins 的生態來提供分支發現這樣的功能。Jenkins 自己有支持 Jenkinsfile 發現的功能,它可以支持多種廠商的 SCM,有 Git、GitHub、GitLab、Bitbucket 等,當我們選擇這種方案時,同時還擁有了另一個能力,可以兼容傳統用戶的 Jenkinsfile,我們不需要自己定義一套流水線的編寫規范。如果是傳統的 Jenkins 用戶,他們想遷移到 KubeSphere 的流水線上來也會變得更加簡單。
我們需要圖形化編輯流水線的能力,在了解這個功能時候,我們不得不提到 Jenkinsfile 具體內容。Jenkinsfile 有兩種類型:腳本類型的 Jenkinsfile 和聲明式定義的 Jenkinsfile。
這是我舉的兩個簡單的例子,想說明他倆有什么不同。左邊的 Jenkinsfile 里面有變量定義、有 for 循環,一看感覺我在寫代碼,不是在寫一個流水線的定義。這個方案有一個缺點,學習曲線非常陡峭。我要先學會 Groovy 語言,再學習 Jenkins 上的 Groovy 語言。顯然對于我們前端的同學來說是非常難以解析的,這不是結構化的定義,我們要做圖形化的編輯不能用這種方案。
另一種是聲明式流水線,我們可以來看看聲明式的流水線定義:它有一個流水線,在流水線有很多 Stage,Stage 下面可能有具體的 Step 執行一些命令。這種方案曲線相對平滑,他們的功能相對有限,但足以滿足企業里復雜的 CI/CD 的需求。這時候可以進行結構化的解析,我們后端提供結構化解析的 API,前端同學拿到結構化解析的數據,再進行流水線的展示。于是我們得到 KubeSphere 流水線的編輯頁面,在編輯頁面里我們可以很順利添加順序的 Stage 或者運行的 Stage,添加一些具體的 Step,比如我們要拉代碼、執行命令等。
經過這個階段,我們可以滿足企業用戶的復雜 DevOps 流程,讓他們在 K8s 之上進行 DevOps 的方案,讓企業擁有更快的上線時間、更高的部署頻率、更快的修復時間和更短的恢復時間。
當我們發布了復雜流水線的功能后,我們在社區和其他用戶收到很多反饋。使用 Jenkins 的方案,它的流程定義是相對復雜的,我要定義我的構建、測試、部署等一系列的階段。而有些企業就是想試一試自己能否快速將自己的應用部署到 K8s 之上,復雜的流水線對這種用戶來說是不適用的,于是我們提供了更加簡單易用的 S2I/B2I 功能。
3、更加簡單易用的 S2i
首先,S2I 是什么?S2I 不是我們創造的一個概念,它是開源社區的一個命令行工具。這個工具用來做什么?讓用戶在不需要了解 Docker 情況下可以構建自己的應用鏡像。那 S2I 和傳統的 Docker 構建有什么區別呢?在我們使用傳統 Docker 構建的時候是這樣一個過程,我們有很多應用,每個應用都可以有自己 Dockerfile,通過 Dockerfile 可以得到應用的鏡像。使用 S2I 后,我們的流程變成了什么?我們每個應用可以通過同一個 S2I 的 Builder Image 來構建自己的應用鏡像,Builder Image 也是 Docker 鏡像。
用戶只需要選擇 Image 就可以得到自己應用的鏡像,這時候用戶構建 Docker 的過程變得比較簡單,因為我們只需要使用 S2I Image 進行簡單的選擇,并且在我們企業用戶中,維護、管理起來更加方便。
比如我收到安全警報說某某版本有一個安全漏洞,我們要進行升級。企業當中有很多應用,但其實他們的 Dockerfile 都很類似,我要通知他們一個個進行升級。而當我們使用 S2I 時,我們只需要找專門的人升級 Image,告訴每個應用負責的人員觸發構建就升級完成了,可以一步上線。管理復雜度也減小了。
使用 S2I 后,可以簡化開發者的工作方式。業務開發人員只需要在自己的 IDE 里寫代碼,提交代碼到 Git 倉庫,觸發構建。通過構建器鏡像構建出他自己的應用鏡像,就可以把這個鏡像推送到 Docker 的鏡像倉庫,并且進行部署等一系列的過程。
前面介紹了使用 S2I 開發者的流程是怎樣的。S2I 很好,但它只是一個命令行工具,在企業中使用,大家不是特別喜歡這種方式。因為要學習它的命令,交互起來沒那么輕松。于是我們想著把 S2I 這個不錯的工具融入到我們 KubeSphere Console 或者 KubeSphere 里。讓它成為我們功能模塊的一部分。
這里不得不提到 CRD,CRD 是擴展 K8s 的方式,叫特殊資源定義。使用這個方式我們可以輕松得到和 K8s 一樣的聲明式 API,我們模塊之間是松耦合的。K8s 底層提供了 List-Watch 機制,使用這種方式我們可以快速響應用戶 S2I 的需求。
于是我們 KubeSphere 里,S2I 架構變成這樣。左邊是我們的用戶,右邊是我們的具體細節。用戶可以通過 Kubectl 或者其他工具,比如 KubeSphere Console,或者直接請求我們的 API。這個 API Server 是 K8s 的 API Server,K8s 的 API Server 在收到用戶請求時,會把數據存儲到 etcd 里,會通知 S2I Operator 完成一系列的任務操作。
在這里我們主要定義了三個 CRD,第一個是 S2I Template,有 Java、Golang 和 Binary。在填寫很多信息時不想填寫重復的信息,他想要選擇一個模板。比如我創建流水線時,我是 Java 語言的,S2I Template 就是這樣。S2i Builder 類似我們的流水線定義,當我們選擇好要使用語言,這時候只需要選擇代碼倉庫或者上傳制品,加上填寫我們要制作的鏡像或者推送的鏡像名稱,這時候我們定義好一個流水線。我們可以觸發,每次觸發都會生出一個 S2I Run,在 S2I Run 里我們具體執行 S2I 的過程,完成構建鏡像、推送鏡像,幫忙把這一系列的信息上報到 API Server 進行匯總展示。
于是我們簡單的 CI/CD 只需要填寫簡單的表單,我們選擇構建環境是什么,填寫鏡像、倉庫地址、名稱,就可以完成了。通過這種方式,在我們 KubeSphere Console 上,可以從源代碼一鍵直達服務,我們不是只有構建鏡像的過程,我還會自動為你創建 Deployment 以及 Service 等資源。用戶通過填寫一個表單可以一鍵啟動自己的服務。
當然,用戶可能有一些團隊協作需求。我們會在平臺展示構建數據、統計,方便團隊協作。比如我看到他今天下午構建了一次等。這是我們 KubeSphere S2I 2.1 版本后有一個單獨的頁面展示其狀態,用戶使用起來是非常簡單的。通過簡單的 S2I/B2I,我們為了讓用戶有更快的上線時間、更高的頻率、更快的修復時間和更短的恢復時間。
4、持續優化,讓 CI/CD 更加迅速
我們 DevOps 流水線和 S2I/B2I 可以滿足絕大多數用戶的需求,包括復雜的 CI/CD 場景,還有簡單的 CI/CD 場景。我們還會有一些優化點,這是我們 KubeSphere 中的頁面。
如果你是 KubeSphere 早期流水線的用戶,相信你一定見過這個頁面。這個地方是流水線在初始化,初始化時間很長,這困擾了我們團隊很久,也困擾我們的客戶、用戶很久。于是我們在 Jenkins 社區中進行了一些研究。
這里提到 Jenkins EMA 算法,這個算法你們不需要了解它是什么,我簡單介紹它的功能是什么。Jenkins 誕生的年代非常早,大概有十幾二十年的時間,這個 EMA 算法其實是 Jenkins 內部的 Agent 調度算法,在這個調度算法里當時的目標滿足一個目標,不要讓我們 Agent 發生頻繁的變化,比如你要擴容,我判斷是否有任務運行完了,如果有任務要運行完了,要等一等。因為做動態的擴容、縮容非常消耗資源,也很慢。
但 EMA 算法不適合這個時代,我們知道 K8s 的能力是瞬間啟動,啟動完后瞬間執行。于是我進行了一系列的調研,發現默認的調度算法是可以進行擴展的。我改變了 Kubernetes Agent 的調度算法,現在已經貢獻給社區。
最后變成右邊的樣子,策略非常簡單,沒有任何延遲的調度策略。如果有一個流水線來了,我們立馬對它啟動一個 Agent 執行流水線。我們進行測試會發現原來初始化時間等待有幾十秒、幾分鐘甚至幾十分鐘。通過優化可以讓調度時間變成毫秒級,也就是說我收到一個任務后,立馬可以啟動,執行我的任務。
在這張圖是 Jenkins 的監控圖,灰色線指的是等待構建任務的數量,紅色線指的是忙碌 Agent 數量。可以看到灰色線剛剛開始后,紅色的線會迅速升起來,完成 CI/CD 的任務。我們當時做了 Master 節點的監控,可以從這個時間看出來,我們當時測試了 100 個流水線,大概完成時間,同時完成需要 4 分鐘。我們這里看到 Jenkins Master CPU 用量當時打滿了。如果我們對 Master 節點進行擴容,或者對 K8s 環境進行擴容,我們可能可以跑得更快。
這一部分是我們 Jenkins Agent 的擴展。當 Agent 啟動后有一個問題,我們怎么讓流水線跑的更快?這時候提到用戶的場景,用戶很多是 Java 的開發者,他們會在企業的環境搭一個 Nexus 作為自己的制品管理和依賴緩存。
他們當時使用這種方案,在我們 Jenkinsfile 里配置使用 Nexus,看起來是沒問題的,實際上真的沒有問題嗎?這時不得不說到開發高峰期,當開發高峰期來的時候,一個下午開發人員最多提交代碼有幾十個或者上百人同時提交代碼,會啟動非常多的 Agent。因為 Agent 每次會銷毀,所以每次運行流水線的時候,Agent 都需要從 Nexus 重新拉緩存,這時候我們發現 Agent 宿主機的硬盤 I/O 跑滿了,并且 Nexus 出口帶寬也跑滿了。Nexus 還會出現部分的連接失敗,它的流水線雖然啟動很快,但運行不夠快。
于是我們在 2.1 版本中添加了另一個功能,基于硬盤的依賴緩存。之前我們使用 Nexus 緩存中心倉庫,在這個版本中我們從節點硬盤中加上緩存,當沒有依賴的時候,我會先檢查節點的上有沒有依賴,如果沒有,我會去 Nexus,Nexus 沒有再去中心倉庫。使用這種方式我們硬盤 I/O 和 Nexus 出口帶寬已經降下來,可以避免每次重新拉緩存,讓我們 CI/CD 跑得更迅速。
這是我們的構建,主要針對緩存的對比。可以看到這里有兩個測試用例,一個是 Java 的,另一個是 Node.js,二者也是開源的。通過用它滿足依賴,我們速度提升,Java 可以做到 9 倍,Node.js 差不多有 5 倍之多。當我們的運行速度、調度速度很快的時候,企業的發布速度也會變得很快。
當然,我介紹了 KubeSphere DevOps 優化的兩個點,我們優化了很多地方,包括前面提到的用戶優化、無感知緩存以及我們支持 S2i Runtime Image 等,還有 DevOps 動態 PVC,我們做的優化是想讓用戶使用得更方便,讓他擁有更快上線時間、更高的部署頻率、更快的修復時間和更短的恢復時間。
針對前兩部分,我的 CI/CD 可以跑得很快,并且滿足用戶的任何需求。其實我們還是有很多未來的規劃,在這里我只是展示了未來規劃的設計圖。
這是我們在 3.0 計劃做的目標和創建,我們 Jenkinsfile 定義起來相對比較復雜,我們希望用戶可以通過選擇語言,選擇自己做什么的方式可以完成自己復雜的流水線創建,簡化用戶的創建過程。我們也會支持用戶的郵件通知等。我這里沒有列出太多,我希望能夠從社區的用戶、社區貢獻者當中得到更多的反饋,包括你可以向我們反饋你想要什么樣的功能,或者你直接給我們提交代碼,讓我們的社區變得更好。(來源:青云QingCloud)
以上內容屬作者個人觀點,不代表雨果網立場!如有侵權,請聯系我們。