프로덕션 Rails 진단 + 데이터 수정: kamal exec, 전각 물음표, Base64, thread 연속성.
프로덕션 400/500은 로컬이랑 완전히 다르다. 로컬에선 에러 나면 테스트 다시 돌리고, 고치고, 또 돌린다. 프로덕션에선 사용자가 이미 "결제" 누르고 빈 화면 쳐다보고 있다.
세 갈래:
rails console이 없을 수도 있고, 변경이 감사되지 않고, 손 한 번 떨면 끝이 글은 세 번째 경로 얘기. 실전 두 케이스: 전각 ?가 credentials에 박혀서 x402 결제가 500 나는 케이스, 그리고 thread 중간 트윗의 status: :failed를 thread 연속성 지키면서 재전송하는 케이스. 이런 작업 시키면 Claude는 기본값으로 4개 방향에서 어긋난다—하나하나 잡아줘야 한다.
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초. 재배포보다 두 자릿수 빠름.
?가 일으킨 프로덕션 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는 필수. 하지만 진단엔 deploy 비용 안 쓴 셈.
규칙: 프로덕션 깨지면 먼저 kamal app exec --reuse로 읽기. 추측 금지, 먼저 배포 금지.
증상: 글 발행 후 트윗이 x_queue_tweets 테이블에 스케줄됨. 4개짜리 thread의 2번째가 status: :failed로(X API 레이트 리밋, 콘텐츠 검증, 이유야 뭐든). 재전송 시 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
# => 64Ks7IucIOyNrCDtirjsnZggsrgfjgu2...
# 전달
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('64Ks7IucIOyNrCDtirjsnZggsrgfjgu2...'), 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 코드 정확히 쓸 수 있음—하지만 "프로덕션에서 뭘 바로 해도 되고, 뭘 먼저 진단해야 하는지" 이 판단은 대신 안 해줌. 그건 네 프로덕션 규율이다.