最近在公司實踐持續集成,使用到了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的安裝,可以參考我的這一篇文章進行安裝。
相信很多童鞋都已經在使用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的系列文章,敬請期待。
參考資料
大寶魚,《玩轉Jenkins Pipeline》
李志強,《Jenkins高級用法 - Pipeline 安裝》
李志強,《Jenkins高級用法 - Jenkinsfile 介紹及實戰經驗》
三只松鼠,《jenkins + pipeline構建自動化部署》
ofnhkb1,《.NET項目從CI到CD-Jenkins_Pipeline的應用》