Files
tf_provider/docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md
T
2026-09-23 19:23:31 +03:00

15 KiB
Raw Blame History

Штурвал через IaC: анализ проблемы modify и скрытых платформенных зависимостей

Дата: 2026-09-23 Контекст: дискуссия в Telegram про запуск цепочки Штурвал полностью через Terraform.


1. Исходная задача

Клиенту нужен IaC (Infrastructure as Code): вся инфраструктура описывается одним конфигом, команда terraform apply разворачивает её целиком, plan/destroy дают полную картину. Никаких обязательных ручных шагов посередине.

Цепочка для стенда Штурвал:

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 нельзя ввести вручную — он выводится

имя 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» заработает без правки провайдера.