chore: save current changes

This commit is contained in:
Repinoid
2026-09-23 19:23:31 +03:00
parent d78573de45
commit a96e38fb1e
8 changed files with 994 additions and 0 deletions
@@ -0,0 +1,169 @@
# Штурвал через IaC: анализ проблемы `modify` и скрытых платформенных зависимостей
**Дата:** 2026-09-23
**Контекст:** дискуссия в Telegram про запуск цепочки Штурвал полностью через Terraform.
---
## 1. Исходная задача
Клиенту нужен IaC (Infrastructure as Code): вся инфраструктура описывается одним конфигом, команда `terraform apply` разворачивает её целиком, `plan`/`destroy` дают полную картину. Никаких обязательных ручных шагов посередине.
Цепочка для стенда Штурвал:
```text
vcOrg -> create
vcVdc -> create
vcNsxt -> create
------------------------------
vcOrg -> modify (аллоцировать внешние IP в организацию)
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg)
------------------------------
k8sShturval -> create
```
Ключевой конфликт: `create` у Terraform работает штатно, а операции `modify` в текущем провайдере никак не выражаются — Terraform не умеет «создать ресурс, а через несколько шагов поменять в нём же параметр».
---
## 2. Почему `modify` не выражается в текущем провайдере
### 2.1. Генератор строит схему только из `create`
Провайдер генерируется из YAML-спеков (`generated/{stand}/resources_yaml/*.yaml`). Схема ресурса (какие поля можно писать в `.tf`) строится **только из операции `create`**. Параметры, которые есть только в `modify`, в схему не попадают.
Подтверждено по файлам:
- `generated/dev/resources_yaml/19_vc_org.yaml`:
- `create` (id 136) → только `resourceRealm` (418), `organizationType` (556), `orgSuffix` (1125);
- `vIPConfigure` (выделение внешних IP) есть **только** в `modify` (id 207), с sub-полями `name` (39) и `count` (40).
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`:
- `create` (id 10) → `vdcUid`, `needEnableAVI` (340), `virtualServicesCount` (341), `qosProfile` (825), `routedNetConfiguration` (1110) и др.;
- `ipSpaceName` (372) есть **только** в `modify` (id 111).
Вывод: `vIPConfigure` (vc_org) и `ipSpaceName` (vc_nsxt) живут только в `modify`, в схеме tf-ресурсов их нет. Поэтому «прописать параметр в tf и сделать apply» падает ещё на `plan` (атрибут не известен провайдеру).
### 2.2. Эти параметры — не «настройки», а отложенные действия
- `vIPConfigure=[{name,count}]` — транзакция «выделить N внешних IP» на ресурсной платформе. Повторный вызов **накапливает** (аккумулирует квоту), а не задаёт состояние. Это не декларативное значение.
- `ipSpaceName` — включение SNAT на конкретный ipSpace, который возникает **только после** того, как на орге выделены IP.
- Эти операции требуют порядка (org.modify → затем nsxt.modify) и зависят от живого состояния инстанса, а не от дефолтов формы.
### 2.3. Схема в state ≠ реальное состояние
Вписывать недостающие параметры «насильно» в tfstate нельзя и бесполезно:
1. Terraform валидирует атрибуты по **схеме провайдера**, а не по state — неизвестный атрибут будет отброшен/вызовет ошибку.
2. Записывать в state «SNAT включён», когда этого нет на площадке, — значит получить ложный `plan` (чистый) при сломанной инфраструктуре.
3. Ручная правка tfstate/`state push` ломает целостность (серийник, конфликты на следующем apply).
Работает только косвенно: `terraform_data`/`null_resource` + `local-exec` → в state попадает **факт** «операция выполнена» (маркер с `triggers`), но не **состояние** SNAT/IP. Порядок задаётся через `depends_on`, но дрейф по самим параметрам `plan` не видит.
---
## 3. Каноничное решение: отдельный ресурс (resource association)
Это принятая в Terraform практика — «resource association / separate resource». Классические примеры:
- `aws_security_group` + `aws_security_group_rule`
- `aws_vpc` + `aws_route_table_association`
- `google_project` + `google_project_iam_member`
Базовый ресурс создаётся отдельно, а донастройка/привязка — отдельным ресурсом с `depends_on`. Граф сам выстраивает порядок, `destroy` разворачивает его корректно.
### 3.1. Прецедент из Cloud Director (VCD)
В репозитории лежит сторонний шаблон — `/home/naeel/TF/tf_provider/!/` (network.tf.tmpl, vmware_org.tf, vdc.tf), показывающий, как та же цепочка делается провайдером VMware Cloud Director:
- `resource "vcd_nsxt_alb_settings"` — включение ALB, `count = var.alb_enable ? 1 : 0`, `depends_on = [vcd_nsxt_edgegateway...]`;
- `resource "vcd_nsxt_alb_edgegateway_service_engine_group"` — выделение SE, `reserved_virtual_services = var.alb_segroup_count`;
- `resource "vcd_network_routed_v2"` — routed-сеть, `edge_gateway_id`, `dns1/dns2/static_ip_pool`;
- `resource "vcd_ip_space_custom_quota"` — квота IP на **оргу**, `depends_on = [edge]`.
Приём «включить/выключить» = `count`. Обратная операция (выключить ALB / снять квоту) получается **удалением ресурса** — inverse логика не нужна.
Маппинг на наши сервисы:
| Nubes API | Канон VCD |
|---|---|
| `needEnableAVI` | `vcd_nsxt_alb_settings` (+ `count`) |
| `virtualServicesCount` | `reserved_virtual_services` в SE-группе |
| `routedNetConfiguration` (mainDns/secondDns/ipAddrPool) | `vcd_network_routed_v2` (`dns1/dns2/static_ip_pool`) |
| `vIPConfigure` (IP на оргу) | `vcd_ip_space_custom_quota` (на оргу) |
### 3.2. Чем наш случай сложнее канона
В классическом паттерне ребёнок — **отдельный объект API** со своим CRUD (правило, association, attachment). Его можно создать/прочитать/удалить.
У нас отдельного объекта нет — есть **операция `modify` над родителем**. Поэтому требуются:
1. `Read` — не свой объект, а чтение состояния родителя;
2. `Delete` — не удаление, а **обратный modify** (inverse);
3. `Create/Update` — вызов той же операции с параметрами;
4. идемпотентность (не дёргать `run`, если live уже целевое) и live-сверку.
Именно поэтому «просто завести поля из modify в схему» не работает — нужна полноценная механика, а не одна правка.
---
## 4. Более глубокая проблема: скрытые платформенные зависимости
Это главное из всей дискуссии (реплики Виталия Зайцева, 18:30–18:34).
### 4.1. `ipSpaceName` нельзя ввести вручную — он выводится
```text
имя ipSpace → зависит от providerGateway
providerGateway → зависит от providerVdc
providerVdc → никто не знает изначально
```
Пользователь **не может** заполнить `ipSpaceName`, потому что это значение выводится из внутренней топологии (providerVdc → providerGateway → ipSpace), а не из того, что он видел в ЛК. Это не «поле, которое забыли отдать через API», а **вычисляемое от скрытых зависимостей** значение.
### 4.2. Текущий костыль платформы
«При создании орги/vdc/edge, если организация ничего не знает про недостающие параметры — они подкладываются». То есть одноразовая подстановка при создании пустой орги, чтобы избавить пользователя от «мучительных приседаний» в ЛК.
### 4.3. Ограничение модели: один T0
«Другая проблема — что будет, если в облаке появится больше 1 T0». Пока принято допущение на уровне кода: **в организации всё одно подключение**. Решение осознанно отложено («пара лет спокойствия»), но для IaC это риск: текущее решение завязано на «в орге всегда один провайдер-шлюз».
### 4.4. Ожидание изменений спеков
«Мне надо увидеть, как спеки поменяются, чтобы понять, что исправлять… Надеюсь, появится сначала в sandbox.nubes.ru, а не в ngcloud». То есть платформа меняется, форма ресурсов зависит от **новых спеков**, и строить модификатор сейчас = работать по устаревшим спекам.
---
## 5. Итог: где правда
1. **Ручной ЛК и скрипт не подходят** — клиент требует IaC (Георгий прав). Это не «костыль против красоты», это невыполнение требования.
2. **Отдельный ресурс под модификацию — необходимое, но не достаточное условие.** Он закрывает «как expressить modify», но не закрывает «откуда юзер возьмёт значения».
3. **Главная блокировка — не Terraform, а платформа.** `ipSpaceName` (и подобные) выводятся из `providerVdc → providerGateway → ipSpace`, которые юзер не знает. Пока платформа не отдаёт эти значения в спеках (или провайдер не резолвит их data-источником), честный IaC не собрать — ни модификаторами, ни скриптом, ни руками.
4. **«Ломается агностичность» — верно, но это не порок, а цена.** Ресурсы-модификаторы доменные и «ручные», как в VCD. Без них IaC невозможен, прецедент — перед глазами (`!/network.tf.tmpl`).
5. **Состояние дел:** платформа в движении (ждут новые спеки). Правильная последовательность — дождаться, что придёт в спеках (snandbox), а затем решать форму ресурса; не строить по старым спекам.
---
## 6. Возможные пути (по убыванию «честности» перед IaC)
| Вариант | Что делает | Вердикт |
|---|---|---|
| **A. Полноценные ресурсы-модификаторы + data-источники** | отдельный tf-ресурс на modify + data-source, резолвящий `ipSpace`. Полный IaC. | правильно, но только после новых спеков |
| **B. Data-source через `http`/`external` + `jsondecode`** | DevOps сам дёргает API и подставляет динамические списки, без правки провайдера | рабочая «дожималка», не полный IaC |
| **C. `terraform_data`/`null_resource` + `local-exec`** | модификации скриптом, факт в state, порядок через `depends_on` | полумера, состояние SNAT/IP вне state |
| **D. Прессеты/дефолтное окружение** | готовый набор компонентов, экспорт через провайдер | снижает боль на старте, IaC не заменяет |
| **E. Ручной ЛК / скрипт вне tf** | модификации руками | не подходит (требование клиента) |
---
## 7. Моё мнение
**Коротко:** для настоящего IaC нужны обе вещи одновременно — **отдельный ресурс под `modify`** и **механизм получения динамических значений** (`ipSpace` и пр.). Пока платформа не отдаёт второе через API/спеки, все «быстрые» способы (скрипт, руками, пресеты) закрывают только симптом, а не требование клиента.
**Рекомендация:** не городить модификатор сейчас по устаревшим спекам. Дождаться изменений спеков (сначала sandbox), параллельно — обсчитать два blockers: (1) как провайдер будет резолвить `providerVdc → providerGateway → ipSpace` без ручного ввода; (2) допущение «один T0». После этого проектировать форму ресурсов.
**Что точно не делать:** вписывать параметры «насильно» в tfstate; дёргать `modify` через скрипт на каждый apply без live-сверки (накопит `vIPConfigure`); ждать, что «прописал поле в tf → apply» заработает без правки провайдера.