译者 | 刘汪洋
审校 | 重楼
概括:这篇文章分享了作者在使用 Github Actions 作为 CI/CD 工具时遇到的一些问题和解决方案,包括如何避免重复代码、如何管理环境变量、如何使用缓存和工件、如何利用复用工作流等。
开始构建发布流水线
GreptimeDB 自开源伊始,就采用 GitHub Actions 实现了自动化软件构建过程,从而诞生了首个发布流水线。
对于开源项目,构建一个稳定且一致的发布流水线具有以下关键价值:
- 供应随时可用的软件构件:身为软件供应链的上游生产者,我们必须为不同的下游用户提供安全、可信赖、随时可用的软件构件,如二进制文件、镜像等。
- 优化开发者体验:用户可无需繁琐配置或从零开始设置和编译,即可获取适合各自平台的即时可执行的软件构件。
- 实现发布工作流的自动化测试:结合不同类型的回归测试(例如性能、稳定性、集成测试等)和自动发布流程,以提高软件的整体质量。
虽有其他替代方案,如 Circle CI、Travis CI、GitLab CI,或自托管的开源项目如 Tekton 和 Argo Workflow, 但选择 GitHub Actions 的理由显而易见:它与 GitHub 生态系统融合,为用户提供了便捷的操作界面和丰富的软件市场访问权限。
然而,用户友好并不意味着维护轻松。相反,GitHub Actions 的维护可能变得复杂。GreptimeDB 的最初开源版本中的 release.yml 仅包含了精炼的183行代码。但随着许多贡献者的修改,这份文件逐渐演变并整合了:
- 构建多样化平台上的构件;
- 构建激活软件构件的不同功能开关;
- 在实际构建之前执行集成测试;
- 将软件构件推送到不同仓库(如 DockerHub、ACR、S3等);
- 控制不同的发布条件(如手动触发、错误容限等);
- 等等。
还有,由于一些特殊需求(如调试发布、每日构建等),在不同的内部仓库中产生了许多只有微小差异的相似流水线分支,这增加了维护的压力。
随着构建要求的复杂性不断提高,release.yml 文件迅速变得庞大,充满了冗余配置,维护难度增大。如果不及时进行重构,发布流水线将面临迅速甚至彻底的失效风险。
发布流水线退化问题分析
随着构建要求的复杂性不断提高,release.yml 文件迅速变得庞大,充满了冗余配置,维护难度增大。如果不及时进行重构,发布流水线将面临迅速甚至彻底的失效风险。
发布流水线退化问题分析
审查release.yml文件后,我们需要识别一些促使它迅速退化的因素。只有深入了解问题的根源,我们才能制定合适的重构方案。
- 语言局限性:GitHub Actions 的基于 YAML 的领域特定语言(DSL)与通用编程语言相比表现力不足,从而可能产生冗余和难以维护的代码。
- 调试难度高:GitHub Actions 因难以调试而闻名,特别是项目使用的 Rust 语言编译成本较高,进一步加长了调试周期。尽管如 act 等工具可以在本地执行 GitHub Actions,但实际运行操作仍然必须进行,因此无法有效缩短编写-运行-调试的周期。
- 动作解耦不足:GitHub Actions 通过 Composite 组合不同动作。我们由于缺乏经验,未将逻辑分解为独立动作,而是将所有内容集中到一个 YAML 文件中,因此维护起来较为困难。
- 重复构建问题:由于 GitHub 缺乏支持 ARM64 的虚拟机实例,为了优化编译性能,我们选择在 GitHub 的 x86_64 虚拟机实例上进行 AMD64 和 ARM64 软件构件的跨平台编译。虽然可以使用 Docker Buildx 激活 QEMU 进行 ARM64 平台模拟构建,但性能不佳。由于我们依赖于 GitHub Runner 主机环境而非 Dockerfile,实现一致的可重复构建颇具挑战性。
能够在首次尝试时就成功执行 GitHub Actions 的人非常罕见。
我们在重构过程的开始阶段,应该首先注重可维护性优于性能(构建速度)。发布的流水线将随项目的增长不断演化,因此可维护性至关重要。如果忽视了可维护性,可能会逐渐陷入困境,最终导致研发效率的降低。只有保障了可维护性,我们才能着手提高性能。在编译/构建场景中,合理利用各种缓存机制和优质的构建机器通常能够提升性能。
重构计划
重构 YAML 文件不同于常规编程项目,它更多地是一次对配置流程的全面审查,虽然这听起来并不完全符合逻辑,却存在较高的偶发复杂性。在此过程中容易陷入难以察觉的问题,进而可能面临解决的艰难挑战。以下总结了针对正在经历此类重构的一些实用建议。
- 采用 Dockerfile 实现构建标准化:虽然基于 Dockerfile 的构建可能影响性能,但这样的方式增强了可维护性,统一了跨平台构建流程,确保了构建的可重复性。
- 统一构建命令接口:基于上述考虑,需将各类构建命令精简为单一 make 命令。将编译的复杂性限制在 yaml 文件之外,避免发布阶段隐藏过多细节,且在开发阶段的 Makefile 或脚本中展现出来。通过 Makefile,用户可以体验到与发布阶段一致的构建过程,从而提升开发效率。
- 采用 AWS EC2:鉴于 GitHub Actions 目前缺乏 ARM64 VM 实例,需要采用交叉编译。使用 AWS EC2 ARM64 实例来构建 ARM64 平台的软件,以实现所有平台构建过程的标准化。
- 模块化解耦合:将 release.yml 拆分,使其成为一组明了的任务集合。每个位于 actions 目录下的 action.yml 文件都应保持简洁明了,以便根据相同操作自定义各种流程,提高整个过程的灵活性和效率。由于 GitHub Actions 内无组任务机制,此法是最佳方案。
- 简化任务执行:每个任务需专注于单一、特定的任务,增强其幂等性。这样在出错时,重试工作将更容易。此外,有助于更有效地提取顶层控制变量,允许更精确的手动触发控制。
- 避免在 Actions 中过度加载 Shell 命令:不要在单个 GitHub Actions 步骤中打包过多 Shell 命令,以利于维护。如果命令较多,可考虑转换为外部脚本并精简输入参数,确保脚本的独立执行和验证。
- 引入 Pre Job 分配 Runners:Allocate Runners 是首要任务,用于为后续工作分配 Runners 并创建全局版本标签。例如,使用 EC2 时,Allocate Runners 将通过 EC2 API(由 ec2-github-runner Action 实现)分配相应平台的 EC2 实例。未来计划引入更精确的选择算法以优化 Runner 分配成本。
- 实现全球统一流程:避免创建功能相似的 GitHub Actions 分支,以减少维护负担。为促进透明的开源开发,所有内部使用的构建流程已合并至主要的 GreptimeDB 存储库。代码若为开源,则软件产品和构建过程也应同样开放。
- 在 GitHub 存储库中合理使用变量和秘密:不应把大部分外部参数当作机密处理。一些非机密的外部参数应配置为 GitHub 变量,以便未来可以方便修改。应避免在 YAML 中硬编码将需要频繁修改的变量。
展望
GreptimeDB 对发布流水线的重构只是其走向成熟旅程中的一个环节。我们正朝着构建更高质量、更强大的 CI 努力发展:
- 拓展平台生态系统: 我们 windows 系统软件产品即将发布,届时欢迎来测试和体验。
- 增加自动化测试种类:在未来的计划中,我们将在 CI 流程中融合各类测试方式,例如混沌测试和性能测试,以持续提高软件的质量。
- 优化 CI 使用成本:通过精确分配各类 Runners,满足不同用例的需求,我们计划使整个 CI 运行更为经济和高效。
- 提升构建效能:虽然重构发布流水线在一定程度上对构建性能造成影响(#2113),但我们计划通过采用更智能的构建缓存技术,进一步加速构建过程。
- 强化软件供应链的安全性:在现代软件制品管理中,软件供应链安全日渐重要。作为一个开源项目,我们将确保软件产品的安全性、可信性和透明度。我们计划在发布流程中加入基本安全措施,如 SBOM 管理、软件签名和验证等。
虽然充分利用 GitHub Actions 存在挑战,我们仍将坚持不懈地改进。如你对此感兴趣并想要深入了解,欢迎加入我们在 Slack 上的社区进行讨论!你的建议对我们下一阶段的改进很可能会起到关键作用。
译者介绍
刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 JAVA 工程师,拥有多个主流技术博客平台博客专家称号。
标题:Practical Tips for Refactoring Release CI using GitHub Actions,作者:Greptime