目次
バックアップが壊れていたと気づくのは、たいてい復元が必要になったその日です。ジョブは 3 週間前にエラーで終了していて、誰もログを見ておらず、最新のスナップショットは取り消したい失敗よりも古い。
解決策は、より良いバックアップツールではありません。バックアップの問題が起きたその時間のうちに、人間に届くようにすることです。このガイドでは restic と borg(およびシェルから動くあらゆるもの)について、バックアップが実際に失敗する 3 つのパターンをカバーし、コピーして使えるスクリプトを紹介します。最後のステップでは Echobell を通じてアラートをスマートフォンに送るので、壊れたバックアップはメールボックスに埋もれず、着信として鳴ります。
バックアップが失敗する 3 つのパターン
最初の 1 つだけを監視するのが、最もよくある間違いです。
- 実行されて失敗する。 リポジトリのロック、ディスク満杯、パスワード違い、ネットワーク切断。ツールは非ゼロで終了します。
- 実行されて半分だけ成功する。 restic は、スナップショットは作成できたが一部のファイルを読めなかった場合に終了コード
3を返します。borg は警告のとき1を返します。どちらも見過ごしやすく、いちばん大事なディレクトリを黙ってスキップする権限問題を隠していることがあります。 - そもそも実行されない。 サーバーが落ちていた、タイマーが無効になっていた、crontab が上書きされた。プロセスがないのでエラーもありません。人を最も痛い目に遭わせるのはこの失敗です。
それぞれ別の検知方法が必要なので、3 つとも作ります。
ステップ 1:気づける channel を作る
Echobell で backups という channel を作り、重要なジョブ向けに通知タイプを 着信(Calling) にします。Webhook URL をコピーします。
https://hook.echobell.one/t/<channel-token>
寝ぼけていても意味が分かるテンプレートを、これから送るフィールドで作ります。
タイトル: {{host}} のバックアップ {{status}}
本文: {{job}}: {{detail}}
構文はテンプレートリファレンスを参照してください。
ステップ 2:ラッパースクリプトで失敗を捕まえる
小さなラッパーが終了コードをアラートに変えます。/usr/local/bin/backup-run として保存します。
#!/usr/bin/env bash
set -uo pipefail
HOOK="https://hook.echobell.one/t/<channel-token>"
JOB="nightly-restic"
alert() {
curl -fsS -m 10 -X POST "$HOOK" \
-H "Content-Type: application/json" \
-d "{\"job\":\"$JOB\",\"host\":\"$(hostname)\",\"status\":\"$1\",\"detail\":\"$2\"}" || true
}
restic backup /srv /etc --exclude-caches
code=$?
case $code in
0) ;; # 成功:何も通知しない
3) alert "incomplete" "Snapshot saved, but some files were unreadable" ;;
11) alert "locked" "Repository is locked by another process" ;;
12) alert "auth-failed" "Wrong repository password" ;;
*) alert "failed" "restic exited with code $code" ;;
esac
exit $code
細部が大事な理由は次のとおりです。
set -eは意図的に使っていません。restic が失敗してもスクリプトがアラートまで到達できるようにするためです。curlの|| trueにより、Webhook に届かなくても本来の終了コードが隠れません。-m 10は、ネットワーク呼び出しのハングが次回のスケジュール実行を妨げるのを防ぎます。
上で使った restic の終了コードは restic のマニュアルに記載されています。0 成功、1 致命的エラー、3 スナップショットは作成されたが一部のソースファイルが読めなかった、10 リポジトリが存在しない、11 ロックに失敗、12 パスワード違い。コードは時期によって追加されているので、手元のマシンの restic version に照らして確認してください。
borg と borgmatic
borg は成功で 0、警告で 1、エラーで 2 以上を返します。バックアップ中にファイルが変更されると 1 になりますが、ログディレクトリでは日常的なことなので、2 以上のときだけ着信で呼び出し、1 は静かな通知にするのが一般的です。
borg create --stats ::'{hostname}-{now}' /srv /etc
code=$?
if [ "$code" -ge 2 ]; then
alert "failed" "borg exited with code $code"
elif [ "$code" -eq 1 ]; then
alert "warning" "borg finished with warnings"
fi
borgmatic を使っているなら、borgmatic 自身がエラーになったときにコマンドを実行する on_error フックがあり、同じ curl を置く自然な場所です。ただし borgmatic のドキュメント自身が述べている限界に注意してください。borgmatic がそもそも動かなければ on_error は発火しません。それが失敗パターン 3 で、ステップ 3 が必要です。
ステップ 3:実行されなかったバックアップを捕まえる
ラッパーは、実行された回についてしか報告できません。沈黙を捕まえるには論理を反転させます。バックアップが 成功したとき に監視サービスへ ping を送り、ping が 遅れたら 監視サービスがあなたに知らせます。これがデッドマンスイッチです。
Healthchecks.io(ホスト版でもセルフホストでも)がまさにこれを行います。
- スケジュール
0 3 * * *、猶予時間 1〜2 時間でチェックを作成する。 - ジョブの最後にその ping を追加し、成功したときだけ発火するようにする。
- チェックのインテグレーションで、Echobell の channel URL を指す Webhook を追加する。
ping はラッパーの末尾に置きます。
# backup-run の最後の `exit $code` の直前に置く。成功時のみ ping する
if [ $code -eq 0 ]; then
curl -fsS -m 10 --retry 3 -o /dev/null https://hc-ping.com/<your-uuid>
fi
Healthchecks は /start と /fail の ping バリエーションにも対応しており、URL に終了コードを渡すこともできるので、1 つのチェックでハングしたジョブも検知できます。結果として、サーバーダウン、無効化されたタイマー、削除された crontab の行は、どれも同じ結末、つまり鳴るスマートフォンになります。一般的なパターンと他の監視サービスについては、cron ジョブ失敗アラートを参照してください。
ステップ 4:バックアップが新しく、復元できるか確認する
バックアップはスケジュールどおりに動き、終了コード 0 を返しても、役に立たないことがあります。たとえばマウントに失敗して空のディレクトリをバックアップしている場合です。別の日次ジョブに、手軽な 2 つの健全性チェックを追加しましょう。
スナップショットの鮮度。 最新のスナップショットが想定より古ければアラートを出します。
latest=$(restic snapshots --latest 1 --json | jq -r '.[0].time')
age_h=$(( ( $(date +%s) - $(date -d "$latest" +%s) ) / 3600 ))
if [ "$age_h" -gt 30 ]; then
alert "stale" "Newest snapshot is ${age_h}h old"
fi
リポジトリの整合性。 restic check はリポジトリ構造を検証します。週 1 回実行し、大きなリポジトリでは restic check --read-data-subset=5% で全部を読まずにデータをサンプリングできます。非ゼロ終了ならアラートを出します。
どちらも実際の復元テストの代わりにはなりません。四半期に 1 度、検証できるファイルを作業用ディレクトリに復元してください。バックアップが機能することを証明できる唯一のテストです。
ステップ 5:本当に必要なときだけ鳴らす
警告のたびに電話が鳴れば、channel をミュートしてしまい、肝心の 1 件で鳴らなくなります。Echobell の条件を使って、1 つの channel 内で段階を分けましょう。
status == "failed" || status == "stale" || status == "auth-failed"
incomplete と warning は、時間指定(Time Sensitive) または通常の 2 つ目の channel に送ります。鳴らずにロック画面に表示されます。日中の失敗は静かにしたいなら、条件リファレンスにある UTC 変数で時間帯を加えてください。
よくある質問
バックアップが失敗したときに通知を受け取るには?
バックアップコマンドをラップして終了コードを読み、非ゼロなら Webhook を送ります。Healthchecks.io のようなデッドマンスイッチを加えれば、開始されなかった実行にも気づけます。
restic のどの終了コードでアラートを出すべき?
実際の失敗を意味する 1、10、11、12 では強く知らせます。3(スナップショットは作成されたが一部のファイルが読めない)は権限問題を隠すことがあるため、調査すべき警告として扱います。
バックアップが黙って止まったことをどう知る?
ハートビートを使います。ジョブが成功時に監視サービスへ ping し、ping が遅れたら監視サービスが知らせます。エラーを上げるものが何も動いていないので、エラーフックではこれを捕まえられません。
終了コードが成功なら、バックアップを信用していい?
いいえ。ツールが完了したことを意味するだけで、データが完全であることも復元できることも意味しません。スナップショットの鮮度を確認し、定期的に整合性チェックを行い、復元テストを定期的に行ってください。
メールではなく電話で通知できる?
できます。メールは見逃しやすいものです。Echobell の着信 channel は着信のように振る舞い、iOS の集中モードを突破するので、午前 3 時のバックアップ失敗にも気づけます。
まとめ
3 つの検知器が 3 つの失敗パターンをカバーします。失敗した実行には終了コードのラッパー、無意味に成功した実行には鮮度チェック、そもそも起きなかった実行にはハートビート。それらを着信 channel に流し、警告は静かな channel に置けば、壊れたバックアップは週末を失う事態ではなく、5 分で直せる問題になります。
iPhone 版 Echobell をダウンロード、または Google Play で入手して backups channel を作り、わざとパスワードを間違えてラッパーを 1 回実行し、実際に鳴るのを確かめてください。