GCP Firestore Veritabanı Yönetimi: Kapsamlı Rehber

Prodüksiyonda bir Firestore veritabanını yönetmek, ilk bakışta kolay görünebilir. Sonuçta Google’ın yönettiği, serverless bir NoSQL çözümünden bahsediyoruz. Ama işin içine girince, koleksiyon tasarımından güvenlik kurallarına, yedekleme stratejilerinden maliyet optimizasyonuna kadar ciddi kararlar vermeniz gerektiğini anlıyorsunuz. Bu yazıda, Firestore’u gerçek dünyada nasıl yönettiğimizi, hangi tuzaklara düştüğümüzü ve bunlardan nasıl kurtulduğumuzu paylaşacağım.

Firestore Nedir ve Ne Zaman Kullanmalısınız

Firestore, GCP’nin sunduğu tam yönetimli, serverless bir belge veritabanıdır. İki modda çalışır: Native mode ve Datastore mode. Yeni projeler için her zaman Native mode kullanın. Datastore mode, eski Datastore API’siyle geriye dönük uyumluluk için var.

Firestore’un parladığı senaryolar şunlardır:

  • Gerçek zamanlı senkronizasyon gereken mobil ve web uygulamaları
  • Esnek şema gerektiren hızlı gelişen ürünler
  • Okuma yoğunluklu workload’lar
  • Firebase Authentication ile entegre çalışan projeler

Ama şunu da söyleyeyim, Firestore her şeyin çözümü değil. Karmaşık JOIN sorguları yapmanız gerekiyorsa, ağır aggregation işlemleri yapacaksanız ya da strict transactional bütünlük kritikse, Cloud Spanner veya Cloud SQL daha uygun olabilir.

Firestore Veritabanı Oluşturma ve Temel Yapılandırma

Önce gcloud CLI ile bir Firestore veritabanı oluşturalım. Bu adımları her yeni proje kurulumunda uyguluyorum.

# GCP projesini ayarla
gcloud config set project my-production-project

# Firestore veritabanı oluştur (Native mode, europe-west1 bölgesi)
gcloud firestore databases create 
  --location=europe-west1 
  --type=firestore-native

# Mevcut veritabanı bilgilerini görüntüle
gcloud firestore databases describe

# Birden fazla veritabanı varsa listeleyebilirsiniz (multi-database özelliği)
gcloud firestore databases list

Bölge seçimi burada kritik. Verilerinizi hangi coğrafyada tutmanız gerektiğini, kullanıcılarınızın nereden erişeceğini ve GDPR gibi yasal gereksinimleri göz önünde bulundurun. Bir kez seçtikten sonra bölgeyi değiştiremezsiniz, bu yüzden bu kararı aceleyle vermeyin.

Koleksiyon ve Belge Yapısı Tasarımı

Firestore’da her şey koleksiyonlar ve belgeler üzerine kurulu. Flat bir yapı mı, yoksa subcollection’lar mı kullanmalısınız? Bu soruyu yanlış cevaplamak ilerleyen süreçte ciddi sorunlara yol açar.

Bir e-ticaret projesinde şu yapıyı kullanıyoruz:

/users/{userId}
/users/{userId}/orders/{orderId}
/users/{userId}/orders/{orderId}/items/{itemId}
/products/{productId}
/categories/{categoryId}

Subcollection kullanmanın avantajı, bir kullanıcının siparişlerini çekerken tüm kullanıcı verilerini yüklemek zorunda kalmamanızdır. Ama dezavantajı da var, bir koleksiyon grubunu (collection group) sorgulamak için composite index gerekiyor.

gcloud CLI ile koleksiyondaki belgeleri listeleyelim:

# Bir koleksiyondaki belgeleri listele
gcloud firestore documents list 
  --collection=users 
  --limit=10

# Belirli bir belgeyi getir
gcloud firestore documents get 
  projects/my-production-project/databases/(default)/documents/users/user123

# Yeni bir belge oluştur
gcloud firestore documents create 
  projects/my-production-project/databases/(default)/documents/users/user456 
  --fields='name=string:Ahmet Yilmaz,email=string:[email protected],createdAt=timestamp:2024-01-15T10:00:00Z'

Güvenlik Kuralları: En Kritik Konu

Firestore güvenlik kuralları, yanlış yapılandırıldığında prodüksiyon felaketine davetiye çıkarır. Bir proje devralırken gördüğüm en yaygın hata şu:

// YANLIŞ - Asla bunu yapma
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

Bu kural herkese her şeyi açar. Bunun yerine granüler kurallar yazın:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Kullanıcılar sadece kendi verilerini okuyabilir ve güncelleyebilir
    match /users/{userId} {
      allow read, update, delete: if request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null;
    }

    // Siparişler: kullanıcı kendi siparişlerini görür, admin her şeyi görür
    match /users/{userId}/orders/{orderId} {
      allow read: if request.auth != null &&
        (request.auth.uid == userId ||
         get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin');
      allow create: if request.auth != null && request.auth.uid == userId;
      allow update, delete: if request.auth != null &&
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
    }

    // Ürünler herkese açık okunabilir, sadece admin yazabilir
    match /products/{productId} {
      allow read: if true;
      allow write: if request.auth != null &&
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
    }

    // Kategoriler herkese açık
    match /categories/{categoryId} {
      allow read: if true;
      allow write: if false;
    }
  }
}

Güvenlik kurallarını deploy etmek ve test etmek için:

# Firebase CLI ile güvenlik kurallarını deploy et
firebase deploy --only firestore:rules

# Kuralları test modunda çalıştır (emulator ile)
firebase emulators:start --only firestore

# Mevcut kuralları görüntüle
firebase firestore:rules:get

Güvenlik kuralı yazarken şu prensipleri her zaman uyguluyorum: En az ayrıcalık ilkesi her zaman geçerli. Bir kullanıcının neye erişmesi gerektiğini belirleyin, geri kalanı kapatın. request.resource.data ile yazılacak veriyi, resource.data ile mevcut veriyi validate edin. Ve asla get() çağrılarını aşırı kullanmayın, her çağrı ek okuma maliyeti demek.

Index Yönetimi ve Performans Optimizasyonu

Firestore, tek alanlı sorgular için otomatik index oluşturur. Ama composite sorgular için manuel index oluşturmanız gerekir. Bunu atlamak, FAILED_PRECONDITION hatası almanıza neden olur.

# Mevcut indexleri listele
gcloud firestore indexes composite list

# Composite index oluştur
gcloud firestore indexes composite create 
  --collection-group=orders 
  --field-config=field-path=status,order=ASCENDING 
  --field-config=field-path=createdAt,order=DESCENDING

# Index oluşturma durumunu kontrol et
gcloud firestore indexes composite list 
  --filter="state=CREATING"

# Kullanılmayan indexi sil
gcloud firestore indexes composite delete INDEX_ID

firestore.indexes.json dosyasıyla indexleri kod olarak yönetmek en iyi pratik:

# firestore.indexes.json dosyasını uygula
firebase deploy --only firestore:indexes

# Mevcut indexleri export et
firebase firestore:indexes > firestore.indexes.json

Index maliyeti de önemli bir faktör. Her index ek yazma maliyeti demek. Bir belgeye her yazma işleminde, o belgeyi kapsayan tüm indexler güncellenir. Gereksiz indexleri temizlemek hem maliyet düşürür hem de yazma performansını artırır.

Veri Yedekleme ve Dışa Aktarma

Firestore managed service olsa da yedekleme konusunda rehavete kapılmayın. Firestore’un yerleşik yedekleme özelliği var ama bunu bilen ve aktif kullanan sysadmin sayısı şaşırtıcı derecede az.

# Firestore verisini Cloud Storage'a export et
gcloud firestore export gs://my-backup-bucket/firestore-backup-$(date +%Y%m%d) 
  --collection-ids=users,orders,products

# Tüm koleksiyonları export et
gcloud firestore export gs://my-backup-bucket/firestore-full-backup-$(date +%Y%m%d)

# Export durumunu kontrol et
gcloud firestore operations list

# Belirli bir operasyonun detayını gör
gcloud firestore operations describe OPERATION_NAME

# Import işlemi (restore senaryosu)
gcloud firestore import gs://my-backup-bucket/firestore-backup-20240115 
  --collection-ids=users

Bu export komutlarını bir Cloud Scheduler job’ı ile otomatikleştiriyorum. Şöyle bir bash script’i Cloud Functions veya Cloud Run Job olarak çalıştırabilirsiniz:

#!/bin/bash
# firestore-backup.sh

PROJECT_ID="my-production-project"
BUCKET="gs://my-firestore-backups"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH="${BUCKET}/daily/${TIMESTAMP}"

echo "Firestore backup basliyor: ${TIMESTAMP}"

gcloud firestore export "${BACKUP_PATH}" 
  --project="${PROJECT_ID}" 
  --async

if [ $? -eq 0 ]; then
  echo "Backup basarıyla baslatildi: ${BACKUP_PATH}"

  # 30 gunden eski yedekleri temizle
  gsutil -m rm -r $(gsutil ls ${BUCKET}/daily/ | 
    sort | head -n -30) 2>/dev/null || true

  echo "Eski yedekler temizlendi"
else
  echo "HATA: Backup baslatılamadı!"
  # Alert gonder (PagerDuty, Slack vb.)
  exit 1
fi

Yedek alma sıklığınızı RTO ve RPO gereksinimlerinize göre belirleyin. Kritik bir uygulama için günlük yeterli olmayabilir, saatlik export da ayarlayabilirsiniz.

Firestore Point-in-Time Recovery (PITR)

GCP, Firestore için Point-in-Time Recovery özelliği sunuyor. Bu özellik, veritabanınızı belirli bir geçmiş zamana geri döndürmenizi sağlar. Varsayılan olarak 7 gün boyunca etkin.

# PITR ile belirli bir zamana dön
gcloud firestore databases restore 
  --source-database=projects/my-production-project/databases/(default) 
  --destination-database=projects/my-production-project/databases/restored-db 
  --snapshot-time=2024-01-14T15:30:00Z

# Mevcut PITR ayarlarını kontrol et
gcloud firestore databases describe 
  --database=(default) 
  --format="value(pointInTimeRecoveryEnablement)"

# PITR süresini 14 güne çıkar
gcloud firestore databases update 
  --database=(default) 
  --enable-pitr

PITR’ı etkinleştirmek ek storage maliyeti doğurur ama prodüksiyonda kesinlikle aktif tutun. Bir geliştirici yanlışlıkla toplu silme yaptığında sizi kurtaracak olan bu özellik olacak.

Maliyet Yönetimi ve Optimizasyon

Firestore maliyetleri üç ana başlıkta toplanır: okuma işlemleri, yazma işlemleri ve depolama. Bir projeyi devraldığımda ilk baktığım yer Cloud Billing raporları oluyor.

Maliyetleri düşürmek için şu stratejileri uyguluyorum:

Okuma maliyetini azaltmak:

  • Uygulama katmanında agresif caching kullan (Redis, Memorystore)
  • select() ile sadece ihtiyacınız olan alanları çekin
  • limit() kullanmayı asla unutmayın
  • Realtime listener’ları gereksiz yere açık bırakmayın

Yazma maliyetini azaltmak:

  • Batch write kullanın, tek tek yazmak yerine
  • Gereksiz field güncellemelerinden kaçının
  • TTL (Time To Live) politikaları ile eski verileri otomatik silin
# Cloud Monitoring ile Firestore metriklerini izle
gcloud monitoring metrics list 
  --filter="metric.type=starts_with(firestore.googleapis.com)"

# Firestore okuma/yazma operasyon sayısını sorgula
gcloud logging read 
  'resource.type="firestore_database" AND protoPayload.methodName="google.firestore.v1.Firestore.RunQuery"' 
  --limit=50 
  --format=json | 
  python3 -c "import sys,json; data=json.load(sys.stdin); print(f'Toplam sorgu: {len(data)}')"

Firestore TTL özelliği, eski belgeleri otomatik silmek için harika bir araç:

# TTL policy oluştur (sessions koleksiyonu için)
gcloud firestore fields ttls update expiresAt 
  --collection-group=sessions 
  --enable-ttl

Bu komuttan sonra, sessions koleksiyonundaki her belgede expiresAt alanı varsa, bu tarih geçtikten sonra Firestore belgeyi otomatik siler. Session yönetimi için bu özelliği mutlaka kullanın.

IAM ve Erişim Kontrolü

Servis hesabı izinleri konusunda da dikkatli olmak gerekiyor. Her uygulama servisi için ayrı servis hesabı oluşturun ve minimum gerekli izinleri verin.

# Firestore için servis hesabı oluştur
gcloud iam service-accounts create firestore-app-sa 
  --display-name="Firestore Application Service Account"

# Sadece okuma izni ver
gcloud projects add-iam-policy-binding my-production-project 
  --member="serviceAccount:[email protected]" 
  --role="roles/datastore.viewer"

# Okuma ve yazma izni (uygulama servisi için)
gcloud projects add-iam-policy-binding my-production-project 
  --member="serviceAccount:[email protected]" 
  --role="roles/datastore.user"

# Admin izni (dikkatli kullanın, sadece CI/CD pipeline için)
gcloud projects add-iam-policy-binding my-production-project 
  --member="serviceAccount:[email protected]" 
  --role="roles/datastore.owner"

# Mevcut IAM politikalarını listele
gcloud projects get-iam-policy my-production-project 
  --flatten="bindings[].members" 
  --format="table(bindings.role,bindings.members)" 
  --filter="bindings.members:firestore"

Monitoring ve Alerting

Firestore için izleme kurulumu çoğu zaman ihmal ediliyor. Şu metrikleri mutlaka takip edin:

  • firestore.googleapis.com/document/read_count: Okuma işlem sayısı
  • firestore.googleapis.com/document/write_count: Yazma işlem sayısı
  • firestore.googleapis.com/document/delete_count: Silme işlem sayısı
  • firestore.googleapis.com/network/request_count: İstek sayısı
# Alert policy oluştur (okuma sayısı günlük 1 milyonu geçerse uyar)
gcloud alpha monitoring policies create 
  --notification-channels=projects/my-production-project/notificationChannels/CHANNEL_ID 
  --display-name="Firestore High Read Count Alert" 
  --condition-display-name="Read count threshold" 
  --condition-filter='metric.type="firestore.googleapis.com/document/read_count" resource.type="firestore_database"' 
  --condition-threshold-value=1000000 
  --condition-threshold-comparison=COMPARISON_GT 
  --condition-aggregations-alignment-period=86400s 
  --condition-aggregations-per-series-aligner=ALIGN_SUM

# Firestore hatalarını loglardan sorgula
gcloud logging read 
  'resource.type="firestore_database" AND severity=ERROR' 
  --freshness=1d 
  --format=json | jq '.[].jsonPayload.status'

Gerçek Dünya Senaryosu: Prodüksiyon Olayı

Bir gün sabah 3’te alarm geldi. Bir mobil uygulamamızın Firestore okuma maliyeti normalin 15 katına çıkmıştı. Logları incelediğimizde şunu gördük: bir geliştirici, pagination yapmadan tüm orders koleksiyonunu çeken bir endpoint deploy etmişti. Saatte milyonlarca gereksiz okuma yapılıyordu.

Hızlı çözüm şu oldu:

# Hangi koleksiyonların en çok okunduğunu tespit et
gcloud logging read 
  'resource.type="firestore_database" AND protoPayload.request.parent=~"orders"' 
  --freshness=1h 
  --limit=100 
  --format=json | 
  jq '[.[].protoPayload.response.document.name] | group_by(.) | map({key: .[0], count: length}) | sort_by(.count) | reverse | .[0:10]'

# Geçici çözüm: IAM ile o servis hesabının Firestore erişimini kıs
gcloud projects remove-iam-policy-binding my-production-project 
  --member="serviceAccount:problematic-service@my-production-project.iam.gserviceaccount.com" 
  --role="roles/datastore.user"

Bu olaydan sonra tüm Firestore sorgularına zorunlu limit() ve pagination parametresi ekledik. Güvenlik kurallarına da belge başına maksimum döndürülebilecek kayıt sayısı kısıtlaması koyduk.

Firestore Emulator ile Yerel Geliştirme

Prodüksiyon Firestore’una bağlanarak geliştirme yapmak hem tehlikeli hem de maliyetli. Firestore Emulator bu sorunu çözüyor.

# Firebase CLI kur
npm install -g firebase-tools

# Firestore emulator'ı başlat
firebase emulators:start --only firestore

# Emulator UI'ı tarayıcıda aç (varsayılan: http://localhost:4000)

# Emulator'a veri yükle (seed data)
firebase emulators:exec --only firestore "node seed-data.js"

# Emulator verilerini dışa aktar (CI/CD için)
firebase emulators:export ./emulator-data

# Kaydedilmiş veriyle emulator'ı başlat
firebase emulators:start --only firestore --import=./emulator-data

CI/CD pipeline’ınıza emulator testlerini entegre edin. Her pull request’te güvenlik kuralları ve iş mantığı testleri otomatik çalışsın. Bu adımı atlamak, prodüksiyonda sürprizlerle karşılaşmanıza neden olur.

Sonuç

Firestore, doğru yapılandırıldığında gerçekten güçlü bir veritabanı platformu. Ama “serverless ve managed” olması, onu yönetimsiz bırakabileceğiniz anlamına gelmiyor. Güvenlik kurallarını ihmal etmek, index yönetimine dikkat etmemek ve yedekleme stratejisi oluşturmamak, önünde sonunda ciddi sorunlara kapı aralıyor.

Özetlemek gerekirse, şu adımları mutlaka uygulayın: Güvenlik kurallarını granüler yazın ve düzenli test edin. PITR’ı etkinleştirin ve düzenli export ile yedek alın. Maliyet izlemesi için alert kurun, beklenmedik artışlar sizi şaşırtmasın. IAM izinlerini minimum ayrıcalık prensibine göre yapılandırın. Index yönetimini kod olarak tutun ve gereksiz indexleri temizleyin. Yerel geliştirme için emulator kullanın, prodüksiyon verisiyle oynamayın.

Firestore’la ilgili sormak istedikleriniz ya da benzer tuzaklara düştüğünüz durumlar varsa, yorumlarda paylaşın.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir