共计 11165 个字符,预计需要花费 28 分钟才能阅读完成。
最近在公司实践持续集成,使用到了 Jenkins 的 Pipeline 来提高团队基于 ASP.NET Core API 服务的集成与部署效率,因此这里总结一下。
一、关于持续集成与 Jenkins Pipeline
1.1 持续集成相关概念
互联网软件的开发和发布,已经形成了一套标准流程,最重要的组成部分就是持续集成(Continuous integration,简称 CI)。
持续集成指的是,频繁地 (一天多次) 将代码集成到主干。
它的好处主要有两个:
(1)快速发现错误。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易。
(2)防止分支大幅偏离主干。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。
持续集成的目的,就是 让产品可以快速迭代,同时还能保持高质量。
Martin Fowler 说:“持续集成并不能消除 Bug,而是让它们非常容易发现和改正。”
与持续集成相关的,还有持续交付和持续部署。
持续交付指的是:频繁地将软件的新版本,交付给质量团队或者用户,以供评审 。如果评审通过,代码就进入生产阶段。它强调的是, 不管怎么更新,软件是随时随地可以交付的。
持续部署是持续交付的下一步,指的是 代码通过评审以后,自动部署到生产环境 。它强调的是 代码在任何时刻都是可部署的,可以进入生产阶段。
1.2 Jenkins Pipeline
Jenkins 是一款流行的开源持续集成(CI)与持续部署(CD)工具,广泛用于项目开发,具有自动化构建、测试和部署等功能。有关 Jenkins 的安装,可以参考我的这一篇文章进行安装 https://www.linuxidc.com/Linux/2019-09/160627.htm。
相信很多童鞋都已经在使用 Jenkins 或者计划使用 Jenkins 来代替传统的人工发布流程了,因此我们创建了很多自由风格(Free Style)的构建任务用于多个 Job,而我们经常会听到说流水线任务,那么流水线是什么呢?
流水线 Pipeline 是一套运行于 Jenkins 上的工作流框架,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的 复杂流程编排与可视化。下图是一个 Jenkins Pipeline 的实例效果:
Pipeline:Build => Test => Deploy
这里涉及到 Pipeline 中的几个重要概念,需要了解一下:
- Stage: 阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作。注意,Stage 是一个逻辑分组的概念,可以跨多个 Node。如上图所示,Build,Test 和 Deploy 就是 Stage,代表了三个不同的阶段:编译、测试和部署。
- Node: 节点,一个 Node 就是一个 Jenkins 节点,或者是 Master,或者是 Slave,是执行 Step 的具体运行期环境。
- Step: 步骤,Step 是最基本的操作单元,小到创建一个目录,大到构建一个 Docker 镜像,由各类 Jenkins Plugin 提供。
二、准备 ASP.NET Core Docker 环境
2.1 安装 Docker 环境
可以参考我的这一篇《.NET Core 微服务之 ASP.NET Core on Docker》来安装和配置 Docker 环境,建议在 Linux 环境下配置。
2.2 安装 SFTP 服务
在 Linux 下,SSH 服务默认会安装,而在 Windows Server 下,需要单独安装,可以借助 FreeSSHD 这个免费工具来实现。由于我的物理机都是 Windows Server,物理机上的 VM 是 Linux(Docker 运行环境),所以需要给物理机配置 FreeSSHD,用来实现从 CI 服务器发布 Release 到物理服务器中的 VM。
至于如何安装配置 FreeSSHD,可以参考这一篇《freeSSHD 在 windows 环境下搭建 SFTP 服务器》。
三、配置 Jenkins Pipeline 流水线任务
3.1 总体目标
(1)持续集成:实现编译 + 单元测试的自动运行
这里我要实现的目标是:当有人 push 代码到 git server 中(这里我使用的 git server 是 Gogs,需要给 Gogs 设置一个 Webhook,如下图所示,需要注意的是设置的密钥文本要和在 Pipeline 中填写的一致,否则 Jenkins 无法正确接收 Web 钩子),git server 会触发一个 webhook 发送一个 post 的请求给 CI server,CI server 会触发 Pipeline 任务的构建,一路 pull 代码 + 编译 + 单元测试。
(2)持续发布:实现编译 + 发布到具体的测试环境
由于在开发阶段,我不需要每次 Push 都进行发布,因此我这里设置的是手动在 Jenkins 中触发发布任务来实现自动化发布。
3.2 全局设置
首先,肯定是 Jenkins 的插件安装了。
(1)Generic WebHook Trigger => 触发 WebHook 必备
(2)Gogs Plugin => 因为我使用的 Git Server 是 Gogs 搭建的
(3)MSBuild Plugin => 进行 sln、csproj 项目文件的编译
(4)MSTest & xUnit => 进行基于 MSTest 或基于 xUnit 的单元测试
(5)Nuget Plugin => 拉取 Nuget 包必备
(6)Pipeline => 实现 Pipeline 任务必备,建议将 Pipeline 相关插件都安装上
(7)Powershell Plugin => 如果你的 CI 服务器是基于 Windows 的,那么安装一下 Powershell 插件来执行命令吧
(8)Publish Over SSH => 远程发布 Release 必备
(9)WallDisplay => 电视投屏构建任务列表必备
其次,为了提示邮件,也要 Email 插件(Email Extension)的支持,并进行以下配置:
(1)第一处:Jenkins Location
(2)第二处:Email 扩展插件全局变量设置
这里主要是需要设置 Subject 和 Content,就可以在各个 Pipeline 中使用了。因此,这里贴出我的 Default Content 内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${ENV, var="JOB_NAME"}- 第 ${BUILD_NUMBER}次构建日志</title>
</head>
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
offset="0">
<table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Microsoft YaHei, Tahoma, Arial, Helvetica">
<tr>
<td>各位同事,大家好,以下为 ${PROJECT_NAME} 构建任务信息</td>
</tr>
<tr>
<td><br />
<b style="font-weight:bold; color:#66cc00">构建信息</b>
<hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>任务名称:${PROJECT_NAME}</li>
<li>构建编号:第 ${BUILD_NUMBER}次构建</li>
<li>触发原因:${CAUSE}</li>
<li>构建状态:<span style="font-weight:bold; color:#FF0000">${BUILD_STATUS}</span></li>
<li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
<li>构建 Url:<a href="${BUILD_URL}">${BUILD_URL}</a></li>
<li>工作目录:<a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>
<li>项目 Url:<a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
</ul>
</td>
</tr>
</table>
</body>
</html>
为了能够发给更多的人,建议勾选以上两个选项。
这里是 Email 通知必填的 SMTP 服务器配置。
最后,是 SSH 服务器的声明,指定可以进行 SSH 发布的服务器有哪些,IP 又是多少:
3.3 新增 Pipeline 脚本
(1)持续集成 Pipeline
首先,填写 Webhook 的密钥文本:
其次,Build Triggers 的时机选择“Build when a change is pushed to Gogs”,即有人 push 代码到仓库就触发。当然,这里需要提前在 Gogs 设置 Webhook。
其次,编写 Pipeline 脚本,各个 Stage 写清楚职责:
具体的 Pipeline 脚本在下边:
pipeline{
agent any
stages {stage('XDP Core Services Checkout') {
steps{checkout([$class: 'GitSCM', branches: [[name: '*/dev-xds']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '35b9890b-2338-45e2-8a1a-78e9bbe1d3e2', url: 'http://192.168.18.150:3000/EDC.ITC.XDP.Core/EDC.XDP.Core.git']]])
echo 'Core Services Checkout Done'
}
}
stage('XDP Core Services Build') {
steps{bat '''cd "D:\\Jenkins\\workspace\\XDS.Dev.CI.Pipeline\\src\\services\\EDC.XDP.Core\\"
dotnet build EDC.XDP.Core-All.sln'''echo'Core Services Build Done'
}
}
stage('Core Delivery Service Unit Test') {
steps{bat '''cd "D:\\Jenkins\\workspace\\XDS.Dev.CI.Pipeline\\src\\services\\EDC.XDP.Core\\Services\\EDC.XDP.Core.Delivery.UnitTest"
dotnet test -v n --no-build EDC.XDP.Core.Delivery.UnitTest.csproj'''echo'Core Delivery Service Unit Test Done'
}
}
stage('XDS Delivery Service Checkout') {
steps{checkout([$class: 'GitSCM', branches: [[name: '*/dev-service']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '35b9890b-2338-45e2-8a1a-78e9bbe1d3e2', url: 'http://192.168.18.150:3000/EDC.ITC.XDP.XDS/EDC.XDP.XDS.git']]])
echo 'Core Delivery Service Checkout Done'
}
}
stage('XDS Delivery Service Build') {
steps{bat '''cd "D:\\Jenkins\\workspace\\XDS.Dev.CI.Pipeline\\src\\services\\EDC.XDP.XDS"
dotnet build EDC.XDP.XDS.sln'''echo'XDS Service Build Done'
}
}
stage('XDS Delivery Service Unit Test') {
steps{bat '''cd "D:\\Jenkins\\workspace\\XDS.Dev.CI.Pipeline\\src\\services\\EDC.XDP.XDS\\EDC.XDP.XDS.Delivery.UnitTest"
dotnet test -v n --no-build EDC.XDP.XDS.Delivery.UnitTest.csproj'''echo'XDS Service Unit Test Done'
}
}
}
post{
failure {
emailext (subject: '${DEFAULT_SUBJECT}',
body: '${DEFAULT_CONTENT}',
to: "edisonchou@qq.com,xxxxx@qq.com")
}
}
}
(2)持续发布 Pipeline
持续发布 Pipeline 与持续集成 Pipeline 类似,只是在脚本处有所不同:
pipeline{
agent any
stages {stage('Core Delivery Service Checkout') {
steps{checkout([$class: 'GitSCM', branches: [[name: '*/dev-xds']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '35b9890b-2338-45e2-8a1a-78e9bbe1d3e2', url: 'http://192.168.18.150:3000/EDC.ITC.XDP.Core/EDC.XDP.Core.git']]])
echo 'Core Delivery Service Dev Branch Checkout Done'
}
}
stage('Core Delivery Service Build & Publish') {
steps{bat '''cd "D:\\Jenkins\\workspace\\XDS.API.Dev.CD.Pipeline\\src\\services\\EDC.XDP.Core"
dotnet build EDC.XDP.Core-DataServices.sln
dotnet publish "%WORKSPACE%\\src\\services\\EDC.XDP.Core\\Services\\EDC.XDP.Core.Delivery.API\\EDC.XDP.Core.Delivery.API.csproj" -o "%WORKSPACE%\\EDC.XDP.Core.Delivery.API/publish" --framework netcoreapp2.1
'''echo'Core Delivery Service Build & Publish Done'
}
}
stage('Core Delivery Service Deploy To 190 Server') {
steps{sshPublisher(publishers: [sshPublisherDesc(configName: 'XDP-DEV-Server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand:'''docker stop xdp_core_deliveryservice; docker rm xdp_core_deliveryservice; docker run --ulimit core=0 --restart=always -v /etc/localtime:/etc/localtime -d -e ASPNETCORE_ENVIRONMENT=dev --privileged=true --name=xdp_core_deliveryservice -p 8010:80 -v /XiLife/publish/EDC.XDP.Core.Delivery.API/:/app -w /app xdp_service_runtime:latest dotnet EDC.XDP.Core.Delivery.API.dll''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator:'[,]+', remoteDirectory:'EDC.XDP.Core.Delivery.API/', remoteDirectorySDF: false, removePrefix:'EDC.XDP.Core.Delivery.API/publish/', sourceFiles:'EDC.XDP.Core.Delivery.API/publish/**')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
echo 'Delivery Service Deploy To 190 Done'
}
}
stage('Core Delivery Service Deploy To 175 Server') {
steps{sshPublisher(publishers: [sshPublisherDesc(configName: 'XDP-DEV-MT-Server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand:'''docker stop xdp_core_deliveryservice; docker rm xdp_core_deliveryservice; docker run --ulimit core=0 --restart=always -v /etc/localtime:/etc/localtime -d -e ASPNETCORE_ENVIRONMENT=devmt --privileged=true --name=xdp_core_deliveryservice -p 8010:80 -v /XiLife/publish/EDC.XDP.Core.Delivery.API/:/app -w /app xdp_service_runtime:latest dotnet EDC.XDP.Core.Delivery.API.dll''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator:'[,]+', remoteDirectory:'EDC.XDP.Core.Delivery.API/', remoteDirectorySDF: false, removePrefix:'EDC.XDP.Core.Delivery.API/publish/', sourceFiles:'EDC.XDP.Core.Delivery.API/publish/**')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
echo 'Delivery Service Deploy To 175 Done'
}
}
stage('XDS Delivery Service Checkout') {
steps{checkout([$class: 'GitSCM', branches: [[name: '*/dev-service']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '35b9890b-2338-45e2-8a1a-78e9bbe1d3e2', url: 'http://192.168.18.150:3000/EDC.ITC.XDP.XDS/EDC.XDP.XDS.git']]])
echo 'XDS Delivery Service Checkout Done'
}
}
stage('XDS Delivery Service Build & Publish') {
steps{bat '''cd "D:\\Jenkins\\workspace\\XDS.API.Dev.CD.Pipeline\\src\\services\\EDC.XDP.XDS"
dotnet build EDC.XDP.XDS.sln
dotnet publish "%WORKSPACE%\\src\\services\\EDC.XDP.XDS\\EDC.XDP.XDS.Delivery.API\\EDC.XDP.XDS.Delivery.API.csproj" -o "%WORKSPACE%\\EDC.XDP.XDS.Delivery.API/publish" --framework netcoreapp2.1
'''echo'XDS Delivery Service Build & Publish Done'
}
}
stage('XDS Delivery Service Deploy To 190 Server') {
steps{sshPublisher(publishers: [sshPublisherDesc(configName: 'XDP-DEV-Server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand:'''docker stop xdp_xds_delivery_service;docker rm xdp_xds_delivery_service; docker run --ulimit core=0 --restart=always -v /etc/localtime:/etc/localtime -d -e ASPNETCORE_ENVIRONMENT=dev --privileged=true --name=xdp_xds_delivery_service -p 9020:80 -v /XiLife/publish/EDC.XDP.XDS.Delivery.API/:/app -w /app xdp_service_runtime:latest dotnet EDC.XDP.XDS.Delivery.API.dll''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator:'[,]+', remoteDirectory:'EDC.XDP.XDS.Delivery.API/', remoteDirectorySDF: false, removePrefix:'EDC.XDP.XDS.Delivery.API/publish/', sourceFiles:'EDC.XDP.XDS.Delivery.API/publish/**')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
echo 'XDS Delivery Service Deploy to 190 Done'
}
}
stage('XDS Delivery Service Deploy To 175 Server') {
steps{sshPublisher(publishers: [sshPublisherDesc(configName: 'XDP-DEV-MT-Server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand:'''docker stop xdp_xds_delivery_service;docker rm xdp_xds_delivery_service; docker run --ulimit core=0 --restart=always -v /etc/localtime:/etc/localtime -d -e ASPNETCORE_ENVIRONMENT=devmt --privileged=true --name=xdp_xds_delivery_service -p 9020:80 -v /XiLife/publish/EDC.XDP.XDS.Delivery.API/:/app -w /app xdp_service_runtime:latest dotnet EDC.XDP.XDS.Delivery.API.dll''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator:'[,]+', remoteDirectory:'EDC.XDP.XDS.Delivery.API/', remoteDirectorySDF: false, removePrefix:'EDC.XDP.XDS.Delivery.API/publish/', sourceFiles:'EDC.XDP.XDS.Delivery.API/publish/**')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
echo 'XDS Delivery Service Deploy to 175 Done'
}
}
}
}
这里由于我的测试环境分为两个,一个是开发人员联调环境 190,另一个是集成测试环境 175,统一在一个 Pipeline 任务中进行发布。
对于 Master 分支,我们还可以将 Web 系统的发布也集成到同一个 Pipeline 任务中,实现一个一条龙的发布流水线任务,由于各个 Web 系统的实现技术不一样,这里就不再贴脚本了。
四、效果演示
(1)持续集成示例
(2)持续发布示例
(3)构建失败告警
(4)构建大屏显示
再来一张投屏到工作区域电视屏幕中的效果,大家抬头就可以看到构建结果,是绿了还是红了?当然,我们都喜欢“绿”的,呼呼。
五、小结
借助持续集成和持续发布,我们开发人员可以节省很多质量保证和发布部署的时间,从而减少很多因为人为 QA 和 Deploy 造成的失误影响,从另一个层面上,它也可以使我们避免 996(好吧,虽然关联有点牵强)。后续,我还会探索 K8S,到时候希望能够分享一个 ASP.NET Core on K8S 的系列文章,敬请期待。