初试 Jenkins 使用 Kubernetes Plugin 完成持续构建与发布

目录

1、Jenkins CI/CD 背景介绍

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

由于以上种种痛点,我们渴望一种更高效更可靠的方式来完成这个 CI/CD 流程,而 Docker 虚拟化容器技术能很好的解决这个痛点,下图是基于 Kubernetes 搭建 Jenkins 集群的简单示意图。
这里写图片描述

从图上可以看到 Jenkins Master 和 Jenkins Slave 以 Docker Container 形式运行在 Kubernetes 集群的 Node 上,Master 运行在其中一个节点,并且将其配置数据存储到一个 Volume 上去,Slave 运行在各个节点上,并且它不是一直处于运行状态,它会按照需求动态的创建并自动删除。

这种方式的工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Docker Container 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且 Docker Container 也会自动删除,恢复到最初状态。

这种方式带来的好处有很多:

  • 服务高可用,当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。
  • 动态伸缩,合理使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况。
  • 扩展性好,当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node 到集群中,从而实现扩展。

2、环境、软件准备

本次演示环境,我是在本机 MAC OS 以及虚拟机 Linux Centos7 上操作,以下是安装的软件及版本:

  1. Docker: version 17.09.0-ce
  2. Oracle VirtualBox: version 5.1.20 r114628 (Qt5.6.2)
  3. Minikube: version v0.22.2
  4. Kuberctl:
    • Client Version: v1.8.1
    • Server Version: v1.7.5

注意:Minikube 启动的单节点 k8s Node 实例是需要运行在本机的 VM 虚拟机里面,所以需要提前安装好 VM,这里我选择 Oracle VirtualBox。k8s 运行底层使用 Docker 容器,所以本机需要安装好 Docker 环境,Minikube 和 Kuberctl 的安装过程可参考之前文章 初试 minikube 本地部署运行 kubernetes 实例

3、部署 Jenkins Server 到 Kubernetes

在执行部署之前,我们要确保 Minikube 已经正常运行,如果使用已搭建好的 Kubernetes 集群,也要确保正常运行。接下来,我们需要准备部署 Jenkins 的 Yaml 文件,可以参考
GitHub jenkinsci kubernetes-plugin 官网提供的 jenkins.yamlservice-account.yaml 文件,这里官网使用的是比较规范的 StatefulSet(有状态集群服务)方式进行部署,并配置了 Ingress 和 RBAC 账户权限信息。不过我本机测试的时候,发现 Volume 挂载失败,日志显示没有权限创建目录。所以我精简了一下,重新写了个以 Deployment 方式部署方式以及 Service 的配置文件(这里偷个懒,不使用 RBAC 认证了)。

$ cat jenkins-deployment.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: jenkins
  labels:
    k8s-app: jenkins
spec:
  replicas: 1
  selector:
    matchLabels:
      k8s-app: jenkins
  template:
    metadata:
      labels:
        k8s-app: jenkins
    spec:
      containers:
      - name: jenkins
        image: jenkins
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: jenkins-home
          mountPath: /var/jenkins_home
        ports:
        - containerPort: 8080 
          name: web
        - containerPort: 50000
          name: agent
      volumes:
        - name: jenkins-home
          emptyDir: {}
$ cat jenkins-service.yml
kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: jenkins
  name: jenkins
spec:
  type: NodePort
  ports:
    - port: 8080
      name: web
      targetPort: 8080
    - port: 50000
      name: agent
      targetPort: 50000
  selector:
    k8s-app: jenkins

说明一下:这里 Service 我们暴漏了端口 8080 和 50000,8080 为访问 Jenkins Server 页面端口,50000 为创建的 Jenkins Slave 与 Master 建立连接进行通信的默认端口,如果不暴露的话,Slave 无法跟 Master 建立连接。这里使用 NodePort 方式暴漏端口,并未指定其端口号,由 Kubernetes 系统默认分配,当然也可以指定不重复的端口号(范围在 30000~32767)。

接下来,通过 kubectl 命令行执行创建 Jenkins Service。

$ kubectl create namespace kubernetes-plugin
$ kubectl config set-context $(kubectl config current-context) --namespace=kubernetes-plugin
$ kubectl create -f jenkins-deployment.yaml
$ kubectl create -f jenkins-service.yml

说明一下:这里我们创建一个新的 namespace 为 kubernetes-plugin,并且将当前 context 设置为 kubernetes-plugin namespace 这样就会自动切换到该空间下,方便后续命令操作。

$ kubectl get service,deployment,pod
NAME      TYPE       CLUSTER-IP   EXTERNAL-IP   PORT(S)                          AGE
jenkins   NodePort   10.0.0.204   <none>        8080:30645/TCP,50000:31981/TCP   1m

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
jenkins   1         1         1            1           1m

NAME                      READY     STATUS    RESTARTS   AGE
jenkins-960997836-fff2q   1/1       Running   0          1m

此时,我们会发现 Jenkins Master 服务已经启动起来了,并且将端口暴漏到 8080:3064550000:31981,此时可以通过浏览器打开 http://<Cluster_IP>:30645 访问 Jenkins 页面了。当然也可以通过 minikube service ... 命令来自动打开页面。

$ minikube service jenkins -n kubernetes-plugin
Opening kubernetes service kubernetes-plugin/jenkins in default browser...
Opening kubernetes service kubernetes-plugin/jenkins in default browser...

在浏览器上完成 Jenkins 的初始化插件安装过程,并配置管理员账户信息,这里忽略过程,初始化完成后界面如下:
这里写图片描述

注意: 初始化过程中,让输入 /var/jenkins_home/secret/initialAdminPassword 初始密码时,因为我们设置的 emptyDir: {} 没有挂载到外部路径,可以进入到容器内部进行获取。

$ kubectl exec -it jenkins-960997836-fff2q cat /var/jenkins_home/secrets/initialAdminPassword

4、Jenkins 配置 Kubernetes Plugin

管理员账户登录 Jenkins Master 页面,点击 “系统管理” —> “管理插件” —> “可选插件” —> “Kubernetes plugin” 勾选安装即可。
这里写图片描述

安装完毕后,点击 “系统管理” —> “系统设置” —> “新增一个云” —> 选择 “Kubernetes”,然后填写 Kubernetes 和 Jenkins 配置信息。
这里写图片描述

说明一下:

  1. Name 处默认为 kubernetes,也可以修改为其他名称,如果这里修改了,下边在执行 Job 时指定 podTemplate() 参数 cloud 为其对应名称,否则会找不到,cloud 默认值取:kubernetes
  2. Kubernetes URL 处我填写了 https://kubernetes.default 这里我填写了 Kubernetes Service 对应的 DNS 记录,通过该 DNS 记录可以解析成该 Service 的 Cluster IP,注意:也可以填写 https://kubernetes.default.svc.cluster.local 完整 DNS 记录,因为它要符合 <svc_name>.<namespace_name>.svc.cluster.local 的命名方式,或者直接填写外部 Kubernetes 的地址 https://<ClusterIP>:<Ports>
  3. Jenkins URL 处我填写了 http://jenkins.kubernetes-plugin:8080,跟上边类似,也是使用 Jenkins Service 对应的 DNS 记录,不过要指定为 8080 端口,因为我们设置暴漏 8080 端口。同时也可以用 http://<ClusterIP>:<Node_Port> 方式,例如我这里可以填 http://192.168.99.100:30645 也是没有问题的,这里的 30645 就是对外暴漏的 NodePort。

配置完毕,可以点击 “Test Connection” 按钮测试是否能够连接的到 Kubernetes,如果显示 Connection test successful 则表示连接成功,配置没有问题。

5、测试并验证

好了,通过 Kubernetes 安装 Jenkins Master 完毕并且已经配置好了连接,接下来,我们可以配置 Job 测试一下是否会根据配置的 Label 动态创建一个运行在 Docker Container 中的 Jenkins Slave 并注册到 Master 上,而且运行完 Job 后,Slave 会被注销并且 Docker Container 也会自动删除吧!

5.1、pipeline 类型支持

创建一个 Pipeline 类型 Job 并命名为 my-k8s-jenkins-pipeline,然后在 Pipeline 脚本处填写一个简单的测试脚本如下:

def label = "mypod-${UUID.randomUUID().toString()}"
podTemplate(label: label, cloud: 'kubernetes') {
    node(label) {
        stage('Run shell') {
            sh 'sleep 130s'
            sh 'echo hello world.'
        }
    }
}

执行构建,此时去构建队列里面,可以看到有一个构建任务,暂时还没有执行中的构建,因为还没有初始化好,稍等一会,就会看到 Master 和 jenkins-slave-jbs4z-xs2r8 已经创建完毕,在等一会,就会发现 jenkins-slave-jbs4z-xs2r8 已经注册到 Master 中,并开始执行 Job,点击该 Slave 节点,我们可以看到通过标签 mypod-b538c04c-7c19-4b98-88f6-9e5bca6fc9ba 关联,该 Label 就是我们定义的标签格式生成的,Job 执行完毕后,jenkins-slave 会自动注销,我们通过 kubectl 命令行,可以看到整个自动创建和删除过程。
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

# jenkins slave 启动前,只有 jenkins master 服务存在
$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
jenkins-960997836-fff2q   1/1       Running   0          1d

# jenkins slave 自动创建完毕 
$ kubectl get pods
NAME                        READY     STATUS    RESTARTS   AGE
jenkins-960997836-fff2q     1/1       Running   0          1d
jenkins-slave-jbs4z-xs2r8   1/1       Running   0          56s

# Docker Container 启动服务情况
$ docker ps |grep jenkins
aa5121667601        jenkins/jnlp-slave                          "jenkins-slave bd880…"   About a minute ago   Up About a minute                       k8s_jnlp_jenkins-slave-jbs4z-xs2r8_kubernetes-plugin_25a91ed9-3337-11e8-a49f-08002744a3f1_0
d64deb0eaa20        gcr.io/google_containers/pause-amd64:3.0    "/pause"                 About a minute ago   Up About a minute                       k8s_POD_jenkins-slave-jbs4z-xs2r8_kubernetes-plugin_25a91ed9-3337-11e8-a49f-08002744a3f1_0
995c1743552a        jenkins                                     "/bin/tini -- /usr/l…"   27 hours ago         Up 26 hours                             k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d        gcr.io/google_containers/pause-amd64:3.0    "/pause"                 27 hours ago         Up 26 hours                             k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0

# jenkins slave 执行完毕自动删除
$ kubectl get pods
NAME                        READY     STATUS        RESTARTS   AGE
jenkins-960997836-fff2q     1/1       Running       0          1d
jenkins-slave-jbs4z-xs2r8   0/1       Terminating   0          2m
$ kubectl get pods
NAME                      READY     STATUS    RESTARTS   AGE
jenkins-960997836-fff2q   1/1       Running   0          1d
$ docker ps |grep jenkins
995c1743552a        jenkins                                     "/bin/tini -- /usr/l…"   27 hours ago        Up 26 hours                             k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d        gcr.io/google_containers/pause-amd64:3.0    "/pause"                 27 hours ago        Up 26 hours                             k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0

从上边的操作日志中,我们可以清晰的看到 Jenkins Slave 自动创建到注销删除的过程,整个过程是自动完成的,不需要人工干预。

5.2、Container Group 类型支持

创建一个 Pipeline 类型 Job 并命名为 my-k8s-jenkins-container,然后在 Pipeline 脚本处填写一个简单的测试脚本如下:

def label = "mypod-${UUID.randomUUID().toString()}"
podTemplate(label: label, cloud: 'kubernetes', containers: [
    containerTemplate(name: 'maven', image: 'maven:3.3.9-jdk-8-alpine', ttyEnabled: true, command: 'cat'),
  ]) {

    node(label) {
        stage('Get a Maven Project') {
            git 'https://github.com/jenkinsci/kubernetes-plugin.git'
            container('maven') {
                stage('Build a Maven project') {
                    sh 'mvn -B clean install'
                }
            }
        }
    }
}

注意:这里我们使用的 containers 定义了一个 containerTemplate 模板,指定名称为 maven 和使用的 Image,下边在执行 Stage 时,使用 container('maven'){...} 就可以指定在该容器模板里边执行相关操作了。比如,该示例会在 jenkins-slave 中执行 git clone 操作,然后进入到 maven 容器内执行 mvn -B clean install 编译操作。这种操作的好处就是,我们只需要根据代码类型分别制作好对应的编译环境镜像,通过指定不同的 container 来分别完成对应代码类型的编译操作。模板详细的各个参数配置可以参照 Pod and container template configuration

执行构建,跟上边 Pipeline 类似,也会新建 jenkins-slave 并注册到 master,不同的是,它会在 Kubernetes 中启动我们配置的 maven 容器模板,来执行相关命令。
这里写图片描述

这里写图片描述

$ kubectl get pods
NAME                        READY     STATUS    RESTARTS   AGE
jenkins-960997836-fff2q     1/1       Running   0          1d
jenkins-slave-k2wwq-4l66k   2/2       Running   0          53s
$ docker ps
CONTAINER ID        IMAGE                                       COMMAND                  CREATED              STATUS              PORTS               NAMES
8ed81ee3aad4        jenkins/jnlp-slave                          "jenkins-slave 4ae74…"   About a minute ago   Up About a minute                       k8s_jnlp_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
bd252f7e59c2        maven                                       "cat"                    About a minute ago   Up About a minute                       k8s_maven_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
fe22da050a53        gcr.io/google_containers/pause-amd64:3.0    "/pause"                 About a minute ago   Up About a minute                       k8s_POD_jenkins-slave-k2wwq-4l66k_kubernetes-plugin_90c2ee92-33ca-11e8-a49f-08002744a3f1_0
995c1743552a        jenkins                                     "/bin/tini -- /usr/l…"   44 hours ago         Up 44 hours                             k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d        gcr.io/google_containers/pause-amd64:3.0    "/pause"                 44 hours ago         Up 44 hours                             k8s_POD_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0

5.3、非 Pipeline 类型支持

Jenkins 中除了使用 Pipeline 方式运行 Job 外,通常我们也会使用普通类型 Job,如果也要想使用kubernetes plugin 来构建任务,那么就需要点击 “系统管理” —> “系统设置” —> “云” —> “Kubernetes” —> “Add Pod Template” 进行配置 “Kubernetes Pod Template” 信息。

这里写图片描述

注意:这里的 Labels 名在配置非 pipeline 类型 Job 时,用来指定任务运行的节点。Containers 下的 Name 字段的名字,这里要注意的是,如果 Name 配置为 jnlp,那么 Kubernetes 会用下边指定的 Docker Image 代替默认的 jenkinsci/jnlp-slave 镜像,否则,Kubernetes plugin 还是会用默认的 jenkinsci/jnlp-slave 镜像与 Jenkins Server 建立连接,即使我们指定其他 Docker Image。这里我随便配置为 jnlp-slave,意思就是使用默认的 jenkinsci/jnlp-slave 镜像来运行,因为我们暂时还没制作可以替代默认镜像的镜像。

新建一个自由风格的 Job 名称为 my-k8s-jenkins-simple,配置 “Restrict where this project can be run” 勾选,在 “Label Expression” 后边输出我们上边创建模板是指定的 Labels 名称 jnlp-agent,意思是指定该 Job 匹配 jnlp-agent 标签的 Slave 上运行。

这里写图片描述

执行构建后,跟上边 Pipeline 一样,符合我们的预期。

这里写图片描述

这里写图片描述

5.4、配置自定义 jenkins-slave 镜像

通过 kubernetest plugin 默认提供的镜像 jenkinsci/jnlp-slave 可以完成一些基本的操作,它是基于 openjdk:8-jdk 镜像来扩展的,但是对于我们来说这个镜像功能过于简单,比如我们想执行 Maven 编译或者其他命令时,就有问题了,那么可以通过制作自己的镜像来预安装一些软件,既能实现 jenkins-slave 功能,又可以完成自己个性化需求,那就比较不错了。如果我们从头开始制作镜像的话,会稍微麻烦些,不过可以参考 jenkinsci/jnlp-slavejenkinsci/docker-slave 这两个官方镜像来做,注意:jenkinsci/jnlp-slave 镜像是基于 jenkinsci/docker-slave 来做的。这里我简单演示下,基于 jenkinsci/jnlp-slave:latest 镜像,在其基础上做扩展,安装 Maven 到镜像内,然后运行验证是否可行吧。

创建一个 Pipeline 类型 Job 并命名为 my-k8s-jenkins-container-custom,然后在 Pipeline 脚本处填写一个简单的测试脚本如下:

def label = "mypod-${UUID.randomUUID().toString()}"
podTemplate(label: label, cloud: 'kubernetes',containers: [
    containerTemplate(
        name: 'jnlp', 
        image: 'huwanyang168/jenkins-slave-maven:latest', 
        alwaysPullImage: false, 
        args: '${computer.jnlpmac} ${computer.name}'),
  ]) {

    node(label) {
        stage('stage1') {
            stage('Show Maven version') {
                sh 'mvn -version'
                sh 'sleep 60s'
            }
        }
    }
}

说明一下:这里 containerTemplate 的 name 属性必须叫 jnlp,Kubernetes 才能用自定义 images 指定的镜像替换默认的 jenkinsci/jnlp-slave 镜像。此外,args 参数传递两个 jenkins-slave 运行需要的参数。还有一点就是这里并不需要指定 container('jnlp'){...} 了,因为它被 Kubernetes 指定了要被执行的容器,所以直接执行 Stage 就可以了。

执行构建,看下效果如何吧!

这里写图片描述

这里写图片描述

$ kubectl get pods
NAME                        READY     STATUS    RESTARTS   AGE
jenkins-960997836-fff2q     1/1       Running   0          2d
jenkins-slave-9wtkt-d2ms8   1/1       Running   0          12m
bj-m-204072a:k8s-gitlab wanyang3$ docker ps
CONTAINER ID        IMAGE                                       COMMAND                  CREATED             STATUS              PORTS               NAMES
b31be1de9563        huwanyang168/jenkins-slave-maven            "jenkins-slave 7cef1…"   12 minutes ago      Up About a minute                       k8s_jnlp_jenkins-slave-9wtkt-d2ms8_kubernetes-plugin_0ea4bc9d-33f3-11e8-a49f-08002744a3f1_0
b33b7ce3070e        gcr.io/google_containers/pause-amd64:3.0    "/pause"                 12 minutes ago      Up About a minute                       k8s_POD_jenkins-slave-9wtkt-d2ms8_kubernetes-plugin_0ea4bc9d-33f3-11e8-a49f-08002744a3f1_0
995c1743552a        jenkins                                     "/bin/tini -- /usr/l…"   2 days ago          Up 2 days                               k8s_jenkins_jenkins-960997836-fff2q_kubernetes-plugin_27b5c7b2-3256-11e8-a49f-08002744a3f1_0
024d43257e9d        gcr.io/google_containers/pause-amd64:3.0    "/pause"                 2 days ago          Up 2 days

当然,我们也可以使用非 Pipeline 类型指定运行该自定义 slave,那么我们就需要修改 “系统管理” —> “系统设置” —> “云” —> “Kubernetes” —> “Add Pod Template” 修改配置 “Kubernetes Pod Template” 信息如下:

这里写图片描述

然后同样在 Job 配置页面 “Label Expression” 后边输出我们上边创建模板是指定的 Labels 名称 jnlp-agent,就可以啦!测试妥妥没问题的。

最后,贴一下我自定义的预安装了 Maven 的 Jenkins-slave 镜像的 Dockerfile ,当然大家可以基于此预安装一些其他软件,来完成日常持续构建与发布工作吧。

FROM jenkins/jnlp-slave:latest

MAINTAINER huwanyang168@163.com

LABEL Description="This is a extend image base from jenkins/jnlp-slave which install maven in it."

# 切换到 root 账户进行操作
USER root

# 安装 maven-3.3.9
RUN wget http://mirrors.sonic.net/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz && \
    tar -zxf apache-maven-3.3.9-bin.tar.gz && \
    mv apache-maven-3.3.9 /usr/local && \
    rm -f apache-maven-3.3.9-bin.tar.gz && \
    ln -s /usr/local/apache-maven-3.3.9/bin/mvn /usr/bin/mvn && \
    ln -s /usr/local/apache-maven-3.3.9 /usr/local/apache-maven

USER jenkins

参考资料

猜你喜欢

转载自blog.csdn.net/aixiaoyang168/article/details/79767649