本文章案例可用于参考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=|--group=]
---
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 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
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:[email protected]/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搜索关键字 “大坑” 哦~