GCP Cloud Functions ile Cloud Storage Tetikleyici Kullanımı

Bir dosya yükleniyor, tetikleyici çalışıyor, işlem tamamlanıyor. Kulağa basit geliyor ama bu döngünün arkasında ciddi bir mimari karar var. GCP Cloud Functions ve Cloud Storage tetikleyicilerini bir araya getirdiğinizde, sunucu yönetmeden, otomatik ölçeklenen, olay güdümlü bir sistem kurmuş oluyorsunuz. Bu yazıda bu sistemi gerçek dünya senaryolarıyla, hem acemi hem deneyimli sysadmin’lerin anlayacağı bir dille ele alacağız.

Cloud Storage Tetikleyici Nedir ve Neden Kullanırız?

Cloud Storage’a bir nesne yüklendiğinde, silindiğinde ya da güncellendiğinde GCP bu olayı yakalar ve ilgili Cloud Function’ı tetikler. Yani siz bir şeyi manuel olarak başlatmak zorunda kalmazsınız. Sistem kendi kendine farkında olur.

Bu yaklaşımın gerçek hayatta ne işe yaradığına bakalım:

  • Görsel işleme pipeline’ları: Kullanıcı fotoğraf yüklüyor, function thumbnail üretiyor
  • Log analizi: Sunuculardan gelen log dosyaları bucket’a düşüyor, function parse edip BigQuery’ye yazıyor
  • Veri dönüştürme: CSV dosyası geldi, JSON’a çevir, başka bir bucket’a at
  • Güvenlik taraması: Yüklenen her dosyayı otomatik virüs taramasından geçir
  • Bildirim sistemleri: Kritik bir rapor bucket’a düştüğünde Slack’e mesaj gönder

Bunların hepsi için ayrı bir sunucu kaldırmak, cron job yazmak ya da uygulama içine gömülü kod koymak yerine, tek bir function yazıyorsunuz. Hem maliyet açısından hem de operasyonel yük açısından ciddi fark yaratıyor.

Ön Gereksinimler

Başlamadan önce şunların hazır olması gerekiyor:

  • GCP projesi oluşturulmuş ve faturalandırma aktif
  • gcloud CLI kurulu ve yapılandırılmış
  • Cloud Functions API ve Cloud Storage API etkin
  • Gerekli IAM izinleri mevcut

Önce gerekli API’leri etkinleştirelim:

gcloud services enable cloudfunctions.googleapis.com
gcloud services enable storage.googleapis.com
gcloud services enable cloudbuild.googleapis.com
gcloud services enable eventarc.googleapis.com

Ardından çalışacağımız bucket’ı oluşturalım:

# Kaynak bucket (dosyaların yükleneceği yer)
gcloud storage buckets create gs://my-upload-bucket-prod 
  --location=europe-west1 
  --uniform-bucket-level-access

# Hedef bucket (işlenmiş dosyaların gideceği yer)
gcloud storage buckets create gs://my-processed-bucket-prod 
  --location=europe-west1 
  --uniform-bucket-level-access

Cloud Functions 1. Nesil vs 2. Nesil

Bu noktada önemli bir seçim var. GCP’nin Cloud Functions’ın iki versiyonu mevcut ve aralarındaki fark operasyonel açıdan kritik.

1. Nesil (Gen 1):

  • Cloud Storage tetikleyicisi için doğrudan google.storage.object.finalize eventi kullanır
  • Daha az yapılandırma gerektirir
  • Maximum çalışma süresi 9 dakika
  • Legacy projelerde hâlâ yaygın

2. Nesil (Gen 2):

  • Eventarc üzerinden çalışır
  • Cloud Run altyapısını kullanır
  • Maximum çalışma süresi 60 dakika
  • Daha iyi gözlemlenebilirlik ve kontrol
  • Yeni projelerde tercih edilmeli

Bu yazıda her ikisini de göstereceğiz ama yeni kurulumlar için Gen 2’yi öneriyorum.

İlk Function: Dosya Yüklenince Log Tutan Basit Örnek

Önce işleri anlamak için basit bir şeyle başlayalım. Bir dosya bucket’a yüklendiğinde metadata’yı loglayan function:

# main.py
import functions_framework
import logging
from datetime import datetime

logging.basicConfig(level=logging.INFO)

@functions_framework.cloud_event
def process_storage_event(cloud_event):
    """Cloud Storage olaylarını işleyen function"""
    
    data = cloud_event.data
    
    bucket_name = data["bucket"]
    file_name = data["name"]
    content_type = data.get("contentType", "unknown")
    size = data.get("size", 0)
    event_type = cloud_event["type"]
    time_created = data.get("timeCreated", "")
    
    logging.info(f"Olay tipi: {event_type}")
    logging.info(f"Bucket: {bucket_name}")
    logging.info(f"Dosya: {file_name}")
    logging.info(f"İçerik türü: {content_type}")
    logging.info(f"Boyut: {size} bytes")
    logging.info(f"Oluşturulma zamanı: {time_created}")
    
    # Dosya boyutuna göre farklı işlem
    size_mb = int(size) / (1024 * 1024)
    if size_mb > 100:
        logging.warning(f"Büyük dosya tespit edildi: {size_mb:.2f} MB")
    
    return "OK"
# requirements.txt
functions-framework==3.*
google-cloud-storage==2.10.0

Bu function’ı deploy etmek için:

gcloud functions deploy process-storage-event 
  --gen2 
  --runtime=python311 
  --region=europe-west1 
  --source=. 
  --entry-point=process_storage_event 
  --trigger-event-filters="type=google.cloud.storage.object.v1.finalized" 
  --trigger-event-filters="bucket=my-upload-bucket-prod" 
  --trigger-location=europe-west1 
  --memory=256MB 
  --timeout=60s

Deploy tamamlandıktan sonra test edelim:

# Test dosyası yükle
echo "Merhaba GCP" > test.txt
gcloud storage cp test.txt gs://my-upload-bucket-prod/

# Logları kontrol et
gcloud functions logs read process-storage-event 
  --gen2 
  --region=europe-west1 
  --limit=20

Gerçek Dünya Senaryosu 1: Görsel Thumbnail Üretimi

Şimdi işe yarar bir şey yapalım. Kullanıcılar profil fotoğrafı yüklüyor ve sistemin otomatik olarak küçük boyutlu thumbnail oluşturması gerekiyor.

# main.py
import functions_framework
import logging
from google.cloud import storage
from PIL import Image
import io
import os

storage_client = storage.Client()
logging.basicConfig(level=logging.INFO)

THUMBNAIL_SIZES = {
    "small": (150, 150),
    "medium": (400, 400),
    "large": (800, 800)
}

SUPPORTED_FORMATS = ["image/jpeg", "image/png", "image/webp"]
PROCESSED_BUCKET = os.environ.get("PROCESSED_BUCKET", "my-processed-bucket-prod")

@functions_framework.cloud_event
def generate_thumbnails(cloud_event):
    data = cloud_event.data
    
    source_bucket_name = data["bucket"]
    file_name = data["name"]
    content_type = data.get("contentType", "")
    
    # Sadece desteklenen görsel formatlarını işle
    if content_type not in SUPPORTED_FORMATS:
        logging.info(f"Desteklenmeyen format atlandı: {content_type} - {file_name}")
        return "SKIPPED"
    
    # Zaten işlenmiş dosyaları tekrar işleme (sonsuz döngü önlemi)
    if file_name.startswith("thumbnails/"):
        logging.info(f"Thumbnail klasörü atlandı: {file_name}")
        return "SKIPPED"
    
    logging.info(f"Thumbnail üretimi başlıyor: {file_name}")
    
    try:
        # Kaynak dosyayı indir
        source_bucket = storage_client.bucket(source_bucket_name)
        blob = source_bucket.blob(file_name)
        image_data = blob.download_as_bytes()
        
        # Her boyut için thumbnail üret
        dest_bucket = storage_client.bucket(PROCESSED_BUCKET)
        
        with Image.open(io.BytesIO(image_data)) as img:
            # EXIF rotasyon sorununu düzelt
            img = fix_image_orientation(img)
            
            for size_name, dimensions in THUMBNAIL_SIZES.items():
                thumb_data = create_thumbnail(img, dimensions, content_type)
                
                # Dosya adını oluştur
                base_name = os.path.splitext(file_name)[0]
                extension = get_extension(content_type)
                thumb_name = f"thumbnails/{base_name}_{size_name}{extension}"
                
                # Bucket'a yükle
                thumb_blob = dest_bucket.blob(thumb_name)
                thumb_blob.upload_from_string(thumb_data, content_type=content_type)
                
                logging.info(f"Thumbnail oluşturuldu: {thumb_name}")
        
        return "SUCCESS"
        
    except Exception as e:
        logging.error(f"Thumbnail üretimi başarısız: {file_name} - {str(e)}")
        raise


def create_thumbnail(img, size, content_type):
    """Verilen boyutta thumbnail oluşturur"""
    thumb = img.copy()
    thumb.thumbnail(size, Image.Resampling.LANCZOS)
    
    buffer = io.BytesIO()
    
    if content_type == "image/jpeg":
        thumb.save(buffer, format="JPEG", quality=85, optimize=True)
    elif content_type == "image/png":
        thumb.save(buffer, format="PNG", optimize=True)
    elif content_type == "image/webp":
        thumb.save(buffer, format="WEBP", quality=85)
    
    return buffer.getvalue()


def fix_image_orientation(img):
    """EXIF verisiyle görüntü yönünü düzeltir"""
    try:
        exif = img._getexif()
        if exif and 274 in exif:  # 274 = Orientation tag
            orientation = exif[274]
            rotations = {3: 180, 6: 270, 8: 90}
            if orientation in rotations:
                img = img.rotate(rotations[orientation], expand=True)
    except (AttributeError, Exception):
        pass
    return img


def get_extension(content_type):
    """Content type'tan dosya uzantısı döner"""
    extensions = {
        "image/jpeg": ".jpg",
        "image/png": ".png",
        "image/webp": ".webp"
    }
    return extensions.get(content_type, ".jpg")

Bu function için requirements.txt:

# requirements.txt
functions-framework==3.*
google-cloud-storage==2.10.0
Pillow==10.0.0

Deploy komutu (environment variable ile):

gcloud functions deploy generate-thumbnails 
  --gen2 
  --runtime=python311 
  --region=europe-west1 
  --source=. 
  --entry-point=generate_thumbnails 
  --trigger-event-filters="type=google.cloud.storage.object.v1.finalized" 
  --trigger-event-filters="bucket=my-upload-bucket-prod" 
  --trigger-location=europe-west1 
  --memory=512MB 
  --timeout=120s 
  --set-env-vars="PROCESSED_BUCKET=my-processed-bucket-prod" 
  --service-account=thumbnail-sa@PROJECT_ID.iam.gserviceaccount.com

IAM ve Service Account Yapılandırması

Production ortamında function’ınız için ayrı bir service account kullanmak şart. Default compute service account’u kullanmak güvenlik açısından riskli.

# Service account oluştur
gcloud iam service-accounts create thumbnail-sa 
  --display-name="Thumbnail Generator Service Account" 
  --project=YOUR_PROJECT_ID

# Kaynak bucket'tan okuma izni ver
gcloud storage buckets add-iam-policy-binding gs://my-upload-bucket-prod 
  --member="serviceAccount:thumbnail-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" 
  --role="roles/storage.objectViewer"

# Hedef bucket'a yazma izni ver
gcloud storage buckets add-iam-policy-binding gs://my-processed-bucket-prod 
  --member="serviceAccount:thumbnail-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" 
  --role="roles/storage.objectCreator"

# Eventarc için gerekli izin (Gen 2 için)
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID 
  --member="serviceAccount:thumbnail-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" 
  --role="roles/eventarc.eventReceiver"

Gerçek Dünya Senaryosu 2: CSV’den BigQuery’ye Otomatik Yükleme

Log dosyaları ya da raporlar CSV formatında bucket’a düştüğünde, bunları otomatik BigQuery’ye yükleyen bir function:

# main.py
import functions_framework
import logging
import os
from google.cloud import bigquery, storage

bq_client = bigquery.Client()
storage_client = storage.Client()
logging.basicConfig(level=logging.INFO)

PROJECT_ID = os.environ.get("GCP_PROJECT", "")
DATASET_ID = os.environ.get("BQ_DATASET", "raw_data")
TABLE_PREFIX = os.environ.get("TABLE_PREFIX", "imported")

@functions_framework.cloud_event
def csv_to_bigquery(cloud_event):
    data = cloud_event.data
    
    bucket_name = data["bucket"]
    file_name = data["name"]
    content_type = data.get("contentType", "")
    
    # Sadece CSV dosyalarını işle
    if not file_name.endswith(".csv"):
        logging.info(f"CSV değil, atlanıyor: {file_name}")
        return "SKIPPED"
    
    gcs_uri = f"gs://{bucket_name}/{file_name}"
    
    # Tablo adını dosya adından türet
    base_name = os.path.splitext(os.path.basename(file_name))[0]
    # Özel karakterleri temizle
    table_name = f"{TABLE_PREFIX}_{base_name}".replace("-", "_").replace(" ", "_")
    
    table_id = f"{PROJECT_ID}.{DATASET_ID}.{table_name}"
    
    logging.info(f"BigQuery yükleme başlıyor: {gcs_uri} -> {table_id}")
    
    try:
        job_config = bigquery.LoadJobConfig(
            source_format=bigquery.SourceFormat.CSV,
            skip_leading_rows=1,  # Header satırını atla
            autodetect=True,       # Schema otomatik tespit
            write_disposition=bigquery.WriteDisposition.WRITE_APPEND,
            max_bad_records=10,    # 10 hatalı satıra kadar tolerans
            encoding="UTF-8",
        )
        
        load_job = bq_client.load_table_from_uri(
            gcs_uri,
            table_id,
            job_config=job_config,
        )
        
        # İş tamamlanana kadar bekle
        load_job.result()
        
        # Yüklenen satır sayısını al
        table = bq_client.get_table(table_id)
        logging.info(f"Yükleme tamamlandı. Toplam satır: {table.num_rows}")
        
        return "SUCCESS"
        
    except Exception as e:
        logging.error(f"BigQuery yükleme hatası: {str(e)}")
        raise

Tetikleyici Olayları ve Event Tipleri

Cloud Storage tetikleyicileri için mevcut event tipleri şunlar:

  • google.cloud.storage.object.v1.finalized: Dosya yükleme tamamlandı (en yaygın kullanılan)
  • google.cloud.storage.object.v1.deleted: Dosya silindi
  • google.cloud.storage.object.v1.archived: Dosyanın eski versiyonu arşivlendi (versiyonlama açıksa)
  • google.cloud.storage.object.v1.metadataUpdated: Metadata güncellendi

Silme olayı için ayrı bir function örneği:

# Silme olayı için ayrı function deploy et
gcloud functions deploy handle-file-deletion 
  --gen2 
  --runtime=python311 
  --region=europe-west1 
  --source=. 
  --entry-point=handle_deletion 
  --trigger-event-filters="type=google.cloud.storage.object.v1.deleted" 
  --trigger-event-filters="bucket=my-upload-bucket-prod" 
  --trigger-location=europe-west1 
  --memory=256MB 
  --timeout=30s

Hata Yönetimi ve Dead Letter Queue

Production’da hataları yönetmek kritik. Eventarc aracılığıyla dead letter topic yapılandırmak için:

# Dead letter topic oluştur
gcloud pubsub topics create storage-function-dead-letter

# Dead letter subscription oluştur
gcloud pubsub subscriptions create storage-function-dlq 
  --topic=storage-function-dead-letter 
  --ack-deadline=600 
  --message-retention-duration=7d

# Function'ı retry politikasıyla deploy et
gcloud functions deploy generate-thumbnails 
  --gen2 
  --runtime=python311 
  --region=europe-west1 
  --source=. 
  --entry-point=generate_thumbnails 
  --trigger-event-filters="type=google.cloud.storage.object.v1.finalized" 
  --trigger-event-filters="bucket=my-upload-bucket-prod" 
  --trigger-location=europe-west1 
  --memory=512MB 
  --timeout=120s 
  --retry

Function kodu içinde idempotency sağlamak da önemli. Aynı event birden fazla kez gelebilir:

# Idempotency için işaretleme mekanizması
def is_already_processed(file_name, dest_bucket):
    """Dosyanın daha önce işlenip işlenmediğini kontrol eder"""
    marker_blob = dest_bucket.blob(f".processed/{file_name}.done")
    return marker_blob.exists()

def mark_as_processed(file_name, dest_bucket):
    """Dosyayı işlenmiş olarak işaretle"""
    marker_blob = dest_bucket.blob(f".processed/{file_name}.done")
    marker_blob.upload_from_string("", content_type="text/plain")

Monitoring ve Alerting

Function’larınızı kör uçmaması için mutlaka izleme kurmalısınız:

# Hata oranı için alert policy oluştur
gcloud monitoring alert-policies create 
  --display-name="Cloud Function Hata Oranı" 
  --condition-display-name="Function execution errors" 
  --condition-filter='resource.type="cloud_function" AND metric.type="cloudfunctions.googleapis.com/function/execution_count" AND metric.labels.status="error"' 
  --condition-threshold-value=5 
  --condition-threshold-duration=300s 
  --notification-channels=YOUR_NOTIFICATION_CHANNEL_ID

# Son çalışma loglarını izle
gcloud functions logs read generate-thumbnails 
  --gen2 
  --region=europe-west1 
  --limit=50 
  --format="table(time_utc,severity,log)"

Terraform ile Infrastructure as Code

Tüm bu yapıyı elle kurmak yerine Terraform ile yönetmek uzun vadede çok daha sürdürülebilir:

# terraform/main.tf içeriği için gerekli komutlar

# Önce provider'ı başlat
terraform init

# Plan oluştur
terraform plan 
  -var="project_id=your-project-id" 
  -var="region=europe-west1" 
  -out=tfplan

# Uygula
terraform apply tfplan

Terraform HCL örneği:

# Bu blokları main.tf dosyasına yazın
# resource "google_storage_bucket" "upload_bucket" bloğu için:
gcloud storage buckets describe gs://my-upload-bucket-prod --format=json

# Mevcut function'ların state'ini import et
terraform import google_cloudfunctions2_function.thumbnail_processor 
  projects/YOUR_PROJECT/locations/europe-west1/functions/generate-thumbnails

Yaygın Hatalar ve Çözümleri

Gerçek projelerde karşılaştığım sorunları ve çözümlerini paylaşayım:

Sonsuz döngü problemi: Function işlediği dosyayı aynı bucket’a yazarsa kendi kendini tekrar tetikler. Çözüm: Çıktıyı farklı bucket’a veya farklı prefix’e yaz, her zaman kontrol ekle.

Timeout sorunları: Büyük dosyalar için default timeout yetersiz kalır. Gen 2 ile 60 dakikaya kadar çıkabilirsiniz ama bu bir tasarım sorununun işareti de olabilir. Büyük dosyaları chunk’lara bölerek işlemeyi düşünün.

Cold start gecikmesi: Function uzun süre çağrılmadığında ilk başlatmada gecikme olur. Kritik workload’lar için minimum instance sayısını 1 olarak ayarlayın:

gcloud functions deploy generate-thumbnails 
  --gen2 
  --min-instances=1 
  --max-instances=10 
  ...diğer parametreler...

Bellek yetersizliği: Görsel işleme gibi memory-intensive işlemler için başlangıçta 256MB yetmez. 512MB veya 1GB ile başlayın, Cloud Monitoring’den gerçek kullanımı izleyin.

Concurrent execution çakışması: Aynı anda birden fazla dosya gelirse function paralel çalışır. Bu genellikle istenen durumdur ama shared state kullanıyorsanız dikkatli olun.

Maliyet Optimizasyonu

Serverless’ın güzel yanı ödediğiniz miktar kullanımla doğru orantılı. Ama optimizasyon yapmazsanız sürprizler olabilir:

  • Memory ayarını doğru yap: Gereğinden fazla memory tahsis etmek hem maliyeti artırır hem de cold start süresini uzatır
  • Timeout’ı gerçekçi ayarla: Maksimum değer koymak yerine gerçek sürenin 2-3 katını kullan
  • Büyük dosyalar için Dataflow düşün: 1GB üzeri dosyalar için Cloud Functions yerine Dataflow daha verimli
  • Concurrency limitini ayarla: Gen 2’de bir instance birden fazla isteği eş zamanlı işleyebilir, bu maliyeti düşürür
# Gen 2'de concurrency ayarı
gcloud functions deploy generate-thumbnails 
  --gen2 
  --concurrency=5 
  ...diğer parametreler...

Sonuç

GCP Cloud Functions ve Cloud Storage tetikleyicilerini bir araya getirmek, olay güdümlü veri işleme pipeline’ları kurmak için son derece güçlü bir kombinasyon. Sunucu yönetimi yok, otomatik ölçekleme var, yalnızca kullandığınız kadar ödüyorsunuz.

Bu yazıda öğrendiklerimizi özetleyelim:

  • Gen 2 yeni projeler için her zaman tercih edilmeli, daha uzun timeout ve daha iyi gözlemlenebilirlik sunuyor
  • Service account yapılandırması asla atlanmamalı, güvenlik production’da hayati önem taşıyor
  • Sonsuz döngü problemine karşı her zaman koruma mekanizması eklenmeli
  • Idempotency sağlamak retry senaryolarında veri tutarlılığını koruyor
  • Monitoring olmadan production’a gönderilen hiçbir function güvenli değil

Gerçek projelerde bu yapıyı kullanırken en büyük kazancı “ops yükünün azalması” olarak görüyorum. Ekibiniz altyapı yönetmek yerine gerçek iş mantığına odaklanabiliyor. Bu da uzun vadede hem hız hem kalite açısından ciddi fark yaratıyor.

Sonraki adım olarak bu function’ları CI/CD pipeline’ınıza entegre etmeyi, Cloud Build ile otomatik deploy kurmayı düşünebilirsiniz. O konuyu da ayrı bir yazıda ele alabiliriz.

Bir yanıt yazın

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