Полночь, деплой горит, а ты вручную поднимаешь десятый сервер за неделю: копируешь 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, если чего-то не хватает — сообщество открыто и очень приветливо.
-
Делиться своим опытом — ваша статья может вдохновить кого-то на подобное решение.
Спасибо, что осилили столько букв! Если есть вопросы — пишите, я постараюсь ответить.
Удачи в автоматизации! 🚀
Автор: MikeyTide


