Jenkins知识集锦
Jenkin是一个自包含的CI/CD服务器,能够自动化构建、测试、交付、部署等软件工程生命周期中的任务。
| 术语 | 说明 | 
| 管线/Pipeline | 
 Jenkins Pipeline是一套插件,支持实现/集成持续交付流水线(continuous delivery pipelines)到Jenkins服务器 所谓持续交付流水线,是从CVS签出代码,直到将最终软件呈现给用户的自动化处理过程 通过可扩展的工具集,Jenkins将持续交付流水线建模为“代码”,这些代码使用基于Groovy语言的DSL编写,命名为Jenkinsfile,并存放到项目的CVS中  | 
| 阶段/Stage | 
 表示管线中的一个阶段,例如Build、Test、Deploy  | 
| 步骤/Step | 表示阶段中的步骤,每个步骤下具有多个行为,例如sh执行指定的命令 | 
| 节点/Node | 
 用于脚本式管线 Node是一台机器,Stage或者Step可以放到Node上执行,如果不指定则在Master节点上执行  | 
| 代理/Agent | 
 用于声明式管线 表示整个管线,或者某个特定的Stage的执行环境  | 
Jenkins提供了平台原生安装包、Docker镜像,任何安装了JRE的机器都可以运行Jenkins。
| 
					 1 2  | 
						docker run --name jenkins -h jenkins --network local --ip 172.21.0.8 --dns 172.21.0.1 \            --restart=always  -d docker.gmem.cc/jenkins/jenkins:lts  | 
					
启动容器后,执行下面的命令获取初始密码:
| 
					 1 2  | 
						docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword # e8ed8538336a479f9694f5c32db0a2f3  | 
					
登陆到Jenkins首页,根据向导来安装。
| 
					 1 2 3  | 
						git clone https://github.com/jenkinsci/jenkins.git cd jenkins mvn clean package -Dmvn.test.skip=true  | 
					
构建完成后,将jenkins/war/target/jenkins.war部署到任何Servlet容器即可启动。默认情况下Jenksin的数据存储在 ~/.jenkins目录下。
有两种方式:
- 通过Jenkins WebUI来定义:New Item ⇨ Pipeline,在Script中编写管线脚本
 - 编写Jenkinsfile,纳入版本控制系统,通常放在项目的根目录
 
推荐使用第二种方式,WebUI仅仅适合非常简单的任务。
Jenkinsfile就是一段包含了管线定义的Groovy代码,必须以 #!groovy作为开头第一行。
定时从版本库拉取代码,发现有Push动作后,则触发构建。
很多Git服务支持Webhook。例如Gogs,点击Settings ⇨ Webhooks可以找到Webhook的指南。
管线可以访问一系列的全局变量,到底有哪些全局变量可用,取决于你安装的插件。默认可用的变量包括:
| 变量 | 说明 | ||||||
| env | 
 环境变量,你可以在脚本式管线、script中读写,例如env.PATH、env.BUILD_ID 注意:此变量的属性值都是字符串,因此: 
 在字符串中,可以直接使用变量替换来访问环境变量: 
 在sh指令中你也可以使用这些环境变量: 
  | 
||||||
| params | 只读的、传递给管线的参数,例如params.MY_PARAM_NAME | ||||||
| currentBuild | 当前构建对象的相关信息,例如currentBuild.result、currentBuild.displayName | 
Jenkins支持两种风格的管线:声明式、脚本式。
所有内容必须定义在 pipeline{ }块内部,只能包含Section、Directive、Step或者赋值语句。
声明式管线相对简单,而且不需要学习groovy语法,对于日常的一般任务完全够用,示例:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17  | 
						pipeline {     agent any     stages {         stage('Example') {             steps {                 echo 'Hello World'                 script {                     // 可以在script这种Step中定义脚本                     if( $VALUE1 == $VALUE2 ) {                        currentBuild.result = 'SUCCESS'                        return                     }                 }             }         }     } }  | 
					
所有内容必须定义在 node{ }块内部,里面包括多个stage。脚本式管线可以通过Groovy语言的强大特性做任何你想做的事情。示例:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  | 
						node('master') {     // 仅Multibranch Pipeline、Pipeline script from SCM支持checkout scm     checkout scm     stage('Build') {         docker.image('maven:3.3.3').inside {             sh 'mvn --version'         }     }     stage('Deploy') {         // 可以直接编写Groovy脚本         if (env.BRANCH_NAME == 'master') {             echo 'I only execute on the master branch'         }     } }  | 
					
包含一个或者多个stage指令,核心逻辑所在。建议对每个独立的交付部分(例如Build、Test、Deploy)至少定义一个stage指令。示例:
| 
					 1 2 3 4 5 6 7 8 9  | 
						pipeline {     agent any     stages {         stage( 'Build' ) {             steps {             }         }     } }  | 
					
这一段定义管线执行完毕后,需要执行的逻辑。你可以在此指令内部定义以下块:
- always:不管返回什么状态都会执行
 - changed:如果当前管线返回值和上一次已经完成的管线返回值不同时候执行
 - failure:当前管线返回状态值为”failed”时候执行,在Web UI界面上面是红色的标志
 - success:当前管线返回状态值为”success”时候执行,在Web UI界面上面是绿色的标志
 - unstable:当前管线返回状态值为”unstable”时候执行,通常因为测试失败,代码不合法引起的。在Web UI界面上面是黄色的标志
 
示例:
| 
					 1 2 3 4 5  | 
						post {     always {         echo 'Hello'     } }  | 
					
该指令用于定义环境变量:
| 
					 1 2 3  | 
						environment {     CC = 'clang' }  | 
					
你可以通过env变量访问所有环境变量: echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
注意:
- 即使不使用该指令定义,你也可以读写任意环境变量
 - 如果使用了该指令,则流水线前一个Stage对环境变量的修改,无法被下一个Stage看到,不论这些Stage是否在同一Agent上执行。因为每个Stage都会使用environment指令所给出的默认值
 
所有内置环境变量的列表,参考https://ci.gmem.cc/pipeline-syntax/globals#env
用于声明式管线,指定整个管线或者Stage的运行环境,支持取值:
- any:任意一个可用的agent
 - none:如果放在pipeline顶层,那么每一个stage都需要定义自己的agent指令
 - label:在Jenkins环境中指定标签的agent上面执行
 - node:类似于label,但是可用定义更多可选项
 - docker:指定在docker容器中运行,示例:
123agent {docker { image 'docker.gmem.cc/maven:3.5.2' }} - dockerfile:使用源码根目录下面的Dockerfile构建容器来运行
 
用于脚本式管线。不带任何参数表示其内部的Stage可以在任何可用的节点上运行。 node('label')表示必须在具有标签label的节点上运行。
管线的选项,例如:
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						options {     # 执行超时     timeout(time: 1, unit: 'HOURS')     # 重试次数     retry 3     # 在控制台输出前面加上时间戳     timestamps()     # 禁用声明式管线的自动签出代码的行为     # 默认情况下,它会在任何一个Agent上签出Jenkinsfile所在的SCM仓库     options { skipDefaultCheckout() } }  | 
					
触发管线所需要的参数,可以在step中通过params对象引用,示例:
| 
					 1 2 3  | 
						parameters {     string(name: 'user', defaultValue: 'Alex', description: 'User name') }  | 
					
触发器,定义何时执行管线:
| 
					 1 2 3 4 5 6  | 
						triggers {     # 使用Cron表达式指定何时触发     cron('H 4/* 0 0 1-5')     # 使用Jenkins的poll scm语法     pollSCM('H 4/* 0 0 1-5') }  | 
					
如果使用Git服务的Webhooks则不需要该指令。
定义在stages块中,stage内部至少需要保护一个steps指令,一个可选的agent。
定义需要自动安装并放到PATH环境变量的工具集合,工具必须预先在Jenkins的Global Tool Configuration中配置。
在满足特定条件下才执行Stage:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12  | 
						stage('Example Deploy') {     # 仅当分支为production时才执行此Stage     when {         branch 'production'     }     echo 'Deploying' } # 其它: when { branch 'master' } when { environment name: 'DEPLOY_TO', value: 'production' } when { expression { return params.DEBUG_BUILD } }  | 
					
定义一组并行执行的步骤,示例:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  | 
						node {     parallel(         // 下面4个步骤会并发执行         "publish 01" : { build job: '01'  , propagate: false  },         "publish 02" : { build job: '02'  , propagate: false  },         "publish 03" : { build job: '03'  , propagate: false  },         "publish 04" : { build job: '04'  , propagate: false  }     )     parallel(         "publish 05" : { build job: '05'  , propagate: false  },         "publish 06" : { build job: '06'  , propagate: false  },         "publish 07" : { build job: '07'  , propagate: false  },         "publish 08" : { build job: '08'  , propagate: false  }     ) }  | 
					
包含实际的逻辑,声明式/脚本式管线都支持各种Step。
签出源代码,最简单的用法:
| 
					 1 2  | 
						// 根据当前流水线的配置,签出对应代码 checkout scm  | 
					
你可以签出任意代码库,也可以从scm变量读取很多属性:
| 
					 1 2 3 4 5  | 
						checkout( [     $class: 'GitSCM',     branches: scm.branches,     userRemoteConfigs: [ [ credentialsId: 'Bitbucket', url: 'git@bitbucket.org:NAVFREG/jenkinsfile-tests.git' ] ], ] )  | 
					
下面的例子是签出一个标签:
| 
					 1 2  | 
						         // 命名参数 scm, poll checkout scm: [$class: 'GitSCM', userRemoteConfigs: scm.userRemoteConfigs, branches: [[name: 'refs/tags/3.6.1']]], poll: false   | 
					
执行一个脚本,可选的,将标准输出作为返回值:
| 
					 1 2  | 
						cur_dir = sh script:'basename $PWD',returnStdout: true echo "$cur_dir"   | 
					
注意:
- Jenkins默认使用
			-xe参数来执行Shell脚本,这意味着:
- -x 打印每个执行的命令
 - -e 任意一个命令失败,则Shell脚本立即退出
 
 - 不论如何,脚本的最后一个命令如果返回值为非0,则整个Step总是被标记为失败
 - 关于Shell特殊字符:
1234echo \ # 未结束的命令echo \s # 输出 secho \\s # 输出 \secho '\s' # 输出 \s此外,步骤sh默认会将参数中的单引号脱去,例如:
12345678value = /\,/ // 字面值 \,value = /'"$value"'/echo value // 输出 '"\,"'sh "echo $value"// 输出如下,可以看到$value外面的单引号被脱去了// + echo "\,"// "\,"因此,要想将反斜杠传递给Shell脚本,需要在外部包上单引号
 
要改变默认行为1,可以在脚本开始处添加 set +ex
触发指定Job的构建,参数:
| 参数 | 说明 | ||
| job | 下游Job的名称,可以指定Job的相对、绝对路径 | ||
| parameters | 
 传递给Job的参数。对象的数组。示例: 
  | 
||
| propagate | 
 如果设置为true,则下游Job没有成功的话当前Job失败  | 
||
| quietPeriod | 执行构建之前的安静期,单位秒 | ||
| wait | 是否等待完成 | 
递归的删除当前目录及其内容,无参数。
在指定的目录下执行一系列Step:
| 
					 1 2 3  | 
						dir('target'){     deleteDir() }  | 
					
打印消息到控制台。示例: echo 'message'
触发一个错误,类似于throw new Exception(),但不会打印异常栈。示例: error 'message'
可以使用此Step让一个流水线Fail。
验证指定的文件是否存在于工作区,返回布尔值
克隆Git仓库到工作区:
| 
					 1  | 
						git branch: 'master', url: 'https://git.gmem.cc/alex/spring-data-ext.git', credentialsId: 'alex.git.gmem.cc'   | 
					
验证当前运行在的节点,是否是类UNIX系统
用于发送邮件
返回当前目录的路径字符串
从工作区读取文件,返回字符串形式的内容。示例: readFile file: path, encoding: 'UTF-8'
重试最多N次,如果块内出现异常,自动重试,如果最后一次重试失败,则向外抛出异常:
| 
					 1 2 3  | 
						try(3){     ... }  | 
					
休眠指定的时间,示例:
| 
					 1 2  | 
						// 单位:NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS sleep time:10, unit: SECONDS   | 
					
定义一段Groovy脚本并执行。
将当前Agent的工作区中的某些文件进行保存,供管线后续其它Stage的Agent使用:
| 
					 1 2 3 4 5 6  | 
						// 暂存target目录下的所有jar,保持目录结构不变 stash includes: 'target/*.jar', name: 'target' // 暂存单个文件 stash includes: 'Dockerfile', name: 'dockerfile' // 暂存.chart完整的目录结构和文件,允许仅仅暂存空目录 stash includes: '.chart/**/*', name: 'chart', allowEmpty: true  | 
					
从之前的暂存区中恢复文件:
| 
					 1 2  | 
						// 目录结构保持不变 unstash 'target'  | 
					
指定内部代码的超时时间,如果超时后仍然没有执行完毕,则抛出异常:
| 
					 1 2 3 4  | 
						// activity为true,则对日志中多长时间没有新输出进行计时,而非绝对执行时间 timeout( time: 10, unit: DAYS, activity: false ){     // long running steps }  | 
					
不指定单位,默认分钟。
循环执行内部代码,直到其返回true,重试间隔会不断增加。
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						timeout( 5 ) {     waitUntil {         if ( statusId == "FINISHED" ) {             return true         } else if ( statusId == "STOP_BY_ERROR" || statusId == "CANCELED" || statusId == "UNKNOWN" ) {             return false         } else {             return false         }     } }   | 
					
提取指定凭证的信息,以便在管线中引用:
| 
					 1 2 3 4 5 6 7 8  | 
						withCredentials( [ usernamePassword( credentialsId: 'alex.chartmuseum.gmem.cc', usernameVariable: 'USER', passwordVariable: 'PSWD' ) ] ) {     // 可以作为环境变量使用,但是尝试打印到控制台时,会被遮罩为****     sh 'echo $PSWD'     // 可以作为Groovy变量使用     echo USER     // 可以使用Groovy的变量替换     echo "username is $USER" }  | 
					
为内部代码提供环境变量:
| 
					 1 2 3  | 
						withEnv(['MYTOOL_HOME=/usr/local/mytool']) {     sh '$MYTOOL_HOME/bin/start' }  | 
					
将字符串作为文件写入到当前目录下: writeFile file: 'log', text: 'OK', encoding: 'UTF-8'
提供了一系列工具,这些工具只能在脚本中使用。
从文件、文本读取JSON,并返回一个对象。返回的对象是字符串为Key的映射、基本类型或映射的列表。
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						def props = readJSON file: 'dir/input.json' assert props['attr1'] == 'One' assert props.attr1 == 'One' def props = readJSON text: '{ "key": "value" }' assert props['key'] == 'value' assert props.key == 'value' def props = readJSON text: '[ "a", "b"]' assert props[0] == 'a' assert props[1] == 'b'  | 
					
从文件、文本中读取YAML,并返回一个对象:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  | 
						pipeline {     environment {         VERSION = '1.0.0'     }     stages {         stage('test'){             steps{                 git branch: 'refactoring', url: 'http://10.255.223.213/code-ddreader/media-job.git', credentialsId: 'wangzhen.10.255.223.213'                  script {                     def meta = readYaml file: '.chart/Chart.yaml'                     echo meta.version                     echo meta.name                     env.VERSION = meta.version                     env.NAME = meta.name                 }                 sh 'echo $NAME:$VERSION'             }         }     } }  | 
					
| 
					 1 2 3 4 5 6 7  | 
						// 拷贝sourceproject最后一次Stable构建的结果 copyArtifacts(projectName: 'sourceproject') // 触发构建 def built = build('downstream') // 拷贝指定编号的构建的结果 copyArtifacts(projectName: 'downstream', selector: specific("${built.number}"));  | 
					
| 参数 | 说明 | 
| projectName | 目标项目的名称 | 
| selector | 构建选择器 | 
| parameters | k1=v1,k2=v2形式的过滤器,用于过滤构建 | 
| filter | Ant风格表达式,用于包含、排除需要拷贝的构件 | 
| excludes | |
| target | 拷贝到的目标目录 | 
| flatten | 清除拷贝来的构件的目录层次 | 
| optional | 即使没有找到可用的目标构建,也不会导致当前构建失败 | 
| 选择器 | 说明 | 
| lastSuccessful | 最后一次成功的构建 | 
| specific | 特定构建 | 
| permalink | 使用永久连接指定目标构建 | 
| lastCompleted | 最后一次完成的构建(不论构建状态) | 
| latestSavedBuild | 最后一个保存的构建(标记为永久保存的) | 
| buildParameter | 根据构建参数来匹配 | 
| upstream | 触发此构建的上游构建 | 
该插件提供的Step,能够发起任意的HTTP请求:
| 
					 1 2 3 4 5 6 7 8 9  | 
						def resp = httpRequest url: "http://host:port/path",                        httpMode: "POST",  // GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH                        authentication: "$credential", // 已经定义的Jenkins凭证的ID                        acceptType: 'APPLICATION_JSON',                        contentType: 'APPLICATION_JSON',                        consoleLogResponseBody: true // 打印响应内容到控制台 // 读取响应体 def result = readJSON text: resp.content  | 
					
当项目规模变大后,你会有很多内容相似的Pipeline,你可以利用共享库来减少代码重复。共享库由名称、可选的默认版本、源码获取方法(例如SCM)来定义。版本可以是SCM识别的任何东西,对于Git来说,branches、tag、commit hash都可以。
你可以从Jenkins提供的原型来创建共享库的代码骨架:
| 
					 1  | 
						mvn -U -P jenkins archetype:generate -Dfilter="io.jenkins.archetypes:"  | 
					
选择原型:io.jenkins.archetypes:global-shared-library。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12  | 
						(root) +- src                     # Groovy源代码目录,编译结果会被放到Pipeline的classpath下 |   +- org |       +- foo |           +- Bar.groovy  +- vars                    # 全局变量定义目录,这些全局变量可以在Pipeline中访问 |   +- foo.groovy          # 全局变量foo |   +- foo.txt             # 变量foo的文档,可以是HTML、Markdown,但是扩展名必须为txt +- resources               # 外部库使用的资源文件 |   +- org |       +- foo |           +- bar.json  | 
					
- 全局共享库,在Manage Jenkins ⇨ Configure System ⇨ Global Pipeline Libraries定义
 - 目录共享库,在目录或者子目录上定义
 - 某些插件可能会自动安装共享库
 
如果你把共享库标记为Load implicitly,则Pipeline可以直接使用。
否则,必须通过注解@Library来启用:
| 
					 1 2 3 4 5 6  | 
						// 使用库的默认版本 @Library('my-library') _ // 指定版本 @Library('my-library@1.0') _ // 使用多个库 @Library(['my-library', 'otherlib@abc1234']) _  | 
					
如果Pipeline仅仅需要使用全局变量,或者库仅仅包含了var目录,则可以使用 _后缀。否则,你需要使用import语句:
| 
					 1 2  | 
						@Library('somelib') import com.mycorp.pipeline.somelib.UsefulClass  | 
					
在编译Pipeline脚本时,共享库被加载,之后Pipeline被执行。全局变量是在运行时动态解析的 。
2.7之后的Pipeline提供一个Step,用于动态加载共享库:
| 
					 1 2  | 
						// 仅仅需要使用全局变量/函数 library 'my-library'  | 
					
| 
					 1 2 3 4 5  | 
						// 调用方法 library('my-shared-library').com.mycorp.pipeline.Utils.someStaticMethod() // 引入包 def lib = library('my-shared-library').com.mycorp.pipeline lib.Helper.new(...)  | 
					
任何Groovy代码都是允许的,例如数据结构、工具函数、类。
共享库不能在全局作用域直接调用sh或者git这样的Step ,你需要声明方法,在其中调用:
| 
					 1 2 3 4 5 6 7  | 
						package cc.gmem.ci; def checkOutFrom(repo) {   git url: "git@github.com:jenkinsci/${repo}" } return this  | 
					
对于上述共享库,你可以在脚本式Pipeline中这样使用:
| 
					 1 2  | 
						def u = new cc.gmem.ci.Utils() u.checkOutFrom(repo)  | 
					
当前可用的Step集合,可以通过this关键字传递给共享库。例如共享库代码:
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						package cc.gmem.ci class Utils implements Serializable {   // 实例变量   def steps   // 构造函数   Utils(steps) {this.steps = steps}   // 方法   def mvn(args) {     steps.sh "${steps.tool 'Maven'}/bin/mvn -o ${args}"   } }  | 
					
使用上述共享库的代码:
| 
					 1 2 3 4 5 6  | 
						@Library('utils') import cc.gmem.ci.Utils def utils = new Utils(this)  // 传递this node {   // 调用共享库   utils.mvn 'clean package' }  | 
					
共享库的var目录中的脚本,会被按需的创建为单例:
| 
					 1 2 3 4 5 6 7  | 
						def info(message) {     echo "INFO: ${message}" } def warning(message) {     echo "WARNING: ${message}" }  | 
					
你可以在Pipeline中这样调用:
| 
					 1 2 3 4 5 6  | 
						// utils为库名 @Library('utils') _ // log为脚本文件名,info为其中定义的方法 log.info 'Starting' log.warning 'Nothing to do!'  | 
					
当使用声明式Pipeline时,你必须在script指令内部来使用共享库全局变量。
如果要定义一个名为sayHello的Step,你需要:
- 创建脚本vars/sayHello.groovy
 - 在脚本中定义call方法
 
这个call方法允许你像调用Step那样调用全局变量:
| 
					 1 2 3 4  | 
						def call(String name = 'human') {     // 这个方法中可以调用任何合法的Step     echo "Hello, ${name}." }  | 
					
你可以在自己的Pipeline中调用上述Step:
| 
					 1 2 3 4 5 6  | 
						// 传递参数 sayHello 'Alex' // 使用默认参数 sayHello()  // 传递闭包  | 
					
自定义Step可以接受闭包:
| 
					 1 2 3 4 5 6  | 
						// 允许传递闭包 def call(Closure body) {     node('windows') {         body()     } }  | 
					
调用上述Step:
| 
					 1 2 3  | 
						windows {     bat "cmd /?" }  | 
					
注意:要使用命名参数传参,必须使用下一节的def call(Map config)方式,否则会报错:is applicable for argument types: (java.util.LinkedHashMap) value
如果你有很多大规模的、非常相似的Pipeline,可以将其直接封装到自定义Step中:
| 
					 1 2 3 4 5 6 7  | 
						def call(Map config) {     node {         git url: "https://github.com/jenkinsci/${config.name}-plugin.git"         sh 'mvn install'         mail to: '...', subject: "${config.name} plugin build", body: '...'     } }  | 
					
这样,只需要一行代码的脚本式Pipeline即可,代码大大简化:
| 
					 1  | 
						buildPlugin name: 'git'  | 
					
甚至,完整的声明式Pipeline也可以封装到自定义Step中:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | 
						def call(int buildNumber) {   if (buildNumber % 2 == 0) {     pipeline {       agent any       stages {         stage('Even Stage') {           steps {             echo "The build number is even"           }         }       }     }   } else {     pipeline {       agent any       stages {         stage('Odd Stage') {           steps {             echo "The build number is odd"           }         }       }     }   } }  | 
					
使用@Grab可以从Maven仓库动态的载入共享库:
| 
					 1 2 3 4 5 6 7  | 
						@Grab('org.apache.commons:commons-math3:3.4.1') import org.apache.commons.math3.primes.Primes void parallelize(int count) {   // 调用第三方库   if (!Primes.isPrime(count)) {   } }  | 
					
尽管你可以在单个Jenkins服务器中完成所有构建操作,但是在云环境下分布式构建更加常见。
Jenkins从根本上说是Master + Agent的架构:
- Master负责任务协调、提供GUI和API端点
 - Agent负责执行任务,运行Agent的服务器称为Node
 
Agent可以被打标签,在流水线中可以指定哪些标签可以运行流水线的步骤。
Agent上不需要安装Jenkins服务,Agent和Master需要创建双向通信连接(TCP)以进行交互。
最流行的配置Agent的途径是经由Master创建的连接,这要求Master能够访问到Agent(通常是基于SSH)。
出于安全的原因,某些环境下可能无法访问SSH,这种情况下就需要使用JNLP,从Agent连接到Master。
在云环境下,按Job所需安装工具很重要,因为Agent可能仅仅是一个最小化的操作系统。
默认情况下,任何Agent支持:执行任何Shell命令、下载和解压缩归档文件、下载并安装Oracle官方JDK,下载并安装Ant、Maven。
Jenkins可以作为一个Service或者Pod,在K8S中运行。这样可以避免Master节点的单点故障。
Jenkins提供了Kubernetes插件,利用此插件,可以在K8S中动态创建Pod,作为Slave节点使用。可以获得的好处包括:
- 构建环境固化:针对不同的开发语言、项目,分别打包Docker镜像作为构建环境
 - 节约硬件资源:Slave节点是按需创建,用后即毁的,不会长时间占用资源
 
以下插件可能有用:
| 插件 | 说明 | 
| Kubernetes | 动态的在Kubernetes集群中创建Agent | 
| Kubernetes Continuous Deploy | 
 支持将K8S资源部署到Kubernetes集群中: 
 Slave节点不需要安装kubectl  | 
本文的例子中,Jenkins是部署在Kubernetes集群外部的。如果Jenkins作为集群服务运行则配置方式有所不同,需要注意。
要配置用于动态创建Agent的K8S宿主机,定位到Manage Jenkins ⇨ Configure System ⇨ Cloud ⇨ Add a new Cloud ⇨ Kubernetes,参考下图进行设置:
需要注意的地方:
- Kubernetes URL即API Server的URL
 - 禁用HTTP证书检查,除非你的API Server证书是经过知名CA签名的
 - Credentials使用数字证书
 - Jenkins URL需要使用HTTP方式,除非你的服务器证书经过知名CA签名
 - 如果在K8S集群内部可以直接访问Jenkins URL,则Jenkins tunnel不需要填写
 - Container Cap貌似是目标命名空间中整个Pod数量的限制。因为我看到Jenkins日志中提示超过Cap,但是一个Agent都没有
 
配置界面如下图示意:
证书可以从Kubectl配置文件中获取,一般位于~/.kube/config,内容如下:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  | 
						apiVersion: v1 clusters:   - cluster:       certificate-authority-data: ***       server: 'https://10.0.0.100:6443'     name: kubernetes contexts:   - context:       cluster: kubernetes       namespace: jx       user: kubernetes-admin     name: kubernetes-admin@kubernetes current-context: kubernetes-admin@kubernetes kind: Config preferences: {} users:   - name: kubernetes-admin     user:       client-certificate-data: Base64编码后的证书       client-key-data: Base64编码后的私钥  | 
					
可以使用OpenSSL来把上面的证书、私钥合并为Jenkins要求的PKCS#12格式:
| 
					 1 2 3 4 5  | 
						# admin.pfx上传给Jenkins,其密码为passwd # ca.crt为根证书,可以在APIServer的/etc/kubernetes/pki目录下找到 echo "上面client-key-data的值"           | base64 -d > admin.key echo "上面client-certificate-data的值"   | base64 -d > admin.crt openssl pkcs12 -export -out admin.pfx -inkey admin.key -in admin.crt -certfile ca.crt -passout pass:passwd  | 
					
上文的步骤,仅仅完成了Jenkins到Kubernetes的连接的配置。要进行代码构建,必须配置K8S Pod。点击ADD POD TEMPLATE按钮,参考下图配置:
注意点:
- Name会成为K8S Pod实际名称的前缀
 - Namespace指定在哪个名字空间创建Pod
 - Labels + Usage用于指明,哪些构建任务在此Pod中运行
 - Containers配置:
- 你可以配置1-N个容器,在Jenkins Pipeline中,你可以通过Name引用任意一个容器,并在此容器内执行具体构建Step
 - 必然有一个名为jnlp的容器,此容器负责连接到Jenkins服务器。如果不显式定义此容器,则Jenkins自动创建,并使用jenkinsci/jnlp-slave作为其镜像
 - 你可以FROM jnlp-slave镜像,把其它所需的构建工具整合到单个容器中,但是并不推荐这样做。对于基于Java的构建工具,建议FROM openjdk:8-jdk
 - Command to run仅仅指定一个让容器不会退出的命令即可。如果容器入口点已经有此功能,则不必配置
 - EnvVars覆盖针对单个容器的环境变量
 
 - 下面的EnvVars,定义覆盖所有容器的环境变量
 - Node Selector可以指定,哪些节点可以运行此Pod
 
PodTemplate也可以直接在Pipeline脚本中定义:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19  | 
						podTemplate( label: 'maven-k8s', cloud: 'k8s', containers: [         containerTemplate(                 name: 'maven',                 image: 'docker.gmem.cc/dang/maven:3.5.2',                 ttyEnabled: true,                 command: 'cat'         ),         containerTemplate(                 name: 'jnlp',                 image: 'docker.gmem.cc/jenkinsci/jnlp-slave',                 alwaysPullImage: false,                 args: '${computer.jnlpmac} ${computer.name}'         ), ] ) {     node( 'k8s-maven' ) {     } }  | 
					
Jenkins任务脚本如下:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30  | 
						pipeline {     agent none     options { skipDefaultCheckout() }     stages {         stage( 'maven build' ) {             agent {                 label 'maven-k8s'             }             steps {                 checkout scm                 container( 'maven' ) {                     sh 'mvn clean package'                 }                 stash includes: 'target/*.jar', name: 'target'                 stash includes: 'Dockerfile', name: 'dockerfile'             }         }         stage( 'docker build' ) {             agent {                 label 'docker'             }             steps {                 unstash 'target'                 unstash 'dockerfile'                 sh 'docker build --force-rm -t docker.gmem.cc/ci-maven .'                 sh 'docker push docker.gmem.cc/ci-maven'             }         }     } }  | 
					
以下插件可能有用:
| 插件 | 说明 | 
| docker-build-step | 
 支持将多种Docker命令作为Step使用: 
  | 
| Docker | 利用一个Docker主机,动态创建容器作为Agent(Slave),然后运行一次构建,最后销毁Agent | 
要配置用于动态创建Agent的Docker宿主机,定位到Manage Jenkins ⇨ Configure System ⇨ Cloud ⇨ Add a new Cloud ⇨ Docker,参考下图进行设置:
如果Docker主机不是Jenkins Master主机,需要TCP方式连接,否则可以使用Unix Domain Socket,具体取决于Dockerd的配置。点击Test Connection可以检查Docker宿主机是否能够连通。
点击Add Docker Template按钮,可以创建一个基于Docker的Agent的模板。示例:
注意点:
- 如果镜像仓库私服要求身份验证,请正确填写Registry Authentication
 - Labels用于标识此Agent的用途,可以设置Usage,仅仅允许标签匹配的的任务在此Agent上运行
 - Idle Time设置多少分钟后删除Agent
 - Remove volumes,删除Agent的容器后,同时删除其卷
 - 注意Container Settings,要进行适当的配置,确保容器能够访问到Jenkin Server。主要是DNS、网络的设置
 
Java工程常常基于Maven构建,因此需要定义好包含了Maven运行时的Docker镜像,并且配置好到公司Maven仓库私服的连接。
推荐以jenkins/jnlp-slave 为基础制作镜像。此镜像可以和上面使用的Attach Docker container连接方式配合工作,Dockerfile示例如下:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19  | 
						FROM jenkins/jnlp-slave # Maven配置文件需要进行修改,包括: # 1、正确的私服信息 # 2、本地仓库位置设置为maven_repository COPY /mvn /maven COPY /repo /maven_repository COPY /jenkins-slave /usr/local/bin/jenkins-slave COPY logging.properties /usr/share/jenkins/logging.properties ENV PATH=/maven/bin:/usr/local/bin:$PATH ENV JENKINS_URL=http://ci.gmem.cc:80 ENV JENKINS_AGENT_WORKDIR=/home/jenkins/agent USER root RUN chown -R jenkins:jenkins /maven_repository && chmod +x /usr/local/bin/jenkins-slave USER jenkins ENTRYPOINT ["jenkins-slave"]  | 
					
此镜像项目的完整源码:
| 
					 1  | 
						git clone https://git.gmem.cc/alex/dockerfile-maven-docker.git  | 
					
可以使用如下的脚本式管线测试:
| 
					 1 2 3 4 5 6  | 
						// 匹配上面配置的Agent模板 node('maven-docker'){     stage('test'){         sh 'mvn --version'     } }   | 
					
Jenkins的子项目,基于Kubernetes的CI/CD解决方案。
Jenkinsx提供命令行接口jx,你可以在任何K8S客户端上运行此CLI,实现:
- 安装Jenkins X到K8S集群
 - 创建新K8S集群并安装Jenkins X到其中
 - 导入项目、及其CD流水线到Jenkins X
 - 创建Spring Boot项目并集成到Jenkins X
 
你不需要了解Jenkins流水线机制的细节,Jenkins X提供了很多优秀的默认流水线,完整的实现了CI/CD。
这里的环境是指应用程序被部署到的运行环境。环境的命名例如Dev、Testing、Staging/UAT、Production。
使用Jenkins X时,每个团队都拥有自己的一套(Dev/Testing...)环境。你可以随时创建新的环境。
Jenkins X使用GitOps(一款基于Kubernetes的高速CI/CD框架)来管理部署到各环境中的K8S资源的配置/版本信息。每个环境都有自己的Git仓库,其中包含了所有Helm Charts及其配置/版本信息。
每个环境映射为K8S的独立名字空间。当Pull Request被合并到环境的Git仓库时,环境的流水线会运行,Git仓库中最新的Helm Charts会部署到K8S名字空间。
强调一下:
- 每个环境都有自己的Git仓库,开发和运维都使用此仓库
 - 该Git仓库管理环境中部署的所有应用程序、资源的所有配置/版本信息
 - 所有对环境的变更都被捕获、记录,便于追踪、回滚
 
所谓升级(Promotion),是指变更被应用到环境的过程。默认情况下Staging环境被设置为自动(Auto)升级,Production环境被设置为手工(Manual)升级。
变更是通过GitOps实现的,首先是向环境的Git仓库提交一个PR,保证所有变更可基于Git进行审核、批准或回滚。
当一个新的变更被Merge到master分支后,环境的流水线被触发,通过Helm部署新版本的应用。
升级的过程如下图:
很多和CI/CD相关的软件,被打包为Helm Chart,并且和Jenkins X进行了预集成。这些工具包括Nexus、Chart Museum、Monocular、Prometheus、Grafana。
其中的一部分软件被打包自动安装,其它则作为加载项: jx create addon grafana
K8S提供了定制资源(Custom Resources)的机制,Jenkins X添加了多种自定义资源。
表示Jenkins X的环境:
| 
					 1 2 3 4 5 6  | 
						jx get environments jx edit environment  # 基于K8S客户端访问 kubectl get environments kubectl edit env staging  | 
					
Jenkins X 流水线生成了一种Release资源,用来跟踪:
- 某个Chart Release(对应一组K8S资源)对应的版本、Git Tag
 - 执行某次Chart Release使用的Jenkins X 流水线URL、流水线日志
 - 每个Release关联的Commit、Issue、PR
 
以Pipeline Stage + Promotion Activity的形式存储流水线状态。
典型安装的Jenkins X 包含以下组件:
- 每个团队一个的开发环境,对应K8S名字空间
 - 0-N个永久环境( Permanent Environments):
- 每个团队开箱即用的Staging、Production环境
 - 只要需要,每个团队可以创建任意多的任意名字的环境
 
 - 可选的预览环境(Preview Environments)
 
通常每个环境都关联自己的K8S命名空间,确保隔离性。
开发环境中安装了基于K8S进行CI/CD所必需的核心应用,你可以通过加载项扩充此应用集。默认安装的包括:
- Jenkins:提供CI/CD自动化
 - Nexus:作为Java、NodeJS应用的依赖仓库(缓存)
 - Docker Registry:集群内部私服,流水线推送应用程序镜像到其上
 - Chartmuseum:Helm Chart仓库
 - Monocular:发现、运行(Release)Helm Chart的UI工具
 
类似于Staging、Production的环境,使用GitOps进行自我管理。它们对应各自的Git仓库,包含了配置应用程序所需的所有代码(DevOps —— Config as Code),这些代码主要是一些Helm Chart相关的配置文件。
Jenkins X使用Draft风格的Build Packs,并基于BP实现:
- 对不同语言、运行时、构建工具的支持
 - 在导入/创建项目时,添加必要的配置文件。如果导入/创建时以下文件不存在,则自动创建:
- Dockerfile 把代码转换为不可变镜像
 - Jenkinsfile,定义声明式的CI/CD流水线
 - chart目录,包含Helm Chart定义。用于生成K8S资源
 - charts/preview目录,包含Preview Chart的定义,包含基于PR部署Preview Enviroment所需的依赖
 
 
官方提供的默认BP位于https://github.com/jenkins-x/draft-packs,不同语言/构建工具对应了子目录。jx命令会将此BP克隆到~/.jx/draft/packs/目录,并在你创建/导入项目时pull以更新。
要安装Jenkins到现有的K8S集群,需要满足条件:启用了RBAC的1.8+版本的K8S,并且有一个非HTTPS的Docker Registory,此外Helm也需要被安装。
如果集群在网络访问方面有困难,可以提前拉取以下镜像:
bitnami/mongodb:3.4.9-r1
bitnami/monocular-api:v0.6.1
bitnami/monocular-ui:v0.6.1
chartmuseum/chartmuseum:v0.2.8
jenkinsxio/exposecontroller:2.3.58
jenkinsxio/jenkinsx:0.0.15
jenkinsxio/nexus:0.0.14
k8s.gcr.io/addon-resizer:1.7
k8s.gcr.io/heapster:v1.3.0
migmartri/prerender:latest
rawlingsj/pipeline-controller:dev
registry:2.6.2
这是Jenkins X的命令行工具,执行下面的命令安装:
| 
					 1 2  | 
						curl -L https://github.com/jenkins-x/jx/releases/download/v1.2.8/jx-linux-amd64.tar.gz | tar xzv  sudo mv jx /usr/local/bin  | 
					
首先在~/.jx/extraValues.yaml中添加一些覆盖值。例如默认配置下给的磁盘空间太大,可以减小一点:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  | 
						expose:   config:     exposer: Ingress     http: "true"     tlsacme: "false"   Annotations:     helm.sh/hook: post-install,post-upgrade     helm.sh/hook-delete-policy: hook-succeeded docker-registry:   persistence:     size: 16Gi jenkins:   Persistence:     Size: 32Gi  | 
					
执行下面的命令即可安装:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27  | 
						# 确保以下域名的DNS正确配置 10.0.0.102      chartmuseum.jx.k8s.gmem.cc 10.0.0.103      docker-registry.jx.k8s.gmem.cc 10.0.0.104      jenkins.jx.k8s.gmem.cc 10.0.0.105      monocular.jx.k8s.gmem.cc 10.0.0.100      nexus.jx.k8s.gmem.cc # Jenkins X 默认使用GitHub,在企业中工作时往往使用其它种类的Git服务器 # 列出现有的Git服务器 jx get git    # 添加一个GitLab服务器 jx create git server gitlab https://vcs.gmem.cc --name GitLab # 也可以使用Addon,支持gitea服务器 jx create addon gitea # 根据需要,创建K8S资源 kubectl -n jx delete secret gmemk8scert-jx kubectl -n jx create secret generic gmemk8scert-jx --from-file=/home/alex/Documents/puTTY/jx.k8s.gmem.cc # 初始化 jx init # 根据提示选择匹配的运行环境 jx install # 或者 jx install --provider=kubernetes # 减少不必要的步骤 jx install --provider=kubernetes --default-admin-password=admin --domain='k8s.gmem.cc' --http='false' --default-environments=false --skip-ingress=true  | 
					
注意:
- 如果kube-system没有可用的Ingress控制器,则jx可以帮你自动安装
 - 你需要拥有GitHub账号,该账号用于拉取代码
 - Jenkins X会注册一个名为jenkins-x的Chart仓库
 - 如果使用自己的Git服务器,则服务器必须兼容GitHub API。可以考虑GitBucket
 
你将获得以下服务:
| 服务 | 说明 | 
| Jenkins 2 | 老牌的CI服务 | 
| Helm | Kubernetes包管理器,如果K8S集群中已经存在则不会重复安装 | 
| Monocular | Helm的GUI | 
| Chartmuseum | 支持云后端(Amazon S3、Google Cloud Storage、 Microsoft Azure Blob Storage、Alibaba Cloud OSS Storage)存储的Helm Chart仓库 | 
| Nexus | Maven仓库管理器 | 
| Docker Registory | Docker镜像私服 | 
| 
					 1 2 3 4 5 6 7 8 9 10 11 12  | 
						# 基于Helm的卸载程序 helm del --purge jenkins-x # 删除文件 rm -rf ~/.jx # 删除Env资源 kubectl -n jx delete env dev kubectl -n jx delete env staging kubectl -n jx delete env production # 确保名字空间中没有多余的资源 kubectl -n jx get all  | 
					
| 子命令 | 说明 | ||
| 
 crud git  | 
 资源管理类命令: create 创建 管理Git服务器 示例: 
  | 
||
| cdx | 打开CDX仪表盘,此仪表盘用于CI/CD、环境的可视化 | ||
| console | 打开Jenkins X控制台 | ||
| context | 查看或者修改当前使用的Kubernetes Context | ||
| environment | 查看或修改当前K8S集群中的当前环境 | ||
| import | 导入本地项目或Git仓库到Jenkins X | ||
| install | 
 安装Jenkins X到当前K8S集群 选项: --http='true' 设置为false则创建基于HTTPS的Ingress规则  | 
||
| init | 初始化Jenkins X环境,例如创建K8S角色、角色绑定等对象 | ||
| preview | 创建或更新应用程序当前版本的Preview环境 | ||
| promote | 升级某个环境的某个应用到新版本 | ||
| rsh | 在某个Pod中打开Terminal或者执行命令 | ||
| start | 启动一个进程,例如Pipeline | ||
| status | 显示K8S集群或者命名节点的状态 | ||
| step | 和Pipeline Step有关 | ||
| uninstall | 从集群移除Jenkins X | ||
| upgrade | 
 升级某个组件或者整个Jenkins X 示例: 
  | 
在Jenkins的流水线配置界面,填写好Repository URL 、Credentials后,报错:/tmp/ssh8260207358463523765.sh: ssh: not found
原因:可能是Master Agent的环境变量PATH设置有问题,没有包含ssh所在目录。
如果任务一直再转圈,停止不了,打开Manage Jenkins中的Script Console:
| 
					 1 2 3 4 5 6  | 
						Jenkins.instance.getItemByFullName("任务名称")  // 例如 dev-1/media-hapi                 .getBuildByNumber(任务编号)                 .finish(                         hudson.model.Result.ABORTED,                         new java.io.IOException("Aborting build")                 );  | 
					
修改hudson.model.UpdateCenter.xml(Docker环境下此文件默认位于/var/jenkins_home),将其中的URL改为修改后的版本。
或者修改updates/default.json将connectionCheckUrl改为国内可访问的URL。
启动报错:java.io.IOException: DerValue.getBigInteger, not an int 48
解决办法:
| 
					 1 2  | 
						# 转换为RSA私钥 sudo openssl rsa -in  /etc/letsencrypt/live/ci.gmem.cc/privkey.pem -out /etc/letsencrypt/live/ci.gmem.cc/priv.key   | 
					
Jenkins UI长时间没有出现Docker Agent对应的节点。
很可能是镜像存在问题,无法启动容器。可以查看Jenkins日志发现端倪。示例:
com.github.dockerjava.api.exception.NotFoundException: {"message":"invalid header field value \"oci runtime error: container_linux.go:247: starting container process caused \\\"exec: \\\\\\\"jenkins-slave\\\\\\\": executable file not found in $PATH\\\"\\n\""}
后面会跟着找不到容器的错误。
Apr 19, 2018 8:00:53 AM com.github.dockerjava.core.async.ResultCallbackTemplate onError
SEVERE: Error during callback
com.github.dockerjava.api.exception.NotFoundException: {"message":"No such container: 829878a88c745360b6141b86b7d825590a7de697c869545e63e1cc6b97ca2414"}
首先尝试直接运行容器,解决镜像本身的问题。executable file not found in $PATH可能是因为可执行文件不存在,或者没有可执行权限。
考虑使用插件。下面是通过System Groovy Choice Parameter的动态脚本获取Git分支列表的例子:
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						def cmd = "/var/jenkins_home/extendsible-choice/scripts/fetch-git-branches.sh git.dangdang.com/prodapi/api-k8s.git" // Groovy中,字符串的execute方法,可以执行操作系统命令 def proc = cmd.execute() // 等待命令完成 proc.waitFor()               // 脚本退出码 if ( proc.exitValue() != 0 ) {     return [""] } //      读取标准输出,注意split返回的是数组,该插件要求返回值是列表 return  proc.in.text.split() as List  | 
					
| 
					 1 2  | 
						#!/bin/sh git ls-remote --heads --tags http://user:pswd@$1 | awk '{print $2}' | cut -d'/' -f3 | egrep '^[0-9\\.]+$'   | 
					
主要问题如下:
- gitea-expose权限不足,没有集群级别的权限。授权即可:
1234567subjects:- apiGroup: rbac.authorization.k8s.iokind: Groupname: system:masters- kind: ServiceAccountname: gitea-exposenamespace: jx - 某些资源要删除,否则后续进行的jx install会出错:
1kubectl -n jx delete configmap exposecontroller - 创建Ingress时出错:no known automatic ways to get an external ip to use with nip,解决办法:
12345678910cat <<EOF | kubectl create -n jx -f -apiVersion: "v1"data:config.yml: |-exposer: "Ingress"domain: "k8s.gmem.cc"kind: "ConfigMap"metadata:name: "exposecontroller"EOF 
由于jx install默认创建的PVC没有设置StorageClass,因此需要手工创建PV。或者可以修改资源定义,添加正确的storageClassName字段。
原因是Ingress没有指定有效的证书,解决办法:
| 
					 1 2  | 
						# 把密钥tls.key和证书tls.crt放在当前目录 kubectl -n jx create secret generic gmemk8scert-jx --from-file=.  | 
					
修改各Ingress,例如:
| 
					 1 2 3 4 5 6  | 
						# kubectl -n jx edit ingress chartmuseum   tls:   - hosts:     - chartmuseum.jx.k8s.gmem.cc     # 添加下面这行     secretName: gmemk8scert-jx  | 
					
            





Leave a Reply