實(shí)踐:devops之云主機(jī)模式持續(xù)部署(ci-cd)
目錄
推薦文章
https://www.yuque.com/xyy-onlyone/aevhhf?# 《玩轉(zhuǎn)Typora》
0、流程分析
2條Jenkins pipeline
CI pipeline
CD pipeline
標(biāo)準(zhǔn)規(guī)范
項(xiàng)目規(guī)范與總體設(shè)計(jì)
公司里面要使用流水線要做持續(xù)集成CI/CD的項(xiàng)目越來越多,這對(duì)流水線的設(shè)計(jì)和開發(fā)有不同的要求。我們經(jīng)常聽到用戶的反饋:
-
各種不同語言的技術(shù)棧, 如何使流水線適配呢? 從不同技術(shù)棧維護(hù)一套流水線模版,到我們使用共享庫進(jìn)行統(tǒng)一的管理和維護(hù)。
-
對(duì)于不同的項(xiàng)目,大家管理代碼的方式也不同??赡苓€有一部分用戶在使用Svn等不同的版本控制系統(tǒng)。
-
不同的項(xiàng)目,開發(fā)模式也不太一樣, 編譯構(gòu)建工具不同,發(fā)布的方式也有不同的地方…
等等,不止上面的問題。所以在做流水線的使用應(yīng)該提前把項(xiàng)目團(tuán)隊(duì)的規(guī)范定義好, 這樣后期項(xiàng)目改造后可以直接集成CI/CD流水線。更加便捷。
1.團(tuán)隊(duì)信息
信息項(xiàng) | 描述 |
---|---|
業(yè)務(wù)簡(jiǎn)稱/編號(hào) | devops4 |
開發(fā)模式 | 特性分支開發(fā),版本分支發(fā)布,主干分支作為最新代碼 |
項(xiàng)目類型與構(gòu)建方式 | 前端: vue項(xiàng)目, npm打包, 制品目錄 dist 后端:springboot項(xiàng)目, maven打包, 制品目錄 target |
發(fā)布主機(jī)環(huán)境(vm) | LB: 192.168.1.200 Server: 192.168.1.121~192.168.1.122 |
2.CI/CD規(guī)范
通過上面的信息,我們采用如下規(guī)范:
工具鏈 | |
---|---|
GitLab 代碼庫 | 倉庫組: devops4 項(xiàng)目倉庫后端 devops4-ops-service 前端 devops4-ops-ui |
Jenkins作業(yè) | 文件夾: devops4 |
作業(yè)命名: 后端 devops4-ops-service 前端 devops4-ops-ui | |
CI構(gòu)建規(guī)范 | 前端項(xiàng)目采用npm打包后統(tǒng)一放到dist目錄下, 靜態(tài)文件以tgz打包。 |
后端項(xiàng)目采用maven打包后統(tǒng)一放到target目錄下,以jar包。 | |
Sonar代碼報(bào)告 | 前端項(xiàng)目:devops4-ops-ui 后端項(xiàng)目:devops4-ops-service |
項(xiàng)目團(tuán)隊(duì)可以使用devops4命名的自定義質(zhì)量規(guī)則和質(zhì)量閾。 | |
Nexus制品庫目錄 |
devops4-ops-service/version/devops4-ops-service-version.jar |
devops4-ops-ui/version/devops4-ops-ui-version.tar.gz | |
devops4 | 版本: 分割release分支獲取版本號(hào) |
發(fā)布規(guī)范 | 用戶輸入版本,下載制品庫,使用腳本啟動(dòng)服務(wù)。 |
標(biāo)準(zhǔn)化
版本分支命名:RELEASE-1.1.1
分支策略
特性分支開發(fā),版本分支發(fā)布。
環(huán)境管理
使用virtualbox創(chuàng)建2臺(tái)虛擬機(jī), 或者采用terraform操作云平臺(tái)創(chuàng)建2臺(tái)虛機(jī)。
本次,自己使用2臺(tái)本地vmwareworkstation虛機(jī)測(cè)試。
制品管理
制品版本命名:版本號(hào)-CommitID
發(fā)布流水線
Jenkins pipeline * 2
-
CI pipeline
-
CD pipeline
-
- 復(fù)選框參數(shù): 發(fā)布主機(jī)
- 字符參數(shù):版本分支
- 選項(xiàng)參數(shù):目標(biāo)環(huán)境 【dev/uat/stg/prod】
應(yīng)用發(fā)布與回滾策略
Deploy發(fā)布策略
藍(lán)綠發(fā)布
環(huán)境存在兩個(gè)版本,藍(lán)版本和綠版本同時(shí)存在,部署新版本然后進(jìn)行測(cè)試,將流量切到新版本,最終實(shí)際運(yùn)行的只有一個(gè)版本(藍(lán)/綠)。好處是無需停機(jī),并且發(fā)布風(fēng)險(xiǎn)較小。
nginx upstream模塊實(shí)現(xiàn):
upstream webservers {
server 192.168.1.253:8099 weight=100;
server 192.168.1.252:8099 down;
}
server {
listen 8017;
location / {
proxy_pass http://webservers;
}
}
nginx -s reload
灰度發(fā)布
將發(fā)行版發(fā)布到一部分用戶或服務(wù)器的一種模式。這個(gè)想法是首先將更改部署到一小部分服務(wù)器,進(jìn)行測(cè)試,然后將更改推廣到其余服務(wù)器。一旦通過所有運(yùn)行狀況檢查,當(dāng)沒有問題時(shí),所有的客戶將被路由到該應(yīng)用程序的新版本,而舊版本將被刪除。
nginx 權(quán)重模擬:
upstream webservers {
server 192.168.1.223:8099 weight=100;
server 192.168.1.222:8099 weight=100;
server 192.168.1.221:8099 weight=100;
}
server {
listen 8017;
location / {
proxy_pass http://webservers;
}
}
nginx -s reload
版本回滾
- 版本一直升級(jí),則無需回滾。
- 選擇舊版本文件,進(jìn)行發(fā)布。
前端后端項(xiàng)目發(fā)布
1、前端項(xiàng)目
復(fù)制靜態(tài)文件到nginx站點(diǎn)目錄,nginx -s reload
## 進(jìn)入Web服務(wù)器的站點(diǎn)目錄下
## 下載包
[root@master html]# curl -u admin:admin123 http://192.168.1.200:8081/repository/anyops/com/anyops/anyops-devops-ui/1.1.1/anyops-devops-ui-1.1.1.tar.gz -o anyops-devops-ui-1.1.1.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 196k 100 196k 0 0 24.0M 0 --:--:-- --:--:-- --:--:-- 24.0M
## 解壓包
[root@master html]# tar zxf anyops-devops-ui-1.1.1.tar.gz
[root@master html]# ls
anyops-devops-ui-1.1.1.tar.gz index.html static
## 觸發(fā)nginx重載
[root@master html]# nginx -s reload
2、后端項(xiàng)目
- 復(fù)制jar包到目標(biāo)目錄, 使用nohup java -jar 啟動(dòng)服務(wù)。
- nohup java -jar app.jar >output 2>&1 &
1、CI
拷貝Jenkins流水線
- 拷貝Jenkins作業(yè)
devops6-maven-service
到devops6-maven-service_CI
保存后,點(diǎn)擊參數(shù)化構(gòu)建,會(huì)發(fā)現(xiàn)branchName
的頁面參數(shù)為空,我們先直接運(yùn)行一次流水線看看效果:
運(yùn)行一次流水線后,再次運(yùn)行時(shí),就會(huì)看到branchName
正常了。
接下來我們就用devops6-maven-service_CI
來測(cè)試。
- 我們?cè)俅芜\(yùn)行下,看下效果
可以看到,流水線運(yùn)行成功。
可以看到nexus倉庫里制品被上傳成功了。
優(yōu)化pipeline代碼,去除制品庫里CI字樣
appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service_CI
repoName = appName.split('-')[0] //devops6
appVersion = "${env.branchName}".split("-")[-1] // RELEASE-1.1.1 1.1.1
targetDir="${appName}/${appVersion}"
再次運(yùn)行測(cè)試效果:
符合預(yù)期。
新建Jenkins CD流水線
- 創(chuàng)建一個(gè)
devops6-maven-service_CD
作業(yè),然后添加一些頁面參數(shù)
點(diǎn)擊參數(shù)化構(gòu)建:
創(chuàng)建一個(gè)devops6的視圖
優(yōu)化pipeline代碼,nexus倉庫的版本里要帶上commitID
- 之前倉庫是這樣的
- 先來手動(dòng)獲取下項(xiàng)目倉庫的commitID
[root@Devops6 ~]#cd /opt/jenkinsagent/workspace/
[root@Devops6 workspace]#ls
day2-pipeline-demo devops6-gradle-service devops6-maven-service devops6-maven-service_CI@tmp devops6-maven-test devops6-npm-service test-maven
day2-pipeline-demo@tmp devops6-gradle-service@tmp devops6-maven-service_CI devops6-maven-service@tmp devops6-maven-test@tmp devops6-npm-service@tmp test-maven@tmp
[root@Devops6 workspace]#cd devops6-maven-service_CI
[root@Devops6 devops6-maven-service_CI]#ls
mvnw mvnw.cmd pom.xml sonar-project.properties src target
[root@Devops6 devops6-maven-service_CI]#git rev-parse HEAD #通過這個(gè)命令之可以獲取倉庫comitID的。
b5cfb8eeee597edd752cb11f5daa9ac843fb9f97
[root@Devops6 devops6-maven-service_CI]#
然后利用片段生成器生成代碼:
sh returnStdout: true, script: 'git rev-parse HEAD'
然后集成到piepeline代碼里。
- 我們想讓這里的版本號(hào)也帶上commitID
這里直接寫代碼:
appVersion = "${appVersion}-${env.commitID}"
//獲取commitID
env.commitID = gitlab.GetCommitID()
println("commitID: ${env.commitID}")
package org.devops
//獲取CommitID
def GetCommitID(){
ID = sh returnStdout: true, script:"git rev-parse HEAD"
ID = ID -"\n"
return ID[0..7] //取前8位id
}
- 在gitlab的
devops6-maven-service
里以main分支創(chuàng)建RELEASE-9.9.9
分支
- 運(yùn)行
devops6-maven-service_CI
流水線
測(cè)試成功。
2、CD
下載制品
cd部分就不用再下載代碼獲取commitID了。
我們來使用gitlab api獲取分支commit。
Step1: 獲取GitLab 分支CommitID
- 打開gitlab api官方文檔
https://docs.gitlab.com/
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches/main"
- 在postman里調(diào)試
先拿取下一些參數(shù):
這里拿到Project ID
:
然后在gitlab上創(chuàng)建一個(gè)token:
TLDgj3sz-cioyk6AfxZi
調(diào)試:
http://172.29.9.101:8076/api/v4/projects/7/repository/branches/RELEASE-9.9.9
添加get請(qǐng)求,添加PRIVARE-TOKEN
,點(diǎn)擊Send。
此時(shí),我們就通過gitalb api拿到了分支commitID了,和之前手動(dòng)執(zhí)行g(shù)it命令獲取的commitID信息一致。
- 此時(shí)拿到postman給出的cURL命令
curl --location 'http://172.29.9.101:8076/api/v4/projects/7/repository/branches/RELEASE-9.9.9' \
--header 'PRIVATE-TOKEN: TLDgj3sz-cioyk6AfxZi'
- 優(yōu)化pipeline代碼
創(chuàng)建Gitlab.groovy文件
package org.devops
//發(fā)起HTTP請(qǐng)求
def HttpReq(method, apiUrl){
response = sh returnStdout: true,
script: """
curl --location --request ${method} \
http:172.29.9.101:8076/api/v4/${apiUrl} \
--header "PRIVATE-TOKEN: TLDgj3sz-cioyk6AfxZi"
"""
response = readJSON text: response - "\n" //json數(shù)據(jù)的讀取方式
return response
}
但是,這里的gitlab token是明文的,因此需要在jenkins里配置個(gè)憑據(jù)。
然后利用片段生成器來利用次token,生成代碼:
withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
// some block
}
- 優(yōu)化pipeline代碼
package org.devops
//發(fā)起HTTP請(qǐng)求
def HttpReq(method, apiUrl){
withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
response = sh returnStdout: true,
script: """
curl --location --request ${method} \
http:172.29.9.101:8076/api/v4/${apiUrl} \
--header "PRIVATE-TOKEN: ${gitlabtoken}"
"""
}
response = readJSON text: response - "\n" //json數(shù)據(jù)的讀取方式
return response
}
但是,存在一個(gè)問題,apiUrl里我們還需要知道ProjectID才行,這里繼續(xù)查找gitlab api。
- 獲取ProjectID
http://172.29.9.101:8076/api/v4/projects?search=devops6-maven-service
curl --location 'http://172.29.9.101:8076/api/v4/projects?search=devops6-maven-service' \
--header 'PRIVATE-TOKEN: TLDgj3sz-cioyk6AfxZi'
- 繼續(xù)優(yōu)化pipeline代碼
package org.devops
//發(fā)起HTTP請(qǐng)求
def HttpReq(method, apiUrl){
withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
response = sh returnStdout: true,
script: """
curl --location --request ${method} \
http:172.29.9.101:8076/api/v4/${apiUrl} \
--header "PRIVATE-TOKEN: ${gitlabtoken}"
"""
}
response = readJSON text: response - "\n" //json數(shù)據(jù)的讀取方式
return response
}
//獲取ProjectID
def GetProjectIDByName(projectName, groupName){
apiUrl = "projects?search=${projectName}"
response = HttpReq("GET", apiUrl)
if (response != []){
for (p in response) {
if (p["namespace"]["name"] == groupName){
return response[0]["id"]
}
}
}
}
//獲取分支CommitID
def GetBranchCommitID(projectID, branchName){
apiUrl = "projects/${projectID}/repository/branches/${branchName}"
response = HttpReq("GET", apiUrl)
return response.commit.short_id
}
- 創(chuàng)建
cd.jenkinsfile
@Library("devops06@main") _
//import src/org/devops/Gitlab.groovy
def mygit = new org.devops.Gitlab()
//pipeline
pipeline{
agent { label "build"}
options {
skipDefaultCheckout true
}
stages{
stage("GetArtifact"){
steps{
script{
env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
env.groupName = "${env.projectName}".split('-')[0] //devops6
projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
println(commitID)
// appVersion = "${env.branchName}".split("-")[-1] //9.9.9
// println(appVersion)
// currentBuild.description = "Version: ${appVersion}-${commitID}"
currentBuild.displayName = "第${BUILD_NUMBER}次構(gòu)建-${commitID}"
currentBuild.description = "構(gòu)建分支名稱:${env.branchName}"
}
}
}
}
}
Gitlab.groovy
代碼
package org.devops
//發(fā)起HTTP請(qǐng)求
def HttpReq(method, apiUrl){
withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
response = sh returnStdout: true,
script: """
curl --location --request ${method} \
http://172.29.9.101:8076/api/v4/${apiUrl} \
--header "PRIVATE-TOKEN: ${gitlabtoken}"
"""
}
response = readJSON text: response - "\n" //json數(shù)據(jù)的讀取方式
return response
}
//獲取ProjectID
def GetProjectIDByName(projectName, groupName){
apiUrl = "projects?search=${projectName}"
response = HttpReq("GET", apiUrl)
if (response != []){
for (p in response) {
if (p["namespace"]["name"] == groupName){
return response[0]["id"]
}
}
}
}
//獲取分支CommitID
def GetBranchCommitID(projectID, branchName){
apiUrl = "projects/${projectID}/repository/branches/${branchName}"
response = HttpReq("GET", apiUrl)
return response.commit.short_id
}
- 編輯
devops6-maven-service_CD
流水線使用共享庫
運(yùn)行流水線:
測(cè)試成功。??
Step2: 下載制品
- nexus倉庫制品地址如下
http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1/devops6-maven-service-6.1.1.jar
- 這里編寫pipeline代碼
@Library("devops06@main") _
//import src/org/devops/Gitlab.groovy
def mygit = new org.devops.Gitlab()
//pipeline
pipeline{
agent { label "build"}
options {
skipDefaultCheckout true
}
stages{
stage("GetArtifact"){
steps{
script{
env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
env.groupName = "${env.projectName}".split('-')[0] //devops6
projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
println(commitID)
appVersion = "${env.branchName}".split("-")[-1] //9.9.9
println(appVersion)
// currentBuild.description = "Version: ${appVersion}-${commitID}"
currentBuild.displayName = "第${BUILD_NUMBER}次構(gòu)建-${commitID}"
currentBuild.description = "構(gòu)建分支名稱:${env.branchName}"
//下載制品
//http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1-b5cfb8ee/devops6-maven-service-6.1.1-b5cfb8ee.jar
repoUrl = "http://172.29.9.101:8081/repository/${env.groupName}"
artifactName = "${env.projectName}-${appVersion}-${commitID}.jar"
artifactUrl = "${repoUrl}/${env.projectName}/${appVersion}-${commitID}/${artifactName}"
sh "wget --no-verbose ${artifactUrl} && ls -l"
}
}
}
}
}
- 運(yùn)行觀察效果
下載制品成功。
我們?cè)龠\(yùn)行一次流水線:
會(huì)看到多了一個(gè)包,
最后我們發(fā)布完,會(huì)把它清掉的:
這里先手動(dòng)給清掉。
Step3: 發(fā)布
準(zhǔn)備2臺(tái)linux機(jī)器
devops-deploy1-172.29.9.110
devops-deploy2-172.29.9.111
- 給這2臺(tái)機(jī)器裝好
java-11
yum install -y java-11-openjdk.x86_64
devops機(jī)器安裝ansible環(huán)境
yum install epel-release -y
yum install ansible -y
- 編輯下ansible的主機(jī)管理文件:
[root@Devops6 ~]#vim /etc/ansible/hosts
172.29.9.110
172.29.9.111
- 給ansible機(jī)器到2個(gè)節(jié)點(diǎn)做個(gè)免密
ssh-keygen
ssh-copy-id -i ~/.ssh/id_rsa.pub root@172.29.9.110
ssh-copy-id -i ~/.ssh/id_rsa.pub root@172.29.9.111
- 查看當(dāng)前主機(jī)是否在線:
[root@Devops6 ~]#ansible all -m ping -u root
構(gòu)建一次devops6-maven-service_CD
,下載制品
我們先來手動(dòng)發(fā)布一次,再集成到CD流水線里
- 拷貝制品到deploy1
[root@Devops6 devops6-maven-service_CD]#ansible 172.29.9.110 -m copy -a "src=devops6-maven-service-9.9.9-b5cfb8ee.jar dest=/opt/devops6-maven-service-9.9.9-b5cfb8ee.jar"
- 啟動(dòng)服務(wù):
- 用準(zhǔn)備好的服務(wù)啟動(dòng)腳本來啟動(dòng)/停止java服務(wù)
服務(wù)啟動(dòng)腳本:service.sh
(原始腳本如下)
#!/bin/bash
# sh service.sh anyops-devops-service 1.1.1 8091 start
APPNAME=NULL
VERSION=NULL
PORT=NULL
start(){
port_result=`netstat -anlpt | grep "${PORT}" || echo false`
if [[ $port_result == "false" ]];then
nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
else
stop
sleep 5
nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
fi
}
stop(){
pid=`netstat -anlpt | grep "${PORT}" | awk '{print $NF}' | awk -F '/' '{print $1}' | head -1`
kill -15 $pid
}
check(){
proc_result=`ps aux | grep java | grep "${APPNAME}" | grep -v grep || echo false`
port_result=`netstat -anlpt | grep "${PORT}" || echo false`
url_result=`curl -s http://localhost:${PORT} || echo false `
if [[ $proc_result == "false" || $port_result == "false" || $url_result == "false" ]];then
echo "server not running"
else
echo "ok"
fi
}
case $1 in
start)
start
sleep 5
check
;;
stop)
stop
sleep 5
check
;;
restart)
stop
sleep 5
start
sleep 5
check
;;
check)
check
;;
*)
echo "sh service.sh {start|stop|restart|check}"
;;
esac
參數(shù)寫入后腳本如下
#!/bin/bash
# sh service.sh anyops-devops-service 1.1.1 8091 start
APPNAME=devops6-maven-service
VERSION=9.9.9-b5cfb8ee
PORT=8080
start(){
port_result=`netstat -anlpt | grep "${PORT}" || echo false`
if [[ $port_result == "false" ]];then
nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
else
stop
sleep 5
nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
fi
}
stop(){
pid=`netstat -anlpt | grep "${PORT}" | awk '{print $NF}' | awk -F '/' '{print $1}' | head -1`
kill -15 $pid
}
check(){
proc_result=`ps aux | grep java | grep "${APPNAME}" | grep -v grep || echo false`
port_result=`netstat -anlpt | grep "${PORT}" || echo false`
url_result=`curl -s http://localhost:${PORT} || echo false `
if [[ $proc_result == "false" || $port_result == "false" || $url_result == "false" ]];then
echo "server not running"
else
echo "ok"
fi
}
case $1 in
start)
start
sleep 5
check
;;
stop)
stop
sleep 5
check
;;
restart)
stop
sleep 5
start
sleep 5
check
;;
check)
check
;;
*)
echo "sh service.sh {start|stop|restart|check}"
;;
esac
- 將
service.sh
腳本拷貝到測(cè)試節(jié)點(diǎn):
[root@Devops6 devops6-maven-service_CD]#ansible 172.29.9.110 -m copy -a "src=service.sh dest=/opt/service.sh"
172.29.9.110 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"checksum": "666b4746afbb9fa684f79a89102715906417c848",
"dest": "/opt/service.sh",
"gid": 0,
"group": "root",
"md5sum": "22868400cb2784f7c7bcf63f38a977fe",
"mode": "0644",
"owner": "root",
"size": 1367,
"src": "/root/.ansible/tmp/ansible-tmp-1688219597.85-41901-255991227715874/source",
"state": "file",
"uid": 0
}
然后啟動(dòng)程序:
給予腳本執(zhí)行權(quán)限:
[root@devops-deploy1 opt]#ll
total 17284
-rw-r--r-- 1 root root 17690913 Jul 1 20:12 devops6-maven-service-9.9.9-b5cfb8ee.jar
-rw-r--r-- 1 root root 1367 Jul 1 21:53 service.sh
[root@devops-deploy1 opt]#chmod +x service.sh
啟動(dòng)程序:
[root@devops-deploy1 opt]#sh service.sh start
ok
[root@devops-deploy1 opt]#ps -aux|grep java
root 7626 37.4 8.7 3202716 163300 pts/0 Sl 21:55 0:04 java -jar -Dserver.port=8080 devops6-maven-service-9.9.9-b5cfb8ee.jar
root 7674 0.0 0.0 112708 972 pts/0 R+ 21:55 0:00 grep --color=auto java
[root@devops-deploy1 opt]#
開始集成
- 最終代碼如下
Deploy.groovy
文件
package org.devops
//rollback
def AnsibleRollBack(){
sh """
# 停止服務(wù)
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh stop" -u root
sleep 300
# 清理和創(chuàng)建發(fā)布目錄
ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
# 將備份目錄內(nèi)容復(fù)制到發(fā)布目錄
ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}.bak/* ${env.targetDir}/${env.projectName}/ || echo file not exists"
# 啟動(dòng)服務(wù)
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
# 檢查服務(wù)
sleep 10
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
"""
}
//發(fā)布制品
def AnsibleDeploy(){
//將主機(jī)寫入清單文件
sh "rm -fr hosts "
for (host in "${env.deployHosts}".split(',')){
sh " echo ${host} >> hosts"
}
// ansible 發(fā)布jar
sh """
# 主機(jī)連通性檢測(cè)
ansible "${env.deployHosts}" -m ping -i hosts
# 創(chuàng)建備份目錄
ansible "${env.deployHosts}" -m shell -a "mkdir -p ${env.targetDir}/${env.projectName}.bak || echo file is exists"
# 備份上次構(gòu)建
ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}/* ${env.targetDir}/${env.projectName}.bak/ || echo file not exists"
# 清理和創(chuàng)建發(fā)布目錄
ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
# 復(fù)制app
ansible "${env.deployHosts}" -m copy -a "src=${env.artifactName} dest=${env.targetDir}/${env.projectName}/${env.artifactName}"
"""
// 發(fā)布腳本
fileData = libraryResource 'scripts/service.sh'
println(fileData)
writeFile file: 'service.sh', text: fileData
sh "ls -a ; cat service.sh "
sh """
# 修改變量
sed -i 's#APPNAME=NULL#APPNAME=${env.projectName}#g' service.sh
sed -i 's#VERSION=NULL#VERSION=${env.releaseVersion}#g' service.sh
sed -i 's#PORT=NULL#PORT=${env.port}#g' service.sh
# 復(fù)制腳本
ansible "${env.deployHosts}" -m copy -a "src=service.sh dest=${env.targetDir}/${env.projectName}/service.sh"
# 啟動(dòng)服務(wù)
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
# 檢查服務(wù)
sleep 10
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
"""
}
cd.jenkinsfile
文件
@Library("devops06@main") _
//import src/org/devops/Gitlab.groovy
def mygit = new org.devops.Gitlab()
def mydeploy = new org.devops.Deploy()
//pipeline
pipeline{
agent { label "build"}
options {
skipDefaultCheckout true
}
stages{
stage("GetArtifact"){
steps{
script{
env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
env.groupName = "${env.projectName}".split('-')[0] //devops6
projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
println(commitID)
appVersion = "${env.branchName}".split("-")[-1] //9.9.9
println(appVersion)
// currentBuild.description = "Version: ${appVersion}-${commitID}"
currentBuild.displayName = "第${BUILD_NUMBER}次構(gòu)建-${commitID}"
currentBuild.description = "構(gòu)建分支名稱:${env.branchName}"
//下載制品
//http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1-b5cfb8ee/devops6-maven-service-6.1.1-b5cfb8ee.jar
repoUrl = "http://172.29.9.101:8081/repository/${env.groupName}"
env.artifactName = "${env.projectName}-${appVersion}-${commitID}.jar"
artifactUrl = "${repoUrl}/${env.projectName}/${appVersion}-${commitID}/${env.artifactName}"
sh "wget --no-verbose ${artifactUrl} && ls -l"
env.releaseVersion = "${appVersion}-${commitID}"
}
}
}
stage("Deploy"){
steps{
script{
mydeploy.AnsibleDeploy()
}
}
}
stage("RollBack"){
input {
message "是否進(jìn)行回滾?"
ok "Yes"
submitter ""
parameters {
choice choices: ['NO','YES'], name: 'OPS'
}
}
steps {
echo "OPS ${OPS}, doing......."
script{
if ("${OPS}" == "YES"){
mydeploy.AnsibleRollBack()
}
}
}
}
}
}
- 測(cè)試效果
執(zhí)行CD流水線:
運(yùn)行成功:
再看下2個(gè)節(jié)點(diǎn)的java運(yùn)行情況:
符合預(yù)期。
給gitlab上devops6-maven-service
項(xiàng)目配置個(gè)健康檢查端口
- 默認(rèn)這個(gè)生成的jar包啟動(dòng)后,是沒配置健康檢查端口的,我們的測(cè)試現(xiàn)象不明確
我們來啟動(dòng)下服務(wù):
- 因此我們來改下這個(gè)java代碼:
BasicController.java
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author <a href="mailto:chenxilzx1@gmail.com">theonefx</a>
*/
@Controller
public class BasicController {
// http://127.0.0.1:8080/hello?name=lisi
@RequestMapping("/hello")
@ResponseBody
public String hello(@RequestParam(name = "name", defaultValue = "xyy") String name) {
return "Hello RELEASE-10.1.0 " + name;
}
}
然后打包,運(yùn)行,觀察效果:
-
最后,將
devops6-maven-service
的RELEASE-9.9.9/
代碼合并到main分支。 -
打上tag
Step4: 回滾
推薦第一種。
第二種方法會(huì)存在很多邏輯問題的。
- 回滾代碼見上述文件,這里測(cè)試下效果
1、直接發(fā)布版本方式來回滾
先運(yùn)行CI流水線
CI pipeline運(yùn)行成功:
再運(yùn)行CD:
觀察效果:
可以看到發(fā)布老版本程序成功。
2、使用回滾代碼
注意:如果要回滾時(shí),就需要跳過發(fā)布階段,否則會(huì)有問題的,因此這里我給發(fā)布
階段加了一個(gè)判斷選項(xiàng)。
發(fā)布1.1.1
發(fā)布9.9.9
回滾到1.1.1:
符合預(yù)期。??
擴(kuò)展:參數(shù)動(dòng)態(tài)獲取實(shí)踐
- 需要安裝active choices插件重啟Jenkins服務(wù)器后再操作。
根據(jù)不同的環(huán)境帶出不同的機(jī)器
- 效果
- envName參數(shù)設(shè)置
return ["dev", "uat", "stag", "prod"]
- deployHosts參數(shù)設(shè)置
if (envName.equals("dev")){
return ["172.29.9.110,172.29.9.111"]
} else if (envName.equals("uat")){
return ["172.29.9.120,172.29.9.121"]
}
?? 注意:記得刪除前面定義好的envName和deployHosts選項(xiàng)參數(shù)。
- 運(yùn)行測(cè)試
符合預(yù)期。??
根據(jù)不同發(fā)布工具,動(dòng)態(tài)展示主機(jī)參數(shù)
這個(gè)就不做演示了,和上面這個(gè)實(shí)踐有沖突。
- 定義發(fā)布工具參數(shù)
return ["ansible", "saltstack"]
單選類型
- 定義發(fā)布主機(jī)
選擇關(guān)聯(lián)的參數(shù),多個(gè)參數(shù)用逗號(hào)分割
3、代碼匯總
- 本次實(shí)驗(yàn)代碼
鏈接:https://pan.baidu.com/s/1mn1EX2oX0XRGO-IjohkyLA?pwd=0820
提取碼:0820
2023.7.2-云主機(jī)模式持續(xù)部署-ci-cd-code
- 實(shí)驗(yàn)環(huán)境
gitlab-ce:15.0.3-ce.0
jenkins:2.346.3-2-lts-jdk11
sonarqube:9.9.0-community
nexus3:3.53.0
- ci-cd流水線
這2條流水線都是測(cè)試ok的。(以后就一直用這2條流水線來測(cè)試devops了)
- 倉庫代碼
gitlab倉庫devops6-maven-service
:RELEASE-9.9.9和main分支都是一樣的代碼。
jenkins共享庫代碼:
- jenkins共享庫代碼匯總
service.sh
#!/bin/bash
# sh service.sh anyops-devops-service 1.1.1 8091 start
APPNAME=NULL
VERSION=NULL
PORT=NULL
start(){
port_result=`netstat -anlpt | grep "${PORT}" || echo false`
if [[ $port_result == "false" ]];then
nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
else
stop
sleep 5
nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
fi
}
stop(){
pid=`netstat -anlpt | grep "${PORT}" | awk '{print $NF}' | awk -F '/' '{print $1}' | head -1`
kill -15 $pid
}
check(){
proc_result=`ps aux | grep java | grep "${APPNAME}" | grep -v grep || echo false`
port_result=`netstat -anlpt | grep "${PORT}" || echo false`
url_result=`curl -s http://localhost:${PORT} || echo false `
if [[ $proc_result == "false" || $port_result == "false" || $url_result == "false" ]];then
echo "server not running"
else
echo "ok"
fi
}
case $1 in
start)
start
sleep 5
check
;;
stop)
stop
sleep 5
check
;;
restart)
stop
sleep 5
start
sleep 5
check
;;
check)
check
;;
*)
echo "sh service.sh {start|stop|restart|check}"
;;
esac
Jenkinsfile
@Library("devops06@main") _
//import src/org/devops/xxx.groovy
def checkout = new org.devops.CheckOut()
def build = new org.devops.Build()
def sonar = new org.devops.Sonar()
def artifact = new org.devops.Artifact()
//def gitlab = new org.devops.GitLab()
//使用git 參數(shù)需要格式化
env.branchName = "${env.branchName}" - "origin/"
println(env.branchName)
pipeline {
agent {label "build"}
//跳過默認(rèn)的代碼檢出功能
options {
skipDefaultCheckout true
}
stages{
stage("CheckOut"){
steps{
script{
checkout.CheckOut()
//獲取commitID
env.commitID = checkout.GetCommitID()
println("commitID: ${env.commitID}")
// Jenkins構(gòu)建顯示信息
currentBuild.displayName = "第${BUILD_NUMBER}次構(gòu)建-${env.commitID}"
currentBuild.description = "構(gòu)建分支名稱:${env.branchName}"
//currentBuild.description = "Trigger by user jenkins \n branch: ${env.branchName}"
}
}
}
stage("Build"){
steps{
script{
build.Build()
}
}
}
stage("CodeScan"){
// 是否跳過代碼掃描?
when {
environment name: 'skipSonar', value: 'false'
}
steps{
script{
sonar.SonarScannerByPlugin()
}
}
}
stage("PushArtifact"){
steps{
script{
//PushArtifactByPlugin()
//PushArtifactByPluginPOM()
// init package info
appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service_CI
repoName = appName.split('-')[0] //devops6
appVersion = "${env.branchName}".split("-")[-1] // RELEASE-1.1.1 1.1.1
appVersion = "${appVersion}-${env.commitID}"
targetDir="${appName}/${appVersion}"
// 通過pom文件獲取包名稱
POM = readMavenPom file: 'pom.xml'
env.artifactId = "${POM.artifactId}"
env.packaging = "${POM.packaging}"
env.groupId = "${POM.groupId}"
env.art_version = "${POM.version}"
sourcePkgName = "${env.artifactId}-${env.art_version}.${env.packaging}"
pkgPath = "target"
targetPkgName = "${appName}-${appVersion}.${env.packaging}"
artifact.PushNexusArtifact(repoName, targetDir, pkgPath, sourcePkgName,targetPkgName)
}
}
}
}
}
/*
//通過nexus api上傳制品--綜合實(shí)踐
def PushNexusArtifact(repoId, targetDir, pkgPath, sourcePkgName,targetPkgName){
//nexus api
withCredentials([usernamePassword(credentialsId: '3404937d-89e3-4699-88cf-c4bd299094ad', \
passwordVariable: 'PASSWD',
usernameVariable: 'USERNAME')]) {
sh """
curl -X 'POST' \
"http://172.29.9.101:8081/service/rest/v1/components?repository=${repoId}" \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F "raw.directory=${targetDir}" \
-F "raw.asset1=@${pkgPath}/${sourcePkgName};type=application/java-archive" \
-F "raw.asset1.filename=${targetPkgName}" \
-u ${USERNAME}:${PASSWD}
"""
}
}
*/
cd.jenkinsfile
@Library("devops06@main") _
//import src/org/devops/Gitlab.groovy
def mygit = new org.devops.Gitlab()
def mydeploy = new org.devops.Deploy()
//pipeline
pipeline{
agent { label "build"}
options {
skipDefaultCheckout true
}
stages{
stage("GetArtifact"){
steps{
script{
env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
env.groupName = "${env.projectName}".split('-')[0] //devops6
projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
println(commitID)
appVersion = "${env.branchName}".split("-")[-1] //9.9.9
println(appVersion)
// currentBuild.description = "Version: ${appVersion}-${commitID}"
currentBuild.displayName = "第${BUILD_NUMBER}次構(gòu)建-${commitID}"
currentBuild.description = "構(gòu)建分支名稱:${env.branchName}"
//下載制品
//http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1-b5cfb8ee/devops6-maven-service-6.1.1-b5cfb8ee.jar
repoUrl = "http://172.29.9.101:8081/repository/${env.groupName}"
env.artifactName = "${env.projectName}-${appVersion}-${commitID}.jar"
artifactUrl = "${repoUrl}/${env.projectName}/${appVersion}-${commitID}/${env.artifactName}"
sh "wget --no-verbose ${artifactUrl} && ls -l"
env.releaseVersion = "${appVersion}-${commitID}"
}
}
}
stage("Deploy"){
// 是否跳過發(fā)布?
when {
environment name: 'skipDeploy', value: 'false'
}
steps{
script{
mydeploy.AnsibleDeploy()
}
}
}
stage("RollBack"){
input {
message "是否進(jìn)行回滾?"
ok "Yes"
submitter ""
parameters {
choice choices: ['NO','YES'], name: 'OPS'
}
}
steps {
echo "OPS ${OPS}, doing......."
script{
if ("${OPS}" == "YES"){
mydeploy.AnsibleRollBack()
}
}
}
}
}
}
Deploy.groovy
package org.devops
//rollback
def AnsibleRollBack(){
sh """
# 停止服務(wù)
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh stop" -u root
sleep 20
# 清理和創(chuàng)建發(fā)布目錄
ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
# 將備份目錄內(nèi)容復(fù)制到發(fā)布目錄
ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}.bak/* ${env.targetDir}/${env.projectName}/ || echo file not exists"
# 啟動(dòng)服務(wù)
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
# 檢查服務(wù)
sleep 10
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
"""
}
//發(fā)布制品
def AnsibleDeploy(){
//將主機(jī)寫入清單文件
sh "rm -fr hosts "
for (host in "${env.deployHosts}".split(',')){
sh " echo ${host} >> hosts"
}
// ansible 發(fā)布jar
sh """
# 主機(jī)連通性檢測(cè)
ansible "${env.deployHosts}" -m ping -i hosts
# 創(chuàng)建備份目錄
ansible "${env.deployHosts}" -m shell -a "mkdir -p ${env.targetDir}/${env.projectName}.bak || echo file is exists"
# 備份上次構(gòu)建
ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}/* ${env.targetDir}/${env.projectName}.bak/ || echo file not exists"
# 清理和創(chuàng)建發(fā)布目錄
ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
# 復(fù)制app
ansible "${env.deployHosts}" -m copy -a "src=${env.artifactName} dest=${env.targetDir}/${env.projectName}/${env.artifactName}"
"""
// 發(fā)布腳本
fileData = libraryResource 'scripts/service.sh'
println(fileData)
writeFile file: 'service.sh', text: fileData
sh "ls -a ; cat service.sh "
sh """
# 修改變量
sed -i 's#APPNAME=NULL#APPNAME=${env.projectName}#g' service.sh
sed -i 's#VERSION=NULL#VERSION=${env.releaseVersion}#g' service.sh
sed -i 's#PORT=NULL#PORT=${env.port}#g' service.sh
# 復(fù)制腳本
ansible "${env.deployHosts}" -m copy -a "src=service.sh dest=${env.targetDir}/${env.projectName}/service.sh"
# 啟動(dòng)服務(wù)
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
# 檢查服務(wù)
sleep 10
ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
"""
}
Gitlab.groovy
package org.devops
//發(fā)起HTTP請(qǐng)求
//調(diào)用gitlab api
def HttpReq(method, apiUrl){
withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
response = sh returnStdout: true,
script: """
curl --location --request ${method} \
http://172.29.9.101:8076/api/v4/${apiUrl} \
--header "PRIVATE-TOKEN: ${gitlabtoken}"
"""
}
response = readJSON text: response - "\n" //json數(shù)據(jù)的讀取方式
return response
}
//獲取ProjectID
def GetProjectIDByName(projectName, groupName){
apiUrl = "projects?search=${projectName}"
response = HttpReq("GET", apiUrl)
if (response != []){
for (p in response) {
if (p["namespace"]["name"] == groupName){
return response[0]["id"]
}
}
}
}
//獲取分支CommitID
def GetBranchCommitID(projectID, branchName){
apiUrl = "projects/${projectID}/repository/branches/${branchName}"
response = HttpReq("GET", apiUrl)
return response.commit.short_id
}
- CI頁面參數(shù)如下
- CD頁面參數(shù)
關(guān)于我
我的博客主旨:
- 排版美觀,語言精煉;
- 文檔即手冊(cè),步驟明細(xì),拒絕埋坑,提供源碼;
- 本人實(shí)戰(zhàn)文檔都是親測(cè)成功的,各位小伙伴在實(shí)際操作過程中如有什么疑問,可隨時(shí)聯(lián)系本人幫您解決問題,讓我們一起進(jìn)步!
?? 微信二維碼
x2675263825 (舍得), qq:2675263825。
?? 微信公眾號(hào)
《云原生架構(gòu)師實(shí)戰(zhàn)》
?? 語雀
https://www.yuque.com/xyy-onlyone
?? csdn
https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421
?? 知乎
https://www.zhihu.com/people/foryouone
最后
好了,關(guān)于本次就到這里了,感謝大家閱讀,最后祝大家生活快樂,每天都過的有意義哦,我們下期見!
response = HttpReq(“GET”, apiUrl)
if (response != []){
for (p in response) {
if (p[“namespace”][“name”] == groupName){
return response[0][“id”]
}
}
}
}文章來源:http://www.zghlxwxcb.cn/news/detail-530404.html
//獲取分支CommitID
def GetBranchCommitID(projectID, branchName){
apiUrl = “projects/
p
r
o
j
e
c
t
I
D
/
r
e
p
o
s
i
t
o
r
y
/
b
r
a
n
c
h
e
s
/
{projectID}/repository/branches/
projectID/repository/branches/{branchName}”
response = HttpReq(“GET”, apiUrl)
return response.commit.short_id
}文章來源地址http://www.zghlxwxcb.cn/news/detail-530404.html
- CI頁面參數(shù)如下
[外鏈圖片轉(zhuǎn)存中...(img-QfPAfydf-1688300616371)]
[外鏈圖片轉(zhuǎn)存中...(img-W6JQCH3L-1688300616371)]
[外鏈圖片轉(zhuǎn)存中...(img-JyUYqNZD-1688300616372)]
[外鏈圖片轉(zhuǎn)存中...(img-cDeigiAQ-1688300616372)]
[外鏈圖片轉(zhuǎn)存中...(img-bDEQtVsW-1688300616372)]
[外鏈圖片轉(zhuǎn)存中...(img-fvknHcLW-1688300616372)]
[外鏈圖片轉(zhuǎn)存中...(img-qyBydz95-1688300616373)]
[外鏈圖片轉(zhuǎn)存中...(img-a14KH4NR-1688300616373)]
- CD頁面參數(shù)
[外鏈圖片轉(zhuǎn)存中...(img-1O4fqE3z-1688300616373)]
[外鏈圖片轉(zhuǎn)存中...(img-jjzRnpju-1688300616374)]
[外鏈圖片轉(zhuǎn)存中...(img-ZK2ULdpb-1688300616374)]
[外鏈圖片轉(zhuǎn)存中...(img-mveq9EUt-1688300616374)]
[外鏈圖片轉(zhuǎn)存中...(img-y9sgD3ZV-1688300616375)]
[外鏈圖片轉(zhuǎn)存中...(img-uwlCPq5f-1688300616375)]
[外鏈圖片轉(zhuǎn)存中...(img-Ws9rX59L-1688300616375)]
[外鏈圖片轉(zhuǎn)存中...(img-GtRKkUw5-1688300616375)]
[外鏈圖片轉(zhuǎn)存中...(img-1DUDIHmE-1688300616376)]
## 關(guān)于我
我的博客主旨:
- 排版美觀,語言精煉;
- 文檔即手冊(cè),步驟明細(xì),拒絕埋坑,提供源碼;
- 本人實(shí)戰(zhàn)文檔都是親測(cè)成功的,各位小伙伴在實(shí)際操作過程中如有什么疑問,可隨時(shí)聯(lián)系本人幫您解決問題,讓我們一起進(jìn)步!
?? 微信二維碼
x2675263825 (舍得), qq:2675263825。
[外鏈圖片轉(zhuǎn)存中...(img-p0fEeEL4-1688300616376)]
?? 微信公眾號(hào)
《云原生架構(gòu)師實(shí)戰(zhàn)》
[外鏈圖片轉(zhuǎn)存中...(img-O6MQ006k-1688300616376)]
?? 語雀
https://www.yuque.com/xyy-onlyone
[外鏈圖片轉(zhuǎn)存中...(img-5TROvRge-1688300616377)]
?? csdn
[https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421](https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421)
[外鏈圖片轉(zhuǎn)存中...(img-vUzjbf7l-1688300616379)]
?? 知乎
[https://www.zhihu.com/people/foryouone](https://www.zhihu.com/people/foryouone)
[外鏈圖片轉(zhuǎn)存中...(img-EYtkEa5o-1688300616379)]
## 最后
好了,關(guān)于本次就到這里了,感謝大家閱讀,最后祝大家生活快樂,每天都過的有意義哦,我們下期見!
[外鏈圖片轉(zhuǎn)存中...(img-MUi6bZl1-1688300616379)]
到了這里,關(guān)于實(shí)踐:devops之云主機(jī)模式持續(xù)部署(ci-cd)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!