Files
CloudRouterAdvanced/docs/changes/2026-09-09-mvm-s3-external-networks-plan.md
T

189 lines
16 KiB
Markdown
Raw Normal View History

# Внешние сети с фиксированными 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 на роутер:
```hcl
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"` в инстансе, как сейчас, вместо дублирования ресурсов:
```hcl
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`) и обобщить:
```hcl
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 из диаграммы не секрет):
```hcl
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 ролями → план создаёт 0 `router_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` — офлайн, без обращения к облаку (как в прошлых изменениях).
## Проверка
1. `terraform fmt -check` / `terraform validate` в `terraform/`.
2. `venv/bin/pytest terraform/tests -v` — все тесты (старые + новые) зелёные.
3. `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 создаваемых приватных сетей/подсетей.
4. Убедиться, что `terraform workspace select default` (PROD) + прежний `-var-file`/creds-файл по-прежнему даёт **пустой diff** (0 changes) — подтверждает, что PROD не задет.