Jenkins 基于 Kubernetes 的動態和靜態節點

提到基于 Kubernetes 的 CI/CD,可以使用的工具有很多,比如 Jenkins、Gitlab CI、Drone 之類的,我們這里會使用大家最為熟悉的 Jenkins 來做 CI/CD 的工具。
安裝
既然要基于 Kubernetes 來做 CI/CD,我們這里最好還是將 Jenkins 安裝到 Kubernetes 集群當中,安裝的方式也很多,我們這里仍然還是使用手動的方式,這樣可以了解更多細節,對應的資源清單文件如下所示:
# jenkins.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkins-local
labels:
app: jenkins
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 5Gi
storageClassName: local-storage
local:
path: /data/k8s/jenkins
persistentVolumeReclaimPolicy: Retain
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-pvc
namespace: kube-ops
spec:
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: kube-ops
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
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/v1
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: apps/v1
kind: Deployment
metadata:
name: jenkins
namespace: kube-ops
spec:
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
serviceAccount: jenkins
initContainers:
- name: fix-permissions
image: busybox:1.35.0
command: ["sh", "-c", "chown -R 1000:1000 /var/jenkins_home"]
securityContext:
privileged: true
volumeMounts:
- name: jenkinshome
mountPath: /var/jenkins_home
containers:
- name: jenkins
image: jenkins/jenkins:lts-jdk11
imagePullPolicy: IfNotPresent
env:
- name: JAVA_OPTS
value: -Dhudson.model.DownloadService.noSignatureCheck=true
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 50000
name: agent
protocol: TCP
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
---
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: networking.k8s.io/v1
kind: Ingress
metadata:
name: jenkins
namespace: kube-ops
spec:
ingressClassName: nginx
rules:
- host: jenkins.k8s.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jenkins
port:
name: web我們這里使用 jenkins/jenkins:lts-jdk11 鏡像,這是 jenkins 官方的 Docker 鏡像,然后也有一些環境變量,當然我們也可以根據自己的需求來定制一個鏡像,比如我們可以將一些插件打包在自定義的鏡像當中,可以參考文檔:https://github.com/jenkinsci/docker,我們這里使用默認的官方鏡像就行,另外一個還需要注意的數據的持久化,將容器的 /var/jenkins_home 目錄持久化即可,我們這里使用的是 Local PV 的方式。
由于我們這里使用的鏡像內部運行的用戶 uid=1000,所以我們這里掛載出來后會出現權限問題,為解決這個問題,我們同樣還是用一個簡單的 initContainer 來修改下我們掛載的數據目錄。
另外由于 jenkens 會對 update-center.json 做簽名校驗安全檢查,這里我們需要先提前關閉,否則下面更改插件源可能會失敗,通過配置環境變量 JAVA_OPTS=-Dhudson.model.DownloadService.noSignatureCheck=true 即可。
另外我們這里還需要使用到一個擁有相關權限的 serviceAccount:jenkins,我們這里只是給 jenkins 賦予了一些必要的權限,當然如果你對 serviceAccount 的權限不是很熟悉的話,我們給這個 sa 綁定一個 cluster-admin 的集群角色權限也是可以的,當然這樣具有一定的安全風險。最后就是通過 Ingress 來暴露我們的服務,這個比較簡單。
我們直接來創建 jenkins 的資源清單即可:
$ kubectl apply -f jenkins.yaml
$ kubectl get pods -n kube-ops -l app=jenkins
NAME READY STATUS RESTARTS AGE
jenkins-55c4676f4d-fhmw2 1/1 Running 0 3m5s
$ kubectl logs -f jenkins-55c4676f4d-fhmw2 -n kube-ops
Running from: /usr/share/jenkins/jenkins.war
webroot: /var/jenkins_home/war
# ......
2023-09-07 06:56:26.123+0000 [id=33] INFO jenkins.install.SetupWizard#init:
*************************************************************
*************************************************************
*************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
278e9dcdcab04d11ae671f7f81f517ba
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
*************************************************************
*************************************************************
*************************************************************
2023-09-07 06:56:38.572+0000 [id=29] INFO jenkins.InitReactorRunner$1#onAttained: Completed initialization
2023-09-07 06:56:38.583+0000 [id=23] INFO hudson.lifecycle.Lifecycle#onReady: Jenkins is fully up and running
2023-09-07 06:57:02.555+0000 [id=49] INFO h.m.DownloadService$Downloadable#load: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller
2023-09-07 06:57:02.556+0000 [id=49] INFO hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1看到上面的 run: Jenkins is fully up and running 信息就證明我們的 Jenkins 應用以前啟動起來了。
然后我們可以通過 Ingress 中定義的域名 jenkins.k8s.local(需要做 DNS 解析或者在本地 /etc/hosts 中添加映射)來訪問 jenkins 服務:

jenkins unlock
然后可以執行下面的命令獲取解鎖的管理員密碼:
$ kubectl exec -it jenkins-55c4676f4d-fhmw2 -n kube-ops -- cat /var/jenkins_home/secrets/initialAdminPassword
278e9dcdcab04d11ae671f7f81f517ba # jenkins啟動日志里面也有然后跳過插件安裝,選擇默認安裝插件過程會非常慢(也可以選擇安裝推薦的插件),點擊右上角關閉選擇插件。

ignore plugin install
跳過后會直接進入 Jenkins 就緒頁面,直接點擊開始使用即可:

jenkins ready
然后就可以進入 Jenkins 主頁了。

Jenkins Home
首先安裝中文插件(如果想要中文界面的話),搜索 Localization: Chinese:

Localization: Chinese
安裝重啟完成后,會自動跳轉到登錄頁面:

Jenkins Login
這里還是使用 admin 和前面的初始密碼進行登錄。然后可以進入用戶管理頁面 http://jenkins.k8s.local/user/admin/configure 修改用戶密碼:

修改密碼
然后就可以使用新的密碼登錄了。
接下來我們可以安裝其他需要的插件,比如 Pipeline 插件。Pipeline 是 Jenkins 的一個核心插件,它定義了一套 DSL 語言,可以用來編寫 Pipeline 腳本,這個腳本可以實現從代碼構建到部署的整個流程。在使用 Pipeline 類型的項目時,需要提前安裝 Jenkins 的 Pipeline 插件。

pipeline plugin
安裝好插件后新建一個 Pipeline 類型的作業:

新建作業
自由風格項目和 Pipeline 類型的項目區別是,構建部分的操作都是在頁面上面完成的。Pipeline 的構建任務描述都是通過代碼的方式。

hello
保存后我們可以點擊立即構建執行這個任務,也可以查看這個任務的執行結果輸出:

執行結果
架構
Jenkins 安裝完成了,接下來我們不用急著就去使用,我們要了解下在 Kubernetes 環境下面使用 Jenkins 有什么好處。
我們知道持續構建與發布是我們日常工作中必不可少的一個步驟,目前大多公司都采用 Jenkins 集群來搭建符合需求的 CI/CD 流程,然而傳統的 Jenkins Slave 一主多從方式會存在一些痛點,比如:
- 主 Master 發生單點故障時,整個流程都不可用了
- 每個 Slave 的配置環境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導致管理起來非常不方便,維護起來也是比較費勁
- 資源分配不均衡,有的 Slave 要運行的 job 出現排隊等待,而有的 Slave 處于空閑狀態
- 資源有浪費,每臺 Slave 可能是物理機或者虛擬機,當 Slave 處于空閑狀態時,也不會完全釋放掉資源。
正因為上面的這些種種痛點,我們渴望一種更高效更可靠的方式來完成這個 CI/CD 流程,而 Docker 虛擬化容器技術能很好的解決這個痛點,又特別是在 Kubernetes 集群環境下面能夠更好來解決上面的問題,下圖是基于 Kubernetes 搭建 Jenkins 集群的簡單示意圖:

k8s jenkins slave
從圖上可以看到 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 集群環境下面是不是都沒有了啊?看上去非常完美。
Agent 節點
雖然我們上面提到了動態節點的好處,但是還是會有一部分人比較喜歡堅持靜態節點的方式,選擇靜態或者動態的 Jenkins Agent 節點都是可以的。接下來我們就分別來介紹下如何在 Kubernetes 集群中為 Jenkins 提供動靜態 Agent 節點。
靜態節點
首先在 Jenkins 頁面 http://jenkins.k8s.local/computer/new 新建一個節點:

新建節點
點擊創建后配置節點信息,然后點擊保存:

配置節點
保存后我們可以看到節點已經創建成功了:

節點列表
然后點擊列表中的 agent1 名稱,進入節點詳情頁面,在詳情頁面我們將獲取到運行該節點的一些密鑰信息。

密鑰信息
然后創建一個如下所示的資源清單文件:
# jenkins-agent.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins-agent
namespace: kube-ops
spec:
selector:
matchLabels:
app: jenkins-agent
template:
metadata:
labels:
app: jenkins-agent
spec:
containers:
- name: agent
image: jenkins/inbound-agent
securityContext:
privileged: true
imagePullPolicy: IfNotPresent
env:
- name: JENKINS_URL
value: http://jenkins.k8s.local
- name: JENKINS_SECRET
value: 9c4c5159b111083705eed5802ceb021cfad002a18dd59c692aa59a9616e6285a
- name: JENKINS_AGENT_NAME
value: agent1
- name: JENKINS_AGENT_WORKDIR
value: /home/jenkins/workspace上面的清單文件中的 JENKINS_URL、JENKINS_SECRET 和 JENKINS_AGENT_WORKDIR 這些環境變量的值就是上面我們在節點詳情頁面獲取到的信息,然后我們將這個文件應用到集群中:
$ kubectl apply -f jenkins-agent.yaml創建后正常該 agent 的 Pod 會啟動報錯,錯誤日志如下所示:
INFO: Locating server among [http://jenkins.k8s.local/]
Sep 07, 2023 7:55:51 AM hudson.remoting.jnlp.Main$CuiListener error
SEVERE: Failed to connect to http://jenkins.k8s.local/tcpSlaveAgentListener/: jenkins.k8s.local
java.io.IOException: Failed to connect to http://jenkins.k8s.local/tcpSlaveAgentListener/: jenkins.k8s.local
at org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver.resolve(JnlpAgentEndpointResolver.java:216)
at hudson.remoting.Engine.innerRun(Engine.java:760)
at hudson.remoting.Engine.run(Engine.java:543)
Caused by: java.net.UnknownHostException: jenkins.k8s.local
at java.base/java.net.AbstractPlainSocketImpl.connect(Unknown Source)
at java.base/java.net.Socket.connect(Unknown Source)
at java.base/sun.net.NetworkClient.doConnect(Unknown Source)
at java.base/sun.net.www.http.HttpClient.openServer(Unknown Source)
at java.base/sun.net.www.http.HttpClient.openServer(Unknown Source)
at java.base/sun.net.www.http.HttpClient.<init>(Unknown Source)
at java.base/sun.net.www.http.HttpClient.New(Unknown Source)
at java.base/sun.net.www.http.HttpClient.New(Unknown Source)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(Unknown Source)
at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect0(Unknown Source)
at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect(Unknown Source)
at java.base/sun.net.www.protocol.http.HttpURLConnection.connect(Unknown Source)
at org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver.resolve(JnlpAgentEndpointResolver.java:213)
... 2 more這其實是因為我們配置的 jenkins.k8s.local 域名是一個自定義的域名,需要在 K8s 集群中解析的話,我們還需要在 CoreDNS 中去添加一條 hosts 映射:
$ kubectl edit cm coredns -n kube-system
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
hosts {
10.206.16.10 jenkins.k8s.local
fallthrough
}
# ......
kind: ConfigMap但其實還有更簡單的方式,那就是直接將 JENKINS_URL 值替換為 Jenkins 的 Service 地址 http://jenkins.kube-ops.svc.cluster.local:8080 即可,這樣就不需要在 CoreDNS 中添加 hosts 映射了。
正常現在的 Jenkins Agent Pod 應該是正常運行的,我們可以通過下面的命令查看:
$ kubectl get pods -n kube-ops -l app=jenkins-agent
NAME READY STATUS RESTARTS AGE
jenkins-agent-76884cd44c-dd9ds 1/1 Running 0 2m32s再次查看節點列表,我們可以看到節點已經在線了:

agent
接下來我們再創建一個 Pipeline 類型的作業,然后在 Pipeline 腳本中添加下面的內容:

流水線
這里我們定義的流水線腳本中,我們使用到了 agent 關鍵字,這個關鍵字的作用就是指定這個流水線的運行環境,這里我們指定的是 build 這個標簽,也就是我們上面創建的 agent1 這個節點,這樣這個流水線就會在這個節點上運行。
點擊保存后,我們可以點擊立即構建來執行這個流水線,然后我們可以查看這個流水線的執行結果:

執行結果
這樣我們就可以在 Jenkins 中使用這個靜態節點來構建任務了。
動態節點
除了靜態節點之外,我們還可以使用動態節點的方式來構建任務,這樣可以更好的利用資源,我們這里使用的是 Kubernetes 的方式來創建動態節點,這樣我們就可以在 Jenkins 中使用動態節點來構建任務了。
第 1 步. 首先需要安裝 Kubernetes 插件。

kubernetes plugin
第 2 步. 安裝完畢后,進入 http://jenkins.k8s.local/manage/cloud/ 頁面:

new kubernetes plugin config
在該頁面點擊 New cloud 新建一個 Cloud 服務:

kubernetes plugin config
這里注意一定要選擇上 Kubernetes 這個 Type,然后點擊 Create 按鈕,然后我們就可以看到下面的配置頁面了:

create cloud
首先,配置連接 Kubernetes APIServer 的地址,由于我們的 Jenkins 運行在 Kubernetes 集群中,所以可以使用 Service 的 DNS 形式進行連接 https://kubernetes.default.svc.cluster.local:

jenkins k8s apiserver
命名空間這里填 kube-ops,然后點擊 連接測試,如果出現 Connected to Kubernetes v1.26.2 這樣的提示信息證明 Jenkins 已經可以和 Kubernetes 系統正常通信了。
然后下方的 Jenkins URL 地址為 http://jenkins.kube-ops.svc.cluster.local:8080,根據上面創建的 jenkins 的服務名填寫,包括下面的 Jenkins 通道,默認是 50000 端口(要注意是 TCP,所以不要填寫 http):

jenkins url
然后點擊最后的 save 按鈕保存配置。到這里我們的 Kubernetes 插件就算配置完成了。
測試
Kubernetes 插件的配置工作完成了,接下來我們就來添加一個 Job 任務,看是否能夠在 Slave Pod 中執行,任務執行完成后看 Pod 是否會被銷毀。
在 Jenkins 首頁點擊 新建任務,創建一個測試的任務,同樣還是選擇 Pipeline 類型的任務,這次我們需要使用的流水線腳本就更復雜了,如下所示:
pipeline{
agent{
kubernetes{
label "test01"
cloud 'Kubernetes'
yaml '''
---
kind: Pod
apiVersion: v1
metadata:
labels:
k8s-app: jenkins-agent
name: jenkins-agent
namespace: kube-ops
spec:
containers:
- name: jenkinsagent
image: jenkins/inbound-agent
imagePullPolicy: IfNotPresent
'''
}
}
stages{
stage("Hello"){
steps{
script{
echo "Hello Slave Pod on Kubernetes!"
}
}
}
}
}這次的腳本中定義的執行 agent 就比較復雜了,通過一個 kubernetes 屬性來指定這個流水線的運行環境,然后通過 yaml 屬性來定義這個運行 Pod 的清單文件,這里我們定義的是一個簡單的 Pod,然后我們將這個 Pod 部署到 kube-ops 這個命名空間中,這樣我們就可以在這個 Pod 中運行我們的 Jenkins Slave 了,需要注意 cloud 后面的值需要和前面我們定義的 Cloud 服務名稱一致。
最后點擊保存,同樣我們可以點擊左側的 立即構建 來執行這個任務,然后我們可以查看這個任務的執行結果:

slave pod execute command
雖然我們在這里的腳本中定義的 Pod 非常簡單,但可以看到 Jenkins 會幫我們配置一些默認的環境變量。當任務執行的過程中我們也可以觀察 Kubernetes 集群中的 Pod 變化:
$ kubectl get pods -n kube-ops -w
NAME READY STATUS RESTARTS AGE
jenkins-55c4676f4d-fhmw2 1/1 Running 3 (12m ago) 91m
jenkins-agent-76884cd44c-dd9ds 1/1 Running 0 22m
test01-jnzmb-ht0n7 0/1 Pending 0 0s
test01-jnzmb-ht0n7 0/1 Pending 0 0s
test01-jnzmb-ht0n7 0/1 ContainerCreating 0 0s
test01-jnzmb-ht0n7 1/1 Running 0 1s
test01-jnzmb-ht0n7 1/1 Terminating 0 3s
test01-jnzmb-ht0n7 0/1 Terminating 0 4s
test01-jnzmb-ht0n7 0/1 Terminating 0 4s
test01-jnzmb-ht0n7 0/1 Terminating 0 4s我們可以看到在我們點擊立刻構建的時候可以看到一個新的 Pod:test01-jnzmb-ht0n7 被創建了,這就是我們的 Jenkins Slave。當任務構建完抽這個 Slave Pod 也會自動刪除。
到這里,我們就完成了使用 Kubernetes 動態生成 Jenkins Slave 的方法。



























