Jenkins for Kubernetes实现Slave动态伸缩


本文章案例可用于参考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。
1.png

单击之,添加一个云:
2.png

  • 名称:名字随便取,后面连接云的时候需要这个名字。
  • 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方法:
    3.png
  • 下面就是开始编译啦~,因为是一个Java服务,编译完成后会生成一个jar包。
  • deploy步骤就是开始发布了,下面的Pipeline是用流水线语法自动生成的。
    4.png
  • 然后点击构建进行测试。
    5.png
  • 构建过程中,可以看到Pod调度到master3上进行构建了。
  • 构建过程中用到了两个镜像,一个Maven(已被上传到了私有仓库),一个inbound-agent镜像。inbound-agent镜像是官方的镜像,和Maven的关系是都在同一个Pod中共享数据,并和Jenkins-master进行交互。(inbound-agent镜像怎么修改为私有仓库镜像还没搞明白,总是去公网下载速度慢)
  • 构建过程中不断的下载Java程序依赖的各种包,因为是第一次时间久了一点,但是我们已经把.m2缓存目录挂载出来了,下次再次构建的时候就可以大大缩减构建的时间。
  • workspace也被挂载了出来,每次构建的数据也会保留,以备不时之需。


构建成功后查看NFS共享目录中的数据:
6.png

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搜索关键字 “大坑” 哦~

原文链接:https://blog.csdn.net/qq_36165 ... 33259

0 个评论

要回复文章请先登录注册