Обновления Ubuntu без простоя: график, волны и откат

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

Политика: что ставим сами, что — волнами

Мы делим обновления на две группы.

  • Исправления безопасности из репозитория -security ставятся автоматически каждую ночь на всех серверах. Они точечные, регрессии в них редки, а цена задержки высока: эксплойты к опубликованным уязвимостям появляются за дни.
  • Всё остальное — обычные исправления из -updates, сторонние репозитории, новые ядра — раз в две недели волнами: сначала несколько пилотных серверов, через два дня — четверть парка, ещё через два дня — остальные.

Ubuntu и сама распространяет обновления -updates постепенно (phased updates): новая версия пакета сначала приходит небольшой доле машин, и при росте числа отчётов об ошибках выкатка останавливается. Это хорошая страховка, но не замена собственному пилоту: Canonical не знает, что на ваших серверах работает печать этикеток.

день 0 день 1 день 2 день 3 день 4 пилот · 3 сервера волна 1 · 25 % волна 2 · остальные наблюдение 48 ч
Плановые обновления идут волнами; исправления безопасности — каждую ночь на всех

unattended-upgrades для исправлений безопасности

Пакет unattended-upgrades установлен в Ubuntu Server по умолчанию. Включается он файлом /etc/apt/apt.conf.d/20auto-upgrades:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";

Основные настройки — в /etc/apt/apt.conf.d/50unattended-upgrades. Мы не правим исходный файл, а кладём свой 52ops-local — он читается позже и переопределяет значения, а при обновлении пакета не будет конфликта:

// /etc/apt/apt.conf.d/52ops-local
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}-security";
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
};

// Пакеты, которые обновляем только вручную (регулярные выражения)
Unattended-Upgrade::Package-Blacklist {
        "postgresql-";
        "docker-ce";
};

Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";

// Перезагрузка — только если нужна, и только в окно
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:30";

Unattended-Upgrade::Mail "admin@example.org";
Unattended-Upgrade::MailReport "only-on-error";

Сервер базы данных мы почти всегда исключаем из автоматического обновления СУБД: обновление пакета PostgreSQL перезапускает службу, а это обрыв соединений в рабочее время. Такие пакеты обновляем вручную, в окно, после проверки на копии.

Проверить настройки без реальной установки:

sudo unattended-upgrade --dry-run --debug 2>&1 | tail -n 30
# журнал реальных запусков
less /var/log/unattended-upgrades/unattended-upgrades.log

Перезапуск служб после обновления библиотек

Если обновилась, например, libssl, запущенные службы продолжают использовать старую копию библиотеки из памяти, пока их не перезапустят. Это делает needrestart. В интерактивном режиме он спрашивает, что перезапускать, — для серверов включаем автоматический режим:

# /etc/needrestart/conf.d/50-auto.conf
$nrconf{restart} = 'a';
# службы, которые нельзя перезапускать днём, — только при перезагрузке
$nrconf{override_rc}{qr(^postgresql)} = 0;
sudo needrestart -r l    # показать, что требует перезапуска, ничего не трогая

Окно обслуживания и перезагрузки

По умолчанию списки пакетов обновляются таймером apt-daily.timer, а установка идёт по таймеру apt-daily-upgrade.timer около 6 утра со случайной задержкой до часа. Для магазина, который открывается в 8, это поздно. Переносим:

sudo systemctl edit apt-daily-upgrade.timer
[Timer]
# пустая строка сбрасывает расписание по умолчанию — без неё добавится второе
OnCalendar=
OnCalendar=*-*-* 02:30
RandomizedDelaySec=20m
sudo systemctl daemon-reload
systemctl list-timers 'apt-daily*'

Установка заканчивается к 03:00, а Automatic-Reboot-Time "03:30" перезагружает сервер, только если появился файл /var/run/reboot-required — обычно после обновления ядра или libc. Какие пакеты потребовали перезагрузки, видно в /var/run/reboot-required.pkgs.

Если у вас два сервера, дублирующих друг друга, — например, два узла за балансировщиком нагрузки, — не давайте им перезагружаться одновременно: разнесите Automatic-Reboot-Time хотя бы на 30 минут.

Волны через Ansible

Плановые обновления запускаем плейбуком. Серверы в инвентаре разбиты на группы pilot, wave1 и wave2; в пилот попадают по одному серверу каждого типа.

# upgrade.yml — запуск: ansible-playbook upgrade.yml -e target=wave1
- hosts: "{{ target }}"
  become: true
  serial: "20%"            # не больше пятой части группы за раз
  max_fail_percentage: 0   # первая ошибка останавливает волну
  tasks:
    - name: Обновить пакеты
      ansible.builtin.apt:
        update_cache: true
        upgrade: safe
        autoremove: true

    - name: Нужна ли перезагрузка
      ansible.builtin.stat:
        path: /var/run/reboot-required
      register: reboot_flag

    - name: Перезагрузить
      ansible.builtin.reboot:
        reboot_timeout: 600
      when: reboot_flag.stat.exists

    - name: Служба печати работает
      ansible.builtin.command: systemctl is-active cups
      changed_when: false

    - name: Нет упавших служб
      ansible.builtin.command: systemctl --failed --no-legend
      register: failed_units
      changed_when: false
      failed_when: failed_units.stdout | length > 0

Последние две задачи — самые важные. Обновление считается успешным не тогда, когда apt завершился без ошибок, а когда работает то, ради чего сервер существует. Для каждого типа серверов у нас своя проверка: печать тестовой этикетки, запрос к базе, открытие страницы кассового приложения.

Если на пилоте что-то пошло не так, до следующей волны есть два дня. Проблемный пакет замораживается на остальных серверах одной командой, а волна идёт дальше без него:

ansible wave1:wave2 -b -m ansible.builtin.dpkg_selections \
  -a "name=cups-filters selection=hold"

Снимки и откат

Перед плановой волной виртуальным машинам делаем снимки. В Proxmox VE это одна команда на узле:

qm snapshot 104 pre-upgrade-2026-10-13 --description "перед плановым обновлением"
# если всё плохо
qm rollback 104 pre-upgrade-2026-10-13
# если всё хорошо — через неделю удаляем, снимки замедляют диск
qm delsnapshot 104 pre-upgrade-2026-10-13

Для физических серверов без снимков остаётся откат пакета на предыдущую версию. Старые .deb обычно ещё лежат в кэше:

grep -A3 'Start-Date: 2026-10-13' /var/log/apt/history.log   # что обновилось
ls /var/cache/apt/archives/ | grep cups-filters
sudo apt install ./cups-filters_1.28.17-3ubuntu1_amd64.deb  # старая версия из кэша
sudo apt-mark hold cups-filters

Если новое ядро не загружается, в меню GRUB можно выбрать предыдущее: apt не удаляет ни загруженное сейчас ядро, ни предыдущую установленную версию, так что Remove-Unused-Kernel-Packages безопасен.

Что мониторить

  • Возраст последнего успешного запуска unattended-upgrades — тревога, если больше 3 суток.
  • Число ожидающих исправлений безопасности — тревога, если не ноль дольше недели.
  • Наличие /var/run/reboot-required дольше 7 дней — значит, автоматическая перезагрузка отключена или не срабатывает.
  • Свободное место в /boot — переполнение ломает установку ядра и, как следствие, все последующие обновления.
  • Упавшие службы после перезагрузки: systemctl --failed.

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