# 基于Jenkins 的CI/CD(一)
## 介紹
本文將討論和探索兩個令人驚奇和相當有趣的技術。一個是Jenkins,一個流行的持續集成/發布的工具,另一個是Kubernetes,一個流行的容器編排引擎。
持續構建與發布是我們日常工作中必不可少的一個步驟,目前大多公司都采用 Jenkins 集群來搭建符合需求的 CI/CD 流程,然而傳統的 Jenkins Slave 一主多從方式會存在一些痛點,比如:主 Master 發生單點故障時,整個流程都不可用了;每個 Slave 的配置環境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導致管理起來非常不方便,維護起來也是比較費勁;資源分配不均衡,有的 Slave 要運行的 job 出現排隊等待,而有的 Slave 處于空閑狀態;最后資源有浪費,每臺 Slave 可能是實體機或者 VM,當 Slave 處于空閑狀態時,也不會完全釋放掉資源。
提到基于Kubernete的CI/CD,可以使用的工具有很多,比如Jenkins、Gitlab CI已經新興的drone之類的,我們這里會使用大家最為熟悉的Jenins來做CI/CD的工具。
## 優點
Jenkins 安裝完成了,接下來我們不用急著就去使用,我們要了解下在 Kubernetes 環境下面使用 Jenkins 有什么好處。
我們知道持續構建與發布是我們日常工作中必不可少的一個步驟,目前大多公司都采用 Jenkins 集群來搭建符合需求的 CI/CD 流程,然而傳統的 Jenkins Slave 一主多從方式會存在一些痛點,比如:
* 主 Master 發生單點故障時,整個流程都不可用了
* 每個 Slave 的配置環境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導致管理起來非常不方便,維護起來也是比較費勁
* 資源分配不均衡,有的 Slave 要運行的 job 出現排隊等待,而有的 Slave 處于空閑狀態
* 資源有浪費,每臺 Slave 可能是物理機或者虛擬機,當 Slave 處于空閑狀態時,也不會完全釋放掉資源。
正因為上面的這些種種痛點,我們渴望一種更高效更可靠的方式來完成這個 CI/CD 流程,而 Docker 虛擬化容器技術能很好的解決這個痛點,又特別是在 Kubernetes 集群環境下面能夠更好來解決上面的問題,下圖是基于 Kubernetes 搭建 Jenkins 集群的簡單示意圖

從圖上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式運行在 Kubernetes 集群的 Node 上,Master 運行在其中一個節點,并且將其配置數據存儲到一個 Volume 上去,Slave 運行在各個節點上,并且它不是一直處于運行狀態,它會按照需求動態的創建并自動刪除。
這種方式的工作流程大致為:當 Jenkins Master 接受到 Build 請求時,會根據配置的 Label 動態創建一個運行在 Pod 中的 Jenkins Slave 并注冊到 Master 上,當運行完 Job 后,這個 Slave 會被注銷并且這個 Pod 也會自動刪除,恢復到最初狀態。
那么我們使用這種方式帶來了哪些好處呢?
* 服務高可用,當 Jenkins Master 出現故障時,Kubernetes 會自動創建一個新的 Jenkins Master 容器,并且將 Volume 分配給新創建的容器,保證數據不丟失,從而達到集群服務高可用。
* 動態伸縮,合理使用資源,每次運行 Job 時,會自動創建一個 Jenkins Slave,Job 完成后,Slave 自動注銷并刪除容器,資源自動釋放,而且 Kubernetes 會根據每個資源的使用情況,動態分配 Slave 到空閑的節點上創建,降低出現因某節點資源利用率高,還排隊等待在該節點的情況。
* 擴展性好,當 Kubernetes 集群的資源嚴重不足而導致 Job 排隊等待時,可以很容易的添加一個 Kubernetes Node 到集群中,從而實現擴展。
是不是以前我們面臨的種種問題在 Kubernetes 集群環境下面是不是都沒有了啊?看上去非常完美。
### 安裝
聽我們課程的大部分同學應該都或多或少的聽說過Jenkins,我們這里就不再去詳細講述什么是 Jenkins 了,直接進入正題,后面我們會單獨的關于 Jenkins 的學習課程,想更加深入學習的同學也可以關注下。既然要基于Kubernetes來做CI/CD,當然我們這里需要將 Jenkins 安裝到 Kubernetes 集群當中,新建一個 Deployment:(jenkins-deployment.yaml)
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: jenkins2
namespace: kube-ops
spec:
template:
metadata:
labels:
app: jenkins2
spec:
terminationGracePeriodSeconds: 10
serviceAccountName: jenkins2
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: 2000m
memory: 4Gi
requests:
cpu: 1000m
memory: 2Gi
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: jenkins2
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: jenkins2
namespace: kube-ops
labels:
app: jenkins2
spec:
selector:
app: jenkins2
ports:
- name: web
port: 8080
targetPort: web
- name: agent
port: 50000
targetPort: agent
為了方便演示,我們把本節課所有的對象資源都放置在一個名為 kube-ops 的 namespace 下面,所以我們需要添加創建一個 namespace:
kubectl create namespace kube-ops
我們這里使用一個名為 jenkins/jenkins:lts 的鏡像,這是 jenkins 官方的 Docker 鏡像,然后也有一些環境變量,當然我們也可以根據自己的需求來定制一個鏡像,比如我們可以將一些插件打包在自定義的鏡像當中,可以參考文檔:https://github.com/jenkinsci/docker,我們這里使用默認的官方鏡像就行,另外一個還需要注意的是我們將容器的 /var/jenkins_home 目錄掛載到了一個名為 opspvc 的 PVC 對象上面,所以我們同樣還得提前創建一個對應的 PVC 對象,當然我們也可以使用我們前面的 StorageClass 對象來自動創建:(jenkins-pvc.yaml)
apiVersion: v1
kind: PersistentVolume
metadata:
name: opspv
spec:
capacity:
storage: 200Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Delete
nfs:
server: 10.34.11.12
path: /opt/nfs
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: opspvc
namespace: kube-ops
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Gi
創建需要用到的 PVC 對象
$ kubectl create -f jenkins-pvc.yaml
另外我們這里還需要使用到一個擁有相關權限的 serviceAccount:jenkins2,我們這里只是給 jenkins 賦予了一些必要的權限,當然如果你對 serviceAccount 的權限不是很熟悉的話,我們給這個 sa 綁定一個 cluster-admin 的集群角色權限也是可以的,當然這樣具有一定的安全風險:(jenkins-rbac.yaml)
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins2
namespace: kube-ops
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: jenkins2
namespace: kube-ops
rules:
- 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: RoleBinding
metadata:
name: jenkins2
namespace: kube-ops
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins2
subjects:
- kind: ServiceAccount
name: jenkins2
namespace: kube-ops
創建 rbac 相關的資源對象:
$ kubectl create -f jenkins-rbac.yaml
最后為了方便我們測試,我們這里通過 ingress的形式來訪問Jenkins 的 web 服務,Jenkins 服務端口為8080,50000 端口為agent,這個端口主要是用于 Jenkins 的 master 和 slave 之間通信使用的。(jenkins-ingress.yaml)
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: jenkins-ingress
namespace: kube-ops
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: jenkins-k8s.jiedai361.com
http:
paths:
- backend:
serviceName: jenkins2
servicePort: 8080
一切準備的資源準備好過后,我們直接創建 Jenkins 服務:
kubectl create -f jenkins-deployment.yaml
最后創建ingress 路由服務
kubectl apply -f jenkins-ingress.yaml
創建完成后,要去拉取鏡像可能需要等待一會兒,然后我們查看下 Pod 的狀態:
# kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins2-58df5d8fdc-w9xqh 1/1 Running 0 7d
等到服務啟動成功后,我們就可以根據ingress 服務域名(jenkins-k8s.jiedai361.com)就可以訪問 jenkins 服務了,可以根據提示信息進行安裝配置即可:

初始化的密碼我們可以在 jenkins 的容器的日志中進行查看,也可以直接在 nfs 的共享數據目錄中查看
cat /opt/nfs/jenkins2/secret/initAdminPassword
然后選擇安裝推薦的插件即可。

安裝完成后添加管理員帳號即可進入到 jenkins 主界面: jenkins home

## 配置
接下來我們就需要來配置 Jenkins,讓他能夠動態的生成 Slave 的 Pod
jenkins依賴插件清單
- kubernetes
- managed scripts
**第1步**. 我們需要安裝kubernetes plugin, 點擊 Manage Jenkins -> Manage Plugins -> Available -> Kubernetes plugin 勾選安裝即可。 kubernetes plugin

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

注意 namespace,我們這里填 kube-ops,然后點擊Test Connection,如果出現 Connection test successful 的提示信息證明 Jenkins 已經可以和 Kubernetes 系統正常通信了,然后下方的 Jenkins URL 地址:http://jenkins2.kube-ops.svc.cluster.local:8080,這里的格式為:服務名.namespace.svc.cluster.local:8080,根據上面創建的jenkins 的服務名填寫,我這里是之前創建的名為jenkins,如果是用上面我們創建的就應該是jenkins2
**第3步.** 配置 Pod Template,其實就是配置 Jenkins Slave 運行的 Pod 模板,命名空間我們同樣是用 kube-ops,Labels 這里也非常重要,對于后面執行 Job 的時候需要用到該值,然后我們這里使用的是 cnych/jenkins:jnlp 這個鏡像,這個鏡像是在官方的 jnlp 鏡像基礎上定制的,加入了 kubectl 等一些實用的工具。

另外需要注意我們這里需要在下面掛載一個主機目錄,一個是 /var/run/docker.sock,該文件是用于 Pod 中的容器能夠共享宿主機的 Docker,這就是大家說的 docker in docker 的方式,Docker 二進制文件我們已經打包到上面的鏡像中了。如果在slave agent中想要訪問kubernetes 集群中其他資源,我們還需要綁定之前創建的Service Account 賬號:jenkins2

另外還有幾個參數需要注意,如下圖中的Time in minutes to retain slave when idle,這個參數表示的意思是當處于空閑狀態的時候保留 Slave Pod 多長時間,這個參數最好我們保存默認就行了,如果你設置過大的話,Job 任務執行完成后,對應的 Slave Pod 就不會立即被銷毀刪除。
到這里我們的 Kubernetes Plugin 插件就算配置完成了。
## 測試
Kubernetes 插件的配置工作完成了,接下來我們就來添加一個 Job 任務,看是否能夠在 Slave Pod 中執行,任務執行完成后看 Pod 是否會被銷毀
在 Jenkins 首頁點擊create new jobs,創建一個測試的任務,輸入任務名稱,然后我們選擇 Freestyle project 類型的任務: jenkins demo

注意在下面的 Label Expression 這里要填入haimaxy-jnlp,就是前面我們配置的 Slave Pod 中的 Label,這兩個地方必須保持一致

然后往下拉,在 Build 區域選擇Execute shell

然后輸入我們測試命令
echo "測試 Kubernetes 動態生成 jenkins slave"
echo "==============docker in docker==========="
docker info
echo "=============kubectl============="
kubectl get pods -n kube-system
最后點擊保存

現在我們直接在頁面點擊做成的 Build now 觸發構建即可,然后觀察 Kubernetes 集群中 Pod 的變化
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins2-7c85b6f4bd-rfqgv 1/1 Running 3 1d
jnlp-hfmvd 0/1 ContainerCreating 0 7s
同樣也可以查看到對應的控制臺信息:

到這里證明我們的任務已經構建完成,然后這個時候我們再去集群查看我們的 Pod 列表,發現 kube-ops 這個 namespace 下面已經沒有之前的 Slave 這個 Pod 了。
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins2-7c85b6f4bd-rfqgv 1/1 Running 3 1d
到這里我們就完成了使用 Kubernetes 動態生成 Jenkins Slave 的方法。下節課我們來給大家介紹下怎么在 Jenkins 中來發布我們的 Kubernetes 應用
- 第一章 kubernetes 功能介紹
- 第二章 在CentOS上部署kubernetes1.7.6集群
- 第三章 創建TLS證書和秘鑰
- 第四章 安裝kubectl命令行工具
- 第五章 創建kubeconfig 文件
- 第六章 etcd 集群部署
- 第七章 部署k8s-master-v1.7.6節點
- 第八章 部署k8s-v1.7.6 node 節點
- 第九章 kubectl 操作示例
- 第十章 在kubernetes 部署第一個應用
- 第十一章 kubernetes之pod 調度
- 第十二章 K8S服務組件之kube-dns&Dashboard
- 第十三章 Kubernetes中的角色訪問控制機制(RBAC)支持
- 第十四章 部署nginx ingress
- 第十五章 使用Prometheus監控Kubernetes集群和應用
- 第十六章 使用helm 應用部署工具
- 第十七章 kubernetes 從1.7 到1.8升級記錄
- 第十八章 在kubernetes 使用ceph
- 第十九章 基于 Jenkins 的 CI/CD(一)
- 第二十章 基于jenkins的CI/CD(二)
- 第二十一章 基于prometheus自定指標HPA彈性伸縮