Swissquote 千应用规模下的 Renovate 依赖自动化实践:从 700 个 PR 风暴到每周 2 小时维护
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
本文基于 Renovate 官方仓库收录的 Swissquote 用户案例 编写,作者为 Swissquote Bank 首席软件工程师 Stéphane Goetz。Swissquote 在 1000+ 生产应用中用 Renovate 自动化依赖更新,历经四年实践沉淀出一套可复制的工程方法论——从最初的"PR 打地鼠",到分组、排程、自动合并,再到自建调度器支撑 857 个仓库。读完本文,你将理解为什么"保持依赖最新"比想象中复杂,并掌握一套可直接落地的 Renovate 启用路径与配置策略。
Swissquote Bank 拥有超过 1000 个运行于生产环境的应用程序,涵盖服务(services)、守护进程(daemons)、Web 应用等多种形态,它们的"年龄"从几天到十几年不等。在软件维护的众多议题中,依赖管理是其中最具普遍性的一个:第三方依赖就像一柄悬顶之剑(sword of Damocles),你永远不知道哪一个新问题会迫使你放下手头一切去升级软件。
依赖安全在过去几年成为热议话题:Log4Shell 漏洞、供应链攻击、SSL 根证书过期事件,以及依赖自身引入的 bug,都在提醒我们依赖带来的双重性——每个依赖在解决一个问题的同时,也会给软件带来新的风险。而本文给出的答案正如标题所示:保持依赖持续更新,用自动化工具把这件事变成日常而非救火。
为什么"不想升级"行不通
有人会反驳:如果新版本引入新的安全问题呢?比如某个依赖的新版本恰好是供应链攻击的受害者,从不更新反而能避开被感染的版本。这个论点听起来合理,但你的应用并不存在于真空中——许多外部因素会在某个时刻强迫你升级,而且往往发生在最不合适的时机。
外部因素变化,被迫改造应用
假设应用运行的机器已过时,需要迁移到新机器。看似"装上去就行",但现实通常是:
- 新版操作系统不允许安装过时的运行时(runtime)
- 你需要为新硬件寻找兼容的运行时
- 应用的依赖与新的运行时不兼容,必须一并升级
于是一个单一变更变成了组合变更,每一步都可能以不同方式出错。
所依赖的库被发现新漏洞
如文章开头所述,这可能演变成全员上阵的紧急事件。想象一个遗留应用:持续集成(CI)常年失败——甚至可能根本不存在。部署一个库更新需要多长时间?答案往往令人绝望。
团队积累的依赖版本过多过杂
只要团队或公司存在得够久,这种状况几乎必然发生。升级库不只是 bump 版本号,有时还意味着用一个库替换另一个库。
如何看待你的依赖:火车与认知负荷
把软件想象成一列火车:你是火车头,每个依赖是一节车厢。火车头能拉动几十甚至上百节车厢,但总有极限;大脑同理,每个依赖都在增加认知负荷(cognitive load)。举例来说,如果项目里用了三个不同的缓存库,你(以及现在和未来的同事)可能都需要了解每个库的工作原理。
面对以下两个项目,你更愿意接手哪个:
- 一个依赖基本保持最新、升级依赖后跑通部署流水线就能继续日常工作的项目
- 一个几年无人触碰、每个依赖都已过时、CI 在所有分支上全红(如果还在运行的话)的项目
这不是夸大其词——作者表示见过非常接近这些情况的真实案例。不妨自问:
- 有多少次你创建了项目却从未升级过它的依赖?
- 有多少次你回到旧项目,想用新库却因为另一个库版本过旧不兼容而无法使用?
- 在你们的所有应用上,升级某一个依赖能有多快?
时间推移,项目来来去去,你很可能同时拥有十年历史的老项目和刚创建的新项目:有的用最新版 Java + 最新版 Spring,有的库略微过时,还有的在使用七年没有发版的老框架。每家公司都有一两个仍在运行的"老古董"项目,大家谈论时一笑置之,但一旦有人提出要改动它,气氛就紧张起来。
Swissquote 的三种依赖升级策略
作者识别出团队升级依赖的三种主流做法:
- 只做关键修复:仅更新带有 CVE 的依赖
- 机会主义(童子军法则):接手项目时先升级依赖,再做业务任务
- 定期更新:手动或用工具,每隔几周例行升级
策略一:只做关键修复
每当某个库发布修复漏洞的更新,安全团队会紧盯关键漏洞并迅速警告所有受影响团队。Swissquote 还配置了 GitHub 的 Dependabot Alerts,通知团队发现漏洞以及应升级到哪个安全版本。
这个策略大体可行,但风险很高:如果项目很久未被触碰,根据漏洞严重程度需要升级的依赖数量可能很多,升级耗时将显著增加。
策略二:机会主义(童子军法则)
规则很简单:接到项目任务后,第一步是升级它的依赖,然后再开始实现任务。这类似于在每个业务项目上顺手偿还一部分技术债,有助于在漏洞出现时保持主动,并为所有项目维持一条基线。
策略三:定期更新依赖
听起来容易:每隔几周检查过时的依赖,bump 版本,跑 CI,通过就推送。但能不能自动化?于是 Renovate 登场了。
试试 Renovate
2019 年 11 月,作者发现 Renovate 提供了可配合私有包仓库(private package registries)在本地运行的 Docker 镜像。应用会自动创建拉取请求(PR)告知依赖更新,CI 自动构建,让你知道合入是否安全。
首次尝试时,Swissquote 启用了 30 个仓库,一个 cron 任务每小时运行并创建 PR——第一个月收到了700 个 PR,陷入了无休止的"PR 打地鼠":每合并一个,另一个立刻顶上。
Renovate 的真正优势在于高度可配置,且配置可以共享。Swissquote 很早就为团队创建了带自定义策略的共享配置(shared configuration),核心策略包括:
- 将
minor和patch依赖的 PR分组(Group PRs) - 内部依赖(internal dependencies)可在一天中任意时间创建 PR
- 第三方依赖(third party dependencies)只在周末创建 PR
这大幅降低了 PR 噪音:第二个月收到 400 个 PR,第三个月只有 200 个。
从依赖自动化中学到的五件事
- 代码覆盖率要能预警:刚开始时团队漏掉了一些破坏性更新——构建是绿的,但应用一部署就崩。必须先确信测试覆盖率能兜住升级风险。
- 足够自信后,自动合并是必须的:团队在约 100 个仓库上启用了 Renovate,通常每周只花1–2 小时就能保持依赖最新。
- "走在最前沿,流血的不是前沿,是你":第一时间升级到新 major 版本可能踩坑。曾多次遇到 patch 版本破坏库的情况,通常第二天就有修复,但团队仍花了几个小时调试为什么更新破坏了应用,并为此开了不少 Issue、提了一些 PR。
- 升级与发布解耦:由于团队主要提供库(library),不希望每次依赖升级都发布新版本(这会在下游产生 PR 噪音)。他们决定仅在关键升级或贡献后发布,并用一个仪表盘提示某个仓库超过 30 天未发布。
关于 Renovate 的评价
接下来的部分看起来像广告或赞助内容;不是。我们只是这个产品的忠实粉丝。
Swissquote 被 Renovate 的 Docker 镜像轻松上手所吸引,而最终确认选择的是它海量的功能与配置选项(详见 Configuration Options)。团队喜爱的特性与选项包括:
- 共享配置(presets):Swissquote 为所有仓库设置了一套默认配置,每个团队可在此基础上扩展自己的实践,参见 Presets 概念
- 与 GitHub Dependabot alerts 集成:提升漏洞 PR 优先级、尽快发送安全修复 PR,参见 vulnerabilityAlerts 配置项
- 规则可全局定制,也可按包定制:参见 packageRules 配置项
- 支持私有包仓库:参见 Private Packages 入门
- 支持 70+ 语言与包管理器:Maven、Docker、npm、Docker Compose、Python 等,参见 支持的 Manager 列表
customManagers选项:以自定义方式使用依赖时,可将任意文本模式转换为依赖,参见 customManagers 配置项
Renovate 提供本地部署(on-premise)选项,也可使用 Mend Renovate App。Swissquote 没有使用官方 on-premise 方案,而是基于开源 Docker 镜像自建了调度器。
四年后的数据:自建调度器的理由
以下数据更新于 2023 年 11 月。
Swissquote 从 2019 年开始使用 Renovate,最初使用现已弃用的renovate/proDocker 镜像,以 GitHub App 形式安装,供早期采用者使用。很快他们撞上了最大的限制:这个 Docker 镜像串行地(one after another)处理所有仓库,单轮循环耗时数小时,且日志没有按仓库分离,排查非常困难。
因此他们自建了调度器:每小时将所有仓库排队运行,GitHub App 事件则触发单个仓库运行;同时开始收集指标,并按仓库单独存储日志。
以下是截至 2023 年 11 月的一些统计:
- 857 个仓库启用了 Renovate,约占 2000 个活跃仓库的四成多
- 11000 个 PR自安装以来被合并
- 上个月合并了239 个 PR
- 由于要反复克隆海量项目,Renovate 机器上两块 SSD 先后报废
团队不强制任何团队使用 Renovate,各团队可为每个项目自行决定是否启用。
调度器如何工作
调度器是一个 Node.js 应用,维护一个内存队列(in-memory queue),并启动 Docker 容器来运行 Renovate。调度器定期向 InfluxDB 发送数据点,再通过 Grafana 展示。
仪表盘上的所有信息来自三类测量数据:
- 队列(Queue):每 5 分钟发送一次队列大小与当前运行中的任务数
- Webhook:收到 GitHub webhook 请求时,发送该事项处理时长的数据点
- 运行(Runs):每次运行后,发送运行时长、成功与否,以及创建/更新/合并/关闭的 PR 数量
队列由 webhook或按固定间隔重新入队所有仓库来填充。每个仓库启动一个 Renovate Docker 镜像,并将其日志管道输出到文件。这让团队能够并行运行 10 个 worker——技术上可以更多,但团队决定不冲击自家的 GitHub 实例。
Swissquote 中 Renovate 的未来
目前并非所有团队都在使用 Renovate,有些团队仍偏好手动更新依赖。Swissquote 计划为所有仓库的关键依赖启用 Renovate,并希望让它足够有用、足够简单,从而被更多团队采纳到更广泛的依赖上。
你应该如何开始使用 Renovate?
如果这篇文章说服了你,以下是经过验证的启动路径:
- 先手动升级,再启用 Renovate:如果已知软件非常过时,不要立刻启用 Renovate——你会被 PR 淹没(Swissquote 深有体会)。先花时间手动升级所有能升的:
npm outdated、mvn versions:display-dependency-updates或你所用包管理器的等价命令,可以帮助你起步,测试应用并提交这些变更。 - 认真阅读首个配置 PR:启用 Renovate 后会收到一个用于添加配置文件的 PR,务必仔细阅读这个首个 PR,它会解释你将收到什么类型的 PR 以及何时收到。
- 务必设定排程(schedule):否则一天 24 小时都可能收到 PR。Swissquote 将所有第三方依赖排程在周末,测试通过则自动合并,周一早上再调查失败的 PR。关于
schedule的语义与 Cron 写法,可参考 Configuration Options 中的 schedule 章节:它只是"允许创建分支的时间窗口"而不会主动触发运行,官方建议每个排程窗口至少留 3–4 小时,并优先使用五段式 Cron 语法(分钟位必须用*)。 - 分组 PR:如果每个 PR 都要过 CI,负担会很重。当大多数 PR 都能通过后,就可以开始将 minor 和 patch 更新分组,使每个仓库只收到单个 PR(代价是问题排查变得更复杂)。以 90 个启用 Renovate 的仓库为例,Swissquote 平均每周只需调查4 个 PR,其余全部自动合并。
- 一段时间后启用自动合并:确保测试足够可靠,避免把未经测试、上线即崩的升级合入。
automerge的具体行为可参考 Configuration Options 中的 automerge 章节。
这一切努力值得吗?
答案是肯定的。
Swissquote 花了将近一年——比预想长得多——才追上技术栈中所有依赖的最新版本。但当测试足够可靠、可以启用 PR 自动合并后,团队对更新软件栈所做的工作感到满意:他们知道当计划外变更到来时,自己已经准备好了。
这一天在 2021 年 12 月到来——Log4Shell爆发。瑞士报价银行只花了几个小时就发布了刚合并的 PR,部署了少数受影响的应用,并通知了依赖其库的团队。事实上,因为那一周 Log4j 漏洞接连被发现,他们不得不这样快速响应了三次。
最后,请记住:保持依赖最新不只是工具问题,更是流程问题。每个团队都需要回答这些问题:
- 何时合并这个 PR?
- 如何处理构建失败的 PR?
- 外部库的新 major 版本与其他库尚不兼容怎么办?
- 何时发布这源源不断的库更新?
- 想要白天、夜里,还是仅周末收到 PR?
Swissquote 已经为自己的场景找到了答案,而你的答案需要你来决定。从本文的实践中可以看到:依赖自动化真正的价值不在于消灭维护工作,而在于把"被迫在错误时间救火"变成"任何时候都能从容应对"——这需要工具、测试覆盖率、排程与分组策略、自动合并机制,以及一个明确的发布流程共同支撑。
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考