提到基于 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 一主多从方式会存在一些痛点,比如:
正因为上面的这些种种痛点,我们渴望一种更高效更可靠的方式来完成这个 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 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 的方法。