Jenkins Pipeline在不同阶段指定不同的 agent 或 Docker 容器进行执行任务和固定一个节点分段执行任务,一文带你搞定

混合节点使用

混合节点(即在不同阶段指定不同的 agent 或 Docker 容器)的核心优势在于为每个任务匹配最合适的执行环境,从而在资源、效率、稳定性和成本之间取得最佳平衡。

还有一种混合使用的方式就是固定在一个节点,但是每个任务可以指定不同的方式去执行,比如在节点上获取代码,然后在节点上启动nodejs的容器进行打包。

知识点整理

🔄 主要区别对比

方面 示例(全局 agent label) 示例(agent none + 分段指定)
代码拉取位置 固定在 label 'slave' 节点 固定在 label 'build-node' 节点
编译阶段位置 同样在 label 'slave' 节点上启动 Docker 容器 在任意有 Docker 的节点上启动 maven:3.8-openjdk-11 容器(可能与拉取节点不同)
阶段间文件传递 依赖 共享 workspace(因为全局 agent 固定在同一节点) 使用 stash / unstash 跨节点传递文件
构建镜像位置 label 'slave' 节点上启动 Docker 容器执行 agent any(任意可用节点)上直接执行 docker build
是否需要节点预装 Docker 需要 slave 节点安装 Docker 编译阶段自动拉取 Docker 容器,不需要节点预装特定编译工具,但构建镜像阶段需要节点有 Docker

🧠 为什么会有两种写法?

因为 Jenkins Pipeline 的 agent 机制提供了灵活的选择:

  1. 全局固定节点
    • 适合所有阶段都在同一个 Jenkins 节点上运行的场景。
    • 优势:不需要 stash/unstash,文件传递快,没有跨节点序列化开销。
    • 劣势:该节点必须同时满足所有阶段的要求(例如既有 Docker 又能执行编译),可能会成为单点瓶颈。
  2. 分段指定不同节点
    • 适合不同阶段需要不同环境,比如拉取代码用物理机,编译用高性能云主机,构建镜像用专门的 Docker 节点。
    • 优势:资源利用更灵活,可以为每个阶段选择最优节点。
    • 劣势:需要使用 stash / unstash 在节点间传递文件,有序列化和网络传输开销,且 stash 默认只保留少量文件(大文件可能失败)。
打包前端使用分段但是同一个节点

准备Dockerfile

bash 复制代码
[root@jenkins-slave frontend-docker]# cat Dockerfile
FROM nginx:alpine
WORKDIR /usr/share/nginx/html
COPY dist/ /app/dist
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

新建一个任务--->frontend-docker----->流水线

编写pipeline

注意⚠️:

当固定一个节点分段执行任务的时候,比如启动容器去构建,这个时候会遇到一个目录挂载的问题,当使用agent { docker }时,Jenkins会自动将当前工作空间(workspace)挂载到容器内的一个特定路径,并且默认的工作目录(working directory)通常是该挂载点。当在全局agent中指定了customWorkspace,后续的docker agent应该会使用相同的workspace目录,但是实际情况确实使用的还是默认workspace路径即/root/.jenkins/workspace/frontend-docker/。所以这个时候会因为找不到指定目录下的代码而报错。即使将customWorkspace去掉也会报错,(也许是我哪里配置错了)还是找不到代码,通过日志查看还是路径不对。所以我才用的方式是不指定customWorkspace路径,才用默认路径,同时不再依赖 Jenkins 的自动挂载,完全手动控制挂载点和工作目录。在编译阶段不声明 agent { docker },而是使用 docker.image(...).inside脚本方式,手动指定挂载和工作目录。这种方式更可控。

ini 复制代码
pipeline {
    // 全局固定在一个节点上执行所有阶段(该节点必须安装 Docker)
    agent {
        node {
            label 'jenkins-slave'               // 你的构建节点标签
        }
    }

    options {
        timestamps()
        timeout(time: 1, unit: 'HOURS')
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }

    environment {
        // ========== 镜像仓库配置 ==========
        DOCKER_REGISTRY = '172.20.10.3'              // 私有镜像仓库地址
        DOCKER_NAMESPACE = 'frontend'                // 命名空间
        DOCKER_IMAGE_NAME = 'demo-frontend'          // 镜像名称
        DOCKER_IMAGE_FULL = "${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/${DOCKER_IMAGE_NAME}"
        
        // ========== 构建配置 ==========
        IMAGE_TAG_BUILD = "${BUILD_NUMBER}"
        
        // ========== Git 仓库配置 ==========
        GIT_URL = 'git@gitee.com:testpm/frontend-docker.git'   // 你的前端仓库
        SLAVE_GIT_ID = 'jenkins-slave-git'
        
        // ========== 前端构建配置 ==========
        NODE_IMAGE = 'node:16'                    // Node 版本
        BUILD_OUTPUT_DIR = 'dist'                 // 构建输出目录
    }

    stages {
        // 阶段1:代码拉取
        stage('代码拉取') {
            steps {
                echo "------拉取前端代码-------"
                deleteDir()                         // 清理旧代码
                git credentialsId: "${SLAVE_GIT_ID}", url: "${GIT_URL}", branch: 'master'
                // 验证拉取结果
                sh 'ls -la'
                script {
                    env.GIT_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
                    env.COMMIT_SHORT = env.GIT_COMMIT.take(8)
                }
            }
        }

        // 阶段2:前端编译(使用 Node 容器)
        stage('前端编译') {
            steps {
                script {
                    // 使用 docker.image().inside 完全控制挂载和工作目录
                    docker.image("${NODE_IMAGE}").inside("-v /data/npm-cache:/root/.npm -w ${WORKSPACE}") {
                        sh '''
                            echo "当前目录: $(pwd)"
                            ls -la
                            echo "Node 版本: $(node -v)"
                            echo "npm 版本: $(npm -v)"
                            npm config set registry https://registry.npmmirror.com
                            if [ ! -f "package.json" ]; then
                                echo "错误: 找不到 package.json"
                                exit 1
                            fi
                            npm install
                            npm run build
                            ls -lh ${BUILD_OUTPUT_DIR}/
                        '''
                    }
                }
            }
        }

        stage('构建并推送镜像') {
            steps {
                script {
                    docker.image('docker:20.10.17').inside('-v /var/run/docker.sock:/var/run/docker.sock -u root') {
                        sh """
                            docker build -t ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD} .
                            docker tag ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD} ${DOCKER_IMAGE_FULL}:${COMMIT_SHORT}
                        """
                        withCredentials([usernamePassword(credentialsId: 'harbor-secret', 
                                                          usernameVariable: 'DOCKER_USER', 
                                                          passwordVariable: 'DOCKER_PASS')]) {
                            sh """
                                echo ${DOCKER_PASS} | docker login ${DOCKER_REGISTRY} -u ${DOCKER_USER} --password-stdin
                                docker push ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD}
                                docker push ${DOCKER_IMAGE_FULL}:${COMMIT_SHORT}
                                docker logout ${DOCKER_REGISTRY}
                            """
                        }
                        sh "docker rmi ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD} ${DOCKER_IMAGE_FULL}:${COMMIT_SHORT} || true"
                    }
                }
            }
        }

        // 阶段4:归档编译产物
        stage('归档产物') {
            steps {
                archiveArtifacts artifacts: "${BUILD_OUTPUT_DIR}/**", fingerprint: true
            }
        }
    }

    post {
        success {
            echo "✅ 前端镜像构建成功并已推送:${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD}"
        }
        failure {
            echo "❌ 前端构建失败,请检查日志"
        }
        always {
            cleanWs()  // 清理工作空间
        }
    }
}

参数解释

docker.image().inside()参数(核心)
docker.image("${NODE_IMAGE}").inside("-v /data/npm-cache:/root/.npm -w ${WORKSPACE}") { sh ... }
docker.image(imageName):指定使用的 Docker 镜像。

.inside():在指定容器内执行命令,自动挂载工作目录、处理容器启动/删除。

括号内的字符串参数(传递给 docker run 的额外参数):

-v /data/npm-cache:/root/.npm:挂载宿主机目录/data/npm-cache到容器的/root/.npm,用于持久化 npm 缓存,加速后续构建。

-w ${WORKSPACE}:设置容器的工作目录为当前 Jenkins 工作空间(保证容器内能访问拉取的代码)。

-u root:以 root 用户运行容器,避免权限问题(因为挂载了 socket 需要高权限)。

打包后端使用分段指定不同的节点

准备Dockerfile

ini 复制代码
[root@jenkins-slave ~]# cd backend-docker/
[root@jenkins-slave backend-docker]# cat Dockerfile 
FROM openjdk:21-jre-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

新建一个任务--->backend-docker----->流水线

编写pipeline

ini 复制代码
pipeline {
    agent none
    options {
        timestamps()
        timeout(time: 1, unit: 'HOURS')
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }
    environment {
        // ========== 镜像仓库配置 ==========
        DOCKER_REGISTRY = '172.20.10.3'          // 你的私有镜像仓库地址
        DOCKER_NAMESPACE = 'backend'                 // 命名空间/项目名
        DOCKER_IMAGE_NAME = 'demo-service'           // 镜像名称
        DOCKER_IMAGE_FULL = "${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/${DOCKER_IMAGE_NAME}"
        
        // ========== 构建配置 ==========
        // 镜像标签:构建号 + Git commit 短ID(双标签便于回滚)
        IMAGE_TAG_BUILD = "${BUILD_NUMBER}"
        // GIT_COMMIT 在代码拉取后动态获取,这里先占位
        // ==========Git仓库配置=========
        GIT_URL = 'git@gitee.com:testpm/backend-docker.git'
        SLAVE_GIT_ID = 'jenkins-slave-git'
    }

    stages {
        // 阶段1:代码拉取(固定在专用的节点)
        stage('代码拉取') {
            agent {
                node {
                    label 'jenkins-slave'
                    customWorkspace '/home/jenkins/backend-pm'
                }
            }
            steps {
                echo "------获取代码-------"
                // 清理旧代码(避免残留文件干扰)
                deleteDir()
                git credentialsId: "${SLAVE_GIT_ID}", url: "${GIT_URL}", branch: 'master'
                // 获取 Git commit ID 并存入环境变量
                script {
                    env.GIT_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
                    env.COMMIT_SHORT = env.GIT_COMMIT.take(8)
                }
                // 保存全部源码(供后续阶段使用)
                stash name: 'source', includes: '**/*'
            }
        }

        // 阶段2:Maven 编译(在 Docker 容器内,可运行于不同节点)
        stage('Maven 编译') {
            agent {
                docker {
                    image 'maven:3.9.9-eclipse-temurin-21'
                    // 挂载 Maven 本地仓库缓存,极大提升构建速度
                    args '-v /data/maven-cache:/root/.m2 --entrypoint='
                }
            }
            steps {
                unstash 'source'
                sh '''
                    mvn clean package -DskipTests
                    echo "编译完成,产物列表:"
                    ls -lh target/*.jar
                '''
                // 仅保存编译产物(jar包),减少跨节点传输量
                stash name: 'artifact', includes: 'target/*.jar'
            }
        }

        // 阶段3:构建 Docker 镜像并推送(使用 Docker 容器执行 docker 命令)
        stage('构建并推送镜像') {
            agent {
                docker {
                    image 'docker:20.10.17'
                    args '-v /var/run/docker.sock:/var/run/docker.sock --entrypoint='
                }
            }
            steps {
                unstash 'source'
                unstash 'artifact'
                script {
                    // 打两个标签:构建号 和 commit-id
                    sh """
                        docker build -t ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD} .
                        docker tag ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD} ${DOCKER_IMAGE_FULL}:${COMMIT_SHORT}
                    """
                    // 使用凭据安全登录镜像仓库
                    withCredentials([usernamePassword(credentialsId: 'harbor-secret', 
                    usernameVariable: 'DOCKER_USER', 
                    passwordVariable: 'DOCKER_PASS')]) {
                        sh """
                            echo ${DOCKER_PASS} | docker login ${DOCKER_REGISTRY} -u ${DOCKER_USER} --password-stdin
                            docker push ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD}
                            docker push ${DOCKER_IMAGE_FULL}:${COMMIT_SHORT}
                            docker logout ${DOCKER_REGISTRY}
                        """
                        // 立即清理本地镜像
                        sh "docker rmi ${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD} ${DOCKER_IMAGE_FULL}:${COMMIT_SHORT} || true"
                    }
                }
            }
        }

        // 阶段4:归档编译产物(可选,用于 Jenkins 界面下载)
        stage('归档产物') {
            agent {
                node {
                    label 'jenkins-slave'
                }
            }  // 回到slave节点归档(也可以 any)
            steps {
                unstash 'artifact'
                archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
            }
        }
    }

    post {
        success {
            echo "✅ 镜像构建成功并已推送:${DOCKER_IMAGE_FULL}:${IMAGE_TAG_BUILD}"
            // 可在此发送钉钉/企微/邮件通知
        }
        failure {
            echo "❌ 构建失败,请检查日志"
            // 可发送失败告警
        }
    }
}

参数解释:

deleteDir():删除当前工作目录下的所有内容(相当于 rm -rf .),确保拉取全新代码,避免旧文件干扰。

stash name: 'source', includes: '**/*':将当前工作目录下的所有文件保存为一个名为source的 stash(存储)。后续其他 stage 可以用unstash恢复这些文件,即使在不同节点也能传递。

unstash 'source':恢复之前 stash的源代码到当前工作目录

withCredentials:Jenkins 的凭据管理步骤,安全地从 Jenkins 获取用户名/密码(凭据 ID 为 harbor-secret),并设置为两个环境变量 DOCKER_USERDOCKER_PASS,仅在块内有效。

fingerprint: true:为这些文件生成 MD5 指纹,方便追踪产物在多个构建间的关联。

这个流水线展示了 Jenkins 声明式 Pipeline 的典型实践:

多环境隔离:代码拉取、编译、镜像构建、归档分别在独立节点或容器中执行。

跨阶段文件传递:通过stash/unstash在不同 agent 间传递源码和编译产物。

安全凭据管理:withCredentials避免密码泄露。

资源清理:及时删除本地镜像,避免磁盘爆满。

相关推荐
Engineer邓祥浩2 小时前
知识点1 时间复杂度、空间复杂度
java·数据结构·算法
旷世奇才李先生2 小时前
Docker\+K8s的核心价值与应用场景
docker·容器·kubernetes
小小仙。2 小时前
IT自学第三十七天补充
java·开发语言
梵得儿SHI2 小时前
SpringCloud 生产级落地:Docker 容器化 + K8s 编排部署全攻略(含完整 yaml + 避坑指南)
docker·云原生·kubernetes·k8s·springcloud·微服务部署·java 后端
knight_9___2 小时前
RAG面试篇7
java·面试·agent·rag·智能体
song8546011342 小时前
idea问题解决
java·ide·intellij-idea
问水っ2 小时前
Qt高级编程 第7章 用QtConcurrent实现线程处理
java·开发语言
SimonKing2 小时前
AI编程工具装了一大堆,Skills 管理乱成粥?这个开源神器一招搞定!
java·后端·程序员
one_love_zfl2 小时前
java面试-微服务篇
java·微服务·面试