Free

Debug Rails sản xuất với Claude, không SSH, không redeploy

Chẩn đoán + sửa dữ liệu Rails sản xuất: kamal exec, `?` full-width, Base64, liên kết thread.


400/500 ở sản xuất hoàn toàn khác local. Ở local bạn chạy lại test, sửa, chạy lại. Ở sản xuất, người dùng đã nhấn "Thanh toán" rồi đang nhìn màn hình trắng.

Ba đường:

  1. SSH vào server — làm được, nhưng shell trong container có thể không có rails console, thay đổi không có audit, gõ nhầm một lần là xong
  2. Redeploy — đẩy một bản sửa, tối thiểu 10+ phút, có thể gián đoạn ngắn, mà nhiều vấn đề không phải code (là dữ liệu hay credentials sai)
  3. Chạy Rails runner bên trong container đang chạy — không restart, không redeploy, không SSH, độ chính xác mức dao mổ

Bài này kể đường thứ ba. Hai ca thật: một full-width nhét vào credentials khiến thanh toán x402 sinh 500, và một tweet giữa chuỗi thread có status: :failed cần gửi lại mà vẫn giữ liên kết chuỗi. Dọc đường, Claude sẽ cố đi sai 4 hướng mặc định — bạn phải chặn từng hướng.


Công cụ: kamal app exec --reuse

Kamal là công cụ deploy Rails của 37signals. Có lệnh:

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

--reuse nghĩa là: không dựng container mới, thực thi bên trong container web đang chạy. Không build mới, không docker pull, không restart, không nạp lại ENV. Lệnh chạy, container quay lại xử request.

Output chảy qua stdout về terminal — giống như puts trong Rails console sản xuất, không SSH, không tmux, không rời laptop.

Phiên điển hình:

$ 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

Đi về 3-6 giây. Nhanh hơn redeploy hai bậc.

Ca 1: Dấu full-width làm sản xuất 500

Commit eba9ac9.

Triệu chứng: Vài tiếng sau khi ra mắt thanh toán x402, mọi yêu cầu thanh toán đều 500. Log đầy Net::HTTPBadResponse từ HTTP client. Local chạy hoàn hảo.

Chẩn đoán: Bảo Claude in cấu hình x402 ở sản xuất trước:

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

Đầu ra:

"0xAbC123...def?"

Đuôi có thêm dấu chấm hỏi full-width (U+FF1F), không phải ? half-width. IME của ai đó chuyển giữa lúc sửa config/credentials/production.yml.enc, ký tự này lọt vào.

config/credentials.yml.enc local (giải mã bằng master.key dev/test) không có — ở Rails 8 production và dev là hai encrypted credentials tách biệt, nội dung không chia sẻ.

Sửa: Không thể SSH vào sửa trực tiếp (đã mã hóa), cũng không thể kéo về local sửa (master.key không ở laptop). Đáp án là cho Claude viết script Ruby dùng một lần và inject qua EDITOR= vào credentials:edit:

# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Bỏ dấu hỏi full-width ở đuôi
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

Luồng credentials:edit: giải mã → ghi ra file tạm → gọi $EDITOR → mã hóa lại → xóa tạm. Đổi EDITOR sang script Ruby của mình, việc sửa được tự động hóa, không cần nhìn bản mã ở local.

Sau đó, git commit + kamal deploy một lần — deploy này bắt buộc vì production.yml.enc đã đổi. Nhưng khâu chẩn đoán không tốn deploy.

Quy tắc: Khi sản xuất hỏng, đọc trước bằng kamal app exec --reuse. Đừng đoán, đừng redeploy trước.

Ca 2: Gửi lại XQueue::Tweet thất bại giữa thread

Triệu chứng: Sau khi bài đăng, tweet vào bảng x_queue_tweets. Một trong số đó — tweet thứ 2 của thread 4 cái — kết thúc status: :failed (rate limit X API, validate nội dung, gì cũng được). Gửi lại phải giữ liên kết chuỗi với tweet thứ 1.

Tìm tweet thất bại:

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
"'

Ra id=87, thread_id=15, thread_position=2.

Cạm bẫy 1: escape shell. Nội dung tweet thường có ngoặc, xuống dòng, backtick. Nếu viết thế này:

# nổ — shell nuốt ngoặc và dấu chéo ngược
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'

Điệu Base64 — mã hóa ở local, truyền chuỗi base64, giải mã bên trong runner:

# Mã hóa local
echo -n 'Nội dung tweet viết lại...' | base64
# => Tuo8aSBkdW5nIHR3ZWV0IHZpdCBsYWkuLi4=

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

Chuỗi Base64 chỉ ASCII, an toàn cho shell.

Cạm bẫy 2: liên kết chuỗi. XQueue::PostTweetJob.perform_later(87) đăng một tweet độc lập — không nối vào tweet #1 — vì X API cần reply_to_tweet_id, còn Job mặc định không mang theo.

Tìm x_tweet_id của tweet trước đó (tweet đã gửi thành công sẽ điền trường này):

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

Đưa vào hàng đợi kèm target reply:

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

polling_interval của worker là 0.1 giây — nhặt job gần như lập tức. Vài giây sau, kamal app exec xem status nhảy từ scheduled sang postedx_tweet_id được điền — chuỗi đã liền mạch.

Quy tắc: Thao tác dữ liệu sản xuất phải tôn trọng ràng buộc tầng nghiệp vụ, không chỉ "bản ghi đã cập nhật". Liên kết chuỗi là ràng buộc nghiệp vụ; Rails runner không tự kiểm tra cho bạn.

4 hướng Claude mặc định đi sai (và cách chỉnh lại)

Làm loại việc này, phản xạ đầu của Claude thường sai. Bắt từng cái:

1. Muốn SSH vào server

"Để tôi SSH vào xem..."

Chỉnh lại: kamal app exec --reuse hơn SSH — trong container, env Rails đã nạp, có audit (log kamal giữ), không đụng shell host, không lo drift container (reuse đảm bảo bản sản xuất hiện tại).

2. Muốn viết migration để sửa dữ liệu

"Tôi viết migration bỏ full-width khỏi wallet_address..."

Chỉnh lại: Đổi một giá trị credentials không cần migration (DB không bị đụng). Rails runner một lần giải quyết trong 10 giây; migration cần deploy và ở lại schema vĩnh viễn. Chỉ dùng migration khi bản sửa có thể phải chạy lại; typo là chuyện một lần.

3. Nhét ký tự đặc biệt vào chuỗi shell

"Cứ update!(content: "...") là được..."

Chỉnh lại: Mọi nội dung do người dùng tạo (tweet, bình luận, markdown người dùng nhập) nên đi qua Base64. Shell phân tích ngoặc, dấu chéo ngược, $ và backtick là bãi mìn kinh điển — sản xuất không phải chỗ tập.

4. perform_later không có tham số nghiệp vụ

"Cứ chạy lại PostTweetJob.perform_later(87)..."

Chỉnh lại: Trước hỏi "bản ghi này có liên quan đến các bản khác không?" Thread có quan hệ reply_to, job theo lô có batch_id, job phân trang có cursor — danh sách tham số của Job là phương tiện mang những quan hệ ấy. Bỏ một tham số, đứt một chuỗi.

Checklist

Debug + sửa dữ liệu Rails sản xuất với Claude — 6 điều:

  1. Đọc trước khi viết. kamal app exec --reuse 'bin/rails runner "puts X"'. Định vị vấn đề trước khi đụng gì.
  2. kamal app exec --reuse là công cụ mặc định, không phải SSH, không phải redeploy. Trong container, Rails đã nạp, đi về 3-6 giây.
  3. Đổi credentials qua EDITOR=ruby-script bin/rails credentials:edit --environment production. Script Ruby tự sửa, không cần nhìn bản mã ở local.
  4. Escape shell là bãi mìn; định tuyến nội dung đặc biệt qua Base64. echo -n 'X' | base64 ở local, Base64.decode64 trong runner.
  5. Rails runner không tự áp dụng ràng buộc nghiệp vụ cho bạn. reply_to của thread, batch_id, cursor phân trang — enqueue cùng tất cả.
  6. Chỉ viết migration nếu "việc này có thể xảy ra lại". Sửa dữ liệu một lần đi qua runner, nhanh hơn hai bậc.

Debug ở sản xuất không phải ghép luồng dev sang sản xuất — bạn không có chỗ lặp, cũng không có dung sai lỗi. Thứ thực sự dùng là bề mặt tự xem xét mà sản xuất đã cung cấp (Rails runner + kamal exec + credentials:edit), mỗi bước là thay đổi nhỏ nhất có thể. Claude viết Ruby đúng — nhưng biết "việc nào có thể làm thẳng vs phải chẩn đoán trước" là quyết định nó không đưa thay bạn. Đó là kỷ luật sản xuất của bạn.