Привіт, я Kernelka. Сьогодні ми зробимо для ваших віддалених серверів розумні оновлення: автоматичний апдейт пакетів, швидкі smoke‑тести сервісів і миттєвий відкат, якщо щось пішло не так. Це безпечніший підхід до оновлення Linux на проді й деві, і він не складніший за звичайний скрипт 🙂

Що саме ми побудуємо

  • bash скрипти, які: створюють знімок системи (Btrfs snapper або LVM snapshot), оновлюють пакети, запускають smoke‑тести, і за потреби відкатують зміни;
  • systemd unit + timer (так, саме cron та systemd timers), щоб це все працювало за розкладом;
  • простий механізм розгортання на віддалених вузлах через SSH підключення.

Передумови та безпека

  • Ви маєте root або sudo з NOPASSWD на серверах.
  • Сховище: бажано Btrfs зі snapper або root на LVM. Без цього відкат буде обмежений.
  • Пакетний менеджер: apt (Debian/Ubuntu) або dnf (RHEL/Alma/Rocky/Fedora).

Основний How‑to: скрипт оновлення + smoke‑тести + відкат

Створимо головний скрипт /usr/local/sbin/auto-update-smoke.sh. Він без інтерактиву, логгер, підтримка apt/dnf, Btrfs/LVM snapshots.

#!/usr/bin/env bash
set -euo pipefail
LOG=/var/log/auto-update-smoke.log
SMOKE_DIR=/etc/auto-smoke.d
ROLLBACK_HINT=/run/auto-update.rollback

log(){ echo "[$(date -Is)] $*" | tee -a "$LOG"; }

# Визначення пакетного менеджера
pkg_mgr() {
  if command -v apt-get >/dev/null; then echo apt; elif command -v dnf >/dev/null; then echo dnf; else echo none; fi
}

# Детекція бекаенду знімків
have_snapper(){ command -v snapper >/dev/null && snapper list >/dev/null 2>&1; }
have_lvm(){ lsblk -o TYPE | grep -q lvm; }
VG_NAME=${VG_NAME:-vg0}
LV_ROOT=${LV_ROOT:-root}
SNAP_SIZE=${SNAP_SIZE:-1G}

create_snapshot(){
  if have_snapper; then
    PRE=$(snapper -c root create --print-number --type pre --description "pre-update")
    echo "btrfs:$PRE" > "$ROLLBACK_HINT"
    log "Created Btrfs pre snapshot #$PRE"
  elif have_lvm && [ -e "/dev/${VG_NAME}/${LV_ROOT}" ]; then
    lvcreate -L "$SNAP_SIZE" -s -n ${LV_ROOT}_pre "/dev/${VG_NAME}/${LV_ROOT}" >>"$LOG" 2>&1
    echo "lvm:${VG_NAME}:${LV_ROOT}_pre" > "$ROLLBACK_HINT"
    log "Created LVM snapshot ${VG_NAME}/${LV_ROOT}_pre"
  else
    log "No snapshot backend found; rollback will be limited"
    echo "none" > "$ROLLBACK_HINT"
  fi
}

cleanup_snapshot(){
  if [ ! -f "$ROLLBACK_HINT" ]; then return; fi
  H=$(cat "$ROLLBACK_HINT")
  case "$H" in
    btrfs:*)
      PRE=${H#btrfs:}
      snapper -c root create --type post --pre-number "$PRE" --description "post-update" >>"$LOG" 2>&1 || true
      ;;
    lvm:*)
      IFS=':' read -r _ VG SNAP <<< "$H"
      lvremove -f "/dev/${VG}/${SNAP}" >>"$LOG" 2>&1 || true
      ;;
  esac
  rm -f "$ROLLBACK_HINT"
}

rollback_now(){
  if [ ! -f "$ROLLBACK_HINT" ]; then log "Nothing to rollback"; return 1; fi
  H=$(cat "$ROLLBACK_HINT")
  case "$H" in
    btrfs:*)
      PRE=${H#btrfs:}
      log "Rolling back to Btrfs snapshot #$PRE"
      snapper -c root rollback "$PRE" >>"$LOG" 2>&1 || true
      log "Rebooting to finish Btrfs rollback"; systemctl reboot || reboot
      ;;
    lvm:*)
      IFS=':' read -r _ VG SNAP <<< "$H"
      log "Merging LVM snapshot ${VG}/${SNAP}"
      lvconvert --merge "/dev/${VG}/${SNAP}" >>"$LOG" 2>&1 || true
      log "Rebooting to finish LVM rollback"; systemctl reboot || reboot
      ;;
    *)
      log "No snapshot backend; cannot auto-rollback"
      return 1
      ;;
  esac
}

run_updates(){
  case "$(pkg_mgr)" in
    apt)
      DEBIAN_FRONTEND=noninteractive apt-get update >>"$LOG" 2>&1
      DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confnew dist-upgrade >>"$LOG" 2>&1
      ;;
    dnf)
      dnf -y --refresh upgrade >>"$LOG" 2>&1
      ;;
    *) log "Unsupported package manager"; exit 2;;
  esac
}

run_smoke(){
  local ok=0
  if [ -d "$SMOKE_DIR" ]; then
    for t in "$SMOKE_DIR"/*; do
      [ -x "$t" ] || continue
      log "Run smoke: $t"
      if "$t" >>"$LOG" 2>&1; then ok=$((ok+1)); else return 1; fi
    done
  else
    # Мінімальний дефолтний тест SSH
    systemctl is-active sshd >/dev/null 2>&1 || systemctl is-active ssh >/dev/null 2>&1
  fi
  log "Smoke passed ($ok tests)"; return 0
}

main(){
  log "===== Auto update start ====="
  create_snapshot
  if ! run_updates; then log "Update failed"; rollback_now; exit 1; fi
  if ! run_smoke; then log "Smoke tests failed"; rollback_now; exit 1; fi
  cleanup_snapshot
  log "All good. Bye!"
}

main "$@"

Надайте права і створіть каталог для тестів:

sudo install -m 0755 -o root -g root /dev/stdin /usr/local/sbin/auto-update-smoke.sh <<'EOF'
# (вставте сюди вміст скрипту з прикладу вище)
EOF
sudo chmod +x /usr/local/sbin/auto-update-smoke.sh
sudo mkdir -p /etc/auto-smoke.d

Приклад простого smoke‑тесту HTTP (перевірка 200 на :80):

sudo tee /etc/auto-smoke.d/http80.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
curl -fsS --max-time 5 http://127.0.0.1:80/ >/dev/null
EOF
sudo chmod +x /etc/auto-smoke.d/http80.sh

systemd service + timer

Створюємо unit і таймер для регулярних запусків (наприклад, щодня о 03:30). Це зручніше за cron, і саме так ми використовуємо cron та systemd timers у сучасних дистрибутивах.

sudo tee /etc/systemd/system/auto-update-smoke.service >/dev/null <<'EOF'
[Unit]
Description=Auto update with smoke tests and rollback
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/auto-update-smoke.sh
Nice=10
IOSchedulingClass=idle
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

sudo tee /etc/systemd/system/auto-update-smoke.timer >/dev/null <<'EOF'
[Unit]
Description=Daily auto update with smoke tests

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=15m

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now auto-update-smoke.timer
sudo systemctl list-timers | grep auto-update-smoke

Альтернативні способи

  • Ansible + systemd: розгортаєте файли unit/скриптів як шаблони й керуєте розкладом централізовано.
  • Unattended-upgrades (Debian/Ubuntu): добре для безпечних оновлень, але без знімків/відкату. Можна комбінувати з нашим smoke‑ранером.
  • dnf-automatic (RHEL/Fedora): аналогічно, але з мінімальною логікою тестів.

GUI‑спосіб (якщо хоч трохи зручності хочеться)

Через веб-консоль Cockpit ви можете переглядати журнали, керувати systemd юнітами/таймерами та вручну запускати сервіс оновлення. Це не замінює автоматизацію, але допомагає спостерігати за оновлення Linux у реальному часі.

Розгортання на віддалені сервери

Маючи список хостів у файлі hosts.txt і налаштоване SSH підключення по ключах, виконайте:

while read -r H; do
  echo "-- $H"
  scp /usr/local/sbin/auto-update-smoke.sh $H:/tmp/
  ssh $H 'sudo mv /tmp/auto-update-smoke.sh /usr/local/sbin/ && sudo chmod +x /usr/local/sbin/auto-update-smoke.sh'
  scp /etc/systemd/system/auto-update-smoke.* $H:/tmp/
  ssh $H 'sudo mv /tmp/auto-update-smoke.* /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now auto-update-smoke.timer'
  ssh $H 'sudo systemctl status auto-update-smoke.timer --no-pager -l'
done < hosts.txt

FAQ

Як перевірити, що все працює зараз, не чекаючи таймера?

Запустіть сервіс вручну і відстежте лог:

sudo systemctl start auto-update-smoke.service
sudo journalctl -u auto-update-smoke.service -f

Чи обов’язкові Btrfs або LVM?

Ні, але без них справжній автоматичний відкат неможливий. Скрипт спробує оновити й протестувати, але у разі збою відкат вимагатиме ручного втручання.

Що саме перевіряють smoke‑тести?

Те, що ви туди покладете: статуси сервісів (systemd), відповідь HTTP, доступність портів, виконання ключових CLI. Додавайте нові виконувані файли до /etc/auto-smoke.d.

apt проти dnf — є різниця?

Скрипт підтримує обидва: apt-get dist-upgrade для Debian/Ubuntu і dnf upgrade для RPM-дистрибутивів. Логіка snapshot/rollback однакова.

Потрібне перезавантаження?

Тільки якщо стався відкат (Btrfs або LVM), бо треба активувати попередній знімок. Після успішних оновлень ребут не потрібен, якщо не оновлювалось ядро або критичні компоненти.

Як задати інший час запуску?

Змініть OnCalendar у таймері на свій графік, наприклад Mon,Fri 02:00, і перезавантажте таймер: systemctl daemon-reload && systemctl restart auto-update-smoke.timer.

Чи можна пропустити окремі пакети?

Так, у apt використовуйте apt-mark hold pkg, у dnf — exclude= у /etc/dnf/dnf.conf. Це добре працює для критичних сервісів.

Порада від Kernelka

Додайте короткий «канарейковий» сервер у кожному середовищі. Спочатку таймер вмикайте там, аналізуйте логи, а вже потім розкочуйте на всі вузли. Так помилки ловляться раніше, і прод спить спокійніше 🫶

Підсумок

  • Автоматизація через systemd timers дає передбачуваність і історію виконань.
  • Знімки Btrfs/LVM роблять відкат швидким і надійним.
  • Smoke‑тести ловлять проблеми відразу після оновлення.
  • Розгортання через SSH просте; для масштабу підключайте Ansible.
  • Це безпечний та відтворюваний підхід до оновлення Linux на сервері.