İçeriğe geç
wedevit

26 Eylül 2026 · 10 dk okuma · yazılım

İlhan Buğra Aslan

Test ortamında çalışıyor, canlıda çalışmıyor: ortam farkı nereden çıkar?


Aynı kod test ortamında çalışıp canlıda çalışmıyorsa, kod nadiren suçludur. Fark neredeyse her zaman şu beş başlıktan birinden çıkar: yapılandırma değerleri, verinin hacmi ve biçimi, kaç kopya aynı anda çalıştığı, uygulamanın önünde duran ağ katmanı (vekil sunucu, CDN, güvenlik duvarı) ve iki ortamın zaman içinde birbirinden sessizce uzaklaşması. Bu beşini yazılı bir listeye dökmeden hata aramak, kodu satır satır okuyup hiçbir şey bulamamakla sonuçlanır. Çünkü kodda gerçekten bir şey yoktur.

Farkı tahmin etmeyin, tabloya dökün

Twelve-Factor App metodolojisi bu meseleyi 2011'den beri aynı şekilde tarif ediyor ve üç boşluk sayıyor: zaman boşluğu (geliştiricinin yazdığı kod günler ya da haftalar sonra canlıya çıkar), personel boşluğu (kodu yazan ile dağıtımı yapan farklı kişilerdir), araç boşluğu (yerelde SQLite, canlıda MySQL). On beş yılda konteynerler araç boşluğunu büyük ölçüde kapattı, diğer ikisi duruyor.

Pratik ilk adım basit bir tablo. Satırlar: çalışma zamanı sürümü (Node, Python, JVM), veritabanı sürümü ve eklentileri, işletim sistemi ve yerel ayar (locale), ortam değişkenlerinin listesi, örnek sayısı ve bellek limiti, önündeki vekil sunucu ve CDN yapılandırması, dış servislerin hangi modda olduğu, veri hacmi. Sütunlar: yerel, test, canlı. Bu tabloyu bir kez doldurmak, tipik bir "canlıda çalışmıyor" vakasında saatlerce süren tahmin turunu ortadan kaldırır. Çoğu ekipte tabloyu doldurmak da zaten iki saat sürüyor ve dolduranlar en az bir sürprizle karşılaşıyor.

Aynı derleme çıktısı iki ortama gitsin

Kural şu: bir kez derle, her yere dağıt. Test ortamı için ayrı, canlı için ayrı derleme yapıyorsanız test ettiğiniz şey canlıya çıkan şey değildir.

Bunu bozan üç alışkanlık var. Birincisi değişebilir etiketler: Docker'da :latest ya da :staging etiketi zaman içinde başka bir imaja işaret edebilir, dolayısıyla iki ortam aynı etiketle farklı kod çalıştırabilir. Sabitlemek istiyorsanız digest kullanın (image@sha256:...). İkincisi bağımlılık kilidini atlamak: npm ci kilit dosyasını birebir kurar, npm install kilidi güncelleyebilir. Dağıtım hattında npm install çağırıyorsanız iki ortamda farklı sürümler oluşması an meselesidir.

Üçüncüsü daha sinsi: derleme anında pakete gömülen değişkenler. Next.js'in kendi dokümanı açıkça uyarıyor, NEXT_PUBLIC_ ile başlayan değişkenler next build sırasında tarayıcıya giden pakete gömülür ve tek bir imajı iki ortama terfi ettiriyorsanız bu değerler ilk derlemedeki hallerinde donar. Uygulamada görüntüsü şudur: sunucu tarafı doğru API adresine bakar, tarayıcı tarafı hâlâ test ortamının adresine istek atar. Ortamdan ortama değişmesi gereken değerleri tarayıcıya derleme anında gömmeyin, çalışma zamanında sunun.

Yapılandırma ortama ait, ama doğrulanmazsa işe yaramaz

Ortam değişkeni kullanmak tek başına yetmiyor. Asıl arıza, eksik ya da yanlış yazılmış bir değişkenin uygulama ayağa kalkarken değil, o kod yolu ilk kez çalıştığında fark edilmesi. Ödeme sağlayıcısının anahtarı eksikse bunu ilk ödeme denemesinde öğrenmek istemezsiniz.

Çözüm iki satırlık: açılışta yapılandırmayı şemayla doğrulayın ve eksikse süreci başlatmayın. Zorunlu değişkenlerin listesi, tipleri, izin verilen değerleri bir yerde tanımlı olsun; uygulama eksik değişkenle ayağa kalkmasın. Bu kontrol, canlıya çıkıştaki "unuttuk" sınıfı hataların büyük kısmını sağlık kontrolü aşamasında yakalar.

İkinci kural varsayılan değerlerle ilgili. Kodda DB_HOST ?? "localhost" gibi bir varsayılan bırakırsanız, değişkeni canlıda tanımlamayı unuttuğunuzda uygulama hata vermez, sessizce yanlış yere bağlanır. Sessiz yanlış davranış, gürültülü çökmeden her zaman pahalıdır.

Üçüncüsü sır yönetimi. Test ortamı canlının veritabanı kullanıcısıyla, canlının ödeme anahtarıyla ya da canlının e-posta gönderim anahtarıyla çalışmamalı. Her ortamın kendi kimlik bilgisi olsun ve test ortamının yetkisi dar kalsın. Uygulama tarafındaki yöntemler sır yönetimi yazısında duruyor. Gördüğümüz en kötü versiyonu, test ortamının canlı veritabanına bağlı olması: bir toplu güncelleme denemesi gerçek müşteri kayıtlarını siliyor ve kimse bunu bir ortam hatası olarak görmüyor.

Veri farkı en pahalı olanı

Test veritabanında 5 bin satır var, canlıda 5 milyon. Maliyet tabanlı sorgu planlayıcısı küçük tabloda sıralı taramayı en ucuz yol olarak seçer ve sorgu milisaniyelerle döner. Aynı sorgu canlıda başka bir plana düşer, eksik bir indeks yüzünden diske iner ve zaman aşımına uğrar. Kod aynı, sorgu aynı, sonuç farklı. Planlayıcı tarafının ayrıntısı veritabanı darboğazları yazısında.

Hacim tek fark değil. Gerçek veri, temiz veri değildir: adres alanında satır sonu, isimde apostrof, telefonda boşluk, ürün adında emoji bulunur. MySQL'de utf8 yerine utf8mb4 kullanmadıysanız emoji içeren kayıt canlıda hata verir, test ortamındaki uydurma veride hiç görünmez. Postgres tarafında daha az bilinen bir tuzak var: metin indeksleri işletim sisteminin sıralama kurallarına (collation) göre kurulur ve glibc 2.28 ile gelen kural değişikliği, farklı dağıtım sürümlerinde kurulmuş iki veritabanının aynı metni farklı sıralamasına yol açtı. Postgres 13 ve sonrası bu durumda sürüm uyuşmazlığı uyarısı basıyor; uyarıyı görüyorsanız REINDEX gerekiyor demektir. İki ortamı farklı işletim sistemi sürümlerinde çalıştırmanın sonuçlarından biri bu.

Yapılması gereken, canlıdan kopya almak değil. Maskelenmiş ve üretim hacmine yakın bir veri kümesi hazırlayın: kişisel alanlar değiştirilmiş, satır sayısı ve dağılım korunmuş. Kişisel veriyle test ortamı beslemenin neden ayrı bir mesele olduğu ve maskeleme yöntemleri test verisi yazısında anlatılıyor.

Tek kopyada çalışır, ikinciyi eklediğinizde bozulur

Test ortamları genelde tek örnek koşar, canlı ise iki ya da daha fazla. Bu tek fark bir hata sınıfının tamamını gizler.

Oturumu ya da önbelleği uygulama belleğinde tutuyorsanız, kullanıcı ikinci örneğe düştüğünde çıkış yapmış görünür. Zamanlanmış görevi uygulama içinde çalıştırıyorsanız, üç örnekte aynı iş üç kez çalışır ve e-posta üç kez gider. Yüklenen dosyayı sunucunun diskine yazıyorsanız, dosya sadece o örnekte vardır ve bir sonraki dağıtımda kaybolur. Aynı kaydı iki örnek aynı anda güncellediğinde ortaya çıkan yarış durumları da sadece burada görünür. Okuma kopyası (read replica) kullanıyorsanız bir tane daha eklenir: kullanıcı kaydı günceller, hemen ardından gelen okuma replikaya düşer ve eski değeri görür.

Bu sınıfın tek gerçek panzehiri test ortamında da en az iki örnek çalıştırmaktır. İki küçük örnek, tek büyük örnekten daha faydalıdır. Örnek sayısı ve çalıştırma modeli kararı için Kubernetes gerekli mi yazısına bakabilirsiniz.

Uygulamanın önündeki katman yerelde hiç yok

Geliştirici makinesinde istek doğrudan uygulamaya gelir. Canlıda önce CDN, sonra yük dengeleyici, sonra bir vekil sunucu, belki bir web uygulama güvenlik duvarı vardır. Bu katmanların her biri isteği değiştirir.

Sık karşılaşılanlar: TLS'i vekil sunucu sonlandırdığı için uygulama isteği HTTP olarak görür, Secure işaretli çerez gönderilmez ve oturum açılmaz. Çözüm X-Forwarded-Proto başlığını güvenerek okumaktır (Express'te trust proxy). Üçüncü taraf bir alan adından gelen iframe ya da ödeme dönüşü için SameSite=None gerekiyorsa, tarayıcı bunu ancak Secure ile kabul eder; yerelde HTTP üzerinde çalıştığınız için bu hiç görünmez. Dosya yükleme canlıda 413 döner, çünkü nginx'in client_max_body_size varsayılanı 1 MB'tır. CDN, geliştirme ortamında olmayan bir önbellek katmanı ekler ve kullanıcı eski fiyatı görmeye devam eder. Güvenlik duvarı, içinde SQL benzeri metin geçen meşru bir formu engeller.

Bu maddelerin ortak özelliği, uygulama loglarında hiç görünmemeleri. Bu yüzden ortam farkı listesine vekil sunucu ve CDN yapılandırmasını da ekleyin; hata ayıklarken önce isteğin uygulamaya hangi halde ulaştığına bakın.

Dış servislerin test modu gerçeği anlatmaz

Ödeme sağlayıcısının sandbox ortamı her zaman başarılı döner, gerçek banka reddini simüle etmez. Kargo firmasının test servisi çoğu zaman yoktur. SMS sağlayıcısında test modu mesajı hiç göndermez. Üstelik bazı ayrıntılar ortama özgüdür: webhook imza anahtarı genelde uç nokta başına farklıdır, yani test ortamının anahtarıyla canlının imzasını doğrulayamazsınız. Hız limitleri ve IP beyaz listeleri de sadece canlıda devrededir.

Yapılabilecek iki şey var. Birincisi sözleşme testi: dış servisin yanıt biçimini bir kez kaydedip kendi testlerinizde onu oynatın, böylece servis değiştiğinde haberiniz olsun. İkincisi, servis yokmuş gibi davranan bir yol her zaman bulunsun. Kesinti senaryolarının nasıl kurgulandığı dış servis kesintisi yazısında duruyor.

İki ortam kendiliğinden birbirinden uzaklaşır

Sürüklenme (drift) tek bir kararla olmuyor. Biri canlıda acil bir sorunu çözmek için sunucuya bağlanıp bir ayar değiştiriyor ve test ortamına yansıtmayı unutuyor. Bir indeks canlıda elle ekleniyor. Bir ortam değişkeni panelden güncelleniyor. Altı ay sonra iki ortam arasında kimsenin haritasını çıkaramadığı onlarca fark oluşuyor.

Üç pratik önlem: altyapı ve yapılandırma değişikliklerinin tek yolu sürüm kontrolündeki dosyalar olsun, elle yapılan değişiklik istisna sayılsın ve aynı gün geri yazılsın. Şema karşılaştırmasını dağıtım hattına koyun, iki ortamın şeması ayrıştığında derleme uyarsın. Ortam değişkeni listelerini (değerleri değil, anahtarları) periyodik olarak karşılaştırın; canlıda olup testte olmayan bir anahtar, bir sonraki sürprizin adresidir. Şema değişikliklerini kesintisiz uygulama tarafı için sıfır kesintili yayına alma yazısına bakın.

Kalıcı test ortamı mı, dal başına geçici ortam mı?

Tek bir paylaşımlı test ortamı, ekip büyüdükçe kilit noktasına dönüşüyor. Aynı anda üç kişinin çalışması sıraya giriyor, kimin neyi bozduğu belirsizleşiyor, ortamda kimsenin sahiplenmediği yarım kalmış veri birikiyor. Bunun yerine yaygınlaşan düzen, her birleştirme isteği (pull request) için otomatik kurulan ve kapanınca silinen geçici ortamlar. Vercel ve Netlify gibi platformlarda ön uç için hazır geliyor; arka uç ve veritabanı için kurulum gerekiyor ama mantık aynı.

Kalıcı bir ortamın hâlâ yeri var: dış sistemlerle uzun süreli entegrasyon denemeleri, yük testi ve müşterinin imza attığı kabul testi. Bu ikisi birbirini dışlamıyor. Geçici ortamlar geliştirme sırasındaki geri bildirimi hızlandırır, kalıcı ortam sürüm öncesi son kontrolü taşır. Kabul testinin nasıl kurgulanacağı kabul testi yazısında, yük tarafı yük testi yazısında.

Bazı farkları kapatamazsınız, o zaman canlıda doğrulayın

Canlının trafiğini, veri hacmini ve gerçek kullanıcı davranışını hiçbir test ortamı tam olarak taklit edemez. Bunu kabul edip riski yayına alma yöntemine taşımak daha gerçekçi.

Pratikte üç araç iş görüyor. Özellik bayrağı: kod canlıya çıkar ama kapalıdır, önce iç ekibe, sonra kullanıcıların küçük bir yüzdesine açılır (özellik bayrakları yazısı). Sentetik kontrol: kritik akışları (giriş, sepete ekleme, ödeme başlatma) dakikada bir gerçek canlı ortamda çalıştıran bir robot. Ve dağıtım sonrası ilk on beş dakikayı izlemek: hata oranı, gecikme, kuyruk uzunluğu. Bu üçünü kurduğunuzda "canlıda çalışmıyor" bir müşteri şikâyeti olmaktan çıkıp bir alarma dönüşür. Ölçüm eşiği ve hedef bağlama için SLO ve hata bütçesi yazısı var.

Bu haftaya sığan üç adım

Bir: ortam değişkenlerinin anahtar listesini iki ortamdan çekip karşılaştırın. Değerleri değil sadece anahtarları. Fark çıkarsa, ki genelde çıkar, her birini not edin ve sahibini bulun.

İki: test veritabanını üretim hacmine yakın maskelenmiş veriyle doldurun, sonra en çok kullanılan beş sorgunun planını iki ortamda karşılaştırın. Planlar ayrışıyorsa eksik indeksi bulmuşsunuz demektir.

Üç: test ortamında örnek sayısını ikiye çıkarın ve bir hafta öyle bırakın. Bellekte tutulan oturum, çift çalışan zamanlanmış görev ve diske yazılan dosya varsa o hafta içinde ortaya çıkar. Üçü de bir günlük iş ve üçü de aynı sınıftan arızayı tekrar tekrar yaşamanızı engeller.


Bu konuda yardıma mı ihtiyacınız var?

iletişime geç →← tüm yazılar