本文章案例可用于参考Jenkins for Kubernetes部署。因每个公司的架构和环境不一样,需要改变一些部署的方式。
Jenkins for Kubernetes的好处:
- Jenkins-Master的高可用。Kubernetes的RC或Deployment可以监控副本的存活状态(通过探针)和副本数量,如果Master出现无法提供服务的情况,就会重启或者迁移到其他节点。
- Jenkins-Slave的动态伸缩。每次构建都会启动一个Pod用于部署Slave,构建完成后就会释放掉。那么Pod在创建的时候,Kubernetes就会选择集群内资源剩余较多的节点创建Slave的Pod,构建完成后Pod会自动删除。
- 扩展性好。 因为可以同时拥有很多个Slave,可以配置Jenkins同时执行很多构建操作,减少排队等待构建的时间。
部署思路
首先在Kubernetes中部署Jenkins-Master然后使用Kubernetes Plugin插件进行Slave的动态伸缩。并且使用NFS作为后端存储的PersistentVolume来挂载Jenkins-Master的jenkins_home目录、构建时Slave的Maven缓存m2目录(可以利用缓存加快每次构建的速度)、保留Slave每次构建产生的数据(workspace目录中的每个Job)。
使用PersistentVolume的原因是Kubernetes任何节点都可以访问到挂载的目录,不会因为Master迁移节点导致数据丢失。NFS方便部署而且性能也满足Jenkins的使用需求所以选择了NFS,也可以使用其他的后端存储。
部署
部署方式可以自定义也可以使用Kubernetes Pugin官网提供的部署yml。自定义使用Deployment也是可以的,但是官网的部署方式使用了StatefulSet。Jenkins是一个有状态的应用,我感觉使用StatefulSet部署更加严谨一点。我这里使用了官网提供的文档进行部署的,但是也根据实际情况修改了一些东西。
首先需要在Kubernetes所有节点部署NFS客户端:
yum -y install nfs-utils
systemctl start nfs-utils
systemctl enable nfs-utils
rpcinfo -p
NFS服务端配置文件增加配置:
/data/dev_jenkins 10.0.0.0/24(rw,sync,no_root_squash,no_subtree_check)
dev环境Jenkins Slave节点挂载workspace
/data/dev_jenkins/workspace 0.0.0.0/0(rw,sync,no_root_squash,no_subtree_check)
dev环境Jenkins Slave节点挂载m2 Maven缓存目录
/data/dev_jenkins/m2 0.0.0.0/0(rw,sync,no_root_squash,no_subtree_check)
共享目录一定要给777权限。不然容器内部会报错没有写入权限。
service-account.yml此文件用于创建Kubernetes的RBAC,授权给后面的Jenkins应用可以创建和删除Slave的Pod。
# In GKE need to get RBAC permissions first with
# kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin [--user=<user-name>|--group=<group-name>]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: jenkins
rules:
- 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"]
verbs: ["get","list","watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: jenkins #与jenkins.yml中的serviceAccountName: jenkins相对应
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
jenkins-pv.yml和jenkins-pvc.yml用于创建挂载jenkins_home目录:
[root@dev-master1 kubernetes]# cat jenkins-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkins-home
spec:
capacity: #指定容量
storage: 20Gi
accessModes:
- ReadWriteOnce #访问模式,还有ReadOnlyMany ##ReadOnlymany
# persistenVolumeReclaimPolicy: Recycle
# storageClassName: nfs ##指定存储的类型
nfs:
path: /data/dev_jenkins #指明NFS的路径
server: 10.0.0.250 #指明NFS的IP
[root@dev-master1 kubernetes]# cat jenkins-pvc.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
namespace: kubernetes-plugin
name: jenkins-home
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
创建Jenkins的Master,可以根据实际情况限制Jenkins的资源使用。
[root@dev-master1 kubernetes]# cat jenkins.yml
# jenkins
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: jenkins
labels:
name: jenkins
spec:
selector:
matchLabels:
name: jenkins
serviceName: jenkins
replicas: 1
updateStrategy:
type: RollingUpdate
template:
metadata:
name: jenkins
labels:
name: jenkins
spec:
terminationGracePeriodSeconds: 10
serviceAccountName: jenkins
containers:
- name: jenkins
image: 10.0.0.59/jenkins/jenkins:lts-alpine #官方镜像为jenkins/jenkins:lts-alpine,为了节省下载时间已经push到自己到Harbor仓库
imagePullPolicy: Always
ports:
- containerPort: 8080
- containerPort: 50000
resources:
limits:
cpu: 1
memory: 1Gi
requests:
cpu: 0.5
memory: 500Mi
env:
- name: LIMITS_MEMORY
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1Mi
- name: JAVA_OPTS
# value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
value: -Xmx$(LIMITS_MEMORY)m -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
volumeMounts: #挂载PVC存储到Jenkins容器的/var/jenkins_home
- name: jenkinshome
mountPath: /var/jenkins_home
livenessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 600 #存活探针时间改为600s,如果服务器配置低,Jenkins还没有启动成功就被重启了。
timeoutSeconds: 5
failureThreshold: 12 # ~2 minutes
readinessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
failureThreshold: 12 # ~2 minutes
securityContext:
fsGroup: 1000
volumes: #此处声明Jenkins的PVC存储
- name: jenkinshome
persistentVolumeClaim:
claimName: jenkins-home
# imagePullSecrets: 如果使用私有仓库,并且仓库对镜像设置了访问权限,需要在Kubernetes Master创建一个secret
# - name: registry-secret
jenkins-sv.yml用于创建Jenkins的Service。
[root@dev-master1 kubernetes]# cat jenkins-sv.yml
apiVersion: v1
kind: Service
metadata:
name: jenkins
spec:
sessionAffinity: "ClientIP"
type: NodePort
selector:
name: jenkins
ports:
-
name: http
port: 80
nodePort: 31006
protocol: TCP
-
name: agent
port: 50000
nodePort: 31007
protocol: TCP
挂载Maven缓存目录。
[root@dev-master1 kubernetes]# cat m2-pv.yml
m2是Maven的缓存,挂载以提高build速度
apiVersion: v1
kind: PersistentVolume
metadata:
name: maven-m2
spec:
capacity: #指定容量
storage: 200Gi
accessModes:
- ReadWriteOnce #访问模式,还有ReadOnlyMany ##ReadOnlymany
# persistenVolumeReclaimPolicy: Recycle
# storageClassName: nfs ##指定存储的类型
nfs:
path: /data/dev_jenkins/m2 #指明NFS的路径
server: 10.0.0.250 #指明NFS的IP
[root@dev-master1 kubernetes]# cat m2-pvc.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
namespace: kubernetes-plugin
name: maven-m2
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Gi
挂载Slave节点保存构建结果的目录。
[root@dev-master1 kubernetes]# cat workspace-pv.yml
m2是maven的缓存,挂载以提高build速度
apiVersion: v1
kind: PersistentVolume
metadata:
name: workspace
spec:
capacity: #指定容量
storage: 200Gi
accessModes:
- ReadWriteOnce #访问模式,还有ReadOnlyMany ##ReadOnlymany
# persistenVolumeReclaimPolicy: Recycle
# storageClassName: nfs ##指定存储的类型
nfs:
path: /data/dev_jenkins/workspace #指明NFS的路径
server: 10.0.0.250 #指明NFS的IP
[root@dev-master1 kubernetes]# cat workspace-pvc.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
namespace: kubernetes-plugin
name: workspace
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Gi
创建Jenkins的Ingress。因为我的Kubernetes集群里面使用的是Traefik,所以我把Traefik的配置文件和kubernetes-plugin官网给出的Ingress一起贴出来。
[root@dev-master1 kubernetes]# cat jenkins-traefik.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: jenkins
namespace: kubernetes-plugin
annotations:
kubernetes.io/ingress.class: traefik
spec:
rules:
- host: jenkins-dev.doudou.com
http:
paths:
- path: /
backend:
serviceName: jenkins
servicePort: 80
[root@dev-master1 kubernetes]# cat jenkins-Ingress.yml
因为集群使用Traefik所以此Ingress配置文件不创建,此文件为官方原版
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: jenkins
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/tls-acme: "true"
# "413 Request Entity Too Large" uploading plugins, increase client_max_body_size
nginx.ingress.kubernetes.io/proxy-body-size: 50m
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
# For nginx-ingress controller < 0.9.0.beta-18
ingress.kubernetes.io/ssl-redirect: "true"
# "413 Request Entity Too Large" uploading plugins, increase client_max_body_size
ingress.kubernetes.io/proxy-body-size: 50m
ingress.kubernetes.io/proxy-request-buffering: "off"
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: jenkins
servicePort: 80
host: jenkins.example.com
tls:
- hosts:
- jenkins.example.com
secretName: tls-jenkins
创建以上的配置文件:
kubectl create namespace kubernetes-plugin #创建kubernetes-plugin namespace,下面创建的所有东西都归属到这个namespace
kubectl config set-context $(kubectl config current-context) --namespace=kubernetes-plugin #修改Kubernetes默认的namespace为kubernetes-plugin,这样下面创建的都默认为kubernetes-plugin命名空间
kubectl create -f service-account.yml
kubectl create -f jenkins-Ingress.yml
kubectl create -f jenkins-pv.yml
kubectl create -f jenkins-pvc.yml
kubectl create -f jenkins-sv.yml
kubectl create -f jenkins.yml
kubectl create -f m2-pvc.yml
kubectl create -f m2-pv.yml
kubectl create -f workspace-pvc.yml
kubectl create -f workspace-pv.yml
查看创建状态:
[root@dev-master1 ~]# kubectl get service,pod,StatefulSet -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/jenkins NodePort 10.105.123.193 <none> 80:31006/TCP,50000:31007/TCP 9d name=jenkins
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/jenkins-0 1/1 Running 0 6d5h 100.78.0.141 dev-node4 <none> <none>
NAME READY AGE CONTAINERS IMAGES
statefulset.apps/jenkins 1/1 7d jenkins 10.0.0.59/jenkins/jenkins:lts-alpine
[root@dev-master1 ~]# kubectl get pv,pvc
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/jenkins-home 20Gi RWO Retain Bound kubernetes-plugin/jenkins-home 13d
persistentvolume/maven-m2 200Gi RWO Retain Bound kubernetes-plugin/maven-m2 7d5h
persistentvolume/workspace 200Gi RWO Retain Bound kubernetes-plugin/workspace 7d5h
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/jenkins-home Bound jenkins-home 20Gi RWO 13d
persistentvolumeclaim/maven-m2 Bound maven-m2 200Gi RWO 7d5h
persistentvolumeclaim/workspace Bound workspace 200Gi RWO 7d5h
PV的状态为Bound状态表示已经绑定到对应的PVC上。Jenkins的Pod状态为1/1就说明启动成功了,可以通过绑定Ingress的域名访问了。或者使用Service配置中的nodePort端口访问Kubernetes任意节点IP:nodePort。
查看Jenkins密码:
kubectl exec -it jenkins-0 -n kubernetes-plugin -- cat /var/jenkins_home/secrets/initialAdminPassword
Jenkins配置
Jenkins安装完成后进入UI界面,首先需要安装需要的插件。
Jenkins可以根据实际情况选择适合的源:
系统管理->插件管理->高级
https://updates.jenkins.io/update-center.json #官方源
https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json #清华源
然后安装需要的插件:
-
Git pPugin
-
Maven Integration Plugin
-
Docker Plugin
-
Kubernetes Continuous Deploy Plugin
-
Kubernetes Plugin
-
Publish Over SSH Plugin
-
SSH Agent Plugin
-
SSH Build Agents Plugin
-
promoted builds plugin
-
Promoted Builds (Simple)
配置
Kubernetes Plugin插件安装完成后在Jenkins设置里面点击【系统配置】拉到最下面就可以看到一个Cloud。
单击之,添加一个云:
- 名称:名字随便取,后面连接云的时候需要这个名字。
- Kubernetes地址:访问Kubernetes Master上kube-apiserver服务的地址。
- Kubernetes命名空间:Jenkins部署在哪个命名空间里面了。
- Jenkins地址:Jenkins访问地址。
- Jenkins通道(这特么是一个大坑) :访问Jenkins容器内50000端口地址。因为Jenkins的Service配置文件中我把50000端口映射为nodePort,再加上我配置了DNS所以我这里写了域名:端口号的格式,也可以使用IP地址+端口号。
因为Jenkins-Master和Jenkins-Slave都在Kubernetes集群内部,所以写ClusterIP:端口号应该也是可以的,但是我没试过,略略略:),地址只要能访问到容器内部的50000端口就可以,但是有一点需要注意,这里的格式不能加http不能加/感觉应该是协议的问题,但是还没搞懂。
点击连接测试,是否能够成功。
测试
连接成功后,创建一个流水线Job进行测试使用。
podTemplate(label: 'jnlp-slave', cloud: 'kubernetes', containers: [
containerTemplate(name: 'maven', image: '10.0.0.59/jenkins/maven:3.3.9-jdk-8-alpine', ttyEnabled: true, command: 'cat'),
],
volumes: [
persistentVolumeClaim(mountPath: '/root/.m2', claimName: 'maven-m2'),
persistentVolumeClaim(mountPath: '/home/jenkins/agent/workspace', claimName: 'workspace'),
]
)
{
node("jnlp-slave"){
stage('Build'){
git branch: 'master', url: 'http://root:qrGw1S_azFE3F77Rs7tA@gitlab.gemantic.com/java/$JOB_NAME.git'
container('maven') {
stage('Build a Maven project') {
sh 'mvn clean package -U deploy'
}
}
}
stage('deploy'){
sshPublisher(publishers: [sshPublisherDesc(configName: '76', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '/data/script/jenkins.sh $JOB_NAME', execTimeout: 120000000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/data/kubernetes/service/$JOB_NAME', remoteDirectorySDF: false, removePrefix: 'target', sourceFiles: 'target/$JOB_NAME*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}
}
Pipeline解读:
-
podTemplate创建了一个Pod模版。Cloud字段指定了连接哪个Kubernetes云,Kubernetes就是刚才创建一个一个Kubernetes,云的名字就是kubernetes。
-
Maven镜像为了加快下载速度,我传到了私有仓库,官方镜像就是把IP地址去掉对应的镜像。
-
persistentVolumeClaim定义了目录挂载,把Maven构建的缓存目录.m2和构建产生的数据目录workspace都挂载了一下
-
下面的Pipeline指定后面的操作在jnlp-slave中(也就是Pod模版同时也是Slave节点)
-
在build操作中,需要先拉取代码,GitLab拉取代码这里使用了GitLab的root token进行拉取的。GitLab用户获取Token方法:
-
下面就是开始编译啦~,因为是一个Java服务,编译完成后会生成一个jar包。
-
deploy步骤就是开始发布了,下面的Pipeline是用流水线语法自动生成的。
-
然后点击构建进行测试。
-
构建过程中,可以看到Pod调度到master3上进行构建了。
-
构建过程中用到了两个镜像,一个Maven(已被上传到了私有仓库),一个inbound-agent镜像。inbound-agent镜像是官方的镜像,和Maven的关系是都在同一个Pod*享数据,并和Jenkins-master进行交互。(inbound-agent镜像怎么修改为私有仓库镜像还没搞明白,总是去公网下载速度慢)
-
构建过程中不断的下载Java程序依赖的各种包,因为是第一次时间久了一点,但是我们已经把.m2缓存目录挂载出来了,下次再次构建的时候就可以大大缩减构建的时间。
-
workspace也被挂载了出来,每次构建的数据也会保留,以备不时之需。
构建成功后查看NFS共享目录中的数据:
root@sa-storage:/data/dev_jenkins# du -sh m2/
218M m2/
root@sa-storage:/data/dev_jenkins# du -sh workspace/
65M workspace/
至此所有的需求都实现了,Slave实现了动态伸缩,相关的目录都被挂载出来了。
排错
kubectl get pod -n kubernetes-plugin -o wide命令可以查看Slave的Pod状态,如果出现问题Slave一直无限重启,需要查看Pod日志。
kubectl logs `kubectl get pod -n kubernetes-plugin -o wide|grep jnlp-slave|awk '{print $1}'` -n kubernetes-plugin
每次重启Pod的名字都会重新生成,而且正在创建中的Pod是无法查看日志的,就算有问题Pod也是瞬间就重启了,所以只能上面的这个命令无限的刷。手速快的可以手动哦~手速跟不上的也可以写个循环哒。主要就是文中说的那个大坑,那个坑过去,小问题都可以通过看日志解决的。如果忘记大坑在哪里,可以ctrl+f搜索关键字 “大坑” 哦~