14. 代码部署流程与案例
1 代码部署流程
1.1 代码克隆
公司的代码都是放到gitlab仓库, 代码的克隆会由Jenkins的项目构建配置去触发. 运维人员需要在Jenkins中创建项目, 并且配置构建的步骤. 之后点击构建, 触发代码的克隆以及后续阶段
代码克隆阶段需要配置的内容
1. Jenkins服务器上的Jenkins用户和Gitlab服务器的免秘钥认证: Jenkins是以jenkins用户运行, 因此, 需要在Jenkins服务器生成jenkins用的秘钥对, 把公钥添加到Gitlab的root账号下,防止其他用户误删除
2. 在Jenkins上创建项目, 填写项目的gitlab代码地址, 添加jenkins用户的私钥信息
配置好这些信息后, Jenkins就可以从gitlab拉取代码到本地的/var/lib/jenkins/workspace目录下

1.2 源代码测试
代码测试可以使用商业的软件, 也可以使用开源的Sonarqube. 需要部署单独Sonarqube服务器并且在Jenkins服务器上安装Sonar-Scanner客户端, 让Jenkins拉取代码后, 在本地进行代码扫描, 之后上传到Sonarqube服务器进行分析
代码扫描一定是在编译前进行
代码测试阶段需要配置的内容
1. 部署Sonarqube Server和PgSQL
2. 在PgSQL创建数据库, 配置Sonarqube和PgSQL的连接
3. 配置SonarScanner和服务器的连接
4. 配置项目构建使用Sonarqube进行扫描. 如果是使用脚本, 那么只需在构建中使用Shell脚本, 同时这样也可以把代码克隆到自定义的Jenkins服务器目录下, 之后在项目目录下, 创建properties文件. 并且在项目目录下执行scanner客户端命令
5. 如果是使用Jenkins的Scanner插件, 那么需要安装插件, 配置Sonarqube服务器地址, 以及本地的Scanner客户端命令的路径, 包括编辑properties文件
6. 执行构建即可完成代码克隆和扫描

1.3 代码编译
对于Java这类编译性语言, 一旦源代码扫描没有问题, 就会执行编译, 如果代码有问题, 那么就不会继续构建. 如果是html, php这些语言, 那么编译步骤是不用的
Java代码编译可以在单独的Maven服务器进行执行, 并且可以搭建本地的仓库, 去缓存编译的依赖包, 如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共享的, 因此, 即使用户正在访问的服务器下线了, 下次再访问被调度到其他的服务器, Session信息也不会丢失
HAProxy可以通过socket文件, 进行动态的上线下
LVS需要使用lvsadmin命令或者在Keepalived中注释掉被下线的服务器
Nginx需要修改配置文件, 然后reload服务

1.6 停止web服务
服务器从负载均衡下线后, 要停止服务, 否则某些文件正在被占用时, 执行代码替换会出现错误

1.7 代码替换
代码替换有多种方案, 可以把压缩包存放到统一的路径, 把解压后的目录存放到统一路经, 然后把用户访问的路径软连接到代码目录. 之后代码升级或者降级只需要删除和重新创建软连接即可
对于旧版本的代码, 也可以暂时保留上一个版本, 一旦升级失败, 可以直接把上一个版本的代码拷贝到目标目录下, 修改软连接, 即可完成回滚

1.8 启动服务
代码替换后, 进行服务启动

1.9 服务测试
在把服务器重新添加到负载均衡前, 要先进行服务测试, 因为, 代码扫描只能扫描语法和bug等信息. 但是具体的功能, 还需要部署后才能进行测试. 因此, 在上线前, 要进行功能测试, 或者至少访问以下监控的url. 确保服务启动, 且运行正常. 如果发现服务启动失败, 或者功能缺失, 可以在这一步进行回滚

1.10 将服务器添加到负载均衡器
添加到负载均衡器后, 用户就可以正常访问了


2 代码部署策略
2.1 灰度/金丝雀部署
对于灰度部署, 可以先只升级一部分服务器, 让剩余的服务器正常运行在旧版本下. 新版本服务器升级测试没问题后, 上线到负载均衡器, 让用户访问一段时间, 确定没问题, 再继续升级后续的服务器
2.2 蓝绿部署
蓝绿部署要求有两套相同的环境, 每次升级只升级一套, 当运行稳定后, 再把另一套升级到相同的版本
2.3 滚动部署
每次都升级一个或者几个服务器, 并且连续的把所有的服务器都升级完.
具体的部署流程和策略, 还需要根据具体情况和业务, 进行修改
3 Jenkins参数化构建
3.1 创建一个项目


3.2 配置构建
- 丢弃旧的构建
只保留7天内的5次构建

- 参数化构建过程
字符参数: 用于指定部署的是哪个分支, 也就是代码部署到哪个环境
选项参数: 用于选择此次部署的内容, 比如可以定义某个选项为执行灰度部署, 部署后, 测试没问题, 再选择部署剩余的服务器
选项参数案例:


Jenkins服务器创建脚本, 测试$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

在执行Shell构建时, 脚本后要接$GROUP
. 之后执行构建时, 可以在Jenkins页面选择GROUP1
或者GROUP2
, 如果执行GROUP1
, 那么GROUP1
就会作为参数, 传给GROUP
变量, 最终传给脚本中的$1
. 只需要在脚本中判断$1
的值, 就可以判断此次是灰度发布, 还是发布剩余的服务器
- 此时, 执行构建时, 是没有立即构建选项的, 而是要使用
Build with Parameters

- 构建时, 可以根据不同的选项进行构建

Group1


Group2


字符参数案例:
一般用于指定部署到不同的环境, 比如生产, 开发或者测试环境
- 字符参数创建

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分支的代码是部署到生产环境的
部署的服务器是:GROUP1



- 在构建脚本中, 定义克隆的分支
#!/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 通过选项参数, 控制部署的服务器组
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

#测试关闭指定服务器组的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运行状态, 倒计时3秒
3
2
1
Tomcat正在运行, 进程ID为5167, 共1个进程
三秒后, 准备关闭Tomcat
3
2
1
已尝试关闭Tomcat, 30秒后检查是否关闭成功!
3
2
1
Tomcat关闭成功!
验证10.0.0.199的tomcat服务关闭成功
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
正在判断服务状态, 请稍等!
倒计时三秒钟!
3
2
1
Tomcat没有运行, 三秒后准备启动!
3
2
1
Tomcat started.
Tomcat启动已经执行, 五秒后判断是否启动成功!
5
4
3
2
1
Tomcat启动成功! 共1个java进程, 其PID为5660
- 到此, 确保了可以通过构建脚本, 对于指定的服务器, 通过选项和字符参数进行服务的停止和启动, 以及根据分支, 进行代码克隆
4 灰度部署案例-静态页面
- 实现qq/web-02项目的灰度发布, 先升级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执行服务器上下线
- 将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 修改构建配置
- 选项参数GROUP, 用来控制部署/回滚的服务器

- 选项参数METHOD, 表示此次部署是升级还是回滚

- 选项参数BRANCH, 表示此次部署的是哪个分支

- 构建脚本

#!/bin/bash
DATE=`date +%Y-%m-%d_%H-%M-%S`
# METHOD变量用来指定本次执行是升级代码还是回滚代码
METHOD=$1
# BRANCH变量用来指定本次执行部署或者回滚的分支
BRANCH=$2
# 服务器分组
# GROUP1灰度环境服务器,10.0.0.199
# GROUP2灰度以外的服务器, 10.0.0.209,10.0.0.219
# GROUP3一次新部署所有服务器, 用于紧急的服务升级, 比如需要执行紧急的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查看扫描结果"
}
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 "灰度部署环境服务器-->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 "当前历史构建数量为: $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
- 如果发现全部升级后, 出现bug, 可以直接rollback GROUP3
- 如果是部署开发/测试环境, 那么还需要做额外的判断, 添加开发/测试环境的服务器, 在Jenkins上创建单独的目录存放测试环境的代码, 然后传给测试环境服务器.
- 整个灰度升级过程是分多个阶段的, 首先代码克隆后, 要进行代码质量检测, 检测通过后, 会进行灰度服务器的代码拷贝, 并且把灰度服务器在负载均衡器上线, 此时, 可以给灰度服务器专门配置一个vip, 在haproxy添加一个灰度服务器的监听组, 让开发测试人员, 修改本地hosts文件, 把网站的域名, 解析到灰度服务器的vip, 之后进行测试, 测试没问题, 再把灰度服务器上线到生成环境的vip组上. 之后再升级剩余的服务器
- 如果想基于指定版本进行升级或者回滚, 可以基于tag号标签, 或者再给构建脚本传一个参数, 先确定要回滚到的指定的目录名, 如,
web-02-2021_xx_xx-xx-xx-xx
, 之后把这个目录名, 作为参数, 传给构建脚本, 修改软链接, 直接指向这个指定的版本
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专门给内网用户使用, 并不会在防火墙做解析, 因此, 公网用户是访问不到的
}
}