8.7

查看英文版

8.7 构建系统与制品管理

概述与动机

构建(build)是源代码变成可交付成果的地方。第 8.1 章中的每一条流水线都从这里开始:在你能够测试、扫描、部署或推广任何东西之前,构建系统必须先把一棵源文件树转变成具体的制品(artifact)()编译好的二进制文件、软件包、容器镜像,或是一捆静态资源。如果这第一步缓慢、不稳定或不可复现,之后的每一步都会继承这种损害。在两台机器上产生不同输出的构建,会削弱你所做的每一次测试和获得的每一次批准的可信度,因为你审查过的东西无法被证明就是你交付的东西。

本章关注的正是这第一步及其产出:构建出制品的构建系统,以及存储、版本化、加固并推广这些制品的制品管理(artifact management)。本章的范围刻意比第 8.1 章更窄,第 8.1 章涵盖完整的持续集成与持续交付(CI/CD)流水线,而本章的主题是构建本身及其产出的制品。本章与第 2.10 章的软件配置管理相辅相成,后者管理你如何追踪和控制输入;也与第 2.18 章的依赖与供应链管理相辅相成,后者管理你引入的第三方代码。构建正是这些输入汇聚之处:你的源代码、依赖项和配置都在此汇合,产出一个不可变的输出。

对大型团队而言,这里的风险是具体的。当数百名工程师每天多次等待耗时数分钟的构建时,累积浪费的时间会超过几乎任何其他工程成本。当制品是可变的、未被追踪的,或按环境重新构建的,你就失去了准确说出生产环境中正在运行什么的能力。在企业和政府场景中,这种可追溯性不是可选项。审计人员和安全官员需要证据,证明生产环境中的二进制文件来自经过评审的源代码,由可信系统构建,并有可记录的监管链(chain of custody)。严谨的构建与制品实践,能让这种证据成为日常工作的自然产物,而不是每次审计前的手忙脚乱。

关键原则

  • 构建是交付的第一步:把它的速度和正确性当作生产问题来看待。
  • 追求可复现的构建,并在可行的情况下追求封闭式(hermetic)构建:相同的输入,每次都产出相同的输出。
  • 构建一次制品,然后将这个确切的制品在各环境间推广。
  • 让制品不可变且以内容寻址,并有意义地对其进行版本管理。
  • 将制品存储在具备留存策略、访问控制和溯源信息的受管理仓库中。
  • 积极使用缓存,但要把缓存当作一个安全边界,而不仅仅是提速手段。
  • 在构建时而非事后,采集溯源信息、签名和物料清单。

建议

把构建当作交付的第一步

你的构建系统是生产基础设施,应当按此投入资金并加以维护。快速、正确地构建出制品,是第 8.1 章所述 CI/CD 得以立足的基础。当团队把构建当作事后补充()一堆没人负责的 shell 脚本时,就会为此付出代价:流水线不稳定、神秘的“在我机器上能跑”缺陷,以及拖慢整个第 11 部分所述工程流程的缓慢反馈。为构建指定负责人,将其定义与代码一起纳入版本控制(参见第 2.14 章关于仓库结构的内容),并施以与任何其他关键系统同等的评审纪律。

让构建可复现,并尽可能封闭式

可复现构建(reproducible build)从相同的源代码产出逐字节相同的输出,因此任何人都可以独立地重新构建并验证某个制品是否与其源代码相符。正是这种特性让你能够信任一个二进制文件在提交与部署之间没有被篡改。要做到这一点,意味着要消除不确定性的来源:内嵌的时间戳、绝对文件路径、构建顺序的随机性,以及结果会随时间漂移的网络请求。

封闭式构建更进一步,它预先声明每一项输入,并在一个无法访问网络或宿主环境状态的隔离环境中运行。除了你所声明的内容()锁定版本的工具链、锁定版本的依赖项、明确指定的源文件()之外,没有任何东西能进入构建过程。封闭性正是让可复现性变得可靠、而非全凭运气的关键。完全的封闭性在工具和纪律上都有实实在在的成本,所以应把它当作一个努力方向,而非非此即彼的二元选择。即便是部分进展()锁定编译器版本、对依赖项进行锁定或纳入仓库(vendoring)、剥离时间戳()也能以很小的代价换来大部分的信任。

用锁文件确定性地解析依赖

每次构建都会引入第三方代码,而你如何解析它,决定了你的构建是否具有确定性。锁文件(lockfile)记录了每个直接依赖和传递依赖的确切解析版本及加密哈希值,因此几个月后的构建也会解析出完全相同的依赖图。提交锁文件,把对它的更改当作需要评审的事件,并在每次拉取时校验哈希值,这样被篡改的上游软件包就无法悄悄混入。这是第 2.18 章所述供应链纪律在构建时的体现。没有锁文件,“昨天能构建成功”对今天能构建出什么毫无参考价值,因为一个浮动的版本范围可能悄悄拉入一个新版本,攻击者也可能发布一个恶意版本。

在本地和远程都使用增量构建与缓存

没有人应该重新构建没有变化的部分。增量构建会追踪哪些输入影响哪些输出,只重新构建受变更影响的部分。构建缓存以输入的哈希值为键,存储先前工作的输出,因此一个未发生变化的目标会被直接获取,而不是重新计算。本地缓存能加速单个开发者的循环;远程或分布式构建缓存则能在整个团队和 CI 集群之间共享结果,这样第一个构建某个给定输入的人承担成本,其他所有人都能获得缓存命中。在大型单体仓库(monorepo)上,这就是十分钟构建和十秒钟构建之间的差别。

这样做的回报是开发者反馈速度,这是你能做出的杠杆效应最高的投资之一。快速、正确的反馈能让工程师保持心流状态(flow),缩短从写代码到知道它是否可行之间的循环。不过要仔细守护缓存的正确性:如果缓存键遗漏了某个真实的输入(一个环境变量、一个工具版本),就会产生令人抓狂、难以调试的陈旧结果。缓存的可信度,取决于其输入哈希的完整程度。

选择与你的规模相匹配的构建工具

构建工具处在一个光谱上。轻量端,Make和语言原生工具对一个简单的依赖图建模,对单个服务或小型仓库已经够用。中间地带,诸如 Java 世界的 Gradle 和 Maven,或是 Go、Rust、JavaScript 的标准工具链等生态系统工具,加入了依赖解析和约定。重量级的一端,诸如 Bazel 及类似的单体仓库构建工具,把整个构建建模为一个细粒度、封闭式的有向无环图(directed acyclic graph),从而在大型代码库上实现精确的增量构建、远程缓存和远程执行。

当你有许多相互依赖的项目、一个大型单体仓库(参见第 2.14 章),或构建耗时已经在拖累团队时,重量级工具就值得投入。它的代价是实实在在的:陡峭的学习曲线、迁移成本,以及需要一个专门团队来维护构建定义。不要因为 Bazel 类工具流行就采用它。只有当你的构建图足够庞大,以至于细粒度缓存和并行化所挽回的工程时间超过工具本身的运行成本时,才值得采用。对大多数中小型系统而言,搭配远程缓存的优质生态系统工具就是最佳平衡点。

将制品存储在受管理的仓库中

一旦你构建出一个制品,它就需要一个归宿。制品仓库(artifact repository,也称为注册表 registry)以带版本管理、访问控制和元数据的方式存储你的软件包、容器镜像和二进制文件。它是你源代码仓库的对应物:源代码进,制品出,两者都受管理。一个好的仓库能给你一个单一的可信位置来发布和获取内部制品,代理并缓存外部制品,使你不必在每次构建时都访问公共互联网,并记录谁在何时发布了什么。容器镜像有自己的注册表约定,其他包类型也各有各的约定,但纪律是一致的:没有任何东西能在没有经过受管理、访问受控的存储库的情况下运行于生产环境。

对制品进行版本管理,并使其不可变且以内容寻址

给每一个制品一个有意义的版本号。语义化版本(semantic versioning)(major.minor.patch)向消费者传达变更的性质:主版本号(major)提升表示破坏性变更,次版本号(minor)增加意味着新增了兼容功能,修订号(patch)则表示修复了缺陷。除了这种人类可读的版本号之外,还要用其内容的加密哈希值来标识每一个制品,使其以内容寻址(content-addressed)。内容地址,常被称为摘要(digest),是一种只要有一个字节发生变化就会改变的指纹,它让你能够明确无误地引用某个确切的制品,并检测任何篡改。

让已发布的制品保持不可变:一旦某个版本发布,它就永远不再改变。在同一个版本号下重新发布不同的字节内容,是一种供应链风险,也是调试的噩梦,因为两个人手中的“版本 1.4.2”可能是不同的软件。像“latest”这样的可变标签(mutable tag)对人类来说很方便,但对任何要紧的事情而言,它必须始终能解析到你所记录的某个确切、不可变的摘要。按摘要而非浮动标签进行部署,这样你测试过的东西才能被证明就是你运行的东西。

构建一次,处处推广

只构建一次制品,然后将同一个制品在你的各个环境中移动:开发、预发布(staging)、生产。这条“构建一次,处处推广”(build once, promote everywhere)的原则,是最重要的单一制品管理实践。如果你按环境重新构建,你就丢掉了“经过测试的制品就是被部署的制品”这一保证,因为每次重新构建都可能拉取不同的依赖,或在略有差异的机器上运行。推广(promotion)是一种元数据操作:你把一个已经构建、已经测试过的摘要标记为批准进入下一个环境,并通过外部化配置(参见第 2.10 章)而非重新构建来为该环境配置它。这样能保持二进制文件不变、配置项可变,这正是你在可靠性和可审计性两方面都想要的那种分离。

采集溯源信息、为制品签名,并生成 SBOM

在构建时,记录制品的来源,并证明它未被篡改。溯源信息(provenance)是关于制品如何被构建的一份签名声明:来自哪个源代码提交、由哪个构建系统构建、使用了哪些输入。为制品签名,能让消费者在运行之前验证其真实性和完整性,而在部署时验证签名则闭合了这个环路。软件物料清单(software bill of materials, SBOM)是制品内部组件和依赖项的完整清单,当一个新漏洞被披露时,它能让你在几分钟内回答“我们是否受影响?”,而不必花上几天时间在构建日志里翻找。

诸如 SLSA(Supply-chain Levels for Software Artifacts,软件制品供应链等级)之类的框架,为构建时完整性提供了一个分级模型:更高的等级要求封闭、隔离的构建以及不可伪造的溯源信息。在构建过程中生成这一切,因为此时的信息是权威且易于采集的,而不是事后再去重建,那样既昂贵又不可靠。这项工作直接服务于第 4.9 章所述的安全软件开发生命周期,以及第 2.18 章所述的供应链关切。

保护缓存,并管理留存策略与成本

一个共享的构建缓存是一个共享的信任边界。如果攻击者能写入一个被投毒的条目,每一个获取该条目的消费者都会运行被攻破的代码,速度优势就变成了攻击面。用身份验证保护缓存,严格限定写入权限的范围(通常只限可信的 CI,绝不包括开发者的笔记本电脑),并确保缓存键对每一项真实输入都进行了哈希处理,这样被投毒或陈旧的条目就无法冒充合法条目。要把缓存投毒当作一个真实的威胁模型来对待,尤其是对跨团队共享的远程缓存而言。

制品同样会累积成本。容器镜像和构建产出体积庞大,一个不受限制增长的注册表最终会让存储账单和缓慢的查找问题变得无法忽视。制定留存策略:保留每一个已推广到生产环境的制品以及任何正在运行系统所引用的一切,自动让旧的开发构建和拉取请求(pull-request)构建过期,并记录你删除了什么。目标是打造一个既能满足可复现性和审计需求、又能剔除噪音的存储库,而其成本是你有意识地选择的,而不是让你措手不及的。

权衡取舍:利与弊

决策优点缺点
重型图构建工具(Bazel 类)细粒度增量构建、远程缓存与执行,可扩展至巨型单体仓库学习曲线陡峭、迁移成本高,需要专门的构建团队
轻量构建工具(Make、语言原生工具)简单、开销低、上手快大规模下增量构建与缓存能力较弱,封闭性较差
远程/分布式构建缓存共享结果,在整个团队范围内带来显著提速存在缓存投毒面,正确性依赖于完整的输入哈希
完全封闭式构建可靠的可复现性,强溯源保证实实在在的工具和纪律成本,本地工作流更繁琐
构建一次,处处推广经测试的制品等同于交付的制品,审计轨迹清晰需要外部化配置和严谨的推广纪律
不可变、内容寻址的制品可发现篡改、引用明确无歧义不如浮动标签方便,需要管理更多存储
长期制品留存完整的可复现性和审计历史存储成本高,若无清理策略则查找变慢

这里反复出现的张力,存在于速度与信任之间。缓存、共享构建集群和浮动标签都能让构建更快、更方便,而每一种如果使用不慎,都会削弱你准确说出自己构建了什么、并证明它未被篡改的能力。解决办法是让可信的路径同时也是快速的路径。完整的输入哈希能让缓存既快又对。按摘要部署和按标签部署一样快,却安全得多。在构建中生成 SBOM 只需几秒钟,却能节省数天时间。只要你从一开始就把完整性工程化地内建到快速路径中,你就很少需要在速度和完整性之间做取舍。

与团队讨论的问题

  1. 我们今天能否重新构建出上个季度的生产制品,并得到完全相同的字节内容?如果不能,缺了什么? 这是对你的构建纪律最严苛的检验,因为可复现性依赖于锁定的工具链、锁定的依赖项以及被消除的不确定性协同发挥作用。挑一个几个月前发布的具体制品,真正尝试从记录的源代码提交重新构建它。你从这次尝试中学到的东西,比任何政策文档都更有价值:也许某个依赖范围是浮动的,也许编译器版本从未被锁定,也许某个时间戳被烙进了代码里。你发现的这些缺口就是你的可复现性待办事项,补齐它们正是让你能够信任“被审计过的东西就是被运行的东西”的关键所在,这在监管链是法律要求的受监管和政府场景中意义重大。

  2. 我们是每个制品只构建一次然后推广,还是按环境重新构建?我们如何证明是哪一种? 许多团队自认为只推广单一制品,但仔细审视后却发现,预发布和生产环境各自触发了一次输入略有不同的全新构建。追踪一次从提交到生产的真实发布,确认到底是完全相同的摘要在各个环境间流转,还是过程中产生了新的字节内容。如果你发现了重新构建,你就发现了一个测试保证比你以为的更弱的地方,因为经过测试的制品和被部署的制品无法被证明是相同的。解决方案()外部化配置,让二进制文件保持不变、而设置项可变()在可靠性和更清晰的审计叙事这两方面都能带来回报。

  3. 如果明天某个常用库中被宣布存在严重漏洞,我们能多快列出所有包含该漏洞的制品? 这个问题检验的是你的构建时溯源和 SBOM 实践是真实存在还是只是纸上谈兵。当一个被广泛使用的组件被发现存在可被利用的漏洞时,能在数小时内恢复的组织,是那些在构建时就生成物料清单并将其与每个制品一起存储的组织;而需要数周才能恢复的组织,则是在翻找构建日志、逐个询问工程师。用一个你实际依赖的库具体地过一遍这个场景,给出今天的答案需要多长时间计时。这个时间与“几分钟”之间的差距,直接衡量了你的供应链暴露程度,并与第 4.9 章所述的安全开发生命周期工作直接相关。

  4. 我们的构建每天要耗费多少工程时间?加快构建的商业理由是什么? 构建延迟是每次变更、每位工程师都要缴纳的税,在大型团队的规模下,累积总量很容易被低估,因为单次等待感觉并不算贵。同意去度量它,就能把一个模糊的抱怨变成一个可以与远程缓存、更好的增量构建或更重的构建工具的成本相权衡的数字。带上你的中位数和最坏情况下的本地及 CI 构建时间、每天的构建次数,以及一个诚实的估计()缓慢的构建有多频繁地把某人从心流状态推入上下文切换。与之相竞争的考量是,更快的构建并非免费:远程缓存和分布式执行会增加需要运行和保护的基础设施,更重的工具也需要一个维护团队。对企业或政府组织而言,要计入缓慢反馈在多个团队之间造成的吞吐量和士气成本,这通常远超基础设施账单,而这正是领导层已经愿意投资的角度。

  5. 谁能写入我们共享的构建缓存?什么机制能阻止一个被投毒的条目进入生产环境? 共享缓存用一个速度上的胜利换来了一个新的信任边界,同一个让一名工程师的结果服务于整个团队的机制,也让一个被腐化或恶意的条目能够危及所有获取它的人。对大型团队而言,爆炸半径就是整个组织,所以这值得一个经过深思熟虑的决策,而不是随工具默认设置而定。带上谁及什么系统拥有对每个缓存的写入权限的清单、写入是否仅限于可信的 CI 而非开发者的笔记本电脑,以及你的缓存键是否对每一项真实输入进行了哈希,使陈旧或被投毒的条目无法冒充合法条目。张力在于,最严格的控制会拖慢开发者从自己机器推送缓存条目这种便利路径。在企业和政府场景中,要把缓存投毒当作供应链模型中一项明确的威胁,并要求与任何能向发布中注入代码的其他生产系统同等的访问控制、日志记录和评审。

  6. 我们的构建图在什么规模上才值得使用更重的工具?我们如何知道已经越过了那个门槛? 在轻量生态系统工具和诸如 Bazel 这样的基于图的系统之间做选择,是这个领域中代价最高、最难逆转的决策之一,因为把一个大型代码库迁移到细粒度的构建定义需要数月时间和一个专门的团队。提前决定这个门槛,能让你既不会因为追赶潮流而承担不必要的复杂性,也不会在构建耗时已经拖累每个团队之后仍然固守轻量工具。带上你构建图的规模和相互依赖程度、当前的构建和缓存命中率指标,以及一个针对迁移和持续维护成本、对比工具能挽回的工程时间的切合实际的估计。与之相反的拉力在于,重量级工具能提供精确的增量构建和远程执行,这在规模化场景下是其他方案无法比拟的,但前提是你的构建图确实大到值得为此投入。对大型企业或政府机构而言,还要权衡该工具的封闭性和溯源保证是否有助于满足审计和供应链要求,这可能会让权衡的天平超出单纯的速度考量。

行业视角

初创企业。 团队规模很小,也没有余力投入构建基础设施,应保持轻量化:使用语言原生构建工具,从第一天起就采用锁文件,并按摘要而非“latest”标签部署容器镜像,因为这些习惯的成本几乎为零,却能省去日后一整类“在我机器上能跑”的痛苦。抵制住采用重型图构建工具的诱惑;你最稀缺的资源是工程注意力。一旦构建时间开始逼近几分钟,远程构建缓存就是唯一值得追求的升级。

小型企业。 没有专职的构建工程师,应依靠托管服务而非自行运行制品基础设施:托管的注册表加上你的 CI 提供商内置的缓存,能给你版本管理、留存策略和访问控制,而无需一个平台团队。把这个选择当作“买而非造”来定位,设置自动过期策略以保持存储成本可预测,并确保基础到位()锁定的依赖项和不可变、按摘要固定的部署()因为即使没人全天候盯着流水线,这些也能保护你。

企业。 在多个团队之间,问题在于一致性:一个共享、有专人负责的构建平台,一个通用的制品仓库,以及针对锁文件、签名、SBOM 和“构建一次、推广处处”的强制标准,这样就没有哪个团队会重新发明一套不可靠的流水线。投资于远程缓存,并在构建图足以支撑的地方投资基于图的工具,把缓存当作一个受治理的信任边界,配以受限的写入权限和审计日志。以带留存策略和溯源信息的受控资产的方式管理制品,使任何生产组件都能按需追溯到经过评审的源代码。

政府。 采购规则、透明度和公共问责制,使构建时完整性成为一项合规要求,而非锦上添花之举。让流水线对齐诸如 SLSA 这样的分级框架,在隔离、受网络限制的环境中,从锁定版本的工具链出发运行构建,并通过一个内部仓库来代理第三方依赖,该仓库在使用前对其进行扫描和批准。为满足记录留存法律所要求的年限,不可变地存储已签名的 SBOM 和溯源信息,只部署已签名、以摘要标识的制品,并随时准备向审计人员和公众证明,生产环境中的软件正是经过评审和批准的那个版本。

示例

初创企业。 一家拥有十五名员工、运行着一个小型单体仓库的初创公司,一开始使用语言原生构建工具和快速反馈,这在他们的规模下是正确的选择。随着公司成长,构建时间逼近五分钟,工程师们开始在等待时进行上下文切换。他们没有直接跳到重型图构建工具,而是添加了一个在开发者机器和 CI 之间共享的远程构建缓存,这把大多数构建缩短到几秒钟,因为未变化的目标是被获取而非重新计算的。他们为每种语言都采用了锁文件,按摘要而非“latest”标签部署容器镜像,并为拉取请求(pull-request)的镜像构建开启了自动过期,使他们的注册表账单保持平稳。整个努力耗时几周,却每天换回数小时的工程时间。

企业。 一家全球金融服务公司在数百名工程师之间运行着一个大型单体仓库,并采用了带远程缓存和远程执行的基于图的构建系统,因为在他们的规模下,细粒度的增量构建所挽回的工程时间远超构建团队的成本。每个制品都在隔离环境中以封闭方式构建、签名,并发布到一个附带 SBOM 和签名溯源信息的受管理注册表。部署按内容摘要进行,一个策略引擎会拒绝运行任何签名验证不通过的镜像。制品是被推广的,从不重新构建,从预发布到生产,因此通过测试的二进制文件被证明就是服务于客户的那一个。当审计人员要求把某个生产组件追溯到经过评审的源代码时,监管链是一次查询,而不是一场调查。

政府。 一家正在实现系统现代化的国家税务机构,把构建时供应链完整性当作一项合规要求来对待,使其流水线对齐诸如 SLSA 这样的分级框架。构建在隔离、受网络限制的环境中,从锁定版本的工具链和锁定版本的依赖项出发运行,因此输出是可复现且可独立验证的。每个制品都携带已签名的 SBOM 和溯源信息,不可变地存储数年以满足记录留存法律的要求。第三方依赖通过一个内部仓库代理,该仓库会在任何构建能够使用它们之前对其进行扫描和批准,使未经审查的代码完全无法接触网络。由于该机构只部署已签名、已推广、以摘要标识的制品,它能够向监管机构和公众证明,处理公民报税信息的软件正是经过评审和批准的那一个。

商业理由:动机、投资回报率与总拥有成本

构建与制品纪律的投资回报,首先体现为被挽回的工程时间。缓慢的构建对每一次变更、每一位工程师都是一种税负,这种成本在大型组织中会累积放大:从一个每天运行数千次的构建中削减几分钟,每年就能挽回数人年的时间,而更难量化、却同样真实的是,它能让工程师保持心流状态,而不是不断切换上下文。远程缓存和良好的增量构建往往能在几周内收回成本。可复现、只推广不重建的制品,能减少一整类“在预发布环境里没问题”的事故,降低变更失败率和平均恢复时间()这些正是领导层已经在关注的交付指标。

更大、也更不显眼的回报是风险的降低。已签名的制品、SBOM 和溯源信息,能把一次供应链事件从数周的紧急状态,变成一次有范围限定、耗时数小时的响应,也能把审计从一场消防演习变成一次查询。在受监管和政府场景中,这种可追溯性是能够运营下去的先决条件,所以这项投资不是可选的,而是结构性的。而如果你忽视这一点,总拥有成本会朝相反方向走:可变的制品和不可复现的构建会累积成一个没人能完全说清楚的资产,存储量在没有留存策略的情况下无限增长,每一次审计和每一次事故的成本都会超出应有水平。要向领导层说明理由,就把构建速度与工程吞吐量联系起来,把制品完整性与审计成本和泄露风险联系起来()这两者都是他们已经在出资支持的方向。

反模式与陷阱

  • 按环境重新构建: 为预发布和生产环境各自产出新的字节内容,丢弃了“经过测试的制品就是被部署的制品”这一保证。
  • 按浮动标签部署: 运行“latest”或其他可变标签,而非不可变摘要,导致运行的内容不可预测、不可追溯。
  • 没有锁文件: 浮动的版本范围让构建随时间悄悄拉入不同的甚至恶意的依赖。
  • 不完整的缓存键: 缓存键遗漏了某个真实输入,产生陈旧结果,浪费数天时间用于调试。
  • 未受保护的共享缓存: 让不受信任的写入者投毒一个远程缓存,导致消费者获取并运行被攻破的输出。
  • 不确定性的构建: 内嵌的时间戳、绝对路径和未锁定版本的工具,使输出产生变化并使验证失效。
  • 过早采用重型工具: 在构建图尚未大到足以证明其合理性之前,就承担了 Bazel 类的复杂性。
  • 把 SBOM 和溯源信息当作事后补充: 在构建之后重建供应链元数据,而不是在构建中生成,这样做既昂贵又不可靠。
  • 无限制的留存: 从不让旧制品过期,直到存储成本和缓慢的查找迫使一次仓促的清理。

成熟度模型

  • 第 1 级,启动(Initiate): 构建是没人负责的临时脚本,常常从开发者的机器上运行。输出不具确定性,依赖项没有锁文件地浮动,制品按环境重新构建并按可变标签部署,没有共享缓存,没有签名,也没有物料清单。
  • 第 2 级,发展(Develop): 一些团队已经把构建迁移到 CI 中,使用了纳入版本控制的定义,并采用了锁文件,但组织内的实践并不一致。制品可能落地到一个带基本版本管理的受管理仓库,一个本地或简单的远程缓存能加速常见构建,但按环境重新构建的情况依然存在,溯源信息也不完整。
  • 第 3 级,标准化(Standardize): 可复现、基本封闭的构建在全组织范围内被记录并强制执行,工具链被锁定版本,共享的远程缓存的键对所有真实输入进行哈希。制品不可变、以内容寻址、按语义化版本编号、构建一次并处处推广、经过签名,并附带 SBOM 一起交付。缓存访问受到控制,留存策略在各团队之间被一致地应用。
  • 第 4 级,管理(Manage): 构建资产被度量并对照基准进行控制。构建时间、缓存命中率、开发者反馈时间和存储成本都被追踪并设有明确目标,出现回退会触发行动,签名和溯源验证在部署时被强制执行,验证失败会阻止发布。构建时供应链完整性会对照诸如 SLSA 这样的分级框架进行评估,这些数字决定了下一步的投资方向。
  • 第 5 级,协同(Orchestrate): 构建、缓存、制品和供应链实践在整个组织范围内持续改进并相互整合。随着从事故和审计中吸取经验,工具、留存策略和安全态势不断调整,远程执行和缓存随代码库演进而调优,构建时完整性被融入更广泛的安全开发生命周期,而不是事后附加上去的。

讨论要点

  1. 你目前本地构建的中位数时间和最坏情况时间分别是多少?一个远程缓存会对它们各自产生什么影响?
  2. 你今天有哪些制品是按可变标签部署的?让每一个都改为按摘要部署需要做些什么?
  3. 你的构建图在哪些地方值得采用更重的工具?在哪些地方,这种工具的成本会超过它节省的成本?
  4. 谁能写入你共享的构建缓存?什么机制能阻止一个被投毒的条目到达生产环境?
  5. 你能为最近一次发布的东西生成一份签名的 SBOM 吗?如果不能,朝这个方向迈出的最小一步是什么?
  6. 你的构建制品留存策略是什么?今天的存储成本与应有的成本相比如何?

关键要点

  • 构建是交付的第一步:把它的速度和正确性当作生产问题来投入资金,因为下游的一切都会继承它的缺陷。
  • 让构建可复现,并在可行的情况下封闭式运行,配合锁定的工具链和锁文件,这样你审查的制品才能被证明就是你交付的制品。
  • 在本地和远程都进行增量构建和缓存,但要对每一项真实输入进行哈希并保护好缓存,因为共享缓存就是一个共享的信任边界。
  • 每个制品只构建一次,使其不可变且以内容寻址,有意义地对其进行版本管理,并在各环境间推广这个确切的制品。
  • 在构建时生成溯源信息、签名和 SBOM,并以带留存策略和访问控制的方式存储制品,把供应链完整性和审计准备状态变成日常工作的自然产物。

参考文献与延伸阅读

  • Jez Humble and David Farley, Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation
  • Nicole Forsgren, Jez Humble, and Gene Kim, Accelerate: The Science of Lean Software and DevOps
  • Titus Winters, Tom Manshreck, and Hyrum Wright (eds.), Software Engineering at Google: Lessons Learned from Programming Over Time
  • Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy (eds.), Site Reliability Engineering: How Google Runs Production Systems
  • Peter Smith, Software Build Systems: Principles and Experience
  • The Open Source Security Foundation, SLSA: Supply-chain Levels for Software Artifacts (specification)
  • National Institute of Standards and Technology, Secure Software Development Framework (SSDF), SP 800-218
  • Tom Preston-Werner, Semantic Versioning Specification (SemVer)