Несколько сайтов с for_each
Один сайт уже получает общую сеть, образ и собственную страницу. Теперь расширим «Контур» до двух сайтов: home и docs. Для каждого зададим заголовок и внешний порт. Важная цель этого урока — устойчивые адреса экземпляров Terraform. Добавление нового сайта должно объясняться новым ключом, а не случайным сдвигом порядковых номеров.
Операция for_each размножает объявление ресурса по элементам карты или подходящего множества. У каждого экземпляра появляется ключ. В нашей карте sites ключ home обозначает назначение сайта, а объект рядом с ним хранит настройки. Адрес docker_container.web["home"] сохраняет смысл независимо от того, в каком порядке записаны элементы. Это удобная модель для именованных частей инфраструктуры.
Карта с проверяемым контрактом
В variables.tf теперь определён вход типа map(object(...)). Каждый объект обязан содержать числовой порт и строковый заголовок. Тип не проверяет уникальность портов и корректность имени ключа, поэтому добавлена validation. Она сравнивает число уникальных портов с общим числом сайтов и проверяет каждый элемент. Даже если карта читается из внешнего файла, ограничения остаются частью модуля.
Для заголовка сохранён узкий допустимый алфавит предыдущего урока. Это важно: переход к нескольким объектам не должен случайно убрать защиту шаблона от вставки разметки. Ключи также ограничены, поскольку попадут в имя контейнера. Значения по умолчанию создают home на 8088 и docs на 8089. Разные входные файлы могут изменить числа, но не отменяют необходимость уникальности в пределах одного daemon.
Полный файл variables.tf для этого состояния:
variable "prefix" {
type = string
default = "kontur"
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,24}$", var.prefix))
error_message = "Use a lowercase prefix, 2–25 characters."
}
}
variable "sites" {
type = map(object({ port = number, title = string }))
default = {
home = { port = 8088, title = "Kontur home" }
docs = { port = 8089, title = "Kontur documentation" }
}
validation {
condition = length(var.sites) > 0 && length(distinct([for site in values(var.sites) : site.port])) == length(var.sites) && alltrue([
for key, site in var.sites : can(regex("^[a-z][a-z0-9-]{0,20}$", key)) && site.port >= 1024 && site.port <= 65535 && floor(site.port) == site.port && can(regex("^[A-Za-z0-9 ._-]{1,80}$", site.title))
])
error_message = "Site keys and titles must be safe; ports must be unique integers from 1024 to 65535."
}
}
Полный файл main.tf для этого состояния:
resource "docker_container" "web" {
for_each = var.sites
name = "${var.prefix}-${each.key}"
image = docker_image.web.image_id
networks_advanced { name = docker_network.web.id }
ports {
internal = 80
external = each.value.port
ip = "127.0.0.1"
}
upload {
file = "/usr/share/nginx/html/index.html"
content = templatefile("${path.module}/site/index.html.tftpl", { title = each.value.title })
}
}
Полный файл migration-single-to-map.tf.example для этого состояния:
moved {
from = docker_container.web
to = docker_container.web["home"]
}
Внутри ресурса each.key возвращает имя сайта, а each.value — его объект настроек. Поэтому имя становится ${var.prefix}-${each.key}, внешний порт берётся из each.value.port, заголовок из each.value.title. Ссылка на образ остаётся общей; сеть тоже одна. Получившиеся контейнеры независимы как ресурсы, но используют одинаковые зависимости. Это выражено в конфигурации без дублирования двух почти одинаковых блоков.
Адрес существующего ресурса
Если читатель начал лабораторию с пустого состояния этого урока, карты достаточно. Если же он самостоятельно создал предыдущий одиночный сайт, его адрес был docker_container.web без ключа. Переход к for_each меняет адрес. Чтобы связать существующий контейнер с home, предусмотрен файл migration-single-to-map.tf.example. Его надо осознанно переименовать в .tf в той же рабочей копии, где существует прежнее состояние, и прочитать новый план.
Блок moved переносит связь адресов в модели Terraform. Он не переименовывает контейнер в Docker сам по себе. Если новые аргументы контейнера отличаются от прежних, перенос не уберёт эти различия. Например, имя прежнего контейнера было ${var.prefix}-web, а новое выражение даёт ${var.prefix}-home. План может предложить замену из-за этого изменения. Для настоящей миграции либо сохраняют прежнее имя на переходный период, либо принимают остановку как отдельное решение.
Это ограничение объясняет, почему нельзя обещать «без пересоздания» лишь на основании наличия moved. Перенос адреса устраняет ложное представление о двух разных ресурсах; реальные изменения аргументов остаются. Учебные снимки исходников не содержат state, поэтому копирование lesson-06 не переносит ранее созданный контейнер. Существующую лабораторию продолжают в прежней рабочей папке с понятной историей состояния.
Добавление и удаление ключа
Представим третий сайт status на 8091. Добавление элемента карты вводит новый адрес ресурса, не меняя ключи home и docs. Смена порядка строк не должна создавать новый смысл. Для сравнения, индексируемый список с count часто связывает ресурсы с позициями. Удаление первого элемента такого списка способно изменить назначение всех последующих индексов. Для именованных сайтов карта явно показывает более устойчивую идентичность.
Однако устойчивость ключа не означает бессрочное существование объекта. Если удалить docs из карты, конфигурация перестанет объявлять этот экземпляр. Нормальный план предложит удалить соответствующий ресурс. Если переименовать ключ в documentation, инструмент увидит новый адрес, пока не описан перенос. Изменение терминологии таким образом становится инфраструктурной операцией. Оно требует той же внимательности, что и выделение модуля.
Выход urls теперь является картой адресов. В ней ключ совпадает с ключом сайта, а значение строится из порта. Потребитель не должен предполагать, что первый элемент всегда главная страница. Он выбирает home по имени. Такой контракт легче расширять: новые сайты добавляют ключи, а существующие потребители могут продолжать использовать прежнее назначение. Если ключ удалён, это уже изменение публичного интерфейса модуля.
Разберём ошибку дублирования: оба элемента получили 8088. Типизация примет два числа, но наша validation отвергнет конфигурацию раньше передачи к Docker. Если аналогичный порт занят сторонним проектом, эта проверка не узнает о конфликте. Она видит только карту «Контура». Поэтому внутренний контракт и внешняя доступность снова остаются разными уровнями проверки. Полезно знать, какой уровень дал отказ, прежде чем менять настройки.
После этого урока «Контур» умеет описывать несколько именованных сайтов из одного набора данных. Их адреса понятны, порты проверяются, общая сеть и образ не копируются. Следующий урок вынесет повторяемую конфигурацию контейнера в локальный модуль. Карта сохранится в корне, но адрес ресурса дополнится путём модуля; это даст второй конкретный пример того, как поддерживать инфраструктурную идентичность при реорганизации исходников.
Ключ как часть долгосрочного договора
Выберите ключи по назначению, которое переживёт небольшие изменения дизайна страницы. Ключ purple-page связывает идентичность с цветом и провоцирует ненужное переименование при редизайне. Ключ docs описывает роль страницы устойчивее. Это не универсальный закон именования, но полезный способ уменьшить случайные миграции ресурсов. Человеческое название в HTML может меняться независимо от ключа.
В выходной карте также полезнее выбирать значение по ключу, чем превращать карту в список и брать первый элемент. Внешний потребитель тогда ясно говорит, какой сайт ему нужен. При добавлении status он продолжает использовать home; при удалении home получает конкретное нарушение контракта. Такой сбой легче объяснить, чем незаметную смену адреса из-за нового порядка элементов. Устойчивость инфраструктуры начинается с явной идентичности данных.
При чтении карты назовите все три уровня: ключ сайта, имя контейнера и адрес ресурса. Ключ участвует в Terraform-идентичности, имя существует в Docker, а порт задаёт пользовательский вход. Изменение одного уровня не следует автоматически считать переименованием остальных. Такой разбор помогает выбрать нужный перенос без случайного удаления.
Оглавление серии, общая подборка и полный архив исходников. Для этого урока выберите lesson-06.
Первичные источники: Версии Terraform, Учебник Terraform и Docker, Схема Docker container 3.6.2, Схема Docker image 3.6.2, Адреса и moved, Локальный backend, Import, Refresh-only plan.
Редакционный статус: локальный черновик. Учебные команды, приложение и проверки не запускались; описанные результаты ожидаемые. Профиль версий закреплён, разрешённый lockfile не создавался.