Ziyaret Ettiğiniz Mimari: Bu Site GCP'de Nasıl Çalışıyor?

Bu yazıyı okuduğunuz sayfa, anlatacağım sistemin kendisi.

Aynı kod tabanı üç ayrı canlı web sitesini ve bir beta ortamını çalıştırıyor; her biri Türkçe ve İngilizce olmak üzere iki dilde. Aşağıda nasıl kurulduğunu anlatıyorum.

Bu sistemi neden kurduğumu ve sekiz yılın sonunda maliyetinin ne olduğunu ayrı bir yazıda anlattım: GCP Maliyeti: Bir Kararın 8 Yıllık Faturası

Bir depo, dört uygulama

Angular 20 üzerine kurulu bir monorepo: herkese açık site, yönetim paneli, bulut fonksiyonları ve üçünün paylaştığı ortak kütüphane. Herkese açık sitenin sayfaları sunucuda hazırlanıyor (SSR — server-side rendering), yani tarayıcı ilk isteğinde sayfanın tamamlanmış hâlini alıyor; arama motorlarının ve bağlantı önizlemelerinin gördüğü şey de bu.

Yönetim paneli ise bilerek SSR değil: adresleri statik bir index.html'e yönleniyor ve panel tarayıcıda çalışan düz bir uygulama olarak açılıyor. Arama motorunun görmesi gereken bir şey olmadığı için sunucuda sayfa üretmenin karşılığı da yok.

Bugünkü büyüklüğü: yaklaşık 37 bin satır uygulama kodu — bunun 14 bini test — artı 10 bin satır ajan aracı (agent tooling). 72 rota, 54 bileşen, iki dil, dört ortam.

Bir isteğin yolculuğu

Parçaları tek tek saymak yerine bir isteği izlemek daha çok şey anlatıyor. Şu anda bulunduğunuz adresi açtığınızda sırayla şunlar oluyor:

  1. Tarayıcınız HTTPS GET
  2. DNS Cloud DNS
  3. Firebase Hosting CDN edge statik dosya · burada biter
  4. Yeniden yazma kuralı rewrites
  5. SSR fonksiyonu europe-west1
  6. Tarayıcınız, yeniden hydration
  7. Sonraki veri istekleri Firestore · App Check
  1. Tarayıcınız — adresi açıyorsunuz ve istek yola çıkıyor.
  2. DNS — Cloud DNS alan adını çözüyor; tarayıcı nereye bağlanacağını öğreniyor.
  3. Firebase Hosting — istek buraya düşüyor. Statik bir dosyaysa (görsel, betik, robots.txt) doğrudan CDN'den cevaplanıyor ve iş orada bitiyor.
  4. Yeniden yazma (rewrite) kuralı — makale, sayfa, etiket ve sitemap.xml gibi adresler bir kural listesine takılıp SSR fonksiyonuna yönleniyor.
  5. SSR fonksiyonueurope-west1'de uyanıyor, Firestore'dan dokümanı okuyor, Angular'ı sunucuda çalıştırıyor ve tamamlanmış HTML döndürüyor.
  6. Tarayıcınız, yeniden — sayfayı hazır alıp gösteriyor; ardından Angular devralıp uygulamayı canlandırıyor.
  7. Sonraki veri istekleri — artık doğrudan Firestore'a gidiyor ve her biri bir App Check belirteci taşıyor.

Arama motorunun ve bağlantı önizlemelerinin gördüğü şey 5. adımın çıktısı; ziyaretçinin gezinmeye başladıktan sonra gördüğü şey 7. adım.

İçerik veritabanında, derlemede değil

Bütün içerik Firestore'da duruyor: makaleler, sayfalar, menü, altbilgi, çerez metni, iletişim bilgileri. Bir yazıyı düzeltmek dağıtım gerektirmiyor; yayına alma ile yazma birbirinden ayrı. Bu yazının kendisi de bir Firestore dokümanı.

Node.js 24 üzerinde çalışan Cloud Functions v2 tarafında ise sayfaların sunucuda hazırlanması, site haritaları, yapısal veri ve doküman yazımıyla tetiklenen bir iş kuyruğu (job queue) var. Fonksiyonlar europe-west1 bölgesinde çalışıyor.

Fonksiyonların iki sınırı var ve ikisi de bilinçli: bir istek en fazla 60 saniye çalışabiliyor, ve SSR tarafı en fazla iki örnekle ölçekleniyor. İkincisi hem harcamanın tavanını hem de bir arızanın nereye kadar yayılabileceğini belirliyor: askıda kalan bir istek, o eşzamanlılığı sitedeki bütün sayfalarla paylaşıyor. Aynı tavan meşru trafikte de geçerli: ani bir yükselmede istekler sıraya giriyor. Bu ölçekte kabul ettiğim bir takas.

Aynı tarafta zamanlanmış (scheduled) bir iş de duruyor: her pazar gecesi 02:00'de Firestore'un tamamı otomatik olarak dışa aktarılıyor ve yedek bir Storage kovasına (bucket) yazılıyor. Elle yedek almak da mümkün; aynı yolu kullanıyor.

Sitelerin farkı yapılandırmada, kodun içinde değil

Siteler arasındaki fark ne bir tema kontrolüyle ne de bileşenin içine yazılmış bir koşulla çözülüyor. Her sitenin kendi derleme yapılandırması ve kendi stil sayfası var; bir özellik yalnızca bazı sitelerde açılacaksa adı konmuş bir bayrak alıyor.

Faydası şu: bir bileşene bakan kişi hangi sitede ne olacağını tahmin etmek zorunda kalmıyor. Yeni bir site eklemek de ortak koda dokunmuyor — bir ortam dosyası, bir stil sayfası, bir dağıtım hattı.

Sonuç şu: tek depodan sekiz uygulama derlemesi çıkıyor — dört ortam çarpı iki Angular uygulaması — ve her biri ayrıca iki dil üretiyor. Hepsi aynı ortak kütüphaneyi kullanıyor ve hiçbirinde diğerine özel bir kod yolu yok.

Tarayıcıda: kurulabilir bir uygulama

Dört ortamın hepsinde servis çalışanı (service worker) açık ve site bir web uygulaması olarak kurulabiliyor (PWA — progressive web app). Uygulama kabuğu (app shell) önden indirilip önbelleğe (cache) alınıyor, varlıklar ilk kullanımda önbelleğe giriyor. Pratik karşılığı: tekrar ziyaretlerde sayfa anında açılıyor ve şebeke zayıfken uygulama ayakta kalıyor.

Sınırı da var: içeriği servis çalışanı önbelleğe almıyor. Makale metinleri Firestore'dan geliyor ve servis çalışanının önbellek kuralları veri isteklerini kapsamıyor.

Firestore SDK'sının kendi yerel önbelleği ise devrede: sekme açık olduğu sürece indirdiği veriyi tutuyor, dinleyicilerle sunucuyla senkron kalıyor ve bağlantı kısa süre koparsa okumalar oradan cevaplanıyor. Ama bu önbellek varsayılan olarak yalnızca bellekte duruyor; tarayıcı veritabanına yazılması ayrıca açılması gereken bir seçenek ve ben açmadım. Yani sayfayı yenilediğinizde önbellek de sıfırlanıyor.

Sonuç: burası bir çevrimdışı arşiv değil. Kabuk çevrimdışı açılıyor, içerik için bağlantı gerekiyor. Kalıcı önbelleği açmak mümkün ama nadiren okunan bir arşivi her ziyaretçinin cihazına indirmenin karşılığı yok.

Yönetim panelinin de kendi servis çalışanı var — ayrı bir uygulama olduğu için ayrı bir önbellek kümesi. Herkese açık sitenin servis çalışanı ise panelin adreslerini bilerek kapsam dışında bırakıyor; ikisinin birbirinin sayfalarını önbelleğe alması istenen bir şey değil.

Güvenlik: yamalanacak sunucu yok

2018'de bu mimariyi seçme gerekçelerimden biri güvenlikti. Pratikte şuna karşılık geliyor:

  • Sunucu bakımı bende değil. Yamaladığım bir işletim sistemi, ayarladığım bir web sunucusu ya da güncellediğim bir dil sürümü yok.
  • İçerik veri, eklenti yok. Çalıştırılabilir üçüncü taraf kodu barındırmıyorum. İçerikten gelen HTML de sayfaya konmadan önce süzülüyor: satır içi stil, betik, olay işleyicileri ve satır içi SVG düşüyor. Yani içerik veri; yürütülebilir şablon değil.
  • Yönetim paneli ayrı bir uygulama ve giriş sayfası dışında her rotası kimlik doğrulamasının arkasında.
  • Veritabanı ve dosya erişimi kural dosyalarıyla (security rules) tanımlı, uygulamanın iyi niyetine bırakılmamış.
  • App Check yalnızca formları değil, okuma isteklerini de kontrol ediyor.

App Check'in gerçekte ne yaptığı

İçerik koleksiyonlarının kuralları okumaya açık — herkese açık bir sitede olması gerektiği gibi. Ama App Check dört projenin hepsinde Cloud Firestore ürününün kendisinde zorunlu kılınmış durumda.

Bu ikisi farklı katman. Kural dosyası “bu belgeyi kim okuyabilir” sorusunu cevaplıyor; App Check ise “bu istek gerçekten benim uygulamamdan mı geliyor” sorusunu. Sonuç şu: içerik herkese açık ama rastgele bir istemciden okunamıyor. Geçerli bir doğrulama belirteci olmayan bir betik veritabanına hiç ulaşamıyor.

Sınırını da söylemek gerek: App Check bir kimlik doğrulama değil, köken beyanı. Gelişigüzel bir betiği kapıda durduruyor, kararlı bir saldırganı durdurmuyor — belirteç çıkarılabilir, üstelik geliştirme için bir hata ayıklama belirteci zaten var. Asıl koruma yazma tarafında: içerik koleksiyonlarına yazmak kimlik doğrulamasına ve yönetici yetkisine bağlı. Okumanın herkese açık olması da bilinçli — burası bir web sitesi.

Bunun bir bedeli de var: belirteç bayatladığında çalışan bir dinleyici aniden susabiliyor. Bu yüzden istemci tarafında, izin hatası alan bir dinleyiciyi belirteci tazeleyip yeniden kuran bir kurtarma mekanizması var.

Dağıtım: sekiz workflow ve bir geri dönüş

developbetamain sırası dışında bir yol yok, ve bunu zorlayan şey bir kural metni değil. Her dağıtım workflow'u kendisini tetikleyen birleştirmenin hangi daldan geldiğine bakıyor: beta yalnızca develop'tan gelen bir birleştirmeyle, üç canlı site ise yalnızca beta'dan gelenle yayına giriyor. Yanlış daldan açılmış bir birleştirme hiçbir şeyi dağıtmıyor.

Asıl kapı ise daha erken: pre-push. Kod uzak depoya çıkmadan önce lint ve bütün test paketleri yerelde koşuyor; geçmezse push olmuyor. Sunucudaki hiçbir kontrol bu kadar erken davranamıyor.

Tuhaf tarafı da burada: aynı paket iki kez koşuyor. Bir kez pre-push'ta, yerelde, kapı olarak — geçmezse kod uzak depoya hiç çıkmıyor. Bir kez de develop'a düşen her push'ta CI'da, kayıt olarak. İkisi de aynı lint ve aynı bütünleşik test paketi.

Sebebi şu: bu depoda dal koruması yok, yani CI'daki sonuç bir birleştirmeyi teknik olarak engelleyemiyor. O yüzden asıl kapıyı yerele koydum; CI ise testlerin gerçekten koştuğunu ve sonucun benim makineme özgü olmadığını gösteriyor.

Bir üretim dağıtımının adımları şöyle:

  1. PWA ve bulut fonksiyonları derleniyor
  2. Firebase'e dağıtılıyor
  3. Veritabanı göçleri tetikleniyor ve bitmesi bekleniyor
  4. Canlı sitenin üzerinde Playwright paketi koşuyor
  5. Göç ya da test düşerse geri alma devreye giriyor — önceki sürümün paketi zaten saklanmış durumda

Sıra bilerek böyle: önce dağıtım, sonra göç. Yani yeni kod, göç bitene kadar kısa bir süre eski veriyle birlikte canlıda duruyor. Bu pencere saniyeler mertebesinde ama sıfır değil, ve göçü buna göre yazmak gerekiyor.

Göçlerin kendisi sürümlü: numaralandırılmışlar, sırayla koşuyorlar ve uygulananların listesi veritabanında bir sistem dokümanında tutuluyor, böylece her göç tam bir kez çalışıyor.

Geri alma adımının koşulu da listede göründüğünden geniş: dağıtımın kendisi başarılı olduğu sürece, sonraki herhangi bir adımın düşmesi geri almayı tetikliyor — göç de buna dahil. Buna karşılık derleme aşamasında düşen bir sürümde geri alınacak bir şey yok, çünkü ortada yayınlanmış yeni bir sürüm de yok.

Kalan workflow'lar hiçbir şey dağıtmıyor: biri her birleştirme isteğinde (PR — pull request) yapıyı denetliyor, biri kod incelemesi yapıyor, biri de diğer koşuların kullanacağı bağımlılık önbelleğini önceden hazırlıyor. Üçü de bir şeyi engellemiyor — engelleyen yalnızca pre-push ve yukarıdaki dal kontrolü. Bunu bilerek böyle bıraktım: uyarı üreten bir kontrolle sürümü durduran bir kontrol aynı şey değil, ve hangisinin hangisi olduğu belli olmalı.

Dört test paketi, dört ayrı iş

  • Herkese açık davranış — oturum açmamış bir ziyaretçinin gördüğü her şey.
  • Hosting'in arkasındaki tam paket — yönetim panelinde oturum açıp içerik oluşturma, güncelleme ve silmeyi de kapsıyor.
  • Canlı duman testi (smoke test) — dağıtımın kaderini belirleyen paket budur. Sayfaların yanı sıra sitemap.xml, llms.txt ve robots.txt'in gerçekten servis edildiğini de doğruluyor; bu kontrol bir arızadan sonra eklendi, hikâyesi diğer yazıda.
  • SEO ve WCAG 2.1 AA denetimi — bilerek dağıtımı kesmiyor, çünkü bulguları içeriğe bağlı ve tavsiye niteliğinde.

Bu ayrım kasıtlı: bir erişilebilirlik uyarısının bir sürümü engellememesi gerekiyor, bozuk bir ana sayfanın ise kesinlikle engellemesi.

Sonradan gelen katman: ajan araçları

Bu kısım 2024'te yoktu, 2026'da eklendi. İçerik, yapılandırma, medya, tohum verisi, yedekleme ve denetim için Firestore, Storage ve Auth üzerinde çalışan altı komut satırı aracı (CLI) yazdım. Bunları kodlama ajanları (AI coding agents) kullanıyor ve hepsi tek bir koruma katmanının arkasında duruyor — çünkü bir kurallar dosyası yol göstericidir, kısıt değildir.

Bu katmanı iki kural ayakta tutuyor: her komut hedef ortamı açıkça söylemek zorunda, varsayılanı yok; ve bir alanı sessizce silecek bir yazma işlemi reddediliyor. İkincisi bir kazadan sonra kondu — bir kaydetme işlemi bir makalenin sıra numarasını düşürmüş, makale kendi URL'inde çalışmaya devam ederken liste sayfalarından sessizce kaybolmuştu.

Kapanış

İddiaların çoğunu bu sayfada sınayabilirsiniz: kaynağı görüntüleyin, sayfanın tamamı sunucudan gelen HTML'in içinde; sitemap.xml'i açın, onu da aynı fonksiyon üretiyor.

Kodun kendisini gösteremiyorum; sistem 2024'ten beri kapalı kaynak. Ama dışarıdan gözlemlenebilen her şey burada, önünüzde duruyor.

Gerçek maliyet kalemleri, mimarinin iki yıl sonra verdiği sınav ve bir arızanın bana öğrettikleri ayrı bir yazıda duruyor: GCP Maliyeti: Bir Kararın 8 Yıllık Faturası

Mimariyle ilgili sorunuz varsa iletişim sayfasından yazabilirsiniz.