Free

Để Claude deploy lên production

Sai sót khi deploy không có triệu chứng. Bốn hàng rào — credentials tách riêng, script EDITOR, đọc lại xác minh, lật runtime theo env — để Claude hữu ích mà không thiêu mọi thứ.


Khác biệt lớn nhất giữa deploy và viết code: deploy là một lần, rủi ro cao, rollback khổ. Viết sai một dòng code, test chạy một lần là bắt được. Viết sai một dòng credentials, phải đợi đến lần thanh toán thật đầu tiên mới biết — lúc đó user đã quẹt thẻ, tiền không về tài khoản bạn, log toàn 400.

Gần đây tôi đưa how2claude từ dev local lên production cùng Claude: tài khoản Stripe live, ví x402 mainnet, Google OAuth, Kamal secrets. Claude không biết giá trị nào của test, giá trị nào là thật. Không biết sk_live_sk_test_ chỉ khác một chữ nhưng kết cục trời đất. Hàng rào bạn tự dựng.

Hàng rào #1: Tách file credentials cho dev và prod

Rails mặc định dùng config/credentials.yml.enc, key giải mã là config/master.key. Dùng mặc định này là bạn thua.

Nếu bạn nhét sk_live_xxx vào file đó rồi chạy test local, code test của bạn đang dùng Stripe production. Mỗi lần test chạy là một lần thẻ thật bị trừ.

Tách thành hai:
- config/credentials.yml.enc + config/master.key: dev/test, chứa sk_test_xxx
- config/credentials/production.yml.enc + config/credentials/production.key: prod, chứa sk_live_xxx

Cả hai .key đưa vào .gitignore, cả hai .enc commit. production.key chỉ nằm trên máy deploy.

Sau đó Kamal secrets phải trỏ đúng file:

 # .kamal/secrets
-RAILS_MASTER_KEY=$(cat config/master.key)
+RAILS_MASTER_KEY=$(cat config/credentials/production.key)

Trong container, RAILS_MASTER_KEY giờ trỏ vào key production, và thứ được giải mã là credentials production. Tôi quan sát Claude viết dòng này — template mặc định của Kamal là config/master.key (của dev), nó âm thầm deploy Stripe key dev lên production.

Hàng rào #2: Dùng script EDITOR, không copy-paste

bin/rails credentials:edit --environment production mở editor tương tác. Claude không điều khiển được editor tương tác. Bạn cũng không muốn dán tay hàng chục key (một typo là toang hết).

Dùng pattern này:

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 trông thế này:

# ARGV[0] là path tới file YAML tạm đã giải mã
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 ghi YAML đã giải mã ra file tạm, gọi "EDITOR" của bạn với path đó là đối số, rồi mã hoá lại khi thoát. "EDITOR" của ta thực ra là một script Ruby, sửa chính xác một key rồi lưu.

Ưu điểm:
- Chính xác: chỉ động vào stripe.webhook_secret, không động gì khác.
- Idempotent: chạy hai lần kết quả giống chạy một.
- Có thể kiểm toán: bản thân script là cái diff. Claude viết, bạn liếc qua, biết chính xác nó sẽ đổi cái gì.
- Xoá là mất: sau rm, không còn credential dưới dạng text trên disk, shell history cũng không có vết paste whsec_....

Mỗi credential một script: set_stripe_live_key.rb, set_webhook_secret.rb, set_price_ids.rb, set_wallet_address.rb. Xong mỗi cái là rm liền.

Hàng rào #3: Xác minh bằng đọc lại qua Rails runner

Viết xong, đừng tin rằng nó viết đúng. Đọc lại:

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
"

Điều quan trọng là kiểm tra prefixđộ dài, không phải chỉ in ra nhìn.

  • Stripe live secret luôn bắt đầu bằng sk_live_. Nếu đọc lại thấy sk_test_, Claude đã nhét test key vào file prod — bug bạn sẽ không thấy cho đến lần thanh toán thật đầu tiên.
  • Webhook secret luôn bắt đầu bằng whsec_. Đúng format hay không thấy liền.
  • Địa chỉ ví EVM luôn 42 ký tự, 0x.... Sai độ dài nghĩa là có ký tự lạ lọt vào.

Thêm kiểm tra prefix thôi đã chặn phần lớn typo, sót field, trộn môi trường.

Hàng rào #4: Chuyển ở runtime theo env, không chuyển tay lúc deploy

Rất dễ nghĩ "trước khi deploy tôi đổi network sang mainnet" — kiểu chuyển này dựa vào trí nhớ người, sớm muộn cũng cháy.

Viết quy tắc vào 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

Cùng code, dev chạy testnet (sepolia), prod chạy mainnet (base). Deploy không cần sửa gì. Claude cũng không thể "quên chuyển" vì chuyển không phải việc của nó.

Chiêu tương tự cho basescan_tx_url, độ hiển thị Plan (plan dev-only không hiện ở prod), chọn Stripe price ID, v.v. Mọi thứ khác nhau giữa dev và prod đều lật bằng Rails.env. Đừng dựa vào nhớ lúc deploy.

Bước cuối: tự đi lại flow thật

Bốn hàng rào xong xuôi, lần đầu click thanh toán tiền thật vẫn phải là bạn.

Bài trước Để Claude gỡ những bug im lặng tôi kể: click x402 đầu tiên ở production, console báo liền invalid_string at payTo — ký tự thứ 43 của địa chỉ ví là dấu chấm hỏi fullwidth lọt vào từ IME tiếng Trung. Kiểm tra prefix không bắt được (0x vẫn còn), test không bắt được (test không phát giao dịch thật), chỉ click thật một lần mới lộ.

Hàng rào deploy không phải cho Claude. Là cho bạn — tự động hoá cái có thể tự động hoá (prefix, độ dài, lật env) để sự chú ý tiết kiệm được dồn cho những tương tác thật mà tự động hoá không phủ.


Flow đầy đủ của Claude-deploy:

  1. Tách credentials dev và prod thành các file .enc + .key riêng.
  2. Với mỗi giá trị cần set, viết script Ruby dùng một lần, đẩy qua EDITOR=script, xoá ngay.
  3. Sau mỗi lần ghi, đọc lại bằng Rails runner, kiểm prefix và độ dài.
  4. Mọi khác biệt dev/prod lật bằng Rails.env, không chuyển tay lúc deploy.
  5. Lần đầu bấm nút tiền thật, bạn tự bấm.

Deploy không phải "viết code với rủi ro cao hơn". Deploy là "viết code mà không ai báo bạn sai tức thì". Hàng rào biến "không ai báo" thành "bạn biết trong 15 giây".