- 测试
- 开发工具
- DevOps
- 质量保障
【免费下载链接】terratest
Terratest is a Go library that makes it easier to write automated tests for your infrastructure code.
导读
本文以 Terratest 仓库中的 terraform-aws-s3-example 示例模块为核心,完整讲解如何在 AWS 上部署一个启用版本控制(Versioning)与访问日志(Server Access Logging)的 S3 Bucket 对,并通过 Terratest 的 Go 测试代码对已部署资源做自动断言验证。读完本文,你将掌握 Terratest 测试 Terraform 模块的标准流程:从terraform init/apply的封装调用、随机区域与唯一命名策略,到使用 S3 专属断言函数校验版本控制、Bucket Policy 与日志目标的完整写法,并了解每一处调用背后的源码实现。
一、示例模块要解决什么问题
这个示例演示了 Terratest 最典型的使用场景:编写自动化测试来验证你的 AWS Terraform 基础设施代码。该模块会创建两个 S3 Bucket:
test_bucket(源桶):启用版本控制(Enabled)和服务器访问日志,桶内不放置任何实际对象;test_bucket_logs(日志桶):作为源桶访问日志的落盘位置,接收前缀为TFStateLogs/的日志对象。
两个 Bucket 都会被赋予Name与Environment标签,取值分别来自tag_bucket_name与tag_bucket_environment变量。模块还提供开关with_policy,开启后会为源桶附加一条 Bucket Policy,将非 SSL(aws:SecureTransport为false)的访问请求全部拒绝,强制客户端走加密通道。
配套的测试文件 test/terraform_aws_s3_example_test.go 展示了如何对上述全部能力逐项做自动化验证。需要注意的是,示例模块刻意保持"极简":Bucket 创建后只有版本控制、日志与标签配置,不含任何对象;如果你想看更接近真实生产形态的多服务组合示例,可以继续阅读 terraform-http-example 与 terraform-ssh-example。
二、模块的 Terraform 源码解析
模块由三个 TF 文件构成,位于 examples/terraform-aws-s3-example 目录下:main.tf、variables.tf 和 outputs.tf。
2.1 Provider 与默认标签
main.tf 开头声明了运行前提:
terraform { required_version = ">= 1.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = var.region default_tags { tags = { "gw:repo" = "https://github.com/gruntwork-io/terratest" "gw:example" = "terraform-aws-s3-example" } } }这里明确要求 Terraform 版本不低于 1.0,AWS Provider 使用~> 5.0系列;default_tags会把gw:repo、gw:example作为账号级默认标签附加到所有资源上,便于在账单或控制台溯源这些资源属于哪个示例。
2.2 源桶:版本控制 + 访问日志 + 私有 ACL
源桶相关的资源定义在 main.tf:
resource "aws_s3_bucket" "test_bucket" { bucket = "${local.aws_account_id}-${var.tag_bucket_name}" tags = { Name = var.tag_bucket_name Environment = var.tag_bucket_environment } } resource "aws_s3_bucket_logging" "test_bucket" { bucket = aws_s3_bucket.test_bucket.id target_bucket = aws_s3_bucket.test_bucket_logs.id target_prefix = "TFStateLogs/" } resource "aws_s3_bucket_versioning" "test_bucket" { bucket = aws_s3_bucket.test_bucket.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_ownership_controls" "test_bucket" { bucket = aws_s3_bucket.test_bucket.id rule { object_ownership = "ObjectWriter" } depends_on = [aws_s3_bucket.test_bucket] } resource "aws_s3_bucket_acl" "test_bucket" { bucket = aws_s3_bucket.test_bucket.id acl = "private" depends_on = [aws_s3_bucket_ownership_controls.test_bucket] }几个值得注意的实现细节:
- 桶名带账号 ID 前缀:
local.aws_account_id来自 main.tf 中的aws_caller_identity数据源,桶名形如<account-id>-<tag_bucket_name>,既保证全局唯一,又方便识别归属账号。 - 新版 AWS Provider 的资源拆分:版本控制(
aws_s3_bucket_versioning)、日志(aws_s3_bucket_logging)、所有权控制(aws_s3_bucket_ownership_controls)与 ACL(aws_s3_bucket_acl)均以独立子资源形式声明,这是 AWS Provider v5 的推荐写法。 - ACL 的前置条件:先通过
object_ownership = "ObjectWriter"声明所有权规则,再设置acl = "private",并且用depends_on显式保证顺序。
2.3 日志桶:可被强制销毁的日志接收端
日志桶定义在 main.tf:
resource "aws_s3_bucket" "test_bucket_logs" { bucket = "${local.aws_account_id}-${var.tag_bucket_name}-logs" tags = { Name = "${local.aws_account_id}-${var.tag_bucket_name}-logs" Environment = var.tag_bucket_environment } force_destroy = true } resource "aws_s3_bucket_ownership_controls" "test_bucket_logs" { bucket = aws_s3_bucket.test_bucket_logs.id rule { object_ownership = "ObjectWriter" } depends_on = [aws_s3_bucket.test_bucket_logs] } resource "aws_s3_bucket_acl" "test_bucket_logs" { bucket = aws_s3_bucket.test_bucket_logs.id acl = "log-delivery-write" depends_on = [aws_s3_bucket_ownership_controls.test_bucket_logs] }- 桶名在源桶基础上追加
-logs后缀; force_destroy = true允许terraform destroy时连同桶内对象一并删除,这是测试场景的标准配置,避免清理阶段因桶非空而失败;- ACL 使用
log-delivery-write,这是 AWS 预置的日志投递权限,允许 S3 日志服务向该桶写入对象。
2.4 可选 Bucket Policy:强制 SSL
模块通过with_policy变量控制是否附加策略,策略主体用aws_iam_policy_document数据源拼装,见 main.tf:
resource "aws_s3_bucket_policy" "bucket_access_policy" { count = var.with_policy ? 1 : 0 bucket = aws_s3_bucket.test_bucket.id policy = data.aws_iam_policy_document.s3_bucket_policy.json } data "aws_iam_policy_document" "s3_bucket_policy" { statement { effect = "Allow" principals { identifiers = [local.aws_account_id] type = "AWS" } actions = ["*"] resources = ["${aws_s3_bucket.test_bucket.arn}/*"] } statement { effect = "Deny" principals { identifiers = ["*"] type = "AWS" } actions = ["*"] resources = ["${aws_s3_bucket.test_bucket.arn}/*"] condition { test = "Bool" variable = "aws:SecureTransport" values = [ "false", ] } } }策略由两条 statement 组成:
- Allow 本账号:仅对当前 AWS 账号(
local.aws_account_id)授予桶内对象(arn/*)的全部操作权限; - Deny 非 SSL 请求:对任意主体,只要
aws:SecureTransport为false(即 HTTP 明文请求),一律拒绝。
配合count = var.with_policy ? 1 : 0,该策略默认不创建,测试中通过变量显式开启后再验证"策略确实存在"。
2.5 变量与输出
variables.tf 定义了一个必填变量与三个可选变量:
| 变量 | 类型 | 必填/默认值 | 说明 |
|---|---|---|---|
region | string | 必填 | 部署的 AWS 区域 |
with_policy | bool | 默认false | 为true时为源桶创建强制 SSL 的 Bucket Policy |
tag_bucket_name | string | 默认"Test Bucket" | 桶的Name标签值(也会参与桶名拼装) |
tag_bucket_environment | string | 默认"Test" | 桶的Environment标签值 |
outputs.tf 对外暴露四个输出:bucket_id、bucket_arn、logging_target_bucket、logging_target_prefix。其中后两个正是测试代码用来做日志断言的数据来源。
三、手动运行模块(基线操作)
在动手写测试之前,先按 README.md 的步骤手动跑一遍模块,确认基础设施本身可部署:
- 注册 AWS 账号;
- 配置 AWS 凭证:使用 AWS CLI 支持的任一方式,例如设置环境变量
AWS_ACCESS_KEY_ID与AWS_SECRET_ACCESS_KEY;如果你通过~/.aws/config配置文件使用 profile,还需要导出AWS_SDK_LOAD_CONFIG为"True"; - 设置区域:导出环境变量
AWS_DEFAULT_REGION为你想部署的区域; - 安装 Terraform并确保其位于
PATH中; - 在 examples/terraform-aws-s3-example 目录下执行
terraform init; - 执行
terraform apply(如需开启策略,可附加-var with_policy=true); - 完成后执行
terraform destroy清理资源。
注意:此模块会真实创建两个 S3 Bucket,属于 AWS 免费套餐范围内的资源;但所有 AWS 费用完全由使用者承担,测试完成后务必 destroy,避免产生持续成本。
四、用 Terratest 编写自动化测试
4.1 测试代码整体结构
完整的测试位于 test/terraform_aws_s3_example_test.go。文件第一行带有//go:build aws构建标签,意味着它只会在显式指定aws构建约束时参与编译(配合go test -tags aws使用),把需要真实云账号的测试与纯单元测试区分开。
测试用到了三个 Terratest 子模块:
import ( "github.com/gruntwork-io/terratest/modules/aws/v2" "github.com/gruntwork-io/terratest/modules/core/v2/random" "github.com/gruntwork-io/terratest/modules/terraform/v2" )对应仓库中的 modules/aws、modules/core/random 与 modules/terraform 目录,以及断言库github.com/stretchr/testify/assert。
4.2 命名唯一化与随机区域
func TestTerraformAwsS3Example(t *testing.T) { t.Parallel() expectedName := "terratest-aws-s3-example-" + strings.ToLower(random.UniqueID()) expectedEnvironment := "Automated Testing" awsRegion := aws.GetRandomStableRegionContext(t, t.Context(), nil, nil) ... }random.UniqueID()来自 modules/core/random,生成一段随机后缀,使每个测试运行时的桶名都不同,避免与账号内其他测试相互干扰;strings.ToLower保证桶名符合 S3 的命名约束;aws.GetRandomStableRegionContext定义在 modules/aws/region.go,会从可用区域中随机挑选一个稳定区域,入参为批准的/禁用的区域列表(此处均为nil,表示不限制),"随机换区域测试"是 Terratest 的推荐做法,有助于发现代码在特定区域的兼容性问题。
4.3 组装 Terraform 选项并注入变量
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ TerraformDir: "../examples/terraform-aws-s3-example", Vars: map[string]interface{}{ "tag_bucket_name": expectedName, "tag_bucket_environment": expectedEnvironment, "with_policy": "true", "region": awsRegion, }, })TerraformDir指向模块目录;Vars会以-var参数传入,这里把随机名、随机区域和with_policy=true一并注入;terraform.WithDefaultRetryableErrors实现在 modules/terraform/options.go,它克隆原 Options,并填充DefaultRetryableTerraformErrors这类常见的可重试错误映射,同时把MaxRetries设为 3、TimeBetweenRetries设为默认间隔——也就是说,apply/plan 过程中遇到"重试即可自愈"的瞬时错误时,Terratest 会自动重试,避免测试被偶发的 AWS 限流或传播延迟击垮。
4.4 init + apply + 延迟 destroy
defer terraform.DestroyContext(t, t.Context(), terraformOptions) terraform.InitAndApplyContext(t, t.Context(), terraformOptions)terraform.InitAndApplyContext定义在 modules/terraform/apply.go,它依次执行InitContextE与ApplyContextE;后者会以apply -input=false -auto-approve的形式运行(见 apply.go),全程免交互、自动批准;defer terraform.DestroyContext(...)保证无论后续断言成功还是失败,测试结束时都会执行terraform destroy,从结构上避免资源泄漏。这是 Terratest 测试中"资源回收"的标准范式。
4.5 逐项断言:版本控制、策略与日志
bucketID := terraform.OutputContext(t, t.Context(), terraformOptions, "bucket_id") actualStatus := aws.GetS3BucketVersioningContext(t, t.Context(), awsRegion, bucketID) expectedStatus := "Enabled" assert.Equal(t, expectedStatus, actualStatus) aws.AssertS3BucketPolicyExistsContext(t, t.Context(), awsRegion, bucketID) loggingTargetBucket := aws.GetS3BucketLoggingTargetContext(t, t.Context(), awsRegion, bucketID) expectedLogsTargetBucket := bucketID + "-logs" loggingObjectTargetPrefix := aws.GetS3BucketLoggingTargetPrefixContext(t, t.Context(), awsRegion, bucketID) expectedLogsTargetPrefix := "TFStateLogs/" assert.Equal(t, expectedLogsTargetBucket, loggingTargetBucket) assert.Equal(t, expectedLogsTargetPrefix, loggingObjectTargetPrefix)这条测试链路可以拆成三个"部署即验证"步骤:
- 读取输出:
terraform.OutputContext从 outputs.tf 中取回bucket_id,作为后续所有 AWS 断言的入参; - 版本控制断言:
aws.GetS3BucketVersioningContext实现在 modules/aws/s3.go,底层调用 AWS SDK 的GetBucketVersioning并把返回的Status转成字符串,测试断言其等于"Enabled"; - 策略断言:
aws.AssertS3BucketPolicyExistsContext实现在 modules/aws/s3.go,内部先调用GetS3BucketPolicyContextE读取资源策略,若策略为空则返回NoBucketPolicyError,从而验证"开启with_policy后桶上确实挂载了策略"; - 日志断言:
aws.GetS3BucketLoggingTargetContext与aws.GetS3BucketLoggingTargetPrefixContext分别实现在 modules/aws/s3.go 与 modules/aws/s3.go,底层调用GetBucketLogging,读取LoggingEnabled.TargetBucket与LoggingEnabled.TargetPrefix;测试期望日志目标桶为<bucket_id>-logs、前缀为TFStateLogs/,与 main.tf 的配置一一对应。
值得一提的错误处理细节:在 modules/aws/s3.go 的GetS3BucketLoggingTargetContextE中,若res.LoggingEnabled == nil(即桶未开启日志),会返回S3AccessLoggingNotEnabledErr类型错误,让失败原因可被显式识别。整套函数遵循xxxContext(失败即中断测试)与xxxContextE(返回 error 由调用方处理)成对出现的命名约定。
五、运行自动化测试
原文档 README.md 给出的测试运行步骤为:
- 注册 AWS 账号并配置凭证(同手动运行部分,可设置
AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY环境变量,profile 场景下导出AWS_SDK_LOAD_CONFIG为"True"); - 安装 Terraform 并加入
PATH; - 安装 Golang,并将代码检出到
GOPATH; cd test;- 执行依赖安装;
- 运行
go test -v -run TestTerraformAwsS3Example。
需要说明的是,原文档第 5 步中的dep ensure是早期基于dep工具链的写法。当前仓库已全面采用 Go Modules 管理依赖(根目录的 go.mod 与 go.work 可见),因此实际操作时可直接在仓库根目录执行go test -tags aws -v -run TestTerraformAwsS3Example ./test/(该测试文件带有//go:build aws标签,需带-tags aws才会被编译执行),由 Go 工具链自动解析 go.work 中列出的各个模块。测试执行期间会真实调用 AWS API 创建、断言并销毁资源,因此必须确保凭证有效且账号具备 S3 相关权限。
六、成本与清理提醒
模块与配套测试都会在真实 AWS 账号中部署资源,这会产生真实的云费用。示例用到的 S3 资源属于 AWS 免费套餐范围,若免费额度尚未用完通常不会产生账单,但所有费用由使用者自行负责。测试代码通过defer terraform.DestroyContext(...)保证退出前清理;手动操作时则务必记得执行terraform destroy,特别是日志桶设置了force_destroy = true,可保证桶内对象被一并清空,从而顺利删除。
七、从示例到生产测试的延伸
这个示例是 Terratest 中"Terraform 模块测试"的最小完整闭环,背后可延伸的要点包括:
- 测试分区:真实云资源测试(带
//go:build aws标签)与不依赖云环境的单元测试(如 modules/terraform/options_test.go)在仓库中分开放置,CI 中可按需选择构建约束; - 断言扩展:modules/aws/s3.go 中还有
AssertS3BucketServerSideEncryptionContextE(校验服务端加密算法,aws:kms与aws:kms:dsse精确匹配)、EmptyS3BucketContext(清空桶内容)等更多 S3 能力,可在此基础上补充加密、生命周期等断言; - 重试与超时:
WithDefaultRetryableErrors只是起点,modules/terraform/options.go 中还支持通过RetryableTerraformErrors、MaxRetries、TimeBetweenRetries自定义重试策略,以及通过Context传递超时与取消控制; - 更多参考:如需测试包含其他 AWS 服务的组合模块,可对照 terraform-http-example 与 terraform-ssh-example 两个示例继续阅读,它们展示了更接近真实项目的多资源编排与对应测试写法。
总之,terraform-aws-s3-example把"Terraform 模块 + Terratest 测试"的完整链路压缩在一个小而完整的样例中:命名唯一化、随机区域、带重试的 apply、延迟 destroy、基于真实 AWS SDK 的断言,全部有对应的仓库源码可供逐行研读,是上手 Terratest 编写 AWS 基础设施测试的理想起点。
- 测试
- 开发工具
- DevOps
- 质量保障
【免费下载链接】terratest
Terratest is a Go library that makes it easier to write automated tests for your infrastructure code.
相关推荐
用 Terratest 为 AWS ECS(Fargate)Terraform 模块编写自动化测试:terraform-aws-ecs-example 实战指南
用 Terratest 为 AWS ECS(Fargate)Terraform 模块编写自动化测试:terraform aws ecs example 实战指南
测试开发工具DevOps质量保障用 Terratest 为 GCP Terraform 模块编写自动化测试:terraform-gcp-hello-world-example 实战解析
用 Terratest 为 GCP Terraform 模块编写自动化测试:terraform gcp hello world example 实战解析 本篇指
测试开发工具DevOps质量保障escrcpy 远程设备隧道连接完全指南:远程 ADB 服务器与 SSH 隧道实战
escrcpy 远程设备隧道连接完全指南:远程 ADB 服务器与 SSH 隧道实战 本指南基于 escrcpy 仓库中的 tunnels.md https://
测试开发工具DevOps质量保障
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考