手把手带你一起从0到1搭建一个企业级的自动化构建流程 网上完整的关于多分支流水线的配置很少,希望这篇不短的文章能给你带来帮助
简介
- 从0到1打造前后端自动化工作流
- 企业级的实践笔记
- 最佳实践(可在此基础逐步完善)
缘起
由于公司的Jenkins配置没有部署成功的通知,在我学了几天的Jenkins后终于是对公司的Jenkins配置下手了,结果我刚装完dingtalk
插件自动重启后,发现之前主管配置的构建项目数据都丢失了,害,正好给了我练手的机会,于是就有了以下从0到1的辛酸历程。
在Docker中安装并运行Jenkins
这里假设你的服务器已经装好了docker
使用的镜像是jenkinsci/blueocean
,这是一个jenkins的稳定及持续维护的镜像源,本身就集成了Blue Ocean等使用插件,非常方便。
拉取镜像
docker pull jenkinsci/blueocean
运行Jenkins
docker run -idt --name kmywjenkins -p 9090:8080 -p 60000:50000 -v jenkins-data:/var/jenkins_home -v /data/web-data/docker.sock:/var/run/docker.sock jenkinsci/blueocean
参数解释:
-idt
以交互的方式、新建一个模拟终端运行容器
--name
容器的别名
-p
指定容器映射宿主机的端口 -> 宿主机端口:容器端口
-v jenkins-data:/var/jenkins_home
Jenkins容器在工作的时候,如果要执行Docker的命令(例如 docker ps、docker run等),需要有个途径能连接到宿主机的docker服务,此参数就是用来建立容器和宿主机docker服务的连接的
-v /data/web-data/docker.sock:/var/run/docker.sock
将该容器的数据保留在宿主机的目录,这样即使容器崩溃了,里面的配置和任务都不会丢失
需要注意的是,docker中默认是以jenkins
用户运行的Jenkins,如需以root用户可以加参数-u root
,本示例未指定root。
访问Jenkins Docker容器
有时候需要进入Jenkins容器执行一些命令,可以通过docker exec
命令访问,例如:docker exec -it [containerid] bash
若要手动重启Jenkins,可以执行以下命令:docker restart [containerid]
Jenkins基本配置
通过以上步骤,如果正常走到这里,可以通过以下地址访问http://121.41.16.183:9090/
,ip地址为服务器的地址。
解锁jenkins
输入一下命令获取解锁的token,docker exec kmywjenkins cat /var/jenkins_home/secrets/initialAdminPassword
在浏览器中输入对应的token以解锁:
创建凭据
连接git仓库,ssh连接服务器均需要相应的凭据,可以在凭据管理中先创建好,然后需要使用的地方直接选择凭据即可。这里以连接git、ssh需要的凭据为例:
- 我司用得版本管理工具是gitte,以gitte为例,其它版本管理工具配置也一样
类型选择Username with password
用户名密码为登录gitte的账号密码
ID是凭据的唯一标识,可自定义,后面在JenkinsFile中通过ID去引用凭据
配置后的结果
-
ssh连接服务器时需要密钥,我们先在服务器生成一对公私钥,然后复制私钥,填入即可
类型选择
SSH Username with private key
Username
是连接服务器的用户名,如jenkins
在
Private Key
项选中Enter directly
,点击Add,粘贴刚复制的私钥
配置后的结果
创建一个多分支流水线
之前的Jenkins任务是FreeStyle的方式创建的,这种方式不够灵活,界面也不够清爽,这里选择使用声明式流水线方式(Declarative Pipeline)创建,可以多分支独立构建,便于以后的扩展。
我们这里使用 BlueOcean 这种方式来完成此处 CI/CD 的工作,BlueOcean 是 Jenkins 团队从用户体验角度出发,专为 Jenkins Pipeline 重新设计的一套 UI 界面,仍然兼容以前的 fressstyle 类型的 job,BlueOcean 具有以下的一些特性:
- 连续交付(CD)Pipeline 的复杂可视化,允许快速直观的了解 Pipeline 的状态
- 可以通过 Pipeline 编辑器直观的创建 Pipeline
- 需要干预或者出现问题时快速定位,BlueOcean 显示了 Pipeline 需要注意的地方,便于异常处理和提高生产力
- 用于分支和拉取请求的本地集成可以在 GitHub 或者 Bitbucket 中与其他人进行代码协作时最大限度提高开发人员的生产力。
如果安装的是jenkinsci/blueocean
镜像,默认是已经集成了BlueOcean,没有的可前往插件管理安装对应的插件。
点击打开Blue Ocean,可以看到已经创建好的两个流水线,分别是前端和后台,需要用到不同的工具,在后面会提到,如何创建流水线
点击创建流水线
我司用的是gitte,所以选择Git,然后填入要连接的仓库地址,需要连接到Git仓库的凭据,我们之前已经创建好了,直接选中即可,如果未创建,在下面的表单直接编辑即可,最后点击创建流水线
到这里我们就创建了一个多分支流水线,Jenkins会扫描仓库,带有JenkinsFile的分支会被检测出来,JenkinFile是多分支流水线的配置文件,使用的是Groovy语法,可以直接点击创建流水线,Jenkins会自动为你的项目创建一个JenkinsFile
现在可以可视化地编辑想要执行的阶段及步骤,这里加了一个打包的阶段,里面有个步骤是提示开始打包
,点击保存
填入提交信息,点击Save & Run
,会讲JenkinsFile上传到git,并根据JenkinsFile执行一个构建任务,目前的构建步骤只有一个,是提示开始打包
我这里不知道为什么会卡在这个地方不动,所以我在vscode直接创建并编辑JenkinsFile,这种方式更灵活,我更推荐这种方式,下面我会先简单介绍下JeninsFile的基础语法,仅包含本项目用到的,对于中小企业的构建需求,基本够用了。
JenkinsFile基础语法
只需先了解大致的语法,具体的用法会在后面说明
// 前端项目JenkinsFile配置,后端项目配置稍有不同,后面会区分说明
pipeline {
agent any
environment {
HOST_TEST = 'root@121.41.16.183'
HOST_ONLINE = 'jenkins@39.101.219.110'
SOURCE_DIR = 'dist/*'
TARGET_DIR = '/data/www/kuaimen-yunying-front'
}
parameters {
choice(
description: '你需要选择哪个环境进行部署 ?',
name: 'env',
choices: ['测试环境', '线上环境']
)
string(name: 'update', defaultValue: '', description: '本次更新内容?')
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'ref', value: '$.ref']
],
causeString: 'Triggered on $ref',
token: 'runcenter-front-q1w2e3r4t5',
tokenCredentialId: '',
printContributedVariables: true,
printPostContent: true,
silentResponse: false,
regexpFilterText: '$ref',
regexpFilterExpression: 'refs/heads/' + BRANCH_NAME
)
}
stages {
stage('获取git commit message') {
steps {
script {
env.GIT_COMMIT_MSG = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
}
}
}
stage('打包') {
steps {
nodejs('nodejs-12.16') {
echo '开始安装依赖'
sh 'yarn'
echo '开始打包'
sh 'yarn run build'
}
}
}
stage('部署') {
when {
expression {
params.env == '测试环境'
}
}
steps {
sshagent(credentials: ['km-test2']) {
sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
sh "scp -r ${SOURCE_DIR} ${HOST_TEST}:${TARGET_DIR}"
sh 'echo "部署成功~"'
}
}
}
stage('发布') {
when {
expression {
params.env == '线上环境'
}
}
steps {
sshagent(credentials: ['km-online']) {
sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
sh "scp -r ${SOURCE_DIR} ${HOST_ONLINE}:${TARGET_DIR}"
sh 'echo "发布成功~"'
}
}
}
}
post {
success {
dingtalk (
robot: '77d4c82d-3794-4583-bc7f-556902fee6b0',
type: 'MARKDOWN',
atAll: true,
title: '你有新的消息,请注意查收',
text:[
'# 运营管理系统发布通知',
'---',
'#### **所属:前端**',
"#### **构建任务:${env.BUILD_DISPLAY_NAME}**",
"#### **Git commit:${env.GIT_COMMIT_MSG}**",
"#### **本次更新内容:${params.update}**",
"#### **部署环境:${params.env}**",
'#### **构建结果:成功**'
]
)
}
}
}
pipeline
必须在最外层
agent
定义了在哪个环境里执行,默认any
stages
阶段,标识构建流程的标签块,子节点是stage
steps
执行步骤
post
所有阶段执行完成后执行一些逻辑
when
可以控制该阶段是否执行
environment
环境变量,在这里定义的变量,JenkinsFile的任何地方都可以访问
tools
项目使用到的构建工具,声明系统配置中已经定义好的工具,如maven
parameters
定义参数,可以提供用户输入或者选择
post
构建结束后会执行这里,有success
、failure
、success
,本示例将在success
(构建成功时)发起钉钉通知
CI/CD流程
由于我司的技术团队较小,CI/CD流程就没那么复杂,不会包含代码检查、自动化测试、Code Review等流程,我将简要说明我所搭建的前端与后端CI/CD流程以及为什么这么搭建。
前端
提供两种构建方式,一种是代码上传自动构建,一种是参数化构建,可选择部署到测试环境还是线上环境。
自动构建默认部署到测试环境,由于线上环境很重要,自动化构建会有一定风险,所以需要人工干预选择参数进行构建。
- 提交代码到master,自动触发构建 如果是参数化构建,这一步是手动选择要构建的环境,然后开始构建
- 安装依赖
- 打包
- 上传到服务器
- 如果成功发起钉钉通知
后端
后端的所有项目都是放在一个git仓库中,所以就没有做自动构建
- 参数化构建 可选择要构建的环境、打包的项目、是否需要全量打包
- 清除旧数据
- 打包
- 上传到服务器
- 杀掉相应的进程
- 启动相应的进程
- 如果成功发起钉钉通知
接下来就每一步作详细说明,以及可能遇到的坑
自动触发构建
什么是自动触发构建
当我们提交新的代码到git仓库,Jenkins就会自动开始构建已经配置好的该项目的任务
原理
在git仓库配置一个Jenkins服务器的webhook地址,当git仓库有变动时会请求这个地址,Jenkins就能收到通知然后开始构建任务
配置
-
我们需要先安装一个插件Multibranch Scan Webhook Trigger,可进入插件管理搜索进行安装
-
进入项目的配置界面,勾选Scan by webhook,填入自定义token,需要确保token的唯一性,不会与其它项目的冲突
-
过滤分支
这是一个多分支流水线,Jenkins默认会检出所有包含Jenkinsfile的分支,如果配置了webhook,就会自动触发相应分支的构建任务;有时候我们只想master发生变化后才去构建任务,这时就用到了过滤分支的配置,进入项目配置,在分支源git
项找到add
按钮并点击
选择根据名称过滤(支持通配符)
,或者你可以选择根据名称过滤(支持正则表达式)
,效果一样,只是过滤格式不太一样,我这里在相应的地方填入master
,即只检索master
分支,这样就达到我们想要的效果了。
-
进入远端仓库(我这里是Gitte),点击Webhooks,接着点击添加 WebHook
-
填入URL,IP地址为Jenkins部署的服务器,token为我们刚设置的,
/multibranch-webhook-trigger/invoke
是固定地址,点击添加 -
会自动发起一个请求,即我们刚填写的,如相应如下则表示配置成功,相应的构建任务也会自动执行
自动化打包
前端
使用了yarn
进行安装依赖及打包,需要先配置nodejs环境
- 进入插件管理搜索
nodejs
进行安装 - 进入全局工具配置,新增如下配置,别名可以自定义,建议格式为
nodejs-版本号
,该项目用的是yarn
,所以在Global npm package to install
,加入了配置项,构建的时候会自动安装yarn
,如果是npm可以忽略该配置 - Jenkinsfile配置 前端的比较简单
pipeline {
stage('打包') {
steps {
// 执行环境,nodejs-12.16是我们刚配置的别名,还有一种方式是在agent中配置执行环境,在tools中配置使用的包,感兴趣的可以自行研究
nodejs('nodejs-12.16') {
echo '开始安装依赖'
sh 'yarn'
echo '开始打包'
sh 'yarn run build'
}
}
}
}
后端(Java)
pipeline {
tools {
maven 'Maven3.6.3'
}
parameters {
// 提供要部署的服务器选项
choice(
description: '你需要选择哪个环境进行部署 ?',
name: 'env',
choices: ['测试环境', '线上环境']
)
// 提供构建的模块选项
choice(
description: '你需要选择哪个模块进行构建 ?',
name: 'moduleName',
choices: ['kuaimen-contract', 'kuaimen-core', 'kuaimen-eureka-server', 'kuaimen-manage', 'kuaimen-member', 'kuaimen-order', 'kuaimen-shop', 'tiemuzhen-manage']
)
booleanParam(name: 'isAll', defaultValue: false, description: '是否需要全量(包含clean && build)')
string(name: 'update', defaultValue: '', description: '本次更新内容?')
}
stages {
stage('全量清除旧数据...') {
when {
expression {
params.isAll == true
}
}
steps {
echo "开始全量清除"
sh "mvn package clean -Dmaven.test.skip=true"
}
}
stage('全量打包应用') {
when {
expression {
params.isAll == true
}
}
steps {
echo "开始全量打包"
sh "mvn package -Dmaven.test.skip=true"
echo '打包成功'
}
}
stage('清除旧数据...') {
when {
expression {
params.isAll == false
}
}
steps {
echo "开始清除${params.moduleName}模块"
sh "cd ${params.moduleName} && mvn package clean -Dmaven.test.skip=true"
}
}
stage('打包应用') {
when {
expression {
params.isAll == false
}
}
steps {
echo "开始打包${params.moduleName}模块"
sh "cd ${params.moduleName} && mvn package -Dmaven.test.skip=true"
echo '打包成功'
}
}
}
}
parameters
parameters
中主要是提供参数化构建的选项,在其它地方可以通过"${params.isAll}"
这种形式拿到用户的交互信息,配置后效果如下:
when>expression
表达式中的参数如果未true
,则执行,反之跳过该stage
mvn
在系统配置中默认就已经提供了该环境,进入系统全局工具配置,添加如下配置(类似nodejs)
这种方式引用
tools {
maven 'Maven3.6.3'
}
自动化部署
前端
pipeline {
agent any
environment {
HOST_TEST = 'root@121.41.16.183'
HOST_ONLINE = 'jenkins@39.101.219.110'
SOURCE_DIR = 'dist/*'
TARGET_DIR = '/data/www/kuaimen-yunying-front'
}
stage('部署') {
when {
expression {
params.env == '测试环境'
}
}
steps {
sshagent(credentials: ['km-test2']) {
sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
// 将打包好的文件上传到服务器
sh "scp -r ${SOURCE_DIR} ${HOST_TEST}:${TARGET_DIR}"
sh 'echo "部署成功~"'
}
}
}
stage('发布') {
when {
expression {
params.env == '线上环境'
}
}
steps {
sshagent(credentials: ['km-online']) {
sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
sh "scp -r ${SOURCE_DIR} ${HOST_ONLINE}:${TARGET_DIR}"
sh 'echo "发布成功~"'
}
}
}
}
}
environment
定了全局变量,在其它地方可直接引用
sshagent
用于连接服务器,需要先安装插件ssh-agent,credentials
是连接服务器的凭据ID,我们在一开始已经教大家创建好了
后端
pipeline {
agent any
environment {
HOST_TEST = 'root@121.41.16.183'
TARGET_DIR = '/data/www/kuaimen-auto'
HOST_ONLINE = 'jenkins@39.101.219.110'
}
tools {
maven 'Maven3.6.3'
}
stage('部署应用') {
when {
expression {
params.env == '测试环境'
}
}
steps {
echo "开始部署${params.moduleName}模块"
sshagent(credentials: ['km-test2']) {
sh "ssh -v -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
// 将打包后的文件上传到服务器
sh "cd ${params.moduleName}/target && scp *.jar ${HOST_TEST}:${TARGET_DIR}/${params.moduleName}"
// 匹配出该Java进程然后杀掉
sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} \"uname;ps -ef | egrep ${params.moduleName}.*.jar | egrep -v grep | awk '{print \\\$2}' | xargs -r sudo kill -9\""
// 启动该进程
sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} \"nohup /data/apps/jdk1.8/bin/java -jar ${TARGET_DIR}/${params.moduleName}/${params.moduleName}-0.0.1-SNAPSHOT.jar --spring.profiles.active=test >/dev/null 2>&1 &\""
sh 'echo "部署成功~"'
}
echo '部署成功'
}
}
stage('发布应用') {
when {
expression {
params.env == '线上环境'
}
}
steps {
echo "开始发布${params.moduleName}模块"
sshagent(credentials: ['km-online']) {
sh "ssh -v -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
sh "cd ${params.moduleName}/target && scp *.jar ${HOST_ONLINE}:${TARGET_DIR}/${params.moduleName}"
sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} \"uname;ps -ef | egrep ${params.moduleName}.*.jar | egrep -v grep | awk '{print \\\$2}' | xargs -r sudo kill -9\""
sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} \"nohup /data/apps/jdk1.8/bin/java -jar ${TARGET_DIR}/${params.moduleName}/${params.moduleName}-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev >/dev/null 2>&1 &\""
sh 'echo "发布成功~"'
}
echo '发布成功'
}
}
}
需要注意的是,在匹配进程的那段shell中的awk '{print \\\$2}'
,$
符号需要用三个反斜线进行转义,不然会无法执行成功,这里曾卡了好久,希望你们别踩坑了
部署完成后发起通知
我们这里使用钉钉发起通知,主要原理是在钉钉群创建一个webhook机器人,然后把webhook的地址填入DingTalk插件的配置项,最后在JenkinsFile中进行如下配置即可:
pipeline {
stage('获取git commit message') {
steps {
script {
// 将获取到的git commit赋值给GIT_COMMIT_MSG
env.GIT_COMMIT_MSG = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
}
}
}
post {
success {
dingtalk (
robot: '77d4c82d-3794-4583-bc7f-556902fee6b0',
type: 'MARKDOWN',
atAll: true,
title: '你有新的消息,请注意查收',
text:[
'# 运营管理系统发布通知',
'---',
'#### **所属:后端**',
"#### **构建任务:${env.BUILD_DISPLAY_NAME}**",
"#### **本次更新内容:${params.update}**",
"#### **部署环境:${params.env}**",
'#### **构建结果:成功**'
]
)
}
}
}
GIT_COMMIT
这个是Jenkins系统全局变量,获得的是git commit ID,然后通过它拿到具体的提交信息,并赋值给env.GIT_COMMIT_MSG
,全局变量可以通过这种方式访问env.BUILD_DISPLAY_NAME
robot为机器人ID,在系统配置中添加如下配置项
webhook在创建完机器人的时候能够拿到
如何创建钉钉机器人
点击群设置 -> 智能群助手
选择自定义机器人,配置完成后就可以看到webhook的地址了
开始构建
经过上面的配置,我们已经完成了前端、后台的自动化构建配置,接下来再说明一下分别是如何触发构建的
前端
- 提交代码到master,会自动执行构建任务,并部署到测试环境,部署成功后会在钉钉群发起提醒
- 参数化构建
点击
Build with Parameters
,选择相应的参数进行构建,线上环境必须通过这种方式,保证一定的安全性
后端
后端只配置了参数化构建,原因前面已经说了,选择要构建的环境、模块进行构建
使用Blue Ocean构建(推荐)
点击打开Blue Ocean
选择要构建的分支
弹出参数选择,这和Build with Parameters
差不多,但是界面更好看,更清爽了,选择后点击Run
即可开始构建
构建结果,很直观,根据颜色可以判断构建成功了,如果失败了是红色
回滚
在Blue Ocean的活动栏可以看到历史构建,点击如下位置的按钮可以重新构建该历史项,即回滚
写在最后
到这里终于告一段落了,虽然折腾了不少时间,但是将公司的工程化流程完善了还是有点小小的成就感的,以后可以愉快得写代码了,自动化的事情就交给Jenkins了。
将这个记录下来一个是方便以后随时查阅,还有一个是希望能让朋友们少踩些坑,完~
附录
BlueOcean实战多分支pipeline构建(Jenkins)
Complete Jenkins Pipeline Tutorial for Beginners [FREE]