Kubernetes DevOps: Jenkins
提到基於 Kubernete 的CI/CD,可以使用的工具有很多,比如 Jenkins、Gitlab CI 以及新興的 drone 之類的,我們這裡會使用大家最為熟悉的 Jenkins 來做 CI/CD 的工具。
安裝
既然要基於 Kubernetes 來做 CI/CD,我們這裡最好還是將 Jenkins 安裝到 Kubernetes 叢集當中,安裝的方式也很多,我們這裡仍然還是使用手動的方式,這樣可以瞭解更多細節,對應的資源清單檔案如下所示:
apiVersion: v1 kind: PersistentVolume metadata: name: jenkins-pv spec: storageClassName: local # Local PV capacity: storage: 2Gi volumeMode: Filesystem accessModes: - ReadWriteOnce local: path: /data/k8s/jenkins nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - ydzs-node6 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: jenkins-pvc namespace: kube-ops spec: storageClassName: local accessModes: - ReadWriteOnce resources: requests: storage: 2Gi --- 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", "ingresses"] 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", "events"] 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 --- apiVersion: v1 kind: ConfigMap metadata: name: jenkins-mirror-conf namespace: kube-ops data: nginx.conf: | user nginx; worker_processes 3; error_log /dev/stderr; events { worker_connections 10240; } http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" $request_time'; access_log /dev/stdout main; server { listen 80; server_name mirrors.jenkins-ci.org; location / { proxy_redirect off; proxy_pass https://mirrors.tuna.tsinghua.edu.cn/jenkins/; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Accept-Encoding ""; proxy_set_header Accept-Language "zh-CN"; } index index.html index.htm index.php; location ~ /\. { deny all; } } } --- apiVersion: apps/v1 kind: Deployment metadata: name: jenkins namespace: kube-ops spec: selector: matchLabels: app: jenkins template: metadata: labels: app: jenkins spec: serviceAccount: jenkins hostAliases: - ip: "127.0.0.1" hostnames: - "mirrors.jenkins-ci.org" initContainers: - name: fix-permissions image: busybox command: ["sh", "-c", "chown -R 1000:1000 /var/jenkins_home"] securityContext: privileged: true volumeMounts: - name: jenkinshome mountPath: /var/jenkins_home containers: - name: mirror image: nginx:1.7.9 ports: - containerPort: 80 volumeMounts: - mountPath: /etc/nginx readOnly: true name: nginx-conf - name: jenkins image: jenkins/jenkins:lts imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: web protocol: TCP - containerPort: 50000 name: agent protocol: TCP resources: limits: cpu: 1500m memory: 2048Mi requests: cpu: 1500m memory: 2048Mi readinessProbe: httpGet: path: /login port: 8080 initialDelaySeconds: 60 timeoutSeconds: 5 failureThreshold: 12 volumeMounts: - name: jenkinshome mountPath: /var/jenkins_home volumes: - name: jenkinshome persistentVolumeClaim: claimName: jenkins-pvc - name: nginx-conf configMap: name: jenkins-mirror-conf items: - key: nginx.conf path: nginx.conf --- apiVersion: v1 kind: Service metadata: name: jenkins namespace: kube-ops labels: app: jenkins spec: selector: app: jenkins ports: - name: web port: 8080 targetPort: web - name: agent port: 50000 targetPort: agent # --- # apiVersion: extensions/v1beta1 # kind: Ingress # metadata: # name: jenkins # namespace: kube-ops # spec: # rules: # - host: jenkins.k8s.local # http: # paths: # - backend: # serviceName: jenkins # servicePort: web --- apiVersion: traefik.containo.us/v1alpha1 kind: IngressRoute metadata: name: jenkins namespace: kube-ops spec: entryPoints: - web routes: - kind: Rule match: Host(`jenkins.k8s.local`) services: - name: jenkins port: 8080
我們這裡使用一個名為 jenkins/jenkins:lts 的映象,這是 jenkins 官方的 Docker 映象,然後也有一些環境變數,當然我們也可以根據自己的需求來定製一個映象,比如我們可以將一些外掛打包在自定義的映象當中,可以參考文件:https://github.com/jenkinsci/docker,我們這裡使用預設的官方映象就行,另外一個還需要注意的資料的持久化,將容器的 /var/jenkins_home 目錄持久化即可,同樣為了效能考慮,我們這裡使用 Local PV,將 Pod 排程到固定的節點上。
由於我們這裡使用的映象內部執行的使用者 uid=1000,所以我們這裡掛載出來後會出現許可權問題,為解決這個問題,我們同樣還是用一個簡單的 initContainer 來修改下我們掛載的資料目錄。
另外我們這裡還需要使用到一個擁有相關許可權的 serviceAccount:jenkins,我們這裡只是給 jenkins 賦予了一些必要的許可權,當然如果你對 serviceAccount 的許可權不是很熟悉的話,我們給這個 sa 繫結一個 cluster-admin 的叢集角色許可權也是可以的,當然這樣具有一定的安全風險。
除此之外,這裡我們還添加了一個額外的名為 mirror 的容器,新增這個容器的目的是使用一個 nginx 容器來反向代理 Jenkins 外掛的官方源到清華大學的源上面,因為官方源實在是太慢了,我們這裡將官方的映象地址 mirrors.jenkins-ci.org 通過 hostAlias 對映到了 127.0.0.1 這個地址上,而這個地址恰好就是 mirror 這個 nginx 容器,我們通過一個 ConfigMap 來配置 Nginx,將 mirros.jenkins-ci.org 反向代理到了 proxy_pass
最後就是通過 IngressRoute 來暴露我們的服務,這個比較簡單。
我們直接來建立 jenkins 的資源清單即可:
$ kubectl apply -f jenkins.yaml
$ kubectl get pods -n kube-ops -l app=jenkins
NAME READY STATUS RESTARTS AGE
jenkins-5b957d4b8f-t22gp 2/2 Running 0 18m
$ kubectl logs -f jenkins-5b957d4b8f-t22gp jenkins -n kube-ops
......
2019-12-16 13:26:03.756+0000 [id=39] INFO hudson.model.AsyncPeriodicWork$1#run: Finished Download metadata. 28,073 ms
2019-12-16 13:26:03.760+0000 [id=26] INFO jenkins.InitReactorRunner$1#onAttained: Completed initialization
2019-12-16 13:26:03.863+0000 [id=19] INFO hudson.WebAppMain$3#run: Jenkins is fully up and running
看到上面的 run: Jenkins is fully up and running 資訊就證明我們的 Jenkins 應用以前啟動起來了。
然後我們可以通過 IngressRoute 中定義的域名 jenkins.k8s.local(需要做 DNS 解析或者在本地 /etc/hosts 中新增對映)來訪問 jenkins 服務:
然後可以執行下面的命令獲取解鎖的管理員密碼:
$ kubectl exec -it jenkins-5b957d4b8f-t22gp -c jenkins -n kube-ops -- cat /var/jenkins_home/secrets/initialAdminPassword
35b083de1d25409eaef57255e0da481a # jenkins啟動日誌裡面也有
然後選擇安裝推薦的外掛即可,由於我們已經做了外掛的反向代理了,所以理論上安裝速度會比較快
安裝完成後新增管理員帳號即可進入到 jenkins 主介面:
架構
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,讓他能夠動態的生成 Slave 的 Pod。
第1步. 我們需要安裝 kubernetes 外掛, 點選 Manage Jenkins -> Manage Plugins -> Available -> Kubernetes 勾選安裝即可。
第2步. 安裝完畢後,點選 Manage Jenkins —> Configure System —> (拖到最下方),如果有 Add a new cloud —> 選擇 Kubernetes,然後填寫 Kubernetes 和 Jenkins 配置資訊即可,但是最新版本的 Kubernetes 外掛將配置單獨放置到了一個頁面中:
這個時候需要點選 a separate configuration page 這個連結,跳轉到 Configure Cloud 頁面:
在該頁面我們可以點選 Add a new cloud -> 選擇 Kubernetes,然後填寫 Kubernetes 和 Jenkins 配置資訊:
注意 namespace,我們這裡填 kube-ops,然後點選 Test Connection,如果出現 Connection test successful 的提示資訊證明 Jenkins 已經可以和 Kubernetes 系統正常通訊了,然後下方的 Jenkins URL 地址:http://jenkins.kube-ops.svc.cluster.local:8080,這裡的格式為:服務名.namespace.svc.cluster.local:8080,根據上面建立的 jenkins 的服務名填寫。
第3步. 配置 Pod Template,其實就是配置 Jenkins Slave 執行的 Pod 模板,名稱空間我們同樣是用 kube-ops,Labels 這裡也非常重要,對於後面執行 Job 的時候需要用到該值,然後我們這裡使用的是 cnych/jenkins:jnlp6 這個映象,這個映象是在官方的 jnlp 映象基礎上定製的,加入了 docker、kubectl 等一些實用的工具。
容器的名稱必須是 jnlp,這是預設拉起的容器,另外需要將 Command to run 和 Arguments to pass to the command 的值都刪除掉,否則會失敗。
然後我們這裡需要在下面掛載兩個主機目錄,一個是 /var/run/docker.sock,該檔案是用於 Pod 中的容器能夠共享宿主機的 Docker,這就是大家說的 docker in docker 的方式,Docker 二進位制檔案已經打包到上面的映象中了,另外一個目錄下 /root/.kube 目錄,我們將這個目錄掛載到容器的 /root/.kube 目錄下面這是為了讓我們能夠在 Pod 的容器中能夠使用 kubectl 工具來訪問我們的 Kubernetes 叢集,方便我們後面在 Slave Pod 部署 Kubernetes 應用。
另外如果在配置了後執行 Slave Pod 的時候出現了許可權問題,這是因為 Jenkins Slave Pod 中沒有配置許可權,所以需要配置上 ServiceAccount,在 Slave Pod 配置的地方點選下面的高階,新增上對應的 ServiceAccount 即可:
到這裡我們的 Kubernetes 外掛就算配置完成了。
測試
Kubernetes 外掛的配置工作完成了,接下來我們就來新增一個 Job 任務,看是否能夠在 Slave Pod 中執行,任務執行完成後看 Pod 是否會被銷燬。
在 Jenkins 首頁點選 create new jobs,建立一個測試的任務,輸入任務名稱,然後我們選擇 Freestyle project 型別的任務,注意在下面的 Label Expression 這裡要填入 ydzs-jnlp,就是前面我們配置的 Slave Pod 中的 Label,這兩個地方必須保持一致:
然後往下拉,在 Build 區域選擇Execute shell
然後輸入我們測試命令
echo "測試 Kubernetes 動態生成 jenkins slave"
echo "==============docker in docker==========="
docker info
echo "=============kubectl============="
kubectl get pods
最後點選儲存
現在我們直接在頁面點選左側的 Build now 觸發構建即可,然後觀察 Kubernetes 叢集中 Pod 的變化:
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-68ccff445c-dk24f 1/1 Running 0 12h
jnlp-1g893 0/1 ContainerCreating 0 83s
我們可以看到在我們點選立刻構建的時候可以看到一個新的 Pod:jnlp-1g893 被建立了,這就是我們的 Jenkins Slave。任務執行完成後我們可以看到任務資訊,比如我們這裡是 花費了 21s 時間在 jnlp-1g893 這個 Slave上面
到這裡證明我們的任務已經構建完成,然後這個時候我們再去叢集檢視我們的 Pod 列表,發現 kube-ops 這個 namespace 下面已經沒有之前的 Slave 這個 Pod 了。
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-68ccff445c-dk24f 1/1 Running 0 12h
到這裡我們就完成了使用 Kubernetes 動態生成 Jenkins Slave 的方法。