Язык HCL: блоки, аргументы, выражения

HCL (HashiCorp Configuration Language) — язык, на котором пишут Terraform. Вся его грамматика держится на двух конструкциях: аргументы (имя = значение) и блоки (тип "метка" { ... }).

HCL спроектирован так, чтобы его одинаково легко читали и человек, и машина. Это не язык программирования в привычном смысле — это структурированный способ описать желаемое состояние.

Откройте любой .tf файл — вы увидите только две вещи. Аргумент присваивает значение имени: instance_type = "t2.micro". Блок — это контейнер с типом, опциональными метками и телом в фигурных скобках. Всё остальное — комбинации этих двух элементов.

Анатомия блока

# resource — тип блока
# "aws_instance" — тип ресурса (первая метка)
# "web" — локальное имя (вторая метка)
resource "aws_instance" "web" {
  ami           = "ami-0abcdef"   # аргумент-строка
  instance_type = var.size        # ссылка на переменную
  count         = 3               # аргумент-число
  monitoring    = true            # аргумент-bool

  tags = {                        # аргумент-объект (map)
    Name = "web-${count.index}"   # интерполяция ${}
  }
}

HCL знает основные типы: строки, числа, булевы, списки ["a", "b"], мапы { k = "v" }. Внутри строк работает интерполяция через ${...} — туда можно подставить переменную, результат функции или ссылку на другой ресурс. Комментарии: # и // для строки, /* */ для блока.

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

Когда Terraform читает файл, он не выполняет код сверху вниз, как Python. Он парсит всё дерево блоков, а потом строит граф зависимостей по ссылкам вида var.size или aws_instance.web.id. Поэтому порядок блоков в файле не важен — важны только ссылки. Смоделируем простейший парсер аргументов, превращающий текст HCL в словарь:

hcl = '''
ami           = "ami-0abcdef"
instance_type = "t2.micro"
count         = 3
monitoring    = true
'''

def parse_args(text):
    result = {}
    for line in text.strip().splitlines():
        if "=" not in line:
            continue
        key, raw = line.split("=", 1)
        raw = raw.strip()
        if raw.startswith('"'):
            val = raw.strip('"')
        elif raw in ("true", "false"):
            val = raw == "true"
        else:
            val = int(raw)
        result[key.strip()] = val
    return result

cfg = parse_args(hcl)
print(cfg)
print("тип count:", type(cfg["count"]).__name__)

«Попробуй сам ▶» — Terraform делает то же самое, только умнее: распознаёт строки, числа, булевы и ссылки, сохраняя их типы.

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

  • Кавычки вокруг ссылок: "${var.x}" там, где можно просто var.x. Лишняя интерполяция ухудшает читаемость.
  • Путать список и мапу. [...] — упорядоченный список, {...} — мапа ключ-значение. Их нельзя смешивать.
  • Забыть, что порядок блоков не важен. Новички расставляют ресурсы «по порядку выполнения» — это бессмысленно, граф строится по ссылкам.

Best practices

  • Запускайте terraform fmt — он выравнивает = и приводит код к единому стилю.
  • Используйте «голые» ссылки (var.x), а интерполяцию ${} — только когда строка действительно собирается из частей.
  • Группируйте логически связанные ресурсы в один файл, а не «как пришло в голову».

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

У HCL есть младший брат — JSON-синтаксис. Любую конфигурацию Terraform можно записать в файле .tf.json вместо .tf, и движок её поймёт одинаково. Зачем это нужно? Человек пишет на HCL, потому что он читаемее, а машины (генераторы конфигов, скрипты) иногда генерируют JSON-вариант, потому что его проще собрать программно. Под капотом HCL и его JSON-представление — это одна и та же модель данных, просто два способа записи.

Отдельного внимания заслуживают heredoc-строки и многострочные значения. Когда нужно вписать в аргумент целый скрипт или YAML-манифест, используют синтаксис <<-EOT ... EOT, который сохраняет переносы строк. А блок locals позволяет завести именованные промежуточные значения: вместо того чтобы повторять длинное выражение в десяти местах, вы вычисляете его один раз в locals и ссылаетесь как local.имя. Это не переменные в смысле ввода — это вычисляемые константы конфигурации, и они сильно улучшают читаемость по мере роста проекта.

Ещё одна деталь, экономящая часы отладки: HCL чувствителен к разнице между значением null и отсутствием аргумента. Передать аргументу null — значит явно сказать «используй значение по умолчанию», что иногда отличается от того, чтобы не указывать аргумент вовсе. На практике это всплывает, когда вы условно задаёте аргумент через тернарник: ветка с null корректно откатывает поле к дефолту провайдера, тогда как пустая строка или ноль были бы реальными значениями. Понимание этой тонкости помогает писать аккуратные опциональные конфигурации без сюрпризов на плане.

Итог: весь HCL — это аргументы и блоки. Terraform парсит дерево целиком и строит граф по ссылкам, поэтому порядок не важен. Дальше — главная сущность языка: ресурс.

Проверьте себя
1. Из каких двух базовых конструкций состоит HCL?
AФункции и классы
BАргументы (имя = значение) и блоки (тип «метка» { ... })
CЦиклы и условия
DИмпорты и экспорты
2. Почему порядок блоков в .tf файле не важен?
ATerraform сортирует их по алфавиту
BTerraform строит граф зависимостей по ссылкам, а не по порядку строк
CПорядок на самом деле важен
DБлоки выполняются параллельно случайно