Автоматизируем деплой в vCloud с помощью Terraform и cloud-init

от автора

Предыстория: выбирали сертифицированное облако для всякой там сертифицированной жизни. Остановились на кое-каком 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.

Если что-то не выходит, обратите внимание на следующие ресурсы:


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *