Free

SSH ve yeniden dağıtım olmadan Claude ile prod Rails verisini onarmak

Prod Rails tanısı + veri düzeltme: kamal exec, tam genişlikte `?`, Base64, thread sürekliliği.


Üretim 400/500'leri yereldekine hiç benzemez. Yerelde hata alırsan testleri yeniden koşar, düzenler, yeniden koşarsın. Üretimde, kullanıcı çoktan "Öde"ye tıkladı ve boş bir ekrana bakıyor.

Üç yol:

  1. Sunucuya SSH — işe yarar, ama container içindeki shell'de rails console olmayabilir, değişiklik denetlenmez, bir yanlış tuşla yandın
  2. Yeniden dağıtım — fix'i push et, en az 10+ dakika, olası kısa erişilemezlik, üstelik birçok problem kod değil (veri veya credentials hatası)
  3. Çalışan container içinde Rails runner koşturmak — yeniden başlatma yok, yeniden dağıtım yok, SSH yok, neşter hassasiyetinde

Bu yazı üçüncü yol üzerine. İki gerçek vaka: credentials'a sıkışan tam genişlikte bir 'in x402 ödemelerini 500 ile patlattığı durum, ve thread'in ortasındaki bir tweet'in status: :failed olmasıyla thread bütünlüğünü koruyarak yeniden gönderme. Yol boyunca Claude varsayılan olarak 4 farklı yanlış yöne sapmaya çalışacak — her birini yakalaman gerek.


Araç: kamal app exec --reuse

Kamal 37signals'ın Rails dağıtım aracı. Bir komutu var:

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

--reuse anlamı: yeni container açma, çalışan web container'ının içinde yürüt. Yeni build yok, docker pull yok, yeniden başlatma yok, ENV yeniden enjeksiyonu yok. Komut çalışır, container istekleri karşılamaya geri döner.

Çıktı stdout üzerinden terminaline akar — üretimdeki bir Rails console'da puts yazmış gibi, SSH, tmux veya laptoptan ayrılmak yok.

Tipik oturum:

$ 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

Gidiş-dönüş 3-6 saniye. Yeniden dağıtımdan iki kat mertebe daha hızlı.

Vaka 1: Üretimde 500'e neden olan tam genişlikte

Commit eba9ac9.

Belirti: x402 ödemelerini yayınladıktan birkaç saat sonra, her ödeme isteği 500 veriyor. Loglar HTTP client'tan Net::HTTPBadResponse ile dolu. Yerel mükemmel çalışıyor.

Tanı: Claude'a önce üretimdeki x402 konfigürasyonunu yazdırmasını söyle:

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

Çıktı:

"0xAbC123...def?"

Kuyrukta fazladan bir var — tam genişlikte soru işareti (U+FF1F), yarım genişlikteki ? değil. Birinin IME'si config/credentials/production.yml.enc düzenlenirken değişmiş ve bu karakter sızmış.

Yerel config/credentials.yml.enc (dev/test master.key ile açılan) bu karakteri içermiyor — Rails 8'de production ve dev ayrı encrypted credentials, içerik paylaşılmıyor.

Onarım: SSH yapıp dosyayı direkt düzenleyemezsin (şifreli) ve yerele çekip düzenleyemezsin (master.key laptopta yok). Doğrusu, Claude'a tek kullanımlık bir Ruby script yazdırıp EDITOR= üzerinden credentials:edit'e enjekte etmek:

# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Kuyruğun ucundaki tam genişlikte soru işaretini kaldır
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 akışı: şifre çöz → temp dosyaya yaz → $EDITOR çağır → yeniden şifrele → temp'i sil. EDITOR'ı kendi Ruby scriptimizle değiştirince düzenleme otomatikleşir, şifreli metne yerelde gözle bakmaya gerek kalmaz.

Sonrasında git commit + kamal deploy bir kez — bu deploy zorunlu çünkü production.yml.enc değişti. Ama tanı bir deploy'a mal olmadı.

Kural: Üretim kırıldığında, önce kamal app exec --reuse ile oku. Tahmin etme, önce yeniden dağıtma.

Vaka 2: Thread ortasında başarısız XQueue::Tweet'in yeniden gönderimi

Belirti: Makale yayınlandıktan sonra tweet'ler x_queue_tweets tablosuna gider. 4'lü thread'in 2.'si status: :failed ile biter (X API rate limit'i, içerik doğrulama, ne olursa). Yeniden göndermek 1. tweet ile thread bütünlüğünü korumayı gerektirir.

Başarısızı bul:

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 çıkıyor.

Tuzak 1: shell escape. Tweet içeriğinde çoğunlukla tırnak, satır sonu, backtick olur. Şöyle yazarsan:

# patlar — shell tırnakları ve ters eğik çizgileri yer
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'

Base64 dansı — yerelde kodla, base64 dizesini geçir, runner içinde çöz:

# Yerelde kodla
echo -n 'Yeniden yazılmış tweet içeriği...' | base64
# => WWVuaWRlbiB5YXpsbcsIHR3ZWV0IMSxw6Fyacuv...

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

Base64 dizeleri sadece ASCII, shell güvenli.

Tuzak 2: thread bütünlüğü. XQueue::PostTweetJob.perform_later(87) bağımsız bir tweet yayınlar — tweet #1'e zincirlenmez — çünkü X API reply_to_tweet_id gerektirir ve Job varsayılanda böyle bir değer taşımaz.

Önceki tweet'in x_tweet_id'sini bul (başarılı gönderimler bu alanı doldurur):

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 hedefiyle kuyruğa al:

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

Worker'ın polling_interval'i 0.1 saniye — job'ı neredeyse anında alır. Birkaç saniye sonra kamal app exec ile status'ün scheduled'dan posted'a döndüğünü ve x_tweet_id'nin dolduğunu görürsün — thread kesintisiz.

Kural: Üretim veri operasyonları iş katmanı kısıtlarına saygı duymalı, sadece "kayıt güncellendi" yetmez. Thread bütünlüğü bir iş kısıtıdır; Rails runner bunu senin için kontrol etmez.

Claude'un varsayılanda saptığı 4 yön (ve nasıl yönlendirilir)

Bu tür iş yaparken Claude'un ilk refleksi çoğu zaman yanlış. Her birini yakala:

1. Sunucuya SSH yapmak ister

"Sunucuya SSH yapıp bir bakayım..."

Yönlendir: kamal app exec --reuse SSH'yi yener — container içinde, Rails env yüklü, denetlenir (kamal logları kayıt tutar), host shell'ine dokunmaz, container drift endişesi yok (reuse mevcut üretim sürümünü garanti eder).

2. Veriyi düzeltmek için migration yazmak ister

"wallet_address'teki tam genişlikte 'i silmek için bir migration yazayım..."

Yönlendir: Bir credentials değerini değiştirmek migration gerektirmez (DB'ye dokunulmadı). Tek kullanımlık Rails runner bunu 10 saniyede halleder; migration deploy gerektirir ve şemada sonsuza kadar kalır. Sadece düzeltmenin tekrar gerekebileceği durumlarda migration kullan; bir yazım hatası tek seferlik.

3. Özel karakterleri shell dizelerine sıkıştırmak

"update!(content: "...") ile olur..."

Yönlendir: Kullanıcı tarafından üretilen her içerik (tweet, yorum, kullanıcı markdown'ı) Base64 üzerinden yönlendirilmeli. Shell'in tırnak, ters eğik çizgi, $ ve backtick parsing'i klasik mayın tarlası — üretim pratik yeri değil.

4. İş parametreleri olmadan perform_later

"PostTweetJob.perform_later(87) ile yeniden gönderirim..."

Yönlendir: Önce "bu kayıt diğerleriyle ilişkili mi?" diye sor. Thread'lerin reply_to ilişkisi var, toplu işlerin batch_id ilişkisi, sayfalanmış işlerin cursor ilişkisi — Job'ın argüman listesi o ilişkilerin taşıyıcısıdır. Bir argümanı atla, bir zinciri kır.

Kontrol listesi

Üretim Rails'i Claude ile debug + veri değiştirme — 6 kural:

  1. Yazmadan önce oku. kamal app exec --reuse 'bin/rails runner "puts X"'. Bir şey değiştirmeden önce problemi lokalize et.
  2. kamal app exec --reuse varsayılan araçtır, SSH ya da yeniden dağıtım değil. Container içinde, Rails yüklü, 3-6 saniye gidiş-dönüş.
  3. Credentials değişiklikleri EDITOR=ruby-script bin/rails credentials:edit --environment production ile. Ruby script düzenlemeyi yapar, yerelde şifreli metne bakmaya gerek yok.
  4. Shell escape bir mayın tarlası; özel içeriği Base64 üzerinden yönlendir. Yerelde echo -n 'X' | base64, runner'da Base64.decode64.
  5. Rails runner senin için iş kısıtlarını zorlamaz. thread'in reply_to'su, batch_id, sayfalama cursor'ı — hepsiyle birlikte kuyruğa al.
  6. Sadece "bu yeniden olabilir" ise migration yaz. Tek seferlik veri düzeltmeleri runner'dan gider, iki kat daha hızlı.

Üretimde debug, dev akışını üretime taşımak değil — iterasyon alanın yok, hata tolerasın yok. Gerçekten kullandığın şey, üretimin zaten sunduğu iç gözlem yüzeyi (Rails runner + kamal exec + credentials:edit), her adım mümkün olan en küçük değişikliği yapar. Claude doğru Ruby yazabilir — ama "hangisi direkt yapılabilir, hangisi önce tanı gerektirir" kararı senin için veremez. Bu senin üretim disiplindir.