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