Это классическая боль модульной разработки в Terraform и OpenTofu. Модули - отличная штука для переиспользования, но без аккуратного версионирования они превращаются в мину замедленного действия. В этом уроке разберём, как прибивать версии намертво, где живут реестры модулей, и какие грабли разложены по дороге - от путей к файлам до inline-блоков, которые ломают всю гибкость.
Прибиваем версию: source и ?ref
Когда ты подключаешь модуль из git, source указывает не только репозиторий, но и то, какую именно ревизию тянуть. И вот тут начинается самое интересное. Если ты пишешь просто адрес репозитория, Terraform берёт дефолтную ветку (обычно main) на момент terraform init. То есть твоя версия модуля - это "что было в HEAD, когда я инициализировал". Ветка - это движущаяся мишень. Сегодня там одно, завтра другое.
Лечится это параметром ?ref в конце source. Он принимает имя ветки, тег или хэш коммита:
Код: Выделить всё
# ПЛОХО: ветка - это движущаяся мишень
module "network" {
source = "git::https://github.com/myorg/tf-modules.git//network"
}
# ХОРОШО: прибили к конкретному тегу
module "network" {
source = "git::https://github.com/myorg/tf-modules.git//network?ref=v1.4.0"
}
# Тоже норм: прибили к коммиту, если тегов нет
module "network" {
source = "git::https://github.com/myorg/tf-modules.git//network?ref=a1b2c3d"
}
Правило простое: в проде нельзя ссылаться на изменяемую ветку. Тег v1.4.0 после публикации не двигается (если кто-то его не переписал силой, но это уже саботаж). Коммит a1b2c3d вообще иммутабелен по природе git. А вот main, master, develop - меняются под тобой в любой момент, и ты получаешь невоспроизводимые сборки. На ветку можно ссылаться разве что в песочнице, где ты сам же этот модуль и пилишь.

Реестр модулей: source без git
Кроме git есть реестры модулей. Самый известный - публичный Terraform Registry, но компании поднимают и приватные (в том числе через Terraform Cloud, GitLab, Artifactory). Источник из реестра выглядит компактнее и поддерживает version отдельным аргументом с диапазонами:
Код: Выделить всё
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
В Yandex Cloud, кстати, для самого провайдера версия фиксируется в блоке required_providers, и это та же логика - прибивай явно:
Код: Выделить всё
terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex"
version = ">= 0.13"
}
}
}
Грабли с путями к файлам в модуле
Теперь к подводным камням, об которые бьются почти все. Внутри модуля ты иногда читаешь файлы - шаблон cloud-init, скрипт, ключ. И тут важно не перепутать path-переменные:
- path.module - папка, где лежит сам файл модуля. Это то, что нужно в 99% случаев, когда файл - часть модуля.
- path.root - корневая папка конфигурации, та, откуда ты запускаешь terraform. Зависит от того, кто и откуда вызвал модуль.
- path.cwd - текущая рабочая директория процесса. Ещё более капризная, меняется от способа запуска.
Код: Выделить всё
resource "yandex_compute_instance" "web" {
name = "web-1"
# ... другие обязательные блоки ...
metadata = {
user-data = file("${path.module}/files/cloud-init.yaml")
}
}
Inline-блоки против отдельных ресурсов
Это грабли потоньше, но больнее. Многие ресурсы позволяют описывать связанные сущности двумя способами: вложенным блоком внутри родителя (inline) или отдельным ресурсом, который ссылается на родителя по id. Классика - правила security group.
Код: Выделить всё
# Вариант А: inline-правила внутри группы
resource "yandex_vpc_security_group" "sg" {
network_id = yandex_vpc_network.net.id
ingress {
protocol = "TCP"
port = 443
v4_cidr_blocks = ["0.0.0.0/0"]
}
}
# Вариант Б: правила отдельными ресурсами
resource "yandex_vpc_security_group" "sg" {
network_id = yandex_vpc_network.net.id
}
resource "yandex_vpc_security_group_rule" "https" {
security_group_binding = yandex_vpc_security_group.sg.id
direction = "ingress"
protocol = "TCP"
port = 443
v4_cidr_blocks = ["0.0.0.0/0"]
}
Почему это касается модулей? Inline жёстче. Если внутри модуля правила зашиты inline, пользователь модуля не сможет добавить своё правило снаружи - оно будет конфликтовать. Отдельные ресурсы дают гибкость: модуль создаёт группу, а правила можно навешивать снаружи через _rule с привязкой по id. Когда проектируешь переиспользуемый модуль, выбирай отдельные ресурсы для всего, что пользователь захочет расширять.
Self-переменные и когда дробить на под-модули
Внутри некоторых вложенных блоков (например provisioner или connection) раньше был объект self - ссылка на сам ресурс, к которому блок прицеплен. Нужно это потому, что в provisioner нельзя написать yandex_compute_instance.web.network_interface[0].nat_ip_address - это создало бы циклическую зависимость ресурса на самого себя. Поэтому пишут self.network_interface[0].nat_ip_address. Запомни: self живёт только внутри блоков, привязанных к ресурсу, и в обычных выражениях его нет.
Когда дробить конфигурацию на под-модули? Не делай модуль ради модуля - обёртка из одного ресурса только добавляет уровень косвенности. Дроби, когда: набор ресурсов осмысленно переиспользуется в нескольких местах; есть чёткая граница ответственности (сеть отдельно, кластер отдельно, БД отдельно); конфигурация разрослась настолько, что в одном файле уже не держится в голове. И наоборот - не вкладывай модули слишком глубоко: три-четыре уровня вложенности, и отладка переменных превращается в археологию.
Мини-лаба
Руками, на любом тестовом репозитории:
- Создай простой модуль с одним ресурсом и файлом, который он читает через file("${path.module}/...").
- Подключи модуль из git дважды: один раз через ветку без ?ref, второй - с ?ref=тег. Запусти terraform init и посмотри, что попало в lock-файл.
- Опиши security group с inline-правилом, затем попробуй добавить ещё одно правило отдельным ресурсом _rule. Запусти terraform plan и убедись своими глазами, что план дребезжит.
- Переделай на полностью отдельные _rule и убедись, что дребезг ушёл.
- Почему ссылаться на ветку main в source - плохая идея для прода и чем её заменить?
- В чём разница между path.module, path.root и path.cwd, и какой из них брать для файла, лежащего внутри модуля?
- Что произойдёт, если описать inline-правила security group и одновременно навесить отдельные _rule ресурсы?
- Когда есть смысл выделять под-модуль, а когда это лишний уровень косвенности?
Версионируй модули жёстко: ?ref=тег или коммит для git, version с диапазоном для реестра, и коммить .terraform.lock.hcl. Внутри модуля для своих файлов всегда path.module - так модуль остаётся переносимым. Не мешай inline-блоки и отдельные ресурсы, особенно в правилах security group: для переиспользуемых модулей выбирай отдельные ресурсы ради гибкости. self - только внутри блоков ресурса. А под-модули дроби по смыслу, а не по привычке. Тогда твоя инфраструктура как код будет воспроизводимой, а понедельники - спокойными.