13 KiB
Ответ Opus: анализ решения IaC-развёртывания Штурвала (модификаторы + скрытые зависимости)
Дата: 2026-09-23
Связанный промпт: docs/prompts/prompt_for_opus_iac_shturval_modify.md
Связанный анализ: docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md
Статус: документирование ответа. Конкретный план НЕ составляется.
1. Суть ответа (главный вывод)
Форма IaC-ресурсов фиксируется уже сейчас, потому что она диктуется моделью Terraform (декларативность, идемпотентность, inverse), а не спеками платформы.
НО есть три блокера от платформы, без которых идемпотентность и Delete принципиально недостижимы на стороне провайдера.
2. Ответ Opus по пунктам
Пункт 1 — накопительный vIPConfigure → идемпотентный ресурс
- Ресурс отдельный (
nubes_org_vip_allocation) сdepends_onна оргу, НЕ операция внутри орги. - Ключ идемпотентности — желаемое состояние, а не дельта. Юзер задаёт целевой
countнаname; провайдер сам считаетtarget − currentи модифицирует только разницу. - Create: Read текущего числа vIP → выделить
target − current. Если API не отдаёт «сколько уже есть» — нужен серверный счётчик/тег, иначе идемпотентность недостижима. - Read: читать родителя (оргу), извлекать фактическое число IP по
nameв state. Если API не различает «кем/зачем выделено» — Read вернёт общий пул, drift неизбежен. - Update: та же дельта-логика (target изменился → доначислить/освободить).
- Delete (inverse):
modifyс обратным знаком доcount=0по этомуname. Требует адресного освобождения конкретных IP. Если освобождение — тоже накопительный modify без адресации, inverse корректно сделать нельзя.
Риск: накопительный API без «прочитать текущее» и без «освободить конкретное» несовместим с декларативной моделью. Чинится на стороне платформы, не провайдера.
Пункт 2 — ipSpaceName (цепочка providerVdc → providerGateway → ipSpace)
- Это выводимое значение из инфраструктуры, НЕ пользовательский ввод → data-source, а не аргумент ресурса.
- Правильно:
data "nubes_ip_space" { org/vdc = ... }, который проходит цепочку providerVdc → providerGateway → ipSpace и возвращаетname. Ресурс берёт значение по ссылке. - Граница «данные vs логика»: в реестре хранить тип поля и его источник (что это computed-from-parent, а не user-input). Сама цепочка обхода — логика data-source, не данные реестра.
- НЕ вычислять из state родителя вручную в ресурсе (скрытая связанность, ломается при >1 T0). Data-source явно выражает зависимость в графе tf.
- Пока платформа «подкладывает» значение сама — data-source должен уметь то же читать. Если API этой цепочки нет на чтение — блокер.
Пункт 3 — не завязываться на «один T0»
- Закладывать явный селектор шлюза уже сейчас:
provider_gateway/t0_idкак аргумент (или ключ data-source), даже если сегодня один и выводится автоматически (optional + computed default). - vIP-аллокация и SNAT привязывать к конкретному gateway id, а не к «дефолтному в орге».
- Что сломается при >1 T0, если не заложить:
ipSpaceNameстанет неоднозначным (несколько ipSpace), vIP-аллокация не будет знать, к какому шлюзу. Придётся менять схему (добавлять обязательный селектор) → breaking change. - Заложив optional-селектор сейчас: при росте T0 меняется только default-резолвинг, схема остаётся совместимой.
Пункт 4 — ждать спеки или фиксировать форму сейчас
- Форму ресурсов можно и нужно фиксировать сейчас — она диктуется моделью Terraform, а не спеками.
- Не блокер (делаем сейчас): раздельные ресурсы +
depends_on; целевое состояние вместо дельты; селектор шлюза; data-source дляipSpaceName; inverse через обратный modify. - Блокер (нужно от платформы):
- a) чтение текущего числа vIP по
name(иначе нет Read/идемпотентности); - b) адресное освобождение IP (иначе нет корректного Delete);
- c) API чтения цепочки providerVdc → providerGateway → ipSpace (иначе data-source невозможен).
- a) чтение текущего числа vIP по
- Вывод: проектируем форму сейчас, помечаем a/b/c как зависимости от платформы. Новые спеки повлияют на резолвинг значений, не на форму ресурсов — если форма построена на «целевое состояние + селектор + data-source».
Пункт 5 — минимально-инвазивный порядок внедрения
От платформы (до кодинга ресурсов) — обязательно:
- Read-эндпоинт: текущее количество vIP по
name(для идемпотентности/Read). - Освобождение конкретных vIP (для Delete/inverse).
- Read цепочки →
ipSpaceName(для data-source). - Подтверждение, что генератор умеет строить схему из объединения
create+modifyполей (иначеvIPConfigure/ipSpaceNameвообще не попадут в схему).
На стороне провайдера — можно сейчас, не дожидаясь:
- Раздельные ресурсы vip-allocation / nsxt-snat с
depends_on. - Логика «target − current = дельта» (заглушка current, пока нет Read).
- Data-source-скелет для
ipSpaceName(с TODO на реальный обход цепочки). - Optional+computed селектор шлюза.
- Inverse-контракт (Delete = обратный modify до нуля).
Главный неустранимый на нашей стороне блокер: накопительный API без чтения текущего состояния и без адресного освобождения. Без этого идемпотентность и Delete принципиально недостижимы. Правится в спеках/API платформы, а не в провайдере.
3. Что нового vs то, что уже собирались делать
Совпадает со старыми планами (НЕ новое)
- Отдельный ресурс под модификацию +
depends_on— было (PLAN_modifier_redesign.md, «resource association»). - Inverse через обратный modify (
count→0) — было (inverse_rollback_analysis_2026-09-23.md). - Идемпотентность (skip run, если live уже целевое) — было.
- «Не ждать спеки для формы, а фиксировать сейчас» — по сути было.
Реально новое у Опуса
- «Целевое состояние, а не дельта» — строгий принцип: юзер задаёт целевой
count, провайдер сам считаетtarget − current. Старые планы просто «досылали заданные поля», не формализовали желаемое состояние. ipSpaceName— data-source, не аргумент ресурса — сдвиг от «юзер вписывает значение» к «computed-from-parent». Раньше виделось как ввод.- Селектор шлюза (
t0_id/provider_gateway) как optional+computed сейчас — в старых планах про «один T0» вообще не было (пришло только из реплики Виталия). - Чёткая граница «данные vs логика» — в реестре только «тип поля + что computed-from-parent», цепочка обхода — логика data-source.
- Три конкретных блокера от платформы как API-требования (Read счётчика, адресное освобождение, Read цепочки) — раньше это было «unknown, проверить», теперь жёсткий список.
Главное отличие одной фразой
Старые планы отвечали на «как сделать модификатор в tf». Опус отвечает на «как сделать его идемпотентным и IaC-честным при накопительном API и скрытых зависимостях» — и выявил, что ядро проблемы не в провайдере, а в платформе (Read счётчика + адресное освобождение + Read цепочки).
4. Спорный/непроверенный момент (требует живой проверки)
Опус утверждает: накопительный vIPConfigure без Read-счётчика и без адресного освобождения «несовместим с декларативной моделью».
Проверено частично: из HAR (org2.har, org_enough_.har) видно, что live vIPConfigure с count читается (это уже потенциально «Read текущего числа»). Значит блокер (a) — чтение счётчика — возможно, уже закрыт.
НЕ проверено: умеет ли API адресно освободить конкретный name/count (откат), или освобождение тоже накопительное. Это блокер (b), и его надо проверить живым API, прежде чем принимать вывод Опуса за окончательный.
5. Резюме
- Форма ресурсов — проектируем сейчас, она не зависит от спеков.
- Три блокера платформы: Read счётчика vIP, адресное освобождение, Read цепочки ipSpace.
- Из трёх блокеров (a) частично подтверждён HAR-дампами; (b) и (c) — непроверены.
- Конкретный план внедрения пока НЕ составляется (по решению пользователя).
Следующий возможный шаг (только по запросу): проверить живым API блокеры (b) адресное освобождение и (c) чтение цепочки, после чего фиксировать, что реально блокирует, а что уже закрыто.