バックアップ先を2つから1つに減らした
非公開のGitリポジトリとクラウドストレージの2系統にバックアップしていたが、片方を捨てて1系統に絞った。2系統は「2倍安全」ではなく「壊れたら困るものが2つ」で、どちらも検証されていなかった。一本化に必要だった棚卸しの手順、復元に使う鍵をどこに置くか、実際にやった復元テストまでの記録。
- 公開日:
- 2026-10-05
自分のサーバーに写真・パスワード・ファイルを集めたあと、バックアップを2系統持っていた。GitHubの非公開リポジトリ(自分だけが見られるGitの保管場所)への日次バックアップと、クラウドのオブジェクトストレージ(Backblaze B2)への日次バックアップ。多いほうが安全だと思っていた。
調べたら、どちらも完全ではなく、どちらも検証されていなかった。GitHub側を廃止して1系統に絞り、代わりに「本当に戻せるか」を確かめる作業を足した。
前提
- 常時起動しているLinuxサーバー1台に、パスワード管理・写真・ファイル置き場・認証を同居させた状態。土台は自分の「サーバー」を持つとはどういうことか
- 道具は
rclone(いろいろなクラウドストレージへファイルを同期するコマンド)。送り先はBackblaze B2(安価なオブジェクトストレージ。1GBあたり月$0.006、最初の10GBは無料) - もう1系統は、作業用ディレクトリ一式を毎日
git commitして非公開リポジトリへpushする自作の仕組み
2系統が実際に守っていたもの
同じものを2箇所に置いているつもりだったが、並べたら役割が違っていた。GitHub側はテキスト類(会話ログ・設定・メモ)と添付ファイルで、強みは履歴——「このファイルは3週間前どう書かれていたか」を追える。B2側はデータベースのダンプ・写真ライブラリ・ファイル置き場の実データ・パスワードの保管庫で、強みは容量。つまり二重化ではなく分担で、片方だけでは静かに欠けが出る状態だった。
実際に測ったら、添付ファイルがB2に638件、GitHubに460件、重複ゼロ。時期で真っ二つに割れていた。git cloneだけで戻せば638件が消え、B2だけで戻せば460件が消える。
「2倍安全」ではなく「壊れたら困るものが2つ」
分担になっていた理由は単純で、2つあると、どちらも真剣に見なくなる。
B2側の同期スクリプトのログを読んだら、34回の起動のうち完走は5回だけだった。最後の処理でsudoがパスワードを求めて止まり、set -e(途中で失敗したらそこで中断する設定)で全体が終わっていた。データ本体の各工程は成功していたので実害はなかったが、問題はそこではない。「完走しました」のマーカーが1ヶ月出ていないのに、気づいていなかった。
これが冗長なバックアップ経路の本当のコストだと思う。ディスク代でもなく手間でもなく、どちらも検証されないこと。「もう片方があるから」と思っている限り、片方が壊れても痛みが出ないので、気づく機会そのものが無くなる。
GitHubがこの用途に向いていなかった理由
- サイズと中身。Gitはテキストには強いが、画像のようなバイナリは版が増えるたびに丸ごと溜まる。
.git(履歴の実体)が346MBあり、作業用ディレクトリ751MBの半分近くを占めていた(1年後には847MB) - 履歴は消せない。消さないことが価値の仕組みなので、間違って入れたものが残る。実際、秘密情報を含むディレクトリ(APIトークン類やサーバーの秘密鍵)が気づかないうちに毎日pushされ続けていた。「除外設定に入れてある」と自分のメモに書いてあったが、入っていなかった
- 他人のアカウント方針の上に乗っている。容量制限も規約も将来どう変わるかは自分では決められない
- そもそも用途が違った。置いていたのは日々の記録で、ソースコードではない。全ての版を永久に取っておく必要がない
一方で**「履歴が追える」価値は本物だったので代わりを用意した。B2のバージョン保持**(上書き・削除された古い版を一定期間取っておく設定)で、自分のバケット(ストレージ上のフォルダのような入れ物)は旧バージョンを30日保持している。実際にデータベースのダンプは26世代遡れた。30日の「取り消し」が成立していればGitの履歴機能の大部分は代替できる。なお公開しているもの(ブログ・ポートフォリオ)はGitHubのままにしている。廃止したのは「バックアップ置き場として使うこと」だけ。
一本化の本体は棚卸しだった
ここが一番再利用できる部分だと思う。片方を止める作業の本体は、「GitHub側にしか無いものが、B2側に全部入っているか」を1件ずつ確認すること。やらずに止めると、欠けたことに気づくのは復元が必要になった日になる。
1. 止める側の中身を、実物から列挙する
設計書ではなく実際のファイル一覧から出す。自分はここで一度間違えた。手元のメモの箇条書きだけを読んで「テキスト類がB2に入っていない」と誤判断したが、スクリプト本文を開いたら既に実装済みだった。メモで判断せず、スクリプト本文とファイル一覧を実見する。
2. 残す側の対象リストと突き合わせる
ここで本物の穴が見つかった。環境変数をまとめたファイル(.env。各種サービスのトークンが入っている)が、GitHubにもB2にも無かった。2系統あったのに、どちらにも無いものが存在した。
3. Gitの履歴そのものを、ファイルとして退避する
履歴は普通のファイル同期では運べない。git bundle(リポジトリ全体を1ファイルに固める機能)を使った。
git bundle create /tmp/history-20260928.bundle --all
git bundle verify /tmp/history-20260928.bundle
rclone copy /tmp/history-20260928.bundle <暗号化remote>:<保存先>/
- 🔴
rootでgitを叩くとdetected dubious ownershipで弾かれる。ディレクトリの所有者ユーザーで実行する - 出来上がりは831MB。往復まで確認した——取り出してmd5(内容の指紋)が一致し、
git clone <bundleファイル> <ディレクトリ>で162コミットが復元でき、履歴の中にしか無いPDFを元のサイズどおり取り出せた - 🔴 一度きりのスナップショット。以後履歴が伸びても自動では更新されない
4. 止める前に「止める側にしか無いコミット」がゼロだと確認する
git rev-list --count <リモート>/main --not --allが0であること。
5. 浮いた分を別の軸に回す
テキスト類だけを対象にした軽い同期を1日4回動かすようにした(全体の同期は従来どおり夜1回)。これでRPO(障害時に失われる最大の時間幅)が最大24時間から最大6時間に縮んだ。重い写真ライブラリに触らないので約10秒で完走する。加えて30日を超える長期の記録として、テキスト類のみの日付付きtar.gzスナップショットを週1回置いた。実測16MBなので年0.8GB、費用はほぼゼロ。
- 🔴 バックアップを取る道具自体がバックアップ対象外だった。同期スクリプトを置いたディレクトリは、どちらの系統にも入っていなかった。サーバーが全損したらデータは戻るが戻し方を書いた道具が無い
- 🔴 データベースのダンプを取る行に管理者パスワードが直書きされていた。権限600の
.envに切り出して読み込む形にし、読めなければexit 1、変数が空なら: "${VAR:?未設定}"で止める(黙って空のパスワードで走らせないため)。渡し方は-p'…'のような引数ではなく環境変数経由にする——引数はpsで他のユーザーから見える。なお平文の秘密はスクリプト本文を直しても消えない。過去の退避ファイルと稼働中のコンテナの環境変数に同じ値が残っており、本当に消すにはパスワードの変更が必要だった
何を金庫に入れるか
バックアップを復元するための鍵が、バックアップの中にしか無いと、永久に開かない。 いわゆるブートストラップ問題(起動するために起動済みの何かが必要になる循環)。自分の場合、B2から取り出すための接続情報(rcloneの設定ファイル)はサーバーの中だけにあった。毎日バックアップは成功していたが、サーバーが丸ごと壊れた瞬間に、無事なデータを開ける人が誰もいなくなる。
再発行できる鍵は金庫に入れない。再発行できない鍵だけ入れる。
- 入れる(再発行不能): 暗号化パスワード(失うとバックアップが永久に読めない石になる)、ストレージ事業者のアカウントの2段階認証バックアップコード、復元用の接続情報
- 入れない(再発行可能): 各種APIトークン、ストレージのアプリケーションキー。上位のアカウントにログインできれば1分で作り直せる
- 置くべきは鍵そのものではなく、上位の認証(アカウントのログイン情報と2段階認証)。それがあれば下位の鍵は何度でも生やせる
- 🔴 何でも放り込むと、金庫自体が肥大した単一障害点になる
置き場所はパスワードマネージャー(1PasswordからVaultwardenに移したもの)にした。スマートフォンのアプリは一度ログインすればオフラインのキャッシュが残るので、サーバーが全損してもそこから取り出せる。ただし金庫自身がサーバー上で動いているので、スマートフォンとサーバーを同時に失うと詰む。対処は「紙でもう1部持つ」しかない。
暗号化と、やらなかったこと
実査したら事業者側の暗号化(SSE)すら無効になっていて、写真は生のJPEGのままディスクに置かれていた。「バケットは非公開だから最低限守られている」という感覚は間違いだった。2種類あることを区別しておく。
- 事業者側の暗号化: 倉庫が金庫に入れてくれるが鍵は倉庫が持つ。退役ディスクの流出には効くが事業者自身には無力で、手間はゼロ。🔴 有効にしても遡らない
- 送る前に自分で暗号化: 自分で鍵をかけた箱を渡す。事業者にもアカウント乗っ取りにも強いが、鍵を失えば永久に読めないし、既存分は全量を再アップロードになる
やったのは前者を有効化したうえで、テキスト類・秘密情報・データベースのダンプだけを後者にすること。rcloneにはcryptという、送信前に暗号化して保存名も不可読にする仕組みがある。初回は6,710オブジェクト・69MBで5分43秒、以後は差分で数十秒だった。
やらなかったこと。
- 写真ライブラリ131GBは自分で暗号化していない。全量の再アップロードになるのと、暗号化する動機(「事業者に読まれたくない」のか「凍結で消えるのが嫌」なのか)を整理できていなかったため。後者ならもう達成済みで、前者ならまだ未達。整理できていないまま重い工事をしない
- 🔴 パスワードの保管庫も、意図的に暗号化していない。中身はそれ自体がゼロ知識暗号化(サービス側も読めない方式)済みで平文ではなく、何より暗号化パスワードの保管場所がその金庫なので、暗号化すると循環依存になる
誰も戻したことのないバックアップは、バックアップではない
1. 生のファイルから実際に復元して突き合わせる。 124ファイルが完全一致した。暗号化remote経由でちゃんと復号できることも同時に確かめられる。
rclone copy <暗号化remote>:<対象パス> /tmp/restore-test/
diff -rq /tmp/restore-test/ /home/<user>/<元のディレクトリ>
2. 週次スナップショットを取り出してtar xzfで展開する。 6,644ファイル・74MBが正常に展開でき、自動処理の定義ファイル16本まで含まれていた。
3. 全ファイルを照合する。 差分19件。中身を見たらすべて同期が走った時刻より後に生成されたファイルで、取りこぼしはゼロだった。
rclone check --one-way <ローカル> <暗号化remote>:<対象パス>
--one-wayはリモートにしかない古い版を差分に数えないので、検証にはこちらを使う。
併せて事業者からの通知メールの読み方を決めた。「マスターキーが生成されました」のような通知は、自分がコンソールを触っていた日なら本人操作、触っていない日なら本物の警戒シグナル。先に線引きしないと、通知を全部無視する習慣がつく。
残った1系統の弱いところ
- 事業者1社への依存は未対策。アカウントの凍結や会社都合には何の備えもない。別会社のストレージへ追記専用でもう1本(月€4前後)が答えだが、見送った
- 削除の伝播と容量の兼ね合いが未整理。送信側の鍵から削除権限を外すと
rclone syncが使えずcopyしかできなくなり、ローカルで消したものが残り続けて容量が増える。守りと費用がここで正面衝突する - 一番大きい穴は、サーバーを乗っ取られた場合。1系統しかないので、送信に使っている鍵を盗られたらバックアップ本体を消される。これは別テーマとして切り出し、鍵の権限削減・削除が伝播しない複製先・サーバーの外で動く見張りの3層で作り直した。そちらはバックアップを「消されても戻せる」形にするに書いた
費用は全バックアップ合計125GBで月約$0.69。一本化で減ったのは費用ではなく、気にかけておく仕組みの数だった。
更新履歴 — 2026-10-05: 初版