简介

软件开发和交互本来就是一项复杂的工作,流程上来说常见的就有:代码开发->代码质量检查->单元测试->打包->部署->集成测试等等。在这个过程中,开发人员、测试人员、运维人员需要花费大量的时间和精力协同配合,即使这样都很难确保整个发布流程不会出错,好不容易所有环节都没有出错,却也很难保证开发环境、测试环境、准生产环境、线上环境的完全一致;本文使用jenkins的pipeline功能整合整个开发、测试、发布流程,并使用docker-swarm来进行服务的管理,实现一个简单的devops实践,做到这些流程可以一键自动完成。

本文所用到的工具主要有: jenkins、maven、junit、sonar、docker、docker swarm等,要完成本文示例,需要提前了解一下相关知识,主要有: jenkins pipeline的语法、Groovy 语法、docker容器技术、docker swarm容器集群管理技术、以及一点shell脚本编写能力。

  • 如果对上面技术不是特别熟悉的朋友可以参考下以下几篇文章学习一下:

本文旨在抛砖引玉,希望大家有更多的深入研究和分享!

1、流程和工具使用介绍

1.1 单元测试

使用junit进行单元测试,首先使用mvn test直接运行单元测试;再使用jenkins junit 插件生成测试报告

1.2 需要安装额外的jenkins 插件
  • Git Parameter 自动获取代码分支、tag等进行选择
  • Extended Choice Parameter 多选、复选参数控件

jenkins 全局变量引用插件_devops tooling

  • Junit 插件,用于收集单元测试结果并进行自动统计分析

eg.代码片段生成示例:

jenkins 全局变量引用插件_devops_02

  • sonarqube server配置
  • sonarqube 代码检测结果回调通知jenkins配置

菜单: 配置 -> 网络调用 -> 新增

> 回调地址格式:<http://[jenkins_url]/sonarqube-webhook/>

  • Email 邮件发送配置

菜单路径: 系统管理 -> 配置 ->系统管理员邮件地址(这个地址会作为默认的邮件发送者)

jenkins 全局变量引用插件_jenkins_03

邮件发送服务器配置:

jenkins 全局变量引用插件_docker_04


1.3 需要使用的maven插件
  • docker-maven-plugin 或者 dockerfile-maven-plugin 用于将服务打包成镜像并推送到远程仓库中
  • sonar-maven-plugin 用于代码静态检查
  • Junit 用于单元测试用例
  • jacoco-maven-plugin 代码测试覆盖率分析
1.4 eclipse插件
  • jenkins-editor 用于pipeline脚本的编写和语法静态检查

2、pipeline语法简单介绍

流水线是用户定义的一个CD流水线模型 。流水线的代码定义了整个的构建过程, 他通常包括构建, 测试和交付应用程序的阶段 。

pipeline有如下特性:

  • Code: 流水线是在代码中实现的,通常会检查到源代码控制, 使团队有编辑, 审查和迭代他们的交付流水线的能力。
  • Durable: 流水线可以从Jenkins的主分支的计划内和计划外的重启中存活下来。
  • Pausable: 流水线可以有选择的停止或等待人工输入或批准,然后才能继续运行流水线。
  • Versatile: 流水线支持复杂的现实世界的 CD 需求, 包括fork/join, 循环, 并行执行工作的能力。
  • Extensible:流水线插件支持扩展到它的DSL [1]的惯例和与其他插件集成的多个选项。
2.1 pipeline脚本模式
  • 声明式语法

推荐使用声明式语法

  • 脚本式语法
2.2 pipeline主要元素
节点

节点是一个机器 ,它是Jenkins环境的一部分 and is capable of执行流水线。

另外, node块是 脚本化流水线语法的关键部分

在jenkins集群环境下可以使用此特性。

阶段

stage 块定义了在整个流水线的执行任务的按操作范围认为划分的一系列相关的操作,比如:代码打包、代码静态检查、单元测试、部署等等阶段

步骤

本质上 ,一个单一的操作,一个 step 告诉Jenkins 在特定的时间点要做什么(或过程中的步骤)。 举个例子,要执行shell命令 ,请使用 sh 步骤: sh 'make'。当一个插件扩展了流水线DSL, [1] 通常意味着插件已经实现了一个新的 step,这个可以用于自定义pipeline插件。

2.3 pipeline语法格式
  • 主体结构如下
pipeline {
    agent any //在任何可用的代理上执行流水线阶段
    
    environment { //全局环境变量示例
        CC = 'clang'
    }
    
    parameters {//参数列表,可以直接在pipeline脚本中指定参数,后续步骤中可以使用这些参数,这种模式适用于脚本化参数构建,也就是说不需要再到jenkins中手动指定参数就可以进行构建,比如通过代码push事件触发构建操作
        string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
    }
    
    options { 
        //配置整个构建任务的系统参数,用来控制构建任务的行为,如:构建失败后重试的次数,保留的构建结果数量等等
    }
    
    triggers {//触发器,用于触发自动构建操作
        cron('H */4 * * 1-5')//按定时周期进行构建
    }
    
    tools {
      //指定流水线依赖的插件版本(如maven、jdk等等),如果不指定,使用默认版本的工具
    }
    
    stages {//所有的阶段都需要包含在此标签内
        stage('Build') { 
            environment { //步骤内环境变量示例
                LOCAL_ENV_VAR = "hello local env variable!"
            }
            
            when {//有条件的执行当前阶段,满足条件才执行
                branch 'production'//只有当前代码分支名匹配才执行此阶段内的步骤
            }
            
            steps {//包含该stage内的所有操作步骤,如果只有一条命令,可以省略这个标签
                // 详细步骤
                script{//steps下可以插入groovy脚本,类似于java代码
                    //groovy脚本
                }
            }
        }
        stage('Test') { 
            steps {
                // 
            }
        }
        stage('Deploy') { 
            steps {
                // 
            }
        }
        
        stage('Parallel Stage') {//并行执行的步骤示例
            when {
                branch 'master'
            }
            failFast true //只要有任意一个失败,则立即终止所有任务
            parallel {//此标签下的stage都可以同时并行执行
                stage('Branch A') {
                    agent {
                        label "for-branch-a"
                    }
                    steps {
                        echo "On Branch A"
                    }
                }
                stage('Branch B') {
                    agent {
                        label "for-branch-b"
                    }
                    steps {
                        echo "On Branch B"
                    }
                }
            }
        }
      }
    }

	post {//监听此任务执行状态变化,进行额外的处理,比如告警等等操作
        always {
            //无论流水线或阶段的完成状态如何,都允许在 post 部分运行该步骤。
            script{//可以插入groovy脚本,类似于java代码
                //groovy脚本
            }
        }
        changed {
            //只有当前流水线或阶段的完成状态与它之前的运行不同时,才允许在 post 部分运行该步骤。
        }
        success {
            //只有当前流水线或阶段的完成状态为"success",才允许在 post 部分运行该步骤, 通常web UI是蓝色或绿色。
        }
        unstable {
            //只有当前流水线或阶段的完成状态为"unstable",才允许在 post 部分运行该步骤, 通常由于测试失败,代码违规等造成。通常web UI是黄色。
        }
        failure {
            //只有当前流水线或阶段的完成状态为"failure",才允许在 post 部分运行该步骤, 通常web UI是红色。
        }
        aborted {
            //只有当前流水线或阶段的完成状态为"aborted",才允许在 post 部分运行该步骤, 通常由于流水线被手动的aborted。通常web UI是灰色。
        }
    }
}
  • 组件使用说明
代理

agent 部分指定了整个流水线或特定的部分, 将会在Jenkins环境中执行的位置,这取决于 agent 区域的位置。该部分必须在 pipeline 块的顶层被定义, 但是 stage 级别的使用是可选的。

参数

为了支持作者可能有的各种各样的用例流水线, agent 部分支持一些不同类型的参数。这些参数应用在pipeline块的顶层, 或 stage指令内部。

  • any
    在任何可用的代理上执行流水线或阶段。例如: agent any
  • none
    当在 pipeline 块的顶部没有全局代理, 该参数将会被分配到整个流水线的运行中并且每个 stage 部分都需要包含他自己的 agent 部分。比如: agent none
  • label
    在提供了标签的 Jenkins 环境中可用的代理上执行流水线或阶段。 例如: agent { label 'my-defined-label' }
  • node
    agent { node { label 'labelName' } }agent { label 'labelName' } 一样, 但是 node 允许额外的选项 (比如 customWorkspace )。
  • docker
    使用给定的容器执行流水线或阶段。该容器将在预置的 node上,或在匹配可选定义的label 参数上,动态的供应来接受基于Docker的流水线。 docker 也可以选择的接受 args 参数,该参数可能包含直接传递到 docker run 调用的参数, 以及 alwaysPull 选项, 该选项强制 docker pull ,即使镜像名称已经存在。 比如: agent { docker 'maven:3-alpine' }agent { docker { image 'maven:3-alpine' label 'my-defined-label' args '-v /tmp:/tmp' } }
  • dockerfile
    执行流水线或阶段, 使用从源代码库包含的 Dockerfile 构建的容器。为了使用该选项, Jenkinsfile 必须从多个分支流水线中加载, 或者加载 “Pipeline from SCM.” 通常,这是源代码仓库的根目录下的 Dockerfile : agent { dockerfile true }. 如果在另一个目录下构建 Dockerfile , 使用 dir 选项: agent { dockerfile {dir 'someSubDir' } }。如果 Dockerfile 有另一个名称, 你可以使用 filename 选项指定该文件名。你可以传递额外的参数到 docker build ... 使用 additionalBuildArgs 选项提交, 比如 agent { dockerfile {additionalBuildArgs '--build-arg foo=bar' } }。 例如, 一个带有 build/Dockerfile.build 的仓库,期望一个构建参数 version:agent { // Equivalent to "docker build -f Dockerfile.build --build-arg version=1.0.2 ./build/ dockerfile { filename 'Dockerfile.build' dir 'build' label 'my-defined-label' additionalBuildArgs '--build-arg version=1.0.2' } }
常见选项

有一些应用于两个或更多 agent 的实现的选项。他们不被要求,除非特别规定。

  • label
    一个字符串。该标签用于运行流水线或个别的 stage。该选项对 node, dockerdockerfile 可用, node要求必须选择该选项。
  • customWorkspace
    一个字符串。在自定义工作区运行应用了 agent 的流水线或个别的 stage, 而不是默认值。 它既可以是一个相对路径, 在这种情况下,自定义工作区会存在于节点工作区根目录下, 或者一个绝对路径。比如:agent { node { label 'my-defined-label' customWorkspace '/some/other/path' } }该选项对 node, dockerdockerfile 有用 。
  • reuseNode
    一个布尔值, 默认为false。 如果是true, 则在流水线的顶层指定的节点上运行该容器, 在同样的工作区, 而不是在一个全新的节点上。这个选项对 dockerdockerfile 有用, 并且只有当 使用在个别的 stageagent 上才会有效。
示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent { docker 'maven:3-alpine' } 
    stages {
        stage('Example Build') {
            steps {
                sh 'mvn -B clean verify'
            }
        }
    }
}

在一个给定名称和标签(maven:3-alpine)的新建的容器上执行定义在流水线中的所有步骤 。

阶段级别的 agent 部分

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent none 
    stages {
        stage('Example Build') {
            agent { docker 'maven:3-alpine' } 
            steps {
                echo 'Hello, Maven'
                sh 'mvn --version'
            }
        }
        stage('Example Test') {
            agent { docker 'openjdk:8-jre' } 
            steps {
                echo 'Hello, JDK'
                sh 'java -version'
            }
        }
    }
}

在流水线顶层定义 agent none 确保 an Executor 没有被分配。 使用 agent none 也会强制 stage 部分包含他自己的 agent 部分。

使用镜像在一个新建的容器中执行该阶段的该步骤。

使用一个与之前阶段不同的镜像在一个新建的容器中执行该阶段的该步骤。

post

post 部分定义一个或多个steps ,这些阶段根据流水线或阶段的完成情况而 运行(取决于流水线中 post 部分的位置). post 支持以下 post-condition 块中的其中之一: always, changed, failure, success, unstable, 和 aborted。这些条件块允许在post 部分的步骤的执行取决于流水线或阶段的完成状态。

Conditions
  • always无论流水线或阶段的完成状态如何,都允许在 post 部分运行该步骤。
  • changed只有当前流水线或阶段的完成状态与它之前的运行不同时,才允许在 post 部分运行该步骤。
  • failure只有当前流水线或阶段的完成状态为"failure",才允许在 post 部分运行该步骤, 通常web UI是红色。
  • success只有当前流水线或阶段的完成状态为"success",才允许在 post 部分运行该步骤, 通常web UI是蓝色或绿色。
  • unstable只有当前流水线或阶段的完成状态为"unstable",才允许在 post 部分运行该步骤, 通常由于测试失败,代码违规等造成。通常web UI是黄色。
  • aborted只有当前流水线或阶段的完成状态为"aborted",才允许在 post 部分运行该步骤, 通常由于流水线被手动的aborted。通常web UI是灰色。
示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
    post { 
        always { 
            echo 'I will always say Hello again!'
        }
    }
}

按照惯例, post 部分应该放在流水线的底部。

Post-condition 块包含与 steps 部分相同的steps

stages

包含一系列一个或多个 stage 指令, stages 部分是流水线描述的大部分"work" 的位置。 建议 stages 至少包含一个 stage 指令用于连续交付过程的每个离散部分,比如构建, 测试, 和部署。

示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages { 
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

stages 部分通常会遵循诸如 agent, options 等的指令。

steps

steps 部分在给定的 stage 指令中执行的定义了一系列的一个或多个steps

示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example') {
            steps { 
                echo 'Hello World'
            }
        }
    }
}

steps 部分必须包含一个或多个步骤。

指令
environment

environment 指令制定一个 键-值对序列,该序列将被定义为所有步骤的环境变量,或者是特定于阶段的步骤, 这取决于 environment 指令在流水线内的位置。

该指令支持一个特殊的助手方法 credentials() ,该方法可用于在Jenkins环境中通过标识符访问预定义的凭证。对于类型为 "Secret Text"的凭证, credentials() 将确保指定的环境变量包含秘密文本内容。对于类型为 "SStandard username and password"的凭证, 指定的环境变量指定为 username:password ,并且两个额外的环境变量将被自动定义 :分别为 MYVARNAME_USRMYVARNAME_PSW

示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    environment { 
        CC = 'clang'
    }
    stages {
        stage('Example') {
            environment { 
                AN_ACCESS_KEY = credentials('my-prefined-secret-text') 
            }
            steps {
                sh 'printenv'
            }
        }
    }
}

顶层流水线块中使用的 environment 指令将适用于流水线中的所有步骤。

在一个 stage 中定义的 environment 指令只会将给定的环境变量应用于 stage 中的步骤。

environment 块有一个 助手方法 credentials() 定义,该方法可以在 Jenkins 环境中用于通过标识符访问预定义的凭证。

options

options 指令允许从流水线内部配置特定于流水线的选项。 流水线提供了许多这样的选项, 比如 buildDiscarder,但也可以由插件提供, 比如 timestamps.

可用选项
  • buildDiscarder
    为最近的流水线运行的特定数量保存组件和控制台输出。例如: options { buildDiscarder(logRotator(numToKeepStr: '1')) }
  • disableConcurrentBuilds
    不允许同时执行流水线。 可被用来防止同时访问共享资源等。 例如: options { disableConcurrentBuilds() }
  • overrideIndexTriggers
    允许覆盖分支索引触发器的默认处理。 如果分支索引触发器在多分支或组织标签中禁用, options { overrideIndexTriggers(true) } 将只允许它们用于促工作。否则, options { overrideIndexTriggers(false) } 只会禁用改作业的分支索引触发器。
  • skipDefaultCheckout
    agent 指令中,跳过从源代码控制中检出代码的默认情况。例如: options { skipDefaultCheckout() }
  • skipStagesAfterUnstable
    一旦构建状态变得UNSTABLE,跳过该阶段。例如: options { skipStagesAfterUnstable() }
  • checkoutToSubdirectory
    在工作空间的子目录中自动地执行源代码控制检出。例如: options { checkoutToSubdirectory('foo') }
  • timeout
    设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。例如: options { timeout(time: 1, unit: 'HOURS') }
  • retry
    在失败时, 重新尝试整个流水线的指定次数。 For example: options { retry(3) }
  • timestamps
    预谋所有由流水线生成的控制台输出,与该流水线发出的时间一致。 例如: options { timestamps() }
Example

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    options {
        timeout(time: 1, unit: 'HOURS') 
    }
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

指定一个小时的全局执行超时, 在此之后,Jenkins 将中止流水线运行。

一个完整的可用选项列表正在等待完成第 INFRA-1503次。

阶段选项

stageoptions 指令类似于流水线根目录上的 options 指令。然而, stage -级别 options 只能包括 retry, timeout, 或 timestamps 等步骤, 或与 stage 相关的声明式选项,如 skipDefaultCheckout

stage, options 指令中的步骤在进入 agent 之前被调用或在 when 条件出现时进行检查。

可选的阶段选项
  • skipDefaultCheckout
    agent 指令中跳过默认的从源代码控制中检出代码。例如: options { skipDefaultCheckout() }
  • timeout
    设置此阶段的超时时间, 在此之后, Jenkins 会终止该阶段。 例如: options { timeout(time: 1, unit: 'HOURS') }
  • retry
    在失败时, 重试此阶段指定次数。 例如: options { retry(3) }
  • timestamps
    预谋此阶段生成的所有控制台输出以及该行发出的时间一致。例如: options { timestamps() }
示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example') {
            options {
                timeout(time: 1, unit: 'HOURS') 
            }
            steps {
                echo 'Hello World'
            }
        }
    }
}
参数

parameters 指令提供了一个用户在触发流水线时应该提供的参数列表。这些用户指定参数的值可通过 params 对象提供给流水线步骤, 了解更多请参考示例

可用参数
  • string
    字符串类型的参数, 例如: parameters { string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') }
  • booleanParam
    布尔参数, 例如: parameters { booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') }
示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    parameters {
        string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
    }
    stages {
        stage('Example') {
            steps {
                echo "Hello ${params.PERSON}"
            }
        }
    }
}
触发器

triggers 指令定义了流水线被重新触发的自动化方法。对于集成了源( 比如 GitHub 或 BitBucket)的流水线, 可能不需要 triggers ,因为基于 web 的集成很肯能已经存在。 当前可用的触发器是 cron, pollSCMupstream

  • cron
    接收 cron 样式的字符串来定义要重新触发流水线的常规间隔 ,比如: triggers { cron('H */4 * * 1-5') }
  • pollSCM
    接收 cron 样式的字符串来定义一个固定的间隔,在这个间隔中,Jenkins 会检查新的源代码更新。如果存在更改, 流水线就会被重新触发。例如: triggers { pollSCM('H */4 * * 1-5') }
  • upstream
    接受逗号分隔的工作字符串和阈值。 当字符串中的任何作业以最小阈值结束时,流水线被重新触发。例如: triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) }

pollSCM 只在Jenkins 2.22 及以上版本中可用。

示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    triggers {
        cron('H */4 * * 1-5')
    }
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}
stage

stage 指令在 stages 部分进行,应该包含一个 实际上, 流水巷所做的所有实际工作都将封装进一个或多个 stage 指令中。

示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}
工具

定义自动安装和放置 PATH 的工具的一部分。如果 agent none 指定,则忽略该操作。

支持工具
  • maven
  • jdk
  • gradle
示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    tools {
        maven 'apache-maven-3.0.1' 
    }
    stages {
        stage('Example') {
            steps {
                sh 'mvn --version'
            }
        }
    }
}
input

stageinput 指令允许你使用 input提示输入。 在应用了 options 后,进入 stageagent 或评估 when 条件前,stage 将暂停。 如果 input 被批准, stage 将会继续。 作为 input 提交的一部分的任何参数都将在环境中用于其他 stage

配置项
  • message
    必需的。 这将在用户提交 input 时呈现给用户。
  • id
    input 的可选标识符, 默认为 stage 名称。
  • ok
    input表单上的"ok" 按钮的可选文本。
  • submitter
    可选的以逗号分隔的用户列表或允许提交 input 的外部组名。默认允许任何用户。
  • submitterParameter
    环境变量的可选名称。如果存在,用 submitter 名称设置。
  • parameters
    提示提交者提供的一个可选的参数列表。 更多信息参见 [parameters]
示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example') {
            input {
                message "Should we continue?"
                ok "Yes, we should."
                submitter "alice,bob"
                parameters {
                    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
                }
            }
            steps {
                echo "Hello, ${PERSON}, nice to meet you."
            }
        }
    }
}
when

when 指令允许流水线根据给定的条件决定是否应该执行阶段。 when 指令必须包含至少一个条件。 如果 when 指令包含多个条件, 所有的子条件必须返回True,阶段才能执行。 这与子条件在 allOf 条件下嵌套的情况相同 (参见下面的示例)。

使用诸如 not, allOf, 或 anyOf 的嵌套条件可以构建更复杂的条件结构 can be built 嵌套条件刻意潜逃到任意深度。

Required

No

Parameters

None

Allowed

Inside a stage directive

内置条件
  • branch
    当正在构建的分支与模式给定的分支匹配时,执行这个阶段, 例如: when { branch 'master' }。注意,这只适用于多分支流水线。
  • environment
    当指定的环境变量是给定的值时,执行这个步骤, 例如: when { environment name: 'DEPLOY_TO', value: 'production' }
  • expression
    当指定的Groovy表达式评估为true时,执行这个阶段, 例如: when { expression { return params.DEBUG_BUILD } }
  • not
    当嵌套条件是错误时,执行这个阶段,必须包含一个条件,例如: when { not { branch 'master' } }
  • allOf
    当所有的嵌套条件都正确时,执行这个阶段,必须包含至少一个条件,例如: when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
  • anyOf
    当至少有一个嵌套条件为真时,执行这个阶段,必须包含至少一个条件,例如: when { anyOf { branch 'master'; branch 'staging' } }
在进入 stageagent 前评估 when

默认情况下, 如果定义了某个阶段的代理,在进入该stageagent 后该 stagewhen 条件将会被评估。但是, 可以通过在 when 块中指定 beforeAgent 选项来更改此选项。 如果 beforeAgent 被设置为 true, 那么就会首先对 when 条件进行评估 , 并且只有在 when 条件验证为真时才会进入 agent

示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
                environment name: 'DEPLOY_TO', value: 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                allOf {
                    branch 'production'
                    environment name: 'DEPLOY_TO', value: 'production'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
                anyOf {
                    environment name: 'DEPLOY_TO', value: 'production'
                    environment name: 'DEPLOY_TO', value: 'staging'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        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'
            }
        }
    }
}

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent none
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            agent {
                label "some-label"
            }
            when {
                beforeAgent true
                branch 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}
并行

声明式流水线的阶段可以在他们内部声明多隔嵌套阶段, 它们将并行执行。 注意,一个阶段必须只有一个 stepsparallel 的阶段。 嵌套阶段本身不能包含进一步的 parallel 阶段, 但是其他的阶段的行为与任何其他 stage 相同。任何包含 parallel 的阶段不能包含 agenttools 阶段, 因为他们没有相关 steps

另外, 通过添加 failFast true 到包含 parallelstage 中, 当其中一个进程失败时,你可以强制所有的 parallel 阶段都被终止。

示例

Jenkinsfile (Declarative Pipeline)

pipeline {
    agent any
    stages {
        stage('Non-Parallel Stage') {
            steps {
                echo 'This stage will be executed first.'
            }
        }
        stage('Parallel Stage') {
            when {
                branch 'master'
            }
            failFast true
            parallel {
                stage('Branch A') {
                    agent {
                        label "for-branch-a"
                    }
                    steps {
                        echo "On Branch A"
                    }
                }
                stage('Branch B') {
                    agent {
                        label "for-branch-b"
                    }
                    steps {
                        echo "On Branch B"
                    }
                }
            }
        }
    }
}
步骤

声明式流水线可能使用在 流水线步骤引用中记录的所有可用的步骤, 它包含一个完整的步骤列表, 其中添加了下面列出的步骤,这些步骤只在声明式流水线中 only supported

脚本

script 步骤需要 [scripted-pipeline]块并在声明式流水线中执行。 对于大多数用例来说,应该声明式流水线中的“脚本”步骤是不必要的, 但是它可以提供一个有用的"逃生出口"。 非平凡的规模和/或复杂性的 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流水线脚本存储模式
  • jenkins job内脚本模式

在jenkins 管理后台的job任务中直接编写pipeline脚本

  • 项目源码仓库内脚本

项目源码SCM仓库中包含了pipeline脚本,用于复杂的流水线项目,其很难在流水线配置页面 经典 UI脚本文本区域进行编写和维护。而且可以把jenkins pipeline的构建脚本也纳入源码版本控制中,便于修改和版本回溯。

2.5 使用小技巧总结
  • 脚本化参数和job界面上配置的参数二选一即可

如果习惯手动构建任务,建议直接使用配置界面上的参数配置,使用简单灵活

如果需要自动化触发构建,建议使用脚本化参数

  • script脚本的插入位置说明

需要插入到具体执行步骤的位置,如 steps、success下等等

  • stage动态生成能力

可以借助脚本动态生成多个自定义的stage,注意的是,这些stage必须包含在同一个父stage内才行,内部stage内不允许再写steps标签

  • steps标签使用注意事项

steps标签不允许嵌套

  • 获取shell脚本执行结果
def result = sh returnStdout: true, script: '<具体的shell命令>' //result用于存储返回结果
  • 执行任务异常信息捕获

stage标签和script标签下允许使用try{}catch(Exception e){}进行执行异常的捕获操作

3、pipeline指令生成工具的使用

一些常用的jenkins步骤,如拉取代码、单元测试、发送邮件、远程shell操作等等,jenkins都提供了工具自动生成pipeline代码片段,我们只需要点击界面进行配置,然后生成脚本片段复制到我们的steps下即可。

操作截图如下所示:

jenkins 全局变量引用插件_jenkins_05

我们以拉取git仓库中的代码为例:

jenkins 全局变量引用插件_jenkins_06

4、java应用devops实践

4.1 项目结构
│
├─src
│  ├─main
│  │  ├─java
│  ├─main
│  │  ├─resources
│  ├─test
│    ├─java
│
├─subModule1 #子模块一
│
├─subModule2 #子模块二
│
├─Jenkinsfile #jenkins pipeline脚本
│
├─deploy.sh #docker swarm 服务部署脚本
│
└─pom.xml
4.2 devops相关脚本
4.2.1 依赖的maven插件配置
<build>
	    <plugins>
	        <plugin>
	                <groupId>org.apache.maven.plugins</groupId>
	                <artifactId>maven-surefire-plugin</artifactId>    
	                <configuration>
	                    <skipTests>false</skipTests>                    
	                </configuration> 	                      
	        </plugin>
	        <plugin>
	                <groupId>org.sonarsource.scanner.maven</groupId>
				    <artifactId>sonar-maven-plugin</artifactId>
				    <version>3.7.0.1746</version>
	        </plugin>
            <plugin>
                	<groupId>org.jacoco</groupId>
                	<artifactId>jacoco-maven-plugin</artifactId>
                	<version>0.8.2</version>
            </plugin>
	    </plugins>
    </build>
4.2.2 pipeline代码
def PROJ_VERSION = "1.0.0"
def errMsg='';
pipeline{
    
    agent any
    
    tools {
      maven "apache-maven-3.6.3-bin"
    }
    
    options { 
        //只保留最近三次构建的包
        buildDiscarder(logRotator(numToKeepStr: '6'))
        //跳过默认的代码检出操作
        skipDefaultCheckout(true) 
        //如果整个流程执行异常导致失败可以自动重试
        //retry(1)
    }
    
    
    stages {
        stage('拉取最新代码') {
            steps{
                sh 'printenv'
                echo "拉取[${SOURCE_TAG}]的最新代码......"
                script{
                    try{
                        //提取分支或者tag名
                        // String sourceTag = params.SOURCE_TAG.substring(params.SOURCE_TAG.lastIndexOf("/")+1,params.SOURCE_TAG.length());
                        echo "checkout branches:[ ${SOURCE_TAG} ]"
                        checkout([$class: 'GitSCM', branches: [[name: params.SOURCE_TAG]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'xxxx-xxx-xxx-xxx-xxxxxxx', url: 'http://localhost:3000/test/helloworld.git']]])
                        if(params.PROJ_VERSION == null || params.PROJ_VERSION.trim().equals("")){
                            PROJ_VERSION = sh returnStdout: true, script: 'mvn help:evaluate -Dexpression=project.version -q -DforceStdout'
                        } 
                    }catch(Exception e){
                        errMsg = e;
                        throw e
                    }                    
                }                            
                echo "PROJ_VERSION:${PROJ_VERSION}"                
            }
        }     
        
        stage('代码检查') {
            steps{
                script{   
                    try{
                    
                        echo "mvn clean compile sonar:sonar -Dsonar.projectKey=bsp-ota -Dsonar.host.url=http://localhost:9191 -Dsonar.login=xxxxxxxxx 执行代码静态检查任务......"
                        sh "mvn clean compile sonar:sonar -Dsonar.projectKey=bsp-ota -Dsonar.host.url=http://localhost:9191 -Dsonar.login=xxxxxxxxx"
                        
                    }catch(Exception e){
                        errMsg = e;
                        throw e
                    } 
               } 
            }
        } 
        
        stage('构建依赖模块') {
            steps{
                script{   
                    try{
                        echo "mvn clean install ${MODULE_IDS}和其依赖的基础模块......"
                        sh "mvn clean install -pl ${MODULE_IDS} -am -U -Dmaven.test.skip=true"
                        
                    }catch(Exception e){
                        errMsg = e;
                        throw e
                    } 
               } 
            }
        }  
        
        stage('动态生成子stage'){
            failFast true
            steps{
                echo "开始打包镜像和发布服务......"
                 script{   
                   try{
                  
                    String[] modules = params.MODULE_IDS.split(",");
                    for(module in modules){
                        
                            String testStageName='单元测试:'+module;
                            String pkgStageName='生成镜像:'+module;
                            String deployStageName='部署项目:'+module;
                            stage(testStageName) {
                                    echo "${module}注入jacoco插件配置,clean test执行单元测试代码......并将结果上传到将SonarQaue"
                                    dir (module){            
                                          withSonarQubeEnv('sonar') {
                                                sh "mvn org.jacoco:jacoco-maven-plugin:prepare-agent  test -U -Dautoconfig.skip=true -Dmaven.test.skip=false -Dmaven.test.failure.ignore=true sonar:sonar -Dsonar.projectKey=bsp-ota -Dsonar.host.url=http://localhost:9191 -Dsonar.login=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                                                //95%通过即为合格
                                                junit allowEmptyResults: true, healthScaleFactor: 5.0, testResults: 'target/surefire-reports/*.xml'
                                                                                           
                                          }
                                          echo "配置单元测试覆盖率要求,未达到要求pipeline将会fail,code coverage.LineCoverage>80%. 作用为将结果打印到jenkins 中,通过 jenkins jacoco plugin "
                                          /*jacoco changeBuildStatus: true, maximumLineCoverage:"80"*/
                                          jacoco buildOverBuild: true, deltaClassCoverage: '5', deltaLineCoverage: '10', deltaMethodCoverage: '10', maximumClassCoverage: '80', maximumLineCoverage: '80', maximumMethodCoverage: '80', minimumClassCoverage: '60', minimumLineCoverage: '60', minimumMethodCoverage: '60', runAlways: true, sourceInclusionPattern: '**/*.java'
                                              
                                          timeout(3) { //一分钟   
                                               //利用sonar webhook功能通知pipeline代码检测结果,未通过质量阈,pipeline将会fail
                                               def qg = waitForQualityGate() 
                                               if (qg.status != 'OK') {
                                                   error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
                                               }
                                           }     
                                    }                                    
                            } 
                            stage(pkgStageName) {
                                    echo "构建${module}镜像并推送到镜像仓库......"
                                    dir (module){
                                        sh "mvn install -P docker docker:build docker:push -U -DskipTests=true"
                                    }                                    
                            }                               
                            stage(deployStageName) {
                                    echo "部署项目${module}......"
                                    script{                    
                                        def remote = getServer(SWARM_HOST_IP);
                                        echo "starting ${module}${PROJ_VERSION} docker swarm deploy ......"
                                        def _command='sh deploy.sh '+module+" "+PROJ_VERSION
                                        if(params.EXPOSE_PORT != null && !params.EXPOSE_PORT.trim().equals("")){
                                           _command='sh deploy.sh '+module+" "+PROJ_VERSION +" "+params.EXPOSE_PORT+":"+params.INNER_PORT 
                                        }
                                        echo "执行远程命令:"+_command
                                        sshPut remote: remote, from: 'deploy.sh', into: 'deploy.sh'
                                        sshCommand remote: remote, command: _command
                                        sshRemove remote: remote, path: 'deploy.sh'
                                    }
                            }                                               
                    }
                  }catch(Exception e){
                        errMsg = e
                        throw e
                  }                                      
                }
                
                echo "打包镜像和发布服务完成......"    
            }                       
        }
        //集成测试,可并行测试,节省时间
        
     }   
     
     post { 
        failure { //执行流水线失败进行邮件提醒
            
            script{
                if(NEED_NOTIFY_EMAIL){
                    echo '执行任务失败,邮件通知对应的研发人员!'
                    String content='构建任务'+env.JOB_BASE_NAME+'失败,请及时查看!\n原因:'+errMsg
                    String title='[ERROR]-jenkins任务'+env.JOB_NAME+'-'+env.BUILD_NUMBER+'构建结果提醒'
                    mail bcc: '', body: content, cc: '', from: 'activate@skyroam.com', replyTo: '', subject: title, to: params.EMAILS
                }                
            }           
        }
        success { //执行成功进行邮件提醒
            echo '执行任务成功,邮件通知对应的研发人员!'
            script{
                if(NEED_NOTIFY_EMAIL){
                    String content='执行任务'+env.JOB_BASE_NAME+'-'+env.BUILD_NUMBER+'成功,请及时查看!'
                    String title='[SUCCESS]-jenkins任务'+env.JOB_NAME+'-'+env.BUILD_NUMBER+'构建结果提醒'
                    mail bcc: '', body: content, cc: '', replyTo: '', subject: title, to: params.EMAILS
                }               
            }           
        }
    }
       
}

def getServer(ip){
    def remote = [:]
    remote.name = "server-${ip}"
    remote.host = ip
    remote.port = 52113
    remote.allowAnyHosts = true
    withCredentials([usernamePassword(credentialsId: SWRAM_MANAGER_HOST_ID, passwordVariable: 'password', usernameVariable: 'username')]) {
        remote.user = "${username}"
        remote.password = "${password}"
    }
    return remote
}
4.2.4 docker swarm 服务部署脚本 deploy.sh
#!/bin/bash
    #项目名称
    IMAGE_NAME=$1
    #将要要发布的版本
    IMAGE_VERSION=$2
    PJ_PORT=$3
    SERVICE_NAME=$IMAGE_NAME
    #如果机器上环境变量未指定本地镜像仓库地址,则使用默认的地址
    if [ ! -n "$REMOTE_IMAGE_REPO" ]; then
        REMOTE_IMAGE_REPO=127.0.0.1:5000
    fi
    #检查
    echo $(sudo -E docker service ls | grep $IMAGE_NAME)
    _count=$(sudo -E docker service ls | grep $IMAGE_NAME | wc -l)
    if [ $_count -gt 0 ]; then
      #获取服务名
      SERVICE_NAME=$(sudo -E docker service ls | grep $IMAGE_NAME | awk '{print $2}')
      echo "sudo -E docker service update $SERVICE_NAME $REMOTE_IMAGE_REPO/$IMAGE_NAME:$IMAGE_VERSION"
      sudo -E docker service update $SERVICE_NAME --image $REMOTE_IMAGE_REPO/$IMAGE_NAME:$IMAGE_VERSION
    else
      echo "sudo -E docker service create $IMAGE_NAME $REMOTE_IMAGE_REPO/$IMAGE_NAME:$IMAGE_VERSION"
      if [ $PJ_PORT ]; then
        sudo -E docker service create --name $IMAGE_NAME \
          --network bridge --replicas 1 \
          --publish PJ_PORT \
          --restart-condition on-failure \
          --mount type=bind,src=/home/release/conf/,dst=/home/release/conf/ \
          --constraint 'node.role==worker' $REMOTE_IMAGE_REPO/$IMAGE_NAME:$IMAGE_VERSION
      else
        sudo -E docker service create --name $IMAGE_NAME \
          --network bridge --replicas 1 \
          --restart-condition on-failure \
          --mount type=bind,src=/home/release/conf/,dst=/home/release/conf/ \
          --constraint 'node.role==worker' $REMOTE_IMAGE_REPO/$IMAGE_NAME:$IMAGE_VERSION
      fi  
    fi
4.2.5 jenkins中任务的参数

jenkins 全局变量引用插件_jenkins_07

4.2.6 pipeline执行结果

jenkins 全局变量引用插件_jenkins 全局变量引用插件_08

4.2.7 sonarque检查结果

jenkins 全局变量引用插件_devops_09