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状態に限定します。
バックアップ状態と通知結果を分ける

既存の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_result、health_exit、state、event、deliveryだけを残します。通知URL、トークン、Authorizationヘッダー、HTTPレスポンス本文は残しません。
外部へ送る情報を最小化する

ntfyへ送るのは確認時刻、ホスト名、REDまたはGREEN、最初の1行要約、次の確認先だけです。snapshot一覧、環境ファイル、パスワードファイル、完全なjournalはローカルへ残します。
set -xとcurl -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、通知配送の監視、期限付き再送、別通知先への切り替えを、今回の状態機械を壊さない別レイヤーとして追加します。



コメント