传统的 Jenkins Slave 方式存在的问题

传统的 Jenkins Slave 一主多从式会存在一些痛点。比如:

解决方案

基于上面的问题我们之前的改造方式是把 salve 做成镜像部署到 k8s 中, 比如我们的 UI 自动化所需要的 slave 就是测试 k8s 集群中一个 pod。 这样解决了我们上面说的第一个问题,就是如果节点出现故障,那么 k8s 会帮我们把这个 pod 迁移到其他可用节点,但是它无法很好的解决其他几个问题。 所以最终我们希望将 jenkins 和 k8s 进行整合。 达到如下的效果

上面是我再 jenkins 官网上下载的图。 抛开 master 节点的实现 (我们的 jenkins mater 没有部署在 k8s 中),在这里 jenkins 能调用 k8s 的接口,动态的在 k8s 中创建 pod 并作为 slave 运行我们的测试任务, 任务运行完毕后删除 pod。 并且它充分利用了 k8s 的特性, 创建的 pod 中负责与 jenkins master 连接的 slave 容器是 jenkins 团队发布的 docker 镜像 jenkins/jnlp-slave, 而且 jenkins 的 k8s 插件会自动帮助我们定义 pod 的配置,我们只需要定义一个基本的 pod 信息,jenkins 会把我们的配置与它自己的配置进行 merge。 比如, 在 sage-sdk-test 的 pipeline 中,我们是这么做的:

在 agent 部分跟以往不同的是我们提供了一个 k8s 的 pod 模板, 这样就是告诉 jenkins 我们这个 job 要跑在 k8s 上, 需要在 k8s 上运行这个 pod 然后当做 slave 节点,运行我们的 job。 其他的 pipeline 配置跟以前基本一样,这里只是把 slave 容器从以前的方式变成了现在的动态创建 pod 的方式。

技术细节

那么接下来我们看一下这里面的技术细节。 这个 pod 里面定义了两个容器, 一个是 jnlp, 这里需要注意一下。 jenkins 默认把名字叫 jnlp 的容器当做 slave 容器,所以如果你想要更换这个镜像的话, 就像上面做的一样即可。 也就是我们自己写一个 dockerfile 然后继承 jenkins/jnlp-slave 对它进行扩展。 这样我们既保留了 slave 的能力又扩展了自己的运行依赖。 比如下面我做的:

之所以要扩展 jenkins/jnlp-slave 主要有三个原因:

那么在 sdk 这个测试项目中, 我们需要测试 python 的 sdk, 所以需要一个 python3.7 的镜像来启动容器。 dockerfile 如下:

很简单, 直接使用 python3.7 的官方镜像,我们的扩展只是在镜像里安装一些 pytest 的依赖,这样容器启动后不用实时下载,运行速度会更快。 这样我们通过 python 容器和 jnlp 容器组合成了我们跑 pipeline 需要的运行环境了。 我们看看运行起来后启动的 pod 是什么样子的:

---
apiVersion: "v1"
kind: "Pod"
metadata:
  annotations:
    buildUrl: "http://m7-qa-test03:8081/job/sage-sdk-test/109/"
  labels:
    qa: "python3"
    jenkins: "slave"
    jenkins/label: "sage-sdk-test_109-hpf67"
  name: "sage-sdk-test-109-hpf67-tr47k-95sch"
spec:
  containers:
  - command:
    - "cat"
    image: "registry.4paradigm.com/qa/python3"
    name: "python3"
    tty: true
    volumeMounts:
    - mountPath: "/home/jenkins/agent"
      name: "workspace-volume"
      readOnly: false
  - env:
    - name: "JENKINS_SECRET"
      value: "********"
    - name: "JENKINS_AGENT_NAME"
      value: "sage-sdk-test-109-hpf67-tr47k-95sch"
    - name: "JENKINS_NAME"
      value: "sage-sdk-test-109-hpf67-tr47k-95sch"
    - name: "JENKINS_AGENT_WORKDIR"
      value: "/home/jenkins/agent"
    - name: "JENKINS_URL"
      value: "http://m7-qa-test03:8081/"
    image: "registry.4paradigm.com/tester_jenkins_slave:v1"
    name: "jnlp"
    volumeMounts:
    - mountPath: "/home/jenkins/agent"
      name: "workspace-volume"
      readOnly: false
  imagePullSecrets:
  - name: "docker4paradigm"
  nodeSelector:
    beta.kubernetes.io/os: "linux"
  restartPolicy: "Never"
  securityContext: {}
  volumes:
  - emptyDir:
      medium: ""
    name: "workspace-volume"

这个是从启动的 pod 的配置信息。 这里我们要关注以下几点:

在 pipeline 中切换运行容器

在这里我们依然用 sage sdk 项目为例子看一下, 其实非常简单。 只需要在 pipeline 中的 steps 里使用 container 指令就可以了。

最佳实践

OK, 通过以上技术细节我们就可以总结出一些最佳实践。

下面列一下我们在做 sdk 的兼容性测试的实践方式。

library 'qa-pipeline-library'



pipeline{
    parameters {
        choice(name: 'PLATFORM_FILTER', choices: ['python352', 'python368', 'python376','all'], description: '选择测试的 python 版本')
    }
    agent{
        kubernetes{
            yaml """
            apiVersion: v1
            kind: Pod
            metadata:
              labels:
                qa: python3
            spec:
              containers:
              - name: python352
                image: python:3.5.2
                command:
                - cat
                tty: true
              - name: python368
                image: python:3.6.8
                command:
                - cat
                tty: true
              - name: python376
                image: python:3.7.6
                command:
                - cat
                tty: true
              - name: jnlp
                image: registry.4paradigm.com/tester_jenkins_slave:v1
              imagePullSecrets:
                - name: docker4paradigm
            """
        }
    }
    stages{
        stage('环境部署'){
            steps{
                echo 'deploy'
            }
        }
        stage('拉取测试代码'){
            steps{
                checkout([$class: 'GitSCM', branches: [[name: '*/release/3.8.2']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'LocalBranch', localBranch: 'sage-sdk-test']], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gaofeigitlab', url: 'https://gitlab.4pd.io/qa/sage-sdk-test.git']]])
            }
        }
        stage('sage sdk 功能测试 '){
            when { anyOf {
                    expression { params.PLATFORM_FILTER != 'all' }
                } }
            steps{
              container(params.PLATFORM_FILTER){
                  sh """
                  pip3 install -i http://pypi.4paradigm.com/4paradigm/dev/ --trusted-host pypi.4paradigm.com 'sage-sdk[builtin-operators]'
                  pip3 install -r requirements.txt
                  cd test
                  python3 -m pytest -n 5
                  """
              }
            }
        }
        stage('sage sdk 兼容性测试'){
            matrix {
                when { anyOf {
                    expression { params.PLATFORM_FILTER == 'all' }
                } }
                axes {
                    axis {
                        name 'PLATFORM'
                        values 'python352', 'python368','python376'

                    }
                }
                stages{
                    stage('兼容性测试开始 '){
                        steps{
                          container("${PLATFORM}"){
                              echo "Testing planform ${PLATFORM}"
                              sh """
                              pip3 install -i http://pypi.4paradigm.com/4paradigm/dev/ --trusted-host pypi.4paradigm.com 'sage-sdk[builtin-operators]'
                              pip3 install -r requirements.txt
                              cd test
                              python3 -m pytest -n 5
                              """
                          }
                        }
                    }
                }
            }

        }
    }
    post{
        always{
            allure commandline: 'allure2.13.1', includeProperties: false, jdk: '', results: [[path: 'test/allure-results']]
            sendEmail('sungaofei@4paradigm.com')
        }
    }
}

上面的实践很好了诠释了 jenkins 与 k8s 集成的优势,让我为大家娓娓道来。首先这个项目的需求是测试我们为用户提供的 python sdk, 这些 sdk 可以帮助用户在脱离 UI 操控我们的产品。并且我们需要支持 python3.5.2(包括 3.5.2)以上的所有版本。 所以在测试功能之外我们仍然要进行兼容性测试。所以在上面的 pipeline 中我们使用到了 matrix 指令,该指令里定义了要测试的所有的 python 的版本 (为了简便,我这里裁剪到 3 个 python 版本) 而 matrix 的能力就是使用我们提供的这些参数并发执行测试, 也就是说他会并发的执行 python3.5.3, 3.6.8 和 3.7.6 这 3 个 python 版本的测试用例。 那么这里我们便需要这 3 个 python 版本的环境了。 所以利用上面说的与 k8s 集成的功能。 我们使用这 3 个版本的官方镜像。

如上图所示,我们直接使用官方镜像,不需要自己的任何加工。 这样在兼容性测试方案中,也就是在 matrix 指令里去切换不同的容器来执行测试即可。

这样我们在 pipeline 中的测试流程就变成了下面这个图的样子:

如此, 这样的实践方式除了一开始我提到的各种好处外,我还想着重的提一下通过这种方式我们的兼容性测试的环境准备降低到了一个几乎 0 成本的程度。 不管我们想测试什么样的 python 版本,只需要在 pipeline 中指定启动相应版本的官方镜像即可。 而对比以前我们要为每个 python 版本制作一个 salve 镜像 这样的方式,实在是方便了许多。

尾声

jenins pipeline 与 k8s 集成为我们带来了强大的能力帮助我们更好的完成持续集成的工作。尤其是在大型项目中,每日的构建,测试等操作数以千计。我们需要维护非常多的环境和 slave 节点。 而今天介绍的这套工具将会为我们节省非常大的运维成本。 当然今天只讲到了 jenkins pipeline 与 k8s 集成后的使用方式 ,并没有写怎么打通 jenkins 与 k8s 的通信。 其实这里也是蛮重要的, 它的配置非常的有讲究。 涉及到了 k8s 比较核心的安全认证机制。很多同学在实践这套工具的时候都会卡在这里痛不欲生。 所以我下一次就专门写一篇帖子讲改怎么配置。


↙↙↙阅读原文可查看相关链接,并与作者交流