GCP Cloud Functions ile Olay Tabanlı Fonksiyonlar
Serverless mimarinin en güzel yanlarından biri, altyapı yönetimiyle uğraşmadan sadece kod yazmaya odaklanabilmek. Google Cloud Platform’un sunduğu Cloud Functions hizmeti bu konuda oldukça güçlü bir araç, ancak asıl sihir olay tabanlı (event-driven) fonksiyonlarda gizli. HTTP isteklerine cevap veren basit fonksiyonların ötesine geçip, Cloud Storage’daki dosya yüklemelerine, Pub/Sub mesajlarına veya Firestore değişikliklerine otomatik tepki verebilen sistemler kurmak, modern bulut mimarisinin tam kalbine dokunmak demek.
Olay Tabanlı Fonksiyonlar Nedir?
GCP Cloud Functions’ta iki temel tetikleyici türü vardır: HTTP tetikleyiciler ve olay tabanlı tetikleyiciler. HTTP fonksiyonları bir URL’ye istek geldiğinde çalışır, oldukça basit. Ama olay tabanlı fonksiyonlar çok daha ilginç bir dünyaya kapı açar.
Olay tabanlı fonksiyonlar, GCP ekosistemi içindeki belirli olaylar gerçekleştiğinde otomatik olarak tetiklenir. Bu olaylar şunlar olabilir:
- Cloud Storage: Dosya yükleme, silme, arşivleme, metadata güncelleme
- Cloud Pub/Sub: Bir topic’e mesaj yayınlanması
- Cloud Firestore: Belge oluşturma, güncelleme, silme
- Firebase Realtime Database: Veri değişiklikleri
- Firebase Authentication: Kullanıcı oluşturma veya silme
- Cloud Scheduler: Zamanlanmış tetikleyiciler (Pub/Sub üzerinden)
Eski nesil Cloud Functions (1. nesil) ile yeni nesil (2. nesil) arasında önemli farklar var. 2. nesil, Eventarc altyapısını kullanıyor ve çok daha fazla GCP servisinden olay alabiliyor. Bu yazıda her ikisini de ele alacağız ama yeni projeler için kesinlikle 2. nesli tercih etmenizi öneririm.
Ortamı Hazırlamak
Önce gcloud CLI’yi yapılandıralım ve gerekli API’leri aktif edelim:
# Gerekli API'leri aktif et
gcloud services enable cloudfunctions.googleapis.com
gcloud services enable eventarc.googleapis.com
gcloud services enable cloudbuild.googleapis.com
gcloud services enable pubsub.googleapis.com
# Proje ve bölge ayarla
gcloud config set project PROJE_ID
gcloud config set functions/region europe-west1
# Service account izinleri (2. nesil için gerekli)
PROJECT_NUMBER=$(gcloud projects describe PROJE_ID --format='value(projectNumber)')
gcloud projects add-iam-policy-binding PROJE_ID
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
--role="roles/iam.serviceAccountTokenCreator"
Bu adımı atlamayın. Özellikle Eventarc ile çalışan 2. nesil fonksiyonlarda servis hesabı izinleri doğru yapılandırılmazsa fonksiyonlar tetiklenmez ve hata ayıklamak saatler alabilir.
Cloud Storage Tetikleyicisi ile Çalışmak
En yaygın kullanım senaryolarından biri, bir bucket’a dosya yüklendiğinde otomatik işlem yapılması. Diyelim ki kullanıcılar fotoğraf yüklüyor ve siz bu fotoğrafları işleyip küçük boyutlu versiyonlarını oluşturmak istiyorsunuz.
# main.py - Cloud Storage tetikleyicili fonksiyon
import functions_framework
from google.cloud import storage
from PIL import Image
import io
import os
@functions_framework.cloud_event
def process_image(cloud_event):
data = cloud_event.data
bucket_name = data["bucket"]
file_name = data["name"]
content_type = data.get("contentType", "")
# Sadece görsel dosyaları işle
if not content_type.startswith("image/"):
print(f"Görsel değil, atlanıyor: {file_name}")
return
# Zaten işlenmiş dosyaları tekrar işleme
if file_name.startswith("thumbnails/"):
print(f"Zaten thumbnail, atlanıyor: {file_name}")
return
storage_client = storage.Client()
bucket = storage_client.bucket(bucket_name)
# Orijinal dosyayı indir
blob = bucket.blob(file_name)
image_data = blob.download_as_bytes()
# Thumbnail oluştur
image = Image.open(io.BytesIO(image_data))
image.thumbnail((200, 200), Image.LANCZOS)
# Thumbnail'i kaydet
output = io.BytesIO()
image_format = image.format or "JPEG"
image.save(output, format=image_format)
output.seek(0)
thumbnail_name = f"thumbnails/{file_name}"
thumbnail_blob = bucket.blob(thumbnail_name)
thumbnail_blob.upload_from_file(output, content_type=content_type)
print(f"Thumbnail oluşturuldu: {thumbnail_name}")
Bu fonksiyonu deploy etmek için:
# requirements.txt oluştur
cat > requirements.txt << EOF
functions-framework==3.*
google-cloud-storage==2.*
Pillow==10.*
EOF
# Fonksiyonu deploy et (2. nesil)
gcloud functions deploy process-image
--gen2
--runtime=python311
--region=europe-west1
--source=.
--entry-point=process_image
--trigger-event-filters="type=google.cloud.storage.object.v1.finalized"
--trigger-event-filters="bucket=benim-fotograf-bucket"
--memory=512MB
--timeout=60s
Burada google.cloud.storage.object.v1.finalized event tipi, dosya yüklemesi tamamlandığında tetiklenir. Diğer storage event tipleri şunlardır:
- google.cloud.storage.object.v1.deleted: Dosya silindi
- google.cloud.storage.object.v1.archived: Dosya arşivlendi
- google.cloud.storage.object.v1.metadataUpdated: Metadata güncellendi
Pub/Sub ile Asenkron Mesaj İşleme
Pub/Sub tetikleyiciler, mikroservis mimarilerinde servisler arası iletişim için mükemmel. Gerçek dünya senaryosu olarak, bir e-ticaret sisteminde sipariş verildiğinde farklı servislerin tetiklenmesini düşünelim.
# order_processor/main.py
import functions_framework
import base64
import json
from google.cloud import firestore
from datetime import datetime
@functions_framework.cloud_event
def process_order(cloud_event):
# Pub/Sub mesajı base64 encoded geliyor
message_data = base64.b64decode(
cloud_event.data["message"]["data"]
).decode("utf-8")
order = json.loads(message_data)
print(f"Yeni sipariş alındı: {order.get('order_id')}")
db = firestore.Client()
# Siparişi Firestore'a kaydet
order_ref = db.collection("orders").document(order["order_id"])
order_ref.set({
"order_id": order["order_id"],
"customer_id": order["customer_id"],
"items": order["items"],
"total": order["total"],
"status": "processing",
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
})
# Stok kontrolü için başka bir topic'e mesaj gönder
from google.cloud import pubsub_v1
publisher = pubsub_v1.PublisherClient()
project_id = "PROJE_ID"
topic_path = publisher.topic_path(project_id, "inventory-check")
inventory_message = json.dumps({
"order_id": order["order_id"],
"items": order["items"]
}).encode("utf-8")
future = publisher.publish(topic_path, inventory_message)
future.result()
print(f"Stok kontrolü tetiklendi: {order['order_id']}")
Bu fonksiyonu deploy edelim:
gcloud functions deploy process-order
--gen2
--runtime=python311
--region=europe-west1
--source=./order_processor
--entry-point=process_order
--trigger-topic=new-orders
--memory=256MB
--timeout=120s
--max-instances=50
--max-instances parametresi çok önemli. Pub/Sub topiclerinize ani mesaj yağmuru geldiğinde fonksiyonlarınız sonsuz sayıda instance oluşturmaya çalışabilir. Bu hem maliyeti patlatır hem de downstream servislere (örneğin veritabanı) yük bindirir. Üretim ortamında her zaman makul bir limit koyun.
Firestore Tetikleyicisi ile Gerçek Zamanlı Tepkiler
Firestore değişikliklerine tepki vermek, kullanıcı aktivite takibi veya audit log sistemi kurmak için çok kullanışlı. Şöyle bir senaryo düşünelim: Kullanıcılar profil bilgilerini güncellediğinde, eski ve yeni değerleri karşılaştırıp değişiklik geçmişi tutacağız.
# profile_audit/main.py
import functions_framework
from google.cloud import firestore
from datetime import datetime
@functions_framework.cloud_event
def audit_profile_changes(cloud_event):
# Firestore event verisi
old_value = cloud_event.data.get("oldValue", {})
new_value = cloud_event.data.get("value", {})
# Hangi belge değişti?
resource = cloud_event.subject
# Format: documents/users/{user_id}
parts = resource.split("/")
user_id = parts[-1]
old_fields = old_value.get("fields", {})
new_fields = new_value.get("fields", {})
changes = {}
# Değişen alanları bul
for field, new_val in new_fields.items():
old_val = old_fields.get(field)
if old_val != new_val:
changes[field] = {
"old": extract_firestore_value(old_val),
"new": extract_firestore_value(new_val)
}
if not changes:
print(f"Gerçek değişiklik yok: {user_id}")
return
# Audit kaydı oluştur
db = firestore.Client()
audit_ref = db.collection("audit_logs").document()
audit_ref.set({
"user_id": user_id,
"changes": changes,
"changed_at": datetime.utcnow(),
"event_type": "profile_update"
})
print(f"Audit kaydı oluşturuldu: {user_id}, değişiklikler: {list(changes.keys())}")
def extract_firestore_value(field_value):
if not field_value:
return None
# Firestore değer tiplerini çıkar
for value_type in ["stringValue", "integerValue", "booleanValue", "doubleValue"]:
if value_type in field_value:
return field_value[value_type]
return str(field_value)
Firestore tetikleyicili fonksiyonu deploy etmek 2. nesilde biraz farklı:
gcloud functions deploy audit-profile-changes
--gen2
--runtime=python311
--region=europe-west1
--source=./profile_audit
--entry-point=audit_profile_changes
--trigger-event-filters="type=google.cloud.firestore.document.v1.written"
--trigger-event-filters="database=(default)"
--trigger-event-filters-path-pattern="document=users/{userId}"
--memory=256MB
Hata Yönetimi ve Retry Mekanizması
Olay tabanlı fonksiyonlarda hata yönetimi kritik önem taşır. Pub/Sub ile çalışırken, fonksiyonunuz hata fırlatırsa mesaj yeniden gönderilir. Bu da idempotent (aynı işlemi birden fazla çalıştırmanın sonucu değiştirmemeli) fonksiyonlar yazmanızı zorunlu kılar.
# idempotent_processor/main.py
import functions_framework
import base64
import json
from google.cloud import firestore
from google.api_core import exceptions
@functions_framework.cloud_event
def idempotent_event_processor(cloud_event):
message_data = base64.b64decode(
cloud_event.data["message"]["data"]
).decode("utf-8")
event = json.loads(message_data)
event_id = event.get("event_id")
if not event_id:
# event_id yoksa işleme devam etme
print("HATA: event_id eksik, mesaj reddediliyor")
return # Exception fırlatma, retry istemiyoruz
db = firestore.Client()
# İşlenmiş mi kontrol et
processed_ref = db.collection("processed_events").document(event_id)
try:
# Transaction ile idempotency kontrolü
@firestore.transactional
def process_in_transaction(transaction, ref):
snapshot = ref.get(transaction=transaction)
if snapshot.exists:
print(f"Zaten işlendi, atlanıyor: {event_id}")
return False
# İşlenmiş olarak işaretle
transaction.set(ref, {
"processed_at": firestore.SERVER_TIMESTAMP,
"event_id": event_id
})
return True
transaction = db.transaction()
should_process = process_in_transaction(transaction, processed_ref)
if should_process:
# Asıl işlemi yap
perform_business_logic(db, event)
print(f"Başarıyla işlendi: {event_id}")
except exceptions.AlreadyExists:
print(f"Concurrent request, zaten işleniyor: {event_id}")
except Exception as e:
print(f"İşlem hatası: {e}")
raise # Bu sefer retry istiyoruz
def perform_business_logic(db, event):
# İş mantığı buraya
pass
Dead Letter Queue Yapılandırması
Sürekli hata veren mesajları sonsuz döngüde işlemeye çalışmak hem kaynak israfı hem de sistem sağlığını bozar. Pub/Sub’ın Dead Letter Topic özelliğini kullanarak başarısız mesajları ayrı bir yere gönderebilirsiniz:
# Dead letter topic oluştur
gcloud pubsub topics create orders-dead-letter
# Dead letter subscription oluştur
gcloud pubsub subscriptions create orders-dead-letter-sub
--topic=orders-dead-letter
# Ana subscription'ı dead letter ile güncelle
gcloud pubsub subscriptions modify-push-config orders-sub
--dead-letter-topic=orders-dead-letter
--max-delivery-attempts=5
# Pub/Sub servis hesabına gerekli izni ver
PROJECT_NUMBER=$(gcloud projects describe PROJE_ID --format='value(projectNumber)')
gcloud pubsub topics add-iam-policy-binding orders-dead-letter
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
--role="roles/pubsub.publisher"
gcloud pubsub subscriptions add-iam-policy-binding orders-sub
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
--role="roles/pubsub.subscriber"
Yerel Test Ortamı
Fonksiyonları her değişiklikte GCP’ye deploy etmek hem zaman alır hem de maliyetlidir. Functions Framework sayesinde yerel makinenizde test edebilirsiniz:
# Geliştirme ortamı kur
python -m venv venv
source venv/bin/activate
pip install functions-framework google-cloud-storage google-cloud-pubsub
# Fonksiyonu yerel başlat
functions-framework --target=process_order --signature-type=cloudevent --port=8080
Başka bir terminalde test mesajı gönder:
# Pub/Sub CloudEvent formatında test mesajı
curl -X POST http://localhost:8080
-H "Content-Type: application/json"
-H "ce-id: test-123"
-H "ce-specversion: 1.0"
-H "ce-type: google.cloud.pubsub.topic.v1.messagePublished"
-H "ce-source: //pubsub.googleapis.com/projects/test-project/topics/new-orders"
-d '{
"message": {
"data": "eyJvcmRlcl9pZCI6ICJPUkQtMDAxIiwgImN1c3RvbWVyX2lkIjogIlVTRVItMTIzIiwgIml0ZW1zIjogW3sicHJvZHVjdF9pZCI6ICJQUkQtMSIsICJxdWFudGl0eSI6IDJ9XSwgInRvdGFsIjogNTkuOTl9",
"messageId": "test-msg-001"
},
"subscription": "projects/test-project/subscriptions/test-sub"
}'
Base64 encoded data şunu decode eder: {"order_id": "ORD-001", "customer_id": "USER-123", "items": [{"product_id": "PRD-1", "quantity": 2}], "total": 59.99}
Monitoring ve Alerting
Production’da fonksiyonlarınızın sağlığını izlemek zorundasınız. Cloud Monitoring ile temel metrikler için alert kurabilirsiniz:
# Hata oranı için alert policy oluştur
gcloud alpha monitoring policies create
--notification-channels=KANAL_ID
--display-name="Cloud Function Hata Oranı Yüksek"
--condition-display-name="Error rate > 5%"
--condition-filter='resource.type="cloud_function" AND metric.type="cloudfunctions.googleapis.com/function/execution_count" AND metric.labels.status="error"'
--condition-threshold-value=0.05
--condition-threshold-comparison=COMPARISON_GT
--condition-duration=300s
# Fonksiyon loglarını görüntüle
gcloud functions logs read process-order
--gen2
--region=europe-west1
--limit=50
# Belirli zaman aralığındaki loglar
gcloud logging read
'resource.type="cloud_run_revision" AND resource.labels.function_name="process-order" AND severity>=ERROR'
--limit=100
--format="table(timestamp,textPayload)"
Fonksiyonlarınızda Python logging modülünü kullanın, basit print yerine:
import logging
import functions_framework
logger = logging.getLogger(__name__)
@functions_framework.cloud_event
def my_function(cloud_event):
logger.info("Fonksiyon başladı", extra={
"event_id": cloud_event.id,
"event_type": cloud_event.type
})
try:
# İş mantığı
logger.info("İşlem tamamlandı")
except ValueError as e:
logger.warning(f"Geçersiz veri: {e}")
return # Retry istemiyoruz
except Exception as e:
logger.error(f"Beklenmeyen hata: {e}", exc_info=True)
raise # Retry için exception fırlat
Maliyet Optimizasyonu
Cloud Functions her ne kadar serverless olsa da dikkatli kullanılmazsa fatura sürprizleri yapabilir. Birkaç pratik öneri:
- Minimum instance sayısını sıfır tutun geliştirme ortamlarında. Production’da cold start sorununu gidermek için 1-2 minimum instance mantıklı olabilir, ama her zaman maliyet analizi yapın.
- Memory boyutunu doğru ayarlayın. Varsayılan 256MB çoğu basit fonksiyon için yeterli. Pillow ile görsel işleme yapıyorsanız 512MB veya 1GB gerekebilir. Cloud Monitoring’den memory kullanımını izleyip optimize edin.
- Fonksiyon sürelerini kısaltın. Timeout’a yakın çalışan fonksiyonlar hem pahalıdır hem de muhtemelen yanlış bir şey yapıyordur. Uzun işlemleri Cloud Tasks veya Cloud Run’a devredin.
- Concurrency ayarını doğru yapın. 2. nesil fonksiyonlar Cloud Run üzerinde çalıştığından, tek bir instance birden fazla isteği aynı anda işleyebilir.
--concurrencyparametresini iş yüküne göre ayarlayın.
Sonuç
GCP Cloud Functions olay tabanlı fonksiyonlar, doğru kullanıldığında inanılmaz güçlü otomasyonlar kurmanızı sağlar. Storage’a düşen dosyayı anında işlemek, Pub/Sub üzerinden gevşek bağlı mikroservisler oluşturmak veya Firestore değişikliklerine tepki vermek, tüm bunları yönetilecek sunucu olmadan yapmak büyük bir avantaj.
Ama her güçlü araç gibi, dikkatli kullanılması gerekiyor. Idempotency, dead letter queue, maksimum instance limiti ve doğru hata yönetimi olmadan olay tabanlı sistemler kaos ortamına dönüşebilir. Özellikle Pub/Sub retry mekanizmasını iyi anlamadan üretim ortamına geçmek, aynı işlemin defalarca yapılmasına yol açabilir.
- nesil Cloud Functions’ı tercih edin, Eventarc entegrasyonu sayesinde çok daha geniş bir olay ekosistemine erişebiliyorsunuz. Yerel geliştirme için Functions Framework’ü kurun, her değişiklikte deploy yaparak zaman kaybetmeyin. Ve monitoring’i asla ihmal etmeyin, sessizce başarısız olan fonksiyonlar prodüksiyon sistemlerinde saatler boyunca fark edilmeyebilir.
