Расследуем проблемы terraform-provider-ovirt

от автора

Привет, Хабр. Я Михаил Фучко, технический продакт-менеджер SDN и Terraform в команде zVirt. Я продолжаю серию статей о пути, который мы проделали в процессе разработки собственного провайдера инфраструктуры для Terraform. В предыдущей части мы честно попробовали воспользоваться опенсорс-провайдером Terraform для oVirt и получили неоднозначные результаты (на самом деле вполне однозначные).  

Эта статья посвящена анализу провала проекта terraform-provider-ovirt. Посмотрим на принятые решения, поищем первопричину, оценим, как надо и как не надо делать, и выработаем основные концептуальные решения перед реализацией своего провайдера. 

Эта статья может быть полезна всем, кому предстоит написание своего Terraform-провайдера. Работа с унаследованным API, попытки натянуть одну модель управления ресурсами на абсолютно другую и необходимость предусматривать гораздо больше, чем изначально вложено в систему — все это погубило terraform-provider-ovirt и всего этого следует опасаться любому разработчику подобного решения. 

В предыдущих статьях

В прошлой статье цикла тестирование открытого провайдера terraform-provider-ovirt привело нас к следующим выводам:

  • Оно формально работает, тестовая инфраструктура на нем вполне строится;

  • Очень слабо реализованы механизмы отслеживания состояний объектов;

  • Единая с т.з. пользователя сущность управляется несколькими Terraform-ресурсами;

  • Взаимодействие между этими ресурсами в рамках одного «смысла» тоже фактически сломано. 

Продублирую таблицу из прошлой статьи для демонстрации глубины проблем.

Сценарий

Ожидание

Реальность

Успех?

Имя ВМ изменено

Имя ВМ восстановлено

Имя ВМ восстановлено

Да

ВМ выключена

ВМ включена

ВМ включена

Да

ВМ выключена и запущена на другом кластере

ВМ выключена и запущена на оригинальном кластере

Ресурсы ВМ, подключения ВМ к диску и сети, а также ресурс запуска ВМ должны быть ЗАМЕНЕНЫ. Данные, кроме созданного диска отдельным ресурсом, утеряны

Нет

Увеличено количество ЦП до 4 (1:4:1). Перезагрузка для применения изменений. 

Выключить ВМ. Уменьшить количество ядер. Включить ВМ. 

Нет реакции TF. 

Нет

ВМ прямо прикреплена на запуск к конкретному хосту виртуализации. 

Объект ВМ отредактирован, привязка удалена

Ресурсы ВМ, подключения ВМ к диску и сети, а также ресурс запуска ВМ должны быть ЗАМЕНЕНЫ

Данные, кроме созданного диска отдельным ресурсом, утеряны

Нет

Добавлен еще один сетевой интерфейс

ВМ выключена, интерфейс удален, ВМ включена. 

Нет изменений. 

Нет

Ищем логику

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

  • Пользователи проходят путь, примерно схожий с описанным выше;

  • Четче формулируют ТЗ, определяют ключевые для бизнес-задачи ресурсы и поля; 

  • Силами разработчика реализуют точечные проверки необходимых полей; 

  • Игнорируют любые другие поля за пределами описанного процесса. 

Давайте поговорим о причинах, так сказать, родовых травмах, которые и сформировали облик terraform-provider-ovirt таким, каким мы его наблюдаем. Начать следует с несколько «аномального» подхода к дизайну ресурсов. ВМ — это один ресурс, диск — второй, а подключение их — третий. 

Сам по себе такой паттерн можно наблюдать, например, в Google Cloud Platform (есть отдельный ресурс для управления подключением), но в целом хорошим тоном является управление связями ВМ из ресурса самой ВМ. Подобные механизмы предоставляют провайдеры и VMware и GCP. Также показательно отсутствие механизмов работы с загрузочным диском — он наследуется из шаблона и существующими механизмами провайдера его не получить. Но почему были выбраны именно такие ресурсы, а не иные? Начнем издалека — из БД. Здесь и далее мы в качестве примера будем исследовать работу с дисками ВМ — разглядывать каплю, в которой отражается море. 

Не видим ничего необычного:

  • ВМ и диски существуют в своих таблицах;

  • Промежуточная таблица описывает связь между ВМ и дисками — какой диск к какой ВМ каким способом с какими опциями. 

Странно, что очень похожую логику мы и видим в итоговом продукте, как будто она в неизменном виде доходит до пользователя. Но ведь между нами и БД находится некий слой кода, обеспечивающий нам доступ, и именно с этим слоем работает разработчик провайдера. Давайте рассмотрим ситуацию с точки зрения REST API.

Вся наша активность будет сосредоточена в эндпоинте /ovirt-engine/api/vms/{vm_id}/diskattachments. Можно заметить иерархичность — объект, описывающий подключения дисков, является подсущностью конкретной ВМ. Это шаг в правильном направлении, мы действительно хотим дать пользователю привычный объект «ВМ со всем обвесом». Попробуем создать диск для конкретной ВМ(API позволяет совместить вызовы создания и прикрепления), возьмем пример из документации. (Подчеркнем особо важные моменты)

# curl \--cacert '/etc/pki/ovirt-engine/ca.pem' \--user 'admin@internal:mypassword' \--request POST \--header 'Version: 4' \--header 'Content-Type: application/xml' \--header 'Accept: application/xml' \--data '<disk_attachment>  <bootable>false</bootable>  <interface>virtio</interface>  <active>true</active>  <disk>    <description>My disk</description>    <format>cow</format>    <name>mydisk</name>    <provisioned_size>8589934592</provisioned_size>    <storage_domains>      <storage_domain>        <name>mydata</name>      </storage_domain>    </storage_domains>  </disk></disk_attachment>' \https://myengine.example.com/ovirt-engine/api/vms/007/diskattachments

Разумеется, это POST и он вызывается в контексте конкретной ВМ (007). Нам не приходится дополнительно указывать в теле запроса ID ВМ, он у нас просто есть по факту контекста. Отметим, что можно совместить запросы создания ВМ и прикрепления диска.  Тело запроса будет выглядеть примерно так:

<vm>    <name>myvm</name>    <template id="9e7eb3f3-288e-4099-86b9-eac5fe5e1f54"/>    <cluster>        <name>SDN</name>    </cluster>    <disk_attachments>        <disk_attachment>            <disk id="0ed99f7b-e5ed-4754-bff0-1c76d8592dfc">                <format>cow</format>                <sparse>false</sparse>                <storage_domains>                    <storage_domain id="8cab2b6e-8882-4788-8cd4-97c464f380d6"/>                </storage_domains>            </disk>        </disk_attachment>    </disk_attachments></vm>

Продолжим изыскание и попытаемся получить ВМ со списком дисков. Для этого нам предоставляется механизм follow, с помощью которого мы можем одним запросом получить ВМ и связанную сущность — подключения дисков. Пример из документации:

GET /ovirt-engine/api/vms/123?follow=disk_attachments<vm id="123" href="/ovirt-engine/api/vms/123">  ...  <disk_attachments>    <disk_attachment id="456" href="/ovirt-engine/api/vms/123/diskattachments/456">      <active>true</active>      <bootable>true</bootable>      <interface>virtio_scsi</interface>      <pass_discard>false</pass_discard>      <read_only>false</read_only>      <uses_scsi_reservation>false</uses_scsi_reservation>      <disk id="789" href="/ovirt-engine/api/disks/789"/>    </disk_attachment>    ...  </disk_attacments>  ...</vm>

Как мы видим, получить одним вызовом цельный объект без дополнительного указания ID у нас тоже получается. Очевидно, при удалении все связанные подсущности аттачей будут тоже удалены — проблемы особой нет. 

Остается вопрос с UPDATE. Следует заметить, что follow по зависимым сущностям не может быть применен в обновлении, он используется строго для операций чтения. Мы можем обновить основной объект ВМ, а затем обновить зависимые подсущности. 

Итого, вспомним логику Terraform в отношении некоей сущности и посмотрим на реализуемость задумки в контексте API oVirt:

  • CREATE — есть возможность создать виртуальную машину сразу с прикрепленным диском — API технически такое позволяет;

  • READ — есть возможность считать объект ВМ вместе с зависимой подсущностью связи с дисками;

  • UPDATE — нет возможности одним вызовом прислать обновленный облик объекта ВМ с подчиненной сущностью, follow такое не позволяет;

  • DELETE — есть возможность удалить сущность с автоматическим сносом всех подчиненных. 

Terraform самостоятельно реализует эту логику — вычисляет дельту и делает запрос с текстом изменений. Попытка наложить это напрямую на REST API не сработает, нет возможности обновить ресурс с зависимыми сущностями. В таком контексте выделение логики аттачей дисков в отдельный ресурс — это решение проблемы CRUD на ВМ, но его нельзя назвать хорошим по комплексу причин:

  • Это неудобно — аргумент слабый для разработчика, но очень сильный для заказчика. У всех в ВМ, а у вас отдельно — это придется объяснять;

  • Смена диска это не просто замена пары строк в БД. Это все же некие действия с процессом виртуальной машины, если она запущена. Не все интерфейсы дисков, которые поддерживает oVirt, допускают замену диска «на горячую»;

На самом деле второй пункт красной линией проходит через весь провайдер. Для выполнения некоторых операций необходимо изменять состояние ВМ, которая является отдельным ресурсом. Итоговый процесс выглядит так:

  1. В манифесте в ресурсе запуска ВМ объявить её остановленной;

  2. Выполнить terraform apply, зарегистрировать выключение ВМ;

  3. Отредактировать подключение диска. Указать ВМ как запущенную в ресурсе запуска ВМ;

  4. Выполнить terraform apply.

Таким, образом часть операций отслеживания состояний берет на себя оператор, что не очень приемлемо. 

Кратко рассмотрим другой интересный процесс — запуск ВМ. Напомню, в terraform-provider-ovirt он реализуется отдельным ресурсом, но в составе объекта ВМ есть поле state. Это прямое следствие REST-модели виртуальной машины — запуск выполняется с помощью метода /start. Не изменением поля state, а вызовом экшена. Для Terraform c его CRUD подходом «было-стало» это серьезный вызов. 

Пытаемся исправить

Первые тесты открытого провайдера в отношении zVirt не показали ничего хорошего. Мы поняли, что для достижения адекватного уровня сервиса подобную связку предстояло чинить. Наша команда занялась этим, и в ходе попыток довести концепцию открытого провайдера до работоспособного состояния мы реализовали самые разные инженерные решения. Обобщенно можно выделить следующие направления работ:

  • Реализация валидаций;

  • Реализация отсутствующей логики;

  • Реализация виртуальных полей; 

  • Изменение состояний ВМ «под задачу».

Валидации призваны решить проблему «ложно положительных» запусков оригинального провайдера. Недостаток предварительных проверок позволяет ему успешно пройти стадию Plan и затем аварийно завершиться уже в процессе, в худшем случае уже создав часть ресурсов и оставив систему в подвисшем состоянии. Не все из них удается выполнить на этапе планирования, не все из них легко вычислимы (хватит ли ресурсов на запуск ВМ в момент apply?), но львиную долю наиболее неприятных опечаток удается отсеять достаточно надежно. Были реализованы проверки на корректность показателей ресурсов, валидность переданных UUID и ссылок и прочее, что позволило «упасть» до начала работы, не оставив после себя противоречивую инфраструктуру. Тем не менее, многие проверки может спокойно выполнить только REST API, и этап начальных валидаций будет пройден с последующей ошибкой от API. Здесь мы добавляем корректную обработку такой ситуации и корректную остановку процесса. 

Реализовать отсутствующую логику — значит прямолинейно и последовательно дорабатывать отдельные поля ресурсов. Заказчик хочет корректное управление оперативной памятью — поля добавляют, для них прописывается логика. В пределах отдельного ресурса задача достаточно тривиальная с поправкой на редкие особенности oVirt REST API. 

Реализация виртуальных полей — это уже шаг в сторону от принятых оригинальными разработчиками архитектурных решений. Заказчик хочет управлять состоянием машины с помощью поля state — указывать stopped, started, rebooted, run_on_create (!) и получать результат без отдельных ресурсов. Здесь мы пришли к реализации виртуального поля, определяемого пользователем и хранимого в контексте Terraform.

В момент исполнения плана происходит обращение к API, интерпретация состояния ВМ и сверка с указанным в плане, в зависимости от результата сравнения вызываются методы запуска\остановки\перезагрузки ВМ. Интересный запрос на тему run_on_create — заказчик хочет только начально запускать ВМ, а если она была внешними средствами остановлена — не запускать при обновлении манифеста. 

А вот попытка реализовать сквозное изменение состояния ВМ для обеспечения работы прочих ресурсов уже стала переломным моментом. Рассмотрим на примере ресурса подключения дисков. Он на вход принимает ID ВМ, ID диска и выполняет соответствующую операцию. Не все типы подключений поддерживают смену «на горячую», например, традиционные, не виртуализированные реализации SATA.

Логичная попытаться выйти из положения так:

  • Обратиться в API и узнать статус данной ВМ. Если не запущена, выполнять аттач;

  • Если запущена, вызвать метод остановки. Дождаться смены статуса;

  • Выполнить аттач;

  • Выполнить запуск ВМ, если она была остановлена, вернуть предыдущий статус. 

Это сработает, диски будут меняться. А теперь попытаемся переподключить два диска, или диск и сетевой интерфейс. Что будет? Правильно — две перезагрузки. И три, и четыре, в зависимости от количества «желающих». Почему? Потому что обработка ресурсов аттача выполняется в собственном контексте, Диск А не знает, что Диск Б тоже планирует перезагружать ВМ. Поэтому каждый из них пройдет по своему алгоритму, и хорошо, если они не начнут устраивать гонку (а они с высокой вероятностью начнут). 

Мы потратили некоторое количество времени на разработку механизмов синхронизации действий и все же сдались. Это не будет работать, по крайней мере не будет работать надежно. 

Итог

Корень проблем открытого провайдера лежит в плохой совместимости REST API oVirt с концепциями Terraform. Можно выделить следующие пункты:

  • Применение action вместо редактирования полей;

  • Разделение объекта на подсущности. 

Применение экшонов не позволяет применять штатные механизмы Terraform по вычислению дельты и применению изменения, заставляя дописывать дополнительную логику и имплементировать виртуальное поле. Разбиение объекта на подсущности, этакая «нормализация, как в БД», выглядит для пользователя контринтуитивно — но это архитектурное решение, которое было принято много лет назад, с этим придется жить. Есть подозрение, что крупные вендоры принимали во внимание существования Terraform и его особенностей при плановом обновлении API во второй половине десятых годов. Имеет смысл, Terraform — это потребитель API №1. Стоит учитывать при проектировании API в своих продуктах. 

Отмечу, что проблему действий на запуск\остановку ВМ удалось решить благодаря виртуальному полю с некоторой логикой «под капотом». Именно этот принцип и был взят за основу при проектировании нашего собственного провайдера — создание виртуальных объектов, которые будут полностью подчиняться логике Terraform и следовать всем рекомендациям HashiCorp. Нам же предстоит реализовать дополнительный слой трансляции виртуального комплексного объекта в примитивы REST API oVirt. Об этом я расскажу в следующей статье. 

ссылка на оригинал статьи https://habr.com/ru/articles/1048046/