API Versiyonlama: Eski ve Yeni Sürümü Birlikte Yönetme

Bir API’yi production’a aldıktan sonra geriye dönüp bakınca şunu fark ediyorsun: o ilk tasarım kararları seni ya kurtarıyor ya da batırıyor. Özellikle versiyonlama konusunda baştan doğru bir strateji belirlememek, ilerleyen süreçte hem geliştirici ekibini hem de API’yi kullanan müşterileri mutsuz eden bir kaosa dönüşüyor. Bu yazıda gerçek dünya senaryoları üzerinden API versiyonlamanın nasıl yapılması gerektiğini, eski ve yeni sürümlerin birlikte nasıl yönetileceğini ve bu süreçte sysadmin olarak karşılaşacağın pratik sorunları ele alacağım.

API Versiyonlamanın Temelleri

API versiyonlama, temel olarak bir API’nin farklı sürümlerini birbirinden izole ederek her ikisini de aktif tutma pratiğidir. Bunu neden yapıyoruz? Çünkü bir API’yi tüketen uygulamalar çeşitli hızlarda güncelleniyor. Büyük bir enterprise müşterinin sistemine bağlı olan API client’ı güncelleme onayı almak aylar sürebilir. Bu sürede sen yeni özellikler geliştirip API’yi değiştiriyorsun. İşte bu noktada versiyonlama devreye giriyor.

Üç temel versiyonlama stratejisi var:

  • URL Path Versiyonlama: /api/v1/users ve /api/v2/users gibi endpoint’lerde versiyon numarasını taşıma
  • Header Versiyonlama: Accept: application/vnd.myapi.v2+json şeklinde HTTP header’ında versiyon belirtme
  • Query Parameter Versiyonlama: /api/users?version=2 gibi parametre üzerinden versiyon yönetimi

Pratikte URL path versiyonlama en yaygın ve en anlaşılır yöntem. Cache mekanizmaları, proxy’ler ve log analizi açısından da en temiz çözüm bu. Header versiyonlama teoride daha “RESTful” ama operasyonel açıdan baş ağrısına dönüşüyor. Query parameter ise genellikle kötü pratik olarak kabul ediliyor.

Nginx ile Multi-Version Routing

Diyelim ki bir Python/Flask uygulaması çalıştırıyorsun ve v1 portunu 5001, v2 portunu 5002 olarak ayarladın. Nginx üzerinden bu iki versiyonu aynı anda servis etmek için şöyle bir konfigürasyon işe yarıyor:

# /etc/nginx/conf.d/api.conf

upstream api_v1 {
    server 127.0.0.1:5001;
    keepalive 32;
}

upstream api_v2 {
    server 127.0.0.1:5002;
    keepalive 32;
}

server {
    listen 80;
    server_name api.example.com;

    # v1 trafiği
    location /api/v1/ {
        proxy_pass http://api_v1/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-API-Version "v1";
        add_header X-Served-By "api-v1";
    }

    # v2 trafiği
    location /api/v2/ {
        proxy_pass http://api_v2/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-API-Version "v2";
        add_header X-Served-By "api-v2";
    }

    # Versiyonsuz istekleri v2'ye yönlendir
    location /api/ {
        return 301 /api/v2$request_uri;
    }
}

Bu konfigürasyonu uyguladıktan sonra nginx -t ile syntax kontrolü yapıp systemctl reload nginx ile yüklüyorsun. Dikkat etmen gereken şey proxy_pass direktifindeki trailing slash. Eğer upstream api_v1/ şeklinde yazarsan path stripping gerçekleşiyor, yazmazsan tam path geçiyor. Bu ikisi arasındaki farkı karıştırmak saatlerce debugging yapmana yol açabilir.

Docker Compose ile Version İzolasyonu

Production ortamında her API versiyonunu ayrı bir container’da çalıştırmak en temiz yaklaşım. İşte gerçek bir projeden uyarladığım docker-compose yapısı:

# docker-compose.yml
version: '3.8'

services:
  api-v1:
    image: mycompany/api:1.9.2
    container_name: api_v1
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
      - API_VERSION=v1
    ports:
      - "5001:5000"
    networks:
      - api-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  api-v2:
    image: mycompany/api:2.1.0
    container_name: api_v2
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
      - API_VERSION=v2
    ports:
      - "5002:5000"
    networks:
      - api-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:15
    container_name: api_postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - api-network

  redis:
    image: redis:7-alpine
    container_name: api_redis
    networks:
      - api-network

networks:
  api-network:
    driver: bridge

volumes:
  postgres_data:

Burada dikkat etmen gereken kritik nokta: her iki API versiyonunun aynı veritabanını paylaşıyor olması. Bu migration stratejisini çok önemli bir hale getiriyor. V1 ve V2 aynı tablolara erişiyorsa, V2’de yapacağın schema değişikliklerinin V1’i kırmaması gerekiyor.

Database Migration Stratejisi

Bu konuda en çok görülen hata şu: V2 geliştirirken veritabanı şemasını kırıcı değişikliklerle güncellemek. Bir müşteri projesinde yaşadığım gerçek bir senaryoyu anlatayım. users tablosuna full_name kolonu eklenip first_name ve last_name kolonları kaldırıldığında V1 API’yi kullanan tüm uygulamalar patladı. Çözüm için birkaç gün uğraşıldı.

Doğru yaklaşım şu şekilde olmalı:

# Migration adım 1: Yeni kolonu ekle, eskilerini TUTMAYA devam et
psql -U apiuser -d apidb << 'EOF'
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

-- Mevcut veriyi yeni kolona taşı
UPDATE users SET full_name = first_name || ' ' || last_name;

-- Trigger ile senkronizasyonu sağla
CREATE OR REPLACE FUNCTION sync_user_names()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
        IF NEW.full_name IS NOT NULL AND (NEW.first_name IS NULL OR NEW.last_name IS NULL) THEN
            NEW.first_name := split_part(NEW.full_name, ' ', 1);
            NEW.last_name := split_part(NEW.full_name, ' ', 2);
        END IF;
        IF (NEW.first_name IS NOT NULL OR NEW.last_name IS NOT NULL) AND NEW.full_name IS NULL THEN
            NEW.full_name := COALESCE(NEW.first_name, '') || ' ' || COALESCE(NEW.last_name, '');
        END IF;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER user_names_sync
    BEFORE INSERT OR UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION sync_user_names();
EOF

echo "Migration tamamlandi. V1 ve V2 ayni anda calisabilir."

Bu yaklaşımla V1 first_name ve last_name‘i kullanmaya devam ederken V2 full_name ile çalışıyor. Eski kolonları ancak V1 trafiği tamamen sıfırlandıktan sonra kaldırıyorsun.

Deprecation Header’ları ve Sunset Politikası

Bir API versiyonunu kullanmaya devam eden müşterileri bunu fark etmeleri için uyarman gerekiyor. RFC 8594 standartlaştırdığı Sunset header bunu yapmak için ideal bir yol sunuyor. Nginx’te bu header’ı kolayca ekleyebilirsin:

# V1 için deprecation header'ları ekle
location /api/v1/ {
    proxy_pass http://api_v1/;
    proxy_set_header Host $host;
    
    # Deprecation uyarıları
    add_header Deprecation "true" always;
    add_header Sunset "Sat, 31 Dec 2024 23:59:59 GMT" always;
    add_header Link '</api/v2/>; rel="successor-version"' always;
    add_header Warning '299 - "Bu API versiyonu 31 Aralik 2024 tarihinde kapatilacak"' always;
}

Aynı zamanda uygulama tarafında da bu header’ları ekleyebilirsin. Flask örneği:

# Python/Flask middleware örneği
# Bu scripti API V1 uygulamanın wsgi.py dosyasına entegre et

cat > /opt/api/v1/deprecation_middleware.py << 'EOF'
from datetime import datetime
from functools import wraps
from flask import request, g
import logging

SUNSET_DATE = "2024-12-31T23:59:59+00:00"
SUCCESSOR_URL = "https://api.example.com/api/v2/"

def deprecation_middleware(app):
    @app.after_request
    def add_deprecation_headers(response):
        response.headers['Deprecation'] = 'true'
        response.headers['Sunset'] = SUNSET_DATE
        response.headers['Link'] = f'<{SUCCESSOR_URL}>; rel="successor-version"'
        
        # Sunset'e kaç gün kaldığını hesapla
        sunset = datetime.fromisoformat(SUNSET_DATE)
        days_left = (sunset - datetime.now(sunset.tzinfo)).days
        
        if days_left < 30:
            response.headers['Warning'] = f'299 - "KRITIK: API v1 kapatilmasina {days_left} gun kaldi!"'
            logging.warning(f"V1 API kullanimi: {request.path} - {days_left} gun kaldi")
        
        return response
    return app
EOF

echo "Deprecation middleware hazir."

Traffic Monitoring ve Analiz

Hangi versiyonun ne kadar kullanıldığını takip etmek, deprecation planını gerçekçi yapmak açısından kritik. Bunun için log analizi yapman gerekiyor:

#!/bin/bash
# api_version_stats.sh
# Günlük API versiyon kullanım istatistiği

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

echo "=== API Versiyon Kullanim Istatistigi - $DATE ==="
echo ""

echo "V1 Endpoint Kullanimi:"
grep "GET|POST|PUT|DELETE|PATCH" "$LOG_FILE" | 
    grep "/api/v1/" | 
    awk '{print $7}' | 
    sort | uniq -c | sort -rn | head -20

echo ""
echo "V2 Endpoint Kullanimi:"
grep "GET|POST|PUT|DELETE|PATCH" "$LOG_FILE" | 
    grep "/api/v2/" | 
    awk '{print $7}' | 
    sort | uniq -c | sort -rn | head -20

echo ""
echo "Toplam Istek Sayilari:"
V1_TOTAL=$(grep -c "/api/v1/" "$LOG_FILE" 2>/dev/null || echo "0")
V2_TOTAL=$(grep -c "/api/v2/" "$LOG_FILE" 2>/dev/null || echo "0")
TOTAL=$((V1_TOTAL + V2_TOTAL))

echo "- V1 toplam istek: $V1_TOTAL"
echo "- V2 toplam istek: $V2_TOTAL"
echo "- Toplam: $TOTAL"

if [ "$TOTAL" -gt "0" ]; then
    V1_PERCENT=$(echo "scale=1; $V1_TOTAL * 100 / $TOTAL" | bc)
    echo "- V1 trafik yuzdesi: %$V1_PERCENT"
fi

echo ""
echo "V1 Kullanan Unique IP'ler:"
grep "/api/v1/" "$LOG_FILE" | 
    awk '{print $1}' | 
    sort -u | 
    wc -l

echo ""
echo "V1'i hala kullanan IP listesi (iletisime gecilmesi gerekenler):"
grep "/api/v1/" "$LOG_FILE" | 
    awk '{print $1}' | 
    sort -u | head -10

Bu script’i cron job olarak çalıştırıp sonuçları e-posta ile gönderebilir ya da bir monitoring dashboard’una besleyebilirsin:

# Crontab girisi
0 8 * * * /opt/scripts/api_version_stats.sh | mail -s "Gunluk API Versiyon Raporu" [email protected]

Graceful Shutdown ile V1 Kapatma Süreci

Bir versiyonu kapatmak belki de sürecin en kritik adımı. Panik yaratmadan, müşteri ilişkilerini bozmadan bunu nasıl yapıyorsun?

İlk olarak kademeli bir throttling stratejisi uyguluyorsun. V1’e gelen istekleri yavaş yavaş kısıtlayarak müşterileri geçişe zorluyorsun:

#!/bin/bash
# v1_throttle.sh
# V1 API'ye rate limiting uygula

# Nginx rate limiting konfigürasyonunu guncelle
cat > /etc/nginx/conf.d/api_rate_limit.conf << 'NGINX'
# V1 icin agresif rate limiting
limit_req_zone $binary_remote_addr zone=api_v1_limit:10m rate=10r/m;

server {
    listen 80;
    server_name api.example.com;

    location /api/v1/ {
        limit_req zone=api_v1_limit burst=5 nodelay;
        limit_req_status 429;
        
        # 429 dondugunuzde migration linkini goster
        error_page 429 @v1_rate_limit_exceeded;
        
        proxy_pass http://api_v1/;
        proxy_set_header Host $host;
        add_header Deprecation "true" always;
        add_header Sunset "2024-12-31T23:59:59+00:00" always;
    }

    location @v1_rate_limit_exceeded {
        add_header Content-Type application/json always;
        add_header Retry-After 60 always;
        return 429 '{"error": "rate_limit_exceeded", "message": "V1 API kullanimi kisitlanmistir. Lutfen api/v2 adresine gecin.", "migration_guide": "https://docs.example.com/migration/v1-to-v2", "sunset_date": "2024-12-31"}';
    }
}
NGINX

nginx -t && systemctl reload nginx
echo "V1 throttling aktif edildi."

Canary Release ile V2 Geçişi

Tüm trafiği aniden v2’ye taşımak yerine kademeli bir geçiş yapmak çok daha güvenli. Nginx’in upstream weight özelliğini kullanarak bu geçişi yönetebilirsin:

# /etc/nginx/conf.d/api_canary.conf
# Trafigin %10'unu v2'ye, %90'ini v1'e yonlendir

upstream api_backend {
    server 127.0.0.1:5001 weight=9;  # V1 - %90 trafik
    server 127.0.0.1:5002 weight=1;  # V2 - %10 trafik (canary)
}

server {
    listen 80;
    server_name api-canary.example.com;
    
    location /api/ {
        proxy_pass http://api_backend;
        proxy_set_header Host $host;
        proxy_next_upstream error timeout http_500 http_502 http_503;
    }
}

Canary release sürecinde weight değerlerini aşamalı olarak değiştiriyorsun: önce 90/10, sonra 70/30, ardından 50/50, sonra 20/80 ve son olarak 0/100. Her adımda error rate’i, response time’ı ve kullanıcı şikayetlerini izliyorsun.

Monitoring ve Alerting

Her iki versiyonu aynı anda izlemek için Prometheus ve Grafana kombinasyonunu kullanıyorsun. Basit bir blackbox monitoring script’i:

#!/bin/bash
# api_health_monitor.sh
# Her iki versiyonu periyodik olarak test et

V1_URL="https://api.example.com/api/v1/health"
V2_URL="https://api.example.com/api/v2/health"
ALERT_EMAIL="[email protected]"
SLACK_WEBHOOK="https://hooks.slack.com/services/xxx/yyy/zzz"

check_endpoint() {
    local VERSION=$1
    local URL=$2
    local START=$(date +%s%N)
    
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" 
        --max-time 10 
        --connect-timeout 5 
        "$URL")
    
    local END=$(date +%s%N)
    local RESPONSE_TIME=$(( (END - START) / 1000000 ))
    
    if [ "$HTTP_CODE" != "200" ]; then
        local MSG="KRITIK: API $VERSION cevap vermiyor! HTTP: $HTTP_CODE - URL: $URL"
        echo "$MSG"
        
        # Slack bildirimi
        curl -s -X POST "$SLACK_WEBHOOK" 
            -H 'Content-type: application/json' 
            --data "{"text":"$MSG"}"
        
        # Email bildirimi
        echo "$MSG" | mail -s "API Health Alert - $VERSION" "$ALERT_EMAIL"
        
        return 1
    fi
    
    if [ "$RESPONSE_TIME" -gt "2000" ]; then
        echo "UYARI: API $VERSION yavas! Response time: ${RESPONSE_TIME}ms"
    else
        echo "OK: API $VERSION saglikli. HTTP: $HTTP_CODE, Response: ${RESPONSE_TIME}ms"
    fi
    
    return 0
}

echo "=== API Health Check - $(date) ==="
check_endpoint "V1" "$V1_URL"
check_endpoint "V2" "$V2_URL"

Versiyon Lifecycle Belgelendirmesi

Tüm bu sürecin düzgün işlemesi için bir lifecycle politikası belirlemen ve bunu müşterilerle paylaşman gerekiyor. Pratik olarak şöyle bir zaman çizelgesi işe yarıyor:

  • Announcement (Duyuru): Yeni versiyon yayınlanmadan en az 3 ay önce e-posta, dokümantasyon ve changelog üzerinden duyuru yapılır
  • Parallel Run (Paralel Çalışma): Her iki versiyon en az 6 ay boyunca aktif tutulur, deprecation header’ları eklenir
  • Throttling Başlangıcı: Sunset’ten 90 gün önce V1’e rate limiting uygulanmaya başlanır
  • Sunset: V1 tamamen kapatılır, tüm trafik artık 410 Gone döndürür

Bu politikayı bir konfigürasyon dosyasında tutmak ve otomasyon scriptlerinizde referans almak sürecin disiplinli yürütülmesini sağlıyor:

# /etc/api-lifecycle/v1-sunset.conf
VERSION="v1"
RELEASE_DATE="2022-01-15"
DEPRECATION_DATE="2023-06-01"
THROTTLE_START="2024-10-01"
SUNSET_DATE="2024-12-31"
SUCCESSOR="v2"
MIGRATION_GUIDE="https://docs.example.com/migration/v1-to-v2"
CONTACT="[email protected]"

Sonuç

API versiyonlama, salt bir geliştirici konusu değil; infrastructure, operasyon ve müşteri ilişkilerini birleştiren kapsamlı bir süreç. Doğru yapıldığında hem ekibini hem de müşterilerini koruyorsun. Yanlış yapıldığında ise “neden bu servisi ayağa kaldırdınız?” sorusuyla gece yarısı uyanıyorsun.

Bu yazıda özetlenen yaklaşımın özü şu: URL path versiyonlama ile başla, her versiyonu izole bir ortamda çalıştır, veritabanı migration’larını backward-compatible yap, deprecation header’larını erkenden ekle, trafik analizini sürekli yap ve sunset sürecini kademeli throttling ile yönet.

En önemli tavsiyem ise şu: V1’i kapatma tarihini belirleyip gerçekten o tarihte kapatmak. “Müşteri hazır değil” ya da “şimdilik açık kalsın” gibi gerekçelerle uzatılan versiyonlar sonunda teknik borç olarak biriküyor ve o borcu ödemek her seferinde daha ağır hale geliyor. Sunset tarihi söz vermek demek, o sözü tutmak demek.

Bir yanıt yazın

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