Jenkins+Docker+git多分支实现springboot项目多环境快速交付

简介

jenkins通过Docker plugin部署slave中我们实现了spring项目在jenkins slave上动态构建。但是在实际CI/CD应用过程中,运维可能以下问题:

  1. 环境校验

springboot项目的多个git分支,不同分支对应不同的环境。例如:develop分支对应测试环境,master分支对应生产环境。运维部署过程中稍有疏忽,可能导致应用错用环境配置,给测试、生产引入不必要的问题。因此我们增加环境校验,来保证不同的分支使用正确的环境配置文件。

  1. 发版/回滚/重启

发版过程中不仅要考虑版本的正常发布,还要考虑版本的回滚,以防新版本有重大bug能够及时回退历史版本。当然考虑到java可能出现OOM问题导致进程死掉,因此我们最好需要一个重启功能,来方便及时重启。本着“谁开发,谁运行”的理念,我们的CI/CD考虑了以上3种功能。

  1. 操作校验

发版/回滚会涉及到应用的重启问题,为了避免重复构建导致重启,我们需要验证git分支的版本commitid,防止发版/回滚过程中版本中的重复更新导致的应用重启。

流程

在这里插入图片描述jenkins我们没有使用多分支流水线,因此不同的git分支分别对应不同的任务。如:master分支对应docker-prod-xxxx;develop分支对应docker-test-xxxx。

  1. 版本发布在jenkins slave上进行,在master上通过标签将构建任务绑定到指定的slave上;
  2. 环境校验根据git的test/master分支,分别对应任务名称中的test/prod,以此来实现环境校验;
  3. 操作校验分为发版/回滚/重启,分别实现不同的功能需求;

发版/回滚/重启在服务器上执行的docker操作不一样,如发版/回滚涉及到镜像及容器的停止删除,而重启操作则不需要。

规范

为了保证运维的正确操作,我们制定了docker项目部署的规范:

  1. JOB_NAME命名规范
    格式规范:docker-环境-项目名
    生产:docker-prod-xxxx
    测试:docker-test-xxxx

  2. 全局变量
    APP_NAME 项目名称
    IMAGE_NAME 镜像名称,格式:业务/系统
    MONITOR_URL 监控URL

  3. 环境校验
    通过JOB_NAME提取prod/test关键字,与git分支master/develop匹配,用以进行环境校验。

  4. 操作校验
    发版:git有新版本时,进行发版操作;
    回滚:将镜像回滚至任一版本
    重启:重启容器

  5. docker 相关规范
    (1) 镜像命名规范,格式:业务/系统
    如:helloworld/helloworld
    (2)容器命名规范, 格式:系统名,如helloworld
    (3)tag规范,格式:commitid,如
    7e2c56522188c98f6294d91c8568dfcedf994e42

具体实现

  1. jenkins新建自由风格的job,名称为docker-test-helloworld

  2. 参数化构建
    在这里插入图片描述3.插入全局环境变量及设置Build Name
    在这里插入图片描述 (1)全局变量存在于整个job构建周期,我们只需根据项目实际情况在此设置变量即可,其他内容无需改变。
    (2)Build Name是构建名称,通过jenkins内置变量BUILD_NUMBER和GIT_COMMIT组成,帮助我们识别构建任务基于git哪个版本,方便排查问题。

  3. 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"
    #查看远程分支是否有此版本
    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
  1. Build-远程服务器构建
    在这里插入图片描述通过“SSH Publishers”插件登录远程服务器执行docker相关操作
#!/bin/bash
#服务器ip
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}'`

#健康检查
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"
           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
}

#启动容器
START() {
    echo -e "\033[34mstart ${CONTAINER_NAME}\033[0m"
    sudo docker start ${CONTAINER_NAME}
    [ $? -eq 0 ] && echo -e "\033[32mstart ${CONTAINER_NAME} succss \033[0m" || { 
        echo -e "\033[31mstart ${CONTAINER_NAME} fail \033[0m"
        exit 1
    }
}

#停止容器
STOP() {
    echo -e "\033[34mstop ${CONTAINER_NAME}\033[0m"
    sudo docker stop ${CONTAINER_NAME}
    [ $? -eq 0 ] && echo -e "\033[32mstop ${CONTAINER_NAME} succss \033[0m" || { 
        echo -e "\033[31mstop ${CONTAINER_NAME} fail \033[0m"
        exit 1
    }
}

#删除容器
DEL_CONTAINER() {
    echo -e "\033[34mrm  container ${CONTAINER_NAME}\033[0m"
    sudo docker rm ${CONTAINER_NAME} -v 
    [ $? -eq 0 ] && echo -e "\033[32mrm ${CONTAINER_NAME} succss \033[0m" || { 
        echo -e "\033[31mrm ${CONTAINER_NAME} fail \033[0m"
        exit 1
    }
}

#删除镜像
DEL_IMAGE() {
    echo -e "\033[34mrm image ${IMAGE_NAME}\033[0m"
    sudo docker image rm `sudo docker image ls harbor.cityre.cn/${IMAGE_NAME} -q`  --no-prune
    [ $? -eq 0 ] && echo -e "\033[32mrm ${IMAGE_NAME} succss \033[0m" || { 
        echo -e "\033[31mrm ${IMAGE_NAME} fail \033[0m"
        exit 1
    }
}

#登录harbor
LOGIN_HARBOR() {
    echo -e "\033[34mlogin harbor\033[0m"
    sudo docker login harbor.cityre.cn
    [ $? -eq 0 ] && echo -e "\033[32mlogin harbor.cityre.cn success\033[0m" || {
        echo -e "\033[31mlogin harbor.cityre.cn fail\033[0m"
        exit 1
    }
    echo -e "\033[34mpull image\033[0m"
}

#拉取镜像
PULL() {
    sudo docker pull harbor.cityre.cn/${IMAGE_NAME}:$1
    [ $? -eq 0 ] && echo -e "\033[32mpull image $1 success\033[0m" || {
        echo -e "\033[31mpull image $1 fail\033[0m"
        exit 1
    } 
}

#运行容器
RUN() {
    sudo docker run $(cat /etc/hosts|grep -v ^#|grep -v ^$|awk -F ' ' '{if(NR>2){print "--add-host "$2":"$1}}') -v /etc/timezone:/etc/timezone:ro -v /etc/localtime:/etc/localtime:ro -e JAVA_OPTS="-Xmx512m -Xms512m -Dspring.profiles.active=$ENV" -v /App/java_app/${APP_NAME}/logs:/logs -p 8080:8080 -d --restart=always \
    --health-cmd="curl --silent --fail ${MONITOR_URL} || exit 1"\
    --health-retries=3\
    --health-interval=5s\
    --health-timeout=5s\
    --health-start-period=15s\
    --name ${CONTAINER_NAME} harbor.test.cn/${IMAGE_NAME}:$1
    [ $? -eq 0 ] && echo -e "\033[32mrun  container ${CONTAINER_NAME} success\033[0m" || {
        echo -e "\033[31mrun  container ${CONTAINER_NAME} fail\033[0m"
        exit 1
    }
}

case ${deploy_env} in
deploy)
    echo -e "\033[34mstart docker steps\033[0m"
    LOGIN_HARBOR
    PULL ${GIT_COMMIT}
    RUN ${GIT_COMMIT}
    HEALTHCHECK
    ;;
rollback)
    STOP
    DEL_CONTAINER
    DEL_IMAGE
    PULL $version
    RUN $version
    HEALTHCHECK
    ;;
restart)
    STOP
    START
    HEALTHCHECK
    ;;
*)
    exit 1
    ;;
esac

远程仓库:我们的tag使用git的commitid,用于区分镜像基于的sprintboot版本;
发版:我们直接使用GIT_COMMIT,作为镜像的tag;
回滚:我们通过填写的version到远程harbor 匹配合适的版本;
健康检查:docker内置的healthcheck来帮助我们检查本次发版/回滚/重启,是否成功;

  1. Post-build Actions
    在这里插入图片描述
#删除jenkins slave服务上新构建镜像
echo -e "\033[34mrm old image on jenkins slave\033[0m"
if [ $(docker image ls harbor.cityre.cn/${IMAGE_NAME} -q|wc -l) -ne 0 ];then 
   docker image rm `docker image ls harbor.cityre.cn/${IMAGE_NAME} -q` -f --no-prune
fi
docker image prune -f

删除jenkins slave服务上新构建的镜像及虚悬镜像,保持slave上的环境纯净。

总结

以上是我结合docker+jenkins对持续集成/交付过程的一些理解,通过对docker的不断摸索实践,希望能够持续优化此方案。我认为最终登录远程服务器的操作过程还是太繁琐,发版/回滚过程中需要不断的停止、删除容器、删除镜像,因此后续会通过docker-compose去优化,带来更简洁的配置管理。

另,以上是通过ssh登录远程主机进行docker单机部署,但docker的server-client架构,应该还有更便捷的方式如swarm、k8s,这些是需要在日后不断学习总结的。

PS:如果你对博文感兴趣,请关注我的公众号“木讷大叔爱运维”,与你分享运维路上的点滴。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值