使用Jenkins构建CI/CD之多分支流水线指北(实战)

6,461 阅读15分钟

手把手带你一起从0到1搭建一个企业级的自动化构建流程 网上完整的关于多分支流水线的配置很少,希望这篇不短的文章能给你带来帮助

简介

  • 从0到1打造前后端自动化工作流
  • 企业级的实践笔记
  • 最佳实践(可在此基础逐步完善)

缘起

由于公司的Jenkins配置没有部署成功的通知,在我学了几天的Jenkins后终于是对公司的Jenkins配置下手了,结果我刚装完dingtalk插件自动重启后,发现之前主管配置的构建项目数据都丢失了,害,正好给了我练手的机会,于是就有了以下从0到1的辛酸历程。

在Docker中安装并运行Jenkins

这里假设你的服务器已经装好了docker

使用的镜像是jenkinsci/blueocean,这是一个jenkins的稳定及持续维护的镜像源,本身就集成了Blue Ocean等使用插件,非常方便。

拉取镜像

docker pull jenkinsci/blueocean

运行Jenkins

docker run -idt --name kmywjenkins -p 9090:8080 -p 60000:50000 -v jenkins-data:/var/jenkins_home -v /data/web-data/docker.sock:/var/run/docker.sock jenkinsci/blueocean

参数解释:

-idt 以交互的方式、新建一个模拟终端运行容器

--name 容器的别名

-p 指定容器映射宿主机的端口 -> 宿主机端口:容器端口

-v jenkins-data:/var/jenkins_home Jenkins容器在工作的时候,如果要执行Docker的命令(例如 docker ps、docker run等),需要有个途径能连接到宿主机的docker服务,此参数就是用来建立容器和宿主机docker服务的连接的

-v /data/web-data/docker.sock:/var/run/docker.sock 将该容器的数据保留在宿主机的目录,这样即使容器崩溃了,里面的配置和任务都不会丢失

需要注意的是,docker中默认是以jenkins用户运行的Jenkins,如需以root用户可以加参数-u root,本示例未指定root。

访问Jenkins Docker容器

有时候需要进入Jenkins容器执行一些命令,可以通过docker exec命令访问,例如:docker exec -it [containerid] bash

若要手动重启Jenkins,可以执行以下命令:docker restart [containerid]

Jenkins基本配置

通过以上步骤,如果正常走到这里,可以通过以下地址访问http://121.41.16.183:9090/,ip地址为服务器的地址。

解锁jenkins

输入一下命令获取解锁的token,docker exec kmywjenkins cat /var/jenkins_home/secrets/initialAdminPassword

在浏览器中输入对应的token以解锁:

Unlock Jenkins page

创建凭据

连接git仓库,ssh连接服务器均需要相应的凭据,可以在凭据管理中先创建好,然后需要使用的地方直接选择凭据即可。这里以连接git、ssh需要的凭据为例:

  1. 我司用得版本管理工具是gitte,以gitte为例,其它版本管理工具配置也一样

​ 类型选择Username with password

​ 用户名密码为登录gitte的账号密码

​ ID是凭据的唯一标识,可自定义,后面在JenkinsFile中通过ID去引用凭据

image-20201015110502890

配置后的结果

image-20201015110924897

  1. ssh连接服务器时需要密钥,我们先在服务器生成一对公私钥,然后复制私钥,填入即可

    类型选择SSH Username with private key

    Username是连接服务器的用户名,如jenkins

    Private Key项选中Enter directly,点击Add,粘贴刚复制的私钥

image-20201015111417561

配置后的结果

image-20201015111823495

创建一个多分支流水线

之前的Jenkins任务是FreeStyle的方式创建的,这种方式不够灵活,界面也不够清爽,这里选择使用声明式流水线方式(Declarative Pipeline)创建,可以多分支独立构建,便于以后的扩展。

我们这里使用 BlueOcean 这种方式来完成此处 CI/CD 的工作,BlueOcean 是 Jenkins 团队从用户体验角度出发,专为 Jenkins Pipeline 重新设计的一套 UI 界面,仍然兼容以前的 fressstyle 类型的 job,BlueOcean 具有以下的一些特性:

  • 连续交付(CD)Pipeline 的复杂可视化,允许快速直观的了解 Pipeline 的状态
  • 可以通过 Pipeline 编辑器直观的创建 Pipeline
  • 需要干预或者出现问题时快速定位,BlueOcean 显示了 Pipeline 需要注意的地方,便于异常处理和提高生产力
  • 用于分支和拉取请求的本地集成可以在 GitHub 或者 Bitbucket 中与其他人进行代码协作时最大限度提高开发人员的生产力。

如果安装的是jenkinsci/blueocean镜像,默认是已经集成了BlueOcean,没有的可前往插件管理安装对应的插件。

install BlueOcean

点击打开Blue Ocean,可以看到已经创建好的两个流水线,分别是前端和后台,需要用到不同的工具,在后面会提到,如何创建流水线

image-20201015113510340

点击创建流水线

image-20201015113846536

我司用的是gitte,所以选择Git,然后填入要连接的仓库地址,需要连接到Git仓库的凭据,我们之前已经创建好了,直接选中即可,如果未创建,在下面的表单直接编辑即可,最后点击创建流水线

image-20201015114115860

到这里我们就创建了一个多分支流水线,Jenkins会扫描仓库,带有JenkinsFile的分支会被检测出来,JenkinFile是多分支流水线的配置文件,使用的是Groovy语法,可以直接点击创建流水线,Jenkins会自动为你的项目创建一个JenkinsFile

image-20201015114453409

现在可以可视化地编辑想要执行的阶段及步骤,这里加了一个打包的阶段,里面有个步骤是提示开始打包,点击保存

image-20201015115159132

填入提交信息,点击Save & Run,会讲JenkinsFile上传到git,并根据JenkinsFile执行一个构建任务,目前的构建步骤只有一个,是提示开始打包

image-20201015115411207

我这里不知道为什么会卡在这个地方不动,所以我在vscode直接创建并编辑JenkinsFile,这种方式更灵活,我更推荐这种方式,下面我会先简单介绍下JeninsFile的基础语法,仅包含本项目用到的,对于中小企业的构建需求,基本够用了。

JenkinsFile基础语法

只需先了解大致的语法,具体的用法会在后面说明

// 前端项目JenkinsFile配置,后端项目配置稍有不同,后面会区分说明
pipeline {
  agent any
  environment {
    HOST_TEST = 'root@121.41.16.183'
    HOST_ONLINE = 'jenkins@39.101.219.110'
    SOURCE_DIR = 'dist/*'
    TARGET_DIR = '/data/www/kuaimen-yunying-front'
  }
  parameters {
    choice(
      description: '你需要选择哪个环境进行部署 ?',
      name: 'env',
      choices: ['测试环境', '线上环境']
    )    
    string(name: 'update', defaultValue: '', description: '本次更新内容?')      
  }
  triggers {
    GenericTrigger(
     genericVariables: [
      [key: 'ref', value: '$.ref']
     ],
     causeString: 'Triggered on $ref',
     token: 'runcenter-front-q1w2e3r4t5',
     tokenCredentialId: '',
     printContributedVariables: true,
     printPostContent: true,
     silentResponse: false,
     regexpFilterText: '$ref',
     regexpFilterExpression: 'refs/heads/' + BRANCH_NAME
    )
  } 
  stages {
    stage('获取git commit message') {
     steps {
       script {
         env.GIT_COMMIT_MSG = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
       }
     }
  }
    
    stage('打包') {
      steps {
        nodejs('nodejs-12.16') {
          echo '开始安装依赖'
          sh 'yarn'
          echo '开始打包'
          sh 'yarn run build'
        }
      }
    }

    stage('部署') {
      when {
        expression {
          params.env == '测试环境'
        }
      }
      steps {
        sshagent(credentials: ['km-test2']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
          sh "scp -r ${SOURCE_DIR} ${HOST_TEST}:${TARGET_DIR}"
          sh 'echo "部署成功~"'
        }
      }
    }

    stage('发布') {
      when {
        expression {
          params.env == '线上环境'
        }
      }
      steps {
        sshagent(credentials: ['km-online']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
          sh "scp -r ${SOURCE_DIR} ${HOST_ONLINE}:${TARGET_DIR}"
          sh 'echo "发布成功~"'
        }
      }
    }
  }

  post {
    success {
      dingtalk (
        robot: '77d4c82d-3794-4583-bc7f-556902fee6b0',
        type: 'MARKDOWN',
        atAll: true,
        title: '你有新的消息,请注意查收',
        text:[
          '# 运营管理系统发布通知',
          '---',
          '#### **所属:前端**',
          "#### **构建任务:${env.BUILD_DISPLAY_NAME}**",
          "#### **Git commit:${env.GIT_COMMIT_MSG}**",
          "#### **本次更新内容:${params.update}**",
          "#### **部署环境:${params.env}**",
          '#### **构建结果:成功**'
        ]
      )
    }
  }
}

pipeline 必须在最外层

agent 定义了在哪个环境里执行,默认any

stages 阶段,标识构建流程的标签块,子节点是stage

steps 执行步骤

post 所有阶段执行完成后执行一些逻辑

when 可以控制该阶段是否执行

environment 环境变量,在这里定义的变量,JenkinsFile的任何地方都可以访问

tools 项目使用到的构建工具,声明系统配置中已经定义好的工具,如maven

parameters 定义参数,可以提供用户输入或者选择

post 构建结束后会执行这里,有successfailuresuccess,本示例将在success(构建成功时)发起钉钉通知

CI/CD流程

由于我司的技术团队较小,CI/CD流程就没那么复杂,不会包含代码检查、自动化测试、Code Review等流程,我将简要说明我所搭建的前端与后端CI/CD流程以及为什么这么搭建。

前端

提供两种构建方式,一种是代码上传自动构建,一种是参数化构建,可选择部署到测试环境还是线上环境。

自动构建默认部署到测试环境,由于线上环境很重要,自动化构建会有一定风险,所以需要人工干预选择参数进行构建。

  1. 提交代码到master,自动触发构建 如果是参数化构建,这一步是手动选择要构建的环境,然后开始构建
  2. 安装依赖
  3. 打包
  4. 上传到服务器
  5. 如果成功发起钉钉通知

后端

后端的所有项目都是放在一个git仓库中,所以就没有做自动构建

  1. 参数化构建 可选择要构建的环境、打包的项目、是否需要全量打包
  2. 清除旧数据
  3. 打包
  4. 上传到服务器
  5. 杀掉相应的进程
  6. 启动相应的进程
  7. 如果成功发起钉钉通知

接下来就每一步作详细说明,以及可能遇到的坑

自动触发构建

什么是自动触发构建

当我们提交新的代码到git仓库,Jenkins就会自动开始构建已经配置好的该项目的任务

原理

在git仓库配置一个Jenkins服务器的webhook地址,当git仓库有变动时会请求这个地址,Jenkins就能收到通知然后开始构建任务

配置

  1. 我们需要先安装一个插件Multibranch Scan Webhook Trigger,可进入插件管理搜索进行安装

  2. 进入项目的配置界面,勾选Scan by webhook,填入自定义token,需要确保token的唯一性,不会与其它项目的冲突 image-20201015145053900

  3. 过滤分支

这是一个多分支流水线,Jenkins默认会检出所有包含Jenkinsfile的分支,如果配置了webhook,就会自动触发相应分支的构建任务;有时候我们只想master发生变化后才去构建任务,这时就用到了过滤分支的配置,进入项目配置,在分支源git项找到add按钮并点击

image-20201021161744848

选择根据名称过滤(支持通配符),或者你可以选择根据名称过滤(支持正则表达式),效果一样,只是过滤格式不太一样,我这里在相应的地方填入master,即只检索master分支,这样就达到我们想要的效果了。

image-20201021161836184

  1. 进入远端仓库(我这里是Gitte),点击Webhooks,接着点击添加 WebHook image-20201015145511825

  2. 填入URL,IP地址为Jenkins部署的服务器,token为我们刚设置的,/multibranch-webhook-trigger/invoke 是固定地址,点击添加 image-20201015145820987

  3. 会自动发起一个请求,即我们刚填写的,如相应如下则表示配置成功,相应的构建任务也会自动执行 image-20201015150206085

自动化打包

前端

使用了yarn进行安装依赖及打包,需要先配置nodejs环境

  1. 进入插件管理搜索nodejs进行安装 image-20201015152417028
  2. 进入全局工具配置,新增如下配置,别名可以自定义,建议格式为nodejs-版本号,该项目用的是yarn,所以在Global npm package to install,加入了配置项,构建的时候会自动安装yarn,如果是npm可以忽略该配置 image-20201015152605860
  3. Jenkinsfile配置 前端的比较简单
pipeline {
   stage('打包') {
      steps {
        // 执行环境,nodejs-12.16是我们刚配置的别名,还有一种方式是在agent中配置执行环境,在tools中配置使用的包,感兴趣的可以自行研究
        nodejs('nodejs-12.16') {
          echo '开始安装依赖'
          sh 'yarn'
          echo '开始打包'
          sh 'yarn run build'
        }
      }
    }
}

后端(Java)

pipeline {
  tools {
        maven 'Maven3.6.3'
    }
  parameters {
        // 提供要部署的服务器选项
        choice(
            description: '你需要选择哪个环境进行部署 ?',
            name: 'env',
            choices: ['测试环境', '线上环境']
        ) 
        // 提供构建的模块选项
        choice(
            description: '你需要选择哪个模块进行构建 ?',
            name: 'moduleName',
            choices: ['kuaimen-contract', 'kuaimen-core', 'kuaimen-eureka-server', 'kuaimen-manage', 'kuaimen-member', 'kuaimen-order', 'kuaimen-shop', 'tiemuzhen-manage']
        )   
        booleanParam(name: 'isAll', defaultValue: false, description: '是否需要全量(包含clean && build)')     
        string(name: 'update', defaultValue: '', description: '本次更新内容?')    
    }
  
  stages {
    stage('全量清除旧数据...') {
            when {
                expression {
                    params.isAll == true
                }
            }
            steps {
                echo "开始全量清除"      
                sh "mvn package clean -Dmaven.test.skip=true"
            }
        }
        stage('全量打包应用') {
            when {
                expression {
                    params.isAll == true
                }
            }
            steps {
                echo "开始全量打包"   
                sh "mvn package -Dmaven.test.skip=true"
                echo '打包成功'
            }
        }
        stage('清除旧数据...') {
            when {
                expression {
                    params.isAll == false
                }
            }
            steps {
                echo "开始清除${params.moduleName}模块"      
                sh "cd ${params.moduleName} && mvn package clean -Dmaven.test.skip=true"
            }
        }
        stage('打包应用') {
            when {
                expression {
                    params.isAll == false
                }
            }
            steps {
                echo "开始打包${params.moduleName}模块"   
                sh "cd ${params.moduleName} && mvn package -Dmaven.test.skip=true"
                echo '打包成功'
            }
        }
  }
}

parameters

parameters中主要是提供参数化构建的选项,在其它地方可以通过"${params.isAll}"这种形式拿到用户的交互信息,配置后效果如下:

image-20201015155415457

when>expression表达式中的参数如果未true,则执行,反之跳过该stage

mvn在系统配置中默认就已经提供了该环境,进入系统全局工具配置,添加如下配置(类似nodejs)

image-20201015160714295

这种方式引用

tools {
  maven 'Maven3.6.3'
}

自动化部署

前端

pipeline {
  agent any
  environment {
    HOST_TEST = 'root@121.41.16.183'
    HOST_ONLINE = 'jenkins@39.101.219.110'
    SOURCE_DIR = 'dist/*'
    TARGET_DIR = '/data/www/kuaimen-yunying-front'
  }
   stage('部署') {
      when {
        expression {
          params.env == '测试环境'
        }
      }
      steps {
        sshagent(credentials: ['km-test2']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
          // 将打包好的文件上传到服务器
          sh "scp -r ${SOURCE_DIR} ${HOST_TEST}:${TARGET_DIR}"
          sh 'echo "部署成功~"'
        }
      }
    }

    stage('发布') {
      when {
        expression {
          params.env == '线上环境'
        }
      }
      steps {
        sshagent(credentials: ['km-online']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
          sh "scp -r ${SOURCE_DIR} ${HOST_ONLINE}:${TARGET_DIR}"
          sh 'echo "发布成功~"'
        }
      }
    }
  }
}

environment定了全局变量,在其它地方可直接引用

sshagent用于连接服务器,需要先安装插件ssh-agentcredentials是连接服务器的凭据ID,我们在一开始已经教大家创建好了

后端

pipeline {
  agent any
  environment {
        HOST_TEST = 'root@121.41.16.183'
        TARGET_DIR = '/data/www/kuaimen-auto'
        HOST_ONLINE = 'jenkins@39.101.219.110'
  }
  
  tools {
        maven 'Maven3.6.3'
  }
  
  stage('部署应用') {
            when {
                expression {
                    params.env == '测试环境'
                }
            }
            steps {
                echo "开始部署${params.moduleName}模块"   
                sshagent(credentials: ['km-test2']) {
                    sh "ssh -v -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
                    // 将打包后的文件上传到服务器
                    sh "cd ${params.moduleName}/target && scp *.jar ${HOST_TEST}:${TARGET_DIR}/${params.moduleName}"
                    // 匹配出该Java进程然后杀掉
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} \"uname;ps -ef | egrep ${params.moduleName}.*.jar | egrep -v grep | awk '{print \\\$2}' | xargs -r sudo kill -9\""
                    // 启动该进程
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} \"nohup /data/apps/jdk1.8/bin/java -jar ${TARGET_DIR}/${params.moduleName}/${params.moduleName}-0.0.1-SNAPSHOT.jar --spring.profiles.active=test >/dev/null 2>&1 &\""
                    sh 'echo "部署成功~"'
                }
                echo '部署成功'
            }
        }
        stage('发布应用') {
            when {
                expression {
                    params.env == '线上环境'
                }
            }
            steps {
                echo "开始发布${params.moduleName}模块"   
                sshagent(credentials: ['km-online']) {
                    sh "ssh -v -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
                    sh "cd ${params.moduleName}/target && scp *.jar ${HOST_ONLINE}:${TARGET_DIR}/${params.moduleName}"
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} \"uname;ps -ef | egrep ${params.moduleName}.*.jar | egrep -v grep | awk '{print \\\$2}' | xargs -r sudo kill -9\""
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} \"nohup /data/apps/jdk1.8/bin/java -jar ${TARGET_DIR}/${params.moduleName}/${params.moduleName}-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev >/dev/null 2>&1 &\""
                    sh 'echo "发布成功~"'
                }
                echo '发布成功'
            }
        }
}

需要注意的是,在匹配进程的那段shell中的awk '{print \\\$2}'$符号需要用三个反斜线进行转义,不然会无法执行成功,这里曾卡了好久,希望你们别踩坑了

部署完成后发起通知

我们这里使用钉钉发起通知,主要原理是在钉钉群创建一个webhook机器人,然后把webhook的地址填入DingTalk插件的配置项,最后在JenkinsFile中进行如下配置即可:

pipeline {
  stage('获取git commit message') {
     steps {
       script {
         // 将获取到的git commit赋值给GIT_COMMIT_MSG
         env.GIT_COMMIT_MSG = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
       }
     }
  }
  post {
     success {
            dingtalk (
                robot: '77d4c82d-3794-4583-bc7f-556902fee6b0',
                type: 'MARKDOWN',
                atAll: true,
                title: '你有新的消息,请注意查收',
                text:[
                '# 运营管理系统发布通知',
                '---',
                '#### **所属:后端**',
                "#### **构建任务:${env.BUILD_DISPLAY_NAME}**",
                "#### **本次更新内容:${params.update}**",
                "#### **部署环境:${params.env}**",
                '#### **构建结果:成功**'
                ]
            )
        }
  }
}

GIT_COMMIT这个是Jenkins系统全局变量,获得的是git commit ID,然后通过它拿到具体的提交信息,并赋值给env.GIT_COMMIT_MSG,全局变量可以通过这种方式访问env.BUILD_DISPLAY_NAME

robot为机器人ID,在系统配置中添加如下配置项

webhook在创建完机器人的时候能够拿到

image-20201015162449026

如何创建钉钉机器人

点击群设置 -> 智能群助手

image-20201015162819482

选择自定义机器人,配置完成后就可以看到webhook的地址了

image-20201015162922118

开始构建

经过上面的配置,我们已经完成了前端、后台的自动化构建配置,接下来再说明一下分别是如何触发构建的

前端

  1. 提交代码到master,会自动执行构建任务,并部署到测试环境,部署成功后会在钉钉群发起提醒
  2. 参数化构建 image-20201015163313512 点击Build with Parameters,选择相应的参数进行构建,线上环境必须通过这种方式,保证一定的安全性 image-20201015163435799

后端

后端只配置了参数化构建,原因前面已经说了,选择要构建的环境、模块进行构建

image-20201015163643880

使用Blue Ocean构建(推荐)

点击打开Blue Ocean

image-20201015163808172

选择要构建的分支

image-20201015163855221

弹出参数选择,这和Build with Parameters差不多,但是界面更好看,更清爽了,选择后点击Run即可开始构建

image-20201015164023218

构建结果,很直观,根据颜色可以判断构建成功了,如果失败了是红色

image-20201015164227693

回滚

在Blue Ocean的活动栏可以看到历史构建,点击如下位置的按钮可以重新构建该历史项,即回滚

image-20201015164435230

写在最后

到这里终于告一段落了,虽然折腾了不少时间,但是将公司的工程化流程完善了还是有点小小的成就感的,以后可以愉快得写代码了,自动化的事情就交给Jenkins了。

将这个记录下来一个是方便以后随时查阅,还有一个是希望能让朋友们少踩些坑,完~

附录

Jenkins官方文档

BlueOcean实战多分支pipeline构建(Jenkins)

Complete Jenkins Pipeline Tutorial for Beginners [FREE]

实战笔记:Jenkins打造强大的前端自动化工作流

Jenkins:添加SSH全局凭证

钉钉通知系列Jenkins发布后自动通知

使用 Generic Webhook Trigger 触发 Jenkins 多分支流水线自动化构建

Jenkins pipeline单引号、双引号和转义字符

Jenkins Blue Ocean 的使用

How to Execute Linux Commands on Remote System over SSH