Модули: переиспользование конфигурации
Модуль — это переиспользуемый набор ресурсов с входами (переменные) и выходами (output). По сути это функция инфраструктуры: вызвал с параметрами — получил готовый кусок облака.
Любая папка с
.tfфайлами — уже модуль. Вопрос лишь в том, вызываете вы её напрямую (root) или подключаете из другого места (child). Модули превращают копипасту в переиспользование.
Когда вы три раза копируете один и тот же набор «VPC + subnet + security group», пора делать модуль. Модуль инкапсулирует ресурсы за чётким интерфейсом: входные переменные — это его параметры, выходы — его результат, а внутренние ресурсы скрыты. Это та же абстракция, что функция в программировании.
Root и child модули
# root-модуль (где запускаете terraform) вызывает child:
module "network" {
source = "./modules/network" # путь к child-модулю
vpc_cidr = "10.0.0.0/16" # вход
environment = "prod"
}
# используем выход модуля как обычный атрибут
resource "aws_instance" "web" {
subnet_id = module.network.subnet_id # module.<имя>.<output>
}
Папка, где вы запускаете terraform, — это root-модуль. Всё, что он подключает через module, — child-модули. К выходам обращаются как module.имя.output. Child-модуль не видит переменные родителя — только то, что ему явно передали. Это и есть инкапсуляция.
ROOT module (вы здесь)
/ | \
v v v
module.network module.db module.app
(vpc,subnet) (rds) (ec2,lb)
| ^
+--- output subnet_id ---+
(передаётся как вход)
Как работает под капотом
Модуль — это namespace: ресурсы внутри получают префикс module.имя. в адресе. Родитель видит только объявленные выходы, дочерний — только переданные входы. Смоделируем эту инкапсуляцию:
def network_module(vpc_cidr, environment):
# внутренние ресурсы -- скрыты от родителя
vpc_id = f"vpc-{environment}"
subnet_id = f"subnet-{environment}-a"
_internal_log = "не виден снаружи"
# наружу отдаём только объявленные outputs
return {"vpc_id": vpc_id, "subnet_id": subnet_id}
mod = network_module(vpc_cidr="10.0.0.0/16", environment="prod")
print("Доступные выходы модуля:", list(mod.keys()))
print("subnet_id для инстанса:", mod["subnet_id"])
# попытка достать внутреннее -> провал, инкапсуляция работает
print("Виден ли _internal_log?", "_internal_log" in mod)
«Попробуй сам ▶» — наружу торчат только vpc_id и subnet_id; внутренние детали скрыты. Так модуль прячет сложность за интерфейсом.
Частые ошибки
- Модуль-монолит. Один гигантский модуль «вся инфра» нельзя переиспользовать по частям. Дробите по назначению.
- Слишком много входов. Модуль с 50 переменными неудобен. Группируйте через объекты, давайте разумные дефолты.
- Прямой доступ к внутренним ресурсам. Обращаться к ресурсу внутри модуля в обход output нельзя — это нарушает инкапсуляцию (и невозможно).
Best practices
- Модуль = одна понятная единица (сеть, БД, кластер). Маленькие модули — меньше blast radius.
- Чёткий интерфейс: продуманные входы с типами/дефолтами и только нужные выходы.
- Не передавайте регион, ARN, CIDR хардкодом внутрь модуля — только через входные переменные.
Разбор глубже
Важно понимать, что модуль создаёт собственное пространство имён в state. Ресурс aws_instance.web внутри модуля app в state адресуется как module.app.aws_instance.web. Это значит, что один и тот же модуль можно подключить несколько раз с разными именами, и их ресурсы не столкнутся — каждый вызов изолирован своим префиксом. Именно это делает модуль переиспользуемым: вызвали module "app_eu" и module "app_us" — получили две независимые копии одной и той же логики в разных регионах.
Хороший модуль проектируют по принципу «разумные умолчания, явные исключения». Большинство входных переменных должны иметь продуманные default, чтобы в простом случае модуль вызывался с минимумом параметров. И только то, что действительно обязано отличаться (имя, окружение, размер), делают обязательным. Модуль с пятьюдесятью обязательными переменными невозможно использовать; модуль с тремя обязательными и двадцатью опциональными — удобен и гибок. Это та же дисциплина, что и проектирование хорошего API функции: минимальная обязательная поверхность, богатая опциональная настройка.
Итог: модуль — это функция инфраструктуры с входами и выходами и скрытой реализацией. Root вызывает child, общение — только через интерфейс. Дальше — откуда модули брать и как версионировать.