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)
}

Ключевые паттерны в этом тесте:

  1. t.Parallel() — Запускает тесты конкурентно для большей скорости
  2. random.UniqueId() — Предотвращает коллизии имён ресурсов
  3. defer terraform.Destroy() — Гарантирует очистку даже при падении
  4. 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

АспектTerratestTerraform Test
Что тестируетРеальную инфраструктуру через APIСостояние Terraform
ЯзыкGoHCL
Кривая обученияВышеНиже
Ловит баги провайдеровДаНет
Поддержка нескольких инструментов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 о реальности.


Связанные статьи:

Внешние ресурсы: