Terratest 实战:为 Azure Container Instances(ACI)Terraform 模块编写自动化测试

原创2026-09-26 12:19:17465 阅读
文章标签:测试开发工具DevOps质量保障

Terratest 实战:为 Azure Container Instances(ACI)Terraform 模块编写自动化测试

导读

本文以 Terratest 仓库中的 examples/azure/terraform-azure-aci-example 示例模块为核心,完整讲解如何用 Terraform 在 Azure 上部署一个 Azure Container Instance(容器实例),并借助 Terratest 编写 Go 自动化测试来验证部署结果。读完本文,你将掌握该模块的 Terraform 配置结构、Azure 凭据与环境变量的配置方法、手动执行 terraform apply 的完整流程,以及使用 TestTerraformAzureACIExample 这类端到端测试对真实云资源进行断言验证的完整实战方案。

模块概览:一个可测试的 ACI 部署单元

该示例模块位于 examples/azure/terraform-azure-aci-example 目录,是 Terratest 仓库中 Azure 示例族(terraform-azure-*-example)的一个成员。它的作用是通过 Terraform 在 Azure 中部署一个 Azure Container Instance(下文简称 ACI),用于演示如何为 Azure Terraform 代码编写自动化测试。模块文件结构如下:

文件 作用
main.tf 声明 provider、资源组(Resource Group)与容器组(Container Group)
variables.tf 定义 location 与 postfix 两个输入变量
output.tf 导出资源组名、IP 地址、FQDN、容器实例名四个输出值
README.md 手动运行与自动化测试的操作说明

与之配套的测试代码位于 test/azure/terraform_azure_aci_example_test.go,断言所用的 Azure SDK 封装函数位于 modules/azure/containers.go。整体测试链路为:Terraform 负责"部署资源",Terratest 的 Azure 模块负责"独立核实资源真实属性与 Terraform 输出一致",这正是 Terratest 对基础设施代码进行自动化验证的核心思想。

手动运行该模块的完整步骤

原文档给出了直接手动操作该模块的清单式步骤,本文将其完整保留并补充关键细节:

  1. 注册 Azure 账号:在 Azure 官网注册订阅,用于创建资源组与容器实例。
  2. 配置 Azure 凭据:使用 Azure CLI 官方支持的方式完成认证配置(服务主体、登录会话等任一受支持方式均可)。
  3. 安装 Terraform:安装 Terraform 并将其加入系统 PATH,确保命令行可直接调用 terraform。
  4. 准备环境变量:确保 ARM_CLIENT_ID、ARM_CLIENT_SECRET、ARM_SUBSCRIPTION_ID、ARM_TENANT_ID 等环境变量已在测试主机上就绪,详见下文"环境变量与 Azure 凭据配置"一节。
  5. 初始化:在模块目录下运行 terraform init,下载 hashicorp/azurerm provider。
  6. 应用部署:运行 terraform apply,创建资源组与容器组。
  7. 清理资源:操作完成后运行 terraform destroy,销毁本次部署的全部资源。

注意:手动运行后必须执行 terraform destroy,避免资源长期留存产生不必要的 Azure 账单。

Terraform 配置逐项解析

provider 与版本约束(main.tf)

模块在 main.tf 中声明了 Terraform 与 provider 的版本约束:

terraform {
  required_version = ">= 1.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
}
  • required_version = ">= 1.0":要求 Terraform 1.0 及以上版本;
  • azurerm ~> 3.0:锁定 AzureRM provider 的 3.x 系列,允许 3.0 到 4.0 之前的次版本更新;
  • features {}:开启 azurerm provider 的可选功能块,是 AzureRM 3.x 使用时的常规配置。

资源组与容器组(main.tf)

模块部署两个核心资源:

resource "azurerm_resource_group" "rg" {
  name     = "terratest-aci-rg-${var.postfix}"
  location = var.location
}

resource "azurerm_container_group" "aci" {
  name                = "aci${var.postfix}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  ip_address_type = "Public"
  dns_name_label  = "aci${var.postfix}"
  os_type         = "Linux"

  container {
    name   = "hello-world"
    image  = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
    cpu    = "0.5"
    memory = "1.5"

    ports {
      port     = 443
      protocol = "TCP"
    }
  }

  tags = {
    Environment = "Development"
  }
}

关键配置点说明:

  • 资源组命名:terratest-aci-rg-${var.postfix},通过 postfix 变量规避不同测试执行之间的资源名冲突(详见下文变量说明);
  • IP 地址类型:ip_address_type = "Public",使容器组获得公网 IP,并配合 dns_name_label 生成可访问的 FQDN;
  • 容器镜像:使用微软官方示例镜像 mcr.microsoft.com/azuredocs/aci-helloworld:latest,这是一个内置 Web 服务的最小演示镜像;
  • 资源配置:cpu = "0.5"、memory = "1.5"(单位 GiB),属于 ACI 支持的小规格组合,适合免费额度范围内的演示;
  • 端口映射:暴露 TCP 443 端口;
  • 标签:打上 Environment = Development 便于在 Azure 门户中识别资源用途。

输入变量(variables.tf)

variables.tf 定义了模块仅有的两个可选变量,均带合理默认值:

variable "location" {
  description = "The supported azure location where the resource exists"
  type        = string
  default     = "West US2"
}

variable "postfix" {
  description = "A postfix string to centrally mitigate resource name collisions."
  type        = string
  default     = "1276"
}
  • location:资源部署的 Azure 区域,默认 West US2,可按需改为其他受支持的区域;
  • postfix:附加在资源名后的随机后缀字符串,默认 1276,用于集中缓解资源命名冲突——这是云资源自动化测试中的常见手段,因为 Azure 资源名(尤其是公网 FQDN)具有全局唯一性要求,多次执行部署时必须避免重名。

输出变量(output.tf)

output.tf 导出四个输出值,它们同时是自动化测试的断言目标:

output "resource_group_name" {
  value = azurerm_resource_group.rg.name
}

output "ip_address" {
  value = azurerm_container_group.aci.ip_address
}

output "fqdn" {
  value = azurerm_container_group.aci.fqdn
}

output "container_instance_name" {
  value = azurerm_container_group.aci.name
}

这四个输出分别对应:资源组名称、容器组的公网 IP、FQDN(由 dns_name_label 生成)与容器实例名称。测试正是通过 terraform output 读取这些值,再与 Azure SDK 返回的"真实状态"做比对。

环境变量与 Azure 凭据配置

Terratest 的 Azure 测试链路中,Terraform 与 Azure SDK 均需要凭据支持。仓库的 examples/azure/README.md 对此有完整说明:需要设置四个核心环境变量,并在非公有云场景下设置 AZURE_ENVIRONMENT:

export ARM_CLIENT_ID=your_app_id
export ARM_CLIENT_SECRET=your_password
export ARM_SUBSCRIPTION_ID=your_subscription_id
export ARM_TENANT_ID=your_tenant_id

# AZURE_ENVIRONMENT 取值之一(不设置时默认 AzurePublicCloud):
export AZURE_ENVIRONMENT=AzureUSGovernmentCloud
export AZURE_ENVIRONMENT=AzureChinaCloud
export AZURE_ENVIRONMENT=AzureGermanCloud
export AZURE_ENVIRONMENT=AzurePublicCloud
export AZURE_ENVIRONMENT=AzureStackCloud

在 Windows 环境下,这些变量应设置为系统环境变量,可使用管理员权限的 PowerShell 控制台完成,例如:

[System.Environment]::SetEnvironmentVariable("ARM_CLIENT_ID",$your_app_id,[System.EnvironmentTarget]::Machine)
[System.Environment]::SetEnvironmentVariable("ARM_CLIENT_SECRET",$your_password,[System.EnvironmentTarget]::Machine)
[System.Environment]::SetEnvironmentVariable("ARM_SUBSCRIPTION_ID",$your_subscription_id,[System.EnvironmentTarget]::Machine)
[System.Environment]::SetEnvironmentVariable("ARM_TENANT_ID",$your_tenant_id,[System.EnvironmentTarget]::Machine)
[System.Environment]::SetEnvironmentVariable("AZURE_ENVIRONMENT",$your_azure_env,[System.EnvironmentTarget]::Machine)

从源码看,AZURE_ENVIRONMENT 由 modules/azure/client_factory.go 中的 AzureEnvironmentEnvName 常量读取,getDefaultEnvironmentName() 在未设置该变量时默认返回 AzurePublicCloud;环境名会进一步影响 Azure SDK 客户端的云端点选择(例如 GetStorageURISuffixContextE 会根据环境返回不同的存储服务后缀)。

此外,Terratest 的 Azure 模块还支持两个可选环境变量(见 modules/azure/common.go):

  • ARM_SUBSCRIPTION_ID:测试调用 Azure SDK 时未显式传入订阅 ID 时,从此环境变量读取;
  • AZURE_RES_GROUP_NAME:同理,用于指定目标资源组名称。

这使得测试代码可以省略订阅 ID / 资源组参数,直接从环境变量解析目标。

运行自动化测试:从命令到断言

执行测试的命令

原文档给出的自动化测试运行步骤如下:

  1. 注册 Azure 账号并配置凭据(同上);
  2. 安装 Terraform 并加入 PATH;
  3. 配置 Terratest 的 Go 测试环境(参见 examples/azure/README.md,需安装 Go 并保证仓库已正确检出);
  4. 进入测试目录:cd test/azure;
  5. 编译验证:go build terraform_azure_aci_example_test.go;
  6. 运行测试:go test -v -timeout 60m -tags azure -run TestTerraformAzureACIExample。

其中两个关键参数含义如下:

  • -tags azure:测试文件首行为 //go:build azure(见 terraform_azure_aci_example_test.go),只有带上该构建标签才会把测试编译进二进制,避免默认 go test ./... 误触发需要云凭据的用例;
  • -timeout 60m:因测试需要真实的 terraform init、terraform apply 与资源销毁,整个过程可能长达数分钟,需设置充裕的超时时间。

测试代码逐段解读

完整的测试实现位于 test/azure/terraform_azure_aci_example_test.go,核心流程如下:

第一步:构造 Terraform 选项

uniquePostfix := strings.ToLower(random.UniqueID())

terraformOptions := &terraform.Options{
    TerraformDir: "../../examples/azure/terraform-azure-aci-example",
    Vars: map[string]interface{}{
        "postfix": uniquePostfix,
    },
}
  • 使用 random.UniqueID()(来自 modules/core/v2/random)生成随机后缀并转为小写,作为 postfix 变量注入,保证每次测试执行的资源名互不冲突;
  • TerraformDir 指向示例模块目录。

第二步:延迟销毁

defer terraform.DestroyContext(t, t.Context(), terraformOptions)

defer 保证无论测试成功与否,函数返回前都会执行 terraform destroy 清理本次创建的所有资源。这也是 modules/terraform/apply.go 中 InitAndApplyContext 的设计约定——该方法"不会调用 destroy",清理责任由调用方承担,因此测试必须显式 defer 销毁。

第三步:初始化并应用

terraform.InitAndApplyContext(t, t.Context(), terraformOptions)

InitAndApplyContext 内部依次执行 terraform init 与 terraform apply -input=false -auto-approve(见 modules/terraform/apply.go 与 ApplyContextE 中的参数构造 modules/terraform/apply.go),任一步出错都会通过 require.NoError 直接让测试失败。

第四步:读取输出值

resourceGroupName := terraform.OutputContext(t, t.Context(), terraformOptions, "resource_group_name")
aciName := terraform.OutputContext(t, t.Context(), terraformOptions, "container_instance_name")
ipAddress := terraform.OutputContext(t, t.Context(), terraformOptions, "ip_address")
fqdn := terraform.OutputContext(t, t.Context(), terraformOptions, "fqdn")

通过 terraform output 读取 output.tf 中声明的四个输出值。

第五步:断言真实资源状态

assert.True(t, azure.ContainerInstanceExistsContext(t, t.Context(), aciName, resourceGroupName, ""))

actualInstance := azure.GetContainerInstanceContext(t, t.Context(), aciName, resourceGroupName, "")

assert.Equal(t, ipAddress, *actualInstance.Properties.IPAddress.IP)
assert.Equal(t, fqdn, *actualInstance.Properties.IPAddress.Fqdn)
  • ContainerInstanceExistsContext 确认容器实例确实存在于 Azure 上;
  • GetContainerInstanceContext 获取容器组的完整对象,从中取出 Properties.IPAddress.IP 与 Properties.IPAddress.Fqdn,与 Terraform 输出的 ip_address、fqdn 逐一比对。

底层 Azure SDK 封装原理

上述断言函数并非凭空存在,它们由 modules/azure/containers.go 实现。核心链路如下:

  • ContainerInstanceExistsContext → ContainerInstanceExistsContextE:调用 GetContainerInstanceContextE,若返回 ResourceNotFoundErrorExists(err) 则判定为"不存在"(返回 false, nil),其他错误则原样返回(modules/azure/containers.go);
  • GetContainerInstanceContext → GetContainerInstanceContextE:先通过 getTargetAzureResourceGroupName 解析资源组名(为空时读取 AZURE_RES_GROUP_NAME 环境变量),再调用 CreateContainerInstanceClientContextE 创建 SDK 客户端,最终调用 client.Get(ctx, rgName, instanceName, nil) 向 Azure ARM API 发起查询(modules/azure/containers.go);
  • SDK 客户端的创建位于 modules/azure/client_factory.go:getArmContainerInstanceClientFactory 使用 armcontainerinstance.NewClientFactory 构建,工厂内部基于 AZURE_ENVIRONMENT 配置云端点、基于订阅 ID 与默认凭据(azidentity.NewDefaultAzureCredential)完成认证。

也就是说,测试对资源的核实不依赖 Terraform 自身的状态文件,而是通过 Azure SDK 直接查询云上资源的真实属性后再做比对——这正是 examples/azure/README.md 所描述的 Terratest Azure 测试模式:"使用 azure-sdk-for-go 独立确认实际 Azure 资源属性与 Terraform 输出变量给出的期望状态一致"。此外,modules/azure/containers_test.go 中的 TestGetContainerInstanceWithClient 通过 azure-sdk-for-go 的 fake transport 验证了该查询逻辑的成功与 NotFound 两种分支,可在无真实云环境下对封装函数做单元验证。

成本提示与注意事项

原文档特别强调:该模块及其自动化测试会在你的 Azure 账户中部署真实资源,可能产生费用。不过本次示例所用资源(资源组 + 单容器组,规格 0.5 核 / 1.5 GiB)均属于 Azure 免费账户 覆盖范围,若免费额度尚未用完,一般可免费运行;但用户需对全部 Azure 费用自行负责。因此建议:

  • 测试前确认订阅与预算,避免在非预期订阅中运行;
  • 依赖测试中的 defer terraform.DestroyContext 自动清理,手动部署则务必在结束后执行 terraform destroy;
  • 若已用完免费额度,可先估算 ACI 按秒计费的成本,再决定是否运行。

延伸阅读

总结

通过本文,你可以完整复现 Terratest 对 Azure Container Instance 的自动化测试闭环:Terraform 模块负责可复现的部署(main.tf 定义资源、variables.tf 通过 postfix 规避命名冲突、output.tf 暴露断言目标),而 terraform_azure_aci_example_test.go 则借助 modules/azure 的 SDK 封装,将"Terraform 声称的状态"与"Azure 上的真实状态"进行独立比对。这套模式可以直接迁移到 AKS、Cosmos DB、Key Vault 等仓库中其他 Azure 示例模块,是验证基础设施代码正确性的通用范式。

登录后查看全文
terratest