Чтобы модуль стал переиспользуемым, ему нужны три вещи: ручки снаружи (вход), записная книжка внутри (локальные значения) и табличка с результатами (выход). Это variable, locals и output. Разберём каждую по очереди и соберём один модуль, который умеет разворачивать и крошечный dev, и серьёзный prod без единой правки своего кода. Это базовый кирпич, без которого инфраструктура как код (iac) превращается в копипасту с человеческим лицом.
variable - входные параметры модуля
variable - это аргумент, который модуль принимает снаружи. Объявил переменную - и тот, кто вызывает модуль, может передать в неё значение. Не передал - возьмётся значение по умолчанию, если ты его задал.
Базовый набор полей у variable простой: type (тип), default (значение по умолчанию), description (описание для людей и для terraform plan apply). Типы бывают примитивные (string, number, bool) и составные (list, map, object).
Код: Выделить всё
variable "env_name" {
type = string
description = "Имя окружения: dev, stage, prod"
}
variable "instance_count" {
type = number
default = 1
}
variable "machine_type" {
type = string
default = "standard-v3"
}
variable "memory_gb" {
type = number
default = 2
}
Можно добавить и проверку - validation. Например, не дать никому передать ноль инстансов:
Код: Выделить всё
variable "instance_count" {
type = number
default = 1
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "instance_count должен быть от 1 до 10."
}
}

locals - внутренняя кухня
Теперь частая ошибка новичков. Им нужно склеить имя ресурса вида web-prod или вычислить число ядер, и они заводят под это... ещё одну variable. Не надо. Если значение вычисляется внутри модуля из других значений и снаружи его задавать не должны - это locals, локальное значение.
locals - это записная книжка модуля. Туда складывают константы (порты, теги, префиксы) и промежуточные вычисления, чтобы не повторять одно и то же выражение в десяти местах.
Код: Выделить всё
locals {
app_name = "web"
name_prefix = "${local.app_name}-${var.env_name}"
http_port = 80
https_port = 443
common_labels = {
environment = var.env_name
managed_by = "terraform"
}
}
- Блок называется locals (множественное число), а обращение идёт через local.имя (единственное). Перепутать легко, ошибка будет загадочной.
- Внутри locals можно ссылаться на var, на ресурсы и на другие local. Можно собирать значение из значения - как name_prefix выше.
- Локальные значения не настраиваются снаружи. Это их фишка, а не баг: ты прячешь внутреннюю логику от пользователя модуля.
output - что модуль отдаёт наружу
Ресурсы, созданные внутри модуля, по умолчанию снаружи не видны. Корневой код не может просто так дотянуться до id группы или адреса балансировщика, которые родились в недрах модуля. Чтобы пробросить значение наружу, его объявляют как output.
output - это табличка с результатами на двери модуля. Создал группу инстансов - отдай её id. Поднял балансировщик - отдай его адрес, чтобы другой модуль или человек знал, куда стучаться.
Код: Выделить всё
output "instance_group_id" {
description = "ID группы инстансов"
value = yandex_compute_instance_group.web.id
}
output "instance_ids" {
value = yandex_compute_instance_group.web.instances[*].instance_id
}
output "lb_address" {
description = "Внешний адрес балансировщика"
value = yandex_lb_network_load_balancer.web.listener[*].external_address_spec[*].address
}
Код: Выделить всё
module "backend" {
source = "./modules/app"
env_name = "prod"
instance_count = 3
}
module "frontend" {
source = "./modules/app"
env_name = "prod"
# передаём выход одного модуля во вход другого
upstream_group_id = module.backend.instance_group_id
}
output "backend_lb" {
value = module.backend.lb_address
}
Один модуль - два окружения
Соберём всё вместе. Вот ради чего мы и затевали переменные: один и тот же код модуля даёт нам лёгкий dev и тяжёлый prod. Разница только в значениях при вызове.
Код: Выделить всё
module "app_dev" {
source = "./modules/app"
env_name = "dev"
instance_count = 1
machine_type = "standard-v3"
memory_gb = 2
}
module "app_prod" {
source = "./modules/app"
env_name = "prod"
instance_count = 4
machine_type = "standard-v3"
memory_gb = 8
}
Типичные грабли
- Путают locals и local. Объявляешь в блоке locals, обращаешься через local.x. Множественное и единственное число - классическая засада.
- Заводят variable там, где нужен local. Если значение никто снаружи не задаёт, а оно вычисляется внутри - это local. Лишние input-ы раздувают интерфейс модуля и сбивают с толку.
- Забывают, что output - это контракт. Удалил или переименовал output, на который кто-то ссылается через module.x.y - сломал зависимый код. Меняй выходы осознанно.
- Пытаются достать ресурс модуля напрямую, без output. Снаружи виден только то, что явно отдано через output. Внутренние ресурсы инкапсулированы, и это правильно.
- Ставят default на то, что обязано быть осознанным выбором. Дефолтный env_name = "dev" однажды устроит тебе деплой в dev вместо prod или наоборот. Опасные параметры лучше держать без дефолта.
- Думают, что local кешируется между запусками. Нет, locals вычисляются на каждый terraform plan apply заново, это не переменная состояния.
Мини-лаба
Возьми свой модуль из прошлого урока (или собери учебный на пару ресурсов Yandex Cloud) и проделай руками:
- Вынеси в variable три параметра: env_name (без default), instance_count и machine_type (с дефолтами). Добавь к instance_count блок validation на диапазон 1..10.
- Заведи блок locals с name_prefix вида "${var.env_name}-web" и map common_labels. Используй name_prefix хотя бы в двух местах внутри модуля.
- Объяви два output: id главного ресурса и какой-нибудь адрес или имя. Пропиши description у обоих.
- В корневом коде вызови модуль дважды - app_dev и app_prod - с разными значениями. В корне сделай output, который читает module.app_prod.<твой_output>.
- Прогони terraform plan apply (или tofu plan apply) и убедись, что dev и prod различаются по числу и размеру машин, а ресурсы разведены по адресам module.app_dev.* и module.app_prod.*.
- Чем variable отличается от locals и в каком случае нужно заводить именно local, а не лишний input?
- Что произойдёт при terraform plan apply, если у variable нет default и значение не передали при вызове модуля?
- Как из корневого кода прочитать значение, созданное внутри модуля? Через какую конструкцию?
- Почему опасно ставить default на переменную env_name и для каких параметров дефолты, наоборот, уместны?
variable - вход, locals - внутренняя кухня, output - выход. Этой троицей модуль превращается из бетонной плиты в настраиваемый инструмент. Входы делай минимальными и снабжай дефолтами всё, кроме осознанного выбора. Вычисления и константы прячь в locals, чтобы не раздувать интерфейс. Через output отдавай наружу id и адреса - именно они склеивают модули в цепочку и формируют граф зависимостей. Один код модуля, разные значения при вызове - и вот у тебя dev и prod из одного источника. Это и есть смысл модулей terraform yandex cloud в реальной работе.