Как я автоматизировал сборку образов с Ansible и Packer и попал в community.ansible. ansible.. ansible. DevOps.. ansible. DevOps. hashicorp.. ansible. DevOps. hashicorp. linux.. ansible. DevOps. hashicorp. linux. Open source.. ansible. DevOps. hashicorp. linux. Open source. packer.. ansible. DevOps. hashicorp. linux. Open source. packer. redhat.. ansible. DevOps. hashicorp. linux. Open source. packer. redhat. автоматизация.. ansible. DevOps. hashicorp. linux. Open source. packer. redhat. автоматизация. Блог компании Cloud.ru.. ansible. DevOps. hashicorp. linux. Open source. packer. redhat. автоматизация. Блог компании Cloud.ru. Системное администрирование.

Полночь, деплой горит, а ты вручную поднимаешь десятый сервер за неделю: копируешь ISO, тыкаешь Enter в консоли анакон­ды, правишь fstab, чинишь grub и на утро выясняешь, что забыл gpg-ключ репозитория на трех машинах из десяти. Признавайтесь, бывало? Эта статья для тех, кто устал платить временем и нервными клетками за ручную сборку образов.

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

Как я автоматизировал сборку образов с Ansible и Packer и попал в community.ansible - 1

Зачем

Необычное название главы, правда? Зато она сразу даст понять дорогому читателю, нужно ли идти дальше за болью и подробностями. Поэтому предыстория: пришел я в доблестные ряды Cloud.ru всего пару месяцев назад, но сразу взялся за улучшение процессов. Потому что тварь я дрожащая или все-таки DevOps?

Мы с командой работаем над сертификацией платформы Cloud.ru Evolution Stack — это специализированный продукт для тех, кому нельзя выносить данные в публичные облака. Поэтому в качестве основной системы для него был выбран дистрибутив SberOS как сертифицированный ФСТЭК и наиболее подходящий для определенного сегмента наших заказчиков.

В ходе проекта подготовки сертификации нужно было разворачивать стенды с платформой, накатывать по необходимости обновление на уже развернутый стенд и тестировать весь необходимый и заявленный функционал. Для работы в Cloud.ru Evolution Stack нужны образы виртуальных машин в формате .raw. Учитывая различные обновления безопасности или пакетной базы, образы приходится готовить часто. Важно еще учесть, что они должны быть корректно настроены, чтобы работать с ними в облаке… В общем ручного труда у ребят до моего появления было предостаточно.

Далее в статье я расскажу, как выглядел процесс в начале и что я предложил. Будут как готовые решения и код, так и просто обзор технологии с объяснением, почему я решил построить процесс именно так.

От боли к автоматизации

Когда я пришел в проект, сборка нового образа выглядела так:

  1. Скачать ISO-образ SberOS с портала.

  2. Установить VirtualBox. 

  3. Вручную создать виртуальную машину, выбрать ISO, запустить установку.

  4. Пройти интерактивный инсталлятор: язык, разбивка дисков, пароли.

  5. После установки зайти по SSH, поставить пакеты, настроить репозитории, поправить fstab, пересобрать initramfs…

  6. Выключить ВМ, экспортировать диск, конвертировать в нужный формат.

  7. Повторить это для каждой новой версии ОС или при изменении требований.

Один цикл занимал пару часов, а ошибки были неизбежны: то репозиторий забыл добавить, то не те драйверы для OpenStack.

Я решил — так больше не будет. Мне нужен был инструмент, который по команде делает все от и до. Так я пришел к связке Ansible + Packer.

Как я автоматизировал сборку образов с Ansible и Packer и попал в community.ansible - 2

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

Почему Ansible + Packer, а не наоборот

Как я автоматизировал сборку образов с Ansible и Packer и попал в community.ansible - 3

Часто можно увидеть примеры, где 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 отследил окончание установки.

Как я автоматизировал сборку образов с Ansible и Packer и попал в community.ansible - 4

Для тех, кто начал капать себе «Валокордин» от «изящества» решения имитировать нажатие Enter: спокойствие, господа, трюк выполняется профессионалами. Дело в том, что c официальным non-interactive режимом Anaconda, который уместнее использовать для этой задачи, у меня не сложилось: процесс зависал на последнем экране или выдавал ошибку. Думаю, это может быть связано с тем, что мы имеем дело с закрытым дистрибутивом, а там может быть использован нестандартный версионный форк Anaconda. Короче, в условиях нехватки времени было прагматичнее воспользоваться имитацией нажатия, чем рефлексировать, что да почему. А в итоге оказалось, что это решение полностью рабочее и покрывает мои потребности. Но вы в комментариях можете описать альтернативы для более стандартных дистрибутивов — сообщество только «спасибо» скажет.

Основной Packer-конфиг

Файл sberos.pkr.hcl содержит:

  • Переменные — все параметры вынесены, чтобы их можно было переопределять через vars.yaml.

  • Источник virtualbox-iso — параметры ВМ (CPU, память, диск, сеть).

  • Boot-команды — передача пути к kickstart через встроенный HTTP-сервер Packer.

  • Провижионеры — три shell-блока:

    • Настройка репозиториев SberOS.

    • Обновление и установка пакетов (cloud-initopen-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 и Packer и попал в community.ansible - 5

Теперь переходим к главному — как 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]

Ключевые задачи:

  1. Генерация файлов из шаблонов — модуль template превращает .j2-файлы в готовые ks.cfg и sberos.pkr.hcl с подставленными переменными.

  2. Загрузка ISO — два режима:

    • download_iso: true — скачивает по URL с помощью get_url

    • copy_iso: true — копирует с локальной машины через copy

    • Проверка существования и отображение размера

  3. Валидация — packer validate проверяет синтаксис, не запуская сборку.

  4. Сборка — packer build -force создает образ.

  5. Fetch — fetch забирает готовый sberos.raw на локальную машину.

  6. Cleanup — опциональное удаление ISO после сборки.

Все задачи идемпотентны: при повторном запуске Ansible не пересоздаст директории, если они уже есть, и не пересоберет образ, если мы не меняли конфиг (хотя Packer всегда собирает заново при -force — это уже поведение самого Packer).

Автоматизация всего процесса: одна команда

После настройки переменных и инвентаря (файла inventory/hosts с адресом хоста-билдера) я запускаю:ansible-playbook main.yaml.

И дальше происходит магия:

  1. На хосте создается структура ~/packer/.

  2. Генерируются конфиги.

  3. ISO загружается из указанного источника.

  4. Packer валидирует и собирает образ.

  5. Готовый RAW-образ скачивается на мою локальную машину.

Весь процесс занимает 5–10 минут в зависимости от скорости сети и железа.

Теперь даже забыл, что такое «ручная установка» — я просто говорю ansible-playbook main.yaml и через несколько минут у меня в папке лежит готовый sberos.raw, который можно загружать.

Как я написал модуль для Ansible и попал в community.general

У меня уже был опыт создания модулей для ansible тут и тут.

Весь путь прозрачный и всегда почти одинаков:

  1. Сначала я создал модуль на Python, следуя официальному руководству.

  2. Потом сделал форк репозитория ansible-collections/community.general, создал ветку, добавил свой модуль в plugins/modules/, написал тесты с ansible-test.

  3. Выдохнул, отправил свой pull request, и жду его согласования.

Посмотреть статус PR можно тут.

Сейчас дорабатываю необходимые моменты по комментариям сообщества. Хвастаться, может быть, рановато, но в двух предыдущих случаях все заканчивалось добавлением модуля в документацию и код.

Что в итоге?

Сегодня у меня есть:

  1. Готовый плейбук для сборки образов SberOS под OpenStack.

  2. Один репозиторий, который содержит Ansible-плейбук, Packer-конфиг, шаблоны и скрипты.

  3. Одна команда — ansible-playbook main.yaml, которая дает мне готовый RAW-образ за 5–10 минут.

  4. Мой модуль потенциально будет размещен в  community.general и любой сможет его использовать и ориентироваться на уже готовые примеры для его работы.

Если вы дочитали до этого места — значит, автоматизация вам небезразлична. Призываю вас:

  • Не бояться связывать разные инструменты Ansible + Packer + VirtualBox.

  • Выносить все в переменные и шаблоны — это окупается при первом же изменении требований.

  • Писать модули для Ansible, если чего-то не хватает — сообщество открыто и очень приветливо.

  • Делиться своим опытом — ваша статья может вдохновить кого-то на подобное решение.

Спасибо, что осилили столько букв! Если есть вопросы — пишите, я постараюсь ответить.

Удачи в автоматизации! 🚀

Автор: MikeyTide

Источник