目录



文章目录

##1、Jenkins CI/CD 背景介绍


持续构建与发布是我们日常工作中必不可少的一个步骤,目前大多公司都采用 Jenkins 集群来搭建符合需求的 CI/CD 流程,然而传统的 Jenkins
Slave 一主多从方式会存在一些痛点,比如:主 Master 发生单点故障时,整个流程都不可用了;每个 Slave
的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲;资源分配不均衡,有的 Slave 要运行的
job 出现排队等待,而有的 Slave 处于空闲状态;最后资源有浪费,每台 Slave 可能是实体机或者 VM,当 Slave
处于空闲状态时,也不会完全释放掉资源。

由于以上种种痛点,我们渴望一种更高效更可靠的方式来完成这个 CI/CD 流程,而 Docker 虚拟化容器技术能很好的解决这个痛点,下图是基于
Kubernetes 搭建 Jenkins 集群的简单示意图。


从图上可以看到 Jenkins Master 和 Jenkins Slave 以 Docker Container 形式运行在 Kubernetes 集群的
Node 上,Master 运行在其中一个节点,并且将其配置数据存储到一个 Volume 上去,Slave
运行在各个节点上,并且它不是一直处于运行状态,它会按照需求动态的创建并自动删除。

这种方式的工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Docker
Container 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且 Docker
Container 也会自动删除,恢复到最初状态。

这种方式带来的好处有很多:

* 服务高可用,当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将
Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。
* 动态伸缩,合理使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave
自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave
到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况。
* 扩展性好,当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node
到集群中,从而实现扩展。
##2、环境、软件准备

本次演示环境,我是在本机 MAC OS 以及虚拟机 Linux Centos7 上操作,以下是安装的软件及版本:

* Docker: version 17.09.0-ce
* Oracle VirtualBox: version 5.1.20 r114628 (Qt5.6.2)
* Minikube: version v0.22.2
* Kuberctl:
* Client Version: v1.8.1
* Server Version: v1.7.5
注意:Minikube 启动的单节点 k8s Node 实例是需要运行在本机的 VM 虚拟机里面,所以需要提前安装好 VM,这里我选择 Oracle
VirtualBox。k8s 运行底层使用 Docker 容器,所以本机需要安装好 Docker 环境,Minikube 和 Kuberctl
的安装过程可参考之前文章初试 minikube 本地部署运行 kubernetes 实例
<https://blog.csdn.net/aixiaoyang168/article/details/78331847>。

##3、部署 Jenkins Server 到 Kubernetes

在执行部署之前,我们要确保 Minikube 已经正常运行,如果使用已搭建好的 Kubernetes 集群,也要确保正常运行。接下来,我们需要准备部署
Jenkins 的 Yaml 文件,可以参考
GitHub jenkinsci kubernetes-plugin
<https://github.com/jenkinsci/kubernetes-plugin> 官网提供的 jenkins.yaml
<https://github.com/jenkinsci/kubernetes-plugin/blob/master/src/main/kubernetes/jenkins.yml>
和service-account.yaml
<https://github.com/jenkinsci/kubernetes-plugin/blob/master/src/main/kubernetes/service-account.yml>
文件,这里官网使用的是比较规范的 StatefulSet(有状态集群服务)方式进行部署,并配置了 Ingress 和 RBAC
账户权限信息。不过我本机测试的时候,发现 Volume 挂载失败,日志显示没有权限创建目录。所以我精简了一下,重新写了个以 Deployment
方式部署方式以及 Service 的配置文件(这里偷个懒,不使用 RBAC 认证了)。
$ cat jenkins-deployment.yaml apiVersion: apps/v1beta1 kind: Deployment
metadata: name: jenkins labels: k8s-app: jenkins spec: replicas: 1 selector:
matchLabels: k8s-app: jenkins template: metadata: labels: k8s-app: jenkins
spec: containers: - name: jenkins image: jenkins/jenkins:lts-alpine
imagePullPolicy: IfNotPresent volumeMounts: - name: jenkins-home mountPath:
/var/jenkins_home ports: - containerPort: 8080 name: web - containerPort: 50000
name: agent volumes: - name: jenkins-home emptyDir: {} $ cat
jenkins-service.yml kind: Service apiVersion: v1 metadata: labels: k8s-app:
jenkins name: jenkins spec: type: NodePort ports: - port: 8080 name: web
targetPort: 8080 - port: 50000 name: agent targetPort: 50000 selector: k8s-app:
jenkins
说明一下:这里 Service 我们暴漏了端口 8080 和 50000,8080 为访问 Jenkins Server 页面端口,50000 为创建的
Jenkins Slave 与 Master 建立连接进行通信的默认端口,如果不暴露的话,Slave 无法跟 Master 建立连接。这里使用
NodePort 方式暴漏端口,并未指定其端口号,由 Kubernetes 系统默认分配,当然也可以指定不重复的端口号(范围在 30000~32767)。

接下来,通过 kubectl 命令行执行创建 Jenkins Service。
$ kubectl create namespace kubernetes-plugin $ kubectl config set-context
$(kubectl config current-context) --namespace=kubernetes-plugin $ kubectl
create -f jenkins-deployment.yaml $ kubectl create -f jenkins-service.yml
说明一下:这里我们创建一个新的 namespace 为 kubernetes-plugin,并且将当前 context 设置为
kubernetes-plugin namespace 这样就会自动切换到该空间下,方便后续命令操作。
$ kubectl get service,deployment,pod NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
AGE jenkins NodePort 10.0.0.204 <none> 8080:30645/TCP,50000:31981/TCP 1m NAME
DESIRED CURRENT UP-TO-DATE AVAILABLE AGE jenkins 1 1 1 1 1m NAME READY STATUS
RESTARTS AGE jenkins-960997836-fff2q 1/1 Running 0 1m
此时,我们会发现 Jenkins Master 服务已经启动起来了,并且将端口暴漏到 8080:30645,50000:31981,此时可以通过浏览器打开
http://<Cluster_IP>:30645 访问 Jenkins 页面了。当然也可以通过 minikube service ... 命令来自动打开页面。
$ minikube service jenkins -n kubernetes-plugin Opening kubernetes service
kubernetes-plugin/jenkins in default browser... Opening kubernetes service
kubernetes-plugin/jenkins in default browser...
在浏览器上完成 Jenkins 的初始化插件安装过程,并配置管理员账户信息,这里忽略过程,初始化完成后界面如下:


注意: 初始化过程中,让输入 /var/jenkins_home/secret/initialAdminPassword 初始密码时,因为我们设置的
emptyDir: {} 没有挂载到外部路径,可以进入到容器内部进行获取。
$ kubectl exec -it jenkins-960997836-fff2q cat
/var/jenkins_home/secrets/initialAdminPassword
##4、Jenkins 配置 Kubernetes Plugin

管理员账户登录 Jenkins Master 页面,点击 “系统管理” —> “管理插件” —> “可选插件” —> “Kubernetes plugin”
勾选安装即可。


安装完毕后,点击 “系统管理” —> “系统设置” —> “新增一个云” —> 选择 “Kubernetes”,然后填写 Kubernetes 和
Jenkins 配置信息。


说明一下:

* Name 处默认为 kubernetes,也可以修改为其他名称,如果这里修改了,下边在执行 Job 时指定 podTemplate() 参数
cloud 为其对应名称,否则会找不到,cloud 默认值取:kubernetes
* Kubernetes URL 处我填写了 https://kubernetes.default 这里我填写了 Kubernetes Service
对应的 DNS 记录,通过该 DNS 记录可以解析成该 Service 的 Cluster IP,注意:也可以填写
https://kubernetes.default.svc.cluster.local 完整 DNS 记录,因为它要符合
<svc_name>.<namespace_name>.svc.cluster.local 的命名方式,或者直接填写外部 Kubernetes 的地址
https://<ClusterIP>:<Ports>。
* Jenkins URL 处我填写了 http://jenkins.kubernetes-plugin:8080,跟上边类似,也是使用 Jenkins
Service 对应的 DNS 记录,不过要指定为 8080 端口,因为我们设置暴漏 8080 端口。同时也可以用
http://<ClusterIP>:<Node_Port> 方式,例如我这里可以填 http://192.168.99.100:30645
也是没有问题的,这里的 30645 就是对外暴漏的 NodePort。
配置完毕,可以点击 “Test Connection” 按钮测试是否能够连接的到 Kubernetes,如果显示 Connection test
successful 则表示连接成功,配置没有问题。

##5、测试并验证

好了,通过 Kubernetes 安装 Jenkins Master 完毕并且已经配置好了连接,接下来,我们可以配置 Job 测试一下是否会根据配置的
Label 动态创建一个运行在 Docker Container 中的 Jenkins Slave 并注册到 Master 上,而且运行完 Job
后,Slave 会被注销并且 Docker Container 也会自动删除吧!

###5.1、pipeline 类型支持

创建一个 Pipeline 类型 Job 并命名为 my-k8s-jenkins-pipeline,然后在 Pipeline
脚本处填写一个简单的测试脚本如下:
def label = "mypod-${UUID.randomUUID().toString()}" podTemplate(label: label,
cloud: 'kubernetes') { node(label) { stage('Run shell') { sh 'sleep 130s' sh
'echo hello world.' } } }
执行构建,此时去构建队列里面,可以看到有一个构建任务,暂时还没有执行中的构建,因为还没有初始化好,稍等一会,就会看到 Master 和
jenkins-slave-jbs4z-xs2r8 已经创建完毕,在等一会,就会发现 jenkins-slave-jbs4z-xs2r8 已经注册到
Master 中,并开始执行 Job,点击该 Slave 节点,我们可以看到通过标签
mypod-b538c04c-7c19-4b98-88f6-9e5bca6fc9ba 关联,该 Label 就是我们定义的标签格式生成的,Job
执行完毕后,jenkins-slave 会自动注销,我们通过 kubectl 命令行,可以看到整个自动创建和删除过程。









# jenkins slave 启动前,只有 jenkins master 服务存在 $ kubectl get pods NAME READY
STATUS RESTARTS AGE jenkins-960997836-fff2q 1/1 Running 0 1d # jenkins slave
自动创建完毕 $ kubectl get pods NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d jenkins-slave-jbs4z-xs2r8 1/1 Running
0 56s # Docker Container 启动服务情况 $ docker ps |grep jenkins aa5121667601
jenkins/jnlp-slave "jenkins-slave bd880…" About a minute ago Up About a minute
k8s_jnlp_jenkins-slave-jbs4z-xs2r8_kubernetes-plugin_25a91ed9-3337-11e8-a49f-08002744a3f1_0
d64deb0eaa20 gcr.io/google_containers/pause-amd64:3.0 "/pause" About a minute
ago Up About a minute
k8s_POD_jenkins-slave-jbs4z-xs2r8_kubernetes-plugin_25a91ed9-3337-11e8-a49f-08002744a3f1_0
995c1743552a jenkins "/bin/tini -- /usr/l…" 27 hours ago Up 26 hours
k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 27 hours ago Up
26 hours
k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
# jenkins slave 执行完毕自动删除 $ kubectl get pods NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d jenkins-slave-jbs4z-xs2r8 0/1
Terminating 0 2m $ kubectl get pods NAME READY STATUS RESTARTS AGE
jenkins-960997836-fff2q 1/1 Running 0 1d $ docker ps |grep jenkins 995c1743552a
jenkins "/bin/tini -- /usr/l…" 27 hours ago Up 26 hours
k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 27 hours ago Up
26 hours
k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
从上边的操作日志中,我们可以清晰的看到 Jenkins Slave 自动创建到注销删除的过程,整个过程是自动完成的,不需要人工干预。

###5.2、Container Group 类型支持

创建一个 Pipeline 类型 Job 并命名为 my-k8s-jenkins-container,然后在 Pipeline
脚本处填写一个简单的测试脚本如下:
def label = "mypod-${UUID.randomUUID().toString()}" podTemplate(label: label,
cloud: 'kubernetes', containers: [ containerTemplate(name: 'maven', image:
'maven:3.3.9-jdk-8-alpine', ttyEnabled: true, command: 'cat'), ]) { node(label)
{ stage('Get a Maven Project') { git
'https://github.com/jenkinsci/kubernetes-plugin.git' container('maven') {
stage('Build a Maven project') { sh 'mvn -B clean install' } } } } }
注意:这里我们使用的 containers 定义了一个 containerTemplate 模板,指定名称为 maven 和使用的 Image,下边在执行
Stage 时,使用container('maven'){...} 就可以指定在该容器模板里边执行相关操作了。比如,该示例会在 jenkins-slave
中执行 git clone 操作,然后进入到 maven 容器内执行mvn -B clean install
编译操作。这种操作的好处就是,我们只需要根据代码类型分别制作好对应的编译环境镜像,通过指定不同的 container
来分别完成对应代码类型的编译操作。模板详细的各个参数配置可以参照Pod and container template configuration
<https://github.com/jenkinsci/kubernetes-plugin/blob/master/README.md#pod-and-container-template-configuration>


执行构建,跟上边 Pipeline 类似,也会新建 jenkins-slave 并注册到 master,不同的是,它会在 Kubernetes
中启动我们配置的 maven 容器模板,来执行相关命令。



$ kubectl get pods NAME READY STATUS RESTARTS AGE jenkins-960997836-fff2q 1/1
Running 0 1d jenkins-slave-k2wwq-4l66k 2/2 Running 0 53s $ docker ps CONTAINER
ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8ed81ee3aad4 jenkins/jnlp-slave
"jenkins-slave 4ae74…" About a minute ago Up About a minute
k8s_jnlp_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
bd252f7e59c2 maven "cat" About a minute ago Up About a minute
k8s_maven_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
fe22da050a53 gcr.io/google_containers/pause-amd64:3.0 "/pause" About a minute
ago Up About a minute
k8s_POD_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
995c1743552a jenkins "/bin/tini -- /usr/l…" 44 hours ago Up 44 hours
k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 44 hours ago Up
44 hours
k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
###5.3、非 Pipeline 类型支持

Jenkins 中除了使用 Pipeline 方式运行 Job 外,通常我们也会使用普通类型 Job,如果也要想使用kubernetes plugin
来构建任务,那么就需要点击 “系统管理” —> “系统设置” —> “云” —> “Kubernetes” —> “Add Pod Template”
进行配置 “Kubernetes Pod Template” 信息。



注意:这里的 Labels 名在配置非 pipeline 类型 Job 时,用来指定任务运行的节点。Containers 下的 Name
字段的名字,这里要注意的是,如果 Name 配置为 jnlp,那么 Kubernetes 会用下边指定的 Docker Image 代替默认的
jenkinsci/jnlp-slave 镜像,否则,Kubernetes plugin 还是会用默认的 jenkinsci/jnlp-slave 镜像与
Jenkins Server 建立连接,即使我们指定其他 Docker Image。这里我随便配置为 jnlp-slave,意思就是使用默认的
jenkinsci/jnlp-slave 镜像来运行,因为我们暂时还没制作可以替代默认镜像的镜像。

新建一个自由风格的 Job 名称为 my-k8s-jenkins-simple,配置 “Restrict where this project can be
run” 勾选,在 “Label Expression” 后边输出我们上边创建模板是指定的 Labels 名称 jnlp-agent,意思是指定该 Job
匹配 jnlp-agent 标签的 Slave 上运行。



执行构建后,跟上边 Pipeline 一样,符合我们的预期。





###5.4、配置自定义 jenkins-slave 镜像

通过 kubernetest plugin 默认提供的镜像 jenkinsci/jnlp-slave 可以完成一些基本的操作,它是基于
openjdk:8-jdk 镜像来扩展的,但是对于我们来说这个镜像功能过于简单,比如我们想执行 Maven
编译或者其他命令时,就有问题了,那么可以通过制作自己的镜像来预安装一些软件,既能实现 jenkins-slave
功能,又可以完成自己个性化需求,那就比较不错了。如果我们从头开始制作镜像的话,会稍微麻烦些,不过可以参考jenkinsci/jnlp-slave
<https://github.com/jenkinsci/docker-jnlp-slave/blob/master/Dockerfile> 和
jenkinsci/docker-slave
<https://github.com/jenkinsci/docker-slave/blob/master/Dockerfile> 这两个官方镜像来做,注意:
jenkinsci/jnlp-slave 镜像是基于 jenkinsci/docker-slave 来做的。这里我简单演示下,基于
jenkinsci/jnlp-slave:latest 镜像,在其基础上做扩展,安装 Maven 到镜像内,然后运行验证是否可行吧。

创建一个 Pipeline 类型 Job 并命名为 my-k8s-jenkins-container-custom,然后在 Pipeline
脚本处填写一个简单的测试脚本如下:
def label = "mypod-${UUID.randomUUID().toString()}" podTemplate(label: label,
cloud: 'kubernetes',containers: [ containerTemplate( name: 'jnlp', image:
'huwanyang168/jenkins-slave-maven:latest', alwaysPullImage: false, args:
'${computer.jnlpmac} ${computer.name}'), ]) { node(label) { stage('stage1') {
stage('Show Maven version') { sh 'mvn -version' sh 'sleep 60s' } } } }
说明一下:这里 containerTemplate 的 name 属性必须叫 jnlp,Kubernetes 才能用自定义 images 指定的镜像替换默认的
jenkinsci/jnlp-slave 镜像。此外,args 参数传递两个 jenkins-slave 运行需要的参数。还有一点就是这里并不需要指定
container('jnlp'){...} 了,因为它被 Kubernetes 指定了要被执行的容器,所以直接执行 Stage 就可以了。

执行构建,看下效果如何吧!




$ kubectl get pods NAME READY STATUS RESTARTS AGE jenkins-960997836-fff2q 1/1
Running 0 2d jenkins-slave-9wtkt-d2ms8 1/1 Running 0 12m
bj-m-204072a:k8s-gitlab wanyang3$ docker ps CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES b31be1de9563 huwanyang168/jenkins-slave-maven "jenkins-slave
7cef1…" 12 minutes ago Up About a minute
k8s_jnlp_jenkins-slave-9wtkt-d2ms8_kubernetes-plugin_0ea4bc9d-33f3-11e8-a49f-08002744a3f1_0
b33b7ce3070e gcr.io/google_containers/pause-amd64:3.0 "/pause" 12 minutes ago
Up About a minute
k8s_POD_jenkins-slave-9wtkt-d2ms8_kubernetes-plugin_0ea4bc9d-33f3-11e8-a49f-08002744a3f1_0
995c1743552a jenkins "/bin/tini -- /usr/l…" 2 days ago Up 2 days
k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d gcr.io/google_containers/pause-amd64:3.0 "/pause" 2 days ago Up 2
days
当然,我们也可以使用非 Pipeline 类型指定运行该自定义 slave,那么我们就需要修改 “系统管理” —> “系统设置” —> “云” —>
“Kubernetes” —> “Add Pod Template” 修改配置 “Kubernetes Pod Template” 信息如下:



然后同样在 Job 配置页面 “Label Expression” 后边输出我们上边创建模板是指定的 Labels 名称
jnlp-agent,就可以啦!测试妥妥没问题的。

最后,贴一下我自定义的预安装了 Maven 的 Jenkins-slave 镜像的 Dockerfile
<https://github.com/huwanyang/jenkins-slave-maven/blob/master/Dockerfile>
,当然大家可以基于此预安装一些其他软件,来完成日常持续构建与发布工作吧。
FROM jenkins/jnlp-slave:latest MAINTAINER [email protected] LABEL
Description="This is a extend image base from jenkins/jnlp-slave which install
maven in it." # 切换到 root 账户进行操作 USER root # 安装 maven-3.3.9 RUN wget
http://mirrors.sonic.net/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz
&& \ tar -zxf apache-maven-3.3.9-bin.tar.gz && \ mv apache-maven-3.3.9
/usr/local && \ rm -f apache-maven-3.3.9-bin.tar.gz && \ ln -s
/usr/local/apache-maven-3.3.9/bin/mvn /usr/bin/mvn && \ ln -s
/usr/local/apache-maven-3.3.9 /usr/local/apache-maven USER jenkins
参考资料

* jenkinsci/kubernetes-plugin
<https://github.com/jenkinsci/kubernetes-plugin/blob/master/README.md>
* jenkinsci/docker-slave
<https://github.com/jenkinsci/docker-slave/blob/master/README.md>