Modern QA2026Валидация Terraform
Join

Course08 Infrastructure as Code Testing

Cutting-edge · Chapter 08

Валидация 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

  1. Всегда используйте t.Parallel() -- Тесты Terratest медленные. Параллельный запуск радикально сокращает общее время выполнения.

  2. Всегда используйте defer terraform.Destroy() -- Разместите вызов destroy сразу после создания опций, перед InitAndApply. Это гарантирует очистку даже при провале теста.

  3. Используйте уникальные имена -- random.UniqueId() предотвращает конфликты имён при параллельном запуске тестов или если предыдущая очистка не завершилась.

  4. Устанавливайте таймауты -- Создание облачных ресурсов может быть медленным. Задавайте явные таймауты вместо того, чтобы полагаться на стандартный таймаут Go в 10 минут:

go test -v -timeout 30m ./test/
  1. Используйте этапы тестов для ускорения итераций -- 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 секунд и выявляет большинство проблем до того, как потребуются какие-либо облачные ресурсы.