1 代碼部署流程
1.1 代碼克隆
公司的代碼都是放到gitlab倉庫, 代碼的克隆會由Jenkins的項目構(gòu)建配置去觸發(fā). 運維人員需要在Jenkins中創(chuàng)建項目, 并且配置構(gòu)建的步驟. 之后點擊構(gòu)建, 觸發(fā)代碼的克隆以及后續(xù)階段
代碼克隆階段需要配置的內(nèi)容
1. Jenkins服務器上的Jenkins用戶和Gitlab服務器的免秘鑰認證: Jenkins是以jenkins用戶運行, 因此, 需要在Jenkins服務器生成jenkins用的秘鑰對, 把公鑰添加到Gitlab的root賬號下,防止其他用戶誤刪除
2. 在Jenkins上創(chuàng)建項目, 填寫項目的gitlab代碼地址, 添加jenkins用戶的私鑰信息
配置好這些信息后, Jenkins就可以從gitlab拉取代碼到本地的/var/lib/jenkins/workspace目錄下
1.2 源代碼測試
代碼測試可以使用商業(yè)的軟件, 也可以使用開源的Sonarqube. 需要部署單獨Sonarqube服務器并且在Jenkins服務器上安裝Sonar-Scanner客戶端, 讓Jenkins拉取代碼后, 在本地進行代碼掃描, 之后上傳到Sonarqube服務器進行分析
代碼掃描一定是在編譯前進行
代碼測試階段需要配置的內(nèi)容
1. 部署Sonarqube Server和PgSQL
2. 在PgSQL創(chuàng)建數(shù)據(jù)庫, 配置Sonarqube和PgSQL的連接
3. 配置SonarScanner和服務器的連接
4. 配置項目構(gòu)建使用Sonarqube進行掃描. 如果是使用腳本, 那么只需在構(gòu)建中使用Shell腳本, 同時這樣也可以把代碼克隆到自定義的Jenkins服務器目錄下, 之后在項目目錄下, 創(chuàng)建properties文件. 并且在項目目錄下執(zhí)行scanner客戶端命令
5. 如果是使用Jenkins的Scanner插件, 那么需要安裝插件, 配置Sonarqube服務器地址, 以及本地的Scanner客戶端命令的路徑, 包括編輯properties文件
6. 執(zhí)行構(gòu)建即可完成代碼克隆和掃描
1.3 代碼編譯
對于Java這類編譯性語言, 一旦源代碼掃描沒有問題, 就會執(zhí)行編譯, 如果代碼有問題, 那么就不會繼續(xù)構(gòu)建. 如果是html, php這些語言, 那么編譯步驟是不用的
Java代碼編譯可以在單獨的Maven服務器進行執(zhí)行, 并且可以搭建本地的倉庫, 去緩存編譯的依賴包, 如Nexus
1.4 代碼拷貝
代碼編譯完, 需要把生成的jar|war包拷貝到后端的web服務器, 可以基于scp, rsync或者ansible
這一步需要配置Jenkins和后端服務器的免秘鑰認證, 將jenkins用戶的公鑰拷貝到web服務器的運行用戶的賬號. 如果web服務是tomcat用戶啟動, 那么就是ssh-copy-id tomcat@xxxx
1.5 將web服務器從負載均衡器下線
做代碼替換需要先把服務器從負載均衡器下線, 如果在沒有下線前, 直接停止服務, 那么客戶端訪問會立刻報錯. 因此, 要先把web服務器從負載均衡器下線, 然后停止服務, 再做代碼替換. web服務都是會做Session共享的, 因此, 即使用戶正在訪問的服務器下線了, 下次再訪問被調(diào)度到其他的服務器, Session信息也不會丟失
HAProxy可以通過socket文件, 進行動態(tài)的上線下
LVS需要使用lvsadmin命令或者在Keepalived中注釋掉被下線的服務器
Nginx需要修改配置文件, 然后reload服務
1.6 停止web服務
服務器從負載均衡下線后, 要停止服務, 否則某些文件正在被占用時, 執(zhí)行代碼替換會出現(xiàn)錯誤
1.7 代碼替換
代碼替換有多種方案, 可以把壓縮包存放到統(tǒng)一的路徑, 把解壓后的目錄存放到統(tǒng)一路經(jīng), 然后把用戶訪問的路徑軟連接到代碼目錄. 之后代碼升級或者降級只需要刪除和重新創(chuàng)建軟連接即可
對于舊版本的代碼, 也可以暫時保留上一個版本, 一旦升級失敗, 可以直接把上一個版本的代碼拷貝到目標目錄下, 修改軟連接, 即可完成回滾
1.8 啟動服務
代碼替換后, 進行服務啟動
1.9 服務測試
在把服務器重新添加到負載均衡前, 要先進行服務測試, 因為, 代碼掃描只能掃描語法和bug等信息. 但是具體的功能, 還需要部署后才能進行測試. 因此, 在上線前, 要進行功能測試, 或者至少訪問以下監(jiān)控的url. 確保服務啟動, 且運行正常. 如果發(fā)現(xiàn)服務啟動失敗, 或者功能缺失, 可以在這一步進行回滾
1.10 將服務器添加到負載均衡器
添加到負載均衡器后, 用戶就可以正常訪問了
2 代碼部署策略
2.1 灰度/金絲雀部署
對于灰度部署, 可以先只升級一部分服務器, 讓剩余的服務器正常運行在舊版本下. 新版本服務器升級測試沒問題后, 上線到負載均衡器, 讓用戶訪問一段時間, 確定沒問題, 再繼續(xù)升級后續(xù)的服務器
2.2 藍綠部署
藍綠部署要求有兩套相同的環(huán)境, 每次升級只升級一套, 當運行穩(wěn)定后, 再把另一套升級到相同的版本
2.3 滾動部署
每次都升級一個或者幾個服務器, 并且連續(xù)的把所有的服務器都升級完.
具體的部署流程和策略, 還需要根據(jù)具體情況和業(yè)務, 進行修改
3 Jenkins參數(shù)化構(gòu)建
3.1 創(chuàng)建一個項目
3.2 配置構(gòu)建
- 丟棄舊的構(gòu)建
只保留7天內(nèi)的5次構(gòu)建
- 參數(shù)化構(gòu)建過程
字符參數(shù): 用于指定部署的是哪個分支, 也就是代碼部署到哪個環(huán)境
選項參數(shù): 用于選擇此次部署的內(nèi)容, 比如可以定義某個選項為執(zhí)行灰度部署, 部署后, 測試沒問題, 再選擇部署剩余的服務器
選項參數(shù)案例:
Jenkins服務器創(chuàng)建腳本, 測試$1返回值
jenkins@jenkins:/data/jenkins$ vim tomcat-1.sh
#!/bin/bash
echo $1
jenkins@jenkins:/data/jenkins$ bash tomcat-1.sh GROUP
GROUP
jenkins@jenkins:/data/jenkins$ bash tomcat-1.sh GROUP1
GROUP1
jenkins@jenkins:/data/jenkins$ bash tomcat-1.sh GROUP2
GROUP2
在執(zhí)行Shell構(gòu)建時, 腳本后要接$GROUP
. 之后執(zhí)行構(gòu)建時, 可以在Jenkins頁面選擇GROUP1
或者GROUP2
, 如果執(zhí)行GROUP1
, 那么GROUP1
就會作為參數(shù), 傳給GROUP
變量, 最終傳給腳本中的$1
. 只需要在腳本中判斷$1
的值, 就可以判斷此次是灰度發(fā)布, 還是發(fā)布剩余的服務器
- 此時, 執(zhí)行構(gòu)建時, 是沒有立即構(gòu)建選項的, 而是要使用
Build with Parameters
- 構(gòu)建時, 可以根據(jù)不同的選項進行構(gòu)建
Group1
Group2
字符參數(shù)案例:
一般用于指定部署到不同的環(huán)境, 比如生產(chǎn), 開發(fā)或者測試環(huán)境
- 字符參數(shù)創(chuàng)建
jenkins@jenkins:/data/jenkins$ vim tomcat-1.sh
#!/bin/bash
BRANCH=$1
GROUP=$2
echo "部署的分支是:${BRANCH}"
echo "部署的服務器是:${GROUP}"
~
jenkins@jenkins:/data/jenkins$ bash tomcat-1.sh master GROUP1
部署的分支是:master # master就表示部署的是master分支的代碼, 而master分支的代碼是部署到生產(chǎn)環(huán)境的
部署的服務器是:GROUP1
- 在構(gòu)建腳本中, 定義克隆的分支
#!/bin/bash
BRANCH=$1
GROUP=$2
cd /data/jenkins/qq && rm -rf web-02
git clone -b ${BRANCH} git@10.0.0.239:qq/web-02.git
echo "部署的分支是:${BRANCH}"
#echo "部署的服務器是:${GROUP}"
- 測試部署克隆
jenkins@jenkins:/data/jenkins/qq/web-02$ git status
On branch develop
Your branch is up to date with 'origin/develop'.
nothing to commit, working tree clean
jenkins@jenkins:/data/jenkins/qq/web-02$ git branch
* develop
3.3 通過選項參數(shù), 控制部署的服務器組
jenkins@jenkins:/data/jenkins$ vim tomcat-1.sh
#!/bin/bash
BRANCH=$1
GROUP=$2
if [ ${GROUP} == GROUP1 ]; then
IP_LIST="10.0.0.199"
elif [ ${GROUP} == GROUP2 ]; then
IP_LIST="10.0.0.209 10.0.0.219"
fi
echo "${GROUP} ----> ${IP_LIST}"
#cd /data/jenkins/qq && rm -rf web-02
#git clone -b ${BRANCH} git@10.0.0.239:qq/web-02.git
#echo "部署的分支是:${BRANCH}"
#echo "部署的服務器是:${GROUP}"
jenkins@jenkins:/data/jenkins$ bash tomcat-1.sh master GROUP1
GROUP1 ----> 10.0.0.199
#測試關(guān)閉指定服務器組的tomcat服務
jenkins@jenkins:/data/jenkins$ vim tomcat-1.sh
#!/bin/bash
BRANCH=$1
GROUP=$2
if [ ${GROUP} == GROUP1 ]; then
IP_LIST="10.0.0.199"
elif [ ${GROUP} == GROUP2 ]; then
IP_LIST="10.0.0.209 10.0.0.219"
fi
for node in ${IP_LIST}; do
ssh tomcat@${node} "/etc/init.d/tomcat.sh stop"
done
#echo "${GROUP} ----> ${IP_LIST}"
#cd /data/jenkins/qq && rm -rf web-02
#git clone -b ${BRANCH} git@10.0.0.239:qq/web-02.git
#echo "部署的分支是:${BRANCH}"
#echo "部署的服務器是:${GROUP}"
jenkins@jenkins:/data/jenkins$ bash tomcat-1.sh master GROUP1
正在判斷Tomcat運行狀態(tài), 倒計時3秒
3
2
1
Tomcat正在運行, 進程ID為5167, 共1個進程
三秒后, 準備關(guān)閉Tomcat
3
2
1
已嘗試關(guān)閉Tomcat, 30秒后檢查是否關(guān)閉成功!
3
2
1
Tomcat關(guān)閉成功!
驗證10.0.0.199的tomcat服務關(guān)閉成功
jenkins@jenkins:/data/jenkins$ curl 10.0.0.199:8080/myapp/
curl: (7) Failed to connect to 10.0.0.199 port 8080: Connection refused
驗證10.0.0.199的tomcat服務啟動成功
jenkins@jenkins:/data/jenkins$ vim tomcat-1.sh
#!/bin/bash
BRANCH=$1
GROUP=$2
if [ ${GROUP} == GROUP1 ]; then
IP_LIST="10.0.0.199"
elif [ ${GROUP} == GROUP2 ]; then
IP_LIST="10.0.0.209 10.0.0.219"
fi
for node in ${IP_LIST}; do
ssh tomcat@${node} "/etc/init.d/tomcat.sh start"
done
#echo "${GROUP} ----> ${IP_LIST}"
#cd /data/jenkins/qq && rm -rf web-02
#git clone -b ${BRANCH} git@10.0.0.239:qq/web-02.git
#echo "部署的分支是:${BRANCH}"
#echo "部署的服務器是:${GROUP}"
jenkins@jenkins:/data/jenkins$ bash tomcat-1.sh master GROUP1
正在判斷服務狀態(tài), 請稍等!
倒計時三秒鐘!
3
2
1
Tomcat沒有運行, 三秒后準備啟動!
3
2
1
Tomcat started.
Tomcat啟動已經(jīng)執(zhí)行, 五秒后判斷是否啟動成功!
5
4
3
2
1
Tomcat啟動成功! 共1個java進程, 其PID為5660
- 到此, 確保了可以通過構(gòu)建腳本, 對于指定的服務器, 通過選項和字符參數(shù)進行服務的停止和啟動, 以及根據(jù)分支, 進行代碼克隆
4 灰度部署案例-靜態(tài)頁面
- 實現(xiàn)qq/web-02項目的灰度發(fā)布, 先升級tomcat-1
三臺tomcat服務器
10.0.0.199
10.0.0.209
10.0.0.219
灰度服務器: 10.0.0.199
其余服務器: 10.0.0.209, 10.0.0.219
4.1 準備工作
- 把jenkins上的項目以及
workspace
目錄清空
- 把jenkins上的項目以及
- 準備
sonar-project.properties
文件, 放到web-02
項目, 和代碼一起提交到Gitlab
- 準備
sonar.projectKey=sonarqube-qq-web-02-projectKey
sonar.projectName=sonarqube-qq-web-02-projectName
sonar.projectVersion=1.0
# Jenkins克隆代碼到/var/lib/jenkins/workspace/目錄, 該文件會在web-02目錄下, sonar-scanner會掃描web-02中的所有文件
sonar.sources=./
sonar.language=html
sonar.sourceEncoding=UTF-8
- Jenkins服務器以及tomcat服務器安裝zip和unzip命令
root@jenkins:~# apt -y install zip unzip
root@tomcat-1:~# apt -y install zip unzip
root@tomcat-2:~# apt -y install zip unzip
root@tomcat-3:~# apt -y install zip unzip
- haproxy安裝socat
root@ha-1:~# apt -y install socat
root@ha-2:~# apt -y install socat
- 將Jenkins服務器jenkins賬號的公鑰, 傳給haproxy服務器的root賬號, 做免秘鑰認證.
jenkins是以jenkins用戶連接到haproxy, 而haproxy用root執(zhí)行服務器上下線
- 將Jenkins服務器jenkins賬號的公鑰, 傳給haproxy服務器的root賬號, 做免秘鑰認證.
jenkins@jenkins:~$ ssh-copy-id root@10.0.0.179
jenkins@jenkins:~$ ssh-copy-id root@10.0.0.189
4.2 修改構(gòu)建配置
- 選項參數(shù)GROUP, 用來控制部署/回滾的服務器
- 選項參數(shù)METHOD, 表示此次部署是升級還是回滾
- 選項參數(shù)BRANCH, 表示此次部署的是哪個分支
- 構(gòu)建腳本
#!/bin/bash
DATE=`date +%Y-%m-%d_%H-%M-%S`
# METHOD變量用來指定本次執(zhí)行是升級代碼還是回滾代碼
METHOD=$1
# BRANCH變量用來指定本次執(zhí)行部署或者回滾的分支
BRANCH=$2
# 服務器分組
# GROUP1灰度環(huán)境服務器,10.0.0.199
# GROUP2灰度以外的服務器, 10.0.0.209,10.0.0.219
# GROUP3一次新部署所有服務器, 用于緊急的服務升級, 比如需要執(zhí)行緊急的bug修復
GROUP_LIST=$3
ip_list(){
if [[ ${GROUP_LIST} == "GROUP1" ]];then
Server_IP="10.0.0.199"
echo "灰度服務器地址為: ${Server_IP}"
elif [[ ${GROUP_LIST} == "GROUP2" ]];then
Server_IP="10.0.0.209 10.0.0.219"
echo "剩余服務器地址為: ${Server_IP}"
elif [[ ${GROUP_LIST} == "GROUP3" ]];then
Server_IP="10.0.0.199 10.0.0.209 10.0.0.219"
echo "所有服務器地址: ${Server_IP}"
fi
}
clone_code(){
echo "即將開始克隆 ${BRANCH} 分支的代碼"
cd /var/lib/jenkins/workspace && rm -rf web-02 && git clone -b ${BRANCH} git@10.0.0.239:qq/web-02.git
echo "${BRANCH}分支代碼clone完成核无,準備開始代碼掃描"
}
scanner_code(){
cd /var/lib/jenkins/workspace/web-02 && /apps/sonar-scanner/bin/sonar-scanner
echo "代碼掃描完成,請打開sonarqube查看掃描結(jié)果"
}
code_maven(){
echo "cd /var/lib/jenkins/workspace/web-02 && mvn clean package -Dmaven.test.skip=true"
echo "代碼編譯完成"
}
make_zip(){
cd /var/lib/jenkins/workspace/web-02 && zip -r web-02.zip ./
echo "代碼打包完成"
}
down_node(){
for node in ${Server_IP};do
ssh root@10.0.0.179 "echo "disable server tomcat/${node}" | socat stdio /var/lib/haproxy/admin.sock"
echo "${node} 從負載均衡10.0.0.179下線成功"
ssh root@10.0.0.189 "echo "disable server tomcat/${node}" | socat stdio /var/lib/haproxy/admin.sock"
echo "${node} 從負載均衡10.0.0.189下線成功"
done
}
scp_zipfile(){
for node in ${Server_IP};do
scp /var/lib/jenkins/workspace/web-02/web-02.zip tomcat@${node}:/data/tomcat/tomcat_zip/web-02-${DATE}.zip
ssh tomcat@${node} "unzip /data/tomcat/tomcat_zip/web-02-${DATE}.zip -d /data/tomcat/tomcat_file/web-02-${DATE} && rm -rf /data/tomcat/tomcat_webapps/myapp && ln -sv /data/tomcat/tomcat_file/web-02-${DATE} /data/tomcat/tomcat_webapps/myapp"
done
}
stop_tomcat(){
for node in ${Server_IP};do
ssh tomcat@${node} "/etc/init.d/tomcat.sh stop"
done
}
start_tomcat(){
for node in ${Server_IP};do
ssh tomcat@${node} "/etc/init.d/tomcat.sh start"
done
}
web_test(){
for node in ${Server_IP};do
NUM=`curl -s -I -m 10 -o /dev/null -w %{http_code} http://${node}:8080/myapp/index.html`
if [[ ${NUM} -eq 200 ]];then
echo "${node} 測試通過,即將添加到負載"
add_node ${node}
else
echo "${node} 測試失敗,請檢查該服務器是否成功啟動tomcat"
fi
done
}
add_node(){
node=$1
echo "準備添加${node}到負載均衡"
if [ ${node} == "10.0.0.199" ];then
ssh root@10.0.0.179 ""echo enable server tomcat/${node}" | socat stdio /var/lib/haproxy/admin.sock"
ssh root@10.0.0.189 ""echo enable server tomcat/${node}" | socat stdio /var/lib/haproxy/admin.sock"
echo "灰度部署環(huán)境服務器-->10.0.0.199 部署完畢,請進行代碼測試!"
else
ssh root@10.0.0.179 ""echo enable server tomcat/${node}" | socat stdio /var/lib/haproxy/admin.sock"
ssh root@10.0.0.189 ""echo enable server tomcat/${node}" | socat stdio /var/lib/haproxy/admin.sock"
fi
}
rollback_last_version(){
for node in ${Server_IP};do
echo $node
CURRENT_VERSION=`ssh tomcat@${node} ""/bin/ls -l -rt /data/tomcat/tomcat_webapps/ | awk -F"->" '{print $2}' | tail -n1""`
CURRENT_VERSION=`basename ${CURRENT_VERSION}`
echo "當前版本為: $CURRENT_VERSION"
LAST_VERSION=`ssh tomcat@${node} ""ls -l -rt /data/tomcat/tomcat_file/ | grep -B 1 ${CURRENT_VERSION} | head -n1 | awk '{print $9}'""`
echo "上一個版本為: $LAST_VERSION"
ssh tomcat@${node} "rm -rf /data/tomcat/tomcat_webapps/myapp && ln -sv /data/tomcat/tomcat_file/${LAST_VERSION} /data/tomcat/tomcat_webapps/myapp"
done
}
delete_last_version(){
for node in ${Server_IP};do
ssh tomcat@${node} "rm -rf /data/tomcat/tomcat_zip/*"
NUM=`ssh tomcat@${node} ""/bin/ls -l -d -rt /data/tomcat/tomcat_file/web-02-* | wc -l""`
echo "當前歷史構(gòu)建數(shù)量為: $NUM"
if [ ${NUM} -gt 5 ];then
# 只保留最近5個版本, 超過5個, 那么下次部署完, 就刪除當前最舊的一個版本
NAME=`ssh tomcat@${node} ""/bin/ls -l -d -rt /data/tomcat/tomcat_file/web-02-* | head -n1 | awk '{print $9}'""`
ssh tomcat@${node} "rm -rf ${NAME}"
echo "${node} 刪除歷史版本 /data/tomcat/tomcat_file/${NAME}成功!"
fi
done
}
main(){
case $1 in
deploy)
ip_list;
clone_code;
scanner_code;
#code_maven;
make_zip;
down_node;
stop_tomcat;
scp_zipfile;
start_tomcat;
web_test;
delete_last_version;
;;
rollback)
ip_list;
#echo ${Server_IP}
down_node;
stop_tomcat;
rollback_last_version;
start_tomcat;
web_test;
;;
esac
}
main $1 $2 $3
4.3 部署測試
- 先部署灰度服務器, GROUP1, 測試沒問題后, 部署GROUP2
- 如果發(fā)現(xiàn)全部升級后, 出現(xiàn)bug, 可以直接rollback GROUP3
- 如果是部署開發(fā)/測試環(huán)境, 那么還需要做額外的判斷, 添加開發(fā)/測試環(huán)境的服務器, 在Jenkins上創(chuàng)建單獨的目錄存放測試環(huán)境的代碼, 然后傳給測試環(huán)境服務器.
- 整個灰度升級過程是分多個階段的, 首先代碼克隆后, 要進行代碼質(zhì)量檢測, 檢測通過后, 會進行灰度服務器的代碼拷貝, 并且把灰度服務器在負載均衡器上線, 此時, 可以給灰度服務器專門配置一個vip, 在haproxy添加一個灰度服務器的監(jiān)聽組, 讓開發(fā)測試人員, 修改本地hosts文件, 把網(wǎng)站的域名, 解析到灰度服務器的vip, 之后進行測試, 測試沒問題, 再把灰度服務器上線到生成環(huán)境的vip組上. 之后再升級剩余的服務器
- 如果想基于指定版本進行升級或者回滾, 可以基于tag號標簽, 或者再給構(gòu)建腳本傳一個參數(shù), 先確定要回滾到的指定的目錄名, 如,
web-02-2021_xx_xx-xx-xx-xx
, 之后把這個目錄名, 作為參數(shù), 傳給構(gòu)建腳本, 修改軟鏈接, 直接指向這個指定的版本
listen tomcat
bind 10.0.0.188:80
mode http
log global
server 10.0.0.199 10.0.0.199:8080 check inter 3000 fall 2 rise 5
server 10.0.0.209 10.0.0.209:8080 check inter 3000 fall 2 rise 5
server 10.0.0.219 10.0.0.219:8080 check inter 3000 fall 2 rise 5
listen canary
bind 10.0.0.189:80
mode http
log global
server 10.0.0.199 10.0.0.199:8080 check inter 3000 fall 2 rise 5
vrrp_instance VI_1 {
state MASTER
interface eth0
garp_master_delay 10
smtp_alert
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
10.0.0.188 label eth0:0 dev eth0
10.0.0.189 label eth0:1 dev eth0 # 189這個vip專門給內(nèi)網(wǎng)用戶使用, 并不會在防火墻做解析, 因此, 公網(wǎng)用戶是訪問不到的
}
}