文章目錄
-
- 1、Jenkins CI/CD 背景介紹
- 2、環境、軟體準備
- 3、部署 Jenkins Server 到 Kubernetes
- 4、Jenkins 配置 Kubernetes Plugin
- 5、測試并驗證
-
- 5.1、pipeline 類型支援
- 5.2、Container Group 類型支援
- 5.3、非 Pipeline 類型支援
- 5.4、配置自定義 jenkins-slave 鏡像
1、Jenkins CI/CD 背景介紹
持續建構與釋出是我們日常工作中必不可少的一個步驟,目前大多公司都采用 Jenkins 叢集來搭建符合需求的 CI/CD 流程,然而傳統的 Jenkins Slave 一主多從方式會存在一些痛點,比如:主 Master 發生單點故障時,整個流程都不可用了;每個 Slave 的配置環境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導緻管理起來非常不友善,維護起來也是比較費勁;資源配置設定不均衡,有的 Slave 要運作的 job 出現排隊等待,而有的 Slave 處于空閑狀态;最後資源有浪費,每台 Slave 可能是實體機或者 VM,當 Slave 處于空閑狀态時,也不會完全釋放掉資源。
由于以上種種痛點,我們渴望一種更高效更可靠的方式來完成這個 CI/CD 流程,而 Docker 虛拟化容器技術能很好的解決這個痛點,下圖是基于 Kubernetes 搭建 Jenkins 叢集的簡單示意圖。
從圖上可以看到 Jenkins Master 和 Jenkins Slave 以 Docker Container 形式運作在 Kubernetes 叢集的 Node 上,Master 運作在其中一個節點,并且将其配置資料存儲到一個 Volume 上去,Slave 運作在各個節點上,并且它不是一直處于運作狀态,它會按照需求動态的建立并自動删除。
這種方式的工作流程大緻為:當 Jenkins Master 接受到 Build 請求時,會根據配置的 Label 動态建立一個運作在 Docker Container 中的 Jenkins Slave 并注冊到 Master 上,當運作完 Job 後,這個 Slave 會被登出并且 Docker Container 也會自動删除,恢複到最初狀态。
這種方式帶來的好處有很多:
- 服務高可用,當 Jenkins Master 出現故障時,Kubernetes 會自動建立一個新的 Jenkins Master 容器,并且将 Volume 配置設定給新建立的容器,保證資料不丢失,進而達到叢集服務高可用。
- 動态伸縮,合理使用資源,每次運作 Job 時,會自動建立一個 Jenkins Slave,Job 完成後,Slave 自動登出并删除容器,資源自動釋放,而且 Kubernetes 會根據每個資源的使用情況,動态配置設定 Slave 到空閑的節點上建立,降低出現因某節點資源使用率高,還排隊等待在該節點的情況。
- 擴充性好,當 Kubernetes 叢集的資源嚴重不足而導緻 Job 排隊等待時,可以很容易的添加一個 Kubernetes Node 到叢集中,進而實作擴充。
2、環境、軟體準備
本次示範環境,我是在本機 MAC OS 以及虛拟機 Linux Centos7 上操作,以下是安裝的軟體及版本:
- Docker: version 17.09.0-ce
- Oracle VirtualBox: version 5.1.20 r114628 (Qt5.6.2)
- Minikube: version v0.22.2
- Kuberctl:
- Client Version: v1.8.1
- Server Version: v1.7.5
注意:Minikube 啟動的單節點 k8s Node 執行個體是需要運作在本機的 VM 虛拟機裡面,是以需要提前安裝好 VM,這裡我選擇 Oracle VirtualBox。k8s 運作底層使用 Docker 容器,是以本機需要安裝好 Docker 環境,Minikube 和 Kuberctl 的安裝過程可參考之前文章 初試 minikube 本地部署運作 kubernetes 執行個體。
3、部署 Jenkins Server 到 Kubernetes
在執行部署之前,我們要確定 Minikube 已經正常運作,如果使用已搭建好的 Kubernetes 叢集,也要確定正常運作。接下來,我們需要準備部署 Jenkins 的 Yaml 檔案,可以參考
GitHub jenkinsci kubernetes-plugin 官網提供的 jenkins.yaml 和 service-account.yaml 檔案,這裡官網使用的是比較規範的 StatefulSet(有狀态叢集服務)方式進行部署,并配置了 Ingress 和 RBAC 賬戶權限資訊。不過我本機測試的時候,發現 Volume 挂載失敗,日志顯示沒有權限建立目錄。是以我精簡了一下,重新寫了個以 Deployment 方式部署方式以及 Service 的配置檔案(這裡偷個懶,不使用 RBAC 認證了)。
$ cat jenkins-deployment.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: jenkins
labels:
k8s-app: jenkins
spec:
replicas: 1
selector:
matchLabels:
k8s-app: jenkins
template:
metadata:
labels:
k8s-app: jenkins
spec:
containers:
- name: jenkins
image: jenkins/jenkins:lts-alpine
imagePullPolicy: IfNotPresent
volumeMounts:
- name: jenkins-home
mountPath: /var/jenkins_home
ports:
- containerPort: 8080
name: web
- containerPort: 50000
name: agent
volumes:
- name: jenkins-home
emptyDir: {}
$ cat jenkins-service.yml
kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: jenkins
name: jenkins
spec:
type: NodePort
ports:
- port: 8080
name: web
targetPort: 8080
- port: 50000
name: agent
targetPort: 50000
selector:
k8s-app: jenkins
說明一下:這裡 Service 我們暴漏了端口 8080 和 50000,8080 為通路 Jenkins Server 頁面端口,50000 為建立的 Jenkins Slave 與 Master 建立連接配接進行通信的預設端口,如果不暴露的話,Slave 無法跟 Master 建立連接配接。這裡使用 NodePort 方式暴漏端口,并未指定其端口号,由 Kubernetes 系統預設配置設定,當然也可以指定不重複的端口号(範圍在 30000~32767)。
接下來,通過 kubectl 指令行執行建立 Jenkins Service。
$ kubectl create namespace kubernetes-plugin
$ kubectl config set-context $(kubectl config current-context) --namespace=kubernetes-plugin
$ kubectl create -f jenkins-deployment.yaml
$ kubectl create -f jenkins-service.yml
說明一下:這裡我們建立一個新的 namespace 為 kubernetes-plugin,并且将目前 context 設定為 kubernetes-plugin namespace 這樣就會自動切換到該空間下,友善後續指令操作。
$ kubectl get service,deployment,pod
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
jenkins NodePort 10.0.0.204 <none> 8080:30645/TCP,50000:31981/TCP 1m
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
jenkins 1 1 1 1 1m
NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1m
此時,我們會發現 Jenkins Master 服務已經啟動起來了,并且将端口暴漏到
8080:30645
,
50000:31981
,此時可以通過浏覽器打開
http://<Cluster_IP>:30645
通路 Jenkins 頁面了。當然也可以通過
minikube service ...
指令來自動打開頁面。
$ minikube service jenkins -n kubernetes-plugin
Opening kubernetes service kubernetes-plugin/jenkins in default browser...
Opening kubernetes service kubernetes-plugin/jenkins in default browser...
在浏覽器上完成 Jenkins 的初始化插件安裝過程,并配置管理者賬戶資訊,這裡忽略過程,初始化完成後界面如下:
注意: 初始化過程中,讓輸入
/var/jenkins_home/secret/initialAdminPassword
初始密碼時,因為我們設定的
emptyDir: {}
沒有挂載到外部路徑,可以進入到容器内部進行擷取。
$ kubectl exec -it jenkins-960997836-fff2q cat /var/jenkins_home/secrets/initialAdminPassword
4、Jenkins 配置 Kubernetes Plugin
管理者賬戶登入 Jenkins Master 頁面,點選 “系統管理” —> “管理插件” —> “可選插件” —> “Kubernetes plugin” 勾選安裝即可。
安裝完畢後,點選 “系統管理” —> “系統設定” —> “新增一個雲” —> 選擇 “Kubernetes”,然後填寫 Kubernetes 和 Jenkins 配置資訊。
說明一下:
- Name 處預設為 kubernetes,也可以修改為其他名稱,如果這裡修改了,下邊在執行 Job 時指定 podTemplate() 參數 cloud 為其對應名稱,否則會找不到,cloud 預設值取:kubernetes
- Kubernetes URL 處我填寫了
這裡我填寫了 Kubernetes Service 對應的 DNS 記錄,通過該 DNS 記錄可以解析成該 Service 的 Cluster IP,注意:也可以填寫https://kubernetes.default
完整 DNS 記錄,因為它要符合https://kubernetes.default.svc.cluster.local
的命名方式,或者直接填寫外部 Kubernetes 的位址<svc_name>.<namespace_name>.svc.cluster.local
。https://<ClusterIP>:<Ports>
- Jenkins URL 處我填寫了
,跟上邊類似,也是使用 Jenkins Service 對應的 DNS 記錄,不過要指定為 8080 端口,因為我們設定暴漏 8080 端口。同時也可以用http://jenkins.kubernetes-plugin:8080
方式,例如我這裡可以填http://<ClusterIP>:<Node_Port>
也是沒有問題的,這裡的 30645 就是對外暴漏的 NodePort。http://192.168.99.100:30645
配置完畢,可以點選 “Test Connection” 按鈕測試是否能夠連接配接的到 Kubernetes,如果顯示
Connection test successful
則表示連接配接成功,配置沒有問題。
5、測試并驗證
好了,通過 Kubernetes 安裝 Jenkins Master 完畢并且已經配置好了連接配接,接下來,我們可以配置 Job 測試一下是否會根據配置的 Label 動态建立一個運作在 Docker Container 中的 Jenkins Slave 并注冊到 Master 上,而且運作完 Job 後,Slave 會被登出并且 Docker Container 也會自動删除吧!
5.1、pipeline 類型支援
建立一個 Pipeline 類型 Job 并命名為 my-k8s-jenkins-pipeline,然後在 Pipeline 腳本處填寫一個簡單的測試腳本如下:
def label = "mypod-${UUID.randomUUID().toString()}"
podTemplate(label: label, cloud: 'kubernetes') {
node(label) {
stage('Run shell') {
sh 'sleep 130s'
sh 'echo hello world.'
}
}
}
執行建構,此時去建構隊列裡面,可以看到有一個建構任務,暫時還沒有執行中的建構,因為還沒有初始化好,稍等一會,就會看到 Master 和 jenkins-slave-jbs4z-xs2r8 已經建立完畢,在等一會,就會發現 jenkins-slave-jbs4z-xs2r8 已經注冊到 Master 中,并開始執行 Job,點選該 Slave 節點,我們可以看到通過标簽 mypod-b538c04c-7c19-4b98-88f6-9e5bca6fc9ba 關聯,該 Label 就是我們定義的标簽格式生成的,Job 執行完畢後,jenkins-slave 會自動登出,我們通過 kubectl 指令行,可以看到整個自動建立和删除過程。
# jenkins slave 啟動前,隻有 jenkins master 服務存在
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d
# jenkins slave 自動建立完畢
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d
jenkins-slave-jbs4z-xs2r8 1/1 Running 0 56s
# Docker Container 啟動服務情況
$ docker ps |grep jenkins
aa5121667601 jenkins/jnlp-slave "jenkins-slave bd880…" About a minute ago Up About a minute k8s_jnlp_jenkins-slave-jbs4z-xs2r8_kubernetes-plugin_25a91ed9-3337-11e8-a49f-08002744a3f1_0
d64deb0eaa20 gcr.io/google_containers/pause-amd64:3.0 "/pause" About a minute ago Up About a minute k8s_POD_jenkins-slave-jbs4z-xs2r8_kubernetes-plugin_25a91ed9-3337-11e8-a49f-08002744a3f1_0
995c1743552a jenkins "/bin/tini -- /usr/l…" 27 hours ago Up 26 hours k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 27 hours ago Up 26 hours k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
# jenkins slave 執行完畢自動删除
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d
jenkins-slave-jbs4z-xs2r8 0/1 Terminating 0 2m
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d
$ docker ps |grep jenkins
995c1743552a jenkins "/bin/tini -- /usr/l…" 27 hours ago Up 26 hours k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 27 hours ago Up 26 hours k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
從上邊的記錄檔中,我們可以清晰的看到 Jenkins Slave 自動建立到登出删除的過程,整個過程是自動完成的,不需要人工幹預。
5.2、Container Group 類型支援
建立一個 Pipeline 類型 Job 并命名為 my-k8s-jenkins-container,然後在 Pipeline 腳本處填寫一個簡單的測試腳本如下:
def label = "mypod-${UUID.randomUUID().toString()}"
podTemplate(label: label, cloud: 'kubernetes', containers: [
containerTemplate(name: 'maven', image: 'maven:3.3.9-jdk-8-alpine', ttyEnabled: true, command: 'cat'),
]) {
node(label) {
stage('Get a Maven Project') {
git 'https://github.com/jenkinsci/kubernetes-plugin.git'
container('maven') {
stage('Build a Maven project') {
sh 'mvn -B clean install'
}
}
}
}
}
注意:這裡我們使用的 containers 定義了一個 containerTemplate 模闆,指定名稱為 maven 和使用的 Image,下邊在執行 Stage 時,使用
container('maven'){...}
就可以指定在該容器模闆裡邊執行相關操作了。比如,該示例會在 jenkins-slave 中執行 git clone 操作,然後進入到 maven 容器内執行
mvn -B clean install
編譯操作。這種操作的好處就是,我們隻需要根據代碼類型分别制作好對應的編譯環境鏡像,通過指定不同的 container 來分别完成對應代碼類型的編譯操作。模闆詳細的各個參數配置可以參照 Pod and container template configuration。
執行建構,跟上邊 Pipeline 類似,也會建立 jenkins-slave 并注冊到 master,不同的是,它會在 Kubernetes 中啟動我們配置的 maven 容器模闆,來執行相關指令。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d
jenkins-slave-k2wwq-4l66k 2/2 Running 0 53s
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8ed81ee3aad4 jenkins/jnlp-slave "jenkins-slave 4ae74…" About a minute ago Up About a minute k8s_jnlp_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
bd252f7e59c2 maven "cat" About a minute ago Up About a minute k8s_maven_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
fe22da050a53 gcr.io/google_containers/pause-amd64:3.0 "/pause" About a minute ago Up About a minute k8s_POD_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
995c1743552a jenkins "/bin/tini -- /usr/l…" 44 hours ago Up 44 hours k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 44 hours ago Up 44 hours k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
5.3、非 Pipeline 類型支援
Jenkins 中除了使用 Pipeline 方式運作 Job 外,通常我們也會使用普通類型 Job,如果也要想使用kubernetes plugin 來建構任務,那麼就需要點選 “系統管理” —> “系統設定” —> “雲” —> “Kubernetes” —> “Add Pod Template” 進行配置 “Kubernetes Pod Template” 資訊。
注意:這裡的 Labels 名在配置非 pipeline 類型 Job 時,用來指定任務運作的節點。Containers 下的 Name 字段的名字,這裡要注意的是,如果 Name 配置為 jnlp,那麼 Kubernetes 會用下邊指定的 Docker Image 代替預設的
jenkinsci/jnlp-slave
鏡像,否則,Kubernetes plugin 還是會用預設的
jenkinsci/jnlp-slave
鏡像與 Jenkins Server 建立連接配接,即使我們指定其他 Docker Image。這裡我随便配置為 jnlp-slave,意思就是使用預設的
jenkinsci/jnlp-slave
鏡像來運作,因為我們暫時還沒制作可以替代預設鏡像的鏡像。
建立一個自由風格的 Job 名稱為 my-k8s-jenkins-simple,配置 “Restrict where this project can be run” 勾選,在 “Label Expression” 後邊輸出我們上邊建立模闆是指定的 Labels 名稱 jnlp-agent,意思是指定該 Job 比對 jnlp-agent 标簽的 Slave 上運作。
執行建構後,跟上邊 Pipeline 一樣,符合我們的預期。
5.4、配置自定義 jenkins-slave 鏡像
通過 kubernetest plugin 預設提供的鏡像
jenkinsci/jnlp-slave
可以完成一些基本的操作,它是基于
openjdk:8-jdk
鏡像來擴充的,但是對于我們來說這個鏡像功能過于簡單,比如我們想執行 Maven 編譯或者其他指令時,就有問題了,那麼可以通過制作自己的鏡像來預安裝一些軟體,既能實作 jenkins-slave 功能,又可以完成自己個性化需求,那就比較不錯了。如果我們從頭開始制作鏡像的話,會稍微麻煩些,不過可以參考 jenkinsci/jnlp-slave 和 jenkinsci/docker-slave 這兩個官方鏡像來做,注意:
jenkinsci/jnlp-slave
鏡像是基于
jenkinsci/docker-slave
來做的。這裡我簡單示範下,基于
jenkinsci/jnlp-slave:latest
鏡像,在其基礎上做擴充,安裝 Maven 到鏡像内,然後運作驗證是否可行吧。
建立一個 Pipeline 類型 Job 并命名為 my-k8s-jenkins-container-custom,然後在 Pipeline 腳本處填寫一個簡單的測試腳本如下:
def label = "mypod-${UUID.randomUUID().toString()}"
podTemplate(label: label, cloud: 'kubernetes',containers: [
containerTemplate(
name: 'jnlp',
image: 'huwanyang168/jenkins-slave-maven:latest',
alwaysPullImage: false,
args: '${computer.jnlpmac} ${computer.name}'),
]) {
node(label) {
stage('stage1') {
stage('Show Maven version') {
sh 'mvn -version'
sh 'sleep 60s'
}
}
}
}
說明一下:這裡 containerTemplate 的 name 屬性必須叫 jnlp,Kubernetes 才能用自定義 images 指定的鏡像替換預設的
jenkinsci/jnlp-slave
鏡像。此外,args 參數傳遞兩個 jenkins-slave 運作需要的參數。還有一點就是這裡并不需要指定
container('jnlp'){...}
了,因為它被 Kubernetes 指定了要被執行的容器,是以直接執行 Stage 就可以了。
執行建構,看下效果如何吧!
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 2d
jenkins-slave-9wtkt-d2ms8 1/1 Running 0 12m
bj-m-204072a:k8s-gitlab wanyang3$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b31be1de9563 huwanyang168/jenkins-slave-maven "jenkins-slave 7cef1…" 12 minutes ago Up About a minute k8s_jnlp_jenkins-slave-9wtkt-d2ms8_kubernetes-plugin_0ea4bc9d-33f3-11e8-a49f-08002744a3f1_0
b33b7ce3070e gcr.io/google_containers/pause-amd64:3.0 "/pause" 12 minutes ago Up About a minute k8s_POD_jenkins-slave-9wtkt-d2ms8_kubernetes-plugin_0ea4bc9d-33f3-11e8-a49f-08002744a3f1_0
995c1743552a jenkins "/bin/tini -- /usr/l…" 2 days ago Up 2 days k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 2 days ago Up 2 days
當然,我們也可以使用非 Pipeline 類型指定運作該自定義 slave,那麼我們就需要修改 “系統管理” —> “系統設定” —> “雲” —> “Kubernetes” —> “Add Pod Template” 修改配置 “Kubernetes Pod Template” 資訊如下:
然後同樣在 Job 配置頁面 “Label Expression” 後邊輸出我們上邊建立模闆是指定的 Labels 名稱 jnlp-agent,就可以啦!測試妥妥沒問題的。
最後,貼一下我自定義的預安裝了 Maven 的 Jenkins-slave 鏡像的 Dockerfile ,當然大家可以基于此預安裝一些其他軟體,來完成日常持續建構與釋出工作吧。
FROM jenkins/jnlp-slave:latest
MAINTAINER [email protected]
LABEL Description="This is a extend image base from jenkins/jnlp-slave which install maven in it."
# 切換到 root 賬戶進行操作
USER root
# 安裝 maven-3.3.9
RUN wget http://mirrors.sonic.net/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz && \
tar -zxf apache-maven-3.3.9-bin.tar.gz && \
mv apache-maven-3.3.9 /usr/local && \
rm -f apache-maven-3.3.9-bin.tar.gz && \
ln -s /usr/local/apache-maven-3.3.9/bin/mvn /usr/bin/mvn && \
ln -s /usr/local/apache-maven-3.3.9 /usr/local/apache-maven
USER jenkins
參考資料
- jenkinsci/kubernetes-plugin
- jenkinsci/docker-slave