ClaudeでLitestreamを導入 — SQLiteのバックアップを復元まで確かめる
SQLiteをLitestreamでS3互換ストレージへ複製する設定をClaude Codeに組ませ、別パスへの復元と件数照合までを検証する手順です。復元コマンドの暴走を止めるhookも載せます。
SQLiteのバックアップは、複製を始めるだけでは終わりません。別の場所へ戻して、件数が一致して初めて「バックアップがある」と言えます。Litestreamの設定ファイルを書くところと、復元して照合するところは、どちらもClaude Codeに任せやすい作業です。
任せるときの注意点は1つです。復元は、書き込み先を間違えると本番のデータベースを上書きしかねません。この記事では、litestream.yml の書き方、S3互換ストレージへの複製、別パスへの復元、件数の照合を順に組みます。最後に、危ない形の復元コマンドだけをClaude Codeのhookで止める設定を足します。
Litestreamは何をするツールか
Litestreamは、SQLiteデータベースの変更を継続的に読み取り、S3などのレプリカへ送り続けるプロセスです。litestream replicate を起動すると常駐して待機し、終了しません。バックグラウンドサービスとして動かす前提のコマンドです。
復元は litestream restore で行います。公式の解説では、復元先のファイルがすでに存在すると復元は実行されません。動作中のデータベースを誤って上書きしないための仕様です。ただし後述の -force を付けると、空でない既存ファイルも上書きされます。
この記事の対象はv0.5系です。v0.3.x時代に作ったAge暗号化のレプリカは、v0.5.0以降では復元に失敗します。古い環境から移すときは、先に移行ガイドの手順を確認してください。
設定ファイルをClaude Codeに書かせる
まず依頼の仕方です。パス、バケット名、エンドポイントを渡さずに「Litestreamを設定して」と頼むと、Claudeは一般的な値を推測して埋めます。次のように、決まっている値を先に渡します。
/var/lib/app/app.db を Litestream で複製したい。
複製先は S3互換ストレージ(バケット app-backup、パス app.db、
エンドポイント https://minio.example.com:9000)。
認証情報は設定ファイルに書かず環境変数で渡す。
litestream.yml を作って。出来上がる設定は、公式のMinIO向けの例に沿うと次の形になります。
dbs:
- path: /var/lib/app/app.db
replica:
type: s3
bucket: app-backup
path: app.db
endpoint: https://minio.example.com:9000
region: us-east-1見るべき点は3つあります。
pathはデータベースファイルの絶対パスですendpointはAWS以外のS3互換サービスでだけ必要です。設定するとforce-path-styleは自動で有効になります- MinIOはリージョンの値を無視しますが、Litestream側は値を要求します。
us-east-1のような任意の値で通ります
認証情報の扱いにも落とし穴があります。設定ファイルの値は環境変数より優先されます。ファイルに access-key-id を書いたままにすると、あとで環境変数を差し替えても効きません。ファイルに鍵を書かせない依頼にしておくと、この食い違いを避けられます。
環境変数は LITESTREAM_ACCESS_KEY_ID と LITESTREAM_SECRET_ACCESS_KEY を使います。AWS_ACCESS_KEY_ID なども受け付けます。
複製を始めて、別パスに復元する
設定を置いたら複製を起動します。常駐するので、実運用ではサービス管理に載せます。動作確認だけなら別のターミナルで起動して構いません。
export LITESTREAM_ACCESS_KEY_ID=...
export LITESTREAM_SECRET_ACCESS_KEY=...
litestream replicate -config ./litestream.ymlデータベースに書き込みが入り、同期間隔が1秒の既定なら数秒待てば、バケットにデータが現れます。復元は、必ず本番とは別のパスに出力します。
mkdir -p /tmp/restore-check
litestream restore -config ./litestream.yml \
-o /tmp/restore-check/app.db \
-integrity-check quick \
/var/lib/app/app.db-o は出力先で、省略すると元のデータベースのパスに戻されます。省略した形の復元が、いちばん事故になりやすい書き方です。-integrity-check は復元後にSQLiteの整合性検査を走らせるフラグで、none・quick・full から選びます。既定は none なので、検証目的なら明示します。
実際に書き込む前に計画だけ見たいときは -dry-run が使えます。復元計画を表示するだけで、ファイルは書きません。
件数を照合する
復元できたことの確認は、「ファイルができた」では足りません。元のデータベースと復元したデータベースで、同じ件数が取れるかを比べます。公式のチュートリアルも、復元先に SELECT を流して中身を確かめる形です。
#!/bin/bash
# verify-restore.sh: 本番DBと復元DBの件数を比べる
set -eu
SRC=/var/lib/app/app.db
DST=/tmp/restore-check/app.db
for t in users orders; do
a=$(sqlite3 "$SRC" "SELECT count(*) FROM $t")
b=$(sqlite3 "$DST" "SELECT count(*) FROM $t")
echo "$t: source=$a restored=$b"
[ "$a" = "$b" ] || { echo "MISMATCH: $t" >&2; exit 1; }
done
echo "OK"Claude Codeには、このスクリプトを作らせるだけで終わらせず、実行させて出力を読ませます。不一致が出たときは、どのテーブルがずれたかが出力にそのまま残ります。
不一致の原因として最初に疑うのは、複製が追いついていない場合です。同期間隔の分だけ、直前の書き込みは復元に含まれないことがあります。書き込みが続いている本番に対して照合すると、数件のずれは起こり得ます。止められるなら書き込みを止めて数秒待ってから、止められないなら件数の差が小さいことを確認してから、判断します。
復元できないときの読み方
no matching backup files available というエラーは、2つの別の状況で同じ文言になります。まだ複製されていない場合と、存在はするが粗い粒度でしか戻せない場合です。公式は、結論を出す前に litestream ltx -level all の一覧を見るよう案内しています。
-txid や -timestamp で時点を指定する復元は、保持されているLTXファイルの境界にしか着地しません。境界を越えるファイルは、部分適用されず丸ごと飛ばされます。指定した時点が復元できなくても、データが消えたとは限りません。
また、復元先ごとに出力パスを分けます。空でない既存ファイルがあると、粒度のエラーより先に「出力パスが存在する」という別のエラーで止まります。エラーの文言を読み違えやすい組み合わせなので、Claudeに復元を繰り返させるときは、ループごとに新しいパスを使う指示を入れます。
保持期間にも触れておきます。snapshot の interval と retention の既定は、どちらも24時間です。細かい時点への復元を長く残したいときは、retention と snapshot の設定を見直す対象になります。
起動時の自動復元と、削除権限を絞る設定
運用に入ると、復元は手作業だけではなくなります。コンテナを作り直した直後に、データベースが無ければ自動で戻したい場面です。設定ファイルの restore-if-db-not-exists: true を使うと、Litestreamは起動時にローカルのファイルが無ければレプリカから復元します。既定は false で、v0.5.6で加わりました。replicate コマンドの -restore-if-db-not-exists フラグでも同じことができます。
シェルスクリプトから復元を呼ぶ場合は、restore の -if-replica-exists が使えます。バックアップがまだ1つも無いとき、エラーにせず終了コード0で終わるため、初回起動の自動化で止まりません。反対に、このフラグを付けたまま運用すると、バックアップが消えていても気づけません。照合用のスクリプトには付けず、起動用のスクリプトにだけ付ける分け方が安全です。
複製先の鍵の権限も見直す価値があります。設定の retention ブロックで enabled: false にすると、Litestreamはリモートの古いファイルを消さなくなります。古いファイルの掃除はS3のライフサイクルルールなど、クラウド側の仕組みに任せる形です。ルールの書き方はS3ライフサイクルの解説にあります。公式は、この設定で認証情報に DeleteObject 権限を与えずに済み、鍵が漏れても攻撃者にバックアップを消されにくくなると説明しています。
ただし、リモートの古いファイルが増え続けるため、保持期間はクラウド側で決める必要があります。Claudeに設定を書かせるときは、削除をどちらが担当するのかを依頼文に入れておきます。
危ない復元コマンドをhookで止める
最後に、Claude Codeの側の安全策です。復元のうち危ないのは、次の2つの形です。
-force付き: 空でない既存ファイルでも上書きする-oなし: 出力先が元のデータベースのパスになる
Claude CodeのPreToolUse hookは、ツール呼び出しの前にスクリプトを走らせます。終了コード2を返すと呼び出しがブロックされ、標準エラー出力がブロックの理由になります。if に Bash(litestream restore *) を書くと、該当するコマンドのときだけスクリプトが起動します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(litestream restore *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-restore.sh"
}
]
}
]
}
}スクリプトは標準入力でJSONを受け取り、tool_input.command に実行しようとしているコマンドが入っています。
#!/bin/bash
cmd=$(jq -r '.tool_input.command')
case "$cmd" in
*" -force"*)
echo "litestream restore の -force は使えません。別パスに出力してください" >&2
exit 2 ;;
esac
case "$cmd" in
*" -o "*) exit 0 ;;
*) echo "-o で出力先を指定してください" >&2; exit 2 ;;
esac保存したら chmod +x .claude/hooks/guard-restore.sh で実行権限を付けます。
ここで注意したいのは、終了コード2以外のエラーでは操作が止まらない点です。スクリプトがない、実行できない、2と0以外の終了コードを返す場合は、非ブロッキングエラー扱いで操作は続きます。パスを打ち間違えたhookは、何も検査しないままコマンドを通します。この既定を変える設定はonFailureの解説にまとめています。hookの全体像はhooksの入門ガイドも参照してください。
このhookは litestream restore の文字列に反応する単純な検査です。sh -c でコマンドを包まれるような経路は、hookだけでは見張れません。Litestreamの公式仕様が持つ「既存ファイルがあれば復元しない」という挙動と、hookの二重にしておく位置づけです。
検証の流れを決めておく
ここまでを、繰り返せる手順にまとめます。
バックアップ検証の順序
- 1
複製を起動する
litestream replicate -config ./litestream.ymlを常駐させ、ログにエラーが出ていないか見ます。 - 2
数秒待って別パスに復元する
-o /tmp/restore-check/app.dbと-integrity-check quickを付けます。 - 3
件数を照合する
verify-restore.shを実行し、OKが出るか確認します。 - 4
復元ファイルを消す
次回に同じパスを使えるよう、
/tmp/restore-checkを片づけます。
この手順を定期的に流せば、バックアップが壊れていたことに、障害の当日まで気づかない事態を避けられます。復元の検証は、複製の設定と同じ重さで扱う作業です。
よくある質問
複製先のバケットが空に見えるのはなぜですか
バケット名の指定を間違えている場合が多くあります。コマンドラインモードのURLは s3://BUCKET_NAME.ENDPOINT/PATH の形です。別マシンのMinIOを使うなら、設定ファイルに endpoint を書く必要があります。
litestream start で複製が始まりますか
始まりません。start と stop は、すでに動いているデーモンに対して、特定のデータベースの複製の再開と一時停止を伝えるコマンドです。デーモンを起動するのは replicate です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。