Terraform 1.3: optional() в object-типах и check-блоки вместо ручных smoke-тестов
Terraform 1.3 принёс optional()-атрибуты в object-типах и check-блоки для постдеплойной валидации. Рассказываем, как заменили часть ручных smoke-тестов автоматическими проверками.
Terraform 1.3 GA - поддержка optional() атрибутов в object-типах переменных и новые check-блоки для постдеплойной валидации ресурсов
Terraform 1.3 вышел на прошлой неделе, и в нём две фичи, которые мы ждали достаточно давно, чтобы заметить их появление. Одна решает давнюю головную боль с object-переменными, другая позволяет встроить постдеплойные проверки прямо в конфигурацию - без отдельных скриптов и без ручных кликов по консоли после каждого apply.
optional() в object-типах: наконец-то
До версии 1.3, если ты объявлял переменную с типом object, все атрибуты были обязательными. Обойти это можно было через any или хаки с try(), но оба варианта - плохой компромисс: либо теряешь валидацию типов полностью, либо код превращается в лапшу из try(var.foo.optional_field, null) по всему модулю.
Теперь в type constraint можно писать так:
variable "service_config" {
type = object({
name = string
port = number
healthcheck = optional(string, "/health")
replicas = optional(number, 1)
})
}
Второй аргумент optional() - дефолтное значение. Если его не указать, атрибут просто принимает null. Terraform сам подставит дефолт при обработке переменной - не надо делать это в каждом месте использования.
На практике нам это помогло в первую очередь в шаблонных модулях, которые мы держим для похожих клиентских конфигураций. Раньше приходилось либо плодить обязательные параметры, которые в 80% случаев одинаковы, либо вводить отдельную переменную-объект через any и потом жить без проверок типов. Теперь можно нормально описать тип с разумными дефолтами, и модуль становится заметно чище для вызывающей стороны.
Маленький нюанс: optional() работает только внутри object() в type constraint переменной. Не в локалях, не в output-блоках. Ограничение понятное, но иногда хочется большего.
check-блоки: smoke-тесты внутри конфигурации
Это интереснее. В Terraform 1.3 появился новый блок check:
check "api_endpoint_reachable" {
data "http" "healthcheck" {
url = "https://${aws_lb.main.dns_name}/health"
}
assert {
condition = data.http.healthcheck.status_code == 200
error_message = "Health endpoint вернул ${data.http.healthcheck.status_code}, ожидался 200"
}
}
После успешного apply Terraform выполняет все check-блоки. Если assert не проходит - план не откатывается (инфраструктура уже создана), но Terraform выводит предупреждение и завершается с ненулевым кодом. В CI это означает, что пайплайн упадёт и команда увидит проблему до того, как кто-то начнёт смотреть, почему сервис не отвечает.
Ключевое отличие от precondition/postcondition на ресурсах: check-блок работает с data source, который вычисляется после apply. То есть можно реально проверить, что задеплоенный ресурс работает, а не просто что его конфигурация выглядит правильно.
Как мы это применяем
До 1.3 у нас был ритуал после каждого terraform apply на несколько стендов: открыть консоль, проверить, что load balancer поднялся, что target group зелёная, что health endpoint отвечает. Занимает немного времени, но это чисто ручная работа, которую инженер делает по привычке, а не потому что это нельзя автоматизировать.
Мы перевели часть этих проверок в check-блоки. Конкретно сейчас закрыты:
- Доступность health endpoint балансировщика после деплоя - через
data "http"к /health. - Статус target group в AWS - проверяем через data source, что хотя бы один таргет healthy.
- DNS-резолвинг нового домена - важно при смене record set, чтобы убедиться, что Route53 отдаёт правильный IP.
Не всё можно закрыть check-блоком - есть проверки, которые требуют аутентификации или занимают время (например, дождаться прогрева кеша). Для таких по-прежнему живут отдельные скрипты в пайплайне. Но часть ручного труда всё равно ушла.
Есть одна тонкость с data source внутри check: он не попадает в state и не влияет на plan/apply следующего прогона. Terraform пересчитывает его каждый раз при check-фазе. Это хорошо - check не засоряет state - но нужно помнить, что ошибка в data source внутри check не будет такой же «громкой», как ошибка в основном ресурсе.
Что ещё поменялось
В 1.3 также доработали moved блоки - теперь работает для модулей, не только для ресурсов. Мы ещё не применяли это в боевых условиях, но звучит полезно для рефакторинга модульной структуры без пересоздания ресурсов.
Напомним, что с Terraform 1.1 переменные поддерживают nullable = false - указание, что переменная не может принять null даже если не задана. До этого обходились через validation-блоки.
Общее ощущение
Terraform последние несколько версий двигался в сторону более строгой типизации и встроенных механизмов валидации. check-блоки - следующий шаг в той же логике: не просто «конфигурация описана правильно», а «задеплоенная система работает». Это разные вещи, и хорошо, что теперь это можно выразить в самом Terraform, а не только в обёртке вокруг него.
Smoke-тесты в пайплайне всё равно нужны - там где требуется реальная бизнес-валидация, а не просто HTTP 200. Но те проверки, которые можно выразить декларативно внутри конфигурации, лучше держать там: они всегда актуальны, версионируются вместе с инфраструктурой и не забываются при переезде пайплайна.