自動バックアップが失敗したときだけntfy通知する方法|Ubuntu Server 26.04

初回REDで障害通知し継続REDを抑制してGREEN復旧時に一度だけ通知する状態遷移 はじめての自宅サーバー

15本目では、GREEN・YELLOW・REDを通知文へ変換し、外部送信せずdry-run確認しました。16本目では初めてネットワーク通信を追加し、初回REDとGREEN復旧だけをntfyへ1回ずつ通知します。

REDのたびに送ると通知が増え続けます。反対に通知の失敗をバックアップの状態へ上書きすると、原因が分からなくなります。今回は「バックアップの健康状態」と「通知済みの状態」を分け、通知嵐と見逃しを同時に防ぎます。

バックアップ処理は変更しません

この記事のスクリプトは既存ヘルスチェックを1回呼ぶだけです。バックアップ再実行、修復、削除、restic forget・prune・repair・unlockは行いません。

16本目の到達点と非目標

  • GREEN: 外部通知なし
  • YELLOW: 外部通知せずローカル記録のみ
  • 初回RED: 障害通知を1回送る
  • 継続RED: 同じ通知を繰り返さない
  • ALERTEDからGREEN: 復旧通知を1回送る
  • 配送失敗: pending状態を残し、次回の定期確認で再試行する

複数通知先、指数バックオフ、短周期の自動再送、通知キュー、複数サーバー統合監視は扱いません。まず1台・1通知先・4状態に限定します。

バックアップ状態と通知結果を分ける

バックアップの健康状態と通知プログラムの配送結果を別の終了コードで管理する図
バックアップがREDでも通知配送は成功できます。2つの結果を混ぜません。

既存のouchi-lab-backup-healthは0=GREEN、1=YELLOW、2=REDを返します。通知スクリプトは別に、0=正常、20=設定エラー、21=通信失敗、22=HTTP拒否、23=未知のhealth結果、75=多重実行を返します。

health_exit=2
notification_exit=0

この組み合わせは「バックアップはREDだが、障害通知は正常に送れた」という意味です。REDを通知できたserviceをsystemd上で失敗扱いにしないため、healthの終了コードをそのまま通知serviceから返しません。

set -eの直後にhealthを実行する形も避けます。REDの終了コード2でスクリプトが止まり、一番必要な障害通知へ到達できないためです。

4つの通知状態をローカルへ保存する

状態 意味 次の動作
HEALTHY 通知済み障害なし REDなら障害通知
ALERT_PENDING 障害通知が未配送 RED継続なら次回再試行
ALERTED 障害通知済み 継続REDを抑制、GREENなら復旧通知
RECOVERY_PENDING 復旧通知が未配送 GREEN継続なら次回再試行

状態は/var/lib/ouchi-lab-backup-notify/stateへroot所有・600で保存します。再起動で消える/tmp/runは使いません。一時ファイルへ書いてからmvで置換し、flockで多重実行も止めます。

配送成功後にだけALERTEDまたはHEALTHYを確定します。先に「通知済み」と記録すると、通信失敗した通知を永久に失うためです。

ただし、外部側が受信した直後にローカルstateの書き込みだけが失敗した場合、次回に同じ通知が再送される可能性は残ります。終了コード20やstate異常が出たらtimerを止め、状態ファイルを確認してから再開してください。

ntfyの通知先を安全に用意する

ntfyはHTTP PUT / POSTでメッセージを公開でき、保護されたtopicにはBearer形式のアクセストークンを利用できます。仕様はntfy公式のPublishingで確認できます。

実際のURLとトークンはGit管理するスクリプトへ書かず、固定ファイルをsudoeditで作ります。

sudoedit /etc/ouchi-lab-backup-notify.env
NTFY_URL=https://ntfy.example.com/protected-topic
NTFY_TOKEN=replace-with-dedicated-publish-token
sudo chown root:root /etc/ouchi-lab-backup-notify.env
sudo chmod 600 /etc/ouchi-lab-backup-notify.env
sudo stat -c '%U:%G %a' /etc/ouchi-lab-backup-notify.env

最後の表示がroot:root 600であることを確認します。トークンをコマンド引数やshell履歴へ直接書かないでください。スクリプトはroot所有・600の設定だけを読み、curl用の一時設定も700の状態ディレクトリ内へ作って処理後に消します。

通知スクリプトを配置する

完成版はリポジトリのexamples/ouchi-lab-backup-notify.shです。固定パスのhealthだけを呼び、任意コマンドは受け取りません。

sudo install -o root -g root -m 755 \
  ./ouchi-lab-backup-notify.sh \
  /usr/local/sbin/ouchi-lab-backup-notify

sudo /usr/bin/bash -n /usr/local/sbin/ouchi-lab-backup-notify

curlは接続5秒、通信全体10秒で止め、systemd側も20秒で停止します。curl公式man page--connect-timeout--max-timeを使います。自動retryは入れません。相手へ届いた後に応答だけ失われた場合、即時再送すると二重通知になるためです。

本物のバックアップを壊さず状態遷移を試す

--test-resultは本番と別の/var/lib/ouchi-lab-backup-notify-testへ状態を保存します。healthやresticは変更しません。

sudo /usr/local/sbin/ouchi-lab-backup-notify --test-result GREEN
sudo /usr/local/sbin/ouchi-lab-backup-notify --test-result RED
sudo /usr/local/sbin/ouchi-lab-backup-notify --test-result RED
sudo /usr/local/sbin/ouchi-lab-backup-notify --test-result GREEN

通知は「障害」「復旧」の2通だけ届きます。2回目のREDはduplicate_suppressedとして外部送信されません。

配送失敗も外部サービスを壊さず模擬できます。

sudo /usr/local/sbin/ouchi-lab-backup-notify \
  --test-result RED --force-delivery-failure
printf 'notification_exit=%s
' "$?"

sudo /usr/local/sbin/ouchi-lab-backup-notify --test-result RED

最初は終了コード21でALERT_PENDINGを維持し、次のREDで送信に成功するとALERTEDになります。URLやトークンをわざと壊して試す必要はありません。

systemd serviceとtimerを配置する

sudo install -o root -g root -m 644 \
  ./ouchi-lab-backup-notify.service \
  /etc/systemd/system/ouchi-lab-backup-notify.service

sudo install -o root -g root -m 644 \
  ./ouchi-lab-backup-notify.timer \
  /etc/systemd/system/ouchi-lab-backup-notify.timer

sudo systemd-analyze verify \
  /etc/systemd/system/ouchi-lab-backup-notify.service \
  /etc/systemd/system/ouchi-lab-backup-notify.timer

timerは13〜14本目の確認より後になるよう、例では毎日03:40にしています。自宅のバックアップ時刻に合わせ、十分な余裕を取って変更してください。

sudo systemctl daemon-reload
sudo systemctl enable --now ouchi-lab-backup-notify.timer
systemctl list-timers --all ouchi-lab-backup-notify.timer

Restart=alwaysやcurlの無制限retryは設定しません。配送失敗はpendingのまま次の日の定期実行へ渡します。

journalで2つの結果を確認する

systemctl status ouchi-lab-backup-notify.service --no-pager
systemctl show ouchi-lab-backup-notify.service \
  -p Result -p ExecMainStatus -p InvocationID
journalctl --unit=ouchi-lab-backup-notify.service \
  --since "today" --no-pager

ログにはbackup_resulthealth_exitstateeventdeliveryだけを残します。通知URL、トークン、Authorizationヘッダー、HTTPレスポンス本文は残しません。

外部へ送る情報を最小化する

詳細ログをローカルに残して外部通知へ送る情報を最小化する安全境界
外へ出すのは短い通知だけ。秘密情報と詳細な証拠はサーバー内へ残します。

ntfyへ送るのは確認時刻、ホスト名、REDまたはGREEN、最初の1行要約、次の確認先だけです。snapshot一覧、環境ファイル、パスワードファイル、完全なjournalはローカルへ残します。

set -xcurl -vは禁止です。デバッグ表示は認証ヘッダーや送信先をjournalへ混ぜる可能性があります。

異常時の停止条件

終了コード 意味 止まって確認する場所
20 設定・state異常 所有者、600、値、シンボリックリンク
21 通信・タイムアウト DNS、ネットワーク、通知先の稼働
22 HTTP拒否 topic権限、publishトークン
23 未知のhealth結果 healthスクリプトと記事の版
75 多重実行 手動実行とtimerの重なり

通知のためにバックアップを再実行しません。まず既存のjournalとsnapshotを証拠として残し、11本目の読み取り専用切り分けへ戻ります。

通知だけを停止して切り戻す

sudo systemctl disable --now ouchi-lab-backup-notify.timer
systemctl is-enabled ouchi-lab-backup-notify.timer
systemctl is-active ouchi-lab-backup-notify.timer

状態ファイルとjournalは削除せず残します。バックアップservice、healthスクリプト、resticリポジトリには触れません。通知設定を直した後、テストモードから再確認します。

まとめ

16本目では、初回REDだけを障害通知し、継続REDを抑制し、GREEN復旧を1回通知する状態遷移を作りました。バックアップの健康状態と通知配送の結果を分けたため、ntfyが停止しても元の証拠とRED判定は失われません。

次に拡張するなら、複数サーバーのevent ID、通知配送の監視、期限付き再送、別通知先への切り替えを、今回の状態機械を壊さない別レイヤーとして追加します。

コメント

タイトルとURLをコピーしました