Add router_networks: fixed-IP router interfaces into external networks (mvm-s3)
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
This commit is contained in:
1 parent
e7235d96c9
commit
b2d87c19d8
13 files changed
+740
-132
No files matched your search
@@ -0,0 +1,189 @@
|
||||
# Внешние сети с фиксированными 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 не задет.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Summary: внешние сети с фиксированными IP для окружения mvm-s3
|
||||
|
||||
План: [2026-09-09-mvm-s3-external-networks-plan.md](2026-09-09-mvm-s3-external-networks-plan.md)
|
||||
|
||||
## Что сделано
|
||||
|
||||
### `terraform/variables.tf`
|
||||
- `private_network_cidrs` стала опциональной (`default = []`), убрана валидация «минимум 1 CIDR» — деплой, использующий только `router_networks` (как `mvm-s3`), не обязан её задавать.
|
||||
- Добавлена `router_networks` (`map(object({network_id, subnet_id, cidr, ip_addresses}))`, `default = {}`) — заранее существующие сети (обычно в другом VK Cloud проекте, только по UUID), в которые каждый роутер получает интерфейс с заранее известным IP. Валидации: корректный CIDR, корректные IPv4-адреса, каждый адрес — внутри своего CIDR.
|
||||
|
||||
### `terraform/main.tf`
|
||||
- Оба механизма (project-managed сети по `private_network_cidrs` и внешние по `router_networks`) объединены в один `local.router_interfaces` (роль → `{network_id, subnet_id, cidr, fixed_ips}`) — переиспользован существующий паттерн (`setproduct`, один `vkcs_networking_port`, один `dynamic "network"`) вместо дублирования ресурсов.
|
||||
- `vkcs_networking_port.router_priv_port` переименован в `router_iface_port`, `fixed_ip.ip_address` теперь условный: `null` для project-managed ролей (как раньше — IPAM сам назначает, избегая коллизии с служебным портом `network:dns`), явный статический адрес — для ролей из `router_networks`.
|
||||
- Добавлен `lifecycle.precondition` на порт: понятная ошибка, если `router_networks[role].ip_addresses` короче `var.router_count`.
|
||||
- `network-init.sh.tpl` **не изменялся** — он уже сопоставляет интерфейсы по вхождению живого IP в ожидаемый CIDR, независимо от роли и способа назначения адреса.
|
||||
|
||||
### Окружение `mvm-s3` (файлы)
|
||||
- `terraform/mvm-s3.tfvars` (коммитится) — `router_count = 4`, `private_network_cidrs = []`, `router_networks` с двумя ролями (`primary` = `172.16.252.8/29`, `backup` = `172.16.252.0/29`) и точными IP на роутер из диаграммы пользователя. `ssh_key_name` оставлен плейсхолдером — пользователь должен вписать реальное значение.
|
||||
- `terraform/mvm-s3.secrets.tfvars.example` (коммитится) — шаблон реальных credentials для `mvm-s3`.
|
||||
- `terraform/prod.secrets.tfvars.example` (коммитится, заменил `prod.auto.tfvars.example`) — тот же шаблон для PROD по новой конвенции именования.
|
||||
- `.gitignore` — добавлен паттерн `*.secrets.tfvars` (и `.json`), старый `*.auto.tfvars` оставлен как safety net.
|
||||
|
||||
### Схема окружений
|
||||
Поскольку теперь в одном каталоге `terraform/` живут 2 окружения с разными credentials/router_count/сетями, отказались от неявной авто-загрузки `*.auto.tfvars` в пользу **явных `-var-file`** + **отдельных Terraform workspace** на окружение (`terraform workspace new mvm-s3`). PROD продолжает работать в workspace `default` со своим state — `mvm-s3` их не затрагивает.
|
||||
|
||||
**Важно (ручной шаг для пользователя, не сделан мной):** переименовать существующий `terraform/prod.auto.tfvars` (реальные PROD-креды) в `terraform/prod.secrets.tfvars` — я не трогал этот файл, так как он содержит настоящие секреты.
|
||||
|
||||
### Документация
|
||||
- `docs/changes/2026-09-09-mvm-s3-external-networks-plan.md` — план (копия согласованного).
|
||||
- `README.md` — новый раздел "External fixed-IP networks and multi-environment workspaces", обновлены упоминания `*.auto.tfvars` → `*.secrets.tfvars`, описание тестов.
|
||||
- `docs/QUICKSTART.md` — новый шаг 4а (деплой `mvm-s3`), обновлён шаг 2 (credentials) и шаг 4 (явные `-var-file`, workspace).
|
||||
|
||||
### Тесты (`terraform/tests/test_terraform_delivery.py`)
|
||||
73 теста (было 56 по данным `DEPLOYMENT_SUMMARY.md`, часть добавлена и в предыдущих изменениях), все зелёные. Ключевые добавления/правки:
|
||||
- `test_router_networks_variable`, `test_router_networks_validation_is_enforced` (валидный/невалидный CIDR, IP вне подсети, дефолтный `{}`).
|
||||
- `test_private_network_cidrs_variable`/`test_private_network_cidrs_validation_is_enforced` обновлены под новый `default = []`.
|
||||
- `test_router_interfaces_local_merges_both_network_sources`, `test_router_interface_roles_local_is_keys_of_router_interfaces`, `test_router_network_blocks_scale_with_router_interface_roles` — заменили тесты на удалённый `local.private_roles`.
|
||||
- `test_router_iface_port_ip_address_is_conditional_on_fixed_ips`, `test_router_iface_port_has_length_precondition_for_fixed_ips` — заменили тест на переименованный `router_priv_port`.
|
||||
- `test_mvm_s3_tfvars_*` — сверяют `mvm-s3.tfvars` с диаграммой (UUID, CIDR, IP), запрет project-managed сетей, покрытие `router_count` числом IP.
|
||||
- `test_secrets_tfvars_example_is_not_gitignored`, `test_gitignore_excludes_secrets_tfvars_overlay`, `test_committed_tfvars_have_no_auth_credentials` — заменили тесты на удалённый `prod.auto.tfvars.example`.
|
||||
- `test_terraform_init_and_validate_against_real_provider_schema` — добавлен `mvm-s3`-образный кейс (`private_network_cidrs=[]` + 2 роли `router_networks`) для проверки схемы реального провайдера.
|
||||
|
||||
Прогон: `venv/bin/terraform fmt -check` — чисто; `venv/bin/pytest terraform/tests -v` — **73 passed**.
|
||||
|
||||
## Что НЕ сделано (осознанно, вне зоны моего доступа)
|
||||
|
||||
- `terraform plan`/`apply` в проекте `mvm-s3` — у меня нет доступа к этому VK Cloud проекту; должен выполнить пользователь (команды — в `docs/QUICKSTART.md` §4а).
|
||||
- Переименование реального `terraform/prod.auto.tfvars` → `prod.secrets.tfvars` — файл содержит настоящие секреты, переименовать должен пользователь сам.
|
||||
- Заполнение реальных credentials/`ssh_key_name` для `mvm-s3` — плейсхолдеры оставлены намеренно, я не должен их придумывать.
|
||||
- Проверка актуальности `sdn = "sprut"` на порту в чужую сеть — отмечено в `main.tf` комментарием, требует подтверждения на первом реальном `plan`/`apply`.
|
||||
Reference in new issue
Block a user