概述

持续构建与发布是我们日常工作中必不可少的一个步骤,目前大多公司都采用 Jenkins 集群来搭建符合需求的 CI/CD 流程,然而传统的 Jenkins Slave 一主多从方式会存在一些痛点,比如:主 Master 发生单点故障时,整个流程都不可用了;每个 Slave 的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲;资源分配不均衡,有的 Slave 要运行的 job 出现排队等待,而有的 Slave 处于空闲状态;最后资源有浪费,每台 Slave 可能是实体机或者 VM,当 Slave 处于空闲状态时,也不会完全释放掉资源,提到基于Kubernete的CI/CD,可以使用的工具有很多,比如Jenkins、Gitlab CI已经新兴的drone之类的,我们这里会使用大家最为熟悉的Jenins来做CI/CD的工具

优点

Jenkins 安装完成了,接下来我们不用急着就去使用,我们要了解下在 Kubernetes 环境下面使用 Jenkins 有什么好处,我们知道持续构建与发布是我们日常工作中必不可少的一个步骤,目前大多公司都采用 Jenkins 集群来搭建符合需求的 CI/CD 流程,然而传统的 Jenkins Slave 一主多从方式会存在一些痛点,比如:

  • 主 Master 发生单点故障时,整个流程都不可用了
  • 每个 Slave 的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲
  • 资源分配不均衡,有的 Slave 要运行的 job 出现排队等待,而有的 Slave 处于空闲状态
  • 资源有浪费,每台 Slave 可能是物理机或者虚拟机,当 Slave 处于空闲状态时,也不会完全释放掉资源
    正因为上面的这些种种痛点,我们渴望一种更高效更可靠的方式来完成这个 CI/CD 流程,而 Docker 虚拟化容器技术能很好的解决这个痛点,又特别是在 Kubernetes 集群环境下面能够更好来解决上面的问题,下图是基于 Kubernetes 搭建 Jenkins 集群的简单示意图
    jenkinsdj1.png
    从图上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式运行在 Kubernetes 集群的 Node 上,Master 运行在其中一个节点,并且将其配置数据存储到一个 Volume 上去,Slave 运行在各个节点上,并且它不是一直处于运行状态,它会按照需求动态的创建并自动删除,这种方式的工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态
    那么我们使用这种方式带来了哪些好处呢?
  • 服务高可用,当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用
  • 动态伸缩,合理使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况
  • 扩展性好,当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node 到集群中,从而实现扩展
    是不是以前我们面临的种种问题在 Kubernetes 集群环境下面是不是都没有了啊?看上去非常完美

部署jenkins

Kubernetes 集群当中,新建jenkins.yaml文件

[root@k8s-master jenkins]# cat jenkins.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins
  namespace: jenkins
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
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: ["secrets"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jenkins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins
subjects:
- kind: ServiceAccount
  name: jenkins
  namespace: jenkins
---
apiVersion: v1
kind: Service
metadata:
  name: jenkins
  namespace: jenkins
spec:
  selector:
    app: jenkins
  type: ClusterIP
  ports:
  - name: http
    port: 8080
    targetPort: 8080
    protocol: TCP
  - name: agent
    port: 50000
    protocol: TCP
    targetPort: 50000
---
apiVersion: apps/v1
kind: Deployment  
metadata:  
  name: jenkins
  namespace: jenkins
spec:  
  replicas: 1 
  selector: 
    matchLabels:
      app: jenkins
  strategy:  
    type: RollingUpdate  
    rollingUpdate:  
      maxSurge: 2  
      maxUnavailable: 0  
  template:  
    metadata:  
      labels:  
        app: jenkins  
    spec:
      securityContext:
        fsGroup: 1000
      serviceAccountName: jenkins
      containers:  
      - name: jenkins
#选其一即可(一个包含blueocean)       
       #image: jenkins/jenkins:lts-alpine 
        image: jenkinsci/blueocean:latest
        imagePullPolicy: IfNotPresent  
        ports:  
        - containerPort: 8080  
          name: web  
          protocol: TCP  
        - containerPort: 50000  
          name: agent  
          protocol: TCP  
        volumeMounts:  
        - name: jenkins-home
          mountPath: /var/jenkins_home
        env:  
        - name: LIMITS_MEMORY
          valueFrom:
            resourceFieldRef:
              resource: limits.memory
              divisor: 1Mi
        - name: JAVA_OPTS
          value: -Xmx$(LIMITS_MEMORY)m -XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85 -Duser.timezone=Asia/Shanghai
      volumes:  
        - name: jenkins-home
          persistentVolumeClaim:
            claimName: jenkins-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-data
  namespace: jenkins
  labels:
    app: jenkins
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi
  storageClassName: managed-nfs-storage

为了方便我们测试,我们这里通过 ingress的形式来访问Jenkins 的 web 服务,Jenkins 服务端口为8080,50000 端口为agent,这个端口主要是用于 Jenkins 的 master 和 slave 之间通信使用的(jenkins-ingress.yaml)

[root@k8s-master jenkins]# cat jenkins-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: jenkins-ing
  namespace: jenkins
spec:
  rules:
  - host: jenkins.k8s.com
    http:
      paths:
      - backend:
          serviceName: jenkins
          servicePort: 8080
        path: /

一切准备的资源准备好过后,我们直接创建 Jenkins 服务

[root@k8s-master jenkins]# kubectl create ns jenkins
namespace/jenkins created
[root@k8s-master jenkins]# kubectl apply -f jenkins.yaml
serviceaccount/jenkins created
role.rbac.authorization.k8s.io/jenkins created
rolebinding.rbac.authorization.k8s.io/jenkins created
deployment.extensions/jenkins created
service/jenkins created
persistentvolumeclaim/jenkins-data created
[root@k8s-master jenkins]# kubectl apply -f jenkins-ingress.yaml
ingress.extensions/jenkins-ing created

创建完成后,要去拉取镜像可能需要等待一会儿,然后我们查看下Pod的状态

[root@k8s-master jenkins]# kubectl get -n jenkins po
NAME                       READY   STATUS    RESTARTS   AGE
jenkins-5986dd847f-w5ktn   1/1     Running   0          69s
[root@k8s-master jenkins]# kubectl get -n jenkins svc
NAME      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)              AGE
jenkins   ClusterIP   10.110.195.139   <none>        8080/TCP,50000/TCP   72s
[root@k8s-master jenkins]# kubectl get -n jenkins ing
NAME          HOSTS                   ADDRESS   PORTS   AGE
jenkins-ing   jenkins.k8s.com            80      70s

根据ingress 服务域名(jenkins.k8s.com)就可以访问 jenkins 服务了,可以根据提示信息进行安装配置即可
jenkinsdj2.png
初始化的密码我们可以在 jenkins 的容器的日志中进行查看,也可以直接在 nfs 的共享数据目录中查看

[root@k8s-master jenkins]# kubectl logs -n jenkins jenkins-5986dd847f-w5ktn 
......
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:

5074bcbde59a45da837778399bd3d693

This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
......
#或者在nfs上查看
[root@nfs ~]# cat /storage/jenkins-jenkins-data-pvc-4aaf2d86-0fff-4850-be6e-233b7cbad151/secrets/initialAdminPassword
5074bcbde59a45da837778399bd3d693

然后选择安装推荐的插件即可
jenkinsdj3.png
安装完成后添加管理员帐号即可进入到 jenkins 主界面: jenkins home
jenkinsdj4.png

配置jenkins

接下来我们就需要来配置 Jenkins,让他能够动态的生成 Slave 的 Pod
jenkins依赖插件清单

  • kubernetes
  • managed scripts

第1步. 我们需要安装kubernetes plugin, 点击 Manage Jenkins -> Manage Plugins -> Available -> Kubernetes plugin 勾选安装即可, kubernetes plugin
jenkinsbs1.png
第2步. 安装完毕后,点击 Manage Jenkins —> Configure System —> (拖到最下方)Add a new cloud —> 选择 Kubernetes,然后填写 Kubernetes 和 Jenkins 配置信息
jenkinsbs2.png
注意 namespace,我们这里填 jenkins,然后点击Test Connection,如果出现 Connection test successful 的提示信息证明 Jenkins 已经可以和 Kubernetes 系统正常通信了,然后下方的 Jenkins URL 地址:http://jenkins.jenkins.svc.cluster.local:8080,这里的格式为:服务名.namespace.svc.cluster.local:8080,根据上面创建的jenkins 的服务名填写

第3步. 配置 Pod Template,其实就是配置 Jenkins Slave 运行的 Pod 模板,命名空间我们同样是用jenkins,Labels 这里也非常重要,对于后面执行 Job 的时候需要用到该值,然后我们这里使用的是 cnych/jenkins:jnlp 这个镜像,这个镜像是在官方的jnlp镜像基础上定制的,加入了kubectl等一些实用的工具
注意:由于新版本的 Kubernetes 插件变化较多,如果你使用的 Jenkins 版本在 2.176.x 版本以上,注意将上面的镜像替换成cnych/jenkins:jnlp6,否则使用会报错
jenkinsbs3.png
另外需要注意我们这里需要在下面挂载一个主机目录,一个是 /var/run/docker.sock,该文件是用于 Pod 中的容器能够共享宿主机的 Docker,这就是大家说的 docker in docker 的方式,Docker 二进制文件我们已经打包到上面的镜像中了,如果在slave agent中想要访问kubernetes 集群中其他资源,我们还需要绑定之前创建的Service Account 账号:jenkins
jenkinsbs4.png

jenkinsbs5.png
另外还有几个参数需要注意,如图中的Time in minutes to retain slave when idle,这个参数表示的意思是当处于空闲状态的时候保留 Slave Pod 多长时间,这个参数最好我们保存默认就行了,如果你设置过大的话,Job 任务执行完成后,对应的 Slave Pod 就不会立即被销毁删除,,到这里我们的 Kubernetes Plugin 插件就算配置完成了

测试

Kubernetes 插件的配置工作完成了,接下来我们就来添加一个 Job 任务,看是否能够在 Slave Pod 中执行,任务执行完成后看 Pod 是否会被销毁
在 Jenkins 首页点击create new jobs,创建一个测试的任务,输入任务名称,然后我们选择 Freestyle project 类型的任务:haimaxy-jnlp-slave-demo
jenkinsbs6.png
注意在下面的 Label Expression 这里要填入haimaxy-jnlp,就是前面我们配置的 Slave Pod 中的 Label,这两个地方必须保持一致
jenkinsbs7.png
然后往下拉,在 Build 区域选择Execute shell,输入测试命令,点击保存
jenkinsbs8.png

echo "测试 Kubernetes 动态生成 jenkins slave"
echo "==============docker in docker==========="
docker info

echo "=============kubectl============="
kubectl get pods -n kube-system

直接build创建的jenkins-demo,观察 Kubernetes 集群中 Pod 的变化
jenkinsbs9.png
可以看到在我们点击立刻构建的时候可以看到一个新的 Pod:jnlp-mqxkh 被创建了,这就是我们的 Jenkins Slave。任务执行完成后我们可以看到任务信息,比如我们这里是 花费了 3.3s 时间在 jnlp-mqxkh 这个 Slave上面
jenkinsbs10.png
同样也可以查看到对应的控制台信息:
jenkinsbs11.png
到这里证明我们的任务已经构建完成,然后这个时候我们再去集群查看我们的 Pod 列表,发现 jenkins这个namespace下面已经没有之前的Slave这个Pod了

文章作者: 鲜花的主人
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱吃可爱多
Kubernetes Jenkins Kubernetes Jenkins
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝