Автоматизация сборки образов ОС с помощью Packer
Разворачивать операционную систему с ISO-образа удобно, но только руками и только один раз. Если встаёт задача делать это тысячу раз в день на каком-нибудь облаке, невольно задумаешься как автоматизировать процесс.
Я для этого выбрал инструментарий от Hashicorp - Packer, Vagrant и Terraform.
- Packer - инструмент подготовки образов ОС.
- Vagrant - инструмент запуска виртуальных машин при локальной разработке
- Terraform - инструмент оркестрации облачной инфраструктуры
В посте речь пойдёт о моём опыте использования Packer. В отличие от многих других статей, здесь я не просто расскажу как использовать инструмент, но и покажу как решить вполне конкретную задачу, применив совокупность инструментов.
Вкратце
В чём суть?
В любом облаке самый быстрый способ развернуть ОС - использовать шаблон (template). Шаблон - это предварительно настроенная ОС, "золотой образ", которая будет копироваться на диск создаваемой виртуальной машины. Всё просто.
Шаблон можно подготовить кучей способов - установить руками, применить утилиты, предлагаемые облаком или вендором дистрибутива, либо использовать Packer как наиболее универсальное решение.
Почему я выбрал именно Packer?
Инструмент позволяет подготавливать шаблоны ОС под большинство облачных провайдеров и гипервизоров (Amazon, OpenStack, KVM, VmWare), а также делать образы для контейнеров (Docker). Причём делается это всё легко и непринуждённо он хорошо вписывается в пайплайн CI/CD.
Как работает Packer?
На самом деле никакой магии, он просто повторяет то, что пришлось бы делать руками - подключается к гипервизору или облаку, создаёт виртуальную машину, загружается с ISO-диска (или виртуального диска) и выполняет инструкции, указанные в конфигурации. После этих манипуляций появляется шаблон. Всё.
Пример настройки Ubuntu 20.04 для Vagrant под VirtualBox
Далее я постараюсь рассказать о том, как подготовить ОС из ISO-файла для запуска её с помощью Vagrant в VirtualBox.
Буду готовить образ Ubuntu Server с развёрнутым в нём k0s. Весь код вы сможете найти в моём репозитории. Он будет необходим, если вы захотите повторить всё написанное ниже.
Разберёмся с автоматической установкой Ubuntu 20.04
Первым делом нам нужно понять как вообще происходит установка дистрибутива. Если мы делаем это руками, то просто проходимся по визарду, нажимая кнопочки и заполняя формочки. Метод хороший, но нам нужен более контролируемый, подходящий для автоматической установки. В Ubuntu для этого есть две системы - Debian-installer и Subiquity.
- Debian-Installer - старая система, использующаяся на Debian-based дистрибутивах уже многие годы. Подойдёт "олдам" и тем, кто привык работать с конфигурацией preseed. Для её использования нужен дистрибутив с Legacy-инсталлятором. Следует понимать, что для Ubuntu это тупиковый путь и использовать его в Ubuntu 20.04 не рекомендуется.
- Subiquity - новая система, разработанная Canonical. Здесь используются файлы конфигурации autoinstall в формате yaml, что упрощает их чтение. Autoinstall включен в дистрибутив по умолчанию. Именно его я и буду использовать, потому что обожаю
находить приключения на задницуисследовать новые технологии.
Разбираемся с Autoinstall
Ознакомившись с документацией, выясняю что нужно подготовить директорию в которой будут лежать два файла - meta-data и user-data.
meta-data- просто пустой файл, наличие которого триггерит автоматическую установкуuser-data- файл, в котором описывается конфигурация установки в формате yaml. В нём вся магия.
Есть два способа подготовить user-data и оба требуют работы с напильником
- Установить ОС руками и забрать сгенерированный в процессе инсталляции файл конфигурации. Он будет находиться здесь -
/var/log/installer/autoinstall-user-data. Туда попадёт некоторое количество мусора, но вариант, в целом, рабочий. - Прочитать документацию и написать конфигурацию самому.
На практике оказалось что оба варианта - боль и страдание. Во-первых, автоматически сгенерированный конфиг стопроцентно не работает. Во-вторых, документация умалчивает некоторые нюансы, специфичные для среды запуска, что делает её не очень полезной. Через пару часов безуспешных попыток впихнуть в конфиг максимально возможное количество настроек, решено было вынести их на более поздний этап и оставить только минимально рабочий код в user-data:
#cloud-config
autoinstall:
version: 1
early-commands:
- systemctl stop ssh # Отключить SSH-сервер для корректной работы Packer
network:
network: # Два раза, потому что это официально признанный баг
version: 2
ethernets:
enp0s3:
dhcp4: yes
dhcp-identifier: mac
storage:
layout:
name: lvm
apt:
preserve_sources_list: false
primary:
- arches: [amd64, i386]
uri: "http://archive.ubuntu.com/ubuntu/"
- arches: [default]
uri: "http://ports.ubuntu.com/ubuntu-ports"
ssh:
install-server: yes
authorized-keys: []
allow-pw: yes
identity:
hostname: ubuntu-focal
password: "$6$rounds=4096$2LajYFyw2u5q$47i.HErUFUrr3tGkRIFpsjCM0hUdBnLBh2I0uM5T5k0RUlzWx0UxBa3XpkrkrOl9Gu0MrgnpuvEmJ99egYklF0"
username: vagrant
late-commands:
- echo 'vagrant ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/vagrantДля проверки конфигурации я запускал http-сервер из директории с подготовленными файлами следующей командой:
python3 -m http.server 8080
После чего запускал установку ОС вручную. При запуске указывал следующие параметры загрузки ядра:
initrd=/casper/initrd quiet autoinstall ds=nocloud-net;s=http://_gateway:8080/
Если система не показывает Welcome-экран, а сразу начинает установку, значит autoinstall подготовлен корректно. В противном случае предстоит увлекательная отладка user-data вслепую.
Готовим Packer
Теперь, когда мы знаем как подготовить ОС вручную, можно браться за Packer. Нам необходимо подготовить файл конфигурации, который будет описывать параметры виртуальной машины, в которой будет происходить установка, сервисные команды ОС, provision (запуск скриптов после успешной установки ОС) и некоторые другие задачи.
Документация всё это хорошо описывает, мне остаётся только поделиться своим опытом применения инструмента.
Для начала упростим себе жизнь и определимся со структурой проекта. В дальнейшем это поможет более эффективно применять конфигурацию для подготовки других дистрибутивов. Моя структура выглядит так:
ansible/ - provision-скрипты (в моём случае ansible) http/ - файлы конфигурации autoinstall iso/ - исходные iso-дистрибутивы ОС vagrant/ - конфигурация Vagrant vars/ - переменные Packer |-ubuntu.pkrvars.hcl - файл с переменными Packer template.pkr.hcl - основной файл конфигурации Packer
# Переменные
variable "headless" {
type = bool
default = false
}
variable "iso_url" {
type = string
}
variable "iso_checksum" {
type = string
}
variable "ssh_username" {
type = string
default = "vagrant"
sensitive = true
}
variable "ssh_password" {
type = string
default = "vagrant"
sensitive = true
}
variable "vm_name" {
type = string
default = "ubuntu"
}
variable "output_directory" {
type = string
default = "./builds"
}
variable "output_filename" {
type = string
}
variable "http_dir" {
type = string
default = "http"
}
variable "boot_command" {
type = list(string)
}
# Конфигурация бэкэнда сборки
source "virtualbox-iso" "ubuntu" {
# Запускать виртуальную машину с GUI или в фоновом режиме
headless = "${var.headless}"
# Путь и контрольная сумма исходного ISO-образа
iso_url = "${var.iso_url}"
iso_checksum = "${var.iso_checksum}"
# Учётная запись, под которой будет выполняться настройка образа
ssh_username = "${var.ssh_username}"
ssh_password = "${var.ssh_password}"
ssh_timeout = "15m"
# Параметры сохранения образа
vm_name = "${var.vm_name}"
output_directory = "${var.output_directory}"
output_filename = "${var.output_filename}"
# Конфигурация виртуальной машины
vboxmanage = [
["modifyvm", "{{.Name}}", "--memory", "1024"],
["modifyvm", "{{.Name}}", "--cpus", "1"],
]
disk_size = "5120"
guest_os_type = "Ubuntu_64"
# Параметры http-сервера и подключения к нему
http_directory = "${var.http_dir}"
boot_command = "${var.boot_command}"
boot_wait = "5s"
# Команда выключения виртуальной машины по завершению всех операций
shutdown_command = "echo 'vagrant' | sudo -S /usr/sbin/shutdown -P now"
}
# Конфигурация сборки
build {
# Используемые бэкэнды сборки
sources = ["sources.virtualbox-iso.ubuntu"]
# Параметры запуска скриптов подготовки
provisioner "ansible" {
playbook_file = "./ansible/playbook.yml"
}
# Создание box-файла для Vagrant
post-processor "vagrant" {
output = "vagrant/releases/${var.output_filename}_{{.Provider}}.box"
}
# Запуск виртуальной машины из подготовленного образа
post-processor "shell-local" {
inline = ["cd ./vagrant", "vagrant up"]
}
}ubuntu.pkrvars.hcl. Вынести в отдельный файл переменные - хорошая практика. Таким образом мы можем переиспользовать шаблон для других дистрибутивов.
headless = "false"
iso_url = "./iso/ubuntu-20.04.3-live-server-amd64.iso"
iso_checksum = "sha256:f8e3086f3cea0fb3fefb29937ab5ed9d19e767079633960ccb50e76153effc98"
ssh_username = "vagrant"
ssh_password = "vagrant"
vm_name = "ubuntu-focal"
output_directory = "./builds/ubuntu-20.04"
output_filename = "ubuntu-20.04-server-amd64"
http_dir = "./http"
boot_command = [
"<wait><enter><esc><wait5><f6><esc><wait>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs>",
"<wait>",
"/casper/vmlinuz ",
"initrd=/casper/initrd quiet ",
"<wait>",
"autoinstall ",
"<wait>",
"ds=nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ --- ",
"<enter><wait5>"
]Запуск сборки выполняется следующей командой:
packer build -var-file=./vars/ubuntu.pkrvars.hcl template.pkr.hcl
Во время работы откроется окно Virtualbox, в котором Packer автоматически введёт символы из переменной boot_command, после чего будет ожидать возможности подключиться к виртуальной машине по SSH. Когда это произойдёт, будет запущен плейбук Ansible, затем виртуальная машина будет выключена, а её файлы экспортированы в box для Vagrant
Особенность Packer заключается в том, что при ошибках в сборке нельзя будет продолжить с того места, где всё поломалось - придётся запускать сборку по-новой. То есть, если вы допустили ошибку в таске Ansible, нельзя будет начать с шага provision. По этой причине отладка может затянуться на многие часы.
Ещё одна неожиданная особенность сборки на VirtualBox - Packer может рандомно пропускать некоторые символы при наборе boot_command. Как же у меня от этого пригорело! Заметил я это случайно через полчаса отладки, посмотрев внимательно в /proc/cmdline, когда в очередной раз установщик неожиданно загрузился в интерактивный режим. Помогло добавление дополнительных символов <wait> между словами. Вроде как это проблема переполнения буфера в VNC-сессии.
Проверяем работу в Vagrant
Packer создаёт vmdk и ovf файлы в директории builds. Можно импортировать их в VirtualBox и проверить работоспособность вручную. А можно экспортировать эти файлы в box и запустить с помощью Vagrant. За это отвечает секция post-processor в файле template.hcl. По завершении сборки образа, происходит экспорт в директорию vagrant/releases/.
Сама же конфигурация Vagrant хранится в файле vagrant/Vagrantfile:
Vagrant.configure("2") do |config|
# Использование локального файла box
config.vm.box = "ubuntu-k0s"
config.vm.box_url = "releases/ubuntu-20.04-server-amd64_virtualbox.box"
# Параметры виртуальной машины
config.vm.hostname = "k0s.local"
config.disksize.size = "10GB"
config.vm.provider "virtualbox" do |v|
v.cpus = 1
v.memory = "1024"
end
# Запуск подготовительных скриптов после старта виртальной машины
config.vm.provision "ansible" do |ansible|
ansible.playbook = "ansible/playbook.yml"
end
end
Здесь следует уточнить момент, почему мне пришлось использовать Ansible при запуске виртуальной машины. Диск, используемый при подготовке образа, составляет 5 Гб. Этого недостаточно для полноценного запуска k0s. Сам Vagrant из коробки не поддерживает возможность изменять размер диска виртуальной машины и эту проблему можно обойти парой способов:
- Указать больший размер диска при сборке. Поскольку диск динамического размера, места он будет занимать только фактическими данными. В нашем случае этого бы хватило, но на моей практике случалось, что не было возможности использовать динамические диски, поэтому я предпочитаю собирать образ на маленьком диске. Стандартная же практика подготовки box для Vagrant - делать диск размером 20 Гб. Для тестов этого обычно хватает. Но я такой подход не люблю.
- Использовать плагин disk-size. Он экспериментальный, но позволяет управлять размером диска на стороне Vagrant. Мне этот способ нравится гораздо больше, потому что я могу создать диск любого размера и изменить его уже на запущенной машине. Установить плагин можно командой
vagrant plugin install vagrant-disksize.
С использованием плагина есть нюанс - он не управляет разметкой диска. То есть мы увидим что физический диск стал нужного нам размера, но файловая система по прежнему маленькая, поэтому её придётся расширить руками. В моём случае диск размечен с использованием LVM, поэтому увеличение раздела можно произвести прямо из ОС. Для этого я и использую Ansible.
После того, как Vagrant запустил виртуальную машину, я могу зайти на неё и начать работать с k0s:
# Подключиться к виртуальной машине vagrant ssh # Посмотреть статус службы k0s sudo k0s status # Посмотреть статус ноды k0s sudo k0s get nodes # Запустить деплоймент с nginx sudo k0s kubectl apply -f nginx-deployment.yaml
Когда мы наиграемся, виртуальную машину можно будет удалить:
vagrant destroy -f
Итог
Мы узнали про Packer и собрали свой "золотой образ". И пусть это только VirtualBox на локальной машине, полученных основ уже хватит для подготовки образа под другие платформы (Amazon, Azure, OpenStack и т.п.)
Использование Packer даёт возможность хранить конфигурацию образа как код, что позволяет применять к нему передовые практики DevOps. Например, настроить CI/CD или подключить этап тестирование внесённых изменений.