Jenkins+Docker+git多分支實現springboot項目多環境快速交付

Jenkins+Docker+git多分支實現springboot項目多環境快速交付

簡介

Docker動態構建Jenkins Slave,構建完任務自動銷燬中我們實現了spring項目在jenkins slave上動態構建。但是在實際CI/CD應用過程中,運維可能以下問題:

  1. 環境校驗

springboot項目的多個git分支,不同分支對應不同的環境。例如:develop分支對應測試環境,master分支對應生產環境。運維部署過程中稍有疏忽,可能導致應用錯用環境配置,給測試、生產引入不必要的問題。因此我們增加環境校驗,來保證不同的分支使用正確的環境配置文件。

  1. 發版/回滾/重啟

發版過程中不僅要考慮版本的正常發佈,還要考慮版本的回滾,以防新版本有重大bug能夠及時回退歷史版本。當然考慮到java可能出現OOM問題導致進程死掉,因此我們最好需要一個重啟功能,來方便及時重啟。本著“誰開發,誰運行”的理念,我們的CI/CD考慮了以上3種功能。

  1. 操作校驗

發版/回滾會涉及到應用的重啟問題,為了避免重複構建導致重啟,我們需要驗證git分支的版本commitid,防止發版/回滾過程中版本中的重複更新導致的應用重啟。

流程

Jenkins+Docker+git多分支實現springboot項目多環境快速交付

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

  1. 全局變量

APP_NAME 項目名稱

IMAGE_NAME 鏡像名稱,格式:業務/系統

MONITOR_URL 監控URL

  1. 環境校驗

通過JOB_NAME提取prod/test關鍵字,與git分支master/develop匹配,用以進行環境校驗。

  1. 操作校驗

發版:git有新版本時,進行發版操作;

回滾:將鏡像回滾至任一版本

重啟:重啟容器

  1. docker 相關規範

(1) 鏡像命名規範,格式:業務/系統

如:helloworld/helloworld

(2)容器命名規範, 格式:系統名,如helloworld

(3)tag規範,格式:commitid,如7e2c56522188c98f6294d91c8568dfcedf994e42

具體實現

  1. jenkins新建自由風格的job,名稱為docker-test-helloworld
  2. 參數化構建
Jenkins+Docker+git多分支實現springboot項目多環境快速交付

  1. 插入全局環境變量及設置Build Name
Jenkins+Docker+git多分支實現springboot項目多環境快速交付

(1)全局變量存在於整個job構建週期,我們只需根據項目實際情況在此設置變量即可,其他內容無需改變。

(2)Build Name是構建名稱,通過jenkins內置變量BUILD_NUMBER和GIT_COMMIT組成,幫助我們識別構建任務基於git哪個版本,方便排查問題。

  1. Build-環境校驗、操作校驗
Jenkins+Docker+git多分支實現springboot項目多環境快速交付

Build過程主要進行環境校驗、操作校驗操作,用於

(1)環境校驗,判斷git分支與當前job-test/prod是否一致,不一致則停止後續發版操作;

(2)操作校驗

發版:git對應分支是否有更新,防止在沒有更新時構建多次,導致應用多次重啟;

主要利用jenkins內置變量:

GIT_PREVIOUS_SUCCESSFUL_COMMIT 上次構建成功後的git版本號

GIT_COMMIT 當前構建任務的git版本號

回滾:判斷遠程分支是否有與參數匹配的版本號,沒有則說明不合法,停止回滾;

代碼如下:

<code>#!/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/<code>
  1. Build-遠程服務器構建
Jenkins+Docker+git多分支實現springboot項目多環境快速交付

通過“SSH Publishers”插件登錄遠程服務器執行docker相關操作

<code>#!/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/<code>

遠程倉庫:我們的tag使用git的commitid,用於區分鏡像基於的sprintboot版本;

發版:我們直接使用GIT_COMMIT,作為鏡像的tag;

回滾:我們通過填寫的version到遠程harbor 匹配合適的版本;

健康檢查:docker內置的healthcheck來幫助我們檢查本次發版/回滾/重啟,是否成功;

  1. Post-build Actions


Jenkins+Docker+git多分支實現springboot項目多環境快速交付

<code>#刪除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/<code>

刪除jenkins slave服務上新構建的鏡像及虛懸鏡像,保持slave上的環境純淨。


總結

以上是我結合docker+jenkins對持續集成/交付過程的一些理解,通過對docker的不斷摸索實踐,希望能夠持續優化此方案。我認為最終登錄遠程服務器的操作過程還是太繁瑣,發版/回滾過程中需要不斷的停止、刪除容器、刪除鏡像,因此後續會通過docker-compose去優化,帶來更簡潔的配置管理。

另,以上是通過ssh登錄遠程主機進行docker單機部署,但docker的server-client架構,應該還有更便捷的方式如swarm、k8s,這些是需要在日後不斷學習總結的。


我是【木訥大叔愛運維】,如果喜歡請關注,與你持續分享運維路上的點點滴滴。


分享到:


相關文章: