Привет, Хабр. Я Михаил Фучко, технический продакт-менеджер SDN и Terraform в команде zVirt. Я продолжаю серию статей о пути, который мы проделали в процессе разработки собственного провайдера инфраструктуры для Terraform. В предыдущей части мы поговорили о принципиальных причинах неудач и проблем открытого провайдера terraform-provider-ovirt, сделали выводы. Пятая статья цикла будет посвящена ключевым решениям, принятым в ходе разработки собственного закрытого провайдера инфраструктуры zVirt для Terraform.
Эта статья может быть полезна всем, кому предстоит разработка провайдера под специфичный API, слабо совместимый с концепциями Terraform. Мы разберем основные архитектурные решения, обозначим сознательные концептуальные ограничения и приведем пример построения не самой тривиальной «системы поверх системы» для улучшения пользовательского опыта.

В предыдущей серии
В прошлой статье мы установили, что главным источником бед открытого провайдера стал REST API, а точнее — решения, принятые при его проектировании. Все достаточно просто: REST API oVirt (унаследованный zVirt) создавался задолго до оформления Terraform как самого желанного пользователя API любого инфраструктурного продукта. Его ключевые проблемы:
-
Упор на применение Action-вызовов вместо полей состояния. Terraform предпочитает смотреть содержимое поля и устанавливать одно значение вместо другого, а не вызывать специализированный URL;
-
Разбиение сущности на подсущности со слабой связностью. Terraform считает, что объект — самостоятельная изолированная сущность, ему тяжело установить необходимость для проведения некоей операции последовательно через несколько формально независимых (в его логике) объектов.
Поскольку обновление API не является решением на данном этапе (хотя учесть пожелания TF при проектировании новых вызовов нужно), мы решили реализовать систему виртуальных объектов. Они должны в сторону пользователя демонстрировать желаемый (и привычный!) облик виртуальной машины и других ресурсов, а в сторону провайдера — выполнять комплексные обращения к API, реализуя нетривиальную логику.

Эту статью хочется посвятить виртуальной машине. На примере самого важного ресурса мы рассмотрим те идеи и приемы, которые пришлось применять для достижения комфортного пользовательского опыта.
Дизайн виртуальной машины
Начать хочется с небольшого «философского» обоснования облика объекта. Одной из важных особенностей трансформации инфраструктуры из традиционного облика (серверная виртуализация, on-premises) в cloud-native стало изменение отношения к ВМ. В англоязычной литературе этот сдвиг определяют как переход от «pet» (домашнее животное) к «cattle» (скот). Сама ВМ технически осталась прежней (те же QEMU-KVM процессы что в облаке, что в серверной), но изменилась логика работы с ней:
-
Pet — это ценный актив, изменение состояния которого является инцидентом, потенциально угрожающим непрерывности бизнеса. ПО внутри такой ВМ обновляется через специальные процедуры, такие ВМ проходят резервное копирование, а в случае неполадок спасаются силами инженеров поддержки;
-
Cattle — расходный материал. Если с ВМ что-то не так, она сносится и создается заново. Софт в ней можно не обновлять — достаточно просто развернуть ВМ с новой версией ПО, а ВМ со старой версией после миграции удалить. Такие ВМ создаются массово, часто автоматически (операции автоскейлинга и подобные), удаляются и перезагружаются тоже автоматически и не вызывают особых эмоций. Резервное копирование таких «эфемерных» ВМ зачастую лишено смысла — ее проще развернуть заново.
Несложно предположить, как относится к виртуальным машинам типовой провайдер Terraform. Не можем исправить? Давай снесем и сделаем заново! Но мы помним, что zVirt — это решение серверной виртуализации, а значит виртуальная машина в нашем случае — ресурс ценный, важный и уж точно не расходник. Terraform с его стремлением к прекрасному через прикладную евгенику явно не стыкуется с ожиданиями заказчика. Поэтому часть принципиальных решений необходимо «пролить» через весь провайдер, а именно:
-
Операция удаления виртуальной машины должна быть по умолчанию ограничена, так как ВМ занята в работе бизнеса / обеспечении бизнеса, а значит ее удаление чревато локальной катастрофой;
-
Перезагрузка ВМ, хоть и является менее опасным инцидентом, тоже крайне нежелательна, следовательно, по умолчанию и без явного разрешения она должна быть запрещена.
С этими вводными можно приступить к начальному дизайну объекта виртуальной машины. Пользователи хотят «как привычно», поэтому обратим внимание на объект ВМ из другой инфраструктурной среды. Мне ближе Google Cloud Platform — начнем оттуда (пример из документации HashiCorp к профильному провайдеру):
resource "google_compute_instance" "default" { name = "my-instance" machine_type = "n2-standard-2" zone = "us-central1-a" tags = ["foo", "bar"] boot_disk { initialize_params { image = "debian-cloud/debian-11" labels = { my_label = "value" } } } // Local SSD disk scratch_disk { interface = "NVME" } network_interface { network = "default" access_config { // Ephemeral public IP } } metadata = { foo = "bar" } metadata_startup_script = "echo hi > /test.txt"}
Что мы видим и что отличается от традиционного подхода oVirt:
-
Виртуальная машина — единый целостный объект, готовый к работе. Характеристики ВМ, стартовые скрипты, диски разных видов и сетевые интерфейсы — все в одном месте и с одним жизненным циклом, в отличие от oVirt-концепции;
-
Есть возможность создавать диски прямо из интерфейса ВМ, хотя вполне приемлемым было бы подключать созданные отдельным ресурсом диски. GCP так умеет, но предоставляет два варианта. Также доступен вариант подключения в виде отдельного ресурса — как в oVirt.
Итого нам требуется интегрированный ресурс виртуальной машины, который будет включать в себя дисковые, сетевые и инициализационные операции, будет защищен от нештатного удаления и неплановой перезагрузки при изменении. Приступаем.
Работа с дисками
Рассмотрим, какие диски мы можем встретить в типовой виртуальной машине:
-
Унаследованные из шаблона. Все ВМ создаются на базе шаблона (даже если он Blank), шаблон может иметь диски, один из которых будет загрузочным — готовый сценарий, необходимо изменить размер унаследованного загрузочного / дата диска. Информацию об этих дисках необходимо разведывать;
-
Управляемые Terraform. Подключаются из манифеста, TF владеет всей полнотой данных о них;
-
Подключенные вручную. Интересный кейс о том, как TF может обрабатывать ситуации, когда диск был подключен пользователем самостоятельно и передан в управление TF. Был выделен в отдельную категорию из-за специфичности обработки.
В рамках ресурса виртуальной машины мы ввели блок disk_attachment, способный управлять подключением дисков. Чтобы разделить разные подходы к работе с перечисленными выше типами, используем следующую классификацию: нативные и управляемые (native- и manage-) диски / подключения дисков.
Native-подключения дисков решают задачу доступа к дискам, созданным за пределами TF — унаследованными от шаблона или подключенными вручную (две категории оказались неотличимы с точки зрения получения данных и было решено их объединить в один механизм управления). В основе адресации таких дисков лежит поле alias — человекочитаемая метка, устанавливаемая пользователем для своего удобства. Почему alias?
-
В момент создания виртуальной машины диски, унаследованные от шаблона, будут созданы заново — а значит, их ID будет известен после завершения создания ВМ;
-
Если нет ID — нет возможности адресовать диски шаблона и заранее, до создания, провести по ним операции изменения. Например, увеличить диск или сменить интерфейс;
-
Alias полностью наследуется от шаблона и совпадает с пользовательской логикой: скорее всего, загрузочный диск будет помечен как boot, а диск данных будет содержать слово Data. Учитывая необходимость готовить шаблоны ВМ для автоматизированного развертывания заранее, это вполне нормальное требование.
У решения есть очевидные минусы, которые были приняты как данность:
-
Нельзя переименовать диск, который ты адресуешь по имени;
-
Нельзя отключить унаследованный диск;
-
Нельзя уменьшить унаследованный диск (поскольку это деструктивная операция);
-
Alias диска в пределах одного шаблона должен быть уникален. Обнаружение нескольких дисков с одинаковым alias — остановка работы TF;
-
Из-за особенностей наследования к редактуре доступен лишь ограниченный набор свойств: size, backup, storage_domain_id, interface.
Данный тип подключения позволяет изменять размер унаследованных дисков, перемещать их в другой домен хранения, менять интерфейс — то есть выполнять штатные пользовательские задачи, легко решаемые из UI.
Managed-подключения ориентируются на работу с дисками, полностью созданными средствами Terraform. Они идентифицируются по ID — он будет получен TF при создании диска, а до этого момента TF может использовать ID как переменную, значение которой он узнает позже (это вполне в духе TF).
Раз диск создается средствами TF, он и обслуживаться будет отдельным ресурсом, следовательно, можно отказаться от дублирования функционала и не разрешать его перемещение или, например, изменение размера.
Получается, здесь мы немного отступаем от изначального плана — создания полноценного движка управления дисками в теле ВМ. В перспективе реализацию ограниченного управления жизненным циклом дисков в рамках disk_attachment можно рассмотреть, но пока она кажется избыточной. Запишем в особенности провайдера.
Разделение дисков выполняется по наличию alias/id. Это взаимоисключающие параметры, поэтому в зависимости от обнаруженного признака допустимо редактирование разного набора свойств.

resource "zvirt_vm" "vm" { name = "vm" cluster_id = data.zvirt_cluster.cluster.id template_id = data.zvirt_template.template.id// Пример нативного диска. // Идентифицируется по алиасу “boot”. // Подключается по интерфейсу virtio_scsi disk_attachment { alias = "boot" interface = "virtio_scsi" }// Пример нативного диска.// Идентифицируется по алиасу “data”// Подключается по интерфейсу virtio_scsi, увеличивается до 25 Гб. disk_attachment { alias = "data" interface = "virtio_scsi" size = 25 * pow(1024, 3) // 25 GiB }// Пример управляемого диска// Идентифицируется по наличию ID. // Можно переопределить только подключение, прочие параметры см. оригинальный ресурс диска disk_attachment { id = "f6e2202f-6ebc-4634-92ca-b970f0577a6f" interface = "virtio_scsi" }}
Данное решение нельзя считать идеальным, его главное преимущество — возможность обойтись без модификации существующего REST API, унаследованного от oVirt. Представляет определенный интерес отвязка от параметров самого диска ( alias — это поле диска) к дополнительным полям конструкции disk_attachment.
Работа с сетью
Если интеграция дисков осложняется их двойственной природой и наследованием от шаблона, то работа с сетью осложняется наличием комплексной связи нескольких объектов. В традиционном сетевом подключении схема достаточно прямолинейна, а вот наличие SDN подразумевает существование определенных дополнительных сущностей. Разберем подробнее, как сетевая связность ВМ реализована в zVirt с учетом программно-определяемой сети.
Начнем с технического описания объектов zVirt, участвующих в прохождении пакета. Возьмем самый сложный вариант: ВМ должна быть подключена к VLAN X через SDN (L2-подключение, сетевая архитектура осталась прежней, но получены все плюшки SDN, любимый вариант заказчиков). Полная схема взаимодействия объектов будет выглядеть следующим образом:

По шагам разберем процесс формирования столь нетривиальной цепочки. Идем справа налево:
-
Пользователь создает в обычной сетевой оснастке zVirt традиционную сеть zVirt с тегом желаемого VLAN — по условию задачи это X. Назовем данную сеть Vlan;
-
В интерфейсе SDN в оснастке внешних сетей пользователь создает внешнюю сеть (назовем ее MyVLAN X) и в качестве базы указывает созданную традиционную сеть VlanX. SDN подтянет всю необходимую информацию о VLAN из материнской сети. Так же выбираем опцию «Сеть ВМ», чтобы к нашей внешней сети можно было подключать пользовательские ВМ;
-
В моменте создания внешней сети MyVlanX (поскольку стоит флаг «Сеть ВМ») будет создана традиционная сеть zVirt специального типа — аватар для SDN-сети. Унаследованная от oVirt архитектура подразумевает подключения ВМ только к классическим сетям, поэтому существование таких «аватаров» позволяет не ломать логику. Данная традиционная сеть с именем MyVLAN X (совпадающим с именем оригинальной SDN-сети) будет создана автоматически;
-
В интерфейсе SDN в оснастке IPAM мы можем создать объект подсети, который описывает принятую адресацию в нашей внешней сети (для упрощения менеджмента адресов и их доставки в ВМ). Шаг опциональный, но популярный;
-
Для автоматически созданной традиционной сети MyVLAN X, которая является аватаром для подключения к SDN-сети, создаются vNIC-профили. Они позволяют кастомизировать подключение к одной и той же сети: можно подключать ВМ с помощью набора параметров А, а можно — параметров Б. Для иллюстрации предположим, что пользователь создал еще один vNIC-профиль;
-
В интерфейсе виртуальной машины мы можем выбрать vNIC-профиль (пусть будет №1) от традиционной сети MyVLAN X, которая является аватаром SDN-сети MyVLAN X;
-
В момент подключения ВМ к сети SDN, создается Логический порт — точка подключения ВМ к SDN, которую можно конфигурировать, то есть указывать IP-адрес, правила микросегментации, зеркалирования и т.п.
Итого из всей схемы конкретно в ВМ применяются следующие конфигурации:
-
Конфигурация самого подключения, «физики» — выбор vNIC, интерфейса подключения (virtio, e1000 и т.п.) и имени (nic1);
-
Конфигурация подключения SDN — работа пакетного фильтра, выбор групп безопасности, разрешение на самостоятельное управление адресацией;
-
Конфигурация адресации в SDN — указание объекта подсети и IP-адреса в ней.
Решаем организовать итоговую схему с помощью вложенных блоков:
resource "zvirt_vm" "vm" { name = "vm" cluster_id = data.zvirt_cluster.cluster.id template_id = data.zvirt_template.template.id// Вложенная конструкция сетевого интерфейса// Определяет профиль подключения, отображаемое (в zVirt) имя и тип подключения// Далее - конфигурация SDN. nic { vnic_profile_id = data.zvirt_vnic_profile.vnic_profile.id name = "nic1" interface = "virtio"// Вложенный блог конфигурации SDN. // Поддержка групп безопасности, ID этих групп и т.п. sdn_port { security_groups_enabled = true security_group_ids = [ "600f8bba-9479-4d3f-b1de-52c43950bc00", "fdba5d33-4b2b-4243-94bf-300105fae8ce" ]// Блок адресации SDN. // ID объекта подсети и адрес в ней fixed_ip { subnet_id = "005fb3cf-666d-4a27-ba4e-31a329fa13ea" ip_address = "192.168.0.1" } // Виртуальная машина имеет право менять MAC и IP-адрес. mac_learning = true } }}
Решение не самое удачное. Оно осложняет возможность итеративно пройтись по адресам, например, при генерации файла инвентаря для Ansible (решается преобразованием структур в списки). Но на данном этапе выполняет свою задачу.
При необходимости ВМ может иметь несколько интерфейсов (с помощью простого повторения вложенного блока). SDN-порт у интерфейса может быть только один, фиксированный адрес — тоже только один. Это особенности не провайдера, но подсистемы SDN в текущем виде. По мере развития эти особенности будут сниматься.
Контроль перезагрузки
Неоднократно упомянутый принцип важности конкретной ВМ и ее состояния в классической серверной виртуализации обязывает нас очень внимательно отнестись к контролю за ВМ в ходе самой непредсказуемой операции — обновления. Часть параметров — имя, комментарий и т.п. — можно изменить без перезапуска процесса ВМ. Часть же, например, уменьшение ОЗУ, требует рестарта.
Решение реализуется достаточно просто: существует единый флаг необходимости перезагрузки, и все параметры, поддерживаемые ресурсом ВМ, проверяются на его необходимость. Таким образом добавление новых параметров всегда идет через оценку необходимости перезагрузки — выставлять флаг при изменении или нет. Итоговое разделение блоков параметров на два лагеря выглядит следующим образом:
|
Требует перезагрузки |
Не требует перезагрузки |
|
Конфигурация процессора — сокеты, ядра, потоки |
Имя и описание ВМ |
|
Конфигурация памяти — объем, максимальный объем, гарантированный объем и балунинг |
Состояние питания — для включения, выключения и run_on_create |
|
Параметры высокой доступности |
Поведение при приостановке и аренде |
|
Конфигурация ОС — тип ОС, BIOS, поддержка TPM |
Защита от удаления |
|
Параметры инициализации |
Изменение сетевых интерфейсов — добавление, удаление, изменение свойств (имя, тип интерфейса, профиль vNIC) и управление портами SDN |
|
|
Операции с папками ВМ |
Отдельно следует разобрать работу с дисками:
|
Требует перезагрузки |
Не требует перезагрузки |
|
Добавление / удаление дисков без поддержки горячего подключения — все интерфейсы, кроме virtio или virtio_scsi |
Добавление / удаление дисков с поддержкой горячего подключения — интерфейсы virtio или virtio_scsi |
|
Изменение свойств подключения диска, включая изменение атрибута interface, означающего смену типа используемого интерфейса диска |
Увеличение нативного диска |
|
|
Изменение свойства active |
|
|
Изменение атрибутов резервного копирования нативных дисков |
В случае изменения параметра, вызывающего перезагрузку, выставляется внутренний флаг reboot_required, затем его значение сравнивается с задаваемым пользователем параметром reboot_protected. В зависимости от результата операции перезагрузки вызываются или нет.
Эпилог
Подводя итоги цикла статей, хочется отметить невероятную энергию нашего ведущего разработчика, ставшего «крестным отцом» проекта: его неисчерпаемая энергия и мощный инженерный бэкграунд привели к становлению провайдера в облике, заточенном на решение задач, а не на удобство программиста. В конце концов, любой проект — это люди и их страсть, и наш не стал исключением.
Текущий провайдер версии 1.4.0 уже активно эксплуатируется в промышленной среде в составе сложного многоуровневого контура IaC — по принципу drop-in он заменил аналогичный провайдер для VMware. Проект заслуженно носит звание «combat proven» и рекомендуется к активному внедрению всем, кто решает задачи автоматизации.
Замечу, что есть у провайдера начало, но нет у провайдера конца. В данный момент идет интенсивная разработка версии 2.0.0, где выполнена масштабная переработка внутренних механизмов работы виртуальных объектов, устранены шероховатости при работе с SDN и актуализирована поддержка сетевых ресурсов.
До новых встреч!
P.S. Предыдущие статьи цикла можно почитать здесь:
ссылка на оригинал статьи https://habr.com/ru/articles/1064846/