Через месяц у тебя два каталога, которые почти одинаковые, но не совсем. В проде кто-то поправил healthcheck, в dev забыл. Где-то размер диска 4 ГБ, где-то 10, и никто уже не помнит почему. Любое изменение надо вносить дважды, а синхронность держится на честном слове и твоей памяти. Это и есть копипаста инфраструктуры, и лечится она ровно так же, как копипаста в коде. Функцией. В мире Terraform такая функция называется модуль, и эта тема - водораздел между "я написал terraform" и "я умею в инфраструктуру как код".
Что такое модуль на самом деле
Готовься к разочарованию: модуль в Terraform - это просто папка с файлами .tf. Всё. Никакой магии, никакого специального синтаксиса для объявления "вот тут начинается модуль". Любой каталог, в котором лежит хотя бы один .tf, уже является модулем.
Из этого следует важная штука. Когда ты запускаешь terraform apply в какой-то папке, эта папка становится корневым модулем (root module). Это просто роль, а не отдельная сущность. А вот когда внутри своего кода ты подключаешь чужую папку через блок module, она становится дочерним модулем (child module) по отношению к твоему корневому.
То есть всё это время, с самого первого урока, ты уже писал модули. Просто корневые, в единственном экземпляре. Сегодня мы научимся вызывать их повторно, как обычную функцию. Кстати, ровно по этой же механике подключаются готовые модули из реестра - и в registry.terraform.io, и в реестре OpenTofu. Снаружи они выглядят так же: папка с .tf, у которой есть входы и выходы.

Выносим веб-кластер в отдельный модуль
Возьмём наш кластер из урока 8-9 и переложим его так, чтобы можно было вызывать. Договоримся о структуре каталогов проекта:
Код: Выделить всё
infra/
modules/
webserver-cluster/
main.tf # сами ресурсы: instance group, ALB, target group
variables.tf # входные параметры модуля
outputs.tf # что модуль отдаёт наружу
live/
prod/
main.tf # вызываем модуль для прода
dev/
main.tf # вызываем тот же модуль для dev
Внутри modules/webserver-cluster/main.tf лежит наш знакомый кластер. Я приведу его в урезанном виде, чтобы был виден смысл, а не двести строк:
Код: Выделить всё
resource "yandex_compute_instance_group" "web" {
name = "web-cluster"
folder_id = var.folder_id
service_account_id = var.service_account_id
instance_template {
platform_id = "standard-v1"
resources {
cores = 2
memory = 2
}
boot_disk {
initialize_params {
image_id = var.image_id
size = 4
}
}
network_interface {
network_id = var.network_id
subnet_ids = var.subnet_ids
}
}
scale_policy {
fixed_scale {
size = var.cluster_size
}
}
allocation_policy {
zones = var.zones
}
deploy_policy {
max_unavailable = 1
max_creating = 2
}
}
Входы объявляем в variables.tf (детально про переменные - в следующем уроке, тут по верхам):
Код: Выделить всё
variable "cluster_size" {
description = "Сколько ВМ в группе"
type = number
}
variable "image_id" {
description = "ID образа для загрузочного диска"
type = string
}
variable "folder_id" {
type = string
}
variable "service_account_id" {
type = string
}
variable "network_id" {
type = string
}
variable "subnet_ids" {
type = list(string)
}
variable "zones" {
type = list(string)
}
Код: Выделить всё
output "instance_group_id" {
value = yandex_compute_instance_group.web.id
description = "ID группы экземпляров кластера"
}
Теперь самое вкусное. В live/prod/main.tf вызываем нашу заготовку:
Код: Выделить всё
module "frontend" {
source = "../../modules/webserver-cluster"
cluster_size = 4
image_id = "fd8..."
folder_id = var.folder_id
service_account_id = var.service_account_id
network_id = yandex_vpc_network.this.id
subnet_ids = [yandex_vpc_subnet.this.id]
zones = ["ru-central1-a", "ru-central1-b"]
}
В live/dev/main.tf вызов почти такой же, только cluster_size = 1 (на тестинге незачем держать четыре машины), да фолдер другой. Один и тот же код, два окружения, нулевая копипаста. Поправил healthcheck в модуле - он автоматически поправился и в dev, и в prod. Вот ради этого всё и затевалось.
После любого изменения source или добавления нового блока module нужно скормить Terraform команду:
Код: Выделить всё
terraform init
terraform plan
Типичные грабли
- Забыл terraform init после добавления модуля. Добавил блок module, сразу побежал делать plan и получил "Module not installed". init обязателен после любой возни с модулями и их source, а не только при первом запуске.
- Хардкод внутри модуля. Самая частая беда. Если в modules/webserver-cluster прописать конкретный folder_id, имя "web-cluster-prod" или захардкодить зону - модуль перестаёт быть переиспользуемым. Хуже того, при вызове из двух окружений ресурсы подерутся за одинаковые имена. Всё, что отличается между dev и prod, обязано приходить через variable.
- Конфликт имён ресурсов. Если внутри модуля имя ресурса жёстко задано строкой, два вызова модуля попытаются создать два объекта с одинаковым именем в облаке - и второй apply упадёт. Лечится тем же: подмешивай в имя var.env_name или подобное.
- Путаница с путём source. Путь относительный к папке, где лежит вызывающий .tf, а не к той, откуда ты запустил terraform. ../../modules/... - считай уровни вложенности аккуратно.
- Меняешь модуль и удивляешься, почему dev не обновился. Модуль - общий код, но state у каждого окружения свой. Правка в коде модуля применится к окружению только после apply именно в его папке. Никакой телепатии.
Руками, без копипасты из урока:
- Создай структуру modules/webserver-cluster и live/dev, live/prod.
- Перенеси в модуль группу экземпляров из урока 8 так, чтобы cluster_size, image_id, folder_id, network_id и subnet_ids приходили через variable.
- Добавь output с id группы.
- Вызови модуль из live/dev с размером 1 и из live/prod с размером 3.
- Сделай terraform init и terraform plan в каждой папке. Убедись, что в плане ресурсы идут под именем module.frontend.
- Поменяй memory в модуле с 2 на 4 и убедись, что plan видит изменение в обоих окружениях после apply.
- Что технически делает папку модулем в Terraform и чем корневой модуль отличается от дочернего?
- Зачем выносить веб-кластер в modules/webserver-cluster, если можно просто скопировать папку для dev?
- За что отвечают три файла модуля - main.tf, variables.tf, outputs.tf?
- Почему нельзя хардкодить имена ресурсов и folder_id внутри переиспользуемого модуля, и что будет при двух вызовах?
Модуль - это просто папка с .tf, ставшая функцией для инфраструктуры. Корневым модулем папка становится, когда ты запускаешь в ней apply, дочерним - когда её подключают через block module с параметром source. Всё переменное (размеры, образы, сети, имена) выносим во входные variable, нужное наружу отдаём через output, а внутри модуля - ноль хардкода. После добавления модуля всегда terraform init, затем terraform plan. Так инфраструктура как код перестаёт быть копипастой и становится библиотекой, которую переиспользуешь между dev, stage и prod без боли. В следующем уроке докрутим входные, локальные и выходные переменные, чтобы модуль стал по-настоящему настраиваемым.