Jenkins: Declarative Pipelines

Posted on Tue, Jun 6, 2023 Jenkins CI

Jenkins Declarative Pipeline 是一种以声明方式定义和执行流水线的方法。它是 Jenkins Pipeline 另一个演变,旨在提供一种更简洁、可读性更好的流水线编写方式。

Jenkins Declarative Pipeline 的特点包括:

  1. 结构化定义: 流水线由多个阶段(stage)组成,每个阶段包含一个或多个步骤(step)。这种结构化的定义使得流水线更易于组织和管理。
  2. 可读性和易维护: 声明性的语法使得流水线脚本更易于阅读和理解。提供了更清晰、更简洁的语法结构,使得流水线的意图更明确,同时减少了冗余和重复的代码。
  3. 简单易用:提供界面化表单定义任务的方式创建 pipeline
  4. 可扩展性: 提供了丰富的插件和内置函数,可以轻松地扩展流水线的功能。可以使用插件来集成其他工具、执行特定的操作,并根据需要自定义流水线的行为。
  5. 可视化和报告: Jenkins 提供了可视化界面来展示和监控流水线的执行过程。可以轻松地查看流水线的状态、日志和报告,以便进行故障排查和性能分析。

Scripted Pipelines Versus Declarative Pipelines

The Structure

Declarative Pipeline 是由包含指令(directives)和部分(sections)组成的块(block)。 每个部分又可以包含其他部分、指令和步骤,在某些情况下还包含条件。 块、部分和指令之间的区别有些随意,但由于它们出现在官方文档中,所有下面我们将搞清这些术语的定义。

Block

Block 是指具有开头结尾的代码集,在 Groovy 中,我们称为 clousre(闭包)。如下所示:

pipeline {
 // code in declarative syntax 
}

Section

流水线被划分为多个部分(Sections),每个部分定义了特定的功能和行为。

Section 可以分为以下几个区域:

stages:定义了流水线的不同阶段,每个阶段包含一个或多个步骤(steps)。通过在 stages 中定义多个阶段,可以将流水线划分为不同的任务,并在需要时并行或串行执行

steps:包含了流水线中的具体步骤,每个步骤定义了一个特定的操作或任务。可以使用内置的步骤(如构建、测试、部署等),也可以使用自定义的脚本步骤来执行更复杂的操作

posts:定义了在流水线执行完成后要执行的操作,如发送通知、触发其他任务等。可以定义多个后处理操作,并根据执行结果进行条件判断。

pipeline {
  agent any
  stages { 
     stage('name1') { 
       steps {      
          ...
       } 
       post {
         ...
       }
     }
     stage('name2') {
       steps {
          ...
       }
     }
  }
  post {
    ...
  } 
}

Directives

Directive(指令)是一种特殊的语法结构,用于在流水线中指定特定的行为和配置选项。Directives 允许在流水线中进行更细粒度的控制和定制,以满足特定的需求。以下是几个常见的 Directive:

  1. Defines values

    agent :指定流水线执行所需的运行环境,agent ('worker')。

  2. Configures behavior

    trigger: triggers { cron ('0 7 0 0 1-5') }

  3. Specifies actions to be done

    stage :可以是一个包含要执行 DSL 的 step 。

Steps

Step(步骤)是指在流水线中执行的最小单元操作。每个 Step 执行一个特定的任务或操作,如构建代码、运行测试、部署应用程序等。

Conditionals

Conditionals 用于根据特定的条件来控制流水线中的执行步骤。可以使用以下几种条件语句:

  1. whenwhen 条件用于在流水线中根据特定条件判断是否执行某个阶段。可以根据表达式、环境变量或其他条件来指定执行或跳过阶段。
    stage('Build') {
        when {
            expression { env.BRANCH_NAME == 'master' }
        }
        steps {
            // 执行构建步骤
        }
    }
  2. scriptscript 条件用于在流水线中执行自定义的 Groovy 脚本,并根据脚本的返回值判断是否执行阶段或步骤。
    stage('Deployment') {
        when {
            script {
                return params.DEPLOY_ENV == 'production'
            }
        }
        steps {
            // 执行部署步骤
        }
    }
  3. environmentenvironment 条件用于在流水线中根据环境变量的值判断是否执行特定的阶段或步骤。
    stage('Testing') {
        when {
            environment name: 'TESTING_ENABLED', value: 'true'
        }
        steps {
            // 执行测试步骤
        }
    }

    The Building Blocks

    在本节中,我们将介绍在声明式流水线中每个部分和指令的具体内容,包括语法、参数和示例用法。

    我们从整体来看,这个流水线的构成如图所示。pipeline 是在最外层,所有的指令和结构都在其中。虚框代表可选,实框代表必有。请注意,有一些指令可以不同阶段级别同时出现。它们在一个区域可能是必需的,在另一个区域可能是可选的。

    到目前为止,我们还没有讨论过的指令。 因此,让我们深入了解结构中的每个区域。

    pipeline

    pipeline 是 Declarative Pipeline 的根节点,用于定义整个流水线的开始和结束。它包含了整个流水线的配置信息和阶段定义。

    agent

    agent 用于指定流水线的运行环境,是必须字段。可以指定使用特定的运行环境,也可以使用 any 表示可以在任何可用的节点上执行。下面是一些关于 agent 的详细说明和示例:

    1. agent none

      当在顶层使用 agent none 时,它表示我们在全局上没有指定一个代理节点用于整个流水线。这意味着如果需要的话,将为每个阶段单独指定一个代理节点。

      pipeline {
          agent none
          
          stages {
              stage('Build') {
                  agent {
                      label 'my-build-agent' // 为 Build 阶段指定特定的代理节点
                  }
                  steps {
                      echo 'Performing build...'
                      // 执行构建操作
                  }
              }
              
              stage('Test') {
                  agent {
                      label 'my-test-agent' // 为 Test 阶段指定特定的代理节点
                  }
                  steps {
                      echo 'Performing tests...'
                      // 执行测试操作
                  }
              }
              
              // 更多的阶段和步骤...
          }
          // ...
      }
    2. 指定 Labels 并自定义 workspace
      pipeline {
          agent {
              label "<lablename>"
      				customWorkspace "<desired directory>"
          }
          // ...
      }
    3. Docker Agents
      pipeline {
          agent {
              docker {
                  image 'maven:3.8.3' // 指定要使用的 Docker 镜像
                  label 'docker-agent' // 指定 Docker Agent 节点的标签或名称
      						args '-v /dir:dir'  // 指定运行参数
              }
          }
          // ...
      }
    4. K8S Agents
      pipeline {
          agent {
              kubernetes {
                  // Kubernetes agent 配置选项
                  label 'my-k8s-agent' // 可选:指定要使用的 Kubernetes agent 节点的标签或名称
                  yaml '''
                      // Kubernetes Pod 配置
                      apiVersion: v1
                      kind: Pod
                      metadata:
                        name: my-pod
                      spec:
                        containers:
                        - name: my-container
                          image: my-image
                          command:
                          - sleep
                          - '3600'
                  '''
              }
          }
          // ...
      }
      

    environment

    environment 用于定义流水线执行期间的环境变量。可以在全局范围内定义全局环境变量,也可以在特定的阶段或步骤中定义局部环境变量。下面展示如何在流水线中使用:

    1. 定义环境变量
      environment {
      	TIMEZONE = "eastern"
      	TIMEZONE_DS = "${TIMEZONE}_daylight_savings"
      }
    2. Credentials and environment variables
      pipeline {
          agent any
          
          environment {
              MY_CREDENTIALS = credentials('my-credentials-id')  // 使用凭据 ID 引用用户名和密码凭据
              API_KEY = credentials('api-key-id')  // 使用凭据 ID 引用 API 密钥凭据,并指定变量名
          }
          
          stages {
              stage('Build') {
                  steps {
                      withCredentials([usernamePassword(credentialsId: env.MY_CREDENTIALS, passwordVariable: 'PASSWORD', usernameVariable: 'USERNAME')]) {
                          echo "Username: ${env.USERNAME}"
                          echo "Password: ${env.PASSWORD}"
                          // 执行构建操作,可以使用凭据中的用户名和密码
                      }
                  }
              }
              
              // 更多的阶段和步骤...
          }
          
          post {
              always {
                  withCredentials([string(credentialsId: env.API_KEY, variable: 'API_TOKEN')]) {
                      echo "API Token: ${env.API_TOKEN}"
                      
                      // 完成操作,可以使用凭据中的 API 密钥
                  }
              }
          }
      }

    tools

    tools 用于指定流水线所需的工具(软件或插件)。通过使用 tools,您可以确保流水线在执行期间使用特定版本的工具。以下是一个示例,展示了如何在流水线中使用 tools

    pipeline {
        agent any
        
        tools {
            // 指定工具的名称和版本
            maven 'Maven 3.8.1'
            jdk 'JDK 11'
        }
        
        stages {
            stage('Build') {
                steps {
                    // 在 Build 阶段中使用指定版本的工具
                    sh 'mvn --version'
                    
                    // 执行其他构建操作
                }
            }
            
            // 更多的阶段和步骤...
        }
    }

    options

    options 用于配置流水线的选项和全局设置。通过使用 options,可以定义流水线的行为、错误处理、并发性等方面的设置。以下是一些常见的选项示例:

    • timeout:用于设置流水线的超时时间,指定流水线在一段时间内没有完成时应该被中断。
    • skipDefaultCheckout:用于跳过默认的代码检出操作,当您的流水线不需要进行代码检出时使用
    • disableConcurrentBuilds:用于禁止并发构建,确保同一流水线的多个实例不会同时运行
    • timestamps:用于在流水线日志中显示时间戳,方便跟踪和调试
    • retry:用于在步骤或阶段失败时进行重试

    pipeline {
        agent any
        
        options {
            timeout(time: 1, unit: 'HOURS')
            disableConcurrentBuilds()
            timestamps()
        }
        
        stages {
            stage('Build') {
                options {
                    retry(3)
                }
                steps {
                    // 执行构建操作
                }
            }
            
            // 更多的阶段和步骤...
        }
    }

    triggers

    triggers 用于配置流水线的触发器。触发器定义了触发流水线执行的条件和事件。以下是一些常见的触发器示例:

    1. cron :基于 cron 表达式的定时触发器,可以按照指定的时间间隔或特定的时间点触发流水线的执行。示例使用方法如下:
      triggers {
          cron('H 0 * * *') // 每天凌晨执行
      }
    2. pollSCM :用于定期轮询源代码管理系统 (SCM),当检测到代码变更时触发流水线的执行。示例使用方法如下:
      triggers {
          pollSCM('H/5 * * * *') // 每 5 分钟轮询一次
      }
    3. upstream :当指定的上游流水线完成并成功执行时触发当前流水线的执行。示例使用方法如下:
      triggers {
          upstream(upstreamProjects: 'my-upstream-pipeline', threshold: 'SUCCESS')
      }
    4. github :与 GitHub 代码库集成,当发生 GitHub 事件(例如推送、拉取请求等)时触发流水线的执行。示例使用方法如下:
      triggers {
          githubPush()
      }

    Cron syntax

    Jenkins 使用 Cron 表达式来配置定时任务的调度。Cron 表达式是一个字符串,由空格分隔的五个或六个字段组成,每个字段表示不同的时间单位和时间范围。下面是 Cron 表达式的基本语法:

    * * * * * *
    | | | | | |
    | | | | | +-- Year (可选,1970-2099)
    | | | | +---- Day of the Week (0-7,Sunday=0或7)
    | | | +------ Month (1-12)
    | | +-------- Day of the Month (1-31)
    | +---------- Hour (0-23)
    +------------ Minute (0-59)

    字段值可以是具体的数字,表示特定的时间点或时间单位,也可以是通配符 * 表示任意值。此外,还可以使用逗号 , 分隔多个值,使用连字符 - 指定范围,使用斜杠 / 指定步长,以及使用问号 ? 和星号 * 表示任意值。

    H 是一个特殊的符号,表示“随机值”。当使用 H 作为分钟字段的值时,Jenkins 将选择一个随机的分钟值来执行任务。这样可以减少并发任务在同一时间点上的竞争,提高系统的稳定性和性能。

    以下是一些示例:

    • 0 0 * * *:每天凌晨零点执行一次
    • H 4 * * 1-5:每个工作日(星期一至星期五)的上午 4 点随机分钟执行一次
    • 0 0 1,15 * *:每月的1号和15号的午夜执行一次
    • 0 0 * * 1:每个星期一的凌晨执行一次
    • H * * * *:每小时的随机分钟执行一次任务
    • H H(0-7) * * 1-5:在每个工作日的随机小时和随机分钟执行任务(小时范围为 0-7)

    parameters

    parameters 用来定义流水线参数,以便在流水线执行期间接收用户输入的值。

    1. string 参数:用于接收字符串类型的输入值。
      parameters {
          string(name: 'ENVIRONMENT', defaultValue: 'dev', description: 'Environment for deployment')
      }
    2. booleanParam 参数:用于接收布尔类型的输入值。
      parameters {
          booleanParam(name: 'ENABLE_TESTS', defaultValue: true, description: 'Enable tests')
      }
    3. choice 参数:用于提供一组预定义的选项供用户选择。
      parameters {
          choice(name: 'DATABASE_TYPE', choices: ['MySQL', 'PostgreSQL', 'Oracle'], description: 'Select database type')
      }
    4. password 参数:用于接收敏感信息,如密码。
      parameters {
          password(name: 'API_KEY', defaultValue: '', description: 'API key')
      }
    5. file 参数:用于接收文件类型的输入值,用户可以上传文件。
      parameters {
          file(name: 'CONFIG_FILE', description: 'Upload configuration file')
      }

    libraries

    Jenkins Declarative Pipeline 允许您使用共享库(libraries)来重用和组织流水线中的代码逻辑。

    可以通过以下形式加载 libraries:

    pipeline {
         agent any
         libraries {
            lib("mylib@master")
            lib("alib")
        }
        stages {
           ...

    在后面我们对 shared libraries 进行详细阐述。

    stages

    stages 段落用于定义流水线的各个阶段。流水线的执行被划分为多个阶段,每个阶段包含一个或多个步骤(steps)来执行特定的任务。

    stage

    stage 是用于定义流水线中单个阶段的关键字。每个 stage 代表流水线的一个执行阶段,用于将任务划分为逻辑上的阶段。

    steps

    steps 是用于定义流水线中执行任务的关键字。steps 段落用于包含一系列步骤,每个步骤代表流水线中的一个执行动作或命令。

    每个步骤可以是任何有效的 DSL 语句,例如 echo、archiveArtifacts、git、mail 等。在这个级别上,DSL 语句的语法在 Scripted Pipeline 和 Declarative Pipeline 中是相同的。然而,您不能使用 Groovy 的非 DSL 语句或结构,例如 if-then 或赋值语句。

    steps {
        <individual steps - i.e., DSL statements>
    }
    // example
    steps {
        echo 'This is a message' // 打印消息
        archiveArtifacts 'build/**/*.jar' // 归档构件
        git branch: 'master', url: 'https://github.com/user/repo.git' // 执行 Git 操作
        mail to: 'example@example.com', subject: 'Build Notification', body: 'Build successful' // 发送电子邮件
    }

    when

    when 指令基于特定条件来控制阶段的执行与否。when 指令允许您指定一个条件,以决定是否执行或跳过该阶段。

    stage('Test') {
        when {
            expression { currentBuild.result == 'SUCCESS' }  // 仅在 'Build' 阶段成功时执行该阶段
        }
        steps {
            // 测试步骤
        }
    }

    可以使用逻辑运算符 andornot 来组合条件,实现更复杂的条件执行。

    1. 使用 and 运算符
      stage('Build and Test') {
          when {
              // 仅在 'Build' 和 'Test' 阶段都成功时执行该阶段
              allOf {
                  expression { currentBuild.getBuild('Build').result == 'SUCCESS' }
                  expression { currentBuild.getBuild('Test').result == 'SUCCESS' }
              }
          }
          steps {
              // 构建和测试步骤
          }
      }
    2. 使用 or 运算符
      stage('Deploy or Promote') {
          when {
              // 仅在 'Deploy' 或 'Promote' 阶段至少一个成功时执行该阶段
              anyOf {
                  expression { currentBuild.getBuild('Deploy').result == 'SUCCESS' }
                  expression { currentBuild.getBuild('Promote').result == 'SUCCESS' }
              }
          }
          steps {
              // 部署或促销步骤
          }
      }
    3. 使用 not 运算符
      stage('Build if Not Skipped') {
          when {
              // 仅在 'Build' 阶段没有被跳过时执行该阶段
              not {
                  expression { currentBuild.getBuild('Build').result == 'SKIPPED' }
              }
          }
          steps {
              // 构建步骤
          }
      }

    每个 stages 可以有单独的 post 方法,下面我们将会详细说明。

    post

    post 是一个用于定义在流水线执行完成后执行的步骤或操作的指令。它允许您指定不同的后续处理步骤,无论流水线执行成功还是失败。

    post 指令可以包含多个子指令,每个子指令定义了特定的后续处理操作。常用的子指令包括:

    • always:无论流水线执行结果如何,都会执行的步骤。
    • success:仅在流水线执行成功时执行的步骤。
    • failure:仅在流水线执行失败时执行的步骤。
    • changed:仅在流水线执行结果发生变化时执行的步骤。
    • unstable:仅在流水线执行结果为不稳定时执行的步骤。
    pipeline {
        agent any
        stages {
            // 定义流水线阶段
        }
        post {
            always {
                // 在流水线执行完成后始终执行的步骤
                echo "Pipeline completed"
            }
            success {
                // 仅在流水线执行成功时执行的步骤
                echo "Pipeline succeeded"
            }
            failure {
                // 仅在流水线执行失败时执行的步骤
                echo "Pipeline failed"
            }
            changed {
                // 仅在流水线执行结果发生变化时执行的步骤
                echo "Pipeline result changed"
            }
            unstable {
                // 仅在流水线执行结果为不稳定时执行的步骤
                echo "Pipeline unstable"
            }
        }
    }