1.5.2.1. 端到端测试

在大规模维护和治理 Terraform 模块的过程中,端到端(End-to-End,简称 E2E)测试是确保基础设施代码质量的关键环节。本章将深入探讨如何使用 Terratest 和 Azure 的 terraform-module-test-helper 工具对 Terraform 模块进行全面的端到端测试。

1.5.2.1.1. 什么是端到端测试?

端到端测试是一种模拟实际部署流程的测试方法,旨在验证整个基础设施部署是否符合预期。与单元测试或集成测试不同,端到端测试关注的是系统整体的行为,确保各个组件在实际运行环境中的协同工作。

在 Terraform 的上下文中,端到端测试通常包括以下步骤:

  1. 部署测试环境:使用 Terraform 将模块部署到隔离的测试环境中。
  2. 验证部署结果(可选):通过实际操作(如网络连接测试、API 调用等)验证资源的功能和配置。
  3. 清理测试环境:测试完成后,销毁所有部署的资源,确保环境的整洁。

这种测试方法可以帮助开发者在将代码推送到生产环境之前,发现潜在的问题,降低风险。

1.5.2.1.2. 为什么选择 Terratest?

Terratest 是 GruntWork 开发的一个基于 Go 语言的开源测试框架,专为基础设施即代码(IaC)工具(如 Terraform)设计。它提供了丰富的功能,支持自动化部署、验证和销毁基础设施资源。

1.5.2.1.2.1. Terratest 的优势

  • 自动化测试流程:Terratest 可以自动执行 terraform initapplydestroy 等命令,简化测试流程。
  • 灵活的验证机制:通过 Go 语言编写测试逻辑,可以实现复杂的验证操作,如 API 调用、端口检查等。
  • 集成测试阶段控制:通过 test_structure 模块,可以将测试分为多个阶段,便于调试和复用。

1.5.2.1.2.2. Terratest 的典型测试结构

一个典型的 Terratest 测试文件通常包括以下结构:

package test

import (
    "testing"

    "github.com/gruntwork-io/terratest/modules/terraform"
    test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
)

func TestTerraformModule(t *testing.T) {
    t.Parallel()

    // 设置测试目录
    exampleDir := "../examples/basic"

    // 部署阶段
    test_structure.RunTestStage(t, "deploy", func() {
        terraformOptions := &terraform.Options{
            TerraformDir: exampleDir,
        }
        test_structure.SaveTerraformOptions(t, exampleDir, terraformOptions)
        terraform.InitAndApply(t, terraformOptions)
    })

    // 验证阶段
    test_structure.RunTestStage(t, "validate", func() {
        terraformOptions := test_structure.LoadTerraformOptions(t, exampleDir)
        output := terraform.Output(t, terraformOptions, "resource_id")
        // 添加验证逻辑,例如检查资源是否存在
    })

    // 清理阶段
    test_structure.RunTestStage(t, "destroy", func() {
        terraformOptions := test_structure.LoadTerraformOptions(t, exampleDir)
        terraform.Destroy(t, terraformOptions)
    })
}

通过上述结构,开发者可以清晰地划分测试的各个阶段,便于维护和扩展。

1.5.2.1.3. Azure 的 terraform-module-test-helper 工具

在 Azure 的 Terraform 模块开发中,terraform-module-test-helper 是一个专门用于简化端到端测试的 Go 工具库。它封装了 Terratest 提供的常用的测试逻辑,结合 Azure Verified Module 的结构,提供了更高层次的抽象,降低了测试的复杂度。

1.5.2.1.3.1. RunE2ETest 函数

RunE2ETest 是该工具库中用于执行端到端测试的核心函数,其典型用法如下:

func TestExample(t *testing.T) {

    test_helper.RunE2ETest(t, "../../", "examples/startup", terraform.Options{
        Upgrade: true,
    }, func(t *testing.T, output test_helper.TerraformOutput) {
        // 添加验证逻辑,例如检查输出是否符合预期
        resourceID, ok := output["resource_id"].(string)
        assert.True(t, ok)
        assert.Contains(t, resourceID, "/subscriptions/")
    })
}

该函数接受模块的根路径、示例代码的子目录、Terraform 配置选项以及一个用于验证输出的回调函数。它会自动执行 terraform initapplydestroy,并在部署后调用回调函数进行验证。

1.5.2.1.4. 端到端测试的并行化与全局配置管理

在大规模维护和治理 Terraform 模块的过程中,端到端测试的效率和稳定性至关重要。GitHub Actions 有一个重要限制:所有任务的执行时长上限是 6 小时,如果我们串行测试所有样例,那么在样例比较多且执行比较耗时的情况下会发生超时。为了加快测试速度并确保测试环境的一致性,我们采用了并行化测试策略,并引入了全局的配置管理机制。

1.5.2.1.4.1. 并行化测试的策略

在 AVM(Azure Verified Modules)项目中,每个模块通常包含多个使用示例,位于 examples/ 目录下的不同子目录中。为了提高测试效率,我们利用 GitHub Actions 的矩阵策略(matrix strategy),为每个示例分配一个独立的 Runner 实例,进行并行测试。

具体流程如下:

  1. 获取示例列表:首先,执行一个名为 get-examples 的任务,扫描 examples/ 目录,识别包含 .tf 文件的子目录。

  2. 生成测试矩阵:将上述识别的子目录列表作为输出,传递给后续的测试任务,形成 GitHub Actions 的矩阵配置。

  3. 并行执行测试:对于矩阵中的每个示例,GitHub Actions 会启动一个独立的 Job,使用 terraform-module-test-helper 工具执行测试。

这种并行化的测试策略显著缩短了测试总耗时,提高了测试的覆盖率和效率。

1.5.2.1.4.2. 全局配置与清理机制

在并行执行多个测试任务之前,可能需要对测试环境进行全局的配置,如重置某些全局资源或设置共享的环境变量。为此,我们引入了全局的 setupteardown 机制,确保测试环境的一致性和整洁性。

全局配置(Setup)

在执行测试之前,GitHub Actions 会检查 examples/ 目录下是否存在 setup.sh 脚本。如果存在,则执行一个名为 globalsetup 的 Job,运行该脚本,完成必要的全局配置。

全局清理(Teardown)

测试完成后,GitHub Actions 会检查 examples/ 目录下是否存在 teardown.sh 脚本。如果存在,则执行一个名为 globalteardown 的 Job,运行该脚本,清理测试过程中创建的全局资源。

通过将全局清理操作放在独立的 Job 中,并设置依赖关系,确保在所有测试任务完成后执行清理操作,保持环境的整洁。

1.5.2.1.4.3. 最佳实践建议

在实施并行化测试和全局配置管理时,建议遵循以下最佳实践:

  • 幂等性:确保 setup.shteardown.sh 脚本具有幂等性,即多次执行不会产生副作用。
  • 错误处理:对某些已知的测试中可能会出现的 API 不稳定现象,可以通过设置 Terratest 提供的 Retryable Errors 来自动重试。Retryable Errors 应以 Terragrunt 配置格式保存在样例代码中,提示用户运行该样例可能会遇到可重试错误,并且可以通过 terragrunt 搭配我们提供的配置文件来自动重试。
  • 资源隔离:在并行测试中,确保每个测试任务使用独立的资源,避免资源冲突。
  • 日志记录:在脚本和测试中添加详细的日志记录,便于问题排查和调试。

通过遵循上述最佳实践,可以进一步提升端到端测试的可靠性和可维护性。

1.5.2.1.5. 来自配置漂移的挑战

在大规模 Terraform 模块的维护和治理中,确保基础设施配置的一致性至关重要。配置漂移(Drift)可能导致安全漏洞、合规性问题和运行时错误。因此,在端到端测试中集成漂移检测机制,成为保障基础设施稳定性的关键步骤。

我们下面将继续深入探讨 Azure 官方的 Terraform 模块测试工具库 terraform-module-test-helper 中的 e2etest.goupgradetest.go 文件,分析其如何通过函数如 RunE2ETestinitAndPlanAndIdempotentAtEasyMode 实现自动化的漂移检测,确保样例代码在部署后保持配置的一致性。

配置漂移指的是实际部署的基础设施状态与 Terraform 状态文件(terraform.tfstate)中记录的期望状态之间的差异。这种差异可能由以下原因引起:

  • 手动更改:运维人员或开发者直接在云平台控制台修改资源配置。
  • 外部系统干预:其他自动化工具或脚本对资源进行修改。
  • 资源自动变化:某些资源在运行过程中自动调整配置,如自动扩展组的实例数量变化。

这些漂移可能导致基础设施与代码定义不一致,增加故障风险。因此,及时检测并修复漂移是保障系统稳定运行的重要措施。

在 Terraform Module 维护工作中,我们会使用一个没有外部系统干预的测试订阅,并且该订阅只用于 Terraform Module 的测试维护,所以天然地不受 12 的影响,但我们需要额外的测试验证,确保我们的模块不受 3 的影响。

1.5.2.1.6. terraform-module-test-helper 库的initAndPlanAndIdempotentAtEasyMode 函数

upgradetest.go 文件中,initAndPlanAndIdempotentAtEasyMode 函数用于简化模块的初始化、计划和幂等性验证过程。其核心逻辑包括:

  • 初始化 Terraform 环境:执行 terraform init,确保环境准备就绪。
  • 生成执行计划:运行 terraform plan,生成资源变更计划。
  • 幂等性验证:多次运行 terraform plan,确保在没有代码更改的情况下,计划结果保持一致,验证模块的幂等性。

通过这些步骤,该函数确保模块在多次应用过程中不会引入意外的变更,增强了模块的稳定性和可靠性。以下是用以判断在 apply 后立即执行的 plan 命令得到的计划中是否存在配置漂移的函数定义:

func noChange(changes map[string]*tfjson.ResourceChange) bool {
    if len(changes) == 0 {
        return true
    }
    return linq.From(changes).Select(func(i interface{}) interface{} {
        return i.(linq.KeyValue).Value
    }).All(func(i interface{}) bool {
        change := i.(*tfjson.ResourceChange).Change
        if change == nil {
            return true
        }
        if change.Actions == nil {
            return true
        }
        return change.Actions.NoOp()
    })
}

这个函数的作用是判断 Terraform 的资源变更集合 (changes) 是否表示“没有任何配置变更”(即无配置漂移)。

1.5.2.1.6.1. 函数输入

changes 参数是一个 map,键为资源的唯一标识符,值为对应的 tfjson.ResourceChange 对象,这表示在执行 terraform plan 后 Terraform 解析出的资源变更详情。

1.5.2.1.6.2. 函数实现逻辑分析

函数的逻辑分为两个主要部分:

快速路径检查

首先检查:

if len(changes) == 0 {
    return true
}
  • changes 为空(即没有任何资源的变更)时,函数立即返回 true,表明无任何漂移。

深入检查每个资源变更

若存在资源变更记录(即 len(changes) != 0),则进行更深入的检查:

return linq.From(changes).Select(func(i interface{}) interface{} {
    return i.(linq.KeyValue).Value
}).All(func(i interface{}) bool {
    change := i.(*tfjson.ResourceChange).Change
    if change == nil {
        return true
    }
    if change.Actions == nil {
        return true
    }
    return change.Actions.NoOp()
})

如果左右的变更条目都符合以下条件:

  1. 如果 ResourceChange.Changenil,则表示没有具体的变更内容(安全返回 true)。

    if change == nil {
        return true
    }
    
  2. 如果 ResourceChange.Change.Actionsnil,则表示 Terraform 没有检测到动作,也可视为无变更(安全返回 true)。

    if change.Actions == nil {
        return true
    }
    
  3. 如果 ResourceChange.Change.Actions.NoOp() 返回 true,则表示当前资源在 Terraform 执行计划中明确显示没有动作(即无创建、修改、删除操作)。这正是判断配置漂移最关键的部分:

    return change.Actions.NoOp()
    
  • 只有所有资源满足上述三个条件之一,才会整体返回 true。如果任何一个资源存在实际的变更操作(如 create, update, delete 等),NoOp() 将返回 false,进而导致整个函数返回 false,表明存在配置漂移。

1.5.2.1.6.3. Terraform 的 NoOp() 方法解释

NoOp() 是 Terraform 官方库中定义的一个方法,表示在计划中是否没有实际动作:

  • Actions["no-op"] 时(即明确地无任何操作),NoOp() 返回 true
  • 如果存在其他动作(例如 "create", "update", "delete"),则返回 false

因此,change.Actions.NoOp() 能准确识别该资源是否发生实际变更。

1.5.2.1.6.4. 为什么不使用 Terraform 原生测试框架和命令编写端到端测试?

选择 Terratest 是因为通过 Go 编写测试代码时,可以针对创建的基础设施进行一些功能测试,例如访问一下 Http 服务,看看是否可以得到期待的返回值等。Terraform 原生测试框架只能用于检验 Terraform 计划或是状态是否符合预期,无法执行其他自定义的验证逻辑。

当然,如果我们只需要验证 Terraform 状态文件,那使用原生测试框架也可以。Terratest 的问题在于编写测试需要掌握 Go 语言,而并非所有模块维护者都掌握了 Go。

1.5.2.1.7. Terraform 模块升级中的破坏性变更检测测试

在 Terraform 模块的维护与治理中,确保模块的稳定性和向后兼容性是至关重要的。特别是在大规模模块的管理中,任何未经检测的破坏性变更都可能对依赖该模块的多个项目造成严重影响。因此,建立一套有效的破坏性变更检测测试机制,成为保障模块质量的关键环节。

1.5.2.1.7.1. 破坏性变更的定义与影响

破坏性变更(Breaking Changes)指的是那些会导致现有模块使用者的配置无法正常工作的更改。这些更改可能包括:

  • 移除或重命名已有的输入变量或输出变量;
  • 修改资源的关键属性,导致资源被销毁并重新创建;
  • 更改资源的默认行为或依赖关系;
  • 修改模块的文件结构或路径,影响模块的引用方式。

根据语义化版本控制(Semantic Versioning)的原则,只有在主版本(Major Version)升级时,才允许引入破坏性变更。因此,在次版本(Minor Version)或修订版本(Patch Version)中引入破坏性变更,违反了版本控制的约定,可能导致使用者在不知情的情况下遭遇配置错误或资源中断。

1.5.2.1.7.2. 检测破坏性变更的策略

为了在模块升级过程中及时发现破坏性变更,可以采用以下策略:

  1. 版本对比测试:通过比较当前模块版本与上一个稳定版本的行为差异,识别潜在的破坏性变更。
  2. 自动化测试集成:在持续集成(CI)流程中,加入自动化测试步骤,确保每次提交都经过充分的验证。

1.5.2.1.7.3. 如何实现破坏性变更检测

以下是 terraform-module-test-helper 库中实现破坏性变更检测的代码:

func moduleUpgrade(t *T, owner string, repo string, moduleFolderRelativeToRoot string, newModulePath string, opts terraform.Options, currentMajorVer int) error {
    if currentMajorVer == 0 {
        return SkipV0Error
    }
    latestTag, err := getLatestTag(owner, repo, currentMajorVer)
    if err != nil {
        return err
    }
    if semver.Major(latestTag) == "v0" {
        return SkipV0Error
    }
    tmpDirForTag, err := cloneGithubRepo(owner, repo, &latestTag)
    if err != nil {
        return err
    }

    fullTerraformModuleFolder := filepath.Join(tmpDirForTag, moduleFolderRelativeToRoot)

    exists := files.FileExists(fullTerraformModuleFolder)
    if !exists {
        return CannotTestError
    }
    tmpTestDir := test_structure.CopyTerraformFolderToTemp(t, tmpDirForTag, moduleFolderRelativeToRoot)
    defer func() {
        _ = os.RemoveAll(filepath.Clean(tmpTestDir))
    }()
    return diffTwoVersions(t, opts, tmpTestDir, newModulePath)
}

func diffTwoVersions(t *T, opts terraform.Options, originTerraformDir string, newModulePath string) error {
    opts.TerraformDir = originTerraformDir
    defer destroy(t, opts)
    initAndApply(t, &opts)
    overrideModuleSourceToCurrentPath(t, originTerraformDir, newModulePath)
    return initAndPlanAndIdempotentAtEasyMode(t, opts)
}
  1. 获取上一个稳定版本的版本号:通过 GitHub API 获取模块的最新稳定版本标签,作为对比基准。
  2. 克隆指定版本的代码:使用 go-getter 工具,将指定版本的模块代码克隆到本地临时目录。
  3. 执行 Terraform Plan:在克隆的模块目录中,执行 terraform initterraform apply,确保当前配置可用。
  4. 替换模块源路径:将模块引用路径替换为当前开发版本的本地路径,模拟模块升级场景。
  5. 再次执行 Terraform Plan:执行 terraform plan,并分析输出结果,判断是否存在资源的销毁和重建操作。
  6. 结果判断与报告:如果检测到资源的销毁和重建,且当前版本号未进行主版本升级,则标记为破坏性变更,阻止该变更的合并。

1.5.2.1.7.4. 什么时候应该跳过破坏性变更测试?

在下述特定情况下应该跳过破坏性变更测试:

  1. 当前模块的主版本尚处于 v0 时,此时模块仍处于探索阶段,要求避免一切破坏性变更并不实际
  2. 当我们下一个要发布的新版本将是一个主版本更新时,我们将在一次更新中尽可能多地容纳破坏性变更,应该说,此时是引入破坏性变更的唯一窗口时刻

1.5.2.1.7.5. 持续集成中的应用

将上述检测机制集成到持续集成流程中,可以实现对破坏性变更的自动化检测。具体步骤如下:

  1. CI 触发条件:在每次提交或拉取请求(Pull Request)时,触发破坏性变更检测测试。
  2. 环境准备:在 CI 环境中,设置必要的环境变量,如 GITHUB_TOKENPREVIOUS_MAJOR_VERSION 等。
  3. 执行测试脚本:运行破坏性变更检测测试脚本,自动完成版本对比、计划执行和结果分析。
  4. 结果反馈:根据测试结果,决定是否允许合并当前变更,或提示开发者进行主版本升级。

通过上述策略和实践,可以有效地检测和防范破坏性变更,提升 Terraform 模块的稳定性和可靠性,保障基础设施的持续健康运行。

1.5.2.1.8. 小结

端到端测试不仅是保障 Terraform 模块质量的一道关键防线,更是我们在大规模模块治理中实现“自动化可靠性”的核心机制。本节围绕 Terratest 与 terraform-module-test-helper 两种工具,讲解了如何构建完整的 E2E 测试框架、实现样例并行化测试、配置漂移检测与破坏性变更监测等关键能力。

在 Terratest 中,我们看到了通过 Go 语言实现基础设施行为验证的灵活性与可编程性,它让我们能够以最贴近用户视角的方式,验证模块的实际部署效果。而在 Azure 的 terraform-module-test-helper 工具中,通过封装后的 RunE2ETestinitAndPlanAndIdempotentAtEasyMode 等函数,我们可以将模块测试流程自动化、标准化,并落地到日常的 CI 工作流中。

通过并行测试策略与全局 setup/teardown 的机制设计,我们大幅缩短了测试时长并增强了测试的可控性,避免了串行执行带来的超时风险。在实际生产维护中,配置漂移检测和破坏性变更测试,则进一步增强了我们模块对环境演变与版本升级的鲁棒性。

简而言之,一个没有端到端测试的 Terraform 模块,无法被信任用于生产环境。尤其是在模块数量达到几十、上百的规模后,端到端测试不仅是质量的保障机制,更是维护可扩展性与长期可持续性的基础设施资产。

results matching ""

    No results matching ""