GCP Cloud Functions ile Serverless Fonksiyon Oluşturma

Sunucu yönetmek güzel bir şeydir; ta ki o sunucunun bakımını, güncellemelerini, yamalarını ve ölçeklendirmesini düşünmeye başlayana kadar. İşte tam bu noktada serverless mimarisi hayat kurtarıcı oluyor. Google Cloud Functions, altyapıyla uğraşmadan kod yazıp çalıştırmanı sağlayan, GCP’nin en güçlü serverless çözümlerinden biri. Bu yazıda Cloud Functions’ı sıfırdan kuracağız, gerçek dünya senaryolarıyla pekiştireceğiz ve production ortamında dikkat etmen gereken her şeyi ele alacağız.

Cloud Functions Nedir ve Ne Zaman Kullanmalısın?

Cloud Functions, olay güdümlü (event-driven) bir serverless platformdur. Bir HTTP isteği geldiğinde, bir Pub/Sub mesajı yayınlandığında, Cloud Storage’a dosya yüklendiğinde ya da Firebase olayları tetiklendiğinde senin fonksiyonun çalışır, işini yapar ve durur. Arkada çalışan bir VM yok, yönetmen gereken bir container yok, ölçeklendirme için uğraşman gereken bir config yok.

Ne zaman kullanmalısın?

  • Webhook alıcıları yazmak istediğinde (Stripe, GitHub, Slack gibi servislerin callback’leri)
  • Periyodik görevler için cron job alternatifleri oluştururken
  • Mikroservis mimarisinde küçük, bağımsız iş parçaları tanımlarken
  • ETL pipeline’larında veri dönüştürme adımları için
  • API Gateway arkasında basit backend mantığı çalıştırmak istediğinde

Ne zaman kullanmamalısın?

  • Uzun süren işlemler için (Cloud Functions maksimum 60 dakika timeout’a sahip, ama bu bile ideal değil uzun işler için)
  • Kalıcı durum tutman gereken senaryolarda
  • Yüksek frekanslı, düşük latency gerektiren workload’larda (cold start problemi var)

Ortam Hazırlığı ve gcloud CLI Kurulumu

Başlamadan önce gcloud CLI’nin kurulu ve konfigüre edilmiş olması gerekiyor.

# gcloud CLI kurulumu (Debian/Ubuntu)
curl https://sdk.cloud.google.com | bash
exec -l $SHELL

# Kimlik doğrulama
gcloud auth login

# Proje seçimi
gcloud config set project YOUR_PROJECT_ID

# Gerekli API'lerin aktifleştirilmesi
gcloud services enable cloudfunctions.googleapis.com
gcloud services enable cloudbuild.googleapis.com
gcloud services enable artifactregistry.googleapis.com

# Mevcut konfigürasyonu doğrula
gcloud config list

Cloud Functions 2. nesli (Gen 2) artık Google’ın önerdiği versiyon. Gen 2, Cloud Run altyapısı üzerine inşa edildiği için çok daha güçlü. Bu yazıda ağırlıklı olarak Gen 2 üzerinde çalışacağız.

İlk Cloud Function: Basit HTTP Trigger

En temel senaryodan başlayalım. Bir HTTP isteği alıp JSON dönen basit bir fonksiyon.

# Proje dizinini oluştur
mkdir -p ~/gcp-functions/hello-http
cd ~/gcp-functions/hello-http

main.py dosyasını oluştur:

cat > main.py << 'EOF'
import functions_framework
import json
from datetime import datetime

@functions_framework.http
def hello_http(request):
    """HTTP Cloud Function."""
    
    # CORS başlıkları (gerekirse)
    if request.method == 'OPTIONS':
        headers = {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'GET, POST',
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Max-Age': '3600'
        }
        return ('', 204, headers)
    
    headers = {'Access-Control-Allow-Origin': '*'}
    
    # Request body parse et
    request_json = request.get_json(silent=True)
    request_args = request.args
    
    if request_json and 'name' in request_json:
        name = request_json['name']
    elif request_args and 'name' in request_args:
        name = request_args['name']
    else:
        name = 'Misafir'
    
    response_data = {
        'message': f'Merhaba, {name}!',
        'timestamp': datetime.utcnow().isoformat(),
        'status': 'success'
    }
    
    return (json.dumps(response_data, ensure_ascii=False), 200, headers)
EOF

requirements.txt dosyasını oluştur:

cat > requirements.txt << 'EOF'
functions-framework==3.*
EOF

Şimdi fonksiyonu deploy edelim:

gcloud functions deploy hello-http 
    --gen2 
    --runtime=python311 
    --region=europe-west1 
    --source=. 
    --entry-point=hello_http 
    --trigger-http 
    --allow-unauthenticated 
    --memory=256MB 
    --timeout=30s 
    --min-instances=0 
    --max-instances=10

Deploy sonrası fonksiyonu test edelim:

# Fonksiyonun URL'ini al
FUNCTION_URL=$(gcloud functions describe hello-http 
    --gen2 
    --region=europe-west1 
    --format='value(serviceConfig.uri)')

echo "Function URL: $FUNCTION_URL"

# GET isteği ile test
curl -s "$FUNCTION_URL?name=Ahmet" | python3 -m json.tool

# POST isteği ile test
curl -s -X POST 
    -H "Content-Type: application/json" 
    -d '{"name": "Mehmet"}' 
    "$FUNCTION_URL" | python3 -m json.tool

Pub/Sub Trigger ile Asenkron Fonksiyon

Gerçek dünyada çoğu zaman event-driven mimariye ihtiyaç duyarsın. Örneğin bir sipariş geldiğinde e-posta göndermek, bir dosya yüklendiğinde işlemek gibi senaryolar. Pub/Sub trigger bunun için biçilmiş kaftan.

# Önce Pub/Sub topic oluştur
gcloud pubsub topics create siparis-olaylari

# Fonksiyon dizini
mkdir -p ~/gcp-functions/siparis-isleme
cd ~/gcp-functions/siparis-isleme

main.py:

cat > main.py << 'EOF'
import functions_framework
import base64
import json
import logging

# Logging konfigürasyonu
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@functions_framework.cloud_event
def siparis_isle(cloud_event):
    """Pub/Sub mesajı geldiğinde sipariş işleme fonksiyonu."""
    
    # Pub/Sub mesajını decode et
    pubsub_message = base64.b64decode(
        cloud_event.data["message"]["data"]
    ).decode('utf-8')
    
    try:
        siparis = json.loads(pubsub_message)
        logger.info(f"Sipariş alındı: {siparis.get('siparis_id', 'bilinmiyor')}")
        
        # Sipariş doğrulama
        gerekli_alanlar = ['siparis_id', 'musteri_id', 'urunler', 'toplam']
        for alan in gerekli_alanlar:
            if alan not in siparis:
                logger.error(f"Eksik alan: {alan}")
                return
        
        # İş mantığı buraya gelir
        # Örneğin: veritabanına kaydet, e-posta gönder, vs.
        logger.info(
            f"Sipariş işlendi - ID: {siparis['siparis_id']}, "
            f"Musteri: {siparis['musteri_id']}, "
            f"Toplam: {siparis['toplam']} TL"
        )
        
        # Başarılı işlem bildirimi (örn: başka bir topic'e yayınla)
        logger.info("Sipariş başarıyla işlendi.")
        
    except json.JSONDecodeError as e:
        logger.error(f"JSON parse hatası: {e}")
    except Exception as e:
        logger.error(f"Beklenmeyen hata: {e}")
        raise  # Retry mekanizması için exception'ı yeniden fırlat
EOF

cat > requirements.txt << 'EOF'
functions-framework==3.*
EOF

Deploy:

gcloud functions deploy siparis-isleme 
    --gen2 
    --runtime=python311 
    --region=europe-west1 
    --source=. 
    --entry-point=siparis_isle 
    --trigger-topic=siparis-olaylari 
    --memory=512MB 
    --timeout=120s 
    --retry

Test için Pub/Sub’a mesaj gönder:

# Test mesajı gönder
gcloud pubsub topics publish siparis-olaylari 
    --message='{"siparis_id": "ORD-001", "musteri_id": "USR-123", "urunler": ["kitap", "kalem"], "toplam": 150.50}'

# Log'ları kontrol et
gcloud functions logs read siparis-isleme 
    --gen2 
    --region=europe-west1 
    --limit=20

Cloud Storage Trigger: Görsel İşleme Senaryosu

Kullanıcılar profil fotoğrafı yüklediğinde otomatik olarak thumbnail oluşturan bir sistem düşün. Bu tam anlamıyla Cloud Functions’ın parladığı bir senaryo.

# Storage bucket oluştur
gsutil mb -l europe-west1 gs://PROJE_ADI-fotograflar
gsutil mb -l europe-west1 gs://PROJE_ADI-thumbnail

mkdir -p ~/gcp-functions/gorsel-isle
cd ~/gcp-functions/gorsel-isle

main.py:

cat > main.py << 'EOF'
import functions_framework
from google.cloud import storage
from PIL import Image
import io
import logging

logger = logging.getLogger(__name__)
storage_client = storage.Client()

THUMBNAIL_BOYUT = (200, 200)
THUMBNAIL_BUCKET = "PROJE_ADI-thumbnail"

@functions_framework.cloud_event
def gorsel_isle(cloud_event):
    """Yeni yüklenen görseller için thumbnail oluştur."""
    
    data = cloud_event.data
    bucket_adi = data["bucket"]
    dosya_adi = data["name"]
    
    # Sadece görsel dosyaları işle
    izin_verilen_uzantilar = ['.jpg', '.jpeg', '.png', '.webp']
    if not any(dosya_adi.lower().endswith(ext) for ext in izin_verilen_uzantilar):
        logger.info(f"Görsel değil, atlanıyor: {dosya_adi}")
        return
    
    # Thumbnail klasörüne düşen dosyaları tekrar işleme
    if dosya_adi.startswith('thumb_'):
        logger.info("Thumbnail dosyası, döngü önlendi.")
        return
    
    logger.info(f"İşleniyor: gs://{bucket_adi}/{dosya_adi}")
    
    try:
        # Orijinal dosyayı indir
        kaynak_bucket = storage_client.bucket(bucket_adi)
        blob = kaynak_bucket.blob(dosya_adi)
        gorsel_veri = blob.download_as_bytes()
        
        # Thumbnail oluştur
        gorsel = Image.open(io.BytesIO(gorsel_veri))
        gorsel.thumbnail(THUMBNAIL_BOYUT, Image.Resampling.LANCZOS)
        
        # Thumbnail'i byte'a çevir
        cikti = io.BytesIO()
        format_adi = gorsel.format if gorsel.format else 'JPEG'
        gorsel.save(cikti, format=format_adi, optimize=True, quality=85)
        cikti.seek(0)
        
        # Thumbnail'i hedef bucket'a yükle
        hedef_bucket = storage_client.bucket(THUMBNAIL_BUCKET)
        thumb_adi = f"thumb_{dosya_adi}"
        hedef_blob = hedef_bucket.blob(thumb_adi)
        hedef_blob.upload_from_file(cikti, content_type=blob.content_type)
        
        logger.info(f"Thumbnail oluşturuldu: gs://{THUMBNAIL_BUCKET}/{thumb_adi}")
        
    except Exception as e:
        logger.error(f"Hata: {dosya_adi} - {e}")
        raise
EOF

cat > requirements.txt << 'EOF'
functions-framework==3.*
google-cloud-storage==2.*
Pillow==10.*
EOF

Deploy:

gcloud functions deploy gorsel-isle 
    --gen2 
    --runtime=python311 
    --region=europe-west1 
    --source=. 
    --entry-point=gorsel_isle 
    --trigger-event-filters="type=google.cloud.storage.object.v1.finalized" 
    --trigger-event-filters="bucket=PROJE_ADI-fotograflar" 
    --memory=1GB 
    --timeout=120s

Environment Variables ve Secret Manager Kullanımı

Production ortamında API anahtarlarını, veritabanı şifrelerini doğrudan kod içine yazmak büyük bir güvenlik açığı. Secret Manager kullanmak şart.

# Secret Manager API'yi aktifleştir
gcloud services enable secretmanager.googleapis.com

# Secret oluştur
echo -n "super-gizli-api-anahtari-123" | 
    gcloud secrets create EXTERNAL_API_KEY 
    --replication-policy="automatic" 
    --data-file=-

# Service account'a secret erişim izni ver
SA_EMAIL=$(gcloud functions describe hello-http 
    --gen2 
    --region=europe-west1 
    --format='value(serviceConfig.serviceAccountEmail)')

gcloud secrets add-iam-policy-binding EXTERNAL_API_KEY 
    --member="serviceAccount:$SA_EMAIL" 
    --role="roles/secretmanager.secretAccessor"

Secret Manager kullanan fonksiyon:

cat > main.py << 'EOF'
import functions_framework
from google.cloud import secretmanager
import os
import json

def secret_al(secret_adi, project_id=None):
    """Secret Manager'dan secret değeri al."""
    if not project_id:
        project_id = os.environ.get('GOOGLE_CLOUD_PROJECT')
    
    client = secretmanager.SecretManagerServiceClient()
    secret_yol = f"projects/{project_id}/secrets/{secret_adi}/versions/latest"
    
    response = client.access_secret_version(request={"name": secret_yol})
    return response.payload.data.decode("UTF-8")

# Cold start'ta bir kez yükle (performans optimizasyonu)
_api_key = None

def get_api_key():
    global _api_key
    if _api_key is None:
        _api_key = secret_al("EXTERNAL_API_KEY")
    return _api_key

@functions_framework.http
def guvenli_api(request):
    """Secret Manager'dan alınan key ile dış API çağrısı."""
    try:
        api_key = get_api_key()
        
        # Environment variable'dan da değer okuma örneği
        ortam = os.environ.get('ENVIRONMENT', 'development')
        
        return json.dumps({
            'status': 'success',
            'environment': ortam,
            'api_key_length': len(api_key)  # Key'i asla dışarı verme!
        }), 200
        
    except Exception as e:
        return json.dumps({'error': str(e)}), 500
EOF

cat > requirements.txt << 'EOF'
functions-framework==3.*
google-cloud-secret-manager==2.*
EOF

Deploy ederken environment variable eklemek:

gcloud functions deploy guvenli-api 
    --gen2 
    --runtime=python311 
    --region=europe-west1 
    --source=. 
    --entry-point=guvenli_api 
    --trigger-http 
    --allow-unauthenticated 
    --set-env-vars="ENVIRONMENT=production" 
    --memory=256MB

Lokal Geliştirme ve Test

Production’a her değişikliği deploy etmek hem zaman kaybı hem de gereksiz maliyet. Lokal test ortamı kurmak hayat kurtarır.

# functions-framework kur
pip install functions-framework

# Lokal sunucu başlat
cd ~/gcp-functions/hello-http
functions-framework --target=hello_http --port=8080 --debug

# Başka terminal'de test et
curl -X POST 
    -H "Content-Type: application/json" 
    -d '{"name": "Test Kullanicisi"}' 
    http://localhost:8080

Unit test yazımı:

cat > test_main.py << 'EOF'
import unittest
from unittest.mock import MagicMock
import json
import sys
sys.path.insert(0, '.')

from main import hello_http

class TestHelloHttp(unittest.TestCase):
    
    def _mock_request(self, json_data=None, args=None, method='POST'):
        mock_req = MagicMock()
        mock_req.method = method
        mock_req.get_json = MagicMock(return_value=json_data)
        mock_req.args = args or {}
        return mock_req
    
    def test_json_body_ile_istek(self):
        request = self._mock_request(json_data={'name': 'Ahmet'})
        response, status_code, _ = hello_http(request)
        data = json.loads(response)
        
        self.assertEqual(status_code, 200)
        self.assertIn('Ahmet', data['message'])
    
    def test_isimsiz_istek(self):
        request = self._mock_request(json_data={})
        response, status_code, _ = hello_http(request)
        data = json.loads(response)
        
        self.assertEqual(status_code, 200)
        self.assertIn('Misafir', data['message'])
    
    def test_options_cors(self):
        request = self._mock_request(method='OPTIONS')
        result = hello_http(request)
        
        self.assertEqual(result[1], 204)

if __name__ == '__main__':
    unittest.main()
EOF

# Testleri çalıştır
python -m pytest test_main.py -v

Monitoring, Logging ve Alerting

Production’da görmezden gelinen ama kritik olan kısım bu. Fonksiyonun çalışıyor olması yeterli değil, sağlıklı çalışıyor olması lazım.

# Son 1 saatin loglarını getir
gcloud functions logs read hello-http 
    --gen2 
    --region=europe-west1 
    --limit=50 
    --format='table(time_utc, severity, log)'

# Hata loglarını filtrele
gcloud logging read 
    'resource.type="cloud_run_revision" AND severity>=ERROR' 
    --freshness=1h 
    --format='value(textPayload)' 
    --limit=20

# Fonksiyon metriklerini görüntüle
gcloud monitoring metrics list 
    --filter="metric.type:cloudfunctions"

Alert policy oluşturmak için:

# Hata oranı için alert (gcloud ile basit versiyon)
gcloud alpha monitoring policies create 
    --notification-channels="CHANNEL_ID" 
    --display-name="Cloud Function Hata Alarmı" 
    --condition-display-name="Hata oranı yüksek" 
    --condition-filter='resource.type="cloud_function" AND metric.type="cloudfunctions.googleapis.com/function/execution_count" AND metric.labels.status="error"' 
    --condition-threshold-value=10 
    --condition-threshold-comparison=COMPARISON_GT 
    --condition-duration=60s

Maliyet Optimizasyonu ve Best Practice’ler

Cloud Functions kulağa ücretsizmiş gibi gelir ama dikkat etmezsen fatura sürpriz yapabilir.

Min/Max instance ayarları:

  • –min-instances=0: Sıfır maliyet ama cold start yaşarsın
  • –min-instances=1: Cold start yok ama sürekli bir instance çalışıyor
  • –max-instances=100: Kontrolsüz ölçeklemeyi engeller, maliyet patlamasını önler

Cold start süresini azaltmak için:

  • Küresel değişkenleri fonksiyon dışında tanımla (connection pooling, client başlatma)
  • Gereksiz kütüphane import’larından kaçın
  • requirements.txt‘i minimumda tut

Memory ve CPU ilişkisi: Gen 2’de CPU, memory ile doğru orantılı artar. 256MB bellek az CPU anlamına gelir. CPU-intensive işler için 1GB veya 2GB tercih et.

# Mevcut fonksiyonların maliyetini tahmin etmek için
gcloud functions list 
    --gen2 
    --regions=europe-west1 
    --format='table(name, serviceConfig.availableMemory, serviceConfig.minInstanceCount, serviceConfig.maxInstanceCount)'

# Kullanılmayan fonksiyonları temizle
gcloud functions delete eski-fonksiyon 
    --gen2 
    --region=europe-west1 
    --quiet

Güvenlik best practice’leri:

  • –no-allow-unauthenticated: Herkese açık olmayan fonksiyonlar için mutlaka kullan
  • IAM tabanlı erişim: Servisten servise çağrılarda service account kullan
  • VPC Connector: Private kaynaklara erişim için fonksiyonu VPC’ye bağla
  • Secret Manager: Tüm gizli bilgileri environment variable yerine Secret Manager’da sakla
  • Minimum yetki prensibini uygula, service account’lara sadece gerekli rolleri ver
# Authenticated endpoint test (token ile)
TOKEN=$(gcloud auth print-identity-token)
curl -H "Authorization: Bearer $TOKEN" https://FUNCTION_URL

CI/CD Pipeline ile Otomatik Deploy

Manuel deploy yapmak hata riski yaratır. GitHub Actions ile otomatik pipeline kurmak standart pratik olmalı.

cat > .github/workflows/deploy.yml << 'EOF'
name: Deploy Cloud Function

on:
  push:
    branches: [main]
    paths:
      - 'functions/hello-http/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Google Auth
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_CREDENTIALS }}
      
      - name: Setup gcloud
        uses: google-github-actions/setup-gcloud@v2
      
      - name: Run Tests
        run: |
          cd functions/hello-http
          pip install -r requirements.txt
          python -m pytest test_main.py -v
      
      - name: Deploy Function
        run: |
          gcloud functions deploy hello-http 
            --gen2 
            --runtime=python311 
            --region=europe-west1 
            --source=functions/hello-http 
            --entry-point=hello_http 
            --trigger-http 
            --memory=256MB 
            --timeout=30s 
            --set-env-vars="ENVIRONMENT=production" 
            --quiet
EOF

Sonuç

Cloud Functions, doğru kullanıldığında inanılmaz güçlü bir araç. Sunucu yönetiminden tamamen kurtuluyorsun, sadece iş mantığına odaklanıyorsun. Bu yazıda HTTP trigger’dan Pub/Sub’a, Storage event’lerinden güvenlik pratiklerine kadar geniş bir yelpazede konuları ele aldık.

Gözden kaçırmaman gereken ana noktaları toparlayalım:

  • Gen 2’yi tercih et: Gen 1 hala çalışıyor ama Gen 2 çok daha iyi performans ve Cloud Run altyapısının tüm avantajlarını sunuyor
  • Lokal test şart: Her değişikliği production’a pushlama, functions-framework ile lokal test et
  • Secret Manager kullan: Environment variable’lara gizli bilgi koyma, bir gün log’lara sızar
  • Min/Max instance’ı düşün: Sıfır maliyet mi yoksa sıfır cold start mı, kullanım durumuna göre karar ver
  • Log’ları takip et: Cloud Logging ve Monitoring olmadan kör uçuyorsun
  • CI/CD kur: Manuel deploy hata doğurur, pipeline yatırımı karşılığını hızlı verir

İlk fonksiyonu deploy ettiğinde “bu kadar mı kolaydı” diyeceksin. Ondan sonrası zaten çığ gibi büyüyor. Bir sonraki adım olarak Cloud Workflows ile birden fazla fonksiyonu orkestre etmeyi veya Cloud Scheduler ile zamanlanmış görevler oluşturmayı inceleyebilirsin.

Bir yanıt yazın

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