- BrainTools - https://www.braintools.ru -

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

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

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

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

Зачем

Необычное название главы, правда? Зато она сразу даст понять дорогому читателю, нужно ли идти дальше за болью [2] и подробностями. Поэтому предыстория: пришел я в доблестные ряды Cloud.ru [3] всего пару месяцев назад, но сразу взялся за улучшение процессов. Потому что тварь я дрожащая или все-таки 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. Повторить это для каждой новой версии ОС или при изменении требований.

Один цикл занимал пару часов, а ошибки [4] были неизбежны: то репозиторий забыл добавить, то не те драйверы для 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. Короче, в условиях нехватки времени было прагматичнее воспользоваться имитацией нажатия, чем рефлексировать, что да почему. А в итоге оказалось, что это решение полностью рабочее и покрывает мои потребности [5]. Но вы в комментариях можете описать альтернативы для более стандартных дистрибутивов — сообщество только «спасибо» скажет.

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

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

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

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

  • 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 управляет всем этим хозяйством.

Переменные — мозг [7] процесса

Все настройки я вынес в vars.yaml. Это позволяет легко менять поведение [8] без правки кода.

Осторожно, много кода х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 — это уже поведение [9] самого 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

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

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

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

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

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

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

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

Что в итоге?

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

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

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

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

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

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

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

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

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

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

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

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

Автор: MikeyTide

Источник [15]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/34252

URLs in this post:

[1] логика: http://www.braintools.ru/article/7640

[2] болью: http://www.braintools.ru/article/9901

[3] Cloud.ru: http://cloud.ru?utm_source=habr&utm_medium=article&utm_campaign=ansible_packer_automation_11082026

[4] ошибки: http://www.braintools.ru/article/4192

[5] потребности: http://www.braintools.ru/article/9534

[6] память: http://www.braintools.ru/article/4140

[7] мозг: http://www.braintools.ru/parts-of-the-brain

[8] поведение: http://www.braintools.ru/article/9372

[9] поведение: http://www.braintools.ru/article/5593

[10] опыт: http://www.braintools.ru/article/6952

[11] тут: https://docs.ansible.com/projects/ansible/latest/collections/community/general/sssd_info_module.html#ansible-collections-community-general-sssd-info-module

[12] тут: https://docs.ansible.com/projects/ansible/latest/collections/community/general/logrotate_module.html#ansible-collections-community-general-logrotate-module

[13] официальному руководству: https://github.com/ansible-collections/community.general/blob/main/CONTRIBUTING.md

[14] Посмотреть статус PR можно тут: https://github.com/ansible-collections/community.general/pull/12492

[15] Источник: https://habr.com/ru/companies/cloud_ru/articles/1068806/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1068806

www.BrainTools.ru

Rambler's Top100