Jenkins Pipeline 进阶:多平台账号关联与自动化部署

引言

​ 在当今快速迭代的软件开发环境中,持续集成和持续交付 (CI/CD) 已成为不可或缺的实践。Jenkins 作为一款开源的自动化服务器,凭借其强大的插件生态系统和灵活的 Pipeline 功能,成为 CI/CD 领域的佼佼者。本文将介绍如何使用 Jenkins Pipeline 实现 CI/CD,并重点讲解如何将钉钉账号与 OpenLDAP 联动,以及如何实现 Jenkins、GitLab、Harbor 和禅道等多平台的账号关联和集成。

架构图

image-20250121095804587

Jenkins Pipeline CI/CD

​ Jenkins Pipeline 是一种将构建、测试和部署等步骤以代码形式定义的机制。它使用 Groovy DSL 语法,可以将整个 CI/CD 流程编写成一个脚本文件,方便版本控制和重复使用。

部署

​ 如果还没有部署Jenkins,可以使用下面的docker-compose.yml配置快速拉起一个Jenkins

docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
version: '3.7'
services:
  jenkins:
    image: jenkins/jenkins:lts
    container_name: jenkins
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /data/jenkins/jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
    ports:
      - "8080:8080"
      - "50000:500000"
    expose:
      - "8080"
      - "50000"
    privileged: true
    user: root
    restart: always

配置共享库

系统管理 ->系统配置-> Global Trusted Pipeline Libraries

Jenkins Pipeline CI

CI代码

​ 下面是使用maven构建linux/win平台docker镜像的流程

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
@Library('jenkins-shared-library@main') _
// 引入 Jenkins 共享库,名称为 'jenkins-shared-library',使用 'main' 分支。

pipeline {
    agent any
    // 指定 Pipeline 在任何可用的 Jenkins Agent 上运行。

    parameters {
        string(name: 'BRANCH_NAME', defaultValue: 'master', description: 'Git分支名称')
        // 定义一个字符串参数,用于指定 Git 分支名称,默认值为 'master'。

        string(name: 'ZENTAO_REPOPATH', defaultValue: '', description: 'gitlab 项目链接')
        // 定义一个字符串参数,用于指定 GitLab 项目的 URL。

        string(name: 'ZENTAO_ACCOUNT', defaultValue: '', description: 'zentao 用户')
        // 定义一个字符串参数,用于指定禅道(Zentao)的用户名。

        choice(name: 'BUILD_PLATFORM', choices: ['linux', 'windows'], description: '构建平台')
        // 定义一个选择参数,用于指定构建平台,可选值为 'linux' 或 'windows'。
    }

    environment {
        CURRENT_DATE = sh(script: "date +'%Y%m%d%H%M%S'", returnStdout: true).trim()
        // 定义一个环境变量 CURRENT_DATE,值为当前日期时间,格式为 'YYYYMMDDHHMMSS'。

        BUILD_COMMIT = ''
        // 定义一个环境变量 BUILD_COMMIT,初始值为空,后续会存储 Git 提交 ID。

        ImageName = ''
        // 定义一个环境变量 ImageName,初始值为空,后续会存储 Docker 镜像名称。

        GITLAB_GROUP = ''
        // 定义一个环境变量 GITLAB_GROUP,初始值为空,后续会存储 GitLab 项目组名称。

        GITLAB_PROJECT = ''
        // 定义一个环境变量 GITLAB_PROJECT,初始值为空,后续会存储 GitLab 项目名称。
    }

    stages {
        stage("提取项目和项目名") {
            steps {
                script {
                    def res = getGitLabGroupAndProject.extractGitLabInfo(params.ZENTAO_REPOPATH)
                    // 调用共享库中的方法 extractGitLabInfo,从 GitLab 项目 URL 中提取项目组和项目名称。

                    GITLAB_GROUP = res.group
                    // 将提取到的 GitLab 项目组名称赋值给环境变量 GITLAB_GROUP。

                    GITLAB_PROJECT = res.project
                    // 将提取到的 GitLab 项目名称赋值给环境变量 GITLAB_PROJECT。
                }
            }
        }

        stage("构建邮件通知") {
            steps {
                script {
                    // 触发构建消息。
                }
            }
        }

        stage('拉取代码并构建') {
            agent {
                docker {
                    image "${GlobalVars.MavenBuildImage}"
                    // 使用共享库中定义的 Maven 构建镜像。

                    args '-v /data/.m2:/root/.m2 -v /data/jar-package:/data/jar-package'
                    // 挂载宿主机的 Maven 缓存目录和构建产物目录到容器中。

                    reuseNode true
                    // 重用当前节点,避免切换到其他节点。
                }
            }
            steps {
                checkout scmGit(
                    branches: [[name: "*/${params.BRANCH_NAME}"]],
                    // 指定要拉取的分支名称,使用参数 BRANCH_NAME。

                    extensions: [],
                    // 无额外的 Git 扩展配置。

                    userRemoteConfigs: [[
                        credentialsId: "${GlobalVars.GitLabcredentialsId}",
                        // 使用共享库中定义的 GitLab 凭据 ID。

                        url: "https://${GlobalVars.GitlabUrl}/${GITLAB_GROUP}/${GITLAB_PROJECT}.git"
                        // 指定 GitLab 项目的 URL。
                    ]]
                )
                script {
                    sh 'mvn -B -DskipTests clean package'
                    // 使用 Maven 进行构建,跳过测试。

                    sh "cp ruoyi-admin/target/ruoyi-admin.jar /data/jar-package/${GITLAB_GROUP}-${GITLAB_PROJECT}-${CURRENT_DATE}.jar"
                    // 将构建产物(JAR 文件)复制到宿主机的指定目录,并重命名。
                }
            }
        }

        stage('保存构建产物') {
            steps {
                stash includes: 'ruoyi-admin/target/ruoyi-admin.jar', name: 'jar-package'
                // 将构建产物(JAR 文件)保存为 Jenkins 的 stash,供后续阶段使用。
            }
        }

        stage('构建信息') {
            steps {
                script {
                    wrap([$class: 'BuildUser']) {
                        // 使用 Jenkins 的 BuildUser 插件获取构建用户信息。

                        BUILD_USER = "${ZENTAO_ACCOUNT}"
                        // 将禅道用户赋值给环境变量 BUILD_USER。

                        BUILD_NAME = "${GITLAB_GROUP}/${GITLAB_PROJECT}"
                        // 将项目组和项目名称拼接后赋值给环境变量 BUILD_NAME。

                        BUILD_BRANCH = "${BRANCH_NAME}"
                        // 将分支名称赋值给环境变量 BUILD_BRANCH。

                        BUILD_COMMIT = "${getCommitId()}"
                        // 调用共享库中的方法 getCommitId,获取当前 Git 提交 ID,并赋值给环境变量 BUILD_COMMIT。

                        BUILD_PLATFORM = "${params.BUILD_PLATFORM}"
                        // 将构建平台赋值给环境变量 BUILD_PLATFORM。
                    }
                    currentBuild.description = """构建用户: ${BUILD_USER}\n构建项目: ${BUILD_NAME}\n构建分支: ${BUILD_BRANCH}\n构建平台: ${BUILD_PLATFORM}\nMessageID: ${BUILD_COMMIT}
                    """
                    // 设置当前构建的描述信息,包含构建用户、项目、分支、平台和提交 ID。
                }
            }
        }

        stage('构建镜像名称') {
            steps {
                script {
                    ImageName = "${GlobalVars.HarborUrl}/${GITLAB_GROUP}/${GITLAB_PROJECT}:${CURRENT_DATE}-${BUILD_COMMIT}-${params.BUILD_PLATFORM}"
                    // 根据 Harbor 地址、项目组、项目名称、日期、提交 ID 和构建平台生成 Docker 镜像名称。
                }
            }
        }

        stage('打包镜像') {
            steps {
                script {
                    if (params.BUILD_PLATFORM == 'linux') {
                        buildDockerImage.LinuxRuoyiBuild(ImageName)
                        // 如果构建平台是 Linux,调用共享库中的方法 LinuxRuoyiBuild 打包 Docker 镜像。
                    } else if (params.BUILD_PLATFORM == 'windows') {
                        node('win-agent') {
                            unstash 'jar-package'
                            // 如果构建平台是 Windows,切换到 Windows 节点,并恢复之前保存的构建产物。

                            buildDockerImage.WindowsRuoyiBuild(ImageName)
                            // 调用共享库中的方法 WindowsRuoyiBuild 打包 Docker 镜像。
                        }
                    }
                }
            }
        }

        stage('推送镜像') {
            steps {
                script {
                    if (params.BUILD_PLATFORM == 'linux') {
                        withDockerRegistry(credentialsId: "${GlobalVars.HarborcredentialsId}", url: "https://${GlobalVars.HarborUrl}") {
                            sh "docker push ${ImageName}"
                            // 如果构建平台是 Linux,推送 Docker 镜像到 Harbor。

                            sh "docker rmi ${ImageName}"
                            // 删除本地 Docker 镜像以释放空间。
                        }
                    } else if (params.BUILD_PLATFORM == 'windows') {
                        node('win-agent') {
                            withDockerRegistry(credentialsId: "${GlobalVars.HarborcredentialsId}", url: "https://${GlobalVars.HarborUrl}") {
                                bat "docker push ${ImageName}"
                                // 如果构建平台是 Windows,推送 Docker 镜像到 Harbor。

                                bat "docker rmi ${ImageName}"
                                // 删除本地 Docker 镜像以释放空间。
                            }
                        }
                    }
                }
            }
        }
    }

    post {
        always {
            cleanWs()
            // 无论构建成功与否,始终清理工作空间。
        }
        success {
            script {
                // 构建成功时发送通知。
            }
        }
        unsuccessful {
            script {
                // 构建失败时发送通知。
            }
        }
    }
}

该 Jenkins Pipeline 脚本实现了一个完整的 CI 流程

  1. 参数化构建:支持动态指定 Git 分支、GitLab 项目、禅道用户和构建平台。
  2. 代码拉取与构建:从 GitLab 拉取代码并使用 Maven 进行构建。
  3. 镜像打包与推送:根据构建平台(Linux 或 Windows)打包 Docker 镜像并推送到 Harbor。
  4. 构建信息记录:记录构建用户、项目、分支、平台和提交 ID 等信息。
  5. 通知与清理:支持构建成功或失败时发送通知,并始终清理工作空间。

通过共享库的使用,脚本的逻辑更加简洁和模块化,便于维护和扩展。

创建构建流程

写好CI流程,下面在Jenkins添加构建流程

基于禅道关联Jenkins任务

禅道平台配置关联jenkins,方便用户调用触发

Jenkins Pipeline CD

CD 代码

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@Library('jenkins-shared-library@main') _
// 引入 Jenkins 共享库,名称为 'jenkins-shared-library',使用 'main' 分支。

pipeline {
    agent any
    // 指定 Pipeline 在任何可用的 Jenkins Agent 上运行。

    parameters {
        string(name: 'SSH_USER', defaultValue: 'root', description: 'SSH User')
        // 定义一个字符串参数,用于指定 SSH 用户,默认值为 'root'。

        string(name: 'TARGET_IP', defaultValue: '', description: '部署机器 IP 地址')
        // 定义一个字符串参数,用于指定目标机器的 IP 地址。

        string(name: 'DOCKER_IMAGE', defaultValue: '', description: '运行服务镜像地址')
        // 定义一个字符串参数,用于指定 Docker 镜像的名称。

        string(name: 'IMAGE_VERSION', defaultValue: '', description: '运行服务镜像版本号')
        // 定义一个字符串参数,用于指定 Docker 镜像的版本号。

        string(name: 'SERVICE_NAME', defaultValue: '', description: '容器运行名称')
        // 定义一个字符串参数,用于指定 Docker 容器的名称。

        string(name: 'PORT_MAPPING', defaultValue: '', description: '端口映射,eg: 8080:8080')
        // 定义一个字符串参数,用于指定 Docker 容器的端口映射。

        string(name: 'ENV_VARIABLES', defaultValue: '', description: '环境变量,eg: KEY="VALUE",KEY2="VALUE2"')
        // 定义一个字符串参数,用于指定 Docker 容器的环境变量。
    }

    environment {
        SEND_BUILD_USER_EMAIL = ''
        // 定义一个环境变量 SEND_BUILD_USER_EMAIL,初始值为空,后续会存储构建用户的邮箱。
    }

    stages {
        stage('获取参数') {
            steps {
                script {
                    withBuildUser {
                        SEND_BUILD_USER_EMAIL = env.BUILD_USER_EMAIL
                        // 使用 Jenkins 的 BuildUser 插件获取构建用户的邮箱,并赋值给环境变量 SEND_BUILD_USER_EMAIL。
                    }

                    // 检查参数是否为空
                    if (!params.SSH_USER?.trim()) {
                        error "SSH 用户不能为空!"
                        // 如果 SSH 用户为空,抛出错误并终止 Pipeline。
                    }
                    if (!params.TARGET_IP?.trim()) {
                        error "目标机器 IP 地址不能为空!"
                        // 如果目标机器 IP 地址为空,抛出错误并终止 Pipeline。
                    }
                    if (!params.SERVICE_NAME?.trim()) {
                        error "服务名称不能为空!"
                        // 如果服务名称为空,抛出错误并终止 Pipeline。
                    }
                    if (!params.DOCKER_IMAGE?.trim()) {
                        error "Docker 镜像名称不能为空!"
                        // 如果 Docker 镜像名称为空,抛出错误并终止 Pipeline。
                    }
                    if (!params.IMAGE_VERSION?.trim()) {
                        error "Docker 镜像版本号不能为空!"
                        // 如果 Docker 镜像版本号为空,抛出错误并终止 Pipeline。
                    }
                    if (!params.PORT_MAPPING?.trim()) {
                        error "端口映射不能为空!"
                        // 如果端口映射为空,抛出错误并终止 Pipeline。
                    }
                }
            }
        }

        stage('部署服务') {
            steps {
                script {
                    def envVariables = ""
                    if (params.ENV_VARIABLES?.trim()) {
                        def envList = params.ENV_VARIABLES.split(',')
                        envVariables = envList.collect { envVar ->
                            "-e ${envVar.trim()}"
                        }.join(' ')
                        // 如果环境变量参数不为空,将其拆分为多个环境变量,并格式化为 Docker 命令中的 `-e` 参数。
                    }

                    def dockerCommand = ""
                    withCredentials([usernamePassword(credentialsId: 'devops_harbor', passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
                        // 使用 Jenkins 的 Credentials 插件获取 Harbor 仓库的用户名和密码。

                        dockerCommand = """
                            docker login harbor.wuyinit.com -u $HARBOR_USERNAME -p $HARBOR_PASSWORD &&
                            docker pull ${params.DOCKER_IMAGE}:${params.IMAGE_VERSION} &&
                            docker stop ${params.SERVICE_NAME} || true &&
                            docker rm ${params.SERVICE_NAME} || true &&
                            docker run -itd --name ${params.SERVICE_NAME} -p ${params.PORT_MAPPING} ${envVariables} ${params.DOCKER_IMAGE}:${params.IMAGE_VERSION}
                        """
                        // 构建 Docker 命令,包括:
                        // 1. 登录 Harbor 仓库。
                        // 2. 拉取指定版本的 Docker 镜像。
                        // 3. 停止并删除已存在的同名容器。
                        // 4. 启动新的容器,并指定名称、端口映射和环境变量。
                    }

                    sshagent(['deploy-agent-key']) {
                        // 使用 Jenkins 的 SSH Agent 插件,加载指定的 SSH 私钥('deploy-agent-key')。

                        sh """
                        ssh -o StrictHostKeyChecking=no ${params.SSH_USER}@${params.TARGET_IP} << 'EOF'
                        ${dockerCommand}
EOF
                        """
                        // 通过 SSH 连接到目标机器,并执行 Docker 命令。
                    }
                }
            }
        }
    }

    post {
        success {
            script {
                // 构建成功时发送邮件通知。
            }
        }

        unsuccessful {
            script {
                // 构建失败时发送邮件通知。
            }
        }
    }
}

该 Jenkins Pipeline 脚本实现了一个完整的服务部署流程,包括:

  1. 参数化构建:支持动态指定 SSH 用户、目标机器 IP、Docker 镜像、版本号、服务名称、端口映射和环境变量。
  2. 参数检查:确保所有必填参数不为空。
  3. 服务部署
    • 登录 Harbor 仓库并拉取 Docker 镜像。
    • 停止并删除已存在的同名容器。
    • 启动新的容器,并指定名称、端口映射和环境变量。
  4. 通知与清理:支持构建成功或失败时发送通知。

通过共享库和 Jenkins 插件(如 Credentials、SSH Agent)的使用,脚本的逻辑更加简洁和模块化,便于维护和扩展。

钉钉与OpenLDAP的联动

​ 为了实现统一身份认证,并基于现有的钉钉组织和人员信息,将Jenkins、GitLab、Harbor 和禅道等多平台的账号关联,我们可以使用 OpenLDAP 作为统一的用户认证中心。具体步骤如下:

  1. **配置 Go-Ldap-Admin 等服务:**将钉钉通讯录中的用户和组织结构同步到 OpenLDAP 服务器。
  2. 配置各平台使用 OpenLDAP 认证: 在 Jenkins、GitLab、Harbor 和禅道的系统设置中,配置 LDAP 认证,并指定 OpenLDAP 服务器的地址、端口、Base DN 等信息。

docker-compose参考,详情查看

 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
version: '3'

networks:
  go-ldap-admin:
    driver: bridge

services:
  go-ldap-admin:
    image: registry.cn-hangzhou.aliyuncs.com/eryajf/go-ldap-admin
    container_name: go-ldap-admin
    hostname: go-ldap-admin
    restart: always
    environment:
      WAIT_HOSTS: openldap:389
    configs:
      - source: go_ldap_admin_config
        target: /app/config.yml
    ports:
      - 8888:8888
    volumes:
      - ./data/go-ldap-admin:/app/data
    depends_on:
      - openldap
    links:
      - openldap:go-ldap-admin-openldap
    networks:
      - go-ldap-admin

  openldap:
    image: registry.cn-hangzhou.aliyuncs.com/eryajf/openldap:1.4.1
    container_name: go-ldap-admin-openldap
    hostname: go-ldap-admin-openldap
    restart: always
    environment:
      TZ: Asia/Shanghai
      LDAP_ORGANISATION: "eryajf.net"
      LDAP_DOMAIN: "eryajf.net"
      LDAP_ADMIN_PASSWORD: "123456"
    command: [ '--copy-service' ]
    volumes:
      - ./data/openldap/database:/var/lib/ldap
      - ./data/openldap/config:/etc/ldap/slapd.d
    ports:
      - 389:389
    networks:
      - go-ldap-admin

总结

​ 通过 Jenkins Pipeline、OpenLDAP 和钉钉的集成,我们可以实现高效的 CI/CD 流程,并统一管理多平台的用户账号。本文提供的方案只是一个简单的示例,实际应用中可以根据具体需求进行调整和扩展。

未来展望

未来,我们可以探索以下方向:

  • 使用 Kubernetes 进行容器化部署,进一步提高资源利用率和部署效率。
  • 引入代码质量分析工具,例如 SonarQube,提升代码质量。
  • 实现自动化测试,例如使用 Selenium 进行 UI 自动化测试。

参考文献