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

Jenkins for Kubernetes实现Slave动态伸缩_编程

image

单击之,添加一个云:

Jenkins for Kubernetes实现Slave动态伸缩_编程_02

image
  • 名称:名字随便取,后面连接云的时候需要这个名字。
  • 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方法:

    Jenkins for Kubernetes实现Slave动态伸缩_技术宅_03

    image
  • 下面就是开始编译啦~,因为是一个Java服务,编译完成后会生成一个jar包。

  • deploy步骤就是开始发布了,下面的Pipeline是用流水线语法自动生成的。

    Jenkins for Kubernetes实现Slave动态伸缩_计算机_04

    image
  • 然后点击构建进行测试。

    Jenkins for Kubernetes实现Slave动态伸缩_Java_05

    image
  • 构建过程中,可以看到Pod调度到master3上进行构建了。

  • 构建过程中用到了两个镜像,一个Maven(已被上传到了私有仓库),一个inbound-agent镜像。inbound-agent镜像是官方的镜像,和Maven的关系是都在同一个Pod中共享数据,并和Jenkins-master进行交互。(inbound-agent镜像怎么修改为私有仓库镜像还没搞明白,总是去公网下载速度慢)

  • 构建过程中不断的下载Java程序依赖的各种包,因为是第一次时间久了一点,但是我们已经把.m2缓存目录挂载出来了,下次再次构建的时候就可以大大缩减构建的时间。

  • workspace也被挂载了出来,每次构建的数据也会保留,以备不时之需。

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

Jenkins for Kubernetes实现Slave动态伸缩_技术宅_06

image
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搜索关键字