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/usersve/api/v2/usersgibi 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=2gibi 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.
