Боль, которую это решает, простая. Без типов и функций ты либо копипастишь одно и то же по десять раз, либо хардкодишь значения, которые потом невозможно поменять в одном месте. Инфраструктура как код (iac) ценна ровно тем, что код можно параметризовать и переиспользовать. А значит, надо уметь этим кодом немного программировать. Не пугайся слова "программировать" - HCL не Тьюринг-полный, циклов while тут нет, и это скорее плюс: меньше способов выстрелить себе в ногу.
В этом уроке разберем систему типов HCL, самые ходовые встроенные функции, валидацию переменных и terraform console - инструмент, который сэкономит тебе часы отладки.
Система типов: что вообще бывает
HCL знает три примитивных типа и три составных (коллекции и структуры).
Примитивы:
- string - строка, "hello".
- number - число, причем и целое, и дробное (42, 3.14). Отдельного int нет.
- bool - true или false.
- list(T) - упорядоченный список одного типа: ["a", "b", "c"]. Доступ по индексу.
- set(T) - множество, без порядка и без дубликатов.
- map(T) - словарь ключ-значение, ключи всегда строки: { env = "prod" }.
- object({...}) - структура с фиксированным набором именованных полей разных типов.
- tuple([...]) - как list, но каждый элемент может быть своего типа и количество фиксировано.
Объявление типизированных переменных выглядит так:
Код: Выделить всё
variable "zones" {
type = list(string)
default = ["ru-central1-a", "ru-central1-b", "ru-central1-d"]
}
variable "labels" {
type = map(string)
default = { team = "platform", env = "dev" }
}
variable "instance" {
type = object({
cores = number
memory = number
name = string
})
}
Код: Выделить всё
variable "vm" {
type = object({
cores = number
memory = number
preemptible = optional(bool, false)
nat = optional(bool, true)
})
}

Функции, которые реально пригодятся
Функций в Terraform под сотню, но в повседневной жизни ты крутишь штук пятнадцать. Разберу самые ходовые на примерах Yandex Cloud.
lookup и coalesce - дефолты. lookup достает значение из map по ключу, а если ключа нет - возвращает запасное. coalesce берет первое непустое значение из списка аргументов.
Код: Выделить всё
locals {
zone_to_subnet = {
"ru-central1-a" = "10.1.0.0/24"
"ru-central1-b" = "10.2.0.0/24"
}
subnet = lookup(local.zone_to_subnet, "ru-central1-a", "10.0.0.0/24")
name = coalesce(var.custom_name, "default-vm")
}
Код: Выделить всё
locals {
common_labels = { managed_by = "terraform", project = "cyberlake" }
all_labels = merge(local.common_labels, var.extra_labels)
all_zones = concat(["ru-central1-a"], var.extra_zones)
}
Код: Выделить всё
locals {
nested = [["a", "b"], ["c"], []]
flat = flatten(local.nested) # ["a", "b", "c"]
count = length(local.flat) # 3
}
Код: Выделить всё
resource "yandex_compute_instance" "web" {
name = "web-1"
# ... resources, boot_disk, network_interface ...
metadata = {
ssh-keys = "ubuntu:${file("~/.ssh/id_ed25519.pub")}"
user-data = templatefile("${path.module}/cloud-init.tftpl", {
hostname = "web-1"
packages = ["nginx", "git"]
})
}
}
Код: Выделить всё
locals {
policy = jsonencode({
version = "1"
rules = [{ action = "allow", port = 443 }]
})
}
Код: Выделить всё
variable "zones" {
type = list(string)
default = ["ru-central1-a", "ru-central1-b", "ru-central1-d"]
}
resource "yandex_vpc_subnet" "this" {
count = length(var.zones)
name = "subnet-${var.zones[count.index]}"
zone = var.zones[count.index]
network_id = yandex_vpc_network.this.id
v4_cidr_blocks = [cidrsubnet("10.10.0.0/16", 8, count.index)]
}
formatdate - даты. Форматирует timestamp в нужный вид. Часто используют с timestamp() для меток времени в именах или labels.
Код: Выделить всё
locals {
today = formatdate("YYYY-MM-DD", timestamp())
}
Код: Выделить всё
locals {
# если var.config.timeout не задан или структура другая - возьмем 30
timeout = try(var.config.timeout, 30)
}
Хорошая переменная не только типизирована, но и проверяет сама себя. Блок validation внутри variable не дает запустить plan с мусором на входе. Это дешевле, чем ловить ошибку через десять минут apply где-то в облаке.
Код: Выделить всё
variable "cores" {
type = number
description = "Количество vCPU"
validation {
condition = var.cores >= 2 && var.cores <= 32
error_message = "cores должно быть в диапазоне от 2 до 32."
}
}
variable "env" {
type = string
validation {
condition = contains(["dev", "stage", "prod"], var.env)
error_message = "env должен быть одним из: dev, stage, prod."
}
}
Код: Выделить всё
variable "cidr" {
type = string
validation {
condition = can(cidrnetmask(var.cidr))
error_message = "cidr должен быть валидным CIDR, например 10.0.0.0/16."
}
}
Главный инструмент урока. Команда terraform console открывает интерактивную консоль, где можно вычислять любые выражения на данных текущего проекта: переменные, locals, функции, даже атрибуты ресурсов из state. Не надо делать plan, чтобы проверить, что выдаст твой merge или cidrsubnet - просто спроси у консоли.
Код: Выделить всё
$ terraform console
> 1 + 2
3
> cidrsubnet("10.10.0.0/16", 8, 5)
"10.10.5.0/24"
> merge({a=1}, {b=2})
{
"a" = 1
"b" = 2
}
> [for z in var.zones : upper(z)]
[
"RU-CENTRAL1-A",
"RU-CENTRAL1-B",
"RU-CENTRAL1-D",
]
> try(var.config.timeout, 30)
30
Где функции реально нужны в инфра-коде
Чтобы не сложилось ощущения "красиво, но зачем" - вот живые сценарии:
- Раздача подсетей по зонам через cidrsubnet и count - классика VPC.
- Сборка labels из общих и специфичных через merge - чтобы тегирование было единообразным.
- Генерация cloud-init для виртуалок через templatefile.
- Формирование JSON-политик и конфигов через jsonencode.
- Безопасное чтение опциональных настроек модуля через try и lookup, чтобы модуль не падал на отсутствующих полях.
- Путать list и set. К set нельзя обратиться по индексу - порядка нет. Если делаешь for_each, set как раз то что надо, а вот var.zones[0] на set не сработает.
- Числа и строки. cidrsubnet и подобные ждут number, а из переменной окружения или tfvars иногда прилетает string. Приводи через tonumber, если что.
- try глушит все подряд. Соблазнительно обернуть пол-конфига в try, но тогда настоящая ошибка молча превратится в дефолт, и ты будешь долго гадать, почему ресурс создался не такой. Используй точечно.
- any вместо нормального типа. Работает, пока не сломается. Описывай object явно - получишь подсказки об ошибках на plan.
- Забыть про path.module в file/templatefile. Относительные пути считаются от рабочей директории, а не от модуля. Внутри модуля всегда префиксуй ${path.module}.
Сделай руками, без этого не уложится:
- Открой terraform console и поиграйся: merge двух map, concat двух списков, flatten вложенного списка, cidrsubnet с разными индексами.
- Объяви переменную типа object с двумя обязательными и одним optional полем. Проверь, что без optional-поля plan проходит.
- Добавь к числовой переменной блок validation с диапазоном и убедись, что неверное значение ловится на plan.
- Напиши local с templatefile-шаблоном (хотя бы пустым cloud-init) и посмотри результат через console командой local.имя.
- Чем set отличается от list и почему к set нельзя обратиться по индексу?
- Что делает optional в object и зачем второй аргумент в нем?
- В чем разница между try и can и где каждый уместнее?
- Зачем нужен terraform console и что в нем можно вычислять?
HCL дает тебе типы (string, number, bool, list, set, map, object, tuple, any plus optional) и десятки функций, чтобы инфраструктура как код была параметризуемой и DRY. Самые ходовые функции - lookup, coalesce, merge, concat, flatten, length, file, templatefile, jsonencode, cidrsubnet, try. Валидация переменных ловит ошибки на этапе terraform plan, а не во время apply. И главное - terraform console превращает написание выражений из гадания в нормальную отладку. Проверяй сложную логику там, прежде чем доверять ей реальные ресурсы в Yandex Cloud.