Skip to content

jenkins 部署手册

本文档的作用在于讲解后端项目的持续集成部署方案。基于jenkins多分支流水线来构建。

多分支流水线介绍

  • 提供了一种自动化的方式来构建、测试和部署软件。它可以根据预定义的规则,自动触发构建过程,减少了手动操作的需求,节省了开发人员的时间和精力。
  • 允许定义多个不同的阶段和步骤,以适应不同的构建和部署需求。可以根据项目的特定要求,自定义流水线的不同阶段,并在需要时添加或删除步骤。
  • 提供了一个可视化的用户界面,可以清楚地展示整个构建和部署过程,以及每个阶段和步骤的执行状态。这使得开发团队可以更好地跟踪和监控整个流程的进展和结果。
  • 记录了每次构建的详细信息,包括构建的结果、所用的代码版本、构建的时间等。这些信息可以帮助开发团队追溯问题,分析失败的构建,并作出相应的调整和改进。
  • 可以与各种不同的插件和工具集成,以满足复杂的构建和部署需求。通过与其他工具的集成,可以进一步扩展和定制流水线的功能和能力。

环境准备

  • jenkins docker 安装
bash
docker pull jenkinsci/blueocean
bash
docker run -it --name jenkins -p 9090:8080 -p 60000:50000 -v jenkins-data:/var/jenkins_home -v /data/web-data/docker.sock:/var/run/docker.sock jenkinsci/blueocean

默认安装了Blue Ocean插件

  • 插件安装
    • Extended Choice Parameter Plug-In
    • SSH-Agent
    • Maven Integration plugin 使用 maven 构建时需要
    • Input-Step

快速创建一个多分支流水线任务

在使用jenkins前,需要安装环境准备阶段提到的插件。接下来我们先创建一个多分支流水线任务。

jenkins_18.png

参照下图进行配置

jenkins_19.png

jenkins_20.png

jenkins_21.png

这样配置完就创建了一个流水线任务。

写在前面

接下来重点讲下如何使用jenkins流水线来构建后端Java服务。这里以基础商城为例。先来看几张效果图: jenkins_1.pngjenkins_2.png 从图中可以看到,我们只要配置了gitjenkins便能抓取到对应的分支,点击具体分支又能看到流水线的步骤,这又是怎么做到的呢?它是通过下面的文件来生效的:

Jenkinsfile 文件

bash
pipeline {
    agent any
    tools {
        maven '3.6.3'
    }
    environment {
        _version = "1.0"
    }
    parameters {
        extendedChoice(
            name: 'mode',
            description: '请选择部署方式,deploy 部署,restart 重启,stop 停止',
            type: 'PT_SINGLE_SELECT',
            value: "${modeList}"
        )
        extendedChoice(
            name: 'env',
            description: '请选择一个环境进行部署',
            type: 'PT_SINGLE_SELECT',
            value: "${envList}"
        )
        extendedChoice(
            name: 'moduleName',
            description: '请选择一个模块进行构建,按住 ctrl 可多选',
            type: 'PT_MULTI_SELECT',
            value: "${projectList}"
        )
        booleanParam(name: 'cleanMaven', defaultValue: "${cleanMaven}", description: '是否需要让 maven 每次都 clean 工程?')
        booleanParam(name: 'forceUpdateMaven', defaultValue: "${forceUpdateMaven}", description: '是否需要让 maven 强制更新拉取最新 jar?')
        booleanParam(name: 'offlineMaven', defaultValue: "${offlineMaven}", description: '是否需要让 maven 离线构建?')
        string(
            description: '请填写 jdk 启动参数',
            name: 'jdkArgs',
            defaultValue: "${jdkArgs}"
        )
        string(
            description: '请填写 spring 启动参数,例如 --spring.profiles.active=dev',
            name: 'springArgs',
            defaultValue: "${springArgs}"
        )
        string(
            description: '请填写 nohup 部署参数,当前只支持指定日志输出目录,默认为 /dev/null。\n 但是请注意,指定了输出目录后,Jenkins 发布成功后会出现无法退出的情况,该参数请在排查问题时指定',
            name: 'nohupArgs',
            defaultValue: "${nohupArgs}"
        )
        booleanParam(name: 'isAll', defaultValue: "${isAll}", description: '是否需要全量发布?第一次发布请选择全量')
        text(name: 'includeJar', defaultValue: "${includeJar}", description: '增量发布要包含的 jar 包')
    }
    stages {
        stage('init') {
            steps {
                echo "当前版本:${_version}"
                script {
                    currentSelectModuleNames = params.moduleName.split(',').collect { it }
                }
            }
        }
        stage('build') {
            when {
                expression { params.mode == "deploy" }
            }
            steps {
                script {
                    echo "开始打包${params.moduleName}模块"
                    def forceUpdate = params.forceUpdateMaven ? "-U" : ""
                    def offline = params.offlineMaven ? "-o" : ""
                    def clean = params.cleanMaven ? "clean" : ""
                    sh "mvn -T 1C ${clean} ${offline} -Dmaven.test.skip=true ${forceUpdate} package -P ${params.env} -am -pl ${params.moduleName}"
                    echo '打包成功'
                }
            }
        }
        stage('zip') {
            when {
                expression { params.mode == "deploy" }
            }
            steps {
                zipParallel items: currentSelectModuleNames
            }
        }
        stage('线上部署') {
            when {
                beforeInput true
                expression { params.env == "prod" }
            }
            steps {
                timeout(time: 60, unit: 'SECONDS') {
                    script {
                        println '等待用户确认,60秒后无确认将自动取消'
                        def approvalMap = input(
                            message: "确定要部署到线上环境吗?",
                            ok: "确定",
                            id: "${project.id}",
                            submitter: "yanfa",
                            submitterParameter: "submitUser"
                        )
                        println "输入完成 ${approvalMap}"
                    }
                }
            }
        }
        stage('deploy') {
            steps {
                deployParallel (items: currentSelectModuleNames, projectName: project.name, projectTargetDir: project.targetDir)
            }
        }
    }
}

它的格式就是这样的,没有什么好说的。只能通过官网来看。支持使用groovy语言来做一些更高级的定制。这个文件存在于java项目的根目录之中。如下图:

jenkins_3.png

其中,可以看到parameters指令的作用效果如下图:

jenkins_4.png

也就是说,通过这个指令我们可以添加更多个性化的参数构建需求。但同时,我们看到上面配置文件中parameters参数节点的value使用了${}变量。那这部分又是怎么生效的呢?是这样的,为了更好的控制节点变量值,这部分配置被提取成一段json。配置如下:

json
json_str = '''
{
    "env": ["dev", "test", "prod"],
    "deploy": {
        "dev": {
            "host": "root@xx.xx.xx.xx",
            "credentials": "xxxxxx-2715-4cd0-ada6-693263418623"
        },
        "test": {
            "host": "root@xx.xx.xx.xx",
            "credentials": "xxxxxx-2715-4cd0-ada6-693263418623"
        },
        "prod": {
            "host": "root@xx.xx.xx.xx",
            "credentials": "xxxxxx-2715-4cd0-ada6-693263418623"
        }
    },
    "mode": ["deploy", "restart", "stop"],
    "project": {
        "id": "tlDcDhD6gymbjXmp",
        "name": "general-mall",
        "list": ["framework-admin", "framework-web"],
        "targetDir": "/var/workspace/yanfazhongxin/general-mall"
    },
    "options": {
        "maven": {
            "forceUpdate": false,
            "offline": true,
            "clean": false
        },
        "isAll": false,
        "jdkArgs" : "-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseConcMarkSweepGC -Duser.timezone=Asia/Shanghai -noverify",
        "springArgs": "",
        "nohupArgs": "/dev/null",
        "includeJar": "framework-dao-1.0-SNAPSHOT.jar\nframework-service-1.0-SNAPSHOT.jar\nframework-common-1.0-SNAPSHOT.jar\nframework-core-1.0-SNAPSHOT.jar\nframework-api-1.0-SNAPSHOT.jar"
    }
}
'''

通过维护json配置,来达到变更配置的目的。那流水线的步骤又是怎么来的呢?它是通过stage step来展示流水线步骤的,如下:

bash
stages {
    stage('init') {
       steps {
           echo "当前版本:${_version}"
           script {
               currentSelectModuleNames = params.moduleName.split(',').collect { it }
           }
       }
    }
    stage('build') {
        when {
            expression { params.mode == "deploy" }
        }
        steps {
            script {
                echo "开始打包${params.moduleName}模块"
                def forceUpdate = params.forceUpdateMaven ? "-U" : ""
                def offline = params.offlineMaven ? "-o" : ""
                def clean = params.cleanMaven ? "clean" : ""
                sh "mvn -T 1C ${clean} ${offline} -Dmaven.test.skip=true ${forceUpdate} package -P ${params.env} -am -pl ${params.moduleName}"
                echo '打包成功'
            }
        }
    }
    stage('zip') {
        when {
            expression { params.mode == "deploy" }
        }
        steps {
            zipParallel items: currentSelectModuleNames
        }
    }
    stage('线上部署') {
        when {
            beforeInput true
            expression { params.env == "prod" }
        }
        steps {
            timeout(time: 60, unit: 'SECONDS') {
                script {
                    println '等待用户确认,60秒后无确认将自动取消'
                    def approvalMap = input(
                        message: "确定要部署到线上环境吗?",
                        ok: "确定",
                        id: "${project.id}",
                        submitter: "yanfa",
                        submitterParameter: "submitUser"
                    )
                    println "输入完成 ${approvalMap}"
                }
            }
        }
    }
    stage('deploy') {
        steps {
            deployParallel (items: currentSelectModuleNames, projectName: project.name, projectTargetDir: project.targetDir)
        }
    }
}

效果如下图:

jenkins_5.png

这其中,比较重要的是,要怎么获取用户选择的参数呢?通过如下的方式来获取:

bash
${params.xxx} --xxx 为 parameters 中控件的 name

部署方式

json
"mode": ["deploy", "restart", "stop"]
  • deploy 每次都会根据选择的环境和应用启动参数来执行部署,默认:deploy
  • restart 仅仅只是根据选择的环境和应用启动参数来重启应用
  • stop 仅仅只是杀掉进程

最终的效果是这样的:

jenkins_6.png

环境定义

在 json 配置里存在着如下的配置:

json
"env": ["dev", "test", "prod"]

最终的效果是这样的:

jenkins_7.png

所以,当你需要在定义一个新的环境的时候,可以去调整对应的json配置节点。

编译

编译环节没有什么好介绍的了。根据选择的模块来构建。如下:

bash
stage('build') {
    when {
        expression { params.mode == "deploy" }
    }
    steps {
        script {
            echo "开始打包${params.moduleName}模块"
            def forceUpdate = params.forceUpdateMaven ? "-U" : ""
            def offline = params.offlineMaven ? "-o" : ""
            def clean = params.cleanMaven ? "clean" : ""
            sh "mvn -T 1C ${clean} ${offline} -Dmaven.test.skip=true ${forceUpdate} package -P ${params.env} -am -pl ${params.moduleName}"
            echo '打包成功'
        }
    }
}

重点说明下,这里提供了三个参数来控制 maven 的编译。它们分别是:

bash
"options": {
    "maven": {
       "forceUpdate": false,
       "offline": true,
       "clean": false
    }   
}
  • forceUpdate 是否需要让 maven 强制更新拉取最新 jar
  • offline 是否需要让 maven 离线构建
  • clean 是否需要让 maven 每次都 clean 工程

通过这三个参数的组合,能大大的提高编译的速度。

效果如下图:

jenkins_8.png

可以看到在优化前,常规构建的时间需要近50秒,极端的需要1分多种,偶尔快的话,可以36秒。优化后,可以看到,8秒便构建成功了。

全量压缩与增量压缩

全量与增量的效果如下图:

jenkins_9.png

它是通过下面json配置段来控制的:

bash
"options": {
    "isAll": false
}

从图中的描述可以看出,第一次发布需要使用全量。后续版本发布,没有特殊引入包的情况下,均可以使用增量发布(默认增量)。因为增量发布比较快速,只需要几秒钟就可以发布成功。全量的话,打完包超过100M。发布速度可想而知快不了。那增量又该怎么配置呢?它也是通过如下的json配置段来控制的:

bash
"options": {    
    "includeJar": "framework-dao-1.0-SNAPSHOT.jar\nframework-service-1.0-SNAPSHOT.jar\nframework-common-1.0-SNAPSHOT.jar\nframework-core-1.0-SNAPSHOT.jar\nframework-api-1.0-SNAPSHOT.jar"
}

实际上这里配置的是jar的名称,用\n换行符来分隔。也就是当你引入新的jar时,如果你能确定变更的jar,便可以在发布的时候指定,如果不确定,便就是发布全量。 效果如下图:

jenkins_22.png

发布

发布到底是将服务部署到哪里去呢?首先在如下json配置段:

bash
"deploy": {
    "dev": {
        "host": "root@xx.xx.xx.xx",
        "credentials": "98ea5f9a-2715-4cd0-ada6-693263418623"
    },
    "test": {
        "host": "root@xx.xx.xx.xx",
        "credentials": "98ea5f9a-2715-4cd0-ada6-693263418623"
    },
    "prod": {
        "host": "root@xx.xx.xx.xx",
        "credentials": "6fa9cd30-e20a-4044-9b7f-b81eee47408f"
    }
}

这里定义了与环境相匹配的部署信息。其中:

  • host 部署的主机账号和ip
  • credentials 连接服务器的凭据ID。那这个信息要从哪里来呢? 一开始我们在环境准备阶段安装了ssh-agent插件,后续会通过这个插件来连接远端服务器,然后做发布。而他们之间的通信为了安全,我们选择公私钥的方式进行连接。首先在jenkins部署的机子上执行如下代码:
bash
su -s /bin/bash jenkins
ssh-keygen -t rsa -b 4096
cd /root/.ssh
ls

最终会生成 ssh private key、public key 。id_rsa为 private key,id_rsa.pub为public key。 然后再执行如下代码:

bash
ssh-copy-id -i ~/.ssh/id_rsa.pub xx@x.x.x.x

🎉 格式一般为 root@192.168.0.1 将公钥分发到指定的账号主机上。这样完了后,便可以使用ssh-agent免密登录对应的主机。这样完了后,还需要再jenkins上添加对应的凭证。如下图:

jenkins_10.png

类型选择 ssh username with private key。填写主机对应的 username,再将前面生成id_rsa内容复制到 private key 处。保存后ID的值便是我们前面说的"credentials"连接服务器的凭据ID!!! 这些都有了后,便可以根据选择的环境发布到对应的主机了。而项目的相关配置在如下json配置段:

bash
"project": {
    "id": "tlDcDhD6gymbjXmp",
    "name": "general-mall",
    "list": ["framework-admin", "framework-web"],
    "targetDir": "/var/workspace/yanfazhongxin/general-mall"
}
  • name 项目名称
  • list 项目列表
  • targetDir 项目部署路径

而比较特别的是,当在部署prod环境时,会提供一个交互式应答,发布者必须点击确认才能继续发布,如下图:

jenkins_11.png

服务部署后的服务名称为如下三个元素的组合:

bash
${projectName}-${moduleName}-${env}

例如:general-mall-framework-web-test

代码漏洞扫描

通过集成murphysec插件可以很方便对项目的代码进行漏洞扫描。首先登录到墨菲控制台,如图复制访问令牌。 jenkins_12.png 接着进入 cli 集成界面。复制linux下的命令行。直接在jenkins宿主机安装。

bash
wget -q https://s.murphysec.com/release/install.sh -O - | /bin/bash

安装完后执行。

bash
murphysec auth login

根据提示粘贴访问令牌。 如果不和jenkins进行集成的话,到这里就安装完了。可以体验下如下命令:

bash
murphysec scan 项目路径

关键步骤:先执行下命令

bash
whereis murphysec

默认安装在

bash
/usr/local/bin/murphysec

调整下目录路径

bash
mv /usr/local/bin/murphysec /usr/bin/murphysec

由于jenkins是通过docker安装的。可以进行这样操作:

bash
docker cp /usr/bin/murphysec jenkins:/root/murphysec

进入 docker 容器

bash
docker exec -it jenkins bash

为文件赋予权限

bash
cd /root
chmod +x murphysec

配置完成后。可以在jenkins里配置下访问令牌的存储。 找到系统管理 -> manager credentials -> 添加凭证 jenkins_13.png 将访问令牌粘贴到这里。 最终效果: jenkins_14.png 最后 jenkinsFile 文件里增加一个流水线即可

bash
stage("漏洞扫描") {
    when {
        expression { params.mode == "security check" }
    }
    environment {
        API_TOKEN = credentials('murphysec-token')
    }
    steps {
        sh '''
            murphysec scan .
        '''
    }
}

其他

bash
"options": {
    "isAll": false,
    "jdkArgs" : "-Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -Duser.timezone=Asia/Shanghai -noverify",
    "springArgs": "",
    "nohupArgs": "/dev/null",
    "includeJar": "framework-dao-1.0-SNAPSHOT.jar\nframework-service-1.0-SNAPSHOT.jar\nframework-common-1.0-SNAPSHOT.jar\nframework-core-1.0-SNAPSHOT.jar\nframework-api-1.0-SNAPSHOT.jar"
}
  • isAll 是否全量
  • jdkArgs 部署或重启时可以指定 jdk 运行参数
  • springArgs 部署或重启时可以指定 spring 启动参数
  • nohupArgs 部署时排查错误,可临时指定为某个路径地址
  • includeJar 增量发布所包含的jar

并行构建

并行构建到底并行构建什么呢?我理解的话,是打包阶段,部署阶段能并行就ok了。那我们应该要怎么改呢?先来看下现在的效果图:

jenkins_15.png

  • 需要将模块由下拉单选改成可以多选 只需要 PT_SINGLE_SELECT 改成 PT_MULTI_SELECT 即可
  • 需要处理选取的模块
  • 流水线打包编译阶段需要支持并行操作
bash
def currentSelectModuleNames = params.moduleName.split(',').collect { it }

定义一个变量来表示当前选择的模块。 Maven 编译命令是支持多模块构建的:

bash
mvn clean -Dmaven.test.skip=true package -P ${params.env} -am -pl ${params.moduleName}

接下来部署阶段只要这样做就行了:

bash
stage('deploy') {
    steps {
        deployParallel (items: currentSelectModuleNames, projectName: project.name, projectTargetDir: project.targetDir)
    }
}

void deployParallel(args) {
    def deploy = getConfig("deploy")
    def deployEnv = deploy[params.env]
    parallel args.items.collectEntries { name -> [ "${name}": {
            stage("deploy ${name}") {
                echo "开始部署${name}模块,${params.env}环境"
                remoteDeploy(deployEnv.host, deployEnv.credentials, args.projectName, args.projectTargetDir, name)
                echo "部署成功"
            }
        }]
    }
}

增加parallel关键字即可。最后的效果图如下:

jenkins_16.png

jenkins_17.png

常见问题

  • jenkins运行后程序没有启动 解决方法:调整 nohup 命令中的 /dev/null 为某个路径地址,再次发布。查看该文件内容,如果错误内容为 nohup: failed to run command 'java': No such file or directory。则可以运行如下命令:
bash
# 找到安装位置
find / -name "java" 
# 切换到该目录
cd /usr/bin 
# 建立软链接,替换目录为上面找到的目录
ln -s /usr/local/jdk1.8.0_311/bin/java /usr/bin/java

重新执行完后,可重新在发布。

  • jenkins发布后长时间没有退出,一直 loading 解决方法:查看 nohup 命令中 /dev/null 是否修改为其他路径了,因为你一旦改成其他路径后,jenkins发布完成后会出现无法正确退出的情况,这是一个 bug。目前只能在调整成 /dev/null 规避这个问题。