Композиция: вложенные модули и среды
Сила модулей раскрывается в композиции: высокоуровневые модули собираются из низкоуровневых, а выход одного становится входом другого. Так из кирпичей строят целые окружения.
Хорошая архитектура Terraform похожа на слоёный пирог: сеть → данные → приложение. Каждый слой — модуль, который берёт выходы нижнего и отдаёт выходы верхнему. Циклы между слоями запрещены.
Реальные системы строятся слоями. Модуль network создаёт VPC и подсети. Модуль database берёт subnet_id из сети и поднимает БД. Модуль app берёт и сеть, и эндпоинт БД. Это композиция: связь модулей через передачу выходов входами.
Слоистая сборка
module "network" {
source = "./modules/network"
vpc_cidr = "10.0.0.0/16"
}
module "database" {
source = "./modules/database"
subnet_id = module.network.subnet_id # выход сети -> вход БД
}
module "app" {
source = "./modules/app"
subnet_id = module.network.subnet_id
db_endpoint = module.database.endpoint # выход БД -> вход app
}
Эти связи задают тот же граф зависимостей, что и между ресурсами: Terraform создаст сеть, затем БД, затем приложение. Циклы запрещены: если app отдаёт выход в network, а network зависит от app — ошибка Cycle.
СЛОЙ 1: network (vpc, subnets)
| subnet_id
v
СЛОЙ 2: database (rds в subnet)
| endpoint
v
СЛОЙ 3: app (ec2 + lb, знает БД)
поток выходов СВЕРХУ ВНИЗ, циклов нет = DAG
Как работает под капотом
Композиция модулей — это тот же топологический порядок, что и для ресурсов, только узлы крупнее. Смоделируем сборку окружения с проверкой на циклы:
modules = {
"network": [],
"database": ["network"],
"app": ["network", "database"],
}
def build_order(modules):
order, temp, done = [], set(), set()
def visit(m):
if m in done:
return
if m in temp:
raise ValueError(f"ЦИКЛ через модуль {m}!")
temp.add(m)
for dep in modules[m]:
visit(dep)
temp.discard(m); done.add(m); order.append(m)
for m in modules:
visit(m)
return order
print("Порядок применения модулей:")
for i, m in enumerate(build_order(modules), 1):
print(f" {i}. module.{m}")
«Попробуй сам ▶» — добавьте "network": ["app"] и увидите, как детектор поймает цикл. Это ровно та проверка, что делает Terraform.
Частые ошибки
- Циклы между модулями. Взаимная передача выходов ломает граф — Terraform падает с
Cycle. - Слишком глубокая вложенность. Модуль в модуле в модуле трудно отлаживать; держите 2-3 уровня максимум.
- Дублирование root на окружения. Копипаста root-конфигов для dev/prod расходится со временем; параметризуйте через tfvars.
Best practices
- Стройте чёткие слои: сеть → данные → приложение, поток выходов в одну сторону.
- Один root на окружение с разными tfvars, общие child-модули переиспользуются.
- Избегайте «модуля ради модуля»: оборачивайте только то, что реально повторяется.
Разбор глубже
В сообществе сложилось важное различие между модулями-ресурсами и корневыми модулями. Модуль-ресурс — это переиспользуемый кирпич (VPC, БД), который сам по себе не разворачивается, а только подключается. Корневой модуль — это конкретная сборка под конкретное окружение, которая эти кирпичи компонует и запускается напрямую. Хорошая практика — держать переиспользуемые модули как можно более «глупыми» и параметризуемыми, а всю специфику окружения (имена, размеры, какие модули вообще подключать) выносить в корневые модули. Так один набор кирпичей обслуживает все окружения.
Отдельная тема — инструменты-обёртки вроде Terragrunt, появившиеся именно из боли с композицией на масштабе. Когда корневых модулей на каждое окружение становится много, в них накапливается дублирование: один и тот же блок backend, одни и те же провайдеры, повторяющиеся вызовы. Terragrunt позволяет вынести это в общие шаблоны и держать конфигурацию по-настоящему DRY, а также управлять зависимостями между отдельными state. Для старта он не нужен — но знать о его существовании полезно: когда вы упрётесь в копипасту между десятками окружений, это и будет сигналом, что пора смотреть в сторону таких инструментов.
Итог: композиция собирает окружения из модулей-слоёв через передачу выходов; граф остаётся ацикличным. Один root на окружение, общие child переиспользуются. Дальше — выходим в реальный мир: провайдеры и облако.