本番 Rails の診断 + データ修正:kamal exec、全角疑問符、Base64、thread 連続性。
本番の 400/500 はローカルと全然違う。ローカルならエラーが出ればテストを再実行し、直して、また実行する。本番だと、ユーザーがすでに「支払う」を押して空白画面を見ている。
選択肢は 3 つ:
rails console が無いかもしれない、変更に監査ログなし、タイプミス一発で終わる本稿は 3 つ目のやり方について。実戦 2 ケース:全角の ? が credentials に混入して x402 決済が 500 したケース、そして thread の中ほどにあるツイートの status: :failed を thread 連続性を保ったまま再送するケース。こういう作業をさせると Claude はデフォルトで 4 つの方向に外す。全部 intercept する必要がある。
kamal app exec --reuseKamal は 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 桁速い。
? が引き起こした本番 500commit 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 で読む。推測しない、先にデプロイしない。
症状:記事公開後、ツイートは 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 の第一反応はよく間違える。都度捕まえる:
「サーバーに SSH して見てきます...」
修正:kamal app exec --reuse が SSH より優れている——コンテナ内、Rails env ロード済み、監査あり(kamal ログに残る)、ホスト shell に触れない、コンテナドリフト心配無用(reuse が現在の本番バージョンを保証)。
「wallet_address の全角 ? を置換する migration を書きます...」
修正:credentials 一件の修正に migration は要らない(DB 触っていない)。使い捨ての Rails runner が 10 秒で片付く、migration は deploy が必要でスキーマに永遠に残る。修正が将来また必要になる可能性があるときだけ migration を使う——今回はタイポ、「将来」は無い。
「
update!(content: "...")で行けます...」
修正:ユーザー生成コンテンツ(ツイート、コメント、ユーザー入力 markdown)は全部 Base64 中継。shell の引用符、バックスラッシュ、$、backtick の解釈は古典的地雷——本番で練習するのはコストが高すぎる。
perform_later にビジネスパラメータを渡さない「
PostTweetJob.perform_later(87)で再送すれば...」
修正:まず「このレコードは他と関係があるか」と問う。thread には reply_to 関係、バッチジョブには batch_id 関係、ページ付きジョブには cursor 関係——Job の引数リストがビジネス関係の運搬役、引数を落とせばチェーンが切れる。
Claude に本番 Rails の排査 + データ修正をさせる 6 条:
kamal app exec --reuse 'bin/rails runner "puts X"'、何かを動かす前に問題を特定する。kamal app exec --reuse がデフォルト手段、SSH でも deploy でもない。コンテナ内実行、Rails env ロード済み、往復 3-6 秒。EDITOR=ruby-script bin/rails credentials:edit --environment production。Ruby スクリプトが自動編集、ローカルで暗号文を目視しない。echo -n 'X' | base64 をローカルで、Rails runner 内で Base64.decode64。本番排査の本質は開発フローを本番に移植することではない——反復の余地も無く、エラー許容も無い。本当に使うのは本番自身が提供する内省面(Rails runner + kamal exec + credentials:edit)、各ステップで可能な最小の変更を行う。Claude は Ruby コードを正しく書ける——でも「本番で何を直接やっていいか、何を先に診断しないといけないか」の判断はしてくれない。それはあなたの本番規律だ。