Kubernetes下Jenkins CI的搭建

相比传统的在多台主机上部署Jenkins和slave,K8s环境下有诸多好处,比如:

  • CI的部署流程脚本化,方便重用和跟踪改动
  • master节点出现故障后自动重启
  • slave节点繁忙时自动扩容,空闲时自动释放

部署Master到K8s

为了方便跟踪所有的改动,可以把Jenkins相关的文件放在一个代码库里,并启用Git。

首先在K8s下创建一个namspace:

1
kubectl create namespace ci

创建Deployment

创建一个K8s的deployment(deployment.yml)脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: jenkins2
namespace: ci
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
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
- name: dockersock
mountPath: /var/run/docker.sock
nodeSelector:
available-for-jenkins: "true"
securityContext:
fsGroup: 995
volumes:
- name: jenkinshome
persistentVolumeClaim:
claimName: jenkinspvc
- name: dockersock
hostPath:
path: /var/run/docker.sock
  • 在metadata中指定了namespace为刚创建的ci
  • 使用了一个名为jenkins2的service account,创建方式如下。
  • 在containers中指定了镜像为Jenkins的官方镜像jenkins/jenkins:lts。如果需要在官方镜像上加一些定制化的功能,也可以使用自己的镜像,但是注意如果镜像改动频繁的话要把imagePullPolicy设置为Always
  • 8080端口用来访问UI,50000端口用来和Slave通信。
  • 这里挂载了两个volume,其中一个以PVC和PV的方式创建,用来保存Jenkins的各种配置,这样的话运行Jenkins的pods重新创建后就不会丢失配置信息。创建方式如下。
  • securityContext,以及名为dockersock的volume都是用来解决所谓的docker-in-docker的问题,简单来说就是在jenkins里使用K8s master的docker,下面有详细说明。

创建Service Account

Jenkins部署上K8s之后会对K8s进行一些操作,比如部署时需要创建Pod,这些操作需要权限才能完成,这里使用了Role-based access control,创建方式如下(rbac.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins2
namespace: ci

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: ci-self-rolebinding
namespace: ci
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: admin
subjects:
- kind: ServiceAccount
name: jenkins2
namespace: ci
  • 第一部分声明了service accountmetadata里指定了service account的name,这个name也就是前面deployment.yml里使用到的serviceAccountName
  • 第二部分声明了RoleBinding,也就是给service account赋权,其中:
    • ci-self-rolebinding是这个RoleBinding的名字
    • roleRef里引用了admin这个角色,这是K8s内置的一个角色,拥有大部分的权限。这一步声明了这个RoleBinding要用到的权限。
    • subjects描述了要将上面的权限绑定到什么地方,这里绑定到了之前创建的service account上。

最后apply一下:

1
kubectl create -f rbac.yaml

至此,给service account赋权的操作就完成了。

创建PV及PVC

Persist VolumePersis Volume Claim是K8s里的概念,其中前者就像一块云盘,需要使用时通过后者来申请其中的一小块。

首先创建一个Persist Volume(pv.yml):

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkinspv
spec:
capacity:
storage: 9Gi
accessModes:
- ReadWriteOnce
awsElasticBlockStore:
fsType: ext4
volumeID: vol-xxxxxxx
  • 这里使用了AWS的EBS作为Persist Volume的载体,需要注意的是不同载体的accessModes会不同,EBS就只支持ReadWriteOnce(只能被一个node读写)。详细的列表可以在这里找到

然后创建一个Persis Volume Claim(pcv.yml):

1
2
3
4
5
6
7
8
9
10
11
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkinspvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi

最后apply一下:

1
2
kubectl create -f pv.yml
kubectl create -f pvc.yml

一切准备好之后,就可以创建Deployment了:

1
kubectl create -f deployment.yml

这时候运行kubectl get all -n ci,就能看到Jenkins已经起起来了。

创建Service

这时候Jenkins虽然已经起起来了,但是并不能通过浏览器访问,因为还没对外暴露,所以需要创建一个service(service.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
---
apiVersion: v1
kind: Service
metadata:
name: jenkins2
annotations:
service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
namespace: ci
labels:
app: jenkins2
spec:
type: LoadBalancer
selector:
app: jenkins2
ports:
- name: web
port: 8080
targetPort: web
- name: agent
port: 50000
targetPort: agent
- name: https
port: 443
targetPort: web
protocol: TCP
- name: http
port: 80
targetPort: web
protocol: TCP
  • 使用了AWS ELB来暴露端口,端口配置和deployment里的一样。

apply一下:

1
kubectl create -f service.yml

这时候运行kubectl get all -n ci应该就能看到类似的界面:

1
2
3
➜  ~ kubectl get service -n ci
NAME TYPE CLUSTER-IP EXTERNAL-IP
jenkins2 LoadBalancer xxx.xx.xx.xx internal-xxx.xxx.elb.amazonaws.com

然后就可以通过external IP来访问了:

Docker-in-Docker

Jenkins是以Docker的方式部署到K8s里去的,而在使用Jenkins的时候,又可能会需要运行Docker来打包或者运行镜像,这就产生了所谓的Dcoker-in-Docker的问题,如果在Jenkins里再安装一个Docker,就可能会遇到一些非常底层的问题(详细资料可以看这篇文章),所以推荐的方式是将K8s master机器上的Docker socket绑定进Jenkins容器中来解决。简单来说就是在Jenkins里使用宿主的Docker。

我们在Deployment脚本中有这么一个volume:

1
2
3
4
volumes:
- name: dockersock
hostPath:
path: /var/run/docker.sock

然后又有这么一个mount:

1
2
3
volumeMounts:
- name: dockersock
mountPath: /var/run/docker.sock

可以看到我们将宿主的/var/run/docker.sock绑定到了Jenkins的/var/run/docker.sock,比较理想的情况下这样就够了,但是往往还会遇到权限的问题,这时候如果进入到jenkins的pod里运行docker ps,应该就会出现权限的错误。

解决的方法就是将docker的group ID加入到Deployment的fsGroup字段里,一开始的值是995,但是这个值每个人都可能会不同,可以通过以下命令查看group ID。

首先通过kubectl get pods -n ci拿到pod的ID,然后运行:

1
2
kubectl exec -it pod/jenkins2-6c68468fd4-ccqz2 /bin/bash -n ci
ls -l /var/run/docker.sock

这时候就能看到如下输出:

1
2
jenkins@jenkins2-6c68468fd4-ccqz2:/$ ls -l /var/run/docker.sock
srw-rw---- 1 root 995 0 Aug 6 07:05 /var/run/docker.sock

这个995就是所需要的fsGroup的值,将其写入deployment脚本后重新apply以下就可以了。

动态创建Slave

至此Jenkins就已经可以使用了,但是build时用的slave还是master自己,这并不是一个很好的选择,原因在于官方的Jenkins镜像内置的东西非常少,而且默认不启用root账户,所以在上面装东西非常困难,其次是随着要build的任务种类的增加,master可能会变得越来越臃肿,并且build比较繁忙时也会产生排队很久的现象。

所以利用K8s的特性来动态的创建和释放slave是一个更好的选择。

首先,在Jenkins->系统管理->插件管理里,安装Jenkins的Plugin,这个插件能让Jenkins和K8s实现集成:

然后,在Jenkins->系统管理->系统设置里,对插件进行配置:

这一步里填写K8s的信息:

  • 命名空间就是一开始创建的ci
  • Jenkins地址的格式为:service-name.namespace.svc.cluster.local:8080,创建service时名字为jenkins2,所以这里用这个名字

下一步填写Slave的信息:

  • 标签列表非常重要,build脚本里就是用这个名字来指定slave
  • container的name必须为jnlp,否则会报错

  • 这里使用了cnych/jenkins:jnlp6来作为slave的镜像,包含了kubectl等实用功能

最后一步做了一些额外配置:

  • 把K8s的docker.sock挂载到slave容器里面去,解决docker-in-docker的问题
  • 把K8s的.kube挂载到slave容器里面去,slave就能通过kubectl来访问K8s集群
  • 这里代理的空闲存活时间要注意,如果用默认值的话slave用完后就会立即销毁,这里为了查看一些build后的产物,设置成了一个月后才会销毁

测试

至此Jenkins就已经完全配置好了,跑个简单的build测试一下。

首先创建一个freestyle的project:

然后在限制项目的运行节点里,写入之前配置的标签列表的值。

最后在构建里新建一个shell:

命令如下:

1
2
3
4
echo "************test docker in docker************"
docker -v
echo "************test kubectl************"
kubectl get pods

创建完成后点立即构建,可以看到如下输出:

同时可以看到,slave已经被自动创建出来了。

Java与Kotlin中的协变、逆变、不变 重写Spring MVC的异常处理来提供自定义错误消息

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×