Наша команда в Okko занимается автоматизацией облачной инфраструктуры. В этой статье расскажем, как мы переводили VMware Cloud Director (VCD) с ручного управления через интерфейс на Infrastructure as Code (IaC), почему выбрали Pulumi и Python, как устроили работу с пользовательскими YAML‑конфигурациями и их валидацией.

Отдельно остановимся на практической части: как с помощью Jaeger нашли узкие места в производительности и что в итоге помогло ускорить работу с VCD.

Прежде чем перейти к оптимизации, нужно описать исходную задачу. Мы работаем с облаком на базе VMware Cloud Director. Нам хотелось не просто перенести инфраструктуру в код, а сделать удобный слой поверх VCD, в котором пользователи описывают нужные ресурсы через понятную YAML‑конфигурацию, а модуль берёт на себя внутреннюю логику работы с платформой.

Для реализации этой идеи мы выбрали Pulumi и написали инфраструктурную логику на Python. Такой подход позволяет удобно валидировать конфигурацию, подставлять значения по умолчанию, искать нужные объекты в VCD и описывать связи между ресурсами.

Однако довольно быстро выяснилось, что удобные модули и компактная YAML‑конфигурация — это только половина задачи. Не менее важен быстрый цикл обратной связи: инженер должен заранее увидеть, что именно изменится после его правки. В Pulumi для этого используется pulumi preview (аналог terraform plan) — предварительный расчёт изменений. Если этот этап занимает минуты, цикл «изменил → проверил → исправил» замедляется, а IaC теряет одно из главных преимуществ — возможность быстро проверить изменение до применения.

В стеке примерно из 1000 объектов pulumi preview в периоды высокой нагрузки на облако мог выполняться до 10 минут, а применение изменений занимало ещё больше времени. Поэтому помимо разработки самих модулей нам пришлось разбираться в производительности: искать лишние обращения к VCD API, анализировать трассировки и подбирать уровень параллельности с учётом особенностей платформы.

Когда ручное управление перестает работать

Пока инфраструктура небольшая и меняется редко, ручной подход выглядит вполне рабочим. Например, виртуальную машину в VMware Cloud Director можно создать через веб‑интерфейс: выбрать шаблон, сеть, параметры процессора и памяти, подключить диски и указать остальные настройки.

Для одной‑двух тестовых машин этого достаточно. Проблемы начинаются, когда такие операции приходится регулярно повторять для разных окружений, команд и сервисов. Чем больше изменений проходит через ручной процесс, тем больше времени он занимает и тем выше вероятность ошибки.

Есть и другая проблема. Информация о ручных изменениях часто оказывается размазанной между веб‑интерфейсом облачной платформы, рабочими чатами, заявками и памятью инженеров. В такой ситуации сложно быстро ответить на простые вопросы: кто изменил ресурс, зачем это было сделано, какие параметры были раньше и можно ли безопасно повторить такую же настройку в другом окружении.

Ручные изменения также плохо проверяются до применения. Их нельзя полноценно обсудить в merge request, автоматически прогнать через CI или заранее увидеть в виде плана. Результат во многом зависит от внимательности конкретного инженера и от того, насколько точно команда представляет текущее состояние облака.

Состояние, которое возникает, когда историю изменений приходится восстанавливать без Git.
Состояние, которое возникает, когда историю изменений приходится восстанавливать без Git.

Infrastructure as Code меняет этот процесс. Источником истины становится репозиторий, в котором описано желаемое состояние инфраструктуры: виртуальные машины, сети, IP‑сеты, параметры ресурсов и связи между ними. Каждая правка проходит ревью, а перед применением команда видит план изменений: какие ресурсы будут созданы, изменены или удалены.

В результате работа с инфраструктурой становится ближе к привычной разработке. Все изменения остаются в истории Git, видно, кто и что поменял, конфигурация проходит автоматические проверки, а при необходимости её можно воспроизвести в другом окружении.

Для нас это было ключевым аргументом в пользу IaC. Лучше получить красную сборку ещё на этапе merge request, чем разбираться с последствиями неудачного изменения уже после того, как оно попало в тестовое окружение или продакшен.

Почему мы используем Pulumi

При выборе IaC‑инструмента нам было важно не только уметь создавать ресурсы VMware Cloud Director из кода. Мы хотели скрыть от пользователей внутреннюю механику платформы и оставить им простой способ описания инфраструктуры.

Момент выбора между декларативностью Terraform и полноценной программной логикой Pulumi.
Момент выбора между декларативностью Terraform и полноценной программной логикой Pulumi.

Это особенно важно для VCD, где за созданием одной виртуальной машины стоит целый набор связанных сущностей: виртуальный дата‑центр, vApp, каталог, шаблон, сеть и другие объекты. Пользователю не нужно каждый раз собирать эту конструкцию вручную. Достаточно указать в YAML основные параметры виртуальной машины — имя, образ, количество процессоров, объём памяти и сеть.

Остальную работу берёт на себя Python‑модуль. Он проверяет и нормализует параметры, подставляет значения по умолчанию, сопоставляет понятные пользователю имена с идентификаторами объектов в VCD и создаёт необходимые ресурсы. За счёт этого YAML остаётся компактным, а вся сложность работы с платформой переносится в код модуля.

Для нас ключевым преимуществом Pulumi стала возможность описывать не только ресурсы, но и сопутствующую логику на обычном языке программирования. Поскольку инфраструктурный код в таком случае пишется на Python, мы можем использовать привычные подходы к разработке: разбивать его на классы и функции, реализовывать сложную валидацию, добавлять кэширование и интеграции с системами учёта инфраструктуры, управления серверами, хранения секретов, CI/CD и внешними API.

Основная логика пользовательского сценария находится в Python‑модулях. Именно там удобно описывать правила проверки YAML‑конфигурации, например контролировать, что IP‑адреса не конфликтуют между собой, параметры ресурсов соответствуют ограничениям площадки, а указанные каталог, образ и сеть существуют в VCD.

Пример валидации YAML-конфигурации при создании merge request: модуль обнаруживает дублирование IP-адреса и адрес вне указанной сети.
Пример валидации YAML-конфигурации при создании merge request: модуль обнаруживает дублирование IP-адреса и адрес вне указанной сети.

Такая валидация позволяет обнаружить проблему до начала применения изменений. Некорректная конфигурация должна быть отклонена во время проверки merge request или выполнения pulumi preview, а не посередине развёртывания, когда часть ресурсов уже успела измениться.

Как Pulumi взаимодействует с VCD

Отдельный вопрос — как Pulumi взаимодействует с VMware Cloud Director. Сам по себе Pulumi не знает API каждой облачной платформы. Он строит граф ресурсов, хранит состояние, показывает preview, считает diff и управляет жизненным циклом ресурсов. Чтобы создать в облаке виртуальную машину, сеть, IP‑set или правило межсетевого экрана, ему нужен провайдер — прослойка между Pulumi CLI и API платформы.

Провайдер описывает, какие ресурсы доступны в VCD, какие параметры у них есть и какие CRUD‑операции над ними можно выполнять: создание, чтение, обновление и удаление.

Получить такой провайдер можно разными способами. Например, взять готовый из экосистемы Pulumi, использовать Terraform‑провайдер через Any Terraform Provider, написать native provider с нуля или собрать bridged‑провайдер на базе существующего Terraform‑провайдера.

Для VMware Cloud Director мы выбрали вариант с Terraform Bridge через pulumi‑tf‑provider‑boilerplate. Это оказалось самым практичным решением, потому что для VCD уже существует готовый и достаточно зрелый Terraform‑провайдер, который реализует работу с большим количеством ресурсов платформы. Вместо того чтобы заново писать весь API‑слой, мы использовали его как основу и получили собственный Pulumi‑провайдер.

Такой подход дал нам Pulumi SDK, возможность версионировать провайдер, собирать его в CI и развивать под наши сценарии. При этом провайдер решает только задачу взаимодействия с VCD API. Пользовательская YAML‑конфигурация, значения по умолчанию, интеграции и оптимизация остались на уровне наших Python‑модулей поверх него.

Наш Pulumi‑провайдер для VCD доступен в публичном репозитории pulumi‑vcd. Мы используем его в production‑сценариях и развиваем под практические задачи автоматизации VMware Cloud Director. Проект не является официальным провайдером Pulumi или VMware, но если вы тоже работаете с VCD и хотите попробовать Pulumi в похожем сценарии, будем рады обратной связи, issue и pull request.

Как устроен путь от YAML файла до объектов в VCD

Теперь посмотрим, что происходит после того, как пользователь описал инфраструктуру в YAML. На первый взгляд это просто небольшой конфигурационный файл, но за ним стоит несколько этапов обработки.

Сначала YAML загружается в Python‑код и преобразуется в экземпляры классов, описанных через dataclass. На этом этапе модуль нормализует значения, подставляет параметры по умолчанию и проверяет конфигурацию. Если пользователь ошибся в имени поля, указал некорректный IP‑адрес или задал параметры, которые не проходят ограничения площадки, ошибка появляется ещё до применения изменений.

После валидации данные передаются в Pulumi‑модуль. Связанные ресурсы объединяются через ComponentResource. Это позволяет представить набор объектов как одну логическую сущность. Например, компонент виртуальной машины может включать саму VM, дополнительные диски, сетевые подключения и связанные записи во внешних системах.

Общая схема того, как пользовательская конфигурация превращается в объекты облака.
Общая схема того, как пользовательская конфигурация превращается в объекты облака.

Дальше модуль с помощью ResourceOptions задаёт положение ресурсов в графе Pulumi: родительские связи, явные зависимости, используемый провайдер и другие параметры выполнения. Это важно, потому что не все операции можно выполнять в произвольном порядке. Например, сначала нужно найти или создать связанные объекты, а уже потом использовать их при создании виртуальной машины.

На основе объявленных ресурсов и зависимостей Pulumi строит граф выполнения. Затем VCD provider преобразует запланированные операции в вызовы к VMware Cloud Director API и создаёт, изменяет или удаляет объекты в облаке.

На отдельных этапах модуль также взаимодействует с внешними системами: регистрирует объект в системе учёта инфраструктуры, получает секреты из Vault или обращается к внешним API.

В результате пользователь работает только с компактной YAML‑конфигурацией. Ему не нужно вручную собирать структуру ресурсов VCD, помнить все зависимости и отдельно выполнять действия во внешних системах. Эту работу берёт на себя модуль: валидирует конфигурацию, строит зависимости, вызывает провайдер и связывает облако с остальной инфраструктурной экосистемой.

Анализ производительности Pulumi с помощью Jaeger

Первые версии модулей работали стабильно, но с ростом инфраструктуры стало заметно новое ограничение: цикл обратной связи оказался слишком длинным. В пилотном стеке было около 1000 объектов, и в периоды высокой нагрузки на облако pulumi preview мог выполняться до 10 минут. Применение изменений занимало еще больше времени.

Для инфраструктурной платформы это серьезное ограничение. Чем дольше выполняется предварительный расчёт изменений, тем дороже обходится каждая правка. В результате инженеры реже запускают проверку, объединяют больше изменений в один merge request, а ошибки обнаруживаются уже на более поздних этапах.

В такой ситуации IaC начинает терять одно из своих главных преимуществ — возможность заранее увидеть планируемые изменения и убедиться, что конфигурация описана правильно.

По стандартным логам определить источник задержек не получилось. Они показывали только общее время выполнения команды, но не помогали понять, что именно тормозит процесс: Pulumi Engine, Python‑код, работа провайдера или ответы VMware Cloud Director API.

До трассировки в Jaeger расследование выглядело примерно так.
До трассировки в Jaeger расследование выглядело примерно так.

Чтобы перейти от предположений к конкретным данным, мы включили встроенную трассировку Pulumi и загрузили результаты в Jaeger. Так мы получили подробную временнУю картину выполнения и смогли увидеть, сколько времени занимала каждая операция, где возникали повторные вызовы и какие запросы к VCD API задерживали процесс.

Узкое место № 1: повторные lookup‑запросы

В YAML пользователь работает с понятными именами: выбирает каталог с шаблонами Ubuntu, сам шаблон Ubuntu_24.04.2, нужную сеть и так далее. Однако VCD API во многих операциях ожидает не имена, а технические идентификаторы объектов. Поэтому перед созданием или изменением ресурса модулю приходится выполнять lookup‑запрос и определять, какой ID соответствует указанному имени.

На небольших конфигурациях такие обращения к API почти незаметны. Но по мере роста стека одни и те же объекты начинают запрашиваться десятки и даже сотни раз.

Трассировка в Jaeger показала, что ожидание ответа на отдельный lookup‑запрос может занимать несколько секунд. Когда одни и те же каталоги, шаблоны и сети запрашиваются повторно, суммарное время таких вызовов начинает заметно влиять на продолжительность всего запуска.

Повторяющийся lookup-запрос в трассировке Jaeger. Получение информации о шаблоне ВМ через getCatalogVappTemplate заняло 4,56 секунды.
Повторяющийся lookup‑запрос в трассировке Jaeger. Получение информации о шаблоне ВМ через getCatalogVappTemplate заняло 4,56 секунды.

Решение оказалось простым: не запрашивать у API повторно данные, которые уже были получены ?

Для этого мы добавили in‑memory кэширование на уровне Python‑обёртки. При первом обращении модуль получает объект через VCD API и сохраняет результат в памяти до конца выполнения команды. Все последующие обращения к тому же объекту используют уже сохранённое значение.

Упрощенная версия реализации in-memory кэширования: если идентификатор шаблона уже сохранен в памяти, модуль обходится без повторного обращения к VCD API.
Упрощенная версия реализации in‑memory кэширования: если идентификатор шаблона уже сохранен в памяти, модуль обходится без повторного обращения к VCD API.

Так мы закэшировали lookup‑запросы для объектов, которые часто переиспользуются при построении стека: Edge Gateway, VDC, Data Center Group, каталоги и шаблоны. В результате модуль перестал многократно ходить в VCD API за одними и теми же данными в рамках одного запуска.

После этой оптимизации время выполнения pulumi preview на одной из площадок сократилось примерно с 3 минут 40 секунд до 25 секунд. В очередной раз подтвердилось простое правило: самый быстрый запрос к API тот, которого удалось избежать.

Узкое место № 2: параллельность

Pulumi умеет выполнять операции с ресурсами параллельно. Максимальное количество одновременно выполняемых операций задаётся переменной окружения PULUMI_PARALLEL.

На первый взгляд зависимость кажется очевидной: чем выше это значение, тем быстрее должен выполняться pulumi up. Однако в случае с VMware Cloud Director это работает только до определённого предела, после которого дальнейшее увеличение параллельности уже не ускоряет развёртывание, а иногда, наоборот, увеличивает время выполнения.

Причина в том, что параллельный запуск операций на стороне Pulumi ещё не означает их параллельного выполнения внутри VCD. Pulumi строит граф зависимостей и старается параллельно выполнять всё, что не связано явными зависимостями. Если граф получается слишком плоским, а ресурсы с точки зрения Pulumi выглядят независимыми, он может запустить больше операций, чем VCD способен эффективно обработать одновременно.

Но независимость в графе Pulumi не всегда означает независимость внутри VCD. Несколько операций могут затрагивать один и тот же vApp, сетевой объект или Edge Gateway, поэтому платформа всё равно обрабатывает часть действий последовательно. Например, подключение дисков и сетей, изменение сетевых объектов и работа с Edge Gateway могут упираться во внутренние очереди и блокировки.

На трассировках в Jaeger это выглядело как характерная «лесенка». Множество операций начинались почти одновременно, но завершались одна за другой. Такая картина указывала на то, что ограничение находилось уже на стороне VCD, а не в Pulumi или клиентском коде.

Pulumi бодро раздаёт задачи worker’ам, а VCD уже сам решает, что можно выполнить параллельно, а что придётся поставить в очередь.
Pulumi бодро раздаёт задачи worker’ам, а VCD уже сам решает, что можно выполнить параллельно, а что придётся поставить в очередь.

Поэтому управлять параллельностью пришлось на двух уровнях. Первый уровень — глобальный лимит PULUMI_PARALLEL, который ограничивает количество одновременно выполняемых операций. Второй — сам граф ресурсов. Там, где операции действительно должны идти последовательно или могут конфликтовать на стороне VCD, мы явно задаём зависимости через ResourceOptions(depends_on=[...]). Это не ускоряет выполнение напрямую, но помогает не создавать лишнюю конкуренцию за одни и те же внутренние блокировки VCD.

При этом эффективность параллельности зависит от архитектуры конкретной площадки. В одном окружении ресурсы могут быть сосредоточены в одном VDC и одном NSX‑домене, а в другом — распределены между несколькими VDC и NSX‑доменами. Чем больше независимых областей, в которых VCD может обрабатывать операции, тем выше потенциальная польза от параллельного выполнения.

Поэтому универсального значения PULUMI_PARALLEL, которое одинаково хорошо работает для любой конфигурации VCD, не существует.

Чтобы подобрать подходящий уровень параллельности для наших окружений, мы провели серию тестов на одном и том же сценарии. Во время pulumi up создавалось около 164 ресурсов. Тесты последовательно запускали со значениями PULUMI_PARALLEL: 8, 16, 24, 32 и 40.

Сравнение времени выполнения pulumi up при разных значениях PULUMI_PARALLEL для компактной и распределённой конфигураций.
Сравнение времени выполнения pulumi up при разных значениях PULUMI_PARALLEL для компактной и распределённой конфигураций.

В компактной конфигурации, то есть в окружении с одним VDC и одним NSX‑доменом, рост параллельности давал заметное ускорение до значения 24. После этого выигрыш исчезал. При значениях 32 и 40 время выполнения, наоборот, увеличивалось. Большее количество одновременно запущенных операций создавало дополнительную нагрузку, но не ускоряло работу, поскольку VCD упирался во внутренние очереди и блокировки.

В распределённой конфигурации, где ресурсы находились в нескольких VDC и NSX‑доменах, параллельность работала эффективнее. Несколько независимых областей позволяли платформе одновременно обрабатывать больше операций. Лучший результат в этом сценарии показало значение PULUMI_PARALLEL=32. Однако при увеличении до 40 время выполнения снова выросло — ожидание во внутренних очередях и дополнительные накладные расходы начали перевешивать пользу от более высокой параллельности.

Эксперимент показал, что для pulumi up в VCD не стоит бездумно выставлять максимальную параллельность. В наших конфигурациях оптимальным оказался диапазон от 24 до 32 одновременно выполняемых операций.

Значение 24 мы выбрали в качестве стабильной базовой настройки для компактной конфигурации, а 32 используем для распределённой, где ресурсы размещены в нескольких независимых VDC и NSX‑доменах. Параметр PULUMI_PARALLEL нужно подбирать под архитектуру конкретной площадки, а зависимости между ресурсами — явно фиксировать там, где порядок выполнения важен для VCD. Иначе вместо ускорения можно получить дополнительные очереди и более длительное развёртывание.

Что в итоге сработало

Производительность Pulumi в подобных проектах зависит не только от самого инструмента. На итоговое время выполнения влияют архитектура облачной платформы, особенности провайдера, поведение API и зависимости между ресурсами.

VMware Cloud Director нельзя воспринимать как простой API, где каждый ресурс создаётся независимо от остальных. За привычными сущностями скрывается множество связей между vApp, виртуальными машинами, сетями, каталогами, шаблонами, Edge Gateway и NSX‑доменами. Одни операции можно выполнять параллельно, тогда как другие требуют строго последовательного выполнения. Если не учитывать это при разработке модулей, Pulumi запустит операции одновременно, но платформа всё равно поставит часть из них в очередь.

Поэтому оптимизация затронула сразу несколько уровней. Мы добавили кэширование lookup‑запросов, подобрали безопасное базовое значение PULUMI_PARALLEL, выработали рекомендации по более эффективной работе с облаком через Pulumi. По сути, IaC‑процесс пришлось адаптировать к реальному поведению платформы, а не только к её декларативной модели.

В результате команда отказалась от ручного управления инфраструктурой и получила воспроизводимый процесс. Пользователь описывает нужные ресурсы в YAML, Python‑модули отвечают за логику и валидацию, Pulumi управляет состоянием и графом ресурсов, а провайдер VCD взаимодействует с облачной платформой.

Ещё один важный вывод заключается в том, что инфраструктурный код приходится оптимизировать почти так же, как обычное приложение. Метрики, трассировка, кэширование, анализ блокировок и подбор уровня параллельности стали полноценной частью разработки платформы.

На первый взгляд могло показаться, что проблема в самих Pulumi‑модулях. Но трассировка показала, что дело не в Pulumi как инструменте и не в Python‑коде как таковом. Первая версия модулей работала слишком универсально и не учитывала реальное поведение VMware Cloud Director. После замеров стало понятно, какие обращения к API можно убрать, а какой уровень параллельности VCD способен эффективно обработать.

Эффективный IaC начинается не только с выбора инструмента, но и с понимания платформы, которой он управляет. Код задаёт желаемое состояние, однако скорость, надёжность и предсказуемость процесса зависят от того, насколько хорошо модули учитывают особенности и ограничения облака.

PS: если вы работаете с Pulumi, VMware Cloud Director или решаете похожие задачи с IaC, буду рад обменяться опытом. Telegram: https://t.me/sneboshinsky

Также приглашаем в русскоговорящее Pulumi‑сообщество в Telegram: https://t.me/pulumi_ru. Там можно задать вопросы, поделиться практическими кейсами и обсудить использование Pulumi в реальной инфраструктуре.

Комментарии (4)


  1. proximity
    30.07.2026 09:38

    Идея смотреть IaC через Jaeger моё уважение. Но я не до конца понял, как сама трассировка Pulumi тут устроена. Он из коробки пишет спаны для engine или вы ещё как-то отдельно инструментировали запросы к API? Было бы интересно чуть подробнее про саму кухню.


  1. Xelld
    30.07.2026 09:38

    IaC для VCD вспоминаю как кошмар. Раньше управляли правилами FW в NSX через terraform - пайплайны по часу были обычным явлением.

    API у него тоже монструозный :)

    Уже думали (и даже начинали) писать свою автоматизацию под это, но уехали с VCD раньше.


  1. kollegaru
    30.07.2026 09:38

    Прям знакомая история с долгим preview. А почему вы вообще сразу пошли в Pulumi, а не остались на Terraform с готовым vcd провайдером? И CDKTF не смотрели? Там же вроде тоже можно писать всё на обычном Python, но при этом использовать Terraform под капотом.

    Просто интересно, Pulumi в итоге выбрали потому что с Python там всё это удобнее собирать и можно сверху оставить пользователям простой YAML или в Terraform/CDKTF вылезли какие-то ограничения, которые уже нормально не обходились?



  1. Rom_Snow
    30.07.2026 09:38

    Провайдер pulumi-vcd вы уже выложили в опенсорс, но по статье самая интересная часть как раз осталась в закрытых Python модулях. Не планируете их тоже открыть? Ну или хотя бы вынести отдельно кэширование lookup-запросов и другие более универсальные обёртки. А то сейчас понятно, что подход работает, но переиспользовать его по факту особо нечего.