Github-DevOps-加速指南-全-

Github DevOps 加速指南(全)

原文:annas-archive.org/md5/677f27c30764b3701bc2b6cf6de3a30e

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

我们已经进入了 2020 年代,研究已经向我们展示了超过 10 年,高开发者绩效的公司不仅在速度和吞吐量上胜过竞争对手,而且在质量、创新、安全性、员工满意度以及最重要的客户满意度方面也得分更高。

然而,除了一些独角兽公司之外,大多数传统企业都在努力转型。已建立的严格结构和缓慢的流程,单片应用架构以及传统产品的长发布周期使得公司难以改变。

然而,这并不是一个新现象。变革性的改变总是困难的,并且可能需要多年才能成功,如果公司真的成功的话。失败的概率也非常高。这是因为变革必须在许多层面上发生 - 如果这些变化不协调,变革注定会失败。本书将帮助您进行转型 - 不仅提供高开发者绩效的研究,还提供实际例子,说明您如何加速软件交付。

本书是一本关于 DevOps 的实用指南。它帮助已经在 DevOps 旅程中的团队进一步提升,并通过提供简单的解决方案来加速他们的软件交付性能。它将帮助团队找到衡量成功的正确指标,并从其他成功案例中学习,而不仅仅是复制这些团队已经做过的事情。本书使用 GitHub 作为 DevOps 平台,并展示了如何利用 GitHub 的力量进行协作、精益管理以及安全快速的软件交付。

读者在本书结束时将理解什么影响软件交付性能,以及他们如何衡量交付能力。因此,他们将了解自己的位置,并且如何透明地向前迈进,通过跨团队协作的简单解决方案。装备了常见问题的简单解决方案,他们将了解如何利用 GitHub 加速:通过 GitHub Projects 使工作可见,通过 GitHub Insights 测量正确的指标,使用 GitHub Actions 和高级安全性进行坚实和成熟的工程实践,以及转向基于事件的、松耦合的软件架构。

适合读者

这本书适合开发人员、解决方案架构师、DevOps 工程师、SRE(Site Reliability Engineer),以及希望增强软件交付性能的工程或产品经理。他们可能是 DevOps 的新手,也可能已经有经验,但难以实现最大的性能。他们可能已经有 GitHub 企业版的经验,或者来自 Azure DevOps、Team Foundation Server、GitLab、Bitbucket、Puppet、Chef 或 Jenkins 等平台。

这本书涵盖的内容

第一章关键指标,解释了精益管理背后的理论,以及如何衡量绩效和文化变化。它探讨了开发者的生产力以及为什么这一点对于吸引人才和实现卓越的客户满意度如此重要。

第二章规划、跟踪与可视化你的工作,讲解了工作洞察:通过应用精益原则加速软件交付绩效。你将学习如何使用 GitHub Issues、Labels、Milestones 和 Projects 规划、跟踪并可视化团队和产品的工作。

第三章团队合作与协作开发,解释了软件协作开发的重要性,以及如何利用 GitHub 在不同团队和学科之间进行协作。

第四章异步工作:随时随地协作,解释了异步工作方式的好处,以及如何利用它们实现更好的共享责任、分布式团队、更高的质量和跨团队协作。它展示了如何使用 GitHub Mobile、Microsoft Teams、Slack 以及 GitHub Pages、Wikis 和 Discussions 来实现随时随地的协作。

第五章开放源代码和内部源代码对软件交付绩效的影响,描述了自由和开源软件的历史以及它在近年来,尤其是在云计算背景下,所获得的重要性。它将教你如何利用开源加速软件交付。此外,它还将解释如何将开源实践应用于内部源代码,以帮助你转型组织,并讨论开源和内部源代码对外包和内包策略的影响。

第六章使用 GitHub Actions 进行自动化,解释了自动化对质量和速度的重要性。它介绍了 GitHub Actions,并说明如何使用它们进行各种自动化任务——不仅限于持续交付。

第七章运行你的工作流程,解释了如何使用 GitHub Actions 工作流运行器的不同托管选项来应对混合云场景或硬件在回路测试。它展示了如何设置和管理自托管运行器。

第八章使用 GitHub Packages 管理依赖关系,描述了如何将 GitHub Packages 与语义版本控制以及 GitHub Actions 结合使用,管理团队和产品之间的依赖关系。

第九章部署到任何平台,展示了如何通过简单的实操示例,轻松部署到任何云平台,包括 Microsoft Azure、AWS 弹性容器服务和 Google Kubernetes Engine。它展示了如何通过 GitHub Actions 进行分阶段部署,以及如何使用基础设施即代码(Infrastructure as Code)自动化资源配置。

第十章特性开关与特性生命周期,解释了特性开关(或特性切换)如何帮助减少复杂性,并管理特性及软件的生命周期。

第十一章基于主干的开发,解释了基于主干的开发的优势,并介绍了最佳 Git 工作流,以加速软件交付。

第十二章通过左移测试提升质量,详细探讨了质量保证和测试在开发者速度中的作用,并展示了如何通过测试自动化实现左移测试。本章还涵盖了生产环境中的测试和混沌工程。

第十三章左移安全与 DevSecOps,从更广泛的角度探讨了安全在软件开发中的作用,如何将安全嵌入开发流程,并实践 DevSecOps、零信任模型,以及如何左移安全。本章探讨了常见的攻击场景,展示了如何通过攻击模拟和红队/蓝队演习来提高安全意识。本章还介绍了 GitHub Codespaces 作为云中的安全开发环境。

第十四章保障代码安全,描述了如何使用 GitHub 高级安全功能,通过 CodeQL 和其他工具执行静态代码分析,消除安全和合规问题,如何通过 Dependabot 有效管理软件供应链,以及如何使用秘密扫描(Secret Scanning)消除代码库中的敏感信息。

第十五章保障部署安全,展示了如何确保环境中的部署安全,并如何以安全、合规的方式自动化完整的发布流水线,以满足监管要求。本章涵盖了软件材料清单SBoM)、代码和提交签名、动态应用安全测试,以及如何加强发布流水线的安全性。

第十六章松耦合架构与微服务,解释了松耦合系统的重要性,以及如何通过演进软件设计来实现这一目标。本章涵盖了微服务、演进设计和基于事件的架构。

第十七章赋能你的团队,讲述了组织的沟通结构与系统架构(康威定律)之间的关系,以及如何利用这一点来改进架构、组织结构和软件交付性能。本章涵盖了“两披萨团队”、反向康威操作和单一仓库与多仓库策略。

第十八章精益产品开发与精益创业,讲述了精益产品管理在产品和特性层面的重要性。它展示了如何将客户反馈融入产品管理中,创建最小可行产品,以及如何管理企业的产品组合。

第十九章实验与 A|B 测试,解释了如何通过进行实验来验证假设,从而不断发展和改进你的产品,使用基于证据的 DevOps 实践,如 A|B 测试。它还解释了如何利用 OKR 来赋能团队进行正确的实验,并构建正确的产品。

第二十章GitHub:所有开发者的家园,解释了 GitHub 如何作为一个整体的开放平台来服务你的团队。它解释了不同的托管选项、定价以及如何将其集成到现有的工具链中。

第二十一章迁移到 GitHub,将讨论从不同平台迁移到 GitHub 的策略以及其他系统的集成点。它解释了如何找到合适的迁移策略,如何使用 GitHub Enterprise Importer 和 Valet 进行繁重的工作。

第二十二章组织你的团队,讲述了如何将仓库和团队结构化成组织和企业的最佳实践,以促进协作和简化管理。本章涉及基于角色的访问、定制角色以及外部协作者。

第二十三章转型你的企业,将所有内容汇集在一起。本书提供了许多工具,帮助你推动成功的转型并提升开发者的工作效率。但只有将所有部分结合起来,转型才会成功。本章将解释为什么许多转型失败,以及你应该做些什么来确保转型成功。

想要从本书中获得最大的收益

如果你想跟随动手实验部署到 Azure、AWS 或 Google,你将需要一个相应云环境的账户。

如果你使用的是本书的数字版,建议你亲自输入代码,或者从本书的 GitHub 仓库访问代码(下一个章节中会提供链接)。这样做将帮助你避免由于复制粘贴代码而可能产生的错误

下载示例代码文件

本书的示例和实践实验可以在 GitHub 上找到,网址是 github.com/wulfland/AccelerateDevOpsgithub.com/PacktPublishing/Accelerate-DevOps-with-GitHub。如果代码或实验有更新,GitHub 仓库会随之更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,欢迎访问 github.com/PacktPublishing/。快去看看吧!

下载彩色图片

我们还提供了本书中使用的截图和图表的彩色图片 PDF 文件。你可以在这里下载:packt.link/vzP6B

使用的约定

本书中使用了许多文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“你可以通过将文件 config.yml 添加到 .github/ISSUE_TEMPLATE 来自定义对话框以选择问题模板。”

代码块如下所示:

name: 💡 Custom Issue Form
description: A custom form with different fields
body:
  - type: input
    id: contact
    attributes:
      label: Contact Details

当我们希望引起你对某一部分代码块的注意时,相关的行或项会以粗体显示:

blank_issues_enabled: true
contact_links:
  - name: 👥 Discussions
    url:  https://github.com/wulfland/AccelerateDevOps/discussions/new
    about: Please use discussions for issues that are not a bug, enhancement or feature request

任何命令行输入或输出均按如下格式书写:

$ gh secret set secret-name

粗体:表示新术语、重要词汇或你在屏幕上看到的词汇。例如,菜单或对话框中的词汇以粗体显示。示例:“打开以下仓库并通过点击仓库右上角的Fork来创建一个分支。”

提示或重要说明

如下所示。

与我们联系

我们始终欢迎读者的反馈。

一般反馈:如果你对本书的任何内容有疑问,请通过电子邮件联系我们,地址是 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:虽然我们已尽力确保内容的准确性,但难免会有错误。如果你在本书中发现错误,我们将非常感激你能向我们报告。请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果你在互联网上发现任何非法复制的我们作品的版本,我们将感激你提供其位置或网站名称。请通过电子邮件联系我们 copyright@packt.com,并附上相关链接。

如果你有兴趣成为作者:如果你在某个领域有专长,并且有意撰写或参与编写书籍,请访问 authors.packtpub.com

分享你的想法

阅读完加速 DevOps 与 GitHub后,我们非常希望听到您的想法!请点击这里直接进入该书的亚马逊评论页面并分享您的反馈。

您的评论对我们以及技术社区非常重要,它将帮助我们确保提供优质的内容。

第一部分:精益管理与协作

第一部分中,你将学习如何减少开发过程中的负担,并转向一种精益与协作的工作方式,使团队能够加速价值交付。你将学习如何使用 GitHub 高效地实现远程协作,并利用工作洞察与正确的度量指标来优化工程生产力。

本书的这一部分包括以下章节:

  • 第一章重要的度量指标

  • 第二章规划、追踪并可视化你的工作

  • 第三章团队合作与协同开发

  • 第四章异步工作——随时随地协作

  • 第五章开源与内源对软件交付性能的影响

第一章:重要的指标

实施DevOps时最困难的部分是与管理层的对话转变。管理层习惯于提出以下问题:

  • 这将花费多少?

  • 我们将从中赚取多少?

从管理角度来看,这些是合理的问题。但在 DevOps 的世界里,如果在错误的时间和错误的方式回答这些问题,它们可能会变得有害,并导致大量的前期规划。在本章中,我将向你展示一些指标,这些指标能够将与管理层的讨论从一般的工程速度和开发者生产力转向实际的努力。

我将解释如何衡量工程速度和开发者生产力,以及如何使 DevOps 加速变得可衡量。

本章将涵盖以下主题:

  • 为什么要加速?

  • 工程速度

  • 高性能公司

  • 衡量重要指标

  • SPACE)框架用于开发者生产力

  • 目标与关键成果

为什么要加速?

公司预期寿命正在迅速缩短。根据耶鲁管理学院的理查德·福斯特(Richard Foster)的说法,100 年前,标准普尔S&P500上市公司的平均寿命是 67 年。如今,它是 15 年。每两周,一家标准普尔上市公司就会退出市场,到 2027 年,预计 75%的前 500 强公司将被新公司取代。圣塔菲研究所的另一项研究(《公司死亡率》)得出结论,美国各行业公司的平均寿命约为 10 年。

为了保持竞争力,公司不仅需要解决客户的问题;他们还需要提供让客户满意的产品和服务,并且必须能够与市场互动并迅速响应需求的变化。市场时间是商业敏捷性最重要的驱动力。

软件是每个行业中每个产品和服务的核心,不仅因为数字体验已变得与(或甚至可能比)物理体验同样重要。软件涉及产品生命周期的每个部分,例如:

  • 生产:

    • 供应链管理

    • 成本优化/预测性维护/机器人技术

    • 产品个性化(单件生产)

  • 销售、售后和服务:

    • 网上商店

    • 客户服务和支持

    • 社交媒体

    • 数字助手

  • 数字产品:

    • 配套应用

    • 集成

    • 移动体验

    • 新商业模式(按需付费、租赁等)

这些只是一些例子,用来说明客户与公司之间的大多数互动是数字化的。今天你不仅仅是买一辆车——你已经通过社交媒体和新闻了解了品牌。你通过网站或与销售人员在店内购买并配置汽车,也可能是通过平板电脑屏幕查看。汽车的价格受机器人和人工智能AI)优化装配线的影响。你对汽车做的第一件事是连接你的手机。开车时,你可以听音乐、打电话或用语音回复短信。驾驶助手通过在有障碍物时为你刹车,确保你保持在车道内,帮助你保持安全;很快,汽车将会自动完成大部分驾驶任务。如果你在使用汽车或应用程序时遇到问题,你很有可能会通过应用程序或电子邮件联系售后服务,尤其是年轻一代。汽车主要是一个数字化产品。汽车内不仅有数百万行代码在运行,还有数百万行代码在支持汽车的应用程序、网站以及装配线的工作,(见图 1.1)。

图 1.1 – 软件和数据是客户体验的核心

图 1.1 – 软件和数据是客户体验的核心

好消息是,软件的更改速度远远快于硬件。为了加快市场推广速度和提高业务敏捷性,软件是关键驱动力。它比硬件组件更具灵活性,可以在几天或几周内进行更改,而不是几个月或几年。它还可以更好地与客户连接。使用你应用程序的客户比在实体店的客户更有可能参与调查。同时,硬件无法提供关于你产品使用情况的遥测数据。

要成为能够持续经营超过 10 年的公司,你的公司必须利用软件的力量,加速市场响应,并通过出色的数字体验取悦客户。

工程速度

你们公司如何衡量开发者速度?最常见的做法是通过工作量来衡量。过去有些公司使用如代码行数或代码测试覆盖率等指标,但这些显然是错误的选择,今天我没有听说过有公司还在这样做。如果你能用一行代码解决问题,或者用 100 行代码解决问题,显然一行代码更优,因为每一行代码都有维护成本。代码测试覆盖率也是一样,覆盖率本身并不说明测试质量,而糟糕的测试也会带来额外的维护成本。

注意

我尽量保持措辞对开发方法保持中立。我见过一些团队采纳了 DevOps 实践,使用了敏捷(Agile)、Scrum、规模化敏捷框架SAFe)和看板(Kanban),还有瀑布(Waterfall)。但每个系统都有自己的术语,我尽量保持中立。例如,我说需求,而不是用户故事或产品待办项,但我使用的大多数例子基于 Scrum。

衡量开发人员速度的最常见方法是通过估算需求。你将需求拆解为小项——比如用户故事——然后产品负责人分配一个业务价值。接着,开发团队对故事进行估算并分配一个努力值。无论你使用故事点、小时、天数还是其他任何数字都无所谓。这基本上是对完成需求所需努力的表示。

用努力来衡量速度

用估算的努力和业务价值来衡量速度,如果你将这些数据报告给管理层,可能会产生副作用。这里有某种观察者效应:人们会试图改善数字。对于努力和业务价值,这是容易的——你可以简单地给故事分配更大的数字。这通常是发生的情况,尤其是当你比较不同团队之间的数字时:开发人员会给故事分配更大的数字,产品负责人也会分配更大的业务价值。

虽然这种方法对衡量开发人员的速度并非最优,但如果估算是在团队和产品负责人之间的正常对话中进行的,也不会造成很大危害。但如果估算是在正常开发过程之外进行的,估算可能会变得有毒,并产生非常负面的副作用。

有毒的估算

寻找“它需要多少钱?”这个问题的答案,通常会导致在正常开发过程之外进行估算,且在实施决策之前。但我们如何估算一个复杂的特性或计划呢?

我们在软件开发中做的每一件事都是新的。如果你已经做过了,你就可以使用现有的软件,而不是重新编写。因此,即使是完全重写一个现有模块,它仍然是新的,因为它采用了新的架构或框架。任何前所未做的事情只能在有限的确定性下进行估算。这是猜测,而且复杂性越大,不确定性锥越大(见图 1.2)。

图 1.2 – 不确定性锥

图 1.2 – 不确定性锥

不确定性锥(cone of uncertainty)在项目管理中使用,其前提是项目初期,成本估算具有一定程度的不确定性,随着滚动计划的推进,这种不确定性会逐渐减少,直到项目结束时为零。x轴通常表示所用时间,但它也可以与复杂性和抽象性相关:需求越抽象和复杂,估算的不确定性就越大。

为了更好地估算复杂的特性或计划,这些内容会被拆解成更小的部分,以便更好地进行估算。你还需要在工作分解中提出解决方案架构。由于这是在正常开发流程之外进行的,并且是在前期和脱离上下文的情况下完成的,因此会产生一些不希望出现的副作用,具体如下:

  • 通常,并不是整个团队都在场。这会导致多样性减少,沟通减少,因此在解决问题时也会导致更少的创造力

  • 焦点是发现问题。你能提前发现更多问题,估算可能就越准确。特别是如果你后来将估算作为衡量绩效的标准,人们很快会学到,如果他们发现更多问题,就可以争取更多时间,因此可以对需求进行更高的估算。

  • 如果有疑问,负责估算的工程师会选择更复杂的解决方案。例如,如果他们不确定能否使用现有框架解决问题,他们可能会考虑编写自己的解决方案,以确保万无一失。

如果这些数字仅仅是被管理层用来决定是否实现某个特性,那影响不会太大。但通常情况下,需求——包括估算和解决方案架构——不会被丢弃,而是会在后续用来实现特性。在这种情况下,也会显现出一个针对问题优化而非针对解决方案的较不具创造性的解决方案。这不可避免地会导致在实现特性时缺乏创造力和突破性思维。

NoEstimates

估算并不糟糕。如果在合适的时机进行,它们可能是有价值的。如果开发团队和产品负责人讨论下一步的故事,估算可以帮助推动讨论。例如,如果团队进行计划扑克(planning poker)来估算用户故事,并且估算结果不同,这表明大家对如何实现存在不同的看法。这可能会引发有价值的讨论,可能更加富有成效,因为你可以在达成共识的情况下跳过一些故事。这一点在商业价值上也适用。如果团队不明白为什么产品负责人会分配一个非常高或非常低的数字,也可能会引发重要的讨论。也许团队已经知道如何实现成功的解决方案,或者不同的角色对问题的看法存在差异。

但是许多团队更倾向于完全不进行需求估算。这通常被称为#noestimates。特别是在高度实验性的环境中,估算往往被视为浪费时间。远程和分布式团队也常常不进行估算。他们通常会将面对面的讨论转移到关于问题和拉取请求PRs)的讨论中。这也有助于记录讨论,并帮助团队以更异步的方式工作,从而帮助跨越不同时区的差异。

在开发者速度不再是考虑因素的情况下,团队应当允许自己决定是否进行估算。这也可能随着时间的推移而变化。有些团队从中获得了价值,而有些则没有。让团队自行决定什么对他们有效,什么无效。

高级项目估算的正确方法

那么,估算更复杂的特性或项目的最佳方式是什么,以便产品负责人决定这些是否值得实施?将整个团队聚集在一起,并提出以下问题:这能在天数、周数还是月数内交付? 另一种选择是使用类比估算,并将该项目与已交付的项目进行比较。问题是:这个项目比之前交付的项目小、相等,还是更复杂?

最重要的是不要将需求细分或者已经制定解决方案架构——重要的是所有工程师的直觉。然后,让每个人为单位分配一个最小值和最大值。对于类比估算,使用相对于原始项目的百分比,并使用历史数据计算结果。

报告这一点最简单的方式是这样的:

Given the current team,
if we prioritize the initiative <initiative name>,
the team is confident to deliver the feature in between <smallest minimum> and <highest maximum>

选择最小的最小值和最大的最大值是最安全的方法,但如果悲观和乐观估算相差很大,这也可能导致数字失真。在这种情况下,平均值可能是更好的选择,如下所示:

Given the current team,
if we prioritize the initiative <initiative name>,
the team is confident to deliver the feature in between <average minimum> and <average maximum>

但是,采用平均值(算术平均数;在 Excel 中使用=AVERAGE())意味着偏差可能较大或较小,具体取决于单个估算的分布。偏差越大,你就越不能确定是否能够在规定时间内交付该功能。为了了解你的估算分布情况,你可以计算标准差(在 Excel 中使用=STDEV.P())。你可以查看最小值和最大值的偏差,也可以查看每个成员的估算。偏差越小,数值就越接近平均值。由于标准差是绝对值,因此无法与其他估算进行比较。为了获得相对数值,你可以使用=STDEV.P() / AVERAGE()(在 Excel 中)。数值越高,表示数值偏离平均值越远;数值越低,表示每个团队成员对估算的信心越大,或者整个团队对最小值和最大值的信心越大。请参见下表中的示例:

表格 1.1 –

表格 1.1 – 估算计算示例

为了表示值的偏差的不确定性,您可以为估算添加一个置信度级别。这个置信度可以是文本(例如lowmediumhigh)或者百分比级别,如下所示:

Given the current team,
if we prioritize the initiative <initiative name>,
the team is <confident level> confident to deliver the feature in <arithmetic mean>

我在这里不使用固定公式,因为这涉及到对团队的了解。如果你查看示例中的数据(表 1.1),你会发现最小值(2.7)和最大值(6.3)的平均值相差并不远。如果你观察单个团队成员,你会发现有些成员比较悲观,有些成员比较乐观。如果过去的估算能证实这一点,那么即使最小值和最大值的变异系数较高,也能让你非常有信心认为这个平均值是现实的。你的估算可能是这样的:

Given the current team,
if we prioritize the initiative fancy-new-thing,
the team is 85% confident to deliver the feature in 4.5 months"

这种估算方法并非火箭科学。它与复杂的估算和预测系统无关,比如三点估算技术(en.wikipedia.org/wiki/Three-point_estimation)、PERT 分布(en.wikipedia.org/wiki/PERT_distribution)或蒙特卡洛模拟方法(en.wikipedia.org/wiki/Monte_Carlo_method),这些方法都依赖于需求的详细拆解和任务(工作)层面的估算。其核心理念是避免提前规划和拆解需求,更依赖工程团队的直觉感知。这里的技巧只是为了给你一些关于团队收集的数据点的见解,依然只是猜测。

从开发者到工程速度

努力并不是衡量开发者速度的一个好指标,特别是当它基于估算时,在跨职能团队中,速度不仅仅取决于开发者。那么,如何从开发者速度转变为工程速度呢?

高绩效公司

拥有高工程速度的组织能够超越竞争对手并颠覆市场。那么,究竟什么是高绩效公司?

开发者速度指数

2020 年 4 月,麦肯锡发布了他们关于开发者速度指数DVI)的研究报告(Srivastava S., Trehan K., Wagle D. & Wang J. (2020))。这项研究涉及了来自 12 个行业的 440 家大型组织,分析了 13 个能力领域中的 46 个驱动因素。这些驱动因素不仅包括工程能力,还包括工作实践和组织能力,如公司文化。研究表明,DVI 排名前四分之一的公司在市场上的表现比其他公司高出四到五倍,不仅在整体业务表现上如此。排名前四分之一的公司在以下领域的得分比其他公司高出 40%到 60%:

  • 创新

  • 客户满意度

  • 品牌认知

  • 人才管理

该研究对来自 12 个行业的 440 家大型组织的 100 多位高级工程领导进行了访谈。访谈内容涵盖了 13 个能力领域中的 46 个驱动因素,分为以下三个类别:

  • 技术:架构;基础设施与云计算采用;测试;工具

  • 工作实践:工程实践;安全性与合规性;开源采用,敏捷团队实践

  • 组织赋能:团队特征;产品管理;组织敏捷性;文化;人才管理

因此,DVI 远超单纯的开发者速度。它分析了工程速度以及所有影响工程速度的因素,并将其与业务成果相关联,如收入、股东回报、运营利润率,以及诸如创新、客户满意度和品牌认知等非财务绩效指标。

DevOps 现状

这些发现与DevOps 研究与评估DORADevOps 现状报告(https://www.devops-research.com/research.html#reports)中的结果一致,但通过加入业务成果,将其提升到了一个新层次。DevOps 报告 2019指出,精英公司与低效公司之间的比较(Forsgren N., Smith D., Humble J. & Frazelle J. (2019)),如以下所述:

  • 更快的价值交付:它们的交付时间LT)比普通公司快 106 倍,从提交到部署。

  • 更高的稳定性与质量:它们从事故中恢复的速度比普通公司快 2,604 倍,并且变更失败率CFR)低 7 倍。

  • 更高的吞吐量:它们的代码部署频率是普通公司的 208 倍。

高绩效公司不仅在吞吐量和稳定性上表现卓越,而且在创新、客户满意度和业务表现上也更为突出(见图 1.3)。

图 1.3 – 高绩效公司

图 1.3 – 高绩效公司

专注于那些突显高绩效公司与中低绩效公司之间差异的能力的指标,你可以使你的转型更加可视化,并为管理层提供更具意义的指标,希望这些指标比代码行数或基于估算的开发速度更能打动他们。

衡量重要指标

“成功变革的关键是衡量和理解正确的事物,聚焦于能力。”

– Forsgren. N., Humble, J. & Kim, G. (2018) 第 38 页

为了衡量你在转型旅程中的进展,最好专注于 DORA 使用的四个指标——两个用于绩效,两个用于稳定性,如下所示:

  • 交付绩效指标:

    • 交付周期时间

    • 部署频率

  • 稳定性指标:

    • 平均恢复时间

    • 变更失败率

交付周期时间

交付周期时间(DLT)是指从工程师开始工作一个特性到特性可供最终用户使用的时间。你可以说是从代码提交到生产——但通常情况下,你会从团队开始处理需求并将其状态更改为进行中或类似状态时开始计时。

要想将这个指标自动化并从系统中获取并不容易。我将在第七章《运行工作流》中展示如何结合使用 GitHub Actions 和 Projects 来自动化该指标。如果你无法从系统中提取该指标,你可以设置一个包含以下选项的调查:

  • 少于 1 小时

  • 少于 1 天

  • 少于 1 周

  • 少于 1 个月

  • 少于 6 个月

  • 超过 6 个月

根据你在这个尺度上的位置,你进行调查的频率会有所不同。当然,系统生成的值更为理想,但如果你处于该尺度的较高位置(几个月),这就无关紧要了。如果你测量的是小时或天数,情况就会更加有趣。

为什么不是领先时间(Lead Time)?

精益管理的角度来看,LT 将是更好的指标:从客户反馈中获得的学习信息要流经整个系统需要多长时间?但软件工程中的需求通常很复杂。通常,在实际工程工作开始之前需要经历很多步骤。结果可能会有很大的变化,如果必须依赖调查数据,指标也很难预测。一些需求可能在队列中停留几个月——而一些需求则可能只停留几小时。从工程的角度来看,集中关注 DLT 会更好。你将在第十八章《精益产品开发与精益创业》中了解更多关于 LT 的内容。

部署频率

部署频率专注于速度。你交付更改需要多长时间?一个更加注重吞吐量的指标是 DF。你多久将更改部署到生产环境?DF 表示你的批量大小。在精益生产中,减少批量大小是非常理想的。较高的 DF 则表示较小的批量大小。

初看起来,似乎很容易在系统中衡量 DF。但仔细想想,你的部署中有多少最终能到达生产环境呢?在第七章《运行工作流》中,我将解释如何使用 GitHub Actions 来捕获该指标。

如果你还无法衡量该指标,你也可以使用调查。使用以下选项:

  • 按需(每天多次)

  • 每小时一次到每日一次之间

  • 每日一次到每周一次之间

  • 每周一次到每月一次之间

  • 每月一次到每 6 个月一次之间

  • 每 6 个月一次以下

平均恢复时间

衡量稳定性的一个重要指标是恢复的平均时间(MTTR)。它衡量的是在发生故障时,恢复你的产品或服务所需的时间。如果你衡量的是正常运行时间(uptime),那基本上是指服务不可用的时间段。为了衡量你的正常运行时间,可以使用冒烟测试——例如在 Application Insights 中(见docs.microsoft.com/en-us/azure/azure-monitor/app/monitor-web-app-availability)。如果你的应用程序安装在客户端机器上且无法访问,则会更复杂。通常,你可以回退到帮助台系统中特定票种的时间。

如果你根本无法衡量它,你仍然可以回退到一个调查,提供以下选项:

  • 少于 1 小时

  • 少于 1 天

  • 少于 1 周

  • 少于 1 个月

  • 少于 6 个月

  • 超过 6 个月

但这应该仅作为最后的手段。MTTR 应该是你能轻松从系统中获取的一个指标。

变更失败率

与性能的 DLT 类似,MTTR 是衡量稳定性的时间指标。专注于吞吐量的 DF 的对应指标是变更失败率(CFR)。对于问题你有多少次部署会导致生产环境的失败?,CFR 以百分比形式给出。为了确定哪些部署应该计入该指标,您应该使用与 DF 相同的定义。

四大关键仪表板

基于 DORA 研究的这四个指标是衡量你在 DevOps 之旅中所处位置的绝佳方式。它们是改变与管理层对话的一个良好起点。把它们放在仪表板上,并为之骄傲。不要担心如果你还不是精英表现者——重要的是在旅程中,并不断改进。

从基于调查的数值开始非常简单。但如果你想使用自动生成的系统数据,可以使用四大关键项目将数据展示在一个漂亮的仪表板上,(见图 1.4)。

图 1.4 – 四大关键仪表板

图 1.4 – 四大关键仪表板

该项目是开源的,并基于 Google Cloud(见github.com/GoogleCloudPlatform/fourkeys),但它依赖于 webhook 从你的工具中获取数据。你将在第七章运行你的工作流中学习如何使用 webhook 将数据发送到仪表板。

你不应该做的事

这些指标不应被用来进行团队之间的比较。你可以将它们汇总以获得组织的概况,但不要比较单个团队!每个团队的情况不同。重要的是这些指标朝着正确的方向发展。

此外,指标不应成为目标。仅仅获得更好的指标并不可取。始终应专注于导致这些指标的能力,在本书中我们讨论的正是这些能力。专注于这些能力,指标会随之而来。

开发者生产力的 SPACE 框架

DORA 指标是一个完美的起点。它们易于实施,并且有大量数据可供比较。如果您想进一步添加更多指标,可以使用开发者生产力的 SPACE 框架(Forsgren N., Storey M.A., Maddila C., Zimmermann T., Houck B. & Butler J. (2021))。

开发者生产力是实现高工程速度和高 DVI 的关键要素。开发者生产力与开发者的整体幸福感和满意度高度相关,因此是在人才争夺战中蓬勃发展和吸引优秀工程师的最重要因素之一。

但是,开发者生产力不仅仅关乎活动。相反,情况通常相反:在紧急情况和截止日期逼近时,活动通常很高,但通过频繁的任务切换和较少的创造力导致生产力下降。因此,衡量开发者生产力的指标永远不应孤立使用,也不应用于惩罚或奖励开发者。

此外,开发者的生产力不仅仅关乎个人表现。就像团队运动一样,个人表现很重要,但只有整个团队才能获胜。平衡个人和团队表现的度量是至关重要的。

SPACE 是一个多维框架,将开发者生产力的指标分类为以下几个维度:

  • Satisfaction and well-being

  • Performance

  • Activity

  • Communication and collaboration

  • Efficiency and flow

所有这些维度都适用于个人、团队和整个系统。

满意度和幸福感

满意度和幸福感关乎我们有多快乐和满足。身体和心理健康也属于这一维度。这里给出了一些示例指标:

  • 开发者的满意度

  • 净推荐值NPS)用于团队(衡量某人会向他人推荐他们的团队的可能性)

  • 保留率

  • 对工程系统的满意度

Performance

Performance 是系统或过程的结果。个体开发者的表现难以衡量。但对于团队或系统级别,我们可以使用 LT、DLT 或 MTTR 等度量标准。其他例子包括正常运行时间或服务健康状态。其他好的指标包括客户满意度或产品的 NPS(衡量某人会向他人推荐产品的可能性)。

Activity

活动可以为生产力提供有价值的见解,但正确衡量它很难。个人活动的一个良好指标是专注时间:开发者在多少时间内不参加会议和沟通?其他指标的例子包括完成的工作项数、问题、PR、提交或错误。

沟通与协作

沟通与协作是开发人员生产力的关键因素。衡量它们很难,但查看 PR 和问题可以让你对沟通情况有一个很好的了解。这个维度的指标应该聚焦于 PR 的参与度、会议的质量以及知识共享。此外,跨团队的代码审查(跨团队X-team)是一个很好的衡量标准,帮助你了解团队之间的边界。

效率与流动性

效率和流动性衡量多少次交接和延迟增加了整体的 LT。好的指标包括交接次数、被阻塞的工作项和中断。对于工作项,你可以衡量总时间、增值时间和等待时间。

如何使用 SPACE 框架

“间接地了解一个组织中重要事物的一种方式是查看被衡量的内容,因为它通常传达了被重视的事物,并影响人们的行为和反应方式。”

– Forsgren N., Storey M.A., Maddila C., Zimmermann T., Houck B. & Butler J. (2021) 第 18 页

所有维度都适用于个人、团队、群体和系统层面,(见图 1.5)。

图 1.5 – SPACE 指标的示例

重要的是不仅要看维度,还要看范围。一些指标在多个维度中有效。

选择哪些指标被衡量也非常重要。指标会塑造行为,而某些指标可能会产生你最初没有考虑到的副作用。目标是使用少数几个指标,但要确保它们有最大化的正面影响。

你应该从三个维度中选择至少三个指标。你可以混合使用个人、团队和系统范围的指标。要小心个人指标——它们可能产生最难以预见的副作用。

为了尊重开发人员的隐私,数据应该进行匿名化处理,并且你只应该在团队或小组层面报告汇总结果。

目标与关键结果

许多实践 DevOps 的公司正在使用目标与关键结果(OKRs)——其中包括谷歌、微软、推特和优步。

OKR 是一个灵活的框架,帮助公司定义并跟踪目标及其成果。

OKR 方法可以追溯到 1970 年代,当时 OKR 的创始人安德鲁·格罗夫将这种方法引入英特尔。这个方法叫做iMBO,即英特尔目标管理法。他在他的书《高效能管理》中描述了这一方法(Grove, A. S. (1983))。

1999 年,约翰·多尔将 OKR 引入谷歌。在安德鲁·格罗夫将 iMBO 引入英特尔时,约翰·多尔也在那里工作。OKR 很快成为谷歌文化的核心部分。约翰·多尔出版了他的书《度量重要的事》(Doerr, J. (2018)),使 OKR 变得广为人知。如果你想深入了解 OKR,我强烈推荐阅读这本书。

什么是 OKRs?

OKR 是一种框架,帮助组织在实现战略目标时,保持团队和个人的最大自主权,同时实现高度的目标一致性。目标是具有方向性的定性目标,能够激励和鼓舞人心。每个目标都与明确可衡量的定量指标——关键结果相关联。关键结果应侧重于结果而非活动,具体示例如下表所示:

表 1.2 – OKR 的特征

表 1.2 – OKR 的特征

OKR 绝不应与公司的绩效管理体系或员工奖金挂钩!目标不是要实现 100%的成功率——如果达到 100%成功率,意味着 OKR 的挑战性不足。

OKR 的写作格式如下:

We will [objective]
As measured by [set of key results]

重要的是,OKR 应专注于结果而不是活动。一个好的例子是谷歌首席执行官CEO)桑达尔·皮查伊在 2008 年设定的一个目标,当时谷歌推出了 Chrome 浏览器。这个 OKR 是:

We will build the best browser
As measured by 20 million users by the end of 2008

这个目标对于一个新浏览器来说非常大胆,谷歌在 2008 年未能实现这个目标,用户不到 1000 万。到了 2009 年,关键结果被提高到 5000 万用户,但谷歌依然未能实现这个目标,用户大约为 3700 万。但谷歌并没有放弃,2010 年关键结果再次被提高——这次是 1 亿用户!这一次,谷歌超额完成了目标,用户数量达到了 1.11 亿!

OKR 是如何运作的?

为了使 OKR 发挥作用,公司需要有一个清晰的愿景和使命,以定义为什么我们为什么要为这家公司工作? 然后,愿景被分解为中期目标(称为MOALS)。这些 MOALS 本身也是 OKR,并被细分为每个 OKR 周期的 OKR,通常周期为 3 到 4 个月。在 OKR 规划和对齐过程中,OKR 会在组织中逐层分解,以确保每个个人和团队都有自己的 OKR,并且这些 OKR 最终为更大的目标做出贡献。然后,OKR 会持续被监控,通常是每周一次。在 OKR 周期结束时,OKR 会进行复盘,成果(希望是)被庆祝。根据这一周期的学习,MOALS 会得到更新,并开始新的周期(见图 1.6)。

图 1.6 – OKR 周期

图 1.6 – OKR 周期

理论上的 OKR 很简单,但实施起来并非如此。写出好的 OKR 尤其困难,需要大量的实践。此外,它还强烈依赖于公司文化和现有的衡量指标和关键绩效指标KPI)。

OKR 与 DevOps

一旦正确实施,OKR 可以让你在保持团队自主决策权的同时,实现团队之间的强大协作对齐,决定他们要构建什么,而不仅仅是如何构建它,(见 图 1.7)。这在我们讨论第十九章在 GitHub 上的实验和 A/B 测试时非常重要。你的团队可以定义自己的实验并衡量结果。基于此,他们决定哪些代码应该保留在项目中,哪些不应该。

图 1.7 – OKR 有助于实现自主性和一致性

图 1.7 – OKR 有助于实现自主性和一致性

现在让我们来看一个例子。

你公司的愿景是成为在线视觉项目管理工具的市场领导者。你的产品当前的市场份额为 12%。公司的 MOAL 是如下所示:

We will build the best visual project management tool
As measured by a 75% market share by the end of 2025

你的产品由两个团队共同开发:一个团队专注于产品的核心部分,负责项目管理的视觉设计。他们关注现有客户,并致力于构建客户喜爱的产品。他们一致达成以下 OKR:

We will build the visual project management tool that is loved by our customers
As measured by an NPS of higher than 9

当前的 NPS(净推荐值)为 7.9,因此团队必须自行找到方法让客户感到满意。经过与一些客户的访谈后,他们提出假设:所有的项目管理工具都基于较旧的项目管理技术,在更注重敏捷的项目世界中显得过于复杂。他们决定对一部分客户进行实验,提出一种全新的可视化项目的概念,以验证或削弱这个假设。

第二个团队是共享服务团队。他们专注于用户管理、企业集成和账单。为了实现公司的 MOAL(最优化客户获取率),产品不仅要让现有客户感到满意,还需要吸引更多新用户。因此,在本次 OKR 周期中的重点是吸引新客户,如下所示:

We will build a project management tool that is easy to use for new customers
As measured by a 20% increased monthly new registered users

目前,新注册的用户数量已经趋于平稳,因此目标是重新开始增长。团队分析数据后发现,很多新客户在填写地址和银行信息的详细页面时中途放弃了注册过程。他们假设,如果注册流程更简化,更多客户可能会尝试该产品,并有可能留在平台上。他们决定进行实验,将注册流程简化到仅需进行身份验证的最基本内容。他们为新用户提供 30 天的免费试用期,并在此期间过后才要求提供付款信息。

我将在第十八章精益产品开发与精益创业,以及第十九章在 GitHub 上的实验和 A/B 测试中,讲解如何进行假设驱动的开发和实验。这些内容与 OKR 独立,但它们可以非常好地协同工作。

如果你对现实世界中的 OKR 感兴趣,GitLab 会公开分享他们的 OKR(about.gitlab.com/company/okrs/)。他们还分享了整个过程,以及他们如何将 OKR 与史诗和问题关联起来。

OKR 不是 DevOps 的前提条件。但与敏捷实践一样,它们是天生匹配的。如果你没有以敏捷的方式工作并开始进行 DevOps,你的工作方式最终会变得敏捷,而且你可以通过使用 Scrum 等框架来避免重新发明轮子。OKR 也是如此:当你在大规模组织中扩展 DevOps 时,它们会自然而然地出现,且你希望通过保持与全球目标的对齐,为团队提供很大的自主权。

总结

在这一章中,我解释了软件如何接管世界,它对公司寿命的影响,以及如果公司希望保持经营,必须加速软件交付。这有助于你通过使工程速度可见来改变与管理团队的对话。

衡量对公司重要的指标,并专注于能力。首先从 DORA 的四个关键指标开始,再从 SPACE 框架的不同维度中添加更多指标。但请记住,指标会塑造行为,因此要小心选择哪些指标。

通过选择正确的指标,你使得 DevOps 转型和加速变得可衡量和透明。

本章的大部分内容集中在效率上:做对的事情。只有 OKR 也涉及效果:做正确的事情。OKR 对于精益产品开发也很相关,且在第十八章《精益产品开发与精益创业》中有涉及。

在下一章中,你将学习如何规划、追踪和可视化你的工作。

案例研究

Tailwind Gears是一家制造公司,生产许多不同的零件,这些零件被集成到其他产品中。他们有五个以产品为中心的部门,总共有 600 多名开发人员。每个部门都有自己的开发流程。有些使用 Scrum,有些使用 SAFe,还有些使用经典的瀑布方法(验证模型V 模型)。五个部门中的两个部门负责构建包含关键系统中使用的软件的组件,因此这些部门的产品高度受监管(国际标准化组织ISO26262通用良好实践GxP))。这些软件所使用的编程语言包括嵌入式 C 和 C++代码(用于硬件和芯片)、移动应用(Java;Swift)以及 Web 应用(JavaScript;.NET)。

与开发流程一样,工具的生态非常异构。有些团队仍然使用本地部署的老旧Team Foundation ServerTFS);有些团队使用 Jira、Confluence 和 Bitbucket,还有些团队使用 GitHub 和 Jenkins。有些团队已经实施了持续集成/持续部署CI/CD)实践,而其他团队仍然手动构建、打包和部署。有些团队已经以 DevOps 方式工作并运营自己的产品,而其他团队则仍然将生产版本交给单独的运维团队。

Tailwind Gears 面临以下问题:

  • 管理层无法看到开发进展情况。由于各团队的工作方式不同,因此没有统一的方式来衡量开发速度。

  • 各个部门报告发布周期缓慢(从几个月到几年不等)且失败率高

  • 每个部门都有自己的团队来支持其工具链,因此存在很多冗余。例如,模板和流水线没有共享。

  • 很难将开发者和团队分配到最具商业价值的产品上。工具链和开发实践差异太大,且入职时间过长。

  • 开发者对自己的工作不满意不高效。一些开发者已经离开公司,且在市场上招聘新人才变得困难。

为了解决这些问题,公司决定实施一个统一的工程平台,并计划统一开发流程。这项倡议的目标如下:

  • 加速各部门的软件交付。

  • 提高软件质量并降低失败率。

  • 通过提升协同效应并只设立一个平台团队负责一个工程系统,节省时间和成本

  • 通过将开发者和团队分配给价值更高的产品,提升软件的价值

  • 提高开发者满意度,以留住现有人才,并使招聘新开发者变得更容易。

为了让转型成果可见,公司决定衡量以下四个 DORA 关键指标:

  • DLT

  • DF

  • MTTR

  • CFR

由于目前还没有统一的平台,指标将通过调查问卷收集。计划是逐步将一个团队接入新的统一平台,并在那里使用系统指标。

开发者满意度是转型中的一个重要部分。因此,还将增加两个新的指标,具体如下:

  • 开发者满意度

  • 对工程系统的满意度

这是至少三个 SPACE 维度的六个指标的组合。当前还没有针对沟通与协作的指标,未来将随着转型进展加入到系统中。

进一步阅读

以下是本章中的一些参考资料,你可以用来获取更多关于这些话题的信息:

第二章:计划、跟踪和可视化你的工作

在上一章中,你学习了如何衡量工程速度和绩效,以使你的加速过程可视化,并改变与管理层的对话方式。

本章将重点介绍如何在团队内部组织工作并应用精益原则。你将学习如何利用 GitHub 的问题和项目来简化工作流。

本章将涉及以下主题:

  • 工作就是工作

  • 未计划的工作和返工

  • 可视化你的工作

  • 限制 WIP(工作进行中的数量)

  • GitHub 问题、标签和里程碑

  • GitHub 项目

工作就是工作

工作是一种为了实现某个目标或结果而进行的活动。这不仅包括你正在进行的产品或项目,还包括你为公司必须执行的所有活动。在我合作的一些团队中,有些人将多达 50% 的工作时间花费在他们项目/产品团队之外的任务上。有些人是团队负责人,需要与组织团队成员开会和承担责任。有些人是工作委员会的成员。有些人接受个人发展路径的培训。有些人则必须修复他们以前参与的项目中的 bug 和现场问题。

这些任务中的许多不能从团队成员那里去除。团队成员可能喜欢这些任务,也可能不喜欢——但它们通常是他们个人发展的一部分。

这种工作的问题在于,任务的优先级和协调工作是由个人决定的,而且是在他们的团队上下文之外进行的。谁来决定是否应该优先处理开发者在以前系统中处理的一个 bug,而不是当前项目中的 bug?通常,个人会自己进行计划和优先排序。这往往导致前期更多的规划。当团队成员在冲刺开始时报告他们的可用时间时,团队开始围绕这些事件来规划当前的任务。这可能会阻止整个团队建立拉取机制,迫使他们去规划依赖任务并将任务分配给单个团队成员(推动)。

为了解决这个问题,你应该让团队看到所有的工作,并将其添加到团队的待办事项列表中。你是否在工作委员会中?将其添加到待办事项列表中。你是否有培训?将其添加到待办事项列表中。

所以,第一步是弄清楚你的团队执行的是什么类型的工作并将所有任务汇集到一个待办事项列表中。

第二步是简化。每个人都能让事情变得更加复杂——但要让事情变得简单则需要一点天才。这就是为什么大多数公司中的流程和表单会随着时间的推移变得越来越复杂。我见过有 300 个字段的表单以及基于这些字段的复杂路由规则——只是为了处理现场故障。不要将这种复杂性转移到你的待办事项中。无论背景中的流程如何——工作对你的团队有明确的触发条件,经过团队处理后,便不再是你的责任——所以从你的角度看,它已经完成。一个流程或工单可能会导致你待办事项中的多个小工作项。每个工作项应该简化为待办进行中完成

注意

第十八章《精益产品开发与精益创业》中,我们将更多地关注价值流、约束理论,以及如何优化工作流。在本章中,我们将重点关注团队层面,以及如何开始优化跨团队边界的工作。

未计划的工作和返工

所有开发者都知道频繁的上下文切换会导致生产力降低。如果我们在编码时被打扰,我们需要一些时间才能重新进入代码,并恢复到打扰发生时的生产力。因此,处理多个项目或任务也会降低生产力。在他的著作《质量软件管理:系统思维》中,Gerald M. Weinberg 呈现了一项研究结果,得出结论:当同时只处理两个项目时,性能会下降约 20%(Weinberg G.M. 1991)。每增加一个项目,性能会进一步下降 20%(见图 2.1):

图 2.1 – 上下文切换导致的生产力损失

图 2.1 – 上下文切换导致的生产力损失

2017 年的另一项研究表明,从事两三个项目的开发者平均有 17%的时间花费在上下文切换上(Tregubov A., Rodchenko N., Boehm B., & Lane J.A., 2017)。我认为实际的百分比可能因产品和团队而异。开发者在小批量工作时,比起从事大批量工作的人更容易进行上下文切换。事情越复杂,恢复工作时所需的精力就越大。测试驱动开发TDD)等实践有助于在上下文切换后更容易恢复工作。

但不管实际百分比如何:上下文切换会降低生产力,开发者在一项任务上花费更多的集中时间,效率会更高。这意味着你应该减少团队的进行中的工作WIP)——尤其是未计划的工作和返工。

为了帮助你日后优化,你应该从一开始就正确标记你的工作项。未计划的工作可能来自项目内部或外部。返工可能会发生,例如存在 bug、技术债务或误解的需求。确保从一开始就使用正确的标签,这样你可以在以后分析你的工作。这不需要是一个复杂的治理框架——只需选择一些有助于你日后优化工作的标签即可。表 2.1只是一个示例,展示了你可以如何分类你的工作项:

表 2.1 – 工作项的示例分类法

表 2.1 – 工作项的示例分类法

保持简单,选择简单明了且团队能够理解的分类。

可视化你的工作

为了专注于重要工作并减少多任务处理和任务切换,你应该将工作可视化——通常以看板的形式呈现。看板起源于精益生产,但现在已被认为是精益软件开发的重要组成部分。看板可以帮助你提高工作流在系统中的效率。

可视化将帮助你完成以下任务:

  • 识别瓶颈、等待时间和交接点。

  • 优先处理工作并首先处理最重要的任务。

  • 将工作分解为小批次。

  • 完成工作。

建立拉动机制

没有完美的计划。如果你曾经规划过项目,你会知道项目计划只有在有大量缓冲时间的情况下才会有效——而且你总是需要调整计划。因此,即使你只为接下来的 2 到 3 周规划工作,规划也会导致等待时间和上下文切换。解决方案是停止规划并建立拉动系统;团队成员从队列中拉取最高优先级的工作并开始执行。理想情况下,任务会被完成并移至已完成(见图 2.2):

图 2.2 – 从待办事项中拉取工作以表示状态变化

图 2.2 – 从待办事项中拉取工作以表示状态变化

如果你的任务无法仅通过你一个人完成,这可能表明任务过大,需要拆分为更小的任务。如果你必须同时处理多个任务才能完成任何工作,任务可能太小。随着时间的推移,视觉表示会帮助你识别瓶颈和等待时间,从而进行调整。

优先排序

使用可视化看板的好处在于,你可以轻松地优先处理你的工作。只需将优先级最高的工作项移到顶部。如果你的看板上有不同种类的工作,你可能需要额外的视觉分隔。这可以通过泳道来实现。泳道是在看板上对工作进行的水平分组(见图 2.3):

图 2.3 – 使用泳道在看板上组织工作

图 2.3 – 使用泳道在看板上组织工作

如果你的团队需要处理现场问题,你可能需要一个优先级泳道,以向所有团队成员表示当前的问题比正常工作更紧急。或者,如果团队成员有团队外的责任,你也希望将这些责任与正常工作区分开来。

许多看板还允许你为每张卡片设置不同的颜色——通常是通过给卡片应用标签或标记。这也可以帮助你在视觉上区分看板上不同种类的工作。特别是在与泳道结合使用时,彩色卡片可以帮助你一眼看出团队的进展情况,以及哪些任务是需要关注的最重要事项。

保持简洁!

从简单开始,使用三列(待办、进行中、已完成),根据需要添加更多列和泳道来优化团队的工作流。但要小心保持简洁!在每次自定义之前问问自己:这有必要吗?这带来价值吗?有没有更简单的方法?

复杂的事物有粘性——在使用看板时,我看到团队将看板发展成了一个怪物,拥有 10 列、8 条泳道(大部分时间都折叠着)以及卡片上有许多字段和信息。

看板的核心是简化——尽量保持尽可能简单!

限制 WIP

看板的目标之一是限制 WIP。通过减少 WIP,你可以减少上下文切换,增加专注力。这有助于你完成任务!停止开始,开始完成!

即使是在指导 Scrum 团队时,我也看到过团队在冲刺的前几天就开始处理他们计划中的所有用户故事。每当开发人员被阻塞时,他们就会开始处理另一个故事。到冲刺结束时,所有的故事都处理过了,但没有一个是完成的。

在看板中,你处理的是少量任务——并且保持恒定的节奏。

设置 WIP 限制

大多数看板支持 WIP 限制。WIP 限制是指你希望在某一列中同时处理的最大任务数量。假设“正在进行”列的 WIP 限制为五个,而你正在处理三个任务。该列会显示 3/5——通常是绿色的,因为还没有达到限制。如果你开始处理另外三个任务,那么它会显示 6/5,变为红色,因为已达到限制。

WIP 限制可以帮助你专注于少量任务,避免开始过多的工作。从小任务开始,只有在绝对必要时才增加任务量。一个好的默认值是五个。

减小批量大小

限制 WIP 可以很好地指示你的工作项是否大小合适。如果很难保持在 WIP 限制内,那么你的工作项可能仍然太大。尝试在增加限制之前将任务拆分为更小的任务。

减少交接

对于交接也是如此。如果你的工作项需要许多团队成员的输入——或者更糟,来自团队外部的输入——就会产生等待时间,降低你的 流程效率。流程效率是你在工作项上所花的时间除以完成它所需的总时间——包括等待时间:

流程效率是软件工程中一个非常理论化的度量指标,因为通常你不会精确测量工作和等待时间。但是,如果你经历了许多交接和阻塞项,该指标可能有助于查看工作在系统中的流转情况。你可以在将工作项移动到“进行中”时启动工作计时器,在将其移回时启动等待计时器。

GitHub 问题、标签和里程碑

GitHub 问题让你能够跟踪任务、增强功能和错误。它们具有高度的协作性,并且有一个显示历史记录的时间轴。问题可以与提交、拉取请求和其他问题关联。GitHub 问题是开发者在 GitHub 上喜爱的体验的一部分。这也是为什么它们是管理工程团队工作的一种良好解决方案。

创建一个新问题

你可以在你的仓库下的 Issues | New Issue 创建一个新问题。问题包括一个标题和一个支持 Markdown 的正文(参见 图 2.4):

图 2.4 – 创建一个新问题

图 2.4 – 创建一个新问题

一个工具栏可以帮助你格式化文本。除了常规格式化(如标题、粗体和斜体文本、列表、链接和图片)外,它还有一些值得注意的功能:

  • :+1: (👍) 和 :100: (💯),以及典型的 GitHub 样式的 :shipit: 松鼠。你可以在这里找到完整的列表:gist.github.com/rxaviers/7360908#file-gistfile1-md

  • 提及:你可以通过 GitHub 用户名提及单个成员,或者提及整个团队。只需按下 @ 并开始输入。然后从列表中选择成员或团队。他们会收到通知,提及会显示为指向该成员或团队个人资料的链接。

  • 引用:通过按下 # 键并从列表中选择项目,可以引用其他问题、拉取请求或讨论。

  • – [ ]。如果任务完成,中心会加上x- [x]

  • ``` 用于打开和关闭代码块。语法高亮由 Linguist(github.com/github/linguist)处理,并且支持大多数编程语言。

    Markdown

    Markdown 是一种非常流行的轻量级标记语言。与 JSON 或 HTML 不同,它基于单行格式化文本,并且没有开闭标签或括号。这就是为什么它非常适合使用 Git 进行版本管理,并通过拉取请求协作进行更改。这也是为什么 YAML 成为机器可读文件的事实标准的原因。Markdown 是人类可读文件的等价物。在 DevOps 团队中,一切都是代码:图表、架构、设计和概念文档、配置文件以及基础设施。这意味着 YAML、Markdown 或两者的混合都在使用。

    如果你还没有学习 Markdown,现在是时候开始了。许多团队在拉取请求中广泛使用 Markdown 来协作处理可读的内容。由于大多数工作管理解决方案也支持 Markdown,它几乎无处不在。

    Markdown 语法非常简单,容易学习。使用几次之后,它应该不会成为负担。

你可以随时切换到预览模式,查看你的 Markdown 输出(见 图 2.5):

图 2.5 – 在新问题中预览 Markdown

图 2.5 – 在新问题中预览 Markdown

在 GitHub 上有一个很好的 Markdown 入门教程,您可以在这里找到:guides.github.com/features/mastering-markdown/

提示:

如果你经常使用相似的文本块,还可以保存回复。按 Ctrl + .(Windows/Linux)或 Cmd + .(Mac),然后从列表中选择回复,或者创建一个新的保存回复。欲了解更多信息,请参见 docs.github.com/en/github/writing-on-github/working-with-saved-replies

协作处理问题

一旦问题被创建,你可以随时添加评论。你可以将最多 10 个人指派给该问题,并为其应用标签进行分类。所有的变更都会作为事件显示在问题的历史记录中(见 图 2.6):

图 2.6 – 编辑问题

图 2.6 – 编辑问题

如果你的问题包含任务列表,它用于显示问题的进展。你可以将每个任务转换为一个独立的问题,然后将其链接到当前的问题。如果你点击 打开转换为问题 按钮(请注意 图 2.6 中鼠标悬停时的显示),该任务就会被转换成一个新问题,并显示为链接。如果你点击该链接并打开问题,你会看到该问题在另一个问题中被追踪(见 图 2.7):

图 2.7 – 创建问题的层级结构

图 2.7 – 创建问题的层级结构

通过这种方式,你可以创建灵活的工作层级结构,并将工作拆分成更小的任务。

问题待办事项

问题概览并不是一个真正的待办事项列表,因为它不能通过拖放轻松排序。但它有一个非常先进的语法用于过滤和排序。每个你应用的过滤器都会作为文本添加到搜索框中(见 图 2.8):

图 2.8 – 筛选和排序问题列表

图 2.8 – 筛选和排序问题列表

在概述中,你可以看到任务的进展和标签。你还可以看到与问题关联的拉取请求。

里程碑

里程碑是一种将问题分组的方法。一个问题只能被分配到一个里程碑。里程碑通过已关闭的问题数量与总问题数量的比例来衡量进展。里程碑有一个标题、一个可选的截止日期和一个可选的描述(见图 2.9):

图 2.9 – 使用里程碑规划问题

图 2.9 – 使用里程碑规划问题

里程碑是一种将问题分组的方法,以便发布具有特定目标日期的版本。它们还可以用于将不属于任何发布版本的问题归为一类。

固定问题

你最多可以将三个问题固定到你的代码库。这些问题将在问题概览的顶部显示(见图 2.10):

图 2.10 – 固定的问题

图 2.10 – 固定的问题

固定问题是与其他贡献者或新团队成员沟通重要事项的好方法。

问题模板

你可以配置不同的模板来为问题提供预定义内容。如果用户创建一个新问题,他们可以从列表中选择模板(见图 2.11):

图 2.11 – 问题模板

图 2.11 – 问题模板

你可以激活.github/ISSUE_TEMPLATE。点击提议更改并将文件提交到你的代码库。一旦模板文件进入你的代码库,你可以直接在其中编辑或删除它们。你也可以添加新的模板文件。无需通过设置来进行这项操作(添加新的模板文件)。

模板可以是 Markdown(.md)文件或 YAML(.yml)文件。Markdown 文件包含一个标题,指定名称和描述。它还可以为标题、标签和指派人设置默认值。以下是一个 Markdown 模板的示例:

---
name: 🐞 Bug report
about: Create a report to help us improve
title: ‘[Bug]:’
labels: [bug, unplanned]
assignees: 
  - wulfland
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
...

如果你点击问题 | 新建问题,你可以选择模板并点击开始。新的问题将会填充模板中的值。结果如图 2.12所示:

图 2.12 – 一个 Markdown 问题模板

图 2.12 – 一个 Markdown 问题模板

使用 YAML 模板,你可以定义带有文本框、下拉框和复选框的完整表单。你可以配置控件并将字段标记为必填。一个示例表单可以这样定义:

name: 💡 Custom Issue Form
description: A custom form with different fields
body:
  - type: input
    id: contact
    attributes:
      label: Contact Details
      description: How can we get in touch with you if we need more info?
      placeholder: ex. email@example.com
    validations:
      required: false
  - type: textarea
    id: what-happened
    attributes:
      label: What happened?
      description: Also tell us, what did you expect to happen?
      placeholder: Tell us what you see!
      value: “Tell us what you think”
    validations:
      required: true
  - type: dropdown
    id: version
    attributes:
      label: Version
      description: What version of our software are you running?
      options:
        - 1.0.2 (Default)
        - 1.0.3 (Edge)
    validations:
      required: true
  - type: dropdown
    id: browsers
    attributes:
      label: What browsers are you seeing the problem on?
      multiple: true
      options:
        - Firefox
        - Chrome
        - Safari
        - Microsoft Edge
  - type: checkboxes
    id: terms
    attributes:
      label: Code of Conduct
      description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com)
      options:
        - label: I agree to follow this project’s Code of Conduct
          required: true

结果如图 2.13所示:

图 2.13 – 一个 YAML 问题模板

图 2.13 – 一个 YAML 问题模板

你可以在这里找到更多关于YAML 问题模板的信息:docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms

您可以通过向.github/ISSUE_TEMPLATE添加config.yml文件来自定义对话框以选择 issue 模板。您可以设置是否支持空白 issue 并添加额外的行:

blank_issues_enabled: true
contact_links:
  - name: 👥 Discussions
    url:  https://github.com/wulfland/AccelerateDevOps/discussions/new
    about: Please use discussions for issues that are not a bug, enhancement or feature request

结果如图 2.11所示——附加链接显示为打开按钮。

注意

在撰写本书时,YAML issue 模板仍处于测试阶段,因此可能会发生变化。

GitHub 项目

GitHub issues 是协作的绝佳方式——但由于它们仅限于仓库范围,并且缺乏拖放式待办事项和可视化的看板,因此它们并不是可视化和跟踪工作任务的完美场所。

在 GitHub 中,管理不同仓库间工作任务的中心枢纽是GitHub Projects。它基于 GitHub issues,并支持来自多达 50 个仓库的 issues。

GitHub 项目是一个灵活的协作平台。您可以自定义待办事项列表和看板,并与其他团队或社区共享。

注意:新 GitHub Issues 或 GitHub Projects(测试版)

在撰写本书时,Git 项目正在进行全面重构。新的部分目前被称为GitHub Projects(测试版)新 GitHub Issues,并将在准备好后取代 GitHub Projects。目前还不完全清楚最终的名称是什么。由于新的体验是未来的方向,本书将专注于这一部分。

目前,新的体验尚不如 Jira 或 Azure Boards 成熟。但有一支优秀的团队在致力于此,我相信如果它准备就绪,它将成为市场上最好的解决方案之一!

请注意,每月都会有如此多的新功能发布,以至于所有截图可能很快就会过时。请关注变更日志github.blog/changelog/)以随时了解每月发布的所有新内容。

开始使用

GitHub 项目可以包含来自多个仓库的 issues 和 pull requests。因此,它们必须在组织层级或您的个人资料中为个人仓库创建。

要创建一个新项目,请在组织的主页或您的 GitHub 个人资料页面上导航到项目,然后点击新建项目(参见图 2.14):

图 2.14 – 在您的个人资料或组织中创建新项目

图 2.14 – 在您的个人资料或组织中创建新项目

向项目中添加工作项

项目的默认视图是表格视图。它经过优化以便于输入数据。按Ctrl + 空格或点击表格的最后一行。您可以直接输入新工作项的名称,稍后将该项转换为一个 issue。或者您可以输入#并选择一个仓库,然后选择可用的 issues 和 pull requests(参见图 2.15):

图 2.15 – 向待办事项列表中添加 issues、pull requests 或草稿工作项

图 2.15 – 向待办事项列表中添加 issues、pull requests 或草稿工作项

为您的工作添加元数据

你可以轻松地向项目中添加不同的元数据字段。目前,支持以下类型:

  • 日期字段:值必须是有效的日期。

  • 数字字段:值必须是一个数字。

  • 单选:值必须从值列表中选择。

  • 文本字段:值可以是任何文本。

  • 迭代:值必须从一组日期范围中选择。过去的日期范围会自动标记为已完成。包括当前日期的日期范围标记为当前。

要添加新字段,请按 Cmd + K(Mac)或 Ctrl + K(Windows/Linux)打开命令面板并开始输入 Create new field。你也可以点击右上角的加号并选择 + 新建字段。输入字段名称并选择字段类型。

使用表格视图

项目的默认视图是高度灵活的 表格视图,你可以使用它来输入数据并通过拖放排序行以进行优先级排列。你可以通过打开列标题菜单或打开命令面板(Cmd + KCtrl + K)并选择其中的命令来对数据进行排序、筛选和分组。如果你对表格视图进行分组,你可以直接将项目添加到组中,或者通过将项目拖动到另一个组中来更改其值(见 图 2.16):

图 2.16 – 表格视图支持分组、筛选和排序

图 2.16 – 表格视图支持分组、筛选和排序

使用看板视图

你可以切换视图为 看板视图,将你的工作以可配置的看板形式显示。每个字段的每个值都可以显示为一个列!你可以通过视图的 列字段 属性进行设置。你可以将项目拖动到另一个列中以更改状态。目前无法对看板进行分组或使用泳道,但你可以筛选看板,以便为不同种类的工作项设置独立的看板(见 图 2.17):

图 2.17 – 看板视图

图 2.17 – 看板视图

你可以通过点击看板右侧的加号为你选择的任何字段添加新列。这为你提供了一个非常灵活的方式来可视化你的工作(见 图 2.18):

图 2.18 – 为你的看板选择任何字段作为列字段

图 2.18 – 为你的看板选择任何字段作为列字段

看板视图优化了工作可视化,优化了流程,并限制了在制品(WIP)。

使用视图

每次对视图中的数据进行排序、筛选或分组,或在表格视图和看板视图之间切换时,标签页标题中的蓝色图标会指示该视图有未保存的更改。你可以在菜单中查看这些更改并保存或放弃它们。你也可以将它们保存为新视图(见 图 2.19):

图 2.19 – 工作视图修改

图 2.19 – 工作视图修改

创建新的自定义视图、重命名它们以及使用拖放功能进行排列都很简单。

工作流

你可以使用工作流来定义当问题或拉取请求转换到另一个状态时会发生什么。目前,你只能启用或禁用默认的工作流——但未来你将能够编写自己的工作流(见图 2.20):

图 2.20 – 工作流定义了项目状态变化时的行为

图 2.20 – 工作流定义了项目状态变化时的行为

洞察

你可以为迭代字段获得@current@next,也可以为受指派者字段获得@me。你可以通过点击图表上的状态来禁用它们,并且可以将鼠标悬停在日期上查看详细信息(见图 2.21):

图 2.21 – 通过实时数据的灵活图表获得洞察

图 2.21 – 通过实时数据的灵活图表获得洞察

在撰写本文时,洞察功能仅支持一种图表类型——燃尽图——并且仅按项目数和状态进行分类。但这一点很快会改变,你将能够创建多种灵活的图表,并可以修改成各种列。

管理访问权限

由于项目可以跨多个仓库共享,你可以在设置中配置可见性和访问权限。项目可以设为公开或私有。这使得你可以创建可以与公众共享的路线图。在组织中,你可以设置组织成员的基础权限为无访问权限只读写入管理员。个人项目无法做到这一点,但你可以邀请明确的协作者并授予他们只读写入管理员权限。

为了更好的可发现性,你可以将项目添加到仓库(见图 2.22):

图 2.22 – 将项目添加到仓库

图 2.22 – 将项目添加到仓库

GitHub 项目是一个非常灵活的解决方案,帮助你管理工作并根据需要进行调整。要了解更多关于 GitHub 项目的信息,请参阅docs.github.com/en/issues/trying-out-the-new-projects-experience/about-projects

项目仍处于 Beta 版本。但已经发布的功能令人印象深刻,在不久的将来,这将是一个最灵活的解决方案,允许轻松与社区共享你的配置。请关注github.blog/changelog/label/issues/中的更新。

第三方集成

如果你已经习惯使用像JiraAzure Boards这样的成熟解决方案,你也可以继续使用该解决方案。GitHub 几乎支持所有可用产品的集成。我将在这里向你展示如何与 Jira 和 Azure Boards 进行集成——但是在 GitHub 市场上还有许多其他解决方案。

它简单吗?

Jira 和 Azure Boards 是非常棒的产品,且可以高度自定义。如果你希望继续使用当前工具,确保你已经应用了本章所述的所有内容。它简单吗?你能把所有工作都放进去吗?你有从队列中拉取工作的方式吗?你有设置 WIP 限制吗?流畅度如何?

你可能需要考虑调整你的流程和项目模板,以实现更加精简的工作方式。迁移到新平台是一个减少负担的好机会。如果进行集成,确保不要继承那些会拖慢进度的债务。

Jira

GitHub 和 Jira 都有各自的应用,可以将这两个应用连接起来。如果你创建了一个新的 Jira 项目,可以在创建过程中直接添加 GitHub(见 图 2.23)。你也可以在 Jira 的 应用 | 查找新应用 中稍后添加它。

图 2.23 – 将 GitHub 添加到你的 Jira 项目

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_02_026.jpg)

图 2.23 – 将 GitHub 添加到你的 Jira 项目

安装过程非常简单,详细说明可以参考这里:github.com/marketplace/jira-software-github

你需要在 Jira 中安装这两个应用,并连接到 GitHub 组织。在 GitHub 中,你可以选择将所有仓库与组织关联,或者仅选择特定的仓库。如果你的组织有很多仓库,同步可能会需要一些时间!

你可以在 Jira 中通过 应用 | 管理你的应用 | GitHub | 开始使用 来检查你的配置和同步状态(见 图 2.24):

图 2.24 – Jira 中的 GitHub 配置与同步状态

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_02_023.jpg)

图 2.24 – Jira 中的 GitHub 配置与同步状态

一旦同步生效,你可以通过在提交信息中提及 Jira 问题的 ID,将问题、拉取请求和提交与 Jira 问题关联。ID 通常由项目密钥和表示项目项的整数构成(例如,GI-666)。

如果你在 GitHub 问题中指定了 Jira 问题 [GI-1][GI-2],文本会自动链接到对应的 Jira 问题(见 图 2.25):

图 2.25 – 将 GitHub 问题链接到 Jira 问题

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_02_026.jpg)

图 2.25 – 将 GitHub 问题链接到 Jira 问题

如果你在提交信息中提到 Jira 问题,它们会自动在 开发 部分与对应的 Jira 问题关联(见 图 2.26)。你还可以深入查看提交内容,查看包含更改数量的文件。

图 2.26 – 将 GitHub 工件链接到 Jira 中

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_02_025.jpg)

图 2.26 – 将 GitHub 工件链接到 Jira 中

你还可以使用 智能提交 来从提交信息中对 Jira 问题执行操作。智能提交具有以下语法:

<ignored text> <ISSUE_KEY> <ignored text> #<COMMAND> <optional COMMAND_ARGUMENTS>

目前支持三个命令:

  • comment:向 Jira 问题添加评论。

  • time:添加你在 Jira 问题上花费的时间。

  • transition:更改 Jira 问题的状态。

以下是一些智能提交工作原理的示例:

  • 以下提交信息向问题 GI-34 添加了评论 纠正缩进问题

    GI-34 #comment corrected indent issue
    
  • 该提交信息向 GI.34 添加了时间:

    GI-34 #time 1w 2d 4h 30m Total work logged
    
  • 该提交信息向 GI-66 添加了评论并关闭了该问题:

    GI-66 #close #comment Fixed this today
    

关于智能提交的更多信息,请参见 support.atlassian.com/jira-software-cloud/docs/process-issues-with-smart-commits

注意!

智能提交仅在提交信息中的电子邮件地址在 Jira 中拥有足够权限时有效!

Jira 和 GitHub 具有紧密的集成。如果你的团队已经习惯使用 Jira,最好继续使用 Jira 并利用与 GitHub 的集成。

Azure Boards

Azure Boards 还与 GitHub 紧密集成,设置起来非常简单。你只需从 GitHub 市场安装 Azure Boards 应用(参见 github.com/marketplace/azure-boards),并按照说明操作。

你可以直接从 Azure Boards 问题(或任何其他工作项类型)链接 GitHub 提交和 GitHub 拉取请求,使用 AB#<Azure Board 问题的 ID>(例如,AB#26)。

GitHub 链接会显示在卡片上,带有 GitHub 图标(参见图 2.27):

图 2.27 – 在 Azure Boards 中链接 GitHub 工件

图 2.27 – 在 Azure Boards 中链接 GitHub 工件

如果你在 AB 引用之前添加了 fixfixesfixed 关键词,Azure Boards 问题将自动过渡到 Done 状态。请参见以下示例:

  • 以下提交信息将链接问题 666,并在提交合并时将该问题转为已完成状态:

    Fixes AB#666
    Update documentation and fixed AB#666
    
  • 以下提交信息将链接问题 42 和 666,但仅将 666 转为已完成状态:

    Implemented AB#42 and fixed AB#666
    
  • 该关键词仅对一个引用有效。以下提交信息将链接所有三个问题,但只有 666 会被移动到已完成状态:

    Fixes AB#666 AB#42 AB#123
    
  • 如果关键词不直接放在引用之前,则不会转移任何问题!

    Fixed multiple bugs: AB#666 AB#42 AB#123
    

你可以在 GitHub 的 README 文件中添加一个徽章,显示 Azure Boards 中的问题数量。你可以从 Azure Boards 的设置中获取徽章的 URL(在看板上方右侧的小齿轮图标)中的状态徽章部分。你可以选择显示进行中的问题数量或所有项目的数量(参见图 2.28):

图 2.28 – 向你的 GitHub README 文件添加徽章

图 2.28 – 向你的 GitHub README 文件添加徽章

Azure Boards 的集成非常简单,感觉也很自然。如果你的团队已经习惯使用 Azure Boards,继续使用 Azure Boards 并与 GitHub 紧密集成是一个不错的选择。

案例研究

为了开始他们的 DevOps 转型,Tailwind Gears 选择了两个团队,将其迁移到 GitHub 作为新的 DevOps 平台。

战略决策是将所有内容迁移到 GitHub,并使用 GitHub 项目GitHub 问题 来管理工作。这还实现了某些团队在受监管环境中工作时所需的端到端可追溯性。此外,开发流程应在迁移到新平台时保持一致。

其中一个试点团队已经使用 Scrum 超过一年。他们使用 Jira 来管理他们的待办事项,并在 3 周的冲刺周期内工作。仔细查看这些冲刺会发现,在每个冲刺中,都有许多问题无法关闭。此外,大多数问题在冲刺开始时就开始同时处理。当被问及时,团队表示他们在冲刺开始时就规划了所有工作,但由于依赖公司 ERP 系统,一些工作被阻塞。当遇到阻塞时,开发人员会转而处理另一个任务。此外,一些开发人员仍然有他们以前项目的职责。他们会从帮助台的工单系统中接收工单,并且必须提供三级支持。这些工单很难规划,导致其他依赖于这些开发人员工作的团队成员的等待时间。

为了开始在新平台上工作,我们从 Jira 导入所有开放的需求,并将其标记为 requirementplannedbusiness。如果有工单进来,我们会手动添加一个新问题,并将其标记为 bugunplannedIT。我们创建一个单独的 infrastructureplannedteam 并将其优先移至待办事项的顶部。

为了减少规划和等待时间,并建立更基于拉动的工作流,我们同意不规划整个冲刺,而是专注于待办事项中排名前三的需求。团队将这三项任务分解,并为正在进行的任务设定 WIP 限制 为 5。

第二个团队仍然使用传统的瀑布式方法;他们的需求在 IBM Rational DOORS 中,并且习惯于基于规范文档进行工作。为了向更加敏捷的方式过渡,团队加入了一些新成员:

  • 一名充当 Scrum Master敏捷教练

  • 一名充当 产品负责人需求工程师

  • 一名来自架构团队的 架构师,负责在开发开始前更新软件架构

  • 一名负责在应用程序发布前进行测试的 质量工程师

为了开始工作,我们将需求从 DOORS 导出并导入到 GitHub 项目中。我们保留 DOORS ID,以便能够追溯到原始需求。

当我们拆解第一个需求时,发现工作量对迭代周期来说太多了。产品负责人将需求拆分成多个小项,以减小批量大小。对最重要的两个项目的拆解显示,这些项目每个大约需要 1 周的时间来完成。虽然架构师和质量工程师仍需等待一段时间,但团队有信心,他们有任务可以让这两位帮助完成。对团队来说,这比将工作交给其他团队的等待时间要快。

总结

上下文切换和计划外的工作会降低生产力。在本章中,你学习了如何通过转向精益工作方式来提高生产力。你通过在看板上建立拉动而不是推送、限制在制品数量(WIP)并专注于完成任务,以及减少批量大小和交接,来实现这一点。

你学习了如何使用 GitHub 问题和 GitHub 项目来实现这一点,并且如果你希望继续使用现有的工作管理系统,你还可以将 Jira 和 Azure Boards 集成进来。

在下一章中,我们将更深入地探讨团队合作和协同开发。

进一步阅读和参考资料

第三章:团队合作与协作开发

高效能的团队不仅仅是其成员的总和,一个高效能的团队才能打造出人们喜爱的产品。

在本章中,你将学习如何通过拉取请求为你的团队设置高效的协作开发流程。你将了解什么是拉取请求,以及哪些功能能够帮助你为团队建立良好的代码审查工作流程。

在本章中,我们将讨论以下核心主题:

  • 软件开发是一项团队运动

  • 协作的核心:拉取请求

  • 实操:创建拉取请求

  • 提议变更

  • 拉取请求审查

  • 实操:提出建议

  • 代码审查的最佳实践

软件开发是一项团队运动

设计师兼工程师 Peter Skillman 设计了一个实验:他挑战四人一组的团队,在棉花糖挑战中互相竞争。规则很简单——使用以下材料,搭建一个能够支撑棉花糖的最高结构:

  • 20 根未煮过的意大利面条

  • 1 码长的透明胶带

  • 1 码长的绳子

  • 1 个棉花糖

这个实验的目的并不是解决问题本身,而是观察各个团队如何合作解决问题。在实验中,斯坦福大学和东京大学的商学院学生队伍与幼儿园小朋友队伍展开竞争。猜猜谁赢了?

商学院的学生们审查了材料,讨论了最佳策略,并精心挑选出最有前景的想法。他们表现得非常专业、理性且聪明,但幼儿园的小朋友们却总是赢得比赛。他们并没有决定最佳策略——他们只是开始动手,进行实验。他们站得很近,通过简短的交流合作:这里,不,那里!

幼儿园的小朋友们并不是因为更聪明或更有技能才获胜的,而是因为他们作为团队合作得更好(Coyle D.(2018)).

你可以在体育比赛中观察到类似的情况:即便你把最优秀的选手放到一个队里,如果他们不能形成良好的团队合作,仍然会输给一个由技能较差但合作无间的队伍。

在软件工程中,我们追求的是高凝聚力的团队,而不仅仅是能合作的个人专家,而是像“棉花糖实验”中的幼儿一样一起实验的团队成员。我们通过寻找所谓的E 形团队成员来实现这一目标,E 形是T 形团队成员的进化。I 形专家在某一领域有深厚的经验,但在其他领域的技能或经验很少。T 形的人是通才,某一领域有深入的经验,同时在多个领域拥有广泛的技能。进化后的 E 形人——E代表经验专长探索执行。他们在多个领域拥有深厚的经验和经过验证的执行能力。他们总是在创新,渴望学习新技能。E 形的人是将不同领域的专长结合成一个高协作团队的最佳方式(Kim G., Humble J., Debois P. 和 Willis J.)。

你可以通过查看一些拉取请求,快速了解你的团队如何进行协作。谁在做代码审查,审查的主题是什么?人们讨论的是什么问题?讨论的语气如何?如果你曾看过高效团队的拉取请求,你就会发现,可以很容易看出一些不太顺利的地方。以下是你可以轻松发现的拉取请求反模式:

  • 拉取请求太大,包含了许多更改(批量大小)。

  • 只有在功能已经完成或冲刺的最后一天时才创建拉取请求(最后一分钟批准)。

  • 拉取请求被批准时没有任何评论。这通常是因为人们只是为了避免打扰其他团队成员而批准(自动批准)。

  • 评论中很少包含问题。这通常意味着讨论的内容是无关紧要的细节——如格式和风格——而不是关于架构设计的问题。

我稍后会向你展示代码审查的最佳实践,以及如何避免这些反模式。首先,我们先仔细看看什么是拉取请求。

协作的核心——拉取请求

拉取请求不仅仅是传统的代码审查。它还是一种实现以下目标的方式:

  • 协作编写代码

  • 共享知识

  • 创建代码的共享所有权

  • 跨团队边界协作

那么拉取请求到底是什么呢?拉取请求,也称为合并请求,是将其他分支的更改整合到目标分支的过程,通常发生在Git仓库中。这些更改可以来自仓库中的其他分支,或者来自fork——你仓库的副本。拉取请求通常缩写为PR。没有写权限的人可以 fork 你的仓库并创建拉取请求。这允许开源仓库的所有者在不给予每个人写权限的情况下允许贡献。因此,在开源世界中,拉取请求是将更改整合到仓库中的默认方式。

Pull 请求也可以用于跨团队协作,这种方式被称为内源(inner source),类似于开源风格(请参阅 第五章开源和内源对软件交付性能的影响)。

关于 Git

git – 愚蠢的内容跟踪器(参见 图 3.1 中的 Git man 页面)。

Git 是由 Linus Torvalds 于 2005 年创建的,用作 Linux 内核的版本控制系统(RCS)。在 2005 年之前,BitKeeper 被用于这个目的,但由于许可证的变化,BitKeeper 不再可以免费用于开源项目。

Git 是目前最流行的 RCS,并且有许多关于 Git 的书籍(参见 Chacon S. 和 Straub B., 2014;Kaufmann M., 2021;以及更多其他书籍)。Git 是 GitHub 的核心,但在本书中,我将 GitHub 作为 DevOps 平台而非 RCS 来讨论。

第十一章基于主干的开发 中,我将讨论分支工作流,因为它与工程速度相关,但我不会深入探讨分支和合并的问题。请参考 进一步阅读和参考文献 部分了解更多信息。

图 3.1 显示了 Git 的 man 页面:

图 3.1 – Git 的 man 页面 – 愚蠢的内容跟踪器

图 3.1 – Git 的 man 页面 – 愚蠢的内容跟踪器

Git 在逐行的基础上对文本文件进行版本控制。这意味着 pull 请求专注于更改的行:一行可以被添加、删除,或者两者兼有——在这种情况下,你可以看到旧行和新行之间的差异。在合并之前,pull 请求允许你执行以下操作:

  • 审查更改并对其发表评论

  • 将更改与源代码库中的新代码一起构建和测试,而不必先合并它

只有当更改通过所有检查时,它们才会被 pull 请求自动合并回去。

由于现代软件工程中的一切都是代码,这不仅仅是源代码。你可以在以下方面进行协作:

  • 架构、设计和概念文档

  • 源代码

  • 测试

  • 基础设施(作为代码)

  • 配置(作为代码)

  • 文档

一切都可以在文本文件中完成。在上一章中,我已经讲过 markdown 作为人类可读文件的标准。它非常适合用于协作编写概念文档和文档。如果你需要可以归档或发送给客户的实体文档,你还可以将 markdown 渲染为 便携文档格式PDF)文档。你还可以通过图表扩展 markdown——例如,使用 Mermaid(请参阅 mermaid-js.github.io/mermaid/)。虽然 markdown 是为人类可读文件设计的,YAML Ain't Markup LanguageYAML)则是为机器可读文件设计的。因此,通过结合源代码、markdown 和 YAML,你可以自动化创建开发生命周期的所有工件,并像协作源代码一样协作处理更改!

示例

在 GitHub 上,一切基本上都使用 Markdown 来处理。即使是法务团队和人力资源HR)也使用 Markdown、问题和拉取请求来协作处理合同。一个例子是招聘过程:职位描述存储为 Markdown,整个招聘过程通过问题进行跟踪。其他例子包括 GitHub 站点政策(如服务条款社区指南)。它们都是用 Markdown 编写的,并且是开源的(github.com/github/site-policy)。

如果你想了解更多关于 GitHub 团队协作的信息,参考youtu.be/HyvZO5vvOas?t=3189

实操 – 创建拉取请求

如果你是第一次使用拉取请求,最好创建一个来体验它的功能。如果你已经熟悉拉取请求,可以跳过这部分,继续阅读有关拉取请求功能的内容。按以下步骤操作:

  1. 打开以下仓库,通过点击仓库右上角的Fork按钮来创建一个 fork:github.com/wulfland/AccelerateDevOps

在 fork 中,导航到Chapter 3 | ch3_pull-request/Create-PullRequest.md)。该文件还包含说明,方便你无需在浏览器和书籍之间切换。

通过点击文件内容上方的编辑铅笔图标来编辑文件。

  1. 删除文件中标记的行。

  2. 添加几行随机文本。

  3. 修改一行,删除超出允许长度的字母。

  4. 提交你的更改,但不要直接提交到main分支。将它们提交到一个新的分支,如图 3.2 所示:

图 3.2 – 提交更改到一个新分支

图 3.2 – 提交更改到一个新分支

  1. 你会自动被重定向到一个页面,在那里你可以创建一个拉取请求。输入标题和描述。注意,你可以使用完整的 Markdown 支持,具备你在 第二章中熟悉的所有功能,规划、跟踪和可视化你的工作:表情符号(:+1:)、提及(@)、引用(#)、任务列表(– [ ])和带有语法高亮的源代码(```)。你还可以分配受托人、标签、项目和里程碑。

在页面顶部,你会看到目标分支(base)是main,而要集成的源分支是你刚刚创建的那个。创建拉取请求按钮是一个下拉菜单。你也可以选择创建一个草稿拉取请求。现在,我们跳过这个步骤,通过点击创建拉取请求按钮来创建一个拉取请求(见图 3.3)。

图 3.3 – 为你对文件所做的更改创建拉取请求

图 3.3 – 为你对文件所做的更改创建拉取请求

  1. 在拉取请求中,导航到更改的文件,注意你对文件所做的更改:删除的行为红色,添加的行为绿色,修改过的行是先删除一行然后添加一行。如果你将鼠标悬停在行上,左侧会出现一个加号+图标。如果你点击该图标,可以添加单行注释。如果你按住图标并拖动它,你可以为多行添加注释。该注释同样支持与问题相同的标记功能,并具有所有丰富的功能!添加注释后,点击添加单行注释(见图 3.4):

图 3.4 – 为更改的行添加注释

图 3.4 – 为更改的行添加注释

经典的代码审查和拉取请求之间的重要区别在于,你可以更新拉取请求。这使得你能够回应评论并共同解决问题,直到问题被关闭。为了展示这一点,你将编辑文件并提交到新的分支,查看拉取请求是否会反映这些更改。

  1. 你可以直接从拉取请求中编辑文件,方法是打开右上角的菜单并选择编辑文件(见图 3.5):

图 3.5 – 从拉取请求中编辑文件

图 3.5 – 从拉取请求中编辑文件

  1. 修改文件,添加一行新文本。在创建拉取请求之前,将更改提交到你创建的分支(见图 3.6):

图 3.6 – 将更改提交到你的分支

图 3.6 – 将更改提交到你的分支

  1. 返回到拉取请求页面,注意到你的更改已自动显示。你可以在更改的文件下查看所有更改,或者你可以在提交下查看单独的提交更改(见图 3.7):

图 3.6 – 将更改提交到你的分支

图 3.7 – 在单独的提交中评论更改

  1. 如果你是 GitHub 拉取请求的新手,重要的要点如下:

    • 拉取请求是关于将一个分支的更改合并到基础分支。如果你更新了分支,拉取请求会自动更新。

    • 你可以利用 GitHub 问题中已知的丰富功能来协作所有更改:任务列表、提及、引用、源代码等。

    • 你可以按每个文件每个提交查看更改。这有助于将重要的更改与不重要的更改(例如重构)区分开来。

提出更改

GitHub 拉取请求具有丰富的功能集,可以帮助你改善协作流程。

草稿拉取请求

什么时候是创建拉取请求的最佳时机?这可以争论,但我会说:越早越好!理想情况下,你应该在开始处理某个任务的瞬间就创建拉取请求。这样,团队成员通过查看打开的拉取请求,始终能知道每个人在做什么。但如果你创建拉取请求太早,审阅者就不知道何时给出反馈。草稿拉取请求在此时非常有用。你可以提前创建拉取请求,但大家都知道工作仍在进行中,审阅者不会收到通知,但你仍然可以在评论中提到人员,以便提前获取代码反馈。

在创建拉取请求时,你可以直接将其创建为草稿状态(见图 3.8):

图 3.8 – 创建草稿拉取请求

图 3.8 – 创建草稿拉取请求

草稿拉取请求会明确标记为draft:truedraft:false作为搜索参数:

图 3.9 – 草稿拉取请求会用自己的符号标记

图 3.9 – 草稿拉取请求会用自己的符号标记

如果你的拉取请求已经处于审查状态,你仍然可以随时通过点击Reviewers | Still in progress? | Convert to draft下方的链接将状态更改回草稿。

如果你的拉取请求准备好进行审查,只需点击Ready for review(见图 3.10):

图 3.10 – 移除拉取请求的草稿状态

图 3.10 – 移除拉取请求的草稿状态

草稿拉取请求是一个很好的功能,能够以透明的方式在团队中及早合作进行变更。

代码所有者

代码所有者是一种在仓库中的特定文件发生更改时自动将审阅者添加到拉取请求中的好方法。这个功能还可以跨团队边界协作,或在早期开发阶段添加审批,而不需要在发布流程中进行审批。假设你在仓库中定义了基础设施代码。你可以使用代码所有者来要求共享运维团队中的某个人进行审查,或者你有定义应用外观和体验的文件。每次更改这些文件时,你可能希望获得设计团队的审批。代码所有者不仅仅是关于审批;它们还可以用于在跨团队的实践社区中传播知识。

代码所有者可以是团队或个人。他们需要有写权限才能成为代码所有者。如果拉取请求脱离草稿状态,代码所有者会作为审阅者被添加进来。

要定义代码所有者,你需要在仓库的根目录、docs/文件夹或.github/文件夹中创建一个名为CODEOWNERS的文件。文件的语法非常简单,如下所示:

  • 使用@username@org/team-name来定义代码所有者。你也可以使用用户的电子邮件地址。

  • 使用模式匹配文件来分配代码所有者。顺序很重要:最后匹配的模式优先。

  • 使用#进行注释,使用!来否定模式,使用[ ]定义字符范围。

下面是一个代码所有者文件的示例:

# The global owner is the default for the entire repository
*          @org/team1
# The design team is owner of all .css files
*.css      @org/design-team
# The admin is owner of all files in all subfolders of the
# folder IaC in the root of the repository
/IaC/      @admin
# User1 is the owner of all files in the folder docs or 
# Docs – but not of files in subfolders of docs!
/[Dd]ocs/* @user1

有关更多详细信息,请参见以下页面,关于代码所有者docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-on-github/about-code-owners

代码所有者是跨团队共享知识的一种好方法,它可以将审批从发布管道中的变更委员会转移到变更发生时的早期审批。

所需审查

你可以要求在合并拉取请求之前获得指定数量的批准。这是在分支保护规则中设置的,该规则可以应用于多个分支之一。你可以在设置 | 分支 | 添加规则中创建分支保护规则。在规则中,你可以设置合并前所需的审查数量,选择是否在代码更改时取消批准,并强制要求代码所有者的批准(见图 3.11):

图 3.11 – 特定分支所需的审查

图 3.11 – 特定分支所需的审查

有关分支保护的更多信息,请参见 https://docs.github.com/en/github/administering-a-repository/defining-the-mergeability-of-pull-requests/about-protected-branches#about-branch-protection-rules。我将在第七章,“基于主干的开发”中更详细地讲解这一主题。

请求拉取请求审查

如果你的代码准备好进行审查,你可以手动添加所需的审查员数量。GitHub 会根据你修改的代码的作者提供审查员建议(见图 3.12)。你可以点击请求,或者手动搜索人员来进行审查:

图 3.12 – 推荐的审查员

图 3.12 – 推荐的审查员

你还可以让 GitHub 自动为你的团队分配审查员。你可以在设置 | 代码审查分配中为每个团队进行配置。你可以选择自动分配的审查员数量,并选择以下两种算法之一:

  • 循环轮询:根据谁最近收到的请求最少来选择审查员

  • 负载均衡:根据每个成员的总审查请求数量来选择审查员,同时考虑待处理的审查

你可以排除某些成员不参与审查,并且可以选择在分配审查员时不通知整个团队。有关如何为你的团队配置代码审查分配,请参见图 3.13:

图 3.13 – 管理团队的代码审查分配

图 3.13 – 管理团队的代码审查分配

自动合并

我最喜欢的拉取请求功能之一是自动合并。这使你在处理小更改时可以提高工作效率,尤其是当你启用了持续部署CD)时。如果你完成了更改,你启用自动合并,并且可以继续处理其他更改。如果你的拉取请求获得了所需的审批数量,并且所有自动检查都通过,拉取请求将会自动合并并部署到生产环境。

拉取请求审核

如果你被选中进行审核,你可以对许多更改发表评论、提出建议,并最终通过以下符号之一提交你的审核:

  • 评论

  • 批准

  • 请求更改

在前一部分中,我集中介绍了与拉取请求作者相关的功能。在本节中,我描述了一个帮助审阅者进行审核并向作者提供适当反馈的功能。

审核拉取请求中的提议更改

你可以通过一次查看一个文件来开始审核。如果你将鼠标悬停在行上,你会看到左侧的+图标。可以用它添加单行评论,或者通过拖动它覆盖多行,你可以创建多行评论。如果你有评论,可以选择开始审核来开始审核过程,但还不提交评论。如果你添加了更多评论,按钮会变为添加审核评论;你可以根据需要向审核添加任意数量的评论。评论在提交审核之前只对你可见!你可以随时取消审核。

标记文件为已查看

在审核时,你会看到文件顶部的进度条。当你完成一个文件后,可以勾选已查看复选框。该文件将被折叠,进度条会显示进度(参见图 3.14):

图 3.14 – 标记文件为已查看

图 3.14 – 标记文件为已查看

实践练习 – 提出建议

提供反馈的最佳方式是通过提出建议,这样拉取请求的作者可以轻松地将这些建议集成到他们的分支中。这个功能非常重要,如果你从未尝试过,值得一试。以下是操作步骤:

  1. 打开你在之前实践练习中创建的仓库的 fork:https://github.com//AccelerateDevOps。

在 fork 中,导航至Chapter 3 | ch3_pull-request/Review-Changes.md)。该文件还包含说明,以便你不必在浏览器和书本之间切换。

通过点击源代码块右上角的复制图标来复制示例源代码。

  1. 导航至src/app.js(使用 Markdown 中的链接)。选择你在之前实践练习中创建的分支,点击右上角的编辑图标(铅笔)来编辑文件(参见图 3.15):

图 3.15 – 编辑代码文件以添加示例代码

图 3.15 – 编辑代码文件以添加示例代码

  1. 删除 第 2 行 并通过按 Ctrl + V 插入代码。

  2. 直接提交到你的拉取请求源分支。

  3. 返回拉取请求,查找 已更改的文件 中的 src/app.js。注意 第 6 行第 9 行 的嵌套循环没有正确缩进。标记 第 6 行第 9 行 并创建多行评论。点击 建议 按钮,你会看到代码出现在建议框中,包括空格(参见 图 3.16):

图 3.16 – 创建多行评论的建议

图 3.16 – 创建多行评论的建议

  1. 请注意,suggestion 代码块包含完整的代码,包括空格。为了修正缩进,请在每行开头添加四个空格。

你可以将建议作为审核的一部分提交(开始审核),或者直接将建议提交给作者(添加单条评论)。对于这个动手练习,我们将建议作为单条评论添加。

将反馈融入到你的拉取请求中

由于你既是审核员又是作者,你可以直接切换角色。作为作者,你可以看到所有针对你的拉取请求的建议。

你可以将建议直接提交到你的分支,或者你也可以将多个建议批量处理成一个提交,然后一次性提交所有更改。将更改添加到批量处理并在文件顶部应用批量处理(参见 图 3.17):

图 3.17 – 将建议融入到你的代码中

图 3.17 – 将建议融入到你的代码中

建议是一种提供反馈和建议代码更改的好方法。对于作者来说,这些建议非常容易融入到他们的代码中。

提交审核

如果你已完成审核并添加了所有评论和建议,你可以提交审核。作者会被通知审核结果,并且可以回应你的评论。你可以留下最终评论并选择以下三个选项之一:

  • 批准:批准更改。这是唯一会计入所需审核员数量的选项!

  • 评论:提交反馈,但不做批准或拒绝。

  • 请求更改:表示需要更改以获得你的批准。

完成审核后,点击 提交审核(参见 图 3.18):

图 3.18 – 完成你的审核

图 3.18 – 完成你的审核

完成你的拉取请求

如果你想放弃你分支中的更改,你可以在不合并的情况下关闭拉取请求。要将更改融入到基础分支中,你有三种 合并 选项,概述如下:

  • 创建合并提交:这是默认选项。它会创建一个合并提交,并将你分支的所有提交显示为历史记录中的一个独立分支。如果你有许多长期存在的分支,这可能会使历史记录显得杂乱无章。你可以在这里看到这种合并选项的表示:

图 3.19 – 如果你做了合并提交,Git 历史记录

图 3.19 – 如果你做了合并提交,Git 历史是这样的

  • Squash 和合并:分支的所有提交将合并为一个提交。这创建了一个干净的、线性的历史,如果在合并后删除分支,这是一个不错的合并方法。如果你还在继续工作,建议不要使用这种方法。你可以在这里看到这种合并选项的表示:

图 3.20 – 如果你做了 squash 和合并,Git 历史是这样的

图 3.20 – 如果你做了 squash 和合并,Git 历史是这样的

  • Rebase 和合并:将分支的所有提交应用到基础分支的头部。这也创建了线性的历史,但保留了各个提交。如果你还在继续工作,建议不要使用这种方法。你可以在这里看到这种合并选项的表示:

图 3.21 – 如果你做了 rebase 和合并,Git 历史看起来是线性的

图 3.21 – 如果你做了 rebase 和合并,Git 历史看起来是线性的

选择你想要的合并方法,然后点击合并拉取请求(见 图 3.22):

图 3.22 – 完成一个拉取请求

图 3.22 – 完成一个拉取请求

修改合并信息并点击确认合并。合并后,如果需要,你可以删除分支。

代码审查最佳实践

拉取请求是协作任何代码的一个好方法。本章只触及了你在协作工作流中所能实现的可能性,但为了让你的团队能够有效地协作,你应该考虑一些有效的代码审查最佳实践。

教 Git

这看起来可能很显然,但请确保你的团队在 Git 上经过良好的培训。精心编写的提交和良好的提交信息,每个提交只做一个目的,比分散在多个提交中的许多变化更容易审查。特别是,重构和业务逻辑混在一起会让审查变成噩梦。如果团队成员知道如何修复提交、如何修补他们在不同提交中所做的更改,以及如何编写好的提交信息,那么生成的拉取请求将更容易审查。

将拉取请求链接到问题

将拉取请求链接到发起工作对应的问题。这有助于为拉取请求提供背景。如果你使用第三方集成,将拉取请求链接到 Jira 票据、Azure Boards 工作项或任何你与 GitHub 连接的其他来源。

使用草稿拉取请求

当团队成员开始工作时,让他们立即创建草稿拉取请求。这样,团队就能知道谁在做什么。这也鼓励大家在审查开始之前,使用评论和提及来请求他人反馈。对变更的早期反馈有助于加快最终审查的速度。

至少需要两名审批人

你应该有至少两个必需的审批者。越多越好,具体取决于团队的规模。但一个审批者是不够的。拥有多个审查员能给审查带来某种动态。我注意到通过仅将审批者从一个增加到两个,某些团队的审查实践发生了巨大的变化!

进行同伴审查

将审查视为同伴审查。不要让高级架构师审查他人的代码!年轻的同事也应该做同伴审查,以便学习。一种好做法是将整个团队作为审查员,并要求一定比例的批准(例如,50%),然后让人们选择他们想要的拉取请求。或者,你可以使用自动审查分配,随机分配审查给团队成员。

自动化审查步骤

许多审查步骤可以自动化,特别是格式化。让一个好的代码检查工具检查代码格式(例如,github.com/github/super-linter),或者编写一些测试来检查文档是否完整。使用静态和动态代码分析自动发现问题。你自动化检查琐碎任务的越多,审查就能越集中于重要事项。

部署并测试变更

在合并之前自动构建并测试你的变更。如有必要,安装代码进行测试。人们越有信心变更不会破坏任何东西,他们就越能信任这个过程。如果所有审批和验证通过,使用自动合并自动合并并发布你的变更。高度的自动化使得人们可以在更小的批次中工作,这使得审查变得更容易。

审查准则/行为规范

一些工程师对做事的正确方式有强烈的看法,辩论可能很快失控。你希望进行激烈的讨论,以获得最佳的解决方案,但这些讨论需要以包容的方式进行,让团队中的每个人都能平等参与。有了审查准则行为规范作为门卫,如果有人行为不当,你可以引用这些规则。

总结

软件开发是一项团队运动,拥有一个共同拥有代码的团队,并紧密协作进行新变更是非常重要的。如果正确使用,GitHub 拉取请求可以帮助实现这一点。

在下一章中,你将了解异步与同步工作,以及异步工作流如何帮助你随时随地进行协作。

进一步阅读和参考文献

以下是本章的参考文献,你也可以使用它们获取更多关于这些主题的信息:

第四章:异步工作:随时随地协作

在上一章中,您了解了如何通过拉取请求进行协作开发,并且如何利用它们为您构建的代码和产品创建共享所有权。在本章中,我们将重点讨论同步和异步工作,以及如何利用异步工作流的优势,在分布式、远程和混合团队中实现更好的协作,并提高跨团队的协作效率。

本章将涵盖以下主题:

  • 比较同步工作和异步工作

  • 分布式团队

  • 跨团队协作

  • 转向异步工作流

  • 团队与 Slack 集成

  • GitHub 讨论

  • 页面和维基

  • 使用 GitHub 移动版在任何地方工作

  • 案例研究

比较同步工作和异步工作

我们信息工作者所做的每一项工作,基本上都是沟通。即使是编程的所有内容:你必须传达你正在编写的代码,必须传达架构,甚至代码本身也是与未来的人——包括你自己——传达如何修改程序的方式。因此,我们沟通的方式直接影响我们完成工作的效率。

通信历史

人类的互动和沟通方式在历史上经历了许多变化。直到 1450 年约翰内斯·古腾堡发明印刷机之前,通信大多是纯口头的,少量是书面通信。印刷机的发明引发了一场印刷革命,对宗教和教育产生了巨大影响,因为它使信息能够更广泛地传播。在 17 世纪,报纸的发明再次革新了沟通方式,极大地缩短了信息从发送者到接收者的传递时间。18 世纪,公共邮政系统变得如此高效,以至于越来越多的沟通通过信件进行。这使得私人通信的速度与报纸一样迅速。在 19 世纪,电报的发明第一次实现了远距离的实时通信。1861 年,菲利普·赖斯在法兰克福发明了第一部电话。虽然传输质量仍有波动,但大多数人低估了这一发明。直到 1876 年,亚历山大·格雷厄姆·贝尔最终获得了电话的专利,这一发明彻底改变了沟通方式,使得实时口头交流成为可能。

在那之前,通讯的变化更多的是与几个世纪而非几十年相关。人们有时间去适应,并且通常可以清晰直观地知道哪种通讯方式是最好的。这种情况在过去 30 年里发生了迅速的变化。到 1990 年代末,手机变得既小巧又负担得起。任何人都可以随时与任何人通话。有趣的是,这导致了一种新现象:人们开始互相发送简短的消息,并且常常偏好异步通讯而非同步通讯。随着互联网的兴起,电子邮件迅速取代了信件。但在最初,互联网并不具备移动性,因此电子邮件的预期回复时间仍然是几天。这一情况在本世纪的第一个十年中发生了变化。互联网变得可以移动,而智能手机使得随时随地访问电子邮件成为可能。同时,新的通讯形式也变得流行起来:Facebook、Twitter、Instagram 和 Snapchat。它们以文本、语音和视频的形式允许与不同群体(传播范围与隐私)进行多种类型的沟通,并且具有不同的信息存活时间TTL)或存活时间

图 4.1 展示了我们世界人口的指数增长与沟通行为变化之间的关系:

图 4.1 – 指数增长与沟通变化

图 4.1 – 指数增长与沟通变化

过去 30 年的迅速发展导致了非常不同的沟通模式。无论你是写短信、打视频电话,还是给一个群体发送故事,更多的是取决于个人偏好,而不是消息的内容。现在已经没有社会共识来决定哪种沟通形式适合某一类消息。

工作与沟通

工作不仅仅是沟通。信息工作是对话中所期望输出的增加。你可以将工作分为同步工作和异步工作。同步工作是指两人或更多人在实时互动中共同达成预期的输出。而异步工作则发生在两人或更多人交换消息以达成预期输出时。

如果你在一个传统企业工作,异步工作和同步工作的组合可能依然如图 4.2 所示。至少几年前是这样的。大多数工作通过电子邮件或会议进行,会议通常发生在同一个房间里:

图 4.2 – 传统企业中的工作与沟通

图 4.2 – 传统企业中的工作与沟通

大多数异步工作是通过电子邮件和远程方式进行的,而大多数同步工作则是在面对面会议中进行。主导方式通常取决于公司的文化。在有强大电子邮件文化的公司中,人们通常在几分钟内回复邮件。在这些公司里,许多人在开会时也会打开笔记本电脑,大家通常抱怨邮件太多。而在有强大会议文化的公司中,人们由于参加会议,往往不能及时回复邮件。这导致了邮件减少,但会议增多。

在过去的几年里,这一情况发生了巨大变化。特别是小公司和初创公司,已经放弃了电子邮件,转而使用聊天等其他异步工作媒介。许多公司也发现了远程工作的好处,有些公司是在疫情迫使他们远程工作后才意识到这一点。

我已经在第二章中展示了,规划、跟踪和可视化你的工作,如何通过上下文切换来破坏生产力。因此,对于开发团队来说,异步工作是可取的,因为它允许你为工作项建立拉取机制,并减少上下文切换。优化过的现代工作模式对于开发人员可能是这样的:

图 4.3 – 为开发优化的工作与沟通

图 4.3 – 为开发优化的工作与沟通

人们越少进行同步工作,就越能在没有上下文切换和规划的情况下集中精力工作。重要的是要有意识地思考:我们应该以同步的方式进行哪类工作,哪些工作可以异步进行?我们应该面对面地进行哪些工作,哪些工作可以远程进行?

面对面与远程工作

同步工作可以是面对面进行,也可以是远程进行。两者都有各自的优缺点。

面对面会议如果需要说服某人,通常是更可取的。销售人员总是更倾向于面对面会议,而非电话或远程会议,因为面对面的会议更适合社交和建立关系/团队。关键性的反馈和敏感问题也更适合面对面讨论而非远程讨论。对于需要创造力的复杂讨论或问题,物理上的接近也有帮助。

远程会议的优势在于,由于减少了旅行时间,它们更加高效。人们可以独立于物理位置参与,这让你能够拥有跨越多个时区的团队。远程会议可以录制,即使人们未能参与,也可以回放会议。

远程会议应该与面对面会议有所不同。一个 8 小时的工作坊(2x4)适合面对面进行,但不适合远程进行。远程会议应当更短小且更有针对性。人们如果长时间面对计算机,往往容易分心。

在未来几年,我们将看到越来越多的混合工作。混合工作使员工能够在不同地点独立工作:在家、移动中或办公室内。66%的公司正在考虑重新设计办公室空间以适应混合工作,73%的员工希望获得更多灵活的远程工作选项(参见 www.microsoft.com/en-us/worklab/work-trend-index/hybrid-work)。混合工作将在组织会议时带来很大的挑战。远程会议优化了个人的需求,而面对面会议则优化了团队的需求。将两者结合起来将是一项挑战,不仅仅是会议室中的技术设备问题,还包括负责组织会议的人。

分布式团队

几乎 100% 远程工作的科技公司,以及其团队分布在全球各地的情况,已经存在了一段时间。我知道有一家公司,其招聘过程完全是远程的。每个员工都有预算,可以用来投资自己的家庭办公室,或租用一个共享工作空间。公司团队分布在全球,只会每年一次进行面对面的聚会。

随着疫情以及远程和混合工作模式的兴起,越来越多的公司开始意识到分布式团队的好处,包括以下几点:

  • 你不再局限于在某个大都市地区招聘,因此可以招聘到更多的人才和专家(人才争夺战)。

  • 在其他地区招聘通常伴随着成本降低

  • 如果你在多个市场上销售产品,拥有来自这些不同背景的团队成员有助于更好地理解客户(多样性)。

  • 通过提供支持,你自动可以覆盖更多的工作时间,这意味着工程师在正常工作时间之外的更少的值班责任

分布式团队也面临着挑战,最大的问题就是语言。非母语者在沟通上有更多问题,因此需要一个良好的基础语言——如果你希望跨越多个国家,最有可能是英语。此外,文化差异也可能使沟通变得更加困难。团队建设以及团队的文化契合度在远程招聘过程中必须发挥更大作用。

如果你希望通过增加更多远程工程师来扩展团队,一定要相应地计划时区。你应该至少确保不同的时区工作时间有 1 或 2 小时的重叠,具体取决于你的会议数量。这意味着你通常最多可以在一个方向上延伸大约 8 小时,以确保有 1 小时的重叠。如果你已经在一个方向上有 4 小时的时差,那么在另一个方向上最多只能再增加 4 小时的时差(参见 图 4.4)。

图 4.4 – 计划有重叠时区的会议时间

图 4.4 – 计划有重叠时区的会议时间

请考虑到夏令时和不同的工作时间使得跨时区的重叠规划成为一项相当复杂的任务!

分布式团队有其优势,未来几年我们将会看到这一点。最好从一开始就采用能够让你后来从其他时区的其他国家聘请专家的工作方式。这意味着,所有的沟通都应该使用英语或你所在地区的其他通用语言,并尽可能多地使用异步工作流。

跨团队协作

为了加速软件交付,你希望你的团队尽可能自主。能够在任何时候向最终用户交付价值而不依赖于其他团队,是影响工作速度的最大因素之一,但你仍然需要一些团队之间的对齐:设计、安全性和架构是必须跨团队解决的共同问题。良好的跨团队协作是团队之间健康对齐的标志。

良好的跨团队协作通常不需要管理层的参与,通常是通过上下指挥链到达第一个共同的管理者。良好的跨团队协作是基于异步工作流的,直接将合适的人聚集在一起解决问题。日常工作中所需的会议越少越好。

转向异步工作流

要转向更为异步的工作方式并允许远程和混合工作,你可以轻松采纳一些最佳实践,以下是其中的一些:

  • 偏好聊天而非电子邮件:依赖电子邮件的工作流有很多缺点:你没有共同的历史;如果团队成员生病或离职,你会被阻碍;等等。尽量将所有与你工作相关的对话转移到聊天平台上,如 Microsoft Teams 或 Slack。

  • 让(大多数)会议成为可选:让所有与工作相关的会议都成为可选。如果你认为会议没有价值,就离开。这有助于让会议更加专注并且准备充分,因为没有人希望在自己的会议中成为唯一的参与者。当然,一些团队建设或全员大会会议应该是不可选的。

  • 记录所有会议:记录所有会议使得那些无法参与的人也有机会赶上进度。录制的会议可以以更快的速度观看,这有助于在更短的时间内消化会议内容。

  • 有意为之:明确你所安排的会议是什么,以及什么是异步工作流(聊天、问题、拉取请求和维基)。

  • 回顾你的设置:确保了解你的指标,并定期检查你的设置。会议是否成功,或者它们是否可以转移到问题或拉取请求中的讨论?问题和拉取请求中的讨论是否花费了太长时间,是否有些事情可以通过会议更快解决?不要过于频繁地改变设置,因为人们需要一些时间来适应,但至少每 2 到 3 个月要回顾并调整你的设置。

  • 使用提及和代码所有者:使用提及和代码所有者(请参阅 第三章团队合作与协同开发),动态地将合适的人聚集在一起完成任务。这两个功能也非常适合跨团队协作。

  • 将一切视为代码:尽量将一切视为代码,并像对待代码一样进行协作:基础设施、配置、软件架构、设计文档和概念。

Teams 和 Slack 集成

如果你更喜欢使用聊天而不是电子邮件,可以利用 GitHub 的集成功能与 Microsoft Teamsteams.github.com)或 Slackslack.github.com)进行互动。这些功能允许你直接在聊天频道中接收通知,并与问题、拉取请求或部署进行互动。Slack 和 Teams 的功能非常相似,如下所示:

  • 通知:订阅仓库中的事件。你可以使用分支或标签过滤器来过滤通知。

  • GitHub 链接的详细信息:GitHub 链接会自动展开,显示链接指向项的详细信息。

  • 打开新问题:直接从对话中创建新问题。

  • 互动:直接在你的频道中与问题、拉取请求或部署批准进行互动。

  • 安排提醒:在你的频道中接收代码审查提醒。

安装过程非常简单。你必须在 Microsoft Teams 或 Slack 中安装 GitHub 应用,并在你的组织中将相应的 Teams 或 Slack 应用与 GitHub 配对。

安装后,你可以与 GitHub 机器人互动并发送消息。在 Teams 中,你可以通过 @GitHub 提及机器人,而在 Slack 中,则通过 /GitHub 来提及。如果你提及了机器人,你将收到一份可以使用的命令列表(见 图 4.5):

图 4.5 – 向 GitHub 机器人发送命令

图 4.5 – 向 GitHub 机器人发送命令

第一个必须使用的命令是 signin,它会将你的 GitHub 账户与 Teams/Slack 账户关联起来:

@GitHub signin

之后,你可以订阅通知或安排提醒。链接的展开和与问题的互动无需任何配置。图 4.6 显示了一个从对话中创建的问题,你可以直接对问题进行评论或关闭它:

图 4.6 – 在 Microsoft Teams 中集成问题

图 4.6 – 在 Microsoft Teams 中集成问题

聊天集成是一个强大的功能,当你越来越多的工作流通过聊天而非会议或电子邮件启动和管理时,这个功能将非常有用。

GitHub 讨论

第二章规划、跟踪和可视化您的工作,您学会了如何使用 GitHub 问题和 GitHub 项目来管理您的工作。GitHub Discussions 是一个允许成员提问、分享更新并进行开放性对话的社区论坛。讨论是减少问题和拉取请求负荷的一个很好的方式,为长时间的讨论和问答Q&A)提供了另一个场所。

开始使用 Discussions

要开始使用 GitHub Discussions,您必须在您的存储库中启用它,在设置 | 选项 | 功能下勾选讨论。一旦勾选了此选项,您的存储库中就会有一个新的主菜单项讨论

注意

GitHub Discussions 是在撰写本书时仍处于 Beta 阶段的一个功能。某些功能可能已经发生了变化。您可以在 github.com/github/feedback/discussions 下提供反馈并参与讨论,这当然也是一个 GitHub 讨论。

讨论被组织成类别。您可以像搜索和筛选问题一样在讨论中搜索和筛选。讨论本身可以被投票和指示评论数量,并标记是否已回答。您可以将最多四个讨论置顶以发布一些重要公告。排行榜显示了在过去 30 天内回答最多问题的最有帮助的用户。图 4.7 展示了讨论的外观:

图 4.7 – GitHub Discussions 概述

图 4.7 – GitHub Discussions 概述

讨论类别

您可以通过点击旁边的编辑铅笔来管理类别类别。您可以编辑、删除或添加新的类别。一个类别包括以下内容:

  • 图标

  • 标题

  • 可选描述

这里列出了三种类别:

  1. 问题/答案

讨论类别用于提问、建议答案,并投票选出最佳答案。这种类别类型是唯一允许标记评论已答复的类型

  1. 开放性讨论

一个不需要对问题给出明确答案的类别。非常适合分享技巧或只是聊天。

  1. 公告

与您的社区分享更新和新闻。仅维护者和管理员可以在这些类别中发布新的讨论,但任何人都可以评论和回复。

开始讨论

您可以通过单击#(引用)、拉取请求和其他讨论以及提到(@)其他人、语法高亮显示的代码和附件(参见图 4.8)来开始讨论:

图 4.8 – 开始新讨论

图 4.8 – 开始新讨论

参与讨论

你可以评论或直接回答原始讨论描述,或者回答已有的评论。在每种情况下,你都可以完全使用 Markdown 支持!你可以通过表情符号对所有评论和原始描述进行反应。你还可以为讨论或评论/回答点赞。在右侧菜单中,你可以将讨论转换为问题。作为管理员或维护者,你还可以锁定对话,将其转移到另一个仓库,将讨论置顶,或删除它。图 4.9 给出了正在进行的讨论概览:

图 4.9 – 参与讨论

图 4.9 – 参与讨论

Discussions 是一个很好的平台,可以让你与同行以及跨团队边界进行异步协作。有关 Discussions 的更多信息,请参阅 docs.github.com/en/discussions

Pages 和 wikis

你有许多选择可以以协作的方式分享内容。除了问题和讨论外,你还可以使用 GitHub Pageswikis

GitHub Pages

GitHub Pages 是一项静态网站托管服务,可以直接从 GitHub 中的仓库提供你的文件。你可以托管常规的 超文本标记语言 (HTML)、层叠样式表 (CSS) 和 JavaScript 文件,并自行构建一个网站。但你也可以利用内置的预处理器 Jekyll(请参阅 jekyllrb.com/),它允许你使用 Markdown 构建漂亮的网站。

GitHub Pages 网站默认托管在 github.io 域名下(例如 wulfland.github.io/AccelerateDevOps/),但你也可以使用自定义域名。

GitHub Pages 是公共仓库的免费服务。对于内部使用(私有仓库),你需要 GitHub Enterprise。

注意

GitHub Pages 是一项免费服务,但并不适用于运行商业网站!禁止运行网店或任何其他商业网站。它有 1 吉字节 (GB) 的配额,以及每月 100 GB 的带宽限制。有关更多信息,请参见 docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages

学习 GitHub Pages 的最佳方式是亲自查看它是如何运作的。方法如下:

  1. 如果你已经在前几章的动手练习中 fork 了 github.com/wulfland/AccelerateDevOps 仓库,你可以直接访问你的 fork。如果没有,点击仓库右上角的 Fork 按钮创建一个 fork。这将会在 https://github.com//AccelerateDevOps 下创建一个 fork。

  2. 在分叉的仓库中,导航到main分支,并选择/docs文件夹作为网站的根目录。你只能选择仓库的根目录或/docs!不能使用其他文件夹。点击保存以初始化网站(见图 4.10):

图 4.10 – 为仓库启用 GitHub Pages

图 4.10 – 为仓库启用 GitHub Pages

  1. 创建站点可能需要几分钟的时间。如果站点尚不可用,点击图 4.11 中突出显示的链接,并刷新页面:

图 4.11 – 导航到网页

图 4.11 – 导航到网页

  1. 检查网站,并注意到它有一个包含静态页面的菜单,以及一个显示带有摘录的帖子菜单(见图 4.12):

图 4.12 – 使用 Jekyll 的网站

图 4.12 – 使用 Jekyll 的网站

  1. 返回到你的代码。首先,检查/docs/_config.yaml配置文件。在这里,你可以放置网站的全局配置,例如标题和描述,如以下代码片段所示:

    title: Accelerate DevOps with GitHub
    description: >-
      This is a sample Jekyll website that is hosted in   GitHub Pages.
      ...
    

有许多主题可以用来渲染你的站点。每个主题都有自己的特点,所以一定要查看文档。我使用的是默认的 Jekyll 主题jekyll-feed,并且通过show_excerpts选项,你可以设置是否在首页显示帖子的摘录,如以下代码片段所示:

theme: minima
Markdown: kramdown
plugins:
  - jekyll-feed
show_excerpts: true

许多主题支持附加值。例如,你可以设置社交媒体账户,这些账户会显示在你的站点上,如下所示:

twitter_username: mike_kaufmann
github_username: wulfland

通常,静态页面会按字母顺序显示在顶部导航栏中。要过滤和排序页面,你可以在配置中添加一个部分。由于我们想添加一个新页面,可以在About.md前添加一个my-page.md条目,像这样:

header_pages:
- get-started.md
- about-Markdown.md
- my-page.md
- About.md

直接将更改提交到main分支。

  1. /docs文件夹中,选择my-page.md作为文件名,并在文件中添加以下头部:

    ---
    layout: page
    title: "My Page"
    permalink: /my-page/
    ---
    

如果需要,可以添加更多 Markdown 内容。直接提交到main分支。

  1. 现在,前往/docs/_posts/文件夹。选择YYY-MM-DD-my-post.md作为文件名,其中YYYY是当前年份,MM是两位数的月份,DD是两位数的日期。添加以下头部,并将日期替换为当前日期:

    ---
    layout: post
    title:  "My Post"
    permalink: /2021-08-14_writing-with-Markdown/
    ---
    

向页面添加更多 Markdown 内容,并直接提交到main分支。

  1. 给后台处理器一些时间,然后刷新页面。你应该能看到页面和开始页上的帖子,并且可以导航到它们(见图 4.13):

图 4.13 – 检查 Jekyll 中新页面和帖子的情况

图 4.13 – 检查 Jekyll 中新页面和帖子的情况

你已经看到如何在 GitHub Pages 上发布内容是多么简单。Jekyll 是一个非常强大的工具,你几乎可以自定义一切,包括主题。你还可以在安装 Ruby 和 Jekyll 后离线运行你的站点进行测试(更多详情请见 docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/testing-your-github-pages-site-locally-with-jekyll)。然而,这是一个非常复杂的话题,超出了本书的范围。

使用 Jekyll 的 GitHub Pages 是一种很好的方式,可以以美观的方式呈现内容,并像处理代码一样通过拉取请求协作修改内容。你可以将它用作技术博客或用户文档。在一个分布式团队中,你可以用它发布每个 Sprint 的成果,或许还可以添加小视频。这有助于即使当人们无法参加 Sprint 评审会议时,也能传达你的成功。

Wiki

GitHub 在每个仓库中都包含了一个简单的 Wiki,但你也可以选择创建一个基于 Markdown 的 Wiki,并将其与代码一起使用。

GitHub Wiki

每个仓库中都有一个非常简单的Wiki。你可以选择以不同的格式编辑页面:MarkdownAsciiDocCreoleMediaWikiOrg-modeProdRDocTextilereStructuredText。由于 GitHub 中其他所有内容都是 Markdown,我认为这是最好的选择,但如果你已经有了其他格式的 wiki 内容,这也能帮助你迁移内容。

注意

其他编辑格式,如 AsciiDocMediaWiki,具有更多高级功能,例如自动生成的目录ToC)。如果你的团队已经熟悉这些语法,那么它可能对你有意义,但同时学习 Markdown 本身和另一种 Markdown 语言可能弊大于利。

Wiki 非常简单。你可以编辑主页,并可以添加自定义的侧边栏和页脚。指向其他页面的链接用双大括号表示为 [[Page Name]]。如果你想要一个独立的链接文本,可以使用 [[Link Text|Page Name]] 格式。如果你创建一个指向尚不存在的页面的链接,它会以红色显示,并且你可以通过点击该链接创建页面。

Wiki 是一个 Git 仓库,其名称与仓库相同,并带有 .wiki 扩展名(<name_of_repository>.wiki)。你可以克隆一个仓库并在本地的分支中使用 wiki,但不幸的是,直到现在还没有办法使用拉取请求来协作修改!这是 GitHub Wiki 最大的缺点!

此外,Wiki 不支持嵌套页面。所有页面都位于仓库的根目录下。你可以使用侧边栏通过 Markdown 嵌套列表来创建具有层次结构的菜单,如下所示:

[[Home]]
* [[Page 1]]  
  * [[Page 1.1]]  
  * [[Page 1.2]]  

如果你希望菜单的某些部分是可折叠的,可以使用 <details></details> GitHub Markdown 功能。这将在 Markdown 中创建一个可折叠部分,并通过 <summary></summary> 自定义标题,如下所示:

* [[Page 2]]
  * <details>
    <summary>[[Page 2.1]] (Click to open)</summary>
      * [[Page 2.1.1]]  
      * [[Page 2.1.2]]  
    </details>  

请注意,空行对于使其正常工作是必要的!结果如图 4.14 所示:

图 4.14 – GitHub Wiki 的结构

图 4.14 – GitHub Wiki 的结构

GitHub Wiki 是一个非常简单的 Wiki 解决方案,缺乏许多其他 Wiki 解决方案的功能,特别是你不能使用拉取请求,这限制了它在异步工作流中的优势。但幸运的是,你可以在自己的仓库中托管 Markdown 并自己构建一个自定义的 Wiki。

自定义 Wiki

如果你不想使用 GitHub Pages 的复杂功能,但仍然希望在 Wiki 上使用拉取请求,你可以将 Markdown 文件直接放入你的仓库中。GitHub 会自动为所有的 Markdown 文件渲染一个目录(参见图 4.15)。你可能已经在 GitHub 仓库的 README 文件中注意到这一点:

图 4.15 – GitHub Markdown 文件的目录

图 4.15 – GitHub Markdown 文件的目录

自定义 Wiki 的问题在于导航。使用 Markdown 嵌套列表和相对链接来构建导航系统是很容易的。你还可以使用 details 使其可折叠,如下面的代码示例所示:

<details>
    <summary>Menu</summary>
* Home  
* Page1  
  * Page 1.1
  * Page 1.2
* Page2
</details>

但是,如果你需要在每个页面上都有它,那么在更改时必须将其复制粘贴到所有页面。你可以自动化此过程,但它仍然会使历史记录变得冗余。最好是在每个页面上都有类似面包屑的导航,用户可以用来返回主页,并从那里使用菜单。你可以在这里看到一个自定义导航的 Markdown 示例:github.com/wulfland/AccelerateDevOps/blob/main/ch4_customWiki/Home.md

从社区论坛到简单的 Markdown Wiki,再到使用 Jekyll 的完全自定义网页,有很多选项可以在 GitHub 上托管你工作的额外内容。选择适合当前任务的方案并不容易。有时,你只需要尝试找出适合你团队的方案。

使用 GitHub 移动端在任何地方工作

大多数时候,你会通过浏览器在 GitHub 问题、拉取请求和讨论中进行协作。但也有其他选项可以帮助你将 GitHub 带到你所在的地方。

GitHub 移动端是一个可以通过 Android 和 Apple 市场下载的移动应用(参见 github.com/mobile)。该应用允许你访问所有仓库中的问题、拉取请求和讨论。它具有暗黑模式和亮色模式,你可以将喜欢的仓库固定到首页(参见图 4.16):

图 4.16 – GitHub 移动端主页,暗黑模式与亮色模式

图 4.16 – GitHub 移动版主页的深色和浅色模式

我个人非常喜欢 GitHub 移动版应用——它设计得非常精妙,帮助你在日常工作中不依赖工作站或笔记本电脑,随时协作处理问题和讨论。你可以配置通知,以便在被提及、被分配任务或请求审查时收到通知。这些通知会出现在收件箱中,你可以使用可配置的滑动操作将通知标记为完成已读未读;将通知保存;或者取消订阅通知源。默认的标记选项是完成和保存。收件箱的界面如下图 4.17 所示:

图 4.17 – GitHub 移动版中的通知

图 4.17 – GitHub 移动版中的通知

我第一次使用该应用时,给我印象最深刻的是它在移动设备上提供的代码审查体验。你可以开启换行功能,这样可以方便地阅读代码、查看更改并进行评论(见图 4.18):

图 4.18 – 使用 GitHub 移动版进行拉取请求审查

图 4.18 – 使用 GitHub 移动版进行拉取请求审查

GitHub 移动版是解锁团队成员的绝佳方式,即使你不在办公室。它允许你参与讨论并评论代码更改和问题。随时审查小范围更改的可能性有助于团队实现更小的工作批次,因为审批的等待时间更短。

案例研究

我们的两个试点团队在Tailwind Gears的第一件事就是将他们的代码迁移到 GitHub 仓库。其中一个团队已经在 Bitbucket 服务器上使用Git。对于该团队来说,迁移就像将仓库推送到一个新的远程仓库一样简单。另一个团队使用的是Team Foundation ServerTFS)版本控制,必须先将代码迁移到服务器上的 Git 中,然后再推送到 GitHub。

两个团队决定参加为期 2 天的Git 培训,以便充分利用 Git 的强大功能,并编写易于审查的高质量提交。他们使用草稿拉取请求,确保团队中的每个人始终知道其他人正在做什么,并且暂时设置了至少两个必需审阅者

许多工作仍然在仓库之外进行,发生在存储在公司 SharePoint 服务器上的 Word、Excel 和 Visio 文档中。一些文档被转换为便携文档格式PDF),并在发布符合某些规定的产品之前,由管理层签署。这些文档太多,无法一次性全部转换为 Markdown 格式。团队在他们的代码仓库中创建了一个自定义基于 Markdown 的维基,将所有内容都放在接近代码的地方。他们添加了对当前 SharePoint 文档的链接。每当需要更改文档时,内容将移到 Markdown 文件中,并删除链接。管理层不再签署 PDF 文档,而是作为代码所有者加入相应文件,并直接在拉取请求中批准更改。结合审计日志,这对所有必要的合规审计是有效的。

在迁移到新平台时,许多方面对两个团队都是相关的,稍后其他团队在迁移到新平台时也会涉及到这些方面。因此,他们创建了一个共享的平台仓库。该仓库包含GitHub Discussions,可以与所有工程师合作,即使他们还未加入这两个团队中的任何一个。通过GitHub Pages建立了一个技术博客,分享技巧和窍门。Jekyll 网站也被用来协作制定通用的审查指南和行为准则。

总结

在本章中,你已了解同步和异步工作的优缺点。你可以利用这些信息创建有效的异步工作流程,从而实现更好的跨团队协作,并使得远程和混合团队能够覆盖多个领域和时区。你已经学会了如何利用 GitHub Discussions、Pages 和维基来为除代码和需求之外的其他话题创建异步工作流程。

在下一章中,我将解释开放源代码和内部源代码对软件交付性能的影响。

进一步阅读和参考资料

以下是本章中提供的参考资料,你也可以用它们获取更多关于这些主题的信息:

第五章: 开源与内部源代码对软件交付性能的影响

20 年前,即 2001 年 6 月 1 日,前微软 CEO Steve Ballmer 在接受《芝加哥星期日时报》采访时曾说过以下话:

Linux 是一种癌症,它在知识产权上附着于它接触到的每一个事物。

(Greene T. C. (2001))

他关注的不仅仅是Linux,而是开源许可证的整体问题。如今,20 年后,微软已成为全球开源贡献的最大单一公司,超越了 Facebook、Google、Red Hat 和 SUSE。不仅如此,他们拥有许多开源产品,如 PowerShell、Visual Studio Code 和.NET,他们还在 Windows 10 中附带了完整的 Linux 内核,使得用户可以在其上运行任何发行版。微软总裁 Brad Smith 承认,“当开源在世纪初爆发时,微软站在了历史的错误一方”(Warren T. (2020))。

如果你查看对开源做出贡献的前 10 家公司,你会发现所有大型科技公司都参与了商业软件的开发:

表格 5.1 – 开源贡献者指数,2021 年 8 月 2 日 (https://opensourceindex.io/)

表格 5.1 – 开源贡献者指数,2021 年 8 月 2 日 (https://opensourceindex.io/)

过去 20 年发生了什么变化,以至于重要的科技公司现在都开始接受开源?

本章将解释自由与开源软件的历史,以及为何它在过去几年变得如此重要。我将阐述它对工程开发速度的影响,并说明如何利用开源原则来促进公司内部团队之间更好的协作(内部源代码)。

本章将涵盖以下主题:

  • 自由与开源软件的历史

  • 开源与开放开发的区别

  • 企业采用开源的好处

  • 实施开源战略

  • 开放源代码与内部源代码

  • 内部源代码的重要性

  • GitHub Sponsors

自由与开源软件的历史

要理解开源,我们必须回到计算机科学的早期阶段。

公有领域软件

在 1950 年代和 1960 年代,软件的价格相较于所需硬件较低。任何软件的产生主要由学术界和企业研究团队负责。通常,源代码与软件一同发布——通常是作为公有领域软件。这意味着软件是自由获取的,不受所有权、版权、商标或专利的限制。这些开放与合作的原则对当时的黑客文化产生了深远的影响。

在 1960 年代末期,操作系统和编译器的兴起提高了软件的成本。这是由于一个不断增长的软件行业,与硬件供应商竞争,后者将自己的软件与硬件捆绑销售。

在 1970 年代和 1980 年代,出售软件使用许可证成为常见做法,1983 年,IBM 停止将源代码与购买的软件一起分发,其他软件厂商纷纷效仿。

自由软件

理查德·斯托曼坚信这是不道德的,他于 1983 年创立了GNU 项目,并随后成立了自由软件运动。自由软件运动认为,软件只有在接收者被允许执行以下操作时,才算是自由软件:

  • 为任何目的运行程序。

  • 研究软件并以任何方式修改它。

  • 重新分发程序并制作副本。

  • 改进软件并发布改进。

理查德于 1985 年创立了自由软件基金会FSF)。FSF 以以下名言而闻名:

自由如言论自由——而非像免费啤酒那样的自由。”

这意味着自由一词指的是分发自由,而不是免除费用的自由(Gratis versus libre)。由于许多自由软件已经是免费的,因此这类免费软件(Freeware)与自由软件和零成本产生了关联。

自由软件运动创造了一个叫做版权左(copyleft)的概念。它赋予用户使用和修改软件的权利,但同时保持软件的自由性质。这些许可证的例子包括 GNU 通用公共许可证GPL)、Apache 许可证和Mozilla 公共许可证MPL)。

目前仍在数百万台设备上运行的许多优秀软件,都是通过这些版权左许可证进行分发的;例如,Linux 内核(由林纳斯·托瓦兹于 1992 年发布)、BSD、MySQL 和 Apache。

开源软件

1997 年 5 月,在德国维尔茨堡的 Linux 大会上,埃里克·雷蒙德介绍了他的论文《大教堂与集市》(Raymond, E. S. 1999)。他反思了自由软件原则、黑客文化以及这些原则对软件开发的好处。该论文引起了广泛关注,并促使 Netscape 将其浏览器Netscape Communicator作为自由软件发布。

雷蒙德和其他人希望将自由软件原则带到更多的商业软件厂商,但“自由软件”这一术语对于商业软件公司来说具有负面含义。

1998 年 2 月 3 日,在帕洛阿尔托,许多自由软件运动的重要人物聚集在一起,进行战略会议,讨论自由软件的未来。与会者包括埃里克·雷蒙德迈克尔·蒂曼克里斯蒂娜·彼得森,他们被认为提出了开源这一术语,作为对自由软件的替代。

开源倡议OSI)由埃里克·雷蒙德布鲁斯·佩伦斯于 1998 年 2 月底成立,雷蒙德为首任主席(OSI 2018)。

1998 年,在出版商Tim O'Reilly的历史性自由软件峰会——后来更名为开源峰会——上,术语被迅速采纳,早期支持者包括Linus TorvaldsLarry Wall(Perl 的创始人)、Brian Behlendorf(Apache)、Eric Allman(Sendmail)、Guido van Rossum(Python)和Phil Zimmerman(PGP)(O'Reilly 1998)。

理查德·斯托曼和自由软件基金会(FSF)拒绝了新术语开源Richard S. 2021)。这就是为什么自由开源软件FOSS)运动至今依然存在分歧,并且使用不同术语的原因。

在 1990 年代末和 2000 年代初的互联网泡沫期间,开源开源软件OSS)这两个术语被大众媒体广泛采用,并最终成为了更受欢迎的术语。

开源软件的崛起

在过去的二十年里,开源软件的普及度持续上升。像 Linux 和 Apache 这样的软件支撑着大部分互联网。刚开始,商业化 OSS 是很困难的。最初的想法是围绕开源产品提供企业级支持服务。在这方面取得成功的公司有 Red Hat 和 MySQL。但这条路更为艰难,且无法像商业许可那样提供规模效应。因此,那些在开源软件构建上投入大量资金的公司开始创建开放核心产品:一个免费的开源核心产品,以及可以由客户购买的商业附加组件。

从传统许可证到软件即服务SaaS)订阅的商业模式转变,帮助开源公司将其 OSS 商业化。这激励了传统软件厂商发布他们的软件——至少是核心部分——作为开源,以便与社区进行互动。

不仅像微软、谷歌、IBM 和亚马逊这样的巨大软件公司成为了重要的开源公司。像 Red Hat 和 MuleSoft 这样的纯开源公司也获得了大量的价值和市场认可。例如,Red Hat 于 2018 年被 IBM 以 320 亿美元收购。MuleSoft 也在同年被 Salesforce 以 65 亿美元收购。

所以,今天的开源并不是来自那些创造替代性、自由软件的革命性思维。如今,推动云服务商软件和平台服务的大多数顶尖软件都是开源软件(Volpi M. 2019)。

开源与开放开发的区别

因此,OSS 指的是在一种许可证下发布的计算机程序,许可证授予用户使用、研究、修改和共享该软件及其源代码的权利。

但将源代码公开并采用 copyleft 许可只是第一步。如果一家公司想要获得开源的所有好处,它必须采纳开源的价值观,这就引出了一个概念——开放开发开放式开发。这意味着你不仅仅是提供源代码的访问权限,而是必须使整个开发和产品管理过程透明化。这包括以下内容:

  • 要求

  • 架构与研究

  • 会议

  • 标准

.NET 团队是一个很好的例子,他们在 Twitch 和 YouTube 上进行社区站立会议(见 dotnet.microsoft.com/live/community-standup)。

开放开发也意味着创造一个开放和包容的环境,让每个人都能安全地提出更改建议。这包括强有力的道德准则和清晰的代码库,代码库具有高度的自动化,能够让每个人都能快速且轻松地贡献代码。

公司采纳开源的好处

那么,开源是如何与更好的开发绩效相关联的?贵公司如何从一个好的开源策略中受益?

使用开源软件更快交付

根据不同来源,新产品中已经有 70%到 90%的代码是开源的。这意味着你将自己编写 70%到 90%更少的代码,从而显著提高市场推出的速度。

除了在产品中重用开源代码,许多平台工具也作为开源提供。可重用的 GitHub Actions、测试工具或容器编排……你用来更快交付软件的最有效且最强大的工具,在大多数情况下,都是开源软件。

通过与社区互动,打造更好的产品

如果你在公开的环境中开发产品的某些部分,你可以利用社区的集体智慧来构建更好、更安全的软件。它还帮助你从全球的优秀工程师那里获得关于你所做工作的早期反馈。

尤其是对于复杂、关键性和与安全相关的软件,参与社区通常会带来更好的解决方案:

"问题越大,开源开发者就像磁铁一样,越容易被吸引来解决它。"

(Ahlawat P., Boyne J., Herz D., Schmieg F., & Stephan M. (2021))

使用风险较低的过时工具

使用开源可以减少工具过时的风险。如果你自己构建工具,你必须自己维护它们——这并不是你的优先事项。使用来自小供应商的工具或让合作伙伴为你构建工具,则存在工具无法得到维护或合作伙伴退出市场的风险。投资开源工具可以显著降低这些风险。

吸引人才

让工程师能够在工作中利用开源并贡献于开源项目,可以对你的招聘能力产生重大影响。参与社区并在开源中发挥作用,将帮助你吸引人才。

影响新兴技术和标准

许多新兴技术和标准都是在开放环境中发展的。为这些倡议做出贡献,可以使你的公司有能力影响这些技术,并成为前沿发展的参与者。

通过学习开源项目来改进你的流程

当然,如果你拥抱开源,你的公司可以学习协作开发,并将这些原则应用于改善公司内部跨团队的协作(称为内源)。

实施开源策略

然而,尽管拥抱开源有很多好处,但也有一些风险需要应对。当你在产品和工具链中使用开源软件时,必须小心并确保遵守许可证规定。如果开源组件造成损害,你还需要承担责任,因为你没有供应商可以起诉。此外,如果你依赖太多依赖项——无论是直接还是间接——其中一个发生故障时,你也将面临风险。

注意

第十四章《保护你的代码》中,你将学习到,包中的 11 行代码和一个关于名称的冲突如何造成严重损害并影响到互联网的大片区域。

这就是为什么你的公司应该建立一个开源策略。该策略应明确开发者可以使用哪些类型的开源软件,且使用目的是什么。不同的目的可能有不同的规则。如果你希望将开源纳入产品,你需要某种治理机制来管理相关的风险。

策略还应该明确是否允许开发者在工作时间贡献开源代码,以及相关的条件。

我不会深入探讨策略的细节。这很大程度上取决于你如何计划使用开源,以及你如何开发和发布产品。只要确保你的公司有一个关于开源策略的文档——即使它很小。随着开源的成熟和经验增长,它将不断发展。

一个建议是实施一个中心或卓越社区,帮助你制定一个策略,开发者可以在有疑问或者不确定某个开源组件是否合规时寻求帮助(Ahlawat P., Boyne J., Herz D., Schmieg F., & Stephan M. 2021)。

开源与内源

开源的成功在于其开放和协作的文化。让合适的人在远距离异步合作,可以以最佳的方式解决问题。其原则如下:

  • 开放协作

  • 开放沟通

  • 代码评审

将这些原则应用于组织内的专有软件被称为内部源代码。这一术语由 Tim O'Reilly 于 2000 年提出。内部源代码是打破壁垒并促进跨团队和跨产品强大协作的好方法。

但与开源开放开发一样,仅仅让你的代码公开并不足以创造一个内部源代码文化。许多成功因素会影响内部源代码方法是否能成功:

  • 模块化产品架构:如果你的架构庞大且单一,这将限制人们的贡献。此外,代码的质量、文档的完善程度以及理解代码和贡献的速度都会对内部源代码的采纳产生重大影响。

  • 标准化工具与流程:如果每个团队都有自己的工具链和工作流,这将排除其他工程师的贡献。拥有一个共同的工程系统,以及类似的分支和 CI/CD 方法,能够帮助其他人专注于问题,而不会因为必须先学习其他工具和工作流而受到阻碍。

  • 自主性与自我组织:只要你的组织将需求推送给团队,且工程师们忙于赶工期,其他团队的贡献就不会发生。只有当团队能够自主地进行优先级排序,并以自我组织的方式工作时,他们才能有自由参与其他社区——包括开放源代码和内部源代码社区。

内部源代码有助于打破壁垒,提高工程效率。但这也与 DevOps 的成熟度息息相关。内部源代码是随着您的 DevOps 能力和开源成熟度的提高而发展的。因此,将其视为加速的输出,而不是输入。

注意

从技术角度讲,内部源代码通常通过在企业内部启用分叉来实现。这与您的分支工作流密切相关,我们将在第十一章中讨论,基于主干的开发

内部化的重要性

许多公司并不认为软件开发是其核心业务,因此它们倾向于外包。外包是指一家公司雇佣另一家公司或自由职业者来执行特定功能。外包通常不是一个坏主意:你有另一家专注于某一领域的公司为你完成工作,这样你就可以将人员和投资集中在核心产品上。专门的公司通常能更便宜、更好地完成工作——而自己培养这些技能可能需要大量时间和资金。

但是现在,软件基本上是所有产品的关键差异化因素。不仅是数字客户体验,智能制造或供应链管理也能为你带来竞争优势。定制软件正成为你核心业务的一部分。因此,许多公司已经有了内部开发策略——即招聘和雇佣内部的软件开发人员和 DevOps 工程师。

问题在于,软件开发人员和 DevOps 工程师的市场竞争非常激烈(所谓的人才争夺战)。这常常导致一个分散的局面,合作伙伴负责核心产品的开发,而开发人员则维护工具。

一个好的内部开发策略是问自己软件是否是你业务的核心——也就是说,是否能为你提供竞争优势:

  • 核心软件应由内部开发人员开发。如果你无法雇佣足够的技术熟练开发人员,你可以联合外包,并通过你信任的合作伙伴的工程师来增强你的团队。但最终目标应始终是用你自己公司的工程师替代这些外部开发人员。

  • 补充软件可以外包。在最佳情况下,你可以使用已经存在的产品。如果没有这样的产品,你可以让合作伙伴为你构建。此时,开源发挥了重要作用:你可以利用现有的开源解决方案,或者让合作伙伴在开源环境中构建解决方案。这可以降低你成为唯一客户并导致解决方案过时的风险。由于软件仅是你业务的补充部分,因此你并不介意其他公司是否也使用它。相反的是,你的软件被使用的越多,软件过时的风险越小。如果软件是以开放方式开发的,其质量也更可靠。

向其他公司或个人支付费用,让他们为你开发特殊的开源软件或为现有的开源解决方案添加功能并不常见。但随着越来越多的公司采取内部开发策略,以及人才争夺战的持续进行,这种做法将在未来几年大幅增加。

GitHub 赞助者

开源策略似乎与内部开发策略存在冲突。但问题更为复杂。对于核心软件来说,向开源项目贡献一个小功能可能比自己实现一个临时解决方案更有用。但在许多公司中,团队层面的自制还是购买决策总是倾向于选择自制,因为购买或资助某样东西的过程太复杂。一个好的内部开发策略应该始终包括一个轻量且快速的流程,并拥有一定的预算来投资工具和软件供应链。如果你的公司缺乏内部开发人员,购买软件或资助开源贡献者应该不成问题。

一个让你的团队能够投资开源项目的好方法是利用名为 GitHub Sponsors 的功能。它允许你投资你的产品所依赖的项目(即你的 软件供应链),并让这些项目继续发展。它还可以让维护者有自由编写新请求功能,而不需要你亲自实现它们。

一个积极的副作用是资助对开源社区可见。这是一种良好的营销方式,能够提升你公司的信誉,并帮助你吸引新人才

你可以在开发者或组织参与 GitHub Sponsors 项目时对其进行资助。你也可以代表你的组织进行资助。此资助可以是一次性支付或每月支付,并且会显示在你的个人资料或你组织的资料中(见图 5.1):

图 5.1 – 已启用 GitHub Sponsors 组织资料

图 5.1 – 已启用 GitHub Sponsors 组织资料

GitHub Sponsors 对用户账户的资助不收取任何费用,因此这些资助的 100% 会直接给到受资助的开发者或组织。

赞助层级

赞助者可以为资助设置不同的层级。这既可以用于一次性资助,也可以用于持续的每月支付(见图 5.2):

图 5.2 – 月度或一次性资助选项

图 5.2 – 月度或一次性资助选项

所有者可以设置最多 10 个月度资助层级和最多 10 个一次性支付的层级。这样他们可以将定制奖励与不同层级关联。例如,奖励可能如下:

  • 可见性:赞助者可以在网站或社交媒体上被提及。也可能会有徽章(如银牌、金牌和铂金赞助商),用以区分不同的赞助层级。

  • 访问权限:赞助者可以获得私有仓库或早期版本的访问权限。

  • 优先处理:来自赞助者的 Bug 或功能请求可以获得优先处理。

  • 支持:一些赞助者还会为解决方案提供一定程度的支持。

接下来我们来看看资助目标。

资助目标

赞助账户可以设置资助目标。目标可以基于赞助者的数量或每月赞助的美元金额,并显示在赞助页面上(见图 5.3):

图 5.3 – Python 每月目标为 $12,000 的资助计划

图 5.3 – Python 每月目标为 $12,000 的资助计划

资助目标可以与某些里程碑挂钩。例如,维护者可以设置一个目标金额,当他们辞去日常工作,开始全职投入项目时,目标金额达到时即可实现。组织也可以设置一个金额,用于雇佣新的开发者来帮助维护项目。

总结

在本章中,您了解了自由和开源软件历史价值观原则,以及它们对您的软件交付性能可能产生的影响。一个好的开源策略,结合良好的内包策略和您的团队能够资助和支持开源项目的能力,可以帮助您显著缩短市场时间,并让您的工程师专注于公司重要的功能。将这些原则应用于您的公司作为内源(inner source)可以帮助您建立一种协作文化,实现更好的跨团队协作。

在下一章中,我们将学习如何使用 GitHub Actions 进行自动化。

进一步阅读和参考资料

请参考以下资料,了解本章内容的更多信息:

第二部分:工程 DevOps 实践

第二部分 讲解了有效 DevOps 所需的最重要的工程实践。你将学习如何使用 GitHub Actions 自动化发布流水线和其他工程任务,如何与主干和功能开关协作,以及如何将安全性和质量保证向左移动。

本部分包括以下章节:

  • 第六章使用 GitHub Actions 实现自动化

  • 第七章运行你的工作流

  • 第八章使用 GitHub Packages 管理依赖关系

  • 第九章部署到任何平台

  • 第十章功能开关与功能生命周期

  • 第十一章基于主干的开发

第六章:使用 GitHub Actions 实现自动化

许多敏捷实践将工程实践视为不如管理和团队实践重要。然而,工程能力如持续集成CI)、持续交付CD)和基础设施即代码IaC)是更频繁、更稳定且更低风险发布的推动力(Humble, J., & Farley, D. 2010)。这些实践可以减少部署痛苦,从而减少加班和职业倦怠。

本质上,所有这些实践都涉及自动化:计算机执行重复性任务,以便人们可以专注于重要问题和创造性工作。

"计算机执行重复性任务,人类解决问题。"

Forsgren, N., Humble, J., & Kim, G. 2018

自动化对企业文化和工作方式有着巨大的影响,因为许多习惯都是为了避免手动的、重复的任务——特别是当这些任务容易出错时。在本章中,我将向你介绍 GitHub Actions——GitHub 提供的自动化引擎,你可以用它做的不仅仅是 CI/CD。

本章将涵盖以下主题:

  • GitHub Actions 概览

  • 工作流、管道和动作

  • YAML 基础

  • 工作流语法

  • 使用机密

  • 实践操作 – 你的第一个工作流

  • 实践操作 – 你的第一个动作

  • GitHub 市场

GitHub Actions 概览

GitHub Actions 是 GitHub 的本地自动化引擎。它允许你在 GitHub 上的任何事件触发工作流——不仅仅是提交到版本控制!GitHub 可以在问题状态改变或被添加到里程碑时触发工作流,在 GitHub 项目中移动卡片时触发,或者当有人点击你的仓库的Star时,或者当评论被添加到讨论时触发。几乎所有的操作都有触发器。这些工作流本身是为重用而构建的。你可以通过将代码放入一个仓库来构建可重用的动作。或者,你也可以通过GitHub Marketplacegithub.com/marketplace)分享动作,当前该市场上大约有 10,000 个动作。

这些工作流可以在每个主要平台的云端执行:Linux、macOS、Windows、ARM 和容器。你甚至可以配置和托管运行器——无论是在云端还是你的数据中心——而无需打开入站端口。

GitHub 学习实验室

学习 GitHub 的好地方是 GitHub Learning Lab (lab.github.com)。它是完全实操的,并通过问题和拉取请求进行自动化。有一个完整的学习路径是 DevOps with GitHub Actions (lab.github.com/githubtraining/devops-with-github-actions)。另外,你也可以参加单独的课程,如 GitHub Actions: Hello World (lab.github.com/githubtraining/github-actions:-hello-world)。所有课程都是免费的。试试看——特别是如果你是实操型学习者,并且没有 GitHub 经验。

工作流、管道和 actions

存放在仓库中的 .github/workflows 目录。工作流可以用来构建和部署软件到不同的环境或阶段,并且在其他 CI/CD 系统中通常被称为管道

runs-on 属性。作业默认并行运行。可以通过使用依赖关系(使用 needs 关键字)将它们串联在一起,按顺序执行。作业可以在特定环境中运行。环境是资源的逻辑分组。环境可以在多个工作流中共享,并且可以通过 保护规则 进行保护。

一个作业由一系列任务组成,这些任务被称为步骤步骤可以运行命令、脚本或GitHub ActionAction是工作流中可重用的部分。并不是所有的步骤都是 actions——但所有的 actions 都作为步骤在作业中执行。

下表显示了理解工作流时最重要的术语:

表格 6.1 —— GitHub Actions 的重要术语

表格 6.1 —— GitHub Actions 的重要术语

YAML 基础

工作流是编写在 .yml.yaml 扩展名的 YAML 文件中的。YAML(即 YAML Ain't Markup Language)是一种数据序列化语言,优化为可以直接被人类编写和读取。它是 JSON 的严格超集,但使用语法相关的换行符和缩进来代替大括号。像 Markdown 一样,它也与拉取请求非常兼容,因为变更始终是按行进行的。让我们看一些 YAML 基础知识,帮助你入门。

注释

YAML 中的注释以井号 # 开始:

# A comment in YAML

标量类型

可以使用以下语法定义单个值:

key: value

支持多种数据类型:

integer: 42
float: 42.0
string: a text value
boolean: true
null value: null
datetime: 1999-12-31T23:59:43.1Z

请注意,键和值可以包含空格,并且不需要引号。但是你可以使用单引号或双引号引用键和值:

'single quotes': 'have ''one quote'' as the escape pattern'
"double quotes": "have the \"backslash \" escape pattern"

跨越多行的字符串——例如脚本块——使用管道符号 | 和缩进:

literal_block: |
    Text blocks use 4 spaces as indentation. The entire
    block is assigned to the key 'literal_block' and keeps
    line breaks and empty lines.
    The block continuous until the next element.

集合类型

嵌套数组类型——也称为映射——通常用于工作流中。它们使用两个空格进行缩进:

nested_type:
  key1: value1
  key2: value2
  another_nested_type:
    key1: value1

序列在每个项目前使用破折号

sequence:
  - item1
  - item2

由于 YAML 是 JSON 的超集,因此你也可以使用 JSON 语法将序列和映射放在一行中:

map: {key: value}
sequence: [item1, item2, item3]

这应该足以帮助你开始在 GitHub 上编辑工作流。如果你想了解更多关于 YAML 的内容,可以查看 yaml.org/ 上的规范。现在,让我们来看看工作流语法。

工作流语法

你在工作流文件中看到的第一件事就是它的名称,该名称会在你的仓库的 Actions 下显示:

name: My first workflow

这个名称后面跟着触发器。

工作流触发器

触发器是on键的值:

on: push

触发器可以包含多个值:

on: [push, pull_request]

许多触发器包含其他可以配置的值:

on:
  push:
    branches:
      - main
      - release/**
  pull_request:
    types: [opened, assigned]

有三种类型的触发器:

  • Webhook 事件

  • 定时事件

  • 手动事件

push),如果你创建或更新一个拉取请求(pull_request),或者创建或修改一个问题(issues)。完整列表请访问 docs.github.com/en/actions/reference/events-that-trigger-workflows

定时事件使用与 cron 作业相同的语法。语法由五个字段组成,分别表示分钟(0 – 59)、小时(0 – 23)、日期(1 – 31)、月份(1 – 12 或 JAN – DEC)和星期几(0 – 6 或 SUN-SAT)。你可以使用下表中所示的操作符:

![表 6.2 – 定时事件的操作符]

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_Ch_6_Table_02.jpg)

表 6.2 – 定时事件的操作符

这里是一些示例:

on:
  schedule:
    # Runs at every 15th minute of every day
    - cron:  '*/15 * * * *'
    # Runs every hour from 9am to 5pm
    - cron:  '0 9-17 * * *'
    # Runs every Friday at midnight
    - cron:  '0 0 * * FRI'
    # Runs every quarter (00:00 on day 1 every 3rd month)
    - cron:  '0 0 1 */3 *'

手动事件允许你手动触发工作流:

on: workflow_dispatch

你可以配置 homedrive,并在工作流中使用 ${{ github.event.inputs.homedrive }} 表达式:

on:
  workflow_dispatch:
    inputs:
      homedrive:
        description: 'The home drive on the machine'
        required: true
        default: '/home'

你也可以使用 GitHub API 触发工作流。为此,你必须定义一个 repository_dispatch 触发器,并为你想要使用的事件指定一个或多个名称:

on:
  repository_dispatch:
    types: [event1, event2]

当发送HTTP POST请求时,工作流会被触发。以下是使用 curl 发送 HTTP POST 的示例:

curl \
  -X POST \
  -H "Accept: application/vnd.github.v3+json" \
  https://api.github.com/repos/<owner>/<repo>/dispatches \
  -d '{"event_type":"event1"}'

以下是使用 JavaScript 的示例(有关Octokit API 客户端的更多详细信息,请参阅 github.com/octokit/octokit.js):

await octokit.request('POST /repos/{owner}/{repo}/dispatches', {
  owner: '<owner>',
  repo: '<repo>',
  event_type: 'event1'
})

使用 repository_dispatch 触发器,你可以在任何系统中使用任何 Webhook 来触发你的工作流。这有助于你自动化工作流并集成其他系统。

工作流作业

工作流本身在 jobs 部分进行配置。作业是映射而非列表,默认情况下并行运行。如果你希望它们按顺序执行,可以使用 needs 关键字让一个作业依赖于其他作业:

jobs:
  job_1:
    name: My first job
  job_2:
    name: My second job
    needs: job_1
  job_3:
    name: My third job
    needs: [job_1, job_2]

每个作业都会在运行器上执行。运行器可以是自托管的,或者你可以从云端选择一个。云端为所有平台提供了不同版本。如果你希望始终使用最新版本,可以选择 ubuntu-latestwindows-latestmacos-latest。你将会在 第七章 运行你的工作流 中了解更多关于运行器的内容:

jobs:
  job_1:
    name: My first job
    runs-on: ubuntu-latest

如果你想以不同的配置运行工作流,可以使用 ${{ matrix.key }} 表达式:

strategy:
  matrix:
    os_version: [macos-latest, ubuntu-latest]
    node_version: [10, 12, 14]
jobs:
  job_1:
    name: My first job
    runs-on: ${{ matrix.os_version }}
    steps:
      - uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node_version }}

工作流步骤

一个作业包含一系列步骤,每个步骤可以运行一个命令:

steps:
  - name: Install Dependencies
    run: npm install

字面块允许你运行多行脚本。如果你希望工作流在不同于默认 shell 的 shell 中运行,你可以与其他值一起配置,例如working-directory

- name: Clean install dependencies and build
  run: |
    npm ci
    npm run build
  working-directory: ./temp
  shell: bash

以下是可用的 shells:

表 6.3 – 工作流中可用的 shells

表 6.3 – 工作流中可用的 shells

非 Windows 系统的默认 shell 是bash,并回退到sh。Windows 上的默认 shell 是cmd。你也可以使用command [options] {0}语法配置自定义 shell:

run: print %ENV
shell: perl {0}

大多数时候,你会重用步骤。可重用的步骤称为uses关键字,语法如下:

{owner}/{repo}@{ref}

{owner}/{repo}是 GitHub 上操作的路径。{ref}引用的是版本:它可以是一个label、一个branch,或是通过其哈希值引用的单个commit。最常见的应用是使用标签进行显式版本控制,带有主版本和次版本:

# Reference a version using a label
- uses: actions/checkout@v2
- uses: actions/checkout@v2.2.0
# Reference the current head of a branch
- uses: actions/checkout@main
# Reference a specific commit
- uses: actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953 675

如果你的操作与工作流位于同一存储库中,你可以使用相对路径引用该操作:

uses: ./.github/actions/my-action

你可以使用存储在容器注册表中的操作——例如 Docker Hub 或 GitHub Packages——使用docker//{image}:{tag}语法:

uses: docker://alpine:3.8

上下文和表达式语法

当我们查看矩阵策略时,你看到了一些表达式。表达式具有以下语法:

${{ <expression> }}

表达式可以访问上下文信息,并将其与运算符结合使用。提供上下文的不同对象包括matrixgithubenvrunner。例如,使用github.sha,你可以访问触发工作流的 commit SHA;使用runner.os,你可以获取 runner 的操作系统,而使用env,你可以访问环境变量。完整列表,请访问docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts

有两种语法可以用来访问上下文属性——字母语法或属性语法,后者更常用:

context['key']
context.key

根据键的格式,你可能需要使用第一个选项。如果键以数字开头或包含特殊字符,可能就是这种情况。

表达式通常用于if对象中,以在不同条件下运行作业:

jobs:
  deploy:
    if: ${{ github.ref == 'refs/heads/main' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying branch $GITHUB_REF"

你可以使用许多预定义的函数,例如contains(search, item)

contains('Hello world!', 'world')
# returns true

其他函数示例包括startsWith()endsWith()。还有一些特殊函数可以用来检查当前作业的状态:

steps:
  ...
  - name: The job has succeeded
    if: ${{ success() }}

只有在所有其他步骤都成功的情况下,这个步骤才会被执行。下表显示了所有可以用来响应当前作业状态的函数:

表 6.4 – 检查作业状态的特殊功能

表 6.4 – 检查作业状态的特殊功能

除了函数,你还可以结合上下文和函数使用运算符。下表显示了最重要的一些运算符:

表 6.5 – 表达式的运算符

表 6.5 – 表达式的运算符

要了解更多关于上下文对象和表达式语法的信息,请访问docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions

工作流命令

要从步骤内部与工作流交互,你可以使用echo命令,并通过发送字符串如::set-output name={name}::{value}到进程中。以下示例设置一个步骤的输出,并在另一个步骤中访问它。请注意如何使用步骤的 ID 来访问输出变量:

- name: Set time
  run: |
    time=$(date)
    echo '::set-output name=MY_TIME::$time'
  id: time-gen
- name: Output time
  run: echo "It is ${{ steps.time-gen.outputs.MY_TIME }}"

另一个示例是::error命令。它允许你将错误消息写入日志中。你可以选择设置文件名、行号和列号:

::error file={name},line={line},col={col}::{message}

你还可以写入警告和调试消息,分组日志行,或设置环境变量。有关工作流命令的更多详细信息,请访问docs.github.com/en/actions/reference/workflow-commands-for-github-actions

处理机密

所有自动化工作流中一个非常重要的部分是处理机密。无论你是部署应用程序还是访问 API——你总是需要凭据或密钥,而你必须小心处理它们。

在 GitHub 中,你可以在仓库级别、组织级别或环境级别安全存储机密。机密会被加密存储和传输,并且不会出现在日志中。

对于组织级别的机密,你可以定义哪些仓库可以访问该机密。对于环境级别的机密,你可以定义必需的审核者:只有在审核者批准工作流后,他们才能访问机密。

提示

秘密名称不区分大小写,只能包含普通字符([a-z][A-Z])、数字([0-9])和下划线字符(_)。它们不能以 GITHUB_ 或数字开头。

最佳实践是使用大写字母并通过下划线(_)分隔来命名机密。

存储你的机密

要存储加密的机密,你必须是仓库管理员角色的一部分。可以通过网页或 GitHub CLI 创建机密。

要创建新机密,请导航到设置 | 机密。机密被分为Actions(默认)、CodespacesDependabot 类别。要创建新机密,点击新建仓库机密,然后输入机密的名称和内容(参见图 6.1):

图 6.1 – 管理仓库机密

图 6.1 – 管理仓库机密

组织的机密大致相同。你可以在Settings | Secrets | New organization secret下创建机密,并将访问策略设置为以下任意一种:

  • 所有仓库

  • 私有仓库

  • 选择的仓库

当你选择选择的仓库时,你可以为单个仓库授予访问权限。

如果你更喜欢使用 GitHub CLI,可以使用gh secret set创建新的机密:

$ gh secret set secret-name

系统将提示你输入机密。你还可以从文件中读取机密,将其通过管道传递给命令,或将其指定为正文(-b--body):

$ gh secret set secret-name < secret.txt
$ gh secret set secret-name --body secret

如果机密是为某个环境设置的,你可以使用--env-e)参数来指定它。对于组织机密,你可以将其可见性(--visibility-v)设置为allprivateselected。对于selected,你必须使用--repos-r)指定一个或多个仓库:

$ gh secret set secret-name --env environment-name
$ gh secret set secret-name --org org -v private
$ gh secret set secret-name --org org -v selected -r repo

访问你的机密

你可以通过secrets上下文在工作流中访问机密。将其作为with:env:变量添加到工作流文件中的步骤。组织和仓库的机密在工作流运行排队时读取,而环境机密在引用该环境的作业开始时读取。

注意

GitHub 会自动从日志中移除机密。但请小心你在步骤中对机密的处理方式!

根据你的 Shell 和环境,访问环境变量的语法有所不同。在 Bash 中,它是$SECRET-NAME,在 PowerShell 中,它是$env:SECRET-NAME,在cmd.exe中,它是%SECRET-NAME%

以下是如何在不同的 Shell 中将机密作为输入或环境变量进行访问的示例:

steps:
  - name: Set secret as input
    shell: bash
    with:
      MY_SECRET: ${{ secrets.secret-name }}
    run: |
      dosomething "$MY_SECRET "
  - name: Set secret as environment variable
    shell: cmd
    env:
      MY_SECRET: ${{ secrets.secret-name }}
    run: |
      dosomething.exe "%MY_SECRET%"

注意

这些只是示例,向你展示如何将机密传递给操作。如果你的工作流步骤是run:步骤,你也可以直接访问机密上下文,${{secrets.secret-name}}。如果你想避免脚本注入,这种做法不推荐。但是,由于只有管理员才能添加机密,你可以考虑出于工作流可读性的考虑使用这种方式。

GITHUB_TOKEN机密

一个特殊的机密是GITHUB_TOKEN机密。GITHUB_TOKEN机密是自动创建的,可以通过github.tokensecrets.GITHUB_TOKEN上下文进行访问。即使工作流没有将其作为输入或环境变量提供,GitHub 操作仍然可以访问该令牌。该令牌可以用于身份验证,以便访问 GitHub 资源。默认权限可以设置为permissiverestricted,不过这些权限可以在工作流中调整:

on: pull_request_target
permissions:
  contents: read
  pull-requests: write
jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/labeler@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

你可以在此链接中找到更多关于GITHUB_TOKEN机密的信息:docs.github.com/en/actions/reference/authentication-in-a-workflow

实操——你的第一个工作流

这些理论应该足以让你入门。接下来的章节我们将深入探讨运行器、环境和安全性。如果你是 GitHub Actions 新手,现在正是时候创建你的第一个工作流和第一个 Action。

提示

你可以通过使用 GitHub 的代码搜索,并通过编程语言的 YAML (language:yml) 和工作流路径 (path:.github/workflows) 进行筛选,找到现有的 GitHub Actions 工作流模板。以下搜索将返回所有关于德国 Corona-Warn-App 的工作流:

language:yml path:.github/workflows @corona-warn-app

步骤如下:

  1. 通过访问 github.com/wulfland/getting-started 并点击右上角的 Fork 按钮来 fork 仓库。

  2. 在 fork 的项目中,点击 Actions。你应该能看到你可以使用的工作流模板。这些模板已针对仓库中的代码进行优化——在这个案例中是 .NET。选择 设置此工作流

图 6.2 – 为 .NET 设置 GitHub Action

图 6.2 – 为 .NET 设置 GitHub Action

  1. GitHub 创建一个工作流文件并打开编辑器。编辑器支持语法高亮和自动补全(按 Ctrl + Space)。你可以在市场中搜索 Actions。将 dotnet-version 设置为 3.1.x 并提交工作流文件:

图 6.3 – 设置版本并提交工作流文件

图 6.3 – 设置版本并提交工作流文件

  1. 工作流将自动触发,你可以在 Actions 下找到该工作流的运行情况。如果你打开它,你可以找到工作流中的作业以及其他头部信息:

图 6.4 – 工作流摘要页面

图 6.4 – 工作流摘要页面

  1. 点击作业以查看所有步骤的详情:

图 6.5 – 作业和步骤详情

图 6.5 – 作业和步骤详情

如果你更喜欢其他语言,你可以使用以下仓库,它使用 Java with Mavengithub.com/MicrosoftDocs/pipelines-java

选择工作流模板时,向下滚动至 持续集成工作流,然后点击 更多持续集成工作流…

选择 Java with Maven,工作流应该就能正常工作:

图 6.6 – 其他 CI 模板,例如 "Java with Maven"

图 6.6 – 其他 CI 模板,例如 "Java with Maven"

所有的内容都有模板。设置一个基本的工作流来构建你的代码应该是很简单的。

实践操作 – 你的第一个 Action

GitHub Actions 的强大之处在于其可重用性,因此理解如何创建和使用 Actions 至关重要。在这个实践练习中,你将创建一个在 Docker 容器内运行的容器 Action。

提示

你可以在docs.github.com/en/actions/creating-actions/creating-a-docker-container-action找到这个示例,并可以从那里复制并粘贴文本文件的内容。如果你愿意,也可以使用github.com/actions/container-action上的模板仓库并点击使用此模板。它会为你创建一个包含所有文件的仓库。

步骤如下:

  1. 创建一个名为hello-world-docker-action的新仓库并将其克隆到你的工作站。

  2. 打开终端并导航到仓库:

    $ cd hello-world-docker-action
    
  3. 创建一个名为Dockerfile的文件(没有扩展名)。将以下内容添加到该文件中:

    # Container image that runs your code
    FROM alpine:3.10
    # Copies your code file from your action repository to the filesystem path '/' of the container
    COPY entrypoint.sh /entrypoint.sh
    # Code file to execute when the docker container starts up ('entrypoint.sh')
    ENTRYPOINT ["/entrypoint.sh"]
    

将此entrypoint.sh文件放入容器中。如果容器被执行,它将运行entrypoint.sh

  1. 创建一个名为action.yml的新文件,内容如下:

    # action.yml
    name: 'Hello World'
    description: 'Greet someone and record the time'
    inputs:
      who-to-greet:  # id of input
        description: 'Who to greet'
        required: true
        default: 'World'
    outputs:
      time: # id of output
        description: 'The time we greeted you'
    runs:
      using: 'docker'
      image: 'Dockerfile'
      args:
        - ${{ inputs.who-to-greet }}
    

action.yml文件定义了操作,以及其输入和输出参数。

  1. 现在,创建entrypoint.sh脚本。该脚本将在容器中运行并调用其他二进制文件。将以下内容添加到该脚本中:

    #!/bin/sh -l
    echo "Hello $1"
    time=$(date)
    echo "::set-output name=time::$time"
    

输入参数作为参数传递给脚本,并通过$1访问。脚本使用set-output工作流命令将time参数设置为当前时间。

  1. 你必须使entrypoint.sh可执行。在非 Windows 系统上,你只需在终端运行以下命令,然后添加并提交更改:

    $ chmod +x entrypoint.sh
    $ git add .
    $ git commit -m "My first action is ready"
    

在 Windows 上,这将不起作用。但你可以在将文件添加到索引时将其标记为可执行:

$ git add .
$ git update-index --chmod=+x .\entrypoint.sh
$ git commit -m "My first action is ready"
  1. Action 的版本管理是通过 Git 标签完成的。添加一个v1标签并推送所有更改到远程仓库:

    $ git tag -a -m "My first action release" v1
    $ git push --follow-tags
    
  2. 你的操作现在已经准备好使用。返回到getting-started仓库中的工作流(.github/workflows/dotnet.yaml)并编辑文件。删除jobs下的所有内容(第 9 行),并用以下代码替换:

    hello_world_job:
      runs-on: ubuntu-latest
      name: A job to say hello
      steps:
      - name: Hello world action step
        id: hello
        uses: your-username/hello-world-action@v1
        with:
          who-to-greet: 'your-name'
      - name: Get the output time
        run: echo "The time was ${{ steps.hello.outputs.time }}"
    

现在,工作流调用你的操作(uses),并指向你创建的仓库(your-username/hello-world-action),后面跟着标签(@v1)。它将你的名字作为输入参数传递给操作,并接收当前时间作为输出,然后将其写入控制台。

  1. 保存文件后,工作流将自动运行。查看详细信息,检查日志中的问候语和时间。

    提示

    如果你想尝试其他类型的操作,可以使用现有模板。如果你想尝试action.yml文件(请参见docs.github.com/en/actions/creating-actions/creating-a-composite-action)。

    处理这些操作的方法是相同的——只是创建方式不同。

GitHub 市场

你可以使用 GitHub 市场(github.com/marketplace)来搜索可以在工作流中使用的动作。发布一个动作到市场非常容易,这也是为什么已经有近 10,000 个动作可用的原因。你可以按类别筛选动作,或者使用搜索栏限制显示的动作数量(见图 6.7):

图 6.7 – 市场中包含了近 10,000 个动作

图 6.7 – 市场中包含了近 10,000 个动作

该动作显示来自仓库的 README 和其他信息。你可以查看所有版本的完整列表,并获取有关如何使用当前版本的信息:

图 6.8 – 市场中的一个动作

图 6.8 – 市场中的一个动作

发布动作到市场非常容易。确保该动作位于公共仓库中,动作名称唯一,并且动作包含良好的 README。选择一个图标和颜色并将其添加到 action.yml 中:

branding:
  icon: 'award'  
  color: 'green'

GitHub 会自动检测到 action.yml 文件,并提供一个名为草拟发布的按钮。如果你选择将此动作发布到 GitHub 市场,你需要同意服务条款,并且你的动作将会检查是否包含所有必需的文件。此时,你可以选择一个标签或创建一个新的标签,并为发布添加标题和描述:

图 6.9 – 将动作发布到市场

图 6.9 – 将动作发布到市场

发布发布版本或将其保存为草稿。

市场正在快速增长,它使自动化变得简单,因为几乎每个任务都有对应的动作。

总结

在本章中,我解释了自动化的重要性,并向你介绍了 GitHub Actions,作为一个灵活且可扩展的自动化引擎。

在下一章,你将学习不同的托管选项以及如何托管工作流运行器。

进一步阅读

如需了解本章涉及的更多内容,请查阅以下参考文献:

第七章:运行你的工作流

在本章中,我将向你展示运行你的工作流的不同选项。我们将研究托管和自托管的运行器,我将解释如何使用不同的托管选项解决混合云场景或硬件在环测试。我还会向你展示如何设置、管理和扩展自托管的运行器,并展示如何进行监控和故障排除。

以下是本章将要涵盖的核心主题:

  • 托管运行器

  • 自托管的运行器

  • 通过运行器组管理访问权限

  • 使用标签

  • 扩展你的自托管运行器

  • 监控和故障排除

托管的运行器

我们在前一章中已经使用过托管运行器。托管运行器是 GitHub 托管的虚拟机,可用于运行你的工作流。这些运行器支持LinuxWindowsmacOS操作系统。

隔离与权限

工作流中的每个作业都在一个新的虚拟机实例中执行,并且是完全隔离的。你拥有完全管理员访问权限(在 Linux 上为无密码 sudo),并且在 Windows 机器上禁用了用户账户控制UAC)。这意味着你可以在工作流中安装任何需要的工具(这只会增加构建时间的代价)。

运行器还可以访问用户界面UI)元素。这使得你可以在运行器内部执行UI 测试,例如Selenium,无需通过其他虚拟机来执行这些测试。

硬件

GitHub 在Microsoft AzureStandard_DS2_v2虚拟机上托管 Linux 和 Windows 运行器。Windows 和 Linux 虚拟机的硬件规格如下:

  • 2 核 CPU

  • 7 GB 的内存

  • 14 GB 的 SSD 硬盘空间

MacOS 运行器托管在 GitHub 的 macOS 云上,具有以下硬件规格:

  • 3 核 CPU

  • 14 GB 的内存

  • 14 GB 的 SSD 硬盘空间

软件

表 7.1中,你可以看到当前可用镜像的列表:

表 7.1 – 当前可用的托管运行器镜像

表 7.1 – 当前可用的托管运行器镜像

你可以在 github.com/actions/virtual-environments 找到当前的列表和所有包含的软件。

这是你可以提交问题的代码库,如果你希望请求将某个工具作为默认工具安装。这个代码库还包含有关运行器所有重大软件更新的公告,你可以使用 GitHub 仓库的watch功能来接收新版本发布的通知。

网络

托管的运行器使用的 IP 地址会不定期变更。你可以通过 GitHub API 获取当前的列表:

curl \
  -H "Accept: application/vnd.github.v3+json" \
  https://api.github.com/meta

你可以在 docs.github.com/en/rest/reference/meta#get-github-meta-information 查找更多信息。

如果您需要一个允许列表来防止互联网访问您的内部资源,您可以使用这些信息。但请记住,每个人都可以使用托管运行器并执行代码!阻止其他 IP 地址并不能使您的资源更安全。不要将内部系统与这些您无法信任其安全性的 IP 地址对立!这意味着系统必须及时修补并具备安全的身份验证。如果做不到这一点,您必须使用自托管运行器。

注意

如果您为您的 GitHub 组织或企业账户使用了 IP 地址允许列表,则不能使用 GitHub 托管的运行器,必须使用自托管的运行器。

定价

对于公共仓库,使用托管运行器是免费的。根据您的 GitHub 版本,您将获得一定的存储量和每月免费的构建分钟(见 表 7.2):

表 7.2 – 不同 GitHub 版本中包含的存储和构建分钟

表 7.2 – 不同 GitHub 版本中包含的存储和构建分钟

如果您通过 Microsoft 企业协议 购买了 GitHub Enterprise,您可以将您的 Azure 订阅 ID 连接到 GitHub Enterprise 账户。这使得您可以为额外的 GitHub Actions 使用付费,除此之外还包括您 GitHub 版本中提供的内容。

在 Windows 和 macOS 运行器上运行的作业消耗的构建分钟比 Linux 更多!Windows 消耗的分钟是 因素 2,而 macOS 是 因素 10。这意味着使用 1,000 个 Windows 分钟将消耗您账户中 2,000 个分钟,而使用 1,000 个 macOS 分钟将消耗 10,000 个分钟。

这是因为构建分钟的费用更高。您可以支付超出 GitHub 版本中包含的分钟的额外费用。以下是每个操作系统的构建分钟费用:

  • 在 Linux 上:$0.008

  • 在 macOS 上:$0.08

  • 在 Windows 上:$0.016

    提示

    您应该尽可能多使用 Linux 进行工作流,并将 macOS 和 Windows 降到最低,以减少构建成本。Linux 还具有最佳的启动性能。

额外存储的费用在所有运行器中都是相同的,收费为每 GB $0.25。

如果您是按月计费的客户,您的账户将默认设置为 $0(美元)的支出限额。这可以防止使用额外的构建分钟或存储空间。如果您按发票付款,您的账户将默认没有支出限额。

如果您配置的支出限额高于 $0,您将在超过账户中包含的分钟或存储量后被计费,直到达到支出限额为止。

自托管运行器

如果你需要比 GitHub 托管的 runner 提供更多的控制(例如硬件、操作系统、软件和网络访问),可以自行托管 runner。自托管的 runner 可以安装在物理机器、虚拟机或容器中。它们可以在本地或任何公共云环境中运行。

自托管的 runner 允许从其他构建环境轻松迁移。如果你已经有自动化构建,只需在机器上安装 runner,代码就应该能够构建。但如果你的构建机器仍然是手动维护的“脚踏式”机器——有时还放置在开发者桌面之外——那么这并不是一个永久解决方案。请记住,构建和托管一个动态扩展的环境需要专业知识并且耗费资金,无论它是托管在云端还是本地。因此,如果可以使用托管的 runner,通常这是更简单的选择。然而,如果你需要一个自托管的解决方案,请确保将其设计为具有弹性可扩展的解决方案。

注意

托管你自己的 runner 使你能够在 GitHub Enterprise Cloud 内的本地环境中安全地构建和部署。这使你能够以 混合模式 运行 GitHub——也就是说,你可以将 GitHub Enterprise 与托管的 runner 一起使用进行基本的自动化和云环境的部署,但使用自托管的 runner 来构建或部署托管在本地的应用程序。这可能比运行 GitHub Enterprise Server 和为所有构建和部署创建自己托管的构建环境更便宜、更简单。

如果你依赖硬件来测试软件(例如,使用硬件在回路测试时),就必须使用自托管的 runner。这是因为无法将硬件连接到 GitHub 托管的 runner 上。

runner 软件

该 runner 是开源的,可以在 github.com/actions/runner 上找到。它支持 Linux、macOS 和 Windows 上的 x64 处理器架构。它还支持 ARM64 和 ARM32 架构,但仅限于 Linux。该 runner 支持多种操作系统,包括 UbuntuRed Hat Enterprise Linux 7 或更高版本、Debian 9 或更高版本、Windows 7/8/10Windows ServermacOS 10.13 或更高版本等。有关完整列表,请参见 docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#supported-architectures-and-operating-systems-for-self-hosted-runners 中的文档。

runner 会自动更新,因此你无需自己管理此事。

runner 与 GitHub 之间的通信

runner 软件使用 443 端口通过出站连接轮询 GitHub。它会保持连接 50 秒,如果没有响应则超时。

您必须确保机器可以访问以下网址:

github.com
api.github.com
*.actions.githubusercontent.com
github-releases.githubusercontent.com
github-registry-files.githubusercontent.com
codeload.github.com
*.pkg.github.com
pkg-cache.githubusercontent.com
pkg-containers.githubusercontent.com
pkg-containers-az.githubusercontent.com
*.blob.core.windows.net

您无需在防火墙上打开任何入站端口。所有通信都通过客户端进行。如果您为 GitHub 组织或企业使用 IP 地址允许列表,您必须将自托管运行器的 IP 地址范围添加到该允许列表中。

在代理服务器后使用自托管运行器

如果您需要在代理服务器后运行自托管运行器,可以这样做。但请注意,这可能会导致很多问题。运行器本身可以正常通信——然而,软件包管理、容器注册表以及所有由运行器执行且需要访问资源的操作都会增加额外开销。如果您能避免这种情况,我建议您这么做。但如果您必须在代理服务器后运行工作流,您可以通过以下环境变量配置运行器:

  • https_proxy:此项包括 HTTPS(端口443)流量的代理 URL。您还可以包含基本身份验证(例如 https://user:password@proxy.local)。

  • http_proxy:此项包括 HTTP(端口80)流量的代理 URL。您还可以包含基本身份验证(例如 http://user:password@proxy.local)。

  • no_proxy:此项包括应绕过代理服务器的主机的以逗号分隔的列表。

如果更改环境变量,您必须重新启动运行器才能使更改生效。

使用环境变量的替代方法是使用.env文件。在运行器的应用程序文件夹中保存一个名为.env的文件。之后,语法与环境变量相同:

https_proxy=http://proxy.local:8081
no_proxy=example.com,myserver.local:443

接下来,我们来看看如何将自托管运行器添加到 GitHub。

将自托管运行器添加到 GitHub

您可以在 GitHub 上以不同级别添加运行器:仓库、组织和企业。如果在仓库级别添加运行器,它们将专用于该单个仓库。组织级别的运行器可以处理一个组织中多个仓库的工作,而企业级别的运行器可以分配给您企业中的多个组织。

安装运行器并将其注册到 GitHub 实例上非常简单。只需进入设置 | 操作 | 运行器,在您想要添加它们的级别。然后,选择操作系统和处理器架构(参见图 7.1):

图 7.1 – 安装自托管运行器

图 7.1 – 安装自托管运行器

这将为您生成一个脚本,执行以下操作:

  1. 下载并解压运行器

  2. 配置运行器的相应值

  3. 启动运行器

脚本的第一部分始终创建一个名为actions-runner的文件夹,然后将工作目录更改为该文件夹:

$ mkdir actions-runner && cd actions-runner

在 Linux 和 macOS 上,使用curl命令下载最新的运行器包,在 Windows 上使用Invoke-WebRequest

# Linux and macOS:
$ curl -o actions-runner-<ver>.tar.gz -L https://github.com/actions/runner/releases/download/<ver>/actions-runner-<ver>.tar.gz
# Windows:
$ Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/<ver>/actions-runner-<ver>.zip -OutFile actions-runner-<ver>.zip

出于安全原因,会验证下载包的哈希值,以确保包没有被篡改:

# Linux and macOS:
$ echo "<hash> actions-runner-<ver>.tar.gz" | shasum -a 256 -c
# Windows:
$ if((Get-FileHash -Path actions-runner-<ver>.zip -Algorithm SHA256).Hash.ToUpper() -ne '<hash>'.ToUpper()){ throw 'Computed checksum did not match' }

然后,运行器会从 ZIP/TAR 文件中提取:

# Linux and macOS:
$ tar xzf ./actions-runner-<ver>.tar.gz
# Windows:
$ Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD/actions-runner-<ver>.zip", "$PWD")

配置通过使用 config.sh / config.cmd 脚本完成,网址和令牌由 GitHub 自动为您创建:

# Linux and macOS:
$ ./config.sh --url https://github.com/org --token token
# Widows:
$ ./config.cmd --url https://github.com/org --token token

配置会要求输入运行器组(默认是 Default 组)、运行器的名称(默认是机器名称)以及额外的标签。默认标签用于描述自托管状态、操作系统和处理器架构(例如,self-hostedLinuxX64)。默认工作文件夹是 _work,不应更改。在 Windows 上,您还可以选择将操作运行器作为服务运行。在 Linux 和 macOS 上,您必须在配置后使用另一个脚本安装服务:

$ sudo ./svc.sh install
$ sudo ./svc.sh start

如果您不想将运行器作为服务运行,您可以使用 run 脚本以交互方式运行它:

$ ./run.sh
$ ./run.cmd

如果运行器正在运行,您可以在设置 | 操作 | 运行器下看到它,并查看其状态和标签(见图 7.2):

图 7.2 – 自托管运行器及其标签和状态

图 7.2 – 自托管运行器及其标签和状态

现在让我们学习如何从 GitHub 中移除这些自托管的运行器。

移除自托管运行器

如果您想重新配置或移除 GitHub 上的运行器,您必须使用带有 remove 选项的 config 脚本。如果您通过点击运行器名称查看其详细信息,您将看到一个移除按钮(见图 7.2)。点击此按钮后,它会为您生成脚本和令牌。

图 7.3 – 运行器详细信息

图 7.3 – 运行器详细信息

对于不同的操作系统,脚本看起来如下所示:

# Linux and macOS
./config.sh remove --token <token>
# Windows
./config.cmd remove --token <token>

在销毁机器之前,务必先移除运行器!如果您忘记这样做,您仍然可以在移除对话框中使用强制移除此运行器按钮。但这应仅作为最后手段。

使用运行器组管理访问权限

如果您在组织或企业级别注册运行器,Default 是无法删除的。

注意

一个运行器只能属于一个运行器组。

要管理访问权限,请在企业级别打开策略,或在组织级别打开设置,并在菜单中找到操作 | 运行器组。在这里,您可以创建一个新的运行器组,或者点击现有的运行器组以调整其访问设置。根据您的级别是企业级别还是组织级别,您可以选择允许对特定的组织或仓库访问(见图 7.3):

图 7.4 – 运行器组的选项

图 7.4 – 运行器组的选项

警告

默认情况下,公共仓库的访问权限是禁用的。请保持这个设置!你不应该在公共仓库中使用自托管运行器!Fork 的仓库可能会在你的运行器上执行恶意代码,因此这是一个风险。如果你需要为公共仓库使用自托管运行器,请确保使用临时强化的运行器,它们无法访问你的内部资源。如果你需要为一个开源项目使用特定的工具,而这些工具在托管运行器上安装太慢,这种情况可能适用。但这种情况较为少见,你应尽量避免。

当你注册一个新的运行器时,会要求你输入运行器组的名称。你也可以将此作为参数传递给config脚本:

$ ./config.sh --runnergroup <group>

现在我们已经学习了如何通过运行器组来管理访问权限,接下来我们将学习如何使用标签。

使用标签

GitHub Actions 通过搜索正确的标签将你的工作流与运行器匹配。在注册运行器时会应用这些标签。你也可以将它们作为参数传递给config脚本:

$ ./config.sh --labels self-hosted,x64,linux

你可以稍后通过点击标签旁边的齿轮图标来修改标签,并在运行器的详细信息中创建新标签(见图 7.4):

图 7.5 – 为运行器创建新标签

图 7.5 – 为运行器创建新标签

如果你的工作流有特定的需求,你可以为其创建自定义标签。自定义标签的一个例子可以是为工具添加标签,比如matLab或者必要的gpu访问权限。

所有自托管运行器默认都有self-hosted标签。

要在工作流中使用运行器,你需要通过标签的形式指定需求:

runs-on: [self-hosted, linux, X64, matlab, gpu]

这样,你的工作流就能找到满足必要需求的相应运行器。

扩展你的自托管运行器

在现有构建机器上安装操作运行器可以轻松迁移到 GitHub。但这不是一个长期解决方案!如果你不能使用托管运行器,你应该自行构建一个弹性扩展的构建环境。

临时运行器

如果你为构建机器或容器构建了一个弹性扩展解决方案,你应该使用临时运行器。这意味着你使用一个虚拟机或Docker镜像从空白镜像开始,并安装一个临时运行器。然后,运行结束后所有内容都会被删除。我们不推荐使用持久运行器的弹性扩展解决方案!

要将你的运行器配置为临时,你需要将以下参数传递给config脚本:

$ ./config.sh --ephemeral

使用 GitHub webhooks 进行扩展

为了上下扩展你的虚拟环境,你可以使用workflow_job webhook,如果新的工作流被排队,queued操作键会被调用。你可以使用这个事件启动一个新的构建机器并将其添加到机器池中。如果工作流运行完成,workflow_job webhook 会调用completed操作。你可以利用这个事件清理并销毁机器。

更多信息,请参见文档:docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_job

现有解决方案

@jonico构建的弹性虚拟构建环境中,已整理了一个包含所有现有解决方案的矩阵。你可以在github.com/jonico/awesome-runners找到这个仓库。矩阵以 GitHub 页面的形式呈现,通常更易于阅读,因此你可能更愿意访问jonico.github.io/awesome-runners。该矩阵根据目标平台、是否支持 GitHub Enterprise、自动扩展能力、清理因素及其他标准来比较这些解决方案。

提示

请记住,使用自定义镜像构建和运行可扩展的构建环境需要大量时间和精力,这些时间和精力本可以用于其他事务。使用托管运行器是更便宜、更可持续的解决方案。在进行投资前,确保你确实需要为自己的平台做出这样的投入。通常,托管自己的运行器还有其他选择——例如将自己的 Docker 镜像带入 GitHub Actions,或者使用机器人自动化部署到本地资源。

监控与故障排除

如果你在使用自托管运行器时遇到问题,有几个方面可以帮助你进行故障排除。

检查运行器的状态

你可以在IdleActiveOffline状态下检查运行器的状态。如果运行器状态是Offline,可能是机器关机、未连接网络,或自托管运行器应用未在机器上运行。

审查应用程序日志文件

日志文件保存在运行器的根目录中的_diag文件夹下。你可以查看运行器的_diag。应用程序日志文件名以Runner_开头,并附加有 UTC 时间戳:

Runner_20210927-065249-utc.log

审查工作日志文件

_diag。每个工作都有自己的日志。应用程序日志文件名以Worker_开头,并附加有 UTC 时间戳:

Worker_20210927-101349-utc.log

检查服务状态

如果你的运行器作为服务运行,你可以根据操作系统检查服务状态。

Linux

在 Linux 上,你可以从运行器文件夹中的.service文件中获取服务名称。使用journalctl工具来监控运行器服务的实时活动:

$ sudo journalctl -u $(cat ~/actions-runner/.service) -f 

你的服务配置可以在/etc/systemd/systemd/下检查和定制:

$ cat /etc/systemd/system/$(cat ~/actions-runner/.service)

macOS

在 macOS 上,你可以使用svc.sh脚本检查服务的状态:

$ ./svc.sh status

上述脚本的输出包含服务名称进程 ID

要检查服务配置,请在以下位置找到文件:

$ cat /Users/<user_name>/Library/LaunchAgents/<service_name>

Windows

在 Windows 上,你可以使用PowerShell来获取关于服务的信息:

$ Get-Service "action*"

使用EventLog监控你服务的最近活动:

Get-EventLog -LogName Application -Source ActionsRunnerService

监控跑步器更新过程

跑步器应自动更新自身。如果更新失败,跑步器将无法运行工作流。你可以在Runner_*日志文件中检查其更新活动,日志文件位于_diag目录下。

案例研究

Tailwind Gears的两个试点团队在新平台上开始了他们的第一个冲刺。他们自动化的第一件事是构建过程,以便所有的拉取请求在合并前都能被构建。Tailwind Gears 尽可能多地使用 GitHub 托管的跑步器。大多数软件构建都非常顺利。然而,一些用C编写的代码使用了旧版编译器,并且在当前的构建机器上安装了其他依赖项。这些代码目前在两台由开发人员自己维护的本地Jenkins服务器上构建。这些服务器还连接着用于硬件在环测试的硬件设备。为了顺利过渡,自托管的跑步器被安装在这些机器上,并且构建过程正常。IT 部门反正打算淘汰本地服务器,因此他们与 GitHub 合作,构建了一个弹性、可扩展、基于容器的解决方案,可以运行具有访问附加硬件的自定义镜像。

摘要

在本章节中,你了解了两种运行工作流的托管选项:

  • GitHub 托管的跑步器

  • 自托管跑步器

我们解释了自托管跑步器如何允许你在混合云场景中运行 GitHub。你学习了如何设置自托管跑步器,并了解了在哪里可以找到帮助你构建弹性可扩展构建环境的信息。

在下一章中,你将学习如何使用GitHub Packages管理你的代码依赖。

深入阅读

有关本章节内容的更多信息,你可以参考以下资源:

第八章:使用 GitHub Packages 管理依赖关系

使用包注册表来管理依赖关系应该是显而易见的选择。如果你编写 .NET 代码,你会使用 NuGet;如果你编写 JavaScript 代码,可能会使用 npm;如果你使用 Java,通常是 Maven 或 Gradle。然而,我遇到过许多团队仍然使用文件系统或 Git 子模块在多个代码库中重用代码文件,或者构建程序集并将它们存储在源代码管理中。转向使用带有语义版本控制的包是简单且低成本的,而且能够提升共享代码的质量和可发现性。

在本章中,我将向你展示如何像管理软件供应链一样使用 GitHub Packages 管理内部依赖关系。主要内容如下:

  • GitHub Packages

  • 使用 npm 包与 Actions

  • 使用 Docker 和 Packages

  • Apache Maven、Gradle、NuGet 和 RubyGems 包

    语义版本控制

    1.0.01.5.99-beta。格式如下:

    <major>.<minor>.<patch>-<pre>

    [0-9A-Za-z-])。文本越长,预发布版本越小(意味着 -alpha < -beta < -rc)。预发布版本始终比正常版本小(1.0.0-alpha < 1.0.0)。

    请参阅 semver.org/ 获取完整规范。

使用包并不意味着你自动使用了松耦合架构。在大多数情况下,包仍然是强依赖关系。是否能真正解耦发布节奏取决于你如何使用这些包。

GitHub Packages

GitHub Packages 是一个托管和管理你的包、容器及其他依赖项的平台。

你可以将 GitHub Packages 与 GitHub Actions、GitHub APIs 和 Webhooks 集成。这使你能够创建一个端到端的工作流来发布和使用你的代码。

GitHub Packages 目前支持以下注册表:

  • 容器 注册表支持 DockerOCI 镜像

  • package.json)

  • nupkg)

  • pom.xml)

  • build.gradle)

  • Gemfile)

定价

公共包是免费的。对于私有包,每个 GitHub 版本包括一定量的存储和数据传输。超过该限额的任何使用将单独收费,并且可以通过支出限制进行控制。

月度计费的客户默认有 $0 美元的支出限额,这会阻止额外使用存储或数据传输。开具发票的客户则有无限的默认支出限额。

每个产品的包含存储和传输数据量列在 表 8.1 中:

表 8.1 – GitHub 产品中包的包含存储和数据传输

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/Table_8.1.jpg)

表 8.1 – GitHub 产品中包的包含存储和数据传输

所有由 GitHub Actions 触发的外部数据传输都是免费的。任何来源的内部数据传输也都是免费的。

当达到包含的限制时,将收取以下费用:

  • 存储:每 GB $0.25 美元

  • 数据传输:每 GB $0.50 美元

欲了解更多关于定价的信息,请参阅 docs.github.com/en/billing/managing-billing-for-github-packages/about-billing-for-github-packages

权限和可见性

发布到仓库的包继承该仓库的权限和可见性。当前,只有容器包提供详细的权限和访问控制(见图 8.1)。

图 8.1 – 管理对容器包的访问

图 8.1 – 管理对容器包的访问

所有其他包类型遵循仓库作用域包的仓库访问权限。在组织级别,包是私有的,所有者具有写权限,成员具有读权限。

如果你对容器镜像具有管理员权限,你可以将容器镜像的访问权限设置为privatepublic。公开镜像允许匿名访问,无需认证。你还可以为容器镜像设置与组织和仓库级别的权限分开的访问权限。

在组织级别,你可以设置成员可以发布的容器包类型。你还可以查看和恢复已删除的包(见图 8.2)。

图 8.2 – 组织级别的包权限

图 8.2 – 组织级别的包权限

对于由用户帐户拥有的容器镜像,你可以为任何人分配访问角色。对于由组织发布并拥有的容器镜像,你只能为组织中的人员或团队分配访问角色。

欲了解更多关于权限和可见性的信息,请参阅 docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility

使用 npm 包与 Actions

使用 GitHub Actions 设置包的发布工作流非常简单。你可以使用GITHUB_TOKEN进行认证,并使用包管理器的原生客户端。要尝试使用 npm,你可以按照这里的逐步说明进行操作:github.com/wulfland/package-demo

如果你的机器上已经安装了 npm,你可以使用npm init来创建包。否则,只需从上述仓库复制package.jsonpackage-lock.json的内容。

发布包的工作流很简单。每次创建新版本时,它都会被触发:

on:
  release:
    types: [created]

工作流由两个作业组成。第一个作业仅使用 npm 构建和测试包:

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 12
      - run: npm ci
      - run: npm test

第二个操作将图像发布到注册表。这个操作需要写入包和读取内容的权限。它使用${{ secrets.GITHUB_TOKEN }}来进行注册表认证:

  publish-gpr:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 12
          registry-url: https://npm.pkg.github.com/
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

这个工作流很简单,每次在 GitHub 中创建新发布时,它都会将新包发布到你的 npm 注册表中。你可以在Code | Packages下找到包的详细信息和设置(参见图 8.3)。

图 8.3 – 包的详细信息和设置

图 8.3 – 包的详细信息和设置

然后,你可以在其他项目中使用npm install @<owner-name>/<package-name>来使用该包。

注意

请注意,包的版本不是标签或发布版本,而是package.json文件中的版本。如果在创建第二个发布版本之前没有更新版本,工作流将会失败。

如果你想自动化此过程,有一些操作可以帮助你。你可以使用github.event.release.name或标签(github.event.release.tag_name),并将包版本设置为此:

- name: 'Change NPM version'
  uses: reedyuk/npm-version@1.1.1
  with:
    version: ${{github.event.release.tag_name}}

如果你想要一种更灵活的方法,根据标签和分支计算语义版本号,可以使用GitVersion(请参见gitversion.net/)。GitVersionGitTools操作的一部分(请参见github.com/marketplace/actions/gittools)。

对于 checkout 操作的fetch-depth参数并将其设置为0

    steps:
      - uses: actions/checkout@v2
        with: 
          fetch-depth: 0

接下来,安装execute操作。如果你想获取语义版本的详细信息,请设置id

      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v0.9.7
        with:
          versionSpec: '5.x'
      - name: Determine Version
        id:   gitversion
        uses: gittools/actions/gitversion/execute@v0.9.7

计算出的最终语义版本号将作为环境变量$GITVERSION_SEMVER存储。你可以将其用作例如npm-version的输入。

注意

请注意,GitVersion支持配置文件,以了解它应该如何计算版本!有关更多信息,请参见gitversion.net/

如果你需要访问gitversion任务中的详细信息:

  - name: Display GitVersion outputs
    run: |
      echo "Major: ${{ steps.gitversion.outputs.major }}"

使用GitVersion,你可以扩展工作流,从分支或标签创建包——不仅限于发布版本:

on:
  push: 
    tags:
      - 'v*'
    branches:
      - 'release/*'

构建一个带有自动语义版本控制的发布工作流是复杂的,并且在很大程度上取决于你使用的工作流和包管理器。本章应能帮助你入门。此技巧也可以应用于NuGetMaven或任何其他包管理器。

使用 Docker 与包

GitHub 的容器注册表是ghcr.io。容器镜像可以由组织或个人帐户拥有,但你可以自定义每个镜像的访问权限。默认情况下,镜像继承了运行工作流的仓库的可见性和权限模型。

如果你想亲自试一下,可以在这里找到逐步指南:github.com/wulfland/container-demo。按照这些步骤了解构建过程的工作原理:

  1. 创建一个名为container-demo的新仓库,并添加一个非常简单的Dockerfile(没有扩展名):

    FROM alpine
    CMD ["echo", "Hello World!"]
    

该 Docker 镜像继承自 alpine 发行版,并将Hello World!输出到您的控制台。如果您是 Docker 新手并想尝试,可以克隆仓库并将目录切换到本地仓库的根目录。然后为容器构建镜像:

$ docker build -t container-demo 

然后运行容器:

$ docker run --rm container-demo

--rm参数在完成时自动移除容器。这应该会将Hello World!输出到您的控制台。

  1. 现在在.github/workflows/目录下创建一个名为release-container.yml的工作流文件。每当创建新的发布时,工作流将被触发:

    name: Publish Docker image
    on:
      release:
        types: [published]
    

注册表和镜像名称被设置为环境变量。我使用仓库名称作为镜像名称。您也可以在这里设置固定名称:

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

该工作需要对packages具有写权限,并且需要克隆仓库:

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

docker/login-action使用GITHUB_TOKEN对工作流进行身份验证。这是推荐的做法:

- name: Log in to the Container registry
  uses: docker/login-action@v1.10.0
  with:
    registry: ${{ env.REGISTRY }}
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

metadata-action从 Git 上下文中提取元数据,并将标签应用于 Docker 镜像。当我们创建发布时,我们推送一个标签(refs/tags/<tag-name>)。该操作将创建一个与 Git 标签同名的 Docker 标签,并为镜像创建最新标签。请注意,元数据作为输出变量传递给下一个步骤!这就是为什么我为此步骤设置了一个id

- name: Extract metadata (tags, labels)
  id: meta
  uses: docker/metadata-action@v3.5.0
  with:
    images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

build-push-action构建镜像并将其推送到容器注册表。标签和标签从meta步骤的输出中提取:

- name: Build and push Docker image
  uses: docker/build-push-action@v2.7.0
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
  1. 创建一个新的发布和标签以触发工作流。工作流完成后,您可以在Code | Packages下找到包的详细信息和设置(参见图 8.4)。

图 8.4 – 容器包的详细信息和设置

图 8.4 – 容器包的详细信息和设置

如果您创建了新的发布,GitHub 现在将创建一个新的 Docker 镜像并将其添加到注册表中。

  1. 您可以从注册表将容器拉取到本地并运行:

    $ docker pull ghcr.io/<user>/container-demo:latest
    $ docker run --rm ghcr.io/<user>/container-demo:latest
    > Hello World!
    

请注意,如果您的包不是公开的,在拉取镜像之前,您必须使用docker login ghcr.io进行身份验证。

容器注册表是发布软件的好方法。从命令行工具到完整的微服务,您可以将软件及其所有依赖项一起发布,供其他人使用。

Apache Maven、Gradle、NuGet 和 RubyGems 包

其他包类型与 npm 和 Docker 基本相同:如果您了解本地包管理器,它们非常容易使用。我将简要介绍每种类型。

使用 Apache Maven 的 Java

对于pom.xml文件:

<distributionManagement>
  <repository>
    <id>github</id>
    <name>GitHub Packages</name>
    <url>https://maven.pkg.github.com/user/repo</url>
  </repository>
</distributionManagement>

然后,您可以使用GITHUB_TOKEN在工作流中发布您的包:

- name: Publish package
  run: mvn --batch-mode deploy
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

要从您的开发机器获取包,您必须使用read:packages作用域进行身份验证。您可以在 GitHub 中的~/.m2/settings.xml文件下生成新的令牌。

有关更多信息,请参阅 docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry

Gradle

build.gradle文件中,你可以从环境变量中读取用户名和访问令牌:

repositories {
  maven {
    name = "GitHubPackages"
    url = "https://maven.pkg.github.com/user/repo"
    credentials {
      username = System.getenv("GITHUB_ACTOR")
      password = System.getenv("GITHUB_TOKEN")
    }
  }
}

在工作流中,你可以使用gradle publish进行发布:

- name: Publish package
  run: gradle publish
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

有关更多详细信息,请参阅 docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry

RubyGems

如果你想构建并发布你仓库中的所有.gemspec文件,可以使用市场中的一个操作:

- name: Build and publish gems got .gemspec files
  uses: jstastny/publish-gem-to-github@master
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    owner: OWNER

要使用包,你需要至少 RubyGems 2.4.1 和 bundler 1.6.4。修改~/.gemrc文件并通过提供你的用户名和个人访问令牌,将注册表作为源来安装包:

---
:backtrace: false
:bulk_threshold: 1000
:sources:
- https://rubygems.org/
- https://USERNAME:TOKEN@rubygems.pkg.github.com/OWNER/
:update_sources: true
:verbose: true  

要使用bundler安装包,你还需要配置它与你的用户和令牌:

$ bundle config \
https://rubygems.pkg.github.com/OWNER \
USERNAME:TOKEN

有关更多详细信息,请参阅 docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-rubygems-registry

NuGet

要发布setup-dotnet操作。它有额外的参数source-url。令牌通过环境变量设置:

- uses: actions/setup-dotnet@v1
  with:
    dotnet-version: '5.0.x'
    source-url: https://nuget.pkg.github.com/OWNER/index.json
  env:
    NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

然后,你可以构建并测试你的项目。之后,只需打包并将包推送到注册表:

- run: |
  dotnet pack --configuration Release
  dotnet nuget push "bin/Release/*.nupkg"

要安装包,你必须将注册表作为源添加到nuget.config文件中,包括你的用户和令牌:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <packageSources>
        <add key="github" value="https://nuget.pkg.github.com/OWNER/index.json" />
    </packageSources>
    <packageSourceCredentials>
        <github>
            <add key="Username" value="USERNAME" />
            <add key="ClearTextPassword" value="TOKEN" />
        </github>
    </packageSourceCredentials>
</configuration>

有关更多信息,请参阅 docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry

摘要

使用包是直接的,最大的挑战是身份验证。但通过在 GitHub Actions 中使用GITHUB_TOKEN,你可以轻松设置一个完全自动化的发布工作流。这就是为什么它对你的团队来说是工具箱中不可或缺的一部分。如果你将代码作为容器或包共享,并使用语义版本控制和单独的发布流程,许多发布代码的问题可以减少。

本章中,你已了解如何使用语义版本控制和包来更好地管理你的内部依赖项并共享代码。你已了解包是什么,以及如何为每种包类型设置发布工作流。

在下一章,我们将更详细地探讨环境以及如何使用 GitHub Actions 将应用部署到任何平台。

进一步阅读

有关本章中主题的更多信息,请参考以下内容:

第九章:部署到任何平台

现在你已经学会了如何使用 GitHub Actions 作为自动化引擎,并使用 GitHub Packages 轻松分享代码和容器,我们可以通过自动化部署来完成我们的持续集成/持续交付CI/CD)能力。

在本章中,我将向你展示如何以安全且合规的方式轻松部署到任何云或平台。

在本章中,我们将涵盖以下主要主题:

  • 分阶段部署

  • 自动化你的部署

  • 基础设施即代码

  • 如何部署到 Azure 应用服务

  • 如何部署到 AWS 弹性容器服务ECS

  • 如何部署到 Google Kubernetes 引擎GKE

  • 测量成功

    CI/CD

    CI 意味着每次将代码更改推送到你的仓库时,代码都会被构建和测试,并且输出将打包为构建工件。在 CD 中,每当创建新的构建工件时,你将自动将构建工件部署到环境中。

    在实践 CI/CD 时,开发和交付阶段是完全自动化的。代码随时可以部署到生产环境。

    有多种定义区分持续交付持续部署(两者都是CD)——但这些定义在文献中并不一致,并且对主题的贡献微乎其微。

分阶段部署

一个 DevelopmentTestStaging(或 Pre-Production)和 Production。通常,StagingPre-Production 阶段是生产环境的完整镜像,有时,使用负载均衡切换这两个环境来实现零停机时间部署。通常,越接近生产环境的阶段需要手动批准才能部署。

如果公司使用功能标志(请参见 第十章功能标志与功能生命周期)和 CD,通常阶段的数量会减少。我们可以讨论基于环的部署扩展单元,而不是使用阶段。基于环的部署的想法是,你有客户处于不同的生产环中。你将更新部署到一个环,并自动监控系统是否出现意外异常或异常指标,如 CPU 或内存使用情况。此外,你可以在生产环境中运行自动化测试。如果没有错误,发布过程将持续进行并部署到下一个环。在讨论基于环的部署时,通常意味着没有手动批准。然而,环之间也可以进行手动批准。

在 GitHub 中,你可以使用环境进行分阶段和基于环的部署。你可以在你的仓库的设置 | 环境中查看、配置或创建新的环境。

对于每个环境,你可以定义以下内容:

  • 必需的审查员:这些审查员包括最多五个用户或团队作为手动批准者。在执行部署之前,必须由其中一位批准者批准部署。

  • 等待计时器:指的是部署执行前的宽限期。最大时间为 43,200 分钟或 30 天。此外,如果你在之前的阶段发现任何错误,可以使用 API 来取消部署。

  • 受保护的分支 或者定义你自己的模式。该模式可以包含通配符(例如 release/*)。

  • 环境密钥:环境中的密钥会覆盖仓库或组织范围内的密钥。密钥只有在必需审核员批准部署后才会加载。

配置看起来类似于 图 9.1

图 9.1 – 在 GitHub 中配置环境

图 9.1 – 在 GitHub 中配置环境

在工作流文件中,你需要在作业级别指定环境:

jobs:
  deployment:
    runs-on: ubuntu-latest
    environment: prod

此外,你还可以指定一个 URL,该 URL 将显示在概览页面上:

jobs:
  deployment:
    runs-on: ubuntu-latest
    environment: 
      name: production
      url: https://writeabout.net

使用 needs 关键字,你可以定义作业之间的依赖关系,因此也定义了环境之间的依赖(见 图 9.2):

图 9.2 – 阶段性部署的概览页面

图 9.2 – 阶段性部署的概览页面

环境的状态也会显示在仓库的主页上(见 图 9.3):

图 9.3 – 主页上的环境

图 9.3 – 主页上的环境

如果你想在不同环境中进行实验,你可以在 github.com/wulfland/AccelerateDevOps/ 的分支上运行 阶段性部署 工作流,并将自己添加为某些阶段的必需审核员。

自动化你的部署

如果我问我的客户是否自动化了他们的部署,通常的回答是 。然而,仔细一看,自动化意味着 我们有一个脚本,或者 我们有一个安装程序的回答文件。这只是部分自动化。只要有人需要登录服务器、创建账户或 DNS 记录,或手动配置防火墙,那么你的部署就没有完全自动化!

人类会犯错——机器不会!确保你自动化了部署的所有步骤,而不仅仅是最后几个步骤。由于 GitHub Actions 是完美的自动化引擎,执行所有自动化部署的工作流是一个很好的做法。

如何部署到 Azure App Service

为了帮助你快速入门使用 GitHub Actions 进行自动化部署,我创建了三个实操实验:

  • 部署到 Azure App Service

  • 部署到 AWS ECS

  • 部署到 GKE

所有实操实验假设你已经在指定的云平台上设置了帐户。如果你采用单云策略,你可以直接跳到与你相关的实操步骤,跳过其他部分。

实操实验的逐步指导位于 GitHub,链接为 github.com/wulfland/AccelerateDevOps/blob/main/ch9_release/Deploy_to_Azure_App_Service.md。建议按照那里提供的步骤进行操作,因为它提供了易于复制和粘贴的链接。这里,我将以逐步指南的形式,重点介绍如何部署应用程序。

部署 Azure 资源

Azure 资源的部署发生在 setup-azure.sh 脚本中。该脚本创建资源组、应用服务计划和应用服务。你可以轻松地在工作流中执行该脚本。部署完成后,我们从 Web 应用中获取 publish 配置文件并将其存储在 GitHub 的密钥中。你可以通过 Azure 门户或 Azure CLI 获取发布配置文件:

$ az webapp deployment list-publishing-profiles \
    --resource-group $rgname \
    --name $appName \
    --xml

使用 GitHub Actions 部署应用程序

工作流由两个任务组成:BuildDeploy。构建任务为正确的 dotnet publish 配置运行器,将网站发布到名为 publish 的文件夹中:

- name: Build and publish with dotnet
  working-directory: ch9_release/src/Tailwind.Traders.Web
  run: |
    dotnet build --configuration Release
    dotnet publish -c Release -o publish

下一步将工件上传到 GitHub,以便在后续任务中使用。这允许你将相同的包发布到多个环境:

- name: Upload Artifact
  uses: actions/upload-artifact@v2
  with:
    name: website
    path: ch9_release/src/Tailwind.Traders.Web/publish

此外,你还可以在工作流完成后查看并检查工件(见 图 9.4):

图 9.4 – 工作流工件

图 9.4 – 工作流工件

Deploy 任务依赖于 Build,并将应用程序部署到 prod 环境。在环境内,你设置了密钥并添加了一个必要的审阅者:

Deploy:
  runs-on: ubuntu-latest
  environment: prod
  needs: Build

工作流将工件名为 website 的文件下载到一个名为 website 的文件夹中:

- uses: actions/download-artifact@v2
  with:
    name: website
    path: website

然后,它使用 azure/webapps-deploy 操作,使用发布配置文件部署网站:

- name: Run Azure webapp deploy action using publish profile credentials
  uses: azure/webapps-deploy@v2
  with:
    app-name: ${{ env.appName }}
    slot-name: Production
    publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE }}
    package: website

最后一步仅是一个示例,展示如何验证部署。当然,你需要使用 curl 请求一个同时也指向数据库的站点 URL:

u=https://${{ env.appName }}.azurewebsites.net/
status=`curl --silent --head $u | head -1 | cut -f 2 -d' '`
if [ "$status" != "200" ]
then
  echo "Wrong HTTP Status. Actual: '$status'"
  exit 1
fi

如果你完成了实操实验中的逐步指南,你将拥有一个可以添加额外环境并部署到不同应用服务部署槽的游乐场(更多信息,请访问 docs.microsoft.com/en-us/azure/app-service/deploy-staging-slots)。

如何部署到 AWS ECS

我们将部署相同的代码到 AWS,但这次,我们将通过 Docker 容器部署到 ECS。ECS 是一种高度可扩展的容器管理服务,允许你在集群中运行、停止和管理容器。你可以在 github.com/wulfland/AccelerateDevOps/blob/main/ch9_release/Deploy_to_AWS_ECS.md 上找到逐步指导。

以下是一些附加说明和背景信息。

AWS 资源的部署

我找不到一个简单的脚本,可以将所有内容部署到 AWS,而且不包含一些复杂的 JSON。这就是为什么我使用实验中的手动步骤。首先,你需要创建一个 Access Key IDSecret Access Key

在第一次部署之后,容器已经在注册表中,你可以通过向导与它一起设置你的 ECS 资源。

你需要提取任务定义并将其保存到 aws-task-definition.json 文件中。工作流第二次运行时,它会成功地将容器部署到 ECS。

使用 GitHub Actions 部署容器

我还将工作流分为 Build 阶段和 Deploy 阶段。这样可以让你轻松地在之后添加环境和更多的阶段。为了使其生效,你必须将镜像名称从 Build 作业传递给 Deploy 作业。为此,你可以使用 job outputs

jobs:
  Build:
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.build-image.outputs.image }}

为了配置认证,我们使用 configure-aws-credentials 操作,传入 Access Key IDSecret Access Key 值。

请注意,GitHub 会隐藏部分镜像名称,并不会将其传递到下一个作业。为了避免这种情况,你必须防止 configure-aws-credentials 操作隐藏你的账户 ID:

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ${{ env.AWS_REGION }}
    mask-aws-account-id: no

登录 ECR 返回你在后续操作中使用的注册表名称:

- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v1

在下一步中,你构建镜像并将其推送到 ECR。此外,你设置了下一个作业的输出:

- name: Build, tag, and push image to Amazon ECR
  id: build-image
  env:
    ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
    IMAGE_TAG: ${{ github.sha }}
  working-directory: ch9_release/src/Tailwind.Traders.Web
  run: |
    imagename=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
    echo "Build and push $imagename"
    docker build -t $imagename .
    docker push $imagename
    echo "::set-output name=image::$imagename"

下一个作业依赖于 Build,并在 prod 环境中运行:

Deploy:
  runs-on: ubuntu-latest
  environment: prod
  needs: Build

此外,它需要配置 AWS 凭证,然后使用通过 needs 上下文传递给作业访问的镜像名称配置 aws-task-definition.json 文件:

- name: Fill in the new image ID in the ECS task definition
  id: task-def
  uses: aws-actions/amazon-ecs-render-task-definition@v1
  with:
    task-definition: ${{ env.ECS_TASK_DEFINITION }}
    container-name: ${{ env.CONTAINER_NAME }}
    image: ${{ needs.Build.outputs.image }}

最后一步是使用前一步任务的输出部署容器:

- name: Deploy Amazon ECS task definition
  uses: aws-actions/amazon-ecs-deploy-task-definition@v1
  with:
    task-definition: ${{ steps.task-def.outputs.task-definition }}
    service: ${{ env.ECS_SERVICE }}
    cluster: ${{ env.ECS_CLUSTER }}
    wait-for-service-stability: true

如果你按照逐步指南操作,你将拥有一个分阶段的工作流,能够部署到 ECS。你可以添加更多阶段,并在不同的服务中运行不同版本的容器。

如何部署到 GKE

我们也将相同的代码部署到 GKE。你可以在 github.com/wulfland/AccelerateDevOps/blob/main/ch9_release/Deploy_to_GKE.md 找到操作步骤。

在执行这些操作步骤之前,这里有一些关于发生的事情的详细信息。

Google 资源的部署

完整的部署过程发生在你在 Cloud Shell 中执行的 setup-gke.sh 脚本中。该脚本会创建一个包含一个节点的 GKE 集群。为了测试目的,这足够了:

gcloud container clusters create $GKE_CLUSTER --num-nodes=1

此外,脚本还创建了一个用于 Docker 容器的制品库和一个用于执行部署的服务账户。

在 Kubernetes 中,有一个名为 Deployment.yaml 的概念。部署定义了容器并将其绑定到镜像:

spec:
  containers:
  - name: $GKE_APP_NAME
    image: $GKE_REGION-docker.pkg.dev/$GKE_PROJECT/$GKE_PROJECT/$GKE_APP_NAME:$GITHUB_SHA
    ports:
    - containerPort: 80
    env:
      - name: PORT
        value: "80"

我在文件中使用环境变量,并在将它们传递给 kubectl apply 命令之前,使用 envsubst 进行替换:

envsubst < Deployment.yml | kubectl apply -f -

一个服务暴露了 Pods —— 在这个例子中,暴露到互联网。该服务使用 Service.yml 文件以相同的方式进行部署:

spec:
  type: LoadBalancer
  selector:
    app: $GKE_APP_NAME
  ports:
  - port: 80
    targetPort: 80

服务的部署需要一些时间。你可能需要多次执行以下命令:

$ kubectl get service

如果你获得了外部 IP 地址,可以使用它来测试你的部署(见 图 9.5):

图 9.5 – 获取 GKE 负载均衡器的外部 IP

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_09_005.jpg)

图 9.5 – 获取 GKE 负载均衡器的外部 IP

服务帐户的凭证保存在key.json文件中。你必须对它们进行编码,并将它们保存为 GitHub 中的加密密钥,名为GKE_SA_KEY

$ cat key.json | base64

脚本已经完成了这个步骤。所以,你只需要复制输出并将其粘贴到密钥中。

使用 GitHub Actions 部署容器

GitHub Actions 工作流中的部署非常直接。gcloud CLI 的身份验证和设置发生在setup-gcloud操作中:

- uses: google-github-actions/setup-gcloud@v0.2.0
  with:
    service_account_key: ${{ secrets.GKE_SA_KEY }}
    project_id: ${{ secrets.GKE_PROJECT }}
    export_default_credentials: true

然后,工作流会构建并将容器推送到注册表。它使用gcloud进行 Docker 注册表的身份验证:

gcloud auth configure-docker \
    $GKE_REGION-docker.pkg.dev \
    --quiet

要将新镜像部署到 GKE,我们使用get-gke-credentials操作进行身份验证:

- uses: google-github-actions/get-gke-credentials@v0.2.1
  with:
    cluster_name: ${{ env.GKE_CLUSTER }}
    location: ${{ env.GKE_ZONE }}
    credentials: ${{ secrets.GKE_SA_KEY }}

接下来,我们只需替换部署文件中的变量,并将它们传递给kubectl apply

envsubst < Service.yml | kubectl apply -f -
envsubst < Deployment.yml | kubectl apply -f –

就这样。按照这些操作步骤,你应该能成功部署到 GKE!

部署到 Kubernetes

部署到 Kubernetes 可能非常复杂;不过,这超出了本书的范围。你可以使用不同的策略:重建滚动更新(也称为渐进更新)、蓝绿部署金丝雀部署A/B 测试。一个很好的起点是官方文档,可以在kubernetes.io/docs/concepts/workloads/controllers/找到。此外,关于这些策略的实用可视化以及如何执行部署的实际示例,可以在github.com/ContainerSolutions/k8s-deployment-strategies找到。

在与 Kubernetes 配合使用时,还有许多其他工具可供利用。例如,Helmhelm.sh/)是 Kubernetes 的包管理器,而 Kustomizekustomize.io/)是一个帮助你管理多个配置的工具。

基础设施即代码

基础设施即代码IaC)是通过机器可读文件来管理和提供所有基础设施资源的过程。通常,这些文件会进行版本控制,并像代码一样在 Git 中管理。在这种情况下,通常称为 GitOps

IaC 可以是命令式的、声明式的,或者两者的结合。命令式意味着文件是过程性的,例如脚本,而声明式则是指使用 YAML 或 JSON 等标记语言描述期望状态的功能性方法。为了充分发挥IaC的能力,你应该以一种可以应用更改而不仅仅是完成配置和撤销配置的方式来管理它。这通常被称为持续配置自动化CCA)。

工具

有许多工具可以用于IaCCCA。例如,有些是云特定工具,如Azure ARMBicepAWS CloudFormation。然而,也有许多独立的工具可以用于本地基础设施。以下是一些最受欢迎的工具:

  • Puppet:这是由 Puppet 于 2005 年发布的 (puppet.com)。

  • Chef:这是由 Chef 于 2009 年发布的 (www.chef.io)。

  • Ansible:这是由 RedHat 于 2021 年发布的 (www.ansible.com)。

  • Terraform:这是由 HashiCorp 于 2014 年发布的 (www.terraform.io)。

  • Pulumi:这是由 Pulumi 于 2017 年发布的 (www.pulumi.com)。

    IaC 和多云部署

    请注意,支持多个云提供商的IaC 工具并不意味着它可以将相同的资源部署到多个云!这是一个常见的误解。你仍然需要编写特定于云的自动化脚本。但你可以使用相同的语法和工具。

这只是冰山一角。市场上有许多工具。找到最佳组合的过程可能非常复杂,超出了本书的范围。如果你采用单云策略,可能最好从云原生工具开始。如果你有一个复杂的环境,涉及多个云和本地资源,并希望用相同的工具进行管理,你必须投入时间做详细分析。

最佳实践

无论你使用什么工具,在实现IaC时,有一些事情需要考虑:

  • 将配置存储在 Git 中,并像管理代码一样使用受保护的分支、拉取请求和代码所有者。代码所有者是确保合规性的好方法,特别是当你将其存储在接近应用程序代码的位置时。

  • 使用 GitHub Actions 执行部署。虽然在编写和调试 IaC 时可以交互式发布资源,但一旦完成,你应该通过工作流实现完全自动化的发布。IaC 就是代码,和应用程序代码一样,从开发者机器上部署它存在无法复现的风险。

  • 秘密和密钥管理是 IaC 中最关键的部分。确保不要将它们保存在代码中,而是将它们存储在安全的地方(例如 GitHub Secrets)。像Hashicorp VaultAzure KeyVault这样的金库可以在其中一个秘密泄露时轻松进行密钥轮换。此外,它还将你的安全管理与资源配置解耦。

  • 如果可能,使用OpenID ConnectOIDC)。这是为了避免使用凭证来访问云资源,而是使用短期的令牌,这些令牌也可以轮换(更多信息请参考docs.github.com/en/actions/deployment/security-hardening-your-deployments)。

我在本书中使用了云原生工具。从这些工具过渡到IaCCCA工具比反过来要容易。

策略

关于如何以可管理、可扩展和符合要求的方式组织你的基础设施代码,有不同的策略。本质上,这取决于你的组织结构以及哪种方式最适合你。以下是一些常见策略:

  • 集中式:基础设施资源存放在中央仓库中,功能团队可以通过自服务(即触发工作流)从中配置。这种方法的优点是所有资源都集中在一个地方,责任单位可以对其进行强有力的控制。缺点是对开发人员来说灵活性较差,而且从代码到基础设施的距离会影响工程师对基础设施的处理方式。

  • 分散式:基础设施资源与代码一起存放。你可以使用模板(请参考工作流模板部分)帮助工程团队设置基础设施。此外,你还可以使用CODEOWNERS和受保护分支来要求由共享责任团队进行审批。这种方法非常灵活,但成本控制和治理更加困难。

你可以在每次构建时部署——或者确保基础设施处于正确的状态。但这样会减慢构建速度并消耗宝贵的构建时间。在大多数情况下,最好在需要时通过单独的工作流部署资源。

  • 模板化:负责共享基础设施的团队提供固定的模板,功能团队可以使用这些模板。这些模板可以是Actions,即预配置的本地动作的复合动作,或者是完全自定义的 Docker 或 JavaScript 动作。或者,你可以使用可复用的工作流(请参考可复用工作流部分)。无论哪种方式,被复用的工作流或动作的所有权仍然属于中央团队。如果你限制企业中允许的动作数量,这种方法效果很好。

  • 混合:这是前三种策略的结合。例如,测试和开发基础设施可以是去中心化的,而生产环境则可以是模板化的。

无论您使用哪种策略,都要有意识地去选择。解决方案将大大影响您的团队协作方式以及基础设施在价值交付中的使用!

工作流模板

.github 仓库中的 workflow-templates 文件夹,包含元数据文件和图标文件(见图 9.6):

图 9.6 – 组织的工作流模板

图 9.6 – 组织的工作流模板

模板本身是一个普通的工作流文件。您可以使用 $default-branch 变量在触发器中按默认分支进行过滤。

除模板外,您还需要保存一个 .svg 格式的图标和一个属性文件。属性文件如下所示:

{
    "name": "My Workflow Template",
    "description": "Description of template workflow",
    "iconName": "my-template",
    "categories": [
        "javascript"
    ],
    "filePatterns": [
        "package.json$",
        "^Dockerfile",
        ".*\\.md$"
    ]
}

在此,namedescriptioniconName 是必需的。请注意,iconName 值不包含扩展名。在 categories 数组中,您可以指定该工作流模板相关的编程语言。对于文件模式也是一样的:您可以为用户的仓库中的特定文件指定模式。如果仓库中包含与某个模式匹配的文件,则模板会被更加显著地展示。

现在,如果组织的用户创建了一个新的工作流,他们将看到该组织的模板(见图 9.7):

图 9.7 – 从模板创建工作流

图 9.7 – 从模板创建工作流

模板已被复制并可以修改!这就是为什么工作流模板不适合用于模板化策略的原因。

要了解更多关于工作流模板的信息,请访问 docs.github.com/en/actions/learn-github-actions/creating-workflow-templates

可重用工作流

workflow_call 触发器是可重用的:

on: 
  workflow_call:

您可以定义可以传递给工作流的输入。输入可以是 booleannumberstringsecret

on:
  workflow_call:
    inputs:
      my_environment:
        description: 'The environment to deploy to.'
        default: 'Prod'
        required: true
        type: string
    secrets:
      my_token:
        description: 'The token to access the environment'
        required: true

您可以通过 inputs 上下文(${{ inputs.my_environment }})访问可重用工作流中的输入,通过 secrets 上下文(${{ secrets.my_token }})访问机密。

要使用可重用工作流,您必须按以下格式引用该文件:

{owner}/{repo}/{path}/{filename}@{ref}

工作流在作业中被调用,您可以按如下方式指定输入和机密:

jobs:
  call-workflow-1:
    uses: org/repo/.github/workflows/reusable.yml@v1
    with: 
      my_environment: development
    secrets:
      my_token: ${{ secrets.TOKEN }}

可重用工作流非常适合避免重复。结合语义版本控制和标签,这是将可重用工作流发布到您组织中团队的绝佳方式。

要了解更多关于可重用工作流的信息,请访问 docs.github.com/en/actions/learn-github-actions/reusing-workflows

衡量成功

第一章重要的指标 中,我向你介绍了 四个关键指标仪表盘。这是一个显示 DORA 指标的仪表盘。如果你自动将代码部署到生产环境,现在是从调查数据转向真实的指标了。仪表盘是实现这一目标的一种方式。

要安装仪表盘,请按照 github.com/GoogleCloudPlatform/fourkeys/blob/main/setup/README.md 中的说明操作。

首先,在 Google Cloud 中创建一个启用计费的项目,并记下项目 ID(不是名称!)。然后,打开 Google Cloud Shell(位于 cloud.google.com/shell),克隆仓库并执行部署脚本:

$ git clone \
   https://github.com/GoogleCloudPlatform/fourkeys.git
$ cd fourkeys
$ gcloud config set project <project-id>
$ script setup.log -c ./setup.sh

脚本会问你一些问题,你可以根据这些问题来定制你的部署。如果一切顺利,你应该能在 Grafana 中看到一个漂亮的仪表盘。要配置 GitHub 将数据发送到 Google 的事件处理程序,你需要获取事件处理程序的端点和密钥。只需在 Cloud Shell 中执行以下两个命令并复制输出:

$ echo $(terraform output -raw event_handler_endpoint)
> https://event-handler-dup4ubihba-uc.a.run.app
$ echo $(terraform output -raw event_handler_secret)
> 241d0765b5a6cb80208e66a2d3e39d254051377f

现在,前往你希望将数据发送到仪表盘的 GitHub 仓库,在 设置 | Webhooks | 添加 webhook 下创建一个 webhook。将事件处理程序的 URL 和密钥粘贴到相应字段中,并选择 发送所有事件。点击 添加 webhook 以开始将所有事件发送到事件处理程序(见 图 9.8):

图 9.8 – 添加 webhook 以将数据发送到四个关键指标仪表盘

图 9.8 – 添加 webhook 以将数据发送到四个关键指标仪表盘

不幸的是,目前你只能将部署数据发送到仪表盘。在之前的版本中,你可以将单独的事件发送到工作流。

要表示现场问题,你必须将名为 Incident 的标签添加到一个打开的 issue 中。在正文中,添加 root cause: 后跟导致事件的提交的 SHA

四个关键指标仪表盘是查看你 DevOps 指标的一个好方法(见 图 9.9):

图 9.9 – 四个关键指标仪表盘

图 9.9 – 四个关键指标仪表盘

然而,别忘了这些指标不是用来比较团队之间的表现的。不要让指标成为最终目标!

案例研究

设置 CI 后,我们 Tailwind Gears 的两个试点团队接下来要做的事情是自动化软件的部署和发布流程。

第一个团队运行一些仍托管在本地的 Web 应用。团队决定将这些应用从本地部署迁移到云端的Kubernetes托管服务。集群实例、网络和其他云资源已由 IT 部门在过去的冲刺中设置好。因此,团队可以轻松地将部署转移到分阶段的部署流程中。他们将应用部署到测试实例,并运行所有自动化测试。同时,他们还添加了一个使用curl的测试,该测试调用一个网站,检查数据库和后端的可访问性,以确保一切正常。如果所有测试通过,部署将自动进行滚动更新,确保用户零停机时间地部署到生产环境。

某些包含共享关注点的 Web 应用代码需要调整以便在云端工作。这些代码也包含在其他团队的 Web 应用中。团队决定将这些代码迁移到GitHub Packages(JavaScript 使用NPM,.NET 使用NuGet),并拥有自己的发布周期和语义版本控制,以便将来其他团队在迁移到云端时能更方便地重用这些代码。

第二个团队为硬件产品开发软件,这些硬件产品用于机器中的安全关键功能。这意味着开发过程受到严格监管。他们需要对所有更改进行端到端的可追溯性。由于所有需求已导入 GitHub 问题,并通过嵌套问题进行链接,因此这不成问题。他们只需在提交信息中引用最低级别的问题。除了端到端的可追溯性外,还有一些不同级别需求的测试文档尚未实现自动化。另外,还有一些风险管理文档。为了确保在发布产品之前满足所有这些标准,必需的审阅者在部署到生产环境之前手动批准发布,以确保所有要求已到位并符合规范。结合受保护的分支代码所有者(必需的文档已经转换为 markdown),这减少了一次性发布大量内容的工作量。

二进制文件安装到硬件上是通过一款由公司拥有并在生产机器上运行的定制工具来执行的。该工具用于从文件共享中提取二进制文件。这对于端到端可追溯性并不理想,因为它依赖于日志文件。部署到测试环境是手动执行的,这意味着二进制文件的分发方式不一致。为了解决这个问题,团队将二进制文件和工具一起放入Docker 容器,并将镜像发布到 GitHub Packages 的容器注册表。然后,可以使用 Docker 镜像将版本传输到测试机器,并在组装过程中以相同方式进行操作。

总结

本章中,您学习了如何使用GitHub 环境来阶段性部署并保护您的部署,如何使用 GitHub Actions 安全地部署到任何云平台。我展示了如何使用工作流模板和可重用工作流来帮助您在IaC上进行协作。

在下一章中,您将学习如何使用FeatureFlags/FeatureToggles优化您的功能发布以及整个功能生命周期。

深入阅读

下面是本章中提到的参考资料列表,您也可以使用这些资料来获取更多关于我们讨论的主题的信息:

第十章:功能标志与功能生命周期

功能标志(Feature Flags)是我在多年与团队合作过程中见过的最具变革性的功能之一。它们有许多不同的用例。功能标志可以帮助你通过提前合并代码来减少开发工作流中的复杂性,或者帮助你执行零停机时间部署。功能标志帮助你通过管理整个功能生命周期来从功能中获得更多的价值。

在本章中,我将解释什么是功能标志——也称为功能切换(Feature Toggles)——以及你可以用它们做什么。不幸的是,GitHub 本身没有原生的解决方案来支持功能标志。市面上有许多框架和服务可以用来实现功能标志,但我会为你提供一些选择最佳工具的建议,帮助你根据用例来选择。

本章的主要内容如下:

  • 什么是功能标志(Feature Flags)?

  • 功能的生命周期

  • 功能标志的优势

  • 入门功能标志

  • 功能标志与技术债务

  • 使用功能标志进行实验

什么是功能标志(Feature Flags)?

功能标志(Feature Flags)是一种软件开发技术,允许在不更改代码的情况下修改运行时行为。它将功能发布与二进制文件的部署解耦,使得功能可以独立于代码更新进行发布。

功能标志像开关或切换一样工作,因此通常被称为功能切换(Feature Toggles)功能开关(Feature Switches),因为它们具有布尔性质。但功能标志有许多不同的用例,可能比简单的切换要复杂。因此,功能标志(Feature Flag)这一术语更为合适。

功能标志允许你将新代码封装在一个功能标志后面,并将其推广到生产系统。然后,可以根据特定目标受众的上下文来启用该功能(见图 10.1):

图 10.1 – 功能标志的工作原理

图 10.1 – 功能标志的工作原理

功能标志对于开发者来说是一种非常自然的技术,特别是如果你有持续交付功能,并且有一个独立的团队负责基础设施的话。对于开发者而言,向代码中添加标志比更改基础设施要容易得多,因此你通常会看到功能标志被用来让测试人员执行不同于普通用户的操作,或者让一些 beta 用户测试某些功能。问题在于,如果你没有明确规定功能标志,配置通常会分散在不同的地方:配置文件、组成员资格和应用程序数据库中。明确功能标志有助于提高团队透明度,确保统一的处理方式,同时支持更复杂的用例,确保安全性和可扩展性。

功能的生命周期

直到几年前,大多数软件每 1 到 2 年发布一次重大版本,这些版本通常需要单独购买,或者至少与订阅的许可证紧密捆绑。所有的新功能都会被挤进这些新版本中。新版本通常伴随着培训材料、书籍和在线课程,以教授用户使用新功能。

这些销售模式今天几乎已经不存在了。客户希望将软件作为服务使用。无论是像 Facebook 或 WhatsApp 这样的移动应用,还是像 Office 或 Windows 这样的桌面软件,软件都在持续更新和优化,并不断增加新功能。这带来了一个挑战:如何教育最终用户正确使用新功能。直观的用户体验和易于发现的新功能比旧的销售模式下更为重要。功能必须是自我解释的,屏幕上的简单对话框应该足以教导用户如何使用新功能。

此外,价值创造完全不同。客户不再每几年才做一次购买决定。他们每天都在决定是否使用软件来完成手头的任务。因此,重点不再是通过将大量新功能挤进新版本来影响购买决策,而是通过去除不使用的功能或优化它们直到具有高价值,从而提供少而精的高价值功能。

这意味着每个功能都需要经历一个生命周期。功能的生命周期可能类似于图 10.2所示:

图 10.2 – 特性生命周期

图 10.2 – 特性生命周期

生命周期包括以下几个阶段:

  • 构思与开发:新功能的构思之后,实施会从少数内部用户开始。利用这些用户的反馈来改进功能。

  • Alpha 或 beta:在 Alpha 和/或 Beta 阶段,该功能提供给更广泛但仍然非常有限的用户群体。这个用户群体可以是内部用户或选定的外部客户。Alpha 或 Beta 阶段可以是封闭(私密的)或开放(公开的),但该阶段的功能仍然非常动态,可能会发生显著变化。

  • 采纳:如果该功能足够成熟以进入市场,它将逐渐暴露给更广泛的用户群体。采纳阶段可以分为以下几个子阶段:

    1. 预览版:用户可以选择加入并启用预览功能。

    2. 新用户默认设置:该功能是新用户的默认设置,但用户仍然可以选择退出,如果他们不想使用该功能。

    3. 所有用户默认设置:该功能对所有用户启用,但用户仍然可以选择退出。

  • 正常运行:该功能被所有用户使用,且不再允许选择退出。功能的旧版本会从系统中移除。正常运行阶段可能会持续多年。

  • 日落:该特性被一个更新的、更有可能更好的特性所取代。使用该特性的用户数量逐渐下降,维护该特性的成本超过其价值。当所有用户都能转向新特性时,该特性会从系统中移除。

请注意,特性的价值在早期采用阶段最大,因为它吸引了新的用户到你的应用程序中。在正常操作阶段,热度可能已经平稳,竞争对手也从你的特性中学到了东西,并通过调整他们的软件做出了回应。

特性标志的好处

在没有使用特性标志的情况下,无法管理特性的生命周期,但还有许多其他的用例,特性标志可以为你的 DevOps 团队带来价值:

  • 发布标志:这些标志用于在标志后推出代码。发布标志通常会一直存在于代码中,直到特性完全推出。这可能是几周或几个月。发布标志会随着每次部署或系统配置的变化而变化。这意味着它们可以通过读取配置值轻松实现。但是,如果你想使用发布标志进行金丝雀发布(逐渐向更多用户曝光特性)或蓝绿部署(交换暂存和生产环境),它们会更具动态性。

  • 实验标志:如果你推出多个版本的相同特性,并将其暴露给不同的受众,这就叫做A/B 测试实验。通常用于通过测量用户与特性版本的交互情况来确认或减弱一个假设。实验标志是高度动态的,并依赖于大量的上下文来使用它们以应对不同的目标受众。

  • 权限标志:特性标志的一个常见用例是控制用户可以访问的内容。这可以是管理功能测试功能,仅暴露给特定受众,或高级功能,仅暴露给付费客户。权限标志是高度动态的,通常会在代码中存在很长时间——有时直到应用程序生命周期的结束。它们还存在较高的欺诈风险,因此必须谨慎使用。

  • 操作标志:有些标志用于应用程序的操作方面——例如,杀死开关用于禁用某些可能成为其他特性瓶颈的功能(也叫做断路器)。用于控制后端系统不同版本的标志也被认为是操作标志。多变体标志通常用于控制日志详细信息或其他操作方面。

图 10.3 展示了按动态性和在系统中存在的时间对不同类型的特性标志的概览:

图 10.3 – 特性标志类型

图 10.3 – 特性标志类型

现在我们已经了解了什么是功能标志以及您可以如何使用它们,我将向您展示如何在代码中实现它们。

开始使用功能标志

在代码中,功能标志只是一个if语句。假设您已经有了一个用于注册新用户对话框的当前实现:

function showRegisterDialog(){
    // current implementation
}

现在,您希望使用功能标志创建一个新的对话框,并能够在运行时打开新的对话框:

function showRegisterDialog(){
    var newRegisterDialog = false;
    if( newRegisterDialog ){
        return showNewRegisterDialog();
    }else{
        return showOldRegisterDialog();
    }
}
function showNewRegisterDialog(){
    // new implementation
}
function showOldRegisterDialog(){
    // old implementation
}

要动态启用或禁用功能,您必须将功能标志的验证提取到其自己的函数中:

function showRegisterDialog(){
    if( featureIsEnabled("new-register-user-dialog") ){
        return showNewRegisterDialog();
    }else{
        return showOldRegisterDialog();
    }
}

有许多选项可以存储功能标志的配置:

  • 系统配置

  • 用户配置

  • 应用程序数据库

  • 单独的数据库

  • 单独的系统(通过 API 访问)

这高度依赖于您的用例,哪些位置适用或不适用。

功能标志和技术债务

如果您开始使用功能标志,通常会得到一个高度可配置的系统,可以在运行时更改其行为 - 通常是通过分散在多个配置源中的许多标志。这些标志往往彼此之间存在依赖关系,因此启用或禁用标志会对系统的稳定性造成很大风险。您设法通过避免并行分支来避免合并地狱,但最终却陷入了功能标志地狱,数百个标志,没有人知道它们的用途。

要避免这种情况,您应该遵循以下最佳实践:

  • 度量:即使它们提供了所有的价值,功能标志在您的代码中也是某种技术债务。您应该像测量代码覆盖率或其他与代码相关的指标一样测量它们。测量功能标志的数量,它们存在的时长,它们在每个环境中的评估情况(,在生产中为 100%可能意味着可以移除该标志),以及标志被使用的频率(调用)。

  • 集中管理:在一个中心位置管理您的标志,特别是如果您使用不同的方法来管理这些标志。每个标志应该有一个所有者和一个描述。记录功能标志之间的依赖关系

  • 集成到您的流程中:将功能标志的管理集成到您的流程中。例如,如果您使用 Scrum,可以在评审会议中审查功能标志。确保所有定期使用标志的人都仔细查看所有标志,并检查哪些标志可以从系统中移除。

  • 作为临时标志的前缀tmp-和作为永久标志的前缀perm-。不要把它弄得太复杂,但标志的名称应立即表明它是何种类型的标志,以及它在代码库中应该存在多长时间。

一些团队喜欢,而另一些团队不喜欢的技术是清理分支。你可以看看这种技术是否适合你。其思路是,在你创建标志并编写代码的那一刻,你最清楚如果标志有一天被移除,代码应该是什么样子。因此,你可以创建一个清理分支并同时创建拉取请求,直到标志被移除为止。这个技术在有良好命名规范的情况下效果最好。

以之前的示例为例,你有一个用于新特性对话框的标志。带有标志的代码如下所示:

function showRegisterDialog(){
    if( featureIsEnabled("tmp-new-register-user-dialog") ){
        return showNewRegisterDialog();
    }else{
        return showOldRegisterDialog();
    }
}

代码是在features/new-register-dialog分支中开发的,你创建了一个拉取请求来合并代码。

你已经知道,当标志被移除后,代码的最终状态将只使用新对话框,因此你创建了一个新的分支(例如,cleanup/new-register-dialog)并添加了代码的最终版本:

function showRegisterDialog(){
    return showNewRegisterDialog();
}

然后,你可以创建一个拉取请求,并保持其打开,直到特性完全推出,你希望清理代码。

正如我所说,这种技术并不适合所有团队。在复杂环境中,维护清理分支可能是一项繁重的工作,但你可以尝试一下。

如果特性标志没有被清理,并且没有得到积极维护,那它就是技术债务,但其优点大于缺点。如果你从一开始就小心谨慎,你可以避免进入特性标志的地狱,并且在发布和操作你的应用程序时,只会受益于它们提供的灵活性。

框架和产品

在实现特性标志时,有许多可供利用的框架。最适合你的框架在很大程度上取决于你的编程语言和使用案例。有些框架更专注于 UI 集成,有些则更专注于发布和操作。在选择框架时,你应该考虑以下方面:

  • 性能:特性标志必须快速,不能降低应用程序的性能。应该使用适当的缓存,并且如果数据存储无法及时访问,也应使用默认值。

  • 支持的编程语言:你的解决方案应该适用于所有的编程语言,特别是当你使用客户端标志时;你还必须在服务器端评估它们以确保安全。你不想在不同的地方配置标志。

  • UI 集成:如果你想让用户能够选择是否启用某个功能,那么你需要一个良好的 UI 集成。通常,你需要两个标志:一个控制可见性,另一个启用或禁用功能。

  • 上下文:当你想使用特性标志进行 A/B 测试和实验时,你需要大量的上下文信息来评估标志:例如用户、组成员资格、地区和服务器。这是许多框架的弱点所在。

  • 中央管理:例如,为每个环境分别配置的标志是无法维护的。你需要一个中央管理平台,在一个地方控制所有的标志。

  • 数据存储:一些框架将配置存储在你的应用数据库中。这在许多场景下是有问题的。通常,你在所有环境中都有不同的数据库,因此在各个环境中管理设置是困难的。

构建一个可扩展、高性能且成熟的解决方案需要大量的时间和精力,即使是使用框架时也是如此,但也有一些现成的产品可以安装或作为服务使用。一个存在多年的、成熟的产品是LaunchDarkly (launchdarkly.com/)。现在有很多竞争者,包括以下几种:

Unleash (www.getunleash.io/) 也值得一提。它有一个开放核心 (github.com/Unleash/unleash),可以作为 Docker 容器免费自托管。Unleash 也是 GitLab 使用的解决方案。

我找不到一个好的资源来比较这些解决方案,所以我在 GitHub 上添加了一个页面 (wulfland.github.io/FeatureFlags/),提供了解决方案的独立比较。

在做决策时,关于自建与购买的选择,大多数公司更适合使用现有的服务或产品。构建和运行一个优秀的功能标志解决方案是困难且耗时的,特别是如果你对功能标志还不熟悉的话。从一个好的产品开始。如果经过一段时间后,你仍然觉得有必要自己构建解决方案,至少你已经有了了解一个解决方案应具备功能的经验。

使用功能标志进行实验

实验和 A/B 测试不仅可以通过功能标志进行。你也可以在不同的分支中开发容器,并使用 Kubernetes 在生产环境中运行不同版本;然而,这会增加你在 Git 中的复杂性,并且不易扩展。你也无法获得用户的上下文,因此收集数据来验证或削弱你的假设会更加困难。大多数功能标志的解决方案都内置了实验支持,所以这是最快的入门方式。

要进行实验,你需要定义一个假设,进行实验,然后从结果中学习。一个实验可以这样定义(见图 10.4):

  • 假设:我们相信{客户群体},想要{产品/功能},因为{价值主张}

  • 实验:为了验证或反驳前述假设,团队将进行一次实验。

  • 学习:实验将通过影响以下指标来证明假设。

图 10.4 – 使用功能标志进行实验

图 10.4 – 使用功能标志进行实验

让我们看一个例子。通过查看你的应用程序的使用数据,你发现新用户注册对话框的第一页浏览量远高于完成注册流程的人数。只有大约 20% 的人完成注册。假设是注册对话框过于复杂,当对话框简化时,完成注册的人数将大幅增加。

为了进行实验,你在应用程序中添加了两个新指标:started-registration,每当用户点击时增加;finished-registrations,每当用户成功注册应用程序时增加。这两个指标使得计算aborted-registrations(注册中止)变得容易。你收集了接下来几周的数据,并确认在这些周里,中止注册的平均率为 80%。你的团队使用new-register-dialog功能标志创建了一个新的、简化的对话框。它移除了所有不必要的字段,例如地址和支付信息,这些信息并不是注册本身所必需的,然后将代码部署到生产环境中。数据在结账前 anyway 进行验证,因此即使这可能会成为结账过程中的一个可用性问题,简化后的注册仍然有效。

在生产环境中,你为 50% 的新用户启用功能标志,并比较两组的aborted-registrations(注册中止)率。看到旧对话框的用户的中止率保持在大约 70% 到 80% 之间,而看到新对话框的用户的中止率仅为 55%。

结果仍然不完美,所以你开始添加新的指标,以找出用户在对话框中的困难点。这引出了下一个假设(见图 10.5):

图 10.5 – 使用功能标志进行实验

图 10.5 – 使用功能标志进行实验

要使用功能标志进行实验,你需要数据。只有具备正确的指标并能够将这些指标与开启或关闭特定标志的受众进行映射,你才能真正进行基于证据的开发。

第十九章使用 GitHub 进行实验和 A/B 测试中,我们将不出所料地深入探讨使用 GitHub 进行实验和 A/B 测试的内容。

摘要

功能标志是加速 DevOps 团队工作的重要功能之一。不幸的是,GitHub 目前还没有内建的解决方案。但有许多产品可以帮助你快速上手。

在本章中,你了解了功能生命周期以及如何使用功能标志(Feature Flags)来管理它。你还学会了如何利用功能标志通过提前检查代码来减少复杂性。

在下一章中,你将了解基于主干的开发(trunk-based development)以及支持快速 DevOps 团队的最佳 Git 工作流。

进一步阅读

你可以在这里获取更多相关信息:

第十一章:基于主干的开发

与加速工程速度高度相关的能力之一是git 工作流,但它实际上是一种自 80 年代以来一直使用的分支模型选择。它定义不够明确,留有很多解释的空间,尤其是在与 GitHub 结合使用时。此外,我个人认为,仅仅转向基于主干的工作流并不会显著提升性能。只有那些在合并地狱中困扰的大型团队,才会有显著的影响。对于大多数团队来说,真正有效的是将不同的能力结合起来使用,比如功能标志和持续集成/持续部署CI/CD),并与基于主干的工作流配合使用,这才会产生显著的效果。

在本章中,我将解释基于主干的工作流的好处。我们还将讨论它们与其他分支工作流的区别,我将向你介绍我认为是加速软件交付的最佳git工作流。

本章涵盖以下主题:

  • 基于主干的开发

  • 为什么你应该避免复杂的分支

  • 其他git工作流

  • 使用 MyFlow 加速

  • 案例研究

基于主干的开发

基于主干的开发是一种源代码管理的分支模型,在这种模型中,开发者将小而频繁的更新合并到一个单一的分支(通常称为git,常被称为main分支),并抵制任何创建长期存在的开发分支的压力(见 https://trunkbaseddevelopment.com)。

其基本理念是,主分支始终保持干净的状态,以便任何开发者在任何时候都能基于主分支创建一个构建成功的新分支。

为了保持分支的干净状态,开发者必须采取多项措施,确保只有不破坏任何东西的代码才能被合并回主分支,具体措施如下:

  • 获取主分支的最新更改

  • 执行干净测试

  • 运行所有测试

  • 与团队保持高度凝聚(结对编程或代码审查)

如你所见,这种方式特别适用于保护的主分支和没有分支保护的main。在小型、高度凝聚和共同工作的团队中,实践结对编程时,这种方式非常有效,但需要高度的纪律性。在复杂的环境中,或者在分布式、异步工作的团队中,我始终建议使用分支保护和 PR。

为什么你应该避免复杂的分支

当我们谈论分支时,我们经常使用长期存在和短期存在这些术语,这些术语涉及时间。我发现这有些误导。分支是关于变更的,而变更几乎无法用时间来衡量。开发人员可以花 8 小时编写大量重构的代码,并试图在 1 天内合并那个非常复杂的分支。即使他们只用时间来衡量,这仍然被认为是短期存在的。相反,如果他们有一个只改变了一行代码的分支——例如,更新代码依赖的包——但是团队必须解决一些关于变更的架构问题,从时间的角度来看,它会是长期存在的,即使在main分支上重新基于这些变更会非常简单。

时间似乎不是区分分支好坏实践的最佳衡量标准;它是复杂性和时间的组合。

基础分支从您创建分支到尝试合并您的更改之间发生的更改越多,合并这些变更与您的分支的变更就越困难。复杂性可能来自一个非常复杂的合并,或者来自许多开发人员合并许多小的变更。为了避免合并,许多团队试图在合并回去之前完成功能工作。当然,这会导致更复杂的变更,然后使得其他功能合并变得困难——所谓的合并地狱——在发布之前,所有功能必须集成到新版本中。

为了避免合并地狱,你应该定期拉取主分支的最新版本。只要您能够无问题地合并或重新基于,您的分支的集成就不是问题,但如果您的更改变得过于复杂,对其他开发人员来说就是个问题,因为如果您合并您的更改回去,他们可能会遇到问题。这就是为什么在复杂度超过一定程度之前,您应该合并您的更改的原因。复杂性的程度很大程度上取决于您修改的代码,并且您需要考虑以下几点:

  • 你是在处理现有代码还是新代码?

  • 代码是复杂且具有许多依赖关系,还是简单的代码?

  • 您是在处理隔离的代码还是具有高内聚力的代码?

  • 同时有多少人改变了代码?

  • 同时进行大量代码重构吗?

我认为这就是人们倾向于以时间作为衡量标准而不是复杂性的原因——没有一个好的复杂性衡量标准。因此,作为一个经验法则:如果你在处理更复杂的功能,你应该每天至少将你的更改合并回主分支,但如果你的更改很简单,将你的分支/PR 保持打开更长时间也没有问题。记住,重点不在于时间,而在于复杂性!

其他的 git 工作流程

在我们深入研究我认为对使用 GitHub 的 DevOps 团队来说最有效的git工作流程之前,我想介绍一下最流行的工作流程。

Gitflow

git 如使用标签发布并与合并后会被删除的分支协作(见 图 11.1):

图 11.1 – Gitflow 概览

图 11.1 – Gitflow 概览

Gitflow 非常适合那些每几个月向不同客户交付软件、希望将一些特性打包成一个单独授权的新版,并且需要维护多个版本多年的人。在 2010 年,这几乎是所有软件的常见发布流程,但在复杂环境中,这种工作流会引发一些问题。该工作流不是基于主干的,并且有多个长期存在的分支。这些分支之间的整合可能会在复杂环境中导致“合并地狱”。随着 DevOps 和 CI/CD 实践的兴起,这种工作流名声不好。

如果你想通过 DevOps 加速软件交付,Gitflow 不是适合你的分支工作流!但很多概念可以在其他工作流中找到。

GitHub 流程

GitHub 流程非常注重与 PR 的协作。你创建一个有描述性名称的分支并进行首次更改。然后,创建一个 PR,通过对代码的评论与评审者进行协作。一旦 PR 准备好,它会被部署到生产环境中,合并到主分支之前(见 图 11.2)。

图 11.2 – GitHub 流程

图 11.2 – GitHub 流程

GitHub 流程是基于主干的,且非常流行。基本部分——没有 PR 部署——是大多数其他工作流的基础。问题在于部署。将每个 PR 部署到生产环境会造成瓶颈,且扩展性不佳。GitHub 本身使用 ChatOps 和部署列车来解决这个问题(Aman Gupta, 2015),但对我来说,这似乎有些过度。只有在生产环境中证明有效的变更才会被合并到 main 分支上,这个观点很有说服力,但在复杂环境中基本上无法实现。你需要相当长的时间才能确认变更在生产环境中独立运行有效,从而确保它们没有破坏任何东西,但在这段时间里,瓶颈阻止了其他团队或团队成员合并他们的变更。我认为,在一个 DevOps 的世界中,采用 push 触发主分支的原则。如果变更破坏了生产环境,你仍然可以部署最后一个有效版本(回滚),或者修复错误并立即部署修复(前进回滚)。你不需要一个干净的主分支就能执行这两种操作。

我不喜欢 GitHub 流程的另一点是,它对用户数量、分支和 PR 数量不够明确。一个特性分支可能暗示多个开发者会在同一个分支上提交。我并不常看到这种情况,但仅从文档上来看,这并不明确。

发布流程

发布流程基于 GitHub 流程,但它不是持续地部署 PR,而是加入了单向的发布分支。这些分支不会被合并回去,bug 修复跟随 main,并且变更会被精确挑选合并到发布分支的分支中(Edward Thomson, 2018)。通过这种方式,无法忘记将 bug 修复应用到 main(见 图 11.3):

图 11.3 – 发布流程

图 11.3 – 发布流程

发布流程不是 CD!创建发布仍然是一个必须单独触发的过程。如果你必须维护软件的不同版本,发布流程是一个不错的选择。但如果可能的话,你应该尝试实现 CD。

GitLab 流程

GitLab 流程同样基于 GitHub 流程。它增加了环境分支(如开发、预发布、预生产和生产),每次部署都发生在合并到这些环境时(见 图 11.4):

图 11.4 – GitLab 环境分支

图 11.4 – GitLab 环境分支

由于变更仅向下游流动,你可以确保所有的变更都在所有环境中经过了测试。GitLab 流程同样跟随 main,并将变更挑选到所有环境中。Bug 修复在 GitLab 流程中与发布流程中的工作方式相同。

如果你没有支持多个环境的管道——比如 GitHub Actions——GitLab 流程可能提供了一种很好的方式来自动化你的审批和部署到各个环境。就个人而言,如果你在上游执行 bug 修复,我不认为将环境代码分开有什么价值。我更倾向于一次构建代码,然后按顺序将输出部署到所有环境。但在某些情况下,这种工作流是有意义的——例如,对于直接从代码库部署的静态网站。

使用 MyFlow 加速

正如你所看到的,git 工作流仅仅是为不同使用场景提供的一些解决方案集合。它们之间的主要区别在于是否基于主干,是否在某些方面明确。如果我发现所有工作流都有缺失,我便创建了自己的工作流:MyFlow

MyFlow 是一种基于 PR 的轻量级主干式工作流。MyFlow 并不是一种新发明!许多团队已经在这样工作了。如果你专注于与 PR 的协作,这是一种非常自然的分支和合并方式。我只是给它起了个名字,我相信人们会很容易接受它。

主分支

由于 MyFlow 是基于主干的,所以只有一个名为 main 的主分支,并且它应该始终保持干净状态。主分支应该始终能够构建,并且应该能够随时发布到生产环境。这就是为什么你应该用分支保护规则来保护 main。一个好的分支保护规则至少应该包括以下标准:

  • 合并前需要至少两次 PR 审查

  • 推送新提交时撤销过时的 PR 审批

  • 需要代码所有者的审查

  • 在合并之前要求通过状态检查,包括你的 CI 构建、测试执行、代码分析和代码检查工具

  • 将管理员包括在限制中

  • 允许强制推送

你通过 CI 构建自动化的程度越高,你的分支保持干净的可能性就越大。

所有其他分支总是从main分支分出。由于这是默认分支,你在创建新分支时不必指定源分支。这简化了操作并消除了错误的源。

私有主题分支

图 11.5 显示了 MyFlow 的基本概念:

图 11.5 – MyFlow 基础

图 11.5 – MyFlow 基础

私有主题分支可用于处理新特性、文档、漏洞、基础设施以及你仓库中的其他一切。它们是私有的,意味着它们只属于一个特定用户。其他团队成员可以检出该分支进行测试,但不允许直接推送更改到该分支。相反,他们必须通过 PR 中的suggestions建议更改给 PR 的作者。

为了表示分支是私有的,我建议使用一种命名约定,比如users/*private/*,这样可以使其一目了然。我还建议在名称中包含问题标识符ID)。这样做可以方便日后在提交信息中引用它。一个好的约定可能是这样的:

users/<username>/<id>_<topic>

要开始处理一个新主题,你需要创建一个新的本地分支,如下所示:

$ git switch -c <branch> main

你可以在这里看到一个示例:

$ git switch -c users/kaufm/42_new-feature main
> Switched to a new branch 'users/kaufm/42_new-feature'

创建你的第一次修改并提交,之后推送到服务器。无论你修改了什么都没关系——你可以只是给文件加一个空格。反正以后可以覆盖它。你可以在这里看到一个示例:

$ git add .
$ git commit
$ git push --set-upstream origin <branch>

现在,这是前面的示例,附加了更多信息:

$ git add .
$ git commit -m "New feature #42"
$ git push --set-upstream origin users/kaufm/42_new-feature

注意

请注意,我使用GitHub 命令行界面GitHub CLI)(cli.github.com/)与 PR 交互,因为我觉得它比使用网页用户界面UI)的截图更容易阅读和理解。你也可以使用网页 UI 进行相同的操作。

创建一个 PR 并将其标记为草稿,如下所示:

$ gh pr create --fill --draft

这样,团队就知道你正在处理这个主题。快速查看打开的 PR 列表应能让你清楚地看到团队目前正在处理的主题。

注意

提交更改时,你可以省略-m参数,并在默认编辑器中添加多行提交信息。第一行将是 PR 的标题,其余部分将是正文。你也可以在创建 PR 时设置标题(--title-t)和正文(--body-b),而不是使用--fill

现在,你可以开始在你的主题上工作,并可以使用git的全部功能。例如,如果你想向之前的提交添加更改,可以使用--amend选项,如下所示:

$ git commit --amend

或者,如果你想将最后三个提交合并为一个提交,可以运行以下命令:

$ git reset --soft HEAD~3
$ git commit

如果你想将一个分支中的所有提交合并为一个提交,可以运行以下命令:

$ git reset --soft main
$ git commit

或者,如果你希望完全自由地重新排列并合并所有提交,可以使用交互式变基,如下所示:

$ git rebase -i main

要将更改推送到服务器,你可以使用以下命令:

$ git push origin +<branch>

这是前面示例中已填充分支名称的版本:

$ git push origin +users/kaufm/42_new-feature

请注意在分支名称前的 + 加号。这会导致强制推送,但仅限于特定分支。如果你不想更改分支历史,可以执行正常的 git push 操作,并且如果你的分支已得到良好的保护,并且你知道自己在做什么,正常的强制推送可能会更方便,如下所示:

$ git push -f

如果你希望得到同事对代码的帮助或意见,可以在 PR 评论中提及他们。如果他们想提出更改,他们可以使用 PR 评论中的 suggestions 功能。这样, 应用更改,并确保在此之前你的仓库处于干净的状态。

每当你觉得工作已准备好时,可以将 PR 的状态从 draft 改为 ready,并激活自动合并,方法如下:

$ gh pr ready
$ gh pr merge --auto --delete-branch --rebase

注意

请注意,我指定了 --rebase 作为合并方式。这是一个适合喜欢打造简洁且精确提交历史的小团队的合并策略。如果你更喜欢 --squash--merge,请相应调整你的合并策略。

你的审阅者仍然可以在评论中提出建议,你可以继续协作。但一旦所有批准和自动化检查完成,PR 将自动合并,且分支将被删除。自动化检查会在 pull_request 触发器上运行,可能包括在隔离环境中安装应用程序并运行各种测试。

如果你的 PR 已被合并且分支已被删除,你可以像这样清理本地环境:

$ git switch main
$ git pull --prune

这将把你当前的分支切换到 main,从服务器拉取更改后的分支,并删除已在服务器上删除的本地分支。

发布

一旦你的更改合并到 mainmain 上的 push 触发器将启动生产环境的部署,不管你是否使用环境或基于环的方式。

如果你需要维护多个版本,可以在工作流中将标签与 release 触发器一起使用来部署应用程序,并使用 GitVersion 自动生成版本号,如下所示:

$ gh release create <tag> --notes "<release notes>"

这是一个示例:

$ gh release create v1.1 --notes "Added new feature"

你还可以利用自动生成发布说明。不幸的是,这个功能目前通过 CLI 不可用。你必须通过 UI 来创建发布,才能使其生效。

由于我们遵循 上游优先 原则修复 bug,如果不需要进行热修复,实际上没有必要为每个版本创建发布分支。在你创建发布时生成的标签就足够了。

热修复

如果你需要为旧版本提供热修复,可以检出标签并创建一个新的热修复分支,像这样:

$ git switch -c <hotfix-branch> <tag>
$ git push --set-upstream origin <branch>

以下是一个示例:

$ git switch -c hotfix/v1.1.1 v1.1
$ git push --set-upstream origin hotfix/1.1.1

现在,切换回 main 并修复 users/kaufm/666_fix-bug 中的错误。接着,挑选带有修复的提交并合并到修复分支,如下所示:

$ git switch <hotfix-branch>
$ git cherry-pick <commit SHA>
$ git push

你可以使用你想要挑选的提交的安全哈希算法SHA)。或者,如果提交是分支的最新提交,你也可以使用分支的名称,如下所示:

$ git switch hotfix/v1.1.1
$ git cherry-pick users/kaufm/42_fix-bug
$ git push

这将会挑选出主题分支的最新提交。图 11.6 展示了如何对旧版本进行热修复:

图 11.6 – 对旧版本进行热修复

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_11_006.jpg)

图 11.6 – 对旧版本进行热修复

你也可以先将修复合并到 main 分支,然后再从那里挑选提交。这样可以确保代码遵循所有分支策略。

你还可以基于修复分支创建一个临时分支并挑选修复,然后通过另一个 PR 合并该修复。这取决于你的环境复杂程度以及主分支和修复分支之间的差异有多大。

自动化

如果你的工作流程中有命名约定,那么你可能会非常频繁地使用某些命令序列。为了减少输入错误并简化工作流程,你可以使用 git.gitconfig 文件在你选择的编辑器中自动化这些命令,像这样:

$ git config --global --edit

如果 [alias] 部分还不存在,添加它,并添加一个别名,像这样:

[alias]
    mfstart = "!f() { \
        git switch -c users/$1/$2_$3 && \
        git commit && \
        git push --set-upstream origin users/$1/$2_$3 && \
        gh pr create --fill --draft; \
    };f"

这个别名叫做 mfstart,在指定用户名、问题 ID 和主题时会被调用,如下所示:

$ git mfstart kaufm 42 new-feature

它会切换到一个新分支并提交当前在索引中的更改,推送到服务器,并创建一个 PR。

你可以引用单个参数($1$2,……)或使用 $@ 引用所有参数。如果你想要独立于退出代码串联命令,你必须使用 ; 来终止命令。如果你只希望第一个命令成功后才执行下一个命令,可以使用 &&。请注意,你必须在每一行末尾加上反斜杠(\)。这个符号也是用来转义引号的。

你可以添加 if 语句来分支你的逻辑,像这样:

mfrelease = "!f() { \
    if [[ -z \"$1\" ]]; then \
        echo Please specify a name for the tag; \
    else \
        gh release create $1 --notes $2; \
    fi; \
};f"

或者,你可以将值存储在变量中以便稍后使用,像这个示例一样——当前分支名称(HEAD 指向的分支):

mfhotfix = "!f() { \
    head=$(git symbolic-ref HEAD --short); \
    echo Cherry-pick $head onto hotfix/$1 && \
    git switch -c hotfix/$1 && \
    git push --set-upstream origin hotfix/$1 && \
    git cherry-pick $head && \
    git push; \
};f"

这些仅仅是示例,自动化依赖于你的工作方式的细节,但它是一个非常强大的工具,可以帮助你提高生产力。

案例研究

在自动化发布流程之后,两个试点团队已经注意到生产力大幅提高。交付时间部署频率的指标显著上升。

该团队在从 Bitbucket 迁移到 GitHub 之前使用了git,并遵循了Gitflow作为他们的分支工作流。由于他们的 Web 应用程序可以通过分阶段部署工作流持续发布,他们转向了基于主干的工作流,使用 PR 和私有分支,并在合并到主分支后通过 CI/CD 工作流(MyFlow)进行部署。为了频繁集成,他们决定使用功能标志。由于公司需要在云端和本地管理功能,他们决定选择Unleash。团队可以使用软件即服务SaaS)服务,且无需等待本地解决方案即可立即开始使用。

第二个从Team Foundation ServerTFS)迁移过来的团队,习惯于使用复杂的分支工作流,包括长期存在的发布、服务包、热修复分支和集成所有功能的开发分支。由于软件安装在硬件产品上,多个版本会并行稳定,并且有多个版本需要维护多年。这意味着软件不能持续发布。该团队选择了发布流来管理发布和热修复。对于开发,他们还使用了带 PR 的私有分支和基于主干的方法。由于这些产品未连接到互联网,团队依赖配置系统来启用功能标志。这项技术之前已经用于在硬件上测试新功能。现在,团队扩展了这一技术,以更频繁地集成更改。

总结

git工作流彼此之间没有太大差异,大多数工作流都是基于其他工作流构建的。重要的是遵循快速失败快速前进的原则,而不是将某个特定工作流视为教条。所有工作流只是最佳实践的集合,你应该根据需要选择合适的部分。

重要的是你更改的大小和合并回去的频率。

始终遵循以下规则:

  • 始终从主分支(基于主干)分支出你的主题分支。

  • 如果你在开发复杂的功能,确保至少每天提交一次(使用功能标志)。

  • 如果你的更改很简单,只需更改几行代码,你可以让你的 PR 保持开放状态更长时间。但要确保你的开放 PR不太多。

只要遵循这些规则,你实际使用的工作流并不那么重要。选择对你有效的部分。

在本章中,你了解了 TBD 的好处,以及如何将其与git工作流结合使用,以提高你的工程效率。

在下一章,我将解释如何使用 Shift-left 测试来提高质量并以更大的信心进行发布。

进一步阅读

你可以参考本章以下内容,获取更多关于所涵盖主题的信息:

第三部分:自信发布

第三部分 解释了如何通过将质量保证和安全性嵌入发布流水线,进一步加速并自信地频繁发布。这包括左移测试和安全性、生产环境中的测试、混沌工程、DevSecOps、保护软件供应链以及基于环的部署等概念。

本书的这一部分包含以下章节:

  • 第十二章通过左移测试提高质量

  • 第十三章左移安全性与 DevSecOps

  • 第十四章保护你的代码

  • 第十五章保护你的部署

第十二章:通过左移测试提高质量

测试质量保证QA)仍然是大多数公司面临的瓶颈。在本章中,我们将深入探讨 QA 和测试在开发者速度中的作用,以及如何进行左移测试。

本章将涵盖以下主题:

  • 使用测试自动化进行左移测试

  • 消除不稳定的测试

  • 代码覆盖率

  • 右移——生产环境中的测试

  • 故障注入与混沌工程

  • 测试与合规性

  • GitHub 中的测试管理

使用测试自动化进行左移测试

如果你实践敏捷开发并尝试频繁发布,那么手动测试就不是一个可扩展的选项。即使你不进行 CI/CD,且仅在一个迭代周期内发布,运行所有必要的回归测试仍然会消耗大量人力、时间和金钱。但是,要做到自动化测试正确并不容易。例如,自动化测试如果由 QA 部门或外包机构创建和维护,并不会与更高的工程速度相关联(Forsgren N., Humble, J., & Kim, G., 2018, 第 95 页)。要看到速度上的提升,你需要由团队创建并维护的可靠测试。其背后的理论是,如果开发人员维护测试,他们会生成更多可测试的代码。

每个人都知道一个好的测试组合应该是什么样的:你应该有大量自动化的单元测试(Level 0),较少的集成测试(Level 1),一些需要测试数据的集成测试(Level 2),以及少数功能性测试(Level 3)。这被称为测试金字塔(见图 12.1):

图 12.1 – 测试金字塔

图 12.1 – 测试金字塔

然而,在大多数公司中,测试组合并非如此。有时会有一些单元测试,但其他大多数测试仍然停留在较高的层次(见图 12.2):

图 12.2 – 示例测试组合

图 12.2 – 示例测试组合

这些高级测试可能是自动化的,也可能是手动的。但即便如此,这也不是一个能帮助你持续高质量发布的测试组合。要实现持续的质量,你必须将测试组合“左移”(见图 12.3):

图 12.3 – 左移测试

图 12.3 – 左移测试

这不是一项容易的任务。以下是一些有助于左移测试的原则:

  • 所有权:团队对质量保证(QA)负责,测试与代码一同开发——最好采用先测试后编码的方法。QA 工程师应该成为团队的一部分。

  • 左移:测试应该始终在可能的最低层次编写。

  • 一次编写,处处执行:测试应在所有环境中执行,甚至在生产环境中。

  • 测试代码就是生产代码:适用于普通代码的质量标准同样适用于测试代码。这里不应允许任何捷径。

  • 你写代码 – 你测试它:作为开发者,你对代码的质量负责,必须确保所有的测试都到位,以确保质量。

2013 年,创建了一个测试宣言,描述了 QA 角色的转变(Sam Laing, 2015):

  • 测试贯穿整个过程 而不是 在最后进行测试

  • 防止错误 而不是 发现错误

  • 测试理解 而不是 检查功能

  • 构建最佳系统 而不是 破坏系统

  • 团队对质量的责任 而不是 测试员的责任

听起来很简单,但实际上并非如此。开发人员必须学会像测试员一样思考,而测试员必须学会像工程师一样思考。传达愿景并确保变革的可持续性不是一项容易的任务。

测试驱动开发

测试自动化的关键是拥有一个可测试的软件架构。为了实现这一点,你必须尽早开始——也就是说,在内部循环中,当开发人员编写代码时。

测试驱动开发 (TDD) 是一种软件开发过程,你首先编写自动化测试,然后编写使测试通过的代码。它已经存在超过 20 年,并且通过不同的研究证明了其质量效益(例如,Müller, Matthias M.; Padberg, Frank, 2017Erdogmus, Hakan; Morisio, Torchiano, 2014)。TDD 不仅对调试所花费的时间和整体代码质量有很大影响;它还对稳固和可测试的软件设计产生重要影响。正因如此,它也被称为测试驱动设计

TDD 很简单。步骤如下:

  1. 添加或修改测试:总是从一个测试开始。在编写测试时,你设计你的代码的样子。有时候你的测试不会编译,因为你调用的类和函数还不存在。大多数开发环境支持直接在测试中创建必要的代码。当你的代码编译并且测试可以执行时,这一步就完成了。测试应该失败。如果测试通过,修改它或编写一个新测试,直到它失败。

  2. 运行所有测试:运行所有测试并验证只有新测试失败。

  3. 编写代码:编写一些简单的代码使测试通过。在这个阶段,代码不需要很漂亮,允许使用快捷方式。只需确保测试通过。糟糕的代码能让你知道接下来需要什么测试,以确保代码变得更好。

  4. 所有测试通过:如果所有测试都通过,你有两个选择:编写一个新测试或修改现有的测试。或者,你可以重构你的代码和测试。

  5. 重构:重构代码和测试。由于你已经拥有了一个可靠的测试框架,你可以进行比通常情况下更加极端的重构,而无需担心没有 TDD 的支持。确保在每次重构后都运行所有的测试。如果有一个测试失败,撤销上一步并重试,直到所有测试在重构后都通过。成功重构后,你可以开始一个新的迭代,进行新的失败测试。

图 12.4 展示了 TDD 循环的概览:

图 12.4 – TDD 循环

图 12.4 – TDD 循环

一个好的测试遵循以下模式:

  • 安排:为测试设置必要的对象以及待测试系统SUT)本身——通常这是一个类。你可以使用模拟存根来模拟系统行为(要了解更多关于模拟和存根的内容,请参考马丁·福勒,2007)。

  • 执行:执行你想要测试的代码。

  • 断言:验证结果,确保系统的状态符合预期,并确保方法已调用正确的方法,并传递了正确的参数。

每个测试应该是完全自给自足的——也就是说,它不应该依赖于先前测试所操作的系统状态,并且可以在隔离环境下执行。

TDD 也可以用于结对编程,这被称为乒乓结对编程。在这种形式的结对编程中,一位开发者编写测试,另一位开发者编写代码使得测试通过。这是一种非常适合结对编程的模式,也是一种很好的方式,帮助年轻的同事理解 TDD 的好处。

TDD 已经存在很久了,并且实践它的团队收获了巨大的价值——然而我遇到过许多没有使用它的团队。有些团队因为他们的代码运行在嵌入式系统上而没有使用 TDD,另一些团队则因为他们的代码依赖于难以模拟的 SharePoint 类而放弃使用它。但这些都只是借口。虽然可能有一些无法测试的管道代码,但当你编写逻辑时,你总是可以先进行测试。

管理你的测试组合

使用 TDD,你应该能够迅速得到一个可测试的设计。即使在已有项目的环境中,自动化测试的数量也会迅速增长。问题在于,测试的质量往往不是最优的,而且随着测试库的增长,执行时间会变得非常长,并且测试结果可能会变得不稳定(例如间歇性失败)。最好是有更少但质量更高的测试。长时间的执行会妨碍你快速发布,而不稳定的测试会产生不可靠的质量信号,降低对测试套件的信任(参见图 12.5)。随着团队质量保证能力的提升,测试套件的质量会不断提高——即使在首次增长后的测试数量减少:

图 12.5 – 自动化测试的数量和质量

图 12.5 – 自动化测试的数量和质量

为了积极管理你的测试组合,你应该为你的测试定义基本规则,并不断监控测试数量及其执行时间。例如,我们来看微软团队用于其测试组合的分类法

单元测试(Level 0)

在这里,我们有的是内存中的单元测试,没有外部依赖,也没有部署。它们应该很快,平均执行时间少于 60 毫秒。单元测试与被测试的代码共同存在。

对于单元测试,你不能更改系统的状态(如文件系统或注册表)、查询外部数据源(如 Web 服务和数据库)或使用互斥锁、信号量、计时器以及Thread.sleep等操作。

集成测试(Level 1)

这一层涉及更复杂的测试需求,可能依赖于轻量级部署和配置。测试仍然应该非常快速,每个测试必须在 2 秒内完成。

对于集成测试,你不能依赖于其他测试并存储大量数据。你也不能在一个组件中放入太多的测试,因为这会阻止测试并行执行。

带数据的功能性测试(Level 2)

功能性测试在可测试的部署环境中运行,并使用测试数据。对像身份验证提供者这样的系统的依赖可以被模拟,从而允许使用动态身份。这意味着每个测试都有一个独立的身份,以便可以在部署环境中并行执行这些测试,且不会互相影响。

生产测试(Level 3)

生产测试在生产环境中运行,且需要完全的产品部署。

这只是一个示例,你的分类法可能会有所不同,具体取决于你的编程语言和产品。

如果你已经定义了你的分类法,你可以设置报告并开始转变你的测试组合。确保首先让编写和执行高质量的单元测试和集成测试变得容易。然后,开始分析你的遗留测试——无论是手动测试还是自动化测试——并检查哪些可以丢弃。将其余的转换为良好的功能性测试(Level 2)。最后一步是编写你的生产测试。

微软团队从 27,000 个遗留测试(橙色)开始,并在 42 个迭代(126 周)内将其减少为零。大多数测试被替换为单元测试;一些被替换为功能性测试。许多测试直接删除了,但单元测试数量稳步增长,最终超过了 40,000 个(见图 12.6):

图 12.6 – 测试组合随时间的变化

图 12.6 – 测试组合随时间的变化

参见向左移动以使测试快速且可靠,在进一步阅读部分,了解微软团队如何将其测试组合向左移动。

根除不稳定的测试

非确定性不稳定测试是那些有时通过、有时失败的测试,即使代码相同(Martin Fowler, 2011)。不稳定的测试会破坏对测试套件的信任。这可能导致团队忽略红色测试结果,或者开发人员停用测试,从而降低测试覆盖率和套件的可靠性。

不稳定测试有很多原因。通常,它们是由于缺乏隔离性造成的。许多测试在同一台机器上的同一个进程中运行 —— 所以每个测试必须找到并保持系统的干净状态。另一个常见的原因是异步行为。测试异步代码具有挑战性,因为你无法知道异步任务的执行顺序。其他原因可能包括资源泄漏或对远程资源的调用。

处理不稳定测试的方式有很多种:

  • git blame

  • git blame

一些公司会将不稳定测试隔离,但这样也会阻止你收集额外的数据,因为测试无法运行。最好的做法是继续执行不稳定测试,但将它们从报告中排除。

如果你想了解 GitHub 或 Google 是如何处理不稳定测试的,可以阅读 Jordan Raine, 2020John Micco, 2016

代码覆盖率

代码覆盖率是一个衡量指标(以百分比表示),它计算通过测试调用的代码元素的数量,除以总的代码元素数量。代码元素可以是任何东西,但常见的有代码行、代码块或函数。

代码覆盖率是一个重要的指标,因为它能显示哪些代码部分没有被你的测试套件覆盖。我喜欢在完成代码更改之前查看代码覆盖率,因为我经常忘记为一些边界情况(如异常处理)或更复杂的语句(如 lambda 表达式)编写测试。现在添加这些测试没有问题 —— 但事后再添加就困难得多。

但你不应过于关注绝对数字,因为代码覆盖率本身并不能说明测试的质量。拥有 70%的代码覆盖率且测试质量高,比拥有 90%代码覆盖率但测试质量差更好。根据你使用的编程语言和框架,可能存在一些在测试中耗费大量精力但价值很低的代码。通常,你可以将这些代码排除在代码覆盖率计算之外,这就是为什么代码覆盖率的绝对值是有限的。不过,在每个管道中测量其价值并专注于新代码,有助于随着时间的推移提高自动化测试的质量。

向右移动 —— 在生产环境中测试

如果你从自动化测试开始,你会迅速看到质量提升和工程师调试工作的减少。但在某个时刻,你必须大幅增加投入才能看到质量的显著改善。另一方面,测试执行所需的时间会减缓发布管道的速度,尤其是当你将性能测试负载测试加入其中时(见图 12.7):

图 12.7 – 测试工作量对质量和速度的影响

图 12.7 – 测试工作量对质量和速度的影响

如果你的流水线运行时间超过 24 小时,那么一天发布多次是无法实现的!流水线执行时间的增加还会减少你快速推进和部署修复的能力,特别是当生产环境出现 bug 时。

解决这个问题的方法很简单:向右移一些测试到生产环境中。所有在生产环境中执行的测试不会影响你快速发布的能力,而且你不需要性能测试或负载测试,因为你的代码已经经历了生产负载。

然而,在生产环境中进行测试有一些前提条件,这些条件能够提高用户的性能质量,而不是降低它。让我们一起来看看。

健康数据和监控

对于生产环境中的测试,你必须时刻关注应用程序的健康状况。这超出了普通日志记录的范畴。你需要深入了解应用程序的运行情况。一种好的做法是编写测试代码,调用所有依赖的系统——例如数据库、Redis 缓存或依赖的 REST 服务——并将这些测试结果提供给你的日志解决方案。这样,你可以拥有一个持续的心跳,以指示所有系统都在运行并且正常协作。如果测试失败,可以通过警报立即通知团队某些地方出了问题。你还可以自动化这些警报,并触发某些功能,比如启动熔断器

熔断器

熔断器是一种模式,防止应用程序反复尝试执行可能失败的操作,使得应用程序能够在不等待失败操作成功的情况下,继续执行经过修改的功能(参见 Michael Nygard, 2018)。

特性标志和金丝雀发布

你不希望在生产环境中进行测试,从而导致所有客户的完全宕机。这就是为什么你需要特性标志、金丝雀发布、基于环的部署,或者这些技术的组合(参见 第九章第十章)。逐步暴露变更非常重要,这样如果发生宕机,你就不会让整个生产环境崩溃。

业务连续性与灾难恢复

另一种生产环境测试的形式是业务连续性与灾难恢复BCDR)或故障转移测试。每个服务或子系统都应该有一个 BCDR,并且你应该定期执行 BCDR 演练。如果系统宕机,而灾难恢复无法正常工作,那是最糟糕的情况。只有定期测试,才能确保它正常工作。

探索性测试和可用性测试

测试自动化并不意味着你应该完全放弃手动测试。但手动测试的重点已经从验证功能和每个发布都进行回归测试转移到可用性、快速且高质量的反馈,以及那些通过结构化测试方法难以发现的错误。

探索性测试由 Cem Kaner 于 1999 年提出(Kaner C., Falk J., H. Q. Nguyen, 1999)。它是一种测试方法,旨在同时关注发现、学习、测试设计和执行。它依赖于测试人员个人发现那些其他测试方法难以发现的缺陷。

有许多工具可以促进探索性测试。它们帮助你记录会话、拍摄带注释的截图,并且通常允许你从已经执行的步骤中创建测试用例。一些扩展与 Jira 集成,如 Zephyr 和 Capture,还有浏览器扩展,如用于 Azure 测试计划的 Test 和 Feedback 客户端。如果以独立模式使用,后者是免费的。这些工具为开发人员提供了来自利益相关者的高质量反馈——不仅仅是关于发现的缺陷。

获取反馈的其他方式包括使用可用性测试技术——如走廊测试游击式可用性测试——通过在新用户、没有偏见的用户身上测试,来评估你的解决方案。可用性测试的一种特殊形式是 A/B 测试,我们将在第十九章使用 GitHub 进行实验和 A/B 测试中详细讨论。

这里的重要部分是,所有这些测试都可以在生产环境中执行。你的 CI/CD 流水线中不应该有任何手动测试。快速发布,并使用功能标志和金丝雀发布允许在生产环境中进行手动测试。

故障注入与混沌工程

如果你想提升生产环境中的测试,可以练习故障注入——也称为混沌工程。这意味着你向生产系统中注入故障,以查看它在压力下的表现,以及你的故障转移机制和断路器是否正常工作。可能的故障包括高 CPU 占用、高内存使用、磁盘 I/O 压力、低磁盘空间,或服务或整个机器被关闭或重启。其他可能的情况包括进程被终止、系统时间被更改、网络流量丢失、延迟注入,以及 DNS 服务器被阻塞。

实践混沌工程能使你的系统更具弹性。这无法与传统的负载或性能测试相比!

不同的工具可以帮助你进行混沌工程。例如,Gremlin (www.gremlin.com/) 是一个基于代理的 SaaS 产品,支持大多数云服务提供商(如 Azure、AWS 和 Google Cloud)以及所有操作系统。它也可以与 Kubernetes 配合使用。Chaos Mesh (chaos-mesh.org/) 是一个专门为 Kubernetes 设计的开源解决方案。Azure Chaos Studio (azure.microsoft.com/en-us/services/chaos-studio) 是一个专门为 Azure 设计的解决方案。最适合你的工具取决于你支持的平台。

混沌工程可以非常有效,并使你的系统具备韧性,但它应该仅限于对客户影响较小或没有影响的金丝雀环境。

测试与合规性

大多数合规性标准,如汽车行业的ISO26262 或制药行业的GAMP,都遵循V 模型作为开发过程。V 模型要求对用户和系统需求进行分解,并在不同的详细层级中创建规格。这是 V 的左侧。它还要求验证所有层级,以确保系统满足需求和规格。这是 V 的右侧。两个侧面可以在 图 12.8 中看到:

图 12.8 – V 模型中的验证

图 12.8 – V 模型中的验证

该模型必须与风险分析结合使用,风险分析在每个细节层级中执行。在发布阶段,许多文档必须签署。这导致了一个缓慢的瀑布流程,包含了较长的规格定义、开发和发布阶段。

但是,标准是基于最佳实践的——如果你的实践优于标准中的实践,你可以在审计中为此辩护。标准并没有要求你手动进行验证,也没有涉及阶段的时间要求。解决方案是自动化所有验证逻辑,并在修改测试时将批准作为代码审查添加到拉取请求中(左移)。不能自动化的测试必须转移到生产环境中(右移)。这样,你可以自动化整个 V 模型并多次执行:

  1. 添加或修改需求(例如,问题)。

  2. 创建拉取请求并将其链接到问题。

  3. 在你的代码库中修改系统设计和架构(例如,在 markdown 中),或在拉取请求中声明无需修改。

  4. 编写你的单元测试(即软件设计)和实现代码。

  5. 编写或修改你的功能、系统和集成测试。

  6. 确保所有必要的角色都批准拉取请求,并在推送新更改时确保批准是过时的。

  7. 将你的更改发布到生产环境并在那里运行最终的测试。

你还可以将风险作为代码进行管理。这样,你就可以将它们集成到自动化流程中。如果不能,你仍然可以将文档附加到问题上。这样,你就能对所有的变更、必要的审批和所有完成的验证步骤进行端到端的可追溯性管理。同时,你仍然可以快速迭代并定期发布到生产环境。

GitHub 中的测试管理

不幸的是,GitHub 没有一个很好的方式来跟踪你随时间变化的测试运行和代码覆盖率,也无法帮助你检测或隔离不稳定的测试。你可以将测试作为工作流的一部分来执行,并将结果反馈给系统——但对于报告,你必须依赖你的测试工具。

一个与 GitHub 很好集成的优秀解决方案是Testspacehttps://www.testspace.com/)。它是一个 SaaS 服务,开源项目可以免费使用。设置非常简单——只需从市场安装扩展(https://github.com/marketplace/testspace-com),选择你想要的计划,并授予对你的仓库的访问权限。然后,将以下步骤添加到你的工作流中:

- uses: testspace-com/setup-testspace@v1
  with:
    domain: ${{github.repository_owner}}

如果你的仓库是私有的,那么你必须在Testspace中创建一个令牌,并将其作为机密添加到该步骤中:token: ${{ secrets.TESTSPACE_TOKEN }}

然后,你必须添加一个步骤,在执行测试后的步骤中,将你的测试和代码覆盖率结果推送到Testspace。你可以使用 glob 语法来指定动态文件夹中的文件。确保即使发生错误,也能执行该步骤(if: '!cancelled()'):

- name: Push test results to Testspace
  run: |
    testspace **/TestResults.xml **/coverage.cobertura.xml
  if: '!cancelled()'

Testspace 提供了可靠的检测来处理不稳定的测试。它有一个构建机器人,如果有新的结果到达,它会发送通知给你。你可以通过回复邮件来评论结果(见图 12.9):

图 12.9 – 来自 Testspace 的构建结果通知

图 12.9 – 来自 Testspace 的构建结果通知

它会自动集成作为拉取请求中的一个检查(见图 12.10):

图 12.10 – Testspace 集成到你的拉取请求检查中

图 12.10 – Testspace 集成到你的拉取请求检查中

Testspace 的用户界面看起来并不特别华丽,但它有非常丰富的报告和大量功能(见图 12.11):

图 12.11 – 你的测试指标的丰富报告

图 12.11 – 你的测试指标的丰富报告

如果你还没有测试管理解决方案,可以尝试Testspace。如果你已经有了,它应该很容易集成到你的工作流中。

案例研究

Tailwind Gears的两个试点团队通过应用 DevOps 实践,取得了显著提高的交付前置时间部署频率。由于发布流水线有助于更快地发布修复,恢复时间均值也大大改善。然而,变更失败率下降了。更频繁的发布意味着更多的部署会失败,且在代码中找出 bug 变得更加困难。来自自动化测试套件的质量信号并不可靠,修复一个 bug 通常会引入另一个模块中的 bug。应用程序中仍有许多部分需要手动测试——但由于团队中只有一名 QA 工程师,这已经不再是一个可行的选项。因此,这些部分中的一些已被 UI 测试替代,而另一些则被直接丢弃。

为了评估测试组合,团队必须引入测试分类法,并在流水线中包括报告。团队中的 QA 工程师负责分类法,报告显示功能性和 UI 测试过多,而单元测试不足。许多工程师仍然不相信 TDD 能够节省时间,也不认为在某些情况下开发嵌入式软件时能够使用 TDD。团队决定一起安排一次 TDD 培训课程,学习并实践 TDD。

之后,所有新代码都以 TDD 编写,并要求新代码的代码覆盖率至少达到 90%。团队还将每个迭代中 30%的时间用于消除不稳定的测试并重新编写更低层次的测试。

为了发现不稳定的测试,团队在通过绿色测试的流水线上运行可靠性测试。不稳定的测试被优先处理。之后,团队会选择执行时间最长的测试,并决定如何处理每个测试。大多数测试被转换为单元测试,尽管有些被转换为集成测试。有些测试可以被删除,因为它们没有带来额外的价值。

结构化的手动测试完全被探索性测试所替代。如果在这些会话中发现任何问题,会在修复之前创建单元测试。

运行 Web 应用程序的团队还增加了一种新的测试类别,其中的测试将在生产环境中执行。他们实现了应用性能监控并收集了大量的度量,以便了解应用程序在所有环境中的健康状况。团队还在每个迭代中进行一次首次的 BCDR 演练,开始实施生产环境中的测试混沌工程

总结

在本章中,你学到了如何通过测试自动化将测试向左迁移并加速软件交付,随后通过生产中的测试和混沌工程向右迁移。这样,你就可以以快速的节奏发布,而不必在质量上做出妥协。最后,你学到了如何管理你的测试组合,消除不稳定测试,并通过注入故障和混沌使你的应用程序更加具有韧性。

在下一章中,你将学习如何将安全性向左迁移,并将 DevSecOps 实践融入你的开发过程。

进一步阅读

本章中使用了以下参考文献,以帮助你进一步了解讨论过的主题:

第十三章:左移安全与 DevSecOps

报告给互联网犯罪投诉中心IC3)的由网络犯罪造成的总损失,已经达到了历史新高,从 2019 年的 35 亿美元(USD)增加到 2020 年的 41 亿美元(USD)(IC3,2019 和 2020 年)。这一趋势在过去几年持续强劲增长(见图 13.1):

图 13.1 – 报告给 IC3 的网络犯罪总损失

图 13.1 – 报告给 IC3 的网络犯罪总损失

受影响的公司包括初创企业,以及财富 500 强企业。受影响的有科技巨头如 Facebook、Twitter、T-Mobile 和 Microsoft,也有公共机构如旧金山国际机场,甚至是安全公司如 FireEye。没有公司能够声称网络犯罪对他们没有威胁!

在本章中,我们将更广泛地探讨安全在开发中的角色,以及如何将其融入到你的流程中,并实现零信任文化。

以下是我们将在本章中讨论的关键点:

  • 左移安全

  • 假设漏洞、零信任和安全优先的思维模式

  • 攻击模拟

  • 红队-蓝队演练

  • 攻击场景

  • GitHub Codespaces

左移安全

在传统的软件开发中,安全通常是在下游处理的:当软件准备发布时,安全部门或外部公司会进行安全审查。这个方法的问题在于,到那个时候很难修复架构问题。一般来说,修复安全漏洞越晚,成本就越高;如果不修复漏洞,可能导致数百万的损失,甚至可能导致一些公司破产。在开发生命周期中,越早修复安全漏洞,成本越低(见图 13.2):

图 13.2 – 开发生命周期中修复安全漏洞的成本

图 13.2 – 开发生命周期中修复安全漏洞的成本

这就是我们所说的左移安全:将安全融入到开发生命周期中,使其成为所有活动的必要部分。

问题在于,市场上没有足够的安全专家可以将他们安排到每个工程团队中。左移安全就是要通过教育工程师,培养安全优先的思维模式。

假设漏洞、零信任和安全优先的思维模式

传统的安全方法是防止漏洞。最重要的措施有:

  • 信任层级:内部网络被认为是安全的,并通过防火墙进行保护。只有公司拥有的设备才能访问网络,并通过虚拟专用网络VPN)隧道连接。公众互联网不被信任——两者之间是非军事区DMZ)。

  • 风险分析:通过威胁建模进行风险分析。

  • 安全评审:来自安全专家的架构和代码审查。

  • 安全测试:具有特定范围的外部安全测试。

但在防止攻击的方式下,是否公司已经遭受攻击这个问题几乎无法回答。

在 2012 年的一次采访中,前国家安全局NSA)和中央情报局CIA)局长迈克尔·海登将军表示:

“从根本上说,如果有人想进入,他们就能进入……接受这一点。”

这是假设已被攻破范式的基础:您很可能已经遭到攻击,无论您是否知道。始终假设您已经被攻破。这种思维方式能识别防止攻击方法中的漏洞。您如何做到以下几点?

  • 检测攻击和渗透?

  • 响应攻击?

  • 恢复数据泄漏或篡改?

这改变了安全措施,并增加了全新的重点。在假设已被攻破的范式下,您需要以下内容:

  • 一个中央的安全监控安全信息与事件管理SIEM)系统,用于检测异常。

  • 持续的现场测试您的事件响应IR)(演习)。

  • 战争游戏(红队-蓝队模拟)用来发现漏洞、提高意识、学习像攻击者一样思考,并训练响应措施。

  • 现场渗透测试:包括网络钓鱼、社会工程学和物理安全等复杂的攻击模拟。

  • 即使在您的网络中,也不要信任身份和设备(零信任)。

如果您的安全主要依赖于防御层次,一旦黑客通过钓鱼、社会工程学或物理攻击进入网络——他们可以轻松向前推进。在一个信任的网络中,通常会发现未保护的文件共享、没有安全套接字层SSL)保护的未打补丁的服务器、弱密码以及大多数系统中的单因素认证SFA)。在以云为先的世界中,这完全没有意义。

通过零信任访问您的服务,您始终验证身份——例如,通过多因素认证MFA),您验证设备、访问权限和参与交易的服务。图 13.3展示了如何实现零信任访问您的服务的示例:

图 13.3 – 零信任访问您的公司服务

图 13.3 – 零信任访问您的公司服务

如果你在公司使用软件即服务SaaS)云服务,你可能已经熟悉零信任。你必须使用多因素认证(MFA),但可以信任你的浏览器和设备以提高便利性。如果你出差,你会收到通知或必须批准来自不常见位置的登录尝试。如果你安装第三方应用程序,你必须授予这些应用访问信息的权限,且你可能不能从公共的、不受信任的设备访问高度机密的信息。

零信任意味着对所有服务应用相同的原则,无论你是否从内部网络访问它们。

攻击模拟

为了知道在发生事件时该怎么做,你应该定期进行演练,练习你的标准操作程序SOPs)以应对 IR 并提高响应时间。就像办公室里的火灾演习一样,如果不进行这些演练,你无法知道你的安全措施在真实火灾发生时是否真的有效。

你应该尝试在以下指标上有所改进:

  • 平均检测时间MTTD

  • 平均恢复时间MTTR

在这种演练中,你将模拟攻击场景,练习你的 IR 流程,并进行一次事后分析,总结演练中的经验教训。

以下是一些攻击场景的例子:

  • 服务被妥协

  • 内部攻击者

  • 远程代码执行

  • 恶意软件爆发

  • 客户数据被妥协

  • 拒绝服务DoS)攻击

通过练习这些演练,你可以确认你的 SOP 有效,并在发生真实事件时迅速高效地做出反应。

红队-蓝队演练

这些演练的一种特殊形式是红队-蓝队演练,也叫战争游戏,其中两个团队互相对抗,且都有内部知识。红队是攻击方,试图访问生产系统或窃取用户数据,蓝队则负责防守。如果蓝队检测到攻击并能够阻止它,蓝队获胜。如果红队能够证明他们成功访问了生产系统或窃取了数据,红队获胜。

团队构成

与普通攻击模拟的不同之处在于,团队对你的系统拥有的洞察力,因此更容易发现漏洞。红队-蓝队模拟是最复杂的攻击形式,具有最多的洞察力,相比其他所有减少安全风险的努力更具优势(见图 13.4):

图 13.4 – 通过攻击者的洞察力和攻击深度来减少风险

图 13.4 – 通过攻击者的洞察力和攻击深度来减少风险

团队应该由来自不同组织单位的成员组成。不要仅仅为红队和蓝队各选一个团队。团队的构成是成功游戏的关键。

对于红队,执行以下任务:

  • 使用来自不同团队的创意工程师,这些团队已经对安全感兴趣。

  • 添加具有组织内部经验的安全专家,或者寻求外部支持。

对于蓝队,执行以下任务:

  • 选择熟悉日志记录、监控和网站可靠性的运维工程师。

  • 添加了解网络安全和身份的工程师。

两队都应有向专家求助的可能性。例如,如果红队需要编写结构化查询语言SQL)语句来执行复杂的 SQL 注入攻击,他们可以寻求数据库管理员DBA)团队的帮助;或者当蓝队需要了解应用程序的内部信息,或者需要应用程序记录额外数据时,可以直接向开发和维护应用程序的团队求助。

游戏规则

游戏的主要目标是所有参与者的学习——学习像攻击者一样思考,学习如何检测和响应事件,学习公司中哪些漏洞可能被利用。第二个目标是娱乐。与黑客马拉松类似,这个活动应该是一个团队建设的活动,所有参与者都能感到有趣。

但为了确保游戏的成功而不伤害任何人,你需要一些游戏规则。

持续时间

红队与蓝队的演练可能会持续数天、数周甚至数月。选择一个攻击发生的时间段以及攻击本身的持续时间。一个好的起点是 3 周的时间和 3 天的攻击时间。根据需要调整时间。

法典和规则

为了让演练成功,你必须建立一些规则和行为准则,参与者必须遵守,具体如下面所述:

  • 两队不得造成实际伤害。这也意味着红队不应做超出实现目标所需的事情,且物理攻击应遵循常识(不要骚扰或威胁任何人,不要偷窃同事的钥匙或证件等)。

  • 不要透露被攻破人员的姓名。

  • 不要让付费客户的服务中断或泄露他们的数据!

  • 被攻破的数据必须加密存储和保护,且不能暴露给真实的攻击者。

  • 生产系统的安全性不得削弱,不能让客户面临风险。例如,如果红队能够修改源代码,禁用所有生产系统的身份验证,之后在代码中留下注释,并在部署完成后声称获胜。那么,尽管如此,你不能禁用真实客户使用的生产系统的身份验证。

这看起来似乎很显而易见,但如果你有竞争的团队,他们可能会在游戏中过于投入。最好提前阐明这些显而易见的规则。

提交物品

游戏结束时,团队提交以下项目:

  • 一个待办事项清单,列出必须修复的漏洞。严重漏洞必须立即修复。

  • 改进取证和分析能力的待办事项。

  • 向整个组织公开演练中的学习成果报告。

记住要让整个过程没有责备,不要暴露出被攻破的人员的名字。

从哪里开始

我知道很多人认为红队-蓝队演练只适合成熟度非常高的公司,但我认为红队-蓝队演练是每个公司提高意识、学习和成长的绝佳方式,尤其是在公司还在防止漏洞的发生并认为其内网是安全的情况下。如果你的成熟度不高,攻击会更容易。如果成熟度非常高,攻击需要更加复杂,而且很难成功进行攻击而不造成真正的伤害。

我更倾向于选择红队-蓝队演练而非常规的攻击模拟——它们更有趣,也是更好的学习方式。如果你不知道从哪里开始,可以寻求外部帮助。

如果你在第一次演练中发现了很多问题,而且红队轻松获胜,你可能会考虑更频繁地进行演练。如果没有,每年一次是我看到很多公司成功执行的节奏,但这也很依赖于你的具体情况。

只需完成第一次演练——接下来的进展会自然而然地跟上。

攻击场景

大多数人想到的 DevOps 和 DevSecOps 中的第一个攻击场景是利用漏洞执行代码,如SQL 注入跨站脚本XSS)或内存泄漏,例如缓冲区溢出。在第十四章《保护你的代码》中,我们将详细了解如何寻找这些漏洞以及如何将这一过程整合到你的交付流水线中。

但是,还有一些更简单的攻击场景,例如以下几种:

  • 未保护的文件共享和代码库

  • 文本文件、配置文件和源代码中的机密(例如测试账户、个人访问令牌PATs)、连接字符串等)

  • 钓鱼攻击

钓鱼攻击是一种特别简单的攻击方式。根据 2021 年的一项研究,19.8%的钓鱼邮件接收者点击了邮件中的链接,14.4%下载了附件(见Terranova 和 Microsoft,2021),而在定期进行钓鱼攻击演练的公司中,数字大致相同。在我一位客户的公司中,几乎 10%的员工在钓鱼攻击演练期间收到邮件后,点击邮件中的链接并在弹出的登录框中输入了自己的凭证!而这家公司已经进行了多年的钓鱼攻击演练。

网络钓鱼的一个问题是心理效应,叫做启动效应。即使你一般知道网络钓鱼攻击的样子以及检测它们的标志,一旦你正在等待某封邮件,或者你认为邮件属于你正在参与的某个情境,你就更容易忽视这些标志。一个好的例子是月底收到的一封钓鱼邮件,声称是来自你人力资源HR)部门,内容说你的薪水支付出现了问题。由于是月底,且你在期待薪水,这封邮件看起来并不奇怪。也许之前就曾遇到过问题。也许你刚查过,钱还没到账。它还会制造一定的紧迫感。如果你很急,可能会想尽快解决这个问题,确保薪水按时到账。如果在月末发送这样的钓鱼邮件,那么人们更有可能在整个月内点击类似邮件。另一个例子是共享文档。如果你刚和同事通话,他们说会与您共享一个文件,你可能只是好奇他们为什么选择这种方式,但并不怀疑,因为你本来就期待收到文件。你发送的钓鱼邮件越多,就越有可能有人恰好具备正确的情境,从而让你中招。

一旦攻击者设法攻破了第一个受害者并获得了公司凭证或访问了受害者的机器,局面将完全改变。现在,攻击由内部攻击者执行,他们可以从内部地址针对公司中的特定人员进行攻击。这被称为鱼叉式网络钓鱼,极其难以检测。

一个好的鱼叉式网络钓鱼目标是管理员或工程师。如果你没有执行最小特权用户权限,攻击者可能已经可以访问生产系统,或者已经是域管理员,届时就彻底完蛋了。但如果他们攻破了开发人员的账户,他们同样有多种选择,如下所示:

  • 使用mimikatz(见 https://github.com/gentilkiwi/mimikatz/wiki)从内存中读取凭证。

  • 测试环境:许多开发人员可以访问测试环境,通常是作为管理员。攻击者可以登录并使用 mimikatz 窃取其他凭证。

  • 修改代码:通常只需要一行代码就可以禁用身份验证。攻击者可以尝试修改代码或更改依赖项的版本,将其更改为一个已知存在漏洞并可以被利用的版本。

  • 执行脚本:如果开发人员可以修改管道代码或在部署过程中执行的脚本,攻击者可以插入在部署期间执行的代码。

这就是为什么在工程领域中,安全性如此重要,必须格外小心的原因。与组织中大多数其他部门相比,攻击面要大得多。

若要从一个已被攻陷的账户获取到域管理员权限,或者至少是具有生产访问权限的管理员权限,你可以使用一个名为BloodHound的工具(github.com/BloodHoundAD/BloodHound)。它支持Active DirectoryAD)和Azure ADAAD),并揭示所有隐藏的关系:谁在什么机器上有会话?谁是哪个组的成员?谁是某台机器的管理员?

蓝队和红队都可以使用此工具来分析 AD 环境中的关系。

GitHub Codespaces

由于开发环境在安全性方面是一个大问题,因此将其虚拟化并为每个产品配置一个专用机器是一个不错的主意。这样,你可以实现最小权限用户权限,工程师们就不需要在自己的机器上使用本地管理员权限了。同时,你还可以限制每个特定产品所需的工具数量,并减少攻击面。

当然,你可以使用传统的虚拟桌面基础架构VDI)镜像来实现,但你也可以选择一个更轻量的选项:开发容器(请参阅code.visualstudio.com/docs/remote/containers,这是Visual Studio CodeVS Code)的一个扩展,建立在其客户端-服务器架构之上)。你可以将 VS Code 连接到正在运行的容器,或实例化一个新的实例。完整的配置存储在代码库中(配置即代码),你可以与团队共享相同的开发容器配置。

GitHub Codespaces是开发容器的一种特殊形式,它是一个托管在 Azure 中的虚拟开发环境。你可以选择不同的虚拟机VM)规格,范围从 2 核/4 千兆字节GB随机存取存储器RAM)/32 GB 存储,到 32 核/64 GB RAM/128 GB 存储。虚拟机的启动时间非常快。默认镜像超过 35 GB,并在不到 10 秒钟内启动!

基础镜像包含了开发 Python、Node.js、JavaScript、TypeScript、C、C++、Java、.NET、gitkubectl、Gradle、Maven 和vim所需的一切。在你的代码空间内部运行devcontainer-info content-url,并打开它返回的统一资源定位符URL),以查看所有预安装工具的完整列表。

但是你不必使用基础镜像——你可以完全自定义你的代码空间,使用开发容器。你可以通过浏览器中的 VS Code、你本地的 VS Code 实例,或者通过终端使用安全外壳协议SSH)来使用代码空间。如果你在代码空间内部运行应用程序,你可以转发端口,从本地机器进行测试。图 13.5 展示了 GitHub Codespaces 的架构:

图 13.5 – GitHub Codespaces 架构

图 13.5 – GitHub Codespaces 架构

你可以在代码 | Codespaces | 新建 Codespace见图 13.6)下打开例如github.com/wulfland/AccelerateDevOps的仓库,如果你的帐户已启用 Codespaces。该仓库没有开发容器配置,因此它将加载默认镜像:

图 13.6 – 在 Codespace 中打开仓库

图 13.6 – 在 Codespace 中打开仓库

你可以在前面的截图中看到,我已经在main分支上运行了一个代码空间。除了创建一个新的代码空间外,我还可以打开已有的代码空间。选择虚拟机大小(见图 13.7):

图 13.7 – 为你的代码空间选择虚拟机大小

图 13.7 – 为你的代码空间选择虚拟机大小

在终端中,切换到ch9_release/src/Tailwind.Traders.Web目录,并使用以下命令构建并运行应用程序:

$ cd ch9_release/src/Tailwind.Traders.Web
$ dotnet build 
$ dotnet run

这将启动一个监听50005001端口的 Web 服务器。Codespaces 会自动检测到这一点,并将端口5000转发到本地端口。只需点击在浏览器中打开,即可在本地浏览器中查看正在你代码空间内运行的应用程序(见图 13.8):

图 13.8 – 将端口转发到你的机器

图 13.8 – 将端口转发到你的机器

你还可以在PORTS标签中手动添加应该转发的端口,并更改可见性,如果你想与同事共享链接—例如,让他们试用新功能(见图 13.9):

图 13.9 – 在你的代码空间中配置端口转发

图 13.9 – 在你的代码空间中配置端口转发

如果你想更好地控制开发环境,可以在你的代码空间中创建一个开发容器。通过点击绿色的Codespaces: 添加开发容器配置文件...打开 VS Code 中的命令面板,然后跟随向导选择要安装的语言和功能。向导会在你仓库的根目录创建一个.devcontainer文件夹,并在其中创建两个文件:一个devcontainer.json文件和一个Dockerfile文件。

Dockerfile文件定义了在初始化代码空间时创建的容器。Dockerfile文件可以非常简单—只要包含一个FROM语句,指示它从哪个基础镜像继承即可。

devcontainer.json文件中,你可以传递镜像创建的参数,可以定义与所有团队成员共享的 VS Code 设置,可以使用默认安装的 VS Code 扩展,还可以在容器创建后运行命令(见图 13.10):

图 13.10 – 示例 Dockerfile 文件和 devcontainer.json 文件

图 13.10 – 示例 Dockerfile 文件和 devcontainer.json 文件

查看 code.visualstudio.com/docs/remote/devcontainerjson-reference 获取关于如何自定义你的 devcontainer.json 文件的完整参考资料。

如果你更改了 Dockerfile 文件或 devcontainer.json 文件,你可以通过打开命令面板并执行 Rebuild Container 来重建容器。

如果你在 Codespace 中需要使用机密信息,你可以像处理其他机密一样,在组织或仓库级别的 settings/secrets/codespaces 中创建它们。机密信息可以作为环境变量在 Codespace 容器内使用。如果你添加了新的机密信息,你必须停止当前的 Codespace——仅重建容器是不够的。

当然,GitHub Codespaces 不是免费的——你需要为实例的运行时间付费。这些分钟数会每天报告给计费系统,并按月收费。费率取决于虚拟机的大小(见表 13.1):

表 13.1 – GitHub Codespaces 定价

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/Table_013.jpg)

表 13.1 – GitHub Codespaces 定价

此外,你需要为所使用的存储付费,每 GB 每月 $0.07。

如果你关闭浏览器,Codespaces 不会被终止。如果它们仍在后台运行,你可以更快地连接,但你仍然需要为它们付费。默认的空闲超时为 30 分钟,相当于 4 核机器的 $0.18。这个价格真的很便宜,但毕竟还是花钱的。如果你不再需要它,应该始终停止你的 Codespace。你可以在 设置 | Codespaces 下更改默认的空闲超时。

GitHub Codespaces 不仅在安全方面表现出色——它还可以提高你的入职时间和生产力。GitHub 本身在开发过程中使用它,它将新工程师的入职时间从几天缩短到了不到 10 秒!而且这还是一个几乎有 13 GB 磁盘空间、通常需要 20 分钟才能克隆的仓库(Cory Wilkerson,2021 年)。

Codespaces 可能并不适合所有产品,但对于 Web 应用程序来说,它是未来,它将彻底改变我们管理开发者机器的方式。它还帮助你弥补开发管道中的安全漏洞——你的本地开发机器。

总结

在本章中,你已经了解了安全对于开发过程的重要性,以及如何开始将安全向左转,并实施假设被攻破零信任文化。我介绍了攻击模拟红队-蓝队演练,帮助提高对安全的意识,发现漏洞,并练习应急响应(IR)。

我还向你展示了 GitHub Codespaces 如何帮助你降低本地开发环境的风险并提高你的生产力。

在下一章中,你将学习如何保护你的代码和软件供应链。

深入阅读

你可以使用本章中的以下参考资料获取更多关于所涵盖主题的信息:

第十四章:保障你的代码安全

2016 年,消息服务 Kik(www.kik.com/)与开源贡献者Azer Koçulu之间关于Kik名称的争执,导致了整个互联网的完全宕机。至少那天每个人都注意到出了问题。发生了什么?由于争执以及 npm 支持消息服务 Kik,Azer 将自己所有的包从 npm 注册表中撤回。撤回的包中包括一个名为left-pad的包。这个包的功能是向文本字符串的开头添加字符。left-pad是一个只有 11 行代码的简单模块:

module.exports = leftpad;
function leftpad (str, len, ch) {
  str = String(str);
  var i = -1;
  if (!ch && ch !== 0) ch = ' ';
  len = len - str.length;
  while (++i < len) {
    str = ch + str;
  }
  return str;
}

这是一个简单的单一功能,每个开发者都应该能够自己编写。然而,这个包却被纳入了全球使用的框架,比如React。当然,React 本身并不直接需要这 11 行代码,但它依赖的包又依赖于其他包——而这个依赖树中的某个包就依赖于left-pad。这个包的缺失几乎让整个互联网崩溃(参见Keith Collins 2016Tyler Eon 2016)。

如今,软件依赖于许多不同的软件——工具、包、框架、编译器和语言——每一个都有自己的依赖树。确保不仅是你自己的代码,整个软件供应链的安全性和许可证合规性都非常重要。

本章中,你将学习如何通过 GitHub Actions 和高级安全功能,帮助你消除代码中的错误和安全问题,并成功管理你的软件供应链。

本章的关键内容如下:

  • 依赖管理与 Dependabot

  • 秘密扫描

  • 代码扫描

  • 编写你自己的 CodeQL 查询

    GitHub 高级安全

    本章讨论的许多功能仅在你获得高级安全许可证后,才能在 GitHub Enterprise 中使用。其中一些功能对开源项目免费提供——但如果你的组织无法使用某些功能,可能是因为你没有获得相应的许可证。

依赖管理与 Dependabot

为了管理你的依赖关系,你可以使用软件组成分析SCA)工具。GitHub 提供了依赖图Dependabot 警报Dependabot 安全更新来管理你的软件依赖关系。

依赖图帮助你了解你的依赖树。Dependabot 警报检查你的依赖项是否存在已知漏洞,如果 Dependabot 发现任何问题,它会发出警报。如果你启用Dependabot 安全更新,Dependabot 将在依赖包的作者发布漏洞修复后自动创建拉取请求来更新你的依赖项。

依赖图默认对公共仓库启用,但私有仓库则没有启用。Dependabot 的警报和更新必须在所有仓库中启用。你可以在设置 | 安全性与分析中完成此操作(见图 14.1):

![图 14.1 – 启用依赖图和 Dependabot](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/图 14.1)

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_14_001.jpg)

图 14.1 – 启用依赖图和 Dependabot

在组织级别,你可以为所有仓库启用这些选项,并将其设为新仓库的默认设置。

探索你的依赖关系

如果启用了依赖图,它将开始寻找依赖关系。以下包生态系统是受支持的(参见表 14.1):

表 14.1 – 依赖图和 Dependabot 支持的格式

表 14.1 – 依赖图和 Dependabot 支持的格式

要探索你的依赖关系,你可以导航到洞察 | 依赖图。在依赖项标签下,你可以找到仓库中所有的清单文件依赖关系。你可以打开每个依赖项,并浏览树状结构。如果某个依赖项存在已知漏洞,你可以在右侧看到它。漏洞会标明严重性,并且有一个常见漏洞和暴露CVE)标识符。通过该标识符,你可以在国家漏洞数据库nvd.nist.gov)中查找漏洞详情。点击链接,它会将你引导到数据库中的条目(nvd.nist.gov/vuln/detail/CVE-2021-3749)或GitHub 安全顾问数据库github.com/advisories)。如果有修复该漏洞的版本,依赖图会建议你升级到该版本(参见图 14.2):

`

图 14.2 – 使用依赖图探索你的依赖关系

图 14.2 – 使用依赖图探索你的依赖关系

组织级别,在洞察 | 依赖项下,你可以找到所有启用了依赖图的仓库中的依赖项。除了仓库的洞察外,你还可以在此处找到所有使用的许可证。这可以帮助你检查产品的许可证合规性(参见图 14.3):

图 14.3 – 组织级别的依赖关系洞察

图 14.3 – 组织级别的依赖关系洞察

如果你想利用 GitHub 通知其他依赖你的包的人,你可以在安全 | 安全顾问 | 新草稿安全顾问下起草一个新的安全顾问。安全顾问包含标题、描述、生态系统、包名称、受影响版本(例如,< 1.2.3)、修复版本(1.2.3)和严重性。你可以选择性地添加多个常见弱点枚举CWE)(参见cwe.mitre.org/)。如果你已有 CVE ID,可以在此添加;如果没有,你可以选择稍后添加。

草稿在发布之前只对仓库所有者可见。发布后,公共仓库中的安全公告对所有人可见,并会添加到GitHub Advisory Databasegithub.com/advisories)。对于私人仓库,只有有权访问仓库的人员可以看到这些公告,且在请求官方 CVE 标识符之前,它们不会被添加到公告数据库中。

Dependabot

Dependabot是 GitHub 中的一个机器人,能够检查你的依赖项是否存在已知漏洞。它还可以自动创建拉取请求,以保持你的依赖项最新。

Dependabot 支持 npm、GitHub Actions、Docker、git 子模块、.NET(NuGet)、pip、Terraform、Bundler、Maven 等多个生态系统。完整的支持列表,请参见 docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates#supported-repositories-and-ecosystems

要启用 Dependabot,在.github目录中创建一个dependabot.yml文件。你需要选择包生态系统和包含包文件(即package.json文件)的目录。你必须指定 Dependabot 是每天、每周还是每月检查更新:

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"

你可以使用Dependabot secrets对私有注册表进行身份验证。在Settings | Secrets | Dependabot下添加一个新的秘密(见图 14.4

图 14.4 – 添加 Dependabot 秘密

图 14.4 – 添加 Dependabot 秘密

然后,将注册表添加到dependabot.yml文件中,并从secret上下文中访问该秘密:

version: 2
registries:
  my-npm-pkg:
    type: npm-registry
    url: https://npm.pkg.github.com
    token: ${{secrets.PAT}}
updates:
  - package-ecosystem: "npm"
    directory: "/"
    registries: 
      - my-npm-pkg
    schedule:
      interval: "daily"

有许多其他选项可以配置 Dependabot——你可以允许或拒绝某些包,向拉取请求添加元数据(如标签、里程碑和审阅者),自定义提交信息,或者更改合并策略。有关完整选项列表,请参见 docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates

你可以在Insights | Dependency graph | Dependabot下查看 Dependabot 更新的状态。如果有问题,每个更新条目会显示一个状态和警告图标。点击状态查看完整日志(见图 14.5):

图 14.5 – 查看 Dependabot 状态和日志文件

图 14.5 – 查看 Dependabot 状态和日志文件

你可以在 安全 | Dependabot 警报 中找到所有的 Dependabot 警报。你可以点击每个项目查看详细信息。如果 Dependabot 已经创建了拉取请求来修复该漏洞,你可以在列表中看到一个带有弹出菜单的链接(参见图 14.6):

图 14.6 – 查看 Dependabot 警报

图 14.6 – 查看 Dependabot 警报

请注意,这个列表中仅包含安全警报 —— 并不是所有创建的拉取请求都用于更新你的依赖项。这里也有很多尚未修复的安全警报。有时,唯一的修复方法是降级,如果你的某个依赖声明了更高的最低版本,那么就没有自动修复(参见图 14.7):

图 14.7 – 没有修复的漏洞详情

图 14.7 – 没有修复的漏洞详情

如果你仔细查看 Dependabot 的拉取请求,你会注意到许多附加信息。当然,变更本身只是清单文件中的版本号更新。但在描述中,它会添加包的发布说明 —— 如果有的话 —— 以及新版本中的所有提交的完整列表。Dependabot 还会添加一个兼容性评分,表示该更新与您的代码兼容的可能性有多大(参见图 14.8):

图 14.8 – Dependabot 拉取请求详情

图 14.8 – Dependabot 拉取请求详情

在描述中,你还会找到一份可以通过评论拉取请求发送给机器人的命令列表。你可以使用以下任何一个命令:

  • @dependabot cancel merge:取消先前请求的合并。

  • @dependabot close:关闭拉取请求,并阻止 Dependabot 重新创建它。你也可以通过手动关闭拉取请求来实现相同的结果。

  • @dependabot ignore this dependency:关闭拉取请求,并阻止 Dependabot 为此依赖创建更多拉取请求(除非你重新打开拉取请求或自行升级到该依赖的建议版本)。

  • @dependabot ignore this major version:关闭拉取请求,并阻止 Dependabot 为此主版本创建更多拉取请求(除非你重新打开拉取请求或自行升级到该主版本)。

  • @dependabot ignore this minor version:关闭拉取请求,并阻止 Dependabot 为此次要版本创建更多拉取请求(除非你重新打开拉取请求或自行升级到该次要版本)。

  • @dependabot merge:在你的 CI 测试通过后,合并拉取请求。

  • @dependabot rebase:为拉取请求执行变基操作。

  • @dependabot recreate:重新创建拉取请求,覆盖对拉取请求所做的任何编辑。

  • @dependabot reopen:如果拉取请求已关闭,则重新打开该拉取请求。

  • @dependabot squash and merge:在你的 CI 测试通过后,会压缩并合并拉取请求。

只需在拉取请求中的某个命令上评论,Dependabot 会为你处理剩下的工作。

使用 GitHub Actions 自动化 Dependabot 更新

你可以使用 GitHub Actions 为 Dependabot 更新添加更多自动化,但需要注意一些事项。如果 Dependabot 触发了一个工作流,则 GitHub 演员是 Dependabot(github.actor == "Dependabot[bot]")。这意味着GITHUB_TOKEN默认只有只读权限,若有需要,你必须授予写权限。填充在秘密上下文中的密钥是 Dependabot 的密钥!GitHub Actions 的密钥不能用于工作流。

以下是一个仅在 Dependabot 拉取请求触发时才会启动的工作流示例,并且该工作流会被授予对拉取请求、问题和项目的写权限:

name: Dependabot automation
on: pull_request
permissions:
  pull-requests: write
  issues: write
  repository-projects: write
jobs:
  Dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'Dependabot[bot]' }}

你可以使用Dependabot/fetch-metadata动作来提取关于更新依赖的信息。以下是一个示例,使用这些信息为拉取请求应用标签:

steps:
  - name: Dependabot metadata
    id: md
    uses: Dependabot/fetch-metadata@v1.1.1
    with:
      github-token: "${{ secrets.GITHUB_TOKEN }}"
  - name: Add label for production dependencies
    if: ${{ steps.md.outputs.dependency-type == 'direct:production' }}
    run: gh pr edit "$PR_URL" --add-label "production"
    env:
      PR_URL: ${{ github.event.pull_request.html_url }}

使用 GitHub CLI,添加自动化非常容易。例如,你可以自动批准并自动合并所有新的修复补丁:

- name: Enable auto-merge for Dependabot PRs
  if: ${{ steps.md.outputs.update-type == 'version-update:semver-patch' }}
  run: |
    gh pr review --approve "$PR_URL"
    gh pr merge --auto --merge "$PR_URL"
  env:
    PR_URL: ${{github.event.pull_request.html_url}}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GitHub Actions 与 Dependabot 的结合非常强大,可以消除几乎所有手动任务,以保持你的软件最新。结合一个可靠的 CI 构建和测试套件,你基本可以自动合并所有通过测试的 Dependabot 拉取请求。

使用 Dependabot 保持你的 GitHub Actions 最新

GitHub Actions 也是你必须管理的依赖项。每个动作都固定在某个版本(@后面的部分,例如uses: Dependabot/fetch-metadata@v1.1.1)。版本也可以是分支名,但这样会导致工作流不稳定,因为你的动作会在你不知情的情况下发生变化。最好将版本固定为标签或单独的提交 SHA。你可以让 Dependabot 检查更新并为你创建拉取请求,就像其他任何生态系统一样。将以下部分添加到你的Dependabot.yml文件中:

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"

如果有新的版本可用,Dependabot 将创建拉取请求。

秘密扫描

最常见的攻击路径之一是纯文本文件中的密钥。密钥绝不应以未加密和未保护的方式存储。GitHub 通过不断扫描你的所有公共仓库中的密钥来帮助你解决这个问题。你还可以为启用了GitHub 高级安全的组织中的私有仓库启用此功能。

当前,在公共和私有仓库中,约有 100 个公开的密钥和 145 个私有密钥被检测到——包括 Adobe、阿里巴巴、亚马逊、Atlassian、Azure 等。完整的列表请参见docs.github.com/en/code-security/secret-scanning/about-secret-scanning

作为服务提供商,您可以加入 秘密扫描合作伙伴计划 (参见 docs.github.com/en/developers/overview/secret-scanning-partner-program)。您的秘密将通过正则表达式检测,然后发送到一个端点,在那里您可以验证秘密是真实的还是误报。合作伙伴决定是撤销秘密还是仅通知客户秘密已泄露。

您可以在 设置 | 安全与分析 | GitHub 高级安全 中为私有存储库启用秘密扫描。在这里,您还可以通过点击 新模式 定义自定义模式(参见 图 14.9):

图 14.9 – 启用秘密扫描并添加自定义模式

图 14.9 – 启用秘密扫描并添加自定义模式

自定义模式是匹配您想要检测的秘密的正则表达式。您必须提供一些测试字符串以查看您的模式是否有效。GitHub 标记在测试字符串中找到的秘密为黄色(参见 图 14.10):

图 14.10 – 添加自定义秘密模式

图 14.10 – 添加自定义秘密模式

您还可以在秘密之前和之后自定义模式,并且可以添加必须匹配或不匹配的模式 – 例如,您可以使用额外模式 ([A-Z]) 强制字符串至少包含一个大写字母(参见 图 14.11):

图 14.11 – 自定义模式的高级选项

图 14.11 – 自定义模式的高级选项

自定义模式也可以在组织和企业级别定义,GitHub 将扫描启用了 GitHub 高级安全的企业或组织中的所有存储库。

当检测到新的秘密时,GitHub 根据用户的通知偏好通知所有具有存储库安全警报访问权限的用户。如果您正在关注存储库,已启用安全警报或存储库上的所有活动的通知,并且是包含秘密的提交的作者,则会收到警报,并且不会忽略存储库。

您可以在 安全 | 秘密扫描警报 下管理警报(参见 图 14.12):

图 14.12 – 管理秘密扫描警报

图 14.12 – 管理秘密扫描警报

如果在 GitHub 上提交了一个秘密,即使它只提交到了私有存储库,也应该认为该秘密已经泄露。请更换并撤销秘密。一些服务提供商会为您撤销它。

您可以使用 RevokedFalse positiveUsed in testsWon't fix 状态关闭警报(参见 图 14.13):

图 14.13 – 管理秘密扫描警报的状态

图 14.13 – 管理秘密扫描警报的状态

你还可以通过向.github文件夹添加secret_scanning.yml文件,排除源代码中的路径进行秘密扫描。该文件支持使用通配符的多个路径模式:

paths-ignore:
  - "tests/data/**/*.secret"

但要小心!这不应当用于在源文件中存储真实的秘密,即使是为了测试——应该将秘密存储为 GitHub 加密的秘密,或存储在安全的保险库中。

秘密扫描很简单——你基本上只需要启用它。但安全的重要性不容低估。

代码扫描

要在自己的代码中查找漏洞,可以使用静态应用程序安全测试SAST)。SAST 被认为是白盒测试,因为它可以完全访问源代码。它不仅仅是静态代码分析,通常静态分析包括构建软件。但与动态应用程序安全测试DAST)不同——我们将在第十五章《保护你的部署》中学习更多——它不是在运行时执行,而是在编译时执行。

GitHub 中的代码扫描

在 GitHub 中,SAST 被称为代码扫描,并且对所有公共仓库以及启用了 GitHub 高级安全的私有仓库都可用。你可以使用支持静态分析结果交换格式SARIF)的所有工具进行代码扫描。SARIF 是基于 JSON 的OASIS 标准,定义了静态分析工具的输出格式。GitHub 代码扫描目前支持SARIF 2.1.0,这是该标准的最新版本(见 https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning)。因此,任何支持 SARIF 2.1.0 的工具都可以集成到代码扫描中。

运行代码扫描

代码扫描使用 GitHub Actions 执行分析。大多数代码扫描工具会自动将结果上传到 GitHub——但如果你的代码扫描工具不这样做,你可以使用以下操作上传任何 SARIF 文件:

- name: Upload SARIF file
  uses: github/codeql-action/upload-sarif@v1
  with:
    sarif_file: results.sarif

该操作接受单个.sarif(或.sarif.json)文件,或者包含多个文件的文件夹。如果你的扫描工具不支持 SARIF,但结果可以转换,这很有用。例如,ESLint。你可以使用@microsoft/eslint-formatter-sarif将输出转换为 SARIF 并上传结果:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v2
      - name: Run npm install
        run: npm install
      - name: Run ESLint
        run: node_modules/.bin/eslint build docs lib script spec-main -f node_modules/@microsoft/eslint-formatter-sarif/sarif.js -o results.sarif || true
      - uses: github/codeql-action/upload-sarif@v1
        with:
          sarif_file: results.sarif

然而,大多数代码扫描工具已原生集成到 GitHub 中。

入门

要开始使用代码扫描,请转到/security/code-scanning/setup,这里会显示代码扫描选项的列表。顶部是 GitHub 原生的代码扫描工具——CodeQL 分析。但 GitHub 还会分析你的仓库,展示它可以在市场中找到的其他工具,这些工具适用于在你的仓库中检测到的语言——42CrunchAnchoreCxSASTVeracode 等等。在本书中,我们将重点介绍CodeQL——但其他工具的集成方式相同。如果你点击设置此工作流,GitHub 将为你创建一个工作流(见图 14.14):

图 14.14 – 设置代码扫描

图 14.14 – 设置代码扫描

如果你已经设置了代码扫描,你可以通过点击添加更多扫描工具来从结果页面添加额外的工具(见图 14.15):

图 14.15 – 仓库中的代码扫描警报

图 14.15 – 仓库中的代码扫描警报

工作流模板包含pushpull_requestschedule的触发器。可能会对schedule感到意外,但它有一个简单的解释——可能有新的规则检测到代码库中之前未曾识别的漏洞。因此,定期运行构建是一个不错的选择。触发器每周在随机的一天和时间运行一次。当然,GitHub 不希望所有代码扫描在同一时间运行。根据需要调整时间表:

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '42 16 * * 2'

工作流需要对安全事件具有写权限:

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

CodeQL 支持 C++(cpp)、C#(csharp)、Go、Java、JavaScript、Python 和 Ruby。GitHub 会尝试检测你仓库中使用的语言,并设置矩阵,使每种语言独立构建。如有必要,可以添加额外的语言:

strategy:
  fail-fast: false
  matrix:
    language: [ 'csharp', 'javascript' ]

分析本身相当简单——检查仓库,初始化给定语言的分析,运行autobuild,并执行分析:

steps:
- name: Checkout repository
  uses: actions/checkout@v2
- name: Initialize CodeQL
  uses: github/codeql-action/init@v1
  with:
    languages: ${{ matrix.language }}
- name: Autobuild
  uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
  uses: github/codeql-action/analyze@v1

autobuild步骤尝试构建你的源代码。如果失败,你需要更改工作流并手动构建代码。有时,仅仅在环境中设置正确的版本就足够了——例如,Node.js 或 .NET 的版本:

- name: Setup Node
  uses: actions/setup-node@v2.5.0
  with:
    node-version: 10.16.3  

代码扫描警报

你可以在每个仓库的设置 | 安全与分析 | 代码扫描下管理代码扫描警报——正如你在图 14.15中看到的。在组织级别,你可以看到所有仓库的概览,并跳转到各个结果页面(见图 14.16):

图 14.16 – 组织的安全概述

图 14.16 – 组织的安全概述

你可以像处理问题一样过滤、排序和搜索警报。

严重性

每个代码扫描警报都有一个分配的严重性。严重性是使用常见漏洞评分系统CVSS)计算的。CVSS 是一个用于沟通软件漏洞特征和严重性的开源框架(有关更多信息,请参见GitHub 博客 2021)。

严重性有助于你筛查警报。

在问题中追踪警报

跟踪代码扫描警报的最佳方式是在问题中。你可以通过点击警报中的创建问题来创建一个问题(参见图 14.17):

图 14.17 – 从代码扫描警报创建问题

图 14.17 – 从代码扫描警报创建问题

但这只是打开一个新问题,并将警报的链接添加到 Markdown 任务列表中(参见图 14.18):

图 14.18 – 将问题与代码扫描警报关联

图 14.18 – 将问题与代码扫描警报关联

警报会有一个指示器,表明它正被追踪在一个问题中——就像嵌套问题那样(参见下面的图 14.19)。

数据流分析

在代码下方区域,你可以看到代码中的警报详细信息。CodeQL 支持数据流分析,可以检测由数据流通过应用程序时产生的问题。点击显示路径以查看数据如何在应用程序中流动(参见图 14.19):

图 14.19 – 代码扫描警报的详细信息

图 14.19 – 代码扫描警报的详细信息

你可以跟踪整个应用程序中的数据流。在此示例中,你可以看到 12 个步骤,其中数据被分配并传递,直到被记录(参见图 14.20):

图 14.20 – 示例数据流

图 14.20 – 示例数据流

这就是 CodeQL 的真正力量。它不仅仅是对源代码的语义分析。

CodeQL 查询

在代码扫描警报中,你可以找到触发问题的查询引用。点击查看源代码以在 GitHub 上查看该查询(参见图 14.21):

图 14.21 – CodeQL 查询是开源的

图 14.21 – CodeQL 查询是开源的

这些查询是开源的,你可以在github.com/github/codeql找到它们。每种语言都有一个文件夹,在 CodeQL 文件夹中,你可以在 ql/src 下找到查询。查询的文件扩展名为 .ql

时间线

代码扫描警报还包含一个具体的时间线和 git blame 信息——问题首次被检测到的时间和提交?何时以及在哪里被修复?是否重新出现?这些信息有助于你进行警报筛查(参见图 14.22):

图 14.22 – 代码扫描警报的时间线

图 14.22 – 代码扫描警报的时间线

拉取请求集成

代码扫描与拉取请求集成良好。代码扫描结果集成到拉取请求检查中,详情页面显示结果概览(见图 14.23):

图 14.23 – 拉取请求中的代码扫描结果

图 14.23 – 拉取请求中的代码扫描结果

代码扫描还会在代码中为警报添加评论,你可以直接在此对结果进行分级,修改状态为误报已用于测试不修复(见图 14.24):

图 14.24 – 拉取请求源中的代码扫描评论

图 14.24 – 拉取请求源中的代码扫描评论

你可以在设置 | 安全性与分析 | 代码扫描下定义哪些警报严重性会导致拉取请求因安全问题和其他发现而失败(见图 14.25):

图 14.25 – 配置导致拉取请求失败的严重性级别

图 14.25 – 配置导致拉取请求失败的严重性级别

拉取请求集成帮助你保持主分支的清洁,并在合并之前检测问题,使代码分析成为审核过程的一部分。

代码扫描配置

有很多选项可以配置代码扫描。工作流中的init CodeQL 操作有一个名为queries的参数。你可以使用它选择默认查询集之一:

  • security-extended:比默认查询更低严重性的更多查询

  • security-and-quality:来自security-extended的查询,以及可维护性和可靠性查询

- name: Initialize CodeQL
  uses: github/codeql-action/init@v1
  with:
    languages: ${{ matrix.language }}
    queries:  security-and-quality

你还可以使用queries参数添加自定义查询。该参数接受本地路径或对其他仓库的引用,包括 Git 引用(branchtagSHA)。添加加号以将查询添加到默认查询之上:

  with:
    queries: +.github/codeql/custom.ql,org/repo/query.ql@v1

packs 参数:

  with:
    packs: +.github/codeql/pack1.yml,org/repo/pack2.yml@v1

重要提示

CodeQL 包在写作时仍处于测试阶段。有关包的更多信息,请参见codeql.github.com/docs/codeql-cli/about-codeql-packs/

你还可以使用配置文件,例如./.github/codeql/codeql-config.yml

- uses: github/codeql-action/init@v1
  with:
    config-file: ./.github/codeql/codeql-config.yml

如果上述内容位于另一个私有仓库中,则可以添加一个访问令牌,用于加载查询、包或配置文件:

    external-repository-token: ${{ secrets.ACCESS_TOKEN }}

在配置文件中,你通常会禁用默认查询并指定自己的查询。你还可以排除特定路径。以下是一个示例 – codeql-config.yml

name: "Custom CodeQL Configuration"
disable-default-queries: true
queries:
  - uses: ./.github/codeql/custom-javascript.qls
paths-ignore:
  - '**/node_modules'
  - '**/test'

你的自定义查询集(custom-javascript.qls)可以导入其他查询集(javascript-security-extended.qls),并从CodeQL 包codeql-javascript)中排除特定规则:

- description: "Custom JavaScript Suite"
- import: codeql-suites/javascript-security-extended.qls
  from: codeql-javascript
- exclude:
    id:
      - js/missing-rate-limiting

你还可以添加单个查询(- query : <查询路径>)、多个查询(-queries: <文件夹路径>)或查询包(- qlpack: <包名称>)。

CodeQL 非常强大,你有很多选项可以精细调整配置。详情请参见 docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning

编写你自己的 CodeQL 查询

CodeQL 附带了许多开箱即用的查询,尤其是如果你使用 security-and-quality 套件。但 CodeQL 的完整功能会在你开始编写自己的查询时展现。当然,这并不简单。CodeQL 是一种复杂的查询语言,如果你查看 github.com/github/codeql 上的一些查询,你会发现它们可能相当复杂。但如果你熟悉你的编程语言,编写一些简单的查询应该相当容易。

要编写 CodeQL 查询,你需要 Visual Studio CodeVS Code)和 GitHub CodeQL 扩展marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql)。

如果你已经安装了两者,克隆起始工作区:

$ git clone --recursive https://github.com/github/vscode-codeql-starter.git

注意 --recursive 参数!如果你忘记它,你将不得不手动加载子模块:

$ git submodule update --remote

在 VSCode 中,从起始工作区选择 vscode-codeql-starter.code-workspace 文件。

要从你的源代码创建数据库,你需要 CodeQL CLI。在 Mac 上,你可以通过 Homebrew 安装它:

$ brew install codeql

对于其他平台,你可以在此处下载二进制文件:github.com/github/codeql-cli-binaries/releases/latest.

将它们提取到一个文件夹中,并将其添加到 $PATH 变量中(在 Windows 上为 %PATH%)。

现在,进入你想存储数据库的文件夹并运行以下命令:

$ codeql database create <database name> \
  --language=<language> \
  --source-root=<path to source code>

这将为你的仓库中的语言创建一个数据库。对仓库中的所有语言重复此步骤。

现在,在 VSCode 中打开 QL 扩展并点击 数据库 | 从文件夹中导入。选择你在上一步骤中创建的数据库。你可以附加多个数据库并在它们之间切换(见 图 14.26):

图 14.26 – 将数据库附加到 VSCode CodeQL 扩展

图 14.26 – 将数据库附加到 VSCode CodeQL 扩展

你可以在起始工作区中找到所有支持语言的示例查询(codeql-custom-queries-<language>/example.ql)。查询文件中有一个带有元数据的注释头:

/**
 * @name Empty block
 * @kind problem
 * @problem.severity warning
 * @id javascript/example/empty-block
 */ 

然后,它们导入必要的模块。这些模块通常以语言命名(javascriptcsharpjava 等),但也可能是类似 DataFlow::PathGraph 这样的名称:

import javascript

查询本身有一个变量声明,一个可选的 where 块来限制结果,以及 select 语句:

from BlockStmt b
where 
  b.getNumStmt() = 0
select b, "This is an empty block."

查看 GitHub 上的 CodeQL 示例,了解如何开始。你对某种语言了解得越深,写查询就越容易。以下查询会搜索 C# 中的空 catch 块:

import csharp
from CatchClause cc
where
  cc.getBlock().isEmpty()
select cc, "Poor error handling: empty catch block."

在 VSCode 中,你拥有完整的 IntelliSense 支持(见 图 14.27),这在编写查询时非常有帮助:

图 14.27 – VSCode 中的 IntelliSense

图 14.27 – VSCode 中的 IntelliSense

如果你从上下文菜单中运行查询(CodeQL: 运行查询),它将在结果窗口中显示结果(见 图 14.28):

图 14.28 – CodeQL 查询结果

图 14.28 – CodeQL 查询结果

select 子句中的每个元素都有一个列。你可以点击代码元素,VSCode 会在准确的位置打开相应的源文件。

你完全可以写一本关于 CodeQL 的书。这只是一个非常简短的介绍,但我认为能够使用你自己的规则扩展代码扫描是非常有价值的。

查看 CodeQL 文档和语言参考,获取更多信息。

总结

在本章中,你已经学会了如何保护你的代码并控制你的依赖关系:

  • 你已经了解了 SCA,并且知道如何使用依赖关系图、Dependabot 警报和 Dependabot 安全更新来管理你的软件依赖。

  • 你已经了解了秘密扫描,它可以防止秘密信息在源代码中泄露。

  • 你已经学会了 SAST,并且知道如何使用 CodeQL 或其他支持 SARIF 的工具进行代码扫描,在开发过程中就发现问题。现在你可以编写自己的查询,以执行质量和编码标准。

在下一章,我们将看看如何保护我们的发布管道和部署。

进一步阅读

这是本章中的一些参考资料,你也可以用来进一步了解这些主题:

)

)

)

)

)

第十五章:保障你的部署安全

在本章中,我们将讨论如何保障整个部署和发布流水线的安全,超越代码和依赖关系,能够快速、安全、合规地将软件交付到安全环境中并满足监管要求。

本章我们将涵盖以下主要内容:

  • 容器和基础设施安全扫描

  • 自动化基础设施变更过程

  • 源代码和基础设施的完整性

  • 动态应用安全测试

  • 强化发布流水线的安全

容器和基础设施安全扫描

近年来最引人注目的黑客事件之一是SolarWinds,这是一家为网络和基础设施监控提供系统管理工具的软件公司。攻击者成功地在Orion软件中植入了后门,该软件被推出到超过 30,000 个客户中,并通过这个后门使其遭到入侵。客户中包括国土安全部和财政部(Oladimeji S., Kerner S. M., 2021)。

SolarWinds 攻击被视为一起软件供应链攻击,这对安装了被入侵版本的 Orion 客户来说是事实。但对 Orion 的攻击远比单纯更新一个受感染的依赖要复杂,攻击者获得了 SolarWinds 网络的访问权限,并成功在 SolarWinds 的构建服务器上安装了一个名为Sunspot的恶意软件。Sunspot 将后门Sunburst插入到 Orion 的软件构建中,通过替换源文件而不触发任何构建失败或其他可疑输出(Eckels S., Smith J., & Ballenthin W., 2020)。

该攻击展示了如果你的网络被入侵,内部攻击是多么致命,并且强调了保障整个生产线安全的重要性——不仅仅是代码、依赖关系和开发环境。构建服务器和所有其他参与软件生产的系统必须保持安全。

容器扫描

容器在今天的每个基础设施中都扮演着重要角色。与传统的虚拟机VMs)相比,容器有许多优点,但也存在一些缺点。容器需要一种新的运营文化,现有的流程和实践可能并不完全适用(见 Souppaya M., Morello J., & Scarfone K., 2017)。

容器由许多不同的层组成,像软件依赖一样,这些层可能会引入漏洞。为了检测这些漏洞,可以使用所谓的容器漏洞分析CVA),也叫做容器安全分析CSA)。

GitHub 本身并没有内建的 CVA 工具,但几乎所有的解决方案都可以很好地与 GitHub 集成。

一个非常受欢迎的开源容器镜像和文件系统漏洞扫描器是 Anchore 提供的grype(github.com/anchore/grype/)。它非常容易集成到你的 GitHub Actions 工作流中:

- name: Anchore Container Scan
  uses: anchore/scan-action@v3.2.0
  with:
    image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
    debug: true

另一个 CVA 扫描器的例子是Clair(github.com/quay/clair),它是一个开源解决方案,用于对 Docker 和开放容器倡议OCI)容器中的漏洞进行静态分析。Clair 可以作为一个容器运行,并将扫描结果存储在 Postgres 数据库中。请查看quay.github.io/clair/以获取完整的文档。

有商业化的容器扫描工具,通常是更全面安全平台的一部分。例如,Aqua容器安全(www.aquasec.com/products/container-security/)就是一个例子。Aqua 平台(www.aquasec.com/aqua-cloud-native-security-platform/)是一个云原生安全平台,适用于容器化、无服务器和基于虚拟机的应用程序。Aqua 可以作为 SaaS 或者自托管版本运行。

另一个例子是WhiteSource(www.whitesourcesoftware.com/solution-for-containers/))。他们在 GitHub 市场中提供了GP 安全扫描操作,用于在将镜像推送到 GitHub Packages 之前进行扫描(github.com/marketplace/actions/gp-security-scan)。

这两个都是很好的解决方案,但由于它们价格不菲且与 GitHub 的高级安全功能有很大重叠,因此我这里不会详细介绍。

基础设施策略

并非所有与基础设施相关的内容都是容器。从安全角度来看,还有更多的事项需要考虑,特别是在云环境中。

如果你使用的是云服务提供商,值得查看他们的安全组合。例如,Microsoft Azure 包含了 Microsoft Defender for Cloud,这是一款云安全姿态管理CSPM)工具,用于保护多云和混合环境中的工作负载,并查找云配置中的薄弱环节(azure.microsoft.com/en-us/services/defender-for-cloud)。它支持 Microsoft Azure、AWS、Google Cloud Platform 以及本地工作负载(通过 Azure Arc)。Microsoft Defender for Cloud 中的一些功能对于 Microsoft Azure 是免费的——但并非全部。

Microsoft Azure 还包含了 Azure Policy (docs.microsoft.com/en-us/azure/governance/policy/),这是一个帮助你强制执行标准并评估合规性的服务。它允许你定义某些规则作为策略定义,并按需评估这些策略。此示例位于一个每天早上 8 点运行的 GitHub Action 工作流中:

on:
  schedule:
    - cron:  '0 8 * * *'
jobs:
  assess-policy-compliance:
    runs-on: ubuntu-latest
    steps:
    - name: Login to Azure
      uses: azure/login@v1
      with:
        creds: ${{secrets.AZURE_CREDENTIALS}}
    - name: Check for resource compliance
      uses: azure/policy-compliance-scan@v0
      with:
        scopes: |
          /subscriptions/<subscription id>
          /subscriptions/<...>

与 AI 驱动的 安全信息和事件管理SIEM)系统 Microsoft Sentinel (azure.microsoft.com/en-us/services/microsoft-sentinel) 一起,这是一个非常强大的安全工具链。但是否适合你取决于你的设置。如果你的主要云提供商不是 Azure,那么你对 CSPM 和 SIEM 的选择可能完全不同,AWS 安全中心可能更适合你。

一个很棒的开源工具,用于确保 基础设施即代码IaC)的安全性是 Checkov (github.com/bridgecrewio/checkov),这是一个静态代码分析工具,扫描使用 TerraformTerraform planCloudFormationAWS 无服务器应用程序模型SAM)、KubernetesDockerfileServerlessARM 模板 提供的云基础设施,并检测安全性和合规性错误配置。它内置了超过 1000 个针对不同平台的策略。它在 GitHub 中非常易于使用,只需在工作流中使用 Checkov GitHub Action (github.com/marketplace/actions/checkov-github-action) 并指向包含你基础设施的目录:

- name: Checkov GitHub Action
  uses: bridgecrewio/checkov-action@master
  with:
    directory: .
    output_format: sarif

该操作支持 SARIF 输出,并可以集成到 GitHub 的高级安全功能中:

- name: Upload SARIF file
  uses: github/codeql-action/upload-sarif@v1
  with:
    sarif_file: results.sarif
  if: always()

结果会显示在 Security | Code scanning 警报下(见 图 15.1):

图 15.1 – GitHub 中的 Checkov 结果

图 15.1 – GitHub 中的 Checkov 结果

Checkov 非常适合检查你的 IaC,但它并不会检查你的基础设施变更。不过,如果你使用的是 Terraform 或 ARM 等解决方案,你可以定期在工作流中运行验证,以确保没有发生变化。

自动化基础设施变更过程

大多数 IT 组织都有变更管理流程,以降低操作和安全风险。大多数公司遵循 信息技术基础设施库ITIL)。在 ITIL 中,你需要通过 变更请求RFC)并由 变更咨询委员会CAB)批准。问题在于,CAB 的批准往往与不良的软件交付表现有关(见 Forsgren N., Humble, J., & Kim, G., 2018)。

从安全角度来看,变更管理职责分离非常重要,而且通常也是合规性要求的一部分。关键是要以 DevOps 方式重新思考这些基本原则。

使用基础设施即代码(IaC)和完全自动化的部署,所有基础设施变更都有完整的审计记录。如果你对过程有完全控制,最好的做法是将 CAB 设置为CODEOWNERS,并在拉取请求中进行审批。对于应用层的简单标准变更(例如 Kubernetes 集群中的容器),同行评审可能就足够了。对于更深层次的基础设施变更,涉及网络、防火墙或机密的,审查人员的数量会增加,并且你可以相应地增加专家。这些文件通常也存在于其他代码库中,不会影响开发人员的效率,也不会拖慢发布速度。

如果你受限于企业流程,这可能并不容易。在这种情况下,你需要尝试重新分类你的变更,使大部分变更获得预批准,并为这些变更使用同行评审和自动化检查,以确保安全。然后,为高风险变更自动化该过程,以便为 CAB 提供尽可能完整和准确的信息,以便快速审批(见Kim G., Humble J., Debois P. & Willis J., 2016, Part VI第二十三章)。

源代码和基础设施的完整性

在制造业中,提供物料清单BOM)是生产订单的常规做法。BOM 是一个包含原材料、子组件、中间组件、子部件和用于制造最终产品的零件的清单。

软件领域也有类似的概念:软件物料清单SBOM),但它仍然较为少见。

SBOM

如果你仔细观察软件供应链攻击,例如npm包,SBOM 可以帮助进行法医分析,并且可以用来比较不同版本的哈希值。

MsBuild.exe中。为了帮助防止和调查这类攻击,你需要扩展 SBOM,包含构建过程中的所有工具和构建机器上所有正在运行的进程的详细信息。

SBOM 有多种常见格式:

  • 软件包数据交换SPDX):SPDX 是一个开放标准的 SBOM,起源于 Linux 基金会。它最初是为了许可证合规性,但也包含版权、安保参考和其他元数据。SPDX 最近被批准为 ISO/IEC 标准(ISO/IEC 5962:2021),并且它符合 NTIA 的软件物料清单的最小元素要求。

  • CycloneDXCDX):CDX 是一个轻量级的开源格式,起源于OWASP社区。它经过优化,旨在将 SBOM 生成集成到发布流水线中。

  • 软件标识SWID)标签:SWID 是一种 ISO/IEC 行业标准(ISO/IEC 19770-2),由各种商业软件发布商使用。它支持自动化软件库存、对机器上软件漏洞的评估、缺少补丁的检测、配置检查清单评估、软件完整性检查、安装和执行白名单/黑名单等安全和操作性用例。它是进行构建机器上安装的软件库存的一个很好的格式。

每种格式有不同的工具和使用场景。SPDX 是由 syft 生成的。你可以使用 Anchore SBOM Action(见 github.com/marketplace/actions/anchore-sbom-action)为 Docker 或 OCI 容器生成 SPDX SBOM:

      - name: Anchore SBOM Action
        uses: anchore/sbom-action@v0.6.0
        with:
          path: .
          image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          registry-username: ${{ github.actor }}
          registry-password: ${{ secrets.GITHUB_TOKEN }}

SBOM 作为工作流工件上传(见 图 15.2):

图 15.2 – SPDX SBOM 作为构建工件上传

图 15.2 – SPDX SBOM 作为构建工件上传

FOSSology (github.com/fossology/fossology) 是一个开源的许可证合规解决方案,它也使用 SPDX。

.NET

- name: CycloneDX .NET Generate SBOM
  uses: CycloneDX/gh-dotnet-generate-sbom@v1.0.1
  with:
    path: ./CycloneDX.sln
    github-bearer-token: ${{ secrets.GITHUB_TOKEN }}

与 Anchore 操作不同,SBOM 不会自动上传,你需要手动上传:

    - name: Upload a Build Artifact
      uses: actions/upload-artifact@v2.3.1
      with:
        path: bom.xml

CDX 也用于 DependencyTrack 实例:

uses: DependencyTrack/gh-upload-sbom@v1.0.0
with:
  serverhostname: 'your-instance.org'
  apikey: ${{ secrets.DEPENDENCYTRACK_APIKEY }}
  projectname: 'Your Project Name'
  projectversion: 'main'

SWID 标签更多用于 软件资产管理SAM)解决方案,如 snow (www.snowsoftware.com/)、Microsoft System CenterServiceNow ITOM。如果存在,CDX 和 SPDX 可以使用 SWID 标签。

如果你想了解更多关于 SBOM 的信息,可以参见 www.ntia.gov/sbom

如果你完全在 GitHub Enterprise Cloud 上工作,并使用托管运行器,那么 SBOM 并不是那么重要。无论如何,所有相关数据都会连接到 GitHub。但如果你使用的是 GitHub Enterprise Server,拥有自托管的运行器,并且在发布管道中有其他商业软件,这些软件没有被公共包管理器消费,那么为所有发布生成 SBOM 可以帮助检测漏洞、许可证问题,并且在发生事件时帮助进行取证。

签署你的提交

我经常讨论的一个问题是是否应该签署你所有的提交。Git 是非常强大的,它给你提供了修改现有提交的可能性。但这也意味着,提交的作者不一定就是提交代码的人。一个提交有两个字段:authorcommitter。这两个字段的值来自 git config 中的 user.nameuser.email,再加上时间戳。如果你进行 rebase 操作,例如,committer 会变为当前的值,但 author 保持不变。这两个字段与 GitHub 的身份验证完全没有关系。

你可以在 Linux 仓库中查找Linus Torvalds的电子邮件地址,配置本地 Git 仓库使用该电子邮件地址,并将其提交到你的仓库。该提交将显示为 Linus 进行的提交(见图 15.3):

图 15.3 – 提交的作者信息与认证完全解耦

图 15.3 – 提交的作者信息与认证完全解耦

个人资料图片中的链接也可以工作,并会将你重定向到正确的个人资料页面。但该提交不会像你在服务器上通过 Web UI 修改文件或使用拉取请求合并更改时所做的提交那样,带有Verified徽章。Verified 徽章表示提交已使用GNU 隐私保护GPG)密钥签名,并包含你账户的已验证电子邮件地址(见图 15.4):

图 15.4 – GitHub 上的签名提交具有已验证徽章

图 15.4 – GitHub 上的签名提交具有已验证徽章

你可以在本地创建一个 GPG 密钥,并用它来签署你的提交(git commit -S)。当然,你完全可以在密钥中设置名称和电子邮件地址,它们必须与你在git config中配置的电子邮件和用户名匹配。只要你不修改提交,签名就是有效的(见图 15.5):

图 15.5 – 如果电子邮件和用户名匹配,本地签名的提交是有效的

图 15.5 – 如果电子邮件和用户名匹配,本地签名的提交是有效的

但是即使你将Pretty Good PrivacyPGP)密钥上传到你的 GitHub 个人资料(github.com/settings/gpg/new),提交仍然不会被验证,因为 GitHub 会在具有已验证电子邮件地址的个人资料中查找密钥(见图 15.6):

图 15.6 – 另一个用户的签名提交无法验证

图 15.6 – 另一个用户的签名提交无法验证

这是否意味着你必须在本地签署所有提交?我认为不必如此。问题在于强制开发者签署所有提交会拖慢工作进度。许多 IDE 和工具不支持签名。保持密钥同步、处理多个电子邮件地址——这一切都会变得更加麻烦。如果所有开发者都使用相同的电子邮件地址在公司设备上工作,这可能会非常顺利。但通常情况并非如此。人们远程工作,使用不同的机器和环境,他们在同一台机器上处理开源软件时,使用的电子邮件地址与处理公司代码时的电子邮件地址不同。其好处并不值得。如果攻击者拥有对你仓库的推送权限,你最不担心的就是伪造的电子邮件地址。

我的建议如下:

  • 选择一种依赖于mergesquashrebase来在服务器上合并更改的工作流,这样它们就会默认签名。

  • 如果你需要确保发布的完整性,可以签署你的标签(git tag -S)。由于 Git 是基于 SHA-1 或 SHA-256 的树,签署标签将确保所有父提交未被修改。

与其要求开发人员在本地签署所有提交并拖慢团队进度,不如在构建过程中签署代码,以确保构建后没有人篡改你的文件。

签署你的代码

签署二进制文件称为代码签署,即使你签署的是二进制文件而不是代码。你需要来自受信任机构的证书才能做到这一点。你如何在构建过程中签署代码在很大程度上取决于你的语言和编译方式。

要在 GitHub Actions 中签署你的 Apple XCode 应用程序,你可以使用此文档在构建过程中安装 base64 编码的证书和发布配置文件:docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development。不要忘记在与其他团队共享的自托管运行器上清理这些信息。在 GitHub 托管的运行器上,每个构建都会得到一个干净的环境。

根据你的代码签署解决方案,你可以在市场上找到多个适用于 Authenticode 和 signtool.exe 的操作。但由于所有签署解决方案都是基于命令行的,你可以像示例中 Apple 的做法一样,使用 secret 上下文将你的签名证书传递给工作流。

动态应用程序安全测试

为了增强应用程序安全性,你可以将动态应用程序安全测试DAST)集成到发布工作流中。DAST 是一种黑盒测试,模拟对正在运行的应用程序进行现实世界攻击。

有许多商业工具和 SaaS 解决方案(如Burp Suite来自PortSwiggerWhiteHat Sentinel),但分析这些内容超出了本书的范围。

也有一些开源解决方案。一个例子是 OWASP 的Zed 攻击代理ZAP)(www.zaproxy.org/)。它是一个独立的应用程序,可在 Windows、macOS 和 Linux 上运行(见www.zaproxy.org/download/),可以用来攻击 Web 应用程序。该应用程序允许你分析 Web 应用程序、拦截和修改流量,并使用 ZAP 蜘蛛对网站或其部分进行攻击(见图 15.7):

图 15.7 – OWASP ZAP 应用程序

图 15.7 – OWASP ZAP 应用程序

OWASP ZAP 启动浏览器并使用头显显示HUD)在网站顶部显示控件。你可以使用这些控件分析站点,使用蜘蛛执行攻击,或在不离开应用程序的情况下拦截请求(见图 15.8):

图 15.8 – HUD 显示正在被攻击的网站上的控制

图 15.8 – HUD 显示正在被攻击的网站上的控制

即使你不是渗透测试员,作为一名 Web 开发人员,使用 OWASP ZAP 开始并学习如何攻击你的网站应该是容易的。但为了向左推进安全,你应该将扫描集成到工作流中。OWASP ZAP 在 GitHub 市场上有三个 Actions(见图 15.9):

图 15.9 – GitHub 市场上可用的 OWASP ZAP Actions

图 15.9 – GitHub 市场上可用的 OWASP ZAP Actions

基线扫描完整扫描更快。API 扫描可用于扫描OpenAPISOAPGraphQL API。使用这些 Actions 非常简单:

- name: OWASP ZAP Full Scan
  uses: zaproxy/action-full-scan@v0.3.0
  with:
    target: ${{ env.TARGET_URL }}

该 Action 使用 GITHUB_TOKEN 将结果写入 GitHub 问题。它还将报告作为构建工件添加。报告可以作为 HTML、JSON 或 Markdown 格式获取(见图 15.10):

图 15.10 – 来自 OWASP ZAP 扫描的发现

图 15.10 – 来自 OWASP ZAP 扫描的发现

当然,这只适用于 Web 应用程序。还有其他用于其他场景的 DAST 工具。但这个例子展示了它如何轻松地集成到你的流水线中。大多数 DAST 工具是命令行工具或容器,或者它们已经具有集成,比如 OWASP ZAP。

安全加固你的发布流水线

CI/CD 流水线很复杂,攻击面很大。基本上,发布流水线是远程代码执行环境,应谨慎对待(参考Haymore A., Smart I., Gazdag V., Natesan D., & Fernick J., 2022 中的一些攻击示例)。

小心建模你的流水线并遵循最佳实践,尤其是在你构建高度定制化的流水线时。如果太晚了,最好寻求外部帮助,而不是后悔。

保护你的运行器

如果你使用 GitHub 托管的运行器,它们的职责是保持安全。运行器是临时的,每次执行都从干净的状态开始。但你执行的代码可能会访问 GitHub 中的资源,包括秘密。确保对你的 GitHub Actions 进行安全加固(参考加固你的 Actions部分)并限制 GitHub_TOKEN 的权限(工作流应以最小权限运行)。

自托管的运行器在你的环境中运行,你需要对其安全负责!以下是你应该遵循的一些规则:

  • 永远不要为公开仓库使用自托管的运行器。

  • 使你的运行器临时(或者至少在每次运行后进行清理,不要在磁盘或内存中留下工件)。

  • 保持你的镜像精简修补(只安装你需要的工具,并保持一切更新)。

  • 不要为所有团队和技术使用通用运行器。保持镜像的分离和专业化。

  • 保持运行器在隔离网络中(仅允许运行器访问所需资源)。

  • 仅运行安全的 Actions

  • 将跑步者包括在你的安全监控中,并检查是否有异常的进程或网络活动。

最好的解决方案是拥有一个动态扩展的环境(例如,Kubernetes 服务),并运行具有精简和修补的镜像的短期跑步者。

参见第七章运行你的工作流,了解有关自行托管和托管跑步者的详细信息。

保护你的 Actions

GitHub Actions 非常有用,但它们是你执行并授予访问权限的代码。你应该非常小心使用哪些 Actions,特别是在自行托管跑步者时。来自可信来源的 Actions,例如 GitHub、Microsoft、AWS 或 Google,不是问题。但即便如此,它们也接受拉取请求,因此仍然有可能漏洞会悄然通过。Actions 的最佳实践如下:

  • 始终审查代码。此外,查看所有者、贡献者数量、提交次数和日期、星标数等指标,以确保该 Action 属于一个健康的社区。

  • 始终通过明确的提交 SHA引用一个 Action。SHA 是不可变的,而标签和分支可能会被修改,导致你不知情地执行新的代码。

  • 如果你正在与分支合作,要求批准所有外部协作者,而不仅仅是首次贡献者。

  • 使用Dependabot保持你的 Actions 更新。

如果你是自行托管跑步者,你应该更加严格地限制可以使用的操作。有两个可能的选项:

  • 仅允许本地 Actions,并创建一个你已分析的 Action 的分支,引用该分支。这是额外的工作,但可以让你完全控制所使用的 Actions。你可以将 Actions 添加到本地市场中,方便发现(见 Rob Bos, 2022)。

  • Azure/*)。这个选项的安全性低于选项 1,但维护起来也较为简单。

你可以将这些选项配置为企业政策或为每个组织配置。

Actions 是来自其他人的代码,你在自己的环境中执行它们。它们是依赖项,可能会破坏你的发布能力,并引入漏洞。确保你的策略在速度和安全性之间找到最合适的平衡,以满足你的需求。

保护你的环境

使用环境保护规则必需的审阅者来批准发布,在它们被部署到环境中之前(请参见第九章部署到任何平台)。这确保在访问环境的密钥和执行代码之前,发布已被审查。

将其与分支保护代码所有者(请参见第三章团队合作与协作开发)结合使用,通过仅允许特定分支进入你的环境。这么做可以确保在批准部署时,必要的自动化测试和代码所有者的批准都已到位。

尽可能使用令牌

不使用作为机密存储的凭证连接到云服务提供商——如 Azure、AWS、GCP 或 HashiCorp——你可以使用OpenID ConnectOIDC)。OIDC 会交换短期有效的令牌进行身份验证,而不是使用凭证。你的云服务提供商也需要在其端支持 OIDC。

使用 OIDC,你不需要在 GitHub 中存储云凭证,你可以更细粒度地控制工作流可以访问哪些资源,并且你将拥有在工作流运行后会过期的轮换式、短期有效的令牌。

图 15.11展示了 OIDC 的工作原理概览:

图 15.11 – OIDC 与云服务提供商的集成

图 15.11 – OIDC 与云服务提供商的集成

步骤如下:

  1. 在你的云服务提供商和 GitHub 之间创建OIDC 信任。将信任限制为组织和存储库,并进一步限制对环境、分支或拉取请求的访问。

  2. GitHub OIDC 提供者在工作流运行期间自动生成 JSON Web Token。该令牌包含多个声明,用以建立特定工作流作业的安全且可验证的身份。

  3. 云服务提供商验证这些声明,并提供一个短期有效的访问令牌,该令牌仅在作业生命周期内有效。

  4. 访问令牌用于访问身份所拥有权限的资源。

你可以使用该身份直接访问资源,或者可以使用它从安全的保管库(例如Azure Key VaultHashiCorp Vault)获取凭证。这样,你可以安全地连接到不支持 OIDC 的服务,并通过保管库实现自动化的机密轮换。

在 GitHub 中,你可以找到配置 OIDC 以用于 AWS、Azure 和 GCP 的说明(见 docs.github.com/en/actions/deployment/security-hardening-your-deployments)。这些步骤是直接的。例如,在 Azure 中,你需要在Azure Active DirectoryAAD)中创建应用注册:

$ az ad app create --display-name AccelerateDevOps

然后,使用注册输出中的应用 ID 创建服务主体:

$ az ad sp create --id <appId>

然后,你可以在 AAD 中打开应用注册,并在证书和机密 | 联合凭证 | 添加凭证下添加 OIDC 信任。填写表单,如图 15.12所示:

图 15.12 – 为应用注册创建 OIDC 信任

图 15.12 – 为应用注册创建 OIDC 信任

然后,在订阅级别为服务主体分配一个角色。打开门户中的订阅。在访问控制(IAM) | 角色分配 | 添加 | 添加角色分配下,按照向导进行操作。选择一个角色(例如贡献者),然后点击下一步。选择用户、组或服务主体,并选择你之前创建的服务主体。

在 GitHub 中,你的工作流需要对 id-token 具有 write 权限:

permissions:
      id-token: write
      contents: read

在 Azure 登录操作中,使用客户端 ID(appId)、租户 ID 和订阅 ID 从 Azure 获取令牌:

- name: 'Az CLI login'
  uses: azure/login@v1
  with:
      client-id: ${{ secrets.AZURE_CLIENT_ID }}
      tenant-id: ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

此后,你可以使用Azure CLI访问资源:

- run: az account show

你也可以使用其他 Azure 操作并移除认证部分,在本例中是发布配置文件。他们将使用登录操作提供的访问令牌表单:

- name: Run Azure webapp deploy action using OIDC
  uses: azure/webapps-deploy@v2
  with:
    app-name: ${{ env.APPNAME }}
    slot-name: Production
    package: website

每个云提供商不同,但文档应能帮助你快速启动并运行:docs.github.com/en/actions/deployment/security-hardening-your-deployments

收集安全遥测数据

要确保从代码到生产环境的整个流水线的安全性,你需要对各个层级的实时数据进行洞察。不同层级有不同的监控解决方案(见图 15.13):

图 15.13 – 不同层级的监控

图 15.13 – 不同层级的监控

所有这些层级应将它们的数据报告给你的 SIEM 系统,以执行分析,并使用 AI 检测异常。许多组织在不同层级收集数据,但由于责任不同,常常忘记将其纳入监控。为了加固发布的安全性,你应该考虑以下事项:

  • 所有监控来源和事件纳入你的 SIEM 解决方案。

  • 监控整个流水线,包括你的代理和测试环境。涵盖所有进程和网络活动。

  • 记录部署事件及其对应的版本。如果在部署后突然有新进程运行或端口被打开,你希望能够将这些变化与此次部署关联,便于事后分析。

  • 收集实时应用安全数据并在工程师的仪表盘上显示。这可能包括程序异常终止SQL 注入尝试、跨站脚本攻击XSS)尝试、登录失败暴力破解攻击)或DDoS 攻击,但具体取决于你的产品。为了检测 SQL 注入或 XSS,你需要在编码用户输入之前加入额外的日志记录,特别是当输入包含可疑字符或元素时。

提高意识的最佳方式是看到威胁是真实存在的。

案例研究

直到现在,Tailwind Gears已经支付外部公司进行架构安全评审,帮助进行威胁建模风险分析,并在重大发布之前进行安全测试。他们从未遭受过数据泄露,至今大部分投资集中在网络安全上。但现在,随着他们越来越多地使用云服务,他们已经意识到,必须采取措施以能够检测响应恢复

IT 部门已经开始使用Splunk作为他们的SIEMITIM解决方案,并整合了越来越多的数据源,但直到现在,IT 部门仍无法确定是否能实时检测到正在进行的攻击。Tailwind Gears 决定改变他们处理安全问题的方式。他们与安全合作伙伴进行沟通,并计划首次进行红队/蓝队模拟。场景设定为内部攻击者攻击我们 DevOps 试点团队的 Web 应用程序。

模拟持续了 3 天,红队通过找到两种方式破坏生产环境获得胜利:

  • 对另一团队中几位开发者的鱼叉式钓鱼攻击成功,并揭露了其中一位开发者的凭证。利用BloodHound,他们发现该开发者可以访问以前的 Jenkins 服务器,而该服务器现在运行 GitHub Actions 运行器,且尚未完全迁移到 Kubernetes 解决方案中。该服务器未启用 MFA,且mimikatz能够捕获测试帐户的凭证。该测试帐户可以访问测试环境,他们可以在其中捕获管理员帐户的凭证,从而允许提取暂存环境的数据(在此情境中,这算作生产环境)。

  • 由于所有开发者都具有对所有代码库的读取权限,对 Web 应用程序的依赖分析显示,存在一个易受 XSS 攻击且尚未修补的依赖项。该组件是一个搜索控件,允许红队在另一团队的前端开发者协助下,在其他用户的上下文中执行脚本。他们在内部 GitHub 仓库中打开一个问题,并利用 GitHub API 在每次执行时向该问题发布评论作为证据。

模拟结果产生了许多待办事项,将在接下来的几周内处理。有些事情与我们的 DevOps 团队无关,例如为所有内部系统启用 MFA,或定期执行钓鱼模拟以提高员工的安全意识。

但许多事项也与团队有关。Tailwind Gears 决定将安全性嵌入开发流程中。这包括机密扫描、使用 Dependabot 进行的依赖管理代码扫描

团队还将与 IT 部门合作,通过将构建服务器迁移到 Kubernetes、在整个管道中实现安全日志记录、并使用OpenID Connect和安全的密钥库来处理机密信息,从而安全地加固发布管道。

大家都期待着三个月后的下一次红队/蓝队模拟。

摘要

在本章中,你已经学习了如何通过扫描容器和 IaC、确保代码和配置的一致性、以及对整个管道进行安全加固来保护发布管道和部署。

在下一章中,我们将讨论软件架构对软件交付性能的影响。

延伸阅读

以下是本章节的参考资料,你也可以通过这些资料进一步了解相关话题:

第四部分:软件架构

第四部分讲述的是软件架构与组织内部沟通的关系。你将学习如何将单体架构逐步转变为松耦合、基于事件的架构。

本书的这一部分包括以下章节:

  • 第十六章松耦合架构与微服务

  • 第十七章赋能团队

第十六章:松散耦合的架构与微服务

有趣的是,软件架构对软件交付性能的影响要大于你所构建的系统类型。无论你的产品是云服务、运行在制造硬件上的嵌入式软件、消费类应用、企业应用,甚至是大型机软件,这几乎对工程性能没有影响。如果你的架构具有某些特性,这对工程性能几乎没有影响 (Forsgren N., Humble, J., and Kim, G., 2018)。对于每种系统类型,都有高性能和低性能的差异。但架构的特性显然与工程开发速度相关,并使其成为关键的加速因素。

在本章中,我将给你介绍松散耦合的系统,以及如何通过演进你的软件和系统设计来实现更高的工程开发速度。

本章将涵盖以下主题:

  • 松散耦合的系统

  • 微服务

  • 演化设计

  • 基于事件的架构

松散耦合的系统

所有曾经在紧密耦合的单体应用程序上工作过的开发者都知道它所带来的问题。通信开销以及为了进行更大规模的变更所需的会议。在修复应用程序其他部分的错误后出现的新错误。改变破坏了其他开发者的功能。所有这些问题都会导致对集成和部署的恐惧,从而减慢开发者的工作速度。

在设计你的系统和软件时,你应该关注以下特性:

  • 可部署性:每个团队是否能够独立于其他应用程序或团队发布他们的应用程序?

  • 可测试性:每个团队是否能够在不需要部署其他团队独立解决方案的测试环境的情况下,完成大部分测试?

这里的团队规模是一个小型的两比萨团队(见第十七章赋能你的团队)。如果你为小团队的可部署性和可测试性设计系统,它将自动导致松散耦合的系统,并具有明确的接口。

微服务

松散耦合系统最常见的架构模式是微服务模式,“将单个应用程序开发为一组小型服务的方式,每个服务运行在自己的进程中,通过轻量级机制进行通信,通常是一个 HTTP 资源 API” (Lewis J. & Fowler M., 2014)。

微服务是从面向服务架构SOA)演化而来,并且具备一些额外的特性。微服务具有去中心化的数据管理——意味着每个服务完全拥有自己的数据。此外,微服务更倾向于使用轻量级消息传递,而非复杂的协议或中央协调来进行服务间通信——智能端点愚蠢的管道

微服务的一个重要特点常常被忽视——它们是围绕业务能力构建的。这也定义了一个服务应该有多小。为了定义服务的范围,你必须理解业务领域。一个微服务对应一个限界上下文,这是领域驱动设计中的概念(Eric Evans,2003)。

另一个特点是,微服务是完全独立且可部署测试的。这也是它们与高工程效率相关联的原因。

微服务有许多优势。它们的扩展性非常好,因为你可以独立扩展每个服务。它们还允许每个团队使用最适合其需求的编程语言和数据存储解决方案。最重要的是,它们允许大型和复杂应用中的团队快速行动,而不会干扰其他团队。

但这些优势也伴随着代价。基于微服务的应用程序复杂且难以操作和排除故障。

有许多著名的基于微服务的解决方案——例如,Netflix 和 Amazon。它们运行全球规模的服务,并拥有一种架构,使它们能够每天进行成千上万次的部署。

但也有许多公司尝试实施微服务并失败了。尤其是在绿色领域项目中,失败的比例特别高。其原因通常是对业务领域缺乏了解,并且错误地定义了每个服务的限界上下文,特别是当应用由外部公司开发,而该公司尚未学习业务领域的通用语言时。另一个原因是低估了操作服务的复杂性。

所以,在实现微服务之前,你应该关注架构的可部署性可测试性特性,并根据需求调整解决方案设计。需求是不断变化的,架构也应随着时间而演变。

演化设计

一些架构风格的优劣随着不同原因而发生变化。一个原因是你的应用的规模。另一个原因是你对业务领域和客户的了解,以及在大规模环境中操作的能力。根据这些因素,不同的架构风格可能更适合你(见图 16.1):

图 16.1 – 优势和劣势随着规模的变化而变化

图 16.1 – 优势和劣势随着规模的变化而变化

不断根据当前需求调整架构和系统设计被称为演化设计。要开始一个新项目,最好从单体架构和一个团队入手。这样可以在不增加太多开销的情况下快速推进。如果你扩展规模并对业务领域有了更多了解,可以开始使用编程语言的功能对应用进行模块化。最终,复杂度和规模会变得非常高,这时微服务能帮助你保持产品的可测试性和可部署性。

问题是——如何从现有的架构到达所需的架构?完全重写既昂贵又具有风险。更好的方法是逐步演化你的设计。马丁·福勒称之为缚树应用程序模式(马丁·福勒,2004)。缚树是一种植物,它在树的上部枝干上播种,逐渐将根伸向树下,直到它在土壤中扎根。支持的树被缚死并最终死亡——留下一个有机结构,它能够自我支撑。

与其重写应用程序,不如围绕它构建一个新的“缚树”应用程序,逐步让其发展,直到旧系统被“缚死”并可以关闭。

事件驱动架构

除了微服务、单体应用和多层应用外,还有其他架构风格——例如,事件驱动架构EDA)。EDA 是一个围绕事件的发布、处理和持久化的模式。其核心是消息代理——例如,Apache Kafka——各个服务或组件可以发布事件(发布者)或订阅事件(订阅者)。

EDA 与基于微服务的方法非常契合,但它也可以与其他架构风格一起使用。它可以帮助你保持松耦合组件或服务之间的一致性,并且由于事件的异步特性,它可以完美地水平扩展,因此非常适用于处理大量动态数据的解决方案,例如近实时处理传感器数据的物联网解决方案。

特别是在云原生环境中,EDA 可以帮助你快速行动,并在非常短的时间内构建松耦合和全球可扩展的解决方案。

与 EDA 经常一起使用的一种模式是事件溯源。事件溯源并不是持久化实体,而是将应用程序状态的所有变化——包括实体——作为一系列事件进行捕捉(见 马丁·福勒,2005)。为了检索实体,应用程序必须重放所有事件,直到获得最新状态。由于事件是不可变的,这提供了完美的审计追踪。你可以将事件流视为一个不可变的事实流,作为唯一的真实来源。除了可审计性之外,事件溯源在可扩展性和可测试性方面也具有许多优点。

如果你需要捕捉数据的意图、目的或原因,或者当避免冲突更新至关重要,并且需要保持历史记录并频繁回滚更改时,事件溯源是一个合适的模式。事件溯源与命令查询职责分离CQRS)模式非常契合——该模式将读取和写入操作分离开来。

但请注意,事件溯源是非常复杂的,并且将领域建模为事件并不是大多数开发者的自然方式。如果上述标准不适用于你的产品,那么事件溯源可能不是一个好的模式。

更适合简单领域的架构风格是Web-Queue-Worker。这是一种主要与无服务器 PaaS 组件一起使用的模式,包含一个用于处理客户端请求的 Web 前端和一个在后台执行长时间运行任务的工作进程。前端和后端是无状态的,并通过消息队列进行通信。该模式通常与其他云服务结合使用,如身份提供者、数据库、Redis 缓存和 CDN。Web-Queue-Worker 是开始构建云原生应用程序的一个不错的模式。

无论选择哪种架构风格,都应尽量保持简单。最好从简单开始,并随着需求的增加逐步演化设计,而不是过度设计,最终导致一个复杂的解决方案,这样反而会拖慢进度。

总结

如果您正在采用 CI/CD 和 DevOps 实践,但未能加速进展,那么您应仔细审视您的解决方案架构,因为它是衡量工程速度的关键指标之一。关注可部署性和可测试性特征,而不是架构风格。

在这一章中,我概述了松耦合系统的进化设计,并介绍了一些相关的架构风格和模式。

在下一章中,我们将讨论组织结构与软件架构之间的关联,以及它们如何在 GitHub 中结合起来。

进一步阅读

以下是本章中的参考资料,您可以通过它们获得更多关于这些话题的信息:

  • Forsgren N., Humble, J., 和 Kim, G. (2018). 加速:精益软件与 DevOps 的科学:构建与扩展高效能技术组织 (第 1 版) [电子书]. IT Revolution Press.

  • Lewis J. 和 Fowler M. (2014). 微服务: martinfowler.com/articles/microservices.html.

  • Eric Evans (2003). 领域驱动设计:软件核心复杂性应对. Addison-Wesley Professional.

  • Martin Fowler (2004). StranglerFigApplication: martinfowler.com/bliki/StranglerFigApplication.html.

  • Michael T. Nygard (2017). 发布它!:设计与部署生产就绪软件. Pragmatic Programmers.

  • Martin Fowler (2005). 事件溯源: martinfowler.com/eaaDev/EventSourcing.html.

  • Lucas Krause (2015). 微服务:模式与应用——通过应用模式设计细粒度服务 [Kindle 版].

第十七章:赋能你的团队

如果我的客户对他们的架构不满意,我会让他们解释他们产品的组织结构,并画出它的图示。如果你将这个组织结构图与他们的架构图进行比较,你总是能发现许多相似之处。组织结构与软件架构之间的这种关联被称为康威定律

在本章中,你将学习如何利用这种关联来改善你的架构、组织结构和软件交付性能。

以下是本章的核心主题:

  • 康威定律

  • 两个披萨团队

  • 反向康威操作

  • 交付节奏

  • 单库或多库策略

康威定律

康威定律追溯到 1968 年的一篇文章(康威,梅尔文,1968,第 31 页):

"设计系统的组织(……)被迫产生与组织通信结构相匹配的设计。"

– 梅尔文·E·康威

该法则不仅适用于软件或系统架构,也适用于任何系统的设计。请注意,它并不是指一个组织的管理结构,而是指其通信结构。这两者可能是相同的——但在某些情况下,它们并不相同。通常,如果组织结构图与软件设计不匹配,你可以寻找其通信流,它与组织结构图是不同的。

例如,如果你有许多小团队或独立开发者,他们从不同的客户或顾问那里接收需求,他们可能会在没有任何组织边界的情况下相互沟通。他们正在开发的系统将反映这一点,包含许多具有高度内聚性的模块,这些模块相互引用——这就是所谓的意大利面架构。而那些共同工作并通过一个通信渠道接收输入的团队——例如一个产品负责人——将构建一个模块内聚性较高的系统,而其他团队所开发的系统部分将引用较少。用埃里克·S·雷蒙德的话来说,"如果三个团队在做一个编译器,你会得到一个三遍编译器"(见雷蒙德,埃里克 S. 1996,第 124 页)。图 17.1 直观地展示了这两个示例:

图 17.1 – 基于通信结构的不同软件设计示例

图 17.1 – 基于通信结构的不同软件设计示例

但是什么样的通信结构能够导致有助于加速团队工程速度的系统设计呢?

两个披萨团队

其中一个最受讨论的基于微服务的架构,可以在大规模下实现每天成千上万次的部署,就是亚马逊的架构。他们为团队设置使用两个披萨法则亚马逊,2020):

"我们试图创建的团队不应超过两个披萨能够喂饱的规模。"

– 杰夫·贝佐斯

但是,究竟能用两块披萨喂多少人呢?在我们的用户小组中,我们通常按照每三到四个人一块披萨来计算。这意味着团队的规模应为 6 到 8 人。在美国的 Giordano's 餐厅,他们采用了 3/8 规则——你订购的披萨数量应为喂食人数的三倍再除以 8:

这将导致每个团队最多由 5 到 6 人组成。所以,所谓的“两披萨团队”规模并没有明确的定义——我认为这与团队成员的饥饿感无关。这个规则仅仅意味着团队应该保持小规模。

大型团队的问题在于,随着每个新增成员,团队中成员之间的链接数量迅速增长。你可以使用以下公式计算链接数量:

这里,n表示团队中的人数。这意味着一个 6 人的团队有 15 个成员之间的链接——而一个 12 人的团队则已经有 66 个链接(见图 17.2):

图 17.2 – 团队成员之间的链接数量

图 17.2 – 团队成员之间的链接数量

如果人们在团队中工作,他们会体验到积极的协同效应。多样性和沟通有助于提高质量和成果。但是,如果你在团队中增加更多人,沟通成本和决策速度减慢会导致负协同效应(见图 17.3):

图 17.3 – 协作效果与团队规模

图 17.3 – 协作效果与团队规模

那么,什么是神奇的数字——团队的最佳人数是多少?

美国海军海豹突击队认为,四人是作战团队的最佳规模(Willink, J. 和 Leif Babin, L., 2017)。他们还依赖于在复杂环境中的高频率沟通。但是,作战团队的技能可能比跨职能开发团队的技能更为线性。因此,并没有证据表明这个数字对于开发团队也是最优的。

在 Scrum 中,米勒定律认为神奇的七这个数字,±2(Miller, G.A., 1956),用来定义推荐的团队规模。米勒定律是 1956 年关于我们短期记忆限制的文章,旨在探讨与沟通能力相关的问题。但米勒定律已经被科学证伪,Scrum 之所以仍使用这个数字,是因为五到九人的团队在许多情况下确实是一个不错的规模——但这没有任何科学依据。也有只有 3 名成员的高效 Scrum 团队——还有一些团队有 14 名成员。

有一项来自 QSM 的研究分析了 491 个开发项目。研究得出结论,小型团队具有更高的生产力、更少的开发努力和更好的开发进度表(QSM, 2011)。团队规模在 1.5 到 3 人、3 到 5 人和 5 到 7 人之间的聚类结果非常接近。超过七人则会导致开发努力的急剧增加(见图 17.4):

图 17.4 – QSM 研究结果的总结

图 17.4 – QSM 研究结果的总结

有几个原因解释了为什么较小的团队表现优于较大的团队(见Cohn M., 2009,第 177–180 页):

  • 社会懒散:社会懒散是一种现象,指的是当人在一个团队中工作时,由于个人表现无法被衡量,他们往往会减少为达成目标所投入的努力(Karau, S.J., 和 Williams, K.D., 1993)。较小的团队通常较不受社会懒散的影响。

  • 凝聚力和主人翁精神:较小的团队更容易进行建设性的互动,成员们更容易建立信任、共同拥有感和凝聚力(Robbins S., 2005)。

  • 协调工作:在较小的团队中,协调所花费的时间较少。简单的事情——例如协调会议——在更大的团队中往往会变得更加复杂。

  • 更有回报:在较小的团队中,个人的贡献更容易被看到。这一点,加上更好的社会凝聚力,导致如果团队规模较小,环境会更具回报性(Steiner, I.D., 1972)。

当然,较小的团队也有一些缺点。最大的缺点是失去一个或多个团队成员的风险,这在小团队中更难以弥补。另一个缺点是缺乏某些专家技能。如果你需要在五个领域拥有深厚的专业知识,三人团队几乎不可能提供这种能力。

从这些数据来看,两块披萨团队的最佳规模大约是在三人到七人之间——根据环境的不同,平衡优缺点。

反向 Conway 法则

现在我们知道了团队的最佳规模,我们可以进行一种叫做反向 Conway 法则的操作(Forsgren N., Humble, J., 和 Kim, G., 2018,第 102 页)。如果你将组织结构发展为自主的两块披萨团队,你的架构将演变为一个更加松耦合的结构。

但不仅仅是团队的规模!如果你围绕功能创建团队,这将导致分层或多层架构。如果你将前端开发人员和数据库专家放在同一个团队中,你的架构将在这些沟通点解耦(见图 17.5):

图 17.5 – 功能性团队导致分层架构

图 17.5 – 功能性团队导致分层架构

为了实现可部署和可测试的架构,赋能团队,你必须创建对业务成果负责的跨职能团队。这将导致理想的架构,帮助你快速行动(见图 17.6):

图 17.6 – 围绕业务能力对齐的跨职能团队,实现快速价值交付

图 17.6 – 围绕业务能力对齐的跨职能团队,实现快速价值交付

有四种团队拓扑结构对系统架构产生积极影响,因此也能提升软件交付绩效(Skelton M. 和 Pais M., 2019):

  • 价值流对齐团队:这是最重要的团队拓扑——跨职能团队,能够独立交付显著的客户价值,而不依赖其他团队的帮助。这些团队需要具备所有交付价值所需的技能——例如,用户体验(UX)、质量保证(QA)、数据库管理员(DBA)和运营技能。

  • 平台团队:负责构建平台的团队,通过减少复杂性和简化软件交付流程,使得与价值流对齐的团队能够交付价值。

  • 支持团队:帮助其他团队承担责任的团队,通常出现在入职、过渡或培训阶段。

  • 子系统团队:只有在绝对必要的情况下才应创建这种团队!如果某个子系统过于复杂,无法由价值流对齐团队或平台团队处理,可能更适合设置一个专门处理该子系统的职能团队。

每个团队必须有明确的责任,能够独立交付价值,而无需依赖其他团队完成某些任务,这一点非常重要。

为了实现对绩效的预期效果,必须限制团队之间的互动方式,确保互动方式仅限于以下三种互动模式之一:

  • 协作:两个或更多团队在一定时间内密切合作,共同承担责任。

  • 自助服务:一个团队将其价值作为服务提供给另一个团队。职责明确分离,服务尽可能容易且自动化地提供。

  • 促进:一个团队帮助另一个团队,协助其在一定时间内学习新知识或培养新习惯。

构建一个有效的团队拓扑,拥有良好且明确的沟通和互动,对系统架构和工程速度都有巨大的影响。

交付节奏

即使是跨职能的自主团队,你们之间仍然会有一些相互依赖和沟通流动。在本书的前几章中,当我解释工作流和度量标准时,我重点讲解了效率、流动、批量大小和持续交付的价值。但你仍然需要一些节奏来控制你的工作流。在 Scrum 中,这叫做经验过程控制。经过一段时间后,你需要暂停来检查采纳——不仅是你交付的内容,还有你的过程和团队动态。这个时间跨度在 Scrum 中被称为冲刺。我不喜欢这个词,因为它暗示了快速的节奏,而开发应该有一个持续稳定的节奏。如果你想跑马拉松,你不会进行短跑——产品开发是马拉松,而不是一系列短跑(但显然,马拉松这个比喻并不适用于橄榄球)。但无论你怎么称呼这些间隔,它们对持续学习、采纳和团队建设都很重要。这些间隔同样重要,用来进行沟通——与利益相关者和其他团队沟通。

这就是为什么这些间隔应该在所有团队之间对齐。它们应该确定稳定的节奏,并作为工程组织的心跳。

这些间隔不应太长,也不应太短。大多数公司最大为一个月,最小为 2 周。这并不意味着团队不能做更小的迭代或冲刺。它们仍然可以做 1 周的冲刺,只是需要与全局节奏对齐。你可以有更快的节奏,并与较慢的节奏对齐——但反之则不行(见图 17.7):

图 17.7 – 将更快的迭代与交付节奏对齐

图 17.7 – 将更快的迭代与交付节奏对齐

在这种情况下,x不一定需要以周为单位来衡量。当定义节奏时,要考虑到整个组织的脉搏。如果你们组织中的一切都按月运行,那么 3 周的节奏就无法与公司其他部分同步。在这种情况下,定义一个月的节奏——或者它的一部分——是更好的选择,且能减少摩擦。如果你的公司是上市公司并采用 4-4-5 日历制度,那么财务季度可能就是你的脉搏。观察组织的脉搏,并使你的冲刺节奏与其同步,这样这些间隔就能与组织脉搏保持和谐(见图 17.8):

图 17.8 – 将你的节奏与组织的脉搏同步

图 17.8 – 将你的节奏与组织的脉搏同步

如果你的节奏没有与组织同步,就会产生摩擦。会议将会冲突,反馈和数字可能在你需要时无法获得。与组织脉搏同步的一致节奏将有助于平滑工作流并改善沟通(Reinertsen D., 2009, 第 176–178 页)。

单一仓库或多仓库策略

除了团队规模和节奏外,如果你想执行逆康威操作,代码结构的方式也会影响你的架构。有两种策略:

  • 单一仓库策略:只有一个仓库,包含应用程序所需的所有模块(或微服务)。

  • 多仓库策略:每个模块或微服务都有自己的仓库,你必须部署多个仓库才能获得一个完整的工作应用程序。

两种策略各有优缺点。单一仓库策略的最大优点是可以轻松地部署和调试整个应用程序。但单一仓库往往会迅速变得非常庞大,这会降低 Git 的性能。此外,随着仓库的增长,独立部署和测试应用程序的不同部分变得困难,这导致了架构的耦合度更紧密。

使用大型单一仓库

在 Git 的上下文中,大型仓库意味着什么?Linux 内核的仓库大约是 3 GB。克隆这个仓库需要一些时间,单个 Git 命令也比较慢——但仍在可以接受的范围内。而 Windows 仓库大约是 300 GB——是 Linux 内核的 100 倍。在 Windows 仓库上执行某些 Git 操作需要一些时间:

  • git clone:大约 12 小时

  • git checkout:大约 3 小时

  • git status:大约 8 分钟

  • git add 和 git commit:大约 3 分钟

这就是为什么微软维护自己分叉的 Git 客户端 (github.com/microsoft/git)。这个分叉包含了许多针对大型仓库的优化。它包括 scalar CLI (github.com/microsoft/git/blob/HEAD/contrib/scalar/docs/index.md),可以用来设置高级 Git 配置、在后台维护仓库,并帮助减少网络传输的数据。这些改进大大减少了 Git 操作在 Windows 仓库中的时间:

  • git clone:从 12 小时到 90 秒

  • git checkout:从 3 小时到 30 秒

  • git status:从 8 分钟到 3 秒

许多这些优化现在已经是 Git 客户端的一部分。你可以使用例如 git sparse-checkout (git-scm.com/docs/git-sparse-checkout),它允许你只下载仓库中需要的部分。

只有当你的仓库真的非常庞大时,你才需要使用微软的分叉版本;否则,你可能可以通过正常的 Git 功能进行优化。

通过主题和星标列表组织你的仓库

多仓库策略的最大优点是降低了各个仓库的复杂性。每个仓库可以独立维护和部署。最大缺点是很难构建和测试整个应用程序。但是,要从真实用户那里获取反馈或调试复杂的错误,通常仅部署单一服务或模块是不够的——你需要更新整个应用程序。这意味着需要在多个仓库边界之间协调多个部署。

如果你选择多仓库策略,你将会拥有许多小型仓库。一个好的命名规范有助于结构化它们。你也可以使用话题来组织你的仓库。话题可以在仓库的右上角设置(见图 17.9)。

图 17.9 – 你可以为仓库设置话题,以提高可发现性

图 17.9 – 你可以为仓库设置话题,以提高可发现性

你可以使用 topic:关键词来过滤你的仓库(见图 17.10):

图 17.10 – 根据话题过滤仓库

图 17.10 – 根据话题过滤仓库

另一个可以帮助你组织大量仓库的功能是星标列表。这是一个个人功能,不能共享。在你的 GitHub 个人主页中,你可以创建列表并组织你收藏的仓库(见图 17.11):

图 17.11 – 将你收藏的仓库按列表进行组织

图 17.11 – 将你收藏的仓库按列表进行组织

你可以像浏览器中的收藏夹一样使用这些功能,但它们并不能解决部署、调试或测试整个应用程序的问题。

如果你正在使用 Kubernetes 来管理微服务,你可以在 Visual Studio Code 中使用Bridge to Kubernetes 插件(marketplace.visualstudio.com/items?itemName=mindaro.mindaro)在生产或测试集群的上下文中调试本地服务(见Medina A. M., 2021)。但是,如果你依赖一次性构建和部署所有服务,那么最好的解决方案是拥有一个引用所有服务作为子模块的元仓库。

使用 Git 子模块来构建你的代码结构

你可以使用一个包含所有其他仓库作为子模块的元仓库。这样,你可以通过一条命令克隆所有仓库:

$ git clone --recurse-submodules

或者,如果你已经克隆了元仓库,可以使用以下命令来更新它:

$ git submodule update --init --recursive

该仓库可以包含部署整个应用程序的脚本或工作流。

你可以使用这个元仓库来进行发布管理,并将稳定版本打包在一起。如果你使用分支进行发布,那么你可以将子模块设置为某个分支,并在发布最新版本之前更新它:

$ git config -f .gitmodules submodule.<SUB>.branch main
$ git submodule update --remote

如果你使用标签来管理版本,那么你可以将每个子模块设置为特定版本,并将其提交到你的元仓库中:

$ cd <SUB>
$ git checkout <TAG>
$ cd ..
$ git add <SUB>
$ git commit -m "Update <SUB> to <TAG>"
$ git push

其他人员可以拉取更改并更新子模块到与标签对应的版本:

$ git pull
$ git submodule update --init --recursive

Git 子模块是处理多仓库并独立部署的一个不错方式,同时仍能管理整个应用程序。但要注意,你的相互依赖关系越多,元仓库的维护和保持其可部署状态的复杂性就越大。

什么是正确的策略?

如果单一仓库策略或多仓库策略更适合你的团队,它紧密联系到第十六章松耦合架构和微服务,我们在其中讨论了进化设计。单一仓库适用于小型产品和全新项目。随着规模和复杂性的增长,最好将微服务或模块拆分并将其移到独立的仓库中。但始终要牢记可测试性和可部署性——既要考虑单个服务/模块,也要考虑整个应用程序。

案例研究

在前三个成功的冲刺后,更多的团队在Tailwind Gears被迁移到新平台。首批团队已经被选中,负责一个已经可以独立测试和部署的产品。尽管包含了 Scrum Master、产品负责人和 QA 成员,团队的规模稍大,超出了两披萨团队的规则,但这个问题稍后会解决。接下来的团队则大得多,他们正在处理有很多相互依赖关系的大型单体应用。为了执行“逆康威”操作,所有团队汇聚在一起,进行自我组织,决定哪些团队将被迁移到新平台。约束条件如下:

  • 不大于一个两披萨团队

  • 负责业务能力(StranglerFigApplication 模式,并能独立进行测试和部署)。

这有助于推动应用程序设计的演变。新的微服务是云原生的,并拥有自己的云原生数据存储。它们通过 API 和事件驱动架构集成到现有应用中。微服务被移到新平台上的独立仓库中,因为它们大多数时候是独立部署的。与其他团队的同步是通过功能标志来完成的。

对于嵌入式软件,这种方法不可行。团队需要一种方式来构建和部署整个应用程序。但他们也希望能够独立部署和测试单个模块。这就是为什么团队决定将应用程序拆分成不同的仓库,并拥有一个包含其他仓库作为子模块的元仓库。这允许各个团队随时将其模块部署到测试硬件上,以在现实场景中测试新功能——但它仍然保持产品在一个可以随时发布的状态。

当第一个团队迁移到新平台时,他们保持了原有的冲刺节奏,每个周期为 3 周。由于团队可以或多或少地自主工作,这并不成问题。随着越来越多的团队加入新平台,节奏逐渐与其他团队同步。Tailwind Gears 是一家上市公司,过去所有的业务报告都是按季度进行的。公司也会每周进行报告,并采用标准化的 4-4-5 日历。每个季度的开始和结束时有很多会议,这些会议经常与冲刺会议发生冲突。团队决定调整他们的节奏以适应这种节奏。一个季度由 13 周组成——但其中一周会有季度会议,因此这一周会从冲刺日历中去除。这一周也用于季度的“大房间规划”。剩下的 12 周被分为 6 个两周的冲刺。

总结

在这一章中,你学习了如何利用团队结构和沟通流对软件和系统架构的影响,执行逆向 Conway 操作。这有助于你实现松耦合的架构,使得单元能够自主测试和部署,并对软件交付表现产生积极影响。

在接下来的章节中,我们将更多关注构建什么,而不是如何构建它。你将学习精益产品开发,以及如何将客户反馈融入到你的工作中。

进一步阅读

这些是本章的参考资料,你还可以通过它们了解更多相关话题:

  • Conway, Melvin (1968). 《委员会是如何发明的》www.melconway.com/Home/pdf/committees.pdf

  • Raymond, Eric S. (1996). 《新黑客词典》第 3 版,MIT 出版社

  • Amazon (2020): 《AWS 上的 DevOps 简介》 - 双披萨团队docs.aws.amazon.com/whitepapers/latest/introduction-devops-aws/two-pizza-teams.html

  • Willink, J. 和 Leif Babin, L. (2017). 《极端责任:美国海军海豹突击队如何领导并赢得胜利》,Macmillan

  • Miller, G.A. (1956). 《神奇的七个数字,加减二:我们处理信息能力的限制》psychclassics.yorku.ca/Miller/

  • Cohn M. (2009). 《敏捷成功之道:使用 Scrum 进行软件开发》,Addison-Wesley

  • QSM (2011). 《团队规模可能是成功软件项目的关键》www.qsm.com/process_improvement_01.html

  • Karau, S. J. 和 Williams, K. D. (1993). 《社会懒散:一项元分析回顾与理论整合》《人格与社会心理学杂志》,65(4),681–706。 doi.org/10.1037/0022-3514.65.4.681

  • Robbins S. (2005). 《组织行为学精要》,Prentice Hall

  • Steiner, I.D. (1972). 小组过程与生产力。Academic Press Inc.

  • Forsgren N., Humble, J. 和 Kim, G. (2018). 加速:精益软件与 DevOps 的科学:构建和扩展高效能技术组织(第 1 版)[电子书]。IT Revolution Press

  • Skelton M. 和 Pais M. (2019). 团队拓扑:为快速流动组织业务和技术团队。IT Revolution

  • Reinertsen D. (2009). 产品开发流程的原则:第二代精益产品开发。Celeritas Publishing

  • Medina A. M. (2021). 使用 VS Code 进行 Kubernetes 的远程调试developers.redhat.com/articles/2021/12/13/remote-debugging-kubernetes-using-vs-code

第五部分:精益产品管理

第五部分中,你将学习精益产品管理的重要性,如何将客户反馈融入工作流程,以及如何将假设驱动开发与 OKR 结合起来。

本书的这一部分包括以下章节:

  • 第十八章精益产品开发与精益创业

  • 第十九章实验和 A/B 测试

第十八章:精益产品开发与精益创业

到目前为止,我们只关注了如何构建和交付软件,而没有关注构建什么以及如何确定你是否在构建正确的东西。然而,精益产品开发实践对软件交付表现、组织表现和组织文化有着巨大的正面影响(Forsgren N., Humble J., & Kim G., (2018), 第 129 页)。因此,许多 DevOps 转型开始时会分析价值流,并试图优化产品管理与工程实践。然而,在我看来,这会导致太多的变化因素,而且这也是一个“先有鸡还是先有蛋”的问题。如果你不能在小批量的情况下频繁交付,就很难应用精益产品管理实践。

在本章中,我们将探讨如何应用精益产品开发和精益创业实践来构建让终端用户喜爱的产品。本章内容包括以下几点:

  • 精益产品开发

  • 融入客户反馈

  • 最小可行产品MVP

  • 企业投资组合管理

  • 商业模型画布

精益产品开发

构建正确的东西是困难的,且常常被低估。你不能仅仅问潜在客户他们想要什么。人们说他们想要的、他们真正想要的以及他们愿意为之付费的,是三件完全不同的事情。

精益产品开发是由丰田引入的,目的是解决其产品开发方法中的挑战,尤其是缺乏创新、开发周期长和许多重复开发周期的问题(Ward, Allen 2007 第 3 页)。

精益产品开发建立在跨职能团队的基础上,这些团队采取增量方法。其主要特点如下:

  • 小批量工作。

  • 使工作流可见。

  • 收集并实施客户反馈

  • 团队实验

正如你所看到的,这与我们在第一部分中学到的内容——精益管理和协作——完全一致。新的维度是客户反馈和实验。但没有小批量工作的能力和可见的工作流,就无法基于客户反馈进行实验。

融入客户反馈

但是,如何收集客户反馈并将其作为学习融入产品中呢?最重要的是,你需要具备自主权才能做到这一点。只要你的团队仍然需要按照要求交付产品,你就无法从客户反馈中学习并将反馈融入到产品中。除此之外,你还需要团队中拥有正确技能的人,或者必须培训你的工程师。产品管理用户体验设计是大多数团队中缺乏的技能,但它们对于从客户反馈和互动中学习至关重要。

收集客户反馈的一种方式是通过采访客户或进行游击式可用性测试(见第十二章通过左移测试提升质量)。但在解读结果时,必须非常小心。人们说什么和他们的行为通常是完全不同的两件事。

要真正完成反馈闭环并从客户行为中学习,你需要以下几个方面:

  • 客户数据(不仅仅是采访,还包括反馈、使用数据、评估和性能数据等)

  • 解读数据的知识(产品管理技能

  • 一种科学的方法

精益创业方法论将产品管理从直觉(炼金术)转变为科学方法,通过假设驱动的实验,采用构建-衡量-学习循环(见Ries, Eric 2011):

  • 你基于当前客户反馈/数据的分析,制定一个假设

    We believe {customer segment},wants {product / feature} because {value proposition}
    
  • 为了验证或证伪假设,团队进行实验。实验将影响某些度量指标。

  • 团队分析受实验影响的度量指标并从中学习,通常是通过制定新的假设。

图 18.1展示了用于假设驱动实验的构建-衡量-学习循环:

图 18.1 – 假设驱动的实验,构建-衡量-学习循环

图 18.1 – 假设驱动的实验,构建-衡量-学习循环

实践假设驱动的开发并不容易。你需要大量的度量数据,并且要很好地理解最终用户是如何使用你的应用程序的。仅有使用数据是不够的。你必须能够将这些数据与性能指标、错误日志、安全日志以及自定义指标结合起来,以全面了解发生了什么。多少人因为应用太慢而停止使用?有多少人无法登录?多少密码重置是攻击行为,多少是正常用户无法登录?你进行的实验越多,你会发现自己对用户行为的了解中存在的空白越多。但每完成一个循环,你都会学习到更多,并增加更多的度量指标,最终你将构建出对用户更有价值的应用程序。而且,你将学到哪些功能能为用户带来真正的价值,哪些只是浪费,你可以去除它们,使产品更加精简。

假设驱动的实验可以与目标与关键结果OKRs)完美结合——见第一章重要的度量指标。OKRs 使你能够通过设定特定度量标准上的可衡量关键结果(如增长、参与度或客户满意度),将你的自主团队与更大的愿景对齐。

最小可行产品(MVP)

过去几年中,MVP 这个术语被滥用得最为严重。曾经被称为概念验证PoC)或技术探索spike)的所有东西,现在都被称为 MVP。但 MVP 是产品的一个版本,它通过最小的努力完成一次完整的构建-测量-学习循环(Ries, Eric 2011 第 996 页)。

我经常看到的一个图示非常能够引起观众的共鸣:

图 18.2 – 构建 MVP 的错误示例

图 18.2 – 构建 MVP 的错误示例

它展示了你应该在每次迭代中通过解决问题领域来交付价值——在这个例子中是交通问题。问题在于,这不是一个 MVP。这是敏捷交付。但一辆自行车并不能帮助你测试一辆跑车的价值主张!特斯拉不可能通过创造一辆电动自行车来进行电动跑车成功的实验。

如果你用真实客户测试 MVP,始终要记住,它可能会毁掉你的声誉,并让你失去客户。MVP 不能只是最基本的功能。它还必须可靠、易用,并且具有吸引力:

图 18.3 – 一个 MVP 必须测试需求层次中的所有级别

图 18.3 – 一个 MVP 必须测试需求层次中的所有级别

因此,如果你已有现有产品和客户基础,使用 MVP 进行实验会更加容易。对于初创公司和新产品来说,这要困难得多,并且在将 MVP 推出市场之前,必须进行可用性和可靠性测试。如果不这样做,实验可能完全失败。但即使是对于现有产品,在尝试新功能时,确保这些功能可靠、易用并令人愉悦也同样重要!

企业投资组合管理

在初创公司中,通常较为容易——至少在开始阶段是这样。但是,如果你有多个团队和多个产品,问题就来了:如何确保跨职能、自主的团队朝着相同的方向努力,并做出服务于组织长期目标的决策呢?

要实践精益产品开发,你的公司需要从指挥控制型流程转向使命原则Humble J., Molesky J. & O'Reilly B. 2020)。这会影响到公司中的投资组合管理:

  • 预算编制:管理层不再像传统做法那样为下一个财年制定预算,而是跨多个维度设定高层次的目标,并定期审查。这种引导可以在多个层次上进行,并在需要时动态分配资源。

  • 项目管理:管理层在项目层面上明确下一阶段的可衡量目标,而不是制定详细的前期计划。然后,团队自行决定如何实现这些目标。

这可以与 OKRs 完美结合(参见第一章重要的度量指标)。

但任务的原则意味着你需要在各个层面上拥有关于产品管理和市场的知识。重要的是要理解,像每个功能一样(参见 第十章功能标志与功能生命周期),每个产品都有一个生命周期。新技术会被不同的人群所接受。首先是创新者,他们会尝试一切;然后是早期采用者或远见者,他们试图走在时代前沿。接下来是大多数人——大约 70%。这些人可以分为早期大多数(务实者)和后期大多数(保守者)。最后,你还会遇到滞后者或怀疑者,他们只有在没有其他选择时才会跟随趋势:

图 18.4 – 技术采用生命周期

图 18.4 – 技术采用生命周期

这里有一个有趣的概念——鸿沟——这是早期采用者与早期大多数之间的逻辑分界线。鸿沟的形成源于这样的观察:许多创新在不再被创新者视为竞争优势的来源时,遇到了困境,但还没有被早期大多数认为是安全的,因此很多产品正是在这个节点失败。

一旦早期大多数开始采用新技术,通常,其他产品和服务也会进入市场。市场总量仍在增长,但市场会发生变化,因为竞争者增多,质量和价格的预期也会改变:

图 18.5 – 市场成熟度

图 18.5 – 市场成熟度

了解一个产品处于生命周期的哪个阶段非常重要,因为每个阶段都需要不同的策略才能成功。

初创公司从探索开始。它们寻找与创始人愿景一致、能够创造客户价值并推动盈利增长的新商业模式。在这个探索阶段,当初创公司找到了问题/解决方案契合时,它会尽可能快速地通过使用 MVP 来评估是否也能实现产品/市场契合

一旦商业模式确定,策略就转向利用。初创公司通过扩大规模、提高效率以降低成本,来利用这个商业模式。

探索和利用是完全不同的策略,需要不同的能力、流程、风险管理和思维方式。初创公司通常擅长探索,但在利用方面表现较差——企业则擅长利用,但在探索方面较弱。

对于所有公司来说,找到在利用现有产品和探索新商业模式之间的平衡至关重要,因为从长远来看,只有能够同时管理这两者,才能生存。这也是为什么现在如此多的企业拥有创新孵化中心,以模仿初创公司,评估新的商业模式。

为了管理你的投资组合,你可以将产品绘制在增长矩阵上。矩阵有四个象限,分别代表增长和产品相对于其他投资的重要性:

图 18.6 – 投资组合管理的增长矩阵

图 18.6 – 投资组合管理的增长矩阵

产品的大小可以是收入或利润。你应该始终在新兴象限中拥有足够的产品,这些产品有潜力发展到增长成熟象限,因为有些产品会衰退,而没有获得足够的关注。左侧显示的是你应该探索的产品,右侧则是你应该开发的产品。

该矩阵与波士顿咨询集团BCG)的增长-市场份额矩阵非常相似。该框架由 Alan Zakon(后来的 BCG CEO)于 1970 年创建。增长-市场份额矩阵使用市场份额而不是财务重要性作为X轴:

图 18.7 – 增长-市场份额矩阵

图 18.7 – 增长-市场份额矩阵

该矩阵适用于你清晰了解市场的情况下,但它的运作方式是一样的。问号代表增长潜力大的产品,必须进行探索并发展成明星现金牛,然后进行开发。宠物是失败的实验或衰退中的现金牛。无论如何,你应该尽早关闭它们。

企业面临的挑战是创造出明星(或增长)象限的产品,而不通过收购。原因在于市场动态(图 18.5)以及企业如何管理其投资组合。你可以使用三视野模型来管理企业投资组合:

图 18.8 – 三视野模型

图 18.8 – 三视野模型

三个视野如下:

  • 视野 1:产生今天的现金流

  • 视野 2:今天的收入增长和明天的现金流

  • 视野 3:未来高增长业务的选项

视野 1 是你的成熟产品或现金牛。这些产品的投资会在同一年内带来回报。视野 2 是新兴产品,它们有潜力成为新的现金牛。它们需要大量投资,但不会像视野 1 中的投资那样带来相同水平的回报。视野 3 是未来的潜在明星,但也有很大的失败风险。

三个地平线完全不同,你需要不同的策略才能成功(Humble J., Molesky J. & O'Reilly B. 2020)。但不仅仅是需要不同的策略,通常新产品会扰乱市场,夺走现有业务的市场份额和收入。柯达于 1975 年发明了数码相机,但他们的业务建立在开发照片上,而不是捕捉回忆上,这项发明被管理层拒绝。柯达于 2012 年申请破产——那一年几乎每个人随时口袋里都有至少两部数码相机。成功的例子是亚马逊,电子书从他们传统的实物书籍销售业务中夺走了大量份额,或者是微软的云业务,这导致了本地产品许可证销售的下降。

随着新产品从现有市场夺走市场份额和收入,重要的是要引导企业朝着一个让所有人都共享长期成功目标的方向发展。如果没有这样做,人们就会开始联合起来反对新产品,以维护他们在企业中的主导地位。

为了平衡三个地平线,你应该有一个透明的资源分配过程,并应用不同的策略。通常在地平线 3 的投资比例为 10%——通常是按季度进行,根据验证学习来资助。表 18.1显示了三个地平线的不同策略和投资:

表 18.1 – 三个地平线的不同策略

表 18.1 – 三个地平线的不同策略

但是,是否应该将不同的地平线放在不同的业务单元中?我认为不应该。这样只会导致更多的竞争和孤岛效应。拥有良好的公司文化,具备成长心态和能够平衡短期与长期目标的目标,能够让你在各个层面上都保持创新,并拥抱创新。但良好的产品管理是必要的,它能帮助你清晰地了解每个产品和功能处于生命周期的哪个阶段,以便大家理解所应用的不同策略。

提升你的产品管理技能

产品管理是一项对想要实践精益产品开发的成功 DevOps 团队至关重要的技能。许多敏捷项目失败,是因为产品负责人无法推动愿景,并作出常常必要的艰难决策。产品管理基于三大支柱:

  • 了解你的客户

  • 了解你的业务

  • 了解你的产品

了解你的客户

要构建令客户满意的产品,必须对使用产品的人有深刻的共鸣。在软件开发中,我们自 90 年代以来使用人物角色(虚拟人物)来代表使用我们产品的用户群体(Goodwin, Kim (2009))。在设计功能时,考虑具体的角色帮助我们比单纯把客户看作一个拥有各种特征的大群体时,更能体会客户的需求和局限。

但今天,我们可以做得更多。我们可以收集关于客户如何使用我们产品的数据。我们能从这些数据中提取哪些人物角色(可用性群体)?最常见的使用场景是什么?哪些功能没有被使用?哪些使用场景在完成之前就被终止了?这些都是我们应该通过数据分析定期回答的问题。

了解你的业务

为了构建成功的产品,你的团队还必须了解业务。我们处在哪个市场,我们的市场份额有多大?我们的竞争对手是谁,他们的优势和劣势是什么?

理解业务通常是工程师们一个全新的领域。传统上,这是在不同层面上完成的,且只有少量的信息传递给工程师。像商业模型画布见下一节)这样的练习可以帮助你在团队中培养这些技能。

理解你的产品

理解产品通常是工程团队的强项。但理解产品不仅仅意味着你知道功能,还意味着你了解产品是如何操作的,如何进行负载均衡,性能如何,以及你积累了多少技术债务。

当然,你可以增加有经验的产品经理和用户体验设计师到团队中。但正如我们在上一章中讨论的,你应该保持团队精简。每个你创建和发布的功能、每一个你进行的实验、每一个你做出的决策,都需要这些技能。最好是提升团队技能,让他们拥有例如用户体验设计师等能够在需要时帮助团队的能力。

商业模型画布

为了加强工程师的产品管理技能,你可以进行一项练习,创建商业模型画布——这是一个用于创建或记录现有商业模型的模板。商业模型画布由亚历山大·奥斯特瓦尔德(Alexander Osterwalder)于 2005 年开发。你可以在这里下载该模板的免费副本:https://www.strategyzer.com/canvas/business-model-canvas。

画布是为了在大幅纸张上打印出来,团队可以一起头脑风暴,绘制草图或在上面添加便签。它包含了商业模型的九个关键组成部分:

  • 价值主张:我们要解决什么问题?我们满足了哪些需求?

  • 客户群体:我们为谁创造价值?我们最重要的客户是谁?

  • 客户关系:我们的每个客户期望我们与他们建立什么样的关系?

  • 渠道:我们的客户希望通过哪些渠道与我们接触?

  • 关键伙伴:我们需要与谁建立伙伴关系?我们的关键供应商是谁?

  • 关键活动:为了实现我们的价值主张,哪些活动是必需的?

  • 关键资源:我们的价值主张需要哪些资源——如人员、技术和流程?

  • 成本结构:商业模型中最重要的成本驱动因素是什么?是固定成本还是可变成本?

  • 收入流:客户愿意为什么价值支付?支付多少,支付频率如何?

画布包含了一些帮助你的团队创建商业模型的提示,正如你在以下截图中看到的:

图 18.9 – 商业模型画布

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_18_09.jpg)

图 18.9 – 商业模型画布

通过填写画布的所有区域,你在考虑任何潜在的创意时,都已经从整个商业模型的角度进行了思考,并且你被鼓励以整体的方式思考所有元素如何协调在一起。

总结

在本章中,你已经学习了精益产品管理的重要性,以及如何将客户反馈融入到你的工作流程中。你了解了什么是 MVP,以及如何通过假设驱动开发来构建正确的东西。

在下一章中,我们将更深入地了解如何进行 A/B 测试来进行实验。

进一步阅读

以下是本章的参考文献,你可以进一步参考以获得更多相关话题的信息:

  • Forsgren N., Humble, J., & Kim, G. (2018). 加速:精益软件与 DevOps 的科学:构建和扩展高绩效技术组织 (第 1 版) [电子书]。IT Revolution Press

  • Ward, Allen (2007). 精益产品与过程开发。精益企业研究所,美国

  • Ries, Eric (2011). 精益创业:今天的企业家如何利用持续创新创造极其成功的企业 [Kindle 版]。Currency

  • Humble J., Molesky J. & O'Reilly B. (2015). 精益企业:高绩效组织如何在规模化中创新 [Kindle 版]。O'Reilly Media

  • Osterwalder, Alexander (2004). 商业模型本体论:设计科学方法中的一个提案http://www.hec.unil.ch/aosterwa/PhD/Osterwalder_PhD_BM_Ontology.pdf。

  • Goodwin, Kim (2009). 为数字时代设计——如何创建以人为本的产品和服务。Wiley

第十九章:实验与 A|B 测试

在本章中,我们将讨论如何通过进行实验来验证假设,通过基于证据的 DevOps 实践(如A|B 测试)来发展和持续改进产品。这有时被称为假设驱动开发或仅仅是实验

本章将涉及以下主题:

  • 使用科学方法进行实验

  • 使用 GrowthBook 和 Flagger 进行有效的 A|B 测试

  • 实验与 OKR

使用科学方法进行实验

传统上,需求管理更多的是猜测而非科学。最接近科学方法的是访谈或一般的市场调研。这种方法的问题在于,你不能询问人们他们还不知道的东西。你可以问他们想要什么,但不能问他们需要什么,因为他们可能还不知道,特别是在一个被颠覆的市场领域。

假设驱动的开发理念是将科学方法应用于产品管理,这是一种基于实证的获取知识的方式。

科学方法是通过实验来探索观察结果并回答旨在发现因果关系的问题的过程。它遵循特定的步骤(见图 19.1):

图 19.1 – 科学方法

图 19.1 – 科学方法

我们将详细介绍各个步骤:

  1. 观察:通过五种感官观察现实:嗅觉、视觉、听觉、触觉和味觉。

  2. 问题:基于观察、现有研究或以往实验提出问题。

  3. 假设:根据在提出问题时获得的知识陈述假设。假设是基于观察和研究预测你认为将会发生的事情。假设通常以如果……那么……的形式写出,例如:“如果我们修改这个变量,那么我们预期能够观察到这个变化。”

  4. 实验:实验验证或推翻假设。在实验中,有不同的变量。自变量是你用来引发结果的变量。因变量是你衡量并期望变化的变量。在实验中,你通过观察收集定性数据,通过测量和收集指标得到定量数据

实验还使用对照组来证明差异不仅仅是偶然的。为了测试药物的治疗效果,必须设计一个实验,其中一部分人群——对照组——不接受治疗,而是给他们安慰剂,而实验组则接受潜在药物的治疗(见图 19.2)。

图 19.2 – 进行科学实验

图 19.2 – 进行科学实验

为了进行一个好的实验,你应该一次只改变一个变量,同时保持其他所有变量不变。你还应该避免偏见。无论你多么努力,偏见很容易悄悄进入你的观察和结论中。

  1. 结论:实验结束后,你需要分析结果并将实际结果与预期结果进行比较。你从实验中学到了什么?你能验证或反驳你的假设吗?是否有新的假设或新的问题需要提出?还是需要更多实验来确认?

  2. 结果:最后一步是分享你的结果。即使你的假设被反驳,它仍然是宝贵的学习。

科学方法是一种迭代的、经验性的方式,但这些步骤不一定按照这个顺序进行。在任何时候,你都可以修改你的问题并改变假设——观察始终在进行中。这个过程的图示看起来不像一个清晰的循环,更像是图 19.3

图 19.3 – 该过程中的步骤没有严格的顺序

图 19.3 – 该过程中的步骤没有严格的顺序

科学方法在我们的行业中非常重要——不仅仅是为了构建正确的东西。你也应该在寻找漏洞或生产问题时使用这种方法:基于观察到的事实提出假设。通过一次只改变一个因素(通常是配置值)来进行实验。进行交叉检查,以确保没有其他系统或变量干扰你的实验。得出结论并记录结果,然后再开始下一个假设。

让我们来看一下如何使用科学方法来不断进化和改进你的软件。

观察——收集和分析数据

你可以通过观察人们使用你的应用来进行观察。我们在第十二章,“通过左移测试提高质量”中,讨论了可用性测试技巧,如走廊测试游击式可用性测试。然而,通常用户分布在世界各地,观察他们产生的数据比采访他们更为方便。

数据是假设驱动开发中最重要的成分!你进行的实验越多,随着时间的推移,你收集的数据就会越多。

在观察数据时,你不应只关注手头的数据点。问问自己数据没有告诉你什么。如果你的目标是每个月增加活跃用户的数量,你不应只将观察集中在当前用户的数据上。检查失败的登录尝试数据。多少用户想要使用你的应用程序,但被锁定无法恢复密码或第二次认证因素?多少人在需要验证邮件或手机号码后没有回来?有多少人取消了注册过程,他们在取消之前等待了多久?

为了回答这些问题,你不能仅仅查看使用数据。你必须结合所有可用的数据来源(参见图 19.4):

图 19.4 – 用于收集数据的日志来源

图 19.4 – 用于收集数据的日志来源

这些定量数据可以与定性数据结合,如客户调查、来自客户服务中心的数据或任何种类的分析数据。图 19.5 显示了你可以用来获取见解并制定问题的不同数据来源:

图 19.5 – 用于观察的数据来源

图 19.5 – 用于观察的数据来源

牢记这些问题后,你可以开始制定假设。

制定假设

假设是你基于观察和研究所预测的结果。假设可以用简单的如果……那么……的形式表达:如果 <我们修改这个变量>, 那么 <我们预期会观察到这个变化>

如果 我们通过删除如电话号码和邮寄地址等字段来缩短注册表单,那么 放弃注册过程的用户数量(放弃率)将会减少。

由于你将有许多假设在待办事项中,通常会有一个固定的格式,类似于用户故事,其中包括客户细分和功能名称。这使得你的假设在待办事项中更加可发现:

我们相信 {客户细分}

想要 {功能}

因为 {价值主张}

这种形式也迫使你将三个方面纳入你的假设:

  • :我们是为了谁改变应用程序?

  • 什么:我们正在改变什么?

  • 如何:这一变化将如何影响用户?

这些要素构成了一个好的假设:

我们相信 新用户

想要 一个更短的注册表单,减少输入字段

因为 这让他们能够在公开个人数据之前测试应用程序并获得信心。

请注意,专注于价值主张会导致对如何的描述更加抽象,重点放在为什么。在市场营销中,你常常在假设中找到这样的细节:

  • 影响是什么?

  • 变化的幅度是多少?

  • 在多长时间后?

这导致假设与实验之间建立一对一的关系。特别是在开始实验时,我认为将实验与潜在假设分开是有帮助的。在你最终能确定假设是正确还是错误之前,可能需要多个实验。

构建实验

在定义实验时,你应该尽量保持尽可能多的变量固定。最好的方法是查看你的基线数据。周末和假期将如何影响你的数据?政治和宏观经济趋势将如何影响你的实验?

同样,请确保您的控制组和实验组足够大。如果您只在一个小组进行实验,则您的结果可能不具有代表性。如果您的对照组太小,则可能没有足够的数据可以进行比较,特别是如果存在您没有预见到的其他外部因素。一个好的实验应包含以下信息:

  • 有什么改变?

  • 预期影响是什么?

  • 谁是受众或客户群体?

  • 我们期望有多大的变化?

  • 实验运行多长时间?

  • 我们将数据与基线进行比较(控制组或历史数据)。

这里有一个例子。

新的、更短的注册表单什么改变)将使我们的新用户为谁)的注册表单弃用率影响)在14 天多久后)后比我们的对照组(基线)降低超过15%多少)。

定义实验后,您可以开始实施和运行它。如果您使用功能标志开发(见第十章功能标志和功能生命周期),这就像编写新功能一样简单。唯一的区别是,您不是为所有用户打开该功能,而是为您的实验组打开该功能。

验证结果

实验结束后,分析结果并将实际结果与预期结果进行比较。您从实验中学到了什么?您能验证或证伪您的假设吗,还是需要更多实验来确保?是否有新的假设或新的问题需要提出?

结果的回顾性研究是一个重要部分。不要跳过它,也不要仅仅假设假设成立或失败,因为您的指标超过了一个阈值。分析数据并检查意外的影响、离群值和统计异常。

从您的假设和实验中学习应该会导致新的想法,并完成构建-测量-学习循环(参见图 19.6):

图 19.6 – 假设驱动的构建-测量-学习循环

图 19.6 – 假设驱动的构建-测量-学习循环

有许多可用工具可以帮助您进行有效的 A|B 测试和实验。

使用 GrowthBook 和 Flagger 进行有效的 A|B 测试

GitHub 没有提供帮助进行 A|B 测试的工具,但市场上有许多相关工具。问题在于这些工具的范围差异很大。一些更像是网站体验工具,你可以用它们通过内容管理系统CMS)来构建你的网站,或者使用可视化编辑器来构建并测试 A|B 测试变体(例如,Optimizely – 参见 https://www.optimizely.com/)。有些则更加关注市场营销、登录页和活动管理,例如HubSpothttps://www.hubspot.com/)。这些工具很棒,但可能不是工程团队的最佳选择。

更好的解决方案来自于功能标志工具,如LaunchDarklyVWOUnleash。我在第十章《功能标志与功能生命周期》中已经介绍了这些工具,因此这里不再重复。如果你正在使用这些功能标志解决方案,这就是你寻找 A|B 测试解决方案的第一站。

在本章中,我将重点介绍GrowthBookFlagger,这两个开源项目都非常注重实验,但采用了完全不同的方法。

GrowthBook

GrowthBook (github.com/growthbook/growthbook)是一个具有免费和开放核心的解决方案。它也有 SaaS 和企业计划,并为ReactJavaScriptPHPRubyPythonGoKotlin提供 SDK。

GrowthBook 的解决方案设计是完全容器化的。如果你想尝试它,只需要克隆该仓库并运行以下命令:

docker-compose up -d

一旦启动,你可以通过localhost:3000访问 GrowthBook。

在 GitHub Codespaces 中运行 GrowthBook

如果你想尝试使用 GrowthBook,可以在 GitHub Codespaces 中运行它。为了使其正常工作,你需要配置docker-compose.yml以使用正确的 DNS 名称,因为 GrowthBook 使用 localhost 连接到其 MongoDB。将APP_ORIGINenvironment下设置为本地3000端口的地址,将API_HOST设置为本地3001端口的地址,并确保3001端口是可访问的。

连接成功后,你可以使用它来服务功能标志或构建实验。要构建实验,你需要将数据源连接到 GrowthBook——例如,BigQuerySnowflakeRedshiftGoogle Analytics等多种选择。系统提供了预定义的数据架构,也可以让你自定义。然后,你可以根据数据源创建度量指标。度量指标可以是以下任何一种:

  • 账户已创建

  • 页面访问

  • 网站停留时间

  • 每用户收入

进行实验时,通常会使用功能标志。你也可以直接使用其中一个 SDK 进行内联实验。以下是一个用 JavaScript 进行实验的示例:

const { value } = growthbook.run({
  key: “my-experiment”,
  variations: [“red”, “blue”, “green”],
});

实验根据你定义的度量标准运行,结果与图 19.7中的示意图类似:

图 19.7 – GrowthBook 中实验结果

图 19.7 – GrowthBook 中实验结果

你可以向实验中添加或删除度量指标,也可以将其导出为 Jupyter 笔记本。

GrowthBook 还配有 Google Chrome 扩展GrowthBook DevTools,适用于 JavaScript 和 React SDK,允许你直接在浏览器中与功能标志交互。目前,视觉编辑器处于 Beta 阶段。

GrowthBook 非常直接,也基于功能标志,类似于在第十章中介绍的解决方案。

Flagger

另一种完全不同的方法是使用Flaggerflagger.app/)。它是一个Kubernetes的交付操作员,可以与服务网格****Istio一起使用。Flagger 更常用于金丝雀发布到 Kubernetes 集群,但它也可以根据 HTTP 匹配条件路由流量。

你可以为所有拥有insider cookie 的用户创建一个持续 20 分钟的实验,如下所示:

analysis:
  # schedule interval (default 60s)
  interval: 1m
  # total number of iterations
  iterations: 20
  # max number of failed metric checks before rollback
  threshold: 2
  # canary match condition
  match:
    - headers:
        cookie:
          regex: “^(.*?;)?(type=insider)(;.*)?$”

你可以将 Flagger 与PrometheusDatadogDynatrace等多个度量工具结合使用。我在这里不会进一步详细说明,更多信息请参阅 Flagger 文档(docs.flagger.app/)。Stefan Prodan 还提供了一个很好的教程:使用 Flux v2、Flagger 和 Istio 进行渐进式交付的 GitOps 实践(请参阅github.com/stefanprodan/gitops-istio)。

使用 Flagger 和 Istio 的解决方案提供了很大的灵活性,但也非常复杂,并不适合初学者。如果你已经在 Kubernetes 和 Istio 上,并执行金丝雀发布,那么 Flagger 可能是一个强大的框架。

正如你所看到的,市面上有许多解决方案可以帮助你进行实验和 A/B 测试。从专注于 CMS 和活动的工具到 Kubernetes 操作员,有各种完全不同方法的解决方案。适合你的最佳解决方案取决于许多因素——主要是你现有的工具链、定价和支持。我认为,专注于流程和数据分析更为重要。提供两个版本的应用程序不应该是挑战——理解你的数据可能才是。

实验与 OKR

第一章重要的度量标准中,我向你介绍了目标和关键结果OKRs)作为一种框架,用于透明地定义和跟踪目标及其结果。OKRs 帮助组织在战略目标上实现高度的一致性,同时保持个别团队的最大自主性。

工程团队是一项昂贵的资源,很多利益相关者总是向他们提出要求:测试人员提交缺陷、客户请求新功能、管理层希望赶超竞争对手并向重要客户做出承诺。团队如何才能找到进行实验的自由?又该从哪些实验开始呢?

OKR 可以使你能够与更高层次的目标保持紧密对齐,同时保留自主决定 做什么如何做 的权力。

假设你的公司希望成为市场领导者,市场份额达到 75%,为了实现这一目标,需要不断增长的注册用户数量。你团队的关键结果是每月 20%的增长率。这样就能为你的团队设定优先级。当然,还有其他事情要做,但优先级将是 OKR。团队可能首先会调查有多少人访问注册页面,并且他们的来源是什么。多少人点击了立即注册按钮?有多少人完成了对话框?他们在哪个环节没有再回来?到那个点时,他们就开始自动构思假设,并可以进行实验来验证它们。

OKR 也有利于跨团队合作,因为团队之间很可能有高协同效应的 OKR,因为它们与更高层次的目标保持一致。在这个例子中,团队很可能希望与市场营销部门进行沟通,因为他们可能有类似的 OKR。他们可能会有自己关于实验的想法,帮助提高通向注册页面的着陆页的参与率。

OKR(目标与关键结果)是一个很好的工具,通过确保与其他团队和更高层目标的对齐,为团队提供实验的自由。

总结

实验、A|B 测试和假设驱动的开发是困难的话题,因为它们在多个领域需要高度成熟:

  • 管理:你的团队需要有自主决定 做什么如何做 的权力。

  • 文化:你需要有一种信任文化,在这里人们不怕失败。

  • 跨团队合作:你的团队必须能够跨学科工作,因为实验通常需要不同部门之间的合作。

  • 技术能力:你必须能够在很短的时间内将变更发布到生产环境,并针对不同的客户群体。

  • 洞察力:你必须具备强大的分析能力,并将来自不同来源的数据和指标结合起来。

如果你还没有做到这一点,别担心。我与许多团队合作时,他们也没有做到这一点。只要继续提高你的能力,并检查你的指标是否显示出结果。DevOps 是一个旅程,而不是目标,你必须一步一步走。

在本章中,你已经了解了实验、A|B 测试和假设驱动的开发的基本知识,我也介绍了一些可以帮助你构建解决方案的工具。

在下一章,您将学习 GitHub 的基础知识——托管选项、定价以及如何将其集成到您的现有工具链和企业中。

进一步阅读

这些是本章中的参考资料和链接,您也可以使用它们来获取更多关于这些主题的信息:

第六部分:GitHub 为您的企业服务

第六部分中,您将了解 GitHub 的不同托管和定价选项,如何从其他平台迁移到 GitHub,以及在 GitHub 企业版中组织团队和产品的最佳实践。

本书的这一部分包含以下章节:

  • 第二十章GitHub —— 所有开发者的家园

  • 第二十一章从不同平台迁移到 GitHub

  • 第二十二章组织您的团队

  • 第二十三章转型您的企业

第二十章:GitHub – 所有开发者的家园

在本章中,我将解释 GitHub 平台的一些基础知识。你将了解不同的托管选项、定价以及如何将其集成到现有的工具链中。

关键主题如下:

  • 托管选项和定价

  • GitHub Connect

  • 实操 – 在 GitHub.com 上创建你的账户

  • 企业安全

  • GitHub 学习实验室

托管选项和定价

GitHub 提供多种不同的许可证和托管选项。理解这些选项非常重要,这样才能为你的企业做出正确的选择。

托管选项

GitHubgithub.com)托管在美国的数据中心。你可以免费注册 GitHub 并获得无限的私有和公共仓库。GitHub 中的许多功能对于开源项目是免费的,但对于私有仓库则不免费。

对于企业,你可以选择不同的 GitHub 托管选项(见 图 20.1):

图 20.1 – GitHub 企业版的托管选项

图 20.1 – GitHub 企业版的托管选项

GitHub 企业版云

GitHub 企业版云GHEC)是 GitHub 提供的 SaaS 服务,完全由 GitHub 在其位于美国的云基础设施中托管。你可以为用户应用额外的安全性并支持单点登录。GHEC 允许你托管私有和公共仓库,使你能够在企业背景下托管开源项目。

GHEC 保证你享有 SLA99.9% 的每月正常运行时间,这意味着每月最多停机 45 分钟。

GitHub 企业版服务器

GitHub 企业版服务器GHES)是一种可以自行托管的系统。你可以在自己的数据中心或云环境中(如 Azure 或 AWS)进行托管。你可以使用 GitHub Connect 连接到GitHub.com,这样可以共享许可证并在服务器上使用开源资源。

GHES 基于与 GHEC 相同的代码,因此最终,所有功能将在几个月后同步到服务器。但云端提供的一些功能,诸如 GitHub Actions 中的运行器,你必须在 GHES 上自己处理。在云端,你可以使用 GitHub 托管的运行器;而在 GHES 上,你必须使用自托管的运行器来构建自己的解决方案。

还有一些托管服务可以为你托管 GHES,例如,在你所在地区的 Azure 数据中心。这样,你可以完全控制数据驻留,并且无需自己管理服务器。有些还包括托管 GitHub Actions 运行器的服务。

GitHub 企业版 AE

GitHub 正在构建一个名为GitHub 企业版 AEGHAE)的服务。目前该服务正处于仅限 500 座以上客户的私有 beta 测试阶段,尚未确定公开发布的日期。

GHAE 是 GitHub 提供的完全独立的、托管的服务,位于你选择的 Microsoft Azure 区域。这为你提供完整的数据驻留和合规性。

对于需要数据驻留和合规性的客户,这是未来的一个不错选择,但目前尚不清楚它何时可用、定价是多少,以及最低座位数是多少。

GitHub Connect

GitHub 的强大之处在于其社区以及社区所提供的价值。为了能够在服务器上利用这一点,你可以通过GitHub Connect将你的服务器连接到 GitHub。你可以逐一激活每个功能,如图 20.2所示:

图 20.2 – 在 GHES 上配置 GitHub Connect

图 20.2 – 在 GHES 上配置 GitHub Connect

以下是功能列表:

  • 许可证同步:在多个服务器或组织之间管理企业内的许可证使用情况。这有助于确保每个用户只使用一个许可证,无论他们在哪个地方登录。

  • 统一搜索:一个选择是允许在服务器上进行搜索,并从GitHub.com获取公共仓库的结果。此外,你还可以允许在服务器上搜索,并找到属于你企业的私有仓库(当然,前提是用户有权限访问这些仓库)。

  • GitHub.com actions:要在工作流中加载公共 actions,必须启用此选项。如果不启用,你必须将所有 actions 分叉到你的服务器,并从那里引用它们。你仍然可以在组织级别配置允许哪些 actions。

  • 统一贡献:没有此选项时,用户在服务器上的贡献将不会显示在其公共个人资料中。此选项不会暴露敏感数据。仅会将贡献数量—如提交、问题、讨论或拉取请求—发送到 GitHub.com。

定价

GitHub 的费用按月按用户计费,基于三种不同的定价层级:免费团队企业(见图 20.3):

图 20.3 – GitHub 定价层级概述

图 20.3 – GitHub 定价层级概述

公共仓库—因此也就是开源—是免费的,并且提供很多免费功能,如 Actions、Packages 以及许多安全功能。私有仓库是免费的,但功能有限,并且提供 2000 分钟的 Action 时间和 500 MB 的存储空间。关于 Actions 的定价,已经在第七章《运行你的工作流》中详细介绍。

如果你真的想在私有仓库中与 GitHub 合作,你至少需要 团队 许可证。它包括 保护分支代码拥有者 和其他高级拉取请求功能。你还可以访问 Codespaces,但你必须单独为其付费(有关 Codespaces 的定价,请参见 第十三章向左移动安全与 DevSecOps)。团队级别包含 3000 个 Action 分钟和 2 GB 的包存储空间。

免费和团队版本仅在 GitHub.com 上可用。如果你需要 GHEC、GHES 或 GHAE,你必须购买 GitHub 企业 许可证。此许可证包含所有企业功能,如单点登录、用户管理、审计和政策,并提供 50,000 个 Action 分钟和 50 GB 的包存储空间。它还允许你购买其他附加功能,如 高级安全高级支持

许可证是按 10 个为一组购买的,可以按月或按年支付。如果你想使用 GitHub 高级安全性或高级支持,你必须与 GitHub 销售团队或 GitHub 合作伙伴联系。他们可以为你提供报价。

除了许可证层级之外,还有一些按使用计费的内容,例如:

  • 操作

  • Codespaces

  • 市场按需付费应用

你可以在组织或企业级别配置支出限制。

实操 – 在 GitHub.com 上创建你的账户

到目前为止,我假设你已经有一个 GitHub 账户。GitHub 拥有超过 7000 万用户,你很有可能已经拥有。如果你有一个账户,只需跳过此部分,继续进行 企业安全

注册 GitHub 是非常简单的。它的设计像一个控制台向导。创建新帐户的步骤如下:

  1. 访问 github.com 并点击 注册

  2. 输入你的电子邮件地址并点击 继续 或按 Enter,如 图 20.4 所示:

图 20.4 – 输入你的电子邮件地址

图 20.4 – 输入你的电子邮件地址

  1. 输入一个强密码并点击 继续

  2. 输入一个用户名。用户名必须是唯一的。GitHub 会告诉你该名称是否可用,如 图 20.5 所示:

图 20.5 – 创建密码并选择一个独特的用户名

图 20.5 – 创建密码并选择一个独特的用户名

如果你找到了一个独特的用户名,点击 继续

  1. 现在,你可以选择是否接收电子邮件通信。输入 y 代表 n 代表 ,然后点击 继续 或按 Enter

  2. 通过点击图像中的指定部分来解决验证码。请注意,验证码可能会以浏览器的首选语言显示(见 图 20.6):

图 20.6 – 邮件通信与验证码

图 20.6 – 邮件通信与验证码

  1. 现在,检查你的电子邮件账户。你应该已经收到了一个验证码,可以将其粘贴到以下字段中(见 图 20.7):

图 20.7 – 输入发送到你电子邮件地址的验证码

图 20.7 – 输入发送到你电子邮件地址的验证码

  1. 接下来的对话框是用来个性化你的体验的。如果你不需要,可以跳过它们。

  2. 你可以选择免费试用 GitHub Enterprise 30 天。这段时间应该足够让你尝试所有功能。

成功创建账户后,你应该立刻完成一些步骤:

  1. 前往 github.com/settings/security 并勾选 启用双因素认证 来保护你的账户。

  2. github.com/settings/profile 填写你的个人资料并选择一个好的头像。

  3. github.com/settings/appearance 选择你喜欢的主题。你可以选择单一的浅色或深色主题,或者选择将主题与系统同步。

  4. 前往 github.com/settings/emails 选择如何处理你的电子邮件地址。你可以选择将你的电子邮件地址设为私密。GitHub 会在进行基于 Web 的 Git 操作时使用一个特殊的电子邮件地址,该地址格式为:<user-id>+<user-name>@users.noreply.github.com。如果你想阻止命令行推送时暴露真实邮箱地址,你需要在本地配置此地址:

    $ git config --global user.email <email address>
    

你的 GitHub 账户现在已经准备好,你可以开始创建代码库或贡献开源项目了。

企业安全

作为一个企业,你可以使用 SAML 单点登录SSO)和你的 身份提供者IdP)一起保护你的 GitHub Enterprise 资源。SSO 可以在 GHEC 的企业级和组织级进行配置,在 GHES 中只能为整个服务器配置。

SAML SSO 可以与所有支持 SAML 的 IdP 配置,但并非所有 IdP 都支持 跨域身份管理系统SCIM)。以下 IdP 是兼容的:Azure ADAAD)、Okta 和 OneLogin。

SAML 认证

在 GitHub 中配置 SAML SSO 非常简单。你可以在企业或组织的设置中找到相应的设置,路径为 /settings/security) | SAML 单点登录。在这里,你可以找到配置 IdP 所需的消费者 URL(见 图 20.8):

图 20.8 – 在 GitHub 中配置 SAML SSO

图 20.8 – 在 GitHub 中配置 SAML SSO

字段的值必须在您的 IdP 中进行配置。请查看其文档以获取更多信息。例如,在 AAD 中,您可以在此处找到详细的说明:docs.microsoft.com/en-us/azure/active-directory/saas-apps/github-tutorial。您必须在 AAD 中创建一个新的企业应用。您可以搜索 GitHub 的模板并选择相应的模板(针对企业、组织或服务器)。有关当前可用模板,请参见图 20.9

图 20.9 – AAD 中 GitHub 的企业应用模板

图 20.9 – AAD 中 GitHub 的企业应用模板

为您希望访问 GitHub 的应用分配用户或组。重要的配置在设置单点登录中进行(参见图 20.10):

图 20.10 – 配置您的企业应用

图 20.10 – 配置您的企业应用

使用组织或企业 URL 作为标识符。您可以使用在图 20.8中看到的 URL 的第一部分,只是不要包括 /saml/consume。将此 URL 作为 /saml/consume 用于 /sso 作为登录 URL。结果应类似于图 20.11

图 20.11 – AAD 企业应用中的基本 SAML 配置

图 20.11 – AAD 企业应用中的基本 SAML 配置

属性和声明可以用于调整 AAD 中字段的映射。如果您的 AAD 没有进行自定义,默认设置应该有效(参见图 20.12):

图 20.12 – 配置 SAML 令牌的属性和声明

图 20.12 – 配置 SAML 令牌的属性和声明

下载用于签署 SAML 令牌的Base64证书(参见图 20.13):

图 20.13 – 下载 SAML 签名证书

图 20.13 – 下载 SAML 签名证书

复制登录 URLAzure AD 标识符URL(参见图 20.14):

图 20.14 – 获取登录 URL 和 Azure AD 标识符

图 20.14 – 获取登录 URL 和 Azure AD 标识符

现在,您可以返回 GitHub 并填写数据。然后,将登录 URL信息粘贴到登录 URL字段,将Azure AD 标识符URL 粘贴到发行者字段。用文本编辑器打开证书,并将内容粘贴到公钥证书字段。结果类似于图 20.15

图 20.15 – 在 GitHub 中配置 SAML SSO

图 20.15 – 在 GitHub 中配置 SAML SSO

点击测试 SAML 配置并使用您的 AAD 凭据登录。如果一切成功,您可以勾选需要 SAML 身份验证来强制使用 SAML 进行访问。GitHub 会检查哪些用户没有通过 IdP 获得访问权限,并在您确认后将其移除。

请注意,只有授权的 PAT 令牌和 SSH 密钥才能访问受 SSO 保护的内容。每个用户必须前往其 PAT 令牌/SSH 密钥并按照图 20.16中的示例进行授权:

图 20.16 – 为 SSO 保护的组织授权 PAT 令牌

图 20.16 – 为 SSO 保护的组织授权 PAT 令牌

当然,每个 IdP 的配置不同,具体值也会略有差异,取决于您是在配置企业、组织还是服务器。但有了您的 IdP 文档,设置过程应该非常直接。

SCIM

如果您启用 SAML SSO,用户在您在 IdP 中停用时将不会自动取消配置。您可以在 GHEC 中实施SCIM,以根据您的 IdP 信息自动添加、管理和移除访问权限。

SCIM 是一个 API 端点(见docs.github.com/en/enterprise-cloud@latest/rest/reference/scim),由您的 IdP 用于管理 GitHub 中的用户。兼容的 IdP 例如 Azure ADOktaOneLogin。要配置 SCIM,您必须遵循您的 IdP 的文档,如果它们兼容的话。以下是 AAD 的教程:docs.microsoft.com/en-us/azure/active-directory/saas-apps/github-provisioning-tutorial

禁用第三方访问限制

请注意,在授权您的 IdP 之前,您必须在组织设置中禁用第三方访问限制。您可以在设置 | 第三方访问 | 禁用访问限制下进行此操作。

自动团队同步

如果您在 GHEC 上使用 SAML SSO,您可以设置团队同步以自动与您的 IdP 同步团队成员。目前,团队同步仅支持AADOkta

您可以在组织的设置中启用团队同步,路径为/settings/security。在此处,您可以看到有多少个团队已同步,并跳转到过滤后的审计日志查看所有相关事件(见图 20.17):

图 20.17 – 为组织启用团队同步

图 20.17 – 为组织启用团队同步

启用后,您可以创建新团队,并从您的 IdP 中选择一个或多个组与您的团队同步,如图 20.18所示:

图 20.18 – 创建具有自动同步功能的团队

图 20.18 – 创建具有自动同步功能的团队

您可以将这些团队添加到其他团队中(父团队),但无法将嵌套的组同步到 GitHub。

企业管理用户

在 GHEC 中,即便为企业或组织设置了 SAML SSO,每个用户仍然需要在 GitHub.com 上有一个用户账户。GitHub 用户账户基本上是用户的身份,而 SAML 授权则是授予某些企业资源的访问权限。用户可以利用他们的身份为开源项目和其他组织做贡献,并且必须通过 SSO 认证才能访问企业资源。但许多组织并不希望如此。他们希望完全控制用户的身份。解决方案就是企业托管用户EMU)。通过 EMU,用户的身份完全由 IdP 管理。当用户首次使用 IdP 的身份登录时,系统会创建一个新用户。该用户无法为开源项目做贡献,也不能被添加为其他仓库的外部协作者。此外,贡献只会计入该用户的资料中。

EMU 为你的企业提供了对身份的强大控制,但也带来了许多限制,以下是一些例子:

  • 用户不能在企业外部协作加星关注分叉仓库。他们不能创建问题或拉取请求,推送代码、评论或对这些仓库添加反应。

  • 用户仅对同一企业的其他成员可见,且不能关注企业外部的其他用户。

  • 他们无法在用户账户上安装GitHub Apps

  • 用户只能创建私有内部仓库。

这些限制使得很多事情变得困难。GitHub 的一个主要优点是它与开源仓库的集成。但如果 EMU 允许你使用云服务而不是服务器实例,或许值得尝试一下。

当前,EMU 支持的 IdP 有AADOkta

如果你想尝试 EMU,你必须联系 GitHub 的销售团队,他们会为你创建一个新的企业。

想了解更多关于 EMU 的信息,请参阅docs.github.com/en/enterprise-cloud@latest/admin/identity-and-access-management/managing-iam-with-enterprise-managed-users/about-enterprise-managed-users

使用 GHES 进行身份验证

在服务器上,事情的运作方式有所不同。你可以为SAMLLDAPCAS配置 SSO。配置过程简单,与 GHEC 差别不大。用户无需GitHub.com账户;他们可以通过 IdP 直接登录到服务器,类似于 EMU。但如果配置了 GitHub Connect,用户可以在用户设置 | GitHub Connect中连接他们的 GitHub 账户,并将贡献数量共享到他们的公共 GitHub 资料中。这样,如果他们愿意,便可以将多个企业身份连接到他们的 GitHub 资料中。

审计 API

GHEC 以及 GHES 都支持审计日志。日志包含所有与安全相关的事件的日志条目。每个审计日志条目都显示有关事件的适用信息,如下所示:

  • 执行操作的企业或组织

  • 执行操作的用户(actor

  • 受到该操作影响的用户

  • 执行操作的仓库

  • 执行的操作

  • 操作发生的国家

  • 操作发生的日期和时间

图 20.19显示了 GHEC 企业级别的示例审计日志。你可以搜索并过滤审计日志。你可以选择预定义的过滤器,并点击日志条目的标题元素来创建过滤语句:

图 20.19 – GHEC 实例的审计日志

图 20.19 – GHEC 实例的审计日志

在 GHEC 中,你可以启用日志流并配置将所有事件自动流式传输到以下目标之一:

  • Amazon S3

  • Azure Blob 存储

  • Azure 事件中心

  • Google Cloud Storage

  • Splunk

你可以通过 Azure 事件中心将事件转发到其他工具,如 Log Analytics 或 Sentinel。

你还可以通过审计日志 API 访问审计日志。你可以使用 GraphQL 或 REST API 查询审计日志。以下示例展示了如何使用 REST API 检索特定日期的所有事件:

$ curl -H "Authorization: token TOKEN" \
--request GET \
"https://api.github.com/enterprises/name/audit-log?phrase=created:2022-01-01&page=1&per_page=100"

要了解如何使用 API 查询审计日志,请参见docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/using-the-audit-log-api-for-your-enterprise

GitHub 学习实验室

GitHub 的一个大优势是大多数开发者已经知道它是如何工作的。这意味着培训和入职所需的时间较少。但是,当然,仍然有一些开发者是 GitHub 的新手。GitHub 提供了免费的 GitHub 学习实验室(https://lab.github.com/)。它包含许多学习路径,利用 GitHub 问题和机器人为你提供动手实践经验,帮助你学习 GitHub。

如果你更喜欢这种学习方式,Microsoft Learn 上也有许多免费的学习路径可用。只需访问 Microsoft Learn 并按产品筛选,选择 GitHub:docs.microsoft.com/en-us/learn/browse/?products=github

总结

在本章中,你了解了 GitHub 的不同定价和托管选项。你学习了企业安全性以及如何将 GitHub 集成到你的企业中。

在下一章,我将向你展示如何将现有的源代码管理系统或 DevOps 解决方案迁移到 GitHub。

进一步阅读

使用以下链接获取更多关于这些主题的信息:

第二十一章:迁移到 GitHub

如果你不是初创公司,那么在迁移到新平台时,必须考虑现有的工具和流程。本章将讨论从不同平台迁移到 GitHub 的不同策略。

本章将涵盖以下内容:

  • 选择正确的迁移策略

  • 实现低保真迁移的合规性

  • 同步要求以确保顺利过渡

  • 使用 GitHub 企业导入工具从 Azure DevOps 迁移

  • 使用 Valet 迁移管道

选择正确的迁移策略

当迁移到新平台时,你有不同的选择:

  • 高保真迁移:你尽可能多地将内容迁移到新平台。

  • 彻底切换迁移:你只迁移开始使用新平台所需的最少内容。

高保真迁移到复杂平台面临不同的问题。主要问题是并非所有实体都有一对一的映射,并且不同平台的工作方式不同。通过迁移所有内容,你会影响人们使用新系统的方式。数据是根据旧系统和旧流程进行优化的。此外,高保真迁移所涉及的时间、成本和复杂性并非线性关系。你越是想实现 100%的保真度,复杂性和成本就越高,而 100%通常是无法实现的(见图 21.1)。

图 21.1 – 不同保真度水平的复杂性、时间和成本

图 21.1 – 不同保真度水平的复杂性、时间和成本

彻底切换迁移如果你想实现行为上的变化并最优化地使用新平台,则是最理想的选择。在本书的案例研究中,我假设采用的是彻底切换迁移:团队们从新平台开始,仅迁移绝对必要的内容。

现实情况介于这两种极端之间。如果你希望加速软件交付,你应该从彻底切换迁移开始,但为了在企业中扩大规模并推动采纳,你需要为团队提供一些迁移路径和工具,使其能够快速迁移。同时,会有一些暂时搁置的项目/产品,你可能希望将其归档,以便以后重新激活。你可以保持所有旧系统的运行,或者直接将其迁移过去。

实现低保真迁移的合规性

许多客户关心的一个问题是端到端可追溯性,这是出于合规性的原因。在许多高度监管的行业中,你必须为所有要求和最终功能测试提供端到端的可追溯性。低保真迁移的问题在于,这会导致可追溯链条的中断。

但这并不意味着唯一的解决方案是高保真度迁移。你仍然可以进行一次彻底的切换,并将旧系统保持在只读模式下运行,直到必要时为止。在新系统中,你无论如何都必须实现端到端的可追溯性。为了保持合规性,你需要将旧系统的标识符映射到新系统,涵盖两个系统的需求。

在审计的情况下,你可以提供来自两个系统的报告——无论是旧系统还是新系统。对于某些需求,你可能需要查看这两个报告,但只要有能在系统之间映射的标识符,这仍然能提供有效的可追溯性。

保持旧系统运行的麻烦通常远小于执行高保真度迁移的麻烦,但这取决于许多因素,比如旧系统的许可证。

为顺利过渡同步需求

在这个背景下,尤其是对于拥有多个不同工具的大型企业,一个有趣的选项是通过像 Tasktophttps://www.tasktop.com/)这样的产品,在不同平台之间同步需求。Tasktop 为许多产品提供连接器,如 Jira、Salesforce、ServiceNow、IBM Rational、IBM DOORS、Polarion ALM、Azure DevOps 等。工具间同步需求和工作项可以实现多个用例:

  • 在迁移期间,同时在旧工具和新工具中工作。这给了你更多的时间进行迁移,并且能够在保持完全可追溯性的同时,一个接一个地迁移团队。

  • 给予不同角色和团队自由,使用他们偏好的工具。你的项目经理喜欢 Jira,架构师偏爱 IBM Rational,运维团队使用 ServiceNow,而你的开发人员想切换到 GitHub?你可以通过在这些工具之间同步数据来实现这些工作流程。

尤其是在复杂的环境中,当你有一个大产品并且多个团队同时工作时,同步需求和工作项有助于优化迁移。

迁移你的代码

迁移到 GitHub 时,最简单的事情就是迁移你的代码,尤其是当代码已经存储在另一个 Git 仓库中时。只需使用 --bare 克隆仓库,确保仓库处于干净状态:

$ git clone --bare <URL to old system>

然后将代码推送到仓库:

$ git push --mirror <URL to new repository>

如果仓库已经包含代码,你必须添加 --force 参数来覆盖现有内容。你还可以使用 GitHub CLI 在推送现有代码时即时创建仓库:

$ gh repo create <NAME> --private --source <local path>

由于在 Git 中,作者信息是通过电子邮件地址进行匹配的,你只需在 GitHub 中为所有用户创建账户,并为他们分配在之前 Git 系统中使用的电子邮件地址。这样作者信息就能正确解析。

你还可以使用 GitHub 导入工具导入代码。除了 Git,以下仓库类型也被支持:

  • Subversion

  • Mercurial

  • 团队基础版本控制 (TFVC)

GitHub 导入工具接受源系统的 URL,并创建一个新仓库。大于 100 MB 的文件可以排除或添加到 Git 大文件存储LFS)中。

要使用 GitHub 导入工具导入一个仓库,请点击个人头像旁边的加号,然后选择 导入仓库(见 图 21.2):

图 21.2 – 导入一个仓库

图 21.2 – 导入一个仓库

如果你从 Subversion 迁移,可以使用 git-svngit-scm.com/docs/git-svn)在 Git 和 Subversion 仓库之间同步更改:

$ git svn <command>

如果你从 Azure DevOps/Team Foundation Server 迁移,最佳方法是先从 TFVC 迁移到 Git,然后再迁移到 GitHub。还有一个类似于 git-svn 的工具:git-tfsgithub.com/git-tfs/git-tfs)。它也允许你在 Git 和 TFVC 之间同步更改,或者直接迁移到 GitHub:

$ git tfs <command>

注意

Git 对短生命周期的分支进行了优化——而 TFVC 则没有。你不应仅仅将代码和所有分支迁移过去。利用这个机会进行一次干净的切换,并从新的分支模型开始。将代码迁移到 Git 可以是保留部分历史记录的第一步,但迁移后你应该调整你的分支模型。

将代码迁移到 GitHub 并不困难。有许多工具可以帮助你。挑战在于如何从旧的分支模型迁移到一个新模型,这个新模型优化了你的新平台,并提高了软件交付性能(请参见 第十一章基于主干的开发)。

挑战在于那些不是直接存储在 Git 中的内容:拉取请求、关联的工作项和管道。这些内容比仅仅是 Git 仓库本身更需要关注。

从 Azure DevOps 或 GitHub 迁移

如果你从 GitHub 迁移到 GitHub 或从 Azure DevOps 迁移到 GitHub,可以使用 extension install

$ gh extension install github/gh-gei
$ gh gei --help

你可以将 个人访问令牌PAT)设置为环境变量,以便对源系统和目标系统进行身份验证:

$ export ADO_PAT=<personal access token>
$ export GH_SOURCE_PAT=<personal access token>
$ export GH_PAT=<personal access token>

你也可以通过 generate-script 将它们传递,使用 --github-source-pat--ado-pat 参数。

要创建迁移脚本,请根据你是否想从 GitHub 或 Azure DevOps 迁移,执行以下命令之一:

$ gh gei generate-script --ado-source-org <source> --github-target-org <target>
$ gh gei generate-script --github-source-org <source> --github-target-org <target>

这将生成一个 PowerShell 脚本 migrate.ps1,该脚本可用于实际的迁移。脚本将调用 gh gei migrate-repo 对在 Azure DevOps 或 GitHub 组织中找到的所有团队项目进行迁移。这将排队实际的迁移操作。然后,它通过执行 gh gei wait-for-migration --migration-id,获取前一个命令的输出,以获得迁移状态。

GEI 当前支持以下内容:

  • Azure DevOpsADO

  • GitHub 企业服务器GHES)3.4.1+

  • GitHub 企业云

对于 Azure DevOps,将迁移以下项目:

  • Git 源

  • 拉取请求

  • 用户拉取请求历史

  • 拉取请求中的工作项链接

  • 拉取请求中的附件

  • 仓库的分支保护

对于 GitHub 企业服务器和云,以下内容也会被迁移:

  • 问题

  • 里程碑

  • Wiki

  • 仓库级别的项目板

  • GitHub Actions 工作流(不包括机密和工作流运行历史记录)

  • 提交评论

  • 活跃的 Webhooks

  • 仓库设置

  • 分支保护

  • GitHub Pages 设置

  • 上述数据的用户历史记录

请参见 docs.github.com/en/early-access/github/migrating-with-github-enterprise-importer 获取更多信息。请注意,GEI 仍处于 beta 版本,可能会频繁更改。

如果你使用的是 GitHub 企业服务器,你还可以使用 ghe-migrator 从另一个服务器实例或 GitHub 企业云导入数据。有关 GitHub 企业服务器的数据导入导出更多信息,请参见 docs.github.com/en/enterprise-server@3.4/admin/user-management/migrating-data-to-and-from-your-enterprise/about-migrations

迁移你的管道

要将你的管道迁移到 GitHub Actions,你可以使用一个名为 Valet 的工具。它支持以下来源:

  • Azure DevOps(经典管道、YAML 管道和发布)

  • Jenkins

  • Travis CI

  • Circle CI

  • GitLab CI

Valet 是一个基于 Ruby 的命令行工具,通过 Docker 安装。

注意

Valet 在撰写时仍处于私有 beta 阶段,且可能会发生变化。Valet 不是一个 100% 有效的解决方案,无法迁移一切!它是可扩展的,你将需要编写自己的转换器,并且迁移后可能仍需进行一些手动操作。

Valet 的分发是通过拉取容器镜像并使用两个脚本 valetvalet-update 与之互动:

$ docker pull ghcr.io/valet-customers/valet-cli

一旦你获得私有 beta 访问权限,你需要通过用户名和具有 read:packages 访问权限的 PAT token 进行身份验证才能访问 ghcr.io

$ docker login ghcr.io -u <USERNAME>

最好的方式是将 Valet 安装为 GitHub CLI 扩展,但你仍然需要在你的机器上运行 Docker,并且必须经过身份验证才能访问注册表。要将 Valet 安装为 GitHub CLI 扩展,请执行以下命令:

$ gh extension install github/gh-valet

现在,你可以使用 gh valet update 轻松更新 Valet。

Valet 使用环境变量进行配置。最简单的方法是将这些变量设置在你使用 Valet 的文件夹中的 .env.local 文件里。例如,这是将管道从 Azure 迁移到 GitHub 企业云的配置:

GITHUB_ACCESS_TOKEN=<GitHub PAT>
GITHUB_INSTANCE_URL=https://github.com
AZURE_DEVOPS_PROJECT=<project name>
AZURE_DEVOPS_ORGANIZATION=<org name>
AZURE_DEVOPS_INSTANCE_URL=https://dev.azure.com/<org>

Valet 有三种模式:

  • gh valet audit 将分析所有支持的管道的源下载信息。它将生成一个审计摘要报告(Markdown),包含所有管道、构建步骤和找到的环境。你可以使用审计来规划你的迁移。

  • gh valet dry-run 将把流水线转换为 GitHub Actions 工作流文件,并输出 YAML 文件。

  • gh valet migrate 将把流水线转换为 GitHub Actions 工作流文件,并在目标 GitHub 仓库中创建一个拉取请求,包含对工作流文件的更改。

  • gh valet forecast 根据历史流水线利用率预测 GitHub Actions 的使用情况。

要使用之前的配置进行审核并生成报告,只需运行以下命令:

$ gh valet audit azure-devops --output-dir .

这将生成一个 audit_summary.md 报告,并为每个受支持的流水线生成三个文件:一个 .config.json 文件,包含配置;一个 .source.yml 文件,包含转换为 YAML 的源流水线;以及一个 .yml 文件,包含转换后的 GitHub Actions 工作流,稍后将进行迁移。要执行一个流水线的迁移,运行 valet migrate

$ valet migrate azure-devops pipeline \
  --target-url https://github.com/<org>/<repo-name> \
  --pipeline-id <definition-id>

请记住,这是一项尽最大努力的迁移!并不是所有内容都可以迁移。例如,以下元素无法迁移:

  • 秘密

  • 服务连接

  • 未知任务

  • 自托管运行器

  • 来自 Key Vault 的变量

你可以为流水线步骤编写自己的转换器,无论是对于未知步骤,还是覆盖 Valet 的现有行为。创建一个新的 Ruby 文件(.rb),并以以下格式添加一个函数:

transform "taskname" do |item|
end

对于 Azure DevOps 任务,名称中包括版本号。要查看项目对象包含的内容,可以通过 puts item 将其输出到控制台。

这是一个示例转换器,它会覆盖 DotNetCoreCLI 任务的版本 2,并用 Bash 上的运行步骤替代,使用 globstar 语法迭代所有 .csproj 文件,并使用源流水线中的参数执行命令:

transform "DotNetCoreCLI@2" do |item|
  if(item["command"].nil?)
    item["command"] = "build"
  end
  {
    shell: "bash",
    run: "shopt -s globstar; for f in ./**/*.csproj; do dotnet #{ item['command']} $f #{item['arguments'] } ; done"
  }
end

要使用自定义转换器,你可以使用 --custom-transformers 参数。如果你有多个转换器,可以指定单个转换器或整个目录:

$ valet migrate azure-devops pipeline \
  --target-url https://github.com/<org>/<repo-name> \
  --pipeline-id <definition-id> \
  --custom-transformers plugin/*

每个工作流系统都有不同!确保花时间分析你希望如何转换你的流水线,以便优化新平台,而不是仅仅尝试迁移所有内容。如果你已经搞清楚了这一点,那么 Valet 将是一个很好的工具,帮助你更快地将团队过渡到 GitHub。

概述

GitHub 是一个复杂且快速发展的生态系统,对于任何形式的迁移都充满挑战。在迁移时,确保专注于优化新平台上的生产力,而不是迁移所有内容后再让团队处理混乱。根据你所在组织的规模和源平台的不同,你的迁移过程可能完全不同。

在本章中,你已经了解了 GitHub 和合作伙伴提供的不同工具,这些工具可以帮助你促进迁移过程。

在下一章,我们将讨论如何为最佳协作组织你的团队和仓库。

深入阅读

这些是本章中可以帮助你获取更多相关信息的链接:

第二十二章:组织你的团队

在本章中,你将学习如何将仓库和团队结构化为组织和企业的最佳实践,以促进协作并简化管理。

在本章中,我们将讨论以下主题:

  • GitHub 范围和命名空间

  • 结构化 GitHub 团队

  • 基于角色的访问权限

  • 自定义角色

  • 外部协作者

GitHub 范围和命名空间

GitHub 中的主要实体是仓库。仓库可以为用户或组织创建。仓库的 URL 将采用以下格式:

https://github.com/<username>/<repository>
https://github.com/<organization>/<repository>

对于 GitHub 企业版服务器,你必须将https://github.com替换为你的服务器的 URL。平台上的用户和组织名称必须是唯一的,因为它们提供了命名空间。仓库的名称在该命名空间内必须是唯一的。

GitHub 企业版

在 GitHub 中,企业是多个组织的容器。企业不是命名空间——组织名称仍然必须是唯一的。企业有一个 URL 后缀,用来引用该企业。你的企业 URL 看起来会像这样:

https://github.com/enterprises/<enterprise-slug>

如果你拥有一个通过发票支付的组织,那么你可以在 设置 | 账单和计划 下升级为企业。否则,你必须联系 GitHub 销售部门。

一个 GitHub 企业有三个角色:

  • 所有者:拥有企业的完全管理权限,但对组织没有管理权限

  • 成员:拥有至少一个组织访问权限的成员或外部协作者

  • 账单管理员:仅能查看和管理账单信息

你可以在企业级别为所有组织配置一些设置,例如 SAML 认证SSH 证书颁发机构IP 允许列表。还有一些企业级的 Webhooks,并且你可以访问整个企业的审计日志。审计日志流向云存储、Splunk 或 Azure Event Hubs 仅在企业级别可用,但大多数设置都与 账单许可 相关。

你还可以为许多可以在组织级别配置的设置制定策略。如果已设置策略,则组织的所有者无法更改该设置。如果没有定义策略,则该设置可以由组织的所有者配置。

GitHub 组织

管理你的仓库和团队的主要方式是通过组织。它们也可以在没有企业的情况下存在,并且你可以在不同的企业之间移动它们。组织不应被视为自助服务,让你的团队自行组织。一些公司拥有超过 2000 个组织——这会带来大问题,尤其是在管理集成时。例如,GitHub 应用只能在组织级别配置,而不能在企业级别配置。如果你想与 Jira 实例进行集成,那么你必须为所有组织配置此项。你不能在企业级别配置。

对于大多数客户来说,一个组织应该就足够了。如果您的公司有不同的法人实体,必须将其分开,那么这可能是创建多个组织的原因。另一个原因是如果您希望将开放源代码与内部源代码分开。但是,您不应将所有部门或事业部放在同一个组织中,最好使用团队来实现这一点。

一个组织有以下角色:

  • 拥有者: 拥有对团队、设置和代码库的完全访问权限

  • 成员: 可以看到成员和非私密团队,并创建代码库

  • 外部合作者: 这些人不是组织成员,但可以访问一个或多个代码库

组织拥有项目、包、团队和代码库。您可以为代码库配置许多设置。如果您没有在组织级别配置这些设置,则可以在代码库级别设置这些设置。

结构化您的组织的主要方式是使用团队。我们将在下一节中详细介绍这些内容。

GitHub 团队结构化

团队不仅是授予代码库权限的更便捷方式,还能加速员工的入职和离职过程。它们还可以用于共享知识,并通知特定小组变动。

团队有讨论,您可以看到它们的代码库和项目。团队可以具有以下两种可见性之一:

  • 可见: 可见的团队可以被组织中的每个成员看到并提及

  • 私密: 私密团队只能由其成员查看,并且不能被嵌套

一个团队存在于组织的命名空间中。这意味着团队的名称在组织内必须是唯一的。您可以使用以下语法提及团队或将其添加为代码所有者:

@<organization>/<team-name>

您可以通过嵌套团队,反映公司或团队的结构,使用级联的访问权限和提及方式。您可以在创建新团队时指定父团队,这样新团队就成为子团队。子团队也可以作为父团队——这样,您就可以创建深层次的层级结构。子团队会继承父团队的权限和通知,但反之则不行。

通过嵌套团队,您可以创建公司结构。您可以为所有员工、每个事业部、每个部门和每个产品团队(纵向团队)创建团队。您还可以使用团队来创建横向团队——例如,兴趣小组,如实践社区(见图 22.1):

图 22.1 – 使用团队结构化您的组织

图 22.1 – 使用团队结构化您的组织

这使您能够在您的价值链团队之间共享知识和所有权。如果适合您的社区结构,您还可以嵌套水平团队。

嵌套团队可以在组织的团队标签下展开(见图 22.2):

图 22.2 – 组织中“团队”标签下的嵌套团队

图 22.2 – 组织中“团队”标签下的嵌套团队

团队有讨论页面。组织成员可以创建并参与团队的讨论,但团队也可以有一些私密讨论,其他组织成员无法看到这些讨论(见图 22.3):

图 22.3 – 带有讨论的团队页面

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_22_003.jpg)

图 22.3 – 带有讨论的团队页面

团队可以被提及并分配为评审者和代码所有者。这是一个非常强大的工具,可以简化组织结构。但是,尽量保持简单,使用人们可以轻松理解的名称。您不想构建一个官僚化的怪物!保持简洁。

基于角色的访问

在仓库级别,您可以为团队或个人授予基于角色的访问权限。您可以使用以下默认角色之一:

  • 读取:读取并克隆仓库。打开并评论问题和拉取请求。

  • 处理:读取权限,此外还包括管理问题和拉取请求。

  • 写入:处理权限,此外还包括读取、克隆和推送到仓库。

  • 维护:写入权限,此外还包括管理问题、拉取请求和配置一些仓库设置。

  • 管理员:对仓库的完全访问权限,包括敏感和破坏性操作。

请注意,读取角色不仅仅是读取!它可以打开并评论问题和拉取请求。处理维护是开源项目中的典型角色,在企业场景中使用较少。

您可以为组织设置基础权限(读取、写入或管理员)。这将为所有成员授予对所有仓库的相应权限。外部合作者不会继承基础权限(有关更多信息,请参见docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/setting-base-permissions-for-an-organization)。

自定义角色

您可以在组织设置中的仓库角色(/settings/roles)下定义自定义角色。点击创建角色并为新角色指定名称和描述。然后,选择一个默认角色以继承其权限,并为其添加权限(见图 22.4):

图 22.4 – 在 GitHub 中创建自定义角色

](https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/acc-dop-gh/img/B17827_22_004.jpg)

图 22.4 – 在 GitHub 中创建自定义角色

权限是分类的。因此,如果您在搜索框中输入security,列表将显示与安全相关的所有可用权限。

权限可分为以下几类:

  • 讨论

  • 问题

  • 拉取请求

  • 仓库

  • 安全

请注意,并非所有内容都可以配置。例如,在本文编写时,GitHub Packages 没有特定的权限。

如果某个人被授予不同级别的访问权限,较高的权限总是会覆盖较低的权限。如果某个人被授予多个角色,GitHub 会在此人旁边显示混合角色警告。

再次提醒:尽量不要过度使用自定义角色。保持尽可能简单。

外部协作者

外部协作者是指不是您组织成员的人,但可以访问您组织的一个或多个仓库。

注意

将外部协作者添加到私有仓库将消耗一个您的付费许可证!

外部协作者不是您组织的成员。他们看不到内部仓库,也不会继承基础权限。

您不能在组织级别邀请外部协作者——您只能邀请成员加入您的组织,然后将他们转换为外部协作者(见 图 22.5):

图 22.5 – 将成员转换为外部协作者

图 22.5 – 将成员转换为外部协作者

作为仓库管理员,如果您在 设置 | 协作者和团队 下点击 添加人员,如果他们已经是组织成员,他们将自动作为成员添加。如果不是,他们将作为外部协作者添加。

外部协作者是与合作伙伴和客户轻松协作的绝佳方式,而无需他们成为您组织的一部分。但请记住,如果您使用 企业托管用户,则此方法不起作用。如果您启用了 SAML 单点登录,外部协作者将绕过该功能。这就是为什么组织所有者可以在组织设置中防止仓库管理员邀请外部协作者加入仓库的原因。

摘要

在本章中,您了解了企业中组织、仓库和团队结构的最佳实践。我们讨论了团队的嵌套、使用团队作为兴趣小组、基于角色的访问控制以及外部协作者。

在最后一章,我们将把本书的所有内容整合起来,并指导您使用 GitHub 转变您的企业,并利用它加速您组织中的 DevOps。

进一步阅读

请参阅以下链接和更多 GitHub 文档,了解本章中涵盖的主题:

第二十三章:转型你的企业

在最后一章,我们将讨论企业转型。我将解释如何将本书中解释的所有内容结合起来,转型你的企业,让它拥有工程文化,并提高开发人员的工作效率。

我们将涵盖以下主题:

  • 为什么许多转型会失败

  • 从为什么开始?

  • 数据驱动的转型

为什么许多转型会失败

软件是每个行业中所有产品和服务的核心——从客户体验到供应链管理(参见 第一章关键指标)。这意味着很多企业必须转型成为数字化的高效企业,但这些转型中的许多都失败了。角色被重新命名,管理层被重组,托管被称为私有云,但往往文化和业绩并未发生变化。转型失败的原因有很多,我在这里想给你一些例子。

假设你的公司或行业很特殊

我遇到的许多客户认为他们是独一无二的,但其实并非如此。抱歉地说,你的公司或行业也很可能不是独一无二的。至少在数字化转型方面是这样的。你的产品如果有缺陷,会导致人员伤亡吗?汽车、飞机、卡车、医疗设备等也可能如此。而且,所有为这些产品生产的零部件也是如此。它们并不特别。你必须遵守某些标准吗?你们生产军用产品吗?你们是上市公司吗?你们为政府工作吗?无论你认为是让公司独特的因素是什么,实际上有很多公司面临着和你一样的挑战,并且在 DevOps 转型方面也适用相同的规则。

如果你查看 第一章 中提到的研究,你会发现它们适用于所有公司:从小型初创公司到大型企业,从前沿互联网公司到高度监管的行业,如金融、医疗和政府(Forsgren N., Humble, J., & Kim, G., 2018, p. 22)。

但这实际上是件好事。这意味着你在自己转型过程中可能面临的许多问题,别人已经解决了。你可以从他们的失败中学习,而不必亲自经历它们。

没有紧迫感

改变的最大障碍是自满。如果你公司中的人们感到自满,他们往往会抵制变化,继续保持按部就班的做法。

你必须为员工建立一种真正的紧迫感,让他们现在就开始解决关键问题。在这种情况下,紧迫感并不意味着来自管理层的压力所带来的焦虑。真正的紧迫感应该推动人们以赢的决心去改变——而不是因害怕失败而感到焦虑(约翰·P·科特,2008)。

如果没有真正的紧迫感,人们会抵制变革,并且更有可能维持旧有的行为。请注意,紧迫感可能出于完全不同的原因,在组织的不同层级产生。管理层可能会感受到来自市场的压力,以及由于缺乏敏捷性无法通过频繁发布来应对挑战。工程师们可能会感受到技术债务的压力,以及由于陈旧的流程和工具而面临吸引和留住人才的问题。重要的是要通过清晰的愿景将这些不同的故事对齐到一个共同的根本原因上。只有当你成功地将不同的紧迫感对齐成一股朝着相同方向推动的力量时,才能确保不同的力量不会相互中和。

没有明确的愿景

替换工具、流程和角色很容易,但改变行为、文化和故事却很难。如果没有明确的愿景,转型就无法取得预期的结果。

如果我听到客户说我们不是微软或谷歌我们不是一家前沿的互联网公司,这告诉我他们缺乏明确的愿景。如果你的愿景明确表明你希望成为行业中的数字化领导者,或者从产品公司转型为服务公司,人们就不会轻易说出与之相悖的话。

一个推动变革的好愿景是一个清晰且具有说服力的声明,说明你所有的转型目标将指向何方(John P. Kotter 2012)。

我认为值得注意的是,DevOps 转型并不总是由高层管理推动。我知道很多公司,DevOps 转型是由单个部门甚至团队推动的。不过,同样的规则依然适用——你需要为你团队或部门中的团队制定清晰的愿景,并建立紧迫感,以确保转型成功。

让障碍阻碍你的进步

当你开始进行转型时,许多障碍会阻碍你的转型。我常常经历的一些典型例子是某些行业中的特定规定。许多规定,比如 ISO26262 或 GxP,提出了V 模型用于软件工程。V 模型基于瀑布模型,因此它与我们多年来在 DevOps 研究中学到的几乎所有内容都相矛盾。如果你坚持保持瀑布模型,你的 DevOps 转型很可能会失败,但这通常是由于你对这些规定的内部解读。如果你仔细看看这些规定,你会发现它们只是坚持最佳实践。如果你的做法优于推荐的做法,你可以为此辩护,并且仍然能通过审计。

你遇到的大多数障碍都来源于你的组织,例如,组织结构、紧密的岗位分类、流程,或是工会与管理层之间的阵地战。不要让这些障碍阻碍你的转型。

没有获得帮助

顾问在许多公司中有着不太好的声誉,主要是因为糟糕的经历。我曾经帮助一个客户数字化他们的产品。客户习惯于按照瀑布式方法做事,我向他们介绍了 Scrum 和 CI/CD。我们进行了培训,并在接下来的几年里成功地使用了敏捷开发。两年后,管理层支付了昂贵的顾问公司来引入 Scrum。他们基本上拿着和我两年前讲的一模一样的幻灯片,讲着同样的故事。这种类型的咨询服务导致了糟糕的声誉。

但是,如果你想学一项新的运动,你不会仅仅买些设备,然后看一些 YouTube 视频。你会加入一个俱乐部或找一个教练来指导你。运动不仅仅是关于知识和工具——它们更关乎技能的培养。没有经验丰富的教练,在某些运动中成功是很难的,甚至是不可能的。

对于在你的企业中建立新技能和能力来说,同样适用。向那些更有经验的人寻求帮助并没有什么可耻的,他们可以引导你度过变革的过程。考虑到你节省的时间和精力,帮助的费用很可能会便宜,更不用说失败的成本了。

为什么开始?

为了让变革成功,你需要一个清晰的愿景和紧迫感。愿景应该明确、有说服力、简短,并能激励人们跟随它。为了传达愿景,你可以遵循黄金圈Simon Sinek 2011, p.38)并从内到外进行沟通(见图 23.1):

图 23.1 – 传达愿景应该从**为什么**开始?

图 23.1 – 传达愿景应该从为什么开始?

让我们更详细地看看:

  • 为什么:你的公司为什么要进行这场变革。这为变革提供了目标,并建立了紧迫感。为什么有人会关心?

  • 如何?:你将如何成功地完成转型过程?

  • 什么?:你想要转型的实际内容。你在做什么或制造什么?

以目标为驱动的使命

不要低估愿景的力量!如果你是一个内燃机汽车的制造商,转型为电动汽车并不容易。会有抵触情绪。人们会害怕失去工作的力量。

要成功,你需要一个清晰的愿景并传达为什么?——就像大众集团在 2019 年发布的goTOzero使命声明中那样,集中在四个主要行动领域:气候变化、资源、空气质量和环境合规性。

到 2050 年,整个大众集团希望成为资产负债表上的二氧化碳中和公司。到 2025 年,公司计划将其车队的碳足迹相比 2015 年减少 30%(Volkswagen 2019)。

这完美地解释了为什么,建立了紧迫感,并融入了他们更新后的整体愿景,即让这个世界成为一个移动的、可持续的地方,所有公民都能触及

同样,梅赛德斯-奔驰在其 2019 年发布的雄心 2039声明中表示,他们计划在未来 20 年内实现汽车车队和生产的碳中和目标(梅赛德斯-奔驰集团媒体 2019)。

当你将一个产品公司转型为软件或服务公司时,情况也是一样的。即使你只是将一个瀑布式组织转型为 DevOps 组织,如果你无法描绘出一个理想的未来并解释为什么必须进行转型,人们也会害怕这种变化,并且会产生抵触情绪。

建立工程文化

拥有一个以目标为驱动的愿景,将帮助你在转型过程中建立工程文化:一个包容性和安全的组织文化,能够促进人才发展,并以共享和平等为动力(de Vries, M., & van Osnabrugge, R. 2022)。

这是一种文化,在这种文化中,人们在感觉到某些事物不对时会感到安全地发声,一种人们可以在没有恐惧的情况下进行实验和创新的文化,一种每个人都感到受欢迎和安全的文化——无论其出身、性别或宗教如何。

组织文化是一套共享的假设,指导着组织内部的行为(Ravasi, D., & Schultz, M. 2006)。这就是为什么改变文化很困难的原因。制作带有价值观和使命声明的 PowerPoint 幻灯片可能会影响文化,但也许并不是管理层预期的那种方式。

作为一名工程师,你可能会问,组织文化对你来说有何重要?难道这不是管理层的任务吗?然而,文化是每个系统成员的假设和行为的结果——这意味着每个人都可以改变它。作为工程师,你应该意识到你的文化,并在看到问题时敢于发声。开始做正确的事情,讲述正确的故事。

文化最好通过一些有深刻含义的小格言和原则来融入公司行为中。它们易于记住,并鼓励人们做正确的事。以下是你在拥有卓越工程文化的公司中常常听到的一些例子:

  • 寻求宽恕,而不是许可:鼓励人们做正确的事,即使这违背了当前的规则或流程。

  • 你建设,它就由你运营:建立端到端的责任和所有权,确保对所建设的事物负责。

  • 尽早失败,快速失败,频繁失败(或快速失败,快速推进):尽量尽早和快速地失败,而不是让一切都达到 100% 防弹的程度。

  • 拥抱失败:鼓励人们进行实验和冒险,并确保从失败中得到无责备的学习。承担责任,不责怪他人

  • 协作,而不是竞争共同努力,而不是对抗:促进跨组织界限的协作,并与客户和合作伙伴共同合作。

  • 去修复:鼓励人们主动承担责任并修复问题,而不是仅仅抱怨,但你必须确保创新不会被压制。确保人们有权利真正去修复他们抱怨的问题。

  • 把服务器当作牲畜,而不是宠物:鼓励人们自动化一切

  • 如果疼痛,就多做:激励人们多做那些困难的事情,培养完成任务的技能。这个短语通常用于发布或测试应用程序时。

这些只是一些例子。当你转型文化并建立 DevOps 时,会出现更多的故事和格言。

优秀的工程文化不仅仅是管理层的责任。他们必须让它发生并提供愿景,但最好的文化是在转型过程中由工程师们自己创造的。

数据驱动的转型

如果你想让你的转型成功,关键是要衡量正确的指标,并证明转型的结果真的比旧系统更好。这就是为什么在第一章《重要的指标》中,我介绍了可以收集的数据点,帮助你了解首先需要优化什么,并实现一些小的成功,激励大家继续进行 DevOps 转型。衡量正确的数据应该始终是开始的第一步。优化一个不是瓶颈的东西是资源的浪费,甚至可能带来负面影响。举个例子,假设你在应用程序中添加了缓存,但没有证明操作本身是否减慢了系统的速度,或者没有证明缓存某些数据后能提高多少速度。缓存会引入复杂性,也容易出错。所以,或许你根本没有优化系统,反而因为基于假设做出决策而让系统变得更糟。DevOps 实践也是如此。

约束理论

约束理论TOC)基于系统理论,假设如果没有限制性约束,系统的吞吐量是无限的。TOC 试图最大化系统在当前约束条件下的吞吐量,或者通过减少这些约束来优化系统。

解释这个理论的一个典型例子是高速公路(Small World 2016)。假设我们有一条有五个车道的高速公路,但由于有两个施工区域限制了两个车道的通行能力(见图 23.2):

图 23.2 – 一条车少的高速公路,受其限制

图 23.2 – 一条车少的高速公路,受其限制

交通流经瓶颈,但这只在一定吞吐量下有效。如果车太多,它们会开始相互影响,减慢速度,导致交通堵塞(见图 23.3):

图 23.3 – 如果吞吐量过高,会导致交通停滞

图 23.3 – 如果吞吐量过高,它会将流量阻塞

为了优化流量以实现最大流量,你必须将流量限制在最大约束的容量范围内(见图 23.4):

图 23.4 – 最大流量等于约束的容量

图 23.4 – 最大流量等于约束的容量

优化除最大约束以外的任何事物不会带来任何改进。许多城市曾尝试在隧道前后增加车道,但基本没有改善流量或减少交通拥堵。对于你的价值流也是如此——优化除最大约束外的任何事物都不会带来任何改进。

消除瓶颈

TOC 提供了五个重点步骤,用于消除约束(见图 23.5):

图 23.5 – 识别和消除约束的五个重点步骤

图 23.5 – 识别和消除约束的五个重点步骤

以下是这五个步骤的详细说明:

  • 识别:识别限制当前吞吐量的约束

  • 利用:改善约束的吞吐量

  • 同步:回顾并协调系统中的其他活动,确保它们在最优的方式下支持约束

  • 提升:尝试消除约束,解决问题的根本原因

  • 重复:通过识别限制当前吞吐量的下一个约束,持续改进系统

系统地消除工作流中的瓶颈是成功进行 DevOps 转型的关键!

DevOps 是一个持续改进的旅程

DevOps 是一个通过消除瓶颈不断推动软件交付性能边界的旅程。在他们自己的 DevOps 转型启动时,微软展示了一些来自不同领域的 pit stop 视频:从 1950 年的印第安纳波利斯赛道,停站时间 67 秒,到 2013 年墨尔本赛道,停站时间约为 2.96 秒。这是 DevOps 的一个很好的隐喻,通过自动化和优化的流程不断提升性能。

DevOps 是人、流程和产品的结合,旨在为我们的终端用户持续交付价值(Donovan Brown 2015)。

这是一种涵盖研究、开发、协作、学习和责任的工程文化,只有在各个方面共同实现时才能发挥作用。你不能仅仅选择 DevOps 的某一个方面并实施它,而忽略其他方面。

你只能改进一个关于流量的系统,前提是你知道最大瓶颈并致力于解决它。尝试优化其他方面不会带来任何结果,反而浪费时间和资源。这就是为什么进行数据驱动的转型非常重要,并且需要衡量正确的指标,以持续监控你的改进是否真正带来了预期的结果。识别一个瓶颈,利用它,改进它,然后重复。

优化与价值流对齐的团队

在本书中,我没有讨论任何DevOps 团队拓扑结构Matthew Skelton 2013)。我看到它们通常出现在更多 IT 驱动的转型中,开始时通常会根据某个拓扑结构来对齐你的转型之旅,之后在达到更高的 DevOps 成熟度后再切换到另一种模型(Martyn Coupland 2022p27)。相反,我专注于与价值流对齐的团队(见 第十七章赋能你的团队)。

你的 DevOps 之旅应从这些阶段开始,并优化一切以使其能够交付价值。这将自动引导你进入以开发者为先的思维方式(开发者即为交付价值的工程师)。如果你实践数据驱动的转型并通过消除瓶颈来优化价值,那么像平台团队或支持团队这样的拓扑结构将会出现。无需事先规划这一点。一个 DevOps 组织应该是一个自我改进的系统,所以一旦你达到那个阶段,接下来的事情会顺利展开。

成功的数据驱动 DevOps 转型有三个主要阶段(如图 23.6所示):

图 23.6 – 数据驱动 DevOps 转型的各个阶段

图 23.6 – 数据驱动 DevOps 转型的各个阶段

阶段的更多细节如下:

  • 指标:首先定义指标并收集数据(见第一章重要的指标)。

  • 工具选择:你需要做出一些基本的工具选择。在本书中,我假设GitHub是 DevOps 平台,但关于云使用和与当前治理流程对齐的决策将会有更多需要做的。

  • 人员、流程和文化:仔细选择你的试点团队,通过将他们的工作方式转变为精益管理更高的协作性,将他们带到新平台。教导并使他们能够采纳工程 DevOps 实践,如自动化和基于主干的开发,并使他们能够频繁发布并且充满信心。这些指标应迅速改善。这些就是你需要的快速胜利,能够保持大家的动力。

  • 规模化与优化:在试点团队取得成功后,你可以开始通过创建更多团队来扩大规模,这些团队将在新的平台上使用新的流程和工具工作。这也是你开始优化更多能力的时机,如软件架构精益产品管理技巧。一次解决一个瓶颈,始终观察是否有指标确认期望的结果。

由于 DevOps 是一段旅程而非一个目标,因此这个阶段基本上是永无止境的。你可能会在一段时间后调整指标,优化团队规模和自主性,但优化不会结束。结果只是越来越小,因为你已经处于更高的水平。

  • DevOps 愿景:转型的核心是一个强有力的愿景,解释为什么?并建立紧迫感。确保您有一个良好的沟通和变革管理策略。任何变革都会遭遇抵抗,您必须应对恐惧,并沟通为什么如何?以及什么?过程的各个方面,以及在此过程中您收集的成功案例,激励每个人向前迈进。

总结

为了保持竞争力,企业不能仅仅解决客户的问题。他们需要交付令客户满意的产品和服务,并且必须能够与市场互动,快速响应变化的需求。这使得今天的每个公司都是一家软件公司。如果您的公司不能实现转型,它可能会在几年内退出市场。

许多转型失败,但也有许多成功的案例,这些公司证明了即使是大型企业或高度受监管的公司,也能够实现转型并采纳 DevOps。

通过 GitHub,您拥有市场上最优秀的产品之一,全球超过 7300 万开发者、所有大型开源社区以及超过 84%的财富 500 强公司都喜爱它。这意味着更少的培训、更快的入职和更高的开发者满意度,从而带来更好的人才吸引力和留存率。而且,开源社区还为您提供应用程序的构建模块、工具、流水线,以及为您的流程模板提供模板。利用社区的力量将帮助您加速,而 GitHub 也为您提供了通过贡献或资助您依赖的项目来回馈社区的机会。

我希望本书能作为一本实用指南,帮助您通过 GitHub 的力量实现成功的 DevOps 转型。对于我来说,最有成就感的事莫过于看到工程师们在 DevOps 文化中享受工作,解决实际工程问题,而不是在生产环境中与 bug 作斗争或估算他们认为愚蠢的需求。

进一步阅读

以下是本章的参考资料,您可以通过这些资料获得更多相关主题的信息:

)

  • Forsgren N., Humble, J., & Kim, G.(2018 年)。加速:精益软件与 DevOps 的科学:构建与扩展高绩效技术组织(第一版)[电子书]。IT Revolution Press。

  • John P. Kotter(2008 年),紧迫感,哈佛商业评论出版社

  • John P. Kotter(2012 年),领导变革,哈佛商业评论出版社

  • 大众汽车(2019 年):大众汽车与新企业使命声明——环境“goTOzero”https://www.volkswagenag.com/en/news/2019/07/goTOzero.html

  • 梅赛德斯-奔驰集团媒体(2019 年):“Ambition2039”:我们走向可持续出行的路径group-media.mercedes-benz.com/marsMediaSite/ko/en/43348842

)

)

)

)

  • Matthew Skelton(2013 年):什么样的团队结构最适合 DevOps 蓬勃发展web.devopstopologies.com/

  • Martyn Coupland(2022 年):DevOps 采纳策略:原则、流程、工具与趋势,Packt

posted @ 2025-06-26 15:33  绝不原创的飞龙  阅读(30)  评论(0)    收藏  举报