Azure DevOps 解决方案实现指南(二)

原文:annas-archive.org/md5/5194d46360126922de6c6aa72f8a3efc

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:基础设施和配置作为代码

在上一章中,重点是存储和构建应用程序代码以及发布创建的二进制文件。您学习了如何创建一个流水线,从源代码控制到目标环境,自动重复部署您的应用程序。

在本章中,您将学习如何将相同的原则应用于运行应用程序的基础设施和运行时配置。这样做将帮助您进一步提高向生产环境交付变更的速度,增加向最终用户提供价值的流量。

本章将首先解释将所有内容(包括基础设施和配置)作为代码的价值。接下来,它继续解释 ARM 模板。将解释语法以及如何部署 ARM 模板。然后,它继续解释 Azure 云中提供的 Azure 自动化。Azure 自动化可用于按计划运行脚本或加载和应用 PowerShell DSC 模块。接下来是管理 PaaS 提供的应用程序设置,例如 Azure 应用服务。最后,它讨论了几种具有类似功能的其他工具。

本章将涵盖以下主题:

  • 将所有内容作为代码

  • 使用 ARM 模板

  • 部署 ARM 模板

  • 反向工程模板

  • 使用 Azure 自动化

  • 管理应用程序设置

  • 其他工具

技术要求

要实验本章描述的一个或多个技术,可能需要以下一个或多个:

将所有内容作为代码

如果您过去负责创建和维护应用程序基础设施和配置,您很可能已经经历了所谓的配置漂移*。*配置漂移是指接受和生产环境中服务器配置之间存在差异的现象。或者,更糟糕的是,在生产环境中有多台服务器时,这些服务器的配置可能并不总是相同。

配置漂移的最常见原因是手动更改。在手动更改时,可能是在生产问题的压力下进行,始终存在将不同设置应用于不同服务器或主机的风险。如果你需要扩展并向生产环境中添加另一台服务器,且这台服务器需要与所有已存在服务器采用相同配置的可能性非常小。

通过基础设施即代码IaC)和配置即代码CaC),你不再手动更改应用配置和基础设施,而是通过自动化来完成。实现这一目标的第一步是指定所需的配置和基础设施状态。然后,将所需状态输入配置管理工具,工具会在你的基础设施上强制执行该配置。只指定所需状态被称为声明性方法,与命令性方法不同,后者需要指定所有需要执行的步骤。

这些工具通常还能够定期检查你的基础设施和配置的当前状态,并在检测到任何偏差时重新应用所需的状态。这是由于声明性方法的存在。这使得应用配置成为一个幂等操作。如果一个操作是幂等的,意味着它可以重复执行一次或多次,而结果始终保持一致。

在采用 IaC 和 CaC 时,你甚至可以在部署应用程序之前重新创建完整的基础设施,先在新基础设施上部署应用程序,然后在切换到新部署后放弃旧的基础设施。这是一种极端形式的不变服务器。这种方法的附加好处是,你现在可以确保不再留下任何来自上一次部署的配置或二进制文件痕迹。

在接下来的章节中,你将了解不同的 IaC 技术以及如何使用它们。理解它们是互补的,并且通常一起使用非常重要。例如,ARM 模板可用于在 Azure 中创建虚拟机,完成后可以使用 PowerShell DSC 或 Ansible 来配置这些虚拟机。

使用 ARM 模板

在 Azure 平台上工作时,基础设施使用Azure 资源管理器ARM)模板进行描述。ARM 模板是用 JSON 编写的,骨架模板如下所示:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
  },
  "variables": {
  },
  "resources": [
  ],
  "outputs": {
  }
}

模板本身在最高层次上是一个 JSON 对象。$schema是一个必需的属性,其显示的值也是强制性的。contentVersion属性也是必需的,并且可以用于为内容指定版本。如果需要,作者可以使用此版本来版本化模板。

本章的其余部分将更详细地讨论构成 ARM 模板的不同部分。章节末尾还提供了在线参考链接,此外,还附加了 ARM 模板结构和语法的正式详细解析链接。

参数

每个模板必须以一个参数部分开始。该部分采用 JSON 对象的形式,可以为空,但不能省略。此部分的用途是声明一个或多个可以由 ARM 模板调用者在部署前指定的参数。使用参数部分的常见原因是使用相同的模板,但在测试环境和生产环境之间更改资源名称。一个示例的参数部分可能如下所示:

{
  “appServiceName”: {
    “type”: “string”,
    “metadata”: {
      “description”: “a free to choose text”
    }
}

对于每个参数,指定一个新键,键名为参数的名称。值是一个对象。该对象有一个必需的键,typetype 的允许值为 stringintboolobjectarraysecureStringsecureObjectsecureStringsecureObject 类型用于确保这些参数的运行时值不会出现在任何日志和输出中。它们用于存储密码、密钥或其他机密信息。

元数据对象,带有 description 键,这是可选的,可以用来为参数添加描述,以供将来参考。

其他可以在参数对象上指定的属性如下:

  • minValuemaxValue 用于指定整数值的范围

  • minLengthmaxLength 用于指定字符串值的长度范围

  • defaultValue 用于指定如果在应用模板时未指定值,将使用的默认值

  • allowedValues 用于指定允许值的数组,限制有效的输入值

接下来,让我们了解一下什么是参数文件。

参数文件

在部署模板时指定参数值的一种方式是通过变量文件。通常,一个模板会附带多个参数文件,例如一个用于测试,另一个用于生产。一个参数文件的 JSON 格式如下所示:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "exampleParameter": {
      "value": "exampleValue"
    }
  }
}

与 ARM 模板一样,每个参数文件都是一个 JSON 对象,具有必需的 $schemacontentVersion 属性。第三个属性参数用于指定一个或多个参数值。对于每个参数,指定其名称作为键,并将一个对象作为值。该对象可以包含 value 键,用于提供参数的实际值。

尽管指定资源名称、扩展选项以及其他在不同环境中需要变化的内容时非常有用,但此解决方案不适用于存储机密。密钥、密码和其他机密信息不应作为明文存储在源代码控制的参数文件中。对于机密信息,可以使用另一种表示方式:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "exampleParameter": {
      "reference": {
        “keyvault”: {
          "id": "/subscriptions/…/Microsoft.KeyVault/vaults/<vaultname>"
        },
        “secretName”: “myKeyVaultSecret”
      }
    }
  }
}

使用这种表示法时,不是直接指定值,而是指向 Azure 密钥库中存储正确值的位置。部署模板时,此密钥将在 Azure 中从密钥库中取出并用于部署。只有在启动部署的用户或服务具有密钥库的所有者或贡献者角色,并且密钥库启用了模板部署时,才允许这样做。

严格来说,任何包含Microsoft.KeyVault/vaults/deploy/action权限的角色都可以使用。默认情况下,这些角色是所有者和贡献者角色,但你也可以创建包含此操作的自定义角色。

变量

变量部分用于指定在整个模板中将使用的一个或多个值。一种常见的方法是在变量部分根据一个名为environmentName的单一参数构建所有资源的名称。这确保了资源在不同环境中具有相同的名称。变量还用于指定无法从模板外部指定的值,但应视为可配置的。例如,可能会是这样的:

{
  "appServicePlanType": "B1",
  "appServiceName": "[concat('myAppService-', parameters('environmentName'))]"
}

请注意,appServiceName的示例包含了在后面名为函数的部分中详细讨论的函数。

资源

任何 ARM 模板中的第三部分是资源部分。这是模板的主要部分,所有要创建的资源都在此部分指定。这个部分是唯一一个不是对象,而是数组的部分。在该数组内,会指定一个或多个以下形式的对象:

{
    "type": "Microsoft.Sql/servers",
    "apiVersion": "2015-05-01-preview",
    "name": "mySqlServer",
    "location": "West Europe",
    "properties": {
        "administratorLogin": "myUsername",
        "administratorLoginPassword": "myPassword",
        "version": "12.0"
    }
}

每个资源都以对象的形式指定。前四个属性是每种资源类型必需的:

  • 需要指定要创建或更新的资源类型:这通常由resourceprovider的名称后跟一个斜杠,再加上该resourceprovider下属于的资源类型名称组成。

  • 用于此资源的 API 版本:可以从参考中获取受支持的 API 版本列表。

  • 资源的名称:每个资源类型都有自己的规则来确定有效名称的标准。这些规则也可以在参考文献中找到。

  • 创建资源的 Azure 区域:这必须是有效的 Azure 区域。

对象上的其他属性根据资源类型不同而不同,所有这些属性都在资源中指定。

依赖资源

一种特殊类型的资源是依赖资源。例如,SQL 数据库托管在 SQL Server 上,Service Bus Topics 位于 Service Bus 命名空间中。对于嵌套资源类型,类型和名称反映了这种嵌套关系:

{
    "apiVersion": "2017-04-01",
    "name": "myNamespaceName/myTopicName",
    "type": "Microsoft.ServiceBus/namespaces/topics",
    "dependsOn": [
        "Microsoft.ServiceBus/namespaces/myNamespaceName"
    ]
}

除了嵌套类型和名称外,额外的属性dependsOn也是必需的,用于指定此嵌套资源只能在包含资源存在后创建。位置属性不是必需的,因为它将从包含资源中继承。

嵌套模板

第二种特殊类型的资源是模板部署。通过这种方式,一个模板可以触发另一个模板的部署。以下是将模板部署定义为模板中的资源的示例:

{
    "type": "Microsoft.Resources/deployments",
    "apiVersion": "2018-05-01",
    "name": "linkedTemplate",
    "properties": {
        "mode": "Incremental",
        "templateLink": {
            "uri":"https://.../myLinkedTemplate.json"
        },
        "parametersLink": {
            "uri":"https://.../myParameters.json"
        }
    }
}

模板和参数文件的位置可以通过 HTTP 和 HTTPS 来指定,但必须是公开可访问的位置。作为替代方案,也可以指定一个单一的属性模板。该模板应包含一个完整的 JSON 对象作为模板。

输出

模板的第四个也是最后一个部分是输出部分。这里包含了返回给模板调用者的键。调用者可以使用这些值来启动另一个任务或脚本,并使用模板创建或使用的一个或多个值。

这个部分的主要用途是防止在下游自动化中硬编码名称。输出部分是一个 JSON 对象,格式如下:

{
    "outputName":
   {
        "type": "string",
        "value": "myValue"
    }
}

在指定输出时,可以使用与参数相同的类型。当然,硬编码值没有太大意义,因此使用函数从参数、变量甚至是创建的资源中获取值。

函数

函数用于允许在 ARM 模板中动态评估属性。调用函数的语法与许多编程语言非常相似:functionName(arg1, arg2, …) 函数可以返回一个值,如stringint,也可以返回一个对象或数组。当返回一个对象时,可以使用.propertyName符号访问任何属性。访问数组中的元素可以使用[position]。为了标明字符串的哪些部分应作为函数进行评估,可以将其放入括号中:

"myVariable": "[concat('myAppService-', parameters('environmentName'))]"

上面的示例展示了两个示例函数。首先,调用concat函数来连接两个字符串值。一个是硬编码的,另一个是第二个函数调用的结果,用于获取模板参数的值。

有相当多的可用函数。它们可以用于字符串操作、获取当前订阅、资源组或 Azure Active Directory 租户的详细信息,或获取资源的详细信息。

函数还可以用于获取帐户密钥或其他机密。通常,这样做是为了直接从暴露密钥的服务将密钥自动插入应用程序设置或密钥保管库中。这完全消除了手动传输机密的需求。

到目前为止,我们已经学习了组成 ARM 模板的不同部分,你应该能够自己编写这些模板。接下来,我们将学习如何利用各种工具来实际部署它们。

部署 ARM 模板

一旦 ARM 模板及其附带的参数文件编写完成,就可以将其应用于 Azure 环境。PowerShell Cmdlet 和 Azure CLI 命令可用于从脚本环境应用 ARM 模板。当 ARM 模板用于应用程序的基础设施时,Azure Pipelines 可用于部署不仅仅是代码,还可以是 ARM 模板。

无论使用哪种部署方法,所有方法都将有一个部署模式。这可以是增量模式或完全模式。在增量模式下,模板中指定的所有资源将在 Azure 中创建,或者如果资源已经存在,则更新其属性。在完全模式下,所有模板中未指定且已存在于 Azure 中的资源也将被删除。默认部署模式为增量模式。

在接下来的部分中,我们将讨论几种用于执行部署的工具,首先从 PowerShell 开始。

PowerShell

对于在本地机器上进行 ARM 模板的本地开发和测试,PowerShell 提供了一个快速命令来将 ARM 模板应用于资源组:

New-AzResourceGroupDeployment -ResourceGroupName myResourceGroup -TemplateFile "c:\my\template.json" ` -TemplateParameterFile "c:\my\parameters.json"

上述命令将获取指定的模板和参数文件,并将其应用于指定的资源组。此命令假设当前会话已经登录到 Azure。

有几种可用的命令变体:

  • 有一个名为-Mode的参数,具有CompleteIncremental值。可以用来指定deploymentmode

  • 如果未指定参数文件并且模板需要参数,则该命令将提示在命令行上输入这些值。

  • 作为替代,可以使用-TemplateUri-TemplateParametersUri选项来指定从另一个位置检索模板和参数的位置。

接下来我们将讨论的工具是 Azure CLI。

Azure CLI

Azure CLI 是另一种从命令行部署 ARM 模板的方法。CLI 的好处在于它是完全跨平台的,可以在 Windows、macOS 和 Linux 上运行。用于部署 ARM 模板的 Azure CLI 命令如下:

az group deployment create –resource-group myResourceGroup –template-file "c:\my\template.json" –parameters "c:\my\parameters.json"

PowerShell 中可用的所有其他选项在 CLI 中也都可以使用。

Azure Pipelines

部署 ARM 模板的第三种机制是通过 Azure pipeline。这对于部署应用程序的基础设施和配置以及二进制文件特别有用。要从 pipeline 部署 ARM 模板部署,需要配置至少一个 Azure Resource Manager 的服务连接。完成此配置后,可以按照以下截图配置 pipeline:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/42359bf4-5736-4d18-9c5a-ae9f83e6bd0a.png

在此示例中,有两个 ARM 模板的部署,围绕着应用程序代码的部署展开。第一个部署是增量类型,而第二个部署是完整类型。使用这种方法,第一个部署将创建新版本应用程序所需的所有新基础设施。该部署以增量模式进行,因此模板中不再存在,但当前已部署版本的应用程序仍在使用的基础设施将不会被删除。第二个部署将在新版本代码部署后负责删除这些元素。

反向工程模板

从头开始编写 ARM 模板可能是一项繁琐且耗时的任务。幸运的是,有两种方法可以从现有基础设施生成 ARM 模板:

  • 使用导出模板

  • 使用资源浏览器

在接下来的子章节中,我们将讨论这两种方法。

使用导出模板

第一种方法是使用在 Azure 门户中每个资源和资源组上都能找到的“导出模板”选项。这将生成资源(组)当前状态的 ARM 模板,如下截图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/aeb90981-6db5-4157-8cc0-73b0343b28c5.png

请注意,并非所有服务目前都支持使用此方法进行反向工程提取 ARM 模板。对于任何不支持的服务,屏幕顶部会显示警告。为了绕过这一限制并提取单个资源的 JSON 模板,还有另一种方法,这是我们接下来的讨论主题。

使用资源浏览器

为了提取单个资源的 JSON 模板,我们可以使用资源浏览器*.* 资源浏览器如图所示,并且可以通过 Azure 门户中的菜单 (1) 找到:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/3d56d466-61da-447d-9993-0da540143b9b.png

打开资源浏览器后,会打开两个新的窗格。左侧窗格可以用来浏览订阅,并逐层深入资源组,直到找到单个资源。每当选择一个元素时,相应的 JSON 将显示在右侧。在上述示例中,显示的是硬盘的 JSON。这个 JSON 与可以在 ARM 模板的资源数组中使用的 JSON 是一样的。

订阅级模板

到目前为止,关于 ARM 模板的讨论都集中在资源组部署的 ARM 模板上。模板描述了一个或多个部署到资源组的资源。此外,还有订阅级模板。以下是一个资源组的 ARM 模板示例:

{
    "$schema": "https://schema.management.azure.com/schemas/2018-05-01
                                                ... /subscriptionDeploymentTemplate.json#",
    "contentVersion": "1.0.0.1",
    "parameters": { },
    "variables": { },
    "resources": [
        {
            "type": "Microsoft.Resources/resourceGroups",
            "apiVersion": "2018-05-01",
            "location": "West Europe",
            "name": "myResourceGroup",
            "properties": {}
        }
    ],
    "outputs": {}
}

订阅模板的格式与资源组的格式完全相同。不同之处在于 $schema,它指向另一个模式位置,以及所支持的资源类型。订阅模板不支持直接创建资源,仅支持创建资源组、启动模板部署、创建和分配 Azure 策略以及创建角色分配。

Azure 蓝图

除了订阅级别模板,还有另一种可用的选项:Azure 蓝图。蓝图可以用来描述 Azure 订阅的期望状态,并将其应用于现有订阅。

使用蓝图可以完成的所有任务,如今也可以通过 ARM 模板来实现。然而,反过来就不成立。Azure 蓝图仅支持以下称为工件的构造:

  • 策略分配

  • 角色(RBAC)分配

  • 资源组创建

  • 订阅或资源组级别的嵌套 ARM 模板

这些是构建 Azure 订阅默认布局或蓝图所需的所有元素。

蓝图和 ARM 模板之间有一些关键差异:

  • 蓝图存储在 Azure 内部。蓝图是你可以在门户中创建并导航的资源。创建体验也在门户中,而不是在本地计算机上的文本文件中。

  • 订阅与用于创建它的蓝图之间的关系会一直保留,即使部署完成后也是如此。

  • 通过将蓝图分配给订阅,可以将该分配标记为锁定。如果这样做,通过蓝图部署的所有资源都不能被删除或编辑,只要蓝图仍然被分配——即使是应用该蓝图的订阅的所有者也无法进行操作。

  • 提供了许多内置的蓝图,可以用来实施来自 ISO、NIST 或 HIPAA 等知名标准的控制措施。

一般建议在创建多个新订阅时使用蓝图,这些订阅应该遵循相同的布局,而在其他情况下使用 ARM 模板。蓝图在撰写时仍处于预览阶段。

2019 年 11 月更新

自 2019 年 11 月以来,ARM 模板不再必须是纯 JSON 格式。现在允许使用其他几种构造方式,以便更方便地使用 ARM 模板。

要对一行的其余部分进行注释,可以使用 //,或者使用 /* */ 符号来注释一个块。这使得以下片段在 ARM 模板中都是有效的:

{
  “appServiceName”: {
    // this is a single line comment
    “type”: “string” 
      /*
       This is a multi-line comment
     */
    }*}*

另一个与 JSON 的偏离之处在于,ARM 模板允许多行字符串。在使用 Azure CLI 时,必须通过指定--handle-extended-json-format开关来启用此功能。为了使用这些及其他新功能,必须从模板中引用一个新的 JSON 架构。该架构为schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#

此外,还引入了一个新命令来显示应用模板时将进行的更改。此命令仍在预览阶段,名为New-AzDeploymentWhatIf。文档链接将在本章结束时提供。

虽然 ARM 模板是 Azure 中管理基础架构的首选方法,但在某些情况下,它可能不适用。在这些情况下,Azure 自动化可以作为一种替代方案。Azure 自动化将在下一节中讨论。

使用 Azure 自动化

Azure 自动化是 Azure 中的一项服务,旨在帮助用户创建、管理、部署和维护他们的 Azure 资源。Azure 自动化包含多个概念,这些概念去除了这些操作中的一些复杂性和低级细节。Azure 自动化允许以运行簿的形式制定工作流。这些运行簿可以代表用户执行 Azure 资源的操作。

自动化账户资源

在 Azure 自动化账户中,有多个资源使其不仅仅是一个脚本引擎。这些资源在自动化账户级别上共享,因此可以在多个运行簿中重用。

运行身份账户

这些结构中的第一个是*运行身份(Run As)*账户。此账户是一个服务主体,将在与包含自动化账户的 Azure 订阅关联的 Azure Active Directory 中创建。用于身份验证的服务主体凭据会在自动化账户中安全存储。这些凭据无法检索。该服务主体还会作为贡献者添加到 Azure 订阅中。因此,现在可以设置运行簿,以该账户身份执行。

在创建自动化账户时,可以自动创建运行身份账户。

时间表

自动化工作流的常见方式是安排它们在特定的日期和时间运行,或者按固定的时间间隔运行。可以创建共享计划并在运行簿中重用,而无需为每个工作流指定一个时间表。要创建一个新的计划,首先,打开所有计划的列表。然后,可以添加一个新计划,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/a728d578-1ff0-47b6-aa70-32ecbd5baf63.png

一个计划有一个名称和描述。这些值仅供与计划交互的用户使用。接下来,可以配置一个开始日期和时间,并可以选择性地设置一个重复间隔。如果指定了重复间隔,还可以设置一个到期日期和时间。一旦计划创建完成,就可以用于运行手册。

模块

在 Azure 自动化中使用的运行手册是用 PowerShell 编写的。PowerShell 拥有一个功能非常丰富的模块生态系统,其中包含可以使用的预定义功能。要在自动化帐户中使用 PowerShell 模块,只能使用已上传到模块部分的模块。这样做的一个主要好处是,可以固定模块的版本。这样可以确保脚本在依赖项更新的情况下仍能正常工作,而不会中断。

用于与 Azure 交互的 PowerShell 模块默认安装在每个自动化帐户中。此外,管理员还可以添加更多模块,升级或删除现有模块。

变量

在运行手册中,可能会涉及很多变量,例如资源组名称、虚拟机名称、启动或关闭时间等。将这些值硬编码在脚本中并不是一种好的做法,但将它们与运行手册一起存储也有缺点。例如,如果同一个虚拟机有三个运行手册,这意味着一些变量值(例如资源组名称和虚拟机名称)将至少重复三次。为了避免这种情况,可以在自动化帐户级别存储变量值,并且这些值可以在该帐户下执行的每个运行手册中重复使用。

一旦设置了变量,就可以使用以下命令从运行手册中访问它:

$exampleVar = Get-AutomationVariable -Name 'ExampleVar'

除了在运行手册中读取和使用变量外,还可以从运行手册内部更新这些变量:

Set-AutomationVariable -name 'ExampleVar' -value 'ExampleValue'

尽管是一个非常强大的功能,但从运行手册中更新变量可能会产生意外后果。如果一个变量值在多个运行手册中被使用,并且其中一个运行手册更新了该变量值,这可能会导致其他运行手册出现问题。因此,跟踪哪些变量是只读的,哪些是可写的非常重要。

凭证

一种特殊类型的变量是凭证。凭证包含两个值:用户名和密码。凭证在使用的地方都被视为机密。这意味着它们不会出现在日志中,并且必须使用特定的 PowerShell 语法进行检索:

$myCredential = Get-AutomationPSCredential -Name 'MyCredential'

执行此命令后,myCredential 对象可以用来检索用户名和密码。

连接

在运行手册中连接一个或多个外部服务是一个非常常见的场景。一个常见的例子是用于管理 Azure 中所有资源的 Azure 资源管理器。为了避免在运行手册中存储一系列变量并构建相应的连接,自动化账户允许事先创建一个或多个连接。

在大多数情况下,不需要手动创建连接,因为它们会与 Run As 账户一起提供。

一旦所有共享资源到位,就可以开始编写一个或多个运行手册,这是我们接下来要讨论的主题。

运行手册

支持多种类型的运行手册:PowerShell、Python 2 和图形化的。前两种允许用指定语言编写脚本,图形化运行手册则允许通过拖放的方式,从所有上传的 PowerShell 模块、资产和现有的运行手册中组合一个运行手册。

除了这三种基本类型的运行手册外,还有 PowerShell 工作流和图形工作流类型可用。常规运行手册和工作流运行手册的区别在于,工作流运行手册还支持并行处理。PowerShell 工作流的另一个优点是它支持使用检查点,这允许在执行过程中遇到异常时,可以从中断处恢复脚本。

运行手册执行

运行手册编写完成后,有多种方式可以执行它:

  • 手动:任何运行手册都可以在任何时候通过在 Azure 门户中打开并点击“开始”按钮来运行。当然,也可以使用 PowerShell 或 Azure CLI 执行这些操作。

  • 通过附加 Webhook:一旦运行手册发布,就可以生成一个或多个 Webhook 来执行运行手册。每个 Webhook 可以启用或禁用,或设置过期日期。这些工具允许为每个运行手册的用户生成新的 Webhook,并在将来如果不再授予某个用户访问权限时,进行细粒度的控制。

  • 按计划:已发布的运行手册可以附加到一个或多个共享的计划中。能够附加到多个计划意味着可以轻松地为典型的重复事件(如每小时、每日或每周一)预先创建一系列计划,并将这些计划重用或组合到适当的运行手册中。

从 Webhook 或按计划执行运行手册时,仍然可以手动运行该手册。

作业

每次执行运行手册时,都会在作业日志中创建一个新条目。该日志将记录每次运行手册执行的条目,无论执行是如何启动的。每个条目都将包含运行开始的日期和时间、是否有错误,并且包含完整的执行日志。

运行手册画廊

自动化脚本是自动化常见任务的一个好方法。当然,也有一些任务仅适用于特定客户,但也有许多任务适用于所有 Azure 客户。例如,自动化虚拟机每周一早上 8 点启动,或者每天早上自动扩展数据库并在晚上自动缩减。

对于这些常见场景,Azure 提供了一个自动化脚本库,该库在每个自动化账户中都可以启用。在这个库中,可以浏览和搜索成百上千个预制的自动化脚本。一旦找到合适的脚本,可以将其直接导入账户作为自动化脚本。

除了在设定的时间间隔执行脚本或通过 Webhook 触发执行外,Azure 自动化还可以作为 PowerShell DSC 拉取服务器使用。接下来我们将讨论这一点。

PowerShell DSC

PowerShell DSC 是一种用于指定服务器配置的概念。该配置存储在拉取服务器上,虚拟机可以访问该服务器。这些虚拟机会在指定的时间间隔检查该服务器以获取最新的 DSC 配置,并自我更新以符合该配置。

PowerShell DSC 是对 PowerShell 语言规范的扩展,用于编写期望的状态配置。配置使得可以指定一个或多个节点的期望状态。节点指定要配置的服务器或服务器集。节点的配置以一个或多个资源的形式编写。以下是一个配置示例:

configuration ServerFarmConfig
{
     Node FrontEndServer
     {
         WindowsFeature IIS
         {
             Ensure = 'Present'
             Name = 'Web-Server'
             IncludeAllSubFeature = $true
         }

         File LogDirectory
         {
            Type = 'Directory'
            DestinationPath = 'C:\logs’
            Ensure = "Present"
        }
     }
}

在这个示例中,描述了一个只有单一类型服务器的服务器群配置。该服务器包含两个资源,第一个资源是类型为 WindowsFeature,名称为 IIS,确保 IIS 和所有子特性被安装。第二个资源是类型为 File,确保 c:\logs 目录存在。IISFile 等资源类型是 PowerShell DSC 规范中内置的。所有资源的完整参考文档可以在线查阅,链接会在本章结尾提供。

编译并应用 PowerShell DSC

PowerShell DSC 文件通常以纯文本形式保存在 .ps1 文件中。这些文件可以编译成 MOF 文件。然后,可以将这些 MOF 文件推送到一个或多个服务器,以将服务器的状态更新为 MOF 文件中描述的状态。这种方式称为推送模式

除了推送模式,还有另一种部署 MOF 文件的模型。这种模式称为拉取模式。在拉取模式下,MOF 文件不会直接推送到单独的服务器,而是存储在一个名为拉取服务器的中央服务器上。通过这种方式,拉取服务器能够完整记录所有配置和配置中的节点定义。

一旦拉取服务器启动并运行,单独的服务器将配置为在固定的时间间隔获取其 DSC 配置并应用该配置。应用配置意味着,对于每个定义的资源,将实施所描述的状态。如果实际状态已经与期望的状态匹配,则可以什么都不做,或者通过运行命令来实现期望的状态。在此过程中,所有以前的更改——即使是管理员的更改——如果需要,也会被恢复。

使用 Powershell DSC 与 Azure 自动化

Azure Automation 具有内置的 PowerShell DSC 功能,并可以充当一个或多个虚拟机的拉取服务器角色。

要开始使用内置的拉取服务器功能,将一个或多个配置文件上传到自动化帐户。这是通过以下截图中显示的状态配置视图完成的。现在,完成以下步骤:

  1. 点击左侧菜单选项打开。

  2. 在顶部的标签栏中选择配置:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/e2748a78-ee4d-4f5a-ab2d-cd0a6e90cfe4.png

  1. 打开所有配置的概述后,可以使用“添加”按钮添加新配置。在topHere中,可以选择一个本地的ps1文件,并将其添加到列表中。列表中的任何有效配置都可以点击并在原地进行编译。

  2. 现在,配置也将在带有编译配置的标签中显示,并且可以应用到一个或多个虚拟机。

  3. 一旦编译的配置可用,“Nodes” 标签可以用于将一个或多个虚拟机从订阅中添加到配置节点。

  4. 在显示此标签时,点击“添加”按钮会打开如下所示的视图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/93411ab5-71a1-48f7-947f-b1f7f46a1848.png

  1. 在此视图中,可以选择一个虚拟机,并将所选配置应用到该虚拟机。

  2. 那台机器上的本地配置管理器将被配置为在固定的时间间隔刷新配置。

  3. 每当配置被刷新时,它将重新应用到服务器。

Azure Automation 使用户能够管理虚拟机,例如应用程序配置。在使用 PaaS 服务时,不能使用 PowerShell DSC 等技术,必须使用其他技术来管理应用程序设置。这些将在下一节中讨论。

管理应用程序设置

应用程序的基础设施的另一部分是应用程序配置。在本节中,讨论了存储和加载 Azure 应用服务的应用程序配置的多种方法。包括以下内容:

  • 将配置存储在应用程序设置中

  • 使用托管身份和密钥库的组合

  • 使用 Azure 应用配置服务

第一种方法的缺点是,任何具有管理员(读取)访问权限的用户都可以读取应用程序服务中的应用程序设置。接下来的两种方法没有这个缺点。

来自 ARM 模板的 Azure 应用服务设置

配置应用程序设置为代码的第一种方法是通过在 ARM 模板中将应用设置指定为资源。这应该作为嵌套资源进行指定。可以按照以下屏幕截图所示进行操作:

{
    "name": "[concat(variables(‘websiteName’), ‘/appsettings’)]",
    "type": "config",
    "apiVersion": "2015-08-01",
    "dependsOn": [
        "[concat('Microsoft.Web/sites/', variables('webSiteName'))]"
    ],
    "properties": {
        "key1": " [listKeys(parameters('storagename'), '2018-02-01').keys[0].value]",
        "key2": "value2"
    }
}

listKeys函数在这些场景中特别有用。它允许将机密直接从任何服务复制到应用程序设置,而无需将其存储在任何中间解决方案中。对于非 Azure 来源的机密,应使用模板参数。

ARM 模板中指定的配置对应于在门户中找到的应用服务配置。这些设置用于覆盖appsettings.jsonappsettings.config文件中的相应条目。更新此配置将自动重新加载应用程序。

这种方法的缺点是,以这种方式存储的机密可以通过 Azure 门户查看。任何具有读取访问权限的用户都可以检索所有以这种方式存储的机密。

从密钥库在运行时加载设置

存储应用服务设置的下一个可能位置是 Azure 密钥库,应用程序在运行时从中加载设置。为了实现这一点,必须做到以下几点。

为了能够授权应用程序访问密钥库,应用程序必须首先能够通过Azure Active DirectoryAAD)进行身份验证。当然,可以手动注册一个服务主体,但这会返回一个用户名和密码,必须将其存储在某个地方。用户名和密码是机密,但不能存储在密钥库中,因为它们用于访问密钥库。如何确保密钥安全的问题可以通过使用 Azure 的Managed Identity功能来解决。

安全地存储机密,但访问时返回另一个机密的问题通常被称为turtles all the way down问题。这是一个古老的轶事,章节末尾提供了相关链接。

启用 Azure 托管身份的应用服务,Azure 会自动生成一个服务主体,该主体具有不可检索的用户名和密码。仅在运行时,应用程序才能通过特定代码将自己认证为该主体。Azure 将确保此操作仅适用于运行在属于该托管身份的应用服务中的代码。

现在,应用程序可以拥有自己的身份,必须授予该身份对密钥库的访问权限。这可以通过在 ARM 模板中的密钥库描述中完成,使用以下语法:

{
    "type": "Microsoft.KeyVault/vaults",
    "name": "[parameters('keyVaultName')]",
    "apiVersion": "2015-06-01",
    "location": "[resourceGroup().location]",
    "dependsOn": [
        "[resourceId('Microsoft.Web/sites/', parameters('appServiceName'))]"
    ],
    "properties": {
        "enabledForTemplateDeployment": false,
        "tenantId": "[subscription().tenantId]",
        "accessPolicies": [
          {
            "tenantId": "[subscription().tenantId]",
            "objectId": [reference(concat(resourceId('Microsoft.Web/sites',parameters('appServiceName')),
                    [line continued] '/providers/Microsoft.ManagedIdentity/Idntities/default'), 
                    [line continued] '2015-08-31-preview').principalId]",
            "permissions": {
              "secrets": [ "get", "list" ]
            }
          }
        ],
        "sku": {
          "name": "standard",
          "family": "A"
        }
    }
}

在此示例中,reference()函数用于检索托管身份的信息,并利用这些信息在密钥库上创建访问策略。

最后,设置好密钥保管库及其访问权限后,应用程序必须在启动时检索内容。为此,可以使用配置构建器。它们是在.NET Core 2.0(和.NET Framework 4.7.1)中引入的,并在StartUp类中使用,如以下代码片段所示:

var tokenProvider = new AzureServiceTokenProvider();
var kvClient = new KeyVaultClient((authority, resource, scope) =>
tokenProvider.KeyVaultTokenCallback(authority, resource, scope));

var configurationBuilder = new ConfigurationBuilder().AddAzureKeyVault(
    $"https://{ Configuration["keyVaultName"]}.vault.azure.net/",
    kvClient, 
    new DefaultKeyVaultSecretManager());

Configuration = configurationBuilder.Build();

该代码示例中的所有类型都可以在 NuGet 包Microsoft.Configuration.ConfigurationBuilders.Azure中找到。

Azure 应用程序配置

存储应用程序配置的另一个位置是 Azure 应用程序配置。这是一个新服务,截至本文写作时仍处于预览阶段。应用程序配置允许创建一个可以作为配置使用的键值对中央注册表,这个注册表可以被多个应用程序使用。

应用程序配置是另一种可以通过门户创建的资源。其主要组件是配置资源管理器,如以下截图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/02417fea-c92b-4913-bea5-1998182362cd.png

除了配置资源管理器外,还有一个密钥部分,用于检索应用程序可以用来读取配置的访问密钥。还有选项可以查看配置的最近更改、恢复早期版本,以及导入或导出所有配置设置。

在创建应用程序配置资源并添加配置键后,可以通过使用IConfiguration框架类型的扩展方法在应用程序中检索它们:

config.AddAzureAppConfiguration(settings["ConnectionStrings:AppConfig"]);

从应用程序配置加载设置的加载器是 NuGet 包Microsoft.Azure.AppConfiguration.AspNetCore的一部分。

与将设置存储在 Azure 密钥保管库相比,应用程序配置有两个缺点:

  • 首先,应用程序需要通过连接字符串配置到应用程序配置,并在应用设置中存储至少一个新的密钥。

  • 其次,应用程序配置没有像密钥保管库那样严格的访问控制选项。因此,根据配置值的类型,将配置分布在应用程序配置和密钥保管库中可能更为合理。

这部分内容结束了我们对 Azure 和 Azure DevOps 在基础设施即代码方面的讨论。接下来的部分将讨论一系列其他提供类似功能的工具。

其他工具

还有许多其他工具可以通过代码管理基础设施和配置。在前述的原生 Azure 和 Windows 选项之外,还有许多广泛使用的替代工具,其中一些在本节中列出。了解哪些工具适用于哪些场景,并如何与它们集成是很重要的。

CloudFormation

CloudFormation 是 AWS 云的基础设施即代码(IaC)语言。CloudFormation 模板可以用 JSON 或 YAML 格式编写。创建一个可以公开读取的 AWS S3 存储桶的示例如下所示:

Resources:
 HelloBucket:
 Type: AWS::S3::Bucket
 Properties:
 AccessControl: PublicRead

有一个扩展可以让你从 Azure DevOps 执行 AWS 上的 CloudFormation 模板。这个扩展提供了创建、更新或删除 AWS 堆栈的任务。堆栈的功能类似于 Azure 中的资源组,任务则类似于应用 ARM 模板的任务。

Chef

Chef 是一个用于配置即代码(CaC)的工具,支持描述和执行服务器配置。Chef 使用一个集中式服务器,Chef Server,该服务器保存所有服务器的配置。在这里,为每个服务器确定正确的期望状态,然后由 Chef Client 拉取,该客户端是一个在受管理节点上运行的代理。

为服务器定义期望状态是通过一系列构造来完成的。最低级别是配方(recipe)。配方包含一个或多个资源,资源是可用的内置功能。例如,execute 资源可以执行一个 bash 命令。另一个资源是 apt_update,它提供与 apt 包管理器交互的方式。一个或多个配方可以组合成烹饪书(cookbooks),描述可以分配给节点的能力。一个或多个烹饪书可以通过运行列表(run list)分配给节点。运行列表包含必须应用到节点的所有烹饪书。

与 Chef Server 的交互是通过一个名为 knife 的命令行工具进行的。

尽管术语完全不同,但 PowerShell DSC 和 Chef 之间存在许多概念上的相似之处。

Puppet

Puppet 是一个部署和配置管理工具,采用服务器-客户端模型。它有一个集中式服务器,称为 Puppet Master,负责接收所有期望状态描述并将它们编译成一个内部目录,保存每个受管理服务器的期望状态。所有由 Puppet 管理的服务器都需要在本地服务器上安装 Puppet 代理。该代理连接到服务器,拉取它所管理的服务器的状态,并在本地应用该状态。受管理的服务器称为节点(node)。

Puppet 使用的基本构建块叫做 资源(resource)。资源通过指定资源类型和一系列属性来定义。有许多资源类型可用,例如管理用户和已安装的应用程序。资源被分组在一个或多个 类(classes) 中,这些类又被分配给一个或多个节点。

Puppet 可以安装在 Azure 中的任何 Linux 虚拟机上。Azure 市场中也有一个预构建的包含 Puppet Enterprise 的镜像。

Puppet 与 Chef 和 PowerShell DSC 相似。三者都有一个类似的模型来描述期望的状态,它们的目的相同。

Ansible

Ansible 是另一种配置管理工具,主要用于 Linux,但也支持 Windows。Ansible 与其他工具的一个不同之处在于,它没有集中式服务器来托管所有期望的状态,也不使用代理。Ansible 执行的所有命令都是通过 SSH 执行的。

任何服务器都可以启动一个剧本,针对清单中的一个或多个进行部署。Ansible 清单包含所有可以由 Ansible 管理的服务器。这些服务器可以分组为一个或多个组,并且可以嵌套在其他组中。每台服务器和每个组都是清单项。在 Ansible 中,期望的状态是写入剧本中的。剧本是一系列需要在目标服务器上运行的任务或角色。角色是任务的集合。角色旨在在多个剧本中重用,因此应该足够通用,可以在多种情况下使用。角色还应该是幂等的。这意味着角色中的任务应该确保无论运行剧本多少次,结果都是相同的。

Ansible 脚本可以通过命令行工具或封装该工具的 Azure DevOps 扩展来执行。还有其他管理系统可用,如 Ansible Tower,它在 Ansible 命令行工具的基础上提供了图形用户界面。

Terraform

Terraform 是一个多云基础设施管理解决方案。它可与 ARM 模板相比,区别在于它还支持 Amazon Web Services、Google Cloud Platform 以及其他云平台。Terraform 使用自定义文件格式来指定一个或多个资源,这些资源将通过一个或多个提供商来创建。这些资源对应于云资源,而提供商负责知道如何与不同供应商的 API 交互。

您还可以选择使用 JSON 格式,而不是 Terraform 专有格式。Terraform 还支持使用模块来创建可重用的组件包。

Terraform 配置文件是通过命令行界面执行的。

总结

在本章中,您学习了基础设施和配置即代码的概念、它的价值以及如何在实践中使用它。为了实现这些,您学习了 Azure 的 IaC 机制——ARM 模板。您还了解了用于管理虚拟机配置的 PowerShell DSC 以及管理应用程序配置的不同技术。最后,您了解了市场上可用的几种其他工具。您学会了在不同情况下可以使用哪个工具,以及这些工具是否能够与 Azure DevOps 集成。

有了这些知识,你现在能够使用你所学到的一种或多种工具,开始在源代码管理中描述你的应用程序的基础设施和配置。你还可以通过自动化手段设置交付基础设施的方式,无论是通过发布管道还是使用专门的基础设施管理工具。但无论你选择哪种解决方案,你现在已经具备将基础设施融入到 DevOps 流程中的能力。

在下一章中,你将学习在实施 DevOps 实践时可能遇到的另一个挑战:数据库。在提高功能流向生产的速度时,你可能还需要改变管理数据库架构和应用更改的方式。下一章将讨论这个话题。

问题

在本章结束时,以下是一些问题,可以测试你对本章内容的理解。你可以在附录的 评估 部分找到答案:

  1. 判断正误:ARM 模板可用于创建、更新和删除 Azure 资源。

  2. 以下哪项不是 Azure 自动化账户资源?

    1. 模块

    2. 容器

    3. 运行账户

    4. 变量

  3. 判断正误:基础设施作为代码的一个缺点是,你必须将敏感信息作为 ARM 模板参数文件放入源代码管理中。

  4. 判断正误:Azure 自动化账户允许在预定的时间表上执行 Powershell 运行簿。

  5. 使用基础设施作为代码有什么好处?

深入阅读

第七章:在 DevOps 场景中处理数据库

在前几章中,您已经学习了软件的持续集成和持续部署。您还学到了如何将相同的原则应用于基础设施配置的交付。一旦您采用了这些原则并开始增加价值交付的流动,您可能会遇到另一个挑战:管理数据库模式的变更。

将 DevOps 应用于数据库可能感觉像是在开动的汽车上更换轮胎。你必须找到某种方法,在不让系统停机维护的情况下,协调数据库模式和应用程序代码之间的更改。

在本章中,您将了解几种不同的方法来实现这一点:管理这些模式变更并避免停机。通过适当的规划和严格的方法,可以以良好的风险管理方式实现这一目标。您将看到如何将数据库模式视为代码,并将了解可用的不同方法。您还将看到另一种完全避免使用数据库模式的方法,即无模式化。

本章将涵盖以下主题:

  • 将数据库模式作为代码进行管理

  • 应用数据库模式更改

  • 无模式化

  • 其他方法和问题

技术要求

为了实践本章中阐述的思想,您需要安装以下工具:

  • 一个安装了 Entity Framework Core NuGet 包的应用程序

  • 配备 SQL Server 数据工具的 Visual Studio

  • 访问 Azure Pipelines

  • 一个 Azure 订阅,用于访问 Cosmos DB

将数据库模式作为代码进行管理

对于那些熟悉通过应用程序代码与关系数据库交互的用户来说,很可能他们已经在使用对象关系映射器ORM)。ORM 的出现是为了填补面向对象编程语言与使用表格的关系数据库模式之间的阻抗不匹配。知名的例子有 Entity Framework 和 NHibernate。

ORM 提供了一个抽象层,允许从数据库中存储和检索对象,而不必担心底层的表结构。为了自动将对象映射到表,或反向操作,ORM 通常内建有描述数据库模式、相应对象模型及其之间映射的功能,这些通常使用标记语言来实现。大多数情况下,这些内容不需要手动编写。它们通常可以从对象模型或现有数据库中生成,并且它们之间的映射通常通过约定生成或在可视化编辑器中绘制。

虽然这些方法允许当前的数据库模式以代码的形式定义,但单靠这些方法尚不足以应对模式变化。为了将模式变化作为代码进行处理,有两种常见的方法。一种是在代码中描述每一个变化;另一种只在代码中描述最新版本的模式。这些方法分别被称为基于迁移和基于状态的方法。两者都可以依赖第三方工具,将这些变化应用到数据库中。

迁移

第一种方法基于保持一组必须应用于数据库的有序变更。这些变更通常被称为migrations,可以由工具生成,如 Microsoft Entity Framework、Redgate SQL Change Automation,或者可以手动编写。

工具可以基于当前数据库模式与源代码控制中新的模式定义的比较,自动生成迁移脚本。这被称为脚手架生成。工具生成的脚本并不总是完美的,它们可以通过程序员所掌握的领域知识进行改进,但工具本身没有这些知识。一旦一个或多个新的迁移被脚手架生成或手动编写,它们就可以通过所选工具应用到数据库中。下面是一个展示这一过程的图示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/f7b921f8-9d80-4f1c-a2f4-18aabca2489b.png

在这里,我们可以看到一系列不断增长的迁移(从 m1 到 m4),用于描述数据库的增量变化。为了将数据库更新到最新版本,需要确定已应用的最新迁移,并依次添加其后所有的迁移。

在手动编辑迁移脚本时,必须记住以下几点:

  • 迁移脚本应当是有序的。迁移描述了需要执行的 SQL 语句,以便将数据库从版本x迁移到版本x+1。只有当这一过程完成后,才能开始下一个迁移。

  • 一个迁移脚本不仅应迁移模式,还应迁移数据。这可能意味着需要一些中间步骤。例如,将两列数据移动到另一个表通常意味着先创建新的列,然后将旧列的数据填充到新列中,最后才删除旧列。

  • 建议在迁移脚本中包含所有数据库对象。额外的索引和约束不应仅应用于生产数据库,还应应用于测试环境。使用迁移时,已经有机制可以将这些内容从源代码控制传送过去。将它们包含在相同的迁移脚本中,也确保了索引和约束按相同的顺序应用,避免了它们只存在于生产环境中而意外阻塞迁移的情况。

  • 如果可能,迁移脚本应保持幂等性。如果出现问题或怀疑出现问题,能够重新执行最后一个迁移是确保其完全应用的一个好方法。

这种方法的一个缺点是,生成和应用迁移脚本时对顺序的严格要求。这使得将这种方法集成到依赖分支使用的开发工作流中变得困难。不同分支中创建的迁移在合并后可能会破坏迁移的顺序,或者更糟糕的是,合并迁移路径的分裂。例如,假设在现有迁移a之后,在两个不同的分支中创建了两个迁移bc。这两个迁移如何合并?无论是按顺序应用a, b, c 还是 a, c, b 都不正确,因为bc都是在a之后直接执行的。修复此类错误的唯一方法是执行以下步骤:

  1. 除了第一个新迁移(例如,在此情况下为c)之外,删除所有其他迁移。

  2. 将所有其他迁移应用于没有应用任何新迁移的数据库;在这种情况下,如果a已经应用,则只需应用b,或者同时应用ab

  3. 为其他迁移生成一个新的迁移;在这种情况下,c的替代。

这种方法的一个优点是,每个单独的模式变更都会以相同的方式部署到数据库中。无论一个或多个迁移是否同时应用于生产数据库,它们仍然会按可预测的顺序一个接一个地执行,并且与它们在测试环境中运行的方式相同,即使它们是逐个应用的。

结束状态

管理模式变更的另一种方法是,不跟踪单个变更(或迁移),而是仅将模式的最新版本存储在源代码管理中。然后使用外部工具将源代码管理中的当前模式与数据库的实际模式进行比较,生成迁移脚本,并在运行时应用这些脚本。迁移脚本不会被存储,并且仅用于一次。

与编写迁移不同,手动执行此类任务是不可行的。虽然手动在源代码管理中跟踪最新版本的模式是可以管理的,但对于结束状态方法则不可行。在比较现有模式和新模式时生成迁移脚本并应用该迁移脚本只能通过工具完成。这些工具的例子包括 Redgate SQL 源代码控制和 SQL Server 数据工具。如何使用这些工具,在此展示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/9bdb8efd-8b7a-43a1-9754-eeaa31b91458.png

在这里,我们看到如何将当前实际的数据库模式与期望的数据库模式的描述进行比较,以生成升级脚本,并直接应用这个脚本以进行必要的更改,使实际模式与期望模式相同。

这种方法的一个优势是,不需要生成一系列必须按特定顺序执行的脚本。因此,这种方法与广泛分支的架构非常契合,尤其适合逐步集成变更的场景。它还消除了手动编写迁移的需求,适用于简单的场景,如添加或删除列、表或索引。

这种方法的缺点是,它使得处理需要数据操作的变更变得更加困难。再举个例子,假设将两列移动到另一个表中。由于工具仅强制执行新的架构,如果没有进一步的干预,这将导致数据丢失。

一种可能的干预方式是向架构包中添加部署前和部署后的脚本。在部署前脚本中,当前数据会被暂存到一个临时表中。然后,在应用新架构后,数据会从临时表复制到新位置,过程在部署后脚本中完成。

本节内容介绍了如何以可存储在源代码管理中的格式管理数据库架构变更。下一节将讨论如何在部署时拾取这些变更,并将其应用到数据库中。

应用数据库架构变更

有了数据库架构,并可选地定义了一系列迁移存储在源代码管理中,接下来就该考虑何时将这些变更应用到数据库架构中。有两种方法可以做到这一点。数据库架构的变更可以在部署新版本的应用程序之前应用,或者由应用程序代码本身来应用。

作为发布的一部分进行升级

应用数据库变更的第一种方法是作为发布流程的一部分。当这种情况发生时,负责读取和执行迁移脚本的工具会通过管道中的一个步骤来调用。

这种调用可以通过 PowerShell 或其他脚本语言中的自定义脚本完成。然而,这种方式容易出错,并且每次工具发生变更时,都有可能需要更新脚本。幸运的是,对于大多数基于迁移的工具,Azure Pipelines 提供了现成的任务,能直接从发布阶段开始执行迁移。

例如,Azure Pipelines 提供了一个扩展,用于直接从定义它们的 dll 文件将 Entity Framework Core 迁移应用到数据库中。此任务可以添加到发布管道中,用于在部署新应用程序代码之前更新数据库。

另一种变体是将应用程序的构建和发布阶段进行分离。在这种情况下,迁移脚本会作为独立的构建产物导出,可以直接从源代码导出(如果是 SQL 编写的),或者通过执行一个工具生成必要的 SQL 脚本作为输出。然后,在发布阶段再次下载该构建产物,并使用 Azure Pipelines 的 SQL 执行任务将其应用到数据库中。

通过应用程序代码进行升级

架构变更可以由应用程序本身而非发布管道来应用。一些带有内置迁移支持的 ORM 具备自动检测数据库架构是否与最新迁移匹配的能力。如果不匹配,它们可以自动将架构迁移到最新版本。

支持此功能的 ORM 之一是 Entity Framework。Entity Framework 的核心版本不内置自动迁移支持。在 Entity Framework Core 中,一行应用程序代码就可以在对应用程序而言方便的时刻启动升级。以下代码片段展示了实现此功能的代码:

using (var context = new MyContext(...))
{
    context.Database.Migrate();
}

这种方法的优点在于启用非常简单。例如,Entity Framework 中的一个布尔开关就可以启用此工作流。然而,缺点是大多数支持此功能的 ORM 会对数据库强制执行全局锁定——在迁移运行时停止所有数据库事务。对于任何需要几秒钟以上的迁移或一组迁移,这种方法可能不可行。

这种方法通常仅用于基于迁移的方法。使用终态方法的方案需要一个外部第三方工具,用于生成所需的迁移脚本并应用它们。通常这会通过发布管道完成,而不是由应用程序本身来处理。

添加过程

如前节所示,考虑如何以及何时应用数据库架构或使用该架构的应用程序(或多个应用程序)的变更是非常重要的。但无论架构变更和代码部署的安排如何,总会有一个时期,其中以下情况之一为真:

  • 新的应用程序代码已经运行,而架构变更尚未应用或正在应用过程中。

  • 在架构变更已应用或正在应用的情况下,旧版应用程序代码仍在运行。

  • 在应用架构变更时,应用程序代码未运行。

第三种情况是非常不希望出现的。这通常是这样,尤其是在实践 DevOps 时。如果频繁发布变更并且在工作时间内进行,频繁因每次架构变更停机是无法接受的。

为了避免在应用架构变更时需要停机,必须满足以下条件之一:

  • 架构变更具有向后兼容性,使得旧版应用程序代码能够在架构变更已经应用或正在应用的数据库上无错误地运行。

  • 新的应用程序代码具有向后兼容性,能够在旧版和新版架构上运行。

满足第一个条件可以确保旧的应用程序代码在应用模式更改时仍然能够运行。满足第二个条件则确保新的应用程序版本可以首先部署,完成后可以在该代码运行时升级数据库。虽然两者都能工作,但通常希望满足第一个条件。原因是模式更改通常会支持应用程序代码的更改。

这意味着,以下过程是安全的,在不发生停机的情况下部署模式更改:

  1. 创建一个新的数据库。

  2. 应用数据库更改。

  3. 验证更改是否已正确应用,或者中止部署管道。

  4. 部署新的应用程序代码。

需要意识到,这个过程假设前向失败。这意味着,如果在部署模式更改时遇到问题,应在继续进行代码更改之前先解决这些问题。

最后,满足模式更改的向后兼容性条件,有时对于某些模式更改来说是无法完成的。如果是这种情况,可以将更改拆分为两个部分更改,两个部分共同完成相同的最终结果,并且都满足向后兼容性的条件。例如,重命名一个属性,或者将存储距离的单位从英尺改为米,可以按以下方式执行:

  1. 生成一个迁移,向数据库表中添加一个新列,存储以米为单位的距离。

  2. 添加一个应用程序代码,从旧列中读取数据,但写入两个列。

  3. 将这些更改部署到生产环境。

  4. 添加一个新的迁移,将旧列中的数据迁移到新列中,对于所有尚未填充新列但旧列已填充的情况。

  5. 更新应用程序代码,使其仅读取和写入新列。

  6. 将这些更改部署到生产环境。

  7. 添加一个新的迁移,移除旧列。

使用正确的工具和适当的过程,可以执行有效且安全的模式更改部署。在下一部分中,将介绍另一种方法,即使用无模式数据库。

放弃使用模式

在前面的部分中,重点是关系型数据库,其中对每个表都应用严格的模式。另一种完全不同的数据库模式管理方法是完全放弃使用数据库模式。这可以通过使用无模式或文档型数据库来实现。一个著名的无模式数据库例子是 Azure Cosmos DB。这些数据库可以将不同形式的文档存储到同一表中。这里所说的“表”是指,因为这些类型的数据库通常不使用“表”这个术语,而是将其称为数据库、容器或集合。

由于这些数据库可以在同一集合中存储不同模式的文档,从数据库的角度来看,模式变更已经不存在。但当然,随着时间的推移,应用程序代码中相应对象的结构会发生变化。要处理这种情况,最好区分存储对象到数据库和从数据库读取对象的过程。

将对象写入数据库

存储在无模式数据库中的文档通常是应用程序代码中对象的序列化。当使用关系型数据库时,这些对象通常通过对象关系映射器ORM)进行存储,比如 Entity Framework、Dapper 或 NHibernate。当使用文档数据库时,这些对象通常被序列化并存储在数据库中。这意味着代码对象定义的变化会导致在保存对象时文档结构的变化。由于文档数据库的特点,这种方式是有效的。

举个例子,考虑以下 C# 类及其序列化到文档数据库后的 JSON 表示:

|

public class Person
{
   [JsonConstructor]
   private Person() {}

   public Person(string name) {
      Name = name ?? throw new ArgumentNullException();
   }

   [JsonProperty]
   public string Name { get; private set; }
}

|

{
   “Name”: “Mark Anderson”
}

|

在这段代码在生产环境中运行一段时间后,成千上万的人员已被保存,一个新的需求出现了。除了记录人员的姓名外,还必须记录他们所在的城市。因此,Person类扩展以包括另一个属性。进行此更改并部署新代码后,每当保存一个人员时,以下代码将被使用,结果是生成如下所示的 JSON:

|

public class Person
{
   [JsonConstructor]
   private Person() {}

   public Person(string name, string city) {
      Name = name ?? throw new ArgumentNullException();
      City = city ?? throw new ArgumentNullException();
   }

   [JsonProperty]
   public string Name { get; private set; }

   [JsonProperty]
   public string City { get; private set; }
}

|

{
   “Name”: “Mark Anderson”,
   “City”: “Amsterdam”
}

|

尽管Person类的定义发生了变化——相应的 JSON 也发生了变化——这两种文档形式仍然可以保存在同一集合中。

这表明,从将信息写入数据库的角度来看,无模式方法非常方便,因为开发人员根本不需要考虑模式变更管理。

从数据库读取对象

尽管无模式数据库使得将不同形式的文档写入同一个集合变得非常容易,但在从该集合中读取文档并进行反序列化时,可能会遇到问题。实际上,模式管理的问题并没有消除,而是被推迟到了稍后的时间点。

继续前面的示例,在新的 C# Person 类定义下反序列化第一个保存的人的时候,city属性将得到一个空值。这可能是意料之外的,因为 C# 代码保证不会构造没有城市的人员。这清楚地展示了无模式数据库带来的挑战。

在这个示例中,可以通过将Person类更新为以下内容来规避该问题:

public class Person
{
   [JsonConstructor]
   private Person() {}

   public Person(string name, string city) {
      Name = name ?? throw new ArgumentNullException();
      City = city ?? throw new ArgumentNullException();
   }

   [JsonProperty]
   public string Name { get; private set; }

   [JsonIgnore]
   private string _city;

   [JsonProperty]
   public string City { 
      get { return _city; }
      private set { _city = value ?? _city = string.Empty}
   }
}

除了这个添加属性的场景外,还有许多其他场景会要求对 C# 类进行适配,以处理反序列化场景。以下是一些示例:

  • 添加原始类型的属性

  • 添加一个复杂属性、另一个对象或数组。

  • 重命名属性。

  • 将原始类型的属性替换为复杂属性。

  • 将可空属性改为非空属性。

向对象添加代码以处理这些情况会增加代码库的大小和复杂性,并将处理过去情况的功能污染主代码库。尤其是在这种情况频繁发生时,这可能会导致代码库中出现不必要的复杂性。为避免这种情况,可能的解决方案是,每当对象的模式发生变化时,按照以下过程操作:

  1. 更改对象的模式,确保只添加属性。即使目标是删除一个属性,在此阶段,只有具有新名称的属性被添加。

  2. 在对象上实现逻辑,以应对旧版本对象的反序列化。

  3. 部署对象的新版本。

  4. 启动一个后台进程,从数据库中逐一加载该类型的所有对象,并将其保存回数据库。

  5. 一旦后台进程处理完所有现有实体,删除负责处理反序列化过程中模式更改的代码,以及任何不再使用的属性。

使用这种方法,所有更改将在一段时间内传播到对象的所有存储版本。该方法的缺点是,对象结构的更改被分成两个必须分别部署的更改。此外,第二次更改的部署必须等待数据库中所有对象都已转换完毕。

其他方法和关注点。

除了前面讨论的更常见的方法,以下的一些提示和方法可能有助于减少处理数据库时的工作量,或帮助降低与数据库更改相关的风险。

最小化数据库的影响。

处理数据库的第一步可以是减少需要进行数据库更改的机会。在许多数据库中,可以编写存储过程——或者其他代码或脚本——在数据库引擎内部执行。虽然存储过程有一些好处,但更改它们也可能算作数据库模式的更改,或者至少会导致难以测试的更改。

一种简单的方法是将存储过程替换为应用程序代码,从而使用功能开关进行更简单的并行更改。

完整的并行部署。

在高风险环境或脆弱的数据库中工作时,也可以采取另一种方法进行数据库模式更改。该方法基于应用功能开关和蓝绿部署模式,具体步骤如下:

  1. 以这样的方式更改应用程序代码,使其将任何更新写入不止一个,而是两个数据库。

  2. 在生产环境中,创建现有数据库的完整副本,并配置应用程序代码以同时将所有更改写入两个数据库。这些数据库将被称为数据库和数据库,之后将使用这些名称。

  3. 仅在写入新数据库的路径中,引入对新数据库架构和应用程序代码的必要更改。

  4. 在所有读取数据的代码路径中引入必要的更改,使得所有查询都在两个数据库上运行。

  5. 更新应用程序代码,检测新数据库和旧数据库之间查询结果的差异,并在发现任何不一致时记录错误。

  6. 如果更改顺利运行,请删除旧数据库,以及应用程序代码中的旧读写访问路径。

  7. 如果更改运行时出现错误,修复问题。接下来,通过恢复目标新数据库的备份来重新启动,并从第五步开始恢复。

这种方法的优点是非常轻量级。缺点是它非常复杂,需要大量的工作,且成本较高。同时,还应考虑额外的数据库成本和备份恢复操作的时间。

测试数据库更改

就像应用程序代码一样,通过测试可以获得关于数据库架构更改质量的见解。关于如何对数据库架构进行测试的链接可以在本章末尾找到。

在大多数情况下,为了全面覆盖数据库更改带来的风险,需要进行系统测试,这些测试会在完整部署的应用程序堆栈上执行。这种类型的测试可以覆盖大部分由错误的架构、无效的存储过程、数据库和应用程序代码不匹配引起的风险。

总结

在本章中,您已经学习了如何通过源代码控制管理数据库架构及其更改。您了解了基于迁移和最终状态的更改存储方法,以及如何以安全的方式将它们应用于生产数据库。

此外,您还学习了无架构数据库如何消除传统架构管理的负担。然而,这也意味着在从数据库读取对象的旧版本时,必须应对架构差异。

在下一章中,您将学习持续测试。您不仅将学习测试技术,还将了解在何时应用哪些测试技术,以及测试如何成为 DevOps 的关键组成部分,是持续向最终用户提供价值的关键驱动因素。

问题

在总结时,以下是一些问题,帮助你测试关于本章内容的知识。你可以在附录中的评估部分找到答案:

  1. 对与错:在使用 Entity Framework 时,架构管理是通过基于迁移的支持内建的。

  2. 对与错:在使用基于迁移的架构管理方法时,您不需要在数据库架构中额外的跟踪表。

  3. 对还是错:在使用基于最终状态的架构管理方法时,你的数据库架构中不需要额外的跟踪表。

  4. 完整的并行数据库架构更改方法有哪些好处?(选择多个答案):

    1. 风险几乎降到零。

    2. 你可以在类似生产环境中衡量更改对性能的实际影响。

    3. 并行迁移减少了周期时间。

  5. 对还是错:无模式数据库完全消除了对架构更改的思考需求。

  6. 你可以做出什么样的技术选择来限制对数据库模式的更改影响?

进一步阅读

第八章:持续测试

在前几章中,您了解了用于帮助提高交付变更到生产环境速度的不同技术。如果您已经在日常工作中使用了这些技术,您会很快发现,只有在工作质量足够高的情况下,这才是可能的。如果您的工作质量不够高,您将面临许多停机或问题,最终用户也不会满意。为了取得成功,提升变更的速度和提高工作质量必须同步进行。要识别和提高工作质量,您首先需要了解什么是质量。这就是测试的重要性。测试是报告软件质量的学科。

本章将通过研究如何衡量软件开发的质量来介绍测试主题。接下来,将探讨功能测试的主题。首先,将介绍测试漏斗和金字塔模型。这些模型可用于确定需要哪些类型的测试,以及每种测试需要多少。之后,将逐一讨论不同类型的测试。您将了解它们如何工作、测试内容以及不同类型测试的优缺点。最后一节将集中讨论如何通过流水线生成并收集的所有指标和测试结果,持续报告团队工作的质量,甚至防止低质量的变更传播到用户。所有这些都将帮助您保持软件的高质量,并使您能够自信地快速且频繁地交付该软件。

本章将涵盖以下主题:

  • 定义质量

  • 了解测试类型

  • 执行功能测试

  • 执行非功能性测试

  • 维护质量

技术要求

为了实验本章中描述的技术,您可能需要以下一种或多种工具:

  • 一个可以访问构建和发布流水线以及仪表板的 Azure DevOps 项目

  • Visual Studio 2019

  • Azure DevOps 的 Basic + Test Plans 许可证

  • 一个 SonarCloud 订阅

所有这些工具都是免费的,或者可以在有限的试用期内免费获得。

定义质量

第一章中讨论的 DevOps 思维模式的一个主要目标是增加向最终用户传递价值的流动。为了实现这一目标,软件必须频繁部署,甚至可能每天多次部署。要使频繁部署成为可能,两个因素至关重要:自动化和质量。自动化在前几章中已被广泛讨论,因此现在是时候转向质量主题了。

一旦自动化构建和发布管道就绪,且更改开始以越来越快的速度流向生产环境,就该开始衡量这些更改的质量了。更重要的是,这让我们能够终止质量不合格的更改。什么算是质量足够的标准因项目而异。例如,在开发游戏时,一些 bug 可能会让用户感到烦恼,但不会造成太大问题;然而,在开发飞机或医疗软件时,一个 bug 可能会造成生命损失。在软件开发中,更高的质量通常意味着更高的成本和/或更多的时间。因此,我们可以交付的特性数量和可以保证的质量之间存在权衡。每个项目都有一个不同的最佳权衡点。

在衡量质量之前,首先要确定如何衡量软件的质量。监控软件质量的一种常见方法是收集一个或多个度量指标。例如,可以决定每周收集五个测量值。随着时间的推移,将这些度量指标绘制成图表,可以洞察软件质量的演变情况。其示例如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/3bb1f3cd-63f4-4466-8bb4-64abdbf00639.png

接下来的章节讨论了几个度量指标的示例。

质量度量指标

度量指标是用数字表示某些事物的手段。在软件开发中,度量指标通常用于表示某个质量方面,这一方面本身可能难以量化。例如,软件质量本身可能很难描述,质量变化的情况就更难表述。因此,我们通常会捕捉一些数字,组合起来可以揭示软件质量的情况。

重要的是要意识到,度量指标是一个很好的工具,但使用时必须谨慎。一方面,可能有比正在测量的度量指标更多的因素影响着(感知的)软件质量。此外,一旦人们知道某个特定的度量指标被记录下来,他们就可以优化自己的工作,以提高或降低该指标。虽然这可能在报告中显示出期望的数字,但这不一定意味着软件质量真的在改善。为了应对这个问题,通常会记录多个度量指标。

一个著名的例子是在敏捷工作环境中使用故事点速度。记录团队的冲刺速度,以查看团队是否随着时间的推移变得更高效,听起来很有效;然而,如果团队的规模在每个冲刺中都有所不同,那么该度量指标可能会变得无用,因为人员出席情况也会影响速度。此外,团队也可以轻易地通过在每次冲刺时同意将所有估算值乘以一个随机数来伪造该度量指标。虽然这会让每个冲刺的数字都增大,但这并不代表团队的产出能力在增加。

说到衡量软件质量的指标时,客观地衡量编写代码的质量可能是困难的。开发人员通常对什么构成良好代码有不同的看法,讨论越多,团队越难达成共识;然而,当将注意力转向使用这些代码的结果时,识别有助于提供代码质量洞察的指标就变得更容易了。

例如,以下是一些示例:

  • 集成构建失败的百分比:如果代码无法编译或未通过自动化测试,这表明代码质量不足。由于每当推送新更改时,构建管道可以自动执行测试,因此它们是评估代码质量的绝佳工具。此外,由于测试可以在我们将更改部署到生产环境之前执行并收集结果,测试结果可以用于在将更改部署到发布管道的下一个阶段之前取消该更改。通过这种方式,只有足够质量的更改才能传播到下一阶段。

  • 通过自动化测试覆盖的代码百分比:如果更多的代码通过单元测试进行了测试,这将提高软件的质量。

  • 变更失败率:这是新版本代码部署后导致问题的百分比。例如,在部署新版本应用程序后,Web 服务器出现内存不足的情况。

  • 未计划工作的量:在任何一段时间内必须执行的未计划工作量可以是质量的一个重要指标。如果团队正在开发并且运营一款 SaaS 产品,那么就会有时间用于运营工作。这通常被称为未计划工作。未计划工作的量可以反映计划工作质量。如果未计划工作的量增加,那么这可能表明质量下降了。未计划工作的例子包括现场事故、处理警报、紧急修复和补丁。

  • 用户报告的缺陷数量:如果用户报告的缺陷数量增加,这可能是质量下降的一个信号。通常,这是一个滞后指标,所以一旦这个数字开始增加,质量可能已经下降了一段时间。当然,导致这个数字增加的原因有很多:新的操作系统,用户数量的增加,或者用户期望的变化。

  • 已知问题的数量:即使新发现或报告的缺陷非常少,如果缺陷从未被修复,而已知问题的数量持续缓慢增加,那么软件的质量将随着时间的推移逐渐下降。

  • 技术债务的量:技术债务是一个术语,用于描述为了短期利益(如快速交付代码)而牺牲代码质量的后果。技术债务将在下一节中详细讨论。

测试是一项旨在发现和报告软件质量的活动。测试结果(对质量的洞察)可用于决定是否允许或取消某个变更进入下一个发布阶段。

在下一节中,将探讨质量的另一个维度:代码库中的技术债务量。

技术债务

技术债务是一个术语,用于描述为实现其他目标而牺牲代码质量所带来的未来成本。例如,为了加速新特性的交付,开发人员可能会选择迅速扩展现有类,添加一些新方法来实现该特性。如果结果类不符合面向对象设计原则或变得过于庞大,这将导致该类难以理解、维护或后续修改。术语“债务”意味着有某种东西(时间、质量、关注或工作)欠缺于解决方案。只要这笔债务没有还清,你就需要支付利息,这种利息表现为所有其他工作都会稍微变慢。

技术债务可以采取多种形式,以下是一些示例:

  • 没有任何单元测试覆盖的代码,无法使用原来创建该代码时的测试来验证该代码的实现变更

  • 没有使用有意义的变量名和方法名以自解释的方式编写的代码

  • 不符合编码原则的代码,例如 KISS、YAGNI、DRY 和/或 SOLID

  • 由于有过多的变量和方法而导致过于复杂的类

  • 由于包含过多语句(尤其是流程控制语句)而过于复杂的方法

  • 通过应用程序不同部分存在循环依赖的类或命名空间

  • 不符合应用程序架构设计的类

技术债务有许多形式,要全面管理所有这些形式可能会令人生畏。因此,有许多工具可以自动测量代码库中的技术债务并进行报告。关于此类工具的讨论将在维护质量部分进行。

虽然技术债务通常被认为是坏事,但有时故意产生技术债务也是有其合理原因的。就像普通债务一样,管理债务的规模以及确保能够支付利息和偿还债务是非常重要的。

公司通常在初创阶段承担技术债务,在这一阶段,快速创建一个可用的解决方案往往是一个有意识的决策。虽然这个初始版本用于验证商业方案并吸引资金,但开发人员可以通过重新实现或重构(部分)应用程序来偿还这笔债务。

另一个原因可能是市场机会或一个已经提前几个月计划的重大业务事件。为了按时交付并达成最后期限,承担一些技术债务可能是值得的。

然而,永远不偿还债务并且只是在时间上不断增加债务,最终会增加每次开发人员需要进行更改时所支付的隐性“利息”。结果是任何更改都会比上一次更花时间。如果这种情况开始发生,那么在某个时刻任何更改都将变得不再值得,因为成本总是超过收益。此时,一个项目或产品就会失败。

在谈论测试时,理解存在哪些测试类型非常重要。下一部分将深入讨论这一主题。

理解测试类型

在传统的软件开发中,测试通常是在开发完成应用程序被声明为开发完成功能集被冻结或类似声明之后进行的。宣布开发完成后,开始执行测试,通常会经历一段长时间的反复测试和修复 bug 的过程。结果往往是在上线后仍然发现许多 bug。

向左移动(Shifting left)是一种测试原则,表示自动化测试应当在开发过程中尽早进行。如果将与软件开发相关的所有活动画成一条从开始到发布的时间线,那么向左移动意味着将自动化测试活动移得更靠近开始阶段。

为此,识别出了许多不同类型的测试,例如单元测试、集成测试和系统测试。不同的来源可能建议不同类型的测试,但这些是一些更为知名的类型。不论测试的具体名称是什么,当我们从高层次抽象来看待这些测试时,它们通常被分为以下两类:

  • 功能测试:功能测试的目的是测试应用程序是否实际实现了所需的功能。

  • 非功能性测试:非功能性测试用于验证应用程序的其他所需属性是否得到实现,并且不包含不希望出现的属性。

这些类型进一步细分为更小的子类别,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/bbf7a7ed-dc9f-4f07-94e8-5891cd6dc942.png

以下三个部分简要回顾了不同类型的功能性和非功能性测试。这是为了便于后续讨论在不同情境下选择哪种测试类型以及你的项目可能需要多少种类型的测试。

自动化功能测试的类型

在谈论自动化功能测试时,最常用的三种类型是单元测试、集成测试和系统测试。这些测试类型可以通过多个维度进行比较:创建测试所需的时间、执行测试所需的时间以及它们所测试的范围:

  • 单元测试:单元测试是编写最快的,而且执行速度非常快,通常在不到一毫秒的时间内完成。它们测试的是应用程序中最小的范围,通常是单个类或方法。这意味着一旦编写完成,几乎永远不需要更改单元测试。对于许多系统而言,更可能的是删除一个测试,而不是修改它。

  • 集成测试:集成测试的编写时间较长,因为它们涉及多个单元,这些单元需要共同设置以便协作。尽管如此,这些测试的执行速度仍然应该很快,平均从不到一秒到十几秒不等。集成测试的测试范围较大,这意味着它们能覆盖更多的代码,并且更有可能发现由于更改引入的缺陷。

  • 系统测试:系统测试测试的是一个完整的、正在运行的应用程序。根据应用程序的类型,这些通常是 API 测试或自动化 UI 测试。这些测试的创建需要很长时间,因为它们依赖于已部署的系统运行,并且通常需要在数据库或其他持久存储中设置初始状态。测试的执行时间也很长,有时每个测试需要几分钟。它们也不如单元测试和集成测试可靠,并且比单元测试和集成测试更脆弱。即使是接口的小改动也可能导致一系列测试失败。另一方面,系统测试可以发现单元测试和集成测试无法发现的错误,因为它们实际上是在测试运行中的系统。

请注意,测试中拥有大的测试范围既有优点也有缺点。优点是它能够发现很多错误。缺点是,当一个具有非常大测试范围的测试失败时,它只能提供有限的信息,帮助判断出了什么问题。这样的测试失败通常需要比范围较小的测试失败更多的调查。

以下各节将更详细地探讨每种类型的测试。

单元测试

单元测试用于测试一个单独的单元。对于面向对象编程语言来说,这意味着每个应用程序类都会有一个对应的测试类。为了实现全面的测试覆盖,测试类将为每个公共方法提供一个或多个测试。

单元测试应该运行得非常快——平均来说,在几毫秒以内。为了实现这一点,每个类在没有依赖项的情况下被实例化。这是通过使用接口实现的,其中类依赖于接口,而不是直接依赖于其他类。对于测试,依赖项会被模拟类替换,如下图所示。左侧显示的是运行时配置,右侧显示的是测试期间的配置:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/fd411400-b29b-4582-887e-6cdeb93cb918.png

一个模拟类实现了相同的接口,但默认没有关联任何行为。可以在每个测试中为其设置特定行为。模拟还可以用来验证某个依赖项上的特定操作或函数是否被调用。例如,考虑以下 C#类:

public class WorkDivider
{
    private readonly IMessageSender _messageSender;

    public WorkDivider(IMessageSender messageSender)
    {
        _messageSender = messageSender;
    }

    public void DivideWork(IEnumerable<WorkOrder> workOrders)
    {
        foreach(var workOrder in workOrders)
        {
            _messageSender.SendMessage(workOrder.GetMessage());
        }
    }
}

为了在自动化测试中实例化这个类,需要提供一个IMessageSender接口的实现。为了绕过这个依赖关系,可以使用像 Moq 这样的模拟框架来测试WorkDivider,如下所示。在这些示例中,使用NUnit作为测试框架:

[TestFixture]
public class WorkDividerTest
{
    private Mock<IMessageSender> _messageSender;
    private WorkDivider _subject;

    [SetUp]
    public void SetUp()
    {
        _messageSender = new Mock<IMessageSender>();
        _subject = new WorkDivider(_messageSender.Object);
    }

    [Test]
    public void WhenSendingAnEnumerableOfWorkingOrders_EverOrderIsSendToTheMessageSender()
    {
        var workOrder = new WorkOrder();

        _subject.DivideWork(new[] { workOrder });

        _messageSender.Verify(x => x.SendMessage(workOrder), Times.Once);
    }
}

这意味着,无法为与其他系统(例如数据库、缓存或服务总线)交互的类编写单元测试。为了确保不会导致无法使用测试覆盖应用程序的大片部分,常见的做法是将与其他系统的集成隔离到单独的类中。这些类包含与远程系统的交互,但没有业务逻辑且代码尽可能少。然后接受这些类不需要单元测试覆盖。为此常用的设计模式有外观模式、适配器模式和仓库模式。

本章末尾包含了更详细的单元测试编写指南以及如何模拟类的链接。

单元测试应准备好在每个开发者克隆应用程序代码库时在其计算机上运行。它们不应要求在本地计算机上进行任何特殊配置或设置,应该可以直接使用。这样,所有与代码库一起工作的人都可以在本地计算机上运行单元测试。因此,开发者在推送更改到中央代码库之前,最好在自己的计算机上运行所有单元测试。

除了本地验证步骤,单元测试还应作为持续集成构建的一部分。稍后你将在流水线中执行测试部分学习如何操作。只要拉取请求中有失败的单元测试,最好不要将更改合并到主分支。这甚至可以通过使用 Git 仓库分支策略来强制执行,在第二章中已经讨论过,一切从源代码控制开始

在下一节中,关于自动化功能测试的讨论将继续涉及集成测试。

集成测试

集成测试用于测试一组组件是否能够正确协同工作。这些测试有两个目的:

  • 增加那些未被单元测试覆盖的应用程序部分的测试覆盖率——例如,与其他系统交互的类

  • 解决单元测试中没有涉及的风险,并处理与类交互的情况

理解集成风险可能很困难,因为一旦所有部分都按预期工作,整个系统似乎就能正常工作。为了更好地理解这一风险,假设两个组件一起工作,负责气候控制。其中一个组件测量温度,单位是摄氏度,另一个组件根据这个温度做出反应,期望输入的单位是华氏度。很快就会发现,虽然两个组件都按预期工作,互相交换数据并采取相应的行动,但它们的结合不会产生预期的结果。

集成测试,尤其是那些与其他系统交互的测试,不仅运行时间比单元测试长,而且通常需要更多的配置或设置。这甚至可能包括一些敏感信息,如用户名、密码或证书。为了处理这种配置,可以在测试文件旁边创建一个设置文件,测试执行前从该文件加载设置。每个开发人员都可以创建自己的一份文件副本,并使用自己的配置运行测试。

继续上一节的例子,假设实现了IMessageSender接口的MessageSender类需要一个连接字符串来完成工作。MessageSender的测试类可能如下所示:

[TestFixture]
public class MessageSenderTest
{
    private MessageSender _messageSender;

    [SetUp]
    public void SetUp()
    {
        var connectionString = TestContext.Parameters["MessageSenderConnectionString"];
        _messageSender = new MessageSender(connectionString);
    }
}

构建MessageSender类所需的connectionString是从TestContext中的Parameters对象中获取的。这是NUnit框架通过.runsettings文件提供设置的方式。具体实现方式可能因测试框架不同而有所不同。一个示例.runsettings文件如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
 <TestRunParameters>
 <Parameter name="MessageSenderConnectionString" value="secret-value" />
 </TestRunParameters>
</RunSettings>

将设置移到单独的文件中,确保敏感信息不会被提交到源代码管理系统中。在在流水线中执行测试部分,你将学习如何为在流水线中运行测试构建一个.runsettings文件。

这是因为集成测试应该尽可能也作为持续集成构建的一部分。然而,这样做有可能使得持续集成构建变得过于缓慢。为了解决这个问题,可以实施以下解决方案之一:

  • 集成测试在一个单独的构建中执行,这个构建是与持续集成构建并行触发的。这样,持续集成构建的时间保持较短,同时集成测试仍然会持续执行,开发人员能迅速获得反馈。

  • 集成测试通常在流水线的后期执行,接近软件发布阶段——例如,在部署到测试环境之前或之后。

第一种方法的缺点是,以这种方式执行集成测试意味着测试将不再作为代码合并到master之前的质量门控。它们当然仍然会作为质量报告机制继续工作。这意味着,尽管错误可能会被合并,但它们会被构建过程检测并报告。

第二种方法没有这种风险,因为执行测试仍然是从源代码管理到生产的管道的一部分;然而,在这种方法中,如果不是每个构建都至少进入发布管道的某一部分,测试的执行可能会被推迟到稍后的时刻。这意味着缺陷可能会在稍后才暴露,延长了检测和修复问题之间的时间。

无论采用哪种方法,集成测试失败将不再阻止合并更改,因此你必须找到另一种方法,确保开发人员负责修复导致测试失败的缺陷。

随着系统测试的出现,这些权衡变得更加明显,系统测试通常需要很长时间,因此不可能将其纳入持续集成构建中。

系统测试

第三种也是最后一种类型的自动化功能测试是系统测试。这些测试旨在针对一个完整组装并运行的应用程序进行。系统测试根据应用程序的类型有两种形式:API 测试或 UI 测试。系统测试的执行时间可能很长,尤其是有复杂测试数据设置的长时间测试,往往需要超过一分钟。

你可能会遇到一种叫做编码 UI 测试的东西。这是微软现已弃用的用于编写 UI 测试的解决方案。这些测试可以从 Azure Pipelines 中执行。幸运的是,有许多替代方案,微软在其弃用消息中提到,详细内容请参见devblogs.microsoft.com/devops/changes-to-coded-ui-test-in-visual-studio-2019

系统测试是在运行的应用程序上执行的,这意味着在执行之前需要进行配置和设置。应用程序需要在受控环境中运行,所有与数据存储的集成需要完全正常工作。与其他系统的集成要么需要处于运行状态,要么用替代的模拟替换,以确保所有与这些系统集成的操作能正常进行。

这些条件使得开发人员在修改应用程序时执行这些测试的可能性较小。通常只有在创建新测试或更改现有测试时,他们才可能这样做。然而,即便如此,他们也许并不是在本地运行的应用程序版本上执行这些测试,而是在已经部署到测试环境中的版本上执行。这不一定是好事,但往往是大多数团队中的现实情况。

可惜的是,如何创建 API 或 UI 测试的介绍超出了本书的范围。市场上有许多产品可供选择,哪个最好使用会根据项目不同而有所不同。

在执行系统测试作为流水线的一部分时,它们通常是在代码部署到至少一个环境之后进行的。这通常是测试环境。这意味着系统测试是从源代码变更到生产环境部署的关键路径的一部分。如果这个路径变得太长,它们也可以从流水线中移除,改为按计划执行——例如,每晚执行一次。与集成测试一样,这样可以加速流水线,但也就失去了将系统测试作为质量门控的机会。

系统测试,尤其是 UI 测试,通常比较脆弱,在发生微小变化后可能会意外停止工作。由于这个原因,建议尽量减少此类测试的数量;但是,请记住,这些测试能够捕捉到特定的错误,例如配置错误、其他运行时错误、数据库与应用程序的不匹配,或者一系列导致错误状态的操作。

除了自动化功能测试,还有许多 DevOps 项目中也有价值的手动功能测试。接下来将讨论这些测试。

手动功能测试的类型

虽然自动化测试是快速且频繁获得开发反馈的好工具,但仍然有一些内容需要手动测试。虽然自动化重复性测试是持续监控质量的最佳方式,但有些事情仍然需要人工检查。

手动测试是向左 shift 的关键点*。* 每当任何类型的测试或验证被向左 shift 时,这意味着它是在手动测试执行之前进行的。这样做的好处是,所有这些自动化活动都会增加我们对正在测试的应用程序版本的信心,从而提高该版本通过手动测试的可能性。换句话说,当手动测试开始时,任何新问题被发现的可能性应该非常低。

手动测试有两种类型:

  • 脚本化测试

  • 探索性测试

以下部分将讨论这两种类型的测试。

脚本化测试

脚本化测试是一种技术,用于在确保全面覆盖所有相关测试用例的同时,最大限度地减少测试执行的时间。这是通过将测试分为两个不同的阶段来完成的:测试准备和测试执行。测试准备与待测试功能的开发并行进行,甚至在开发开始之前就可以进行。在测试准备阶段,功能被分析并识别出正式的测试用例。

一旦确定了必须执行的测试用例,就会编写手动测试脚本,描述测试执行阶段需要执行的每一步。这些脚本的设计方式使得它们易于遵循,并且没有任何疑问或不明确的地方。它们也被编写成尽可能减少执行步骤的数量。虽然这可能需要更多的准备时间,但所有这些都是为了确保在测试执行时花费尽可能少的时间。

对于测试分析的更深入讨论,以及如何识别测试用例,超出了本书的范围。虽然你负责创建测试用例,Azure DevOps 会在这方面为你提供支持。使用测试计划服务,你可以创建测试计划并记录其中的测试用例,以便稍后快速执行。

要创建一个新的测试计划,请执行以下步骤:

  1. 打开 Azure 测试计划菜单:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/a6d9a160-c159-410c-95ed-bf3874089d27.png

  1. 在这个菜单中,点击测试计划。这里你将看到当前拥有的所有测试计划的概览。

  2. 点击“新建测试计划”按钮以开始创建新的测试计划。这将打开一个新的对话框,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/80d5b118-758f-4880-bb0c-c9b6cbd8783a.png

  1. 给测试计划起一个有意义的名称,例如,能够说明测试计划用途的名称。

  2. 将测试计划链接到正确的产品区域路径。

  3. 选择与此测试相关的正确迭代或冲刺。

  4. 点击“创建”以完成测试计划的创建。这将自动打开该测试计划,如下所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/8c9a1059-2611-4782-99ee-43c9d3a88ca4.png

一个测试计划可以分为多个测试套件,而每个测试套件又可以再次分割。实际上,测试套件对测试的作用就像文件夹对文件的作用一样。通过点击在测试套件上悬停时出现的省略号按钮,可以管理这些套件。这在之前的截图中有所展示。

创建测试计划后,就该向计划中添加一个或多个测试用例了。为此,确保打开测试套件的定义标签页,并点击“新建测试用例”按钮。会弹出一个新的窗口:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/c2669c64-0e59-4ded-8133-31bd11039dfb.png

在这里,可以定义测试步骤和预期结果。要定义一个新的测试用例,请执行以下步骤:

  1. 输入测试用例的标题。

  2. 在对话框中,输入一个或多个动作及预期结果,详细描述测试用例。

  3. 一旦测试用例完全描述完成,点击保存并关闭按钮以保存测试用例,并返回到之前的屏幕,在那里可以管理测试套件。

一旦准备工作完成并且功能准备好进行测试,所有的测试就会执行。由于所有测试都已经详细脚本化,因此这可以快速且有效地完成。甚至可能会有开发人员、业务分析师或公司其他部门的人协助执行测试。这意味着测试执行本身将非常迅速。

要开始执行测试套件或计划,请执行以下步骤:

  1. 导航到执行标签:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/c5b38e53-2506-453e-83f9-f32e6cfad57e.png

  1. 选择一个或多个测试用例。

  2. 在右上角选择一个运行选项。

当选择对网页应用程序运行测试时,将打开一个带有测试执行器的新浏览器窗口。可以使用此测试执行器逐个测试用例地执行所有步骤,并跟踪所有的成功和错误,如下所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/a9829353-3312-453f-977e-1995ee5ce661.png

每个测试步骤后的勾选框或叉号可以用来跟踪单独步骤的结果。如果某个步骤被标记为错误,可以添加带有缺陷的评论。要将测试用例标记为通过或标记,右上角的蓝色下拉菜单可以用来标记结果。一旦选择了测试结果,测试执行器会自动跳到下一个测试。一旦所有测试都执行完毕,可以通过左上角的保存并关闭按钮保存结果。

要查看测试运行的结果,导航到测试计划,然后选择运行,查看如下仪表板:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/19a4fdc6-e416-4905-bee5-b272bd762c41.png

在这里,您可以选择要查看其结果的运行,从而快速概览测试结果。在第二个标签页“测试结果”中,可以查看所有测试用例的列表,以及它们是否通过。

拥有详细脚本的一个主要好处是相同的测试可以执行多次,从而减少每次执行的成本。如果一个测试计划被执行多次,所有的执行历史都会被保存,并且可以通过前面截图中显示的视图访问。这在手动测试作为回归测试的一部分时非常有用;然而,一旦成为这种情况,通常将测试自动化为系统测试会更加有益,如果可能的话。

可以多次执行相同的测试,但对于不同的配置。在开发网页应用时,通常会通过不同的浏览器进行测试。对于桌面应用程序,这可能用于测试不同的操作系统。有关配置的详细信息,请参考微软文档:docs.microsoft.com/en-us/azure/devops/test/mtm/test-configurations-specifying-test-platforms?view=azure-devops

下一部分将讨论功能测试的最终形式,即探索性测试。

探索性测试

编写和执行详细的测试脚本可能会占用测试工程师和测试执行者大量的时间,因此这些测试通常会被自动化。一旦它们被自动化,它们将属于系统测试和自动化 UI 测试的范畴。

这并不一定意味着手动测试没有价值或没有好的投资回报。只是有些事情是人眼能发现的,而计算机无法察觉的,比如那些不够用户友好的界面、错位的界面元素,或者那些没有完全显示而被其他元素遮住的文本行或图片。

为了在不花费大量时间编写详细测试脚本的情况下捕捉这些错误,探索性测试可能是一个解决方案。在这种方法中,测试人员打开应用程序并开始调查他们认为在即将发布的版本中包含最多风险的部分。在探索应用程序的过程中,测试人员会记录他们访问过的部分以及执行过的测试用例。同时,测试人员还会记录他们发现的新风险或尚未执行的测试用例。通过这种方式,他们在工作过程中创建了一个已覆盖和未覆盖的测试用例列表。这还允许测试人员始终专注于最重要的风险和测试用例。一旦探索性测试结束,测试人员可以报告哪些应用程序区域和测试用例已覆盖,哪些未覆盖,哪些风险仍然没有被探索到。这个报告对于产品经理来说是非常有价值的输入,帮助他们决定是否继续进行发布。

一个常见的误解是,探索性测试意味着测试人员只是随便点击看应用程序是否正常工作。事实并非如此,前面的段落已经表明,探索性测试是一项高度结构化的活动,需要实践。如果执行得当,测试准备和测试执行在探索性测试过程中是交织在一起的。

探索性测试是当时间有限或无法提前知道可用测试时间时的一个非常好的工具。探索性测试可能会产生需要记录为缺陷的发现。接下来将介绍如何进行此操作。

报告手动测试结果

测试的一个重要活动是报告发现的任何缺陷或其他问题。这通常是一项繁琐且耗时的工作。你必须再次尝试重现问题,尽量记住问题是如何表现出来的,并将所有步骤写下来。然后,必须描述期望的结果和不期望的结果,拍摄截图,并将一切内容插入到缺陷跟踪器或工作管理工具中,例如 Azure DevOps。

为了简化这个过程,Azure DevOps 提供了一个Test & Feedback扩展。这个扩展提供了简单的按钮,用于录制截图或视频,并通过文本或图形进行标注。一旦发现并通过录制或截图记录了问题,它可以自动提交到 Azure DevOps 看板中。

这个扩展可以从 Azure DevOps 市场免费获得,并且支持在 Firefox 和 Chrome 浏览器中运行。当前正在开发对 Edge 浏览器的支持。扩展的链接将在本章末尾提供。

Test & Feedback 扩展可以在执行脚本化测试和进行探索性测试时使用。

这部分结束了对不同类型功能测试的讨论。下一部分将帮助你决定在项目中使用哪种类型的测试。

决定需要哪些类型功能测试的策略

面对如此多不同类型的测试,哪种测试最适合你的项目?考虑到测试种类繁多且性质各异,答案正如你所料:混合使用它们,因为它们各有不同的特点。

下图显示了不同类型的测试执行时间与它们提供的质量信心之间的关系。图中表明,尽管成功完成的手动测试具有识别缺陷的最高概率,但它们执行的时间最长。对于自动化测试,成千上万的单元测试通常可以在几分钟内完成,而十到一百个系统测试可能需要超过 30 分钟:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/55e1c0d2-a1ce-4c40-91d3-2fc8ae012b01.png

从这种权衡来看,通常更倾向于优先使用单元测试而非集成测试,优先使用集成测试而非系统测试,任何类型的手动测试都优先于自动化测试。

如果单元测试和集成测试的质量提高,那么这条线将进一步向左上方攀升。高质量的软件架构还将有助于减少对系统测试和集成测试的需求,并增强单元测试带来的保证。这两者都能使快速且频繁执行的自动化测试的积极效果更加显著。

理解这种权衡关系也有助于理解两种可以用于决定测试策略的模型:测试金字塔和测试奖杯,接下来的两部分将讨论这两种模型。

测试金字塔

在许多较旧的项目中,自动化功能测试并不多。通常,这些测试执行较慢,测试范围大,难以维护,且经常失败,且没有明确的原因。这些测试所提供的价值通常非常有限。为了弥补缺乏良好的自动化测试,通常会进行大量的手动测试,在新版本部署之前进行完整的回归测试。这些自动化测试非常耗时且很少执行。开发人员没有快速反馈,缺陷通常在晚些时候才被发现。在这种情况下,很难实践 DevOps,因为 DevOps 的重点是快速且频繁地创建新版本。

这样的应用程序测试组通常被称为冰淇淋锥形测试:许多手动测试和少量自动化测试,其中只有少数是单元测试。冰淇淋锥形测试是一种反模式,但常常出现在较旧或长期运行的项目中:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/4b02dd29-7a62-410c-9acf-2c26d5aaa097.png

为了应对这一点,提出了另一种相对立的模型:测试金字塔。该模型主张拥有大量单元测试,可以在几分钟内反馈应用程序的质量,快速指出大部分错误。在此之上,其他类型的较慢测试被分层,用于捕捉前面层次无法捕捉到的错误。使用这种方法,测试覆盖率和测试时长之间有一个很好的权衡。

请注意,测试金字塔并不倡导分层方法。不要先构建一层单元测试,只有当所有单元测试完成时才开始进行集成测试。相反,它提倡比例:你应该在单元测试、集成测试和系统测试之间保持健康的比例。

关于不同类型测试之间最佳比例的一般建议很难给出。但在大多数项目中,金字塔中每一层的比例为 1:5-15 是合理的。

测试奖杯

虽然测试金字塔是一个广为人知且常用的分类测试方法,用于决定创建哪种类型的测试,但这种方法也受到了批评。虽然在 DevOps 团队中,远离手动测试和系统测试被普遍认为是必要的,但对单元测试的重视并非普遍接受。有些人反对测试金字塔暗示要创建更多的单元测试而非集成测试这一事实。

反对这种做法的原因如下:

  • 单元测试往往与其测试的实现紧密相关。 回顾在“单元测试”部分中对WorkDivider的测试,可以看出它依赖于了解DivideWork方法的实现。这个测试实际上验证的是实际实现:对SendMessage()的调用。许多单元测试都有这种特征,因此,增加大量单元测试会增加更改类级设计实现的难度。

  • 单元测试的变化率通常比集成测试更高。 单元测试类与它们测试的类紧密关联。这意味着,如果被测试的类被替换,这些单元测试也将失去所有价值。因此,有人认为,集成测试可能具有更高的投资回报率。

  • 真正的价值来自于集成组件,而不是单个组件。 即使所有单元在独立运行时都能正常工作,系统可能并不会提供任何价值。软件的真正价值只有在集成并准备运行后才会体现出来。由于测试应该验证价值交付,因此有人认为,重点应该放在编写集成测试而非单元测试上。

为了应对这些反对意见,Kent C. Dodds提出了测试奖杯模型。这个模型采纳了测试金字塔的理念,提倡尽量减少手动和系统测试,但与金字塔不同的是,它并不强调单元测试的重要性,而是更注重集成测试。测试奖杯这一名字来源于,如果将其绘制出来,会形成一个类似奖杯的形状。

不幸的是,并没有“银弹”解决方案,最好的建议是了解三种模型及其背后的推理,并将适当的推理应用到当前情况中。对于测试而言,并没有适用于所有情况的最佳解决方案。

非功能性测试的类型

功能测试主要关注验证应用程序展示的行为是否是预期的行为;然而,在应用程序开发中存在更多的风险:应用程序是否足够快速地执行操作,随着更多用户同时使用系统时,性能是否会下降,以及系统是否易于终端用户使用。验证这些系统属性的测试称为非功能性测试。

有许多类型的非功能性测试,但在 DevOps 场景中,以下三种尤为重要:

  • 性能测试

  • 负载测试

  • 可用性测试

让我们一一回顾它们。

性能测试

性能测试是用来确定在给定资源的情况下,应用程序执行某个操作的速度。性能测试通常使用专门的工具,并在完整的系统上执行。如果用于自动化 API 或 UI 测试的工具记录了测试的持续时间,那么这些测试的持续时间也可以作为性能结果。

为了比较多次测试的结果,必须确保影响性能的所有因素在测试之间保持一致。测试主体和测试执行者的虚拟机设置应保持相同。应用程序配置应保持不变,集成点应尽可能保持在相同状态——例如,应该在每次性能测试前从备份恢复相同的数据库,而不是重复使用同一个数据库。这样可以确保结果具有可比性。

尽管性能测试和负载测试常常被混淆,但它们是两种不同的测试类型。

负载测试

负载测试用于衡量系统在崩溃之前能承受多少负载。这类测试有时也被称为压力测试。与性能测试不同,负载测试会并行执行多个请求。衡量的指标是所有请求的平均性能,同时逐渐增加请求的数量。在大多数情况下,这将确定一个临界点,即每秒请求的特定数量,超出此数量时性能会突然下降。这是系统能够最大承载的每秒请求数。执行负载测试时,收集在最大请求数增加过程中所有请求的平均性能,通常会得到如下图表:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/e8c4d209-1327-409d-951f-70ee08b4bd42.png

这张图表展示了为何了解应用程序的崩溃点很重要:过高的负载可能会意外地让系统崩溃,因为响应时间变化的突发性质。了解这个临界点可以让操作人员在生产环境中到达此点之前采取行动。

本章末尾有一个链接,指向一个微软在线实验室,供开发人员练习负载测试。

可用性测试

另一种重要的测试类型是可用性测试。虽然其他类型的测试侧重于验证实现是否符合产品团队的预期行为,但可用性测试则侧重于验证用户的期望是否得到了满足。这意味着测试范围更广,这些测试可以识别笨拙的用户界面,并帮助发现不清晰的文本或误解的用户请求。

可用性测试是通过让用户在一个或多个任务上使用最终的应用程序,并观察或询问他们与应用程序互动的方式来进行的。结果通常比“通过”或“未通过”更为详细,并且结果通常会反馈给产品负责人,以便编写新的用户故事或更改需求。

一个很好的可用性测试技巧是使用功能标志。功能标志使我们能够逐步将新功能暴露给更多用户。这一功能也可以用来首先仅将新功能暴露给一小部分、参与可用性研究的用户。这使得研究人员或产品负责人能够密切观察这些用户使用新功能的情况,而其他用户则无法访问该功能。

在第四章《持续部署》中曾讨论过功能标志作为渐进式曝光的一种策略。新功能的渐进式曝光本身就是一种可用性或用户接受度测试。

这种方法可以扩展到执行 A/B 测试。在这类测试中,一半的用户会接触到新功能,而另一半则不会。然后会收集关于所有用户的度量数据,看看新功能是否带来了预测的好处——例如,用户是否每天使用应用程序的时间增加了。这个话题将在第十一章《收集用户反馈》中进一步讨论,介绍如何收集用户反馈。

这样做会将可用性测试推向发布过程的后期。也可以通过在最终应用程序之前,使用原型来执行可用性测试,从而将测试提前到左侧。

这部分内容结束了对不同类型测试的讨论。在接下来的章节中,将使用度量标准和测试来自动衡量质量并实现质量控制。

在管道中执行测试

开发人员在提交代码合并请求之前,应在本地机器上执行测试。这样,他们可以确保自己所做的更改没有破坏代码中任何先前的行为。理论上,这可以保证所有合并到主分支的代码都能成功编译并通过所有测试。但在实践中,存在许多原因导致这种情况并不总是成立。一些原因如下:

  • 有些测试可能无法在本地运行。它们依赖于机密的配置值,或者配置为在一个完全配置的系统上运行。系统测试通常会遇到这两种情况中的一种或两种。在许多情况下,无法从本地系统运行系统测试。这些情况并不全是不可取的或无法克服的,但通常确实如此。

  • 开发人员毕竟是人类。他们可能会忘记在做最后一个小调整后,在本地机器上运行测试,或者他们可能确信自己的更改并没有破坏现有的行为。尤其是在紧急修复 bug 时,可能会因为追求速度而跳过测试。

为了防止这些情况导致未完全测试的代码通过流水线传播,建议所有测试也在流水线内执行。接下来的部分将展示如何为单元测试、集成测试以及通过其他系统运行的测试执行此操作。首先是单元测试。

运行单元测试

对于许多语言,Azure DevOps 内置了从流水线运行单元测试的支持。可以为 C#、TypeScript、Python、Maven、C++、Go 等多种语言执行单元测试。

对于其中一些语言,提供了一个现成的任务。例如,用 C# 编写的测试。在执行 .NET 测试(例如 C#)时,测试结果会自动以构建代理能够理解的 XML 格式存储。

这允许流水线代理解释测试结果并在构建结果中可视化它们,如下所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/073d95c2-9ccb-471c-a457-b2b84d0c98a9.png

对于某些语言,可能需要执行多个任务。例如,用 TypeScript 编写的测试通常通过 NPM 命令执行。以下 YAML 可用于执行此操作:

- task: Npm@0
  displayName: 'Run unit tests - npm run tests'
  inputs:
    cwd: src
    command: run
    arguments: test

这将执行在 package.json 中指定的自定义 NPM 命令。不幸的是,这不会以流水线代理可以理解的格式存储测试结果。为了将结果转换为正确的格式,需要另一个任务:

- task: PublishTestResults@2
  displayName: 'Publish Test Results'
  inputs:
    testResultsFiles: '**\reportTests\TEST-*.xml'
    mergeTestResults: true
  condition: succeededOrFailed()

测试结果是否直接可用或需要转换,因编程语言而异。除了发布测试结果外,建议还要收集测试覆盖率结果。

记录单元测试代码覆盖率

最佳实践是,不仅在构建过程中运行所有单元测试,还要确定在任何这些测试中执行的代码基的百分比。这被称为 单元测试代码覆盖率,它是衡量测试全面性的一项指标。构建还可以配置为发布单元测试所实现的代码覆盖率。

要配置构建以发布 .NET Core 单元测试的测试覆盖率,必须执行以下步骤:

  1. 将 NuGet 包 coverlet.msbuild 安装到单元测试项目中。

  2. 使用 .NET Core 任务执行测试,并添加两个参数以生成覆盖报告,/p:CollectCoverage=true/p:CoverletOutputFormat=cobertura

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/0c075189-0c29-43ce-bc30-ab4574b3ba94.png

  1. 添加发布代码覆盖任务:

    1. 将代码覆盖工具设置为 cobertura

    2. 配置 $(System.DefaultWorkingDirectory)/**/coverage.cobertura.xml 作为汇总文件:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/4242f2a9-0b46-4429-bee6-f3b6cd305884.png

  1. 构建的运行详细信息现在将包含代码覆盖报告。

这是生成详细代码覆盖报告所需的所有配置。生成的报告包含已覆盖和未覆盖的代码块数量以及计算出的覆盖率百分比。这些报告是构建结果页面的一部分。

除了单元测试,集成测试也可以作为管道的一部分运行,并且它们通常伴随着管理配置设置的挑战。

运行集成测试

集成测试通常与单元测试使用相同的框架编写。然而,它们有自己独特的挑战。通常,它们需要一个或多个设置,指定如何与测试中涉及的一个或多个其他组件进行集成。回顾之前讨论过的MessageSender类的集成测试,这是一个典型的例子。

记住,这个测试有一个.runsettings文件,应该指定它应使用的队列的connectionString吗?这个connectionString设置不能被检查到源代码控制中。相反,可以将占位符检查到源代码控制中,然后在管道执行过程中将其替换为实际的密钥。

在这种情况下,这意味着以下pipeline.runsettings文件将被检查到源代码控制中:

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
    <TestRunParameters>
        <Parameter name="MessageSenderConnectionString" value="#{MessageSenderConnectionString}#" />
 </TestRunParameters>
</RunSettings>

在开始实际的测试执行之前,将运行另一个任务以用实际值替换占位符。正如在第三章《迁移到持续集成》中讨论的那样,这些值可以从变量组、密钥库或管道变量中安全地检索。Azure DevOps 有多个扩展可以用来将占位符替换为实际值。以下是如何执行此操作的 YAML 示例:

  - task: qetza.replacetokens.replacetokens-task.replacetokens@3
    displayName: 'Replace tokens in pipeline.runsettings'
    inputs:
      targetFiles: $(System.DefaultWorkingDirectory)/integrationtests-location/pipeline.runsettings

在执行替换令牌任务后,可以像单元测试一样调用测试运行器。

运行外部测试

除了单元测试和集成测试,你可能还需要使用其他系统执行测试。例如,Azure DevOps 没有内置支持执行负载测试或自动化 UI 测试。对于这些类型的测试,必须从管道中调用其他系统。许多系统可以通过这种方式进行集成。

如何执行此操作因系统而异,但大多数情况下,以下步骤适用:

  1. 配置外部系统中的测试。

  2. 为 Azure DevOps 安装一个扩展,使得可以从管道中调用外部系统的任务。

  3. 创建一个到外部系统的服务连接。

  4. 将任务添加到管道中。

有关配置集成的详细信息,一个好的起点通常是第三方产品供应商的网站。

维护质量

前面的部分详细介绍了用于描述应用程序质量的各种测试和度量标准。考虑到这些内容,现在是时候开始思考可以用来维持高质量甚至提高质量的工具了。

代码审查

保护代码质量的最强大工具之一就是代码评审。当使用 Git 时,开发者需要提交一个 pull request,将自己的更改合并回主分支。Pull request 允许一个或多个其他开发者对所有更改进行审查并发表评论。打开 pull request 的开发者可以查看评论并根据反馈进行相应修改,从而在继续工作时提高更改的质量。

为了让代码评审发挥最大效果,重要的是不要把它看作必须以尽可能少的努力通过的一个门槛。以一种开放的心态,假设每个人都在努力编写高质量的代码,将代码评审视为关于代码质量讨论的起点,这要比把它看作一个不受欢迎的仪式更有成效。重要的是改变视角,不再将代码评审视为软件开发中的一个烦人的仪式,别人会抱怨你的代码,而是将它视为欢迎他人对你的代码提出意见并帮助你写出更高质量代码的机会。

一旦建立了这样的心态,代码评审将成为学习的来源。它们将引发同行之间的讨论,关于如何以最佳方式解决问题:不仅是现在的最佳方式,还有未来的最佳方式,避免技术债务,并确保与待合并代码一起有足够的单元测试和集成测试。代码评审也是一个很好的辅导工具,能让初级开发者获得关于自己工作的反馈。让初级开发者评审高级开发者的代码,甚至可能更有价值。这样,他们可以提出自己尚未了解的问题,这通常会促使他们指出可能随着时间推移成为技术债务的过于复杂的解决方案。

自动收集质量指标

除了手动评审,还有许多工具可以用来自动确定代码库的质量。部分工具内建于 Azure Pipelines 中,但更复杂的功能则来自于独立的代码扫描工具。衡量技术债务的方法有多种数学方法,使用工具来进行这些衡量,不仅能提供应用程序质量的深刻洞察,还能反映出随时间变化的质量变化。

衡量应用程序质量的一个可能工具是 SonarCloud。SonarCloud 是基于 SonarCube 的 SaaS 服务。该工具可以自动扫描代码库,查找可能的 bug、安全风险、技术债务和其他质量指标。这是一个收费的独立服务,能够与 Azure DevOps pipelines 集成。使用 SonarCloud 时,必须创建一个账户并获取项目密钥,以便通过 Azure DevOps 启动 SonarCloud 扫描。

为了调用 SonarCloud,使用的是三个任务的集合,它们是 Azure DevOps 扩展的一部分。安装扩展并配置 SonarCloud 服务连接后,三个任务将被添加到管道中,以设置分析、执行分析,并(可选)在质量下降时使构建失败。第一个任务是唯一需要配置的任务,配置如以下截图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/42a64a8a-f5fe-4ee5-96dc-fbf895c98852.png

每次执行的构建都会自动由 SonarCloud 扫描其代码,届时将提供关于质量的详细报告。在这些报告的基础上,会生成一个仪表板,提供一些关键质量指标的快速概览:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/563c7578-719b-492b-a5e9-62d26da05f7e.png

这是另一个展示质量指标的仪表板视图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/ad9442d8-72dd-470e-be51-4e065ecc3a03.png

代码扫描工具可以用于报告代码的质量,但也可以充当质量门,如果检测到质量不足,将阻止更改合并或部署到特定环境。

可视化质量

持续衡量应用程序的质量没有意义,除非采取了相应的行动。仪表板可以作为一个强大的工具,持续洞察当前质量水平以及质量随时间的变化。

大多数代码质量工具都具有内置报告选项,它们对于质量保证工程师非常有价值。它们提供了关于应用程序哪些部分质量较高、哪些类型的问题最近发生得更频繁的详细信息。

这种类型的仪表板的缺点是它们可能难以阅读,并且它们不在开发人员执行大部分工作的工具中。因此,创建 Azure DevOps 中的仪表板来报告质量也可能是有益的。以下截图显示了这样一个仪表板的示例:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/2b3d9268-5c56-452d-8164-53df1f5637f7.png

此仪表板显示当前质量和应用程序代码的概览,以及一些最近的历史记录。在这里,您可以找到以下信息:

  • 最近的更改数量显示在顶部,并显示了最新 SonarCloud 质量门的结果,目前显示为“通过”。

  • 本项目中两个不同构建的结果显示在第二行。

  • 第三行和第四行显示了项目中所有构建和发布的聚合结果。使用符号表示构建和发布的状态:成功、失败或仍在运行。

  • 右侧使用了两个小部件来显示过去 10 个构建中失败的测试百分比及相应的失败测试数量。

  • 最新发布运行的结果按环境显示在下方。

像这样的仪表板可以通过内置的小部件或扩展为每个团队或每个项目创建。在 Azure DevOps 市场中有许多可用的扩展。例如,在前面的仪表板中,使用了团队项目健康状况扩展。

Azure DevOps 仪表板可以配置为每五分钟自动刷新,使其也可以用作墙面板。

质量 gates

衡量、报告甚至可视化质量非常重要且有价值;然而,如果没有人根据所有这些指标采取行动,那对开发团队来说是没有价值的。为了防止这种情况,可以引入自动化质量 gates 或检查。

实现质量 gates 的一种方式是,在每次测试失败、测试覆盖率过低或设置的代码扫描工具阈值不再满足时,失败持续集成构建。这些都是之前讨论过的内容。另一种强制执行标准的选项是向管道添加 gates 或检查。这样,必须满足特定条件,管道才能继续。

这在经典发布和 YAML 多阶段管道之间有所不同。

经典发布

另一种选择是使用 Azure 发布管道中的 gates。在这里,可以指定必须满足的一个或多个条件,才能允许发布部署到特定环境。gates 也可以是扩展的一部分,例如之前讨论过的 SonarCloud 扩展。

可以通过选择发布管道中的任何阶段并编辑预部署条件来添加 gates。启用 gates 后,可以添加一个或多个 gates。以下是发布管道的截图,显示了如何阻止任何质量不足的构建部署到环境中:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/465b860b-b319-4278-a6cf-ad246cccc912.png

使用部署批准和 gates 不是互斥的,因此可以混合使用两者。

多阶段管道

Gates(如同经典发布中一样)出现在多阶段 YAML 管道中。在 YAML 管道中,还有另一种机制:检查(checks)。检查被配置为在允许管道继续之前,自动验证是否满足一个或多个条件。检查可以添加到在某个阶段中使用的资源上。如果在某个阶段的一个或多个资源上找到检查,则必须通过所有检查,管道才能继续该阶段。检查也可以添加到环境和服务连接中。

要将检查添加到环境中,请导航到该环境:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/2a067ac8-9272-423a-8f5d-214c1d78a5f7.png

现在执行以下步骤:

  1. 在右上角,展开菜单并选择“批准和检查”:

  2. 在打开的新视图中,选择“查看所有”以查看所有可用的检查类型。选择“调用 Azure 功能”:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/966a8a22-3133-48fb-926e-ac030750cf5e.png

  1. 在打开的弹出窗口中,配置要调用的 Azure 函数。至少需要提供函数的 URL 和密钥。

  2. 选择创建。

创建检查后,每个目标环境的部署任务(见第四章,持续部署)都必须通过此检查。当调用的函数返回成功的响应码时,检查通过。

支持以下类型的检查:

  • 评估工件:验证类型为容器镜像的工件是否通过自定义策略。这些策略是用一种叫做Rego的语言定义的。

  • 调用 REST API:将流水线的详细信息发送到 Azure 函数以执行自定义逻辑。如果 API 返回成功的 HTTP 状态码,则允许流水线继续。

  • 调用 Azure 函数:与调用 REST API 检查相同,但对于 Azure 函数有一些默认设置。

  • 查询 Azure Monitor 警报:仅当指定的警报不处于活动状态时才继续。

  • 所需模板:仅当当前 YAML 流水线扩展一个或多个已配置的基本 YAML 流水线时,才能继续流水线。

检查可以成为一个强大的机制,确保在允许流水线继续之前,满足一个或多个条件。

总结

在这一章中,你学习了如何衡量和验证软件开发过程的质量。快速且频繁的发布要求所写的软件具有高质量。测试是确保你编写高质量软件、减少技术债务的必要步骤。你了解了不同类型的测试以及各种自动化和手动测试的优缺点。最后,你学习了代码审查和工具如何通过报告质量和充当质量门控来帮助保持项目的高质量。

有了这些知识,你现在可以讨论测试和测试类型,帮助你决定应用程序需要哪些测试,哪些风险可以通过哪些类型的测试来解决,以及你是否需要大量的测试还是可以省略它们。你现在也能设置和配置代码扫描工具,以确保不合格的更改不会合并到主线中。

在下一章,你将学习关于安全性和合规性这两个主题,它们在实践 DevOps 时同样重要。

问题

在我们总结时,这里有一些问题供你测试你对本章内容的理解。你可以在附录的评估部分找到答案:

  1. 判断对错:单元测试验证的是单个单元在孤立环境中的工作情况。

  2. 判断对错:集成测试验证的是一个完全组装的系统是否能正常工作。

  3. 关于测试金字塔的原则,以下哪个说法是正确的?

    1. 进行更多的集成测试,少做单元测试,甚至更少做系统测试。

    2. 应该有许多单元测试,较少的集成测试,甚至更少的系统测试。

    3. 应该有很多单元测试,较少的集成测试,以及更少的系统测试。

  4. 以下哪项不是非功能性测试类型?

    1. 负载测试

    2. 可用性测试

    3. 适用性测试

    4. 性能测试

  5. 测试是为了获取关于工作质量的洞察。可以采用哪些技术来防止不合格的工作传递到生产环境?

进一步阅读

第九章:安全性与合规性

就像确保你的应用程序执行所需功能一样重要,你还需要确保它不会做不该做的事。在上一章中,你学习了质量和测试,以便持续衡量应用程序是否按预期工作。在本章中,你将学习如何防止任何不希望发生的行为。这就是安全性和合规性的主题。在增加价值流向终端用户——通过更快地部署和缩短交付周期——的同时,你仍然需要确保交付的是安全且符合规定的软件。在本章中,你将学习如何在 DevOps 流程中解决这些问题。

为此,本章将首先讨论速度与安全性之间的权衡,并解释如何在拥抱 DevOps 时安全性并不会降低,甚至可能增加。接下来,将讨论安全性的一个具体维度:如何安全地处理管道和应用程序所需的机密信息,如密钥和密码。随后,将讨论代码扫描工具,用于自动识别应用程序代码和依赖项中可能存在的安全风险。本章最后将讨论如何保持基础设施和配置部署的合规性,以及如何使用 Azure 策略和安全中心检测运行时安全风险与威胁。

本章将涵盖以下主题:

  • 将 DevOps 原则应用于安全性和合规性

  • 处理机密信息

  • 检测应用程序代码漏洞

  • 处理依赖项

  • 确保基础设施合规性

  • 监控和检测运行时安全风险与威胁

  • 你可以使用的其他工具

技术要求

要尝试本章中描述的技术,你将需要以下一项或多项资源:

  • 一个具有构建和发布流水线访问权限并且有权安装扩展的 Azure DevOps 项目

  • 一个 Azure 订阅。 (如果你还没有账户,可以访问portal.azure.com,并按照指南进行注册)

  • 安装了 PowerShell Azure 模块的 PowerShell。 (有关如何安装 PowerShell Azure 模块的说明,请参考docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-4.1.0

  • 可选的 WhiteSource Bolt、SonarCloud 或类似产品的订阅

前述所有内容都可以免费或作为试用版使用,供学习或评估之用。

将 DevOps 原则应用于安全性和合规性

对安全性和合规性的担忧可能是公司不愿意接受完整 DevOps 思维方式的原因之一,以便能够更频繁、更快速地发布软件。过去,他们通常有较少的发布,每个版本发布前都会交由安全团队或进行渗透测试,然后再部署到生产环境。这种做法使他们确信不会发布包含安全漏洞的软件。

这种较少发布并在最终发布前进行一次大的安全测试的做法,与 DevOps 思维方式相冲突,这也是一些公司面临的挑战所在。他们希望确保将业务价值交付给用户,但又不愿为此牺牲安全性。问题在于,这种权衡是否公平。是否可以在保证速度的同时确保安全性?难道更快、更频繁的发布,并结合严格的自动化,反而可以提高软件开发中的安全性吗?为了回答这个问题,我们首先可以探索一下在非 DevOps 环境中如何进行安全性实践,以及采用 DevOps 后这种实践需要如何改变。

将开发人员和安全工程师聚集在一起

在许多公司中,安全工程师与开发人员通常属于不同的部门。这种分离的背后理念是,保持一定的距离是有益的,即将编写代码的人(即开发人员)与检查代码的人分开。

过去,软件开发人员和软件测试人员之间也常常存在这种分离。然而,最近的研究表明,将开发人员和测试人员放得更近,并不会导致像群体思维、仅测试已知可行的部分或通过仅开发已知测试用例来作弊等不良行为。经验和研究都表明,情况恰恰相反。将开发人员和测试人员放在一起,能产生更高质量的产品。正因如此,像敏捷开发(Agile)这样的运动建议开发团队将测试等学科纳入其中。

正是基于这一思路,将安全工程整合到 DevOps 开发团队中的呼声变得越来越高。这一运动通常被称为“DevSecOps”或“强健的 DevOps”。这两个运动主张,采用 DevOps 原则,如将测试提前(shifting left)和尽可能多地自动化,能够帮助提高安全性。他们主张不再手动进行渗透测试或漏洞审查,而是将其作为交付管道的一部分,完全自动化。这样可以实现自动化、更快的反馈循环和持续交付与部署实践。

也有观点认为,软件发布频率增加还可以进一步提高安全性,原因如下:

  • 当可以使用可靠的机制自动交付软件时,任何解决安全风险的更改都可以在几分钟或几天内部署。能够迅速响应新的发现是一个极大的安全提升。

  • 速度本身也可以是一个安全措施。如果一个系统每天的工作内容发生多次变化,那么在任何时刻要想搞清楚它的内部工作原理并加以滥用将变得极其困难。

  • 应用不可变部署原则,并使用基础设施即代码(Infrastructure as Code)可确保运行应用程序的基础设施得到定期刷新。这是缓解高级持续性威胁(APT)的一个有效方法。

本章将探讨的一个内容是如何配置交付流水线以加入安全扫描。请注意,从流水线运行这些工具是一种不同的实践,它确保这些工具被正确配置,并应用正确的策略和要求。在这些活动中,安全背景和与安全工程师的紧密合作仍然至关重要。这只是另一个可以通过紧密合作产生影响的领域。尤其是在安全问题上,与其他学科的合作将是必需的;并非为了引入手动检查,而是为了共同实现自动化。

安全问题

本章将介绍一些安全问题,但需要认识到,前面的一些章节已经涉及了安全问题。正如你在软件开发中已经知道的,安全并不是仅仅在某个地方添加的东西。安全应当应用于每一个环节。以下图表展示了与软件创建和交付相关的不同活动。在每个活动旁边,列出了相关的安全问题:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/53bc936d-33ad-485a-9d82-684d8268b6d2.png

让我们快速回顾一下每个阶段的安全问题:

  • 主分支合并:在这个阶段,通过拉取请求应用了四眼原则。拉取请求允许另一位工程师在代码合并到主分支之前进行审查。分支策略用于强制使用拉取请求,以确保代码能够编译且单元测试能够通过。这在第二章《一切从源代码管理开始》和第三章《迁移到持续集成》中有讨论,一切从源代码管理开始,以及迁移到持续集成

  • 构建:在这个阶段,通过向构建流水线中添加额外的任务,执行对所有源代码和第三方依赖项的安全扫描。这可以防止安全风险在未受控的情况下扩散。本章的与秘密相关的工作部分将讨论如何进行此操作。

  • 发布:在发布过程中,可以配置审批者。审批者是必须在部署到特定阶段之前给出批准的用户。此外,还使用自动化发布门控,确保(并进一步强制)在发布继续之前满足某些标准。我们将在 第四章,持续部署 中讨论如何做到这一点。

  • 部署环境目标系统):所有应用程序都将在目标环境中运行。它可以是本地的;然而,在本书中,我们的重点是 Azure。对于运行时的安全性和合规性问题,本章将介绍 Azure Policy 和 Azure Security Center。

  • 交叉切割:所有前述的要点只有在 Azure DevOps 环境中有足够的访问控制时才有用。虽然这不在本书的范围内,但它是一个重要的角度需要涵盖。用户应该拥有足够的权限来完成他们的工作,但不应能够对策略、构建和部署过程进行未经授权的更改。此外,适当的秘密管理对于确保在交付过程的各个阶段,诸如证书、密钥和密码等机密信息的安全是必要的。本章还将涵盖我们如何做到这一点。

现在,在理解了软件和安全工程师如何合作开发应用程序之后,接下来是处理这项工作不同方面的内容。本节将讨论如何处理机密。

处理机密

一个重要的安全元素是机密的处理。在部署应用程序时,始终涉及到机密。尤其是在将应用程序部署到云端,即通过互联网部署时,以安全的方式处理这些访问密钥非常重要。除了部署所需的机密外,还有一些机密需要插入到应用程序的运行时配置中。一个常见的例子是访问数据库的机密。

在 第六章,基础设施和配置即代码 中,讨论了多种交付应用程序配置的机制,包括 Azure 资源管理器 (ARM) 模板。然而,模板需要输入外部机密,因为它们不能存储在源代码控制的参数文件中。

机密不应存储在源代码控制中。

如果机密不能存储在源代码控制中,那么应该将它们存储在哪里呢?常见的选择包括将机密存储在服务连接中或在变量组中存储。

将机密存储在服务连接中

部署任何应用程序所需的第一组机密是那些用于连接目标系统的机密。没有任何个人应访问这些机密,因为它们只在部署过程中使用。这就是为什么 Azure Pipelines 允许你将它们安全地存储在服务连接中的原因。

服务连接是一个抽象,表示可以从 Azure DevOps 连接的另一个系统。服务连接有一个特定的类型,即指定它们可以连接的系统系列。例如,有用于连接到 Azure、GitHub、Jira、NPM、NuGet 及其他十多个系统的服务连接类型。也可以通过 Azure DevOps 扩展机制添加新的服务连接类型。

服务连接可以包含对另一个系统位置的引用——通常是一个 URL。在位置旁边,它们可以包含授权令牌、用户名和/或密码,具体取决于服务连接的类型。存储在服务连接中的秘密不能再被取回,甚至管理员也不能访问。此外,每当服务连接的任何详细信息发生更改时,必须重新输入秘密。这是为了防止之前输入的秘密被滥用来访问另一个位置。这些细节表明,服务连接设计用于为存储连接凭据提供安全位置。

服务连接可以在每个 Azure DevOps 项目的中央位置进行管理。您可以创建新的连接,编辑现有的连接,修改用户权限等。通过以下步骤进行操作:

  1. 要打开此视图,请导航到“项目设置”。一个垂直的设置选项列表将会打开。

  2. 从列表中点击“服务连接”。您将能够查看各种连接,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/9bcde9c0-1f2a-4bcf-a868-3b5df785685a.png

  1. 现在,如果您希望创建新的服务连接,请点击屏幕右上方的“新建服务连接”按钮。

  2. 要修改现有条目,只需点击它。这将带您到一个与以下截图类似的屏幕:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/f0a3719a-b771-4afd-b453-648effe9d6f4.png

从此视图,您现在可以执行以下操作:

  1. 编辑服务连接详细信息。

  2. 修改用户权限。

  3. 限制权限。

  4. 添加更多用户或组,并指定每个用户是否可以使用或管理该端点。

  5. 指定哪些管道可以使用此服务连接。

在当前视图中,项目中的每个管道都可以使用该服务连接。这不推荐使用,并且可以通过“限制权限”按钮(3)来加固。在加固管道后,每个希望使用该服务连接的管道必须首先由服务连接管理员授权。

将秘密存储在变量组中

应用程序开发中涉及的秘密比连接其他系统所需的要多。举例来说,包括在应用程序编译期间需要的许可证密钥,或者在部署后需要传递给应用程序的数据库用户名和密码,或者作为 ARM 模板部署的一部分。

这些密钥可以存储在管道变量或变量组中,我们在第三章的在 Azure DevOps 中创建构建定义部分中已涵盖。微软将安全地存储所有标记为密钥的变量,并使其通过用户界面不可检索。

然而,可能有一些原因不希望将密钥存储在 Azure DevOps 中,而是希望将其存储在专用的密钥存储库中,如 Azure 密钥保管库。这样做将提供密钥保管库附带的额外保障,并且能够通过Azure 基于角色的访问控制 (**Azure **RBAC) 和密钥保管库访问策略进一步控制访问权限。

将密钥存储在 Azure 密钥保管库中时,它们仍然可以作为变量组使用,通过通过服务连接将空变量组连接到密钥保管库,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/0c867831-d8e7-4f80-9ad1-bbebbf60e887.png

要将密钥保管库用作变量组的存储,请执行以下操作:

  1. 启用第二个滑动开关,以从密钥保管库加载密钥。

  2. 从下拉菜单中选择一个已存在的 ARM 服务连接,或通过从列表中选择 Azure 订阅,动态创建一个新的带有托管身份的 Azure 服务连接。

  3. 输入应加载密钥的密钥保管库名称。你也可以从下拉菜单中选择一个。在这种情况下,只有所选服务连接可访问的密钥保管库才会显示。

  4. 建议禁用允许访问所有管道的滑动开关。通常,开放授权被认为是一个风险,尤其是包含密钥的变量组应仅对明确授权的用户可用。

  5. 可以通过“安全”标签页为特定用户配置访问权限。

还可以自动创建服务连接到 Azure 和密钥保管库所需的正确授权。请注意,这两个操作都会更改 Azure 安全设置,因此请确保这些设置(仍然)正确。

检测不安全的密钥

如前所述,不应将密钥存储在源代码控制中,这就是为什么之前讨论的功能可用的原因。然而,开发人员可能会因意外或进行本地测试而将密钥写入应用程序源代码中。

为了确保这些秘密不会被提交到源代码管理中,可以使用本地插件来检测秘密并发出警告,提醒开发人员这个风险。一个可以为 Visual Studio 实现这一功能的工具是Visual Studio 的持续交付工具扩展。此扩展会扫描任何打开的文件中的秘密,并在检测到可能的秘密时发出编译器警告。有关此扩展的链接已添加到本章末尾的参考资料中。安装程序运行后,任何在 Visual Studio 中检测到的秘密都会导致编译器警告。遗憾的是,在编写本文时,该扩展尚不支持 Visual Studio 2019。

除此之外,建议将类似的工具作为交付流水线的一部分运行,以识别任何被意外提交的秘密。尽管这时候保护秘密已经太晚,但它确实提供了一个明确的信号,表明秘密已经泄露,需要更换。一个可以做到这一点的工具是CredScan。CredScan 是 Microsoft Security Code Analysis Extension 构建任务的一部分。

Microsoft 安全代码分析扩展不仅仅包含 CredScan。它还包括 Microsoft 提供的其他安全工具。

该扩展的详细信息链接可在本章末尾找到;其中还包括所有安装细节。请注意,该扩展仅在特定条件下可用,并且并非免费。

扩展安装完成后,可以将 CredScan 添加到你的流水线中,如下所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/2d4bc1d8-351f-4bbd-83e5-e088d8eb42c2.png

在参考截图中的注解时,执行这些步骤:

  1. 运行凭证扫描器任务添加到流水线中。

  2. 将工具的主版本更新为 V2。对于所有其他选项,默认设置对于首次扫描已经足够。

  3. 如果之前的扫描导致一个或多个误报,可以通过指向抑制文件将其从结果中移除。

  4. 将创建安全分析报告的任务添加到流水线中。

  5. 将发布安全分析日志的任务添加到流水线中。

  6. 将后分析任务添加到流水线中。

  7. 保存并排队构建定义。

虽然某些任务会在检测到错误时失败并取消构建,但 CredScan 任务则不会。即使检测到密码,它也总是会成功完成。只有在构建结束时的后分析任务会对发现的问题进行处理,并在发现问题时使构建失败。这样做的好处是,所有问题都会被识别,而不仅仅是第一个问题。这也允许任何其他任务在完成后继续执行。

安全分析报告任务(第 4 步)用于收集属于工具套件的一些扫描工具的日志,并将输出汇总成 CSV 和 HTML 文件。发布任务(第 5 步)将所有生成的文件作为构建产物发布。如果检测到可能的密码,以下 HTML 将被生成并作为构建产物发布:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/8b12d7d2-a489-41af-ab81-fc5697587528.png

本节内容到此结束,介绍了如何在 DevOps 流水线中保持秘密的安全。下一节将讨论应用程序漏洞的检测。

检测应用程序代码漏洞

在 DevOps 文化转型之前,通常定期进行的安全评估不能完全被忽视。这意味着,不能把它们省略,而是必须以其他方式进行。对此有两种方法。

第一种方法是继续定期进行渗透测试、安全评审和其他安全检查,就像以前一样。然而,代码不是等到通过测试后才进入生产环境,而是与安全评估分开直接部署到生产环境。这意味着存在一个接受的风险,即可能会有漏洞被部署到生产环境,直到下一次安全扫描时才会发现,并将在下一个版本中解决。采用这种方法,可以提高速度,但也需要接受一些漏洞可能会存在一段时间。

第二种方法是将应用程序安全扫描纳入提交代码到源代码仓库的常规工作流程中。例如,安全代码评审不需要每次增量更新时都做,也不需要每两个月做一次。它们也可以在每次拉取请求时进行——在代码合并之前。这样一来,你就不再是检测漏洞,而是在防止漏洞的产生。安全漏洞扫描也可以采用同样的方式,它们可以成为交付流水线的一部分,或者成为一个完整的夜间 QA 构建,每天早上报告开发质量。

当然,这种方式往往不是非黑即白,许多公司会采用这些方法的组合。他们使用自动化反馈机制来检测能发现的漏洞,将安全代码评审纳入拉取请求工作流,然后结合定期的人工渗透测试。通过这种方式,交付速度得以提高,同时安全风险并没有增加,甚至因为能够快速缓解漏洞而有所减少。

OWASP 前十

在谈到网页应用的安全性时,有几种类型的安全问题是常见的,并且负责大多数所有安全问题。这些问题类型被称为 OWASP Top 10。这是由开放网页应用安全平台OWASP)发布的十大常见安全问题列表。该列表每隔几年会进行审查,但在过去几年里保持了相当的稳定性。

OWASP Top 10 中的大多数错误可以通过实施自动化安全测试来预防;无论是通过使用静态代码分析来发现安全漏洞,还是通过使用OWASP Zed Attack ProxyOWASP ZAP)进行动态测试。

实施自动化漏洞扫描

在前一章节中,我们讨论了持续测试,并已经介绍了 SonarCloud 作为技术债务和代码质量的代码扫描器。除了评估应用代码的质量外,SonarCloud 还可以用来扫描安全漏洞。在第八章《持续测试》中,你已经学会了如何将 SonarCloud 扫描添加到你的流水线中。除此之外,还有其他更专业的工具可用,我们将在本章的最后一节讨论这些工具。

这些工具基于静态测试评估应用程序。它们扫描代码以识别任何有风险的代码。这被称为白盒方法,因为它们可以查看、检查和扫描所有代码。换句话说,所有内容都是可见的。这与黑盒方法相反,在黑盒方法中,运行中的应用程序被视为一个封闭的整体,只通过调用它并观察响应来进行测试。一个可以实现这一点的工具是 OWASP ZAP。

OWASP Zed Attack Proxy

OWASP ZAP是一个可以执行应用自动化渗透测试的工具。该工具可以以两种模式运行:

  • 基线扫描:基线扫描只需几分钟,并且在这几分钟内优化为尽可能多地扫描安全风险。这使得基线扫描足够快速,可以在部署流水线的早期运行。甚至可以在每次部署到第一个测试环境后运行安全扫描,从而为开发人员提供快速反馈。

  • 完整主动扫描:完整主动扫描需要更多时间。在这种类型的扫描中,代理将检查应用的每个响应,以识别属于该应用的其他 URL,并对其进行扫描。通过这种方式,完整的应用程序会在运行时被发现,采用蜘蛛爬行的方法。这种扫描方式更加全面,但也需要更多的时间。因此,完整扫描通常是定期运行的,例如每晚一次。

OWASP ZAP代理尝试识别任何可能的安全风险。一些最显著的风险包括 SQL 注入、JavaScript 反射和路径遍历。

OWASP ZAP 是一个可以安装在任何虚拟机上的应用程序。它的缺点是虚拟机始终在运行,即使没有正在进行的扫描。这会增加成本,当然,虚拟机本身也需要进行补丁更新和安全防护。最近,代理的容器化版本也已推出。这个容器可以在 Azure 容器实例中运行,仅在需要时启动代理,并在执行完毕后立即关闭。

这完成了我们对代码扫描工具及其实施的介绍。借助这些工具,您可以检测应用程序中的漏洞并防止安全问题。接下来的部分将探讨如何扫描应用程序的依赖项。

使用依赖项

除了应用程序代码开发中的安全风险外,还有与重用组件相关的风险。现代应用程序代码中,50% 到 80% 不是在内部开发的,而是通过包或依赖项从其他方获得的。其中一些可能是开源的,但并不一定是。也有一些组件是从其他开发公司购买的,或者是从如 NuGet 这样的库中获取的二进制文件。

依赖不仅会带来安全风险,还可能带来许可风险。如果一个团队开始使用一个根据 GPL 许可证发布的组件来构建闭源组件会怎样?如果有人发现了,团队可能被迫将其产品开源,或者至少因为未按照许可证使用他人的作品而遭遇公众羞耻。

为了减轻这些风险,可以使用多种工具来检测和扫描在构建应用程序时使用的所有依赖项。WhiteSource Bolt 是其中一个可用的工具,它作为扩展从 Azure DevOps 市场提供。

使用 WhiteSource Bolt

要开始使用 WhiteSource Bolt 执行扫描,请执行以下操作:

  1. 从 Azure DevOps 市场安装 WhiteSource Bolt 扩展。

  2. 在管道下导航至 WhiteSource Bolt 菜单。

  3. 注册并接受许可条款。

  4. 将 WhiteSource Bolt 扫描任务添加到构建或发布定义中,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/1b58e509-8bac-44d0-b335-0112b65dc334.png

  1. 一旦安装了 WhiteSource Bolt 任务的管道运行完毕,包含构建结果的页面将会显示一个额外的标签,名为 WhiteSource Bolt Build Report,其中展示了扫描结果,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/b2988506-f292-417e-8e0f-846debbe08d8.png

该报告提供了关于扫描的应用程序构建的整体安全性和许可风险的若干见解:

  • 顶行的四个小部件提供了漏洞评分的概览,并且有三种不同的细分方式,展示了该评分是如何计算的。

  • 在此下方,所有脆弱的包按名称列出,提供依赖项的参考和推荐的缓解措施。

  • 底部的部分提供了所有依赖项使用的许可证列表。该列表按风险高低排序。

  • 在此概述下方,WhiteSource Bolt 还生成了一个依赖项列表,列出了可用新版本的依赖项(在前面的截图中未显示)。

该报告中显示的结果也可以通过 WhiteSource Bolt 菜单中的 Pipelines 菜单访问。在此视图中,可以访问所有构建的所有报告。此视图非常适合负责跨项目或组织访问安全性或许可标准的人。

这完成了我们对依赖扫描的讨论。如前所述,您可以利用这些工具来检测和扫描构建应用程序时使用的所有依赖项。在下一节中,将介绍基础设施合规性。

确保基础设施合规性

另一个重要的话题是合规性。在许多国家或市场中,在创建软件时,必须实施或遵守一套规则和政策。相当一部分这些政策与应用程序运行的基础设施相关。如果该基础设施部署和管理在 Azure 平台上,Azure Policy 可以成为确保基础设施符合规定的强大工具。

在第六章《基础设施与配置即代码》中,讨论了 ARM 模板的话题。ARM 模板可以看作是一种技术,用于将完整的 Azure 环境描述为一个包含许多对象的 JSON 数组,每个对象描述应用程序基础设施中的一个资源。

Azure Policy 允许您编写查询此文档以及通过任何 API 或 ARM 模板所做更改的策略。每当找到与查询匹配的资源时,它将被阻止创建,或者该匹配项可以添加到审计结果列表中。

除了编写自定义策略外,还有许多现成的策略可供所有 Azure 用户使用。这些策略可用于审计不符合最佳实践或一般建议的资源。还提供了名为“倡议”的策略组,描述了市场标准的适用部分。

分配 Azure 策略或倡议

策略可以在 Azure 的不同级别上进行分配,既可以在资源组级别、订阅级别或管理组级别。这可以通过门户、ARM 模板或蓝图,或 PowerShell 来完成。

使用 PowerShell 时,可以使用以下一系列命令:

  1. 要检索资源组和策略的参考,请使用以下命令:
$rg = Get-AzResourceGroup -Name myResourceGroupName
$definition = Get-AzPolicyDefinition | Where-Object { $_.Properties.DisplayName -eq 'Audit VMs that do not use managed disks' }

此处选择的策略是一个内置策略,将审计所有未使用托管磁盘但在存储帐户中有自定义磁盘的虚拟机。此策略定义将在以下任务中的命令中使用。

  1. 要将策略分配到资源组,请使用以下命令:
New-AzPolicyAssignment -Name 'audit-vm-manageddisks' -DisplayName 'Audit VMs without managed disks Assignment' -Scope $rg.ResourceId -PolicyDefinition $definition

在此任务的 30 分钟内,新策略将变为活跃状态。此时,策略评估周期开始,所有在任务范围内的资源将与策略进行评估。编写时,尚未发布有关此类评估周期所需时间的服务级别协议(SLA)。根据经验,这一过程可能需要 15 分钟到多个小时不等——具体取决于任务范围的大小。

编写 Azure 策略

虽然有许多内置的策略可用,但也有许多使用场景需要创建自定义策略。与其他 Azure 资源类似,策略也是以 JSON 文档的形式编写的。适当的 ARM 资源类型是 policyDefinitions,其结构如下:

{
   "name": "string",
   "type": "Microsoft.Authorization/policyDefinitions",
   "apiVersion": "2019-01-01",
    "properties": {
      "parameters": {
        “location”: { …}
      },
        "displayName": "…",
        "description": "…",
        "policyRule": {
            "if": {
              “field”: “location”,
              “equals”: “[parameters(‘location’)]”,
            },
            "then": {
                "effect": "<audit|deny >"
            }
        }
    }
}

parameters 对象可用于指定在稍后分配策略时需要指定的一个或多个参数。这些参数遵循与 ARM 模板参数相同的语法,并且工作方式相同。

displayNamedescription 属性可用于为策略定义提供有意义的名称和描述,供以后参考。

定义的主体包含两个元素,如下所示:

  • if 语句用于指定一个查询,以选择此策略应应用的 Azure 资源。写复杂查询的 JSON 语法有明确规定,详细信息可见本章末尾链接的 ARM 模板参考资料。

  • then 语句用于描述针对符合条件的资源所需执行的操作。这可以是 拒绝,即自动拒绝创建任何不符合要求的资源。另一种方法是,不直接拒绝不合规的部署,而是对其进行审计。虽然从理论上讲,拒绝不合规的部署非常简单,但在某些情况下,暂时允许不合规的部署是有合理原因的。在这种情况下,审计策略可以帮助跟踪这些资源。所有不合规的部署将在 Azure 活动日志中生成审计记录,并可以在 Azure 门户中的“合规性”标签下的 Azure 策略中查看,具体如下:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/3b4745f2-1655-481e-8fcb-d4f7faf0e76c.png

在编写完政策定义后,我们需要在 Azure 订阅中创建它,以便使其可用。可以通过 ARM 模板或在门户中手动创建。 从 DevOps 的角度来看,建议通过源代码管理编写政策,并通过管道将其作为 ARM 模板的一部分进行交付。 这样,Azure 政策的编写与应用程序相同,可以进行审查,并作为 DevOps 管道的一部分自动部署到 Azure。

计划

在使用 Azure Policy 时,许多公司发现他们需要创建许多政策,以定义他们希望软件开发人员遵循的所有规则。因此,将政策分组可能是有益的。这种分组称为“计划”,这些计划也在 JSON 中定义:

{
  "name": "string",
  "type": "Microsoft.Authorization/policySetDefinitions",
  "apiVersion": "2019-01-01",
  "properties": {
    "displayName": "string",
    "description": "string",
    "parameters": { … },
    "policyDefinitions": [
      {
        "policyDefinitionId": "string",
        "parameters": {}
      }
    ]
  }
}

一个计划的主体是一个对象数组。每个对象必须包含一个 policyDefinitionId 属性,并可以选择包含一个带有 parameters 的对象。 policyDefinitionId 属性必须通过 Azure 资源 ID 引用有效的 policyDefinitionsparameters 数组应指定该政策所需的所有参数。通常,这可以通过让计划指定所有政策的参数集合作为计划参数来实现。然后,个别政策的参数通过引用计划参数来指定。

获取审核结果

在分配了审核效果的政策后,一旦其生效,政策将自动评估分配范围内的所有资源。无法保证此过程需要多长时间。对于新资源,政策评估结果通常会在 15 分钟内显示,但通常这个过程会更快。

一旦结果出来,可以在门户中查看每个政策或计划的合规性状态,最终呈现出如下概览,见下图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/8fe2da7d-64e7-42a8-bc52-f6ff76e8a880.png

该报告与其他手动审核报告的区别在于,此概览会持续更新,以反映实际的、当前的合规性状态——它不是某一特定时点的合规性快照。

这种合规性类型的一个重要好处是,规则或政策会持续应用于所有现有资源和任何新进的变更。这意味着可以确保应用环境始终符合要求,并始终遵守所有适用的规则和政策。

与通常每隔几个月进行一次安全和合规性审核的方法相比,这种方式有很大不同。通常,这会导致环境只有在审核前才符合合规要求,且合规性在审核后逐渐下降。直到下次审核时,合规性才会再次接近 100%。在许多公司中,这导致了如下的合规性图表:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/73896784-11f4-4e25-84f2-6cba850381ea.png

通过这一点,我们讨论了 DevOps 实践如何帮助提高安全性和合规性——通过确保基础设施合规性。接下来的章节将讨论本章提到的工具的几种替代方案。

监控和检测运行时安全风险与威胁

迄今为止讨论的所有安全工具都集中在防止将存在漏洞的代码推送到生产环境。然而,完整的已部署软件解决方案,包括所有支持的基础设施,远不止是代码。除此之外,解决方案中可能有许多意外或未计划的交互。在生产环境中持续监控这些内容是必要的,不仅是为了防止安全问题,还要检测任何潜在的安全问题。在 Azure 中,Azure 安全中心就是一个可以实现这一目标的工具。Azure 安全中心通过 Azure 门户提供,可以像其他服务一样从左侧菜单选择或通过顶部搜索栏搜索。

打开安全中心后,会显示类似于以下截图的内容:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/3e3f0a00-e201-42f8-8636-ba95b0082c09.png

此仪表板提供了三大类的洞察:

  • 策略和合规性:此部分概述了所有选定的 Azure 订阅在你配置的安全策略下的合规性状况。

  • 资源安全卫生:Azure 提供了许多安全控制,可以开启或关闭,并且有许多安全配置设置。就像其他地方一样,用户需要平衡成本、安全性、风险和易用性。此仪表板将显示有关提升资源安全性的一些建议。用户可以根据每个建议决定是否采纳。

  • 威胁防护:此部分显示已自动检测和报告的威胁或攻击数量:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/ac28bd46-d684-4cb4-9a4f-0207bee64198.png

所有这些概述和类别都可以进一步钻研。上面的例子显示了打开威胁防护概览的结果。在这里,它列出了它所识别的所有可能的安全威胁。在此案例中,它列出了对在订阅中托管的虚拟机的不同访问尝试。

Azure 安全中心还拥有许多其他功能,并且这些功能正在持续增加。在 Azure 部署时,这是识别和管理安全风险的地方。

这就是我们讨论如何监控运行时环境安全风险的各类技术的结束。接下来的章节将介绍一些替代工具,用于执行之前章节中提到的扫描任务。

你可以使用的其他工具

市场上有许多工具可以用于执行应用程序代码和依赖关系的安全扫描。例如,WhiteSource、Black Duck、Veracode 和 Checkmarx。

WhiteSource 是 WhiteSource Bolt 的付费版本。它提供相同的服务以及更多功能。例如,它不仅在依赖关系扫描时报告风险;它还会在新的风险出现时提醒你,特别是对于在应用程序上次扫描中存在的依赖关系。

Black Duck 是一款帮助团队管理使用开源软件所带来的风险的产品。它提供的服务与 WhiteSource 类似。

VeracodeCheckmarx 是用于识别漏洞代码的代码扫描工具。与 SonarQube 同时检查代码质量和安全风险不同,这两个产品专注于安全风险。总体来说,它们在安全扫描方面表现更好,缺点是价格更高。

总结

在本章中,你已经学到,DevOps 和安全并不是两个对立的目标,DevOps 实践可以帮助你强化安全性。首先,你学习了在处理持续部署管道时如何处理密码和其他机密。接下来,你学会了如何通过代码和依赖关系扫描工具增强你的管道,并将安全的 shift-left 原则应用其中。最后,你学会了如何使用 Azure Policy 来定义基础设施的约束和规则,以及如何自动应用这些规则或审计不合规的部署,甚至自动拒绝。

通过你所学到的知识,你现在能够与公司内的同事讨论如何解决 DevOps 团队中的安全问题。你可以与安全工程师合作,配置你所使用的工具,并获得关于你工作中安全影响的自动反馈。

在下一章中,你将学习应用程序监控。此外,你还将学习如何监控应用程序是否平稳运行,以及如何收集运行时指标。

问题

下面是一些问题,供你测试自己对本章内容的理解。你可以在附录中的评估部分找到答案:

  1. 判断题:确保软件交付的安全性只是部署管道中的一个步骤。对或错?

  2. 哪种工具可以用于安全测试,其中代理用于识别有效的应用程序 URL,然后对应用程序进行不同的攻击,如注入?

  3. 判断题:在大多数现代应用程序中,超过 50% 的代码来自开源库。对或错?

  4. 在部署或运行应用程序时,需要哪些安全位置来存储机密?(你可以选择多个答案。)

    1. 标记为机密的 Azure Pipelines 变量

    2. Azure 密钥库

    3. Azure DevOps 密钥库

    4. Azure 变量组

    5. Azure DevOps 安全变量

    6. Azure DevOps 服务连接

  5. 哪两个 Azure 产品可以用于在运行时检测安全风险?

进一步阅读

第三部分:闭环

在本节中,您将了解到,DevOps 不仅仅是为了更快地将代码推送到生产环境,或是每周交付更多的迭代。另一个重要的方面是观察和衡量已经交付的软件,以此来引导未来迭代的方向。本书的这一部分将涵盖应用监控和用户反馈。

本节包括以下章节:

  • Chapter 10,应用监控

  • 第十一章,收集用户反馈

第十章:应用程序监控

在前几章中,你学习了如何将 DevOps 原则应用到软件交付中。你学习了如何创建从源代码管理到生产的流水线。你还学会了如何确保你的交付符合合规要求和安全标准,同时不牺牲速度或忽视业务价值的交付。在本章中,你将学习如何将这个流水线转变为一个 DevOps 循环,持续交付新软件,并衡量应用程序的表现。这是一个持续的过程,在生产环境中评估应用程序的表现,并学习如何决定下一步的方向。

为了实现这一目标,本章首先介绍了一种收集应用程序崩溃报告的方法。几乎每个应用程序在某个时候都会抛出未处理的异常并崩溃。确保收集和报告应用程序崩溃,可以帮助你调查崩溃的原因并解决它们。接下来,重点转向应用程序的仪器化。仪器化是收集日志和指标的实践,它们可以帮助你了解应用程序在生产环境中的表现。你可以利用这些数据在问题发生时发出警报,或者希望能够在问题发生之前提前发现。最后,本章还探讨了与其他工具集成的几种选择。

本章涵盖以下主题:

  • 调查应用程序崩溃

  • Web 应用程序仪器化

  • 与其他工具集成

技术要求

要尝试本章中描述的技术,你将需要以下之一或更多:

  • 用于收集移动应用程序崩溃的 App Center 账户

  • 用于收集桌面应用程序崩溃的 Raygun 订阅

  • 用于对 Web 应用程序进行仪器化的 Azure 订阅

所有这些都提供免费试用选项。

调查应用程序崩溃

无论应用程序设计得多么完美,最终都会因某些意外情况而崩溃。为了从这些崩溃中学习并尽量防止未来发生类似问题,可以向应用程序添加代码来收集崩溃报告,并将其发送到一个中央位置。在这里,这些报告可以被分析并归类,以识别应用程序的改进方向。具体如何操作取决于应用程序的类型。

以下部分将讨论移动应用程序和桌面应用程序的崩溃报告收集过程。关于 Web 应用程序,崩溃报告的收集可以使用与仪器化相同的工具;我们将在后面的Web 应用程序仪器化部分讨论这个内容。

收集移动应用程序崩溃报告

许多用于收集移动应用程序崩溃报告和错误的工具之一是 Visual Studio App Center。除了分发移动应用程序外,App Center 还允许应用程序提交崩溃和错误进行分析。

要开始使用 App Center 进行崩溃报告,首先需要定义应用程序。这被称为应用程序定义,如何操作它在第四章《持续部署》中进行了讨论。通过这个应用程序定义,可以创建一个应用程序密钥,这对于配置应用程序以发送崩溃报告是必需的。要开始发送崩溃报告,需要执行以下步骤:

  1. 在项目中安装 Microsoft.AppCenter.Crashes NuGet 包。

  2. 将以下代码添加到应用程序初始化中:

AppCenter.Start("ios={appSecret};android={appSecret };uwp={appSecret}", typeof(Crashes));

除了崩溃外,还可以跟踪开发者感兴趣的其他错误。可以使用以下代码来完成此操作:

Crashes.TrackError(ex);

现在,所有未处理的异常都会自动捕获并发送回 App Center。在这里,它们可以进行分析,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/f4aca831-a120-44e4-97fe-a70eedfaec1f.png

  1. 点击任何已报告的错误或崩溃,查看详细信息,如下所示:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/1ee587c6-a5fe-4492-8152-41e45a97463e.png

每个崩溃或错误都会显示一个包含最重要信息的仪表板。这包括报告的数量和受影响用户的数量。同时,还会显示受影响的设备类型和操作系统。在页面顶部,显示了堆栈跟踪,开发者可以使用这些信息来调查并且希望能够修复问题。

这部分内容涵盖了从移动应用程序收集崩溃报告和错误。接下来的部分介绍了桌面应用程序的相同概念。

收集桌面应用程序的崩溃报告

崩溃报告同样适用于桌面应用程序。同样,桌面应用程序有许多解决方案,而且它们大多数的工作原理差不多。这些解决方案之一是 Raygun。Raygun 是一个商业服务,适用于 .NET 应用程序,但也支持许多其他语言和平台。

要使用 Raygun 收集崩溃报告,请按照以下三步操作:

  1. 注册 Raygun 账户。

  2. 在解决方案中安装 Mindscape.Raygun4Net NuGet 包。

  3. 捕获未处理的异常并将其转发到 Raygun。

以下示例展示了如何捕获并将未处理的异常转发到 Raygun:

 class Program
    {
        private static readonly RaygunClient _raygunClient = new RaygunClient("myKey");

        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.UnhandledException += HandleEx;
            throw new Exception("Boom!");
        }

        private static void HandleEx(object sender, UnhandledExceptionEventArgs e)
        {
            _raygunClient.Send(e.ExceptionObject as Exception);
        }
    }

完成此操作后,可以在 Raygun 网络界面中查看所有异常。在这里,如果堆栈跟踪足够相似,异常会自动分组。也可以单独查看并浏览它们,但在大多数情况下,只关注更大的异常组是有意义的。

以下截图展示了如何在 Raygun 中浏览这些组:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/f11e73bb-a904-4bb6-b4be-5e5cecf9b3df.png

点击此界面中的异常消息,可以查看完整的堆栈跟踪以及该异常实例的所有共享属性。

这部分内容结束了我们对移动和桌面应用程序崩溃报告收集的讨论。通过这样的做法,你可以发现并调查客户在生产环境中遇到的问题。在接下来的部分,将介绍 web 应用程序的监控,以进一步增强我们对应用程序在生产环境中表现的理解。

网络应用的监控

网络应用程序在许多方面与移动应用和桌面应用程序不同——例如,大部分应用程序代码并不在客户端运行,而是在服务器上运行。这使得开发人员可以比其他类型的应用程序更容易地收集关于 web 应用程序运行方式的信息。这个过程称为“监控应用程序”。

日志是系统保存的文本消息,用于描述服务器执行路径。这有助于开发人员通过检查日志输出回溯并探索发生过的事情。结构化日志正迅速成为追踪日志的标准。结构化日志是一种技术,其中日志不再只是文本消息,而是带有每个参数一组值的参数化文本消息。这样有两个优势——日志可以更好地压缩,并且可以更快速地进行搜索。

指标是记录应用程序数据的值。它们包括时间戳、指标名称和数值。例如,每秒记录一次 CPU 使用百分比。

在监控应用程序时,很容易集中关注许多服务器级别的日志和指标。例如,许多操作员默认会开始收集诸如 CPU 使用率、内存压力和 I/O 操作等指标。虽然这些指标没有错,但它们并不总能反映从用户角度来看应用程序的性能。其他指标,如响应时间或队列消息处理延迟,可能会提供更好的用户体验洞察。虽然衡量系统指标是没有问题的(它们通常是未来问题的良好指示),但你也应该尝试收集以用户为中心的指标。

Azure 提供了 Application Insights 服务来监控应用程序,重点是 web 应用程序。可以通过 Azure 门户创建 Application Insights 工作区,这将打开如下截图所示的工作区。这里需要注意的一个重要内容是“Instrumentation Key”,它将在后续章节中使用。尽管该字段明确显示,但建议将其视为应用程序的密钥:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/29134293-71b6-495b-91b5-d6b2d63a373c.png

如果你想通过一个现成的应用程序来实验 Application Insights 和 Azure Monitor,本章末尾的 进一步阅读 部分提供了一个示例链接。该示例是一个简单的 URL 缩短应用,使用了多个应用组件,并且内置了日志记录和监控,可以用来在几分钟内开始实验本节介绍的概念。

以下小节将详细介绍日志记录、度量以及如何调查单个请求。

日志记录

最基本的仪器化类型之一是将日志语句添加到应用程序代码中。过去,这些日志被保存在运行应用程序的服务器的磁盘上。然后,检索和调查这些日志需要花费大量的时间和精力。

在现代托管环境中,日志不再保存在本地文件系统中,而是远程存储。随着临时基础设施和服务器的动态增减,无法再将日志保存在服务器上,并确保以后能够检索它们。因此,日志通过 HTTP 传输到专门的日志存储中,如 Application Insights。

发出日志

要将日志条目写入日志存储(例如 Application Insights)中,必须完成以下两项操作:

  1. 日志条目需要在应用程序代码中发出,适用时,使用 ILogger 接口。此接口可通过 Microsoft.Extensions.Logging.Abstractions NuGet 包获得。

  2. 需要安装 Application Insights NuGet 包(Microsoft.ApplicationInsights.AspNetCore),并且需要将 Application Insights 注册为 LoggingProvider。这样,所有发送到前述接口的日志将被转发到 Application Insights 代码中。反过来,这些代码会将所有日志转发到 Application Insights 服务。

以下示例代码展示了如何通过类中的 ILogger 接口生成结构化日志条目:

public class Example
{
    private readonly ILogger<Example> _logger;

    public Example(ILogger<Example> logger)
    {
        _logger = logger;
    }

    public void DoSomething(User user)
    {
      _logger.LogWarning(
        "Doing something for user with id '{userId}' and username '{username}'", 
        user.Id, 
        user.Username);
    }
}

日志条目开头不应有美元符号($)。这里没有使用字符串插值,但文本消息中插入了两个占位符。结构化日志条目将识别这些占位符,并在显示日志条目时插入提供的值。

在生成日志条目后,应注册日志提供程序以捕获这些日志。可以通过 .NET Core 内置的依赖注入来完成此操作。

安装 Application Insights NuGet 包后,需要在 CreateWebHostBuilder 方法中添加以下代码:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) 
{
  return WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
    .ConfigureLogging(builder => {
        builder.AddApplicationInsights(“myKey”);
    }
}

当使用版本 2.7.0-beta3(或更高版本)的 Application Insights NuGet 包,并且使用 Application Insights 来获取度量数据时,前述配置不再需要。

启动应用程序后,所有警告级别及以上的日志条目会自动转发到 Application Insights。要更改转发哪些条目,哪些不转发,可以配置过滤器。本章最后将提供关于如何详细配置 Application Insights 的更多链接。

搜索日志

在将日志条目发送到 Application Insights 后的几分钟内,它将出现在界面上,供查询使用。为此,打开 Application Insights 实例并导航到左侧菜单中的日志(Analytics)(1)。这将打开如下所示的视图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/f220ec5b-f232-4cff-9b39-180f80791a21.png

在这里,可以编写查询(2),用于搜索记录的日志,使用的是Kusto 查询语言KQL)。Application Insights 已优化以处理大量数据,大多数查询的返回结果在一秒钟内,甚至在搜索数百万条日志条目时也能快速返回。

对日志进行警报

收集和搜索日志在排查特定情况或响应用户投诉时非常有用。然而,在某些情况下,当特定条件出现时,最好能够自动收到通知。这就是所谓的警报。

在 Azure 中,可以创建警报规则,当某个条件满足时,开发人员将收到通知。警报功能由 Azure Monitor 提供,且与许多 Azure 服务(包括 Application Insights)集成。

要创建新的警报规则,请按照以下步骤操作:

  1. 使用门户导航到 Azure Monitor。

  2. 现在,选择警报。这样会打开以下截图所示的视图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/d3366681-8a38-4547-af41-060ee38b8ee4.png

如果有任何需要关注的警报,它们会在这里显示。

  1. 要添加新的警报规则,请使用屏幕左上方的按钮。点击后会打开另一个视图,如下截图所示。在这里,可以配置警报条件:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/a964d411-6bf6-43f4-9429-bef7d22d199f.png

在前面的截图中,左侧显示了用于配置警报的视图。在这里,必须做出多个选择来创建一个警报:

  1. 这是作为警报主题的资源。它可以是任何类型的资源,在本示例中,警报将针对 Application Insights 工作区。

  2. 这是设置警报的条件。要选择这些条件,右侧会弹出窗口。在这里,可以在不同类型的警报之间进行选择。选择“日志搜索”警报类型,将打开如下所示的详细视图。在这里,必须做出以下选择:

    • 对追踪日志的查询(参考前面截图中的 2a):在此示例中,查询追踪日志中的条目,这些条目的严重性为 4 或更高,意味着它们是通过 LogWarningLogCritical 方法发出的。

    • 触发警报的条件和操作符(2b):在此情况下,只要有一个或多个匹配,警报就会触发。

    • 评估警报条件的间隔(2c):当指定一个匹配特定数字的查询时,这决定了必须满足该数量的时间间隔。

    • 评估警报条件的频率(2d):过于频繁地评估警报条件可能导致警报以快速的系列频繁开关。过于不频繁地评估警报条件可能会导致警报来得太晚。实验将帮助你了解如何配置这一点。

  3. 这是在警报条件满足时执行的操作。由于可能有很多警报需要调用相同的操作组,因此可以将操作分组,并在此处引用这些操作组。操作的一些示例包括调用 Webhook 或发送短信或电子邮件。

  4. 警报配置通过输入名称和描述来完成。

  5. 最后,警报可以被保存。

警报创建并激活后,系统会自动完成这一过程,几分钟内,警报就准备好检查应用程序日志,并在满足警报条件时发出信号。

日志记录是深入了解请求发生了什么以及错误如何产生的好方法。另一种了解应用程序行为的技术是使用指标。

指标

除了日志记录外,应用程序还可以发出一个或多个指标。指标是随着时间变化的一系列值,描述系统的一个或多个方面。一些指标的示例如下:

  • 当前登录的用户数量

  • 用户查看的产品数量

  • 数据库事务的数量

收集这些指标可以提供关于系统如何使用以及当前如何操作的洞察。指标通常用于创建仪表盘和警报。

发出指标

要开始使用指标,首先必须由应用程序发出指标并存储在集中位置。除了日志记录外,应用程序洞察还可以用于获取指标。

要使用应用程序洞察来获取指标,需要执行以下步骤:

  1. 指标需要从应用程序代码中发出(在适用的情况下),使用TelemetryClient类。此接口可以从Microsoft.Extensions.Logging.Abstractions NuGet 包中获得。

  2. 安装Microsoft.ApplicationInsights.AspNetCore应用程序洞察 NuGet 包。

  3. 使用TelemetryClient注册到Dependency容器。通过在容器构建器上使用扩展方法来完成,如以下代码片段所示:

builder.RegisterType<TelemetryClient>().SingleInstance();
  1. 完成此操作后,应用程序准备开始发出指标。可以使用TelemetryClient类来完成:
public class Example
{
 private readonly TelemetryClient _telemetryClient;

 public Example(TelemetryClient telemetryClient)
 {
     _telemetryClient = telemetryClient;
 }

 public void DoSomething()
 {
     _telemetryClient.GetMetric(“doSomethingCalledCounter”).TrackValue(1.0);
 }
}

发出一个指标涉及两个步骤。首先,使用GetMetric()方法获取该指标的引用。接下来,使用TrackValue方法提交一个值。提交的值应为双精度数或允许隐式转换为双精度数。

一旦指标发出,就可以用来创建图表和指标。然而,在继续讨论这些主题之前,首先要讨论另一种类型的指标——即 Azure 平台指标。

除了应用程序发出的指标之外,还有许多可以从运行系统的 Azure 平台中记录的指标。以下是一些示例:

  • CPU 使用百分比

  • 服务总线上的消息数量

  • 每秒数据库事务数

  • 可用的磁盘空间

这些指标通常与应用程序的性能密切相关,甚至可能是领先指标。例如,当可用磁盘空间达到 0 时,大多数 Web 服务器会停止工作。

在 Azure 中,每个服务默认会发出一系列的指标,这些称为平台指标。不同的服务发出的指标各不相同,且用户无法影响这些指标的生成。Azure Monitor 也会自动收集这些指标,这些指标可以像应用程序发出的指标一样用于绘图和警报。第一个要探讨的内容——绘图——将在下一节中介绍。

平台指标是内置的并且免费提供,且保留 93 天。

绘制指标

所有收集的指标,无论是在 Application Insights 中还是在 Azure Monitor 中,都可以用于构建可视化指标的图表和仪表板。可以使用每个 Azure 资源中可用的“指标”选项卡来创建图表。也可以使用 Azure Monitor 提供的工具来创建图表,这样,可以在一个画布上合并多个资源的图表。操作步骤如下:

  1. 打开 Azure Monitor,可以从左侧菜单访问。

  2. 导航到“指标”菜单。这将打开如下所示的视图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/9285c483-99c6-4df1-8e06-edafd069a3ff.png

  1. 一旦画布打开,可以将一个或多个图表添加到其中。图表是通过顶部的图表构建器构建的。这里需要做四个选择:

    • 需要绘制图表的资源。

    • 需要绘制图表的资源所属的指标命名空间:对于每种 Azure 资源类型,只有一个命名空间。唯一的例外是 Application Insights,它有两个命名空间——一个是默认指标,另一个是通过 TelemetryClient 发出的应用程序指标。

    • 绘制指标:对于自定义指标,这指的是前一节中在 GetMetric() 方法中选择的名称。

    • 用于将多个测量值合并为图表中一个点的数学运算:这可以是最小值、最大值、平均值或总和。

    • 要向同一图表添加多个图表线条,请选择顶部的“添加指标”。重复之前的四个选择以配置新的图表。

  2. 若要将此图表作为仪表板的一部分以便于重复使用,请点击顶部的“固定到仪表板”按钮。

  3. 然后可以通过右侧的菜单直接访问仪表板。

拥有一个度量的图表,甚至是在仪表板中显示多个图表,对于调查问题非常有帮助。然而,没有人愿意持续关注仪表板以查看进展情况。因此,还可以为度量配置警报。

对度量进行警报

与日志条目类似,当一个度量值超过或低于某个阈值时,Azure Monitor 也可以发出警报。需要跟进的日志条目可能与某个单一用户或客户有关,他们遇到问题。而度量值则有助于检测所有用户受到影响的情况,或基础设施无法正常工作或即将停止工作的情况。

为度量创建警报的过程与从日志创建警报的过程非常相似。要创建新的警报规则,请使用门户导航到 Azure Monitor,然后选择“警报”(Alerts)。接下来,点击“新建警报规则”按钮,打开如下所示的视图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/f5f81d34-9588-4b27-b86c-f8e8bed53dc1.png

在此截图中,必须进行以下选择才能对度量创建警报:

  1. 选择作为警报主体的资源。这可以是任何类型的输出度量值的资源——在此实例中,已选择一个 Application Insights 工作区。

  2. 配置触发警报的条件。为此,会在右侧打开一个新窗口。要进入前面截图中显示的视图,选择度量警报类型,并从填充的列表中选择正确的警报。接下来,视图将更改为前面截图中显示的内容。在这里,必须进行以下选择:

    • 选择触发警报的阈值(参见前面截图中的 2a)。静态阈值是默认设置,需要配置操作符、聚合类型和值。

    • 选择警报应评估的粒度间隔(2b)。

    • 选择评估频率(2c)。警报评估得越频繁,发生事件与发送警报之间的延迟就越短。

    • 保存警报条件(2d)。

  3. 选择一个或多个在满足警报条件时需要触发的操作组。

  4. 配置警报规则名称和描述。

  5. 保存警报。

警报在保存后几分钟内变为活动。现在,每当满足警报条件时,开发人员将通过配置的警报组方法收到通知。

调查请求

在使用 Application Insights 进行日志记录和度量时,Application Insights 还提供了许多内置的功能可供使用。其功能之一是可以通过一个视图——称为“搜索”(Search)——对所有由 Application Insights 收集的数据执行搜索查询。

在这里,可以搜索所有由 Application Insights 收集的信息,包括以下内容:

  • 应用程序代码生成的日志,包括 NuGet 包和 .NET 框架。

  • 所有依赖调用:这些是对数据库和其他系统的调用,Application Insights 会自动检测到。Application Insights 会记录目标系统和持续时间。

  • 所有异常:应用程序中发生的所有异常都会被 Application Insights 记录,即使它们已经被应用程序代码正确处理。

  • 请求:所有通过 HTTP 进入的用户请求都会被记录。重要的属性,如 URL、持续时间和 HTTP 方法,也会被包括在内。

要打开搜索视图,请导航到正确的 Application Insights 实例,然后转到搜索标签(1),以获得以下截图所示的视图:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/90ab0619-d7cc-45c4-8584-51e9628f9335.png

在搜索视图中,可以配置多个搜索参数(2):

  • 搜索的时间间隔:默认为过去 24 小时。

  • 搜索的事件类型:可以是请求、日志条目、页面视图、异常、依赖调用等。

  • 搜索的文本内容。

几秒钟内,所有匹配的结果会以条形图形式显示。每个条形代表一个时间段,并显示在该时间段内的匹配次数。此图表下方会显示所有单独的匹配项。这些是所有可用事件类型的混合。

点击任何一个结果会打开一个新视图,显示选定记录与所有其他类型的关系,按请求分组。这使你能够快速导航到所有由 Application Insights 在单个用户请求执行期间收集的日志、依赖调用和异常。结果可以以列表和时间轴的形式显示。这使你能够非常快速地查看在执行用户请求时,服务器所做的操作:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/d38bddfb-25c8-4d85-bccb-5b6de1b664ad.png

通过这些调查应用程序和接收事件通知的方式,决定创建哪些警报、哪些不创建非常重要,这不仅是为了创建一个健康的工作环境,还为了在监控和新工作的平衡中找到合适的点。这是下节的主题。

优化警报

一旦团队开始为其应用程序添加指标和警报,并且关注他们认为重要的指标,警报很快就会开始出现。在此时,重要的不是仅仅响应警报,而是要调查警报并关闭它们。警报还应该被视为学习的机会。

优化警报设置

创建一系列警报后,重要的步骤是定期重新评估它们。通过这样的评估,可能得出以下两种结论:

  • 警报阈值的变化:定期评估警报涉及查看一段时间内的指标,并检查警报阈值目前的位置。这可能导致结论认为阈值过低或过高。

  • 去除重复:查看一个月内已触发的警报,很可能会发现一组或多组警报总是同时触发。例如,某个特定 Web 服务器上设置的一组警报可能是如此相关,它们总是在同一时间触发。一个常见的例子是 CPU 使用率和 HTTP 请求的平均响应时间;这两个指标通常会同时上升。如果是这种情况,值得考虑去除其中一个警报,或者将其中一个警报降级为仅为警告。重复的警报增加了需要立即响应的事项数量,导致团队在没有明确好处的情况下承受更大的压力。

不断优化警报集不仅有助于减少浪费,还能防止所谓的警报疲劳。

警报疲劳

如果警报规则没有持续进行审查和更新,它们可能会对团队产生负面影响,特别是当警报规则触发过于容易或过于频繁时,人员将不再正确响应这些警报。如果警报数量过多,会使人感到疲惫,变得对警报麻木。无论这些警报是虚假的还是实际的,警报的数量足以让人们进入一种不再关心的状态。

如果在团队中观察到这种情况,就该彻底改变警报的生成和响应方式。如果不这样做,团队成员可能会生病或完全离开公司。

防止这种情况的一种方法是实施一个健康的值班计划。

要捕获哪些指标

在讨论指标时,一个常见的问题是应该发出和监控哪些指标。这里有许多可能的指标,以及更多关于此主题的不同看法。作为一个好的起点,通常会收集以下 Web 应用程序的指标:

  • 每分钟请求数、每分钟事务数或类似指标:这是用来捕捉 Web 应用程序当前负载或吞吐量的度量标准。

  • 平均响应时间:这个度量标准捕捉的是在某一时间窗口内所有请求的响应时间。

  • 错误率:这个度量标准捕捉的是所有请求中导致错误的百分比。作为错误的指导标准,通常将所有400及以上的 HTTP 响应码视为错误。

当这三个指标一起被捕获并绘制在同一图表中时,它们为理解应用程序行为提供了第一步。让我们看几个例子:

  • 当平均响应时间上升,但吞吐量(每分钟请求数)保持不变时,这可能表明承载应用程序的基础设施出现了问题。

  • 当吞吐量和平均响应时间同时上升时,这可能意味着流量正在增加,当前的基础设施无法在相同的响应时间下支持这种吞吐量。

  • 当错误率上升,但其他指标保持不变时,这可能意味着部署出现问题,或者某个特定的代码路径开始生成(更多)错误。

当然,这些只是一些示例,还有许多可能的场景。其他指标可以帮助排除特定的场景或尽量避免它们。例如,开始监控数据库负载百分比也可以帮助检测第二种场景的特定实例。如果数据库负载接近 100%,可能是时候将数据库升级到更高性能的层级,以维持相同响应时间下的更高吞吐量。

总结这一部分内容时,最后有一个建议——在开始监控时,往往会倾向于关注托管应用程序的系统。作为替代方案,还应考虑监控那些对业务产生直接影响的指标,或者是能够反映用户在应用程序可用性方面满意度的指标。这比仅仅关注系统要更接近衡量业务价值。

以下是一些示例:

  • 在在线商店中,每分钟售出的书籍数量可以是一个非常有价值的商业指标。试想一下,如果能够通过 Azure Monitor 和应用程序代码中的自定义指标实时获取这个指标,将会对业务产生什么样的影响。

  • 对于在线阅读平台,虚拟翻页的数量可以是一个有价值的指标,表明用户是否愉快地使用该服务。只要这个数字出现急剧下降或迅速增加,可能就表明出现了问题。

要找出在特定场景下哪些指标有意义,可能有助于与业务或领域专家进行沟通。

制定值班计划

一旦配置了警报并开始触发,就没有必要设置警报在早上 8 点之前或下午 5 点之后不触发。换句话说,必须确保某些严重性的警报即使在办公时间之外也能得到跟进。

在许多公司中,警报机制还是新鲜事物,因此隐性地期望某些人在办公时间外(除了他们的常规职责)可以处理这些警报。有时,当警报每年只触发一两次,且没有关于响应时间的约定时,这可能根本不会成为问题。

然而,在许多组织中——尤其是随着时间的推移——人们通常期望在一定的时间范围内对这些警报作出响应。除此之外,随着系统的规模变大、复杂性增加,或者系统数量的增长,警报的数量也可能会增加。

应对这一问题的方式是制定值班计划,并与工程师达成正式协议,明确他们的职责以及组织将如何奖励他们的付出。这使得工程师能够设定清晰的期望,并根据这些协议保护自己的自由时间。足够的休息时间有助于工程师在高压期间得到放松,这样他们才能在值班时保持警觉,随时准备应对预期的任务。

有很多关于什么构成健康值班计划的资料,关键字是健康。一些基本建议如下:

  • 在非工作时间值班的员工,不应在工作时间继续值班。

  • 为值班工程师提供合理的补偿,考虑他们保持电话畅通、避免受影响等情况。什么是合理的补偿因情况而异,但值班的要求越高,补偿应该越高。

  • 为值班人员提供合适的工具。例如,当响应时间要求为 30 分钟或更短时,给值班人员配备一个背包,里面放有笔记本电脑、电话和连接互联网的设备。

  • 确保每位员工的值班时间不超过至少 75%。

  • 允许员工以调休的形式休假,这样如果他们需要在夜间响应警报,可以晚点上班。

每当系统的正常运行受到干扰时,无论是工作时间还是非工作时间,都可以进行实时站点事件回顾,以了解发生了什么以及如何减少再次发生的可能性。

实时站点回顾

在警报触发并且团队已响应并解决问题后,是时候评估发生了什么了。这就是所谓的实时站点事件回顾。在这里,整个团队会聚集在一起,讨论以下内容:

  1. 发生了什么——首先,应该构建一个时间线,从发现事件的时刻到恢复正常操作为止。接着,时间线会扩展,加入导致触发事件的前因后果。

  2. 接下来,评估一系列事件,了解响应过程中哪些做得好。如果团队的某个成员使用了一个新工具快速诊断问题,这也能对团队其他成员产生帮助。

  3. 只有在此之后,才是审视可能改进的地方,并将这些改进点转化为团队的高优先级工作。识别出可能的安全保障措施,并安排实施,或者识别新的警报机制,在类似问题发生之前就能发出警报。

  4. 触发初始响应的警报或一组警报被评估,以确定它们是否足够充分,或者是否可能包含重复内容。

实时站点事件回顾的最佳时间是尽可能接近事件发生的时间。实际上,这意味着要给每个人足够的时间休息和恢复,并计划安排在下一个工作日进行会议。

这完成了我们对 Application Insights 和 Azure Monitor 在监控网络应用程序时功能的概述。下一节将介绍几种将 Application Insights 和 Azure Monitor 与其他工具集成的方法。

与其他工具的集成

Azure Monitor 和 Application Insights 是收集应用程序日志和指标的优秀工具,同时还可以存储这些数据并进行搜索。然而,开发团队或企业可能有一些原因偏好使用其他工具来可视化应用程序性能或响应警报。一个重要的整合驱动因素通常是一个人或团队使用的主要工具。如果一个团队主要在 ServiceNow 或 Grafana 中操作,那么将这些工具与 Azure Monitor 集成通常更为有用,而不是强迫这些团队使用多个工具。

存在许多可能的集成,以下小节中详细介绍了一些例子。

IT 服务管理应用程序

在前一节中我们介绍了操作组,并讨论了如何在网络应用程序中进行监控。操作组是响应警报时执行的一组操作。

除了丰富的内置功能外,还可以在现有的IT 服务管理ITSM)解决方案中自动触发警报。如果公司内已经有 ITSM 解决方案,那么通过 Azure Monitor 创建一个单独的警报通道并不合适。相反,使用 Azure Monitor 的 ITSM 连接器可以让你从一个解决方案中管理公司范围内的所有警报。

当前,ServiceNow、Provance、System Center Service Manager 等工具已有可用的集成。这些连接通过 ITSM 连接器创建。

Azure Boards

在许多开发团队中,Azure DevOps 是开发人员花费大部分时间使用的工具。这也是他们通过 Azure Boards 执行待办事项管理的地方。

同时,操作员(希望开发人员也能参与)在 Application Insights 中进行调查工作,以确定用户错误的原因,并深入分析失败的原因。这项调查工作可能会导致需要在 Azure DevOps 中回溯的任务。

为了简化操作,可以通过以下步骤从 Application Insights 配置与 Azure DevOps 的集成:

  1. 导航到左侧菜单中的工作项选项(1)。这将打开下图所示的视图。在这里,可以配置与 Azure Boards 的连接:

https://github.com/OpenDocCN/freelearn-devops-pt4-zh/raw/master/docs/impl-az-dop-solu/img/88cc179b-182f-4fee-8030-7b2a2c4b411e.png

要配置连接,必须填写以下详细信息:

  1. 输入 Azure DevOps 链接。此时需要附加上组织的名称。

  2. 选择要使用的 Azure DevOps 项目。可以从下拉菜单中选择。

  3. 选择一个产品区域,在该区域中将创建新的项目项。默认情况下,这是与项目名称相同,除非你更改它。

  4. 为新的工作项提供一个用户名称,作为默认所有者。

配置此连接后,在 Application Insights 的相关页面上会显示一个新的“创建工作项”按钮。此按钮允许您直接在待办事项列表中创建一个包含所有相关信息的缺陷。

Grafana

Azure Monitor 允许您构建简单、易用的仪表板。使用 Azure Monitor 仪表板的优势在于,它们与所有其他 Azure 实践(例如基于角色的访问控制RBAC)和 Azure 资源管理器模板)完美集成。

然而,团队可能已经采用了其他可视化工具,如 Grafana。Grafana 是一个广受欢迎的平台,非常适合用于操作仪表板。Grafana 可以配置为通过 Azure Monitor 进行连接,并查询指标以生成图表。Grafana 还具有告警功能,但不支持查询日志。

要将 Grafana 连接到 Azure Monitor,需要执行以下步骤:

  1. 在您的 Azure 订阅使用的 Azure Active Directory 帐户中创建一个新的应用注册。记下该应用注册的Tenant IdClient IdSubscription IdClient Secret属性。

  2. 为应用注册创建新的 RBAC 角色分配,并为要监控的资源至少设置Reader权限。

  3. 在 Grafana 中配置一个新的 Azure Monitor 类型的数据源。插入在第 1 步中收集的用于 Azure 身份验证的属性。

  4. 向仪表板添加一个新的图表,选择 Azure Monitor 作为数据源。

通过执行上述步骤,可以在几分钟内设置 Grafana 与 Azure Monitor 的连接。

总结

在本章中,您学习了如何开始完成 DevOps 循环。您还学会了如何处理崩溃报告,并从各种应用程序中收集它们,以及如何为 Web 应用程序添加监控代码。您现在知道如何使用 Application Insights 集中日志和指标,并深入了解请求和依赖调用。您还学会了如何将 Azure Monitor 与其他工具集成,以进一步简化您的开发过程。

通过这些知识,您现在可以开始了解您的应用程序如何在生产环境中运行。通过这样做,您不仅可以更快地交付软件,还可以从使用中学习并从中进行改进。

在下一章中,您将学习如何收集用户反馈,以补充从系统日志和指标中学到的内容。您还将学习如何衡量应用程序和新功能的最终用户满意度。

问题

在我们总结时,这里有一系列问题供您测试对本章内容的理解。您可以在附录的评估部分找到答案:

  1. 判断正误——是否可以使用 Application Insights 捕获 Azure 平台提供的自定义指标?

  2. Azure Monitor 中平台指标的保留时间是多少?

  3. 判断题 – 使用 Application Insights 可以从您自己的应用程序代码中捕获自定义指标。

  4. 你怎么称呼工程师因为警报过多而开始忽视警报的情况?

  5. 判断题 – 在 Azure 中,触发警报时可以调用 webhook。

进一步阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值