Рано или поздно в любом проекте на Terraform случается момент, когда ты копипастишь блок ресурса в пятый раз, меняя в нем одно слово. Три виртуалки, пять правил фаервола, восемь записей в DNS - и вот ты уже сидишь и руками правишь почти одинаковые куски HCL. Это скучно, это ломается при первой же опечатке, и это полностью противоречит идее инфраструктура как код, ради которой мы вообще взялись за terraform.
В этом уроке разберем два инструмента, которые превращают копипасту в нормальный код. Первый - выражение for: оно берет список или карту и перегоняет в другой список или карту, по дороге трансформируя и фильтруя элементы. Второй - строковые директивы внутри шаблонов: это циклы и условия прямо в тексте, которыми удобно собирать cloud-init, скрипты и конфиги. Плюс функция templatefile(), которая склеивает шаблон с данными. Все это - чистый язык Terraform, никакой магии и никакой отдельной доки провайдера тут не нужно.

Выражение for: трансформация списков и карт
Идея проста до неприличия. У тебя есть коллекция, ты хочешь получить из нее новую коллекцию, где каждый элемент как-то изменен. В обычном языке ты бы написал цикл, в Terraform для этого есть выражение for.
Если результат обернуть в квадратные скобки - получишь список. Если в фигурные - карту (map). Вот базовый пример с приведением имен зон к верхнему регистру:
Код: Выделить всё
variable "zones" {
type = list(string)
default = ["ru-central1-a", "ru-central1-b", "ru-central1-d"]
}
locals {
zones_upper = [for z in var.zones : upper(z)]
# => ["RU-CENTRAL1-A", "RU-CENTRAL1-B", "RU-CENTRAL1-D"]
}
С картами интереснее: при переборе можно достать и ключ, и значение, а в результат отдать пару через стрелку k => v. Допустим, у нас есть карта инстансов с их параметрами, а нам нужна карта "имя -> размер диска":
Код: Выделить всё
locals {
instances = {
web = { cores = 2, disk = 20 }
db = { cores = 4, disk = 100 }
}
disks = { for name, cfg in local.instances : name => cfg.disk }
# => { web = 20, db = 100 }
}
Код: Выделить всё
locals {
big_machines = [
for name, cfg in local.instances : name
if cfg.cores > 2
]
# => ["db"]
}
Строковые директивы и heredoc
Теперь вторая половина урока. Внутри строк Terraform умеет не только подставлять значения через ${...}, но и выполнять директивы через %{...}. Это мини-язык управления прямо в тексте: условие %{ if ... } %{ else } %{ endif } и цикл %{ for x in list } ... %{ endfor }.
Когда строка длинная и многострочная, ее удобно писать через heredoc - синтаксис с <<- и меткой. Это идеальный формат для cloud-init и bash-скриптов. Соберем кусок конфигурации, где список пакетов разворачивается циклом:
Код: Выделить всё
variable "packages" {
type = list(string)
default = ["nginx", "git", "htop"]
}
locals {
cloud_init = <<-EOT
#cloud-config
packages:
%{ for pkg in var.packages ~}
- ${pkg}
%{ endfor ~}
runcmd:
- [ systemctl, enable, nginx ]
EOT
}
Код: Выделить всё
locals {
motd = <<-EOT
Welcome!
%{ if var.env == "prod" ~}
ВНИМАНИЕ: это боевой сервер, думай головой.
%{ endif ~}
EOT
}
Держать большие шаблоны прямо в .tf-файле быстро становится больно. Функция templatefile(path, vars) выносит шаблон в отдельный файл и подставляет в него переменные. Внутри файла работают те же ${...} и %{...}, что и в heredoc.
Делаем файл templates/cloud-init.yaml.tftpl:
Код: Выделить всё
#cloud-config
hostname: ${hostname}
users:
- name: ${user}
sudo: ALL=(ALL) NOPASSWD:ALL
packages:
%{ for pkg in packages ~}
- ${pkg}
%{ endfor ~}
runcmd:
%{ for cmd in commands ~}
- ${cmd}
%{ endfor ~}
Код: Выделить всё
resource "yandex_compute_instance" "vm" {
name = "app-1"
platform_id = "standard-v3"
zone = "ru-central1-a"
resources {
cores = 2
memory = 2
}
boot_disk {
initialize_params {
image_id = "fd80mrhj8fl2oe87o4e1"
}
}
network_interface {
subnet_id = yandex_vpc_subnet.subnet.id
nat = true
}
metadata = {
user-data = templatefile("${path.module}/templates/cloud-init.yaml.tftpl", {
hostname = "app-1"
user = "yc-user"
packages = ["nginx", "git", "htop"]
commands = ["systemctl enable nginx", "systemctl start nginx"]
})
}
}
Типичные грабли
- Забытая тильда. Без ~ директива оставляет пустые строки и отступы. Для YAML и cloud-init это смертельно - конфиг просто не распарсится на загрузке. Всегда ставь ~ там, где важна структура.
- Доллар и проценты в bash-скрипте. Если внутри шаблона есть настоящий ${VAR} баша, Terraform примет его за свою интерполяцию и упадет. Экранируй удвоением: $${VAR} и %%{ соответственно.
- Путь без path.module. templatefile() ищет файл относительно рабочей директории, а не модуля. Всегда пиши ${path.module}/..., иначе при вызове модуля из другого места все развалится.
- Сложная логика в шаблоне. Директивы - это не полноценный язык программирования. Если в шаблоне начинаются вложенные циклы с условиями в три этажа, лучше подготовь данные заранее выражением for в locals, а в шаблон отдай уже готовый плоский список.
- Заведи переменную-список из 3-4 пакетов и через for в верхнем регистре собери из нее новый список. Проверь результат через terraform console.
- Сделай карту инстансов (имя -> число ядер) и выражением for с if отфильтруй только те, где ядер 2 и больше.
- Напиши heredoc cloud-config, который циклом %{ for } разворачивает список пакетов. Поиграй с тильдой: посмотри в terraform plan, как меняется вывод с ~ и без.
- Вынеси этот шаблон в .tftpl-файл и подключи через templatefile() в metadata.user-data у yandex_compute_instance. Прогони plan и убедись, что user-data собирается как ты ждешь.
- Чем отличается [for x in list : ...] от {for k, v in map : k => ...} по типу результата?
- Что делает if в конце выражения for и куда его ставить?
- Зачем нужна тильда ~ в строковой директиве %{ for ... ~}?
- Как заэкранировать настоящий ${VAR} баша внутри шаблона, чтобы Terraform его не трогал?
For - твой главный инструмент трансформации: квадратные скобки дают список, фигурные со стрелкой - карту, if в конце фильтрует. Строковые директивы %{ for } и %{ if } собирают многострочные конфиги прямо в тексте, а тильда спасает отступы. templatefile() выносит шаблон в файл и кормит его переменными - так рождается чистый user-data для виртуалок Yandex Cloud. Готовь данные через for в locals, а рендери через шаблоны - и копипаста в твоем terraform state наконец-то закончится.