Jenkins master位于k8s集群外,实现jenkins slave的动态构建

简述

Jenkins基于"kubernetes plugin"与k8s集成,可以使Jenkins slave以pod的形式在k8s集群内部动态构建、运行、销毁等。

通过 jenkinsci/kubernetes-plugin 了解到,Jenkins master既可以运行在k8s集群内,也可运行在k8s集群外,但是Jenkins slave的整个生命周期都是在k8s集群内,并且通过JNLP与Jenkins master连接。

要想Jenkins master在k8s运行,我们必须提前创建StatefulSet、Service、Ingress、ServiceAccount等系列yaml文件进行部署;而实际Jenkins master在生产中先于k8s使用并已独立运行,如果再次在K8S内部部署,那么我们还需进行迁移,增加了工作量。既然"kubernetes plugin"支持Jenkins master在k8s集群外部,那么就不必要再在k8s中创建了

下面我们就来详细介绍下jenkins master位于k8s集群外,实现jenkins slave的动态构建,其中有很多细节问题牵扯到docker、k8s的使用问题,我们一一讲解。

环境

IP 角色
192.168.3.217 k8s master
192.168.3.218 k8s node
192.168.3.219 k8s node
192.168.3.133 jenkins master
  1. 三节点的k8s集群已经提前部署完毕
  2. jenkins master 服务端口为8080;agent的端口为5000,用于agent通过JNLP与master连接

k8s准备与规划

1.分配namespace

由于Jenkins slave运行在k8s集群内,为方便区分我们为其分配devops的命名空间,日后运维相关的操作都可以在此命名空间中进行。

kubectl create ns devops

2.rbac授权

Jenkins通过kubernetes-plugin对k8s进行操作,需要在k8s内提前进行rbac授权。为方便管理,我们为其绑定cluster-admin角色。当然也可以进一步缩小使用权限。

#创建serviceaccounts
kubectl create sa jenkins
#对jenkins做cluster-admin绑定
kubectl create clusterrolebinding jenkins --clusterrole cluster-admin --serviceaccount=devops:jenkins

3.获取token

kubernetes-plugin与k8s连接时,并不是直接使用serviceaccount,而是通过token。因此我们需要获取serviceaccount:jenkins对应的token,而此token是经过base64加密过的,必须解密后才能使用。

# 1.查看sa
# kubectl get sa -n devops
NAME      SECRETS   AGE
default   1         113m
jenkins   1         19m

# 2.查看secret
# kubectl get sa jenkins -n devops -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2020-07-28T08:27:55Z"
  name: jenkins
  namespace: devops
  resourceVersion: "14403390"
  selfLink: /api/v1/namespaces/devops/serviceaccounts/jenkins
  uid: 43a98176-faa1-43f7-ad91-0352ca2dce2c
secrets:
- name: jenkins-token-44jkm

# 3.获取token,从yaml中得到token
# kubectl get secret  jenkins-token-44jkm -n devops -o yaml
...省略...
token: ZXlKaGJHY2lPaUpTV...
...省略...

# 4.token解密
# 由于此token是经过base64加密的,我们需要通过base64解密获取token值
# echo "xxxxxxxx" |base64 -d

4.添加认证

获取到的token解密值,需要在Jenkins master中添加为secret text类型的secret,才能被kubernetes-plugin使用。
Jenkins master位于k8s集群外,实现jenkins slave的动态构建_第1张图片

5.创建PV

通过构建时动态生成的Jenkins slave可以看出需要pvc会自动匹配pv,实现/home/jenkins/agent的存储挂载,因此我们需要提前创建pv,否则将会导致Jenkins slave无法成功创建。

注意:以下信息中的jenkins-pv就是我们已经提前传建好的pv。

# 查看pvc
# kubectl describe pvc pvc-jenkins-slave-m4ptp -n devops
Name:          pvc-jenkins-slave-m4ptp
Namespace:     devops
StorageClass:  
Status:        Bound
Volume:        jenkins-pv
Labels:        jenkins=slave
Annotations:   pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      15Gi
Access Modes:  RWO
VolumeMode:    Filesystem
Mounted By:    jenkins-slave-m4ptp
Events:        <none>

我们在master创建nfs主目录,但是在主目录下通过子目录对k8s中的服务提供存储,这样可以通过子目录对所有服务的资源进行隔离

注意:此时的accessModes为ReadWriteOnce,需要和上面pvc的Access Modes: RWO一致。下面我会演示不一致情况下出现的问题。

# 1.master上创建nfs主目录
mkdir -p /App/nfs
# 2. jenkins子目录作为jenkins slave的工作目录
mkdir -p /App/nfs/jenkins
# 3.nfs服务,还可创建其他子目录可以为其他服务提供目录挂载
# vim /etc/exports
/App/nfs *(rw,no_root_squash,no_all_squash,sync)
# 4.创建pv,这样k8s集群中的服务可自行匹配绑定pv
# vim jenkins-storage.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins-pv
spec:
  persistentVolumeReclaimPolicy: Recycle
  capacity:
    storage: 15Gi
  accessModes:
    - ReadWriteOnce
  nfs:
    server: 192.168.3.217
    path: /App/nfs/jenkins

至此我们已经提前将准备工作完成了,后续的操作需要配置Jenkins master了。

注意:
如果此时我们还没有Jenkins master的话,可以参考以下链接在k8s中部署Jenkins master。

https://github.com/jenkinsci/kubernetes-plugin/tree/master/src/main/kubernetes

其中:

  • jenkins.yml是部署StatefulSet、Service、Ingress;
  • service-account.yml是添加ServiceAccount认证信息;

Jenkins Master配置

由于Jenkins master先于k8s存在并已独立运行,为避免再次在K8S部署而产生的迁移问题,我们将直接使用Jenkins master。

1.安装kubernetes插件

在这里插入图片描述

2.kubernetes plugin与k8s连接配置

  1. 添加kubernetes云
    “Manager Jenkins”-“Configure System”-“Cloud”
    Jenkins master位于k8s集群外,实现jenkins slave的动态构建_第2张图片以上为kubernetes plugin与k8s连接是配置,其中:
  • kubernetes地址:为k8s api server地址,通过调用apiserver操作k8s。可通过以下来查看:
# kubectl cluster-info
Kubernetes master is running at https://192.168.3.217:6443
KubeDNS is running at https://192.168.3.217:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://192.168.3.217:6443/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy
  • 凭据:kubernetes plugin可以通过key或凭据的方式与k8s进行认证,方便起见,我们采用凭据的方式,使用我们此前创建的secret text凭据,此时我们需要禁用HTTPS证书检查
  • kubernetes命令空间:使用我们提前规划的devops,同时serviceaccount也在此空间内。
  • Jenkins 地址:Jenkins master的地址。
  • Jenkins 通道:Jenkins slave通过此通道与Jenkins master连接,注意此为tcp连接,不要加上http

通过以上配置,我们使用连接测试即可测试kubernetes plugin与k8s是否能够正常连接。但是连接成功并不代表后续Jenkins slave就会如愿正常构建请继续耐心往下看

3.配置 pod template

k8s中最小单元为pod,在此我们定义Jenkins slave所在pod的信息。
Jenkins master位于k8s集群外,实现jenkins slave的动态构建_第3张图片其中:

  • 名称:pod名称,在k8s中实际名称为jenkins-slave-随机值。
  • 命令空间: pod运行在devops命名空间内。
  • 标签列表:此处标签即标识Jenkins agent的,如流水线中agent定义调度在哪个slave上运行。

4.配置 container template

容器模板是我们在pod中运行的容器,此处我们可理解为在pod中创建Jenkins slave容器。
当然此处我们也可不配置,kubernetes plugin将会默认使用jenkins/jnlp-slave:alpine镜像创建。但是kubernetes-plugin官方已停止维护此镜像,而统一使用jenkins/inbound-agent。因此我们需要进行重新设置。
Jenkins master位于k8s集群外,实现jenkins slave的动态构建_第4张图片其中:

  • 名称:pod中容器的名称,注意此处必须设置为jnlp,才能对镜像重写使用jenkins/inbound-agent,否则将会出现以下问题:
    k8s同时拉取jenkins/inbound-agentjenkins/jnlp-slave:alpine两个镜像,第一个为重写后的实际使用镜像,第二个为默认镜像,导致jenkins-slave无法正常运行,不断重复构建。
# kubectl describe pod jenkins-slave-3sgv0 -n devops
Events:
  Type     Reason            Age                From                   Message
  ----     ------            ----               ----                   -------
  Warning  FailedScheduling  57s                default-scheduler      persistentvolumeclaim "pvc-jenkins-slave-3sgv0" not found
  Warning  FailedScheduling  25s (x5 over 57s)  default-scheduler      running "VolumeBinding" filter plugin for pod "jenkins-slave-3sgv0": pod has unbound immediate PersistentVolumeClaims
  Normal   Scheduled         14s                default-scheduler      Successfully assigned devops/jenkins-slave-3sgv0 to uvmsvr-3-218
  Normal   Pulled            13s                kubelet, uvmsvr-3-218  Container image "jenkins/inbound-agent" already present on machine
  Normal   Created           12s                kubelet, uvmsvr-3-218  Created container jenkins-slave
  Normal   Started           12s                kubelet, uvmsvr-3-218  Started container jenkins-slave
  Normal   Pulled            12s                kubelet, uvmsvr-3-218  Container image "jenkins/jnlp-slave:alpine" already present on machine
  Normal   Created           12s                kubelet, uvmsvr-3-218  Created container jnlp
  Normal   Started           12s                kubelet, uvmsvr-3-218  Started container jnlp
  Normal   Killing           8s                 kubelet, uvmsvr-3-218  Stopping container maven
  Normal   Killing           8s                 kubelet, uvmsvr-3-218  Stopping container jnlp
  • Docker镜像:当名称设置为jnlp后,jenkins/inbound-agent即为重写后的镜像,否则默认使用jenkins/jnlp-slave:alpine
  • 工作目录:Jenkins slave的默认工作目录,构建时将会在此目录下创建workspace。
  • 运行的命令和命令参数: 其中运行的命令必须要留空,否则会重写镜像的默认entrypoint,导致agent 无法连接到master,下面我们会进行演示说明。
  • 资源限制:默认的容器是没有资源限制的,我们在此添加了cpu和memory限制,大家可根据实际情况进行修改。

5.Jenkins slave动态构建

“万事俱备,只欠东风”,接下来我们在Jenkins master上创建普通的流水线来测试下是否能够动态构建Jenkins slave来进行CI/CD。

pipeline {
  agent {
      label 'jenkins-slave-k8s'
  }
  stages {
      stage('test') {
          script {
              println "test"
          }
      }
  }
}

通过此流水线来验证Jenkins slave是否动态构建成功。
如果有问题,我们还需进一步排查。下面我将介绍下我所遇到的一些问题。

问题处理

问题信息的排查主要通过以下三种方式:

  • k8s集群错误信息:kubectl describe pod jenkins-slave-xxx -n devops
  • Jenkins master日志:Manage Jenkins--System Log--All Logs
  • node节点docker 日志:docker logs xxxxxx
  • node节点docker 信息:docker inspect xxxxx

由于Jenkins slave动态构建,一旦构建不成功,则会不断重建。如果手速不够快,将无法捕获有效的错误信息。

1.构建时k8s创建Jenkins slave失败

现象:
查看pod状态,发现jenkins-slave不断重建

# kubectl get pod -n devops
NAME                  READY   STATUS    RESTARTS   AGE
jenkins-slave-tsz20   0/2     Pending   0          54s

原因排查:

1.查看失败原因:找不到pvc
# kubectl describe pod jenkins-slave-tsz20 -n devops
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  74s                default-scheduler  persistentvolumeclaim "pvc-jenkins-slave-tsz20" not found
  Warning  FailedScheduling  12s (x2 over 74s)  default-scheduler  running "VolumeBinding" filter plugin for pod "jenkins-slave-tsz20": pod has unbound immediate PersistentVolumeClaims

2.查看pvc:无法创建pvc
# kubectl get pvc -n devops
NAME                      STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-jenkins-slave-v1tw8   Pending

3.查看pv:pvc和pv无法绑定
# kubectl describe pvc pvc-jenkins-slave-v1tw8 -n devops
Events:
  Type    Reason         Age                From                         Message
  ----    ------         ----               ----                         -------
  Normal  FailedBinding  11s (x4 over 44s)  persistentvolume-controller  no persistent volumes available for this claim and no storage class is set

原因: 由于pvc无法和pv绑定,无法为Jenkins slave分配存储,导致Jenkins slave创建失败。

具体分析: 从k8s内默认自动创建的jenkins slave 所使用的pvc 以及我们事先建好的pv来看,由于其"Access modes"不匹配将会导致pv和pvc无法绑定,从而使jenkins slave镜像创建不成功。
因此只要保证pv的access mode 和 默认pvc一致为RWO即可,而我当时pv设置为ReadWriteMany

# PV信息
# kubectl describe pv jenkins-pv
Name:            jenkins-pv
Labels:          <none>
Annotations:     Finalizers:  [kubernetes.io/pv-protection]
StorageClass:    
Status:          Available
Claim:           
Reclaim Policy:  Recycle
Access Modes:    RWX
VolumeMode:      Filesystem
Capacity:        15Gi
Node Affinity:   <none>
Message:         
Source:
    Type:      NFS (an NFS mount that lasts the lifetime of a pod)
    Server:    192.168.3.217
    Path:      /App/nfs
    ReadOnly:  false
Events:        <none>


# PVC信息
# kubectl describe pvc pvc-jenkins-slave-m4ptp -n devops
Name:          pvc-jenkins-slave-m4ptp
Namespace:     devops
StorageClass:  
Status:        Bound
Volume:        jenkins-pv
Labels:        jenkins=slave
Annotations:   pv.kubernetes.io/bind-completed: yes
               pv.kubernetes.io/bound-by-controller: yes
Finalizers:    [kubernetes.io/pvc-protection]
Capacity:      15Gi
Access Modes:  RWO
VolumeMode:    Filesystem
Mounted By:    jenkins-slave-m4ptp
Events:        <none>

解决: 将pv的accessModes 设置为ReadWriteOnce。

2.agent 无法连接到master

现象:
通过Jenkins master日志发现报如下信息:

Aug 05, 2020 11:04:52 AM INFO org.csanchez.jenkins.plugins.kubernetes.KubernetesLauncher launch
Waiting for agent to connect (0/100): jenkins-slave-kzxzg
Aug 05, 2020 11:04:53 AM INFO org.csanchez.jenkins.plugins.kubernetes.KubernetesLauncher launch
Waiting for agent to connect (1/100): jenkins-slave-kzxzg
Aug 05, 2020 11:04:54 AM INFO org.csanchez.jenkins.plugins.kubernetes.KubernetesLauncher launch
Waiting for agent to connect (2/100): jenkins-slave-kzxzg

在Jenkins master查看node状态:
Jenkins master位于k8s集群外,实现jenkins slave的动态构建_第5张图片原因排查:

  1. 此时我们根据提示在宿主机上单独执行java -jar agent.jar -jnlpUrl http://jenkins.test.cn/computer/jenkins-slave-p591m/slave-agent.jnlp -secret 6bd8d43952fe6dc199d95aa55cb975ad2ebde2648c2b260e8dfc1ea6a53042cb -workDir "/root"
    进一步查看是否能够运行成功。
    **注意:**此命令一定要在jenkins-slave-p591m存活期间执行,否则将运行不成功。
    Jenkins master位于k8s集群外,实现jenkins slave的动态构建_第6张图片通过输出可以看到,agent此时通过Jenkins 通道能够发现Jenkins master。如果此处有问题,请检查你的Jenkins master上的配置:
  • Manage Jenkins–Configure Global Security–Agents,端口是否设置为5000或其他
  • kubernetes 插件中Jenkins 通道是否设置为192.168.3.133:5000,注意不要加http
  1. 分析jenkins/inbound-agent镜像
ARG version=4.3-7-alpine
FROM jenkins/agent:$version

ARG version
LABEL Description="This is a base image, which allows connecting Jenkins agents via JNLP protocols" Vendor="Jenkins project" Version="$version"

ARG user=jenkins

USER root
COPY jenkins-agent /usr/local/bin/jenkins-agent
RUN chmod +x /usr/local/bin/jenkins-agent &&\
    ln -s /usr/local/bin/jenkins-agent /usr/local/bin/jenkins-slave
USER ${user}

ENTRYPOINT ["jenkins-agent"]

镜像默认通过entrypoint启动,通过docker run直接运行

# docker run jenkins/inbound-agent
two arguments required, but got []
java -jar agent.jar [options...] <secret key> <agent name>
 -agentLog FILE                        : Local agent error log destination
                                         (overrides workDir)
 -cert VAL                             : Specify additional X.509 encoded PEM
                                         certificates to trust when connecting

......输出省略......

可见镜像entrypoint设置的jenkins-agent就是运行类似上文的java -jar agent.jar -jnlpUrl http://jenkins.test.cn/computer/jenkins-slave-p591m/slave-agent.jnlp -secret 6bd8d43952fe6dc199d95aa55cb975ad2ebde2648c2b260e8dfc1ea6a53042cb -workDir "/root"来连接agent。

再次查看docker ps -a发现容器的entrypoint被重写为/bin/sh -c cat,导致默认的jenkins-agent无法运行。

# docker ps -a
CONTAINER ID        IMAGE                                                           COMMAND                  CREATED             STATUS                      PORTS               NAMES
c53e446efb44        d7be84a67382                                                    "/bin/sh -c cat"         6 seconds ago       Up 5 seconds                                    k8s_jnlp_jenkins-slave-hhx00_devops_246d322a-1bde-4fc8-b436-d0fb3b5b076e_0

解决:
务必将运行的命令留空,否则会重写镜像的entrypoint,而命令参数可有可无。
Jenkins master位于k8s集群外,实现jenkins slave的动态构建_第7张图片重新设置后,实际此问题仍存在,进一步排查发现:

# 在jenkins slave所在节点上查看容器日志
# docker logs 49f51724e101
Aug 05, 2020 3:06:23 AM hudson.remoting.jnlp.Main createEngine
INFO: Setting up agent: jenkins-slave-z1fqn
Aug 05, 2020 3:06:23 AM hudson.remoting.jnlp.Main$CuiListener <init>
INFO: Jenkins agent is running in headless mode.
Aug 05, 2020 3:06:23 AM hudson.remoting.Engine startEngine
INFO: Using Remoting version: 4.3
Exception in thread "main" java.io.IOException: The specified working directory should be fully accessible to the remoting executable (RWX): /home/jenkins/agent
  at org.jenkinsci.remoting.engine.WorkDirManager.verifyDirectory(WorkDirManager.java:249)
  at org.jenkinsci.remoting.engine.WorkDirManager.initializeWorkDir(WorkDirManager.java:201)
  at hudson.remoting.Engine.startEngine(Engine.java:288)
  at hudson.remoting.Engine.startEngine(Engine.java:264)
  at hudson.remoting.jnlp.Main.main(Main.java:284)
  at hudson.remoting.jnlp.Main._main(Main.java:279)
  at hudson.remoting.jnlp.Main.main(Main.java:231)

由此可见/home/jenkins/agent 没有写入权限,由于容器目录是通过pvc绑定pv,因此我们只需将pv的目录192.168.3.217:/App/nfs 的权限改为777,最终问题解决。

总结

通过k8s+jenkins的部署不仅仅是实现了CI/CD的需求,而且让我们发现了一些细节性问题,解决这些问题可以帮助我们更好的了解与使用k8s和docker:

  • serviceaccount与rbac授权
  • pv和pvc的匹配规则
  • docker entrypoint的作用

以上问题虽小,但其实是花了很长时间才解决的,在此分享出来,希望对大家有所帮助。

你可能感兴趣的:(K8S,k8s,jenkins)