Предыстория: выбирали сертифицированное облако для всякой там сертифицированной жизни. Остановились на кое-каком B2B-колоссе, руководство заключило договор, и отделу SRE пришлось работать с облаком на основе VMware vCloud Director. И, как подобает секте свидетелей Infrastructure as Code, хотелось поменьше сидеть в веб-морде облака и больше -- в конфигурациях какого-нибудь Ansible и Terraform.
Эта статья - плод нескольких вечеров девопсера и бог знает скольких дней и ночей CTO. По горячим следам, поэтому, возможно, она несколько скомканная. Тем не менее, если вы столкнулись с облаком на основе vCloud - будет интересно.
Provisioning
Прежде чем перейдём к подготовке образа ВМ, поговорим немного про provisioning виртуалок.
Вот, скажем, есть фаза установки убунты из iso-образа, и есть фаза прогона ансиблового плейбука. Между ними - фаза заведения пользователей, установкой таймзоны и записей ssh-ключей. Как это сделать автоматизированно?
Или другой пример, более привычный для облаков: есть склонированная виртуалка, в которую надо прописать хостнейм, те же ключи, в конце-концов, задать IP-адрес, который вместо DHCP генерируется в интерфейсе облака. Тоже руками делать?
Или... составлять скрипты и вставлять их в раздел Customization script, в случае с vCloud?
"Нет, это всё бред" - подумали в Canonical, и сотворили cloud-init, который на сегодняшний день делает provisioning виртуалок во всех мейнстримных облаках, от Amazon EC2, GCP и Azure до Openstack и того же VMware.
Cloud-init работает с instance-data, документом, состоящим из трёх компонент:
Cloud metadata, содержит важные данные от облака. Например, instance id, адреса сетевых интерфейсов, хостнеймы.
Vendor data (опционально), несёт в себе дополнительные настройки от облака. Например, каких пользователей пользователь через веб-формы облака приказал создать, или какие пакеты надо доустановить.
User data (опционально). По формату идентично vendor data, но юзердата состоит из YAML-а, который пользователь скармливает cloud-init в свободной форме.
Пример user-data:
#cloud-config groups: # в группе ubuntu должны быть пользователи root и sys - ubuntu: [root,sys] # пустая группа - cloud-users users: - default - name: foobar primary_group: foobar groups: users lock_passwd: false passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/ - name: barfoo sudo: ALL=(ALL) NOPASSWD:ALL groups: users, admin lock_passwd: true ssh_authorized_keys: - <ssh pub key 1> - <ssh pub key 2> write_files: - encoding: b64 content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4... owner: root:root path: /etc/sysconfig/selinux permissions: '0644' - content: | SMBDOPTIONS="-D" path: /etc/sysconfig/samba ca-certs: trusted: - | -----BEGIN CERTIFICATE----- YOUR-ORGS-TRUSTED-CA-CERT-HERE -----END CERTIFICATE----- - | -----BEGIN CERTIFICATE----- YOUR-ORGS-TRUSTED-CA-CERT-HERE -----END CERTIFICATE----- manage_resolv_conf: true resolv_conf: nameservers: ['8.8.4.4', '8.8.8.8'] searchdomains: - foo.example.com - bar.example.com domain: example.com options: rotate: true timeout: 1
Полный список встроенных в cloud-init возможностей конфигурации (или модулей) доступен тут.
VMware и cloud-init
Подготавливаем шаблон
Возьмём за основу Ubuntu 20.04.3.
Загружаем ISO
Заходим в vCloud Director, там открываем меню сверху слева и переходим в Libraries:

Из Libraries попадаем в Media & Other, где нажимаем на ADD:
В качестве каталога выбираем любой, в котором в принципе можно хранить образы, выбираем ISO-файл на локальном диске, name подставится автоматически.
Как только медиа загрузится (задача загрузки будет висеть в нижней части экрана, в Recent Tasks), создаём пустой vApp, цепляем к нему какую-нибудь routed-сетку, дальше создаём VM со следующими параметрами:

В принципе, главное, чтобы Memory было не меньше гигабайта, и чтобы Operating System было Ubuntu Linux (64-bit)

Диск будет один, размер - минимальный, который вы собираетесь давать виртуалкам. И в качестве сети достаточно простой routed network, чтобы был выход в интернет для обновлений.
Установка и настройка дистрибутива
Дальше устанавливаем убунту как вам хочется, из скриншотов ограничусь одним-единственным:

cloud-init сам по себе не умеет проводить операции с LVM-разделами. Так что, если вы хотите в дальнейшем расширять корневой раздел через cloud-init -- оставьте эту галочку нетронутой.
После установки и ребута заходим в root shell, делаем там apt update-upgrade:
sudo -i apt update apt upgrade
Теперь, собственно, подготовка образа к cloud-init.
Удаляем сетевые конфиги netplan и cloud-init, созданные установщиком, чтобы не мешались по дороге:
rm /etc/netplan/00-installer-config.yaml rm /etc/cloud/cloud.cfg.d/50-curtin-networking.cfg
А также говорим cloud-init не пытаться перетянуть одеяло с vmware tools. Идея в том, чтобы сначала open-vm-tools задавал сетевые параметры, а cloud-init делал всё остальное:
echo 'disable_vmware_customization: true' > /etc/cloud/cloud.cfg.d/91_vmware_cust.cfg
Настраиваем cloud-init через dpkg так, чтобы был только OVF datasource. То есть кнопкой пробел снимаем все галочки, кроме той, что рядом с OVF:
dpkg-reconfigure cloud-init
Наконец, удаляем данные cloud-init, чтобы при следующей загрузке системы он повторно инициализировался. Бонусом можно удалить логи open-vm-tools, чтобы впоследствии их было проще читать:
cloud-init clean --logs rm -r /var/log/vmware*
Гасим виртуалку через poweroff и также делаем ей Power Off в vCloud. Теперь она готова к шаблонизации.
Создание шаблона в vCloud
Здесь всё просто: открываем Actions нашего vApp и жмём на Add to Catalog, в открывшейся форме выбираем нужный каталог, в имени пишем что-нибудь понятное, типа ubuntu-20-04, и в поле When using this template выбираем Customize VM settings.
Мой девопс делает вкуснейшие терраформ-модули
Рецепт усреднённый.
Берётся стабильный (1.0.4) терраформ, 0.14 не про него, на него ставится vcd-провайдер:
terraform { required_providers { vcd = { source = "vmware/vcd" version = "3.3.1" } } required_version = "= 1.0.4" } variable "vcd_user" { type = string } variable "vcd_pass" { type = string sensitive = true } provider "vcd" { user = var.vcd_user password = var.vcd_pass auth_type = "integrated" org = "org_name" vdc = "vdc_name" url = "https://vcd.someawesomecloud.ru/api" }
Создаёт в той же папке какой-нибудь простой userdata.yaml:
#cloud-config users: - default - name: igor ssh_authorized_keys: - "ecdsa-sha2-nistp256 <...> igor@laptop" groups: sudo shell: /bin/bash sudo: ['ALL=(ALL) NOPASSWD:ALL']
И посыпает виртуалкой:
resource "vcd_vapp_vm" "TestVm" { vapp_name = "vappName" name = "TestVm" computer_name = "cloud-vm" memory = 2048 cpus = 2 cpu_cores = 1 os_type = "ubuntu64Guest" catalog_name = "catalogName" template_name = "ubuntu-20-04" customization { enabled = true } network { type = "org" name = "mainbridge" ip_allocation_mode = "DHCP" is_primary = true } }
И... Стоп, а вонища user data где?
Загадка о OVF-датасорсе cloud-init
Сейчас будет рассказ о том, почему, собственно, этот туториал и был написан.
Как нам передать user data в cloud-init?
В terraform-ресурсе vcd_vapp_vm есть такой аргумент, как guest_properties:
guest_properties - (Optional; v2.5+) Key value map of guest properties
Один из ключей к разгадке получен. Значит, в виртуалку можно передать набор ключ-значение, осталось понять, какие именно ключи.
Обратимся к документации OVF-датасорса:
For further information see a full working example in cloud-init’s source code tree in doc/sources/ovf
Отлично, живой пример! Открываем сорцы, и...

Примеры от 12 года, разумеется, без терраформа или чего-то такого.
Если вы вобьёте в любимый поисковик "vmware cloud-init", то найдёте несколько статей в блогах про то, как накурить cloud-init в vSphere через проперти guestinfo.metadata и guestinfo.userdata.
То есть, мы должны соорудить что-то типа такого?
guest_properties = { "guestinfo.userdata" = base64encode(file("${path.module}/userdata.yaml")) "guestinfo.userdata.encoding" = "base64" }
Нет. К сожалению, если засунуть YAML как guestinfo.userdata, то cloud-init поведёт себя так, словно ничего в OVF не обнаружил.
Днями и ночами я копался над этой загадкой, пока не сдался и не залез в исходники cloud-init. Тут мне пригодилось знание Python и хорошая структурированность кодовой базы cloud-init.
Открываем сорцы. Ищем строку guestinfo. Находим такую функцию:
def transport_vmware_guestinfo(): rpctool = "vmware-rpctool" not_found = None if not subp.which(rpctool): return not_found cmd = [rpctool, "info-get guestinfo.ovfEnv"] try: out, _err = subp.subp(cmd) if out: return out LOG.debug("cmd %s exited 0 with empty stdout: %s", cmd, out) except subp.ProcessExecutionError as e: if e.exit_code != 1: LOG.warning("%s exited with code %d", rpctool, e.exit_code) LOG.debug(e) return not_found
Мимо. Хотя давайте из любопытства выполним команду:
vmware-rpctool 'info-get guestinfo.ovfEnv'
Получим XML с набором данных, а также с пропертями типа таких:
<Property oe:key="guestinfo.userdata" oe:value="..."/> <Property oe:key="guestinfo.userdata.encoding" oe:value="base64"/>
Ага! Значит, cloud-init получает наш набор пропертей, но они его по какой-то причине не устраивают?
Ищем места вызова функции, находим одно:
else: np = [('com.vmware.guestInfo', transport_vmware_guestinfo), ('iso', transport_iso9660)] name = None for name, transfunc in np: contents = transfunc() if contents: break if contents: (md, ud, cfg) = read_ovf_environment(contents) self.environment = contents found.append(name)
В этом же куске кода есть вызов read_ovf_environment(). Посмотрим на него:
# This will return a dict with some content # meta-data, user-data, some config def read_ovf_environment(contents): props = get_properties(contents) md = {} cfg = {} ud = None cfg_props = ['password'] md_props = ['seedfrom', 'local-hostname', 'public-keys', 'instance-id'] for (prop, val) in props.items(): if prop == 'hostname': prop = "local-hostname" if prop in md_props: md[prop] = val elif prop in cfg_props: cfg[prop] = val elif prop == "user-data": # <- !!! try: ud = base64.b64decode(val.encode()) except Exception: ud = val.encode() return (md, ud, cfg)
Ага! Значит, cloud-init вычитывает проперти password, seedfrom, local-hostname, public-keys, instance-id и наш драгоценный user-data!
Пробуем прописать такое в Terraform-ресурсе:
guest_properties = { "local-hostname" = "cloud-vm" "user-data" = base64encode(file("${path.module}/userdata.yaml")) }
Зачем мы два раза задаём hostname, через computer_name и через local-hostname, спросите вы? computer_name предназначен для open-vm-tools, который его выставляет с проблемами (я пробовал исправить это согласно workaround-ам, и получил циклическую зависимость юнитов в systemd), а local-hostname - для cloud-init, который умеет всё делать по красоте.
Делаем экспорт логина-пароля в шелл, terraform apply, смотрим, какой адрес у полученной виртуалки в vCloud, пингуем его и пробуем подключиться:
export TF_VAR_vcd_user=username export TF_VAR_vcd_pass=12345678 terraform apply # на запрос подтверждения отвечаем yes (перечитывая план, конечно ping 10.10.0.155 <куча destination host unreachable> 64 bytes from 10.10.0.155: icmp_seq=40 ttl=64 time=2048 ms 64 bytes from 10.10.0.155: icmp_seq=41 ttl=64 time=1024 ms 64 bytes from 10.10.0.155: icmp_seq=42 ttl=64 time=0.375 ms igor@gateway:~$ ssh igor@10.10.0.155 Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64)
Если удалось подключиться, то поздравляю - всё сработало как надо.
Всё! Мы получили работающий шаблон Ubuntu, который можно раскатать в облако на основе vCloud Director.
Если что-то не выходит, обратите внимание на следующие ресурсы:
Чятик cloud-init, где обитают разработчики и активные пользователи
VMware datasource - новый датасорс, который ещё не зарелизили, но попробовать в nigtly-сборках на vSphere можно. У меня vSphere под рукой нет, а на vCloud я его завести не смог.
Доклады и блогпосты для дополнительного изучения cloud-init
Страница vcd-провайдера в Terraform Registry, с документацией и ссылкой на гитхаб
