首发于 Yingmi Infra
Jenkins-Pipeline实践浅谈

Jenkins-Pipeline实践浅谈

Pipeline as Code

了解Jenkins的人相信对pipeline都有所耳闻,pipeline是Jenkins2.0推出的一套Groovy DSL语法,将原本独立运行于多个Job或者多个节点的任务统一使用代码的形式进行管理和维护,这样的好处比较明显

  1. 将复杂的Job之间的调用关系可视化,减少复杂的Job上下游关系的维护成本
  2. 使用Code的方式进行管理会非常容易进行功能的维护和扩展

本文不会具体的去介绍Pipeline的用法,重点会分享一下如何组织自己的Pipeline框架,使Pipeline使用起来更加灵活,如果想了解基础相关知识的童鞋可以移步 Using a Jenkinsfile

Jenkinsfile之伤

在Pipeline中至关重要的就是Jenkinsfile,类似Dockerfile,包含了Job所有需要执行的步骤,根据Pipeline的使用规则,我们常常需要把Jenkinsfile文件放到对应工程代码的根目录,Jenkins在获取工程代码后会解析Jenkinsfile,然后根据代码顺序执行下去,达到持续集成的效果。

但是这种模式有一个致命的缺点:

所有的Jenkinsfile分散到不同的工程中,对于后期的维护和功能扩展成本会非常高

主要的成本来自四个方面

  1. 我们在建立一个新工程的时候需要重新进行开发一遍Pipeline代码,即使你可以是copy的方式解决,但是依然会浪费一部分工作量,也不太符合code的理念(我们都希望尽可能的复用函数和功能, 而不是重复的造轮子
  2. 假如我们期望在build、deploy、测试完成后增加一个新功能(或者我们deploy的环境改变了);例如笔者最近遇到的新需求--"静态代码检查",我们期望给每一个工程都加上sonarqube检查,对于当前的解决方案来说, 我们需要在64个工程增加此项功能并commit
  3. 第三种情况,假如我们要针对不同的branch需要区分不同的行为,例如:我们期望所有的工程非master分支和develop分支不进行自动化测试, 我们需要在不同的分支改动,并且在merge回master的时候常常还需要处理冲突
  4. 假如某一个工程需要迭代某个功能,我们需要将Jenkinsfile 从master merge到所有的branch

在面对这些需求的时候,这样的设计导致维护成本实在太高。

Default Jenkinsfile

很幸运,我们找到了一个插件 Pipeline Multibranch Defaults Plugin

这个插件是在 Pipeline Multibranch Plugin 基础上增加一个Default Jenkinsfile的功能,可以将Default Jenkinsfile应用到所有的分支,减少多分支模式的Jenkinsfile的同步问题。

Default Jenkinsfile

这样虽然解决了多分支同步问题,但是依然不能解决跨工程同步问题

Design

为了彻底的解决Pipeline的硬伤,我们需要转变Pipeline的用法

  1. 所有的Jenkinsfile文件不能分散到不同的工程中,需要统一的在一个git进行管理和维护
  2. 所有的基础功能需要模块化,将这些功能封装,供上层调用
  3. Caller只需要根据工程的特殊需求调用不同的模块功能,最终完成所有步骤的执行控制

这样的做法可以让所有的文件在同一个地方维护,降低跨工程的维护成功,模块化后的函数可复用性高,具体的功能迭代也可以减少对所有Jenkinsfile的维护成本,彻底的解决Jenkinsfile之伤

YM Jenkinsfile

为什么叫YM Jenkinsfile?因为俺们小公司的缩写是YM(逃)

我们有一个统一管理所有Jenkinsfile的git工程叫pipeline.git

YM Jenkinsfile框架
  • 图中的Default Jenkins依然是使用 Pipeline Multibranch Defaults Plugin ,他是所有工程的默认的Jenkinsfile,所有的工程都共享此文件, 作为统一的入口文件拥有以下几个功能
    • 解析Job的名称,获取到对应工程的名称
    • git clone pipeline.git代码到本地
    • 通过load函数将对应工程的Jenkinsfile加载进来
    • 调用start函数执行对应工程的Jenkinsfile的逻辑
#!/usr/bin/env groovy
import groovy.transform.Field
@Field def job_name=""
@Field def jenkinsFile=""
node()
    // if job is building ...wait
    echo env.JOB_NAME
    job_name="${env.JOB_NAME}".replace('%2F', '/').replace('-', '/').replace('_', '/').split('/')
    job_name=job_name[0].toLowerCase()
    workspace="workspace/${job_name}/${env.BRANCH_NAME}"
    ws("$workspace")
        dir("pipeline")
            git url:"git@git.xxxx:yyyy/pipeline.git"
            def check_groovy_file="Jenkinsfile/${job_name}/Jenkinsfile.groovy"
            def default_groovy_file="Jenkinsfile/default/Jenkinsfile.groovy"
            jenkinsFile=load "${check_groovy_file}"
        jenkinsFile.start()
}
  • 在load完对应工程的Jenkinsfile后,统一调用入口函数 jenkinsFile.start()
  • 在对应工程Jenkinsfile的start函数中就可以完成工程的Pipeline code的编写了
  • 我们同时将底层的一些功能进行了封装,在具体的Jenkinsfile里面会load进来
common_util=load "Jenkinsfile/common_util.groovy" //加载底层模块,方便调用底层函数
  • 调用底层函数
common_util.git_clone() // clone代码
common_util.build_project("Java") // build docker image
common_util.deploy(namespace,"pmdj") //deploy image
common_util.sonarqube(services,services,"${services}/server","**/*.js","**/node_modules/**/*.js")
  • common_util功能,例如我们封装好了静态代码检查的功能
//静态代码检查
def sonarqube(project_name,project_key,sources=".",inclusions="",exclusions="",classes="",project_version="",extra_config=""){
    if (env.BRANCH_NAME != "develop" && env.BRANCH_NAME != "dev" && env.BRANCH_NAME != "birld") {
        return
    //临时关停sonarqube服务
    if (project_name != "ias") {
        return
    if (env.SONARQUBE_PROJECT_LIST.contains(project_name) == false){
        return
    def scannerHome = tool name: 'sonarqube scanner', type: 'hudson.plugins.sonar.SonarRunnerInstallation'
    def cmd = "${scannerHome}/bin/sonar-scanner -Dsonar.projectName=${project_name} -Dsonar.projectKey=${project_key} -Dsonar.host.url=${env.SONARQUBE_SERVER_URL} -Dsonar.sources=${sources} -Dsonar.login=${env.SONARQUBE_LOGIN_USER} -Dsonar.password=${SONARQUBE_LOGIN_PASSWORD}"
    if (inclusions != "") {
        cmd += " -Dsonar.inclusions=${inclusions}"
    if (exclusions != "") {
        cmd += " -Dsonar.exclusions=${exclusions}"
    if (classes != "") {
        cmd += " -Dsonar.java.binaries=${classes}"
    if (extra_config != "")
        cmd += extra_config
    withSonarQubeEnv('k8s-qe-sonarqube') {
        echo "$cmd"
        sh "$cmd"
}
  • coomon_var.groovy包含了一些配置相关的信息
#!groovy
env.QIEMAN_SERVICES_LIST=["pmdj","newaip","albus","seeker","qmwxrails","qmwxsidekiq","potrade"]
env.QIEMAN_AUTO_BUILD_BRANCH=["master","develop","test"]
env.SONARQUBE_PROJECT_LIST=["fbui","fdepgw","fispoms","goqs","ias","iasui","pmdj"]
return this

下面是所有工程的集中管理的pipeline目录结构,由于工程较多,只显示了部分文件夹

➜  pipeline git:(master) ✗ tree -L 2
├── Jenkinsfile
│   ├── account
│   ├── admin
│   ├── adminui
│   ├── albus
│   ├── athena