Webhook Versiyonlama ve Geriye Dönük Uyumluluk Yönetimi

Üretim ortamında bir webhook entegrasyonu çalışırken payload yapısını değiştirmek zorunda kaldığınızda ne hissediyorsunuz? Eğer bu konuda planlı bir versiyonlama stratejiniz yoksa, muhtemelen “şimdi hangi servisi kırdım acaba?” diye düşünüyorsunuzdur. Webhook versiyonlama, çoğu ekibin API geliştirme sürecinde en son düşündüğü ama en çok baş ağrıtan konuların başında gelir. Bu yazıda gerçek dünya senaryolarıyla webhook versiyonlama stratejilerini, geriye dönük uyumluluğu nasıl koruyacağınızı ve migration süreçlerini nasıl yöneteceğinizi ele alacağız.

Webhook Versiyonlamanın Temel Mantığı

Bir webhook esasen sizin sisteminizin dışarıya “hey, şu olay gerçekleşti, işte veriler” diye bağırdığı bir HTTP POST isteğidir. Sorun şu ki bu bağırdığınız yeri dinleyen onlarca, belki yüzlerce farklı istemci sistemi olabilir. Siz payload yapısını değiştirdiğinizde, bu sistemlerin tamamı etkilenir.

Versiyonlama stratejisi temelde üç soruya cevap vermelidir:

  • Hangi versiyonu hangi istemci kullanıyor?
  • Eski ve yeni versiyonlar paralel çalışabilecek mi?
  • Bir versiyonu ne zaman emekliye ayıracaksınız?

Bu soruları cevaplamadan başladığınız her webhook implementasyonu, ilerleyen dönemde teknik borç olarak karşınıza çıkar.

Versiyonlama Yaklaşımları

URL Tabanlı Versiyonlama

En yaygın ve en anlaşılır yöntemdir. Webhook endpoint’inize istek atarken URL içinde versiyon bilgisi taşırsınız.

# v1 endpoint
POST https://api.yourservice.com/webhooks/v1/orders

# v2 endpoint
POST https://api.yourservice.com/webhooks/v2/orders

Nginx tarafında bu trafiği yönetmek için basit bir upstream konfigürasyonu yazabilirsiniz:

# /etc/nginx/sites-available/webhook-routing.conf

upstream webhook_v1 {
    server 127.0.0.1:8081;
}

upstream webhook_v2 {
    server 127.0.0.1:8082;
}

server {
    listen 443 ssl;
    server_name api.yourservice.com;

    location /webhooks/v1/ {
        proxy_pass http://webhook_v1;
        proxy_set_header X-Webhook-Version "1";
        proxy_set_header X-Forwarded-For $remote_addr;
    }

    location /webhooks/v2/ {
        proxy_pass http://webhook_v2;
        proxy_set_header X-Webhook-Version "2";
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

Header Tabanlı Versiyonlama

Bazı ekipler URL’yi temiz tutmak için versiyon bilgisini HTTP header’larında taşımayı tercih eder. Bu yaklaşım daha esnektir ama istemci tarafında daha fazla dikkat gerektirir.

# Header tabanlı versiyon bilgisi gönderimi
curl -X POST https://api.yourservice.com/webhooks/orders 
  -H "Content-Type: application/json" 
  -H "X-Webhook-Version: 2024-01-15" 
  -H "X-API-Key: your-secret-key" 
  -d '{"event": "order.created", "data": {...}}'

Tarih tabanlı versiyonlama (Stripe’ın kullandığı yöntem) özellikle breaking change’lerin ne zaman yapıldığını takip etmek için çok kullanışlıdır. “2024-01-15 sonrası” gibi ifadeler hem teknik ekip hem de müşteriler için anlamlıdır.

Payload İçi Versiyonlama

Her iki yöntemi de beğenmiyorsanız versiyon bilgisini payload’ın içine gömmek de mümkündür:

# Payload içi versiyonlama örneği
cat > sample_payload.json << 'EOF'
{
  "version": "2",
  "schema_version": "2024-01-15",
  "event_type": "order.completed",
  "event_id": "evt_8f9a2b3c4d5e",
  "timestamp": "2024-01-15T14:30:00Z",
  "data": {
    "order_id": "ord_12345",
    "customer": {
      "id": "cust_67890",
      "email": "[email protected]"
    },
    "total_amount": 299.99,
    "currency": "TRY"
  }
}
EOF

Bu yaklaşımın dezavantajı, payload’ı parse etmeden versiyonu bilemezsiniz. Yani routing kararını middleware seviyesinde veremezsiniz.

Geriye Dönük Uyumluluk Stratejileri

Additive Changes: Güvenli Değişiklikler

Geriye dönük uyumluluğu korumanın en basit yolu, sadece ekleyici değişiklikler yapmaktır. Mevcut alanlara dokunmadan yeni alanlar eklemek genellikle güvenlidir.

# v1 payload - mevcut istemciler bunu bekliyor
{
  "order_id": "ord_12345",
  "status": "completed",
  "amount": 299.99
}

# v1.1 - geriye dönük uyumlu ekleme (breaking change değil)
{
  "order_id": "ord_12345",
  "status": "completed",
  "amount": 299.99,
  "currency": "TRY",        # YENİ - eklendi
  "completed_at": "2024-01-15T14:30:00Z"  # YENİ - eklendi
}

İstemciler bilinmeyen alanları ignore edebilecek şekilde yazılmışsa, bu değişiklikler onları kırmaz. Ancak bunu hiçbir zaman garanti edemezsiniz, bu yüzden dokümantasyonunuzda “istemciler bilinmeyen alanları tolere etmelidir” kuralını açıkça belirtmeniz gerekir.

Transformation Layer ile Versiyon Köprüsü

Gerçek dünyada en sık kullandığım pattern, bir transformation layer oluşturmaktır. Yeni bir payload formatınız var ama eski istemcileri kırmak istemiyorsunuz. Bu durumda bir middleware yazıp eski formata dönüştürüyorsunuz.

#!/bin/bash
# webhook_transformer.sh
# Yeni format payload'ı eski formata dönüştürür

transform_v2_to_v1() {
    local payload="$1"
    
    # jq ile payload dönüşümü
    echo "$payload" | jq '{
        order_id: .data.order_id,
        status: .data.status,
        amount: .data.payment.total,
        customer_email: .data.customer.email
    }'
}

# Gelen payload'ı oku
incoming_payload=$(cat)
version=$(echo "$incoming_payload" | jq -r '.version')

case "$version" in
    "2")
        transformed=$(transform_v2_to_v1 "$incoming_payload")
        echo "$transformed"
        ;;
    "1")
        echo "$incoming_payload"
        ;;
    *)
        echo '{"error": "Unsupported version"}' >&2
        exit 1
        ;;
esac

Feature Flag ile Kademeli Geçiş

Tüm istemcileri aynı anda geçirmek yerine, feature flag sistemi kurarak kademeli migration yapabilirsiniz. Bu yaklaşım özellikle büyük müşteri tabanlarında çok işe yarar.

#!/bin/bash
# webhook_dispatcher.sh
# İstemci bazında versiyon kontrolü yapar

REDIS_CLI="redis-cli -h localhost -p 6379"

get_client_version() {
    local client_id="$1"
    local version
    
    # Redis'ten istemci konfigürasyonunu çek
    version=$($REDIS_CLI GET "webhook:client:${client_id}:version")
    
    # Varsayılan versiyon v1
    echo "${version:-1}"
}

dispatch_webhook() {
    local client_id="$1"
    local event_data="$2"
    local client_version
    
    client_version=$(get_client_version "$client_id")
    
    case "$client_version" in
        "2")
            endpoint=$($REDIS_CLI GET "webhook:client:${client_id}:endpoint_v2")
            payload=$(format_v2_payload "$event_data")
            ;;
        "1"|*)
            endpoint=$($REDIS_CLI GET "webhook:client:${client_id}:endpoint_v1")
            payload=$(format_v1_payload "$event_data")
            ;;
    esac
    
    # Webhook'u gönder
    curl -s -X POST "$endpoint" 
        -H "Content-Type: application/json" 
        -H "X-Webhook-Version: ${client_version}" 
        -d "$payload"
}

# Versiyon güncelleme örneği
# $REDIS_CLI SET "webhook:client:cust_123:version" "2"

Deprecation Süreci ve İletişim

Sunset Header Kullanımı

IETF RFC 8594 ile standartlaşan Sunset header’ı, bir endpoint’in ne zaman kapatılacağını bildirmek için idealdir.

# Nginx'te deprecated endpoint için Sunset header ekleme
location /webhooks/v1/ {
    proxy_pass http://webhook_v1;
    
    # Deprecation bildirimi header'ları
    add_header Sunset "Sat, 31 Dec 2024 23:59:59 GMT" always;
    add_header Deprecation "true" always;
    add_header Link '<https://docs.yourservice.com/webhooks/migration-v2>; rel="deprecation"' always;
    
    # Log seviyesini artır - deprecated endpoint kullanımını izle
    access_log /var/log/nginx/webhook_v1_deprecated.log combined;
}

Bu header’ları alan istemciler, uygun altyapıya sahip olduklarında uyarı alabilir ve migration’ı planlayabilirler.

Deprecation Takibi ve Raporlama

Hangi istemcinin hala eski versiyonu kullandığını bilmeden doğru bir geçiş planı yapamazsınız:

#!/bin/bash
# webhook_deprecation_report.sh
# Deprecated endpoint kullanan istemcileri raporlar

LOG_FILE="/var/log/nginx/webhook_v1_deprecated.log"
REPORT_DATE=$(date +%Y-%m-%d)

echo "=== Webhook V1 Deprecation Report - $REPORT_DATE ==="
echo ""
echo "Son 24 saatte v1 endpoint kullanan IP adresleri:"
echo "------------------------------------------------"

# Son 24 saatin loglarını filtrele ve istatistik çıkar
awk -v date="$(date +%d/%b/%Y)" '$4 ~ date {print $1}' "$LOG_FILE" | 
    sort | 
    uniq -c | 
    sort -rn | 
    while read count ip; do
        # IP'ye karşılık gelen istemci adını bul
        client_name=$(redis-cli GET "webhook:ip:${ip}:client_name" 2>/dev/null || echo "Unknown")
        echo "  Istek Sayisi: $count | IP: $ip | Istemci: $client_name"
    done

echo ""
echo "Toplam v1 istegi: $(wc -l < "$LOG_FILE" | tr -d ' ')"
echo "Migration durumu icin: https://docs.yourservice.com/migration"

Gerçek Dünya Senaryosu: E-ticaret Platformu Migration’ı

Diyelim ki bir e-ticaret platformu işletiyorsunuz ve sipariş webhook’larınızı v1’den v2’ye geçirmeniz gerekiyor. v1’de flat bir yapı varken, v2’de nested objeler kullanmaya başlıyorsunuz.

#!/bin/bash
# migration_test.sh
# V1 ve V2 formatlarını test eder

V1_PAYLOAD='{
  "order_id": "ORD-2024-001",
  "customer_name": "Ahmet Yilmaz",
  "customer_email": "[email protected]",
  "total": 450.00,
  "status": "shipped",
  "shipping_address": "Kadikoy, Istanbul"
}'

V2_PAYLOAD='{
  "version": "2",
  "event": "order.shipped",
  "order": {
    "id": "ORD-2024-001",
    "status": "shipped",
    "total": {
      "amount": 450.00,
      "currency": "TRY",
      "formatted": "450,00 TL"
    }
  },
  "customer": {
    "name": "Ahmet Yilmaz",
    "email": "[email protected]",
    "phone": "+90-555-1234567"
  },
  "shipping": {
    "address": {
      "district": "Kadikoy",
      "city": "Istanbul",
      "country": "TR"
    },
    "carrier": "Yurtici Kargo",
    "tracking_code": "YK123456789"
  }
}'

# Test fonksiyonu
test_webhook_endpoint() {
    local version="$1"
    local payload="$2"
    local endpoint="$3"
    
    echo "Testing v${version} endpoint..."
    response=$(curl -s -w "n%{http_code}" -X POST "$endpoint" 
        -H "Content-Type: application/json" 
        -H "X-Webhook-Version: ${version}" 
        -d "$payload")
    
    http_code=$(echo "$response" | tail -1)
    body=$(echo "$response" | head -1)
    
    if [ "$http_code" -eq 200 ]; then
        echo "  BASARILI: HTTP $http_code"
    else
        echo "  HATA: HTTP $http_code - $body"
    fi
}

# Her iki versiyonu da test et
test_webhook_endpoint "1" "$V1_PAYLOAD" "http://localhost:8081/webhooks/v1/orders"
test_webhook_endpoint "2" "$V2_PAYLOAD" "http://localhost:8082/webhooks/v2/orders"

Versiyonlar Arası Signature Doğrulama

Versiyon geçişlerinde sık atlanan bir konu da HMAC signature doğrulamasının versiyon bağımlı olabileceğidir. Her versiyon için farklı bir imzalama şeması kullanıyorsanız bunu açıkça yönetmeniz gerekir.

#!/bin/bash
# webhook_signature_verify.sh
# Versiyon bazinda HMAC dogrulama

verify_webhook_signature() {
    local payload="$1"
    local received_signature="$2"
    local webhook_version="$3"
    local secret_key="$4"
    
    case "$webhook_version" in
        "1")
            # V1: SHA1 kullaniyor (eski, guvenli degil)
            expected_sig="sha1=$(echo -n "$payload" | 
                openssl dgst -sha1 -hmac "$secret_key" | 
                awk '{print $2}')"
            algorithm="SHA1"
            ;;
        "2")
            # V2: SHA256 kullaniyor (onerilen)
            expected_sig="sha256=$(echo -n "$payload" | 
                openssl dgst -sha256 -hmac "$secret_key" | 
                awk '{print $2}')"
            algorithm="SHA256"
            ;;
        *)
            echo "ERROR: Bilinmeyen webhook versiyonu: $webhook_version"
            return 1
            ;;
    esac
    
    if [ "$received_signature" = "$expected_sig" ]; then
        echo "OK: $algorithm imzasi dogrulandi"
        return 0
    else
        echo "ERROR: Imza dogrulama basarisiz (Versiyon: v$webhook_version, Algoritma: $algorithm)"
        echo "  Beklenen: $expected_sig"
        echo "  Alinan:   $received_signature"
        return 1
    fi
}

# Kullanim ornegi:
# verify_webhook_signature "$PAYLOAD" "$HTTP_X_SIGNATURE" "$HTTP_X_WEBHOOK_VERSION" "$SECRET"

Monitoring ve Alerting

Versiyonlama stratejiniz ne kadar iyi olursa olsun, gerçek zamanlı izleme olmadan körle oynarsınız. Prometheus ile webhook metriklerini toplamak için basit bir bash exporter yazabilirsiniz:

#!/bin/bash
# webhook_metrics_exporter.sh
# Prometheus text format ile metrikleri disa aktar

METRICS_FILE="/var/lib/prometheus/webhook_metrics.prom"
LOG_DIR="/var/log/nginx"

collect_webhook_metrics() {
    local timestamp=$(date +%s)
    
    # Her versiyon icin istek sayilari
    for version in v1 v2; do
        log_file="${LOG_DIR}/webhook_${version}_access.log"
        
        if [ -f "$log_file" ]; then
            # Son 60 saniyedeki istek sayisi
            count=$(awk -v ts="$(date -d '60 seconds ago' '+%d/%b/%Y:%H:%M:%S')" 
                '$4 > "["ts' "$log_file" | wc -l)
            
            # HTTP status kodlarina gore dagilim
            success=$(grep -c " 200 " "$log_file" 2>/dev/null || echo 0)
            errors=$(grep -c " 5[0-9][0-9] " "$log_file" 2>/dev/null || echo 0)
            
            echo "webhook_requests_total{version="${version}"} $count $timestamp"
            echo "webhook_success_total{version="${version}"} $success $timestamp"
            echo "webhook_error_total{version="${version}"} $errors $timestamp"
        fi
    done
    
    # Deprecated endpoint kullanimi - kritik metrik
    deprecated_count=$(wc -l < "${LOG_DIR}/webhook_v1_deprecated.log" 2>/dev/null || echo 0)
    echo "webhook_deprecated_endpoint_total{version="v1"} $deprecated_count $timestamp"
}

# Metrikleri dosyaya yaz
collect_webhook_metrics > "$METRICS_FILE"

echo "Metrikler guncellendi: $METRICS_FILE"

Migration Checklist ve Timeline Yönetimi

Başarılı bir versiyon migration’ı için standart bir timeline şu şekilde işler:

  • Ay 0: Yeni versiyon duyurusu, dokümantasyon yayını, sandbox ortamında test imkanı sunulması
  • Ay 1-2: Erken benimseyiciler için v2’ye geçiş desteği, geri bildirim toplama
  • Ay 3: Tüm yeni kayıtların otomatik olarak v2’ye yönlendirilmesi
  • Ay 4: v1 üzerinde Sunset header aktive edilmesi, deprecation e-postaları
  • Ay 6: v1 read-only moda alınması (sadece GET, POST’lar reddedilir)
  • Ay 9: v1 tamamen kapatılması

Bu timeline’ı işlemek için bir cron job kurabilirsiniz:

#!/bin/bash
# deprecation_lifecycle.sh
# Webhook versiyon lifecycle yonetimi

DEPRECATION_CONFIG="/etc/webhook/deprecation.conf"
CURRENT_DATE=$(date +%Y%m%d)

check_and_enforce_lifecycle() {
    while IFS='=' read -r key value; do
        case "$key" in
            "sunset_date")
                sunset_ts=$(date -d "$value" +%s 2>/dev/null)
                current_ts=$(date +%s)
                
                if [ "$current_ts" -gt "$sunset_ts" ]; then
                    echo "KRITIK: v1 sunset tarihi gecti! Endpoint kapatilmali."
                    # Nginx konfigurasyonunu guncelle
                    sed -i 's/proxy_pass http://webhook_v1/return 410/' 
                        /etc/nginx/sites-available/webhook-routing.conf
                    nginx -t && nginx -s reload
                    echo "v1 endpoint devre disi birakildi - $(date)"
                fi
                ;;
            "readonly_date")
                readonly_ts=$(date -d "$value" +%s 2>/dev/null)
                current_ts=$(date +%s)
                
                if [ "$current_ts" -gt "$readonly_ts" ]; then
                    echo "UYARI: v1 read-only modda olmali"
                    # POST isteklerini reddet
                    sed -i '/location /webhooks/v1/a    limit_except GET { return 405; }' 
                        /etc/nginx/sites-available/webhook-routing.conf
                    nginx -t && nginx -s reload
                fi
                ;;
        esac
    done < "$DEPRECATION_CONFIG"
}

check_and_enforce_lifecycle

Sık Yapılan Hatalar

Yıllar içinde gördüğüm en yaygın webhook versiyonlama hatalarını şöyle listeleyebilirim:

  • Versiyon 1’e “son versiyon” gözüyle bakmak: İlk günden itibaren versiyonlama düşünerek tasarlamak, sonradan eklemeye çalışmaktan çok daha kolaydır.
  • İstemcilerle iletişim kurmadan sunset tarihini değiştirmek: Bir kez duyurduğunuz tarihi geri almak mümkündür ama öne almak ciddi güven sorunu yaratır.
  • Transformation layer’ı test etmemek: v2 payload’ını v1’e çevirirken veri kaybı yaşanabilir. Her alan için unit test yazın.
  • Signature doğrulamasını versiyondan bağımsız tutmayı unutmak: Versiyon geçişinde imzalama algoritmasını da değiştirdiyseniz, bunu açıkça belgeleyin.
  • Monitoring kurmadan geçiş yapmak: Hangi istemcinin ne zaman geçtiğini bilmeden, sunset tarihini güvenle belirleyemezsiniz.

Sonuç

Webhook versiyonlama, küçük bir uygulama ayrıntısı değil, API’nizin uzun vadeli yaşam döngüsünü belirleyen kritik bir tasarım kararıdır. Doğru yapıldığında istemcileriniz değişikliklerden habersiz bir şekilde çalışmaya devam eder, yanlış yapıldığında ise bir sabah üretim ortamında alarm yağmuruna tutulursunuz.

Pratik tavsiyem şu: Projenizin en başından URL tabanlı versiyonlama ile başlayın, istemci konfigürasyonlarını bir key-value store’da tutun, Sunset header’larını erkenden aktive edin ve her şeyden önce neyin production’a çarptığını izleyin. Monitoring olmadan yaptığınız tüm stratejiler kağıt üzerinde kalır.

Migration süreçlerinde de acele etmeyin. İstemcilerinize gerçekten yeterli süre tanıyın, proaktif iletişim kurun ve geçiş yaparken el tutun. Teknik mükemmellik kadar bu insan boyutu da önemlidir. Sonuçta webhook entegrasyonunuzu kullanan geliştiriciler de sizin gibi biri, ve sabah 3’te deprecated endpoint hatası almak onların da isteyeceği bir şey değildir.

Bir yanıt yazın

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