Автоматизация сборки образов ОС с помощью 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 или подключить этап тестирование внесённых изменений.