在Jenkins的官方文档中,可以看到到处都是Pipeline的身影,而且文档多数内容都是讲Pipeline。由此可见,Pipeline非常重要,我愿称之为Jenkins的精髓。最重要的是,Pipeline还很好用。接下来,以一个多模块的Maven项目为例,记录一下我使用Pipeline的一些经验。

准备

  • Pipeline
  • Blue Ocean

插件管理里面直接搜就行了。Blue Ocean不是必须的,但是强烈推荐。

项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
cloud
|-auth
|-common           #公共包,被其他模块引用
|-gateway
|-service
|-----system
|-----pom.xml
|-service-api
|-----system-api   #被system引用
|-----pom.xml
|-pom.xml
|-deploy.sh
|-Jenkinsfile

这个项目结构,我相信已经符合大多数人的需要。所以,下面我给出的Jenkinsfile会有一定的参考价值。

流水线任务

新建

图片

安装了Pipeline插件就可以新建流水线任务了。

流水线脚本来源

图片

Pipeline任务的重点当然就是流水线脚本了。几乎所有的工作都在脚本里面了。

可以看出,有两种方式。

  • 直接在Jenkins上写
  • SCM检出

这里我用第二种,从SVN上检出Jenkinsfile

Jenkinsfile

接下来就是重中之重了。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
pipeline {
    agent any
    parameters {
        choice(name: 'MODULE', choices: ['all', 'auth', 'gateway', 'system'], description: '请选择部署模块!')
        choice(name: 'PUBLISH_SERVER', choices: ['127.0.0.1'], description: '目标服务器')
        choice(name: 'ACTIVE', choices:['prod', 'test', 'dev'], description: 'spring.profiles.active')
    }
    environment {
        // SVN仓库地址
        REPO = 'https://xxx/cloud'
        // Jenkins凭据ID
        REPO_CREDENTIALSID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
        // Publish Over SSH Remote Directory
        REMOTE_DIR = '/app/cloud'
    }
    tools {
        maven 'apache-maven-3.6.3'
    }
    stages {
        // 检出代码
        stage('Checkout') {
            steps {
                checkout([$class: 'SubversionSCM', additionalCredentials: [], excludedCommitMessages: '', excludedRegions: '', excludedRevprop: '', excludedUsers: '', filterChangelog: false, ignoreDirPropChanges: false, includedRegions: '', locations: [[cancelProcessOnExternalsFail: true, credentialsId: "${env.REPO_CREDENTIALSID}", depthOption: 'infinity', ignoreExternalsOption: true, local: '.', remote: "${env.REPO}"]], quietOperation: true, workspaceUpdater: [$class: 'UpdateUpdater']])
            }
        }
        // 将deploy.sh传送到目标服务器
        stage('Transfer Shell Script') {
            steps {
                sshPublisher(publishers: [sshPublisherDesc(configName: "${params.PUBLISH_SERVER}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "chmod 777 ${REMOTE_DIR}/shell/deploy.sh", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'shell', remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'deploy.sh')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
            }
        }
        // 使用Maven打包
        stage('Maven Package') {
            parallel {
                // 打包所有项目
                stage('Package All') {
                    when {
                        equals expected: "all", actual: "${params.MODULE}"
                    }
                    steps {
                        sh "mvn clean package -Dmaven.test.skip=true"
                    }
                }
                // 打包指定的项目
                stage('Package Specific Module') {
                    when {
                        not {
                            equals expected: "all", actual: "${params.MODULE}"
                        }
                    }
                    steps {
                        sh "mvn clean package -pl com.github.l-qiang:${params.MODULE} -am -amd -Dmaven.test.skip=true"
                    }
                }
            }
        }
        // 部署: 传送jar包, 然后执行deploy.sh
        stage('Deploy') {
            parallel {
                stage('auth') {
                    when {
                        anyOf {
                            equals expected: "auth", actual: "${params.MODULE}"
                            equals expected: "all", actual: "${params.MODULE}"
                        }
                    }
                    steps {
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${params.PUBLISH_SERVER}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${REMOTE_DIR}/shell/deploy.sh restart auth ${params.ACTIVE}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: "auth/target", sourceFiles: "auth/target/*.jar")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
                    }
                }
                stage('gateway') {
                    when {
                        anyOf {
                            equals expected: "gateway", actual: "${params.MODULE}"
                            equals expected: "all", actual: "${params.MODULE}"
                        }
                    }
                    steps {
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${params.PUBLISH_SERVER}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${REMOTE_DIR}/shell/deploy.sh restart gateway ${params.ACTIVE}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: "gateway/target", sourceFiles: "gateway/target/*.jar")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
                    }
                }
                stage('system') {
                    when {
                        anyOf {
                            equals expected: "system", actual: "${params.MODULE}"
                            equals expected: "all", actual: "${params.MODULE}"
                        }
                    }
                    steps {
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${params.PUBLISH_SERVER}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${REMOTE_DIR}/shell/deploy.sh restart system ${params.ACTIVE}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: "service/system/target", sourceFiles: "service/system/target/*.jar")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
                    }
                }
            }
        }
    }

    options {
        // 丢弃旧的构建, 保持构建的最大个数
        buildDiscarder(logRotator(numToKeepStr: '5'))
    }
}

以上就是完整的Jenkinsfile了。Pipeline脚本分为声明式脚本式, 官方推荐使用声明式,所以这里使用的声明式Pipeline语法就不多说了,接下来主要讲上面这个Jenkinsfile

由外层往内层,先看整体。

1
2
3
4
5
6
7
8
9
pipeline {
    agent any
    parameters {...}
    environment {...}
    tools {...}
    stages {...}
    
    options{...}
}
  • agent

    这里跟Jenkins的节点有关,我这只有一个master节点,所以这里使用any就行。

  • parameters

    这里定义一些参数,及参数的默认值,这些参数是可能会经常改变的值,可与用户交互。这些参数在Pipeline运行的时候,可以修改。通过这些参数,我们可以控制Pipleline的执行流程

    Blue Ocean界面中。点运行就会弹出下列窗口。

    图片

第一次运行的时候,不会弹框,再运行一次即可。不知道之后的版本的Jenkins会不会修复这个问题。

  • environment

    这里定义一些环境变量,这些参数很少需要改变。

​ 注意参数环境变量都要在双引号里面使用,单引号会将内容原样输出。

  • tools

    1
    
    maven 'apache-maven-3.6.3'
    

    配置Maven。这里的Maven必须是全局工具配置里面的。这不是必须的,这里这么做就可以直接执行mvn命令而不需要在Jenkins服务器有任何配置。

  • stages

    这里的步骤大致跟Jenkins部署Maven项目差不多。

    1. Checkout

      从SVN检出代码。这段代码看似复杂其实很简单。使用片段生成器就可以生成。

      图片

      图片

    2. Transfer Shell Script

      将部署脚本传送到目标服务器。相对于之前的Jenkins部署Maven项目,这里有所改动。所有项目模块只使用一份脚本。仍然使用片段生成器来生成Publish Over SSH插件的代码。

默认情况下,Publish Over SSH不输出脚本日志,需要勾选Verbose output in console

图片

  1. Maven Package

    1
    2
    3
    4
    5
    6
    
    stage('Maven Package') {
        parallel {
            stage('Package All'){...}
            stage('Package Specific Module'){...}
        }
    }
    

    这里就是通过whenparameters来控制打包一个模块还是所有模块。这里parallel的作用只是在运行的时候在Blue Ocean里面看起来更像是一个条件分支,而不是用来并行的。

注意

1
sh "mvn clean package -pl com.github.l-qiang:${params.MODULE} -am -amd -Dmaven.test.skip=true"

上述命令如果并行执行,可能会出现问题,因为他们之间有共同的依赖。但是,不会必定出现。

另外,使用groupId:artifactId的方式比使用路径更加灵活。

  1. Deploy

    这一步同样使用whenparameters来控制部署一个模块还是所有模块。选择所有模块可以并行执行。

  • options

    一些额外的配置,这里就配置了丢弃旧的构建。更多配置项见options

运行Pipeline之后的结果如下:

图片


部署脚本

deploy.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
ACTION=$1
MODULE_NAME=$2
ACTIVE=$3
JAR_NAME=$MODULE_NAME-0.0.1-SNAPSHOT.jar
cd /app/cloud
#使用说明,用来提示输入参数
usage() {
echo "Usage: sh deploy.sh [start|stop|restart|status] module spring.profiles.active"
exit 1
}

#检查程序是否在运行
is_exist(){
pid=`ps -ef|grep $JAR_NAME|grep -v grep|awk '{print $2}' `
#如果不存在返回1,存在返回0
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}

#启动方法
start(){
is_exist
if [ $? -eq "0" ]; then
echo "${JAR_NAME} is already running. pid=${pid} ."
else
nohup java -jar $JAR_NAME --spring.profiles.active=$ACTIVE > logs/out_$MODULE_NAME.log 2>&1 &
fi
}

#停止方法
stop(){
is_exist
if [ $? -eq "0" ]; then
kill -9 $pid
else
echo "${JAR_NAME} is not running"
fi
}

#输出运行状态
status(){
is_exist
if [ $? -eq "0" ]; then
echo "${JAR_NAME} is running. Pid is ${pid}"
else
echo "${JAR_NAME} is NOT running."
fi
}

#重启
restart(){
stop
start
}

#根据输入参数,选择执行对应方法,不输入则执行使用说明
case "$ACTION" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac