Автоматизируем деплой в 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:
-
-
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 с набором данных, а также с пропертями типа таких:
Ага! Значит, 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.
Если что-то не выходит, обратите внимание на следующие ресурсы: