
Привет, Хабр. Я Михаил Фучко, технический продакт-менеджер 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, допускают замену диска «на горячую»;
На самом деле второй пункт красной линией проходит через весь провайдер. Для выполнения некоторых операций необходимо изменять состояние ВМ, которая является отдельным ресурсом. Итоговый процесс выглядит так:
-
В манифесте в ресурсе запуска ВМ объявить её остановленной;
-
Выполнить terraform apply, зарегистрировать выключение ВМ;
-
Отредактировать подключение диска. Указать ВМ как запущенную в ресурсе запуска ВМ;
-
Выполнить 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/