AWS 上的雲原生 Jenkins

本文首發於:Jenkins 中文社區html

原文連接    做者:Alberto Alvarezjava

咱們如何運用 Terraform、Packer、Docker、Vault、ELB、ASG、ALB 或 EFS 等 AWS 服務實現 Jenkins Cloud-native,以及咱們一路走來的收穫node

「Butler in action」 by Bernyce Hollingworth

咱們使用 Jenkins 搭建持續交付流水線,和其餘不少團隊同樣,這些年咱們圍繞 Jenkins 建立了不少工做流程和自動化。Jenkins 是咱們團隊取得成功的關鍵,讓咱們可以在上一季度順利進入生產677次,搭建及部署時長平均爲12分鐘。git

咱們的大部分應用和基礎設施能夠看做雲原生,但當時 Jenkins 服務並不徹底適合這個分類:服務在單個服務器上運行,同時不少任務直接在 master 上運行,其部分手動配置包括 secret、插件、定時任務和 startup hacking 的普通膨脹,該膨脹是自2014年首次搭建起不斷累積而成。github

Jenkins 不只變成了單體服務和單點故障,並且拆除及重建 Jenkins 對企業也是很大的風險。docker

咱們決定必須作出改變。這篇博客說明了咱們如何運用 Terraform、Packer、Docker、Vault、和 ELB、ASG、ALB 或 EFS 等 AWS 服務實現 Jenkins Cloud-native,以及咱們一路走來的收穫。npm

Jenkins 狀態

當時不得不面對的關鍵問題是:若是咱們將 Jenkins 服務置於一個容器/自動縮放實例中,咱們須要恢復何種狀態?編程

問題的答案並不簡單,值得一提的是,有個 Jenkins 特別興趣小組(SIG)已經識別出全部致使這一 Jenkins 狀態的存儲組件。這是一個很棒的起點,由於咱們至少得確保那篇文章列出的全部存儲類型都考慮在內。vim

捷徑

這不是新問題。不少團隊使用 Docker 容器運行 Jenkins,官方 Jenkins Docker 鏡像也獲得良好維護。如《Jenkins Dokcer 鏡像》文檔中解釋的:緩存

docker run -p 8080:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts

複製代碼

這會把 workspace 存在 /var/jenkins_home。全部的 Jenkins 數據(包括插件和配置)都存在上述目錄裏。建立一個明確的 volume 能夠方便管理和附加到另外一個容器進行升級。

上述示例裝載主機上的 jenkins_home,其中包括全部 Jenkins 狀態。而後該目錄能夠存在一個外部磁盤上,好比 Kubernetes 持久化存儲卷。或者,若是 Jenkins 在 EC2 上運行,該目錄可存在一個外部 EBS 或 EFS 捲上。

這是一種有效的方法,但咱們認爲這個方法不能達到咱們的標準,由於 jenkins_home 不只包括狀態,還包括配置。Block storage 擁有大量用戶案例,但一個小小的配置修改就必須進行 snapshot 恢復操做,這彷佛並不算是好的解決方案。此外,咱們並非想轉移問題:外部存儲沒法免去手動配置、憑據儲存在文件系統等問題。

SCM 救援

過去,咱們用了 Jenkins 備份插件,該插件基本上把配置修改備份在源碼控制裏,容許配置恢復。這個插件的設計想法很棒,但咱們決定不使用它,由於咱們沒法輕鬆控制哪些數據實現備份,並且該插件自2011年就沒有任何更新了。

這樣的話,若是咱們把 jenkins_home 建立成我的 Git repo,並自動提交對 Jenkins 所作的修改呢?此處的關鍵是排除單獨儲存的任何二進制文件、secrets 或大型文件(稍後詳細介紹)。咱們的 .gitignore 文件以下所示:

/.bash_history
/.java/
/.kube/
/.ssh/
/.viminfo
/identity.key.enc
/jobs/
/logs/
/caches/
# Track static worker and exclude ephemeral ones
/nodes/**
!nodes/static-node/config.xml
/org.jenkinsci.plugins.github_branch_source.GitHubSCMProbe.cache/
/plugins/
/saml-idp-metadata.xml
/saml-jenkins-keystore.jks
/saml-jenkins-keystore.xml
/saml-sp-metadata.xml
/scm-sync-configuration/
/scm-sync-configuration.success.log
/secret.key
/secret.key.not-so-secret
/secrets/
/updates/
/workspaces/

複製代碼

幾乎全部的純文本配置都正在 Git 實現持久化。爲了給 Jenkins 提供這一配置,咱們要作的就是檢查 startup 上的 repo;事情漸漸成形。

Secrets

Jenkins 要訪問不少地方,也就是說咱們須要一個安全的 secret 存儲空間。由於咱們是 HashiCorpVault 的重度用戶,因此天然而然就選了這個工具,不過遺憾的是,Vault 沒法涵蓋全部場景。好比,scm-branch-source 流水線插件須要 SCM 的認證憑據,並默認爲 Jenkins 憑據插件。每次從 Vault 動態檢索這些,咱們都須要同步一個倉庫,這可能致使錯誤,也會須要額外的精力去維護。

這就是爲何咱們採用 Vault 與 Jenkins 憑據混合的方法: 1. 在 startup 實例中,Jenkins 進行認證,VAult採用 IAM 認證方法。 2. 一個引導腳本檢索 Jenkins master.key 和憑據插件所用的其餘加密密鑰。更多詳情請參閱這篇文章。 3. 儲存在 jenkins_home/credentials.xml 上的憑據如今可由 Jenkins 解密和訪問。

用 Vault 徹底取代憑據插件是咱們將來可能探索的問題,不過咱們很開心這個方法知足了安全性要求, 同時能輕鬆與 Jenkins 的其他功能實現集成。

任務和 workspace 數據

問題從這一步開始變得棘手:jenkins_home/jobs and jenkins_home/workspaces 都含有介於非結構化數據、建立製品和純文本之間的混合體。這個信息頗有價值,能夠幫助咱們審計、理解以前的流水線 build。這些 build 尺寸很大,並且不太適合 SCM 同步,所以這兩個目錄都排除在 .gitignore 以外了。

那咱們把這些儲存在哪兒呢?咱們認爲 block storage 最適合存儲這種數據。做爲 AWS 的重度用戶,使用 EFS 徹底說得通,由於 EFS 的文件存儲可擴展、可用性高並能夠經過網絡訪問,很是易於使用。咱們使用 Terraform 整合了 AWS EFS資源,並用 AWS 備份服務制定了一份按期備份計劃。

在 startup,咱們將 EFS 卷 、符號連接 jenkins_home/jobs 和 jenkins_home/workspaces 裝載到 EFS 目錄上,而後啓動 Jenkins 服務。

接下來,Jenkins 服務是惟一能夠讀寫任務 /workspace 數據的界面。值得一提的是,咱們有一個 Jenkins 任務按期刪除幾周前的任務和 workspace 數據,這樣數據不會一直增長。

Packer 和 Terraform 實現編碼化 Jenkins

你可能想知道這些是如何湊在一塊兒的?我甚至沒說過在哪裏運行 Jenkins!咱們普遍使用 Kubernetes,花了一些時間思考將 Jenkins 做爲容器來運行,可咱們決定使用 Packer 和 EC2 來運行 Jenkins master,用短暫 EC2 實例運行這些任務。

儘管將 master 和 worker 雙雙做爲容器運行的想法頗有用,但咱們在當前 Kubernetes 集羣裏沒有找到存儲 Jenkins 的地方。並且只是爲了 Jenkins 就新建一個集羣彷佛有點兒「殺雞用牛刀」。此外,咱們想保留從其他服務中解耦的基礎設施的關鍵部分。這樣的話,若是 Kubernetes 升級對咱們的 app 有影響,咱們但願至少能夠運用 Jenkins 進行回滾。 運行「Docker in Docker」還有另外一個問題,這個問題有解,不過仍是須要說明一下,由於咱們的 build 常常用到 Docker 命令。

其體系架構以下:

architecture

能使用 EC2 實例讓過渡更順暢:咱們當時經過 Jenkins EC2 插件用臨時 worker node 運行流水線工做,並在聲明式流水線代碼上調用了這一邏輯,因此沒必要重構就能用 Dokcer 代理節點是一個加分項。其他工做就是 Packer 和 Terraform 代碼,這是咱們已經很熟悉的部分了。

插件

由於插件也是狀態!咱們在這個項目裏想要解決的問題之一就是更好地審計、管理插件。在手動場景中,插件管理可能不受控制,很難了解安裝插件的時間和緣由。

大多數 Jenkins 級別的插件配置能夠在常規 Jenkins 配置 xml 文檔中找到,但安裝插件也致使 jar 製品、元數據、圖片和其餘文件存在 jenkins_home/plugin 目錄。

一種方法是在 EFS 中存儲插件,不過咱們想將 EFS 使用率保持在最低水平,這沒法解決問題,只是轉移問題。這就是爲何咱們選擇對插件安裝進行「Packer 化」。

基本上,在咱們的 AMI 定義中,有一個插件文件羅列了插件和版本,大體以下:

# Datadog Plugin required to send build metrics to Datadog
datadog:0.7.1# Slack Plugin required to send build notifications to Slack
slack:2.27

複製代碼

而後,咱們的 AMI provision 腳本解析該文件,用 Jenkins CLI 安裝插件和所選版本:

# Wrapper function for jenkins_cli
jenkins_cli() {
  java -jar "$JENKINS_CLI_JAR" -http -auth "${user}:${pw}" "$@"
}for plugin in "${plugins[@]}"; do
  echo "Installing $plugin"
  jenkins_cli install-plugin "$plugin" -deploy
done

複製代碼

而後,任何須要安裝的新插件或升級到當前安裝版本的版本升級都須要 GitHub Pull Request,這會觸發搭建新 AMI。完美!

安裝其餘軟件

根據定義,Jenkins 要安裝不少軟件才能建立、測試和部署。首先,咱們不想讓 master node 運行任何任務,因此咱們避免安裝任何與任務相關的軟件。Master 的主要任務是在其餘短暫 worker node 上提供界面、編排 builds。

這意味着咱們能夠在 worker node 上安裝所需工具,但咱們決定儘量多地使用 docker run。這是由於咱們是使用 Scala、Java、Node、Golang、Python等其餘編程語言的多語言組織。爲全部這些軟件棧維護不一樣 build 工具可能讓 worker node 設置變得有點兒複雜。

以 JavaScript 爲例,咱們想讓 Jenkins 針對 install 和 test 等 app 運行 yarn 命令。簡單將加載檢查過的 repo 目錄做爲一個 volume 安裝到 Docker 容器裏,從該容器中運行任何命令。如下爲運用 Groovy 工做流代碼的例子:

def node(command, image) {
  def nodeCmd = [
    'docker run -i --rm',
    '-u 1000', // Run as non-root user
    '-v ~/.npmrc:/home/node/.npmrc:ro',
    '-v ~/.yarn:/home/node/.yarn',
    '-e YARN_CACHE_FOLDER=/home/node/.yarn/cache',
    "-v ${env.WORKSPACE}:/app",
    '--workdir /app',
    "${image}"
  ].join(' ')
  sh "${nodeCmd} ${command}"
}

複製代碼

而後,咱們檢查倉庫後能夠調用這個功能:

checkout scm
node('yarn install --frozen-lockfile', 'node:12.6.0-alpine')

複製代碼

漂亮收尾!由於除了 Docker 後臺程序或 kubectl,咱們沒必要在 worker machine 上安裝、維護所用工具的多個版本。咱們也相信 build 命令在本地和 CI 環境之間是一致的,由於用的是同一個 Docker 鏡像。

運用臨時 node 建立時要記得緩存依賴。好比,一個 worker node 重建後,咱們丟失了 sbt 緩存,因爲緩存必須重建,這致使建立時間變慢。若是外部依賴不可用,這甚至會致使失敗。咱們決定將相關依賴緩存在另外一個外部 EFS 上,以求得到更快、更可靠的 build。

結語

Jenkins 是一個很棒的工具,但在管理外部狀態上略有不足,所以以 cloud native 的方式建立 Jenkins 較有難度。咱們的方法並不完美,但咱們相信這個方法結合了二者的精華,並且確保安全性、操做簡單、有彈性。使人高興的是,咱們完成這個項目,並把全部的生產 build 遷移到新的 Jenkins 服務以後,能夠終止 master server,讓自動縮放在幾分鐘內完成重建,而不會影響之前儲存的狀態。

相關文章
相關標籤/搜索