ADG Оставить заявку
Блог DevOps 5 мин чтения

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. Но те проверки, которые можно выразить декларативно внутри конфигурации, лучше держать там: они всегда актуальны, версионируются вместе с инфраструктурой и не забываются при переезде пайплайна.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.