云主机模式持续部署
云主机模式持续部署

目录
[toc]
推荐文章
https://www.yuque.com/xyy-onlyone/aevhhf?# 《玩转Typora》

0、流程分析
2条Jenkins pipeline


==CI pipeline==


==CD pipeline==


标准规范

==项目规范与总体设计==
公司里面要使用流水线要做持续集成CI/CD的项目越来越多,这对流水线的设计和开发有不同的要求。我们经常听到用户的反馈:
各种不同语言的技术栈, 如何使流水线适配呢? 从不同技术栈维护一套流水线模版,到我们使用共享库进行统一的管理和维护。
对于不同的项目,大家管理代码的方式也不同。可能还有一部分用户在使用Svn等不同的版本控制系统。
不同的项目,开发模式也不太一样, 编译构建工具不同,发布的方式也有不同的地方…
等等,不止上面的问题。所以在做流水线的使用应该提前把项目团队的规范定义好, 这样后期项目改造后可以直接集成CI/CD流水线。更加便捷。
1.团队信息
| 信息项 | 描述 |
|---|---|
| 业务简称/编号 | devops4 |
| 开发模式 | 特性分支开发,版本分支发布,主干分支作为最新代码 |
| 项目类型与构建方式 | 前端: vue项目, npm打包, 制品目录 dist 后端:springboot项目, maven打包, 制品目录 target |
| 发布主机环境(vm) | LB: 192.168.1.200 Server: 192.168.1.121~192.168.1.122 |
2.CI/CD规范
通过上面的信息,我们采用如下规范:
| 工具链 | |
|---|---|
| GitLab 代码库 | 仓库组: devops4 项目仓库后端 devops4-ops-service 前端 devops4-ops-ui |
| Jenkins作业 | 文件夹: devops4 |
| 作业命名: 后端 devops4-ops-service 前端 devops4-ops-ui | |
| CI构建规范 | 前端项目采用npm打包后统一放到dist目录下, 静态文件以tgz打包。 |
| 后端项目采用maven打包后统一放到target目录下,以jar包。 | |
| Sonar代码报告 | 前端项目:devops4-ops-ui 后端项目:devops4-ops-service |
| 项目团队可以使用devops4命名的自定义质量规则和质量阈。 | |
| Nexus制品库目录 | devops4-ops-service/version/devops4-ops-service-version.jar |
| devops4-ops-ui/version/devops4-ops-ui-version.tar.gz | |
| devops4 | 版本: 分割release分支获取版本号 |
| 发布规范 | 用户输入版本,下载制品库,使用脚本启动服务。 |
标准化
版本分支命名:RELEASE-1.1.1
分支策略
特性分支开发,版本分支发布。

环境管理
使用virtualbox创建2台虚拟机, 或者采用terraform操作云平台创建2台虚机。
本次,自己使用2台本地vmwareworkstation虚机测试。

制品管理
制品版本命名:版本号-CommitID

发布流水线
Jenkins pipeline * 2
CI pipeline
CD pipeline
- 复选框参数: 发布主机
- 字符参数:版本分支
- 选项参数:目标环境 【dev/uat/stg/prod】

应用发布与回滚策略
Deploy发布策略

蓝绿发布
环境存在两个版本,蓝版本和绿版本同时存在,部署新版本然后进行测试,将流量切到新版本,最终实际运行的只有一个版本(蓝/绿)。好处是无需停机,并且发布风险较小。

nginx upstream模块实现:
1upstream webservers {
2 server 192.168.1.253:8099 weight=100;
3 server 192.168.1.252:8099 down;
4}
5
6server {
7 listen 8017;
8 location / {
9 proxy_pass http://webservers;
10 }
11}
12
13nginx -s reload
灰度发布
将发行版发布到一部分用户或服务器的一种模式。这个想法是首先将更改部署到一小部分服务器,进行测试,然后将更改推广到其余服务器。一旦通过所有运行状况检查,当没有问题时,所有的客户将被路由到该应用程序的新版本,而旧版本将被删除。


nginx 权重模拟:
1upstream webservers {
2 server 192.168.1.223:8099 weight=100;
3 server 192.168.1.222:8099 weight=100;
4 server 192.168.1.221:8099 weight=100;
5}
6
7server {
8 listen 8017;
9 location / {
10 proxy_pass http://webservers;
11 }
12}
13
14nginx -s reload
版本回滚
- 版本一直升级,则无需回滚。
- 选择旧版本文件,进行发布。
前端后端项目发布
1、前端项目
复制静态文件到nginx站点目录,nginx -s reload
1## 进入Web服务器的站点目录下
2
3## 下载包
4[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
5 % Total % Received % Xferd Average Speed Time Time Time Current
6 Dload Upload Total Spent Left Speed
7100 196k 100 196k 0 0 24.0M 0 --:--:-- --:--:-- --:--:-- 24.0M
8
9## 解压包
10[root@master html]# tar zxf anyops-devops-ui-1.1.1.tar.gz
11[root@master html]# ls
12anyops-devops-ui-1.1.1.tar.gz index.html static
13
14## 触发nginx重载
15[root@master html]# nginx -s reload

2、后端项目
- 复制jar包到目标目录, 使用nohup java -jar 启动服务。
- nohup java -jar app.jar >output 2>&1 &

1、CI
拷贝Jenkins流水线
- 拷贝Jenkins作业
devops6-maven-service到devops6-maven-service_CI

保存后,点击参数化构建,会发现branchName的页面参数为空,我们先直接运行一次流水线看看效果:
运行一次流水线后,再次运行时,就会看到branchName正常了。
接下来我们就用devops6-maven-service_CI来测试。

- 我们再次运行下,看下效果

可以看到,流水线运行成功。


可以看到nexus仓库里制品被上传成功了。
优化pipeline代码,去除制品库里CI字样

1 appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service_CI
2 repoName = appName.split('-')[0] //devops6
3 appVersion = "${env.branchName}".split("-")[-1] // RELEASE-1.1.1 1.1.1
4 targetDir="${appName}/${appVersion}"
再次运行测试效果:


符合预期。
新建Jenkins CD流水线
- 创建一个
devops6-maven-service_CD作业,然后添加一些页面参数




点击参数化构建:

创建一个devops6的视图



优化pipeline代码,nexus仓库的版本里要带上commitID
- 之前仓库是这样的

- 先来手动获取下项目仓库的commitID

1[root@Devops6 ~]#cd /opt/jenkinsagent/workspace/
2[root@Devops6 workspace]#ls
3day2-pipeline-demo devops6-gradle-service devops6-maven-service devops6-maven-service_CI@tmp devops6-maven-test devops6-npm-service test-maven
4day2-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
5[root@Devops6 workspace]#cd devops6-maven-service_CI
6[root@Devops6 devops6-maven-service_CI]#ls
7mvnw mvnw.cmd pom.xml sonar-project.properties src target
8[root@Devops6 devops6-maven-service_CI]#git rev-parse HEAD #通过这个命令之可以获取仓库comitID的。
9b5cfb8eeee597edd752cb11f5daa9ac843fb9f97
10[root@Devops6 devops6-maven-service_CI]#
然后利用片段生成器生成代码:

1sh returnStdout: true, script: 'git rev-parse HEAD'
然后集成到piepeline代码里。
- 我们想让这里的版本号也带上commitID
这里直接写代码:
1appVersion = "${appVersion}-${env.commitID}"
2
3//获取commitID
4env.commitID = gitlab.GetCommitID()
5println("commitID: ${env.commitID}")
6
7
8package org.devops
9
10//获取CommitID
11def GetCommitID(){
12 ID = sh returnStdout: true, script:"git rev-parse HEAD"
13 ID = ID -"\n"
14 return ID[0..7] //取前8位id
15}



- 在gitlab的
devops6-maven-service里以main分支创建RELEASE-9.9.9分支

- 运行
devops6-maven-service_CI流水线





测试成功。
2、CD
下载制品
cd部分就不用再下载代码获取commitID了。
我们来使用gitlab api获取分支commit。
Step1: 获取GitLab 分支CommitID
- 打开gitlab api官方文档


1curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches/main"
- 在postman里调试
先拿取下一些参数:
这里拿到Project ID:

然后在gitlab上创建一个token:


TLDgj3sz-cioyk6AfxZi
调试:
1http://172.29.9.101:8076/api/v4/projects/7/repository/branches/RELEASE-9.9.9
添加get请求,添加PRIVARE-TOKEN,点击Send。

此时,我们就通过gitalb api拿到了分支commitID了,和之前手动执行git命令获取的commitID信息一致。


- 此时拿到postman给出的cURL命令

1curl --location 'http://172.29.9.101:8076/api/v4/projects/7/repository/branches/RELEASE-9.9.9' \
2--header 'PRIVATE-TOKEN: TLDgj3sz-cioyk6AfxZi'
- 优化pipeline代码
创建Gitlab.groovy文件
1package org.devops
2
3//发起HTTP请求
4def HttpReq(method, apiUrl){
5 response = sh returnStdout: true,
6 script: """
7 curl --location --request ${method} \
8 http:172.29.9.101:8076/api/v4/${apiUrl} \
9 --header "PRIVATE-TOKEN: TLDgj3sz-cioyk6AfxZi"
10 """
11 response = readJSON text: response - "\n" //json数据的读取方式
12 return response
13}

但是,这里的gitlab token是明文的,因此需要在jenkins里配置个凭据。



然后利用片段生成器来利用次token,生成代码:

1withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
2 // some block
3}
- 优化pipeline代码
1package org.devops
2
3//发起HTTP请求
4def HttpReq(method, apiUrl){
5
6 withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
7 response = sh returnStdout: true,
8 script: """
9 curl --location --request ${method} \
10 http:172.29.9.101:8076/api/v4/${apiUrl} \
11 --header "PRIVATE-TOKEN: ${gitlabtoken}"
12 """
13 }
14 response = readJSON text: response - "\n" //json数据的读取方式
15 return response
16}

但是,存在一个问题,apiUrl里我们还需要知道ProjectID才行,这里继续查找gitlab api。
- 获取ProjectID


1http://172.29.9.101:8076/api/v4/projects?search=devops6-maven-service
2
3curl --location 'http://172.29.9.101:8076/api/v4/projects?search=devops6-maven-service' \
4--header 'PRIVATE-TOKEN: TLDgj3sz-cioyk6AfxZi'

- 继续优化pipeline代码
1package org.devops
2
3//发起HTTP请求
4def HttpReq(method, apiUrl){
5
6 withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
7 response = sh returnStdout: true,
8 script: """
9 curl --location --request ${method} \
10 http:172.29.9.101:8076/api/v4/${apiUrl} \
11 --header "PRIVATE-TOKEN: ${gitlabtoken}"
12 """
13 }
14 response = readJSON text: response - "\n" //json数据的读取方式
15 return response
16}
17
18//获取ProjectID
19def GetProjectIDByName(projectName, groupName){
20 apiUrl = "projects?search=${projectName}"
21 response = HttpReq("GET", apiUrl)
22 if (response != []){
23 for (p in response) {
24 if (p["namespace"]["name"] == groupName){
25 return response[0]["id"]
26 }
27 }
28 }
29}
30
31//获取分支CommitID
32def GetBranchCommitID(projectID, branchName){
33 apiUrl = "projects/${projectID}/repository/branches/${branchName}"
34 response = HttpReq("GET", apiUrl)
35 return response.commit.short_id
36}
- 创建
cd.jenkinsfile

1@Library("devops06@main") _
2
3//import src/org/devops/Gitlab.groovy
4def mygit = new org.devops.Gitlab()
5
6
7//pipeline
8pipeline{
9 agent { label "build"}
10 options {
11 skipDefaultCheckout true
12 }
13 stages{
14 stage("GetArtifact"){
15 steps{
16 script{
17 env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
18 env.groupName = "${env.projectName}".split('-')[0] //devops6
19
20 projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
21 commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
22 println(commitID)
23
24 // appVersion = "${env.branchName}".split("-")[-1] //9.9.9
25 // println(appVersion)
26 // currentBuild.description = "Version: ${appVersion}-${commitID}"
27
28 currentBuild.displayName = "第${BUILD_NUMBER}次构建-${commitID}"
29 currentBuild.description = "构建分支名称:${env.branchName}"
30
31 }
32 }
33 }
34
35 }
36}
Gitlab.groovy代码
1package org.devops
2
3//发起HTTP请求
4def HttpReq(method, apiUrl){
5
6 withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
7 response = sh returnStdout: true,
8 script: """
9 curl --location --request ${method} \
10 http://172.29.9.101:8076/api/v4/${apiUrl} \
11 --header "PRIVATE-TOKEN: ${gitlabtoken}"
12 """
13 }
14 response = readJSON text: response - "\n" //json数据的读取方式
15 return response
16}
17
18//获取ProjectID
19def GetProjectIDByName(projectName, groupName){
20 apiUrl = "projects?search=${projectName}"
21 response = HttpReq("GET", apiUrl)
22 if (response != []){
23 for (p in response) {
24 if (p["namespace"]["name"] == groupName){
25 return response[0]["id"]
26 }
27 }
28 }
29}
30
31//获取分支CommitID
32def GetBranchCommitID(projectID, branchName){
33 apiUrl = "projects/${projectID}/repository/branches/${branchName}"
34 response = HttpReq("GET", apiUrl)
35 return response.commit.short_id
36}
- 编辑
devops6-maven-service_CD流水线使用共享库


运行流水线:


测试成功。😘
Step2: 下载制品
- nexus仓库制品地址如下
1http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1/devops6-maven-service-6.1.1.jar
- 这里编写pipeline代码
1@Library("devops06@main") _
2
3//import src/org/devops/Gitlab.groovy
4def mygit = new org.devops.Gitlab()
5
6
7//pipeline
8pipeline{
9 agent { label "build"}
10 options {
11 skipDefaultCheckout true
12 }
13 stages{
14 stage("GetArtifact"){
15 steps{
16 script{
17 env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
18 env.groupName = "${env.projectName}".split('-')[0] //devops6
19
20 projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
21 commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
22 println(commitID)
23
24 appVersion = "${env.branchName}".split("-")[-1] //9.9.9
25 println(appVersion)
26 // currentBuild.description = "Version: ${appVersion}-${commitID}"
27
28 currentBuild.displayName = "第${BUILD_NUMBER}次构建-${commitID}"
29 currentBuild.description = "构建分支名称:${env.branchName}"
30
31
32 //下载制品
33 //http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1-b5cfb8ee/devops6-maven-service-6.1.1-b5cfb8ee.jar
34 repoUrl = "http://172.29.9.101:8081/repository/${env.groupName}"
35 artifactName = "${env.projectName}-${appVersion}-${commitID}.jar"
36 artifactUrl = "${repoUrl}/${env.projectName}/${appVersion}-${commitID}/${artifactName}"
37 sh "wget --no-verbose ${artifactUrl} && ls -l"
38
39 }
40 }
41 }
42
43 }
44}

- 运行观察效果


下载制品成功。
我们再运行一次流水线:

会看到多了一个包,
最后我们发布完,会把它清掉的:
这里先手动给清掉。

Step3: 发布
准备2台linux机器
1devops-deploy1-172.29.9.110
2devops-deploy2-172.29.9.111
- 给这2台机器装好
java-11
1yum install -y java-11-openjdk.x86_64
devops机器安装ansible环境
1yum install epel-release -y
2yum install ansible -y
- 编辑下ansible的主机管理文件:
1[root@Devops6 ~]#vim /etc/ansible/hosts
2172.29.9.110
3172.29.9.111
- 给ansible机器到2个节点做个免密
1ssh-keygen
2ssh-copy-id -i ~/.ssh/id_rsa.pub root@172.29.9.110
3ssh-copy-id -i ~/.ssh/id_rsa.pub root@172.29.9.111
- 查看当前主机是否在线:
1[root@Devops6 ~]#ansible all -m ping -u root

构建一次devops6-maven-service_CD,下载制品


我们先来手动发布一次,再集成到CD流水线里
- 拷贝制品到deploy1
1[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"


- 启动服务:


- 用准备好的服务启动脚本来启动/停止java服务
服务启动脚本:service.sh (原始脚本如下)
1#!/bin/bash
2
3# sh service.sh anyops-devops-service 1.1.1 8091 start
4APPNAME=NULL
5VERSION=NULL
6PORT=NULL
7
8start(){
9 port_result=`netstat -anlpt | grep "${PORT}" || echo false`
10
11 if [[ $port_result == "false" ]];then
12 nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
13 else
14 stop
15 sleep 5
16 nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
17 fi
18}
19
20
21stop(){
22 pid=`netstat -anlpt | grep "${PORT}" | awk '{print $NF}' | awk -F '/' '{print $1}' | head -1`
23 kill -15 $pid
24}
25
26
27check(){
28 proc_result=`ps aux | grep java | grep "${APPNAME}" | grep -v grep || echo false`
29 port_result=`netstat -anlpt | grep "${PORT}" || echo false`
30 url_result=`curl -s http://localhost:${PORT} || echo false `
31
32 if [[ $proc_result == "false" || $port_result == "false" || $url_result == "false" ]];then
33 echo "server not running"
34 else
35 echo "ok"
36 fi
37}
38
39case $1 in
40 start)
41 start
42 sleep 5
43 check
44 ;;
45
46 stop)
47 stop
48 sleep 5
49 check
50 ;;
51 restart)
52 stop
53 sleep 5
54 start
55 sleep 5
56 check
57 ;;
58 check)
59 check
60 ;;
61 *)
62 echo "sh service.sh {start|stop|restart|check}"
63 ;;
64esac
参数写入后脚本如下
1#!/bin/bash
2
3# sh service.sh anyops-devops-service 1.1.1 8091 start
4APPNAME=devops6-maven-service
5VERSION=9.9.9-b5cfb8ee
6PORT=8080
7
8start(){
9 port_result=`netstat -anlpt | grep "${PORT}" || echo false`
10
11 if [[ $port_result == "false" ]];then
12 nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
13 else
14 stop
15 sleep 5
16 nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
17 fi
18}
19
20
21stop(){
22 pid=`netstat -anlpt | grep "${PORT}" | awk '{print $NF}' | awk -F '/' '{print $1}' | head -1`
23 kill -15 $pid
24}
25
26
27check(){
28 proc_result=`ps aux | grep java | grep "${APPNAME}" | grep -v grep || echo false`
29 port_result=`netstat -anlpt | grep "${PORT}" || echo false`
30 url_result=`curl -s http://localhost:${PORT} || echo false `
31
32 if [[ $proc_result == "false" || $port_result == "false" || $url_result == "false" ]];then
33 echo "server not running"
34 else
35 echo "ok"
36 fi
37}
38
39case $1 in
40 start)
41 start
42 sleep 5
43 check
44 ;;
45
46 stop)
47 stop
48 sleep 5
49 check
50 ;;
51 restart)
52 stop
53 sleep 5
54 start
55 sleep 5
56 check
57 ;;
58 check)
59 check
60 ;;
61 *)
62 echo "sh service.sh {start|stop|restart|check}"
63 ;;
64esac
- 将
service.sh脚本拷贝到测试节点:
1[root@Devops6 devops6-maven-service_CD]#ansible 172.29.9.110 -m copy -a "src=service.sh dest=/opt/service.sh"
2172.29.9.110 | CHANGED => {
3 "ansible_facts": {
4 "discovered_interpreter_python": "/usr/bin/python"
5 },
6 "changed": true,
7 "checksum": "666b4746afbb9fa684f79a89102715906417c848",
8 "dest": "/opt/service.sh",
9 "gid": 0,
10 "group": "root",
11 "md5sum": "22868400cb2784f7c7bcf63f38a977fe",
12 "mode": "0644",
13 "owner": "root",
14 "size": 1367,
15 "src": "/root/.ansible/tmp/ansible-tmp-1688219597.85-41901-255991227715874/source",
16 "state": "file",
17 "uid": 0
18}
然后启动程序:
给予脚本执行权限:
1[root@devops-deploy1 opt]#ll
2total 17284
3-rw-r--r-- 1 root root 17690913 Jul 1 20:12 devops6-maven-service-9.9.9-b5cfb8ee.jar
4-rw-r--r-- 1 root root 1367 Jul 1 21:53 service.sh
5[root@devops-deploy1 opt]#chmod +x service.sh
启动程序:
1[root@devops-deploy1 opt]#sh service.sh start
2ok
3[root@devops-deploy1 opt]#ps -aux|grep java
4root 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
5root 7674 0.0 0.0 112708 972 pts/0 R+ 21:55 0:00 grep --color=auto java
6[root@devops-deploy1 opt]#

开始集成
- 最终代码如下

Deploy.groovy文件
1package org.devops
2
3//rollback
4def AnsibleRollBack(){
5
6 sh """
7 # 停止服务
8 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh stop" -u root
9
10 sleep 300
11 # 清理和创建发布目录
12 ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
13
14 # 将备份目录内容复制到发布目录
15 ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}.bak/* ${env.targetDir}/${env.projectName}/ || echo file not exists"
16
17 # 启动服务
18 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
19
20 # 检查服务
21 sleep 10
22 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
23
24 """
25}
26
27
28//发布制品
29def AnsibleDeploy(){
30 //将主机写入清单文件
31 sh "rm -fr hosts "
32 for (host in "${env.deployHosts}".split(',')){
33 sh " echo ${host} >> hosts"
34 }
35
36
37 // ansible 发布jar
38 sh """
39 # 主机连通性检测
40 ansible "${env.deployHosts}" -m ping -i hosts
41
42 # 创建备份目录
43 ansible "${env.deployHosts}" -m shell -a "mkdir -p ${env.targetDir}/${env.projectName}.bak || echo file is exists"
44 # 备份上次构建
45 ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}/* ${env.targetDir}/${env.projectName}.bak/ || echo file not exists"
46
47 # 清理和创建发布目录
48 ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
49 # 复制app
50 ansible "${env.deployHosts}" -m copy -a "src=${env.artifactName} dest=${env.targetDir}/${env.projectName}/${env.artifactName}"
51 """
52
53 // 发布脚本
54 fileData = libraryResource 'scripts/service.sh'
55 println(fileData)
56 writeFile file: 'service.sh', text: fileData
57 sh "ls -a ; cat service.sh "
58
59
60 sh """
61 # 修改变量
62 sed -i 's#APPNAME=NULL#APPNAME=${env.projectName}#g' service.sh
63 sed -i 's#VERSION=NULL#VERSION=${env.releaseVersion}#g' service.sh
64 sed -i 's#PORT=NULL#PORT=${env.port}#g' service.sh
65
66 # 复制脚本
67 ansible "${env.deployHosts}" -m copy -a "src=service.sh dest=${env.targetDir}/${env.projectName}/service.sh"
68 # 启动服务
69 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
70
71 # 检查服务
72 sleep 10
73 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
74 """
75}
cd.jenkinsfile文件
1@Library("devops06@main") _
2
3//import src/org/devops/Gitlab.groovy
4def mygit = new org.devops.Gitlab()
5def mydeploy = new org.devops.Deploy()
6
7
8//pipeline
9pipeline{
10 agent { label "build"}
11 options {
12 skipDefaultCheckout true
13 }
14 stages{
15 stage("GetArtifact"){
16 steps{
17 script{
18 env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
19 env.groupName = "${env.projectName}".split('-')[0] //devops6
20
21 projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
22 commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
23 println(commitID)
24
25 appVersion = "${env.branchName}".split("-")[-1] //9.9.9
26 println(appVersion)
27 // currentBuild.description = "Version: ${appVersion}-${commitID}"
28
29 currentBuild.displayName = "第${BUILD_NUMBER}次构建-${commitID}"
30 currentBuild.description = "构建分支名称:${env.branchName}"
31
32
33 //下载制品
34 //http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1-b5cfb8ee/devops6-maven-service-6.1.1-b5cfb8ee.jar
35 repoUrl = "http://172.29.9.101:8081/repository/${env.groupName}"
36 env.artifactName = "${env.projectName}-${appVersion}-${commitID}.jar"
37 artifactUrl = "${repoUrl}/${env.projectName}/${appVersion}-${commitID}/${env.artifactName}"
38 sh "wget --no-verbose ${artifactUrl} && ls -l"
39
40 env.releaseVersion = "${appVersion}-${commitID}"
41
42 }
43 }
44 }
45
46 stage("Deploy"){
47 steps{
48 script{
49 mydeploy.AnsibleDeploy()
50 }
51 }
52 }
53
54 stage("RollBack"){
55 input {
56 message "是否进行回滚?"
57 ok "Yes"
58 submitter ""
59 parameters {
60 choice choices: ['NO','YES'], name: 'OPS'
61 }
62 }
63 steps {
64 echo "OPS ${OPS}, doing......."
65
66 script{
67 if ("${OPS}" == "YES"){
68 mydeploy.AnsibleRollBack()
69 }
70 }
71
72 }
73 }
74
75
76
77 }
78
79
80}
- 测试效果
执行CD流水线:

运行成功:

再看下2个节点的java运行情况:

符合预期。
给gitlab上devops6-maven-service项目配置个健康检查端口
- 默认这个生成的jar包启动后,是没配置健康检查端口的,我们的测试现象不明确

我们来启动下服务:


- 因此我们来改下这个java代码:

BasicController.java
1/*
2 * Copyright 2013-2018 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.example.demo;
18
19import org.springframework.stereotype.Controller;
20import org.springframework.web.bind.annotation.ModelAttribute;
21import org.springframework.web.bind.annotation.RequestMapping;
22import org.springframework.web.bind.annotation.RequestParam;
23import org.springframework.web.bind.annotation.ResponseBody;
24
25/**
26 * @author <a href="mailto:chenxilzx1@gmail.com">theonefx</a>
27 */
28@Controller
29public class BasicController {
30
31 // http://127.0.0.1:8080/hello?name=lisi
32 @RequestMapping("/hello")
33 @ResponseBody
34 public String hello(@RequestParam(name = "name", defaultValue = "xyy") String name) {
35 return "Hello RELEASE-10.1.0 " + name;
36 }
37}
然后打包,运行,观察效果:


最后,将
devops6-maven-service的RELEASE-9.9.9/代码合并到main分支。打上tag


Step4: 回滚

推荐第一种。
第二种方法会存在很多逻辑问题的。
- 回滚代码见上述文件,这里测试下效果
1、直接发布版本方式来回滚
先运行CI流水线

CI pipeline运行成功:


再运行CD:

观察效果:


可以看到发布老版本程序成功。
2、使用回滚代码
注意:如果要回滚时,就需要跳过发布阶段,否则会有问题的,因此这里我给发布阶段加了一个判断选项。
发布1.1.1

发布9.9.9

回滚到1.1.1:



符合预期。😘
扩展:参数动态获取实践
- 需要安装active choices插件重启Jenkins服务器后再操作。

根据不同的环境带出不同的机器
- 效果


- envName参数设置

1return ["dev", "uat", "stag", "prod"]
- deployHosts参数设置

1if (envName.equals("dev")){
2 return ["172.29.9.110,172.29.9.111"]
3} else if (envName.equals("uat")){
4 return ["172.29.9.120,172.29.9.121"]
5}

⚠️ 注意:记得删除前面定义好的envName和deployHosts选项参数。
- 运行测试



符合预期。😘
根据不同发布工具,动态展示主机参数
这个就不做演示了,和上面这个实践有冲突。

- 定义发布工具参数

1return ["ansible", "saltstack"]
单选类型

- 定义发布主机

选择关联的参数,多个参数用逗号分割

3、代码汇总
- 本次实验代码
链接:https://pan.baidu.com/s/1mn1EX2oX0XRGO-IjohkyLA?pwd=0820 提取码:0820
2023.7.2-云主机模式持续部署-ci-cd-code

- 实验环境
1gitlab-ce:15.0.3-ce.0
2jenkins:2.346.3-2-lts-jdk11
3sonarqube:9.9.0-community
4nexus3:3.53.0

- ci-cd流水线
这2条流水线都是测试ok的。(以后就一直用这2条流水线来测试devops了)



- 仓库代码
gitlab仓库devops6-maven-service:RELEASE-9.9.9和main分支都是一样的代码。

jenkins共享库代码:

- jenkins共享库代码汇总

service.sh
1#!/bin/bash
2
3# sh service.sh anyops-devops-service 1.1.1 8091 start
4APPNAME=NULL
5VERSION=NULL
6PORT=NULL
7
8start(){
9 port_result=`netstat -anlpt | grep "${PORT}" || echo false`
10
11 if [[ $port_result == "false" ]];then
12 nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
13 else
14 stop
15 sleep 5
16 nohup java -jar -Dserver.port=${PORT} ${APPNAME}-${VERSION}.jar >${APPNAME}.log.txt 2>&1 &
17 fi
18}
19
20
21stop(){
22 pid=`netstat -anlpt | grep "${PORT}" | awk '{print $NF}' | awk -F '/' '{print $1}' | head -1`
23 kill -15 $pid
24}
25
26
27check(){
28 proc_result=`ps aux | grep java | grep "${APPNAME}" | grep -v grep || echo false`
29 port_result=`netstat -anlpt | grep "${PORT}" || echo false`
30 url_result=`curl -s http://localhost:${PORT} || echo false `
31
32 if [[ $proc_result == "false" || $port_result == "false" || $url_result == "false" ]];then
33 echo "server not running"
34 else
35 echo "ok"
36 fi
37}
38
39case $1 in
40 start)
41 start
42 sleep 5
43 check
44 ;;
45
46 stop)
47 stop
48 sleep 5
49 check
50 ;;
51 restart)
52 stop
53 sleep 5
54 start
55 sleep 5
56 check
57 ;;
58 check)
59 check
60 ;;
61 *)
62 echo "sh service.sh {start|stop|restart|check}"
63 ;;
64esac
Jenkinsfile
1@Library("devops06@main") _
2
3//import src/org/devops/xxx.groovy
4def checkout = new org.devops.CheckOut()
5def build = new org.devops.Build()
6def sonar = new org.devops.Sonar()
7def artifact = new org.devops.Artifact()
8//def gitlab = new org.devops.GitLab()
9
10
11//使用git 参数需要格式化
12env.branchName = "${env.branchName}" - "origin/"
13println(env.branchName)
14
15pipeline {
16 agent {label "build"}
17
18 //跳过默认的代码检出功能
19 options {
20 skipDefaultCheckout true
21 }
22
23
24 stages{
25 stage("CheckOut"){
26 steps{
27 script{
28 checkout.CheckOut()
29
30 //获取commitID
31 env.commitID = checkout.GetCommitID()
32 println("commitID: ${env.commitID}")
33
34 // Jenkins构建显示信息
35 currentBuild.displayName = "第${BUILD_NUMBER}次构建-${env.commitID}"
36 currentBuild.description = "构建分支名称:${env.branchName}"
37 //currentBuild.description = "Trigger by user jenkins \n branch: ${env.branchName}"
38 }
39 }
40 }
41
42 stage("Build"){
43 steps{
44 script{
45 build.Build()
46 }
47 }
48
49 }
50
51 stage("CodeScan"){
52 // 是否跳过代码扫描?
53 when {
54 environment name: 'skipSonar', value: 'false'
55 }
56
57 steps{
58 script{
59 sonar.SonarScannerByPlugin()
60
61 }
62 }
63 }
64
65 stage("PushArtifact"){
66 steps{
67 script{
68 //PushArtifactByPlugin()
69 //PushArtifactByPluginPOM()
70
71 // init package info
72 appName = "${JOB_NAME}".split('_')[0] //devops6-maven-service_CI
73 repoName = appName.split('-')[0] //devops6
74 appVersion = "${env.branchName}".split("-")[-1] // RELEASE-1.1.1 1.1.1
75 appVersion = "${appVersion}-${env.commitID}"
76 targetDir="${appName}/${appVersion}"
77
78
79 // 通过pom文件获取包名称
80 POM = readMavenPom file: 'pom.xml'
81 env.artifactId = "${POM.artifactId}"
82 env.packaging = "${POM.packaging}"
83 env.groupId = "${POM.groupId}"
84 env.art_version = "${POM.version}"
85 sourcePkgName = "${env.artifactId}-${env.art_version}.${env.packaging}"
86
87 pkgPath = "target"
88 targetPkgName = "${appName}-${appVersion}.${env.packaging}"
89 artifact.PushNexusArtifact(repoName, targetDir, pkgPath, sourcePkgName,targetPkgName)
90 }
91 }
92
93 }
94
95 }
96}
97
98/*
99//通过nexus api上传制品--综合实践
100def PushNexusArtifact(repoId, targetDir, pkgPath, sourcePkgName,targetPkgName){
101 //nexus api
102 withCredentials([usernamePassword(credentialsId: '3404937d-89e3-4699-88cf-c4bd299094ad', \
103 passwordVariable: 'PASSWD',
104 usernameVariable: 'USERNAME')]) {
105 sh """
106 curl -X 'POST' \
107 "http://172.29.9.101:8081/service/rest/v1/components?repository=${repoId}" \
108 -H 'accept: application/json' \
109 -H 'Content-Type: multipart/form-data' \
110 -F "raw.directory=${targetDir}" \
111 -F "raw.asset1=@${pkgPath}/${sourcePkgName};type=application/java-archive" \
112 -F "raw.asset1.filename=${targetPkgName}" \
113 -u ${USERNAME}:${PASSWD}
114 """
115 }
116}
117*/
cd.jenkinsfile
1@Library("devops06@main") _
2
3//import src/org/devops/Gitlab.groovy
4def mygit = new org.devops.Gitlab()
5def mydeploy = new org.devops.Deploy()
6
7
8//pipeline
9pipeline{
10 agent { label "build"}
11 options {
12 skipDefaultCheckout true
13 }
14 stages{
15 stage("GetArtifact"){
16 steps{
17 script{
18 env.projectName = "${JOB_NAME}".split('_')[0] //devops6-maven-service
19 env.groupName = "${env.projectName}".split('-')[0] //devops6
20
21 projectID = mygit.GetProjectIDByName(env.projectName, env.groupName)
22 commitID = mygit.GetBranchCommitID("${projectID}", "${env.branchName}")
23 println(commitID)
24
25 appVersion = "${env.branchName}".split("-")[-1] //9.9.9
26 println(appVersion)
27 // currentBuild.description = "Version: ${appVersion}-${commitID}"
28
29 currentBuild.displayName = "第${BUILD_NUMBER}次构建-${commitID}"
30 currentBuild.description = "构建分支名称:${env.branchName}"
31
32
33 //下载制品
34 //http://172.29.9.101:8081/repository/devops6/devops6-maven-service/6.1.1-b5cfb8ee/devops6-maven-service-6.1.1-b5cfb8ee.jar
35 repoUrl = "http://172.29.9.101:8081/repository/${env.groupName}"
36 env.artifactName = "${env.projectName}-${appVersion}-${commitID}.jar"
37 artifactUrl = "${repoUrl}/${env.projectName}/${appVersion}-${commitID}/${env.artifactName}"
38 sh "wget --no-verbose ${artifactUrl} && ls -l"
39
40 env.releaseVersion = "${appVersion}-${commitID}"
41
42 }
43 }
44 }
45
46 stage("Deploy"){
47
48 // 是否跳过发布?
49 when {
50 environment name: 'skipDeploy', value: 'false'
51 }
52
53 steps{
54 script{
55 mydeploy.AnsibleDeploy()
56 }
57 }
58 }
59
60 stage("RollBack"){
61 input {
62 message "是否进行回滚?"
63 ok "Yes"
64 submitter ""
65 parameters {
66 choice choices: ['NO','YES'], name: 'OPS'
67 }
68 }
69 steps {
70 echo "OPS ${OPS}, doing......."
71
72 script{
73 if ("${OPS}" == "YES"){
74 mydeploy.AnsibleRollBack()
75 }
76 }
77
78 }
79 }
80
81
82
83 }
84
85
86}
Deploy.groovy
1package org.devops
2
3//rollback
4def AnsibleRollBack(){
5
6 sh """
7 # 停止服务
8 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh stop" -u root
9
10 sleep 20
11 # 清理和创建发布目录
12 ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
13
14 # 将备份目录内容复制到发布目录
15 ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}.bak/* ${env.targetDir}/${env.projectName}/ || echo file not exists"
16
17 # 启动服务
18 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
19
20 # 检查服务
21 sleep 10
22 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
23
24 """
25}
26
27
28//发布制品
29def AnsibleDeploy(){
30 //将主机写入清单文件
31 sh "rm -fr hosts "
32 for (host in "${env.deployHosts}".split(',')){
33 sh " echo ${host} >> hosts"
34 }
35
36
37 // ansible 发布jar
38 sh """
39 # 主机连通性检测
40 ansible "${env.deployHosts}" -m ping -i hosts
41
42 # 创建备份目录
43 ansible "${env.deployHosts}" -m shell -a "mkdir -p ${env.targetDir}/${env.projectName}.bak || echo file is exists"
44 # 备份上次构建
45 ansible "${env.deployHosts}" -m shell -a " mv ${env.targetDir}/${env.projectName}/* ${env.targetDir}/${env.projectName}.bak/ || echo file not exists"
46
47 # 清理和创建发布目录
48 ansible "${env.deployHosts}" -m shell -a "rm -fr ${env.targetDir}/${env.projectName}/* && mkdir -p ${env.targetDir}/${env.projectName} || echo file is exists"
49 # 复制app
50 ansible "${env.deployHosts}" -m copy -a "src=${env.artifactName} dest=${env.targetDir}/${env.projectName}/${env.artifactName}"
51 """
52
53 // 发布脚本
54 fileData = libraryResource 'scripts/service.sh'
55 println(fileData)
56 writeFile file: 'service.sh', text: fileData
57 sh "ls -a ; cat service.sh "
58
59
60 sh """
61 # 修改变量
62 sed -i 's#APPNAME=NULL#APPNAME=${env.projectName}#g' service.sh
63 sed -i 's#VERSION=NULL#VERSION=${env.releaseVersion}#g' service.sh
64 sed -i 's#PORT=NULL#PORT=${env.port}#g' service.sh
65
66 # 复制脚本
67 ansible "${env.deployHosts}" -m copy -a "src=service.sh dest=${env.targetDir}/${env.projectName}/service.sh"
68 # 启动服务
69 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh start" -u root
70
71 # 检查服务
72 sleep 10
73 ansible "${env.deployHosts}" -m shell -a "cd ${env.targetDir}/${env.projectName} ;source /etc/profile && sh service.sh check" -u root
74 """
75}
Gitlab.groovy
1package org.devops
2
3//发起HTTP请求
4//调用gitlab api
5def HttpReq(method, apiUrl){
6
7 withCredentials([string(credentialsId: '5782c77d-ce9d-44e5-b9ba-1ba2097fc31d', variable: 'gitlabtoken')]) {
8 response = sh returnStdout: true,
9 script: """
10 curl --location --request ${method} \
11 http://172.29.9.101:8076/api/v4/${apiUrl} \
12 --header "PRIVATE-TOKEN: ${gitlabtoken}"
13 """
14 }
15 response = readJSON text: response - "\n" //json数据的读取方式
16 return response
17}
18
19//获取ProjectID
20def GetProjectIDByName(projectName, groupName){
21 apiUrl = "projects?search=${projectName}"
22 response = HttpReq("GET", apiUrl)
23 if (response != []){
24 for (p in response) {
25 if (p["namespace"]["name"] == groupName){
26 return response[0]["id"]
27 }
28 }
29 }
30}
31
32//获取分支CommitID
33def GetBranchCommitID(projectID, branchName){
34 apiUrl = "projects/${projectID}/repository/branches/${branchName}"
35 response = HttpReq("GET", apiUrl)
36 return response.commit.short_id
37}
- CI页面参数如下








- CD页面参数









FAQ
流水线调试问题
记录一下:
1
2## SoanrQube的项目名称不能带有特殊字符'/'
3{"errors":[{"msg":"Malformed key for Project: 'anyops/anyops-devops-service'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit."}]}
4
5
6## 设置质量规则时,前端项目的language应该是js或者ts, 而不是npm。
7{"errors":[{"msg":"Value of parameter 'language' (npm) must be one of: [java, go, js, ts]"}]}
8
9
10## 在Sonarqube中找不到buildTools方法, 最后发现没有传递进去。
11No such property: buildTools for class: org.devops.sonarqube
扩展: 如何清除工作目录? 安装Workspace Cleanup插件。在Pipeline 的Post中的always添加CleanWs()

关于我
我的博客主旨:
- 排版美观,语言精炼;
- 文档即手册,步骤明细,拒绝埋坑,提供源码;
- 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!
🍀 微信二维码 x2675263825 (舍得), qq:2675263825。

🍀 微信公众号 《云原生架构师实战》

🍀 语雀
https://www.yuque.com/xyy-onlyone

🍀 csdn https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421

🍀 知乎 https://www.zhihu.com/people/foryouone

最后
好了,关于本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!


