文章目录
1. 流水线入门
工厂的流水线如下:
官方文档 的流水线如下:
为什么叫做流水线?其实和工厂产品的生产线类似,pipeline 是从源码到发布到线上环境。
关于流水线,需要知道的几个点:
- 重要的功能插件,帮助 Jenkins 定义了一套工作流框架;
- Pipeline 的实现方式是一套 Groovy DSL( 领域专用语言 ),所有的发布流程都可以表述为一段 Groovy 脚本;
- 将 WebUI 上需要定义的任务,以脚本代码的方式表述出来;
- 帮助jenkins实现持续集成 CI(Continue Integration)和持续部署 CD(Continue Deliver)的重要手段;
🍑 流水线基础语法
官方文档
两种语法类型:
- Scripted Pipeline,脚本式流水线,最初支持的类型
- Declarative Pipeline,声明式流水线,为 Pipeline plugin 在 2.5 版本之后新增的一种脚本类型,后续 Open Blue Ocean 所支持的类型。与原先的 Scripted Pipeline 一样,都可以用来编写脚本。Declarative Pipeline 是后续 Open Blue Ocean 所支持的类型,写法简单,支持内嵌 Scripted Pipeline 代码
注意:为与 BlueOcean 脚本编辑器兼容,通常建议使用 Declarative Pipeline 的方式进行编写,从 jenkins 社区的动向来看,很明显这种语法结构也会是未来的趋势。
🍑 脚本示例
json
脚本
pipeline {
agent {label '172.21.51.68'}
environment {PROJECT='myblog'}
stages {stage('Checkout'){
steps {
checkout scm
}}stage('Build'){
steps {
sh 'make'}}stage('Test'){
steps {
sh 'make check'
junit 'reports/**/*.xml'}}stage('Deploy'){
steps {
sh 'make publish'}}}
post {
success {
echo 'Congratulations!'}
failure {
echo 'Oh no!'}
always {
echo 'I will always say Hello again!'}}}
🍑 脚本解释
checkout
步骤为检出代码;scm
是一个特殊变量,指示checkout
步骤克隆触发此 Pipeline 运行的特定修订- agent:指明使用哪个 agent 节点来执行任务,定义于 pipeline 顶层或者 stage 内部- any,可以使用任意可用的 agent 来执行- label,在提供了标签的 Jenkins 环境中可用的代理上执行流水线或阶段。例如:
agent { label 'my-defined-label' }
,最常见的使用方式- none,当在pipeline
块的顶部没有全局代理,该参数将会被分配到整个流水线的运行中并且每个stage
部分都需要包含他自己的agent
部分。比如:agent none
- docker, 使用给定的容器执行流水线或阶段。 在指定的节点中,通过运行容器来执行任务
agent {
docker {
image 'maven:3-alpine'
label 'my-defined-label'
args '-v /tmp:/tmp'}}
- options:允许从流水线内部配置特定于流水线的选项。- buildDiscarder,为最近的流水线运行的特定数量保存组件和控制台输出。例如:
options { buildDiscarder(logRotator(numToKeepStr: '10')) }
- disableConcurrentBuilds,不允许同时执行流水线。 可被用来防止同时访问共享资源等。 例如:options { disableConcurrentBuilds() }
- timeout,设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。例如:options { timeout(time: 1, unit: 'HOURS') }
- retry,在失败时, 重新尝试整个流水线的指定次数。 For example:options { retry(3) }
- environment:指令制定一个 键-值对序列,该序列将被定义为所有步骤的环境变量
- stages:包含一系列一个或多个 stage 指令,
stages
部分是流水线描述的大部分 “work” 的位置。 建议stages
至少包含一个 stage 指令用于连续交付过程的每个离散部分,比如构建、测试和部署。
pipeline {
agent any
stages {stage('Example'){
steps {
echo 'Hello World'}}}}
- steps:在给定的
stage
指令中执行的定义了一系列的一个或多个 steps。 - post:定义一个或多个 steps ,这些阶段根据流水线或阶段的完成情况而运行
post
支持以下 post-condition 块中的其中之一:always
、changed
、failure
、success
、unstable
和aborted
。- always,无论流水线或阶段的完成状态如何,都允许在post
部分运行该步骤- changed,当前流水线或阶段的完成状态与它之前的运行不同时,才允许在post
部分运行该步骤- failure,当前流水线或阶段的完成状态为 “failure”,才允许在post
部分运行该步骤,通常 web UI 是红色- success,当前流水线或阶段的完成状态为 “success”,才允许在post
部分运行该步骤,通常 web UI 是蓝色或绿色- unstable,当前流水线或阶段的完成状态为 “unstable”,才允许在post
部分运行该步骤,通常由于测试失败,代码违规等造成。通常 web UI 是黄色- aborted,只有当前流水线或阶段的完成状态为 “aborted”,才允许在post
部分运行该步骤,通常由于流水线被手动的aborted。通常 web UI 是灰色
创建 pipeline 示意:新建任务 -> 流水线
jenkins/pipelines/p1.yaml
pipeline {
agent {label '172.21.51.68'}
environment {
PROJECT = 'myblog'
}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}}
stage('check') {
steps {
checkout([$class:'GitSCM',branches:[[name:'*/master']],doGenerateSubmoduleConfigurations:false,extensions:[],submoduleCfg:[],userRemoteConfigs:[[credentialsId:'gitlab-user',url:'http://gitlab.luffy.com/root/myblog.git']]])
}}
stage('build-image') {
steps {
sh 'docker build . -t myblog:latest -f Dockerfile'
}}
stage('send-msg') {
steps {
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=4778abd23dbdbaf66fc6f413e6ab9c0103a039b0054201344a22a5692cdcc54e' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"我就是我, 是不一样的烟火"}}'
"""
}}}}
点击 “立即构建”,同样的,我们可以配置触发器,使用 webhook 的方式接收项目的 push 事件
- 构建触发器选择
Build when a change is pushed to GitLab.
- 生成
Secret token
- 配置
gitlab
,创建webhook
,发送test push events
测试
🍑 Blue Ocean
官方文档
关于 Blue Ocean 我们需要知道的几点:
- 是一个插件, 旨在为 Pipeline 提供丰富的体验 ;
- 连续交付(CD)Pipeline 的复杂可视化,允许快速和直观地了解 Pipeline 的状态;
- 目前支持的类型仅针对于 Pipeline,尚不能替代 Jenkins 经典版UI
思考:
- 每个项目都把大量的 pipeline 脚本写在 Jenkins 端,对于谁去维护及维护成本是一个问题
- 没法做版本控制
2. Jenkinsfile实践
Jenkins Pipeline 提供了一套可扩展的工具,用于将 “简单到复杂” 的交付流程实现为 “持续交付即代码”。
Jenkins Pipeline 的定义通常被写入到一个文本文件(称为
Jenkinsfile
)中,该文件可以被放入项目的源代码控制库中。
🍑 演示一
演示1:使用 Jenkinsfile 管理 pipeline
- 在项目中新建 Jenkinsfile 文件,拷贝已有 script 内容
- 配置 pipeline 任务,流水线定义为 Pipeline Script from SCM
- 执行 push 代码测试
Jenkinsfile:
jenkins/pipelines/p2.yaml
pipeline {
agent {label '172.21.51.68'}
environment {
PROJECT = 'myblog'
}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}}
stage('check') {
steps {
checkout([$class:'GitSCM',branches:[[name:'*/master']],doGenerateSubmoduleConfigurations:false,extensions:[],submoduleCfg:[],userRemoteConfigs:[[credentialsId:'gitlab-user',url:'http://gitlab.luffy.com/root/myblog.git']]])
}}
stage('build-image') {
steps {
sh 'docker build . -t myblog:latest -f Dockerfile'
}}
stage('send-msg') {
steps {
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=4778abd23dbdbaf66fc6f413e6ab9c0103a039b0054201344a22a5692cdcc54e' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"我就是我, 是不一样的烟火"}}'
"""
}}}}
🍑 演示二
演示2:优化及丰富流水线内容
- 优化代码检出阶段由于目前已经配置了使用 git 仓库地址,且使用 SCM 来检测项目,因此代码检出阶段完全没有必要再去指定一次
- 构建镜像的 tag 使用 git 的 commit id
- 增加 post 阶段的消息通知,丰富通知内容
- 配置 webhook,实现 myblog 代码推送后,触发 Jenkinsfile 任务执行
jenkins/pipelines/p3.yaml
pipeline {
agent { label '172.21.51.68'}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}}
stage('check') {
steps {
checkout scm
}}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t myblog:${GIT_COMMIT}'}}}}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=4778abd23dbdbaf66fc6f413e6ab9c0103a039b0054201344a22a5692cdcc54e' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"😄👍构建成功👍😄\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"}}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=4778abd23dbdbaf66fc6f413e6ab9c0103a039b0054201344a22a5692cdcc54e' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"}}'
"""
}
always {
echo 'I will always say Hello again!'}}}
🍑 演示三
演示3:使用 k8s 部署服务
- 新建 deploy 目录,将 k8s 所需的文件放到 deploy 目录中
- 将镜像地址改成模板,在 pipeline 中使用新构建的镜像进行替换
- 执行
kubectl apply -f deploy
应用更改,需要配置 kubectl 认证
$ scp -r k8s-master:/root/.kube /root
jenkins/pipelines/p4.yaml
pipeline {
agent { label '172.21.51.68'}
environment {
IMAGE_REPO = "172.21.51.143:5000/myblog"
}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}}
stage('check') {
steps {
checkout scm
}}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}}}
stage('push-image') {
steps {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}}}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' manifests/*"timeout(time:1,unit: 'MINUTES') {
sh "kubectl apply -f manifests/"
}}}}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=4778abd23dbdbaf66fc6f413e6ab9c0103a039b0054201344a22a5692cdcc54e' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"😄👍构建成功👍😄\n 关键字:myblog\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"}}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=4778abd23dbdbaf66fc6f413e6ab9c0103a039b0054201344a22a5692cdcc54e' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"}}'
"""
}
always {
echo 'I will always say Hello again!'}}}
🍑 演示四
演示4:使用凭据管理敏感信息
上述 Jenkinsfile 中存在的问题是敏感信息使用明文,暴漏在代码中,如何管理流水线中的敏感信息(包含账号密码),之前我们在对接 gitlab 的时候,需要账号密码,已经使用过凭据来管理这类敏感信息,同样的,我们可以使用凭据来存储钉钉的 token 信息,那么,创建好凭据后,如何在 Jenkinsfile 中获取已有凭据的内容?
Jenkins 的声明式流水线语法有一个
credentials()
辅助方法(在 environment 指令中使用),它支持 secret 文本,带密码的用户名,以及 secret 文件凭据。
下面的流水线代码片段展示了如何创建一个使用带密码的用户名凭据的环境变量的流水线。
在该示例中,带密码的用户名凭据被分配了环境变量,用来使你的组织或团队以一个公用账户访问 Bitbucket 仓库;这些凭据已在 Jenkins 中配置了凭据 ID
jenkins-bitbucket-common-creds
。
当在 environment 指令中设置凭据环境变量时:
environment {
BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
}
这实际设置了下面的三个环境变量:
BITBUCKET_COMMON_CREDS
- 包含一个以冒号分隔的用户名和密码,格式为username:password
。BITBUCKET_COMMON_CREDS_USR
- 附加的一个仅包含用户名部分的变量。BITBUCKET_COMMON_CREDS_PSW
- 附加的一个仅包含密码部分的变量。
pipeline {
agent {// 此处定义 agent 的细节}
environment {//顶层流水线块中使用的 environment 指令将适用于流水线中的所有步骤。
BITBUCKET_COMMON_CREDS =credentials('jenkins-bitbucket-common-creds')}
stages {stage('Example stage 1'){//在一个 stage 中定义的 environment 指令只会将给定的环境变量应用于 stage 中的步骤。
environment {
BITBUCKET_COMMON_CREDS =credentials('another-credential-id')}
steps {// }}stage('Example stage 2'){
steps {// }}}}
因此对 Jenkinsfile 做改造:
jenkins/pipelines/p5.yaml
pipeline {
agent { label '172.21.51.68'}
environment {
IMAGE_REPO = "172.21.51.143:5000/myblog"
DINGTALK_CREDS = credentials('dingTalk')
}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}}
stage('check') {
steps {
checkout scm
}}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}}}
stage('push-image') {
steps {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}}}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' manifests/*"timeout(time:1,unit: 'MINUTES') {
sh "kubectl apply -f manifests/"
}}}}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"😄👍构建成功👍😄\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"}}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"}}'
"""
}
always {
echo 'I will always say Hello again!'}}}
🍑 总结
上面我们已经通过 Jenkinsfile 完成了最简单的项目的构建和部署,那么我们来思考目前的方式:
目前都是在项目的单一分支下进行操作,企业内一般会使用 feature、develop、release、master 等多个分支来管理整个代码提交流程,如何根据不同的分支来做构建?
构建视图中如何区分不同的分支?
如何不配置 webhook 的方式实现构建?
如何根据不同的分支选择发布到不同的环境(开发、测试、生产)?
3. 多分支流水线实践
官方示例
我们简化一下流程,假如使用 develop 分支作为开发分支,master 分支作为集成测试分支,看一下如何使用多分支流水线来管理。
🍑 演示一
演示1:多分支流水线的使用
(1)提交develop分支:
$ git checkout -b develop
$ git push --set-upstream origin develop
(2)禁用 pipeline 项目
(3)Jenkins 端创建多分支流水线项目
- 增加 git 分支源
- 发现标签
- 根据名称过滤,
develop|master|v.*
- 高级克隆,设置浅克隆
保存后,会自动检索项目中所有存在 Jenkinsfile 文件的分支和标签,若匹配我们设置的过滤正则表达式,则会添加到多分支的构建视图中。所有添加到视图中的分支和标签,会默认执行一次构建任务。
🍑 演示二
演示2:美化消息通知内容
- 添加构建阶段记录
- 使用 markdown 格式,添加构建分支消息
jenkins/pipelines/p6.yaml
pipeline {
agent { label '172.21.51.68'}
environment {
IMAGE_REPO = "172.21.51.143:5000/myblog"
DINGTALK_CREDS = credentials('dingTalk')
TAB_STR = "\n \n "}
stages {
stage('printenv') {
steps {
script{
sh "git log --oneline -n 1 > gitlog.file"
env.GIT_LOG = readFile("gitlog.file").trim()
}
sh 'printenv'
}}
stage('checkout') {
steps {
checkout scm
script{
env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
}}}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}}}
stage('push-image') {
steps {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}}}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' manifests/*"timeout(time:1,unit: 'MINUTES') {
sh "kubectl apply -f manifests/"
}
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}}}}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{
"title":"myblog","text":"😄👍 构建成功 👍😄 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${GIT_BRANCH} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"}}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{
"title":"myblog","text":"😖❌ 构建失败 ❌😖 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${GIT_BRANCH} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"}}'
"""
}
always {
echo 'I will always say Hello again!'}}}
🍑 演示三
演示3:通知 gitlab 构建状态
Jenkins 端做了构建,可以通过 gitlab 通过的 api 将构建状态通知过去,作为开发人员发起 Merge Request 或者合并 Merge Request 的依据之一。
*注意一定要指定
gitLabConnection('gitlab')
,不然没法认证到 Gitlab 端*
jenkins/pipelines/p7.yaml
pipeline {
agent { label '172.21.51.68'}
options {buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
timeout(time:20,unit: 'MINUTES')
gitLabConnection('gitlab')
}
environment {
IMAGE_REPO = "172.21.51.143:5000/demo/myblog"
DINGTALK_CREDS = credentials('dingTalk')
TAB_STR = "\n \n "}
stages {
stage('printenv') {
steps {
script{
sh "git log --oneline -n 1 > gitlog.file"
env.GIT_LOG = readFile("gitlog.file").trim()
}
sh 'printenv'
}}
stage('checkout') {
steps {
checkout scm
updateGitlabCommitStatus(name: env.STAGE_NAME,state: 'success')
script{
env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
}}}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}updateGitlabCommitStatus(name: env.STAGE_NAME,state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}}}
stage('push-image') {
steps {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}updateGitlabCommitStatus(name: env.STAGE_NAME,state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}}}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' manifests/*"timeout(time:1,unit: 'MINUTES') {
sh "kubectl apply -f manifests/"
}updateGitlabCommitStatus(name: env.STAGE_NAME,state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}}}}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{
"title":"myblog","text":"😄👍 构建成功 👍😄 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"}}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{
"title":"myblog","text":"😖❌ 构建失败 ❌😖 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"}}'
"""
}
always {
echo 'I will always say Hello again!'}}}
我们可以访问 gitlab,然后找到 commit 记录,查看同步状态:
提交 merge request,也可以查看到相关的任务状态,可以作为项目 owner 合并代码的依据之一:
🍑 总结
优势:
- 根据分支展示,视图人性化
- 自动检测各分支的变更
思考:
- Jenkins 的 slave 端,没有任务的时候处于闲置状态,slave 节点多的话造成资源浪费
- 是否可以利用 kubernetes 的 Pod 来启动 slave,动态 slave pod 来执行构建任务
版权归原作者 Albert Edison 所有, 如有侵权,请联系我们删除。