배포 실수는 아무도 안 알려준다. 분리된 credentials, EDITOR 스크립트, 읽기-검증, env 기반 런타임 전환 네 가드레일로 Claude를 쓰되 태우지 않기.
배포와 코드 작성의 가장 큰 차이: 배포는 일회성, 고위험, 롤백이 괴롭다. 코드 한 줄 잘못 써도 테스트 한 번이면 잡힌다. Credentials 한 줄 잘못 쓰면 첫 실결제가 실패할 때야 알게 된다——그때는 사용자가 이미 카드 승인했고, 돈은 네 계좌로 안 들어오고, 로그는 400으로 도배된다.
최근 Claude와 how2claude를 로컬 개발에서 프로덕션으로 올렸다: Stripe 라이브 계정, x402 메인넷 지갑, Google OAuth, Kamal secrets. Claude는 어느 값이 테스트 환경인지 실제인지 모른다. sk_live_와 sk_test_가 한 글자 차이로 세계가 끝난다는 것도 모른다. 가드레일은 직접 세운다.
Rails 기본값은 config/credentials.yml.enc, 복호화 키 config/master.key. 이 기본값을 쓰는 순간 진다.
sk_live_xxx를 이 파일에 넣고 로컬에서 테스트를 돌리면, 테스트 코드가 프로덕션 Stripe를 때린다. 테스트 한 번에 카드가 한 번 긁힌다.
두 개로 나눈다:
- config/credentials.yml.enc + config/master.key: dev/test용, sk_test_xxx 넣기
- config/credentials/production.yml.enc + config/credentials/production.key: 프로덕션용, sk_live_xxx 넣기
.gitignore에 두 .key 모두 무시, 두 .enc 모두 커밋. production.key는 배포 머신에만 둔다.
그다음 Kamal secrets가 맞는 파일을 가리키게:
# .kamal/secrets
-RAILS_MASTER_KEY=$(cat config/master.key)
+RAILS_MASTER_KEY=$(cat config/credentials/production.key)
컨테이너 안의 RAILS_MASTER_KEY가 프로덕션 키를 가리키게 되고, 복호화되는 건 프로덕션 credentials. Claude가 이 줄을 쓸 때 특히 지켜봤다——기본 생성된 Kamal 템플릿은 config/master.key(dev용)이고, 조용히 dev Stripe key를 프로덕션에 배포한다.
bin/rails credentials:edit --environment production은 인터랙티브 에디터를 연다. Claude는 인터랙티브 에디터를 못 다룬다. 너도 수십 개 키를 손으로 복붙하기 싫다 (오타 하나에 전부 망함).
이 패턴을 써라:
EDITOR="ruby script/set_prod_webhook_secret.rb" \
bin/rails credentials:edit --environment production
rm script/set_prod_webhook_secret.rb
script/set_prod_webhook_secret.rb는 이런 모양:
# ARGV[0]은 복호화된 임시 YAML 파일 경로
file = ARGV[0]
require "yaml"
data = YAML.load_file(file) || {}
data["stripe"] ||= {}
data["stripe"]["webhook_secret"] = "whsec_GHWObNAKFh2HPOlJpbGmlYfIiKz1C8EY"
File.write(file, data.to_yaml)
Rails는 복호화된 YAML을 임시 파일에 쓰고, 그 경로를 인자로 "EDITOR"를 호출한 뒤, 종료되면 다시 암호화한다. 우리 "EDITOR"는 사실 Ruby 스크립트로, 키 하나만 정확히 바꿔 저장한다.
장점:
- 정확: stripe.webhook_secret 하나만 건드리고 나머지는 안 건드림.
- 멱등: 두 번 돌려도 결과 같음.
- 감사 가능: 스크립트 자체가 변경의 diff. Claude가 쓴 스크립트를 한 번 훑으면 뭘 바꿀지 정확히 안다.
- 삭제하면 사라짐: rm 후엔 디스크에 평문 credential 없고, shell history에도 whsec_... 복붙 흔적 없음.
credential마다 스크립트 하나: set_stripe_live_key.rb, set_webhook_secret.rb, set_price_ids.rb, set_wallet_address.rb. 끝나면 바로 rm.
쓰고 나서 맞게 썼다고 믿지 마라. 읽어 와서:
bin/rails runner -e production "
c = Rails.application.credentials
puts 'sk_live set: ' + c.dig(:stripe, :secret_key).to_s.start_with?('sk_live_').to_s
puts 'webhook_secret set: ' + c.dig(:stripe, :webhook_secret).to_s.start_with?('whsec_').to_s
puts 'wallet prefix: ' + c.dig(:x402, :wallet_address).to_s[0..5]
puts 'wallet len: ' + c.dig(:x402, :wallet_address).to_s.length.to_s
"
중요한 건 접두사와 길이 검증이지 그냥 출력해서 눈으로 보는 게 아니다.
sk_live_로 시작. 읽은 게 sk_test_로 시작하면 Claude가 test key를 prod 파일에 넣은 것——첫 실결제까지는 발견 못 함.whsec_로 시작. 형식 맞는지 한눈에 보임.0x.... 길이 안 맞으면 다른 문자 섞여 들어간 것.접두사 검증만 붙여도 오타, 필드 누락, 환경 혼용 같은 오류는 대부분 막힌다.
"배포 전에 network를 mainnet으로 바꾸자"고 생각하기 쉽다——이런 전환은 인간의 기억에 의존하고, 언젠간 터진다.
규칙은 initializer에 박아넣는다:
# config/initializers/x402.rb
X402.configure do |c|
c.wallet_address = Rails.application.credentials.dig(:x402, :wallet_address)
c.chain = Rails.env.production? ? "base" : "base-sepolia"
end
같은 코드로 dev는 테스트넷(sepolia), prod는 메인넷(base). 배포 시 아무것도 안 바꿔도 됨. Claude도 "전환 까먹을" 일 없다, 전환이 Claude 일이 아니니까.
같은 트릭을 basescan_tx_url, Plan 가시성(dev 전용 플랜은 프로덕션에 안 보임), Stripe price ID 선택 등에 적용. dev와 prod에서 다른 모든 설정은 Rails.env로 뒤집는다. 배포 시 기억하는 것에 의존하지 마라.
네 가드레일을 다 세워도 첫 실돈 결제 클릭은 네가 직접 해야 한다.
이전 글 《Claude로 조용한 버그 디버깅하기》에서 썼다: 프로덕션 첫 x402 결제 클릭에서 console에 invalid_string at payTo——지갑 주소 43번째 문자가 중국어 IME에서 딸려 들어온 전각 물음표였다. 접두사 검증이 못 잡고 (0x 유지), 테스트도 못 잡고 (테스트는 실거래 안 보냄), 실제로 클릭해야만 드러났다.
배포 가드레일은 Claude를 위한 게 아니다, 너를 위한 거다——자동화할 수 있는 것(접두사, 길이, env 전환)은 자동화하고, 아낀 주의력을 자동화가 못 덮는 실제 상호작용에 쓴다.
Claude에게 배포를 맡기는 전체 플로우:
.key 파일 분리.EDITOR=script로 넣고 즉시 rm.Rails.env로 뒤집기, 배포 시 수동 전환 금지.배포는 "더 위험한 코딩"이 아니다. 배포는 "틀려도 아무도 바로 안 알려주는 코딩"이다. 가드레일의 역할은 "아무도 안 알려줌"을 "15초 안에 알게 됨"으로 바꾸는 것.