簡介
公司原來的項目發佈很繁瑣也很普通,最近搗鼓一下jenkins+docker,做一下一鍵發佈,由於公司服務器都加了堡壘機,所以需要解決不能遠程ssh部署,整體的思路如下:
- jenkins使用pipeline腳本編寫(更靈活,方便多套環境複製使用);
- 拉取代碼並編譯成jar包;
- 將jar包編譯爲docker鏡像;
- 將鏡像上傳到本地私有倉庫(速度快)
- 調用寫好的跑腳本的服務接口實現在堡壘機中實現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