TL;DR
- Terratest тестирует реальную инфраструктуру через облачные API—не состояние Terraform—обнаруживая баги, которые нативное тестирование пропускает
- Паттерн
defer terraform.Destroyгарантирует очистку даже при падении тестов, предотвращая появление orphan-ресурсов- Test stages позволяют пропускать медленные шаги при локальной разработке (пропустить сборку AMI, переиспользовать задеплоенную инфру)
Подходит для: Команд, которым нужно валидировать реальное поведение в облаке, а не только корректность конфигурации Пропустите если: Вам нужно только валидировать синтаксис/логику Terraform (используйте нативный
terraform test) Время чтения: 12 минут
Вот жёсткая правда о тестировании инфраструктуры: нативный тестовый фреймворк Terraform валидирует состояние, а не реальность. Если баг провайдера создаёт неправильно сконфигурированный ресурс, но сообщает об успехе, ваши тесты проходят, пока ваша инфраструктура ломается.
Terratest решает это, запрашивая реальные облачные ресурсы через их нативные API. Ваш S3 бакет не просто “создан” в состоянии Terraform—Terratest проверяет, что он действительно существует, имеет правильное шифрование и корректно отдаёт контент.
Пререквизиты
Перед началом убедитесь, что у вас есть:
- Go 1.21+ установлен (
go version) - Terraform 1.0+ установлен (
terraform version) - AWS CLI настроен с credentials (
aws sts get-caller-identity) - Базовые знания Go (функции, структуры, обработка ошибок)
- Выделенный AWS аккаунт или sandbox для тестирования
Шаг 1: Настройка Проекта
Создайте структуру директорий для вашего Terraform модуля и тестов:
mkdir -p terraform-s3-module/test
cd terraform-s3-module
Инициализируйте Go модуль:
cd test
go mod init github.com/yourorg/terraform-s3-module/test
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/gruntwork-io/terratest/modules/aws
go get github.com/stretchr/testify/assert
Ваш go.mod теперь должен ссылаться на Terratest v0.53.0 или новее.
Шаг 2: Создание Terraform Модуля для Тестирования
Создайте main.tf в корневой директории:
variable "bucket_name" {
description = "Name of the S3 bucket"
type = string
}
variable "environment" {
description = "Environment tag"
type = string
default = "test"
}
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
output "bucket_id" {
value = aws_s3_bucket.this.id
}
output "bucket_arn" {
value = aws_s3_bucket.this.arn
}
Шаг 3: Напишите Ваш Первый Тест на Terratest
Создайте test/s3_bucket_test.go:
package test
import (
"fmt"
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestS3BucketCreation(t *testing.T) {
t.Parallel()
// Генерируем уникальное имя бакета для избежания конфликтов
uniqueID := random.UniqueId()
bucketName := fmt.Sprintf("terratest-example-%s", uniqueID)
awsRegion := "us-west-2"
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"bucket_name": bucketName,
"environment": "test",
},
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": awsRegion,
},
})
// КРИТИЧНО: Всегда очищайте ресурсы
defer terraform.Destroy(t, terraformOptions)
// Деплоим инфраструктуру
terraform.InitAndApply(t, terraformOptions)
// Получаем outputs
bucketID := terraform.Output(t, terraformOptions, "bucket_id")
// Валидируем РЕАЛЬНУЮ инфраструктуру через AWS API
aws.AssertS3BucketExists(t, awsRegion, bucketID)
// Проверяем, что версионирование действительно включено (не только в состоянии)
versioning := aws.GetS3BucketVersioning(t, awsRegion, bucketID)
assert.Equal(t, "Enabled", versioning)
// Проверяем конфигурацию шифрования
encryption := aws.GetS3BucketEncryption(t, awsRegion, bucketID)
assert.Equal(t, "AES256", encryption)
}
Ключевые паттерны в этом тесте:
t.Parallel()— Запускает тесты конкурентно для большей скоростиrandom.UniqueId()— Предотвращает коллизии имён ресурсовdefer terraform.Destroy()— Гарантирует очистку даже при паденииaws.AssertS3BucketExists()— Валидирует реальные AWS ресурсы, не состояние
Шаг 4: Запустите Тест
Выполните из директории test:
go test -v -timeout 30m
Ожидаемый вывод:
=== RUN TestS3BucketCreation
=== PAUSE TestS3BucketCreation
=== CONT TestS3BucketCreation
TestS3BucketCreation 2026-01-11T10:15:30Z command.go:158: Running command terraform with args [init]
TestS3BucketCreation 2026-01-11T10:15:35Z command.go:158: Running command terraform with args [apply -auto-approve]
...
TestS3BucketCreation 2026-01-11T10:16:45Z command.go:158: Running command terraform with args [destroy -auto-approve]
--- PASS: TestS3BucketCreation (95.23s)
PASS
Шаг 5: Test Stages для Быстрой Итерации
Реальные тесты медленные. Сборка AMI, деплой кластеров и запуск валидаций может занять более 30 минут. Test stages позволяют пропускать завершённые этапы при разработке:
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
)
func TestWithStages(t *testing.T) {
t.Parallel()
workingDir := test_structure.CopyTerraformFolderToTemp(t, "../", ".")
// Stage: Deploy
defer test_structure.RunTestStage(t, "teardown", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, workingDir)
terraform.Destroy(t, terraformOptions)
})
test_structure.RunTestStage(t, "deploy", func() {
uniqueID := random.UniqueId()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: workingDir,
Vars: map[string]interface{}{
"bucket_name": fmt.Sprintf("terratest-%s", uniqueID),
},
})
test_structure.SaveTerraformOptions(t, workingDir, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
})
// Stage: Validate
test_structure.RunTestStage(t, "validate", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, workingDir)
bucketID := terraform.Output(t, terraformOptions, "bucket_id")
aws.AssertS3BucketExists(t, "us-west-2", bucketID)
})
}
Пропускайте stages при разработке:
# Пропустить deploy, запустить только валидацию (предполагает, что инфра существует)
SKIP_deploy=true go test -v -run TestWithStages
# Пропустить teardown для сохранения ресурсов для отладки
SKIP_teardown=true go test -v -run TestWithStages
Подходы с Использованием ИИ
В 2026 году ИИ значительно ускоряет разработку с Terratest.
Что ИИ делает хорошо:
- Генерация boilerplate структуры тестов из Terraform модулей
- Написание assertions на основе конфигураций ресурсов
- Предложение edge cases (что если имя бакета содержит спецсимволы?)
- Конвертация HCL outputs в Go struct mappings
Что всё ещё требует людей:
- Решение, какие поведения действительно важно тестировать
- Понимание радиуса поражения, если тесты упадут в production аккаунтах
- Проектирование стратегий изоляции тестов
- Ревью assertions, сгенерированных ИИ, на ложные срабатывания
Полезный промпт:
Дан этот Terraform модуль, создающий RDS инстанс:
resource "aws_db_instance" "main" {
identifier = var.db_identifier
engine = "postgres"
engine_version = "15.4"
instance_class = var.instance_class
allocated_storage = 20
storage_encrypted = true
deletion_protection = var.enable_deletion_protection
}
Сгенерируй Terratest, который:
1. Деплоит RDS инстанс с тестовыми переменными
2. Валидирует, что шифрование действительно включено через AWS API
3. Проверяет, что запущена правильная версия Postgres
4. Тестирует, что deletion protection настроена корректно
5. Использует правильную очистку с defer
Включи обработку ошибок и осмысленные сообщения assertions.
Фреймворк Принятия Решений
Когда Использовать Terratest
Этот подход работает лучше всего когда:
- Вам нужно валидировать реальное поведение облачных ресурсов
- Тестируете взаимодействия между несколькими сервисами (VPC + EC2 + RDS)
- У вашей команды есть опыт Go или готовность учиться
- Интеграционные тесты критичны для вашего deployment pipeline
- Вы тестируете Kubernetes, Docker или Packer вместе с Terraform
Рассмотрите альтернативы когда:
- Вам нужно только валидировать логику Terraform (используйте
terraform test) - Быстрая обратная связь критична, а кривая обучения Go слишком крутая
- Ваша инфраструктура простая (<5 ресурсов)
- Ограничения бюджета не позволяют деплоить эфемерные ресурсы
Terratest vs Нативный Terraform Test
| Аспект | Terratest | Terraform Test |
|---|---|---|
| Что тестирует | Реальную инфраструктуру через API | Состояние Terraform |
| Язык | Go | HCL |
| Кривая обучения | Выше | Ниже |
| Ловит баги провайдеров | Да | Нет |
| Поддержка нескольких инструментов | Terraform, K8s, Docker, Packer | Только Terraform/OpenTofu |
| Скорость | Медленнее (реальные деплои) | Быстрее (на основе plan) |
Моя рекомендация: Используйте оба. Нативные тесты Terraform для быстрого unit-тестирования логики модулей. Terratest для интеграционных тестов, которые валидируют реальную инфраструктуру перед деплоем в production.
Измерение Успеха
| Метрика | Базовая Линия | Цель | Как Измерять |
|---|---|---|---|
| Покрытие тестами | 0% | 80%+ критичных модулей | Подсчёт протестированных vs всего модулей |
| Время выполнения | N/A | < 30 мин для полного набора | Метрики CI pipeline |
| Orphan-ресурсы | Неизвестно | 0 | Теги в AWS Cost Explorer |
| Процент нестабильных тестов | Высокий | < 5% | Анализ сбоев CI |
| Среднее время обнаружения | Дни | Часы | Трекинг инцидентов |
Сигналы тревоги:
- Тесты проходят, но деплои в production падают — тесты недостаточно полные
- Накопление orphan-ресурсов — очистка не работает правильно
- Тесты занимают > 1 часа — нужно распараллеливать или использовать test stages
- Высокая нестабильность — не хватает логики retry или проблемы с таймингом
Распространённые Ошибки
1. Отсутствие defer Destroy
// ПЛОХО: Если Apply падает, ресурсы остаются orphan
terraform.InitAndApply(t, options)
terraform.Destroy(t, options) // Никогда не выполнится, если Apply упадёт
// ХОРОШО: Очистка выполняется независимо от результата теста
defer terraform.Destroy(t, options)
terraform.InitAndApply(t, options)
2. Коллизии Имён Ресурсов
// ПЛОХО: Будет конфликтовать с другими тестами или запусками
bucketName := "my-test-bucket"
// ХОРОШО: Уникально для каждого запуска теста
uniqueID := random.UniqueId()
bucketName := fmt.Sprintf("test-%s", uniqueID)
3. Захардкоженные Регионы
// ПЛОХО: Падает, если регион по умолчанию отличается
aws.AssertS3BucketExists(t, "us-east-1", bucketID)
// ХОРОШО: Согласовано с деплоем Terraform
awsRegion := terraform.Output(t, options, "region")
aws.AssertS3BucketExists(t, awsRegion, bucketID)
4. Недостаточные Таймауты
# ПЛОХО: Таймаут по умолчанию 10м убивает долгие тесты
go test -v
# ХОРОШО: Даём время для реальной инфраструктуры
go test -v -timeout 30m
Что Дальше
Начните с одного критичного модуля—вашей VPC, основной базы данных или главного compute кластера. Напишите один тест, который валидирует, что он действительно работает, а не просто деплоится. Запускайте его в CI перед каждым merge.
Когда освоитесь, расширьтесь до тестирования взаимодействий: может ли ваше приложение действительно подключиться к той базе данных? Правильно ли load balancer маршрутизирует трафик?
Цель не 100% покрытие—это уверенность, что ваша инфраструктура работает в реальности, а не только в представлении Terraform о реальности.
Связанные статьи:
- Лучшие Практики Тестирования Pulumi
- Стратегии Тестирования и Валидации Terraform
- Стратегия Пирамиды Автоматизации Тестов
- Тестирование Шаблонов CloudFormation
Внешние ресурсы: