jenkins CI/CD(動(dòng)態(tài)創(chuàng)建slave)簡述:
持續(xù)構(gòu)建與發(fā)布是我們?nèi)粘9ぷ髦斜夭豢缮俚囊粋€(gè)步驟,目前大多公司都采用 Jenkins 集群來搭建符合需求的 CI/CD 流程,然而傳統(tǒng)的 Jenkins Slave 一主多從方式會(huì)存在一些痛點(diǎn),比如:
- 主 Master 發(fā)生單點(diǎn)故障時(shí),整個(gè)流程都不可用了
- 每個(gè) Slave 的配置環(huán)境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導(dǎo)致管理起來非常不方便,維護(hù)起來也是比較費(fèi)勁
- 資源分配不均衡,有的 Slave 要運(yùn)行的 job 出現(xiàn)排隊(duì)等待,而有的 Slave 處于空閑狀態(tài)
- 資源有浪費(fèi),每臺(tái) Slave 可能是物理機(jī)或者虛擬機(jī),當(dāng) Slave 處于空閑狀態(tài)時(shí),也不會(huì)完全釋放掉資源。

從圖上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式運(yùn)行在 Kubernetes 集群的 Node 上,Master 運(yùn)行在其中一個(gè)節(jié)點(diǎn),并且將其配置數(shù)據(jù)存儲(chǔ)到一個(gè) Volume 上去,Slave 運(yùn)行在各個(gè)節(jié)點(diǎn)上,并且它不是一直處于運(yùn)行狀態(tài),它會(huì)按照需求動(dòng)態(tài)的創(chuàng)建并自動(dòng)刪除。
這種方式的工作流程大致為:當(dāng) Jenkins Master 接受到 Build 請(qǐng)求時(shí),會(huì)根據(jù)配置的 Label 動(dòng)態(tài)創(chuàng)建一個(gè)運(yùn)行在 Pod 中的 Jenkins Slave 并注冊(cè)到 Master 上,當(dāng)運(yùn)行完 Job 后,這個(gè) Slave 會(huì)被注銷并且這個(gè) Pod 也會(huì)自動(dòng)刪除,恢復(fù)到最初狀態(tài)。
使用jenkins動(dòng)態(tài)slave的優(yōu)勢:
- 服務(wù)高可用,當(dāng) Jenkins Master 出現(xiàn)故障時(shí),Kubernetes 會(huì)自動(dòng)創(chuàng)建一個(gè)新的 Jenkins Master 容器,并且將 Volume 分配給新創(chuàng)建的容器,保證數(shù)據(jù)不丟失,從而達(dá)到集群服務(wù)高可用。
- 動(dòng)態(tài)伸縮,合理使用資源,每次運(yùn)行 Job 時(shí),會(huì)自動(dòng)創(chuàng)建一個(gè) Jenkins Slave,Job 完成后,Slave 自動(dòng)注銷并刪除容器,資源自動(dòng)釋放,而且 Kubernetes 會(huì)根據(jù)每個(gè)資源的使用情況,動(dòng)態(tài)分配 Slave 到空閑的節(jié)點(diǎn)上創(chuàng)建,降低出現(xiàn)因某節(jié)點(diǎn)資源利用率高,還排隊(duì)等待在該節(jié)點(diǎn)的情況。
- 擴(kuò)展性好,當(dāng) Kubernetes 集群的資源嚴(yán)重不足而導(dǎo)致 Job 排隊(duì)等待時(shí),可以很容易的添加一個(gè) Kubernetes Node 到集群中,從而實(shí)現(xiàn)擴(kuò)展。
一、安裝jenkins
1、創(chuàng)建一個(gè)kube-ops的 namespace(為了方便管理):
$ kubectl create namespace kube-ops
2、創(chuàng)建pv、pvc或使用storageclass都可以,本實(shí)驗(yàn)使用前者(pvc.yaml):
注:下面使用的是nfs的存儲(chǔ)方式,詳情連接:http://www.itdecent.cn/p/a1089f9bb36e
apiVersion: v1
kind: PersistentVolume
metadata:
name: opspv
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Delete
nfs:
server: 10.8.13.211
path: /data/cmp
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: opspvc
namespace: kube-ops
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 20Gi
3、創(chuàng)建需要用到的 PVC 對(duì)象:
$ kubectl create -f pvc.yaml
4、給jenkins綁定權(quán)限(rbac.yaml),如果對(duì)rbac不熟悉,可以先給定cluster-admin權(quán)限
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: kube-ops
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: jenkins
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["services"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["create","delete","get","list","patch","update","watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create","delete","get","list","patch","update","watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get","list","watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: jenkins
namespace: kube-ops
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
namespace: kube-ops
5、創(chuàng)建 rbac 相關(guān)的資源對(duì)象:
$ kubectl create -f rbac.yaml
serviceaccount "jenkins" created
role.rbac.authorization.k8s.io "jenkins" created
rolebinding.rbac.authorization.k8s.io "jenkins" created
6、新建一個(gè) Deployment:(jenkins.yaml)
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: jenkins
namespace: kube-ops
spec:
template:
metadata:
labels:
app: jenkins
spec:
terminationGracePeriodSeconds: 10
serviceAccount: jenkins
containers:
- name: jenkins
image: jenkins/jenkins:lts
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 50000
name: agent
protocol: TCP
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
failureThreshold: 12
readinessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
failureThreshold: 12
volumeMounts:
- name: jenkinshome
subPath: jenkins
mountPath: /var/jenkins_home
env:
- name: LIMITS_MEMORY
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1Mi
- name: JAVA_OPTS
value: -Xmx$(LIMITS_MEMORY)m -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85 -Duser.timezone=Asia/Shanghai
securityContext:
fsGroup: 1000
volumes:
- name: jenkinshome
persistentVolumeClaim:
claimName: opspvc
---
apiVersion: v1
kind: Service
metadata:
name: jenkins
namespace: kube-ops
labels:
app: jenkins
spec:
selector:
app: jenkins
type: NodePort
ports:
- name: web
port: 8080
targetPort: web
nodePort: 30002
- name: agent
port: 50000
targetPort: agent
這里為了方便,通過 NodePort 的形式來暴露 Jenkins 的 web 服務(wù),固定為30002端口,另外還需要暴露一個(gè) agent 的端口,這個(gè)端口主要是用于 Jenkins 的 master 和 slave 之間通信使用的。
創(chuàng)建 Jenkins 服務(wù):
$ kubectl create -f jenkins.yaml
deployment.extensions "jenkins" created
service "jenkins" created
排錯(cuò):
創(chuàng)建完成后,要去拉取鏡像可能需要等待一會(huì)兒,查看下 Pod 的狀態(tài):
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-7f5494cd44-pqpzs 0/1 Running 0 2m
可以看到該 Pod 處于 Running 狀態(tài),但是 READY 值確為0,然后我們用 describe 命令去查看下該 Pod 的詳細(xì)信息:
$ kubectl describe pod jenkins-7f5494cd44-pqpzs -n kube-ops
...
Normal Created 3m kubelet, node01 Created container
Normal Started 3m kubelet, node01 Started container
Warning Unhealthy 1m (x10 over 2m) kubelet, node01 Liveness probe failed: Get http://10.244.1.165:8080/login: dial tcp 10.244.1.165:8080: getsockopt: connection refused
Warning Unhealthy 1m (x10 over 2m) kubelet, node01 Readiness probe failed: Get http://10.244.1.165:8080/login: dial tcp 10.244.1.165:8080: getsockopt: connection refused
可以看到上面的 Warning 信息,健康檢查沒有通過,具體原因是什么引起的呢?可以通過查看日志進(jìn)一步了解:
$ kubectl logs -f jenkins-7f5494cd44-pqpzs -n kube-ops
touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?
很明顯可以看到上面的錯(cuò)誤信息,意思就是我們沒有權(quán)限在 jenkins 的 home 目錄下面創(chuàng)建文件,這是因?yàn)槟J(rèn)的鏡像使用的是 jenkins 這個(gè)用戶,而我們通過 PVC 掛載到 nfs 服務(wù)器的共享數(shù)據(jù)目錄下面卻是 root 用戶的,所以沒有權(quán)限訪問該目錄,要解決該問題,也很簡單,我只需要在 nfs 共享數(shù)據(jù)目錄下面把我們的目錄權(quán)限重新分配下即可:
$ chown -R 1000 /data/cmp/jenkins
然后重新創(chuàng)建:
$ kubectl delete -f jenkins.yaml
deployment.extensions "jenkins" deleted
service "jenkins" deleted
$ kubectl create -f jenkins.yaml
deployment.extensions "jenkins" created
service "jenkins" created
現(xiàn)在再去查看新生成的 Pod 已經(jīng)沒有錯(cuò)誤信息了:
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-7f5494cd44-smn2r 1/1 Running 0 25s
等到服務(wù)啟動(dòng)成功后,可以根據(jù)Node節(jié)點(diǎn)的 IP:30002 端口就可以訪問 jenkins 服務(wù)了,可以根據(jù)提示信息進(jìn)行安裝配置即可: 
初始化的密碼可以在 jenkins 的容器的日志中進(jìn)行查看,也可以直接在 nfs 的共享數(shù)據(jù)目錄中查看:
$ cat /data/cmp/jenkins/secrets/initAdminPassword
然后選擇安裝推薦的插件即可。 

二、配置jenkins動(dòng)態(tài)slave
第1步. 需要安裝kubernetes plugin, 點(diǎn)擊 Manage Jenkins -> Manage Plugins -> Available -> Kubernetes plugin 勾選安裝即可。(如果搜索沒有Kubernetes plugin,即選擇Kubernetes),然后重啟jenkins,使之生效。

第2步. 安裝完畢后,點(diǎn)擊 Manage Jenkins —> Configure System —> (拖到最下方)Add a new cloud —> 選擇 Kubernetes,然后填寫 Kubernetes 和 Jenkins 配置信息。

注意 namespace,我們這里填 kube-ops,然后點(diǎn)擊Test Connection,如果出現(xiàn) Connection test successful 的提示信息證明 Jenkins 已經(jīng)可以和 Kubernetes 系統(tǒng)正常通信了,然后下方的 Jenkins URL 地址:http://jenkins.kube-ops.svc.cluster.local:8080,這里的格式為:服務(wù)名.namespace.svc.cluster.local:8080,根據(jù)上面創(chuàng)建的jenkins 的服務(wù)名填寫,我這里是之前創(chuàng)建的名為jenkins2,如果是用上面創(chuàng)建的就應(yīng)該是jenkins
另外需要注意,如果這里 Test Connection 失敗的話,很有可能是權(quán)限問題,這里就需要把 jenkins 的 serviceAccount 對(duì)應(yīng)的 secret 添加到這里的 Credentials 里面。
第3步. 配置 Pod Template,其實(shí)就是配置 Jenkins Slave 運(yùn)行的 Pod 模板,命名空間同樣使用 kube-ops,Labels 這里也非常重要,對(duì)于后面執(zhí)行 Job 的時(shí)候需要用到該值,這里使用的是 cnych/jenkins:jnlp 這個(gè)鏡像,這個(gè)鏡像是在官方的 jnlp 鏡像基礎(chǔ)上定制的,加入了 kubectl 等一些實(shí)用的工具。

需要在下面掛載兩個(gè)主機(jī)目錄,一個(gè)是/var/run/docker.sock,該文件是用于 Pod 中的容器能夠共享宿主機(jī)的 Docker,使用 docker in docker 的方式,Docker 二進(jìn)制文件已經(jīng)打包到上面的鏡像中了,另外一個(gè)目錄下/root/.kube目錄,將這個(gè)目錄掛載到容器的/root/.kube目錄下面這是為了能夠在 Pod 的容器中能夠使用 kubectl 工具來訪問 Kubernetes 集群,方便后面在 Slave Pod 部署 Kubernetes 應(yīng)用。

另外還有幾個(gè)參數(shù)需要注意,如下圖中的Time in minutes to retain slave when idle,這個(gè)參數(shù)表示的意思是當(dāng)處于空閑狀態(tài)的時(shí)候保留 Slave Pod 多長時(shí)間,這個(gè)參數(shù)最好保存默認(rèn)就行了,如果你設(shè)置過大的話,Job 任務(wù)執(zhí)行完成后,對(duì)應(yīng)的 Slave Pod 就不會(huì)立即被銷毀刪除。(如沒有,請(qǐng)忽略))


還有一個(gè)問題在配置完成后發(fā)現(xiàn)啟動(dòng) Jenkins Slave Pod 的時(shí)候,出現(xiàn) Slave Pod 連接不上,然后嘗試100次連接之后銷毀 Pod,然后會(huì)再創(chuàng)建一個(gè) Slave Pod 繼續(xù)嘗試連接,無限循環(huán),類似于下面的信息:(如沒有,請(qǐng)忽略)


到這里 Kubernetes Plugin 插件就算配置完成了。
三、測試
Kubernetes 插件的配置工作完成了,接下來添加一個(gè) Job 任務(wù),看是否能夠在 Slave Pod 中執(zhí)行,任務(wù)執(zhí)行完成后看 Pod 是否會(huì)被銷毀。
在 Jenkins 首頁點(diǎn)擊create new jobs,創(chuàng)建一個(gè)測試的任務(wù),輸入任務(wù)名稱,然后我們選擇 Freestyle project 類型的任務(wù):
注意在下面的 Label Expression 這里要填入hwzx-cmp,就是前面我們配置的 Slave Pod 中的 Label,這兩個(gè)地方必須保持一致

然后往下拉,在 Build 區(qū)域選擇Execute shell

然后輸入測試命令
echo "測試 Kubernetes 動(dòng)態(tài)生成 jenkins slave"
echo "==============docker in docker==========="
docker info
echo "=============kubectl============="
kubectl get pods

現(xiàn)在直接在頁面點(diǎn)擊做成的 Build now 觸發(fā)構(gòu)建即可,然后觀察 Kubernetes 集群中 Pod 的變化
$ kubectl get pod -n kube-ops -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
jenkins-7cc9df89dd-bwqwd 1/1 Running 0 59m 172.20.4.14 10.8.13.85 <none> <none>
jnlp-8s1p4 0/1 ContainerCreating 0 53s <none> 10.8.13.84 <none> <none>
可以看到在擊立刻構(gòu)建的時(shí)候可以看到一個(gè)新的 Pod:jnlp-8s1p4 被創(chuàng)建了,這就是我們的 Jenkins Slave。任務(wù)執(zhí)行完成后我們可以看到任務(wù)信息,比如這里是 花費(fèi)了 5.2s 時(shí)間在jnlp-8s1p4 這個(gè) Slave上面 
同樣也可以查看到對(duì)應(yīng)的控制臺(tái)信息:


到這里證明任務(wù)已經(jīng)構(gòu)建完成,然后這個(gè)時(shí)候再去集群查看 Pod 列表,發(fā)現(xiàn) kube-ops 這個(gè) namespace 下面已經(jīng)沒有之前的 Slave 這個(gè) Pod 了。
$ kubectl get pod -n kube-ops -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
jenkins-7cc9df89dd-bwqwd 1/1 Running 0 59m 172.20.4.14 10.8.13.85 <none> <none>
到這里就完成了使用 Kubernetes 動(dòng)態(tài)生成 Jenkins Slave 的方法。