Claude Media
Claude Codeでgh-ostとpg_repackを準備する — 本番実行を権限で止める

Claude Codeでgh-ostとpg_repackを準備する — 本番実行を権限で止める

巨大テーブルの変更をgh-ostとpg_repackで進める前に、ドライランと手順書をClaude Codeに書かせ、本番へ届く実行だけを権限とフックで止める設定を示します。

gh-ostは--executeを付けるまで何も書き換えません。pg_repackには--dry-runがあります。どちらも「確かめるだけ」の実行を持つので、巨大テーブルの変更では、Claude Codeに任せる範囲をそこまでに限れます。

この記事では、ALTERの計画・ドライランのコマンド・カットオーバーとロールバックの手順書をClaude Codeに書かせ、本番を書き換える実行だけを権限とフックで止める構成を扱います。先に断っておくと、pg_repackはスキーマを変えるツールではありません。そこから説明します。

gh-ostとpg_repackは受け持つ範囲が違う

gh-ostはMySQLのテーブルにALTERをかけるためのツールです。ゴーストテーブルと呼ぶ作業用のコピーを作って行を移し、最後にテーブルを入れ替えます。--alterにはADD COLUMNのような変更内容をそのまま渡します。

pg_repackはPostgreSQLの拡張で、テーブルと索引の膨張(bloat)を取り除くためのものです。クラスタ索引の物理順の復元、列を指定した並べ替え、テーブルや索引の別テーブルスペースへの移動もできます。ドキュメントに列の追加や型変更をする機能は載っていません。

目的使うもの備考
MySQLの巨大テーブルに列や索引を足す使うものgh-ost備考--alterに変更内容を渡す
Postgresのテーブルの膨張を取る使うものpg_repack備考VACUUM FULL相当を無停止で行う
Postgresのテーブルを別テーブルスペースへ移す使うものpg_repack備考--tablespaceを使う
Postgresの列追加・型変更使うもの対象外備考ロックレベルで判断する。別記事を参照

Postgresの列追加や制約追加は、DDLごとのロックレベルを見て書き換える領域です。その手順はPostgresのマイグレーションをロックレベルでレビューする記事に、Djangoの場合はsqlmigrateでロックを事前に洗う記事にあります。本記事のpg_repackは、テーブルの再編成を準備する場面だけを扱います。

本番に届く操作を権限で止める

Claude Codeの許可ルールは、Bashコマンドの文面に対して効きます。ここで効くのは、許可ルール(allow)と拒否ルール(deny)で失敗する向きが逆になる点です。

  • 許可ルールは、書いた形に一致しない呼び方だと確認に回ります。失敗しても止まる側に倒れます
  • 拒否ルールは、一致しない呼び方だと素通りします。/usr/local/bin/gh-ost ...やsh -c '...'は、Bash(gh-ost *)のdenyに掛かりません

Bashの拒否ルールは、同じプログラムの別の呼び方を止めないとドキュメントにも明記されています。そこで、安全側に倒れる許可ルールを主に置き、拒否ルールは「よくある書き方を止める保険」と考えます。

{
  "permissions": {
    "allow": [
      "Bash(pg_repack --dry-run *)",
      "Bash(pg_repack -N *)"
    ],
    "ask": [
      "Bash(gh-ost *)"
    ],
    "deny": [
      "Bash(gh-ost *-execute*)",
      "Read(./ops/secrets/**)",
      "Edit(./ops/secrets/**)"
    ]
  }
}

読み方は次のとおりです。

  • pg_repackは、--dry-runか-Nで始まる呼び方だけを通します。pg_repack -d shop -t ordersのように実行形で書くと確認に回ります
  • gh-ostは常に確認に回し、-executeを含む呼び方はdenyで落とします。単一ダッシュの-executeを含めたのは、gh-ostの手引きに単一ダッシュで書かれたフラグ(-throttle-flag-file)があるためです
  • 接続情報を書いた--conf用のファイルは、ops/secrets/に置いてReadとEditをdenyします

ルール内の*は、サブコマンドの後ろに置く形が基本です。Bash(pg_repack --dry-run *)のように末尾に置くと、引数なしのpg_repack --dry-runにも一致します。

複合コマンドの扱いも関係します。pg_repack --dry-run -t orders && pg_repack -t ordersのように&&で連結すると、全部のサブコマンドが許可されない限り実行されません。ドライランに実行を繋げて通す手は使えません。

落とし穴

denyに書いた「--execute」は、呼び方を変えると素通りする

Bash(gh-ost *-execute*)は、Claude Codeが書くふつうの呼び方を止めるだけです。コマンドをシェル経由で包んだり、フルパスで呼んだりすると一致しません。ここで頼るのは、次の節のフックと、Claudeの手元に本番の書き込み資格情報を置かないことです。

フックでコマンド全文を見て止める

拒否ルールが取りこぼす呼び方は、PreToolUseフックで拾えます。フックは、Bashのコマンド文字列をtool_input.commandで受け取り、終了コード2で呼び出しを止めます。標準エラーに書いた理由はClaudeに返るので、Claudeは別の手を考え直します。

#!/bin/bash
# PreToolUse(Bash)。gh-ostの実行形とpg_repackの実行形を止める
CMD=$(jq -r '.tool_input.command // ""')
 
if echo "$CMD" | grep -Eq '(^|[^[:alnum:]_-])gh-ost([[:space:]]|$)' \
   && echo "$CMD" | grep -Eq -- '-execute'; then
  echo "gh-ostの--executeは人が実行します。noop(--executeなし)で止めてください" >&2
  exit 2
fi
 
if echo "$CMD" | grep -Eq '(^|[^[:alnum:]_-])pg_repack([[:space:]]|$)' \
   && ! echo "$CMD" | grep -Eq -- '(--dry-run| -N)([[:space:]]|$)'; then
  echo "pg_repackは--dry-runを付けた形だけ実行できます" >&2
  exit 2
fi
exit 0

.claude/settings.jsonには次のように登録します。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/guard-schema-change.sh" }
        ]
      }
    ]
  }
}

フックのdenyは、権限モードを問わずに効きます。ただし、フックが見るのは文字列です。コマンドを組み立てる別スクリプトの中で--executeが付くなら、フックには見えません。

最後の線は資格情報です。Claudeが使う--confのファイルには、読み取り専用のレプリカと、ゴーストテーブルを作れる最小権限のアカウントだけを書きます。gh-ostのドキュメントによると、必要な権限はALTER, CREATE, DELETE, DROP, INDEX, INSERT, LOCK TABLES, SELECT, TRIGGER, UPDATEと、REPLICATION SLAVEを含む組です。つまり実行できるアカウントは強い権限を持ちます。本番の書き込み用アカウントを、Claudeが開く場所に置かないことが、いちばん確実な止め方です。

gh-ostの準備をClaude Codeに書かせる

準備は3つの成果物に分けて頼みます。計画書、ドライランのコマンド、手順書です。

前提の洗い出し

まず、対象テーブルの定義(SHOW CREATE TABLEの出力を保存したファイル)をリポジトリに置き、gh-ostの制限と突き合わせさせます。ドキュメントに載る制限のうち、実際に当たりやすいものは次のとおりです。

  • 外部キー制約とトリガーは未対応です
  • 元のテーブルとゴーストテーブルに、共通のPRIMARY KEYかUNIQUE KEYが必要です。キーの列にNULLが含まれる場合、既定では実行されません
  • キーにenum列を含むと、移行の性能が落ちます
  • Amazon RDSは動きますが固有の制限があり、Google Cloud SQLは--gcp、Azure Database for MySQLは--azureが必要です

これをCLAUDE.mdに書いておけば、毎回同じ観点で見てもらえます。

## オンラインスキーマ変更(ops/schema-change/)
- gh-ostの前提は ops/schema-change/limits.md の表で必ず照合する
- コマンドは--executeを付けずに書く。実行形は手順書の「人が実行」欄にだけ書く
- --ok-to-drop-tableは付けない。旧テーブルは取り消し先として残す
- pg_repackは--dry-runを先頭に置く
- 接続情報のファイル(ops/secrets/)は読まない

ドライランのコマンド

gh-ostのドキュメントでは、--executeを付けない実行は何も書き換えず、テーブルの作成と移行の妥当性を試すだけの動作です。Claudeが書くコマンドは次の形になります。

gh-ost --conf=ops/secrets/gh-ost.cnf \
  --host=replica.example.internal \
  --database=shop --table=orders \
  --alter="ADD COLUMN shipped_at datetime NULL" \
  --chunk-size=1000 --max-lag-millis=1500 \
  --postpone-cut-over-flag-file=/tmp/ghost.postpone.flag \
  --panic-flag-file=/tmp/ghost.panic.flag

数値は例です。--chunk-sizeは既定が1000で、10から100000の範囲を取ります。--max-lag-millisはレプリケーション遅延がこの値を超えると移行を絞る設定です。

--test-on-replicaは、Claudeに実行させない側に置きます。この指定はレプリカで移行を最後まで進めたうえでレプリケーションを止め、テーブルを入れ替えてすぐ戻します。止めるためにSTOP SLAVEを発行するのでSUPER権限が要り、--executeも付けて実行します。上のhookとdenyに掛かる形です。結果を比べるチェックサムの取り方は、手順書に書かせるだけにします。

カットオーバーの手順書

手順書は、人が上から順に実行できる形で書かせます。入れる項目は次のとおりです。

  1. 事前確認: 前提表の照合結果、レプリケーション遅延、ディスクの空き
  2. 実行コマンド: --executeと--postpone-cut-over-flag-fileを付けた形。ここだけが「人が実行」欄
  3. カットオーバーの判断: 進捗とレプリカの遅延を見て、フラグファイルを消す条件
  4. 中止の操作: --panic-flag-fileで作るファイル

--postpone-cut-over-flag-fileを指定すると、そのファイルが存在する間は最後の入れ替えが起きません。ファイルを消すか、対話コマンドのcut-overで入れ替えに進みます。行のコピーが終わった後、入れ替えの前に人の判断を挟めます。

入れ替えの仕組みは、ドキュメントによれば、一方の接続がロックを保持している間に別の接続が原子的なRENAMEを試みる2段のブロッキング方式です。失敗した場合は元のテーブルが残り、再試行されます。ロックの保持時間の上限は--cut-over-lock-timeout-secondsで、既定は3秒です。

ロールバックの手順

取り消し方は、どの時点かで変わります。

  • カットオーバー前: --panic-flag-fileで指定したファイルを作ると、gh-ostは後始末なしに止まります。元のテーブルは触られていないので、ゴーストテーブルと変更履歴のテーブルが残るだけです。再実行では--initially-drop-ghost-tableと--initially-drop-old-tableで、それらを落としてよいと明示します
  • カットオーバー後: --revertが、完了した移行を戻します。--old-tableに旧テーブルの名前(例では_orders_del)を指定し、--alterなどの移行オプションは無視されます。戻すには旧テーブルが残っている必要があります

--ok-to-drop-tableを付けないと、旧テーブルは自動では落とされません。ドキュメントは、テーブルの削除がロックを長く保持する操作になり得るため、既定で無効にしていると説明しています。CLAUDE.mdで「付けない」と決めておけば、取り消し先が残ります。旧テーブルは、結果を見届けた後に人が落とします。

pg_repackは止まる条件を先に洗う

pg_repackの準備では、実行前に「何が止まるか」と「何が足りるか」を洗わせます。

  • ディスク: 全テーブルの再編成には、対象のテーブルと索引の約2倍の空きが要ります。1GBなら追加で2GBです
  • 対象テーブル: PRIMARY KEYか、NOT NULL列上のUNIQUEな全体索引が必要です。なければmust have a primary key or not-null unique keysというエラーになります
  • 権限: 実行できるのはスーパーユーザーか、テーブルと索引の所有者です。所有者で実行するには--no-superuser-checkが要ります

--dry-run(短縮は-N)は、再編成する対象を列挙して終了するオプションです。テーブルを絞る-tと組み合わせると、どのテーブルが対象に入るかを確かめられます。

pg_repack --dry-run -d shop -t orders

実行時に考えておく点は、ロックです。pg_repackは再編成の最初と最後に排他ロックを1回ずつ取り、取れない場合は--wait-timeout(既定60秒)の後に競合するクエリをキャンセルします。その後、さらに2倍の時間が経つとpg_terminate_backend()で接続を切ります。

業務のクエリを巻き込みたくない場合は、--no-kill-backendを付けます。ロックを取れなければ、そのテーブルの再編成を見送ります。手順書に付けるオプションは、この形にしておく選択肢があります。

もう一つは、実行中のDDLです。pg_repackは全体再編成の間、対象テーブルにSHARE UPDATE EXCLUSIVEロックを保持し、その間は対象テーブルへのDDLを受け付けません。デプロイのマイグレーションが同じ時間帯に走る構成では、実行時刻の調整を手順書の項目にします。

失敗後の後始末もあります。途中で止まると一時索引が残ることがあり、その場合のメッセージは、古いジョブの残りであればDROP INDEXしてからやり直すよう案内します。この操作は人が行う欄に回します。

人が実行する操作の渡し方

Claudeの成果物は、手順書と、貼り付けるだけで動くコマンド列です。ドライランはgh-ostをaskにしてあるので、実行のたびに内容を見て承認できます。

--executeを付けた実行は、Claudeのセッションとは別の端末や踏み台から人が行います。Claudeを通さない経路なら、フックや許可ルールの取りこぼしに左右されません。

まとめ

gh-ostとpg_repackは、どちらも「確かめるだけ」の実行を持つので、Claude Codeには計画書・ドライラン・手順書の作成を任せられます。許可ルールは一致しない呼び方が確認に回る向きで、拒否ルールは素通りする向きです。許可ルールを主、フックを補助にして、資格情報を渡さないことで締める構成にすると、本番を書き換える実行はClaudeの手が届かない場所に残ります。pg_repackは列を変える道具ではないので、列追加の計画は別の手順に分けます。

参照した一次ソース

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

  1. code.claude.com/docs/en/permissions
  2. code.claude.com/docs/en/hooks-guide
  3. github.com/github/gh-ost/blob/master/doc/command-line-flags.md
  4. github.com/github/gh-ost/blob/master/doc/requirements-and-limitations…
  5. github.com/github/gh-ost/blob/master/doc/cut-over.md
  6. reorg.github.io/pg_repack
この記事を共有:XはてブLinkedIn