初试 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/jenkins:lts-alpine
        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

参考资料

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券