第5章 通过 GitHub Actions/Azure DevOps/Azure Automation 实现 SRE 自动化

© Unai Huete Beloki 2025
U. H. Beloki, The Art of Site Reliability Engineering (SRE) with Azure,
https://doi.org/10.1007/979-8-8688-1545-4_5

111

在本章中,我将重点介绍 Dickerson 可靠性层次结构中的第四个支柱:自动化(发布/测试)。这是一个 SRE 需要与组织中的 DevOps 工程师紧密合作的主题。DevOps 工程师将专注于持续实验,而 SRE 将专注于解决方案的可靠性,两者都使用共享的工具和实践。

学完本章后,您应能理解以下内容:

  • ✓ 什么是 DevOps 及主要概念
    • CI 和 CD
    • 基础设施即代码和配置即代码
  • 112
    • 左移测试
    • 安全 DevOps
  • ✓ GitHub Actions 和 Azure DevOps 的基础知识
  • ✓ 现代部署实践和工具

面向 SRE 的自动化

本章将涵盖作为 SRE 至少需要“理解”的最重要的自动化相关实践。为什么如此强调“理解”?正如第一章所讨论的,DevOps 和 SRE 实践有时会重叠,而本实践正是重叠最频繁的领域。作为 SRE,您的责任范围从理解到提供建议(或全面实施)这些自动化方法,具体取决于您的组织。

在 DevOps 实践成熟的组织中,可能更需要 SRE 的帮助,以专注于发布过程的可靠性(更完整的自动化测试方法或更好的部署实践/策略)。另一方面,DevOps 不成熟的组织则需要在自动化实践方面得到 SRE 更多的帮助。

113

Figure 5-1.  Dickerson 可靠性层次结构

本章将重点介绍自动化的三个主要领域:

  • CI/CD DevOps 流程:变更如何被持续构建、测试(功能测试、质量、安全)并部署到我们的服务中
  • 运营/期望状态自动化:涵盖执行 runbook 和管理虚拟机期望状态的选项
  • 现代部署实践:将为 DevOps 团队提供实验工具,并为 SRE 提供零停机部署(以及更多控制能力)

114

使用 DevOps 实现 CI/CD 自动化

什么是 DevOps

DevOps 是过去几年中最热门的话题之一。开发实践正从每月(甚至每年)的发布缓慢转向几周内发布。对于更传统的瀑布式方法论团队来说,经过数月的努力,许多时候团队因未能满足用户期望而挣扎。新代码的部署是“痛苦的”。

Scrum、敏捷、看板以及类似的方法论完全改变了您开发软件的方式,使工程团队能够非常快速地增加增量价值。为了能够在竞争对手之前将价值部署到您的产品中,您需要一种部署代码变更的新方式。

DevOps 向我们展示了一种更好、更快、更可靠的构建/测试/部署代码变更的方式。

尽管第一章已经介绍了 DevOps 的基础知识,但本章将更详细地解释 CI/CD 自动化实践。

持续集成 (CI)

持续集成是一个专注于代码变更验证的过程。每次对源代码应用变更(新实验),您都需要构建、测试(此时主要是单元测试或更简单的集成测试)并发布新的代码构件。更复杂/完整的 CI 流水线还可能包含代码安全/质量扫描工具。

持续集成的支柱之一是您的源代码控制(或版本控制)设置,如今主要使用 Git 仓库,它定义了您跟踪和管理软件代码变更的方式。选择正确的 Git 分支策略将是关键,以便以受控的方式更快地进行实验,并将您的提议变更推进到更稳定的仓库分支。

115

尽管源代码控制和持续集成长期以来一直是开发人员和应用程序代码的流行实践,但如今许多 IT 专业人员也被鼓励应用它们。任何定义和影响您运行中的解决方案的方面,都鼓励使用源代码控制和 CI 实践,例如:

  • 应用程序代码
  • 基础设施
  • 配置
  • 文档
  • 流水线(主要是 YAML)

对上述任何组件应用的任何变更都可能修改(甚至破坏)您正在运行的解决方案。因此,更多的 IT 角色被鼓励学习诸如 Git 之类的技术,并开始将其用于上述任何组件。

拥有一个正确的版本控制过程是关键,以便借助版本控制技术(如 Git)和 DevOps 平台(如 Azure DevOps 和 GitHub),以受控的方式更快地进行实验。

持续交付/部署 (CD)

CD 是一个专注于部署实践和更高级测试实践(如集成测试、UI 测试、端到端测试、性能测试等)的过程。但 CD 究竟代表什么?

在我与客户进行 DevOps 转型时,我经常问的第一个问题是:

“您期望实施 CD 实践吗?”

当然,答案大多是“是的”。然后,我的下一个问题是:

“您想实施持续部署还是持续交付?”

116

Figure 5-2.  持续交付与持续部署

  • 持续交付:是一个将新创建的构件(主要是 CI 产出)部署到所需环境的过程。它会在代码变更到达生产环境之前包含一些手动步骤。
  • 持续部署:从代码变更(提交)到生产的完全自动化。它要求更高,因为您需要有正确的自动化安全/质量/测试机制(软件组成分析器、静态/动态代码分析器、单元测试、集成测试、UI 测试等)以及现代发布策略,以确保到达生产环境的代码变更能够正常运行。它还将实施本章稍后提到的一些策略。

正如那些策略稍后将要提到的,在生产环境中部署代码(发布新代码)和发布代码(向不同用户暴露它)之间将存在区别。

117

WARNING

一些组织/团队会交换这两种实践的定义,将持续交付视为完全自动化的一种,而将持续部署视为在进入生产环境前需要手动批准的一种。从这个话题中获得的最重要的结果应该是:

  • 为您的团队/组织记录定义(达成共识)
  • ✓ 明确目标。正如您所见,追求完全自动化要求更高,并且涉及更高级的实践。并非所有应用程序/解决方案都需要这些要求。

DevOps 中的左移测试

测试是 DevOps 实践中的一个大话题。Dickerson 可靠性层次结构将测试列为支柱之一,原因显而易见:它确保代码按预期工作。测试帮助我们在代码部署到生产环境之前发现错误。那么,什么是“左移测试”?

左移测试意味着将您的测试工作提前到生命周期的早期阶段,在尽可能低的级别编写测试。为什么呢?主要原因如下:

  • 测试级别越低 ➤ 影响越小(错误更早被发现)。
  • 低级别测试更稳定、运行更快,是最容易获得新功能反馈的方式。
  • ✓ 大多数测试在合并到“主”分支之前完成。

118

看看 Azure DevOps 产品团队在微软的左移测试转型:https://learn.microsoft.com/en-us/devops/develop/shift-left-make-testing-fast-reliable#case-study-shift-left-with-unit-tests。该文档清晰地展示了微软如何从后期阶段的 UI 测试转变为频繁执行的单元/集成测试。

您的团队需要定义一个合适的测试分类法,该分类法根据依赖关系或运行时间,指定在每个场景(CI/CD 流水线)中使用的正确测试类型。请参考这个示例分类法:https://learn.microsoft.com/en-us/devops/develop/shift-left-make-testing-fast-reliable#devops-test-taxonomy。

安全 DevOps

安全 DevOps(也称为 Rugged DevOps 或 DevSecOps)是一种实践,确保安全活动在整个应用程序生命周期中实施。它专注于尽可能早地将安全实践引入解决方案的生命周期(左移安全)。

作为站点可靠性工程师,这是一个您必须理解并鼓励的实践。最终,在漏洞到达生产环境之前捕获它们将提高我们解决方案的可靠性,这是您需要为之奋斗的事情。

与安全 DevOps 相关的实践有很多种;这里我列举其中一些:

  • 威胁建模:一项活动,绘制解决方案的架构图,并分析、缓解和验证潜在的威胁。它可以从规划阶段开始运行;您的解决方案甚至可能处于设计阶段(尚未存在)。微软提供了一个免费的威胁建模工具:www.microsoft.com/en-us/securityengineering/sdl/threatmodeling。

119

  • 自动化相关工具
    • CI(持续集成):在持续集成过程中,可以使用两种主要类型的工具:
      • 软件组成分析器(SCA):将帮助分析与应用程序代码使用的库(NuGet、Maven、npm 等)相关的漏洞和许可限制(针对开源组件)。GitHub 提供 Dependabot 作为 GitHub Advanced Security (GHAS) 安全服务集的一部分。像 Dependabot 和 CodeQL 这样的工具也可以在 Azure DevOps 中使用。
      • 静态分析器:将帮助识别漏洞、不良实践、重复代码,甚至衡量应用程序代码的技术债务。像 SonarCloud/SonarQube 和 CodeQL 这样的工具可用于 Azure DevOps 和 GitHub。
    • CD(持续部署):在部署阶段,我们还可以运行动态分析器,该分析器将识别运行环境中的潜在威胁。像 OWASP ZAP 这样的工具可用于基于开放 Web 应用安全项目(OWASP)的建议来识别威胁。
    • 监控/运营:在最后阶段,可以使用 Azure 工具(如 Microsoft Defender for Cloud)来检测和缓解威胁。它可以通过两种不同的方式使用:
      • 被动方法:解决已检测到的威胁。

(注:此处原文在第119页结束,且“被动方法”后未提供完整内容,可能后续部分在文档下一部分,但当前输入仅到此。因此保持原样。)

第5章 通过GitHub Actions/Azure DevOps/Azure Automation实现SRE自动化

  • 主动方法:基于现有架构给出的建议,创建Azure Policy,将这些最佳实践应用于新环境和现有环境。

基础设施即代码(IaC)

高性能的DevOps组织会设计其流程以实现速度和敏捷性。它们无疑会提供自动化的构建和发布,以确保部署高质量且一致的代码。但是,运行这些系统的环境本身呢?

基础设施即代码(IaC)专注于将软件工程实践(如版本控制、验证、测试或安全)应用于基础设施部署,使其一致、可重复且自动化。

IaC有两种不同的方法:

  • 声明式:你只需定义基础设施应该呈现的样子(是什么),云提供商会负责部署具有这些属性的解决方案,通常使用模板文件。示例工具有:
    • Azure原生:ARM模板Bicep
    • 第三方:Terraform、Ansible、Chef、Puppet、Pulumi等。
  • 命令式:由你定义要执行的步骤(如何做)以获得所需的解决方案,通常使用脚本。示例工具有:
    • Azure CLI、Azure PowerShell、Azure REST API和Azure SDK。

无论使用哪种工具,IaC流程都应追求幂等性。什么是幂等性?简单来说:相同的IaC模板每次应用都会生成相同的环境。幂等性是IaC最重要的原则,是自动化CD管道成功执行的关键。换句话说,如果模板没有改变,你的管道将成功执行,保持环境不变。基于前面提到的工具,像ARM/Bicep这样的工具确保了幂等性,而对于命令式工具,我们可能会遇到非幂等的命令(你的脚本需要修改,添加条件检查资源/属性是否存在,仅当不存在时才创建/修改)。

本书在章节演示中主要展示ARM模板和Bicep两种方式。

  • ARM模板:Azure资源管理器(Azure Resource Manager)是Azure内置的一个暴露的API服务,用于资源预配。你只需提供一个基于JSON的模板,包含你想要预配的资源(及其配置)。
  • 图5-3 Bicep

下面我们可以看到Azure应用服务在ARM模板和Bicep中的声明区别(见清单5-1)。Bicep更直观,更易于阅读和编写。

清单5-1 ARM vs. Bicep

ARM模板

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.4.1008.15138",
      "templateHash": "13829464918613725405"
    }
  },
  "parameters": {
    "webAppName": {
      "type": "string",
      "defaultValue": "[uniqueString(resourceGroup().id)]"
    },
    "sku": {
      "type": "string",
      "defaultValue": "F1"
    },
    "linuxFxVersion": {
      "type": "string",
      "defaultValue": "node|14-lts"
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    },
    "repositoryUrl": {
      "type": "string",
      "defaultValue": "https://github.com/Azure-Samples/nodejs-docs-hello-world"
    },
    "branch": {
      "type": "string",
      "defaultValue": "master"
    }
  },
  "functions": [],
  "variables": {
    "appServicePlanName": "[toLower(format('AppServicePlan-{0}', parameters('webAppName')))]",
    "webSiteName": "[toLower(format('wapp-{0}', parameters('webAppName')))]"
  },
  "resources": [
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2020-06-01",
      "name": "[variables('appServicePlanName')]",
      "location": "[parameters('location')]",
      "properties": {
        "reserved": true
      },
      "sku": {
        "name": "[parameters('sku')]"
      },
      "kind": "linux"
    },
    {
      "type": "Microsoft.Web/sites",
      "apiVersion": "2020-06-01",
      "name": "[variables('webSiteName')]",
      "location": "[parameters('location')]",
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
        "siteConfig": {
          "linuxFxVersion": "[parameters('linuxFxVersion')]"
        }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
      ]
    },
    {
      "type": "Microsoft.Web/sites/sourcecontrols",
      "apiVersion": "2021-01-01",
      "name": "[format('{0}/web', variables('webSiteName'))]",
      "properties": {
        "repoUrl": "[parameters('repositoryUrl')]",
        "branch": "[parameters('branch')]",
        "isManualIntegration": true
      },
      "dependsOn": [
        "[resourceId('Microsoft.Web/sites', variables('webSiteName'))]"
      ]
    }
  ]
}

Bicep

param webAppName string = uniqueString(resourceGroup().id) // 为Web应用名称生成唯一字符串
param sku string = 'F1' // 应用服务计划的SKU
param linuxFxVersion string = 'node|14-lts' // Web应用的运行时栈
param location string = resourceGroup().location // 所有资源的位置
 
param repositoryUrl string = 'https://github.com/Azure-Samples/nodejs-docs-hello-world'
param branch string = 'master'
 
var appServicePlanName = toLower('AppServicePlan-${webAppName}')
var webSiteName = toLower('wapp-${webAppName}')
 
resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = {
  name: appServicePlanName
  location: location
  properties: {
    reserved: true
  }
  sku: {
    name: sku
  }
  kind: 'linux'
}
 
resource appService 'Microsoft.Web/sites@2020-06-01' = {
  name: webSiteName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: linuxFxVersion
    }
  }
}
 
resource srcControls 'Microsoft.Web/sites/sourcecontrols@2021-01-01' = {
  name: '${appService.name}/web'
  properties: {
    repoUrl: repositoryUrl
    branch: branch
    isManualIntegration: true
  }
}

你可以在 https://azure.microsoft.com/en-us/resources/templates/ 找到这两种格式的许多快速入门模板。此外,你还可以在MS Learn上找到Bicep的极佳学习路径:https://docs.microsoft.com/en-us/learn/paths/fundamentals-bicep/。

通过DSC/Azure Automation/Guest Configuration实现配置即代码

前面的IaC主题侧重于部署我们期望的基础设施,但如何配置我们创建的这些服务呢?我们如何确保Linux和Windows虚拟机拥有所需的配置(操作系统设置、防火墙规则、功能以及已安装的软件)?

有人可能会提议在创建这些机器后直接运行脚本,但如果有人手动修改了机器呢?我们可能会面临配置漂移:机器不再具有我们期望的配置。我们需要主动地自行重复运行相同的脚本。

配置即代码(CaC)包括编写一个定义,描述你希望环境达到的理想状态。它会定期进行一致性检查,确保理想状态持续满足。换句话说,配置漂移会被检测到并得到解决!

在微软的工具集中,我们有一个名为Desired State Configuration(DSC)的CaC引擎,它是PowerShell的一部分(也称为PowerShell DSC),适用于Windows和Linux机器。你的机器需要安装PowerShell 4.0或更高版本。负责根据配置执行定期检查的服务称为本地配置管理器(LCM)

使用DSC时,我们有两种工作模式:

你可以在Azure Automation的库功能中找到许多快速入门DSC文件。下面的示例确保标记为“Web服务器”的虚拟机将安装IIS Web服务器(见清单5-2)。

清单5-2 DSC示例

configuration TestConfig
{
    Node IsWebServer
    {
        WindowsFeature IIS
        {
            Ensure               = 'Present'
            Name                 = 'Web-Server'
            IncludeAllSubFeature = $true
        }
    }
    Node NotWebServer
    {
        WindowsFeature IIS
        {
            Ensure               = 'Absent'
            Name                 = 'Web-Server'
        }
    }
}

Azure Automation还可用于托管和执行Runbook。Runbook是基于PowerShell(图形和Workflow)和Python的脚本,可以根据计划或Webhook调用执行。Runbook应用于操作自动化(而非CI/CD),例如,每天运行脚本启动/停止虚拟机,或者作为对特定警报触发的反应执行脚本(使用Azure警报操作组)。

第5章 通过GitHub Actions/Azure DevOps/Azure Automation实现SRE自动化

Azure Automation State Configuration 将于2027年9月30日停用。建议客户迁移到 Azure Automanage Machine Configuration(https://learn.microsoft.com/zh-cn/azure/automation/automation-dsc-overview)。

Azure Automanage Machine Configuration

Azure Automanage Machine Configuration 是 Azure Policy 的一项新功能,允许你以代码形式审核或配置运行在 Azure 及混合 Arc 启用的机器上的操作系统设置。此功能可直接按机器使用,也可通过 Azure(https://learn.microsoft.com/zh-cn/azure/governance/machine-configuration/overview)进行大规模编排。

机器配置代理使用本地工具执行任务。它在后端使用 DSC 和 Chef InSpec。

Azure Pipelines

Azure DevOps 是一款产品,为客户提供许多开发相关服务,例如规划、协作、CI/CD、包管理和测试工具。

Azure Pipelines(https://docs.microsoft.com/zh-cn/azure/devops/pipelines/get-started/what-is-azure-pipelines?view=azure-devops)是 Azure DevOps 上集成的服务,支持应用程序的持续集成和交付/部署。但它是如何工作的?该服务由许多不同的组件/部分组成;让我们尝试用简单的问题来定义它们:

管道在哪里运行?

Azure Pipelines 从 Azure DevOps 服务执行/触发,但定义的自动化任务在代理上运行。什么是代理?它是用于运行管道作业的计算基础设施(主要是 VM 或容器)。之所以称为代理,是因为在主机上安装了软件,使其能够监听 Azure DevOps 服务。作业可以在代理的主机或容器上运行。

Microsoft 提供两种主要的代理类型:

  • Microsoft 托管代理:由 Microsoft 维护和升级的一组代理。以 VM 和容器形式提供,每个作业都会获得一台全新的机器。此机器预装了定义的软件:https://github.com/actions/runner-images(第一个“秘密”,GitHub Actions 也用同样的“代理”,它们称之为“运行器”)。

  • 自托管代理:由于网络、计算或软件需求,你可能会倾向于使用自己的私有基础设施主机和设置(已安装的工具/软件)。此选项在 Azure DevOps 之前的本地版本(称为 Team Foundation Server)中早已存在。它也是当前本地版 Azure DevOps Server 解决方案中唯一的代理选项。它提供了对网络、计算、缓存或软件需求的更多控制,但你需要自己维护。代理(软件)支持三大主流操作系统:macOS、Linux 和 Windows。你也可以使用 Docker 容器安装。

我在哪里/如何定义要运行的自动化任务?

Azure DevOps 提供两种不同的方式来定义你的管道:YAML 和经典(也称为可视)。在本书中,我们将重点介绍 YAML,因为新功能主要针对此管道模型。

NOTE

自2024年起,经典管道默认不再启用。

YAML 管道在代码中定义,文件托管在 GIT 仓库中。它必须遵循为源代码控制(分支、拉取请求等)定义的标准实践,并且将从协作、控制和变更跟踪方面受益于这种开发实践。与经典管道相比,它在模块化(创建模板)方面也具有巨大优势。

图 5-4. 管道结构

  • 管道:定义进程的上下文;可以是 CI、CD 或两者兼有。管道可以相互链接;并非所有进程都必须写入单个文件(也可以)。管道将定义触发其执行的触发器(分支更改、拉取请求、其他管道成功执行等)。

  • 阶段:一种组织管道作业的方式。这是一个可选特性;你可以拥有仅由作业组成的管道。主要用于关注点分离,例如:

    • 基础设施部署 ➤ 网站发布 ➤ 端到端测试
    • 构建 CI ➤ QA CD ➤ 预生产/生产 CD

    阶段和作业不一定是线性流程;你可以自由设计带有条件和依赖关系的流程,线性与否均可。

  • 作业:在定义的代理(如前所述)上顺序运行的一系列任务。

  • 步骤:任务的集合。

  • 任务:预打包的任务或用于执行操作的脚本。

  • 其他通用组件

    • 构件:一种在作业之间共享文件的机制(请记住,作业运行在不同的代理上,代理从池中选出),以及持久化结果的方式(例如,每次管道执行的构建产物)。
    • 变量:可以为管道的不同作用域定义变量。也可以在 UI 上定义。变量组可用于跨管道共享变量。系统变量也非常有用:https://docs.microsoft.com/zh-cn/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml
    • 环境:对服务(Web 应用、容器、虚拟机等)的引用。可用于对 YAML 管道作业应用控制(下面提到的检查和审批)。
    • 服务连接:对外部或远程服务(任务需要使用)的经过身份验证的连接。如果你的任务要使用像 SonarCloud 这样的工具进行静态分析、从 GitHub 检出代码或部署/与 Azure 资源交互,它们需要在你的管道上使用经过身份验证的连接。简单来说,在代理(Microsoft 或私有主机)上执行的管道需要知道如何对这些外部组件进行身份验证,才能成功执行任务。Azure 部署实际上需要服务连接;你的代理将使用服务连接对 Azure 目标订阅进行身份验证(使用服务主体/托管标识)。
    • 检查和审批:在多阶段 YAML 管道上应用控制(自动化或手动)的方式。有哪些控制?例如:
      • 手动审批(审阅者)、营业时间、评估构件、调用 REST API、运行 Azure Functions、检查 Azure 警报,或强制环境必须使用的 YAML 模板(进程)。

这为我们提供了很好的工具,以确保变更已准备好应用。它可以应用于以下组件:

  • 代理池
  • 服务连接
  • 环境
  • 仓库
  • 变量组

每当在 YAML 管道中引用这些组件之一时,Azure DevOps 将查找检查和审批条件,并在继续执行任务之前确保满足这些需求。

它是免费的吗?定价模式是什么?

Azure DevOps 提供以下定价模式:https://azure.microsoft.com/zh-cn/pricing/details/devops/azure-devops-services/

对于 Azure Pipelines,定价围绕并行度(我们可以并行运行多少个作业)构建。私有项目将获得 1800 分钟/月的 Microsoft 托管代理,以及用于 Microsoft 托管和自托管代理的单个并行管道。当购买更多并行管道时,时间限制消失。

公共项目将获得 10 个 Microsoft 托管并行管道和无限制分钟数。

WARNING

你可能需要填写本文中提到的表单才能获得免费资源(由于最近在该产品上检测到可疑活动,主要是挖矿):https://devblogs.microsoft.com/devops/change-in-azure-pipelines-grant-for-public-projects/

GitHub Actions

在过去几年中,GitHub 一直是数百万开发者的家园,以提供 Git 仓库的版本控制服务而闻名。

Microsoft 于2018年6月收购了 GitHub(https://news.microsoft.com/2018/06/04/microsoft-to-acquire-github-for-7-5-billion/),几个月后,在2018年的 GitHub Universe 活动上,GitHub Actions 被宣布为一个新的自动化平台(https://github.blog/2018-10-17-action-demos/)。巧合吗?我不这么认为。

Azure DevOps 多年来一直是一款 DevOps/ALM 工具,提供许多开发生命周期服务(如规划、仓库托管、测试和 CI/CD 工具)。它始于很久以前的本地 Team Foundation Server 解决方案(我支持过的最古老的 TFS 解决方案来自2008年)。GitHub 多年来也一直是开发者的家园,尤其是开源社区,但它有一个很大的空白:没有提供 CI/CD 服务。

表 5-1. Azure DevOps 与 GitHub Actions

Azure DevOpsGitHub Actions
YAML 和经典管道YAML 工作流(workflow = pipeline)
需要放在 .github/workflow 文件夹下
工作流语法:https://docs.github.com/zh/actions/using-workflows/workflow-syntax-for-github-actions
定价基于并行度定价基于执行分钟数(https://github.com/pricing/calculator#actions)。并行/并发作业数基于许可证,免费计划从20个开始(https://docs.github.com/zh/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits
代理
- Microsoft 托管
- 自托管
在 GitHub 中称为运行器
- GitHub 托管
- 自托管
注意:后端两个平台使用相同的机器:GitHub 运行器和 Azure Pipelines 代理仓库:https://github.com/actions/runner-images
管道
- 阶段
- 作业
- 步骤
- 任务
管道
- 作业:Azure DevOps 中作业/阶段的混合,在 UI 和执行流程上都是如此
- 步骤:操作的集合
- 操作:GitHub 给任务的名称
触发器触发在工作流上用 On 元素定义。最大的区别之一是:你需要在 YAML 定义中包含 workflow_dispatch 才能进行手动触发(Azure Pipelines 中不需要)
变量在 GitHub 中称为环境变量;它还提供默认环境变量(类似于 Azure DevOps 中的系统变量):https://docs.github.com/zh/actions/learn-github-actions/environment-variables#default-environment-variables
变量组没有完全相同的功能;你可以使用 GitHub Secrets(用于机密或非机密)在组织或仓库级别共享变量
环境也叫做环境,但缺少 Azure DevOps 提供的自动化控制流

第5章 借助 GitHub Actions/Azure DevOps/Azure Automation 实现 SRE 自动化

表 5-1. (续)

Azure DevOpsGitHub Actions
任务 (Tasks):主要来自市场(也可创建自定义任务)操作 (Actions):YAML 模板提供两种变体:
• uses: 预定义操作(市场或私有),基于 JavaScript 或 Docker
• run: 直接在 Runner 上执行 Shell 命令(在 Azure DevOps 中,例如也可使用 script 选项)
YAML 模板
https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops
GitHub 也提供重用工作流的机制以实现模块化。重用工作流:https://docs.github.com/en/actions/using-workflows/reusing-workflows
管道项目 (Pipeline artifacts)工作流项目提供类似功能:https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts#uploading-build-and-test-artifacts

这些基本概念将在下一节之后通过演示展示,因为我希望将 GitHub Actions 与接下来将介绍的一些现代部署实践结合使用。

现代部署策略

部署策略定义了更改/升级应用运行实例的过程。如今,受 Agile/Scrum 方法引入的敏捷性影响,解决方案需要找到在不中断最终用户的情况下更频繁地部署更新的方法(零停机部署)。此外,工程团队不断努力为最终解决方案提供价值(回顾第 1 章中 Donovan Brown 对 DevOps 的定义),而在生产环境中测试将是获取有价值反馈的关键实践。

另一方面,作为站点可靠性工程师,在生产环境中测试听起来令人担忧,对吧?我将展示以下部署实践如何提供机制,以零停机部署更新,并在运行时对已发布(或应该说已暴露?)的功能进行控制。将定义两个主要目标:零停机部署以安全方式进行生产环境(或类似生产环境)的测试

滚动部署

图 5-5. 滚动部署

例如,请查看使用 Azure Pipelines YAML 进行滚动部署的教程:https://docs.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-build-deploy-azure-pipelines?tabs=java。

蓝绿部署

蓝绿部署是一种基于拥有两个相同生产环境实例的策略:暂存环境(蓝色)生产环境(绿色)

图 5-6. 蓝绿部署

  1. 新代码版本发布到“暂存”实例。QA 团队(或自动化 E2E/UI 测试)确保新版本运行正常。
  2. 一旦“暂存”测试完成,流量被切换。部署成功后,角色也会互换。
  3. 有些人喜欢将“蓝色”环境保留为旧版本,以便需要回滚时使用。

此策略可以通过多种方式实现(取决于解决方案使用的架构),但主要基于一个负载均衡器,将流量路由到两个相同的实例。在 Azure 中,Azure App Service 提供了一个称为槽位 (slots) 的服务,用于创建易于交换的相同应用服务;Azure 负责负载均衡需求,使其易于实现。

Azure App Service 槽位:https://docs.microsoft.com/en-us/azure/app-service/deploy-staging-slots

功能标志

功能标志(也称为功能切换、功能开关或条件功能)不是部署策略。它们实际上是一个工具,使我们能够启用本节接下来提到的部署策略。

功能标志是一种现代技术,允许你将新功能部署到生产环境,但限制其暴露。它们使我们能够持续将更改部署到生产环境,同时:

  • 如果功能未按预期执行,则可以禁用它(无需立即回滚!)
  • 仅对特定用户启用(也支持其他筛选器)用于实验
  • 在高资源消耗期间禁用某些功能
  • 或者基于启用/禁用功能的自定义筛选器的其他任何用例

图 5-7. 功能标志

  • 功能标志管理器:用于决定启用/禁用哪些功能的服务,可以选择基于筛选器,例如随机流量百分比、特定用户或基于计划。
  • ✓ 应用程序代码向功能标志管理器发出调用以显示/隐藏新功能。如您所见,开发人员需要使用功能标志管理器工具提供的 SDK。

在 Azure 中,一项名为 Azure App Configurationhttps://docs.microsoft.com/en-us/azure/azure-app-configuration/overview) 的新服务已发布,提供了标志的中央存储库(也用于集中/动态配置)。它为 .NET、Java、Python 和 JavaScript 等语言提供 SDK。本节演示中会展示一个示例。

功能标志需要维护,因为如果代码中遗留“死”代码,会增加解决方案的技术债务。请参考 Martin Fowler 撰写的关于最佳实践的以下文章:https://martinfowler.com/articles/feature-toggles.html。

金丝雀部署/基于环的部署

金丝雀部署策略基于逐步将服务发布给生产环境中的一小部分“自愿”用户。用户需要意识到他们正在测试一个“前沿”(非稳定)版本的代码;他们的主要目的是提供有关新暴露功能的反馈。

金丝雀策略可以通过多种方式实现,取决于架构:基于负载均衡技术、部署槽位或功能标志(我最喜欢的方式)。

它可能比蓝绿部署更“便宜”(例如,我们不需要两个相同的生产环境),但也更复杂,因为我们是直接在生产环境中测试。

图 5-8. 基于环的部署

暗启动

此策略基于与金丝雀策略类似的思路,但在此情况下,用户并不知道他们“被用作”测试新功能。反馈主要基于监控技术收集。

A/B 测试

与之前策略相比,A/B 测试侧重于帮助我们决定新功能的两种(或更多)变体。实验可以通过使用功能标志(基于已认证用户或随机流量显示选项 A 或 B)、流量路由或不同部署来实现。

图 5-9. A/B 测试

与其他策略相比,此策略 100% 专注于实验。

[演示] 使用 GitHub Actions 和 Azure App Configuration 实现现代部署

我将使用以下演示来展示最近解释的部分策略与 GitHub Actions 的结合。演示代码可在以下 GitHub 仓库中找到:https://github.com/unaihuete93/SRE_with_Azure_v2(如有需要,修复和更新将发布到该仓库)。这是仓库的结构:

  • src 目录包含主应用程序代码,包括 C# 文件、配置文件和一个 Dockerfile。
  • infra 目录包含不同 Azure 资源的各种 Bicep 文件。
  • .github/workflows 目录包含 CI/CD 流程的工作流。
  • .devcontainer 包含与 GitHub Codespace 中的 Dev Container 配合使用的配置文件。

图 5-10. GitHub Actions 演示架构

  • .NET 8 网站:一个简单的 .NET 8 网站,将由 GitHub Actions 容器化并推送到 Azure 容器注册表。我们将拉取多个版本的容器,并运行在 Azure 容器应用上。
  • GitHub Actions:将用于 CI/CD。流水线将在下面解释。
  • Azure 容器应用https://learn.microsoft.com/en-us/azure/container-apps/overview)将用于运行我们的应用程序。它是一个在无服务器平台上运行容器化应用程序的服务。它提供了一种运行容器的简便方法,无需关心基础设施管理和像 Kubernetes 这样的复杂编排器。简单来说,该服务是一个超级托管的 Kubernetes 集群;你获得 Kubernetes 集群 98% 的功能,而无需自行运行和配置的复杂性。一个 Azure 容器应用由以下组件组成:
    • 环境:容器应用集合的隔离边界(在我们的案例中,是一个单一应用)。不同环境中的应用不共享资源(计算和网络),并且不能使用 DAPR 相互通信。
    • 容器应用:为你管理编排细节(连接到容器注册表、要使用的容器镜像、密钥或流量分配)。
    • 修订版:一种轻松更新运行中容器实例的方式。每次更改容器镜像属性时,都会创建一个新的修订版。你可以轻松定义哪些版本应处于活动状态(接受流量),并为每个修订版提供流量分配(以轻松实现金丝雀或蓝绿部署)。
  • Azure Key Vault:用于保存网站使用的机密(敏感)配置值。
  • Azure App Configuration:用于:
    • 配置管理:为网站提供配置(与 Key Vault 一起)。App Configuration 保存非敏感配置数据并引用 Key Vault 中存储的敏感数据。所有配置都从此单一服务提供给网站。
    • 功能标志管理器:根据应用于资源的规则暴露一个功能。
  • Azure Monitor/App Insights:将用于监控解决方案(在后续监控相关章节中使用)。
  • Azure 容器注册表:将用于存储项目中构建的容器镜像。

开始前,我们需要一个 Microsoft Entra ID > 服务主体(应用程序标识),它将由 GitHub Actions 用于针对我们的 Azure 环境进行身份验证,并能够部署/更新 Azure 资源。

GitHub Actions 支持使用带有 OpenID 的联合凭据,这是最安全的身份验证选项,因为它通过限制可以从中使用的范围(受信任的服务)来替代使用机密。你可以参考以下文档创建它:

第5章 通过GitHub Actions/Azure DevOps/Azure Automation实现SRE自动化

5.1 准备工作:服务主体与联邦凭证

服务主体还应在用于部署的Azure订阅上拥有**参与者(Contributor)**角色。

5.2 CI流水线

现在来看GitHub工作流,首先从CI开始(见清单5-3):

CI工作流文件https://github.com/unaihuete93/SRE_with_Azure_v2/blob/main/.github/workflows/main-ci.yml

清单5-3 GitHub Actions CI工作流

name: CI Pipeline
on:
  push:
    branches:
      - main
    paths-ignore:
      - '.github/workflows/main-cd-aca.yml'
      - 'infra/**/*.bicep'
      - '!infra/modules/acr.bicep'
  pull_request:
    branches:
      - main
  workflow_dispatch:
permissions:
  id-token: write
  contents: read
  actions: write  # 添加此行以授予必要权限
jobs:
  build-and-push-container:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    # 使用 https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect
    - name: Set up Azure CLI
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - name: Create resource group if it does not exist
      run: |
        az group create --name ${{ secrets.AZURE_RESOURCE_GROUP }} --location eastus
    - name: Create Azure Container Registry
      run: |
        az deployment group create \
          --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
          --template-file infra/modules/acr.bicep \
          --parameters acrName=${{ secrets.ACR_NAME }}
    - name: Build and push Docker image
      run: |
        az acr login --name ${{ secrets.ACR_NAME }}
        docker build -t ${{ secrets.ACR_NAME }}.azurecr.io/net8-app:${{ github.run_number }} -f src/Dockerfile src
        docker push ${{ secrets.ACR_NAME }}.azurecr.io/net8-app:${{ github.run_number }}
    - name: "Trigger ACA release pipeline"
      uses: benc-uk/workflow-dispatch@v1
      with:
        workflow: "CD ACA"
        inputs: '{ "IMAGE_TAG": "${{ github.run_number }}" }'

CI工作流解析

  • 触发器:工作流可通过以下方式触发:
    • main 分支推送(忽略特定文件夹路径,但 infra/modules/acr.bicep 除外)
    • main 分支发起拉取请求
    • 手动触发(workflow_dispatch
  • 作业:CI流程定义了一个作业 build-and-push-container
  • 检出代码:使用 actions/checkout@v2 检出仓库代码,确保工作流可以访问仓库文件。
  • 设置Azure CLI:使用 azure/login@v1 并通过之前为GitHub Actions创建的服务主体进行Azure身份验证。需要 client-idtenant-idsubscription-id,这些信息作为仓库机密(secrets)存储。此步骤对于后续Azure操作至关重要。
  • 创建资源组:运行Azure CLI命令创建资源组(如果尚不存在)。资源组由机密 AZURE_RESOURCE_GROUP 指定,创建在 eastus 区域。
  • 创建Azure容器注册表(ACR):使用Bicep模板文件 (infra/modules/acr.bicep) 部署Azure容器注册表。部署在指定的资源组中,ACR名称由机密 ACR_NAME 提供。
  • 构建并推送容器镜像:登录到Azure容器注册表,从 src 目录中的Dockerfile构建Docker镜像,使用GitHub工作流的运行号(run number)作为标签,然后推送到ACR。
  • 触发ACA发布流水线:最后一步使用 benc-uk/workflow-dispatch@v1 触发另一个名为“CD ACA”的GitHub Actions工作流,并将镜像标签(运行号)作为输入传递。

CI流水线的结果是:创建一个包含演示.NET 8网站容器化版本的Azure容器注册表。

5.3 CD流水线

现在来看用于Azure容器应用(ACA)的CD工作流(见清单5-4):

CD工作流文件https://github.com/unaihuete93/SRE_with_Azure_v2/blob/main/.github/workflows/main-cd-aca.yml

清单5-4 GitHub Actions CD工作流

name: CD ACA
on:
  workflow_dispatch:
    inputs:
        IMAGE_TAG:
          description: "Image tag to be deployed"
          required: true
        FIRST_DEPLOY:
          description: "Is this the first deployment?"
          required: true
          default: "false"
env:
  CONTAINER-APP-NAME: "srewithazurev2-aca-app"
jobs:
  deploy-container-apps:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    # 使用 https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect
    - name: Set up Azure CLI
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
        allow-no-subscriptions: true
    - name: Enable debug logs
      run: echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV
    - name: Create resource group if it does not exist
      run: |
        az group create --name ${{ secrets.AZURE_RESOURCE_GROUP }} --location eastus
    - name: Get latest revision name if not first deploy
      if: ${{ github.event.inputs.FIRST_DEPLOY == 'false' }}
      id: get-revision
      run: |
        revision_name=$(az containerapp revision list \
          --name ${{ env.CONTAINER-APP-NAME}} \
          --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
          --query "[?properties.active].name | [0]" \
          --output tsv)
        echo "revision_name=$revision_name" >> $GITHUB_ENV
        echo "revision_name=$revision_name" >> $GITHUB_OUTPUT
    - name: Create Azure Container App solution
      run: |
        az deployment group create \
          --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
          --template-file infra/main.bicep \
          --parameters  containerAppName=${{ env.CONTAINER-APP-NAME}} \
                       useNewContainerAppModule=${{ github.event.inputs.FIRST_DEPLOY }} \
                       weatherApiKey=${{ secrets.WEATHER_API_KEY }} \
                       acrName=${{ secrets.ACR_NAME }} \
                       containerImage=${{ secrets.ACR_NAME }}.azurecr.io/net8-app:${{ github.event.inputs.IMAGE_TAG }} \
                       revisionName=${{ steps.get-revision.outputs.revision_name }}
  swap-container-apps:
    needs: deploy-container-apps
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    environment:
      name: prod
    steps:
    # 使用 https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect
    - name: Set up Azure CLI
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
        allow-no-subscriptions: true
    - name: swap traffic between revisions
      run: |
        az containerapp ingress traffic set --name ${{ env.CONTAINER-APP-NAME}} \
          -g ${{ secrets.AZURE_RESOURCE_GROUP }} \
          --revision-weight latest=100

CD工作流解析

工作流名称

  • name: CD ACA:工作流名称为“CD ACA”,表明用于Azure容器应用的持续部署。

触发器

  • on: workflow_dispatch:可通过GitHub Actions界面手动触发,或由CI工作流自动触发。

输入参数

  • inputs:
    • IMAGE_TAG:必需输入,指定要部署到容器应用解决方案的镜像标签。
    • FIRST_DEPLOY:必需输入,指示是否为首次部署。默认值为 "false"

环境变量

  • env: CONTAINER-APP-NAME: “srewithazurev2-aca-app”:设置容器应用名称的环境变量。可根据需要修改为唯一名称。

作业(Jobs)

该工作流定义了两个作业:deploy-container-appsswap-container-apps


作业1:deploy-container-apps

  • runs-on: ubuntu-latest:在最新Ubuntu运行器上执行。
  • permissions: id-token: write:授予ID令牌的写入权限。
  • 步骤
    1. 检出代码:使用 actions/checkout@v2 从仓库检出代码。
    2. 设置Azure CLI:使用 azure/login@v1 并利用存储在GitHub机密中的凭据设置Azure CLI。
    3. 启用调试日志echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV 为工作流启用调试日志(用于故障排除)。
    4. 创建资源组:运行 az group create 创建资源组(如果尚不存在)。
    5. 获取最新修订版名称(条件性)
      • if: ${{ github.event.inputs.FIRST_DEPLOY == 'false' }}:仅在非首次部署时执行此步骤。
      • 运行 az containerapp revision list ... 检索容器应用的最新修订版名称,并将其存储到环境变量和输出中。
    6. 创建Azure容器应用解决方案:使用Bicep模板文件 (infra/main.bicep) 和参数部署完整的解决方案架构。

作业2:swap-container-apps

  • needs: deploy-container-apps:此作业依赖于 deploy-container-apps 作业的成功完成。
  • runs-on: ubuntu-latest:在最新Ubuntu运行器上执行。
  • permissions: id-token: write:授予ID令牌的写入权限。
  • environment: name: prod:利用GitHub Actions中的环境功能,在执行该作业前要求手动审批。你需要在仓库中创建一个名为 prod 的环境,并将自己指定为审批者。
  • 步骤
    1. 设置Azure CLI:与作业1相同,使用机密进行身份验证。
    2. 切换修订版之间的流量
      • 运行 az containerapp ingress traffic set 命令,将100%的流量指向最新的修订版(latest),从而实现蓝绿部署或金丝雀发布中的流量切换。

章节小结

通过本章,你学习了如何使用GitHub Actions构建CI/CD流水线,实现基础设施即代码(IaC)和自动化部署。CI流水线负责构建并推送容器镜像,CD流水线负责部署到Azure容器应用并管理流量切换。这些实践是SRE自动化的核心组成部分。

第5章 通过GitHub Actions/Azure DevOps/Azure Automation实现SRE自动化

160

ii. 在修订版之间切换流量: bash run: az containerapp ingress traffic set \ --name ${{ env.CONTAINER-APP-NAME}} \ -g ${{ secrets.AZURE_RESOURCE_GROUP }} \ --revisions-weight latest=100 此命令将流量切换到容器应用的最新修订版。

除前述众多组件外,CD 管道还创建了一个用户分配的托管标识,并将其链接到 Azure 容器应用,用于以下目的:

  • 容器应用获取对 Azure 应用配置的访问权限
  • 容器应用获取对 Key Vault 的访问权限
  • 容器应用获取从 Azure 容器注册表拉取镜像的访问权限

该托管标识和必要角色已由 GitHub CD 工作流通过 Bicep 自动创建。

在实际应用程序代码中,我们可以看到如何利用用户分配的托管标识,使运行在 Azure 应用配置中的容器能够访问 Azure 应用配置和 Azure Key Vault:https://github.com/unaihuete93/SRE_with_Azure_v2/blob/main/src/Program.cs(见代码清单 5-5)。

代码清单 5-5. .NET 中的应用配置设置

builder.Configuration.AddAzureAppConfiguration(options =>
                        options.Connect(new Uri(endpoint),
                            new ManagedIdentityCredential(Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")))
                            .ConfigureKeyVault(kv =>
                            {
                                kv.SetCredential(new ManagedIdentityCredential(Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")));
                            })
                            .ConfigureRefresh(refresh =>
                            {
                                refresh.Register("Refresh:Config", refreshAll: true)
                                       .SetCacheExpiration(new TimeSpan(0, 0, 10)); // 10 秒过期
                            })
                            .UseFeatureFlags());

ManagedIdentityCredential 类能够检测链接到 Azure 容器应用的托管标识,从而同时向 Azure Key Vault 和应用配置进行身份验证。这几行代码将赋予您的应用访问敏感(Key Vault)和非敏感(应用配置)配置值的能力。


图 5-11. Key Vault 访问策略

图 5-12. 应用配置访问控制

https://github.com/unaihuete93/SRE_with_Azure_v2/blob/main/infra/modules/container-app-new.bicep

图 5-13. 容器应用修订版 URL

功能标志和应用配置

要演示功能标志的使用,您需要执行以下操作。

  • .NET 网站通过调用 OpenWeather API 向用户提供天气预报。要获取 API 密钥,请按照以下说明操作:https://openweathermap.org/appid
    • 该密钥必须作为 GitHub 仓库密钥存储,密钥名称为 WEATHER_API_KEY。CD 工作流会在 Key Vault 中为其创建一个密钥。
  • 您需要从 Azure 应用配置中引用 Key Vault 密钥。转到应用配置资源 ➤ 配置资源管理器 ➤ 创建 Key Vault 引用。指向保存 OpenWeather API 密钥的 Key Vault 密钥的最新版本,并将其命名为 Weather:ApiKey
  • 您需要在 Azure 应用配置 ➤ 功能管理器中创建一个名为 WeatherAPI 的功能标志。

https://github.com/unaihuete93/SRE_with_Azure_v2/blob/main/src/Pages/Weather.cshtml.cs

https://github.com/unaihuete93/SRE_with_Azure_v2/blob/main/src/Program.cs

图 5-14. 网站天气功能已启用

图 5-15. 应用配置功能已启用

结合身份验证库使用功能标志,我们还可以仅对特定用户/组启用功能标志(金丝雀/基于环的部署):https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-targetingfilter-aspnet-core

总结

在本章中,您了解了自动化主题,回顾了能使应用程序增量更新更安全、可靠和高效的实践。如前所述,SRE 至少需要理解本章解释的概念。具体实现将取决于组织的 DevOps 成熟度(以及现有角色)。

本章重点解释了基本的 DevOps 概念,并展示了如何使用 Azure DevOps 和 GitHub Actions 等工具来实现它们。

希望本章为您提供了对该主题的良好介绍;每个定义的概念都可以进一步深入研究和学习(但这并非本书的重点)。


图片索引: [Image 497 on Page 127] [Image 519 on Page 133] [Image 584 on Page 152] [Image 589 on Page 153] [Image 597 on Page 155] [Image 602 on Page 156] [Image 607 on Page 157] [Image 612 on Page 158] [Image 659 on Page 173] [Image 661 on Page 173] [Image 666 on Page 174] [Image 671 on Page 175] [Image 676 on Page 176]