mvm-s3 is a separate VK Cloud project whose admin pre-created two private networks/subnets with a known IP per router. Unify project-managed (private_network_cidrs, IPAM-assigned) and externally-owned (router_networks, fixed-IP) private interfaces into one local.router_interfaces so both share the existing port/dynamic-network mechanism instead of duplicating it. Switch from implicit *.auto.tfvars loading to explicit -var-file per environment (now two share this terraform/ directory) plus a dedicated Terraform workspace for mvm-s3, so PROD's state and credentials are never touched by mvm-s3 applies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHfG9FgpMrGdrvC1QUewTw
16 KiB
Внешние сети с фиксированными IP для окружения mvm-s3
Контекст
Текущий Terraform (terraform/) разворачивает 4 маршрутизатора (router1..router4) в PROD-проекте VK Cloud: у каждого 1 WAN-интерфейс (data.vkcs_networking_network.extnet) и 2 приватных интерфейса в сетях, которые Terraform сам создаёт (vkcs_networking_network/vkcs_networking_subnet по var.private_network_cidrs), с IP, которые назначает Neutron IPAM автоматически (адрес заранее не известен и не фиксируется — см. комментарий про коллизию с служебным портом network:dns в main.tf).
Появилось новое окружение — mvm-s3, другой VK Cloud проект, недоступный отсюда напрямую. Администратор этого проекта уже создал там (проверено через OpenStack CLI) две приватные сети/подсети и заранее расписал, какой IP получит каждый из 4 маршрутизаторов в каждой из них (диаграмма пользователя):
| Роль | UUID сети | UUID подсети | CIDR | router1 | router2 | router3 | router4 |
|---|---|---|---|---|---|---|---|
| primary (основной канал связи) | 25532efe-2931-4666-a090-0da3a6f18224 |
1fa5357c-1f11-4d20-89f4-b27a4fb56e6e |
172.16.252.8/29 |
.11 |
.12 |
.13 |
.14 |
| backup (резервный канал связи) | 2b4cc25f-55a1-4d36-a3a2-5b16414af31a |
5eacbc86-e15d-4c49-8704-547a719acc23 |
172.16.252.0/29 |
.3 |
.4 |
.5 |
.6 |
Третий (External/WAN) интерфейс у роутеров тоже есть, но он вне рамок этой диаграммы и этой задачи — переиспользуется существующий механизм (data.vkcs_networking_network.extnet), без изменений.
Согласовано с пользователем:
- Для
mvm-s3эти 2 внешние сети заменяютprivate_network_cidrs-механизм (никаких project-managed приватных сетей в этом окружении не создаётся) — но сам механизм из кода не удаляется, поскольку PROD (10.90.x, уже развёрнут и работает) продолжает от него зависеть в своём отдельном состоянии. mvm-s3— отдельный Terraform workspace (terraform workspace new mvm-s3), отдельный state. PROD (default/текущий workspace) не трогается.- Сейчас
terraform.tfvarsиprod.auto.tfvars(creds) авто-загружаются Terraform'ом всегда, без явного-var-file. С двумя окружениями это небезопасно — переходим на явные-var-fileдля всех переменных, отличающихся между окружениями, для обоих окружений симметрично (в т.ч. переименовываемprod.auto.tfvars→prod.secrets.tfvars, чтобы он больше не подхватывался неявно и не «протекал» в apply дляmvm-s3). - У меня нет доступа к
mvm-s3проекту —terraform plan/applyтам должен будет выполнить пользователь самостоятельно. Проверка с моей стороны — только офлайн (terraform validate, pytest-сьют,terraform planпри наличии тестовых кредов/моков).
Открытый вопрос к пользователю (нужно закрыть до/во время реализации): реальные auth_url/username/password/project_id/region/user_domain_name и ssh_key_name/ssh_public_key для mvm-s3 — я их не знаю и не должен придумывать. Файл с ними создаётся по аналогии с prod.auto.tfvars.example — как шаблон с плейсхолдерами, который пользователь заполнит сам.
Реализация
1. terraform/variables.tf
private_network_cidrs: сделать опциональной (default = []), убрать валидацию «минимум 1 элемент» (дляmvm-s3список должен быть пустым — 0 project-managed приватных сетей). Остальные валидации (валидный CIDR, уникальность) остаются, тривиально проходят на пустом списке.- Добавить новую переменную
router_networks— карта заранее существующих внешних сетей с фиксированным IP на роутер:
variable "router_networks" {
description = "Заранее существующие сети (в другом VK Cloud проекте, только по UUID, не управляются этим Terraform), в которые каждый роутер получает интерфейс с фиксированным IP. Ключ карты — имя роли/интерфейса (например \"primary\"/\"backup\"); ip_addresses[i] соответствует router(i+1)."
type = map(object({
network_id = string
subnet_id = string
cidr = string
ip_addresses = list(string)
}))
default = {}
validation {
condition = alltrue([for r in var.router_networks : can(cidrhost(r.cidr, 0))])
error_message = "router_networks[*].cidr must be a valid IPv4 CIDR."
}
validation {
condition = alltrue([
for r in var.router_networks : alltrue([
for ip in r.ip_addresses : can(cidrhost("${ip}/32", 0))
])
])
error_message = "router_networks[*].ip_addresses entries must be valid IPv4 addresses."
}
validation {
condition = alltrue([
for r in var.router_networks : alltrue([
for ip in r.ip_addresses : cidrhost("${ip}/${split("/", r.cidr)[1]}", 0) == cidrhost(r.cidr, 0)
])
])
error_message = "Every router_networks[*].ip_addresses entry must fall inside that role's own cidr."
}
}
Проверку length(ip_addresses) >= var.router_count вынести в lifecycle.precondition порта (см. ниже) — она версия-независима и даёт понятную ошибку на конкретном роутере/роли, а не общую.
2. terraform/main.tf — объединить оба механизма в один набор ресурсов
Ключевая идея переиспользования: и project-managed сети (CIDR), и внешние сети (UUID) в итоге дают роутеру «роль → {network_id, subnet_id, cidr, опциональный список фиксированных IP}» — собрать это в один local, и дальше вести один vkcs_networking_port + один dynamic "network" в инстансе, как сейчас, вместо дублирования ресурсов:
locals {
cidr_roles = { for idx, cidr in var.private_network_cidrs : "priv${idx + 1}" => cidr }
router_interfaces = merge(
{
for role, cidr in local.cidr_roles : role => {
network_id = vkcs_networking_network.router_priv_net[role].id
subnet_id = vkcs_networking_subnet.router_priv_subnet[role].id
cidr = cidr
fixed_ips = null # IPAM сам назначает адрес, как сейчас
}
},
{
for role, net in var.router_networks : role => {
network_id = net.network_id
subnet_id = net.subnet_id
cidr = net.cidr
fixed_ips = net.ip_addresses # индекс = count.index
}
}
)
router_interface_roles = keys(local.router_interfaces)
}
vkcs_networking_network.router_priv_net / vkcs_networking_subnet.router_priv_subnet — без изменений (по local.cidr_roles, для mvm-s3 карта пустая → 0 сетей/подсетей создаётся).
vkcs_networking_port.router_priv_port — переименовать по смыслу (например router_iface_port) и обобщить:
resource "vkcs_networking_port" "router_iface_port" {
for_each = {
for pair in setproduct(range(var.router_count), local.router_interface_roles) :
"router${pair[0] + 1}-${pair[1]}" => pair
}
name = "router-${each.key}-port"
network_id = local.router_interfaces[each.value[1]].network_id
admin_state_up = true
port_security_enabled = false
full_security_groups_control = true
security_group_ids = []
sdn = "sprut" # проверить на первом plan/apply в mvm-s3 — актуально ли SDN "sprut" для чужой сети
fixed_ip {
subnet_id = local.router_interfaces[each.value[1]].subnet_id
ip_address = local.router_interfaces[each.value[1]].fixed_ips == null ? null : local.router_interfaces[each.value[1]].fixed_ips[each.value[0]]
}
lifecycle {
precondition {
condition = (
local.router_interfaces[each.value[1]].fixed_ips == null ||
each.value[0] < length(local.router_interfaces[each.value[1]].fixed_ips)
)
error_message = "router_networks[\"${each.value[1]}\"].ip_addresses must have at least var.router_count entries."
}
}
}
vkcs_compute_instance.router: заменить local.private_roles → local.router_interface_roles в двух местах (user_data шаблон и dynamic "network"), ссылку на порт — на router_iface_port. Больше никаких изменений в ресурсе инстанса не требуется.
3. terraform/scripts/network-init.sh.tpl
Без изменений. Скрипт уже определяет приватные интерфейсы по членству живого IP в ожидаемом CIDR (ip_in_cidr), а не по имени роли или способу назначения адреса — механизм одинаково работает и для IPAM-адресов, и для явно заданных статических.
4. Файлы окружения mvm-s3
terraform/mvm-s3.tfvars(новый, коммитится — по аналогии с тем, чтоrouter_count/ssh_key_name/private_network_cidrsдля PROD сейчас открыто лежат вterraform.tfvars; UUID сетей и IP из диаграммы не секрет):router_count = 4 ssh_key_name = "<уточнить у пользователя>" private_network_cidrs = [] router_networks = { primary = { network_id = "25532efe-2931-4666-a090-0da3a6f18224" subnet_id = "1fa5357c-1f11-4d20-89f4-b27a4fb56e6e" cidr = "172.16.252.8/29" ip_addresses = ["172.16.252.11", "172.16.252.12", "172.16.252.13", "172.16.252.14"] } backup = { network_id = "2b4cc25f-55a1-4d36-a3a2-5b16414af31a" subnet_id = "5eacbc86-e15d-4c49-8704-547a719acc23" cidr = "172.16.252.0/29" ip_addresses = ["172.16.252.3", "172.16.252.4", "172.16.252.5", "172.16.252.6"] } }terraform/mvm-s3.secrets.tfvars.example(новый, коммитится) — шаблон creds дляmvm-s3, зеркалоprod.auto.tfvars.example(auth_url,username,password,project_id,region,user_domain_name,ssh_public_key).terraform/mvm-s3.secrets.tfvars— пользователь создаёт сам из примера (не коммитится, я его не создаю и не заполняю значениями).- Переименовать
terraform/prod.auto.tfvars→terraform/prod.secrets.tfvars(иprod.auto.tfvars.example→prod.secrets.tfvars.example), чтобы оба окружения выбирались строго явными-var-file, без риска, что один набор creds «протечёт» в apply другого окружения через авто-загрузку. Это переименование реального файла с секретами делает пользователь сам (я не имею доступа/не должен его трогать) — в плане это шаг-инструкция, не мой git-коммит. .gitignore: добавить*.secrets.tfvars/*.secrets.tfvars.json; старый паттерн*.auto.tfvarsоставить (безвреден, ничего под него больше не подпадает).
5. Документация (по правилам проекта — .claude/CLAUDE.md)
docs/changes/2026-09-09-mvm-s3-external-networks-plan.md— копия согласованного плана.docs/changes/2026-09-09-mvm-s3-external-networks-summary.md— по завершении, с фактическимиterraform plan/apply-результатами (которые предоставит пользователь, т.к. я не могу их выполнить).README.md— дополнить разделом проmvm-s3окружение: workspace,-var-file, схема сети (аналогично таблице вDEPLOYMENT_SUMMARY.md).docs/QUICKSTART.md— обновить: явные-var-fileвместо неявной авто-загрузки, инструкцияterraform workspace new/select, где взятьmvm-s3.secrets.tfvars.
6. Тесты (terraform/tests/test_terraform_delivery.py)
Изучить существующие проверки для private_network_cidrs (в частности тест на «минимум 1 CIDR», который перестанет быть валидным) и дописать по аналогии:
private_network_cidrs = []+router_networksс 2 ролями → план создаёт 0router_priv_net/subnet, 8 портов (4 роутера × 2 роли), каждый порт с явнымfixed_ip.ip_addressиз диаграммы.- Смешанный кейс (не боевой, но для полноты обобщённого кода) —
private_network_cidrsнепустой Иrouter_networksнепустой одновременно не ломается (роли не пересекаются). router_networksсip_addressesкорочеrouter_count→terraform planпадает на precondition с понятной ошибкой.- IP вне CIDR роли → падает на validation.
Прогнать venv/bin/pytest terraform/tests -v — офлайн, без обращения к облаку (как в прошлых изменениях).
Проверка
terraform fmt -check/terraform validateвterraform/.venv/bin/pytest terraform/tests -v— все тесты (старые + новые) зелёные.terraform workspace new mvm-s3(если ещё не создан) →terraform plan -var-file=terraform.tfvars -var-file=mvm-s3.tfvars -var-file=mvm-s3.secrets.tfvars— выполняет пользователь (нет доступа к проектуmvm-s3отсюда); ожидаемо: 4 роутера, по 2 порта с точными IP из диаграммы, 0 создаваемых приватных сетей/подсетей.- Убедиться, что
terraform workspace select default(PROD) + прежний-var-file/creds-файл по-прежнему даёт пустой diff (0 changes) — подтверждает, что PROD не задет.