DevOps

提示

此内容摘自电子书《为 Azure 构建云原生 .NET 应用程序》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

Cloud Native .NET apps for Azure eBook cover thumbnail.

软件顾问在回答用户提出的问题时,最喜欢用的口头禅就是“这取决于”。 这倒不是因为软件顾问不喜欢明确表态, 而是因为软件中的任何问题都没有一个真正的答案。 没有绝对的对与错,而只有对立面之间的平衡。

以开发 Web 应用程序的两大流派 - 单页应用程序 (SPA) 与服务器端应用程序为例。 一方面,SPA 的用户体验往往更好,并且可以最大限度地减少进入 Web 服务器的流量,因此可以将其托管在像静态托管这样简单的服务上。 另一方面,SPA 的开发速度往往较慢,测试也更困难。 哪一个才是正确的选择? 这取决于你的实际情况。

这种两极分化的问题就连云原生应用程序也不能幸免。 它们在开发速度、稳定性和可伸缩性方面具有明显的优势,但管理起来可能相当困难。

几年前,将应用程序从开发环境移动到生产环境需要一个月甚至更长时间,这种情况并不少见。 公司以 6 个月甚至每年一次的节奏发布软件。 你只要看看 Microsoft Windows,就能了解在 Windows 10 常青时代到来之前,人们可以接受的发布节奏。 Windows XP 与 Vista 间隔了五年,Vista 与 Windows 7 又间隔了三年。

现在,毋庸置疑的是,能够快速发布软件给快速发展型公司带来了巨大的市场优势,这是那些懒散的竞争对手难以企及的。 正因如此,Windows 10 的主要更新现在大约每六个月发布一次。

利用更快、更可靠的发布向企业提供价值的模式和实践统称为 DevOps。 它们由各种理念组成,涵盖从指定应用程序一直到交付和操作该应用程序的整个软件开发生命周期。

DevOps 在微服务之前就已兴起。如果没有 DevOps,就不可能有更小、契合度更高的服务,也就不可能在生产环境中发布和操作不止一个,而是很多个应用程序。

Figure 10-1 Search trends show that the growth in microservices doesn't start until after DevOps is a fairly well-established idea.

图 10-1 - DevOps 和微服务。

借助出色的 DevOps 实践,可以实现云原生应用程序的优势,而不会在实际操作应用程序的大量工作中窒息。

说到 DevOps,这世上其实并没有什么黄金大锤。 谁都没法提供一个完整且包罗万象的解决方案来发布和操作高质量的应用程序。 这是因为每个应用程序都与其他应用程序千差万别。 但是,有一些工具可以让 DevOps 变得不那么令人生畏。 其中一种工具称为 Azure DevOps。

Azure DevOps

Azure DevOps 历史悠久。 其根源可以追溯到 Team Foundation Server 的首次上线,之后更是经历了各种名称更迭:Visual Studio Online 和 Visual Studio Team Services。 然而,经过这些年的发展,它已经青出于蓝而胜于蓝。

Azure DevOps 分为五个主要组件:

Figure 10-2 The five major areas of Azure DevOps

图 10-2 - Azure DevOps。

Azure Repos - 一种源代码管理工具,支持古老的 Team Foundation 版本控制 (TFVC) 和业界最受欢迎的 Git。 拉取请求提供了一种通过促进对所做更改的讨论来实现社交编码的方法。

Azure Boards - 提供问题和工作项跟踪工具,致力于让用户能够选取最适合自己的工作流。 它带有许多预配置模板,包括支持 SCRUM 和看板开发风格的模板。

Azure Pipelines - 一种支持与 Azure 紧密集成的生成和发布管理系统。 生成可以在从 Windows 到 Linux 到 macOS 的各种平台上运行。 生成代理可以在云端或本地预配。

Azure Test Plans - 有了 Test Plans 功能提供的测试管理和探索测试支持,任何 QA 人员都不会被落下。

Azure Artifacts - 一个工件源,允许公司就 NuGet、npm 等包管理器创建自己的内部版本。 它的另一个作用是在集中式存储库发生故障时充当上游包的缓存。

Azure DevOps 中的顶级组织单位称为项目。 在每个项目中,可以打开和关闭各种组件,例如 Azure Artifacts。 其中每个组件为云原生应用程序提供不同的优势。 最有用的三个分别是存储库、版块和管道。 如果用户想在另一个存储库堆栈(例如 GitHub)中管理源代码,但仍然利用 Azure Pipelines 和其他组件的优势,这是完全可行的。

幸运的是,开发团队在选择存储库时有很多选择。 其中一个就是 GitHub。

GitHub 操作

GitHub 成立于 2009 年,是一个广受欢迎的基于 Web 的存储库,用于托管项目、文档和代码。 许多大型科技公司(如 Apple、Amazon、Google)和主流公司都使用 GitHub。 GitHub 以名为 Git 的开源分布式版本控制系统为基础, 并在此基础上添加了自己的一组功能,包括缺陷跟踪、功能和拉取请求、任务管理以及每个基本代码的 wiki。

随着 GitHub 的发展,它也在添加各种 DevOps 功能。 例如,GitHub 有自己的持续集成/持续交付 (CI/CD) 管道,称为 GitHub Actions。 GitHub Actions 是由社区提供支持的工作流自动化工具。 它让 DevOps 团队能够与其现有工具集成,混合和匹配新产品,并与软件生命周期(包括现有 CI/CD 合作伙伴)挂钩。

GitHub 拥有超过 4000 万用户,是世界上最大的源代码主机。 2018 年 10 月,Microsoft 收购了 GitHub。 Microsoft 承诺,GitHub 仍然是一个任何开发人员都可以插入和扩展的开放平台。 它继续作为一家独立公司运营。 GitHub 提供企业、团队、专业和免费帐户计划。

源代码管理

为云原生应用程序整理代码是一项很有挑战性的工作。 云原生应用程序往往是由相互通信的小型应用程序交织而成的应用程序网,而不是一个大型应用程序。 与计算方面的所有问题一样,代码的最佳排列方式仍然是一个悬而未决的问题。 使用不同类型布局的成功应用程序有不少,但有两种变体似乎最受欢迎。

在深入了解实际的源代码管理工具之前,最好先确定有多少项目适合使用此工具。 单个项目支持多个存储库和生成管道。 版块稍微复杂一些,但也可以轻松地将任务分配给单个项目中的多个团队。 在单个 Azure DevOps 项目中,有可能支持数百甚至数千名开发人员。 这样做可能是最好的方法,因为它为所有开发人员提供了一个统一的工作场所,减少了当开发人员查找某个应用程序时不确定它属于哪个项目的混乱局面。

在 Azure DevOps 项目中拆分微服务代码可能更具挑战性。

Figure 10-3 Single versus Multiple Repositories

图 10-3 - 一个存储库与多个存储库。

每个微服务一个存储库

乍一看,这种方法似乎是拆分微服务源代码的最合理的方法。 每个存储库都可以包含生成一个微服务所需的代码。 这种方法的优势显而易见:

  1. 可以将生成和维护应用程序的相关说明添加到每个存储库根目录的自述文件中。 浏览存储库时,很容易找到这些说明,从而减少了开发人员的启动时间。
  2. 每个服务都位于一个逻辑位置,只要知道服务名称即可轻松找到。
  3. 可以将生成轻松设置成仅在更改所属存储库时触发。
  4. 进入存储库的更改数量由处理该项目的少数开发人员决定。
  5. 通过限制开发人员具有读写权限的存储库,可以轻松设置安全性。
  6. 负责团队只需与其他人稍作讨论,即可更改存储库级别的设置。

微服务背后的一个关键理念是,服务应彼此隔离。 使用域驱动设计确定服务的边界时,服务充当事务边界。 数据库更新不应跨多个服务。 这种相关数据的集合称为有界上下文。 这一理念通过将微服务数据隔离到独立于其他服务的自治数据库来体现。 将此理念贯彻到整个源代码是非常有意义的。

但是,这种方法并非完美无缺。 当下比较棘手的一个开发问题就是如何管理依赖项。 以普通 node_modules 目录包含的文件数为例。 对诸如 create-react-app 之类的项目执行全新安装可能会带来数千个包。 如何管理这些依赖项成了一个大难题。

如果更新某个依赖项,下游包也必须更新此依赖项。 麻烦的是,这其中涉及开发工作,因此,node_modules 目录最终会产生单个包的多个版本,每个版本都是其他某个包(版本控制节奏略有不同)的依赖项。 部署应用程序时,应该使用哪个版本的依赖项? 当前正在生产的版本? 当前为 Beta 版,但在使用者将其投入生产时可能正在生产的版本? 这是单靠微服务无法解决的难题。

有一些库被各种项目所依赖。 通过将微服务划分为每个存储库中一个,可以使用内部存储库 Azure Artifacts 完美解决内部依赖项问题。 库的生成会将其最新版本推送到 Azure Artifacts 以供内部使用。 下游项目仍必须手动更新,以依赖新更新的包。

在服务之间移动代码时,另一个缺点也随之暴露。 虽然我们很愿意相信,第一次将应用程序划分为多个微服务能够做到 100% 正确,但现实是,很少有我们这么有先见之明,不犯服务划分错误的人。 因此,功能和驱动它的代码需要从服务移动到服务:从存储库移动到存储库。 从一个存储库跳转到另一个存储库时,代码会丢失其历史记录。 在很多情况下,尤其是在审核时,拥有一段代码的完整历史记录非常有用。

最后也是最重要的一个缺点是协调更改。 在真正的微服务应用程序中,服务之间不应该存在部署依赖关系。 应该可以按任意顺序部署服务 A、B 和 C,因为它们具有松散耦合性。 但实际上,有时需要同时跨多个存储库进行更改。 例如,更新库以关闭安全漏洞或更改所有服务使用的通信协议。

若要进行跨存储库更改,需要连续向每个库提交更改。 每个存储库中的每个更改都需要单独拉取请求和评审。 这项活动可能难以协调。

作为使用多个存储库的替代方法,可以将所有源代码放在一个大型的全知存储库中。

单一存储库

在这种方法(有时称为单存储库)中,每个服务的所有源代码都放入同一个存储库。 起初,这种方法看上去似乎很糟糕,可能会使源代码处理变得很麻烦。 但是,它也有一些明显的优点。

第一个优点是可以更轻松地管理项目之间的依赖关系。 项目可以直接相互导入,而不必依赖某些外部工件源。 这意味着可以即时更新,并且在编译时可能能够在开发人员工作站上发现冲突的版本。 实际上就是将一些集成测试左移。

现在,在项目之间移动代码时,更容易保留历史记录,因为系统会检测出文件已被移动,而不是被重写。

另一个优点是可以在单次提交中进行跨服务边界的广泛更改。 此活动减少了可能需要单独评审数十个更改的开销。

有许多工具可以对代码执行静态分析,以检测不安全的编程实践或有问题的 API 使用。 在多存储库环境中,系统需要循环访问每个存储库,以找到其中的问题。 单一存储库允许在一个位置运行所有分析。

单一存储库方法也有许多缺点。 其中最令人担忧的是,使用单一存储库会引发安全问题。 在“每个服务一个存储库”模型中,存储库内容发生泄漏时,丢失的代码量极少。 使用单一存储库时,公司拥有的所有代码都有可能丢失。 过去就有很多因出现这种情况而导致整个游戏开发工作停摆的案例。 使用多个存储库可减少受攻击面,这是大多数安全实践中的理想特征。

单一存储库的规模可能会迅速变得无法管理。 这会给性能带来一些有趣的影响。 到时可能需要使用一些专门的工具,例如适用于 Git 的虚拟文件系统,该工具的设计初衷是改善 Windows 团队开发人员的体验。

通常,对使用单一存储库的争论归根究底还是对 Facebook 或 Google 使用此方法进行源代码排列的争论。 如果这种方法对这些公司来说足够好,那么,它肯定是所有公司的正确方法。 但事实上,很少有公司的运营规模能与 Facebook 或 Google 相媲美。 这种规模的公司所发生的问题与大多数开发人员面临的问题并不相同。 故,适用于此者未必适用于彼。

最后,可以使用任一解决方案来托管微服务的源代码。 但是,在大多数情况下,相比于在单一存储库中操作所产生的管理和工程开销,其优势不值一提。 通过将代码拆分到多个存储库中,可以更好地分离关注点,鼓励开发团队的自主性。

标准目录结构

无论是使用单一存储库还是多个存储库,每个服务都会有自己的目录。 让开发人员在项目间快速交叉的最佳优化方式之一是维持一个标准目录结构。

Figure 10-4 A standard directory structure for both the email and sign-in services

图 10-4 - 标准目录结构。

每次创建新项目时,都应该使用具有正确结构的模板。 此模板还可以包含主干自述文件和 azure-pipelines.yml 等有用的项目。 在所有微服务体系结构中,项目之间的高度差异导致更难对服务执行批量操作。

有许多工具可以为包含多个源代码目录的整个目录提供模板化。 Yeoman 在 JavaScript 中很受欢迎,GitHub 最近发布了存储库模板,它们提供许多相同的功能。

任务管理

管理任何项目中的任务皆非易事。 关于建立什么样的工作流来实现最佳开发人员生产力,有无数的问题需要回答。

云原生应用程序往往比传统软件产品更小,或者至少划分为更小的服务。 跟踪与这些服务相关的问题或任务与跟踪任何其他软件项目一样重要。 没有人希望失去某个工作项的踪迹,或者向客户解释他们的问题未正确记录。 版块在项目级别配置,但可以在每个项目中定义区域。 这允许跨多个组件分解问题。 将整个应用程序的所有工作集中在一处的好处是,由于更加了解各个工作项,团队之间的工作项交接变得更轻松。

Azure DevOps 附带许多预配置的常用模板。 在最基本的配置中,只需要知道积压工作中的内容、用户正在处理的内容以及已完成的内容。 对软件生成过程必须有这种程度的了解,才能确定工作优先级并将已完成的任务报告给客户。 当然,很少有软件项目会坚持采用 to dodoingdone 这样简单的流程。 不用多久,用户就会开始在流程中加入 QADetailed Specification 等步骤。

敏捷方法中比较重要的一个环节是定期自检。 这些评审旨在深入了解团队面临的问题以及如何改进这些问题。 通常,这意味着在开发过程中更改问题和功能流。 因此,用其他阶段扩展版块布局是非常明智的。

版块中的阶段并不是唯一的组织工具。 根据版块的配置,会有一个工作项层次结构。 可以出现在版块上的最精细的项是任务。 任务自带标题、描述、优先级、剩余工作量估算字段以及链接到其他工作项或开发项的能力(分支、提交、拉取请求、生成等)。 工作项可以分类到应用程序的不同区域和不同迭代(冲刺 (sprint)),从而更易查找。

Figure 10-5 An example task in Azure DevOps

图 10-5 - Azure DevOps 中的任务。

描述字段支持你所期望的正常样式(粗体、斜体、下划线和删除线),并能插入图像。 这使它成为可用于详细描述工作或 bug 的强大工具。

任务可以汇总为功能,以定义更大的工作单元。 功能又可以汇总为长篇故事。 通过在此层次结构中对任务进行分类,可以更加清楚某项大型功能离正式发布还有多远。

Figure 10-6 Work item types configured by default in the Basic process template

图 10-6 - Azure DevOps 中的工作项。

Azure Boards 中的问题有各种各样的视图。 尚未计划的项显示在积压工作中。 在那里,系统可以将其分配给冲刺 (sprint)。 冲刺是一个时间周期,在此期间预计将完成一定数量的工作。 这项工作可以包括任务,也可以包括票证的解析。 进入冲刺阶段后,可以从冲刺版块部分管理整个冲刺。 以下视图显示了工作的进展情况,并包含一个燃尽图,用于对冲刺是否成功提供不断更新的估计值。

Figure 10-7 A board with a sprint defined

图 10-7 - Azure DevOps 中的版块。

现在应该可以看出,Azure DevOps 中版块的功能非常强大。 对开发人员来说,可以轻松查看正在处理的工作。 对项目经理来说,可以查看即将开展的工作以及现有工作的概述。 对管理者来说,有很多关于资源和容量的报告。 遗憾的是,云原生应用程序并没有什么神奇之处,它没有需要跟踪的工作。 但是,如果必须跟踪工作,有几个地方的体验比 Azure DevOps 更好。

CI/CD 管道

在软件开发生命周期中,几乎没有什么变更像持续集成 (CI) 和持续交付 (CD) 那样具有革命性。 一旦签入更改,系统就针对项目源代码生成和运行自动化测试,从而及早发现错误。 在持续集成生成出现之前,从存储库拉取代码时发现它没有通过测试甚至无法生成的情况并不少见。 这导致需要跟踪中断的源头。

传统上,将软件交付到生产环境需要大量文档和一系列步骤。 其中每个步骤都需要在非常容易出错的流程中手动完成。

Figure 10-8 A checklist

图 10-8 - 清单。

持续集成的姊妹篇是持续交付,后者负责将新生成的包部署到环境中。 手动过程无法扩展,赶不上开发速度,因此自动化变得更加重要。 清单被脚本取代,脚本可以比任何人更快、更准确地执行相同的任务。

持续交付的目标环境可能是测试环境,或者像许多主要技术公司那样,可能是生产环境。 后者需要花钱进行高质量的测试,以确保更改不会中断用户的生产。 像持续集成能及早发现代码中的问题一样,持续交付能及早发现部署过程中的问题。

自动执行生成和交付过程的重要性因云原生应用程序而更加突出。 其部署频率更高,部署到的环境更多,因此手动部署几乎是不可能的。

Azure Builds

Azure DevOps 提供了一组工具,使持续集成和部署比以往更容易。 这些工具位于 Azure Pipelines 下。 其中第一个就是 Azure Builds,这是一种用于大规模运行基于 YAML 的生成定义的工具。 用户可以自带生成计算机(适合生成需要精心设置的环境的情况),也可以使用不断刷新的 Azure 托管虚拟机池中的计算机。 这些托管生成代理预装了各种开发工具,不仅适用于 .NET 开发,还适用于从 Java 到 Python 到 iPhone 的所有开发。

DevOps 包括各种开箱即用的生成定义,可以针对任何生成进行自定义。 生成定义在名为 azure-pipelines.yml 的文件中定义并签入存储库,以便与源代码一起进行版本控制。 这样更容易在分支中对生成管道进行更改,因为可以将更改仅签入该分支。 图 10-9 显示了用于在完整框架上生成 ASP.NET Web 应用程序的 azure-pipelines.yml 示例。

name: $(rev:r)

variables:
  version: 9.2.0.$(Build.BuildNumber)
  solution: Portals.sln
  artifactName: drop
  buildPlatform: any cpu
  buildConfiguration: release

pool:
  name: Hosted VisualStudio
  demands:
  - msbuild
  - visualstudio
  - vstest

steps:
- task: NuGetToolInstaller@0
  displayName: 'Use NuGet 4.4.1'
  inputs:
    versionSpec: 4.4.1

- task: NuGetCommand@2
  displayName: 'NuGet restore'
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  displayName: 'Build solution'
  inputs:
    solution: '$(solution)'
    msbuildArgs: '-p:DeployOnBuild=true -p:WebPublishMethod=Package -p:PackageAsSingleFile=true -p:SkipInvalidConfigurations=true -p:PackageLocation="$(build.artifactstagingdirectory)\\"'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  displayName: 'Test Assemblies'
  inputs:
    testAssemblyVer2: |
     **\$(buildConfiguration)\**\*test*.dll
     !**\obj\**
     !**\*testadapter.dll
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: CopyFiles@2
  displayName: 'Copy UI Test Files to: $(build.artifactstagingdirectory)'
  inputs:
    SourceFolder: UITests
    TargetFolder: '$(build.artifactstagingdirectory)/uitests'

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact'
  inputs:
    PathtoPublish: '$(build.artifactstagingdirectory)'
    ArtifactName: '$(artifactName)'
  condition: succeededOrFailed()

图 10-9 - azure-pipelines.yml 示例

此生成定义使用许多内置任务,这些任务使创建生成变得像搭建乐高套装一样简单(比搭建巨型千年隼号更简单)。 例如,NuGet 任务可还原 NuGet 包,而 VSBuild 任务调用 Visual Studio 生成工具来执行实际编译。 Azure DevOps 提供数百种不同的任务,还有数千种任务由社区维护。 无论你要运行什么生成任务,都有可能已经有人生成了一个。

可以通过签入、按计划或通过完成另一个生成来手动触发生成。 在大多数情况下,最好是在每次签入时进行生成。 你可以筛选生成,以便针对存储库的不同部分或针对不同分支运行不同的生成。 此功能支持许多方案,例如,运行快速生成并减少对拉取请求的测试,以及每晚针对主干运行完整的回归套件。

生成的最终结果是称为生成工件的文件集合。 这些工件可以传递到生成过程的下一步或添加到 Azure Artifacts 源,以供其他生成使用。

Azure DevOps 发布

Builds 负责将软件编译成可交付的包,但仍需要将工件推送到测试环境才能完成持续交付。 为此,Azure DevOps 使用一个名为 Releases 的独立工具。 Releases 工具使用可用于 Builds 的相同任务库,但引入了“阶段”的概念。 阶段是指安装包的隔离环境。 例如,某个产品可能会使用开发、QA 和生产环境。 代码不断交付到可对其运行自动化测试的开发环境中。 一旦通过这些测试,发布就会进入 QA 环境进行手动测试。 最后,代码被推送到生产环境,在那里每个人都可以看到它。

Figure 10-10 An example release pipeline with Develop, QA, and Production phases

图 10-10 - 发布管道

生成中的每个阶段都可以在前一个阶段完成时自动触发。 但是,在许多情况下,这是不可取的。 将代码移动到生产环境可能需要获得某人的批准。 Releases 工具通过在发布管道的每个步骤设置审批者来支持此操作。 你可以设置规则,使发布在进入生产环境之前必须先由特定的人或一组人签字确认。 这些入口支持手动质量检查,并遵守与控制进入生产环境的内容相关的任何法规要求。

每个微服务都有生成管道

许多生成管道可免费配置,因此,最好为每个微服务配置至少一个生成管道。 理想情况下,微服务可以独立部署到任何环境,因此最好让每个微服务都能够通过自己的管道发布,而不发布大量不相关的代码。 每个管道都可以有自己的一套审批程序,从而允许每个服务的生成过程存在差异。

版本控制发布

Releases 功能的一个缺点是它不能在签入的 azure-pipelines.yml 文件中定义。 从让每个分支都有发布定义,到在项目模板中包含发布框架,你可能想要这样做的原因有很多。 幸运的是,我们正在将部分阶段支持转移到 Builds 组件中。 此功能称为多阶段生成,第一个版本现已推出