用代码实现流水线部署,像诗一般优雅
用代码实现流水线部署,像诗一般优雅
你好,我是悟空。
本文目录如下:
目录
前言
上次我们已经聊过关于部署的三个话题:
1、本地打包 docker 镜像部署到服务器
2、通过 Jenkins 工具 一键部署 Gitlab 上的 Java 代码到服务器。
3、一键部署前端 Angular 项目
这次我们要接着上面的话题聊下如何通过通过编写代码的方式实现自动化部署 Java 项目。
而用代码方式其实就是使用 Jenkins 强大的 pipeline 功能来实现的。
通过本篇你可以学习到如下内容:
- Pipeline 的概念、优点、缺点。
- 实战:通过编写 pipeline 来部署一个完整的后端项目。
- pipeline 传参的合理利用。
- 多选插件、远程传输文件、远程执行命令的使用。
一、Pipeline
1.1 流水线
要了解什么是Pipeline,就必须知道什么是流水线。类似于食品工厂包装食品,食品被放到传送带上,经过一系列操作后,包装完成,这种工程就是流水线工程。
流水线示例图(图片来自网络,如侵权,请联系删除)
1.2 Pipeline 是什么
在自动化部署中,开发完成的代码经过一系列顺序操作后被部署完成,这个就是部署过程中的流水线,我们通常称作 pipeline。
之前我们的部署步骤都是通过在 Jenkins 的 UI 界面上配置出来的,但其实 Jenkisn 2.x 版本已经可以支持编写代码的方式来启动自动化部署了,通过“代码”来描述部署流水线。
Jenkins pipeline其实就是基于一种声明式语言,用于描述整条流水线是如何进行的。流水线的内容包括执行编译、打包、测试、输出测试报告等步骤。
1.3 为什么要用 Pipeline
Pipeline 通过代码来实现,其实就具有很多代码的优势了,比如:
- 支持传参:可以在 Pipeline 代码里面配置用户要输入或选择的参数,这个功能真的太棒了。比如可以传 Gitlab 分支名、部署哪个服务等。
- 更好地版本化:将 pipeline 代码提交到软件版本库中进行版本控制。
- 更好地协作:pipeline 的每次修改对所有人都是可见的。除此之外,还可以对pipeline进行代码审查
- 更好的重用性:手动操作没法重用,但是代码可以重用。
当然 pipeline 的缺点也是有的:
- 学习成本高:需要熟悉 pipeline 的语法规则。
- 复杂:代码不够直观,编写的逻辑可能很复杂,容易出错。
1.4 如何使用 Pipeline
在之前的文章中,我是通过创建一个自由风格的项目来实现自动化部署,其实还可以通过创建一个Pipeline 来实现,如下图所示:
创建 Pipeline 任务
然后就可以在配置流水线的地方编写代码了。如下图所示:
编写流水线代码
1.5 Pipeline 基础结构
Pipeline 基础结构如下所示:
pipeline{
//指定运行此流水线的节点
agent any
//流水线的阶段
stages{
//阶段1 获取代码
stage("CheckOut") {
steps {
script {
echo "获取代码"
}
}
}
}
- pipeline 部分:代表整条流水线,包含整条流水线的逻辑。
- stage 部分:代表流水线的某个阶段。每个阶段都必须有名称,本例中,"CheckOut" 就是此阶段的名称。
- stages 部分:流水线中多个stage的容器。stages 部分至少包含一个 stage。steps 部分:代表阶段中的一个或多个具体步骤(step)的容器。steps 部分至少包含一个步骤,本例中,echo就是一个步骤。在一个 stage 中有且只有一个steps。
- agent 部分:指定流水线的执行位置(Jenkins agent)。流水线中的每个阶段都必须在某个地方(物理机、虚拟机或Docker容器)执行,agent 部分即指定具体在哪里执行。
以上每一个部分都不能少,否则 Jenkins 会报错。
二、部署思路
2.1 Jenkins 承担的角色
Jenkins 承担的角色如下图所示:
Jenkins 打包部署原理图
- (1)Jenkins 部署在一台服务器上,然后安装了很多必备的 Jenkins 插件。比如拉取 Gitlab 仓库代码的插件、远程执行命令和拷贝文件的插件。
- (2)Jenkins 开始运行一个任务时,通过 Git 插件从 Gitlab 仓库拉取代码到本地目录。
- (3)Jenkins 通过 JDK 和 Maven 工具对 Java 代码进行打包部署。
- (4)Jenkins 将 JAR 包拷贝到远程服务器的固定目录下。
- (5)Jenkins 通过 SSH 插件执行远程命令,将包进行备份操作。
- (6)Jenkins 通过执行远程命令,更新 JAR 包。
- (7)Jenkins 通过执行远程命令,重启容器。
2.2 通过流水线来部署项目
我们项目是 Java 项目,所以通过流水线来部署项目的步骤如下图所示:
流水线部署步骤
三、获取 Gitlab 分支代码
Pipeline 的强大之处是可以支持传参以及获取参数,为了让用户可以选择获取不同的分支代码,我在 pipeline 代码中配置了一个参数:获取指定的 Gitlab 分支代码。
3.1 Gitlab 分支配置
在 流水线代码中添加 parameters 节点,指定类型为 string,配置相关的属性。
pipeline {
parameters {
string (
name: 'GIT_BRANCH', // 参数名,后面 steps 中会用到
defaultValue: 'dev-01.30', // 默认值,会显示在界面上,用户可以改。
description: '请选择部署的分支' // 说明
)
}
// 其他代码
...
}
通过参数部分,定义了一个名为GIT_BRANCH
的参数,它允许用户在构建过程中选择要构建的分支。默认情况下,分支被设置为dev-01.30
,用户可以选择不同的分支。
在脚本中,这个参数可以通过params.GIT_BRANCH
获取到。
保存配置后,需要先运行一次这个项目才能看到参数配置。如下图所示:右边就是参数配置。
Build with Parameters
3.2 配置环境参数
接着我们还需要定义一些常用的环境变量信息,比如 Gitlab 的仓库地址,代码如下所示:
pipeline {
parameters {
// 配置信息
...
}
// 环境变量:定义 GitLab 仓库的 URL 和分支
environment {
GIT_URL = 'https://xxx/xxx.git'
}
// 其他代码
...
}
environment 节点为环境变量信息,GIT_URL 变量代表 Gitlab 的仓库地址。在脚本中,这个变量可以通过${GIT_URL}
使用。
3.3 获取 Gitlab 分支代码
接下来我们来看下如何在 pipeline 中添加一个获取 gitlab 仓库代码的步骤。
pipeline {
agent any
parameters {
string(
name: 'GIT_BRANCH',
defaultValue: 'dev-01.30',
description: '请选择部署的分支')
}
// 定义 GitLab 仓库的 URL 和分支
environment {
GIT_URL = 'https://XXX/xxx.git'
}
stages {
stage('获取最新代码') {
steps {
script {
// 使用 params 对象获取参数值
def branchName = params.GIT_BRANCH
echo "Building branch: ${branchName}"
// 使用 git 插件检出仓库的特定分支
checkout([
$class: 'GitSCM',
branches: [[name: "${branchName}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [],
submoduleCfg: [],
userRemoteConfigs: [[
credentialsId: '211583e9-8ee1-4fa2-9edd-d43a963de8f2', // 在 Jenkins 凭据中定义的 GitLab 凭据 ID
url: "${GIT_URL}"
]]
])
}
}
}
}
}
- **参数定义:**通过参数部分,定义了一个名为
GIT_BRANCH
的参数,它允许用户在构建过程中选择要构建的分支。默认情况下,分支被设置为dev-01.30
,用户可以选择不同的分支。 - **环境变量定义:**在环境部分,设置了
GIT_URL
变量,它是GitLab仓库的URL。在脚本中,这个变量可以通过${GIT_URL}
使用。 - **阶段定义:**在
stages
部分,定义了一个名为"获取最新代码"的阶段。 - **步骤定义:**在阶段内,使用了
script
块来执行Groovy脚本。这个脚本首先获取了GIT_BRANCH
参数的值,然后使用Jenkins的Git插件检出指定的分支。 - 检出代码:
checkout
步骤是用来从GitLab仓库检出代码的关键部分。它使用了GitSCM类,并传递了相应的参数,包括分支名、GitLab凭据等。
注意:获取分支的凭证是一个 ID,这个凭证信息是在 Jenkins 系统配置中加的。可以按照如下页面路径添加凭证:Dashboard->Manage Jenkins->Credentials->System->Add domain。也可以通过如下 URL 访问
http://<你的服务器 IP>:8080/manage/credentials/store/system/
3.4 测试 pipeline 执行
我们可以运行一下这个项目来测试 pipeline 代码。运行结果如下图所示,可以看到右侧的阶段视图,整体耗时和每个步骤的耗时,以及每个步骤的成功与否都显示出来了,非常直观。
四、编译代码
本篇主要讲解的是部署 Java 项目,所以编译项目也是采用 Maven 打包的方式。在 pipeline 脚本中执行 mvn 打包命令即可。
stage('编译代码') {
steps {
script {
echo "--------------- 步骤:开始编译 --------------- "
bat 'mvn clean package'
echo "--------------- 步骤:编译完成 --------------- "
}
}
}
核心代码:bat 'mvn clean package'
因为我的 Jenkins 是部署在 Windows 机器上,所以执行命令用的 windows 自带的 bat 工具来执行的。
关于 maven 工具的配置可以看之前写的第二篇内容:
五、上传 JAR 包
编译完成后,就可以将 Jenkins 工作空间的 JAR 包上传到服务器的 temp 目录下。
如果你想部署指定的某些微服务,可以通过传参的方式来上传和更新指定的微服务。原理图如下所示:
5.1 支持勾选多个服务
为了实现可以选择部署哪些微服务,需要安装一个多选插件:Extended Choice Parameter。
Extended Choice Parameter 插件
接下来是编写的参数配置代码:
parameters {
extendedChoice (
defaultValue: 'All',
description: '需要部署的微服务',
multiSelectDelimiter: ',',
name: 'SERVICE_NAME',
quoteValue: false,
saveJSONParameterToFile: false,
type: 'PT_CHECKBOX',
value:'All, passjava-account, passjava-file',
visibleItemCount: 10
)
}
defaultValue
: 参数的默认值。在这里,默认值为'All'
。description
: 参数的描述或提示。这里描述为'需要部署的微服务'
,表示选择需要部署的微服务。multiSelectDelimiter
: 多选时的分隔符。这里设置为','
,表示使用逗号作为分隔符。name
: 参数的名称。这里是'SERVICE_NAME'
。quoteValue
: 确定是否对值加上引号。这里设置为false
,表示不加引号。saveJSONParameterToFile
: 是否将 JSON 参数保存到文件。type
: 参数的类型。这里是'PT_CHECKBOX'
,表示复选框类型。value
: 可选的值列表。在这里,可选的值有'All'
、'passjava-account'
、'passjava-file'
。visibleItemCount
: 可见的选项数量。这里设置为10
。
配置保存后并运行一次后,就可以在 pipeline 中看到配置选项:
实现的效果如下图右下角所示,可以支持多选。
5.2 上传 JAR 包
上传需要使用 sshPublisher 插件,在第二篇文章中已介绍了。
下面上传代码的作用是遍历 filesToCopy
列表中的文件,然后通过 SSH 将这些文件上传到远程服务器的指定目录中。
filesToCopy.eachWithIndex { file, index ->
echo "开始上传 JAR 包 ${file} ..."
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: '',
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: 'apps/temp/',
remoteDirectorySDF: false,
removePrefix: removePrefixs[index],
sourceFiles: file
)
]
)
]
)
echo "完成上传 JAR 包 ${file}"
}
filesToCopy.eachWithIndex { file, index -> ... }
: 这是一个 Groovy 中的迭代循环,对列表filesToCopy
中的每个文件执行相应的操作。file
是当前迭代的文件,index
是该文件在列表中的索引。echo "开始上传 JAR 包 ${file} ..."
: 这是一个打印语句,用于输出日志,显示当前正在上传的 JAR 包的文件名。sshPublisher { ... }
: 这是一个 SSH 发布器,用于通过 SSH 连接到远程服务器并执行相应的操作。failOnError: true
: 如果 SSH 连接或传输过程中出现错误,将会终止流水线执行。configName: "${SSH_URL}"
: 这是 SSH 配置的名称,${SSH_URL}
是一个变量,指定 SSH 连接的配置信息。transfers: [sshTransfer { ... }]
: 这是 SSH 传输操作的列表,包含了将要执行的文件传输任务。remoteDirectory: 'apps/temp/'
: 远程服务器上的目标目录,这里设置为apps/temp/
,表示将文件上传到远程服务器的apps/temp/
目录下。removePrefix: removePrefixs[index]
: 这是一个用于移除文件路径前缀的设置,根据当前文件在列表中的索引,从相应的removePrefixs
数组中获取相应的前缀进行移除。sourceFiles: file
: 要传输的源文件,即当前迭代的文件。echo "完成上传 JAR 包 ${file}"
: 这是另一个打印语句,用于输出日志,表示当前文件的上传已经完成。
filesToCopy 就是通过用户勾选的服务名转成了对应的本地 JAR 包路径。
switch(service) {
case 'passjava-account':
filesToCopy.add("passjava-modules/passjava-module-account/passjava-module-account-core/target/passjava-account.jar")
removePrefixs.add("passjava-modules/passjava-module-account/passjava-module-account-core/target")
break
case ...
}
六、备份服务器 JAR 包
备份的思路:逐个处理 serviceNameList
中的服务名称,然后通过 SSH 连接到远程服务器执行备份操作。serviceNameList 就是用户勾选的服务名的集合。原理图如下所示:
备份服务器 JAR 包
核心代码如下:
serviceNameList.each { service ->
echo "开始备份微服务 ${service} 包"
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: "mkdir -p /nfs-data/wukong/apps/bak/${timestamp} && cd /nfs-data/wukong/apps && mv ${service}.jar ./bak/${timestamp}/${service}-${timestamp}.jar"
)
]
)
]
)
serviceNameList.each { service -> ... }
: 这是一个 Groovy 中的迭代循环,对列表serviceNameList
中的每个服务名称执行备份操作。service
是当前迭代的服务名称。echo "开始备份微服务 ${service} 包"
: 这是一个打印语句,用于输出日志,显示当前正在备份的微服务的名称。sshPublisher { ... }
: 这是一个 SSH 发布器,用于通过 SSH 连接到远程服务器并执行相应的操作。failOnError: true
: 如果 SSH 连接或执行过程中出现错误,将会终止流水线执行。configName: "${SSH_URL}"
: 这是 SSH 配置的名称,${SSH_URL}
是一个变量,用于指定 SSH 连接的配置信息。transfers: [sshTransfer { ... }]
: 这是 SSH 传输操作的列表,包含了将要执行的文件传输任务。execCommand: "..."
: 这是要在远程服务器上执行的命令。在这里,使用了mkdir
命令创建备份目录,然后将当前服务的 JAR 包移动到备份目录下,并加上时间戳作为文件名,以实现备份。
这段代码的作用是遍历 serviceNameList
列表中的服务名称,然后通过 SSH 连接到远程服务器执行备份操作,将每个服务的 JAR 包移动到指定的备份目录,并根据时间戳进行命名。
七、更新 JAR 包
更新最新的 JAR 包就是将最新的 JAR 包放到对应的容器映射的目录,后面重启容器的时候,就能用最新的 JAR 包启动了。原理图如下所示:
更新 JAR 包
serviceNameList.eachWithIndex { service, index ->
echo "开始更新第 ${index + 1} 个 JAR 包,/nfs-data/wukong/apps/temp/${service}.jar ..."
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: "cd /nfs-data/wukong/apps && mv -f ./temp/${service}.jar ${service}.jar",
execTimeout: 120000
)
]
)
]
)
echo "----- 完成更新 JAR 包 -----"
}
serviceNameList.eachWithIndex { service, index -> ... }
: 这是一个 Groovy 中的迭代循环,对列表serviceNameList
中的每个服务名称执行更新操作。service
是当前迭代的服务名称,index
是该服务在列表中的索引。echo "开始更新第 ${index + 1} 个 JAR 包,/nfs-data/wukong/apps/temp/${service}.jar ..."
: 这是一个打印语句,用于输出日志,显示当前正在更新的 JAR 包的名称及路径。sshPublisher { ... }
: 这是一个 SSH 发布器,用于通过 SSH 连接到远程服务器并执行相应的操作。failOnError: true
: 如果 SSH 连接或执行过程中出现错误,将会终止流水线执行。configName: "${SSH_URL}"
: 这是 SSH 配置的名称,${SSH_URL}
是一个变量,用于指定 SSH 连接的配置信息。transfers: [sshTransfer { ... }]
: 这是 SSH 传输操作的列表,包含了将要执行的文件传输任务。execCommand: "cd /nfs-data/wukong/apps && mv -f ./temp/${service}.jar ${service}.jar"
: 这是要在远程服务器上执行的命令。在这里,使用mv
命令将位于/nfs-data/wukong/apps/temp/
目录下的${service}.jar
移动到/nfs-data/wukong/apps/
目录,并覆盖同名的文件。execTimeout: 120000
: 这是执行命令的超时时间,单位是毫秒。在这里,设置为120000
,即 120 秒。echo "----- 完成更新 JAR 包 -----"
: 这是另一个打印语句,用于输出日志,表示当前 JAR 包的更新操作已经完成。
这段代码的作用是遍历 serviceNameList
列表中的服务名称,然后通过 SSH 连接到远程服务器执行更新操作,将每个服务在 /nfs-data/wukong/apps/temp/
目录下的 JAR 包移动到对应的位置,完成更新。
八、启动多个服务
启动服务就是将 docker swarm 管理的服务重启下,原理图如下所示:
后端项目使用 Docker Swarm 部署的,重启服务的命令如下:
sudo docker service update --force <服务名>
我们可以编写远程执行这行命令的代码,pipeline 核心代码如下:
serviceNameList.eachWithIndex { service, index ->
echo "开始重启第 ${index + 1} 个微服务,${service} ..."
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: "sudo docker service update --force ${commands[service]}",
execTimeout: 120000
)
]
)
]
)
echo "----- 完成重启微服务 -----"
}
这段代码的作用是遍历 serviceNameList
列表中的服务名称,然后通过 SSH 连接到远程服务器执行重启操作,完成微服务的重启。
最后整个执行结果如下图所示:总共耗时 3min41s。
九、总结
通过本篇的实战内容,我们学习到了通过编写 pipeline 代码来实现部署后端项目。推荐大家都用 pipeline 来部署项目,好处是更加灵活。
另外本篇还没有对 Jenkins pipeline 的版本管理,我们其实可以将 pipeline 代码作为一个文件上传到 Gitlab,然后通过 Jenkins 拉取最新的 jenkins pipeline 文件来执行部署,这样更便于管理 pipeline 文件。
下篇我们会讲解如何来管理 Jenkins 的部署权限:不同的人能部署的项目不一样,比如开发人员只能部署开发环境,测试人员只能部署测试环境。