jenkins 持续部署 docker服务到堡垒机

淩亂°似流年 2023-07-25 09:44 122阅读 0赞

简介

公司原来的项目发布很繁琐也很普通,最近捣鼓一下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安装插件提速

  1. cd {你的Jenkins工作目录}/updates #进入更新配置位置
  2. vim default.json
  3. ##替换软件源
  4. :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 {

    1. // Install the Maven version configured as "M3" and add it to the path.
    2. maven "maven3.6.3"

    }

    //环境变量,一下变量名称都可以自定义,在后面的脚本中使用
    environment {

    1. //git仓库
    2. GIT_REGISTRY = 'https://github.com/WinterChenS/my-site.git'
    3. //分支
    4. GIT_BRANCH = 'sit'
    5. //profile
    6. PROFILES = 'sit'
    7. //如果仓库是私有的需要在凭证中添加凭证,然后把id写到这里
    8. GITLAB_ACCESS_TOKEN_ID = '85465d36-4c3a-469f-b92f-f53dae47fd0c'
    9. //服务名称
    10. SERVICE_NAME = 'my-site'
    11. //镜像名称,aaa_sit是命名空间,可以区分不同的环境
    12. IMAGE_NAME = "127.0.0.1:8999/aaa_sit/${SERVICE_NAME}"
    13. //镜像tag
    14. TAG = "latest"
    15. //远程发布服务的地址
    16. REMOTE_EXECUTE_HOST = 'http://10.85.54.33:7017/shell'
    17. //服务开放的端口
    18. SERVER_PORT = '19070'
    19. //日志目录,容器内目录
    20. LOG_DIR = '/var/logs'
    21. //宿主机目录
    22. MAIN_VOLUME = "${LOG_DIR}/jar_${env.SERVER_PORT}"
    23. //jvm参数
    24. JVM_ARG = "-server -Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/dump/dump-yyy.log -XX:ErrorFile=${LOG_DIR}/jvm/jvm-crash.log"

    }

  1. stages {
  2. stage('Build') {
  3. steps {
  4. // 获取代码
  5. git credentialsId: "${env.GITLAB_ACCESS_TOKEN_ID}", url: "${env.GIT_REGISTRY}", branch: "${env.GIT_BRANCH}"
  6. // maven 打包
  7. sh "mvn -Dmaven.test.failure.ignore=true clean package -P ${env.PROFILES}"
  8. }
  9. }
  10. stage('Execute shell') {
  11. // 将jar包拷贝到Dockerfile所在目录
  12. steps {
  13. //注意,这里的目录一定要跟项目实际的目录结构要对应上
  14. sh "cp ${env.WORKSPACE}/${env.SERVICE_NAME}/target/*.jar ${env.WORKSPACE}/${env.SERVICE_NAME}/src/main/docker/${env.SERVICE_NAME}.jar"
  15. }
  16. }
  17. stage('Image Build And Push') {
  18. steps {
  19. //运行这些脚本的条件就是jenkins运行的服务器有docker环境
  20. //如果jdk版本是你自己编译成的docker镜像,那么首次编译的时候需要pull
  21. sh "echo '================开始拉取基础镜像jdk1.8================'"
  22. //这里根据你的私有仓库而定,如果是使用公共镜像的openjdk那么可以略过这一步
  23. sh "docker pull 127.0.0.1:8999/jdk/jdk1.8:8u171"
  24. sh "echo '================基础镜像拉取完毕================'"
  25. sh "echo '================开始编译并上传镜像================'"
  26. //注意目录结构
  27. 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}"
  28. sh "echo '================镜像上传成功================'"
  29. sh "echo '================删除本地镜像================'"
  30. //删除本地镜像防止占用资源
  31. sh "docker rmi ${env.IMAGE_NAME}:${env.TAG}"
  32. }
  33. }
  34. stage('Execute service') {
  35. //请求堡垒机内的发布服务,具体代码后面会给出
  36. steps {
  37. //以下整个脚本都依赖jenkins插件:HTTP Request
  38. //将body转换为json
  39. script {
  40. def toJson = {
  41. input ->
  42. groovy.json.JsonOutput.toJson(input)
  43. }
  44. //body定义,根据实际情况而定
  45. def body = [
  46. imageName: "${env.IMAGE_NAME}",
  47. tag:"${env.TAG}",
  48. port:"${env.SERVER_PORT}",
  49. simpleImageName: "${env.SERVICE_NAME}",
  50. envs: [
  51. JVM_ARGS: "${env.JVM_ARG}"
  52. ],
  53. volumes: ["${env.MAIN_VOLUME}:${env.LOG_DIR}"]
  54. ]
  55. sh "echo '================开始调用目标服务器发布================'"
  56. response = httpRequest acceptType: 'APPLICATION_JSON', consoleLogResponseBody: true, contentType: 'APPLICATION_JSON', httpMode: 'POST', requestBody: toJson(body), responseHandle: 'NONE', url: "${env.REMOTE_EXECUTE_HOST}"
  57. sh "echo '================结束调用目标服务器发布================'"
  58. }
  59. }
  60. }
  61. }
  62. }

远程堡垒机发布服务

远程发布服务其实是一个很简单的执行脚本的服务

ShellRequestDTO.java

  1. package com.winterchen.jenkinsauto.dto;
  2. import javax.validation.constraints.NotBlank;
  3. import java.util.List;
  4. import java.util.Map;
  5. public class ShellRequestDTO {
  6. @NotBlank
  7. private String imageName;
  8. @NotBlank
  9. private String tag;
  10. @NotBlank
  11. private String simpleImageName;
  12. @NotBlank
  13. private String port;
  14. /** * 环境变量列表 */
  15. private Map<String, String> envs;
  16. private List<String> volumes;
  17. public String getImageName() {
  18. return imageName;
  19. }
  20. public void setImageName(String imageName) {
  21. this.imageName = imageName;
  22. }
  23. public String getTag() {
  24. return tag;
  25. }
  26. public void setTag(String tag) {
  27. this.tag = tag;
  28. }
  29. public String getPort() {
  30. return port;
  31. }
  32. public void setPort(String port) {
  33. this.port = port;
  34. }
  35. public String getSimpleImageName() {
  36. return simpleImageName;
  37. }
  38. public void setSimpleImageName(String simpleImageName) {
  39. this.simpleImageName = simpleImageName;
  40. }
  41. public Map<String, String> getEnvs() {
  42. return envs;
  43. }
  44. public void setEnvs(Map<String, String> envs) {
  45. this.envs = envs;
  46. }
  47. public List<String> getVolumes() {
  48. return volumes;
  49. }
  50. public void setVolumes(List<String> volumes) {
  51. this.volumes = volumes;
  52. }
  53. @Override
  54. public String toString() {
  55. final StringBuilder sb = new StringBuilder("ShellRequestDTO{");
  56. sb.append("imageName='").append(imageName).append('\'');
  57. sb.append(", tag='").append(tag).append('\'');
  58. sb.append(", simpleImageName='").append(simpleImageName).append('\'');
  59. sb.append(", port='").append(port).append('\'');
  60. sb.append(", envs=").append(envs);
  61. sb.append(", volumes=").append(volumes);
  62. sb.append('}');
  63. return sb.toString();
  64. }
  65. }

APIResponse.java

  1. package com.winterchen.jenkinsauto.dto;
  2. public class APIRespose<T> {
  3. private Integer code;
  4. private T data;
  5. private String message;
  6. private Boolean success;
  7. public static APIRespose success(){
  8. APIRespose apiRespose = new APIRespose();
  9. apiRespose.setCode(200);
  10. apiRespose.setSuccess(true);
  11. return apiRespose;
  12. }
  13. public static APIRespose success(Object data) {
  14. APIRespose apiRespose = new APIRespose();
  15. apiRespose.setCode(200);
  16. apiRespose.setSuccess(true);
  17. apiRespose.setData(data);
  18. return apiRespose;
  19. }
  20. public static APIRespose fail(String message) {
  21. APIRespose apiRespose = new APIRespose();
  22. apiRespose.setCode(500);
  23. apiRespose.setSuccess(false);
  24. apiRespose.setMessage(message);
  25. return apiRespose;
  26. }
  27. //get,set省略
  28. }

BaseController.java

  1. package com.winterchen.jenkinsauto.controller;
  2. import com.winterchen.jenkinsauto.dto.APIRespose;
  3. import com.winterchen.jenkinsauto.dto.ShellRequestDTO;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.validation.annotation.Validated;
  7. import org.springframework.web.bind.annotation.PostMapping;
  8. import org.springframework.web.bind.annotation.RequestBody;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RestController;
  11. import java.io.BufferedReader;
  12. import java.io.IOException;
  13. import java.io.InputStreamReader;
  14. import java.text.MessageFormat;
  15. import java.util.List;
  16. import java.util.Map;
  17. @RestController
  18. @RequestMapping("/shell")
  19. public class BaseController {
  20. private static final Logger LOGGER = LoggerFactory.getLogger(BaseController.class);
  21. @PostMapping("")
  22. public APIRespose executeShell(
  23. @RequestBody
  24. @Validated
  25. ShellRequestDTO requestDTO
  26. ) {
  27. LOGGER.info("当前请求参数:" + requestDTO.toString());
  28. StringBuilder sb = new StringBuilder();
  29. try {
  30. doExecuteShell(requestDTO, sb);
  31. return APIRespose.success(sb.toString());
  32. } catch (Exception e) {
  33. return APIRespose.fail(e.getMessage());
  34. }
  35. }
  36. private synchronized void doExecuteShell(ShellRequestDTO requestDTO, StringBuilder sb) throws Exception{
  37. //停止旧的容器
  38. stopContainer(requestDTO.getSimpleImageName(), sb);
  39. //stopContainerByImageId(requestDTO.getImageName(), sb);
  40. //删除旧的容器
  41. removeContainer(requestDTO.getSimpleImageName(), sb);
  42. //removeContainerByImageId(requestDTO.getImageName(),sb);
  43. //删除旧的镜像
  44. removeImage(requestDTO.getImageName(), sb);
  45. removeNoneImages(sb);
  46. //拉取最新的镜像
  47. pullImage(requestDTO.getImageName(), requestDTO.getTag(), sb);
  48. //运行最新镜像
  49. runImage(requestDTO, sb);
  50. /** * TODO 扩展:如果需要等服务可用再返回,可以在服务中增加一个检查接口,循环的调用该接口直至成功返回为止(注意需要有超时机制,如果服务启动失败会一直进入死循环) * TODO 该拓展主要是为了集成测试准备,自动化测试脚本运行的前提是该服务可以正常提供服务 */
  51. }
  52. private void pullImage(String imageName, String tag, StringBuilder sb) throws Exception{
  53. execute(MessageFormat.format("docker pull {0}:{1}", imageName, tag), sb);
  54. }
  55. private void stopContainer(String simpleImageName, StringBuilder sb) {
  56. try {
  57. execute("docker stop " + simpleImageName, sb);
  58. } catch (Exception e) {
  59. LOGGER.error("停止容器失败", e);
  60. }
  61. }
  62. private void removeImage(String imageName, StringBuilder sb) {
  63. try {
  64. execute("docker rmi -f " + imageName, sb);
  65. } catch (Exception e) {
  66. LOGGER.error("删除镜像失败", e);
  67. }
  68. }
  69. private void removeNoneImages(StringBuilder sb) {
  70. try{
  71. execute("docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}'", sb);
  72. execute("docker stop $(docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}')", sb);
  73. execute("docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}'",sb);
  74. execute("docker rm $(docker ps -a | grep `docker images -f 'dangling=true' -q` | awk '{print $1}')", sb);
  75. execute("docker images -f 'dangling=true'|awk '{print $3}'", sb);
  76. execute("docker image rm -f `docker images -f 'dangling=true'|awk '{print $3}'`", sb);
  77. } catch (Exception e) {
  78. LOGGER.error("删除none镜像失败", e);
  79. }
  80. }
  81. private void removeContainer(String simpleImageName, StringBuilder sb) {
  82. try {
  83. execute("docker rm " + simpleImageName, sb);
  84. } catch (Exception e) {
  85. LOGGER.error("删除容器失败", e);
  86. }
  87. }
  88. private void runImage(ShellRequestDTO requestDTO, StringBuilder sb) throws Exception{
  89. StringBuilder shell = new StringBuilder();
  90. shell.append("docker run -p ").append(requestDTO.getPort()).append(":").append(requestDTO.getPort());
  91. shell.append(" --network=host ");
  92. shell.append(" --name=").append(requestDTO.getSimpleImageName());
  93. shell.append(" -d ");
  94. formatEnv(requestDTO.getEnvs(), shell);
  95. formatVolumes(requestDTO.getVolumes(), shell);
  96. shell.append(requestDTO.getImageName()).append(":").append(requestDTO.getTag());
  97. execute(shell.toString(), sb);
  98. }
  99. private void formatVolumes(List<String> volumes, StringBuilder shell) {
  100. if (volumes == null || 0 == volumes.size()) {
  101. return;
  102. }
  103. volumes.forEach(volume -> {
  104. shell.append(" -v ");
  105. shell.append(" '").append(volume).append("' ");
  106. });
  107. }
  108. private void formatEnv(Map<String, String> env, StringBuilder shell) {
  109. if (env == null || env.isEmpty()) {
  110. return;
  111. }
  112. for (Map.Entry<String, String> entry : env.entrySet()) {
  113. shell.append(" -e ");
  114. shell.append(entry.getKey()).append("='").append(entry.getValue()).append("' ");
  115. }
  116. }
  117. private void execute(String command, StringBuilder sb) throws Exception {
  118. BufferedReader infoInput = null;
  119. BufferedReader errorInput = null;
  120. try {
  121. LOGGER.info("======================当前执行命令======================");
  122. LOGGER.info(command);
  123. LOGGER.info("======================当前执行命令======================");
  124. //执行脚本并等待脚本执行完成
  125. String[] commands = { "/bin/sh", "-c", command };
  126. Process process = Runtime.getRuntime().exec(commands);
  127. //写出脚本执行中的过程信息
  128. infoInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
  129. errorInput = new BufferedReader(new InputStreamReader(process.getErrorStream()));
  130. String line = "";
  131. while ((line = infoInput.readLine()) != null) {
  132. sb.append(line).append(System.lineSeparator());
  133. LOGGER.info(line);
  134. }
  135. while ((line = errorInput.readLine()) != null) {
  136. sb.append(line).append(System.lineSeparator());
  137. LOGGER.error(line);
  138. }
  139. //阻塞执行线程直至脚本执行完成后返回
  140. process.waitFor();
  141. } finally {
  142. try {
  143. if (infoInput != null) {
  144. infoInput.close();
  145. }
  146. if (errorInput != null) {
  147. errorInput.close();
  148. }
  149. } catch (IOException e) {
  150. }
  151. }
  152. }
  153. }

相关资源

  • docker rest api 官方文档
  • jenkins 官方文档
  • docker 官方文档

微信公众号:CodeD
微信公众号

发表评论

表情:
评论列表 (有 0 条评论,122人围观)

还没有评论,来说两句吧...

相关阅读