简介
Jenkins+Docker+git多分支实现springboot项目多环境快速交付一文我们介绍了CI/CD交付springboot项目过程中的环境校验、发版/回滚/重启、操作校验等步骤,在实际应用过程中有几点思考:
-
构建前的运行参数定义
构建前我们只按规范定义了APP_NAME(项目名)、IMAGE_NAME(镜像名)、MONITOR_URL(健康检查URL),相关的JVM参数、端口映射等与实际运行的参数仍然需要在后续的容器运行时手动修改,增加了配置难度,因此我们考虑将其提取到环境变量统一设置,以降低出问题的概率。
-
繁琐的pull/start/stop/rm操作
在构建过程中需要在远程服务器上频繁的docker pull/start/stop/rm等操作来更新镜像,有没有更好的方式来简化这些操作,来让整个过程更加简洁。因此,我们使用容器编排工具docker-compose来更新镜像、管理容器。
以上两点是本文重点解决的问题,我们使用docker-compose+环境变量来实现下。
docker-compose配置
1.compose模板文件
version: '3.7'
services:
helloworld:
image: harbor.test.cn/${IMAGE_NAME}:${VERSION}
container_name: ${CONTAINER_NAME}
restart: always
environment:
- JAVA_OPTS=${JAVA_OPTS}
ports:
- $PORT
volumes:
- /App/java_app/${APP_NAME}/logs:/logs
healthcheck:
test: ["CMD", "curl", "-fs", "$MONITOR_URL"]
interval: 8s
timeout: 10s
retries: 3
我们主要提取了以下几个变量:
- 镜像名:${IMAGE_NAME}
- 镜像tag:${VERSION},用于版本回滚
- 容器名:${CONTAINER_NAME}
- JVM参数:${JAVA_OPTS}
- 端口映射:${PORT}
- 项目名:${APP_NAME}
- 健康检查URL:${MONITOR_URL}
其中:镜像tag是我们在回滚时需要输入的参数,其他都是构建前根据实际情况插入到全局环境变量中。
2.env环境变量配置文件
docker-compose默认使用同级目录下的.env文件作为环境变量配置文件,借助此文件我们可以在构建前将jenkins中的全局环境变量写入此文件,以便docker-compose使用。
3. 更新镜像并启动容器
每次构建前修改.env文件后,我们可以通过docker-compose来pull指定的镜像并启动容器了。
docker-compose up -d --build
通过–build参数,在启动容器前,都会更新使用的镜像。
优化实现
1.jenkins新建自由风格的job,名称为docker-test-helloworld
2.参数化构建
3.插入全局环境变量及设置Build Name(优化点)
APP_NAME=helloworld
IMAGE_NAME=helloworld/helloworld
MONITOR_URL="http://127.0.0.1:8080"
JAVA_OPTS="-Xmx129m -Xms129m -Dspring.profiles.active=$(echo ${JOB_NAME}|awk -F'-' '{print $2}')"
PORT=9080:8080
以上是插入的全局环境变量,整个项目我们只需在此处集中修改,以减少配置错误为目的。
4.Build-环境校验、操作校验
Build过程主要进行环境校验、操作校验操作,用于
(1)环境校验,判断git分支与当前job-test/prod是否一致,不一致则停止后续发版操作;
(2)操作校验
发版:git对应分支是否有更新,防止在没有更新时构建多次,导致应用多次重启;
主要利用jenkins内置变量:
- GIT_PREVIOUS_SUCCESSFUL_COMMIT 上次构建成功后的git版本号
- GIT_COMMIT 当前构建任务的git版本号
回滚:判断远程分支是否有与参数匹配的版本号,没有则说明不合法,停止回滚;
代码如下:
#!/bin/bash
CHECK_ENV(){
#判断git分支是否与项目匹配,避免环境与项目混用
ENV=`echo ${JOB_NAME}|awk -F'-' '{print $2}'`
#测试分支develop,生产分支master
BRANCH=${GIT_BRANCH}
if [ $BRANCH = "origin/develop" ];then
[ $ENV="test" ] && echo -e "\033[34m$ENV environment is in building \033[0m" || {
echo -e "\033[31m git branch is $BRANCH, not match environment $ENV \033[0m"
exit 1
}
else
echo -e "\033[31m git branch is $BRANCH, not match environment $ENV \033[0m"
exit 1
fi
}
#环境校验
CHECK_ENV
#操作校验
if [ "${deploy_env}" = "deploy" ];then
echo -e "\033[34mstart ${deploy_env}\033[0m"
echo ${GIT_PREVIOUS_SUCCESSFUL_COMMIT}
echo ${GIT_COMMIT}
[ "${GIT_PREVIOUS_SUCCESSFUL_COMMIT}" != "${GIT_COMMIT}" ] && echo -e "\033[34mstart maven package\033[0m" || {
#版本未更新,停止发版
echo -e "\033[31mRepositories not update, stop ${deploy_env}\033[0m"
exit 1
}
/usr/local/maven/bin/mvn clean package docker:build -DdockerImageTags=${GIT_COMMIT} -Dmaven.test.skip=true -DpushImageTag
[ $? -eq 0 ] && echo -e "\033[32mmaven package success\033[0m" || {
echo -e "\033[31mmaven package fail\033[0m"
exit 1
}
elif [ "${deploy_env}" = "rollback" ];then
echo -e "\033[34mstart ${deploy_env}\033[0m"
echo ${GIT_PREVIOUS_SUCCESSFUL_COMMIT}
echo ${GIT_COMMIT}
#查看远程分支是否有此版本
git branch -r --contains $version
[ $? -eq 0 ] && echo -e "\033[34mstart docker steps\033[0m" || {
echo -e "\033[31mverison is wrong,please check version\033[0m"
exit 1
}
fi
5.Build-远程服务器构建(优化点)
通过“SSH Publishers”插件登录远程服务器执行docker相关操作
#!/bin/bash
IN_FACE=`/sbin/route -n |awk '{if($4~/UG/){print $8}}'|head -n 1`
LOCAL_IP=`/sbin/ip addr show "${IN_FACE}" | grep -w 'inet' | awk '{print $2}'`
CONTAINER_NAME=`echo ${IMAGE_NAME} | awk -F/ '{print $2}'`
#ENV=`echo ${JOB_NAME}|awk -F'-' '{print $2}'`
#删除老镜像
DEL_IMAGE() {
echo -e "\033[34mrm image ${IMAGE_NAME}:$1\033[0m"
sudo docker image rm harbor.test.cn/${IMAGE_NAME}:$1 --no-prune
[ $? -eq 0 ] && echo -e "\033[32mrm ${IMAGE_NAME}:$1 succss \033[0m" || {
echo -e "\033[31mrm ${IMAGE_NAME}:$1 fail \033[0m"
exit 1
}
}
#健康检查
HEALTHCHECK() {
timeout=180
echo -e "\033[34mhealth check\033[0m"
for (( i=1;i<=$timeout;i++ ))
do
status=$(sudo docker inspect --format='{{json .State.Health}}' ${CONTAINER_NAME}|grep -Po '"Status[":]+\K[^"]+')
echo $status
if [ $status = 'healthy' ];then
echo -e "\033[32m${LOCAL_IP} ${CONTAINER_NAME} status is ${status}\033[0m"
[ $deploy_env != "restart" ] && DEL_IMAGE ${OLD_VERSION}
exit 0
elif [ $status = 'starting' ];then
sleep 23
else
echo -e "\033[31m${LOCAL_IP} ${CONTAINER_NAME} status is ${status}\033[0m"
exit 1
fi
done
}
#定义docker-compose变量,注意第一步清空env,后续追加env
INIT_VAR() {
echo -e "\033[34minit docker-compose variable\033[0m"
echo "IMAGE_NAME=${IMAGE_NAME}" > .env
echo "CONTAINER_NAME=${CONTAINER_NAME}" >> .env
echo "APP_NAME=${APP_NAME}" >> .env
echo "ENV=${ENV}" >> .env
echo "MONITOR_URL=${MONITOR_URL}" >> .env
echo "PORT=${PORT}" >> .env
echo "JAVA_OPTS=${JAVA_OPTS}" >> .env
}
#进入项目目录
cd /App/java_app_tmp/${APP_NAME}
#提前读取env文件中的老版本号,用于删除老镜像
OLD_VERSION=$(grep VERSION .env|awk -F= '{print $2}')
echo $OLD_VERSION
case ${deploy_env} in
deploy)
echo -e "\033[34mstart ${deploy_env} steps\033[0m"
INIT_VAR
echo "VERSION=${GIT_COMMIT}" >> .env
sudo docker-compose up -d --build
HEALTHCHECK
;;
rollback)
echo -e "\033[34mstart ${deploy_env} steps\033[0m"
INIT_VAR
echo "VERSION=${version}" >> .env
sudo docker-compose up -d --build
HEALTHCHECK
;;
restart)
sudo docker-compose restart
HEALTHCHECK
;;
*)
exit 1
;;
esac
通过docker-compose将之前的pull/stop/rm/start等一系列的docker操作全部用"docker-compose up -d --build"代替,大大简化了代码。
注意:
1.每次构建前通过">"清除.env文件并重启添加,之所以保留便于我们排查问题。
2.DEL_IMAGE 删除老镜像操作,在restart操作中忽略,因为此时只是容器重启,并没有拉取新镜像。
6.Post-build Actions
#删除jenkins slave服务上的虚悬镜像
echo -e "\033[34mrm old image on jenkins slave\033[0m"
if [ $(docker image ls harbor.test.cn/${IMAGE_NAME} -q|wc -l) -ne 0 ];then
docker image rm `docker image ls harbor.test.cn/${IMAGE_NAME} -q` -f --no-prune
fi
docker image prune -f
删除jenkins slave服务上新构建的镜像及虚悬镜像,保持slave上的环境纯净。
通过以上步骤我们只在第3、5步配合docker-compose做了优化,其他不变仍可参考Jenkins+Docker+git多分支实现springboot项目多环境快速交付。
总结
本文之所以是优化升级,因为在配置过程中提高可读性、集中配置、简化操作可以有效的减少出错的概率,另外在DevOps中交付的效率问题也是非常重要的一个环节。
最重要的是目前的Docker实践我们需要一步一个脚印的走过来,在这过程中要不断的思考、总结。