Полночь, деплой горит, а ты вручную поднимаешь десятый сервер за неделю: копируешь ISO, тыкаешь Enter в консоли анаконды, правишь fstab, чинишь grub и на утро выясняешь, что забыл gpg-ключ репозитория на трех машинах из десяти. Признавайтесь, бывало? Эта статья для тех, кто устал платить временем и нервными клетками за ручную сборку образов.
В этой статье расскажу, как я построил полностью автоматизированный пайплайн для сборки образов закрытого дистрибутива Linux на базе RedHat — без единого ручного клика после старта. Ansible управляет хостом-билдером, Packer с shell-провижионерами собирает сам образ, и в итоге мы получаем готовый RAW, оптимизированный для OpenStack. Подход не привязан к конкретной ОС: если у вас другой дистрибутив, достаточно заменить ks.cfg на preseed.cfg или любой другой автоустановщик — остальная логика пайплайна должна работать без изменений. На десерт расскажу, как оформил часть решения в модуль для Ansible и отправил его в официальную коллекцию community.general.

Зачем
Необычное название главы, правда? Зато она сразу даст понять дорогому читателю, нужно ли идти дальше за болью и подробностями. Поэтому предыстория: пришел я в доблестные ряды Cloud.ru всего пару месяцев назад, но сразу взялся за улучшение процессов. Потому что тварь я дрожащая или все-таки DevOps?
Мы с командой работаем над сертификацией платформы Cloud.ru Evolution Stack — это специализированный продукт для тех, кому нельзя выносить данные в публичные облака. Поэтому в качестве основной системы для него был выбран дистрибутив SberOS как сертифицированный ФСТЭК и наиболее подходящий для определенного сегмента наших заказчиков.
В ходе проекта подготовки сертификации нужно было разворачивать стенды с платформой, накатывать по необходимости обновление на уже развернутый стенд и тестировать весь необходимый и заявленный функционал. Для работы в Cloud.ru Evolution Stack нужны образы виртуальных машин в формате .raw. Учитывая различные обновления безопасности или пакетной базы, образы приходится готовить часто. Важно еще учесть, что они должны быть корректно настроены, чтобы работать с ними в облаке… В общем ручного труда у ребят до моего появления было предостаточно.
Далее в статье я расскажу, как выглядел процесс в начале и что я предложил. Будут как готовые решения и код, так и просто обзор технологии с объяснением, почему я решил построить процесс именно так.
От боли к автоматизации
Когда я пришел в проект, сборка нового образа выглядела так:
Скачать ISO-образ SberOS с портала.
Установить VirtualBox.
Вручную создать виртуальную машину, выбрать ISO, запустить установку.
Пройти интерактивный инсталлятор: язык, разбивка дисков, пароли.
После установки зайти по SSH, поставить пакеты, настроить репозитории, поправить fstab, пересобрать initramfs...
Выключить ВМ, экспортировать диск, конвертировать в нужный формат.
Повторить это для каждой новой версии ОС или при изменении требований.
Один цикл занимал пару часов, а ошибки были неизбежны: то репозиторий забыл добавить, то не те драйверы для OpenStack.
Я решил — так больше не будет. Мне нужен был инструмент, который по команде делает все от и до. Так я пришел к связке Ansible + Packer.

Прежде чем двинемся дальше, обсудим слона в комнате, которого опытный читатель скорее всего заметил и вопрошает по ту сторону экрана: «а почему Viirtual Box»? Да, здесь и далее будет использоваться VB и да, использование ненативного VB выглядит как костыль. Но, во-первых, так исторически сложилось: VB присутствовал в проекте еще до меня и что характерно — работал (работает — не трогай), а во-вторых, мне была важна скорость подготовки множества билдеров для ускорения процесса сборки разных образов. А для VB также есть builder и он поддерживается сообществом. Я могу просто установить один пакет и далее запустить либо мою коллекцию, либо CI/CD, дальше автоматизация все сделает по уже описанному сценарию.
Почему Ansible + Packer, а не наоборот

Часто можно увидеть примеры, где Ansible используется внутри Packer в качестве провижионера. Но я пошел другим путем.
По моему плану Ansible работает на хосте, где запускается сборка. Он готовит окружение (устанавливает Packer и VirtualBox, если нужно), скачивает ISO, генерирует конфигурационные файлы из шаблонов, запускает Packer и забирает результат.
Packer же отвечает за создание образа: поднимает ВМ в VirtualBox, выполняет автоматическую установку (через kickstart), применяет shell-скрипты для донастройки и конвертирует диск.
Такое разделение дает гибкость: один и тот же Ansible-плейбук может управлять разными хостами-билдерами (локальный ноутбук, удаленный сервер, CI-раннер), а Packer-конфиг остается неизменным.
Packer: строим образ как конструктор
Структура Packer-директории
На хосте-билдере Ansible создает следующую структуру:
packer/ ├── files/ │ └── authorized_keys # SSH-ключи ├── http/ │ └── ks.cfg # Kickstart для автоматической установки ├── iso/ │ └── sberlinux_fstec-9.2-x86_64-dvd.iso └── sberos.pkr.hcl # Главный Packer-конфиг
Kickstart — автоматическая установка
Чтобы установка SberOS проходила без участия человека, я использую kickstart. Ansible генерирует ks.cfg из шаблона, подставляя пароли и имя пользователя.
Осторожно, много кода x1
# Generated by Anaconda 34.25.4.9 #version=SBEL9 # Use text mode install text %addon com_kdump --enable --reserve-mb='auto' %end # System language lang ru_RU.UTF-8 # Use CDROM installation media cdrom %packages @^os-server %end # Run the Setup Agent on first boot firstboot --enable # Do not configure the X Window System skipx # Generated using Blivet version 3.6.0 ignoredisk --only-use=sda # System bootloader configuration bootloader --append="crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M" --location=mbr --boot-drive=sda autopart --type=plain # Partition clearing information clearpart --all --initlabel --drives=sda # System timezone timezone Europe/Moscow --utc # Root password rootpw --iscrypted $6$a4de0bCBtIDEosfd$vlDTBeBJ9ChCZC6tO1XxcWUeE4D/ISljjMm6Pt617AUFWbmU5w1MCTMqXHIFhY1CoHtMd/m.BY1BHInCM8IFx/ # User password ( должен совпадать с паролем в variables.pkr.hcl ) user --groups=wheel --name=user --password=$6$tiDeCbYM/OpBTNSe$PZ.eErgEzeKGLdmlc55ilzHhVVjPiay7tMFqopI0pJHy1tmKjA2wb21rRwcSDh8QSj90uhsYjDvZ375nNtHdS0 --iscrypted --gecos="user" %post --log=/root/ks-post.log #!/bin/bash # Разрешить пользователю user выполнять sudo без пароля echo "user ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/user # Включаем SSH systemctl enable sshd # Разрешаем авторизацию по паролю sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config systemctl restart sshd # Очистка dnf clean all rm -rf /var/cache/dnf/* rm -rf /tmp/* # Отключаем интерактивный выход # Создаем скрипт, который будет имитировать нажатие Enter cat > /root/auto_enter.sh << 'EOF' #!/bin/bash # Ждем пока Anaconda завершится sleep 2 # Отправляем Enter в процесс Anaconda echo -e "\n" > /dev/tty1 EOF chmod +x /root/auto_enter.sh # Запускаем в фоне nohup /root/auto_enter.sh & %end # ВАЖНО: Используем reboot вместо poweroff reboot
Важные моменты в моем kickstart:
Автоматическая разбивка диска — autopart --type=plain.
Задание паролей — root и пользователь user с зашифрованными хешами.
Post-скрипт — настройка sudo без пароля, включение SSH, очистка кэша DNF.
Завершение перезагрузкой — reboot, чтобы Packer отследил окончание установки.

Для тех, кто начал капать себе «Валокордин» от «изящества» решения имитировать нажатие Enter: спокойствие, господа, трюк выполняется профессионалами. Дело в том, что c официальным non-interactive режимом Anaconda, который уместнее использовать для этой задачи, у меня не сложилось: процесс зависал на последнем экране или выдавал ошибку. Думаю, это может быть связано с тем, что мы имеем дело с закрытым дистрибутивом, а там может быть использован нестандартный версионный форк Anaconda. Короче, в условиях нехватки времени было прагматичнее воспользоваться имитацией нажатия, чем рефлексировать, что да почему. А в итоге оказалось, что это решение полностью рабочее и покрывает мои потребности. Но вы в комментариях можете описать альтернативы для более стандартных дистрибутивов — сообщество только «спасибо» скажет.
Основной Packer-конфиг
Файл sberos.pkr.hcl содержит:
Переменные — все параметры вынесены, чтобы их можно было переопределять через
vars.yaml.Источник
virtualbox-iso— параметры ВМ (CPU, память, диск, сеть).Boot-команды — передача пути к kickstart через встроенный HTTP-сервер Packer.
Провижионеры — три shell-блока:
Настройка репозиториев SberOS.
Обновление и установка пакетов (
cloud-init,open-vm-tools).Подготовка для OpenStack (virtio-драйверы, fstab с LABEL, grub, dracut).
Пост-процессоры:
manifest — создание JSON с артефактами.
shell-local — конвертация VMDK в RAW через
qemu-img.
Осторожно, много кода x2
# Настройка плагинов packer { required_plugins { virtualbox = { version = "~> 1" source = "github.com/hashicorp/virtualbox" } # Можно добавить другие плагины при необходимости # ansible = { # version = "~> 1" # source = "github.com/hashicorp/ansible" # } } } # ============================================ # ISO ПЕРЕМЕННЫЕ # ============================================ variable "iso_path" { type = string description = "Путь к директории с ISO образами" default = "./iso" } variable "iso_name" { type = string description = "Имя ISO файла с SberOS" default = "{{ iso_filename }}" } variable "iso_checksum" { type = string description = "Контрольная сумма ISO файла (sha256:...)" default = "{{ iso_checksum }}" } # ============================================ # ВИРТУАЛЬНАЯ МАШИНА # ============================================ variable "vm_name" { type = string description = "Имя виртуальной машины" default = "{{ vm_name }}" } variable "vm_description" { type = string description = "Описание виртуальной машины" default = "{{ vm_description }}" } variable "guest_os_type" { type = string description = "Тип гостевой ОС для VirtualBox" default = "{{ guest_os_type }}" } variable "disk_size" { type = number description = "Размер диска в МБ" default = {{ disk_size }} } variable "disk_interface" { type = string description = "Тип интерфейса диска (sata, scsi, ide)" default = "{{ disk_interface }}" } variable "memory" { type = number description = "Объем оперативной памяти в МБ" default = {{ memory }} } variable "cpus" { type = number description = "Количество CPU" default = {{ cpus }} } variable "headless" { type = bool description = "Запуск в фоновом режиме без GUI" default = {{ headless | lower }} } # ============================================ # SSH ПАРАМЕТРЫ # ============================================ variable "ssh_username" { type = string description = "Имя пользователя для SSH доступа" default = "{{ ssh_username }}" } variable "ssh_password" { type = string description = "Пароль для SSH доступа" default = "{{ ssh_password }}" sensitive = true } variable "ssh_timeout" { type = string description = "Таймаут для SSH подключения" default = "{{ ssh_timeout }}" } # ============================================ # СЕТЬ # ============================================ variable "nic_type" { type = string description = "Тип сетевого адаптера" default = "{{ nic_type }}" } # ============================================ # ВЫВОД (OUTPUT) # ============================================ variable "output_dir" { type = string description = "Директория для сохранения артефактов" default = "{{ output_dir }}" } variable "output_format" { type = string description = "Формат экспорта (ovf, ova, vmware)" default = "{{ output_format }}" } # ============================================ # GRAPHICS # ============================================ variable "gfx_controller" { type = string description = "Тип графического контроллера" default = "{{ gfx_controller }}" } variable "gfx_vram_size" { type = string description = "Объем видеопамяти" default = "{{ gfx_vram_size }}" } variable "gfx_accelerate_3d" { type = bool description = "Включить 3D ускорение" default = {{ gfx_accelerate_3d | lower }} } # Источник (только VirtualBox) source "virtualbox-iso" "sberos" { # Основные параметры VM vm_name = var.vm_name guest_os_type = var.guest_os_type nested_virt = false headless = var.headless guest_additions_mode = "disable" # Аппаратные параметры cpus = var.cpus memory = var.memory chipset = "piix3" firmware = "bios" rtc_time_base = "UTC" # Диск disk_size = var.disk_size hard_drive_interface = "{{ disk_interface }}" hard_drive_discard = true hard_drive_nonrotational = true # Сеть nic_type = var.nic_type # Графика gfx_controller = var.gfx_controller gfx_vram_size = var.gfx_vram_size gfx_accelerate_3d = var.gfx_accelerate_3d sound = "none" # ISO iso_url = "${var.iso_path}/${var.iso_name}" iso_checksum = var.iso_checksum # HTTP-сервер для kickstart (папка http/ должна содержать ks.cfg) http_directory = "http" # Команды загрузки – используем HTTP для получения kickstart boot_command = [ "<tab><wait>", " inst.ks=http://{% raw %}{{ .HTTPIP }}{% endraw %}:{% raw %}{{ .HTTPPort }}{% endraw %}/ks.cfg", " rd.live.check=0", "<enter>" ] # Выключение shutdown_command = "echo '${var.ssh_password}' | sudo -S shutdown -P now" post_shutdown_delay = "1m" # Вывод output_directory = var.output_dir format = var.output_format bundle_iso = var.bundle_iso # SSH communicator = "ssh" ssh_username = var.ssh_username ssh_password = var.ssh_password ssh_timeout = var.ssh_timeout } # Сборка build { sources = ["sources.virtualbox-iso.sberos"] # Провижинеры (дополнительная настройка после установки) # Проверка и настройка репозиториев provisioner "shell" { inline = [ # Создаем файл с репозиториями "sudo tee /etc/yum.repos.d/sberlinux.repo << 'EOF'", "[Sberlinux-89-baseos]", "name=Sberlinux-89-baseos", "baseurl=https://ваши-репозитории-тут", "enabled=1", "gpgcheck=1", "", "[Sberlinux-89-extra]", "name=Sberlinux-89-extra", "baseurl=https://ваши-репозитории-тут", "enabled=1", "gpgcheck=1", "", "[Sberlinux-89-baseos-extra]", "name=Sberlinux-89-baseos-extra", "baseurl=https://ваши-репозитории-тут", "enabled=1", "gpgcheck=1", "", "[Sberlinux-89-baseos-rpms]", "name=Sberlinux-89-baseos-rpms", "baseurl=https://ваши-репозитории-тут", "enabled=1", "gpgcheck=1", "EOF", # Проверяем созданные репозитории "echo '=== Проверка файлов репозиториев ==='", "ls -la /etc/yum.repos.d/", "cat /etc/yum.repos.d/sberlinux.repo", # Очищаем кэш и пробуем обновить "sudo dnf clean all", "sudo dnf makecache || echo 'Ошибка при создании кэша, но продолжаем...'", # Проверяем доступность репозиториев "echo '=== Проверка доступности репозиториев ==='", "sudo dnf repolist || echo 'Ошибка при получении списка репозиториев'", # Проверяем доступность пакетов (опционально) "echo '=== Проверка доступности базовых пакетов ==='", "sudo dnf search tree || echo 'Поиск пакетов недоступен'", # Устанавливаем пакеты, если репозитории доступны "if sudo dnf repolist | grep -q 'Sberlinux'; then", " echo 'Репозитории Sberlinux доступны, устанавливаем пакеты...'", " sudo dnf update -y || echo 'Обновление не удалось'", " sudo dnf install -y cloud-init open-vm-tools vim-minimal cloud-utils-growpart || echo 'Установка пакетов не удалась'", "else", " echo 'Репозитории Sberlinux НЕ доступны, пропускаем установку пакетов'", "fi", # ============================================ # НАСТРОЙКА ДЛЯ OPENSTACK # ============================================ "echo '=== Настройка для OpenStack ==='", # 1. Добавляем virtio драйверы в initramfs "echo 'Добавление virtio драйверов в initramfs...'", "sudo tee /etc/dracut.conf.d/99-virtio.conf << 'EOF'", "# Virtio drivers for OpenStack/KVM", "add_drivers+=\" virtio virtio_ring virtio_blk virtio_pci virtio_net virtio_scsi \"", "force_drivers+=\" virtio_blk virtio_net \"", "EOF", # 2. Исправляем fstab - используем LABEL вместо UUID "echo 'Исправление /etc/fstab для универсальности...'", "sudo cp /etc/fstab /etc/fstab.backup", # Проверяем, есть ли уже LABEL или используем UUID "if grep -q 'LABEL=ROOT' /etc/fstab; then", " echo 'LABEL=ROOT уже используется в fstab'", "else", " # Устанавливаем метку ROOT на корневой раздел", " ROOT_DEV=$(blkid | grep 'TYPE=\"ext4\"' | head -1 | cut -d: -f1)", " if [ -z \"$ROOT_DEV\" ]; then", " ROOT_DEV=$(blkid | grep 'TYPE=\"xfs\"' | head -1 | cut -d: -f1)", " fi", " if [ ! -z \"$ROOT_DEV\" ]; then", " sudo e2label $ROOT_DEV ROOT 2>/dev/null || sudo xfs_admin -L ROOT $ROOT_DEV 2>/dev/null", " echo 'Метка ROOT установлена на $ROOT_DEV'", " # Заменяем UUID на LABEL в fstab", " sudo sed -i 's/UUID=[^ ]*/LABEL=ROOT/g' /etc/fstab", " else", " echo 'WARNING: Не найден корневой раздел!'", " fi", "fi", # 3. Исправляем grub для использования LABEL "echo 'Исправление GRUB для использования LABEL...'", "if [ -f /etc/default/grub ]; then", " sudo sed -i 's/root=UUID=[^ ]*/root=LABEL=ROOT/g' /etc/default/grub", " # Добавляем параметры для OpenStack", " sudo sed -i 's/GRUB_CMDLINE_LINUX=\"\\(.*\\)\"/GRUB_CMDLINE_LINUX=\"\\1 console=ttyS0,115200 console=tty0 net.ifnames=0 biosdevname=0\"/' /etc/default/grub", "fi", # 4. Обновляем grub.cfg "echo 'Обновление GRUB...'", "if [ -f /boot/grub2/grub.cfg ]; then", " sudo sed -i 's/root=UUID=[^ ]*/root=LABEL=ROOT/g' /boot/grub2/grub.cfg", " sudo grub2-mkconfig -o /boot/grub2/grub.cfg", "fi", # 5. Пересоздаем initramfs с virtio драйверами "echo 'Пересоздание initramfs...'", "sudo dracut -f -v --regenerate-all || echo 'dracut не удалось выполнить'", # Включение cloud-init и очистка вм "echo 'Включение cloud-init и очистка вм...'", "sudo systemctl enable cloud-init || echo 'Cloud-init не включен'", "sudo cloud-init clean --logs --seed --machine-id || echo 'Очистка не удалась'", ] } # Провижинер для загрузки ключей provisioner "file" { source = "files/authorized_keys" destination = "/tmp/authorized_keys" } provisioner "shell" { inline = [ "mkdir -p /home/${var.ssh_username}/.ssh", "cat /tmp/authorized_keys >> /home/${var.ssh_username}/.ssh/authorized_keys", "chown -R ${var.ssh_username}:${var.ssh_username} /home/${var.ssh_username}/.ssh", "chmod 600 /home/${var.ssh_username}/.ssh/authorized_keys", "rm /tmp/authorized_keys", ] } # Генерация manifest post-processor "manifest" { output = "packer-manifest.json" } # Конвертация в RAW post-processor "shell-local" { inline = [ "echo '=== Конвертация VMDK в RAW ==='", "if [ -f ./output/${var.vm_name}-disk001.vmdk ]; then", " qemu-img convert -f vmdk -O raw ./output/${var.vm_name}-disk001.vmdk ./output/${var.vm_name}.raw", " echo 'RAW образ создан: ./output/${var.vm_name}.raw'", " ls -lh ./output/*.raw", "else", " echo 'VMDK файл не найден!'", " exit 1", "fi" ] } }
Именно подготовка для OpenStack — самая ценная часть. Без нее образ не будет корректно работать в облаке:
Добавление virtio драйверов в
initramfs.Замена UUID на LABEL в fstab.
Обновление GRUB с параметрами
console=ttyS0иnet.ifnames=0.Включение
cloud-initдля автоматической настройки при первом запуске.
Ansible-плейбук: оркестратор на хосте-билдере

Теперь переходим к главному — как Ansible управляет всем этим хозяйством.
Переменные — мозг процесса
Все настройки я вынес в vars.yaml. Это позволяет легко менять поведение без правки кода.
Осторожно, много кода х3
--- # ============================================ # ОБЩИЕ ПЕРЕМЕННЫЕ ДЛЯ ANSIBLE И PACKER # ============================================ # Пути packer_local_path: /Users/aagabidullin/ packer_remote_path: "/home/{{ ansible_user }}" packer_work_dir: "{{ packer_remote_path }}/packer" packer_file_main: sberos.pkr.hcl # ============================================ # ПЕРЕМЕННЫЕ ISO # ============================================ iso_filename: sberlinux_fstec-9.2-x86_64-dvd.iso iso_download_url: "https://example.com/path/to/sberos.iso" iso_checksum: "sha256:ff22b1552353d68330f6eb7ad0589f64fdcffe8c2846db7a1f9e96ca2b847c0e" # ============================================ # SSH КЛЮЧИ # ============================================ ssh_key_pub: | id_rsa256 тут-ваш-открытый-ключ # ============================================ # ПЕРЕМЕННЫЕ ВИРТУАЛЬНОЙ МАШИНЫ # ============================================ vm_name: sberos vm_description: "SberOS" guest_os_type: "RedHat_64" disk_size: 3072 disk_interface: "sata" memory: 4096 cpus: 2 headless: true # ============================================ # SSH ПАРАМЕТРЫ # ============================================ ssh_username: user ssh_password: "P@ssw0rd" ssh_timeout: "20m" # ============================================ # СЕТЬ # ============================================ nic_type: "82540EM" # ============================================ # ВЫВОД # ============================================ output_dir: "./output" output_format: "ovf" bundle_iso: false # ============================================ # GRAPHICS # ============================================ gfx_controller: "vmsvga" gfx_vram_size: "16" gfx_accelerate_3d: false # ============================================ # РЕЖИМЫ ЗАПУСКА ANSIBLE # ============================================ download_iso: false copy_iso: false run_validate: true run_build: true cleanup_iso_after_build: false
Особенно удобны булевы флаги:
download_iso: false # скачать ISO по URL copy_iso: false # скопировать ISO с локальной машины run_validate: true # запустить packer validate run_build: true # запустить packer build cleanup_iso_after_build: false
Это позволяет, например, только валидировать конфиг без сборки, или пересобрать образ с тем же ISO, или полностью обновить ISO из интернета. Основной плейбук Файл main.yaml выполняет следующие шаги:
Осторожно, много кода х4
--- - name: Deploy and run Packer on remote host hosts: all gather_facts: true tasks: - name: Display current mode ansible.builtin.debug: msg: - "Режим работы:" - "Download ISO: {{ download_iso }}" - "Copy ISO: {{ copy_iso }}" - "Validate: {{ run_validate }}" - "Build: {{ run_build }}" - name: Create remote packer directory ansible.builtin.file: path: "{{ packer_work_dir }}" state: directory mode: '0755' - name: Create subdirectories ansible.builtin.file: path: "{{ packer_work_dir }}/{{ item }}" state: directory mode: '0755' loop: - files - http - iso - name: Generate authorized_keys file ansible.builtin.copy: dest: "{{ packer_work_dir }}/files/authorized_keys" content: "{{ ssh_key_pub }}" mode: '0644' when: ssh_key_pub is defined - name: Generate sberos.pkr.hcl from template ansible.builtin.template: src: templates/sberos.pkr.hcl.j2 dest: "{{ packer_work_dir }}/{{ packer_file_main }}" mode: '0644' - name: Generate ks.cfg from template ansible.builtin.template: src: templates/ks.cfg.j2 dest: "{{ packer_work_dir }}/http/ks.cfg" mode: '0644' - name: Handle ISO file block: when: download_iso or copy_iso - name: Download ISO from URL ansible.builtin.get_url: url: "{{ iso_download_url }}" dest: "{{ packer_remote_path }}/iso/{{ iso_filename }}" mode: '0644' timeout: 3600 validate_certs: no when: download_iso register: download_result - name: Copy ISO from local machine ansible.builtin.copy: src: "{{ packer_local_path }}/iso/{{ iso_filename }}" dest: "{{ packer_remote_path }}/iso/" mode: '0644' when: copy_iso register: copy_result - name: Check if ISO exists in remote directory ansible.builtin.stat: path: "{{ packer_remote_path }}/iso/{{ iso_filename }}" register: iso_file - name: Fail if ISO not found ansible.builtin.fail: msg: "ISO file not found! Please check download_iso or copy_iso settings." when: not iso_file.stat.exists - name: Display ISO information ansible.builtin.debug: msg: | ISO файл: {{ iso_filename }} Размер: {{ iso_file.stat.size | filesizeformat }} Путь: {{ iso_file.stat.path }} when: iso_file.stat.exists - name: Validate Packer template ansible.builtin.command: cmd: "packer validate {{ packer_remote_path }}/packer/{{ packer_file_main }}" chdir: "{{ packer_remote_path }}/packer" register: validate_result when: run_validate changed_when: false failed_when: validate_result.rc != 0 - name: Display validation result ansible.builtin.debug: var: validate_result.stdout_lines when: run_validate and validate_result is defined - name: Build with Packer ansible.builtin.command: cmd: "packer build -force {{ packer_remote_path }}/packer/{{ packer_file_main }}" chdir: "{{ packer_remote_path }}/packer" register: build_result when: run_build failed_when: build_result.rc != 0 - name: Copy raw from builder ansible.builtin.fetch: src: "{{ packer_work_dir }}/output/{{ vm_name }}.raw" dest: "{{ packer_local_path }}" flat: yes fail_on_missing: yes validate_checksum: no - name: Display build result ansible.builtin.debug: var: build_result.stdout_lines when: run_build and build_result is defined - name: Cleanup ISO after build ansible.builtin.file: path: "{{ packer_remote_path }}/iso/{{ iso_filename }}" state: absent when: - cleanup_iso_after_build - run_build - build_result is defined - build_result.rc == 0 tags: [cleanup]
Ключевые задачи:
Генерация файлов из шаблонов — модуль template превращает .j2-файлы в готовые
ks.cfgиsberos.pkr.hclс подставленными переменными.Загрузка ISO — два режима:
download_iso: true — скачивает по URL с помощью get_url
copy_iso: true — копирует с локальной машины через copy
Проверка существования и отображение размера
Валидация — packer validate проверяет синтаксис, не запуская сборку.
Сборка — packer build -force создает образ.
Fetch — fetch забирает готовый sberos.raw на локальную машину.
Cleanup — опциональное удаление ISO после сборки.
Все задачи идемпотентны: при повторном запуске Ansible не пересоздаст директории, если они уже есть, и не пересоберет образ, если мы не меняли конфиг (хотя Packer всегда собирает заново при -force — это уже поведение самого Packer).
Автоматизация всего процесса: одна команда
После настройки переменных и инвентаря (файла inventory/hosts с адресом хоста-билдера) я запускаю:ansible-playbook main.yaml.
И дальше происходит магия:
На хосте создается структура
~/packer/.Генерируются конфиги.
ISO загружается из указанного источника.
Packer валидирует и собирает образ.
Готовый RAW-образ скачивается на мою локальную машину.
Весь процесс занимает 5–10 минут в зависимости от скорости сети и железа.
Теперь даже забыл, что такое «ручная установка» — я просто говорю ansible-playbook main.yaml и через несколько минут у меня в папке лежит готовый sberos.raw, который можно загружать.
Как я написал модуль для Ansible и попал в community.general
У меня уже был опыт создания модулей для ansible тут и тут.
Весь путь прозрачный и всегда почти одинаков:
Сначала я создал модуль на Python, следуя официальному руководству.
Потом сделал форк репозитория
ansible-collections/community.general, создал ветку, добавил свой модуль вplugins/modules/, написал тесты сansible-test.Выдохнул, отправил свой pull request, и жду его согласования.
Посмотреть статус PR можно тут.
Сейчас дорабатываю необходимые моменты по комментариям сообщества. Хвастаться, может быть, рановато, но в двух предыдущих случаях все заканчивалось добавлением модуля в документацию и код.
Что в итоге?
Сегодня у меня есть:
Готовый плейбук для сборки образов SberOS под OpenStack.
Один репозиторий, который содержит Ansible-плейбук, Packer-конфиг, шаблоны и скрипты.
Одна команда —
ansible-playbook main.yaml, которая дает мне готовый RAW-образ за 5–10 минут.Мой модуль потенциально будет размещен в
community.generalи любой сможет его использовать и ориентироваться на уже готовые примеры для его работы.
Если вы дочитали до этого места — значит, автоматизация вам небезразлична. Призываю вас:
Не бояться связывать разные инструменты Ansible + Packer + VirtualBox.
Выносить все в переменные и шаблоны — это окупается при первом же изменении требований.
Писать модули для Ansible, если чего-то не хватает — сообщество открыто и очень приветливо.
Делиться своим опытом — ваша статья может вдохновить кого-то на подобное решение.
Спасибо, что осилили столько букв! Если есть вопросы — пишите, я постараюсь ответить.
Удачи в автоматизации! 🚀

