Jenkins持续部署流水线:如何每日仅执行特定步骤一次
这个场景确实是持续部署流程里的典型痛点,我来分享几个经过验证的可行方案,覆盖Jenkins原生能力、插件工具和业界常用实践:
1. Jenkins原生归档+历史构建检索(无额外依赖)
这是你提到的思路的具体落地方式,完全依赖Jenkins自身能力,不需要外部资源:
- 部署标记:当部署成功后,生成一个带环境+日期的标记文件(比如
deployed-TestA-20240520.txt),用archiveArtifacts步骤归档到构建记录中。 - 历史检查:后续构建时,遍历当前Job的近期历史构建(限制在24小时内避免性能问题),检查是否存在对应当日环境的归档标记。
示例代码(声明式流水线的script块中):
script { def today = new Date().format('yyyyMMdd') def targetEnv = '' // 时段判断逻辑 def currentHour = new Date().getHours() if (currentHour >= 11 && currentHour < 14) { targetEnv = 'TestA' } else if (currentHour >=14 && currentHour <18) { targetEnv = 'TestB' } else { echo "当前时段不在部署窗口,跳过部署" return } // 检查当日是否已部署 def deployedToday = false def recentBuilds = currentBuild.rawBuild.parent.builds.findAll { it.timestamp.time >= System.currentTimeMillis() - 24*60*60*1000 } recentBuilds.each { build -> try { if (build.getArtifacts().any { it.fileName == "deployed-${targetEnv}-${today}.txt" }) { deployedToday = true return // 找到标记后终止遍历 } } catch (Exception e) { echo "检查构建${build.number}时出错: ${e.message}" } } if (!deployedToday) { echo "开始部署到${targetEnv}..." // 这里写入你的部署逻辑(比如ssh、kubectl命令等) // 部署成功后归档标记文件 writeFile file: "deployed-${targetEnv}-${today}.txt", text: "Deployed at ${new Date()}" archiveArtifacts artifacts: "deployed-${targetEnv}-${today}.txt", fingerprint: true } else { echo "${targetEnv}今日已完成部署,跳过" } }
- 优点:零外部依赖,配置简单;缺点:如果Job历史量极大,遍历会有轻微性能损耗,限制遍历范围即可解决。
2. 利用Jenkins全局属性存储状态
通过Jenkins的全局节点属性来存储部署记录,跨节点共享状态:
- 借助Groovy脚本读写全局属性,注意加上
@NonCPS注解避免序列化问题,同时需要确保流水线有足够权限。
示例代码:
@NonCPS def checkDeployedToday(String envName) { def today = new Date().format('yyyyMMdd') def key = "${env.JOB_NAME}:${envName}:${today}" def globalProps = Jenkins.instance.getGlobalNodeProperties().get(GlobalStringProperty.class) return globalProps.any { it.key == key && it.value == 'true' } } @NonCPS def markDeployedToday(String envName) { def today = new Date().format('yyyyMMdd') def key = "${env.JOB_NAME}:${envName}:${today}" def globalProps = Jenkins.instance.getGlobalNodeProperties().get(GlobalStringProperty.class) // 先移除旧记录(如果存在) globalProps.removeAll { it.key == key } globalProps.add(new GlobalStringProperty(key, 'true')) Jenkins.instance.save() } // 流水线中调用 script { // 时段判断逻辑... if (!checkDeployedToday(targetEnv)) { // 部署逻辑... markDeployedToday(targetEnv) } }
- 优点:查询速度快,跨节点共享;缺点:需要管理员权限配置,且全局属性是全Jenkins可见,要通过Job名称前缀做隔离。
3. 专用插件简化实现
有几个插件可以直接封装这类逻辑,减少自定义代码量:
- Lockable Resources Plugin:把每个环境+每日窗口当成一个可锁定的资源。比如定义
TestA-Daily和TestB-Daily资源,部署前尝试锁定,若已被锁定(当日已部署)则跳过。还可以配合定时Job每日0点自动释放锁。
示例流水线步骤:stage('Deploy') { when { expression { def h = new Date().getHours() return (h >=11 && h<14) || (h>=14 && h<18) } } steps { script { def lockName = new Date().getHours() >=11 && new Date().getHours()<14 ? 'TestA-Daily' : 'TestB-Daily' lock(resource: lockName, skipIfLocked: true) { echo "部署到${lockName.replace('-Daily', '')}..." // 部署逻辑 } } } } - Pipeline Utility Steps Plugin:可以更方便地检索历史归档文件,简化
findFiles、readFileFromWorkspace等操作。
4. 外部存储方案(业界常用的轻量选项)
如果团队有现成的基础设施,外部存储是更灵活的选择:
- Redis:用键值对存储部署状态,键格式为
${JobName}:${Env}:${Date},值设为true并设置24小时过期。流水线中通过sh调用redis-cli读写。 - 共享存储标记文件:比如NFS共享目录,部署成功后创建
TestA-20240520文件,后续构建通过sh 'ls /shared/deploy/TestA-$(date +%Y%m%d)'判断是否存在。 - 数据库:用轻量数据库(如SQLite、MySQL)存储部署日志,查询当日是否有对应环境的部署记录。
业界实践总结
- 中小团队优先选择Jenkins原生归档+历史检索或Lockable Resources Plugin,无需额外运维成本;
- 大型团队或有统一运维平台的场景,会用Redis/数据库做全局状态管理,方便跨系统查询和统计;
- 很多团队会把时段-环境的对应规则放到配置中心(如内部配置平台),而非硬编码在流水线中,方便随时调整部署窗口。
内容的提问来源于stack exchange,提问作者ami
相关产品推荐
相关产品推荐

