Источники и версионирование модулей

Модуль можно подключить из локальной папки, git-репозитория или Terraform Registry. От источника зависит, как фиксировать версию — и от этого напрямую зависит воспроизводимость деплоя.

Незакреплённая версия модуля — это бомба. Сегодня plan чистый, завтра автор выпустил новую версию, и ваш prod внезапно хочет пересоздать половину сети. Пиньте версии в проде всегда.

Аргумент source говорит Terraform, откуда взять модуль. Варианты: локальный путь (./modules/x), git (git::https://...), Terraform Registry (org/name/provider) и другие. Способ закрепления версии зависит от источника.

Источники и пины версий

# локальный -- версии нет, берётся as-is
module "net" { source = "./modules/network" }

# Registry -- версия через отдельный аргумент version
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.5"     # pessimistic: >=5.5.0, <6.0.0
}

# Git -- версия В САМОМ URL через ?ref=тег
module "app" {
  source = "git::https://github.com/org/mod.git?ref=v1.4.0"
}

Ключевая тонкость: аргумент version работает только с Registry. Для git и других источников версия фиксируется внутри source (через ?ref=). Операторы ограничений: = 1.2.0 (точно), ~> 5.5 (любой 5.x от 5.5), >= 2.0 (диапазон).

Как работает под капотом

Terraform берёт доступные версии модуля и выбирает максимальную, удовлетворяющую ограничению. Смоделируем разрешение версии под оператор ~>:

def parse(v):
    return tuple(int(x) for x in v.split("."))

available = ["5.4.0", "5.5.0", "5.5.3", "5.8.1", "6.0.0", "6.1.0"]

def resolve_pessimistic(available, base):
    # ~> 5.5 означает >=5.5.0 и < 6.0.0
    lo = parse(base)
    upper_major = lo[0] + 1
    ok = [v for v in available
          if parse(v) >= lo and parse(v)[0] < upper_major]
    return max(ok, key=parse) if ok else None

print("~> 5.5 выберет:", resolve_pessimistic(available, "5.5"))
print("~> 6.0 выберет:", resolve_pessimistic(available, "6.0"))
# что НЕ подойдёт под ~> 5.5
excluded = [v for v in available if parse(v)[0] >= 6]
print("исключены 6.x:", excluded)

«Попробуй сам ▶» — ~> 5.5 берёт свежайший 5.x (5.8.1), но не пускает 6.0.0: мажорный апгрейд может ломать совместимость.

Частые ошибки

  • version у git-модуля. Аргумент version для git игнорируется — версия только через ?ref=.
  • Незакреплённая ветка. ?ref=main в проде — плавающая цель, plan меняется между запусками.
  • Слишком широкий диапазон в проде. >= 5.0 однажды притянет ломающее обновление.

Best practices

  • В проде — точные пины (= 1.4.0 или ?ref=v1.4.0), апгрейд осознанный и после тестов.
  • В dev можно ~> для управляемой гибкости — ловить минорные обновления заранее.
  • Для приватных модулей используйте private registry или git с тегами; для публичных — Terraform Registry с семвером.

Разбор глубже

Полезно понимать, как Terraform физически достаёт модуль из каждого источника. Для локального пути он просто читает файлы с диска — поэтому изменения в локальном модуле видны сразу, без переинициализации. Для git и Registry модуль скачивается при terraform init в папку .terraform/modules/ и дальше используется оттуда. Это значит, что после правки удалённого модуля или смены пина версии нужно заново выполнить terraform init (иногда с флагом -upgrade), иначе Terraform продолжит работать со старой скачанной копией.

Семантическое версионирование — это контракт между автором и потребителем модуля. Patch-версия (1.2.3) обещает только исправления багов без изменения поведения. Minor (1.3.0) добавляет новые возможности обратносовместимо. Major (2.0.0) предупреждает: что-то ломается, читайте changelog. Поэтому ограничение ~> 1.2 безопасно пускает патчи и миноры, но защищает от мажорного апгрейда, который может потребовать правок. В проде версии пинят жёстко и поднимают осознанно после тестирования, а changelog модуля становится обязательным чтением перед апгрейдом.

Полезно завести командное соглашение о том, откуда вообще берутся модули. Распространённый зрелый подход — внутренний (private) реестр или выделенный git-репозиторий с модулями, где каждый релиз помечен тегом по семверу. Потребители подключают модули по тегам, а не по ветке main, что делает каждый деплой воспроизводимым и защищённым от случайных правок «в апстриме». Публичные модули из Registry удобны для старта и типовых задач, но в серьёзных проектах их часто оборачивают в собственные модули-фасады, чтобы зафиксировать версию, добавить корпоративные политики и не зависеть напрямую от внешнего сопровождающего.

Итог: источник определяет способ пина: Registry — через version, git — через ?ref=. В проде пиньте жёстко ради воспроизводимости. Дальше — как из модулей собирать многослойную архитектуру.

Проверьте себя
1. С каким источником модуля работает аргумент version?
AС любым
BТолько с Terraform Registry
CТолько с git
DТолько с локальным путём
2. Что выберет ограничение ~> 5.5 из версий 5.5.0, 5.8.1, 6.0.0?
A6.0.0
B5.5.0
C5.8.1 — свежайший 5.x, но не 6.0.0
DЛюбую случайно