Тест-пирамида для инфры не работает
Классическая пирамида: 70% unit, 20% integration, 10% e2e. Для приложения ок. Для Terraform, проблематично.
- Unit-тест Terraform-модуля стоит дёшево (
.tftest.hcl+ mock_provider), но проверяет узкое: «модуль выкатил то, что HCL сказал выкатить». От бага в провайдере или AWS-API он не защитит. - Integration дороже на порядок (поднять LocalStack/aws, минуты), но реально проверяет, что результат, рабочая инфра.
- E2E, собственно production-deploy. Само развёртывание уже есть в pipeline; «отдельный e2e», обычно дублирование.
Реалистичный профиль для Terraform-репо: 40% unit, 40% integration на LocalStack, 20% policy/compliance, e2e, production-pipeline.
Что тестировать
1. Контракт модуля
Модуль принимает var.name и выкатывает aws_s3_bucket.this. Тест:
передал «foo» → в plan aws_s3_bucket.this.bucket == "foo". Это
страховка от случайного переименования переменной.
# tests/contract.tftest.hcl
run "var_name_propagates" {command = plan
variables { name = "foo" } assert {condition = aws_s3_bucket.this.bucket == "foo"
error_message = "var.name does not reach bucket"
}
}
2. Бизнес-правила (политика)
Encryption обязателен. Tag CostCenter обязателен. Не публичный по умолчанию. Это уровень компании, не модуля. Лучше, policy-as-code (tf-policy-as-code / terraform-compliance) на plan-файл в CI.
3. Сложные expressions
Простой name = var.name тестировать незачем. А вот:
locals { bucket_name = "${var.team}-${var.purpose}-${random_id.suffix.hex}"tags = merge(
var.default_tags,
{ Team = var.team, ManagedBy = "terraform" },)
}
Здесь логика. Тест: «при team=ai и purpose=logs имя начинается с ai-logs-».
4. Рефакторинги (moved-блоки)
Перенесли aws_s3_bucket.logs в модуль. Тест на чистый plan:
run "no_diff_after_refactor" {command = plan
# ассерт что plan ничего не делает
}
Сам Terraform pluralized сказал «no changes», но без assert в тесте этот факт нигде не зафиксирован.
5. Преконды/посткондишены
variable "env" {type = string
validation {condition = contains(["dev", "stage", "prod"], var.env)
error_message = "env must be dev/stage/prod"
}
}
Тест с expect_failures = [var.env] при env = "xyz", гарантирует, что
validation сработает. См. tf-test-framework.
Что НЕ тестировать
1. Что HCL правильно описывает AWS API
resource "aws_s3_bucket" "this" {bucket = "foo"
}
Тестировать «когда apply, в AWS появится бакет foo», бесполезно. Это работа HashiCorp + AWS SDK. Ты не тестируешь компилятор, ты тестируешь свой код.
2. Тривиальный pass-through
output "arn" {value = aws_s3_bucket.this.arn
}
Тест «output arn равен ARN», бессмысленный. Нечего ломаться.
3. Что облако ведёт себя как облако
«После apply бакет действительно отвечает 200 на HEAD», это smoke-test уровня операций, а не теста модуля. Делается в production через мониторинг, а не в test suite.
4. Performance
Тестировать «apply 100 ресурсов за < 60 секунд», flaky всегда. Performance Terraform зависит от latency provider'а и сети. Если хочется, отдельный бенч раз в неделю, не в каждом PR.
Уровни и инструменты
| Уровень | Что | Чем |
|---|---|---|
| Static analysis | Синтаксис, форматирование, типичные ошибки | terraform fmt -check, terraform validate, tf-checkov |
| Lint | Стиль, depracated args, провайдерские best-practices | tflint с rule-set'ом |
| Unit (модуль в изоляции) | Контракт модуля, наименования, бизнес-правила | .tftest.hcl + mock_provider |
| Integration | Реально создаются ресурсы, cross-resource взаимодействия | .tftest.hcl с command = apply на LocalStack, либо Terratest |
| Policy | Корпоративные правила (тэги, security) | OPA+Rego, terraform-compliance, Checkov |
| E2E | Развёртывание prod-окружения | Сам production-pipeline |
Golden plan
Один лёгкий, но мощный тест: «текущий код даёт plan, побитово совпадающий с сохранённой эталонной строкой». При любом изменении (HCL, провайдер, модуль) diff. Ревьюер видит точно что изменилось.
Реализация:
terraform plan -out=plan.tfplan
terraform show -no-color plan.tfplan > plan.golden
Закоммитили plan.golden. В CI:
terraform plan -out=plan.tfplan
terraform show -no-color plan.tfplan > plan.current
diff plan.golden plan.current || exit 1
Когда меняешь HCL, обновляешь golden. PR показывает diff в HCL и diff в golden, обе стороны видны. Полезно на root-модулях с zero-diff-ожиданием.
Сколько тестов
Не больше, чем оправдано. Признаки переборщил:
- Тесты дольше, чем сам apply.
- Чаще ломаются от обновления провайдера, чем от твоего кода.
- В тестах больше copy-paste, чем в production-коде.
- Никто не может объяснить, что именно этот тест ловит.
Признаки недотестировал:
- Boilerplate-ошибки доходят до prod.
- Рефакторинги ломают что-то, что не было заметно в plan.
- Бизнес-правила нарушаются (tag забыли, encryption выключили).
Балансируй между.
Подводные камни
-
Тесты, обязательство, не актив. Каждый тест надо поддерживать. Старый тест, который никто не понимает, но боится удалить, токсичный долг.
-
Mock'и не ловят интеграционные баги. Юнит-тест с
mock_providerможет pass'нуться, а реальный apply упадёт, например, потому что AWS API требует определённый порядок аргументов или формат имени. -
Дешёвый тест дорог в обслуживании. Сценарий «при var.foo=true, plan показывает 5 ресурсов», простой, но при добавлении 6-го ломается. Лучше тестировать инварианты («каждый аккаунт имеет KMS-key»), чем точные числа.
-
Тесты не заменяют код-ревью. Хорошо написанный HCL ревьюится быстрее, чем плохой код с 100% test coverage. Тесты, в дополнение.
-
Production-debug пишется тестами. Каждый раз когда что-то сломалось в production, добавляй тест, который бы это поймал. Это единственный надёжный способ копить test suite, который реально ловит реальные баги.