Входные переменные: variable и валидация

Переменные (variable) превращают жёстко прописанный конфиг в гибкий шаблон: один и тот же код запускается для dev и prod, отличаясь лишь значениями на входе.

Хороший Terraform-код не содержит ни одного захардкоженного региона, размера или CIDR внутри логики. Всё, что меняется между окружениями, — это входная переменная.

Блок variable объявляет параметр. У него есть тип (string, number, bool, list, map, object), необязательное значение по умолчанию, описание и флаг sensitive для секретов. Внутри кода переменная доступна как var.имя.

Объявление и валидация

variable "instance_type" {
  type        = string
  default     = "t2.micro"
  description = "Тип EC2-инстанса"
}

variable "environment" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment должен быть dev, staging или prod."
  }
}

variable "db_password" {
  type      = string
  sensitive = true   # не покажется в выводе plan/apply
}

Блок validation позволяет проверить значение и выдать понятную ошибку до создания ресурсов. sensitive = true прячет значение из логов — но не из state-файла (об этом в разделе про state).

Как работает под капотом

Прежде чем строить план, Terraform собирает значения переменных и проверяет каждую через её validation-условия. Смоделируем этот сборщик-валидатор: значения берутся с приоритетом (CLI > tfvars > default), затем проверяются.

definitions = {
    "environment": {"validate": lambda v: v in ("dev", "staging", "prod")},
    "instance_type": {"default": "t2.micro", "validate": lambda v: True},
}
tfvars = {"environment": "prod"}      # из файла
cli    = {"instance_type": "t3.large"}  # из -var, высший приоритет

def resolve(defs, tfvars, cli):
    final, errors = {}, []
    for name, spec in defs.items():
        if name in cli:        value = cli[name]
        elif name in tfvars:   value = tfvars[name]
        elif "default" in spec: value = spec["default"]
        else:
            errors.append(f"{name}: значение не задано!"); continue
        if not spec["validate"](value):
            errors.append(f"{name}={value!r}: не прошло валидацию")
        final[name] = value
    return final, errors

vals, errs = resolve(definitions, tfvars, cli)
print("Итоговые значения:", vals)
print("Ошибки:", errs or "нет")

«Попробуй сам ▶» — CLI перебивает tfvars, tfvars перебивает default. Поменяйте environment на "qa" — увидите ошибку валидации.

Частые ошибки

  • Default для секретов. Пароль с default в коде — это секрет в git. Секреты не должны иметь дефолтов.
  • Отсутствие типов. Без type Terraform не поймает, что вы передали строку вместо числа.
  • Думать, что sensitive = шифрование. Он лишь прячет значение из вывода CLI; в state оно лежит открытым текстом.

Best practices

  • Всегда указывайте type и description — это и проверка, и документация модуля.
  • Используйте validation для бизнес-правил (допустимые регионы, диапазоны, форматы).
  • Секреты передавайте через переменные окружения или менеджер секретов, а не дефолты и не tfvars в git.

Разбор глубже

Система типов переменных мощнее, чем кажется на первый взгляд. Кроме простых string и number, HCL поддерживает структурные типы: object({ name = string, size = number }) описывает объект с фиксированным набором полей, а list(object(...)) — список таких объектов. Это позволяет передавать в модуль сложные конфигурации одной переменной и при этом получать строгую проверку: если потребитель забыл поле или указал неверный тип, Terraform поймает это ещё до создания ресурсов. Хорошо типизированная переменная — это и валидация, и живая документация интерфейса модуля.

Стоит ещё раз подчеркнуть разницу между sensitive и реальной секретностью. Флаг sensitive = true решает ровно одну задачу — не печатать значение в выводе plan и apply, чтобы пароль не утёк в логи CI или историю терминала. Но в state-файле это значение по-прежнему лежит открытым текстом, и любой, у кого есть доступ к state, его прочитает. Поэтому настоящая защита секретов — это контроль доступа к удалённому state, его шифрование и получение секретов из внешнего менеджера (Vault, AWS Secrets Manager) во время выполнения, а не хранение их в коде или tfvars.

Итог: переменные параметризуют конфиг; типы и validation ловят ошибки рано, sensitive прячет секреты из логов. Дальше — как именно передавать им значения.

Проверьте себя
1. Что делает флаг sensitive = true у переменной?
AШифрует значение в state-файле
BПрячет значение из вывода plan/apply, но в state оно открыто
CЗапрещает менять переменную
DДелает переменную обязательной
2. Зачем нужен блок validation у переменной?
AЧтобы ускорить plan
BПроверить значение и выдать понятную ошибку до создания ресурсов
CЗашифровать значение
DСделать переменную sensitive