一、为什么我们要关心“构建产物”?

想象一下,你是一位大厨,Jenkins就是你最得力的厨房自动化助手。你设定好菜谱(代码),它就能帮你完成洗菜、切菜、烹饪等一系列复杂工序。最后,一盘色香味俱全的菜肴(可运行的软件)被端了出来,这就是“构建产物”。

问题来了:这盘菜做出来之后,你是随手放在灶台上,还是精心打包、贴上标签、放进保温箱,并通知服务员可以上菜了?显然,后者才是专业做法。在软件开发中,构建产物(比如Jar包、War包、Docker镜像、安装程序等)就是我们的“菜肴”。如果管理不善,会导致:

  • 找不到历史版本:线上出了问题,想回滚到昨天的版本,却发现找不到了。
  • 环境不一致:测试人员测试的包,和最终上线部署的包,竟然不是同一个。
  • 交付效率低下:每次部署都需要手动从构建机器上拷贝文件,容易出错且麻烦。

因此,对构建产物进行规范的归档与分发,是持续集成/持续部署(CI/CD)流水线中至关重要的一环,它直接关系到软件交付的可靠性、可追溯性和效率。

二、Jenkins的“档案馆”:归档构建产物

Jenkins内置了基础的归档功能,可以理解为给你的“菜肴”建立一个专属的、带编号的储藏柜。

最核心的步骤是在你的流水线脚本或项目配置中,使用 archiveArtifacts 指令。它会将指定的文件或目录,保存到Jenkins服务器本身,并与本次构建记录永久绑定。你可以在每次构建的页面里直接下载这些文件。

技术栈声明:以下示例均基于 Jenkins Pipeline (使用 Groovy 语法)。

让我们看一个完整的Pipeline示例,它构建一个Java应用,并归档其生成的Jar包和文档。

// 示例:基础归档实践
pipeline {
    agent any // 使用任意可用的代理节点执行
    stages {
        stage('检出代码') {
            steps {
                // 从Git仓库拉取源代码
                git 'https://github.com/your-company/your-java-app.git'
            }
        }
        stage('构建与测试') {
            steps {
                // 使用Maven进行编译、运行单元测试
                sh 'mvn clean package'
            }
        }
        stage('归档产物') {
            steps {
                // 使用 archiveArtifacts 步骤归档重要文件
                archiveArtifacts(
                    artifacts: 'target/*.jar, target/site/**/*', // 归档所有Jar包和整个文档站点目录
                    fingerprint: true // 为归档文件生成指纹,用于追踪文件被哪些构建使用过
                )
            }
        }
    }
}

代码注释:

  • artifacts: 这里使用了Ant风格路径表达式。target/*.jar 匹配所有Jar文件,target/site/**/* 递归匹配target/site目录下的所有文件。逗号分隔多个模式。
  • fingerprint: true: 这是一个非常实用的功能。它会为归档的文件计算一个唯一的MD5校验和(指纹)。如果同一个文件出现在多次构建中,Jenkins就能建立关联。例如,你可以轻松查找到某个具体的Jar包被包含在哪几次构建里。

关联技术:构建工具与产出 在上面的例子中,我们使用了Maven。对于不同技术栈,构建产物路径不同:

  • Gradle (Java/Kotlin):产物通常在 build/libs/*.jar
  • .NET Core:使用 dotnet publish 后,产物在 bin/Release/netX.X/publish/ 下。
  • Node.js:可能需要归档打包后的 dist 目录或压缩文件。 理解你所用工具的产出目录,是正确配置归档模式的前提。

三、超越基础:使用插件进行高级归档

Jenkins自带的归档功能虽然方便,但存在明显短板:所有文件都存储在Jenkins Master服务器上,会大量消耗磁盘空间,且不适合长期保存和大文件存储。这时,我们就需要借助更强大的插件。

1. 归档到文件服务器 (Publish Over SSH / FTP) 对于团队共享或需要长期保留的产物,可以将其推送到独立的文件服务器、NAS或云存储。

这里以常用的 Publish Over SSH 插件为例。你需要先在Jenkins系统配置中设置好SSH服务器的连接信息。

// 示例:使用SSH将产物发布到远程服务器
pipeline {
    agent any
    stages {
        stage('构建') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('发布到文件服务器') {
            steps {
                // 使用 sshPublisher 步骤
                sshPublisher(
                    publishers: [
                        sshPublisherDesc(
                            configName: 'prod-file-server', // 在Jenkins全局配置中定义的SSH服务器名称
                            transfers: [
                                sshTransfer(
                                    sourceFiles: 'target/*.jar', // 本地源文件
                                    removePrefix: 'target', // 移除路径前缀,使归档结构更清晰
                                    remoteDirectory: 'java-app/releases/${BUILD_NUMBER}', // 远程目录,按构建号创建子目录
                                    execCommand: '''
                                        # 可以在传输完成后,在远程服务器上执行命令
                                        echo "文件 $(ls java-app/releases/${BUILD_NUMBER}/)" 已成功部署!
                                        # 例如:更新一个指向最新版本的符号链接
                                        ln -sfn $(pwd)/java-app/releases/${BUILD_NUMBER} $(pwd)/java-app/latest
                                    '''
                                )
                            ]
                        )
                    ]
                )
            }
        }
    }
}

2. 归档到制品仓库 (Artifactory / Nexus) 这是目前业界公认的最佳实践。制品仓库(如JFrog Artifactory、Sonatype Nexus)不仅是文件的存储中心,更是二进制制品的“管理中枢”。它提供了版本控制、元数据管理、安全权限、依赖解析、漏洞扫描等强大功能。

以使用 Artifactory Plugin 上传到Artifactory为例:

// 示例:上传构建产物到Artifactory制品库
pipeline {
    agent any
    environment {
        // 定义制品库服务器和目标仓库路径
        ARTIFACTORY_SERVER = 'company-artifactory'
        TARGET_REPO = 'libs-release-local'
        ARTIFACT_PATH = "com/example/myapp/${env.BUILD_NUMBER}/myapp-${env.BUILD_NUMBER}.jar"
    }
    stages {
        stage('构建') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('上传至Artifactory') {
            steps {
                // 使用 rtUpload 步骤
                rtUpload (
                    serverId: "${ARTIFACTORY_SERVER}", // 服务器ID
                    spec: """{
                        "files": [
                            {
                                "pattern": "target/*.jar", // 本地文件模式
                                "target": "${TARGET_REPO}/${ARTIFACT_PATH}" // 制品库目标路径
                            }
                        ]
                    }"""
                )
                // 通常还会上传构建信息
                rtPublishBuildInfo (
                    serverId: "${ARTIFACTORY_SERVER}"
                )
            }
        }
    }
}

使用制品仓库后,你的部署阶段将不再从Jenkins或文件服务器拉取包,而是直接从制品仓库获取指定版本的、经过认证的制品,确保了交付链路的完整性和安全性。

四、构建产物的“快递服务”:高效分发

归档是保存,分发则是送达。常见的分发场景包括:

  • 部署到测试/生产环境:将打包好的应用推送到服务器。
  • 交付给客户或合作伙伴:生成下载链接或安装包。
  • 更新内部工具:将工具新版本推送到各办公电脑。

1. 使用SSH进行部署分发 在“发布到文件服务器”的示例中,我们已经看到了通过SSH执行命令的能力。这可以直接用于部署,例如将Jar包传到服务器,并重启服务。

2. 集成容器化部署 (Docker + Kubernetes) 如果产物是Docker镜像,最佳实践是将其推送到Docker镜像仓库(如Docker Hub、Harbor、ECR)。

// 示例:构建并推送Docker镜像
pipeline {
    agent any
    environment {
        DOCKER_IMAGE = 'my-registry.com/myteam/myapp'
        DOCKER_TAG = "${env.BUILD_NUMBER}"
    }
    stages {
        stage('构建Docker镜像') {
            steps {
                script {
                    // 使用Docker Pipeline插件
                    docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
                }
            }
        }
        stage('推送镜像') {
            steps {
                script {
                    // 推送镜像到私有仓库
                    docker.withRegistry('https://my-registry.com', 'docker-registry-credential') {
                        docker.image("${DOCKER_IMAGE}:${DOCKER_TAG}").push()
                        // 通常还会打一个'latest'标签并推送(需谨慎)
                        docker.image("${DOCKER_IMAGE}:${DOCKEDR_TAG}").push('latest')
                    }
                }
            }
        }
        stage('部署到K8s') {
            steps {
                // 使用kubectl更新K8s部署的镜像版本
                sh "kubectl set image deployment/myapp myapp=${DOCKER_IMAGE}:${DOCKER_TAG} -n production"
            }
        }
    }
}

3. 邮件通知与下载链接 对于需要人工介入的交付(如测试版本),可以通过邮件将归档产物的下载链接发送给相关人员。Jenkins的 emailext 插件可以方便地实现。

post {
    always {
        emailext (
            subject: "构建通知: ${env.JOB_NAME} - ${env.BUILD_NUMBER} - ${currentBuild.currentResult}",
            body: """
                项目:${env.JOB_NAME}<br/>
                构建号:${env.BUILD_NUMBER}<br/>
                状态:${currentBuild.currentResult}<br/>
                控制台输出:${env.BUILD_URL}console<br/>
                <!-- 提供归档产物的直接下载链接 -->
                构建产物下载:${env.BUILD_URL}artifact/target/myapp-1.0.0.jar<br/>
                (如果配置了制品库,这里应放制品库的永久URL)
            """,
            to: 'team@example.com',
            attachLog: false
        )
    }
}

五、实践中的场景、优劣与注意事项

应用场景分析:

  • 小型团队/快速原型:直接使用Jenkins内置归档,简单快捷。
  • 中型团队,有多个项目:强烈建议搭建并接入统一的制品仓库(如Nexus OSS)。这是成本效益比最高的选择,能立即解决版本管理、依赖共享和安全问题。
  • 大型企业,微服务架构,容器化部署:必须采用“制品仓库+Docker镜像仓库”的组合。流水线构建出应用Jar包,上传至制品仓库;然后使用Dockerfile制作镜像,推送至镜像仓库;最终由K8s或其它编排工具从镜像仓库拉取部署。形成一条清晰、自动化的二进制物流链。

技术优缺点:

  • Jenkins内置归档
    • 优点:开箱即用,无需额外配置;与构建记录绑定紧密,查看方便。
    • 缺点:占用Master节点磁盘;无版本管理概念;不适合长期存储和大文件;难以在多个项目/团队间共享。
  • 文件服务器 (SSH/FTP)
    • 优点:存储独立,减轻Jenkins压力;目录结构可自由规划。
    • 缺点:缺乏元数据和搜索能力;权限管理可能较粗糙;本质上仍是“文件堆”,管理能力弱。
  • 专用制品仓库
    • 优点:完整的生命周期管理(上传、搜索、下载、删除);强大的元数据和属性管理;支持依赖解析和代理;集成安全扫描;提供高可用和备份方案。
    • 缺点:需要额外搭建和维护一个服务;有一定的学习成本。

关键注意事项:

  1. 清理策略:无论是Jenkins归档还是制品仓库,都必须设置自动清理策略(如“保留最近10次成功构建的产物”或“清理超过30天的快照版本”),否则磁盘很快会被撑爆。Jenkins有“Discard Old Build”插件,制品仓库也都有对应的清理任务。
  2. 产物命名与版本化:为产物制定清晰的命名规范。推荐使用语义化版本(如 myapp-1.2.3.jar)或与构建号强关联(如 myapp-${BUILD_NUMBER}.jar)。在制品仓库中,通常使用 groupId/artifactId/version/ 的Maven风格路径。
  3. 安全与权限:制品仓库是核心资产。必须配置严格的权限控制,区分哪些人可以上传(开发/构建服务),哪些人可以下载/部署(测试/运维服务),哪些人可以进行删除操作。
  4. 不可变性:一个版本的构建产物一旦被创建并归档,就应该是不可变的。绝对不要覆盖已经存在的版本。这保证了部署的可重复性和问题排查的确定性。如果需要修复,应该生成一个新的版本号。

六、总结

管理好Jenkins的构建产物,就像为你的软件交付流程建立了一个现代化的、高效的物流仓储中心。从最初简单的“厨房储物柜”(内置归档),到建立“区域配送中心”(文件服务器),再到打造“智能中央仓库”(制品仓库),每一步的进化都带来了更强大的管理能力、更高的可靠性和更佳的团队协作体验。

对于绝大多数严肃的软件开发团队,我的建议是:尽早引入并规范使用制品仓库。它将构建产物从简单的“输出文件”提升为可管理、可追踪、可审计的“发布制品”,是夯实CI/CD基础、实现真正意义上的持续交付的基石。结合容器化技术,你就能构建起一条从代码提交到生产环境部署的、全自动且可信赖的软件供应链。