作者 | Balazs Szeti
譯者 | Andy Song (pinewall) ? ? ? 共計翻譯:21 篇 貢獻時間:79 天
容器應用程式平臺能夠動態地啟動具有資源限制的獨立容器,從而改變了執行 CI/CD 任務的方式。
現今,由於 Docker[1] 和 Kubernetes[2](K8S)提供了可擴充套件、可管理的應用平臺,將應用執行在容器中的實踐已經被企業廣泛接受。近些年勢頭很猛的微服務架構[3]也很適合用容器實現。
容器應用平臺可以動態啟動指定資源配額、互相隔離的容器,這是其最主要的優勢之一。讓我們看看這會對我們執行持續整合/持續部署(CI/CD)任務的方式產生怎樣的改變。
構建並打包應用需要一定的環境,要求能夠下載原始碼、使用相關依賴及已經安裝構建工具。作為構建的一部分,執行單元及元件測試可能會用到本地埠或需要執行第三方應用(如資料庫及訊息中介軟體等)。另外,我們一般定製化多臺構建伺服器,每臺執行一種指定型別的構建任務。為方便測試,我們維護一些實體專門用於執行第三方應用(或者試圖在構建伺服器上啟動這些第三方應用),避免並行執行構建任務導致結果互相干擾。為 CI/CD 環境定製化構建伺服器是一項繁瑣的工作,而且隨著開發團隊使用的開發平臺或其版本變更,會需要大量的構建伺服器用於不同的任務。
一旦我們有了容器管理平臺(自建或在雲端),將資源密集型的 CI/CD 任務在動態生成的容器中執行是比較合理的。在這種方案中,每個構建任務執行在獨立啟動並配置的構建環境中。構建過程中,構建任務的測試環節可以任意使用隔離環境中的可用資源;此外,我們也可以在輔助容器中啟動一個第三方應用,只在構建任務生命週期中為測試提供服務。
聽上去不錯,讓我們在現實環境中實踐一下。
註:本文基於現實中已有的解決方案,即一個在 Red Hat OpenShift[4] v3.7 叢集上執行的專案。OpenShift 是企業級的 Kubernetes 版本,故這些實踐也適用於 K8S 叢集。如果願意嘗試,可以下載 Red Hat CDK[5],執行 jenkins-ephemeral
或 jenkins-persistent
模板[6]在 OpenShift 上建立定製化好的 Jenkins 管理節點。
解決方案概述
在 OpenShift 容器中執行 CI/CD 任務(構建和測試等) 的方案基於分散式 Jenkins 構建[7],具體如下:
從技術角度來看,執行任務的動態容器是 Jenkins 代理節點。當構建啟動時,首先是一個新節點啟動,透過 Jenkins 主節點的 JNLP(5000 埠) 告知就緒狀態。在代理節點啟動並提取構建任務之前,構建任務處於排隊狀態。就像通常 Jenkins 代理伺服器那樣,構建輸出會送達主節點;不同的是,構建完成後代理節點容器會自動關閉。
不同型別的構建任務(例如 Java、 NodeJS、 Python等)對應不同的代理節點。這並不新奇,之前也是使用標簽來限制哪些代理節點可以執行指定的構建任務。啟動用於構建任務的 Jenkins 代理節點容器需要配置引數,具體如下:
這裡用到的關鍵元件是 Jenkins Kubernetes 外掛[9]。該外掛(透過使用一個服務賬號) 與 K8S 叢集互動,可以啟動和關閉代理節點。在外掛的配置管理中,多種代理節點型別表現為多種 Kubernetes pod 模板,它們透過專案標簽對應。
這些代理節點映象[10]以開箱即用的方式提供(也有 CentOS7[11] 系統的版本):
註意:本解決方案與 OpenShift 中的 Source-to-Image(S2I)[15] 構建無關,雖然後者也可以用於某些特定的 CI/CD 任務。
入門學習資料
有很多不錯的部落格和檔案介紹瞭如何在 OpenShift 上執行 Jenkins 構建。不妨從下麵這些開始:
閱讀這些部落格和檔案有助於完整的理解本解決方案。在本文中,我們主要關註具體實踐中遇到的各類問題。
構建我的應用
作為示例專案[20],我們選取了包含如下構建步驟的 Java 專案:
在 CI/CD 過程中,我們需要與 Git 和 Nexus 互動,故 Jenkins 任務需要能夠訪問這些系統。這要求引數配置和已儲存憑證可以在下列位置進行管理:
你可以按自己的喜好選擇一種實現方式,甚至你最終可能混用多種實現方式。下麵我們採用第二種實現方式,即首選在 OpenShift 中管理引數配置。使用 Kubernetes 外掛配置來定製化 Maven 代理容器,包括設定環境變數和對映檔案等。
註意:對於 Kubernetes 外掛 v1.0 版,由於 bug[21],在 UI 介面增加環境變數並不生效。可以升級外掛,或(作為變通方案) 直接修改 config.xml
檔案並重啟 Jenkins。
從 Git 獲取原始碼
從公共 Git 倉庫獲取原始碼很容易。但對於私有 Git 倉庫,不僅需要認證操作,客戶端還需要信任伺服器以便建立安全連線。一般而言,透過兩種協議獲取原始碼:
HTTPS:驗證透過使用者名稱/密碼完成。Git 伺服器的 SSL 證書必須被代理節點信任,這僅在證書被自建 CA 簽名時才需要特別註意。
git clone https://git.mycompany.com:443/myapplication.git
SSH:驗證透過私鑰完成。如果伺服器的公鑰指紋出現在 known_hosts
檔案中,那麼該伺服器是被信任的。
git clone ssh://git@git.mycompany.com:22/myapplication.git
對於手動操作,使用使用者名稱/密碼透過 HTTP 方式下載原始碼是可行的;但對於自動構建而言,SSH 是更佳的選擇。
透過 SSH 方式使用 Git
要透過 SSH 方式下載原始碼,我們需要保證代理容器與 Git 的 SSH 埠之間可以建立 SSH 連線。首先,我們需要建立一個私鑰-公鑰對。使用如下命令生成:
ssh keygen -t rsa -b 2048 -f my-git-ssh -N ''
命令生成的私鑰位於 my-git-ssh
檔案中(口令為空),對應的公鑰位於 my-git-ssh.pub
檔案中。將公鑰新增至 Git 伺服器的對應使用者下(推薦使用“服務賬號”);網頁介面一般支援公鑰上傳。為建立 SSH 連線,我們還需要在代理容器上配置兩個檔案:
~/.ssh/id_rsa
~/.ssh/known_hosts
。要實現這一點,執行 ssh git.mycompany.com
並接受伺服器指紋,系統會在 ~/.ssh/known_hosts
檔案中增加一行。這樣需求得到了滿足。將 id_rsa
對應的私鑰和 known_hosts
對應的公鑰儲存到一個 OpenShift 的 secret(或 ConfigMap) 物件中。
apiVersion: v1
kind: Secret
metadata:
name: mygit-ssh
stringData:
id_rsa: |-
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
known_hosts: |-
git.mycompany.com ecdsa-sha2-nistp256 AAA...
在 Kubernetes 外掛中將 secret 物件配置為捲,掛載到 /home/jenkins/.ssh/
,供 Maven pod 使用。secret 中的每個物件對應掛載目錄的一個檔案,檔案名與 key 名稱相符。我們可以使用 UI(管理 Jenkins / 配置 / 雲 / Kubernetes),也可以直接編輯 Jenkins 配置檔案 /var/lib/jenkins/config.xml
:
maven
...
此時,在代理節點上執行的任務應該可以透過 SSH 方式從 Git 程式碼庫獲取原始碼。
註:我們也可以在 ~/.ssh/config
檔案中自定義 SSH 連線。例如,如果你不想處理 known_hosts
或私鑰位於其它掛載目錄中:
Host git.mycompany.com
StrictHostKeyChecking no
IdentityFile /home/jenkins/.config/git-secret/ssh-privatekey
透過 HTTP 方式使用 Git
如果你選擇使用 HTTP 方式下載,在指定的 Git-credential-store[22] 檔案中新增使用者名稱/密碼:
例如,在一個 OpenShift secret 物件中增加 /home/jenkins/.config/git-secret/credentials
檔案對應,其中每個站點對應檔案中的一行:
https://username:password@git.mycompany.com
https://user:pass@github.com
在 git-config[23] 配置中啟用該檔案,其中配置檔案預設路徑為 /home/jenkins/.config/git/config
:
[credential]
helper = store --file=/home/jenkins/.config/git-secret/credentials
如果 Git 服務使用了自有 CA 簽名的證書,為代理容器設定環境變數 GIT_SSL_NO_VERIFY=true
是最便捷的方式。更恰當的解決方案包括如下兩步:
/usr/ca/myTrustedCA.pem
)。透過環境變數 GIT_SSL_CAINFO=/usr/ca/myTrustedCA.pem
或上面提到的 git-config
檔案的方式,將證書路徑告知 Git。
[http "https://git.mycompany.com"]
sslCAInfo = /usr/ca/myTrustedCA.pem
註:在 OpenShift v3.7 及早期版本中,ConfigMap 及 secret 的掛載點之間不能相互改寫[24],故我們不能同時對映 /home/jenkins
和 /home/jenkins/dir
。因此,上面的程式碼中並沒有使用常見的檔案路徑。預計 OpenShift v3.9 版本會修複這個問題。
Maven
要完成 Maven 構建,一般需要完成如下兩步:
對於容器中執行構建的實踐而言,使用內部 Maven 庫是非常關鍵的,因為容器啟動後並沒有本地庫或快取,這導致每次構建時 Maven 都下載全部的 Jar 檔案。在本地網路使用內部代理庫下載明顯快於從因特網下載。
Maven Jenkins 代理[13]映象允許配置環境變數,指定代理的 URL。在 Kubernetes 外掛的容器模板中設定如下:
MAVEN_MIRROR_URL=https://nexus.mycompany.com/repository/maven-public
構建好的成品(JAR) 也應該儲存到庫中,可以是上面提到的用於提供依賴的映象庫,也可以是其它庫。Maven 完成 deploy
操作需要在 pom.xml
的分發管理[25] 下配置庫 URL,這與代理映象無關。
...>
mynexus
上傳成品可能涉及認證。在這種情況下,在 settings.xml
中配置的使用者名稱/密碼要與 pom.xml
檔案中的對應的伺服器 id
下的設定匹配。我們可以使用 OpenShift secret 將包含 URL、使用者名稱和密碼的完整 settings.xml
對映到 Maven Jenkins 代理容器中。另外,也可以使用環境變數。具體如下:
利用 secret 為容器新增環境變數:
MAVEN_SERVER_USERNAME=admin
MAVEN_SERVER_PASSWORD=admin123
利用 config map 將 settings.xml
掛載至 /home/jenkins/.m2/settings.xml
:
...>
external:*
禁用互動樣式(即,使用批處理樣式) 可以忽略下載日誌,一種方式是在 Maven 命令中增加 -B
引數,另一種方式是在 settings.xml
配置檔案中增加
配置。
如果 Maven 庫的 HTTPS 服務使用自建 CA 簽名的證書,我們需要使用 keytool[26] 工具建立一個將 CA 公鑰新增至信任串列的 Java KeyStore。在 OpenShift 中使用 ConfigMap 將這個 Keystore 上傳。使用 oc
命令基於檔案建立一個 ConfigMap:
oc create configmap maven-settings --from-file=settings.xml=settings.xml --from-
file=myTruststore.jks=myTruststore.jks
將這個 ConfigMap 掛載至 Jenkins 代理容器。在本例中我們使用 /home/jenkins/.m2
目錄,但這僅僅是因為配置檔案 settings.xml
也對應這個 ConfigMap。KeyStore 可以放置在任意路徑下。
接著在容器環境變數 MAVEN_OPTS
中設定 Java 引數,以便讓 Maven 對應的 Java 行程使用該檔案:
MAVEN_OPTS=
-Djavax.net.ssl.trustStore=/home/jenkins/.m2/myTruststore.jks
-Djavax.net.ssl.trustStorePassword=changeit
記憶體使用量
這可能是最重要的一部分設定,如果沒有正確的設定最大記憶體,我們會遇到間歇性構建失敗,雖然每個元件都似乎工作正常。
如果沒有在 Java 命令列中設定堆大小,在容器中執行 Java 可能導致高記憶體使用量的報錯。JVM 可以利用全部的宿主機記憶體[27],而不是使用容器記憶體現在並相應設定預設的堆大小[28]。這通常會超過容器的記憶體資源總額,故當 Java 行程為堆分配過多記憶體時,OpenShift 會直接殺掉容器。
雖然 jenkins-slave-base
映象包含一個內建指令碼設定堆最大為[29]容器記憶體的一半(可以透過環境變數 CONTAINER_HEAP_PERCENT=0.50
修改),但這隻適用於 Jenkins 代理節點中的 Java 行程。在 Maven 構建中,還有其它重要的 Java 行程執行:
mvn
命令本身就是一個 Java 工具。總結一下,容器中同時執行著三個重要的 Java 行程,預估記憶體使用量以避免 pod 被誤殺是很重要的。每個行程都有不同的方式設定 JVM 引數:
JAVA_OPTS
。mvn
工具被 Jenkins 任務呼叫。設定 MAVEN_OPTS
可以用於自定義這類 Java 行程。surefire
外掛滋生的用於單元測試的 JVM 可以透過 Maven argLine[31] 屬性自定義。可以在 pom.xml
或 settings.xml
的某個配置檔案中設定,也可以直接在 maven
命令引數 MAVEN_OPS
中增加 -DargLine=…
。下麵例子給出 Maven 代理容器環境變數設定方法:
JAVA_OPTS=-Xms64m -Xmx64m
MAVEN_OPTS=-Xms128m -Xmx128m -DargLine=${env.SUREFIRE_OPTS}
SUREFIRE_OPTS=-Xms256m -Xmx256m
我們的測試環境是具有 1024Mi 記憶體限額的代理容器,使用上述引數可以正常構建一個 SpringBoot 應用併進行單元測試。測試環境使用的資源相對較小,對於複雜的 Maven 專案和對應的單元測試,我們需要更大的堆大小及更大的容器記憶體限額。
註:Java8 行程的實際記憶體使用量包括“堆大小 + 元資料 + 堆外記憶體”,因此記憶體使用量會明顯高於設定的最大堆大小。在我們上面的測試環境中,三個 Java 行程使用了超過 900Mi 的記憶體。可以在容器內檢視行程的 RSS 記憶體使用情況,命令如下:ps -e -o pid,user,rss,comm,args
。
Jenkins 代理映象同時安裝了 JDK 64 位和 32 位版本。對於 mvn
和 surefire
,預設使用 64 位版本 JVM。為減低記憶體使用量,只要 -Xmx
不超過 1.5 GB,強制使用 32 位 JVM 都是有意義的。
JAVA_HOME=/usr/lib/jvm/Java-1.8.0-openjdk-1.8.0.161–0.b14.el7_4.i386
註意到我們可以在 JAVA_TOOL_OPTIONS
環境變數中設定 Java 引數,每個 JVM 啟動時都會讀取該引數。JAVA_OPTS
和 MAVEN_OPTS
中的引數會改寫 JAVA_TOOL_OPTIONS
中的對應值,故我們可以不使用 argLine
,實現對 Java 行程同樣的堆配置:
JAVA_OPTS=-Xms64m -Xmx64m
MAVEN_OPTS=-Xms128m -Xmx128m
JAVA_TOOL_OPTIONS=-Xms256m -Xmx256m
但缺點是每個 JVM 的日誌中都會顯示 Picked up JAVA_TOOL_OPTIONS:
,這可能讓人感到迷惑。
Jenkins 流水線
完成上述配置,我們應該已經可以完成一次成功的構建。我們可以獲取原始碼,下載依賴,執行單元測試並將成品上傳到我們的庫中。我們可以透過建立一個 Jenkins 流水線專案來完成上述操作。
pipeline {
/* Which container to bring up for the build. Pick one of the templates configured in Kubernetes plugin. */
agent {
label 'maven'
}
stages {
stage('Pull Source') {
steps {
git url: 'ssh://git@git.mycompany.com:22/myapplication.git', branch: 'master'
}
}
stage('Unit Tests') {
steps {
sh 'mvn test'
}
}
stage('Deploy to Nexus') {
steps {
sh 'mvn deploy -DskipTests'
}
}
}
}
當然,對應真實專案,CI/CD 流水線不僅僅完成 Maven 構建,還可以部署到開發環境,執行整合測試,提升至更接近於生產的環境等。上面給出的學習資料中有執行這些操作的案例。
多容器
一個 pod 可以執行多個容器,每個容器有單獨的資源限制。這些容器共享網路介面,故我們可以從 localhost
訪問已啟動的服務,但我們需要考慮埠衝突的問題。在一個 Kubernetes pod 模板中,每個容器的環境變數是單獨設定的,但掛載的捲是統一的。
當一個外部服務需要單元測試且嵌入式方案無法工作(例如,資料庫、訊息中介軟體等) 時,可以啟動多個容器。在這種情況下,第二個容器會隨著 Jenkins 代理容器啟停。
檢視 Jenkins config.xml
片段,其中我們啟動了一個輔助的 httpbin
服務用於 Maven 構建:
maven
...
...
...
...
總結
作為總結,我們檢視上面已描述配置的 Jenkins config.xml
對應建立的 OpenShift 資源以及 Kubernetes 外掛的配置。
apiVersion: v1
kind: List
metadata: {}
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: git-config
data:
config: |
[credential]
helper = store --file=/home/jenkins/.config/git-secret/credentials
[http "http://git.mycompany.com"]
sslCAInfo = /home/jenkins/.config/git/myTrustedCA.pem
myTrustedCA.pem: |-
-----BEGIN CERTIFICATE-----
MIIDVzCCAj+gAwIBAgIJAN0sC...
-----END CERTIFICATE-----
- apiVersion: v1
kind: Secret
metadata:
name: git-secret
stringData:
ssh-privatekey: |-
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
credentials: |-
https://username:password@git.mycompany.com
https://user:pass@github.com
- apiVersion: v1
kind: ConfigMap
metadata:
name: git-ssh
data:
config: |-
Host git.mycompany.com
StrictHostKeyChecking yes
IdentityFile /home/jenkins/.config/git-secret/ssh-privatekey
known_hosts: '[git.mycompany.com]:22 ecdsa-sha2-nistp256 AAAdn7...'
- apiVersion: v1
kind: Secret
metadata:
name: maven-secret
stringData:
username: admin
password: admin123
基於檔案建立另一個 ConfigMap:
oc create configmap maven-settings --from-file=settings.xml=settings.xml
--from-file=myTruststore.jks=myTruststore.jks
Kubernetes 外掛配置如下:
xml version='1.0' encoding='UTF-8'?>
...
plugin="kubernetes@1.0">
openshift
maven
MIIC6jCC...
-----END CERTIFICATE-----
嘗試愉快的構建吧!
原文發表於 ITNext[32],已獲得翻版授權。
via: https://opensource.com/article/18/4/running-jenkins-builds-containers
作者:Balazs Szeti[34] 選題:lujun9972 譯者:pinewall 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出