背景
在 dailymotion,我們信奉 DevOps 最佳實踐,并且重度使用了 Kubernetes。我們的部分産品(并非全部)已經部署在 Kubernetes 上。在遷移我們的廣告技術平台時,為了趕時髦(作者你這麼直白的嗎?)我們希望完全采用“Kubernetes 方式”或雲原生!這意味着我們需要重新定義我們的整個 CI/CD 管道,并使用按需配置設定的動态環境來替代永久性的靜态環境。我們的目标是為我們的開發人員提供最好的支援、縮短産品上市時間并降低營運成本。
我們對新 CI/CD 平台的初始要求是:
如果可能的話,盡量避免從頭開始:我們的開發人員已經習慣使用 Jenkins 和聲明性管道,目前這些東西都還好。
采用公有雲基礎設施——谷歌雲平台和 Kubernetes 叢集。
與 gitops 相容——因為我們需要版本控制、評審和自動化。
CI/CD 生态系統中有不解決方案,但隻有一個符合我們的要求,也就是 Jenkins X,它基于 Jenkins 和 Kubernetes,原生支援預覽環境和 gitops。
Jenkins X: Kubernetes 上的 Jenkins
Jenkins X 是一個高度內建化的 CI/CD 平台,基于 Jenkins 和 Kubernetes 實作,旨在解決微服務體系架構下的雲原生應用的持續傳遞的問題,簡化整個雲原生應用的開發、運作和部署過程。
你猜的沒錯,Jenkins X 隻能在 Kubernetes 叢集上運作
Jenkins X 的搭建過程非常簡單,官方網站上已經提供了很好的文檔。由于我們已經在使用 Google Kubernetes Engine(GKE),是以 jx 指令行工具可以自行建立所有的内容,包括 Kubernetes 叢集。在幾分鐘内就可以獲得一個完整的可運作系統真的讓人印象深刻。
Jenkins X 提供了很多快速入門和模闆,不過我們想重用現有代碼庫中的 Jenkins 管道。是以,我們決定另辟蹊徑,并對我們的聲明性管道進行重構,讓它們與 Jenkins X 相容。
實際上,重構工作并不是隻針對 Jenkins X,而是為了能夠使用 Kubernetes 插件 在 Kubernetes 上運作 Jenkins。
如果你習慣使用“經典”的 Jenkins,并在裸機或虛拟機上運作靜态從節點,那麼這裡的主要變化是每個建構都将在自己的短存活期自定義 pod 上執行。你可以指定管道的每個步驟應該在哪個容器中執行。插件的源代碼中提供了一些 管道示例 。
我們面臨的挑戰是如何定義容器的粒度,以及它們應該包含哪些工具:擁有足夠多的容器讓我們可以在不同的管道之間重用它們的鏡像,但又不至于太多,這樣容易維護——我們可不想要花太多時間重建容器鏡像。
在之前,我們在 Docker 容器中運作大部分管道步驟,當我們需要自定義步驟時,就在管道中進行即時建構。
這種方式較慢,但更容易維護,因為所有内容都是在源代碼中定義的。例如,更新 Go 運作時可以在單個拉取請求中完成。是以,需要預先建構容器鏡像似乎是現有的設定中增加了更多的複雜性。它還具備一些優點:代碼庫之間的重複代碼更少、建構速度更快,并且沒有了因為第三方建構平台當機而造成的建構錯誤。
在 Kubernetes 上建構鏡像
在 Kubernetes 叢集中建構容器鏡像是一件很有趣的事情。
Jenkins X 提供了一組建構包,使用“Docker 中的 Docker”在容器内部建構鏡像。但随着新容器運作時的出現,以及 Kubernetes 推出了 Container Runtime Interface(CRI),我們想知道其他選擇是否可行。 Kaniko 是最成熟的解決方案,符合我們的需求。我們很激動,直到遇到以下 2 個問題。
第一個問題是阻塞性的:多階段建構不起作用。通過使用搜尋引擎,我們很快發現我們并不是唯一受到這個問題影響的人,而且當時還沒有修複或解決方法。不過,Kaniko 是用 Go 語言編寫的,而我們又是 Go 語言開發人員,是以為什麼不看一下 Kaniko 的源代碼呢?事實證明,一旦我們找到了問題的根本原因,修複工作就非常簡單。Kaniko 維護人員很快就合并了修複,一天後,修複的 Kaniko 鏡像就已經可用了。
第二個問題是我們無法使用相同的 Kaniko 容器建構兩個不同的鏡像。這是因為 Jenkins 并沒有正确地使用 Kaniko——因為我們需要先啟動容器,然後再進行建構。這一次,我們在谷歌上找到了一個解決方法:聲明足夠多的 Kaniko 容器來建構鏡像,但我們不喜歡這個方法。是以我們又回到了源代碼,在找到了根本原因後,修複就很容易了。
我們測試了一些方案,想自己為 CI 管道建構自定義的“工具”鏡像,最後,我們選擇使用單個代碼庫,每個分支使用一個鏡像,也即一個 Dockerfile。因為我們的代碼托管在 Github 上,并使用 Jenkins Github 插件來建構代碼庫,是以它可以建構所有的分支,并基于 webhook 觸發事件為新分支建立新的作業,是以管理起來十分容易。每個分支都有自己的 Jenkinsfile 聲明性管道檔案,使用 Kaniko 建構鏡像,并将建構好的鏡像推送到容器系統資料庫。Jenkins 幫我們做了很多事情,是以可以快速地添加新鏡像或編輯現有的鏡像。
聲明所請求的資源
我們之前的 Jenkins 平台存在的一個主要問題來自于靜态從屬節點或執行程式,以及有時候會在高峰時段出現的長建構隊列。Kubernetes 上的 Jenkins 可以輕松地解決這個問題,特别是運作在支援叢集自動縮放器的 Kubernetes 叢集上時。叢集将根據目前的負載添加或移除節點。不過這是基于所請求的資源,而不是基于所使用資源的情況。
這意味着我們需要在建構 pod 模闆中定義所請求的資源——比如 CPU 和記憶體。然後,Kubernetes 排程程式将使用這些資訊查找比對的節點來運作 pod——或者它可能決定建立一個新節點。這樣就不會出現長隊列了。
但是,我們需要謹慎定義所需資源的數量,并在更新管道時更新它們。因為資源是在容器級别而不是 pod 級别定義的,是以處理起來會更加複雜。 但我們不關心限制問題,我們隻關心請求 ,是以我們隻将對整個 pod 的資源請求配置設定給第一個容器(jnlp 那個)——也就是預設的那個。
以下是 Jenkinsfile 的一個示例,以及我們是如何聲明所請求的資源的。
pipeline {
agent {
kubernetes {
label'xxx-builder'
yaml"""
kind: Pod
metadata:
name: xxx-builder
spec:
containers:
name: jnlp
resources:
requests:
cpu:4
memory:1G
name:go
image: golang:1.11
imagePullPolicy: Always
command: [cat]
tty: true
name: kaniko
image: gcr.io/kaniko-project/executor:debug
"""
}
stages {
Jenkins X 的預覽環境
現在我們有了所有工具,可以為我們的應用程式建構鏡像,我們已準備好進行下一步:部署到“預覽環境”!
通過重用現有工具(主要是 Helm),Jenkins X 可以輕松部署預覽環境,隻要遵循一些約定,例如鏡像标簽的名稱。Helm 是 Kubernetes 應用程式的包管理器。每個應用程式都被打包為一個“chart”,然後可以使用 helm 指令行工具将其部署為“release”。
可以使用 jx 指令行工具部署預覽環境,這個工具負責部署 Helm 的 chart,并為 Github 的拉取請求提供注釋。在我們的第一個 POC 中,我們使用了普通的 HTTP,是以這種方式奏效了。但現在沒有人再用 HTTP 了,那我們使用加密的吧!
多虧了有 cert-manager ,在 Kubernetes 中建立攝入資源時可以自動擷取新域名的 SSL 證書。我們嘗試在設定中啟用 tls-acme 标志——使用 cert-manager 進行綁定——但它不起作用。
于是我們閱讀了 Jenkins X 的源代碼——它也是使用 Go 開發的。稍後修改一下就好了,我們現在可以使用安全的預覽環境,其中包含了 let’s encrypt 提供的自動證書。
預覽環境的另一個問題與環境的清理有關。我們為每個拉取請求建立了一個預覽環境,在合并或關閉拉取請求時需要删除相應的環境。這是由 Jenkins X 設定的 Kubernetes 作業負責處理的,它會删除預覽環境使用的命名空間。問題是這些作業并不會删除 Helm 的 release——是以,如果你運作 helm list,仍然會看到舊的預覽環境清單。
對于這個問題,我們決定改變使用 Helm 部署預覽環境的方式。我們決定使用 helmTemplate 功能标志,隻将 Helm 作為模闆渲染引擎,并使用 kubectl 來處理生成的資源。這樣,臨時的預覽環境就不會“污染”Helm release 清單。
将 gitops 應用于 Jenkins X
在初始 POC 的某個時候,我們對設定和管道非常滿意,并準備将 POC 平台轉變為可投入生産的平台。第一步是安裝 SAML 插件進行 Okta 內建——允許内部使用者登入。它運作得很好,但幾天後,我發現 Okta 內建已經不在了。我在忙其他的一些事情,是以隻是問了同僚一下他是否做了一些更改,然後繼續做其他事情。幾天後再次發生這種情況,我開始調查原因。我注意到 Jenkins pod 最近重新開機過。但我們有一個持久的存儲,而且作業也在,是以是時候仔細看看了!
事實證明,用于安裝 Jenkins 的 Helm chart 有一個啟動腳本通過 Kubernetes configmap 重置了 Jenkins 配置。當然,我們無法像管理在 VM 中運作的 Jenkins 那樣來管理在 Kubernetes 中運作的 Jenkins!
我們沒有手動編輯 configmap,而是退後一步從大局看待這個問題。configmap 是由 jenkins-x-platform 管理的,是以通過更新平台來重置我們的自定義更改。我們需要将“定制”内容儲存在一個安全的地方,并對變化進行跟蹤。
我們可以使用 Jenkins X 的方式,并使用一個 chart 來安裝和配置所有内容,但這種方法有一些缺點:它不支援“加密”——我們的 git 代碼庫中包含了一些敏感的資訊——并且它“隐藏”了所有子 chart。是以,如果我們列出所有已安裝的 Helm 版本,隻會看到其中一個。但是還有其他一些基于 Helm 的工具,它們更适合 gitops。 Helmfile 就是其中之一,它通過 helm-secrets 插件 和 sops 原生支援加密。
遷移
從 Jenkins 遷移到 Jenkins X 以及如何使用 2 個建構系統處理代碼庫也是我們整個旅程的一個很有趣的部分。
首先,我們搭建新 Jenkins 來建構“jenkinsx”分支,同時更新了舊 Jenkins 配置,用來建構除“jenkinsx”分支之外的所有内容。我們計劃在“jenkinsx”分支上建構新管道,并将其合并。
對于初始 POC,這樣做沒有問題,但當我們開始使用預覽環境時,不得不建立新的拉取請求,并且由于分支的限制,那些拉取請求不是基于新的 Jenkins 建構的。是以,我們選擇在兩個 Jenkins 執行個體上建構所有内容,隻是在 Jenkins 上使用 Jenkinsfile 檔案名和在新 Jenkins 上使用 Jenkinsxfile 檔案名。遷移之後,我們将會更新這個配置,并重命名檔案。這樣做是值得的,因為它讓我們能夠在兩個系統之間平穩過渡,并且每個項目都可以自行遷移,不會影響到其他項目。
我們的目的地
那麼, Jenkins X 是否适合所有人?老實說,我不這麼認為 。并非所有功能和支援的平台——git 托管平台或 Kubernetes 托管平台——都足夠穩定。但是,如果你有足夠的時間進行深挖,并選擇了适合自己用例的功能和平台,就可以改善你的管道。這将縮短釋出周期,降低成本,如果你對測試也非常認真,那麼對你的軟體品質也應當充滿信心。
我們的旅程還沒有結束,因為我們的目标仍在繼續:Jenkins X 仍然處于開發階段,而且它本身正在走向 Serverless,目前正在使用 Knative build。它的目标是雲原生 Jenkins。
我們的旅程也在繼續,因為我們不希望它就這樣結束。我們目前的完成的一些事情并不是我們的最終目的地,它隻是我們不斷演化的一個步驟。這就是我們喜歡 Jenkins X 的原因:與我們一樣,它遵循了相同的模式。你也可以開始你自己的旅程~