Terratest 实战:为 Azure Container Instances(ACI)Terraform 模块编写自动化测试
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 对基础设施代码进行自动化验证的核心思想。
手动运行该模块的完整步骤
原文档给出了直接手动操作该模块的清单式步骤,本文将其完整保留并补充关键细节:
- 注册 Azure 账号:在 Azure 官网注册订阅,用于创建资源组与容器实例。
- 配置 Azure 凭据:使用 Azure CLI 官方支持的方式完成认证配置(服务主体、登录会话等任一受支持方式均可)。
- 安装 Terraform:安装 Terraform 并将其加入系统
PATH,确保命令行可直接调用terraform。 - 准备环境变量:确保
ARM_CLIENT_ID、ARM_CLIENT_SECRET、ARM_SUBSCRIPTION_ID、ARM_TENANT_ID等环境变量已在测试主机上就绪,详见下文"环境变量与 Azure 凭据配置"一节。 - 初始化:在模块目录下运行
terraform init,下载hashicorp/azurermprovider。 - 应用部署:运行
terraform apply,创建资源组与容器组。 - 清理资源:操作完成后运行
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 / 资源组参数,直接从环境变量解析目标。
运行自动化测试:从命令到断言
执行测试的命令
原文档给出的自动化测试运行步骤如下:
- 注册 Azure 账号并配置凭据(同上);
- 安装 Terraform 并加入
PATH; - 配置 Terratest 的 Go 测试环境(参见 examples/azure/README.md,需安装 Go 并保证仓库已正确检出);
- 进入测试目录:
cd test/azure; - 编译验证:
go build terraform_azure_aci_example_test.go; - 运行测试:
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 按秒计费的成本,再决定是否运行。
延伸阅读
- 仓库级 Azure 测试环境配置总览:examples/azure/README.md
- 同一测试目录下的其他 Azure 示例测试:test/azure
- Azure 模块的全部 SDK 封装:modules/azure
- Terraform 模块的封装实现:modules/terraform
总结
通过本文,你可以完整复现 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 示例模块,是验证基础设施代码正确性的通用范式。