持續集成的實踐應用已經在很多公司里推行,其中主流的持續集成工具就是Jenkins,作為過去兩年多來一直致力於公司的持續集成部署的我,回身看到踩過的不少坑,感慨不已,所以決定將這些做個記錄。

剛開始在公司部署Jenkins服務,使用的是插件流的方式部署,部署一個job要關聯到十幾個插件(插件流的方式就不在這裡贅述了,網上也有很多資料),兩個月後我把部署方式改成了Jenkins推薦的pipeline腳本的方式,這也是響應Jenkins2.0的精髓Pipeline as Code。

Jenkins的pipeline有Declarative Pipeline(在Pipeline 2.5中引入,結構化方式)和Scripted Pipeline兩種方式編寫,我選擇的是Declarative Pipeline的寫法,入門簡單。

好了話不多說,直接上實例:

#!/usr/bin/env groovy

pipeline {
//確認使用主機/節點機
agent any /*{
node { label master}
}*/
// 聲明參數
parameters{
//SVN代碼路徑
string(name:repoUrl, defaultValue: http://svn.com/svn/server/job, description: SVN代碼路徑)
// 部署內容的相對路徑
string(name:deployLocation, defaultValue: target/*.jar,target/alternateLocation/*.*,+target/classes/*.*,target/classes/i18n/*.*,target/classes/rawSQL/*.*,+target/classes/rawSQL/mapper/*.*,target/classes/rawSQL/mysql/*.*,+target/classes/rawSQL/sqlserver/*.*, description: 部署內容的相對路徑 )
//伺服器參數採用了組合方式,避免多次選擇
string(name:dev_server, defaultValue: IP,Port,Name,Passwd, description: 開發伺服器(IP,Port,Name,Passwd))
string(name:ZHtest_server, defaultValue: IP,Port,Name,Passwd, description: 中文測試伺服器(IP,Port,Name,Passwd))
string(name:alT19_server, defaultValue: IP,Port,Name,Passwd, description: 生產伺服器T1(IP,Port,Name,Passwd))
string(name:alT20_server, defaultValue: IP,Port,Name,Passwd, description: 生產伺服器T2(IP,Port,Name,Passwd))
}
// 聲明使用的工具
tools {
maven maven
jdk jdk1.8
}
//常量參數,初始確定後一般不需更改
environment{
// SVN服務全系統只讀賬號cred_id【參數值對外隱藏】
CRED_ID=CRED_ID
//項目經理郵箱地址
PM_EMAIL=PM
// Jenkins負責人
JM_EMAIL=QA
//測試人員郵箱地址【參數值對外隱藏】
TEST_EMAIL=Tester
}
triggers {
pollSCM(H/5 * * * 1-5)
}
//pipeline運行結果通知給觸發者
post{
//執行後清理workspace
always{
echo "clear workspace......"
deleteDir()
}
failure{
script {
emailext body: ${JELLY_SCRIPT,template="static-analysis"},
recipientProviders: [[$class: RequesterRecipientProvider],[$class: DevelopersRecipientProvider]],
subject: ${JOB_NAME}- Build # ${BUILD_NUMBER} - Failure!
}
}
}

stages {
stage(清理本地倉庫) {
steps{
sh "/home/jenkins/del_lastUpdated.sh"
}
}
stage(Checkout) {
steps {
script {
//從SVN拉取代碼
def scmVars = checkout ([$class: SubversionSCM,
additionalCredentials: [],
excludedCommitMessages: ,
excludedRegions: ,
excludedRevprop: ,
excludedUsers: ,
filterChangelog: false,
ignoreDirPropChanges: false,
includedRegions: ,
locations: [[credentialsId: CRED_ID,
depthOption: infinity,
ignoreExternalsOption: true,
local: .,
remote: params.repoUrl]],
workspaceUpdater: [$class: UpdateUpdater]])
svnversion = scmVars.SVN_REVISION
}
//sh "echo ${svnversion}"
}
}
// 編譯構建代碼
stage(構建) {
steps{
// maven構建
sh "mvn -Dmaven.test.failure.ignore clean install"
}
}
stage(靜態檢查) {
steps {
echo "starting codeAnalyze with SonarQube......"
//sonar:sonar.QualityGate should pass
withSonarQubeEnv(Sonar-6.4) {
//固定使用項目根目錄${basedir}下的pom.xml進行代碼檢查
//sh "mvn -f pom.xml clean compile sonar:sonar"
sh "mvn sonar:sonar "+
"-Dsonar.sourceEncoding=UTF-8 "//+
//"-Dsonar.language=java,groovy,xml"+
//"-Dsonar.projectVersion=${v} "+
//"-Dsonar.projectKey=${JOB_NAME} "+
//"-Dsonar.projectName=${JOB_NAME}"
}
script {
// 未通過代碼檢查,中斷
timeout(10) {
//利用sonar webhook功能通知pipeline代碼檢測結果,未通過質量閾,pipeline將會fail
def qg = waitForQualityGate()
if (qg.status != OK) {
error "未通過Sonarqube的代碼質量閾檢查,請及時修改!failure: ${qg.status}"
}
}
}
}
}
stage(歸檔) {
steps {
// 歸檔文件
/*archiveArtifacts artifacts: target/*.jar,target/alternateLocation/*.*,+target/classes/*.*,target/classes/i18n/*.*,target/classes/rawSQL/*.*,+target/classes/rawSQL/mapper/*.*,target/classes/rawSQL/mysql/*.*,+target/classes/rawSQL/sqlserver/*.*,fingerprint: true*/
archiveArtifacts params.deployLocation
}
}
stage(部署到開發環境 ) {
steps {
//根據param.server分割獲取參數,包括IP,jettyPort,username,password
script {
def dev_split=params.dev_server.split(",")
dev_serverIP=dev_split[0]
dev_serverPort=dev_split[1]
dev_serverName=dev_split[2]
dev_serverPasswd=dev_split[3]
}
echo Deploying to dev_server
//清理清理舊程序
sh "/home/jenkins/del_158_client.sh bas"
// 部署到開發環境
sh "scp -r target/*.jar ${dev_serverName}@${dev_serverIP}:/jenkins/datacenter/bas/"
sh "scp -r target/alternateLocation ${dev_serverName}@${dev_serverIP}:/jenkins/datacenter/bas/"
sh "rsync -av target/classes/ --exclude=com ${dev_serverName}@${dev_serverIP}:/jenkins/datacenter/bas/"

// 重啟服務
sh "/home/jenkins/kill_158_client.sh bas-job bas"
}
}
stage(開發環境介面自動化測試) {
agent{
label Slave_Linux_69_2
}
steps{
sh "sleep 60s"
echo "starting interfaceTest......"
/*echo 節點是: ${env.NODE_NAME}
echo 節點是: ${env.NODE_LABELS}
echo ${currentBuild}
echo ${env}
echo " 當前BuildId: ${env.BUILD_ID}"*/
dir(/home/jenkins/pm_test)
{
sh (source /etc/profile;newman -c APD201test_bas.postman_collection.json)
}
}
}
stage(對當前版本代碼打tag) {
steps{
timeout(5) {
script {
input message: 需要打tag嘛?
}
}
//sh "echo ${params.repoUrl}"
//sh "echo ${svnversion}"
sh "/home/jenkins/del_crea_tag.sh bas-job ${params.repoUrl} ${svnversion}"
}
}
stage(確認是否部署到測試環境) {
steps{
timeout(5) {
script {
mail to: "${JM_EMAIL} ${PM_EMAIL}",
subject: "PineLine ${JOB_NAME} (${BUILD_NUMBER})人工驗收通知",
body: "提交的PineLine ${JOB_NAME} (${BUILD_NUMBER})進入人工驗收環節
請及時前往${env.BUILD_URL}進行測試驗收"

input message:部署到測試環境?//,submitter:"${PM_EMAIL}"
// 中文環境
def ZHtest_split=params.ZHtest_server.split(",")
ZHtest_serverIP=ZHtest_split[0]
ZHtest_serverPort=ZHtest_split[1]
ZHtest_serverName=ZHtest_split[2]
ZHtest_serverPasswd=ZHtest_split[3]
}
}
//中文測試環境
//清理清理舊程序
sh "/home/jenkins/del_32_client.sh bas"
// 部署到中文測試環境
sh "scp -r target/*.jar ${ZHtest_serverName}@${ZHtest_serverIP}:/jenkins/datacenter/bas/"
sh "scp -r target/alternateLocation ${ZHtest_serverName}@${ZHtest_serverIP}:/jenkins/datacenter/bas/"
sh "rsync -av target/classes/ --exclude=com ${ZHtest_serverName}@${ZHtest_serverIP}:/jenkins/datacenter/bas/"
}
}
stage(部署到測試環境) {
steps{
echo Deploying to ZHtest_server

// 重啟服務
sh "/home/jenkins/kill_32_client.sh bas-job bas"
}
}
stage(測試環境介面自動化測試) {
agent{
label Slave_Linux_69_2
}
steps{
//sh "sleep 60s"
echo "starting interfaceTest......"
dir(/home/jenkins/pm_test)
{
sh (source /etc/profile;newman -c API_test.json)
}
}
}
stage(是否發布到生產環境?) {
steps{
timeout(10) {
script {
mail to: "${JM_EMAIL} ${PM_EMAIL}",
subject: "PineLine ${JOB_NAME} (${BUILD_NUMBER})發布生產環境通知",
body: "提交的PineLine ${JOB_NAME} (${BUILD_NUMBER})進入生產環境部署
請及時前往${env.BUILD_URL}進行確認"

input message:部署到生產環境?,submitter:"${PM_EMAIL}"
// 中文環境
def ZHtest_split=params.ZHtest_server.split(",")
ZHtest_serverIP=ZHtest_split[0]
ZHtest_serverPort=ZHtest_split[1]
ZHtest_serverName=ZHtest_split[2]
ZHtest_serverPasswd=ZHtest_split[3]
// 英文環境
def UStest_split=params.UStest_server.split(",")
UStest_serverIP=UStest_split[0]
UStest_serverPort=UStest_split[1]
UStest_serverName=UStest_split[2]
UStest_serverPasswd=UStest_split[3]
}
}
//文件服務環境
//清理清理舊程序
sh "/home/jenkins/del_file_client.sh bas"
sh "scp -r target/*.jar ${file_serverName}@${file_serverIP}:/data/datacenter/bas/"
sh "scp -r target/alternateLocation ${file_serverName}@${file_serverIP}:/data/datacenter/bas/"
sh "rsync -av target/classes/ --exclude=com ${file_serverName}@${file_serverIP}:/data/datacenter/bas/"

}
}
stage(部署生產環境) {
parallel {
stage(中文環境) {
steps{
//sh "sleep 60s"
echo "starting Deploy Chinese_Server......"
}
}
stage(英文環境) {
steps{
//sh "sleep 60s"
echo "starting Deploy English_Server......"
}
}
}
}
}
}

一段段的來解釋

//確認使用主機/節點機

agent any /*{ node { label master} }*/

就如注釋所寫的,這裡是選擇要使用的Jenkins伺服器,開始之初配置了兩台,1台作為主機master,另一台作為從節點69_2,而any表示的是所有節點中任何一台,這是會根據Jenkins內部的分配機制分配的,如果指定是哪個節點執行腳本就是用注釋里的方法,當然還有其他方式也可以指定。

// 聲明參數

parameters{ //SVN代碼路徑 string(name:repoUrl, defaultValue: Job Board - SVN International Corp., description: SVN代碼路徑) // 部署內容的相對路徑 string(name:deployLocation, defaultValue: target/*.jar,target/alternateLocation/*.*,+target/classes/*.*,target/classes/i18n/*.*,target/classes/rawSQL/*.*,+target/classes/rawSQL/mapper/*.*,target/classes/rawSQL/mysql/*.*,+target/classes/rawSQL/sqlserver/*.*, description: 部署內容的相對路徑 )

//伺服器參數採用了組合方式,避免多次選擇

string(name:dev_server, defaultValue: IP,Port,Name,Passwd, description: 開發伺服器(IP,Port,Name,Passwd)) string(name:ZHtest_server, defaultValue: IP,Port,Name,Passwd, description: 中文測試伺服器(IP,Port,Name,Passwd)) string(name:alT19_server, defaultValue: IP,Port,Name,Passwd, description: 生產伺服器T1(IP,Port,Name,Passwd)) string(name:alT20_server, defaultValue: IP,Port,Name,Passwd, description: 生產伺服器T2(IP,Port,Name,Passwd)) }

接下去這段是參數聲明,格式都是以string形式來聲明string(name:參數名, defaultValue: 默認值, description: 備註),當然參數也有其他形式,這裡只用到這個。這個腳本里聲明了SVN地址、歸檔的部署的文件、部署伺服器地址參數。

// 聲明使用的工具

tools { maven maven jdk jdk1.8

}

這段是說的當前job里會用到的工具,因為當前腳本是編譯maven工程的,所以使用了JDK和maven,引號中的名字是Jenkins全局工具配置里設定好的

//常量參數,初始確定後一般不需更改

environment{ // SVN服務全系統只讀賬號cred_id【參數值對外隱藏】 CRED_ID=CRED_ID //項目經理郵箱地址 PM_EMAIL=PM // Jenkins負責人

JM_EMAIL=QA

//測試人員郵箱地址【參數值對外隱藏】 TEST_EMAIL=Tester }

這一段是常量的聲明,比如拉取SVN代碼時需要的賬號cred_id、需要發送郵件對象的郵件地址等等,賬號cred_id可以通過Jenkins提供的工具得到,後面段落的解讀會說明。

triggers {

pollSCM(H/5 * * * 1-5) }

定時器,顧名思義,這部分是設定定時啟動的,pollSCM表示的是定時檢查代碼庫的變化,如果有變化觸發該job的構建;而cron則表示定時觸發該job的構建,裡面的規則如下:

MINUTE HOUR DOM MONTH DOW

MINUTE 一小時內多少分鐘(0-59)

HOUR 一天內多少小時(0-23小時)

DOM 一個月內多少天(1-31)

MONTH 每月(1-12)

DOW 星期幾(0-7),其中0和7都表示周日。

如果要指定一個欄位允許多個值,就按下面提供的操作步驟(指定)。

優先順序如下:

* 可用來指定所有有效的值。

M-N 可以用來指定一個範圍,比如「1-5」

M-N/X或*/X 可用於在指定範圍內跳躍一個X的值,比如在MINUTE欄位中"*/15"表示"0,15,30,45","1-6/2"表示"1,3,5"。

A,B,...,Z 可以用來指定多個值,比如「0,30」或「1,3,5」。

任何空白行和#開始的行都將表示為注釋而不予理睬。

此外,@yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly都是支持的

舉些例子:

* * * * * 每分鐘

5 * * * * 每一小時後第5分鐘

H/15 * * * * 每15分鐘

H(0-29)/10 * * * * 每小時的0到29分鐘每15分鐘

H 2-19/2 * * 1-5 每周1到周五(工作日)2點到19點每2小時執行

H H 1,15 1-11 * 1到11月1號和15號各執行一次

//pipeline運行結果通知給觸發者

post{

//執行後清理workspace

always{ echo "clear workspace......" deleteDir() } failure{ script { emailext body: ${JELLY_SCRIPT,template="static-analysis"}, recipientProviders: [[$class: RequesterRecipientProvider],[$class: DevelopersRecipientProvider]], subject: ${JOB_NAME}- Build # ${BUILD_NUMBER} - Failure!

}

} }

post部分是在整個構建都執行完之後再執行,這裡主要是一些收尾工作,always表示始終執行,這裡是清除workspace下的對應的工程目錄;failure表示當失敗的時候需要執行的動作,這裡寫的是給手動觸發人員和提交代碼的開發人員發送失敗郵件通知。

stage(清理本地倉庫) {

steps{ sh "/home/jenkins/del_lastUpdated.sh" } }

從這裡開始才是構建過程的第一個stage,這個stage里我首先執行的是清理本地倉庫中更新失敗的文件,第一步首先執行這個主要是因為之前踩過這個坑,因為是Jenkins在構建maven工程時會去下載該工程所依賴的各種jar包,同時也會從伺服器去更新這些jar包,但由於網路環境的問題,有些jar包更新會失敗,會留下後綴名為「.lastUpdated」殘留文件,當job構建的時候就會卡在這裡,所以後來寫了個刪除此類文件的shell腳本放在伺服器上,每次構建maven工程的時候都會首先調用這個腳本刪除殘留文件。該shell腳本內容如下:

#!/bin/bash

#本地倉庫的地址

del_path="/home/jenkins/.m2/repository/"

find $del_path -name *.lastUpdated -print |xargs rm -fecho "是否還有殘留"find $del_path -name *.lastUpdated -print

在這裡也非常感謝運維的同事,在pipeline腳本中用到的shell腳本是由我們的運維小哥協助我完成的,再次感謝我們的運維小哥。

stage(Checkout) {

steps { script { //從SVN拉取代碼 def scmVars = checkout ([$class: SubversionSCM, additionalCredentials: [], excludedCommitMessages: , excludedRegions: , excludedRevprop: , excludedUsers: , filterChangelog: false, ignoreDirPropChanges: false, includedRegions: , locations: [[credentialsId: CRED_ID, depthOption: infinity, ignoreExternalsOption: true, local: ., remote: params.repoUrl]], workspaceUpdater: [$class: UpdateUpdater]]) svnversion = scmVars.SVN_REVISION } //sh "echo ${svnversion}" } }

這一stage就是代碼庫去拉取代碼,我們使用的是SVN,拉取SVN的語句有點長,不用擔心,這其實是通過Jenkins提供的工具生成出來的,這個工具就是Pipeline Syntax,在Jenkins左邊的菜單上有

Sample Step:選擇 checkout:General SCM 這裡可以選擇很多步驟,,具體可以參考Jenkins自帶的說明;

SCM 選擇Subversion

Repository URL:這裡輸入的是拉取代碼的地址

Credentials:這是拉取代碼所用的賬號,需要先期在Jenkins的credentials里添加好

其他內容都默認就行了。然後選擇屏幕下方按鈕 Generate Pipeline Script,就會生產我們剛剛看到的那段內容,因為是沒有格式的,看的不爽的同學可以自行整理,這裡的SVN地址和credentialsId我都做了參數變數的定義,在上面都以提到過,這樣以後的腳本只要替換這兩個就可以編譯其他的maven工程了。

細心的朋友會發現拉取代碼的腳本中還多了一段svnversion = scmVars.SVN_REVISION這段是去提取拉取代碼的SVN版本號,這是為了在之後對代碼打標籤使用,如何在pipeline中獲取SVN的版本我也是花了好久才在國外的Jenkins論壇上找到的方法,pipeline的使用的分享在國內不是特別多,主要都是一些基本語法的介紹,實用的不多,pipeline的自由度比較高,很多設想的步驟都因為沒有資料而不得不放棄了。

後面篇幅不夠了,請轉《Jenkins pipeline腳本編寫實踐分享(一)下篇》


推薦閱讀:
相关文章