混合节点使用
混合节点(即在不同阶段指定不同的 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 机制提供了灵活的选择:
- 全局固定节点
- 适合所有阶段都在同一个 Jenkins 节点上运行的场景。
- 优势:不需要
stash/unstash,文件传递快,没有跨节点序列化开销。 - 劣势:该节点必须同时满足所有阶段的要求(例如既有 Docker 又能执行编译),可能会成为单点瓶颈。
- 分段指定不同节点
- 适合不同阶段需要不同环境,比如拉取代码用物理机,编译用高性能云主机,构建镜像用专门的 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_USER和 DOCKER_PASS,仅在块内有效。
fingerprint: true:为这些文件生成 MD5 指纹,方便追踪产物在多个构建间的关联。
这个流水线展示了 Jenkins 声明式 Pipeline 的典型实践:
多环境隔离:代码拉取、编译、镜像构建、归档分别在独立节点或容器中执行。
跨阶段文件传递:通过stash/unstash在不同 agent 间传递源码和编译产物。
安全凭据管理:withCredentials避免密码泄露。
资源清理:及时删除本地镜像,避免磁盘爆满。