วินิจฉัย + แก้ข้อมูล Rails production: kamal exec, `?` full-width, Base64, thread ต่อเนื่อง
400/500 ใน production ไม่เหมือน local เลย ใน local รัน test ใหม่ แก้ รันใหม่ ใน production ผู้ใช้กด "ชำระเงิน" ไปแล้วจ้องหน้าจอว่าง ๆ
สามทาง:
rails console, การเปลี่ยนแปลงไม่มี audit, พิมพ์ผิดครั้งเดียวก็แย่บทความนี้เกี่ยวกับทางที่สาม สองกรณีจริง: ? full-width ที่ติดใน credentials แล้วทำให้การจ่าย x402 500 หมด และทวีตกลาง thread ที่ status: :failed ต้องส่งใหม่โดยรักษา thread ต่อเนื่อง ระหว่างทาง Claude จะพยายามออกนอกเส้นทาง 4 ทิศที่ผิดโดย default ต้องสกัดทุกอัน
kamal app exec --reuseKamal เป็นเครื่องมือ deploy Rails จาก 37signals มีคำสั่ง:
kamal app exec --reuse 'bin/rails runner "..."'
--reuse หมายถึง: ไม่ต้องยก container ใหม่ ให้รันใน web container ที่กำลังทำงานอยู่ ไม่มี build ใหม่ ไม่มี docker pull ไม่มี restart ไม่มีการ inject ENV ใหม่ คำสั่งรัน container กลับไปจัดการ request ต่อ
ผลลัพธ์ไหลผ่าน stdout กลับมาที่ terminal — เหมือน puts ใน Rails console production โดยไม่ต้อง SSH, tmux หรือออกจาก laptop
Session ทั่วไป:
$ 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 วินาที เร็วกว่า redeploy สองอันดับขนาด
? full-width ทำให้ 500 ใน productionCommit eba9ac9
อาการ: สองสามชั่วโมงหลังปล่อยการชำระ x402, ทุก request การจ่ายได้ 500 log เต็มไปด้วย Net::HTTPBadResponse จาก HTTP client local ทำงานสมบูรณ์
วินิจฉัย: ให้ Claude print config x402 ใน production ก่อน:
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
Output:
"0xAbC123...def?"
มี ? เกินที่หาง — เครื่องหมายคำถามรูปแบบเต็มความกว้าง (U+FF1F) ไม่ใช่ ? ครึ่งความกว้าง IME ของใครสักคนสลับระหว่างแก้ไข config/credentials/production.yml.enc และตัวอักษรนี้ก็แทรกเข้ามา
config/credentials.yml.enc local (ถอดรหัสด้วย master.key ของ dev/test) ไม่มีตัวนี้ — ใน Rails 8 production กับ dev เป็น encrypted credentials แยกกัน เนื้อหาไม่ share
แก้ไข: SSH เข้าไปแก้ไฟล์ตรง ๆ ไม่ได้ (เข้ารหัสแล้ว) ดึงมาแก้ local ก็ไม่ได้ (master.key ไม่อยู่ใน laptop) วิธีถูกคือให้ Claude เขียน Ruby script ใช้ครั้งเดียว inject ผ่าน EDITOR= เข้า credentials:edit:
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# ลบเครื่องหมายคำถาม full-width ที่หาง
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
Flow ของ credentials:edit: ถอดรหัส → เขียนไฟล์ temp → เรียก $EDITOR → เข้ารหัสใหม่ → ลบ temp เปลี่ยน EDITOR เป็น Ruby script ของเรา ทำให้การแก้ไขอัตโนมัติ ไม่ต้องจ้องดู ciphertext ใน local
หลังจากนั้น git commit + kamal deploy หนึ่งครั้ง — deploy นี้จำเป็นเพราะ production.yml.enc เปลี่ยน แต่การวินิจฉัยไม่เปลือง deploy
กฎ: เมื่อ production พัง อ่านก่อน ด้วย kamal app exec --reuse อย่าเดา อย่า redeploy ก่อน
อาการ: หลังบทความ publish, ทวีตเข้าตาราง x_queue_tweets หนึ่งในนั้น — ตัวที่ 2 ของ thread 4 อัน — จบด้วย status: :failed (rate limit X API, validation เนื้อหา หรืออะไรก็ตาม) ส่งใหม่ต้องรักษา 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: escape ของ shell. เนื้อหาทวีตมักมีเครื่องหมายคำพูด, newline, backtick ถ้าเขียนแบบนี้:
# ระเบิด — shell กินเครื่องหมายคำพูดและ backslash
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
ระบำ Base64 — encode ที่ local, ส่ง string base64, decode ข้างใน runner:
# Encode local
echo -n 'เนื้อหาทวีตที่เขียนใหม่...' | base64
# => 4LiE4LiZIOC4iuC4rOC4l+C4teC5iOC4iOC4sOC5gOC4guC4teC4ouC4meC5g+C4q+C4oy4uLg==
# ส่ง
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('4LiE4LiZIOC4iuC4rOC4l+C4teC5iOC4iOC4sOC5gOC4guC4teC4ouC4meC5g+C4q+C4oy4uLg=='), status: :scheduled)
puts t.status
\""
string Base64 เป็น ASCII ล้วน shell ปลอดภัย
กับดัก 2: thread ต่อเนื่อง. XQueue::PostTweetJob.perform_later(87) publish ทวีตเดี่ยว ๆ — ไม่ต่อกับทวีต #1 — เพราะ X API ต้องการ reply_to_tweet_id แต่ Job default ไม่พาค่านี้มา
หา x_tweet_id ของทวีตก่อนหน้า (ตัวที่ส่งสำเร็จจะเติม field นี้):
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
Enqueue พร้อมเป้า reply:
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
polling_interval ของ worker คือ 0.1 วินาที — หยิบ job แทบจะทันที ไม่กี่วินาทีต่อมา kamal app exec ดูว่า status เปลี่ยนจาก scheduled เป็น posted และ x_tweet_id ถูกเติม — thread ต่อเนื่อง
กฎ: การทำงานกับข้อมูล production ต้องเคารพ ข้อจำกัดระดับธุรกิจ ไม่ใช่แค่ "record อัปเดตสำเร็จ" thread ต่อเนื่องคือข้อจำกัดทางธุรกิจ Rails runner ไม่ตรวจสอบให้คุณ
ในงานแบบนี้ ปฏิกิริยาแรกของ Claude มักผิด จับทีละตัว:
"ผมขอ SSH ไปดูหน่อย..."
เปลี่ยนเส้นทาง: kamal app exec --reuse ชนะ SSH — ใน container, env Rails โหลดแล้ว, มี audit (log kamal เก็บ record), ไม่แตะ shell host, ไม่กังวล container drift (reuse รับประกันเวอร์ชัน production ปัจจุบัน)
"ผมเขียน migration ลบ
?full-width ออกจาก wallet_address..."
เปลี่ยนเส้นทาง: เปลี่ยนค่า credentials หนึ่งค่าไม่ต้องใช้ migration (DB ไม่ถูกแตะ) Rails runner ครั้งเดียวเสร็จใน 10 วินาที migration ต้อง deploy และอยู่ใน schema ตลอดไป ใช้ migration เฉพาะเมื่อ fix อาจต้องรันซ้ำ — typo เป็นเรื่องครั้งเดียว
"แค่เรียก
update!(content: "...")ก็พอ..."
เปลี่ยนเส้นทาง: เนื้อหาที่ผู้ใช้สร้าง (ทวีต, comment, markdown ที่ user พิมพ์) ควรผ่าน Base64 การ parse shell ของเครื่องหมายคำพูด, backslash, $ และ backtick เป็นทุ่งระเบิดคลาสสิก — production ไม่ใช่ที่ฝึกซ้อม
perform_later ไม่ส่ง parameter ธุรกิจ"ก็
PostTweetJob.perform_later(87)ใหม่..."
เปลี่ยนเส้นทาง: ถามก่อน "record นี้เกี่ยวข้องกับตัวอื่นไหม?" thread มีความสัมพันธ์ reply_to, batch job มี batch_id, paginated job มี cursor — รายการ argument ของ Job คือผู้พาความสัมพันธ์เหล่านั้น ลืม argument หนึ่งก็ขาดหนึ่งสาย
Debug + แก้ data Rails production ด้วย Claude — 6 กฎ:
kamal app exec --reuse 'bin/rails runner "puts X"' ระบุปัญหาก่อนเปลี่ยนอะไรkamal app exec --reuse คือเครื่องมือ default ไม่ใช่ SSH ไม่ใช่ redeploy ใน container, Rails โหลด, ไปกลับ 3-6 วินาทีEDITOR=ruby-script bin/rails credentials:edit --environment production Ruby script แก้ไข ไม่ต้องจ้อง ciphertext ที่ localecho -n 'X' | base64 ที่ local, Base64.decode64 ใน runnerDebug บน production ไม่ใช่ย้าย dev flow ไป production — คุณไม่มีพื้นที่วนซ้ำ ไม่มีความอดทนต่อ error สิ่งที่ใช้จริงคือ surface การสำรวจตัวเองที่ production ให้อยู่แล้ว (Rails runner + kamal exec + credentials:edit) แต่ละขั้นเปลี่ยนแปลงน้อยที่สุดที่ทำได้ Claude เขียน Ruby ถูกได้ — แต่การรู้ว่า "อันไหนทำตรงได้ vs อันไหนต้องวินิจฉัยก่อน" คือการตัดสินใจที่เขาไม่ทำแทนคุณ นั่นคือวินัย production ของคุณ