Перейти к содержанию

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

Карта сайтов уже даёт два устойчивых экземпляра контейнера. Теперь вынесем механизм одного сайта в локальный модуль modules/site. Корень «Контура» сохранит решение о том, какие сайты нужны и какие общие ресурсы они используют. Дочерний модуль будет отвечать за создание одного контейнера из переданных данных. Это разделение помогает обсуждать интерфейс инфраструктуры, а не только уменьшает размер файла.

Модуль Terraform — каталог конфигурационных файлов со своими входами, ресурсами и выходами. Корневой модуль является особым случаем того же понятия. Модуль не равен отдельному состоянию: дочерние ресурсы входят в состояние корня, который его вызывает. Если вынести файлы в подкаталог без блока module, Terraform не начнёт автоматически читать их как часть корневой конфигурации. Связь должна быть объявлена явно.

Граница одного сайта

Наш модуль принимает имя, идентификатор образа, идентификатор сети, внешний порт и уже построенный HTML. Он не решает, какой образ выбрать, и не создаёт отдельную сеть. Эти решения остаются в корне, где ими пользуются оба сайта. Такой контракт подходит нынешней задаче: home и docs отличаются настройками страницы, но имеют общую среду. Если позже понадобится совершенно иной веб-сервер, интерфейс следует пересмотреть осознанно.

HTML передаётся строкой, а не скрыто читается из корневого каталога. Это позволяет модулю знать только свои входы. Он не обращается через относительные пути к файлам родителя, положение которых может измениться. Шаблон строится в корне с path.module, и готовый результат передаётся каждому экземпляру. При реорганизации директорий такая граница удерживает зависимость от файлов явной.

Полный файл modules/site/main.tf для этого состояния:

terraform {
  required_providers {
    docker = { source = "kreuzwerker/docker", version = "= 3.6.2" }
  }
}
variable "name" { type = string }
variable "image_id" { type = string }
variable "network_id" { type = string }
variable "port" { type = number }
variable "html" { type = string }
resource "docker_container" "web" {
  name  = var.name
  image = var.image_id
  networks_advanced { name = var.network_id }
  ports {
    internal = 80
    external = var.port
    ip       = "127.0.0.1"
  }
  upload {
    file    = "/usr/share/nginx/html/index.html"
    content = var.html
  }
}
output "container_id" { value = docker_container.web.id }
output "url" { value = "http://127.0.0.1:${var.port}" }

Полный файл main.tf для этого состояния:

module "site" {
  source     = "./modules/site"
  for_each   = var.sites
  name       = "${var.prefix}-${each.key}"
  image_id   = docker_image.web.image_id
  network_id = docker_network.web.id
  port       = each.value.port
  html       = templatefile("${path.module}/site/index.html.tftpl", { title = each.value.title })
}

Полный файл moved.tf для этого состояния:

moved {
  from = docker_container.web["home"]
  to   = module.site["home"].docker_container.web
}
moved {
  from = docker_container.web["docs"]
  to   = module.site["docs"].docker_container.web
}

Ресурс внутри модуля называется docker_container.web. В корне есть блок module "site" с тем же for_each = var.sites. Полный адрес экземпляра теперь содержит оба уровня: module.site["home"].docker_container.web. Ключ home по-прежнему обозначает назначение, а путь module.site показывает место объявления механизма. Возвращаемый модулем url доступен корню как module.site["home"].url.

Передача провайдера и требований

В дочернем модуле повторён required_providers с адресом Docker-пакета. Это декларация зависимости модуля, а не повторное подключение к daemon. Конфигурация провайдера остаётся в корне и для нашего простого случая наследуется дочерним модулем. Мы не создаём внутри модуля собственный блок provider, который связывал бы его с особой средой. Такое смешивание мешает повторному использованию и может ограничивать допустимые способы вызова модуля.

Точная учебная версия провайдера согласована на обоих уровнях. В настоящей библиотеке модулей часто выбирают подходящие ограничения совместимости, а точное разрешение фиксирует корневой lockfile. Здесь точный профиль делает исходный разбор однозначным. Если обновить только одно требование, пересечение версий может стать пустым. Это ошибка зависимостей, а не проблема сетевого подключения или данных контейнера.

Выходы модуля сведены к адресу страницы и идентификатору контейнера. Не стоит возвращать весь ресурс «на всякий случай»: потребители начнут зависеть от случайных деталей схемы провайдера. Узкий выход легче поддерживать при изменениях реализации. Корневой outputs.tf собирает карту адресов из модулей и сохраняет прежние ключи home и docs. Для человека результат остаётся тем же, хотя внутренняя организация изменилась.

Перенос адресов существующих объектов

При переходе от предыдущей версии конфигурации изменился адрес контейнера. В moved.tf записано соответствие между docker_container.web["home"] и новым адресом внутри модуля; аналогичное соответствие задано для docs. Эти блоки служат истории реорганизации. При пустом состоянии переносить нечего, но при существующем состоянии они позволяют Terraform сохранить связь с настоящим объектом. Исходники остаются пригодными для обоих случаев.

Передача в модуль тех же аргументов необходима, если мы ожидаем только реорганизацию. moved не подавляет реальные изменения. Если одновременно изменить образ, порт или имя, план всё равно может предложить замену. Поэтому удобный обзор начинается с отдельного вопроса: совпадают ли значения до и после переноса? Ответ вычисляют по конфигурации, а затем подтверждают будущим реальным планом, не по сходству названий файлов.

У нас перечислены два известных ключа. Если читатель раньше добавил третий сайт status, он должен добавить перенос и для него. Нельзя считать демонстрационный moved.tf полным описанием неизвестного состояния. Для более сложной миграции планируют соответствия всех управляемых адресов и сохраняют отдельную копию состояния. Команда ручного перемещения state существует, но в этом уроке предпочтён читаемый декларативный след, который коллега сможет увидеть в исходниках.

Что модуль не обещает

Выделение модуля не повышает само по себе доступность сервера и не создаёт автоматическое восстановление данных. У контейнера всё ещё обычный жизненный цикл провайдера. Два экземпляра модуля не являются отказоустойчивым кластером: это два разных сайта с разными внешними портами. Называть такую конфигурацию балансировкой нагрузки было бы неверно. Структурное повторное использование кода надо отделять от эксплуатационных свойств инфраструктуры.

Представим новое поле memory. Добавить его можно как вход модуля с понятным значением по умолчанию или как внутреннюю политику. Выбор зависит от того, должен ли потребитель управлять лимитом. Если такой вопрос не задан, большой список «универсальных» переменных только переносит сложность из ресурса в интерфейс. Наш модуль сначала выражает реально нужные варианты, а ограничения эксплуатации будут введены отдельно в одиннадцатом уроке.

Теперь корень выбирает сайты и общие зависимости, а локальный модуль реализует один сайт. Адреса ресурсов изменились с явным описанием переноса. Этот результат можно прочитать без обращения к Docker. В следующем уроке мы разберём состояние и backend: границы хранения важнее количества модулей, когда появляются несколько независимых рабочих окружений.

Локальный модуль и его версия

Источник ./modules/site означает часть того же репозитория. Он не содержит автоматически номер опубликованного модуля из реестра. История Git фиксирует его изменения вместе с корнем. Если позднее модуль станет общей библиотекой нескольких репозиториев, придётся отдельно выбрать способ доставки и версионирования. Нельзя просто объявить текущий подкаталог независимой стабильной библиотекой без правил обновления потребителей.

Сначала сохраните малую понятную границу: корень решает, какие сайты нужны, модуль создаёт один контейнер. Потом сравните реальные новые потребности. Если два потребителя постоянно требуют взаимоисключающие настройки, возможно, им нужны разные модули, а не бесконечный набор флагов. Эта проверка удерживает повторное использование полезным. Абстракция должна помогать объяснить инфраструктуру, иначе уменьшение строк превращается в увеличение скрытой сложности.

Оглавление серии, общая подборка и полный архив исходников. Для этого урока выберите lesson-07.

Первичные источники: Версии Terraform, Учебник Terraform и Docker, Схема Docker container 3.6.2, Схема Docker image 3.6.2, Адреса и moved, Локальный backend, Import, Refresh-only plan.

Редакционный статус: локальный черновик. Учебные команды, приложение и проверки не запускались; описанные результаты ожидаемые. Профиль версий закреплён, разрешённый lockfile не создавался.