Claude Media
Claude Codeでpg_restoreのリストアを検証用DBで検証する手順

Claude Codeでpg_restoreのリストアを検証用DBで検証する手順

pg_restoreはsandboxの中から検証用DBへ繋がりません。接続先を固定した1本のスクリプトだけをexcludedCommandsで外に出し、リストア後の行数とmd5を照合する構成を示します。

pg_dumpで取ったバックアップが本当に戻せるかは、戻してみるまで分かりません。この演習をClaude Codeに任せるときの落とし穴は、pg_restore -dがDBへ接続するコマンドであることです。sandboxの中ではDBクライアントが外へ出られず、かといってsandboxを外せば接続先を絞る壁もなくなります。

解決策は、DBに触る処理を接続先固定の1本のスクリプトに閉じ込め、そのスクリプトだけをsandboxの外で動かす構成です。本記事は、その設定、スクリプト、リストア後の行数とチェックサムの照合までを一通りまとめます。

どこまでがsandboxで動き、どこからが動かないか

pg_restoreには2つの動作モードがあります。データベース名を指定すると、そのDBに接続して直接リストアします。指定しなければ、再構築用のSQLをファイルか標準出力に書き出すだけです。

この違いが、sandboxとの相性をそのまま分けます。

くらべる

pg_restoreの2つのモードとsandbox

sandbox内で動く

接続しないモード

pg_restore -lでアーカイブの目次を出す、--fileでSQLスクリプトに書き出す、のどちらもDBへ接続しません。作業ディレクトリへの書き込みだけで済みます。

sandbox内では繋がらない

接続するモード

-dでDBを指定するリストアは、ネットワーク接続を伴います。sandboxの中のコマンドには外へ出る直接の経路がなく、プロキシ環境変数を読まないツールは接続できません。

sandbox内のネットワークは、Claude Codeが手元で動かすプロキシ経由だけです。curlやnpmのようにプロキシ変数を読むツールは許可済みホストへ繋がります。一方、素のsshや多くのデータベースドライバーは、許可済みのホストでも繋がりません。pg_restoreもDBクライアントなので、この側に入ると考えて準備します。手元で試すと、macOSならOperation not permittedか名前解決エラー、Linux・WSL2ならNetwork is unreachableのようなメッセージで止まります。

同じ症状をpsqlとsshで切り分けた記事がClaude Codeのsandboxから繋がらないです。本記事はそこから一歩進めて、リストア演習という具体的な作業に落とします。

接続先を絞る仕組みを、sandboxの代わりに作る

DBに繋ぐコマンドをsandboxの外へ出すと、allowedDomainsによる制限は効きません。外に出したコマンドは、ファイルもネットワークも制限なしで動きます。接続先を絞る役目は、sandboxではなくスクリプトとClaude Codeの権限ルールが担います。

設計の要点は次の4つです。

  1. DBに触るコマンドをすべて1本のスクリプトに入れ、接続先を本文中で固定する
  2. そのスクリプトだけをexcludedCommandsに載せる
  3. 実行のたびにaskルールで承認を挟む
  4. Claudeがスクリプトを書き換えられないようにする

excludedCommandsは、呼び出しのテキストを照合します。restore-drill.shの中でpg_restoreを呼んでも、pg_restoreのエントリには一致しません。この性質のおかげで、外に出す範囲をスクリプト1本に限れます。パターンは *で終え、ワイルドカードがなければ完全一致になる点に注意します。

settings.jsonの設定

プロジェクトの.claude/settings.jsonに、次の内容を置きます。

{
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false,
    "excludedCommands": ["scripts/restore-drill.sh *"]
  },
  "permissions": {
    "ask": ["Bash(scripts/restore-drill.sh *)"],
    "deny": ["Edit(scripts/restore-drill.sh)"]
  }
}

各行の役割は次のとおりです。

設定効果
allowUnsandboxedCommands: false効果sandboxで失敗したコマンドをClaudeが外で再試行する抜け道を閉じる
excludedCommands効果このスクリプトの呼び出しだけを、sandboxの外で動かす
ask ルール効果実行のたびに承認プロンプトを出す
Edit の deny ルール効果スクリプト自体の書き換えを止める

allowUnsandboxedCommandsをfalseにすると、ClaudeはdangerouslyDisableSandboxを使えません。sandboxの中でコマンドが失敗しても、外での再試行は提案されません。つまり、本番へ向けたpg_restore -dをClaudeが直接打っても、繋がらずに終わります。DBへ到達する道が、承認付きのスクリプト1本だけになります。

落とし穴

除外したスクリプトは、書き換えれば外で動く

作業ディレクトリ内のスクリプトを除外すると、Claudeがその中身を編集してから、sandboxの外で実行できてしまいます。接続先をスクリプトに固定する設計では、スクリプトの書き換えが突破口になります。Editのdenyルールを入れたうえで、承認プロンプトではコマンドだけでなくスクリプトの中身も確かめる運用にします。

bypassPermissionsモードでは、除外コマンドはaskルールに当たらない限りプロンプトなしで動きます。ここでaskルールが効くので、演習用の設定ではaskを外しません。承認画面は「Bash command (unsandboxed)」という題で出るため、除外が効いているかの確認にも使えます。

リストア演習スクリプト

使い捨てのPostgreSQLは、人間が別のターミナルで起動しておきます。dockerコマンドはsandboxと相性が悪く、Claudeに任せる範囲が増えるほど承認の線引きが曖昧になるためです。

docker run -d --name pg-drill -p 127.0.0.1:55432:5432 \
  -e POSTGRES_USER=drill -e POSTGRES_PASSWORD=drill \
  postgres:17

パスワードは~/.pgpassに書き、スクリプトは-wで対話入力を拒否します。-wは、パスワードが他の手段で得られないとき接続を失敗させる指定です。次のスクリプトは、あくまで一例です。

#!/usr/bin/env bash
# scripts/restore-drill.sh <dump> <manifest.tsv>
set -euo pipefail
 
HOST=127.0.0.1
PORT=55432
PGUSER_DRILL=drill
DUMP="$1"
MANIFEST="$2"
DB="drill_$(date +%y%m%d%H%M%S)"
 
# 検証用の接続先以外には繋がない
case "$HOST:$PORT" in
  127.0.0.1:55432) ;;
  *) echo "refused: $HOST:$PORT" >&2; exit 2 ;;
esac
 
OPTS=(-h "$HOST" -p "$PORT" -U "$PGUSER_DRILL" -w)
 
# template0 から作ると、空のDBになる
createdb "${OPTS[@]}" -T template0 "$DB"
 
pg_restore "${OPTS[@]}" -d "$DB" \
  --no-owner --no-privileges --single-transaction "$DUMP"
 
# 行数とmd5を、ダンプ時に記録した値と照合する
fail=0
while IFS=$'\t' read -r tbl rows sum; do
  got=$(psql "${OPTS[@]}" -d "$DB" -At -F $'\t' -c \
    "SELECT count(*), md5(string_agg(t::text, '|' ORDER BY t::text))
     FROM $tbl t")
  if [ "$got" = "$rows"$'\t'"$sum" ]; then
    echo "PASS $tbl"
  else
    echo "FAIL $tbl expected=$rows/$sum got=$got"; fail=1
  fi
done < "$MANIFEST"
 
echo "database: $DB"
exit $fail

DBの名前は実行ごとに変えます。createdbが既存の名前にぶつかる事故を避け、前回の演習の残骸と混ざらないようにするためです。

フラグを選んだ理由

pg_restoreのオプションは、演習の目的から選びます。

オプションここで選んだ理由
--single-transaction (-1)ここで選んだ理由全コマンドがBEGIN/COMMITで包まれ、失敗すれば何も適用されない。--exit-on-errorを含意する
--no-ownerここで選んだ理由元のDBの所有者がない環境でも、ALTER OWNERで失敗しない
--no-privilegesここで選んだ理由GRANT/REVOKEを復元しない。演習の目的は中身の復元で、権限ではない
-wここで選んだ理由対話入力を拒否し、パスワードが得られなければ接続失敗にする

--exit-on-errorを付けない場合、pg_restoreはエラーがあっても処理を続け、最後にエラー件数を表示します。ターミナルを目で見ているときは気づけますが、Claudeに結果を読ませるときは、一部のテーブルが欠けたまま「完了」と読まれる恐れがあります。演習では失敗を早く止める設定のほうが安全です。

--jobsによる並列リストアは、大きなDBで時間を短縮できます。ただし--single-transactionとは併用できず、入力は通常のファイルかディレクトリである必要があります(パイプや標準入力は不可)。大きなダンプで速度を測りたいときは、-1を外して--exit-on-errorと--jobsの組み合わせに切り替えます。

行数とチェックサムの照合を設計する

リストアが「エラーなしで終わった」ことと、「中身が正しい」ことは別です。照合の基準は、元のDBから取った値でなければなりません。ここで元のDBにClaudeを直接つなぐと、せっかく絞った接続先が広がります。

そこで、基準値はダンプを取る側が記録します。pg_dumpと同じ時点で、次のようなSQLを流してmanifest.tsvに残します。

SELECT 'public.orders', count(*),
       md5(string_agg(t::text, '|' ORDER BY t::text))
FROM public.orders t;

出力はテーブル名、行数、md5の3列をタブで区切った形にします。演習スクリプトは、このmanifest.tsvだけを受け取ります。

  • 元のDBへ繋ぐのは、ダンプを取る既存のバックアップ処理だけ
  • 演習の側に必要なのは、ダンプファイルとmanifest.tsvだけ
  • Claudeは検証用DBにしか届かず、本番の資格情報を持たない

同じSQLを元の側と検証側の両方で流すため、行の比較条件がずれません。t::textは行全体を文字列にするので、列の追加や型の違いも差分として現れます。ただしstring_aggは結果を1つの値に集めるので、巨大なテーブルではメモリを多く使います。大きなテーブルは行数のみ、または主キーの範囲ごとに分割したmd5にするなど、規模に合わせて変えます。

Claudeに頼む手順

準備ができたら、Claude Codeには次の順で作業させます。

手順

リストア演習の進め方

  1. 1

    アーカイブの目次を見る

    pg_restore -l backup.dumpの結果を読ませます。接続しないモードなのでsandbox内で動き、承認も要りません。

  2. 2

    SQLスクリプトに書き出して中身を点検する

    pg_restore -f backup.sql backup.dumpで書き出し、CREATE FUNCTIONのような関数定義や想定外のSQLがないかをClaudeに確認させます。

  3. 3

    演習スクリプトを承認して実行する

    scripts/restore-drill.sh backup.dump manifest.tsvを実行します。承認画面でスクリプトの中身と引数を目視します。

  4. 4

    PASSとFAILの一覧を読ませる

    FAILの行について、期待値と実測値のどちらがずれたかを、Claudeに分けて報告させます。

手順2を挟む理由は、リストアが復元先のサーバーに任意のコードを実行させる操作だからです。ダンプの出所の管理者を信頼できないなら、実行前にSQLを点検する必要があります。非プレーンテキストのダンプは、--fileで書き出して点検できます。接続しないモードで済むため、sandboxの中で、ネットワークなしで進められます。

FAILが出たとき、Claudeに「スクリプトを直して再実行して」と頼むのは避けます。Editのdenyが効いているので、そもそも書き換えられません。原因の切り分け(元のSQLの違い、型の出力の違い、リストア中のエラー)を報告させ、スクリプトの修正は人間が行います。

残るリスクと、使わないほうがよい場面

この構成が守っているのは、Claudeが勝手に別のDBへ繋ぐ経路です。次のことは守れません。

  • スクリプト自体の中身が誤っていれば、検証用DBへの誤操作は起きます。--cleanや--createは、対象が検証用DBだと確信できるときだけ使います
  • 除外したコマンドは、ファイルシステムの制限もなく動きます。スクリプトに別の処理を足すと、その分だけ外で動く範囲が広がります
  • 検証用DBのコンテナにネットワーク越しに届く人がいれば、そこが次の接続先になります。127.0.0.1にだけ公開しておきます

macOSではsandbox.network.allowLocalBindingをtrueにする手もあります。sandbox内のコマンドがlocalhostの任意のポートへ接続できるようになりますが、同じ機械のlocalhostで動く他のサービスにも届きます。認証のないサービスがあるマシンでは、この設定の影響が広すぎます。Linux・WSL2ではこの設定が効かず、sandbox内のlocalhostはコマンド専用の領域になります。

開発用のDBに直接つなぎたい場面や、docker composeで構成全体を立ち上げる場面は、別の判断になります。除外コマンドの書き方と、効かないときの切り分けはsandbox.excludedCommandsでdockerなど非対応コマンドを対象外にすると効かない原因の切り分けで扱っています。

まとめ

リストア演習は、バックアップの価値を決める作業です。Claude Codeに任せるなら、DBに触る処理を接続先固定のスクリプトに集め、そのスクリプトだけをexcludedCommandsで外に出し、askとEditのdenyで鍵をかけます。照合の基準値は、ダンプ時に別の経路で記録しておく。これで、Claudeの手が届くのは検証用DBに限られます。

sandboxの種類ごとの隔離範囲はClaude Codeのサンドボックスの選び方にまとめています。運用の全体像はClaude Codeとはから辿れます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/sandboxing
  2. postgresql.org/docs/current/app-pgrestore.html
この記事を共有:XはてブLinkedIn