Хороший модуль ловит мусор на входе сам и говорит человеку понятным языком, что именно не так. В этом уроке разберём три уровня защиты, которые Terraform даёт из коробки: validation на переменных, precondition/postcondition внутри ресурсов и output, и check-блоки для проверок после применения. Это и есть та самая разница между "скриптом, который у меня работает" и переиспользуемым модулем, который не стыдно отдать другим. Инфраструктура как код (iac) - это не только про создание ресурсов, но и про то, чтобы код сопротивлялся неверному использованию.
Сразу про инструменты и версии, чтобы было понятно про что речь. precondition и postcondition появились в Terraform 1.2, check-блоки - в Terraform 1.5. К 2026 году всё это давно стабильно и есть как в свежем Terraform, так и в OpenTofu (это форк, который понимает ту же самую конфигурацию). Так что примеры ниже работают и там, и там.
Уровень 1. Валидация входных переменных
Самая первая линия обороны - блок validation внутри variable. Он проверяет значение ещё на этапе terraform plan, до того как Terraform полезет читать data-источники или создавать что-либо. Если условие не выполнено - сразу ошибка с твоим текстом, апрель ресурсов даже не начнётся.
Блок состоит из двух частей: condition (логическое выражение, которое должно быть true) и error_message (что сказать человеку, если false). Ссылаться внутри condition можно на сам var.имя_переменной.
Код: Выделить всё
variable "zone" {
type = string
description = "Зона доступности Yandex Cloud"
validation {
condition = contains(["ru-central1-a", "ru-central1-b", "ru-central1-d"], var.zone)
error_message = "Зона должна быть одной из: ru-central1-a, ru-central1-b, ru-central1-d. Получено: ${var.zone}."
}
}
variable "cores" {
type = number
description = "Количество vCPU"
validation {
condition = var.cores >= 2 && var.cores <= 32
error_message = "cores должно быть в диапазоне 2..32. В Yandex Cloud нечётные значения и единица недопустимы для платформы standard-v3."
}
}
Код: Выделить всё
variable "vm_name" {
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,61}$", var.vm_name))
error_message = "Имя ВМ: строчные латинские буквы, цифры и дефис, начинается с буквы, до 63 символов."
}
}

Уровень 2. precondition и postcondition
Валидация переменной хороша, но она знает только про вход. А что если проверка зависит от данных, которых на момент разбора переменных ещё нет? Например, ты берёшь образ через data-источник и хочешь убедиться, что у него нужная архитектура. Или хочешь гарантировать, что выбранная зона реально входит в список зон, которые отдаёт API. Тут вступают lifecycle-проверки.
precondition проверяется до создания/обновления ресурса (на этапе plan, если данные известны). Смысл - "не начинай, если предпосылки неверны". postcondition проверяется после того, как ресурс создан или обновлён, и смотрит уже на фактический результат через self. Смысл - "то, что получилось, соответствует ожиданиям?". Оба живут внутри блока lifecycle.
Код: Выделить всё
data "yandex_compute_image" "ubuntu" {
family = "ubuntu-2204-lts"
}
resource "yandex_compute_instance" "vm" {
name = var.vm_name
zone = var.zone
platform_id = "standard-v3"
resources {
cores = var.cores
memory = var.memory
}
boot_disk {
initialize_params {
image_id = data.yandex_compute_image.ubuntu.id
}
}
network_interface {
subnet_id = var.subnet_id
nat = true
}
lifecycle {
precondition {
condition = data.yandex_compute_image.ubuntu.os_type == "LINUX"
error_message = "Ожидался Linux-образ, а пришёл ${data.yandex_compute_image.ubuntu.os_type}. Проверь family."
}
postcondition {
condition = self.network_interface[0].nat_ip_address != ""
error_message = "ВМ создана без публичного IP, хотя nat = true. Что-то не так с подсетью или квотой."
}
}
}
precondition и postcondition можно ставить и в блоке output. Это удобно, когда модуль отдаёт наружу значение и ты хочешь гарантировать его корректность - например, что список зон не пустой:
Код: Выделить всё
output "zones" {
value = data.yandex_compute_zones.available.zones
precondition {
condition = length(data.yandex_compute_zones.available.zones) > 0
error_message = "API не вернул ни одной зоны доступности. Проверь folder_id и права сервисного аккаунта."
}
}
precondition и postcondition - жёсткие: упали - и весь apply красный. Иногда это перебор. Допустим, ты хочешь регулярно проверять, что HTTP-эндпойнт жив, но не хочешь, чтобы временная недоступность сайта рушила всё развёртывание. Для таких "мягких" проверок в Terraform 1.5 завезли check-блоки.
Ключевое отличие: проваленный assert внутри check выдаёт предупреждение (warning), а не ошибку. apply при этом завершается успешно. Это идеально для мониторинговых ассертов и для проверок, которые зависят от внешнего мира.
Код: Выделить всё
check "health" {
data "http" "endpoint" {
url = "https://${yandex_compute_instance.vm.network_interface[0].nat_ip_address}/health"
}
assert {
condition = data.http.endpoint.status_code == 200
error_message = "Эндпойнт /health вернул ${data.http.endpoint.status_code}, ожидался 200."
}
}
Типичные грабли
- error_message без контекста. "Неверное значение" - бесполезно. Всегда вставляй в текст само значение и что от него хотели: человек должен понять причину, не открывая код модуля.
- Тяжёлая логика в condition. Условие должно быть булевым выражением. Если оно громоздкое - вынеси промежуточные вычисления в locals и сошлись на них.
- Путаница self. self доступен только в postcondition и только указывает на текущий ресурс. В precondition его нет.
- Ожидание, что check остановит apply. Не остановит. Если тебе нужна жёсткая гарантия - это postcondition, а не check.
- regex без can. Голый regex() на неподходящей строке сам бросит ошибку и сломает план. Оборачивай в can(), чтобы получить аккуратный false.
Возьми любой свой модуль ВМ для Yandex Cloud и руками добавь в него:
- validation на переменную zone (список допустимых зон) и на vm_name (regex формата);
- precondition в yandex_compute_instance, который проверяет os_type образа из data-источника;
- postcondition с self, проверяющий, что у ВМ появился публичный IP;
- output с precondition на непустой список зон;
- check-блок с одним assert (можно проверить, например, что cores * memory укладывается в твой бюджет).
Контрольные вопросы
- На каком этапе срабатывает validation переменной и почему это раньше, чем precondition?
- Чем принципиально отличается поведение проваленного postcondition от проваленного assert в check-блоке?
- Где можно использовать ссылку self, а где нельзя, и почему?
- В какой версии Terraform появились precondition/postcondition, а в какой - check-блоки?
Три уровня проверок - это слои защиты модуля: validation стережёт вход, precondition/postcondition держат инварианты ресурсов и output, check добавляет мягкие ассерты после apply. Жёсткие проверки валят план или apply и тем самым берегут тебя от кривой инфраструктуры; check лишь предупреждает. Самое ценное в любой из них - не само условие, а понятное error_message: именно оно превращает чужой модуль в инструмент, которым приятно пользоваться. Тестируемый модуль - это модуль, который объясняет, почему он отказался работать.