• 我们为什么转到 GitHub Actions
  • 发布于 2个月前
  • 215 热度
    0 评论
Actions 发展简介
当 GitHub Actions 在 2019 年秋天发布时,立即就引起了人们的关注,因为它引领了“CI/CD平台的第三次风潮”。从可重用的开源构建块构建可组合管道,在改善 CI/CD 管道操作的效率和维护方面,这个独特的方法明显有很大的潜力。然而,尽管 Actions 平台的设计具有内在的吸引力,但许多组织,特别是已经构建了 CI/CD 系统的大型企业,对采取行动犹豫不决,这是可以理解的。在一定程度上,这是由于婴儿期的平台存在诸多限制,比如在企业内部执行和共享 Actions 的限制。

幸运的是,自最初发布之后,Actions 已经采取了一些步骤来消除障碍,实现企业使用的可维护性。此后不久,自托管runner推出,这是在现有企业内部网络中执行作业的一项基础能力。然后,2021 年 11 月,可重用工作流发布,极大地扩展了提供可重用管道的能力,减少了重复步骤和样板代码的数量,缩小了与许多更成熟的 CI/CD 产品的差距。真正让这一功能做好企业采用准备的是在 2022 年 1 月宣布的内部共享操作的能力,这种能力使平台工程团队能够打包发布常见的可重用步骤,而又不会暴露给外界。最终,我们感觉 Actions 有能力作为企业 CI/CD 生态系统的基础了。

我们为什么转到 Actions
在 Thrivent,我们最近几年投入了大量的精力来改善 CI/CD 工具的开发体验,包括实现自动化,按照公认的黄金路径快速从模板生成应用程序管道。尽管如此,我们在提供一个一致性和灵活性均衡的平台方面遇到了限制。就我们当前的系统而言,其中一个不足体现在在 CI/CD 管道中共享公共任务。我们的共享脚本存储库提供的封装有限,并且会弄乱应用程序的构建历史,将实际应用程序存储库的提交和共享任务存储库的提交混在一起。

在对管道模板升级时,我们保留用户自定义设置的能力也很有限。如果我们在模板中引入了一个新的步骤或修复了一个 Bug,那么开发人员将不得不重新生成整个管道来获取更改,这会删除他们所做的所有定制。

最后,还有一个不太明显的因素是,最大限度地减少开发人员使用 CI/CD 系统的障碍。当我们的技术组织进入增长期,有大量新的开发人员涌入,提供一个能够让这些开发人员快速上手并满足他们期望的平台成了当务之急。在最近Stack Overflow和State of Frontend的调查中,大多数开发人员都回复说使用了 GitHub。鉴于它在开源社区中的地位,这不足为奇。通过将使用范围扩大到平台原生的 CI/CD 功能,我们看到了以下机会:
1.将工具整合到单个系统中,充分利用开发人员现有的熟悉度,从而减少无关的认知负荷;
2.使用存储库事件最大限度地减少上下文切换,使编码生命周期内的工作更紧凑;
3.提供我们一直在寻找的原生模块化管道架构。

总的来说,GitHub Actions 平台的设计借鉴了现代开发方法。开发人员可以利用可重用组件灵活地构建管道,将团队在开发活动中使用的开源思想转移到 CI/CD 管道中。然而,随着我们对 Actions 生态系统的深入研究,Actions 在 CI/CD 处理方面存在着一些显著的差异,这促使我们重新思考了之前所做的一些假设,并开发了一种新的策略,将平台的限制变成一种优势。

术语速览
在 GitHub Actions 系统中,有几个术语由于在 CI/CD 生态系统中使用广泛,可能具有各种含义,但在 Actions 框架中,它们指的是特定的资源(即“工作流”、“作业”、“操作”、“事件”和“runner”)。如果你不熟悉 GitHub Actions 的定义,那么建议你快速浏览一下GitHub的文档。

思想转变
在转换到 Actions 时,其中一个需要首先理解的区别是存储库和管道工具之间的关系。我们一直在使用的 CI/CD 产品是与应用程序的存储库集成在一起的,虽然本质上是分开的。即使是 CI/CD 领域中的其他工具,它们可能为应用程序的整个生命周期提供了更全面的解决方案(通常将存储库、管道和其他交付工具捆绑到单个产品中),也倾向于提供更高级的结构,如“项目”或“应用程序”,明确区分存储库和管道,使它们的地位相对平等。当 CI/CD 过程开始时,所有的注意力都转到了管道工具视图上。

相比之下,使用 GitHub Actions,存储库仍然是宇宙的中心。Actions 平台中触发的工作流主要存在于存储库的上下文中。这种方法有利也有弊。
优点
1.存储库现在提供了应用程序状态的单一落点,将应用程序运行状况和部署信息与代码本身紧密地结合在一起。
2.在传统的提交/推送触发器之外,它还提供了在各种存储库事件(例如,加标签、pull 请求)上启动工作流的能力,方便我们将 CI/CD 更紧密地集成到开发流程中去。
3.对于共享操作和工作流的系统,它提供的能力让我们可以独立于应用程序存储库的变更,轻松地管理共享管道组件的生命周期。

缺点
1.因为所有事情都是基于存储库中发生的事件进行编排的,所以很难从其他(外部)事件源启动工作流。
2.通过工作流生成的工件在存储库中的可见度与代码的可见度不一样。这意味着分享和推广结果工件的能力是有限的,因为该信息没有持久化到存储库中,也没有在工作流执行之间共享。
3.不同的存储库组织方法可能会为流程带来额外的复杂性,例如,单存储库可能在同一个存储库中包含多个不同的项目。这些相互独立的应用程序在构建和部署的方式上也可能存在差异。因此,需要格外小心,并可能需要借助额外的工具来有效地管理触发事件,以避免构建未经修改的应用程序,以及正确地打包和部署下游工件。

制定 Actions 使用策略
现在,在了解了 GitHub Actions 的视角及其优缺点后,我们可以开始制定有效使用 Actions 平台作为 CI/CD 解决方案的策略。以下是指导我们作出决策的一些主要原则。

优选 Pull 请求审批而非工作流审批
CI/CD 平台的主要关注点之一是有效地收集和显示输出,并对这些结果强制执行质量门检验。以前,我们一直在使用一个许多组织都很熟悉的流程:

在这种方法中,对目标分支的推送会触发管道。管道会分成一系列的阶段执行,每个阶段都会输出一些结果,并且通常会根据这些结果评估工件的生产就绪情况。

我们的新目标是使这些信息更趋近于自然的开发活动,同时又要注意 Actions 结果和工件显示方式的一些固有限制,因此,我们希望有一种更好的方式来提供这些控制,而 pull 请求特性是合乎逻辑的选择。Pull 请求维护的审计历史更久(与工作流执行的保留时间限制相比),并且讨论聚焦于变更本身,代码可供随时参考。将 pull 请求作为主要的审批仪表板需要转换模式,pull 请求收集相同的结果并执行同样的质量检查,但不需要额外导航回存储库查看相关的更改。

制定可重用组件指南
构建可重用管道片段的自助库也带来了许多问题。我们要如何处理共享工作流和操作的更新?当多个团队对看似相同的任务在处理方式上略有差异时,如果我们想要平衡这些团队,什么样的抽象级别才合适?

面对这些挑战,我们的目标是实现一系列“铺好的路(paved roads)”,每个新的存储库都应该能够使用我们团队支持的模板、操作和可重用工作流进行部署,但又有按需定制的灵活性。这为保持存储库始终处于可部署状态的持续交付指令奠定了基础。为了实现这一目标,我们制定了一些指导方针。

推送所有版本的语义版本标签
维护精确的版本让用户有能力根据他们所能接受的风险级别保持更新。在我们之前的 CI/CD 工具中,共享任务是从存储库的主分支上拉取的,这意味着我们需要非常小心,以免引入意外的更改。虽然 Actions 支持这种用法,但最好使用语义版本范围在保持最新和避免破坏性更改之间实现一个平衡。在这里,我们学到的一个关键知识是,确保为每个版本推送/更新多个标签(主标签、次标签和补丁标签),确保跟踪主标签(如“@v2”)的用户也可以获得新的补丁版本。

像管理任何其他 API 一样管理你的接口
当向内部团队提供资源时,很容易陷入这样的陷阱,即假设他们对内部工作方式的理解比对开源项目或外部供应商的理解更深入。这可能会迅速导致抽象泄漏,降低所提供的共享组件的价值,因为用户必须承担额外的认知负荷,而不仅仅是理解如何与它的 API 交互。

为了保持更加一致的封装级别,我们的目标是明确我们的输入和输出,尽量减少假设和对环境副作用的依赖。在此基础上,我们一直在尝试提供一个合理的默认值,这样用户就可以专注于提供大多数用例都需要的值。最后,如果使用环境变量,我们建议将它们的范围限制得越窄越好。

例如,如果一个环境变量只在一个步骤中使用,就在这个步骤中声明它,如果在多个步骤中使用,就在作业中声明它,最后,如果在多个作业中使用,就在工作流中声明它。这是一个通用的建议,与平台无关,其目的是降低变量意外修改或命名冲突的风险,并通过将信息保存在使用位置附近来提高可读性。

聚焦工作流
在 GitHub Actions 生态系统中,人们关注最多的是 Actions;毕竟,产品就是以 Actions 命名的。然而,在铺好的路上,操作不过是砖。它们非常有助于将流程分解为功能步骤,但是,为了向内部交付团队提供最大价值,平台工程师应该投入同样多的时间来开发将这些步骤联系在一起的可重用工作流目录。可重用工作流可以确保不同的步骤按所需的顺序执行,并可以简化设置和拆卸活动。

例如,我们的团队创建了一个将容器部署到 Kubernetes 环境的操作。为此,需要将环境元数据作为输入的一部分提供。为了更好地管理调用此操作的活动,我们使用工作流来执行一些额外的步骤,收集所需元数据、执行部署,然后将结果添加到输出。但是我们发现,在许多不同的情况下,这个工作流的代码都会重复——针对不同的触发事件,以及不同的部署环境(开发、过渡、生产等)。为了进一步最大化重用,我们将这些工作流重构为单个可重用的工作流,它会根据触发事件确定正确的目的地和镜像上下文。

降低使用第三方 Actions 的风险
GitHub的文档提供了一些很好的 GitHub Actions 安全加固通用指南。其中有这样一条警告,“从 GitHub 上的第三方存储库获取操作存在重大风险”。为此,他们提供了一些指导,即将第三方操作的版本锚定到一个提交 SHA,审计操作的源代码,并且只使用你信任的创建者提供的操作。这给我们造成了一些困难,因为我们已经习惯了由软件组合分析(SCA)工具自动提供适当的外部组件的风险(漏洞/许可)信息,这使得我们在应用程序开发活动中能够更广泛地应用开源组件。

回归手动检查所有外部操作的做法,可能会严重限制开源 Actions 市场的效用,而这个市场在我们的旅程之初就有这样的承诺。随着供应链攻击的增加,保证管道安全变的至关重要,但手动检查操作源代码和创建者的方法无法扩展,因为我们团队可能对给定操作使用的语言没有深入的了解,而且,当它来自单个用户的贡献时,创建者的可信度就很难评估。

最近,GitHub 宣布支持Actions Dependabot告警,这是朝着正确的方向迈出了良好的第一步。然而,这仍然没有达到我们所希望的安全等级,原因如下:
1.Dependabot 生成的告警很大程度上依赖于创建者自己报告的漏洞。在个人提供的小操作中,尚不清楚他们可能提供何种程度的报告。
2.仍然没有对风险进行基线评估——当用户正在浏览 Actions 市场时,并没有现成的数据让用户可以了解操作中现有的漏洞或创建者的漏洞管理流程。
3.还有一种风险是,有人故意将恶意代码嵌入到操作中(如果他们是原始作者,则可以在创建操作时嵌入恶意代码,或者通过供应链攻击更隐蔽地嵌入恶意代码),这种方法无法捕获这类恶意代码。
4.最后,目前许多公司在依赖关系管理方面的最佳实践是内部托管一个存储库,并代理到其他主要的公共存储库(NPM、Maven Central、PyPI 等)。对于公司产品使用的依赖项,会有一个内部缓存,为的是可以在依赖源无法访问时帮助确保业务连续性。现在,操作具备了同等的重要性,因为它们对工作管道至关重要,任何中断都可能给开发人员带来大量的时间损失。

为了解决上述限制,我们使用现有的工具开发了一种方法,并结合 GitHub 设置中的控制项,设法在风险和开发速度之间达到一个平衡。首先,我们允许任何内部编写的操作和任何 GitHub 编写的操作。下一个通用控制是允许一个经过验证的创建者列表,并允许来自已与我们建立信任关系的组织的所有操作。最后,对于个别来自较小的、未经验证的第三方操作,我们有一个允许列表。

为了加快这些操作的审批过程,我们利用现有的 SCA、SAST 和容器安全扫描工具来扫描这些操作的存储库和/或容器镜像,将任何潜在的漏洞都暴露出来。在某些情况下,我们使用的最后一种技术是将操作存储库的副本派生或导入到企业自己的存储库中。这提供了三个保证:
1.如果需要,总是有可用的代码;
2.可以修改代码,以便更好地匹配我们自己的用例;
3.可以确保任何更改版本引用的尝试(如更改标签)都不会影响我们。

当然,还需要不断的努力,将源存储库中任何有用的更改或修复都合并到我们自己的版本中,因此,在使用这种方法时应该做好权衡。GitHub 在他们的路线图中也有继续提高操作安全性的计划。显然,这是一个不断发展的领域,我们希望平台和解决方案生态系统的进步可以减少我们对自主开发评估流程的依赖。

扩大基础设施实践,紧跟需求步伐
随着 Actions 平台应用范围的扩大,提高性能和可靠性成了人们关注的焦点。我们依据运营使用的关键经验扩展了我们的战略。

利用自托管 Runner 增强弹性
像许多企业一样,为了满足需求,并帮助管理成本,我们大量使用了自托管 runner,使作业可以与我们内部网络上的资源进行通信,从而可以更好地管理 runner 镜像的工具和设置。尽管依赖于自托管 runner,但我们希望与使用 GitHub 托管的 runner 的体验保持一致,即在作业开始时 runner 已经准备就绪(最小化作业等待 runner 可用的排队时间),并且将这些 runner 视为一次性使用的临时实例,最大限度地减少前一次执行留下的更改影响下一作业幂等性的机会。

为次,我们维护了一个一次性 runner 的暖池——当新作业启动并获取 runner 时,我们使用一个 WebHook 来启动下一个 runner。这样一来,这个池子就总是能够处理当前的工作量。另外,在每个工作日开始时,我们还使用默认分配的 runner 来预先填充 runner 池(然后在晚上关闭它),并随着使用量的增加扩大这个池子。

使用缓存缩短构建时间
如上所述,我们的 runner 是一次性的,我们为每个作业提供的运行环境都是干净的。虽然这在构建一致性和隔离方面带来了明显的好处,但那也意味着生成的任何文件后续作业都无法使用,除非它们直接使用像Artifacts这样的特性共享。在这里,我们要克服的主要挑战之一便是花费大量的时间重新生成现有的资源。

幸运的是,在需要共享大量极少变化的数据(如应用程序依赖关系)时,GitHub 提供了几种缓存功能。我们需要设法解决的三种最常见的情况是共享应用程序工件、容器镜像和操作镜像。第一种情况涉及到作业之间使用的应用程序工件和依赖关系。在这种情况下,我们使用GitHub缓存操作迅速提高了工作流的性能。

容器镜像,包括常用的基础镜像层,是第二种最常见的情况。为了缓解经常重新拉取常用镜像的问题,我们研究了Docker buildx操作支持的不同缓存选项。我们在大多数镜像构建任务中都使用了这些选项。在对内联缓存、GHA(GitHub Actions 缓存)和注册中心缓存选项进行了不同的度量之后,我们目前使用 GitHub 容器注册中心(GHCR)托管的注册中心缓存取得了最大的成功。

最后,最棘手的是操作镜像。需要理解的一个重要概念是,每次使用打包为镜像的 GitHub 操作时,都是使用它们的 Dockerfile 从头开始构建的(有一个替代方案是使用 Docker Hub 中的公共镜像,但我们不希望公开发布内部操作,所以这并不可行)。

为了减轻这种痛苦,我们遵循通用镜像最佳实践来尽量缩小镜像。不过,我们也在研究将一些作业抽离出来,不再作为容器化操作执行,而是将相同的逻辑打包为预构建的独立镜像,然后将作业配置为在该镜像中运行。然后,作业将执行一个脚本。该脚本通常用作操作的 ENTRYPOINT,其行为与在本地作为操作运行时非常相似。这样做的好处是,我们可以拉取一个镜像,而不是每次重新构建每个层,这使得我们可以在 runner 启动过程中预先拉取常用的镜像,在工作流开始之前缓存它们。

GitOps 即将来临
尽管我们提升了操作组合以及工作流的质量和效率,但我们还面临的主要问题之一是,在应用程序部署和运营阶段保持敏捷性和可见性。我们最初的部署方法是将变量替换为参数化模板,使用我们的自定义操作,最终形成一个完整的 Kubernetes 清单,应用于我们的环境。

这给我们的团队带来了很大的负担,因为我们需要使用复杂的逻辑来处理各种部署风格、重试和部署失败恢复,以及清理过时部署。由于在部署后,包含所有变量的最终清单没有在任何地方持久化,所以部署问题也变得更加难以排查。这也意味着,回滚或升级工件需要重新调用整个流程。

为应对这些挑战,我们再次开始研究如何重塑部署策略,以发挥 GitHub CI/CD 平台的优势。在重申上述部分经验教训的基础上,我们认识到,我们需要的解决方案应该:
1.聚焦于存储库;
2.通过 pull 请求控制变更;
3.尽量减少自定义部署逻辑。

到此为止,熟悉GitOps理念的读者可能开始明白我们的思路了。在 GitOps 提倡的部署策略中,运行时状态在 Git 存储库中声明,并由环境中运行的代理自动拉取和协调。

转向 GitOps 部署模型需要采用新的技术和行为模式,但这使得我们可以利用经开源社区专家优化的部署和协调逻辑,并维护一个清晰、持久、有版本控制的环境变更历史,有一个清晰的路径(拉请求)可以轻松地更改版本,并在需要时保留审批记录。这一切都是基于开发人员现有的熟悉度,也正是这种熟悉度把我们吸引到了 GitHub。

我们正处于这一转变的早期阶段,但很高兴我们采取了下一步措施,为我们的客户提供了更好、更可靠的价值交付方式。
用户评论