Free

SSH なし・再デプロイなしで本番 Rails のデータを直す(Claude と)

本番 Rails の診断 + データ修正:kamal exec、全角疑問符、Base64、thread 連続性。


本番の 400/500 はローカルと全然違う。ローカルならエラーが出ればテストを再実行し、直して、また実行する。本番だと、ユーザーがすでに「支払う」を押して空白画面を見ている。

選択肢は 3 つ:

  1. サーバーに SSH——直せる、ただしコンテナ内の shell に rails console が無いかもしれない、変更に監査ログなし、タイプミス一発で終わる
  2. 再デプロイ——コードを修正して push、最短でも 10 分、瞬間的に使えなくなる可能性あり、しかも多くの問題はコード問題ではない(データや credentials のミス)
  3. 動いているコンテナ内で Rails runner を実行——再起動なし、再デプロイなし、SSH なし、メス級の精度で変更

本稿は 3 つ目のやり方について。実戦 2 ケース:全角の が credentials に混入して x402 決済が 500 したケース、そして thread の中ほどにあるツイートの status: :failed を thread 連続性を保ったまま再送するケース。こういう作業をさせると Claude はデフォルトで 4 つの方向に外す。全部 intercept する必要がある。


ツール:kamal app exec --reuse

Kamal は 37signals 製の Rails デプロイツール。こんなコマンドがある:

kamal app exec --reuse 'bin/rails runner "..."'

--reuse の意味:新しいコンテナを立てず、動いている web コンテナの中で実行する。新しい build なし、docker pull なし、再起動なし、ENV 再注入なし。コマンド実行後、コンテナはそのままリクエスト処理を続ける。

出力はそのまま stdout 経由で手元の端末へ——本番 Rails console で puts を打ったのと同じ、SSH も tmux も要らず、ラップトップから離れずに済む。

典型的なセッション:

$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
  INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847

往復 3-6 秒。再デプロイより 2 桁速い。

ケース 1:全角 が引き起こした本番 500

commit eba9ac9

症状:x402 決済をリリースして数時間後、毎回の決済リクエストが 500。ログには HTTP クライアントの Net::HTTPBadResponse 一色。ローカルでは完璧に動作。

診断:まず Claude に本番の x402 設定を出力させる:

kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'

出力:

"0xAbC123...def?"

末尾に が一つ余計に付いている——全角疑問符(U+FF1F)、半角 ? ではない。config/credentials/production.yml.enc を編集した時に IME が切り替わって、この文字が紛れ込んだ。

ローカルの config/credentials.yml.enc(dev/test の master.key で復号するやつ)には無い——Rails 8 では production と dev が別々の encrypted credentials、内容は共有されない。

修正:SSH して中身を直接編集はできない(暗号化されている)、ローカルにプルしてくるのもダメ(master.key がローカルに無い)。正解は Claude に使い捨ての ruby スクリプトを書かせ、EDITOR= 経由で credentials:edit に流し込むこと:

# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# 末尾の全角疑問符を削除
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
  bin/rails credentials:edit --environment production

credentials:edit のフロー:復号 → 一時ファイルに書く → $EDITOR 起動 → 再暗号化 → 一時ファイル削除。EDITOR を自作 ruby スクリプトに差し替えれば編集が自動化できる、ローカルで暗号文を目視する必要もない。

終わったら git commit + kamal deploy 一回——production.yml.enc が変わっているのでこのデプロイは必要。でも診断には deploy コストを払っていない。

法則:本番が壊れたらまず kamal app exec --reuse読む。推測しない、先にデプロイしない。

ケース 2:XQueue::Tweet の失敗再送、thread 連続性込み

症状:記事公開後、ツイートは x_queue_tweets テーブルにスケジュールされる。4 ツイート thread の 2 番目が status: :failed に(X API の rate limit、内容バリデーション、何らかの理由)。再送する際、thread 1 番目との連続性を保つ必要がある。

失敗したレコードを探す

kamal app exec --reuse 'bin/rails runner "
  XQueue::Tweet.where(status: :failed).each do |t|
    puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
  end
"'

id=87、thread_id=15、thread_position=2 と判明。

落とし穴 1:shell エスケープ。ツイート内容には引用符、改行、backtick が頻出する。直接こう書くと:

# 爆発する、shell が引用符とバックスラッシュを食う
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'

Base64 dance——手元で内容をエンコード、base64 文字列を渡し、runner 内でデコード:

# ローカルで生成
echo -n '書き直し後のツイート内容...' | base64
# => 5pu444GN55u044GX5b6M44Gu44OE44Kk44O844OI5YaF5a6544Gn44GZ...

# 渡す
kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  t.update!(content: Base64.decode64('5pu444GN55u044GX5b6M44Gu44OE44Kk44O844OI5YaF5a6544Gn44GZ...'), status: :scheduled)
  puts t.status
\""

base64 文字列は ASCII のみ、shell で安全。

落とし穴 2:thread 連続性XQueue::PostTweetJob.perform_later(87) は独立ツイートとして投稿する、thread 1 番目に繋がらない——X API が reply_to_tweet_id を必要とするが、Job はデフォルトで何も持たないから。

前のツイートの x_tweet_id を取得(送信成功したやつにこのフィールドが入る):

kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
  puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789

reply 先を添えて enqueue:

kamal app exec --reuse 'bin/rails runner "
  XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
  puts \"enqueued\"
"'

worker の polling_interval は 0.1 秒——job をほぼ即座にピックアップ。数秒後に kamal app exec で status が scheduled から posted に変わり x_tweet_id が埋まっていれば thread が繋がった証拠。

法則:本番データ操作はビジネス層の制約を尊重する必要がある、「レコード更新成功」だけではダメ。thread 連続性はビジネス制約、Rails runner は勝手にチェックしてくれない。

Claude がデフォルトで外す 4 方向

こういう作業をさせると Claude の第一反応はよく間違える。都度捕まえる:

1. SSH したがる

「サーバーに SSH して見てきます...」

修正kamal app exec --reuse が SSH より優れている——コンテナ内、Rails env ロード済み、監査あり(kamal ログに残る)、ホスト shell に触れない、コンテナドリフト心配無用(reuse が現在の本番バージョンを保証)。

2. migration を書いてデータを直そうとする

「wallet_address の全角 ? を置換する migration を書きます...」

修正:credentials 一件の修正に migration は要らない(DB 触っていない)。使い捨ての Rails runner が 10 秒で片付く、migration は deploy が必要でスキーマに永遠に残る。修正が将来また必要になる可能性があるときだけ migration を使う——今回はタイポ、「将来」は無い。

3. 特殊文字を shell 文字列にそのまま入れようとする

update!(content: "...") で行けます...」

修正:ユーザー生成コンテンツ(ツイート、コメント、ユーザー入力 markdown)は全部 Base64 中継。shell の引用符、バックスラッシュ、$、backtick の解釈は古典的地雷——本番で練習するのはコストが高すぎる。

4. perform_later にビジネスパラメータを渡さない

PostTweetJob.perform_later(87) で再送すれば...」

修正:まず「このレコードは他と関係があるか」と問う。thread には reply_to 関係、バッチジョブには batch_id 関係、ページ付きジョブには cursor 関係——Job の引数リストがビジネス関係の運搬役、引数を落とせばチェーンが切れる。

チェックリスト

Claude に本番 Rails の排査 + データ修正をさせる 6 条:

  1. 先に読む、後で書くkamal app exec --reuse 'bin/rails runner "puts X"'、何かを動かす前に問題を特定する。
  2. kamal app exec --reuse がデフォルト手段、SSH でも deploy でもない。コンテナ内実行、Rails env ロード済み、往復 3-6 秒。
  3. credentials 問題は EDITOR=ruby-script bin/rails credentials:edit --environment production。Ruby スクリプトが自動編集、ローカルで暗号文を目視しない。
  4. shell エスケープは地雷、特殊内容は Base64 中継echo -n 'X' | base64 をローカルで、Rails runner 内で Base64.decode64
  5. Rails runner はビジネス制約を勝手にチェックしない。thread の reply_to、バッチの batch_id、ページの cursor——まとめて enqueue。
  6. 「将来また発生する可能性」があるときだけ migration を書く。使い捨てのデータ修正は runner、2 桁速い。

本番排査の本質は開発フローを本番に移植することではない——反復の余地も無く、エラー許容も無い。本当に使うのは本番自身が提供する内省面(Rails runner + kamal exec + credentials:edit)、各ステップで可能な最小の変更を行う。Claude は Ruby コードを正しく書ける——でも「本番で何を直接やっていいか、何を先に診断しないといけないか」の判断はしてくれない。それはあなたの本番規律だ。