Версионирование модулей и подводные камни

Рейтинг: 72.2% · 10 голосов
Подробный курс по Terraform и OpenTofu на примерах Yandex Cloud: ресурсы и провайдеры, состояние и бэкенды, модули, циклы и условия HCL, секреты и Lockbox, тестирование, CI/CD и командная работа. С актуализацией на 2026 (Terraform 1.3-1.10, OpenTofu).
Ответить
Аватара пользователя
Vlad_DevOps
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Версионирование модулей и подводные камни

Сообщение Vlad_DevOps »

Оглавление курса (47)
  1. Что такое инфраструктура как код и зачем она нужна
  2. Категории инструментов IaC: чем отличаются Ansible, Packer, Kubernetes и Terraform
  3. Чем Terraform отличается от Ansible, Pulumi и CloudFormation
  4. Как работает Terraform под капотом и кто такой OpenTofu
  5. Установка Terraform и OpenTofu, подготовка Yandex Cloud
  6. Первый ресурс: разворачиваем виртуальную машину в Yandex Cloud
  7. Веб-сервер на ВМ: метаданные, user-data и группы безопасности
  8. Переменные ввода и выходные значения в Terraform
  9. Кластер ВМ: группа экземпляров Yandex Compute Instance Group
  10. Балансировщик нагрузки: Application Load Balancer в Yandex Cloud
  11. Состояние Terraform: что такое tfstate и чем опасен локальный файл
  12. Удалённое хранилище состояния: бэкенд на Yandex Object Storage
  13. Изоляция состояния: рабочие области против раскладки по папкам
  14. Источники данных и terraform_remote_state: связываем компоненты
  15. Модули Terraform: переиспользуем инфраструктуру
  16. Входные, локальные и выходные переменные модуля
  17. Версионирование модулей и подводные камни (вы здесь)
  18. Циклы в Terraform: параметр count
  19. Циклы в Terraform: for_each по множествам и картам
  20. Выражения for и строковые директивы
  21. Условная логика: count-трюк, for_each и директива if
  22. Встроенные функции, типы и выражения HCL
  23. Развёртывание без простоя: create_before_destroy
  24. Подводные камни Terraform и рефакторинг с блоком moved
  25. Управление секретами: основы и типы
  26. Инструменты управления секретами: Yandex Lockbox, Vault, SOPS
  27. Аутентификация провайдера и секреты: сервисные аккаунты и OIDC
  28. Секреты в ресурсах и состоянии, ephemeral-значения
  29. Провайдеры Terraform: установка, версии, required_providers
  30. Несколько копий одного провайдера: alias и мультизона
  31. Несколько провайдеров и аккаунтов: configuration_aliases
  32. Terraform, Docker и Kubernetes: Managed Kubernetes в Yandex Cloud
  33. Код Terraform промышленного уровня: почему это долго
  34. Мелкие компонуемые модули вместо монолита
  35. Тестируемые модули и проверки: validation, precondition, postcondition
  36. Версионирование, публикация модулей и Terragrunt
  37. Ручное тестирование Terraform и очистка ресурсов
  38. Автоматические тесты на Terratest: модульные тесты
  39. Интеграционные и сквозные тесты, пирамида тестирования
  40. Нативный terraform test и статический анализ
  41. Внедрение Terraform в команде: процессы и культура
  42. Конвейер развёртывания прикладного кода
  43. Конвейер инфраструктурного кода и золотое правило Terraform
  44. Стиль, ревью и CI/CD-инструменты: Atlantis, TACOS, policy as code
  45. Что нового в Terraform 1.3-1.10: моды, импорт, провайдер-функции, Stacks
  46. OpenTofu в 2026: чем живёт форк и как мигрировать
  47. Сертификация HashiCorp Terraform Associate и карьера
Представь ситуацию. В пятницу вечером твоя инфраструктура как код разворачивается идеально: terraform plan apply отрабатывает чисто, все ресурсы на месте. А в понедельник утром тот же самый код внезапно пытается пересоздать половину кластера, хотя ты ничего не трогал. Что случилось? Кто-то в общем модуле, на который ты ссылаешься через ветку main, влил коммит и поменял дефолты. Твой terraform state поплыл, а ты узнал об этом по алертам в проде.

Это классическая боль модульной разработки в 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"
}
Запись ~> 5.0 означает "бери любую 5.x, но не 6.0". Это так называемый pessimistic constraint - разрешаем минорные обновления, блокируем мажорные, где по семверу ждём ломающих изменений. Можно жёстко: version = "5.1.2". Для реестра это рабочий компромисс, потому что авторы держат семвер. Для git через ?ref диапазонов нет - там либо тег, либо коммит, и это даже честнее.

В Yandex Cloud, кстати, для самого провайдера версия фиксируется в блоке required_providers, и это та же логика - прибивай явно:

Код: Выделить всё

terraform {
  required_providers {
    yandex = {
      source  = "yandex-cloud/yandex"
      version = ">= 0.13"
    }
  }
}
После init версии замораживаются в .terraform.lock.hcl. Этот файл - твой друг, коммить его в репозиторий. Он гарантирует, что у тебя и у коллеги одинаковые провайдеры до хэша.

Грабли с путями к файлам в модуле

Теперь к подводным камням, об которые бьются почти все. Внутри модуля ты иногда читаешь файлы - шаблон cloud-init, скрипт, ключ. И тут важно не перепутать path-переменные:
  • path.module - папка, где лежит сам файл модуля. Это то, что нужно в 99% случаев, когда файл - часть модуля.
  • path.root - корневая папка конфигурации, та, откуда ты запускаешь terraform. Зависит от того, кто и откуда вызвал модуль.
  • path.cwd - текущая рабочая директория процесса. Ещё более капризная, меняется от способа запуска.
Ошибка новичка - написать внутри модуля file("scripts/init.sh") или file("${path.root}/scripts/init.sh"). В первом случае путь относительный и зависит от cwd, во втором - от того, где сидит корневая конфигурация. Модуль перестаёт быть самодостаточным: его нельзя просто переиспользовать в другом проекте, файл-то ищется не там. Правильно - всегда path.module для собственных файлов модуля:

Код: Выделить всё

resource "yandex_compute_instance" "web" {
  name = "web-1"
  # ... другие обязательные блоки ...

  metadata = {
    user-data = file("${path.module}/files/cloud-init.yaml")
  }
}
Запомни так: файл принадлежит модулю - бери path.module. Файл приходит снаружи от пользователя модуля - принимай его как переменную, а не лезь в path.root.

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-правила и параллельно навесил отдельные _rule, Terraform будет видеть конфликт: при каждом plan он считает, что inline-набор "полный", и попытается снести правила, добавленные отдельно. Получишь вечный дребезг плана и драки за состояние.

Почему это касается модулей? 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 - только внутри блоков ресурса. А под-модули дроби по смыслу, а не по привычке. Тогда твоя инфраструктура как код будет воспроизводимой, а понедельники - спокойными.
👍3 ❤️4 🔥3 😄 🤔2
Аватара пользователя
rabbitgeek
Сообщения: 1
Зарегистрирован: 14 май 2026, 09:32

Re: Версионирование модулей и подводные камни

Сообщение rabbitgeek »

Блин, вот про ветку main прям про нас. У нас именно так в понедельник кластер и переехал, никто понять не мог почему. Теперь буду везде ?ref=тег ставить, спасибо.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
rewohs
Сообщения: 1
Зарегистрирован: 12 май 2026, 04:23

Re: Версионирование модулей и подводные камни

Сообщение rewohs »

А подскажите, lock-файл точно надо коммитить? Я думал он локальный как .terraform папка и добавил его в gitignore. Получается зря?
👍1 ❤️ 🔥1 😄 🤔1
Ответить
← Предыдущая глава
Входные, локальные и выходные переменные модуля
Следующая глава →
Циклы в Terraform: параметр count

Все главы курса «Terraform: инфраструктура как код на Yandex Cloud»

Поделиться темой: ✈ Telegram VK
Похожие запросы: как вынести terraform в модуль и переиспользовать инфраструктурувходные и выходные переменные модуля terraform как передатькак создать роль ansible с нуля и структура каталогов

Вернуться в «Terraform: инфраструктура как код на Yandex Cloud»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость