1. pipeline概述

Jenkins 流水线是jenkins的一套插件,需要在 jenkins 2.x 上安装pipeline插件之后才能使用。在《环境安装》中已经安装成功。Jenkins的流水线式DSL(domain-specific language)语言,即领域专用语言。Jenkins创建流水线的方式有三种:

  • 经典UI: 【新建任务】—> 输入名称,选择流水线,进入流水线创建流程—>填写Pipeline脚本到文本框。Pipeline管理比较复杂
  • Blue Ocean:根据提示创建仓库,编辑和配置Pipeline。配置完毕后的Jenkinsfile会提交到仓库,对仓库有侵入性,由开发者自行维护,方便管理
  • SCM:从git仓库拉取pipeline,并运行指定的pipeline。方便统一管理所有的pipeline

    1.1. 经典UI

    管理台下点击【新建任务】—>填写任务名称,选择【流水线】,点击【确定】—>在流水线文本框填写 pipeline 脚本—>点击【保存】

image.png
image.png
image.png

  1. pipeline {
  2. agent any
  3. stages {
  4. stage("Build") {
  5. steps {
  6. echo "测试 经典UI 的Pipeline"
  7. echo "hello world"
  8. }
  9. }
  10. }
  11. }
  • 运行测试

image.png
image.png

1.2. SCM

和经典UI操作步骤一致,仅在定义Pipeline脚本时不同,Pipeline 脚本从代码仓库拉取,不过需要先配置pipeline拉取的密钥

  • 配置gitee中拉取pipeline的密钥(如果当前仓库为非公开时需要操作)

【系统管理】—>【Manage Credentials】—>【添加凭据】—>添加pipeline仓库的部署密钥即可
image.png
image.png
image.png

  • 配置从git仓库拉取pipeline脚本

基本步骤和经典UI一致,仅在定义流水线时使用从SCM获取,配置方法如下:
image.png

  • 运行Pipeline

image.png


2. pipeline

2.1. Pipeline分类

Jenkins的pipeline分为两类:

  • 声明式流水线:Jenkins 2.5 版本引入了声明式的流水线语法,它较为简洁,入门简单,对pipeline的编码有着较为严格的限制
  • 脚本式流水线:是由 Groovy构建的通用 DSL,功能更加丰富,灵活度更高,但是需要更多的学习成本

当前章节会详细描述Jenkins的语法,内容较为枯燥,可以先大概看完之后,再根据Pipeline案例进行学习。

2.2. Groovy语法

pass

2.3. 声明式pipeline

声明式语法是Jenkins比较推荐的一种形式,简单清晰,能完成绝大部分项目的需求,本人笔记就是围绕声明式语法进行学习,而不是脚本式语法

2.3.1. 基本结构

pipeline {
    agent any
    stages {
        stage ('Build') {
            steps {
                echo "start Building ..."
            }
        }
        stage ('Test') {
            steps {
                echo "Start Testing ..."
            }
        }
        stage ('Deploy') {
            steps {
                echo "Deploying Application ..."
            }
        }
    }
}

最简单的pipeline结构如上述代码所示,这些block是必不可少的字段。

  • pipeline 是最顶级的block
  • agent 表示使用什么方式去执行pipeline,在分布式构建中会详细描述
  • stages 表示所有的构建阶段,至少包含一个stage
  • stage 表示某一个构建阶段,至少包含一个steps,Build,Test,Deploy 是最常用的几个阶段
  • steps 是最小的执行单元,可执行多个命令

    2.3.2. Post

    Post 相当于一个结果回调和处理的模块,可以在 pipeline { } 也可以在 stage ('xx') { } 中指定,post 会根据运行结果执行不同的指令!post支持的条件如下:

  • always:无论当前完成的状态是啥,都要执行

  • faileure:失败时执行
  • success:成功时执行
  • changed:上次执行状态和当前不同时才执行
  • fixed:上次执行状态为失败或者不稳定(unstable),当前执行成功时才执行
  • regression:上次执行成功,当前执行失败、不稳定、终止时才执行
  • aborted:终止时执行
  • unstable:不稳定时执行
  • cleanup:清理条件块

    post {
      always {
          // 清理workspace,如果公司项目很多,可以选择清理
          cleanWs()
      }
      failure {
          mail to: 'duduniao@qq.com', subject: "${env.JOB_NAME}-#${env.BUILD_ID} failed", body: """
          branch: ${env.GIT_BRANCH}
          commit: ${env.GIT_PREVIOUS_COMMIT}
          jenkins url: ${env.JENKINS_URL}
          build url: ${env.BUILD_URL}
          node name: ${env.NODE_NAME}
          """
      }
    }
    

    2.3.3. environment

    参考 https://www.yuque.com/duduniao/linux/gsovr9#nYFTy

    2.3.4. options

    options 用于定义Jenkins属性,可以用在 pipeline 和 stage 块中,相对来说,放到 pipeline 中的场景较多,支持的指令如下:

  • retry:重试次数,根据场景选择放置位置。重试三次: options { etry(3) }

  • timeout:pipeline超时时间,单位 SECONDS MINUTES HOURS ,如定义超时时间10分钟: optons { timeout(time:20,unit:'MINUTES') }
  • disableConcurrentBuilds:仅在并发运行pipeline。 options { disableConcurrentBuilds() }
  • newContainerPerStage:每个stage 启动一个新的容器,仅在agent为docker或者dockerfile时生效: optons { newContainerPerStage() }
  • skipStagesAfterUnstable:一旦构建状态变得UNSTABLE,跳过该阶段: options { skipStagesAfterUnstable() }
  • buildDiscarder:保存最近的流水线记录数量: options { buildDiscarder(logRotator(numToKeepStr: '10')) }

    // 全局配置
    options {
      // 历史构建记录保存时间
      buildDiscarder(logRotator(numToKeepStr: '10'))
      // 总共重试的次数
      retry(2)
      // 超时时间
      timeout(time:10,unit:'MINUTES')
      // 当前pipeline禁止并发运行
      disableConcurrentBuilds()
    }
    

    2.3.5. tools

    指定构建的工具,指定之后,在构建时会将该工具目录放入到PATH中以供使用,当 agent none 时忽略。常常用于配置多版本的JDK、多版本的Maven。
    参考文档:2.6. 构建工具

    2.3.6. when

    只有当when条件为true时,对应的stage才会执行。如果是多个条件,默认以 and 连接。when 支持以下几种表达式:

  • branch:当前构建分支与指定的相符,用于多分支流水线,如: when { branch 'master' }

  • environment:判断环境变量是否是给定的值,如: when { environment name: "TYPE", value: 'prod' }
  • expression:当给定的Groovy表达式是否为true,如: when { expresson { return params.DEBUG_BULD } }
  • not:嵌套条件为false时执行,如: when { not { branch 'feature' } }
  • allOf:嵌套的条件全部为true时执行,如: when { allOf { branch 'master';environment name:"TAG",value:'v.1.0.0'} }
  • anyOf:满足一个嵌套条件即可执行,如: when { anyOf { branch 'master'; branch 'devops' } }

    stage('deploy') {
      when {
          expression { params.deploy }
          environment name: 'GIT_BRANCH', value: 'origin/master'
      }
      steps {
          sh "make deploy"
      }
    }
    
    stage('Example Deploy') {
      when {
          expression { BRANCH_NAME ==~ /(production|staging)/ }
          anyOf {
              environment name: 'DEPLOY_TO', value: 'production'
              environment name: 'DEPLOY_TO', value: 'staging'
          }
      }
      steps {
          echo 'Deploying'
      }
    }
    

    2.3.7. triggers

    参考 触发器 章节

    2.3.8. script

    在声明式流水线中使用groovy脚本,可以使用 script { } ,提供了更加灵活的机制。但是大部分情况下,使用script 时没有必要的,对于复杂的 script 应该存放在共享库中。

    Jenkinsfile (Declarative Pipeline)
    pipeline {
      agent any
      stages {
          stage('Example') {
              steps {
                  echo 'Hello World'
    
                  script {
                      def browsers = ['chrome', 'firefox']
                      for (int i = 0; i < browsers.size(); ++i) {
                          echo "Testing the ${browsers[i]} browser"
                      }
                  }
              }
          }
      }
    }
    

    2.4. 脚本式pipeline

    与声明式流水线不同,脚本式流水线是由 Groovy 构建通用的DSL,大部分groovy功能在脚本式流水线中都能使用,其灵活性和功能性比较强大,比如回调、异常捕获、条件判断等等

    node {
      stage('Example') {
          if (env.BRANCH_NAME == 'master') {
              echo 'I only execute on the master branch'
          } else {
              echo 'I execute elsewhere'
          }
      }
    }
    
    node {
      stage('Example') {
          try {
              sh 'exit 1'
          }
          catch (exc) {
              echo 'Something failed, I should sound the klaxons!'
              throw
          }
      }
    }
    

    2.5. Jenkins的内置变量和参数

    Jenkins 内置了一些环境变量,同时安装的插件也会提供一些变量,在pipeline中会经常使用到,比如当前仓库的分支、commit id,当前流水线的名称、ID等等。Jenkins实例中提供了查看方式${jenkins_url}/pipeline-syntax/globals#env。这里面分四类:

    2.5.1. 环境变量

    环境变量可以使用 sh 'printenv' 将当前环境变量打印出来,调用环境变量的格式是 ${env.xxx} ,在引用过程中,需要使用双引号而不是单引号,常见的内置环境变量有:

  • JENKINS_URL:当前实例的URL地址,需要在系统设置中配置

  • BUILD_URL:当前构建的URL地址,常常在post块中用于通知,方便快速定位
  • JOB_NAME:当前项目的名称,常常在post块中用于通知,也可以和BUILD_ID拼接后用于识别构建记录
  • BUILD_ID:当前项目构建的ID序号
  • NODE_NAME:当前节点名称

插件提供的环境变量,需要在插件的详情页面查看,比如常用的GIT的内置变量:

  • GIT_PREVIOUS_COMMIT:commit标识
  • GIT_BRANCH:分支名称

在声明式pipeline中,经常会自定义环境变量,需要在 environment { } 定义, environment { } 可以定义在pipeline下,也可以定义在 stage内,作用域范围不同而已。以下是定义golang编译的环境变量:

// golang ENV
environment {
    GO111MODULE='on'
    GOPROXY='https://goproxy.cn,direct'
    CGO_ENABLED=0
    GOOS='linux'
    GOARCH='amd64'
    GOPATH='/opt/release/golang/path'
}
// pipeline失败发送邮件,此处会大量使用环境变量,需要使用 mail 还得先参考 3.1 配置邮件服务
post {
    failure {
        mail to: 'duduniao@qq.com', subject: "${env.JOB_NAME}-#${env.BUILD_ID} failed", body: """
        branch: ${env.GIT_BRANCH}
        commit: ${env.GIT_PREVIOUS_COMMIT}
        jenkins url: ${env.JENKINS_URL}
        build url: ${env.BUILD_URL}
        node name: ${env.NODE_NAME}
        """
    }
}

image.png

2.5.2. 参数

Jenkins中常用参数化构建,即指定一些参数,然后再pipeline中调用参数完成构建,比如让用户选择构建时jdk的版本,选择部署的机器等等。
Jenkins 指定参数有两种方式:

  • 在Jenkins UI界面中,选择项目,构建【参数化构建】,并更加需要添加构建参数
  • 如果想通过Jenkinsfile定义,则需要在 pipeline { } 下使用 parameters { } 定义,JenkinsfIle定义参数格式

    parameters {
      // 单选框
      choice(name:"version", choices:['v1.13.15', 'v1.14.14', 'v1.15.10', 'v1.16.2'], description:"choose golang version")
      // 字符串
      string(name:"db_address", defaultValue:"127.0.0.1", description:"数据库地址")
      // 布尔值
      booleanParam(name:"delete_data", defaultValue:false, description:"是否删除数据")
      // 文本
      text(name:"notice", defaultValue:"", description:"部署完毕后提示信息")
      // 密码
      password(name:"db_password",defaultValue:"123456",description:"数据库连接密码")
    }
    
  • 调用参数和调用变量类似,使用 ${params.option_name} 调用,方式如下:

    stages {
      stage('Test') {
          steps {
              echo "${params.version}"
          }
      }
    }
    

    2.6. 构建工具

    构建工具是指在CI/CD流程中经常需要使用的二进制命令或者环境,比如 git, jdk, maven, go 等等。在虚拟机上部署Jenins Agent,并用虚拟机环境进行构建时,需要配置使用的编译环境或者命令。在容器环境下编译基本不使用构建工具。操作步骤如下:

  • 在全局工具定义中,配置工具

  • 在Jenkinsfile中使用 tools { } 引用工具,引用的时候,会自动将工具填入PATH中

tools {} 可以作用在 pipeline {} ,也可以作用于某个 stage {} ,使用工具类型 名称引用工具: tools { jdk 'jdk-1.8' }

2.6.1. 多jdk环境演示

2.6.1.1. 手动安装jdk环境

因为不可抗力,采用jenkins自动安装Jdk基本都是下载失败,因此推荐使用手动安装jdk。本实验采用 openjdk 两个版本作为演示:
下载地址:https://jdk.java.net/archive/
在需要的jenkins node上安装即可,对存在java环境的node,可以配置相关标签,方便调度。这里在 jenkins master 上安装 openjdk进行演示

# 二进制安装过程省略

# jdk安装位置
[root@centos-80 java]# realpath *
/opt/release/java/openjdk-15-ga
/opt/release/java/openjdk-16-ga

2.6.1.2. 添加工具

【系统管理】—>【全局工具管理】—>【新增JDK】
image.png
image.png

2.6.1.3. 使用jdk环境

pipeline {
    agent {
        label 'master'
    }
    stages {
        stage('Test jdk16') {
            tools {
                jdk 'openjdk-16-ga'
            }
            steps {
                sh "java -version"
            }
        }
        stage('Test jdk15') {
            tools {
                jdk 'openjdk-15-ga'
            }
            steps {
                sh "java -version"
            }
        }
    }
}

2.6.2. 多golang环境演示

2.6.2.1. 安装golang

因为网络问题,采用手动安装,在所有需要golang环境的地方安装需要的golang版本。golang其它变量,如GOPROXY,可以在pipeline中配置

# 安装过程跳过,以下是安装目录
[root@centos-80 golang]# realpath v1.1*
/opt/release/golang/v1.12.17
/opt/release/golang/v1.13.15
/opt/release/golang/v1.14.14
/opt/release/golang/v1.15.10
/opt/release/golang/v1.16.2

2.6.2.2. 添加golang环境

  • 安装golang插件:插件管理,安装 Go Plugin ,重启jenkins实例
  • 配置golang环境:【系统管理】—>【全局工具配置】—>【新增Go】

image.png

2.6.2.3. 使用golang工具

  • 配置golang的tools

    pipeline {
      agent {
          label 'master'
      }
      parameters {
          // 单选框
          choice(name:"version", choices:['v1.13.15', 'v1.14.14', 'v1.15.10', 'v1.16.2'], description:"choose golang version")
      }
    
      tools {
          // 指定 golang 版本
          go "${params.version}"
      }
    
      environment {
          GOOS='linux'
          GOARCH='amd64'
          GOPROXY='https://goproxy.cn,direct'
          CGO_ENABLED=0
          GO111MODULE='on'
          GOPATH='/opt/release/golang/path'
      }
    
      stages {
          stage('Test go version') {
              steps {
                  sh "go version"
                  sh "printenv | grep GO"
              }
          }
      }
    }
    

    image.png

    2.7. 多分支构建

    在实际开发中,一个项目往往有多个分支,如 master 分支、feature分支、release分支、fix分支等等,这些分类命名取决于公司采用什么样的开发流程。jenkins针对多分支项目提供了多分支的构建功能。
    在项目中定义仓库地址和需要关注的分支信息(如feature、release等),通过不同分支下定义不同的Jenkinfile或者相同的Jenkins加上when条件的方式执行不同的操作。
    image.png
    image.png
    image.png
    image.png
    image.png

    2.8. 制品管理

    所谓制品,可以是Docker镜像、二进制文件、部署需要的tar包等等,一种软件代码交付的产物。市面有很多类型的制品库,如Harbor,DockerHub,Nexus等。甚至在初期,可以将tar包这种简单的制品放到文件服务器上。这部分较为简单,在Jenkins实践中有演示使用 DockerHub 存储Docker镜像。

    2.9. 凭据管理

    在Jenkins中,为了安全起见,会把密钥、用户密码等敏感信息文件存储到Jenkins凭据中,在需要的时候选择和调用。如API的token、ssh私钥、账号密码等等。
    官方文档:credentials ; jenkins doc

    2.9.1. 添加凭据

    凭据添加非常简单,后续的节点管理中会有较多的案例:07-4-3-Jenkins实践
    【系统管理】—>【Manager Credentails】—>【添加凭据】
    image.png
    下拉选择类型—>下拉选择范围—>填写信息:

  • 全局表示在节点和pipeline中可以使用

  • 系统表示仅在jenkins系统内部和node之间能使用,通常用于添加slave节点

image.png
注意点: 描述和ID一定要填写清楚,避免随着凭据数量增加后混乱

2.9.2. 使用凭据

在系统中使用凭据添加agent节点是非常简单的,重点在于如何在 pipeline 中通过id获取到需要的凭据!
https://www.jenkins.io/doc/pipeline/steps/credentials-binding/

2.9.2.1. username/password

有两种方式可以调用,推荐使用第一种,更加优雅简洁

pipeline {
    agent any
    stages {
        stage('Test user/password cred 1') {
            environment {
                USER_PASSWD_CRED = credentials('password-duduniao-ssh')
            }
            steps {
                // USER_PASSWD_CRED         username:password
                // USER_PASSWD_CRED_USR     username
                // USER_PASSWD_CRED_PSW     password
                // 不安全的命令
                // sh "sshpass -p ${USER_PASSWD_CRED_PSW} ssh -o StrictHostKeyChecking=no ${USER_PASSWD_CRED_USR}@10.4.7.82 "hostname""
                // 安全的命令
                sh 'sshpass -p $USER_PASSWD_CRED_PSW ssh -o StrictHostKeyChecking=no $USER_PASSWD_CRED_USR@10.4.7.82 "hostname" '
            }
        }
        stage('Test user/password cred 2') {
            steps {
                withCredentials([usernamePassword(credentialsId:'password-duduniao-ssh', usernameVariable:'username', passwordVariable:'passwd')]) {
                    sh "sshpass -p ${passwd} ssh -o StrictHostKeyChecking=no ${username}@10.4.7.82 'hostname' "
                }
            }
        }
    }
}

2.9.2.2. ssh key

ssh key 不支持从 credentials 中直接获取密钥用来登陆,而需要使用 SSH Agent 插件调用ssh key。
需要注意:sshagent 代码块内执行ssh命令并不会使用凭据中的用户名

pipeline {
    agent any
    stages {
        stage('Test ssh key cred 1') {
            environment {
                USER_SSH_CRED = credentials('password-duduniao-ssh')
            }
            steps {
                sshagent (credentials: ['key-duduniao-ssh']) {
                    sh 'ssh -o StrictHostKeyChecking=no -l cloudbees $USER_SSH_CRED_USR@10.4.7.82 "id ; hostname"'
                }
            }
        }
        stage('Test user/password cred 2') {
            steps {
                withCredentials([sshUserPrivateKey(credentialsId:'key-duduniao-ssh', usernameVariable:'username', keyFileVariable:'key_file')]) {
                    sh "ssh -o StrictHostKeyChecking=no -i ${key_file} ${username}@10.4.7.82 'id ; hostname' "
                }
            }
        }
    }
}

2.9.2.3. secret text

secret text 类型经常用于存放token,本次模拟远程使用API触发 jenkins 任务,仅作测试使用:

  • 生成Token

【用户】—>【设置】—>【添加新token】
image.png

  • 添加 secret text 类型和用户名密码的凭据
  • 创建测试项目,并配置远程触发器

image.png

  • 测试远程触发
    pipeline {
      agent any
      environment {
          // usernane:password
          USER_PASSWD_CRED = credentials('password-duduniao-jenkins')
          API = 'http://jenkins.ddn.com/job/upstream-1/build'
      }
      stages {
          stage('Test secret text cred 1') {
              environment {
                  PIPELINE_TOKEN = credentials('jenkins-pipeline-token')
              }
              steps {
                  sh 'curl -v -u $USER_PASSWD_CRED $API?token=$PIPELINE_TOKEN'
              }
          }
          stage('Test secret text cred 2') {
              steps {
                  withCredentials([string(credentialsId:'jenkins-pipeline-token', variable:'TOKEN')]) {
                      sh 'curl -v -u $USER_PASSWD_CRED $API?token=$TOKEN'
                  }
              }
          }
      }
    }
    

    2.9.2.3. secret file

    在凭据管理中添加 secret file ,以 k8s 集群的kubeconfig文件为例:
    pipeline {
      agent any
      stages {
          stage('Test secret file cred 1') {
              environment {
                  CONFIG = credentials('kubeconfig-aliyun')
              }
              steps {
                  sh 'kubectl --kubeconfig $CONFIG get node'
              }
          }
          stage('Test secret file cred 2') {
              steps {
                  withCredentials([file(credentialsId:'kubeconfig-aliyun', variable:'CONFIG')]) {
                       sh 'kubectl --kubeconfig $CONFIG get node'
                  }
              }
          }
      }
    }
    

3. 补充

3.1. 通知

pipeline 中的通知分为两种:

  • 各个执行阶段进行通知(一般用于回调给上层触发程序)
  • pipeline 结束后通知(一般用于通知相关任务当前任务是否成功)

    3.1.1. 使用邮件通知

    3.1.1.1. 使用内置邮件功能

  • 配置系统管理员邮箱地址

【系统管理】—>【系统配置】—>填写 系统管理员邮件地址
image.png

  • 配置发件人信息

【系统管理】—>【系统配置】—>填写 邮件通知
image.png

  • 测试pipeline

可以参考 2.3.2. Post 章节

pipeline {
    agent any
    post {
        always {
            // 清理workspace,如果公司项目很多,可以选择清理
            cleanWs()
        }
        failure {
            mail to: 'duduniao@qq.com', subject: "${env.JOB_NAME}-#${env.BUILD_ID} failed", body: """
            branch: ${env.GIT_BRANCH}
            commit: ${env.GIT_PREVIOUS_COMMIT}
            jenkins url: ${env.JENKINS_URL}
            build url: ${env.BUILD_URL}
            node name: ${env.NODE_NAME}
            """
        }
    }

    stages {
        stage('Test') {
            steps {
                sh "false"
            }
        }
    }
}

3.1.1.2. 使用 Email Extension 发送邮件

【系统管理】—>【系统配置】—>填写 Extended E-mail Notification
image.png
image.png
image.png
以下为邮件的默认正文,是从互联网摘抄的,生产中建议根据需求调整下,只需要提取需要的信息,组成html文档即可

<!DOCTYPE html>    
<html>    
<head>    
<meta charset="UTF-8">    
<title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>    
</head>    

<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"    
    offset="0">    
    <table width="95%" cellpadding="0" cellspacing="0"  style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">    
        <tr>    
            本邮件由系统自动发出,无需回复!<br/>            
            各位同事,大家好,以下为${PROJECT_NAME }项目构建信息</br> 
            <td><font color="#CC0000">构建结果 - ${BUILD_STATUS}</font></td>   
        </tr>    
        <tr>    
            <td><br />    
            <b><font color="#0B610B">构建信息</font></b>    
            <hr size="2" width="100%" align="center" /></td>    
        </tr>    
        <tr>    
            <td>    
                <ul>    
                    <li>项目名称 : ${PROJECT_NAME}</li>    
                    <li>构建编号 : 第${BUILD_NUMBER}次构建</li>    
                    <li>触发原因: ${CAUSE}</li>    
                    <li>构建状态: ${BUILD_STATUS}</li>    
                    <li>构建日志: <a href="${BUILD_URL}console">${BUILD_URL}console</a></li>    
                    <li>构建  Url : <a href="${BUILD_URL}">${BUILD_URL}</a></li>    
                    <li>工作目录 : <a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>    
                    <li>项目  Url : <a href="${PROJECT_URL}">${PROJECT_URL}</a></li>    
                </ul>    

<h4><font color="#0B610B">失败用例</font></h4>
<hr size="2" width="100%" />
$FAILED_TESTS<br/>

<hr size="2" width="100%" />
<ul>
${CHANGES_SINCE_LAST_SUCCESS, reverse=true, format="%c", changesFormat="<li>%d [%a] %m</li>"}
</ul>
详细提交: <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a><br/>

            </td>    
        </tr>    
    </table>    
</body>    
</html>
  • 在 pipeline 中,可以自定义邮件内容,也可以选择使用默认的模板:

    emailext (
      subject: "title",                                 // 邮件标题,调用系统默认标题: ${DEFAUTL_SUBJECT}
      recipientProviders: [buildUser(), requestor()], // 收件人类型,可选
      body: "xxx",                                     // 邮件正文, 调用默认正文: ${DEFAULT_SUBJECT}
      to: "xx",                                         // 自定义收件人,可选
      attachLog: true,                                 // 添加日志到附件,可选
      compressLog: true                                 // 是否压缩日志,可选
    }
    
    // 直接调用 Email Extension插件发送邮件
    pipeline {
      agent any
      post {
          always {
              // 清理workspace,如果公司项目很多,可以选择清理
              cleanWs()
          }
          failure {
              script{ emailext (
                                  body: '${DEFAULT_CONTENT}',
                                  subject: '${DEFAULT_SUBJECT}',
                                  to: 'duduniao@ddn.com',
                                  compressLog: true,
                                  attachLog: true,
                                  recipientProviders: [buildUser(), requestor()]
                              )
              }
          }
      }
    
      stages {
          stage('Test') {
              steps {
                  sh "false"
              }
          }
      }
    }
    

    ``` 收件人主要有以下几类:

  1. Build User:

  2. Developers: 最后一次提交代码的开发者,将提交者ID和默认用户邮箱后缀拼接成邮箱

  3. Requestor:手动发起构建请求的用户

  4. Culprits:从最近一次构建成功以后的所以构建失败者

  5. Upstream Committers: 上游Job变更提交者列表

  6. Recipient List: Jenkins项目中定义的收件人列表

    <a name="4l2tD"></a>
    ### 3.1.2. 对接IM
    对接IM机器人,一般是都是使用聊天机器人的Hook URL实现,比如钉钉机器人提供一个URL地址,Jenkins通过调用该URL地址,并传递消息内容完成调用。当然部分IM在Jenkins中存在相关插件,可以简化这个操作。[钉钉消息插件](https://plugins.jenkins.io/dingding-notifications/),[官方文档](https://jenkinsci.github.io/dingtalk-plugin/guide/getting-started.html#%E5%AE%89%E8%A3%85%E6%8F%92%E4%BB%B6)
    <a name="5GC0p"></a>
    #### 3.1.2.1. 在钉钉群配置机器人
    [文档](https://developers.dingtalk.com/document/app/custom-robot-access?spm=ding_open_doc.document.0.0.6d9d28e1zi7hv6#topic-2026027)
    <a name="rMf5V"></a>
    #### 3.1.2.2. 配置钉钉机器人
    【系统配置】-->【钉钉】<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/378176/1621086690609-0ff21812-19b2-4dc7-9b3c-f202fd460018.png#height=781&id=M6MUe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=781&originWidth=983&originalType=binary&size=55815&status=done&style=stroke&width=983)
    <a name="SSeIN"></a>
    #### 3.1.2.3. 测试
    ```groovy
    pipeline {
    agent any
    post {
        always {
            dingtalk (
                robot: 'devops-52773fd28593e81c01094c3e8ac3348a',
                type: "MARKDOWN",
                title: "${env.JOB_NAME}#${env.BUILD_ID} ${currentBuild.result}",
                text: [
                    "# ${env.JOB_NAME}#${env.BUILD_ID} ${currentBuild.result}",
                    "---",
                    "- 项目名称:${env.JOB_NAME}",
                    "- 构建ID:${env.BUILD_ID}",
                    "- 状态:${currentBuild.result}",
                    "- 节点:${env.NODE_NAME}",
                    "- [详情](${env.BUILD_URL})"
                ]
            )
            cleanWs()
        }
    }
    
    stages {
        stage('Test') {
            steps {
                sh "true"
            }
        }
    }
    }
    

    image.png

    3.1.3. HTTP回调

    如果是其它服务调用Jenkins API触发任务,往往Jenkins Pipeline 需要回调执行结果给调用者,简单的golang代码模拟callback接口,并在172.25.117.28运行 go run cmd/main.go ```go // cmd/main.go package main

import ( “encoding/json” “fmt” “io/ioutil” “net/http” )

type CallbackMsg struct { ProjectName string json:"project_name" // 项目名称 BuildID string json:"build_id" // 构建ID Agent string json:"agent" // 执行任务的 jenkins agent JenkinsUrl string json:"jenkins_url" // jenkins 地址 BuildUrl string json:"build_url" // 当前构建的任务的 url Status string json:"status" // 构建状态 }

func callback(w http.ResponseWriter, r *http.Request) { method := r.Method if method != http.MethodPost { , = w.Write([]byte(“Invalid Request Method”)) w.WriteHeader(400) return } body, err := ioutil.ReadAll(r.Body) if err != nil { , = w.Write([]byte(fmt.Sprintf(“Invalid Body, err:%s”, err.Error()))) w.WriteHeader(400) return } result := new(CallbackMsg) err = json.Unmarshal(body, result) if err != nil { , = w.Write([]byte(fmt.Sprintf(“Invalid Body, err:%s”, err.Error()))) w.WriteHeader(400) return } fmt.Println(string(body)) , = w.Write([]byte(fmt.Sprintf(“recv message:%s”, string(body)))) return }

func main() { http.HandleFunc(“/callback”, callback) if err := http.ListenAndServe(“0.0.0.0:9001”, nil); err != nil { panic(err) } }

```groovy
pipeline {
    agent any
    post {
        always {
            // 实际生产环境下,可能需要retry
            // 更多更丰富的信息需要自己进行拼接
            httpRequest contentType: 'APPLICATION_JSON', 
                httpMode: 'POST', 
                requestBody: """{"project_name":"${env.JOB_NAME}","build_id":"${env.BUILD_ID}","agent":"${env.NODE_NAME}","jenkins_url":"${env.JENKINS_URL}","build_url":"${env.BUILD_URL}","status":"${currentBuild.result}"}""", 
                responseHandle: 'NONE', 
                timeout: 5, 
                url: 'http://172.25.117.28:9001/callback'
            cleanWs()
        }
    }

    stages {
        stage('Test') {
            steps {
                sh "false"
            }
        }
    }
}

3.2. 触发器

Jenkins 任务除了用户手动点击操作之外,还可以使用制动计划任务进行周期性触发,或者又其它任务触发,最常用的是其它的服务(如gitlab)通过API进行触发。
所有配置触发的任务都是可以手动触发的!

3.2.1. 计划任务

Jenkins 支持cron格式的计划任务,同时也支持更多类型的写法,一般情况cron格式能满足要求,使用方式如下:

pipeline {
    agent any

    triggers {
        cron('* * * * *')
    }

    stages {
        stage('Builder') {
            steps {
                echo "Builder"
            }
        }
        stage('Test') {
            steps {
                echo "Test"
            }
        }
    }
}

3.2.2. 关联任务

部分场景下,任务A执行成功后,需要触发任务B的执行,则成为A为B的上游任务!使用 upstream 可以定义!
其中如果关注多个上游 project,需要使用逗号分隔,只要任意一个上游 project满足条件,则触发当前 project

pipeline {
    agent any

    triggers {
        upstream(
            upstreamProjects: "upstream-1,upstream-2",
            threshold: hudson.model.Result.SUCCESS
        )
    }

    stages {
        stage('Builder') {
            steps {
                echo "Builder"
            }
        }
        stage('Test') {
            steps {
                echo "Test"
            }
        }
    }
}

3.2.4. API触发

API 触发在自动化运维开发中使用非常多,直接调用 Jenkins 的API,并传递构建参数:

3.2.4.1. 配置Jenkins

点击右上角用户名—>【设置】—>【添加新token】
image.png
在Project中配置远程触发的令牌,可以用户自定义
image.png

3.2.4.2. 远程调用

  • 简单调用

如果pipeline是有参数的,只能使用 buildWithParameters 方式调用,否则报错 HTTP ERROR 400 This page expects a form submission .
项目地址: JENKINS_URL/job/JOBNAME

# curl 测试无参数
[root@duduniao go-simple]# curl -X POST -u duduniao:11566de004ec7f7695905bba51f89d23d4 http://jenkins.ddn.com/view/%E5%85%A5%E9%97%A8/job/triggers/build?token=triggers-a49eee6c5494b1aa3f185693885f78b6

# curl 有参数. 传递 busybox和 tag参数
curl -X POST -u duduniao:11566de004ec7f7695905bba51f89d23d4 "http://jenkins.ddn.com/view/%E5%85%A5%E9%97%A8/job/triggers/buildWithParameters?token=triggers-a49eee6c5494b1aa3f185693885f78b6&image='busybox'&tag='v0.0.1'"
  • json请求处理

想通过json 字符串传参,但是没有找到好的处理方式。后续慢慢补充

3.2.5. 使用Generic Webhook Triggers

官方文档:https://plugins.jenkins.io/generic-webhook-trigger/

3.3. 并行步骤和并发构建

3.3.1. 并行步骤

之前的所以Jenkinsfile都是从上到下逐步执行的,但是部分场景中为了节约时间,可以将多个步骤同时进行,被称为并行步骤。比如同时将服务部署到Docker和虚拟机中,或者同时对手机和平台设备进行测试等,通过并行步骤可以降低很多的等待时间。Jenkins支持 stage 和 step 两个语句块的并行执行!一般将并行的步骤作为一个整体,只要有一个失败了,其它并行步骤就需要停下来,这个功能可以通过 failFast true 指定。
并行的 stage 可以指定在不同的 agent 上操作,而并行的 step 只能在同一个 agent 上,因此并行的 step 局限性很大!

3.3.1.1. stage并行

pipeline {
    agent none

    stages {
        stage('git clone code') {
            agent { label "VM" }
            steps {
                echo "git clone xxxx"
                echo "git checkout branch"
            }
        }
        stage('deploy application') {
            // 只要有一个失败了,就立刻停止所有并行任务
            failFast true
            // 并行的任务块
            parallel {
                stage('deploy to vm') {
                    agent { label "docker" } 
                    steps {
                        echo "start deploy to vm cluster"
                        sh "date +%T ; sleep 20 ; date +%T"
                    }
                }
                stage('deploy to docker-compose') {
                    agent { label "docker" }
                    steps {
                        echo "start deploy to docker-compose"
                        sh "date +%T ; sleep 10 ; date +%T"
                    }
                }
                stage('deploy to kubernetes ') {
                    agent { label "docker" }
                    steps {
                        echo "start deploy to kubernetes"
                        sh "date +%T ; sleep 15 ; date +%T"
                    }
                }
            }
        }
    }
}

3.3.1.2. step并行

pipeline {
    agent none

    stages {
        stage('git clone code') {
            agent { label "VM" }
            steps {
                echo "git clone xxxx"
                echo "git checkout branch"
            }
        }
        stage('compile apps') {
            agent { label 'docker && golang' }
            steps {
                parallel (
                    apiserver: {
                        echo "go build kube-apiserver"
                        sh "date +%T ; sleep 20 ; date +%T"
                    },
                    kube-proxy: {
                        echo "go build kube-proxy"
                        sh "date +%T ; sleep 10 ; date +%T"
                    },
                    controller-manager: {
                        echo "go build controller-manager"
                        sh "date +%T ; sleep 15 ; date +%T"
                    }
                )
            }
        }
    }
}

3.3.2. 并发构建

默认的Pipeline 是允许并发构建的,即同一个流水线可以执行多个任务,但是部分场景中,基于机器资源和性能,或者任务本身可能存在冲突,不适合并发构建。
通过 option { disableConcurrentBuilds() } 可以指定不允许并发构建

pipeline {
    agent any
    options {
        disableConcurrentBuilds()
        buildDiscarder logRotator(artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '10', numToKeepStr: '20')
        timeout(activity: true, time: 20)
    }
    stages {
        stage('Clone') {
            steps {
                sh "sleep 20"
            }
        }
        stage('Build') {
            steps {
                sh "sleep 30"
            }
        }
    }
}

3.3.3. 资源锁

资源锁只针对共享资源进行加锁,避免步骤之间争抢共享资源导致异常,lock指令只能在 options 和 steps 块内使用,例如以下场景:

  • 测试机器数量有限,不允许多个pipeline操作一台测试机

    pipeline {
      agent any
    
      parameters {
          string(name:"test_server", defaultValue:"172.21.21.22", description:"input test host for linux env ")
          string(name:"branch", defaultValue:"master", description:"select branch name to checkout")
      }
    
      stages {
          stage('clone') {
              steps {
                  echo "clone code from gitee ..."
              }
          }
          stage('build') {
              steps {
                  echo "checkout to ${params.branch} ..."
                  echo "make build ..."
              }
          }
          stage('test') {
              steps {
                  // lock {} 只能放在steps内
                  lock (resource: "${params.test_server}") {
                      echo "start testing ..."
                      sh "sleep 60"
                      echo "stop testing ..."
                      echo "clear testing ..."
                  }
              }
          }
          stage('push to nexus') {
              steps {
                  sh "sleep 20"
                  echo "push finish"
              }
          }
      }
    }
    

    测试中会发现,在 Lockable Resources 中会发现当前资源对哪个 Job 锁定,如果其它Job想要执行必须等待当前的锁释放
    image.png

  • 代码仓库太大,选择在公共目录下创建分支目录,每次构建进入对应分支拉取最新代码,就需要避免多个任务同时操作一个分支

执行的任务分为编译任务和非编译任务,编译任务不能在一台机器上并行,而非编译任务可以

pipeline {
    agent { label "centos" }

    parameters {
        // branch名称
        string(name:"branch_name", defaultValue:"master", description:"branch name")
        // 是否涉及编译,非编译的包仅拷贝配置文件,生成config包
        booleanParam(name:"compile", defaultValue:true, description:"compile binary file")
    }

    options { timestamps() }

    stages {
        stage ('run compile job') {
            when {
                // 做条件判断,如果是编译任务走该分支
                expression { return params.compile }
            }
            steps {
                // 编译任务锁住当前节点和当前分支,避免多个编译任务同时在一台机器上运行
                // 之所以不能把 lock 放在 stage 下的 options 内,是因为optionse内无法获取到 env.NODE_NAME 变量
                // 而且自定义的 env 变量也无法在 options 内获取,params 可以在options内正常使用
                lock(extra: [[resource: "${env.NODE_NAME}-${params.branch_name}"]], resource: "${env.NODE_NAME}") {
                    script {
                        stage('git pull all') {
                            echo "${env.AGENT}"
                            echo "git pull"
                        }
                        stage('build') {
                                echo "make build "
                                sh "sleep 60"
                        }
                        stage('compress all') {
                                echo "make compress"
                                sh "sleep 20"
                        }
                    }
                }
            }
        }
        stage ('run config job') {
            when {
                not { expression { return params.compile } }
            }
            steps {
                lock(resource: "${env.NODE_NAME}-${params.branch_name}") {
                    script {
                        stage('git pull config') {
                                echo "git pull"
                        }
                        stage('compress all') {
                            echo "make compress"
                            sh "sleep 30"
                        }
                    }
                }
            }
        }
        stage ('push to nexus') {
            steps {
                echo "push ..."
                sh "sleep 30"
            }
        }
    }
}

05-2-2-pipeline语法 - 图34