
tar+cron でバックアップを回している。rsync で別サーバに同期もしている。「バックアップは取れています」と言える状態だ。
でも、本当にそうだろうか。
障害が起きてデータを戻す必要が生じたとき、実際に手を動かして確認したことはあるか。リストア手順を書いた文書はあるか。暗号化はされているか。——こうした問いに即答できないなら、それは「バックアップが取れている」ではなく「バックアップらしきものが動いている」状態かもしれない。
この記事では、tar+cron・rsync ベースの運用が構造的に抱える問題を整理したうえで、restic + Cloudflare R2 による最小構成への移行イメージを、実際のコマンドフローを通じて示す。
tar+cron・rsync バックアップの「動いてはいる」という罠
典型的な cron バックアップはこんな形をしている。
# /etc/cron.d/backup
0 2 * * * root tar czf /backup/app-$(date +%Y%m%d).tar.gz /var/www/app
毎朝2時に tar で固めて、日付をファイル名に入れて保存する。シンプルで分かりやすい。
問題は、これが積み重なると何が起きるかだ。
フルバックアップが毎日増え続ける。 差分や重複排除の仕組みがないので、変更が少ない日でも同じサイズのアーカイブが生成される。
ディスクが逼迫してきたら古いファイルを手で消す、あるいは find で一定日数より古いものを削除するスクリプトを追加する——という運用になりがちだ。
世代管理がファイル名頼みになるのも問題だ。app-20250101.tar.gz が残っていても、それが正常に取れたアーカイブかどうかは展開してみないと分からない。
圧縮途中で止まったファイルが静かに残っていることも珍しくない。
暗号化は、ほとんどの場合されていない。 バックアップ先のサーバやストレージに侵入されれば、データはそのまま読まれる。
rsync については、別の誤解がある。rsync はミラーリングツールであって、バックアップツールではない。
rsync -avz /var/www/app/ backup-server:/backup/app/
これはソースとデスティネーションを「同期」する。ソース側でファイルを誤って削除すれば、次の rsync 実行時にデスティネーション側からも消える。
「バックアップがある」と思っていたのに、誤操作が即座に伝播していた——というのは、rsync 運用でよくある事故パターンだ。
そして最大の問題は、リストア手順が属人化していることだ。担当者の頭の中にしかない手順、あるいは「やったことがない」という状態で本番障害を迎えると、何が起きるかは想像に難くない。
落とし穴をまとめると:
- 暗号化なし:保存データが平文のまま
- フルバックアップの肥大化:重複排除なしで容量が線形に増える
- 世代管理がファイル名頼み:整合性の確認手段がない
- リストア手順の属人化:障害時に初めて「手順がない」と気づく
- rsync の同期と混同:誤操作・削除がそのまま伝播する
「動いてはいる」状態が危険なのは、問題が顕在化するのが必ず障害時だからだ。平常時には何も警告が出ない。
restic が解決すること:リポジトリ・スナップショット・重複排除
restic を理解するには、まず「リポジトリ」という概念を掴むといい。
リポジトリとは、暗号化済みオブジェクトの倉庫だ。 restic はバックアップデータを独自フォーマットのオブジェクトに変換し、AES-256 で暗号化してリポジトリに格納する。
暗号化はオプションではなく、デフォルトで強制される。パスワードなしにリポジトリを初期化することはできない。
スナップショットは、ある時点のファイルツリーへのポインタだ。 restic backup を実行するたびに新しいスナップショットが作られるが、これは「その時点のファイル一覧と、各ファイルが参照するデータブロックへの参照情報」を持つ軽量なオブジェクトだ。
実データの重複コピーではない。
ここでチャンクベースの重複排除が効いてくる。restic はファイルをコンテンツに基づいた可変長チャンクに分割し、同一チャンクは一度しか保存しない。
ファイルの途中だけが変わった場合でも変更部分のチャンクだけが追加保存される。毎日バックアップを取っても、実際に増えるストレージは変更分だけという動きになる。
バックエンドを選ばない設計も restic の強みだ。S3互換ストレージ・SFTP・ローカルディスクなど、同じコマンドで異なるバックエンドを扱える。
類似ツールとして BorgBackup がある。Borg も暗号化・重複排除・スナップショット管理を備えた優れたツールだが、S3互換オブジェクトストレージへの直接書き込みには対応していない。
restic はオブジェクトストレージをネイティブにサポートしており、自前サーバなしでオフサイトバックアップを実現しやすい。
前節の落とし穴との対応を整理すると:
- 暗号化なし → AES-256 がデフォルト強制。鍵はパスワードから導出され、ストレージ側には渡らない
- フルバックアップの肥大化 → チャンクベース重複排除で変更分のみ追加保存
- 世代管理がファイル名頼み → スナップショット単位で管理。
forget --pruneでポリシーベースの世代管理 - リストア手順の属人化 →
restic restoreコマンドが一貫したリストア手順を提供 - rsync の同期と混同 → スナップショットは独立して保持。誤削除が過去スナップショットに伝播しない
Cloudflare R2 をバックエンドに:init から forget --prune までの実務フロー
Cloudflare R2 を選ぶ実務的な理由は明快だ。エグレス(データ取り出し)が無料で、S3互換 API を持ち、自前サーバが不要。
バックアップデータを取り出す頻度が低い用途では、コスト面で有利に働く。料金の詳細は Cloudflare R2 の公式ページ を参照してほしい。
R2 の初期設定
Cloudflare ダッシュボードで R2 バケットを作成し、「R2 API トークン」を発行する。トークン発行時に「オブジェクトの読み取りと書き込み」権限を付与し、対象バケットを指定する。
発行後に表示される Access Key ID と Secret Access Key を控えておく(再表示できない)。
環境変数を設定する。restic が S3互換ストレージに接続する際は以下の変数が必要だ。
export AWS_ACCESS_KEY_ID="your-r2-access-key-id"
export AWS_SECRET_ACCESS_KEY="your-r2-secret-access-key"
export RESTIC_REPOSITORY="s3:https://<accountid>.r2.cloudflarestorage.com/<bucket-name>"
export RESTIC_PASSWORD="your-strong-passphrase"
RESTIC_REPOSITORY はパススタイル URL で指定する。<accountid> は Cloudflare ダッシュボードの R2 セクションで確認できるアカウント ID だ。
R2 では AWS_DEFAULT_REGION の指定は不要で、省略して問題ない(auto を指定しても動作する)。AWS S3 とは異なりリージョン概念がないため、restic 側でも特別な設定は必要ない。
init:リポジトリの初期化
restic init
初回のみ実行する。R2 バケット内に restic のリポジトリ構造が作成される。このコマンドが成功すれば、接続情報・認証情報・バケット権限がすべて正しいことの確認にもなる。
backup:スナップショットの取得
restic backup /var/www/app /etc
複数のパスをスペース区切りで指定できる。初回は全データをアップロードし、2回目以降は変更チャンクのみ追加される。
restic backup /var/www/app --exclude='*.log' --exclude='node_modules/'
--exclude オプションで除外パターンも指定できる。
snapshots:取得済みスナップショットの確認
restic snapshots
ID Time Host Tags Paths
────────────────────────────────────────────────────────────────
a1b2c3d4 2025-07-01 02:00:05 web-server /var/www/app
e5f6g7h8 2025-07-02 02:00:04 web-server /var/www/app
スナップショット ID・取得日時・ホスト名・対象パスが一覧で確認できる。
restore:リストアの実行
restic restore a1b2c3d4 --target /tmp/restore-test
--target で展開先を指定できるため、本番パスを上書きせずに別ディレクトリで内容を確認できる。最新スナップショットに戻す場合は latest キーワードが使える。
restic restore latest --target /var/www/app --path /var/www/app
forget + prune:世代管理とデータ削除
forget はスナップショットの参照を削除し、--prune を付けることで参照されなくなった実データも同時に削除する。
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 3 \
--prune
この例では「直近7日分・週次4世代・月次3世代」を保持し、それ以外を削除する。--dry-run フラグで実際に何が削除されるかを事前確認できる。
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 3 --dry-run
パスワード管理の注意点
本番運用では RESTIC_PASSWORD を直接環境変数に書くより、パスワードファイルを使う方が扱いやすい。
echo "your-strong-passphrase" > /etc/restic/password
chmod 600 /etc/restic/password
export RESTIC_PASSWORD_FILE="/etc/restic/password"
重要なのは、このパスワードを失うとリポジトリのデータは永久に取り出せなくなる点だ。 パスワード自体をパスワードマネージャーやシークレット管理サービスにバックアップしておくことは必須だ。
「取れている」から「戻せる」へ:リストア検証を cron に組み込む
バックアップが「取れている」ことと「戻せる」ことは、証明するために必要なアクションが違う。前者は backup コマンドの終了コードを見ればいい。
後者は、実際にリストアして内容を確認するか、少なくともリポジトリの整合性を検証しなければ分からない。
restic check の使い分け
restic check はメタデータ(インデックス・スナップショット構造)の整合性を検証する。実データのダウンロードは最小限で、比較的高速に完了する。
restic check --read-data-subset=10% は実データの一部をランダムに抽出してハッシュ検証する。ストレージ上のデータが壊れていないかを確認できる。
毎回全データを検証する --read-data は大規模リポジトリでは現実的でないため、--read-data-subset で割合を指定して週次・月次で回すのが実用的だ。
restic の公式ドキュメントは、prune 実行後に restic check を走らせることを推奨している。prune はリポジトリの内部データ構造を書き換える操作であり、何らかの理由で中断した場合に整合性が崩れる可能性があるためだ。
cron に組み込む最小構成
バックアップスクリプトの基本形はこうなる。check スクリプトと同様、先頭で環境変数を設定する構造だ。
#!/bin/bash
# /usr/local/bin/restic-backup.sh
set -euo pipefail
export AWS_ACCESS_KEY_ID="$(cat /etc/restic/aws-key-id)"
export AWS_SECRET_ACCESS_KEY="$(cat /etc/restic/aws-secret)"
export RESTIC_REPOSITORY="s3:https://<accountid>.r2.cloudflarestorage.com/<bucket-name>"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
restic backup /var/www/app /etc --exclude='*.log'
check スクリプトも同じ構造で、Slack 通知を添える。
#!/bin/bash
# /usr/local/bin/restic-check.sh
set -euo pipefail
export AWS_ACCESS_KEY_ID="$(cat /etc/restic/aws-key-id)"
export AWS_SECRET_ACCESS_KEY="$(cat /etc/restic/aws-secret)"
export RESTIC_REPOSITORY="s3:https://<accountid>.r2.cloudflarestorage.com/<bucket-name>"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
if restic check --read-data-subset=10% > /tmp/restic-check.log 2>&1; then
STATUS="✅ restic check passed"
else
STATUS="❌ restic check FAILED"
fi
curl -s -X POST "${SLACK_WEBHOOK_URL}" \
-H 'Content-type: application/json' \
--data "{\"text\": \"${STATUS}\\n\\\'\\\'\\\'$(cat /tmp/restic-check.log)\\\'\\\'\\\'\"}"
成功・失敗どちらの場合も通知することで、「通知が来ない=cron が止まっている」という別の問題も検知しやすくなる。
これを crontab に登録する。
# /etc/cron.d/restic
# 毎日午前2時にバックアップ
0 2 * * * root /usr/local/bin/restic-backup.sh
# 毎週日曜午前3時に世代管理 + 整合性チェック
0 3 * * 0 root /usr/local/bin/restic-forget.sh && /usr/local/bin/restic-check.sh
tar+cron の1行を置き換えるイメージで、既存の cron 構造を大きく変えずに移行できる。
R2 への prune 時の転送量に注意
R2 バックエンドに対して prune を頻繁に実行すると、転送量が積み上がる可能性がある。
restic の prune は、削除対象のデータと同じパックファイルに残すべきデータが混在している場合、そのパックファイルをダウンロードして再パックする。つまり、prune のたびにリポジトリ内のデータを一定量ダウンロード・再アップロードする処理が走る。
R2 の料金体系については 公式ページ を確認してほしいが、一般的にオブジェクトストレージではオペレーション数に応じた課金が発生する場合がある。prune の頻度は週次程度に留めるのが現実的だ。
--max-unused オプションで未使用データの許容量を設定し、不要な再パックを抑制する方法もある。
「バックアップが取れているか」だけを確認していた運用から、「リストアできるか」を定期的に検証する運用へ。restic + R2 の組み合わせは、暗号化・世代管理・整合性検証が揃った状態を、自前サーバなしで手に入れられる現実的な選択肢だ。
株式会社ホコサキは、山口県宇部を拠点に Web 制作・業務システム開発・AI 活用支援・DX 推進に取り組んでいます。インフラ運用や開発環境の改善についても気軽にご相談ください。詳しくは サービス紹介ページ をご覧ください。

