Представь типичную картину. Команда начинает с Terraform, пишет инфраструктуру как код, и вся прод-среда живет в одном модуле. Сеть, кластер Kubernetes, база данных, балансировщик, мониторинг, бакеты, права доступа - все свалено в одну папку. Поначалу удобно: один terraform apply и готово. А потом ты хочешь поднять идентичный стенд для тестов и обнаруживаешь, что переиспользовать нечего - все ресурсы жестко склеены друг с другом.
Боль начинается раньше, чем кажется. Когда в одном стейте сотни ресурсов, каждый terraform plan читает их все, опрашивает API облака по каждому, и ты ждешь минуты вместо секунд. А самое неприятное - blast radius, радиус взрыва. Тебе нужно поменять одну метку у бакета, а в плане висят изменения по сети и кластеру, потому что они в том же стейте. Один кривой apply - и ты роняешь не бакет, а полпрода. В IaC цена ошибки измеряется не строчкой кода, а живыми сервисами.
Монолитный модуль нарушает базовый инженерный принцип: одна сущность - одна ответственность. Так же как ты не пишешь функцию на тысячу строк, не стоит держать модуль terraform, который отвечает за все сразу. Философия мелких модулей ровно про это: дробим инфраструктуру на маленькие куски, у каждого своя зона ответственности, а собираем их как кубики через входы и выходы.

Как устроена декомпозиция: одна ответственность на модуль
Идея простая. Каждый модуль terraform отвечает за один логический слой инфраструктуры и ничего не знает о соседях напрямую. Типичная нарезка для прод-сервиса:
- network - VPC, подсети, шлюзы, таблицы маршрутизации. Меняется редко, ломать страшно.
- cluster - управляемый Kubernetes или группа ВМ. Меняется чаще: версии, размеры нод.
- database - управляемая БД, бэкапы, параметры. Меняется редко, данные критичны.
- monitoring - дашборды, алерты, экспортеры. Меняется постоянно и почти безопасно.
Два главных критерия, по которым проводишь границу между модулями:
- По частоте изменений. То, что трогаешь раз в полгода (сеть, БД), не должно жить вместе с тем, что меняешь каждую неделю (мониторинг, версии приложения). Иначе частые правки таскают за собой риск задеть стабильное.
- По blast radius. Чем больнее уронить ресурс, тем сильнее его надо изолировать. Сеть и база - в отдельных модулях и часто в отдельных стейтах. Дашборд можно пересоздавать хоть десять раз в день.
Покажу на Yandex Cloud. Сначала маленький модуль сети. Его задача - создать сеть и подсеть и отдать наружу их идентификаторы.
Код: Выделить всё
# modules/network/main.tf
variable "zone" {
type = string
default = "ru-central1-a"
}
resource "yandex_vpc_network" "this" {
name = "app-net"
}
resource "yandex_vpc_subnet" "this" {
name = "app-subnet"
zone = var.zone
network_id = yandex_vpc_network.this.id
v4_cidr_blocks = ["10.10.0.0/24"]
}
output "network_id" {
value = yandex_vpc_network.this.id
}
output "subnet_id" {
value = yandex_vpc_subnet.this.id
}
Теперь модуль кластера. Он принимает id сети и подсети как input-переменные - то есть зависит от выходов network, но не от его внутренностей.
Код: Выделить всё
# modules/cluster/variables.tf
variable "network_id" { type = string }
variable "subnet_id" { type = string }
variable "zone" {
type = string
default = "ru-central1-a"
}
# modules/cluster/main.tf
resource "yandex_kubernetes_cluster" "this" {
name = "app-k8s"
network_id = var.network_id
master {
version = "1.29"
zonal {
zone = var.zone
subnet_id = var.subnet_id
}
public_ip = true
}
service_account_id = var.service_account_id
node_service_account_id = var.service_account_id
}
variable "service_account_id" { type = string }
output "cluster_id" {
value = yandex_kubernetes_cluster.this.id
}
output "cluster_endpoint" {
value = yandex_kubernetes_cluster.this.master[0].external_v4_endpoint
}
Код: Выделить всё
# main.tf (корень)
module "network" {
source = "./modules/network"
zone = "ru-central1-a"
}
module "cluster" {
source = "./modules/cluster"
network_id = module.network.network_id
subnet_id = module.network.subnet_id
service_account_id = yandex_iam_service_account.k8s.id
}
module "database" {
source = "./modules/database"
network_id = module.network.network_id
subnet_id = module.network.subnet_id
}
Тот же подход работает один в один в OpenTofu - это форк, синтаксис HCL и логика модулей идентичны, можно просто заменить бинарь terraform на tofu.
Типичные грабли
- Дробление ради дробления. Модуль на один ресурс - это тоже антипаттерн. Если у тебя модуль из одной строки yandex_vpc_network, ты получил обертку без пользы и лишний слой переменных. Модуль должен инкапсулировать осмысленный кусок, а не каждую железку.
- Скрытые связи через data вместо outputs. Соблазн велик: в модуле кластера сделать data-источник и найти сеть по имени. Не делай так. Это прячет зависимость от графа, Terraform не понимает порядок, и ты ловишь гонки при первом apply. Связь должна идти через явный input.
- Слишком много стейтов. Маятник качается в обе стороны. Если каждый модуль вынести в отдельный стейт, ты больше не передаешь outputs напрямую - приходится тянуть их через remote state или data-источники, и сборка усложняется. Дроби стейты по blast radius, а не по числу модулей.
- Протекающая абстракция в переменных. Если модуль принимает на вход двадцать переменных, его контракт раздут и им неудобно пользоваться. Прячь детали внутри, наружу выноси только то, без чего вызвать модуль нельзя.
Возьми любой свой монолитный конфиг (или собери учебный) и проделай руками:
- Раздели его минимум на два модуля - network и cluster - в папке modules/.
- В модуле network объяви outputs network_id и subnet_id.
- В модуле cluster объяви соответствующие input-переменные и используй их.
- В корне свяжи их: передай module.network.subnet_id в module "cluster".
- Запусти terraform plan apply и посмотри в выводе порядок создания - сеть пойдет раньше кластера.
- Теперь измени что-нибудь только в модуле cluster и снова сделай plan. Убедись, что ресурсы network в плане не дергаются. Это и есть уменьшенный blast radius в действии.
- Какие два критерия используют, чтобы провести границу между модулями?
- Через что модули обмениваются данными и почему нельзя связывать их через data-источник по имени?
- Почему гигантский монолитный модуль замедляет terraform plan?
- В чем антипаттерн модуля, оборачивающего ровно один ресурс?
Мелкие компонуемые модули - это не мода, а способ снизить риск и ускорить работу. Один модуль - одна ответственность. Граница проходит по частоте изменений и по blast radius: редкое и опасное (сеть, БД) отделяй от частого и безопасного (мониторинг). Связывай кубики через outputs и inputs - явный контракт, который Terraform превращает в граф зависимостей и применяет в правильном порядке. Не впадай в крайности: ни монолит на все, ни модуль на каждую строчку. Хороший модуль - тот, который не страшно поменять и приятно переиспользовать.