Free

Отладка прод-Rails с Claude — без SSH и передеплоя

Диагностика + починка данных прод-Rails: kamal exec, полноширинный `?`, Base64, непрерывность треда.


Продакшн-400/500 совсем не то же, что локальные. Локально ты перезапускаешь тесты, правишь, снова запускаешь. В проде пользователь уже нажал «Оплатить» и смотрит на белый экран.

Три пути:

  1. SSH на сервер — работает, но в shell внутри контейнера может не быть rails console, изменения без аудита, одна опечатка — и ты в беде
  2. Передеплой — пушнуть фикс, минимум 10+ минут, возможна короткая недоступность, а куча проблем — вообще не проблемы кода (а данных или credentials)
  3. Запустить Rails runner внутри работающего контейнера — без рестарта, без передеплоя, без SSH, скальпельная точность

Эта статья про третий путь. Два реальных случая: полноширинный , залезший в credentials и положивший x402-платежи 500-ми, и твит в середине треда со status: :failed, который нужно повторить, сохранив целостность треда. По пути Claude будет пытаться свернуть в 4 неверные стороны по умолчанию — каждую надо перехватить.


Инструмент: kamal app exec --reuse

Kamal — инструмент деплоя Rails от 37signals. У него есть команда:

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

--reuse означает: не поднимать новый контейнер, а выполнить внутри уже работающего web-контейнера. Никакого нового билда, никакого docker pull, никакого рестарта, никакой повторной инжекции ENV. Команда выполняется, контейнер возвращается к обработке запросов.

Вывод течёт через stdout к тебе в терминал — фактически puts в продакшн-Rails-консоли, без 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 секунд. На два порядка быстрее передеплоя.

Случай 1: Полноширинный , вызвавший прод-500

Commit eba9ac9.

Симптом: Через несколько часов после запуска x402-платежей каждый платёжный запрос валился в 500. Логи полны Net::HTTPBadResponse от HTTP-клиента. Локально — идеально.

Диагностика: Сначала попроси 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 изменился. Но диагностика деплоя не стоила.

Правило: Когда прод ломается, сначала читай через kamal app exec --reuse. Не гадай, не передеплой сначала.

Случай 2: Повтор упавшего XQueue::Tweet в середине треда

Симптом: После публикации статьи твиты уходят в таблицу x_queue_tweets. Один из них — 2-й из 4-твитового треда — застревает со status: :failed (рейт-лимит X API, валидация контента, что угодно). Повтор требует сохранить целостность треда с 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 — кодируй локально, передавай base64-строку, декодируй внутри runner:

# Кодируем локально
echo -n 'Переписанное содержимое твита...' | base64
# => 0J/QtdGA0LXQv9C40YHQsNC90L3QvtC1INGB0L7QtNC10YDQttC40LzQvtC1INGC0LLQuNGC0LAuLi4=

# Передаём
kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  t.update!(content: Base64.decode64('0J/QtdGA0LXQv9C40YHQsNC90L3QvtC1INGB0L7QtNC10YDQttC40LzQvtC1INGC0LLQuNGC0LAuLi4='), status: :scheduled)
  puts t.status
\""

Base64-строки — чистый ASCII, для shell безопасны.

Ловушка 2: целостность треда. XQueue::PostTweetJob.perform_later(87) публикует твит сам по себе — не цепляется к твиту #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-целью:

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

polling_interval у воркера — 0,1 секунды — он подхватывает job почти сразу. Через пару секунд kamal app exec смотрит, что status перешёл из scheduled в posted, а x_tweet_id заполнился — тред непрерывен.

Правило: Операции с данными в проде должны уважать ограничения бизнес-уровня, а не только «запись обновлена успешно». Целостность треда — бизнес-ограничение; Rails runner её за тебя не проверяет.

4 направления, в которые Claude уходит по умолчанию (и как перенаправить)

При такой работе первый рефлекс Claude часто ошибочен. Ловим каждый:

1. Хочет SSH на сервер

«Я зайду по SSH и посмотрю...»

Перенаправь: kamal app exec --reuse лучше SSH — внутри контейнера, окружение Rails загружено, аудируется (kamal-логи сохраняют запись), не трогает shell хоста, нет контейнерного дрейфа (reuse гарантирует текущую прод-версию).

2. Хочет написать migration для починки данных

«Напишу migration, чтобы выпилить полноширинный из wallet_address...»

Перенаправь: Изменение одного значения credentials не требует migration (БД не трогали). Одноразовый Rails runner делает это за 10 секунд; migration требует деплоя и остаётся в схеме навсегда. Migration — только если фикс может потребоваться снова; опечатка — разовая штука.

3. Пихает спецсимволы в shell-строки

«Просто вызову update!(content: "...")...»

Перенаправь: Любой пользовательский контент (твиты, комментарии, введённый markdown) должен идти через Base64. Парсинг shell кавычек, обратных слешей, $ и backtick — классическое минное поле — прод не место для тренировок.

4. perform_later без бизнес-параметров

«Просто перезапущу PostTweetJob.perform_later(87)...»

Перенаправь: Сначала спроси: «связана ли эта запись с другими?» У тредов — reply_to, у батч-джобов — batch_id, у пагинированных — cursor. Список аргументов джоба — носитель этих бизнес-связей. Выкинешь аргумент — разорвёшь цепь.

Чек-лист

Отладка + модификация данных прод-Rails с Claude — 6 правил:

  1. Читай, прежде чем писать. kamal app exec --reuse 'bin/rails runner "puts X"'. Локализуй проблему, прежде чем что-либо менять.
  2. kamal app exec --reuse — инструмент по умолчанию, не SSH, не передеплой. Внутри контейнера, Rails загружен, туда-обратно 3-6 секунд.
  3. Изменения credentials через EDITOR=ruby-script bin/rails credentials:edit --environment production. Ruby-скрипт делает правку, не пялимся на шифртекст локально.
  4. Shell-экранирование — минное поле; маршрутизируй спецконтент через Base64. echo -n 'X' | base64 локально, Base64.decode64 в runner.
  5. Rails runner не принуждает бизнес-ограничения за тебя. reply_to треда, batch_id, cursor пагинации — ставь в очередь всё вместе.
  6. Migration — только если «это может повториться». Разовые фиксы данных через runner, на два порядка быстрее.

Отладка в проде — не пересадка dev-флоу в прод — у тебя нет пространства для итерации и нет допуска ошибок. На самом деле ты используешь поверхность интроспекции, которую прод уже предоставляет (Rails runner + kamal exec + credentials:edit), каждый шаг — минимально возможное изменение. Claude умеет писать корректный Ruby — но знание «что можно делать напрямую, а что надо сперва диагностировать» — решение, которое он за тебя не примет. Это твоя продовая дисциплина.