公司原来的项目发布很繁琐也很普通,最近捣鼓一下jenkins+docker,做一下一键发布,由于公司服务器都加了堡垒机,所以需要解决不能远程ssh部署,整体的思路如下:
关于jenkins的安装方式一开始尝试了很多种方案:
安装步骤请查询相关文档,这里就略过
cd {你的Jenkins工作目录}/updates #进入更新配置位置
vim default.json
##替换软件源
:1,$s/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g
这个插件用于jenkins将docker镜像push到目标仓库之后调用堡垒机中发布服务进行docker镜像的发布
步骤:新建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