一、背景
我們工作中常用Jenkins部署Java程式碼,因其靈活的外掛特性,例如jdk,maven,ant等使得java專案編譯後上線部署一氣呵成,同樣對於指令碼語言型別如Python上線部署,利用Jenkins強大的外掛功能,輕鬆實現CI/CD,但如果部署多專案到同一臺伺服器涉及環境一致性問題,對此可以利用容器技術Docker解決,也可以利用Python虛擬環境例如virutalenv或conda等優秀等工具解決,在此由於後期根據requirements來安裝依賴包比較慢,且後期需要將Python整個環境打包,利用conda工具來對專案環境進行管理,方便快速移植。
本文較系統的記錄下部署一個具體的Django專案,包括利用conda工具來實現Python多環境管理,Pylint工具來實現程式碼檢查,使用nose框架做單元測試和覆蓋率。
二、必備知識
- Jenkins基礎安裝部署
可參考:blog.51cto.com/kaliarch/20…
- Conda軟體包管理系統
由於conda包較大,通常情況下可以使用Miniconda 官網下載地址:conda.io/en/latest/m… 基礎使用命令:
一、工具包管理命令
1.更新工具包
conda update conda
conda upgrade --all
2.安裝包(進入虛擬環境,也可用pip安裝)
conda install package_name
可以指定版本
conda install package_name=1.10
3.移除包
conda remove package_name
4.檢視包
conda list
5.查詢包
conda search package_name
6.源配置
因為anaconda的伺服器在國外,因此有時候速度會比較慢,可以換到國內源,比如清華的TUNA。
conda config --show-sources #檢視當前使用源
conda config --remove channels 源名稱或連結 #刪除指定源
conda config --add channels 源名稱或連結 #新增指定源
# 新增Anaconda的TUNA映象
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
# TUNA的help中映象地址加有引號,需要去掉
# 設定搜尋時顯示通道地址
conda config --set show_channel_urls yes
二、Python環境管理命令
1.顯示建立的所有環境
conda env list
2.建立python環境:
conda create -n env_name list of packages
eg:conda create -n py2 python=2.7 request
3.進入python環境:(windows 環境不需要加source)
source activate env_name
eg:source activate py2
4.退出環境:
source deactivate
5.檢視某環境下已安裝的包
conda list -n python2
6.複製環境:
conda create --name <new_env_name> --clone <copied_env_name>
7.刪除環境:
conda remove -n go2cloud-api-env --all
8.環境生成為YAML檔案
當分享程式碼的時候,同時也需要將執行環境分享給大家,首先進入到環境中,執行如下命令可以將當前環境下的 package 資訊存入名為 environment 的 YAML 檔案中。
複製程式碼
- conda env export > environment.yaml
同樣,當執行他人的程式碼時,也需要配置相應的環境。這時你可以用對方分享的 YAML 檔案來建立一摸一樣的執行環境
conda env create -f environment.yaml
Shell基礎
可參考:myshell-note.readthedocs.io/en/latest/i…
- GIT基礎
可參考:blog.51cto.com/kaliarch/20…
三、優化
- 生成主題
主題工具生成連結:afonsof.com/jenkins-mat… 在主題工具生成喜歡的顏色已經上傳logo下載生成的主題到Jenkins伺服器的jenkins 家目錄,一般為安裝啟動jenkins系統使用者的家目錄下.jenkins/userContent/material/,如果沒有此目錄需要新建目錄,css檔案移動到目錄下,例如/root/.jenkins/userContent/material/blue.css。
- Jenkins上配置主題
a.Install Jenkins Simple Theme Plugin b.點選Manage Jenkins c.點選Configure System d.找到 Theme 配置 e.填寫本地主題cssurl,例如:http://localhost:8080/jenkins/userContent/material/blue.css f.Click Save g.儲存可檢視效果。
- 注意
本地css的url可以瀏覽器開啟測試訪問,如果訪問不到會預設載入Jenkins預設主題 後期遷移url最好寫成localhost,如果寫公網IP,css檔案不存在404,Jenkins頁面會很卡。
四、部署實戰
4.1 伺服器列表
名稱 | IP | 軟體 | 備註 |
---|---|---|---|
Jenkins-server | 10.57.61.138 | miniconda | Jenkins伺服器 |
Des-server | 172.21.0.10 | miniconda | 專案部署伺服器 |
4.2 架構圖
4.3 前期準備
- 安裝依賴包
- pylint: Python靜態程式碼審查包,參考:pylint.pycqa.org/en/latest/u…
- mock: 用來生成測試資料。
- nose: Python單元測試包。
- coverage: Python程式碼覆蓋率包。
對依賴包需要在Jenkins-server伺服器進行安裝,首先根據專案裡面conda建立對應專案對虛擬環境conda create -n <project_name> python=3.6
,建立完成利用conda env list
檢視環境,為避免環境汙染,在專案環境內利用pip工具安裝軟體pip install pylint mock nose coverage
。
- 安裝Jenkins外掛
- JUnit: 用來展示nose框架生成的單元測試報表(Allows JUnit-format test results to be published.)
- Cobertura Plugin:用來展示Python程式碼測試覆蓋率報表(This plugin integrates Cobertura coverage reports to Jenkins.)
- Violations plugin:用來展示Python靜態程式碼審查報表(This plugin does reports on checkstyle, csslint, pmd, cpd, fxcop, pylint, jcReport, findbugs, and perlcritic violations.),參考:wiki.jenkins.io/display/JEN…
- Git Plugin:用來從Gitbucket原始碼庫拉取程式碼(This plugin allows GitLab to trigger Jenkins builds and display their results in the GitLab UI.)
- Git Parameter:用於引數化構建選擇git的branch(Adds ability to choose branches, tags or revisions from git repositories configured in project.)
4.4 建立任務
- 建立自由風格軟體專案
New任務->構建一個自由風格的軟體專案,填寫描述,在此由於後期會利用pylint進行程式碼檢查,給出了程式碼檢查的訊息型別,可以根據訊息型別進行相應修復處理。
- 配置引數化構建
配置選擇git具體branch進行構建,和可以自定義埠,在此需要注意引數化構建的變了Name,在後續需要用到。
- 原始碼管理
在此需要選擇原始碼倉庫,選擇gitlab已經認證方式,需要注意由於引數化構建選擇了branch,在Branches to build需要引用上面的變數$branch。
- 構建配置
- 執行shell
執行shell此shell為在Jenkins伺服器上執行,所以需要預先在其上配置Python虛擬環境,在其上進行程式碼審查,單元測試生成nosetests.xml檔案,已經程式碼覆蓋率測試生成coverage.xml檔案,pylint測試生成pylint.xml檔案。
以下為此專案示例shell指令碼,此指令碼需要根據自己的實際情況來修改,需要注意Python專案結構與需要程式碼檢查的識別符號。
base_dir=/root/.jenkins/workspace/
project=go2cloud-api-deploy-prod/
project_env=go2cloud-api-env
# 切換python環境
source activate ${project_env}
$(which python) -m pip install mock nose coverage
# 更新python環境
echo "+++更新Python環境+++"
if [ -f ${base_dir}${project}requirements.txt ];then
$(which python) -m pip install -r ${base_dir}${project}requirements.txt && echo 0 || echo 0
fi
# 程式碼檢查/單元測試/程式碼測試覆蓋率
echo "+++程式碼檢查+++"
cd ${base_dir}
# 生成pylint.xml
$(which pylint) -f parseable --disable=C0103,E0401,C0302 $(find ${project}/* -name *.py) >${base_dir}pylint.xml || echo 0
echo "+++單元測試+++"
# 生成nosetests.xml
#$(which nosetests) --with-xunit --all-modules --traverse-namespace --with-coverage --cover-package=go2cloud-api-deploy-prod --cover-inclusive || echo 0
$(which nosetests) --with-xunit --all-modules --traverse-namespace --with-coverage --py3where=go2cloud-api-deploy-prod --cover-package=go2cloud-api-deploy-prod --cover-inclusive || echo 0
echo "+++程式碼覆蓋率+++"
# 生成coverage.xml
複製程式碼
python -m coverage xml --include=go2cloud-api-deploy-prod* || echo 0
- 傳送檔案及命令到目標伺服器
#!/usr/bin/env bash
# 當前目錄
BASEPATH=$(cd `dirname $0`;pwd)
# python直譯器具體路徑
PYTHON_BIN=$1
# mananger檔案路徑
MAIN_APP=$2
# python
SERVER_PORT=$3
[ $# -lt 3 ] && echo "缺少引數" && exit 1
LOG_DIR=${BASEPATH}/logs/
[ ! -d ${LOG_DIR} ] && mkdir ${LOG_DIR}
OLD_PID=`netstat -lntup | awk -v SERVER_PORT=${SERVER_PORT} '{if($4=="0.0.0.0:"SERVER_PORT) print $NF}'|cut -d/ -f1`
[ -n "${OLD_PID}" ] && kill -9 ${OLD_PID}
echo "---------$0 $(date) excute----------" >> ${LOG_DIR}server-$(date +%F).log
# 啟動服務
nohup ${PYTHON_BIN} -u ${MAIN_APP} runserver 0.0.0.0:${SERVER_PORT} &>> ${LOG_DIR}server-$(date +%F).log 2>&1 &
複製程式碼
- 構建後動作
- JUnit外掛實現單元測試報告,需要指定nosetests.xml
- Cobertura Plugin外掛實現覆蓋率測試
- Violations外掛進行程式碼審計,需要制定Jenkins-server上的生成的pylint.xml檔案。
需要注意檔案路徑為jenkins伺服器pylint.xml,以及對應生成檔案的編碼。
- 郵件通知配置
選擇郵件內容為Content Type為HTML,這樣可以編寫郵件HTML模版,生成較為好看的郵件通知模版。 注意選擇觸發告警可以選擇型別,失敗幾次或無論構建成功失敗都傳送,可根據具體需求配置。
郵件HTML模版<!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: Tahoma, Arial, Helvetica, sans-serif">
<tr>
<td>(本郵件是程式自動下發的,請勿回覆!)</td>
</tr>
<tr>
<td><h2>
<font color="#0000FF">構建結果 - ${BUILD_STATUS}</font>
</h2></td>
</tr>
<tr>
<td><br />
<b><font color="#0B610B">構建資訊</font></b>
<hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>專案名稱 : ${PROJECT_NAME}</li>
<li>構建編號 : 第${BUILD_NUMBER}次構建</li>
<li>SVN 版本: ${SVN_REVISION}</li>
<li>觸發原因: ${CAUSE}</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>
<tr>
<td><b><font color="#0B610B">Changes Since Last
Successful Build:</font></b>
<hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>歷史變更記錄 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
</ul> ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="<pre>[%a]<br />%m</pre>",pathFormat=" %p"}
</td>
</tr>
<tr>
<td><b>Failed Test Results</b>
<hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td><pre
style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">$FAILED_TESTS</pre>
<br /></td>
</tr>
<tr>
<td><b><font color="#0B610B">構建日誌 (最後 100行):</font></b>
<hr size="2" width="100%" align="center" /></td>
</tr>
<!-- <tr>
<td>Test Logs (if test has ran): <a
href="${PROJECT_URL}ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip">${PROJECT_URL}/ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip</a>
<br />
<br />
</td>
</tr> -->
<tr>
<td><textarea cols="80" rows="30" readonly="readonly"
style="font-family: Courier New">${BUILD_LOG, maxLines=100}</textarea>
</td>
</tr>
</table>
</body>
</html>
複製程式碼
4.5 進行構建測試
- 觸發構建
選擇對應的branch與埠
- 檢視consle log
- pylint檢查
- nosetests單元測試及程式碼覆蓋率
- 目標伺服器檢視具體專案
4.6 檢視構結果
- 總覽檢視
- 檢視程式碼覆蓋率
(C) convention 慣例。違反了編碼風格標準
(R) refactor 重構。寫得非常糟糕的程式碼。
(W) warning 警告。某些 Python 特定的問題。
(E) error 錯誤。很可能是程式碼中的錯誤。
(F) fatal 致命錯誤。阻止 Pylint 進一步執行的錯誤。
複製程式碼
- 檢視郵件
五、流水線部署
5.1 pipeline基礎概念
- pipeline是什麼
pipeline為執行於Jenkins上的工作流框架,project中的配置資訊以steps的方式放在一個指令碼里將原本獨立執行於單個或者多個節點的任務連線起來,實現單個任務難以完成的複雜流程編排與視覺化。
- 基礎概念
- Stage: 階段:告訴Jenkins做什麼,一個Pipeline可以劃分為若干個Stage,每個Stage代表一組操作。注意,Stage是一個邏輯分組的概念,可以跨多個Node。
- Node: 節點:告訴Jenkins在job在哪執行一個Node就是一個Jenkins節點,或者是Master,或者是slave,是執行Step的具體執行期環境。
- Step: 步驟:具體細化到每一步構建操作,Step是最基本的操作單元,小到建立一個目錄,大到構建一個Docker映象,由各類Jenkins Plugin提供。
- 語發工具
Pipeline提供了一組可擴充套件的工具,通過Pipeline Domain Specific Language(DSL)syntax可以達到Pipeline as Code(Jenkinsfile儲存在專案的原始碼庫)的目的。
參考:wiki.jenkins.io/display/JEN…
5.2 pipeline的特性
基於 Jenkins Pipeline,使用者可以在一個 JenkinsFile 中快速實現一個專案的從構建、測試以到釋出的完整流程,並且可以儲存這個流水線的定義。
- 程式碼:Pipeline以程式碼的形式實現,通常被檢入原始碼控制,使團隊能夠編輯、審查和迭代其CD流程。
- 可持續性:Jenklins重啟或者中斷後都不會影響Pipeline Job。
- 停頓:Pipeline可以選擇停止並等待任工輸入或批准,然後再繼續Pipeline執行。
- 多功能:Pipeline支援現實世界的複雜CD要求,包括fork/join子程式,迴圈和並行執行工作的能力
- 可擴充套件:Pipeline外掛支援其DSL的自定義擴充套件以及與其他外掛整合的多個選項。
5.3 pipeline語法
- 宣告式
pipeline {
/* insert Declarative Pipeline here */
}
複製程式碼
在宣告式流水線中有效的基本語句和表示式遵循與 Groovy的語法同樣的規則, 有以下例外:
- 流水線頂層必須是一個 block, 特別地: pipeline { }
- 沒有分號作為語句分隔符,,每條語句都必須在自己的行上。
- 塊只能由 節段, 指令, 步驟, 或賦值語句組成。 *屬性引用語句被視為無參方法呼叫。 例如, input被視為 input()
示例:
Jenkinsfile (Declarative Pipeline)
pipeline {
agent none
stages {
stage('Example Build') {
agent { docker 'maven:3-alpine' }
steps {
echo 'Hello, Maven'
sh 'mvn --version'
}
}
stage('Example Test') {
agent { docker 'openjdk:8-jre' }
steps {
echo 'Hello, JDK'
sh 'java -version'
}
}
}
}
複製程式碼
詳細學習可參考:jenkins.io/zh/doc/book…
- 指令碼式
指令碼化流水線, 與[declarative-pipeline]一樣的是, 是建立在底層流水線的子系統上的與宣告式不同的是, 指令碼化流水線實際上是由 Groovy構建的通用 DSL [2]。 Groovy 語言提供的大部分功能都可以用於指令碼化流水線的使用者。這意味著它是一個非常有表現力和靈活的工具,可以通過它編寫持續交付流水線。
示例:
node('master') { //master節點執行,以下stage也可指定節點
stage 'Prepare' //清空釋出目錄
bat '''if exist D:\\publish\\LoginServiceCore (rd/s/q D:\\publish\\LoginServiceCore)
if exist C:\\Users\\Administrator\\.nuget (rd/s/q C:\\Users\\Administrator\\.nuget)
exit'''
//拉取git程式碼倉庫
stage 'Checkout'
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [],
       submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c6d98bbd-5cfb-4e26-aa56-f70b054b350d',
url: 'http://xxx/xxx/xxx']]])
//構建
stage 'Build'
bat '''cd "D:\\Program Files (x86)\\Jenkins\\workspace\\LoginServiceCore\\LoginApi.Hosting.Web"
dotnet restore
dotnet build
dotnet publish --configuration Release --output D:\\publish\\LoginServiceCore'''
//部署
stage 'Deploy'
bat '''
cd D:\\PipelineScript\\LoginServiceCore
python LoginServiceCore.py
'''
//自動化測試(python程式碼實現)
stage 'Test'
bat'''
cd D:\\PipelineScript\\LoginServiceCore
python LoginServiceCoreApitest.py
'''
}
複製程式碼
5.4 示例
在此將上面的專案利用pipeline進行釋出
- 建立自由風格軟體專案
New任務->流水線,填寫任務描述。
- Pipeline
如果對Pipeline語法不熟悉,可以利用工具生成
在此的pipelinepipeline {
agent any
parameters {
gitParameter branchFilter: 'origin/(.*)', defaultValue: 'master', name: 'BRANCH', type: 'PT_BRANCH'
}
stages {
stage('checkout src code') {
steps {
echo "checkout src code"
git branch: "${params.BRANCH}",'http://123.206.xxx.xxx/xuel/go2cloud_platform.git'
}
}
stage('exec shell'){
steps{
echo "pylint,Unit test"
sh '''# jenkins 伺服器專案workspace目錄
base_dir=/root/.jenkins/workspace/
# 專案名稱
project=go2cloud_platform
# 專案環境python環境
project_env=go2cloud_platform_pipeline
# 切換python環境
source /data/miniconda3/bin/activate ${project_env}
$(which python) -m pip install mock nose coverage pylint
# 更新python環境
echo "++++++更新Python環境+++"
if [ -f ${base_dir}${project}requirements/requirements.txt ];then
$(which python) -m pip install -r ${base_dir}${project}/requirements/requirements.txt && echo 0 || echo 0
fi
# 程式碼檢查/單元測試/程式碼測試覆蓋率
echo "++程式碼檢查++"
cd ${base_dir}
# 生成pylint.xml
$(which pylint) -f parseable --disable=C0103,E0401,C0302 $(find ${project}/* -name *.py) >${base_dir}${project}_pylint.xml || echo 0
echo "++單元測試++"
# 生成nosetests.xml
#$(which nosetests) --with-xunit --all-modules --traverse-namespace --with-coverage --cover-package=go2cloud-api-deploy-prod --cover-inclusive || echo 0
$(which nosetests) --with-xunit --all-modules --traverse-namespace --with-coverage --py3where=${project} --cover-package=${project} --cover-inclusive || echo 0
echo "++程式碼覆蓋率+++"
# 生成coverage.xml
python -m coverage xml --include=${project_env}* || echo 0'''
}
}
stage("deploy") {
steps {
echo "send file"
sshPublisher(publishers: [sshPublisherDesc(configName: 'go2cloud_platform_host', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''base_dir=/devops_pipeline
project_src=go2cloud_platform
project_env=/data/miniconda3/envs/go2cloud_platform_pipeline/bin/python
echo "+++++++更新部署伺服器python環境++++++++++"
if [ -f ${base_dir}/requirements/requirements.txt ];then
${project_env} -m pip install -r ${base_dir}/requirements/requirements.txt && echo 0 || echo 0
fi
echo -e "\\033[32m 啟動服務指令碼 \\033[0m"
$(which bash) ${base_dir}/run_server.sh ${project_env} ${base_dir}/apps/manage.py ${port}
''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/devops_pipeline', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '**/**')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}
}
}
複製程式碼
- 執行構建
- 檢視結果
六、注意要點
- 在部署前一定梳理好流程,需要理解哪一步在目標伺服器還上在Jenkins伺服器上執行
- 需要對應好目錄及Python虛擬環境,避免環境汙染
七、反思
- 在此利用來Conda虛擬環境管理,來在同一個伺服器上解決環境不一致行,也可以利用Docker來解決
- 利用Pipeline專案釋出視覺化,明確階段,方便stage檢視與排錯,Jenkinsfile可放在git倉庫進行