• 基于Jenkins实现的CI/CD方案


    基于Jenkins实现的CI/CD方案

    前言

    最近基于Jenkins的基座,搭建了一套适用于我们项目小组的持续集成环境。现在把流程整理分享出来,希望可以给大家提供一些帮助和思路。

    使用到的组件和版本

    组件名称组件版本作用
    Harbor2.7.3镜像仓库
    Jenkins2.319.2持续集成工具
    Pipeline2.6Jenkins插件,编排流水线脚本
    SSH Pipeline Steps2.0.0Jenkins插件,提供远程执行ssh能力
    Git4.10.1Jenkins插件,提供拉取git代码仓库的能力
    Httpd2.4.18HTTP服务器,用于归档编译后的软件包,镜像包
    Maven3.6.3后端代码构建工具
    Node8.17.0前端代码构建工具
    Docker20.10.0容器版本
    Docker Composev2.18.1容器编排

    我这边是基于Jenkins的Pipeline+Docker的方式进行的任务编排,Jenkins是找的一个别人做好的,内置了绝大多数插件的容器版本,链接地址:https://hub.docker.com/r/h1kkan/jenkins-docker。这个做好的镜像里面没有SSH Pipeline Steps这个插件,需要自己额外下载一下,这边需要注意一下插件和Jenkins的对应版本。

    基本流程图


    这边有几个需要注意的地方,简单说明一下

    1. 首先需要配置代码仓库的webhook,这个网上有很多资料,可以自行参考配置一下

    2. dockerfile,docker-compose.yml这些文件需要内置在代码仓库中或者服务器内(我们是dockerfile文件内置在代码仓库,docker-compose.yml文件放在服务器里面固定目录)

    3. 在第五步,执行远端ssh时,需要去更改docker-compose.yml中的image节点,一开始准备用shell去做的,但是实现有点难度,然后就内置了一个python脚本,用yaml这个库去实现的,具体脚本updateImageLabel.py参考:

      import yaml
      import sys
      
      filename = sys.argv[1]
      # 加载docker-compose.yml文件
      with open(filename,'r') as f:
          data = yaml.load(f)
      # 更新镜像标签
      data['services'][sys.argv[2]]['image'] = sys.argv[3]
      with open(filename, 'w') as yaml_file:
          yaml_file.write(yaml.dump(data, default_flow_style=False))
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      使用时,传入三个参数,分别是docker-compose.yml文件路径,需要更新的服务,更新后的镜像标签

      python3 updateImageLabel.py ${dockerComposePath}  ${service}  ${image}
      
      • 1
    4. Harbor的安装可以参考我之前的博客,安装完成如果是http的话,还需要配置一下insecure-registries仓库信息,并且执行docker login命令登录镜像仓库,用于拉取Harbor仓库镜像

      {
        "insecure-registries": [
          "harbor服务器地址"
        ]
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
    5. 第七步是可选的,做这个是为了方便每一个版本的归档,这边可以归档软件包或者save之后的镜像包

    具体实现步骤

    下面具体写一下实现的步骤,因为涉及的东西很多,有一些能找到的通用的步骤我就先不写了,大家可以自行去百度或者Google。

    基础中间件部署

    这边只讲Jenkins和Httpd的部署,Harbor部署可以参考我之前的文档。

    Jenkins部署

    通过docker的方式拉起Jenkins

    docker run -u root -e TZ=Asia/Shanghai --name=jenkins -d -e TZ="Asia/Shanghai" -p 8080:8080 -p 50000:50000 -v /data/jenkins:/var/jenkins_home -v /run/docker.sock:/var/run/docker.sock -v /data/archive:/data/archive h1kkan/jenkins-docker:2.319.2
    
    • 1

    容器起来之后,需要进入到容器内部执行一下docker login的命令,在Jenkins容器内部也生成一套Harbor的凭证。

    第三个挂载目录是用来开给Httpd服务器的,用于归档软件包和镜像包。

    Httpd服务器部署

    通过docker拉起Httpd服务器

    docker run -p 8001:80 -v /data/archive:/usr/local/apache2/htdocs/ -d --name httpd httpd:2.4.18 
    
    • 1

    这边挂载目录就是Jenkins开出来的目录

    Maven、Node、JDK等基础镜像

    可以先从外网拉取对应版本,然后本地打成tar包之后上传到服务器解压,再tag之后推送到自己的Harbor仓库。

    在Jenkins配置Git凭证

    这个网上也很多,就不细致展开了,可以自己查一下

    Pipeline流水线编排

    后端流水线pipeline脚本
    import java.text.SimpleDateFormat
    import java.util.TimeZone
    
    // 构建版本
    def createVersion() {
        def simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss")
        simpleDateFormat.timeZone = TimeZone.getTimeZone("Asia/Shanghai")
        return simpleDateFormat.format(new Date()) + "_${env.branch}"
    }
    
    def getTime() {
        def simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss")
        simpleDateFormat.timeZone = TimeZone.getTimeZone("Asia/Shanghai")
        return simpleDateFormat.format(new Date())
    }
    
    // 获取远端服务器信息 
    def GetRemoteServer(ip, username, password) {
        def remote = [:]
        remote.name = ip
        remote.host = ip
        remote.user = username
        remote.password = password
        remote.allowAnyHosts = true
        return remote
    }
    
    pipeline {
        agent none
        environment {
            _version = createVersion()
            _time = getTime()
        }
        stages {
            stage('Git Checkout') {
                agent any
                steps {
                    checkout([
    	                $class: 'GitSCM', 
    	                branches: [[name: "${branch}"]], 
    	                doGenerateSubmoduleConfigurations: false, 
    	                extensions: [], 
    	                gitTool: 'Default', 
    	                submoduleCfg: [], 
    	                userRemoteConfigs: [[url: '{{git_url}}',credentialsId: '{{credentialsId}}',]]
                    ])
                }
            }
            stage('Source Package') {
                agent {
                    docker { 
                        image 'local-maven:3.6.3-openjdk-8' 
                        args '-v maven-repo:/usr/share/maven/ref'
                    }
                }
                steps {
                    sh 'mvn clean install -Dmaven.test.skip=true'
                }
            }
          
            stage('Build Image') {
                agent any
                steps {
                    sh 'docker build -f $WORKSPACE/CI/dockerfile --build-arg JARNAME=backend.jar -t 127.0.0.1:18080/library/backend:${_version} $WORKSPACE/backend/target/'
                    sh 'docker push 127.0.0.1:18080/library/backend:${_version}'
                    sh 'docker rmi 127.0.0.1:18080/library/backend:${_version}'
                }
            }
    
            stage('Publish To Env') {
                agent any
                steps {
                    script {
                        def remote = [:]
                        remote = GetRemoteServer('127.0.0.1', 'username', 'password')
                      	sshCommand remote: remote, command: "python3 updateImageLabel.py docker-compose.yml backend 127.0.0.1:18080/library/backend:${_version}"
                      	sshCommand remote: remote, command: "docker-compose -f docker-compose.yml up -d --build backend"
                    }
                }
            }
              
            stage('Archive Package') {
                agent any
                steps {
                    sh 'mkdir -p /data/archive/backend/${branch}/${_time}'
                    sh 'cp $WORKSPACE/backend/backend.jar /data/archive/backend/${branch}/${_time}'
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    解释一下这个脚本

    • 首选镜像的版本是时间戳+分支的格式,类似20231026095511_dev,可以根据项目组的要求进行生成。

    • GetRemoteServer方法主要是提供获取远端服务器的信息,主要使用了SSH Pipeline Steps这个插件的能力,传入ip,username,password三个参数,这边密码是明文写死在脚本里面的,这个插件也支持密码加密保存,如果安全性要求比较高的可以换成加密的方式。

    • Git Checkout 这个stage主要是进行代码的下载,根据${branch}这个参数来指定代码仓库的版本,参数的具体配置可以看下Jenkins参数化构建的相关指导和文章。

    • Source Package就是代码仓库的打包,这边指定使用Docker镜像作为执行的Agent,只要指定镜像和编译命令就可以。这边我还挂载了一个maven-repo的volume,主要是为了缓存Jar包,不用每次都去下载。

    • Build Image步骤进行代码的构建,通过docker-build命令去生成镜像,这边的dockerfile文件是内置在我们代码库中的,参考如下:

      FROM 127.0.0.1:18080/library/java:8.0
      ARG JARNAME
      COPY ${JARNAME} /data/
      
      • 1
      • 2
      • 3
    • Publish To Env 推送到环境,这边核心就是连接到远程服务器,通过修改镜像标签的脚本去更新docker-compose.yml文件中的镜像标签,然后重新构建容器

    • Archive Package 是否归档,演示的脚本里面只是归档了编译后的文件,还可以归档镜像等等。归档的路径为分支名/时间戳
      在这里插入图片描述

    前端流水线pipeline脚本
    import java.text.SimpleDateFormat
    import java.util.TimeZone
    
    // 构建版本
    def createVersion() {
        def simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss")
        simpleDateFormat.timeZone = TimeZone.getTimeZone("Asia/Shanghai")
        return simpleDateFormat.format(new Date()) + "_${env.branch}"
    }
    
    def getTime() {
        def simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss")
        simpleDateFormat.timeZone = TimeZone.getTimeZone("Asia/Shanghai")
        return simpleDateFormat.format(new Date())
    }
    
    // 获取远端服务器信息 
    def GetRemoteServer(ip, username, password) {
        def remote = [:]
        remote.name = ip
        remote.host = ip
        remote.user = username
        remote.password = password
        remote.allowAnyHosts = true
        return remote
    }
    
    pipeline {
        agent none
        environment {
            _version = createVersion()
            _time = getTime()
        }
        stages {
            stage('Git Checkout') {
                agent any
                steps {
                    checkout([
    	                $class: 'GitSCM', 
    	                branches: [[name: "${branch}"]], 
    	                doGenerateSubmoduleConfigurations: false, 
    	                extensions: [], 
    	                gitTool: 'Default', 
    	                submoduleCfg: [], 
    	                userRemoteConfigs: [[url: '{{git_url}}',credentialsId: '{{credentialsId}}',]]
                    ])
                }
            }
            stage('Source Package') {
                agent {
                    docker { 
                        image 'node:8.17.0' 
                    }
                }
                steps {
                    script {
                        sh 'npm install --registry=http://registry.npm.taobao.org'
                    }
                }
            }
            stage('Image Build') {
                agent any
                steps {
                    sh 'docker build -f $WORKSPACE/CI/dockerfile -t 127.0.0.1:18080/library/frontend:${_version} $WORKSPACE/target/'
                    sh 'docker push 127.0.0.1:18080/library/frontend:${_version}'
                    sh 'docker rmi 127.0.0.1:18080/library/frontend:${_version}'
                }
            }
            stage('Publish To Env') {
                agent any
                steps {
                    script {
                        def remote = [:]
                        remote = GetRemoteServer('127.0.0.1', 'username', 'password')
                        // 更新docker-compose.yml文件,修改镜像
                        sshCommand remote: remote, command: "python3 updateImageLabel.py docker-compose.yml frontend 127.0.0.1:18080/library/frontend:${_version}"
                        sshCommand remote: remote, command: "docker-compose -f docker-compose.yml up -d --build frontend"
    
                    }
                }
            }
            stage('Archive Package') {
                agent any
                steps {
                    sh 'mkdir -p /data/archive/frontend/${branch}/${environment}/${_time}'
                    sh 'cd $WORKSPACE/target/dist/ && zip -r dist.zip *'
                    sh 'cp $WORKSPACE/target/dist/dist.zip /data/archive/frontend/${branch}/${environment}/${_time}'
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    前端和后端除了构建方式不一样,其他基本都相同。

    结语

    参考地址:

    https://www.jenkins.io/doc/book/pipeline/

    https://www.jenkins.io/doc/pipeline/steps/ssh-steps/

  • 相关阅读:
    P1038 [NOIP2003 提高组] 神经网络 ( 拓扑
    登录Tomcat控制台,账号密码输入正确但点击登录没反应不跳转到控制台页面
    嵌入式分享合集46
    Springboot:静态资源映射方式
    Flink TaskManger 内存计算实战
    Linux 命令(142)—— hexdump 命令
    香港服务器在国内访问太慢怎么能提高?
    Tomcat项目启动报错
    IO进线程:进程间通信
    python机器人编程——用python实现一个写字机器人
  • 原文地址:https://blog.csdn.net/qq_32238611/article/details/136214185