jenkins 持續部署 docker服務到堡壘機

簡介

公司原來的項目發佈很繁瑣也很普通,最近搗鼓一下jenkins+docker,做一下一鍵發佈,由於公司服務器都加了堡壘機,所以需要解決不能遠程ssh部署,整體的思路如下:

  1. jenkins使用pipeline腳本編寫(更靈活,方便多套環境複製使用);
  2. 拉取代碼並編譯成jar包;
  3. 將jar包編譯爲docker鏡像;
  4. 將鏡像上傳到本地私有倉庫(速度快)
  5. 調用寫好的跑腳本的服務接口實現在堡壘機中實現docker鏡像的新版本發佈;

關於jenkins的安裝方式一開始嘗試了很多種方案:

  • jenkins部署在docker容器內,使用遠程docker rest api進行鏡像打包上傳,但是遇到很大的問題就是阿里雲私有鏡像倉庫登錄方式不一樣,導致登錄失敗,而且不能使用腳本操作docker,因爲jenkins容器內沒有docker環境,如果安裝docker in docker,這樣就太麻煩了,在容器外面就有一套docker環境;(如果不依賴阿里雲私有倉庫,這種方案就沒有關係了)
  • jenkins安裝在有docker環境的服務器內,那麼可以使用shell腳本靈活的進行編譯上傳等操作(適用於比較靈活的使用場景)

開始:

依賴環境:

  • jenkins
  • docker

jenkins安裝

安裝步驟請查詢相關文檔,這裏就略過

jenkins安裝插件提速

cd {你的Jenkins工作目錄}/updates  #進入更新配置位置

vim default.json

##替換軟件源
:1,$s/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g

安裝jenkins插件 HTTP Request

這個插件用於jenkins將docker鏡像push到目標倉庫之後調用堡壘機中發佈服務進行docker鏡像的發佈

編寫jenkins發佈腳本

步驟:新建item -> 選擇pipeline(流水線) -> 編輯流水線腳本

注意:

  • 在輸入框的左下角有:流水線語法,可以根據某些你需要用到的插件生成模板腳本,非常方便
  • 腳本有一點需要注意,單引號內只能爲文本不能使用變量,如果需要使用變量,使用雙引號;
pipeline {
   agent any

   tools {
      // Install the Maven version configured as "M3" and add it to the path.
      maven "maven3.6.3"
   }
   
   //環境變量,一下變量名稱都可以自定義,在後面的腳本中使用
   environment {
      //git倉庫
	  GIT_REGISTRY = 'https://github.com/WinterChenS/my-site.git'
	  //分支
	  GIT_BRANCH = 'sit'
	  //profile
	  PROFILES = 'sit'
	  //如果倉庫是私有的需要在憑證中添加憑證,然後把id寫到這裏
	  GITLAB_ACCESS_TOKEN_ID = '85465d36-4c3a-469f-b92f-f53dae47fd0c'
	  //服務名稱
	  SERVICE_NAME = 'my-site'
	  //鏡像名稱,aaa_sit是命名空間,可以區分不同的環境
	  IMAGE_NAME = "127.0.0.1:8999/aaa_sit/${SERVICE_NAME}"
	  //鏡像tag
	  TAG = "latest"
	  //遠程發佈服務的地址
	  REMOTE_EXECUTE_HOST = 'http://10.85.54.33:7017/shell'
	  //服務開放的端口
	  SERVER_PORT = '19070'
	  //日誌目錄,容器內目錄
	  LOG_DIR = '/var/logs'
	  //宿主機目錄
      MAIN_VOLUME = "${LOG_DIR}/jar_${env.SERVER_PORT}"
      //jvm參數
      JVM_ARG = "-server -Xms512m -Xmx512m  -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=${LOG_DIR}/dump/dump-yyy.log  -XX:ErrorFile=${LOG_DIR}/jvm/jvm-crash.log"
   }
   
   

   stages {
      stage('Build') {
         steps {
            // 獲取代碼
            git credentialsId: "${env.GITLAB_ACCESS_TOKEN_ID}", url: "${env.GIT_REGISTRY}", branch: "${env.GIT_BRANCH}"

            // maven 打包
            sh "mvn -Dmaven.test.failure.ignore=true clean package -P ${env.PROFILES}"

         }

      }
      
      stage('Execute shell') {
	    // 將jar包拷貝到Dockerfile所在目錄
		steps {
		    //注意,這裏的目錄一定要跟項目實際的目錄結構要對應上
			sh "cp ${env.WORKSPACE}/${env.SERVICE_NAME}/target/*.jar ${env.WORKSPACE}/${env.SERVICE_NAME}/src/main/docker/${env.SERVICE_NAME}.jar"
		
		}
	  }
	  
	  
	 
	  
	  stage('Image Build And Push') {

		steps {
            //運行這些腳本的條件就是jenkins運行的服務器有docker環境
            //如果jdk版本是你自己編譯成的docker鏡像,那麼首次編譯的時候需要pull
			sh "echo '================開始拉取基礎鏡像jdk1.8================'"	
			//這裏根據你的私有倉庫而定,如果是使用公共鏡像的openjdk那麼可以略過這一步
			sh "docker pull 127.0.0.1:8999/jdk/jdk1.8:8u171"
			sh "echo '================基礎鏡像拉取完畢================'"
			
			sh "echo '================開始編譯並上傳鏡像================'"
			//注意目錄結構
			sh "cd ${env.WORKSPACE}/${env.SERVICE_NAME}/src/main/docker/ && docker build -t ${env.IMAGE_NAME}:${env.TAG} . && docker push ${env.IMAGE_NAME}:${env.TAG}"
			sh "echo '================鏡像上傳成功================'"
			
			sh "echo '================刪除本地鏡像================'"
			//刪除本地鏡像防止佔用資源
			sh "docker rmi ${env.IMAGE_NAME}:${env.TAG}"
			
		}
		

	  }
	  
	  stage('Execute service') {

	    //請求堡壘機內的發佈服務,具體代碼後面會給出
		steps {
		    //以下整個腳本都依賴jenkins插件:HTTP Request
		    //將body轉換爲json
            script {
              def toJson = {
                input ->
                groovy.json.JsonOutput.toJson(input)
            }
			//body定義,根據實際情況而定
			def body = [
                imageName: "${env.IMAGE_NAME}",
                tag:"${env.TAG}",
                port:"${env.SERVER_PORT}",
                simpleImageName: "${env.SERVICE_NAME}",
                envs: [
                    JVM_ARGS: "${env.JVM_ARG}"
                ],
                volumes: ["${env.MAIN_VOLUME}:${env.LOG_DIR}"]
            ]
			
			sh "echo '================開始調用目標服務器發佈================'"
			response = httpRequest acceptType: 'APPLICATION_JSON', consoleLogResponseBody: true, contentType: 'APPLICATION_JSON', httpMode: 'POST', requestBody: toJson(body), responseHandle: 'NONE', url: "${env.REMOTE_EXECUTE_HOST}"
		    sh "echo '================結束調用目標服務器發佈================'"			
		}
		

	  }
   }
}
}

遠程堡壘機發布服務

遠程發佈服務其實是一個很簡單的執行腳本的服務

ShellRequestDTO.java

package com.winterchen.jenkinsauto.dto;

import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Map;

public class ShellRequestDTO {

    @NotBlank
    private String imageName;

    @NotBlank
    private String tag;

    @NotBlank
    private String simpleImageName;

    @NotBlank
    private String port;

    /**
     * 環境變量列表
     */
    private Map<String, String> envs;

    private List<String> volumes;

    public String getImageName() {
        return imageName;
    }

    public void setImageName(String imageName) {
        this.imageName = imageName;
    }

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port = port;
    }

    public String getSimpleImageName() {
        return simpleImageName;
    }

    public void setSimpleImageName(String simpleImageName) {
        this.simpleImageName = simpleImageName;
    }

    public Map<String, String> getEnvs() {
        return envs;
    }

    public void setEnvs(Map<String, String> envs) {
        this.envs = envs;
    }

    public List<String> getVolumes() {
        return volumes;
    }

    public void setVolumes(List<String> volumes) {
        this.volumes = volumes;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("ShellRequestDTO{");
        sb.append("imageName='").append(imageName).append('\'');
        sb.append(", tag='").append(tag).append('\'');
        sb.append(", simpleImageName='").append(simpleImageName).append('\'');
        sb.append(", port='").append(port).append('\'');
        sb.append(", envs=").append(envs);
        sb.append(", volumes=").append(volumes);
        sb.append('}');
        return sb.toString();
    }
}

APIResponse.java

package com.winterchen.jenkinsauto.dto;


public class APIRespose<T> {

    private Integer code;

    private T data;

    private String message;

    private Boolean success;


    public static APIRespose success(){
        APIRespose apiRespose = new APIRespose();
        apiRespose.setCode(200);
        apiRespose.setSuccess(true);
        return apiRespose;
    }

    public static APIRespose success(Object data) {
        APIRespose apiRespose = new APIRespose();
        apiRespose.setCode(200);
        apiRespose.setSuccess(true);
        apiRespose.setData(data);
        return apiRespose;
    }

    public static APIRespose fail(String message) {
        APIRespose apiRespose = new APIRespose();
        apiRespose.setCode(500);
        apiRespose.setSuccess(false);
        apiRespose.setMessage(message);
        return apiRespose;
    }

   //get,set省略
}

BaseController.java

package com.winterchen.jenkinsauto.controller;

import com.winterchen.jenkinsauto.dto.APIRespose;
import com.winterchen.jenkinsauto.dto.ShellRequestDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/shell")
public class BaseController {

    private static final Logger LOGGER = LoggerFactory.getLogger(BaseController.class);

    @PostMapping("")
    public APIRespose executeShell(
        @RequestBody
        @Validated
        ShellRequestDTO requestDTO
    ) {
        LOGGER.info("當前請求參數:" + requestDTO.toString());
        StringBuilder sb = new StringBuilder();
        try {
            doExecuteShell(requestDTO, sb);
            return APIRespose.success(sb.toString());
        } catch (Exception e) {
            return APIRespose.fail(e.getMessage());
        }
    }

    private synchronized void doExecuteShell(ShellRequestDTO requestDTO, StringBuilder sb) throws Exception{
        //停止舊的容器
        stopContainer(requestDTO.getSimpleImageName(), sb);
        //stopContainerByImageId(requestDTO.getImageName(), sb);
        //刪除舊的容器
        removeContainer(requestDTO.getSimpleImageName(), sb);
        //removeContainerByImageId(requestDTO.getImageName(),sb);
        //刪除舊的鏡像
        removeImage(requestDTO.getImageName(), sb);
        removeNoneImages(sb);
        //拉取最新的鏡像
        pullImage(requestDTO.getImageName(), requestDTO.getTag(), sb);
        //運行最新鏡像
        runImage(requestDTO, sb);
        /**
         * TODO 擴展:如果需要等服務可用再返回,可以在服務中增加一個檢查接口,循環的調用該接口直至成功返回爲止(注意需要有超時機制,如果服務啓動失敗會一直進入死循環)
         * TODO 該拓展主要是爲了集成測試準備,自動化測試腳本運行的前提是該服務可以正常提供服務
         */
    }


    private void pullImage(String imageName, String tag, StringBuilder sb) throws Exception{
        execute(MessageFormat.format("docker pull {0}:{1}", imageName, tag), sb);
    }

    private void stopContainer(String simpleImageName, StringBuilder sb) {
        try {
            execute("docker stop " + simpleImageName, sb);
        } catch (Exception e) {
            LOGGER.error("停止容器失敗", e);
        }
    }



    private void removeImage(String imageName, StringBuilder sb)  {
        try {
            execute("docker rmi -f " + imageName, sb);
        } catch (Exception e) {
            LOGGER.error("刪除鏡像失敗", e);
        }
    }

    private void removeNoneImages(StringBuilder sb) {
        try{
            execute("docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}'", sb);
            execute("docker stop $(docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}')", sb);
            execute("docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}'",sb);
            execute("docker rm  $(docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}')", sb);
            execute("docker images -f 'dangling=true'|awk '{print $3}'", sb);
            execute("docker image rm -f  `docker images -f 'dangling=true'|awk '{print $3}'`", sb);
        } catch (Exception e) {
            LOGGER.error("刪除none鏡像失敗", e);
        }
    }


    private void removeContainer(String simpleImageName, StringBuilder sb) {
        try {
            execute("docker rm " + simpleImageName, sb);
        } catch (Exception e) {
            LOGGER.error("刪除容器失敗", e);
        }
    }


    private void runImage(ShellRequestDTO requestDTO, StringBuilder sb) throws Exception{
        StringBuilder shell = new StringBuilder();
        shell.append("docker run -p ").append(requestDTO.getPort()).append(":").append(requestDTO.getPort());
        shell.append(" --network=host ");
        shell.append(" --name=").append(requestDTO.getSimpleImageName());
        shell.append(" -d ");
        formatEnv(requestDTO.getEnvs(), shell);
        formatVolumes(requestDTO.getVolumes(), shell);
        shell.append(requestDTO.getImageName()).append(":").append(requestDTO.getTag());
        execute(shell.toString(), sb);
    }

    private void formatVolumes(List<String> volumes, StringBuilder shell) {
        if (volumes == null || 0 == volumes.size()) {
            return;
        }
        volumes.forEach(volume -> {
            shell.append(" -v ");
            shell.append(" '").append(volume).append("' ");
        });
    }

    private void formatEnv(Map<String, String> env, StringBuilder shell) {
        if (env == null || env.isEmpty()) {
            return;
        }
        for (Map.Entry<String, String> entry : env.entrySet()) {
            shell.append(" -e ");
            shell.append(entry.getKey()).append("='").append(entry.getValue()).append("' ");
        }
    }


    private void execute(String command, StringBuilder sb) throws Exception {
        BufferedReader infoInput = null;
        BufferedReader errorInput = null;
        try {
            LOGGER.info("======================當前執行命令======================");
            LOGGER.info(command);
            LOGGER.info("======================當前執行命令======================");
            //執行腳本並等待腳本執行完成
            String[] commands = { "/bin/sh", "-c", command };
            Process process = Runtime.getRuntime().exec(commands);
            //寫出腳本執行中的過程信息
            infoInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
            errorInput = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line = "";
            while ((line = infoInput.readLine()) != null) {
                sb.append(line).append(System.lineSeparator());
                LOGGER.info(line);
            }
            while ((line = errorInput.readLine()) != null) {
                sb.append(line).append(System.lineSeparator());
                LOGGER.error(line);
            }
            //阻塞執行線程直至腳本執行完成後返回
            process.waitFor();
        } finally {
            try {
                if (infoInput != null) {
                    infoInput.close();
                }
                if (errorInput != null) {
                    errorInput.close();
                }
            } catch (IOException e) {

            }
        }
    }
}

相關資源

微信公衆號:CodeD
微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章