Ansible против нехватки места: как я научил плейбук самостоятельно расширять диски виртуалок и патчить RedOS

от автора

Каждый плановый патчинг парка RedOS у меня начинался одинаково: запускаешь обновление — и где‑то на третьем сервере оно падает, потому что в /var или /boot кончилось место. Дальше знакомый ритуал: идёшь в vCenter, увеличиваешь диск, растишь раздел, pvresize, lvextend, молишься, что не перепутал диск, возвращаешься к обновлению. На одном сервере — терпимо. На парке — тоска.

В какой‑то момент я решил, что хватит, и написал комплект из двух Ansible‑ролей: первая сама находит, где тесно, и безопасно доращивает LVM (при необходимости — увеличивая VMDK через vCenter), вторая обновляет ядро и пакеты одной транзакцией и проверяет, что сервер после ребута жив и загрузился с правильным ядром. Всё запускается с jump host, хосты идут по одному (serial: 1), полный цикл — одна команда:

bash

ansible-playbook playbooks/site.yml -l server01 -u local_admin \  --ask-pass --become --ask-become-pass

Расскажу, как это устроено и почему некоторые решения выглядят параноидально. Спойлер: потому что автоматика, которая сама трогает диски продовых виртуалок, обязана быть параноиком.

Сначала план, потом руки

Главный страх при автоматизации работы с дисками — не «не сработает», а «сработает не туда». Поэтому первое архитектурное решение: роль storage живёт в двух режимах. storage_plan.yml только смотрит и считает: какие точки монтирования ниже порога, какой LV/VG/PV за ними стоит, на каком физическом диске лежит PV, что роль собирается сделать. Никаких изменений, даже пароль от vCenter не спрашивается. storage_apply.yml — тот же код, но с реальным применением.

Второе решение: роль почти никогда не падает с голым fail. Каждая точка монтирования получает статус в общем отчёте — «достаточно_места», «требуется_ручная_проверка», «требуется_перезагрузка» и так далее, с человеческим объяснением причины. Когда прогон по парку заканчивается, у тебя не простыня красных ошибок, а внятный список: тут всё хорошо, тут я расширил, сюда не полез и вот почему. С таким отчётом жить сильно легче, чем раскапывать, на какой из вложенных задач что упало.

Одиннадцать проверок перед тем, как тронуть VMDK

Самая нервная операция — увеличение виртуального диска. Новые диски я принципиально не создаю: роль доращивает первый системный VMDK (SCSI 0:0) на фиксированные 10 ГБ. Но прежде чем дёрнуть vCenter, она проверяет буквально всё:

  • целевая VG состоит ровно из одного PV — никаких «VG размазана по трём дискам, расширим какой‑нибудь»;

  • этот PV лежит на том же физическом диске, что и корень. Имя диска при этом не захардкожено: sda, vda, nvme0n1 — роль сама определяет системный диск по корневой ФС;

  • VMDK SCSI(0:0) совпадает с системным Linux‑диском по точному размеру — дополнительная страховка, что мы увеличиваем именно тот диск, а не диск с базой соседнего PostgreSQL;

  • у ВМ нет снапшотов и незавершённой consolidation — vSphere всё равно не даст расширить диск со снапшотом, но лучше узнать это до начала работ, а не в середине;

  • PV занимает диск целиком или находится в последнем разделе — двигать середину таблицы разделов автоматика не должна никогда;

  • на сервере есть parted, а раздел и ФС поддерживают онлайн‑расширение.

Не прошла хоть одна проверка — хост получает «требуется_ручная_проверка» с объяснением, и роль идёт дальше по списку. Отдельные диски с данными (тот же PostgreSQL на своём VMDK) не расширяются вообще: даже если такую точку случайно вписать в конфиг, роль остановится до обращения к vCenter.

Маркер операции, или что будет, если прогон умрёт на середине

Это моя любимая часть. Между «vCenter увеличил VMDK» и «Linux увидел новый размер, pvresize прошёл» есть окно, в котором прогон может умереть: сеть моргнула, кто‑то нажал Ctrl+C, jump host ушёл в ребут. Перезапускаешь плейбук — и наивная реализация снова добавит 10 ГБ, потому что «места‑то всё ещё не хватает». Ещё перезапуск — ещё 10 ГБ. Диск растёт, бюджет датастора плачет.

Поэтому перед обращением к vCenter роль пишет на хост маркер pending-system-disk-resize.json с точным целевым размером диска. При перезапуске она сначала читает маркер: если незавершённая операция есть — применяется записанный в ней размер, а не «текущий плюс 10». Удаляется маркер только после успешного pvresize. Заодно маркер блокирует параллельное расширение другого тома на том же диске, пока первая операция не доведена до конца.

Мелкая деталь с большими последствиями: сколько бы гигабайт ни приехало на диск, LV доращивается не «на всё», а до расчётного целевого остатка свободного места. Излишек остаётся в VG — следующая нехватка места на этом сервере закроется вообще без похода в vCenter.

Патчинг: одна транзакция и никакой самодеятельности

Ядро и остальные пакеты я осознанно не разделяю — обновление идёт одной командой dnf update -y (на RedOS 7 с yum — соответственно yum). Раздельное обновление «сначала ядро, потом всё остальное» звучит аккуратнее, но на практике порождает вдвое больше состояний, в которых что‑то может пойти не так.

Перед транзакцией — preflight: проверка, что на хосте не крутится другой yum/dnf/rpm (привет, коллега, который «быстренько поставил пакетик» прямо во время окна), опциональная раскатка эталонных.repo‑файлов. Причём каталог репозиториев выбирается не по имени группы в инвентаре, а по фактической мажорной версии ОС из Ansible facts — после того как я однажды увидел сервер, живущий не в той группе, доверять инвентарю в таких вещах перестал.

Дальше нюансы, каждый из которых — результат реального инцидента или почти‑инцидента:

/boot в read‑only. Часть серверов у меня монтирует /boot в ro. Роль запоминает исходный режим, перемонтирует в rw, а после всех работ хендлер возвращает как было. Без этого либо обновление ядра падает, либо /boot навсегда остаётся rw — оба варианта так себе.

Чистка старых ядер — без rm. Старые ядра удаляются через dnf repoquery --installed --installonly с явным исключением загруженного ядра. Никаких шаблонов «rm ‑f vmlinuz-5.*» — пакетный менеджер знает про установленные ядра больше, чем любой мой регэксп.

Ядро по умолчанию — с проверкой после ребута. Новое ядро назначается через grubby --set-default (с фолбэком на grub2-set-default 0, если grubby нет), а после перезагрузки assert сверяет uname -r с ожидаемым. Однажды словив сервер, который после обновления тихо загрузился со старым ядром, я больше не считаю эту проверку избыточной.

Стратегия manual_dracut. Для отдельных капризных серверов RedOS 7 есть режим: обновление с DNF_DISABLE_DRACUT=1, затем ручной dracut для самого нового ядра и grub2-mkconfig. Включается пофлажно в group_vars, по умолчанию всем достаётся normal.

Прикладные хосты: PostgreSQL и тд

Патчить голую ОС просто. Интересное начинается на серверах с прикладом.

Для хостов PostgreSQL роль перед обновлением бэкапит то, что обновление любит молча перетирать: unit‑файлы postgre*, .bash_profile пользователя postgres, crypto‑policies для krb5. После транзакции всё это восстанавливается (с daemon_reload), и только потом сервер уходит в ребут. Бэкапы складываются в датированный каталог и по умолчанию не удаляются — сначала руками убеждаешься, что база поднялась, потом чистишь.

ссылка на оригинал статьи https://habr.com/ru/articles/1062398/