Валидация Terraform
Updated Jul 2026
Пирамида валидации
Валидация инфраструктуры следует пирамиде, аналогичной традиционной пирамиде тестирования, но с собственными уровнями. Каждый уровень выявляет различные классы проблем при разной стоимости:
/\
/ \ Интеграционные тесты (Terratest, реальные облачные ресурсы)
/ \
/------\ Анализ на этапе plan (terraform plan, обнаружение дрифта)
/ \
/----------\ Статический анализ (validate, tfsec, checkov)
/ \
/--------------\ Синтаксис и форматирование (terraform fmt, terraform validate)
/________________\
Основание пирамиды бесплатно и быстро. Верхушка дорогая и медленная, но выявляет проблемы, которые ничто другое не способно обнаружить. Зрелая стратегия тестирования IaC использует все четыре уровня, запуская дешёвые проверки при каждом коммите, а дорогие — при изменениях критичных модулей.
Статическая валидация: первый рубеж
Каждый Terraform-пайплайн должен начинаться с бесплатных статических проверок. Они выполняются за секунды, не требуют облачных учётных данных и выявляют удивительно большое количество проблем.
Проверки формата и синтаксиса
# Проверка форматирования -- обеспечивает единообразный стиль
# -check возвращает ненулевой код выхода, если файлы нуждаются в форматировании
# -recursive сканирует все поддиректории
# -diff показывает, что будет изменено
terraform fmt -check -recursive -diff
# Проверка синтаксиса и типов -- выявляет опечатки, пропущенные обязательные поля
# -backend=false пропускает инициализацию бэкенда (учётные данные не нужны)
terraform init -backend=false
terraform validate
Различие между fmt и validate принципиально. fmt обеспечивает стиль — единообразные отступы, выравнивание и пробелы. validate проверяет структурную корректность — парсится ли этот HCL? Все ли обязательные аргументы указаны? Совпадают ли ограничения типов?
Что обнаруживает terraform validate
# Пример: terraform validate обнаружит отсутствие обязательного атрибута
resource "aws_s3_bucket" "data" {
# Ой — забыли указать имя бакета
# terraform validate это обнаружит
acl = "private"
}
# Также обнаруживает несоответствие типов:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
count = "three" # ОШИБКА: count должен быть числом, а не строкой
}
# И ссылки на необъявленные ресурсы:
resource "aws_security_group_rule" "allow_http" {
security_group_id = aws_security_group.nonexistent.id # ОШИБКА
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
Интеграция статических проверок в pre-commit хуки
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-tf
rev: v1.88.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
args:
- --hook-config=--path-to-file=README.md
- --hook-config=--add-to-existing-file=true
- id: terraform_tflint
args:
- --args=--config=__GIT_WORKING_DIR__/.tflint.hcl
Pre-commit хуки гарантируют, что ни один разработчик не сможет закоммитить некорректный Terraform. Это ваш самый дешёвый рубеж контроля качества.
Анализ на этапе plan: что реально изменится?
terraform plan — это ваш интеграционный контракт: он показывает разницу между объявленным состоянием и реальным миром. Здесь вы переходите от вопроса «валиден ли код?» к вопросу «что код сделает?»
Генерация и анализ планов
# Генерация файла плана для последующего анализа
terraform plan -out=tfplan -detailed-exitcode
# Коды выхода:
# 0 = нет изменений
# 1 = ошибка
# 2 = есть изменения (это самый важный)
# Конвертация плана в JSON для программного анализа
terraform show -json tfplan > tfplan.json
Программные проверки плана
JSON-вывод плана — это кладезь информации для автоматизированного тестирования. Вы можете писать утверждения против него так же, как и в любом другом тесте:
# scripts/validate_plan.py
import json
import sys
def load_plan(path="tfplan.json"):
with open(path) as f:
return json.load(f)
def check_no_destroys(plan):
"""No resources should be destroyed without explicit approval."""
destroys = [
change["address"]
for change in plan["resource_changes"]
if "delete" in change["change"]["actions"]
]
if destroys:
print(f"BLOCKED: Plan would destroy {len(destroys)} resources:")
for r in destroys:
print(f" - {r}")
return False
return True
def check_no_replacements(plan):
"""Flag resources that will be replaced (destroy + create)."""
replacements = [
change["address"]
for change in plan["resource_changes"]
if change["change"]["actions"] == ["delete", "create"]
or change["change"]["actions"] == ["create", "delete"]
]
if replacements:
print(f"WARNING: Plan will replace {len(replacements)} resources:")
for r in replacements:
print(f" - {r}")
return False
return True
def check_no_public_s3(plan):
"""S3 buckets must never have public ACLs."""
for change in plan["resource_changes"]:
if change["type"] == "aws_s3_bucket":
after = change["change"].get("after", {})
acl = after.get("acl", "private")
if acl != "private":
print(f"BLOCKED: {change['address']} has ACL '{acl}' (must be 'private')")
return False
return True
if __name__ == "__main__":
plan = load_plan()
checks = [
check_no_destroys(plan),
check_no_replacements(plan),
check_no_public_s3(plan),
]
if not all(checks):
sys.exit(1)
print("All plan checks passed.")
Типичные проверки плана, которые стоит реализовать
| Проверка | Почему это важно | Уровень риска |
|---|---|---|
| Нет удаления ресурсов | Предотвращает случайную потерю данных | Критический |
| Нет замены баз данных | Замена RDS = простой + риск потери данных | Критический |
| Нет публичного ingress в группах безопасности | Предотвращает открытый сетевой доступ | Критический |
| Все S3-бакеты зашифрованы | Требование комплаенса | Высокий |
| Нет oversized-инстансов в не-прод окружениях | Контроль затрат | Средний |
| Все ресурсы имеют обязательные теги | Управление и распределение затрат | Средний |
| Нет изменений IAM-политик без ревью | Уровень безопасности | Высокий |
Terratest: интеграционное тестирование с реальной инфраструктурой
Terratest (написан на Go) разворачивает реальную инфраструктуру, выполняет проверки и удаляет её. Это наиболее тщательная валидация, потому что она задействует реальные облачные API, но также и самая дорогостоящая.
Когда использовать Terratest
Используйте Terratest для переиспользуемых модулей, от которых зависят многие команды. Не используйте его для одноразовых конфигураций — накладные расходы не оправданы.
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/stretchr/testify/assert"
)
func TestS3BucketIsEncrypted(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/s3-data-bucket",
Vars: map[string]interface{}{
"bucket_name": "test-" + random.UniqueId(),
"environment": "test",
},
}
// Deploy real infrastructure
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Get the bucket name from Terraform output
bucketName := terraform.Output(t, terraformOptions, "bucket_name")
region := terraform.Output(t, terraformOptions, "region")
// Verify encryption is enabled on the actual AWS resource
encryption := aws.GetS3BucketEncryption(t, region, bucketName)
assert.Equal(t, "aws:kms", encryption)
// Verify versioning
versioning := aws.GetS3BucketVersioning(t, region, bucketName)
assert.Equal(t, "Enabled", versioning)
// Verify public access is blocked
publicAccess := aws.GetS3BucketPublicAccessBlock(t, region, bucketName)
assert.True(t, publicAccess.BlockPublicAcls)
assert.True(t, publicAccess.BlockPublicPolicy)
}
Лучшие практики Terratest
Всегда используйте
t.Parallel()-- Тесты Terratest медленные. Параллельный запуск радикально сокращает общее время выполнения.Всегда используйте
defer terraform.Destroy()-- Разместите вызов destroy сразу после создания опций, передInitAndApply. Это гарантирует очистку даже при провале теста.Используйте уникальные имена --
random.UniqueId()предотвращает конфликты имён при параллельном запуске тестов или если предыдущая очистка не завершилась.Устанавливайте таймауты -- Создание облачных ресурсов может быть медленным. Задавайте явные таймауты вместо того, чтобы полагаться на стандартный таймаут Go в 10 минут:
go test -v -timeout 30m ./test/
- Используйте этапы тестов для ускорения итераций -- Terratest поддерживает пропуск этапов deploy/destroy во время разработки:
func TestVPC(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/vpc",
}
// Skip deploy if SKIP_deploy is set
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Validation runs even when reusing existing infrastructure
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
subnets := aws.GetSubnetsForVpc(t, vpcId, "us-east-1")
assert.Equal(t, 6, len(subnets)) // 3 public + 3 private
}
Полный пайплайн статической валидации
#!/bin/bash
# scripts/validate-terraform.sh
set -euo pipefail
echo "=== Stage 1: Format ==="
terraform fmt -check -recursive -diff
echo "=== Stage 2: Init + Validate ==="
terraform init -backend=false
terraform validate
echo "=== Stage 3: TFLint ==="
tflint --recursive
echo "=== Stage 4: Security Scan ==="
tfsec . --minimum-severity HIGH
echo "=== Stage 5: Compliance Scan ==="
checkov -d . --framework terraform --compact
echo "=== All static checks passed ==="
Этот скрипт выполняется менее чем за 30 секунд и выявляет большинство проблем до того, как потребуются какие-либо облачные ресурсы.