Jenkins-Pipeline实践浅谈
Pipeline as Code
了解Jenkins的人相信对pipeline都有所耳闻,pipeline是Jenkins2.0推出的一套Groovy DSL语法,将原本独立运行于多个Job或者多个节点的任务统一使用代码的形式进行管理和维护,这样的好处比较明显
- 将复杂的Job之间的调用关系可视化,减少复杂的Job上下游关系的维护成本
- 使用Code的方式进行管理会非常容易进行功能的维护和扩展
本文不会具体的去介绍Pipeline的用法,重点会分享一下如何组织自己的Pipeline框架,使Pipeline使用起来更加灵活,如果想了解基础相关知识的童鞋可以移步 Using a Jenkinsfile
Jenkinsfile之伤
在Pipeline中至关重要的就是Jenkinsfile,类似Dockerfile,包含了Job所有需要执行的步骤,根据Pipeline的使用规则,我们常常需要把Jenkinsfile文件放到对应工程代码的根目录,Jenkins在获取工程代码后会解析Jenkinsfile,然后根据代码顺序执行下去,达到持续集成的效果。
但是这种模式有一个致命的缺点:
所有的Jenkinsfile分散到不同的工程中,对于后期的维护和功能扩展成本会非常高
主要的成本来自四个方面
- 我们在建立一个新工程的时候需要重新进行开发一遍Pipeline代码,即使你可以是copy的方式解决,但是依然会浪费一部分工作量,也不太符合code的理念(我们都希望尽可能的复用函数和功能, 而不是重复的造轮子 )
- 假如我们期望在build、deploy、测试完成后增加一个新功能(或者我们deploy的环境改变了);例如笔者最近遇到的新需求--"静态代码检查",我们期望给每一个工程都加上sonarqube检查,对于当前的解决方案来说, 我们需要在64个工程增加此项功能并commit 。
- 第三种情况,假如我们要针对不同的branch需要区分不同的行为,例如:我们期望所有的工程非master分支和develop分支不进行自动化测试, 我们需要在不同的分支改动,并且在merge回master的时候常常还需要处理冲突
- 假如某一个工程需要迭代某个功能,我们需要将Jenkinsfile 从master merge到所有的branch
在面对这些需求的时候,这样的设计导致维护成本实在太高。
Default Jenkinsfile
很幸运,我们找到了一个插件 Pipeline Multibranch Defaults Plugin ,
这个插件是在 Pipeline Multibranch Plugin 基础上增加一个Default Jenkinsfile的功能,可以将Default Jenkinsfile应用到所有的分支,减少多分支模式的Jenkinsfile的同步问题。
这样虽然解决了多分支同步问题,但是依然不能解决跨工程同步问题
Design
为了彻底的解决Pipeline的硬伤,我们需要转变Pipeline的用法
- 所有的Jenkinsfile文件不能分散到不同的工程中,需要统一的在一个git进行管理和维护
- 所有的基础功能需要模块化,将这些功能封装,供上层调用
- Caller只需要根据工程的特殊需求调用不同的模块功能,最终完成所有步骤的执行控制
这样的做法可以让所有的文件在同一个地方维护,降低跨工程的维护成功,模块化后的函数可复用性高,具体的功能迭代也可以减少对所有Jenkinsfile的维护成本,彻底的解决Jenkinsfile之伤
YM Jenkinsfile
为什么叫YM Jenkinsfile?因为俺们小公司的缩写是YM(逃)
我们有一个统一管理所有Jenkinsfile的git工程叫pipeline.git
- 图中的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