Blue-Green Deployment: Nginx ile Sıfır Kesintili Geçiş

Production ortamında bir uygulama güncellemesi yaparken “acaba bir şeyler ters giderse ne yaparım?” diye düşünmemişsinizdir. İşte bu korku tam olarak blue-green deployment’ın çözdüğü problemi anlatıyor. Sıfır downtime, anlık rollback imkânı ve kullanıcıların hiçbir şey fark etmemesi. Bu yazıda Nginx’i kullanarak gerçek bir blue-green deployment altyapısı kuracağız, tüm senaryoları ele alacağız ve production’da karşılaşabileceğiniz tuzakları göstereceğiz.

Blue-Green Deployment Nedir ve Neden Önemlidir

Blue-green deployment, iki identik production ortamı (blue ve green) tutmak ve trafiği aralarında anahtarlamak üzerine kurulu bir deployment stratejisidir. Bir ortam her zaman canlı (aktif) çalışırken, diğeri yeni sürümü alır ve test edilir. Her şey yolundaysa Nginx upstream’i değiştirirsiniz ve geçiş tamamdır.

Geleneksel deployment’larda şu sorunlarla karşılaşırsınız:

  • Deployment sırasında servis kesintisi yaşanır
  • Yeni sürümde kritik bir bug çıktığında rollback dakikalar alır
  • “Yarım kalmış” deployment durumları oluşur
  • Kullanıcılar hem eski hem yeni kodu aynı anda görür

Blue-green bu sorunların tamamını çözer. Nginx bu işi yapmak için mükemmel bir araçtır çünkü upstream bloklarını değiştirip nginx -s reload dediğinizde mevcut bağlantılar kesilmez, yeni bağlantılar yeni upstream’e yönlendirilir.

Altyapı Tasarımı

Gerçek dünya senaryomuzu şöyle kuralım: Python/FastAPI ile yazılmış bir REST API’niz var. İki uygulama sunucusunda çalışıyor, önünde Nginx reverse proxy var.

[İstemciler] --> [Nginx :80/:443] --> [Blue App :8001]
                                  --> [Green App :8002]

Sunucu yapımız şöyle olacak:

  • app-server-1: Blue ortam (port 8001)
  • app-server-2: Green ortam (port 8002)
  • nginx-server: Reverse proxy ve traffic switcher

Öncelikle dizin yapımızı oluşturalım:

mkdir -p /etc/nginx/blue-green
mkdir -p /opt/deployments/{blue,green}
mkdir -p /var/log/nginx/blue-green
touch /etc/nginx/blue-green/active_env
echo "blue" > /etc/nginx/blue-green/active_env

Nginx Konfigürasyonu

Ana Nginx konfigürasyonunu oluşturuyoruz. Buradaki en kritik nokta upstream bloklarını ayrı dosyalarda tutmak ve aktif ortamı dinamik olarak değiştirebilmektir.

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

upstream blue_backend {
    server 127.0.0.1:8001;
    keepalive 32;
}

upstream green_backend {
    server 127.0.0.1:8002;
    keepalive 32;
}

# Aktif upstream'i include ile dışarıdan alıyoruz
include /etc/nginx/blue-green/active_upstream.conf;

server {
    listen 80;
    listen 443 ssl http2;
    server_name api.sirketiniz.com;

    ssl_certificate /etc/ssl/certs/api.crt;
    ssl_certificate_key /etc/ssl/private/api.key;

    access_log /var/log/nginx/blue-green/access.log;
    error_log /var/log/nginx/blue-green/error.log;

    location /health {
        access_log off;
        proxy_pass http://active_backend;
        proxy_set_header Host $host;
    }

    location /api/ {
        proxy_pass http://active_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}

Aktif upstream konfigürasyon dosyası başlangıçta blue’yu göstersin:

# /etc/nginx/blue-green/active_upstream.conf
echo "upstream active_backend { server 127.0.0.1:8001; keepalive 32; }" 
  > /etc/nginx/blue-green/active_upstream.conf

Deployment Script’i

İşin kalbi burada. Bu script yeni versiyonu pasif ortama deploy eder, health check yapar ve Nginx’i switch eder.

#!/bin/bash
# /usr/local/bin/blue-green-deploy.sh

set -euo pipefail

ACTIVE_ENV_FILE="/etc/nginx/blue-green/active_env"
UPSTREAM_CONF="/etc/nginx/blue-green/active_upstream.conf"
LOG_FILE="/var/log/blue-green-deploy.log"
HEALTH_CHECK_URL="http://127.0.0.1"
HEALTH_CHECK_RETRIES=10
HEALTH_CHECK_INTERVAL=3

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

get_active_env() {
    cat "$ACTIVE_ENV_FILE"
}

get_passive_env() {
    local active
    active=$(get_active_env)
    if [ "$active" = "blue" ]; then
        echo "green"
    else
        echo "blue"
    fi
}

get_port_for_env() {
    local env=$1
    if [ "$env" = "blue" ]; then
        echo "8001"
    else
        echo "8002"
    fi
}

health_check() {
    local port=$1
    local env=$2
    local attempt=1

    log "Health check basliyor: $env ortami (port: $port)"

    while [ $attempt -le $HEALTH_CHECK_RETRIES ]; do
        local http_code
        http_code=$(curl -s -o /dev/null -w "%{http_code}" 
            --connect-timeout 3 
            "${HEALTH_CHECK_URL}:${port}/health" 2>/dev/null || echo "000")

        if [ "$http_code" = "200" ]; then
            log "Health check basarili: $env (deneme: $attempt)"
            return 0
        fi

        log "Health check basarisiz (HTTP $http_code), $HEALTH_CHECK_INTERVAL saniye bekleniyor... (deneme: $attempt/$HEALTH_CHECK_RETRIES)"
        sleep "$HEALTH_CHECK_INTERVAL"
        ((attempt++))
    done

    log "HATA: Health check $HEALTH_CHECK_RETRIES denemede basarisiz oldu!"
    return 1
}

switch_traffic() {
    local target_env=$1
    local target_port
    target_port=$(get_port_for_env "$target_env")

    log "Trafik $target_env ortamina yonlendiriliyor (port: $target_port)"

    # Yeni upstream konfig yaz
    cat > "$UPSTREAM_CONF" << EOF
upstream active_backend {
    server 127.0.0.1:${target_port};
    keepalive 32;
}
EOF

    # Nginx konfig test et
    if ! nginx -t 2>/dev/null; then
        log "HATA: Nginx konfig gecersiz!"
        return 1
    fi

    # Graceful reload - mevcut baglantilari kesmez
    nginx -s reload
    echo "$target_env" > "$ACTIVE_ENV_FILE"

    log "Trafik basariyla $target_env ortamina gecti"
}

main() {
    local image_tag=${1:-"latest"}

    local active_env
    local passive_env
    local passive_port

    active_env=$(get_active_env)
    passive_env=$(get_passive_env)
    passive_port=$(get_port_for_env "$passive_env")

    log "=========================================="
    log "Deployment basliyor"
    log "Aktif ortam: $active_env"
    log "Hedef ortam: $passive_env (port: $passive_port)"
    log "Image tag: $image_tag"
    log "=========================================="

    # Yeni versiyonu pasif ortama deploy et
    log "Yeni versiyon deploy ediliyor: $passive_env"
    /usr/local/bin/deploy-app.sh "$passive_env" "$image_tag" "$passive_port"

    # Health check
    if ! health_check "$passive_port" "$passive_env"; then
        log "KRITIK: Health check basarisiz, deployment iptal ediliyor!"
        exit 1
    fi

    # Traigi switch et
    switch_traffic "$passive_env"

    log "Deployment tamamlandi! Aktif ortam: $passive_env"
    log "Eski ortam ($active_env) standby modunda"
}

main "$@"

Uygulama Deploy Script’i

Uygulamayı Docker ile çalıştırdığımızı varsayalım:

#!/bin/bash
# /usr/local/bin/deploy-app.sh

set -euo pipefail

ENV_NAME=$1
IMAGE_TAG=$2
PORT=$3
APP_IMAGE="registry.sirketiniz.com/api-service"
CONTAINER_NAME="api-${ENV_NAME}"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [deploy-app] $1"
}

log "Container deploy ediliyor: $CONTAINER_NAME"

# Eski container'i durdur (varsa)
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
    log "Eski container durduruluyor: $CONTAINER_NAME"
    docker stop "$CONTAINER_NAME" --time 30 || true
    docker rm "$CONTAINER_NAME" || true
fi

# Yeni image'i cek
log "Image cekiliyor: ${APP_IMAGE}:${IMAGE_TAG}"
docker pull "${APP_IMAGE}:${IMAGE_TAG}"

# Yeni container'i baslat
docker run -d 
    --name "$CONTAINER_NAME" 
    --restart unless-stopped 
    -p "127.0.0.1:${PORT}:8000" 
    -e "ENV=${ENV_NAME}" 
    -e "APP_VERSION=${IMAGE_TAG}" 
    --memory="512m" 
    --cpus="1.0" 
    "${APP_IMAGE}:${IMAGE_TAG}"

log "Container baslatildi: $CONTAINER_NAME (port: $PORT)"

# Container'in ayaga kalkmasi icin kisa bekleme
sleep 2

# Docker health check durumunu kontrol et
local attempt=1
while [ $attempt -le 5 ]; do
    status=$(docker inspect --format='{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "unknown")
    if [ "$status" = "running" ]; then
        log "Container calisıyor: $CONTAINER_NAME"
        exit 0
    fi
    log "Container durumu: $status (deneme: $attempt/5)"
    sleep 3
    ((attempt++))
done

log "HATA: Container basarili sekilde baslamadi!"
docker logs "$CONTAINER_NAME" --tail 50
exit 1

Weighted Traffic ile Canary Release

Bazen tüm trafiği birden switch etmek yerine yavaş yavaş geçiş yapmak istersiniz. Nginx Plus olmadan da bunu yapabilirsiniz, ama biraz farklı bir yöntemle. Lua modülü varsa harika, yoksa basit bir weighted upstream kullanabilirsiniz:

# /etc/nginx/conf.d/api-canary.conf
# Canary release: %10 trafigi green'e, %90'i blue'ya

upstream active_backend {
    server 127.0.0.1:8001 weight=9;  # blue
    server 127.0.0.1:8002 weight=1;  # green (canary)
    keepalive 32;
}

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

    # X-Canary header'i ile belirli istekleri green'e zorla
    set $backend_upstream "active_backend";

    if ($http_x_canary = "true") {
        set $backend_upstream "green_backend";
    }

    location /api/ {
        proxy_pass http://$backend_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        add_header X-Served-By $upstream_addr always;
    }
}

Canary’den full switch’e geçiş script’i:

#!/bin/bash
# /usr/local/bin/canary-to-full.sh
# Canary'yi kademeli olarak artir

UPSTREAM_CONF="/etc/nginx/blue-green/active_upstream.conf"

increase_canary() {
    local blue_weight=$1
    local green_weight=$2

    cat > "$UPSTREAM_CONF" << EOF
upstream active_backend {
    server 127.0.0.1:8001 weight=${blue_weight};
    server 127.0.0.1:8002 weight=${green_weight};
    keepalive 32;
}
EOF
    nginx -s reload
    echo "Trafik dagitimi: Blue=%${blue_weight}0, Green=%${green_weight}0"
}

echo "Canary artiriliyor..."
increase_canary 9 1
sleep 120  # 2 dakika izle

# Hata orani kontrol et (basit ornek)
error_rate=$(tail -n 1000 /var/log/nginx/blue-green/access.log | 
    awk '$9 >= 500' | wc -l)

if [ "$error_rate" -gt 10 ]; then
    echo "Hata orani yuksek ($error_rate), rollback yapiliyor!"
    increase_canary 10 0
    exit 1
fi

increase_canary 5 5
sleep 120

increase_canary 2 8
sleep 120

increase_canary 0 10
echo "Full gecis tamamlandi!"

Rollback Mekanizması

Rollback, blue-green’in en güzel tarafı. Sadece eski ortama geri dönüyorsunuz:

#!/bin/bash
# /usr/local/bin/blue-green-rollback.sh

set -euo pipefail

ACTIVE_ENV_FILE="/etc/nginx/blue-green/active_env"
UPSTREAM_CONF="/etc/nginx/blue-green/active_upstream.conf"
LOG_FILE="/var/log/blue-green-deploy.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ROLLBACK] $1" | tee -a "$LOG_FILE"
}

active_env=$(cat "$ACTIVE_ENV_FILE")

if [ "$active_env" = "blue" ]; then
    rollback_env="green"
    rollback_port="8002"
else
    rollback_env="blue"
    rollback_port="8001"
fi

log "Rollback basliyor: $active_env --> $rollback_env"

# Pasif ortamin hala calisip calismadigi kontrol et
rollback_health=$(curl -s -o /dev/null -w "%{http_code}" 
    "http://127.0.0.1:${rollback_port}/health" 2>/dev/null || echo "000")

if [ "$rollback_health" != "200" ]; then
    log "UYARI: Rollback hedefi ($rollback_env) saglikli degil! HTTP: $rollback_health"
    log "Manuel mudahale gerekebilir."
    exit 1
fi

cat > "$UPSTREAM_CONF" << EOF
upstream active_backend {
    server 127.0.0.1:${rollback_port};
    keepalive 32;
}
EOF

nginx -t && nginx -s reload
echo "$rollback_env" > "$ACTIVE_ENV_FILE"

log "Rollback tamamlandi! Aktif ortam: $rollback_env"
log "Toplam gecis suresi: saniyeler icinde"

Monitoring ve Alerting Entegrasyonu

Deployment sonrası otomatik izleme çok kritik. Basit bir monitoring script’i:

#!/bin/bash
# /usr/local/bin/post-deploy-monitor.sh
# Deployment sonrasi 10 dakika boyunca izle

ACTIVE_ENV_FILE="/etc/nginx/blue-green/active_env"
MONITOR_DURATION=600  # 10 dakika
CHECK_INTERVAL=30
ERROR_THRESHOLD=5
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL:-}"

active_env=$(cat "$ACTIVE_ENV_FILE")
start_time=$(date +%s)
consecutive_errors=0

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [MONITOR] $1"
}

send_alert() {
    local message=$1
    log "ALERT: $message"

    if [ -n "$SLACK_WEBHOOK" ]; then
        curl -s -X POST "$SLACK_WEBHOOK" 
            -H 'Content-type: application/json' 
            -d "{"text": ":rotating_light: Blue-Green Alert: ${message}"}" 
            > /dev/null 2>&1
    fi
}

while true; do
    current_time=$(date +%s)
    elapsed=$((current_time - start_time))

    if [ $elapsed -ge $MONITOR_DURATION ]; then
        log "Izleme tamamlandi. Deployment basarili."
        exit 0
    fi

    # Health check
    http_code=$(curl -s -o /dev/null -w "%{http_code}" 
        --connect-timeout 3 
        "http://127.0.0.1/health" 2>/dev/null || echo "000")

    # Son 100 istekteki 5xx orani
    error_count=$(tail -n 100 /var/log/nginx/blue-green/access.log 2>/dev/null | 
        awk '$9 >= 500 {count++} END {print count+0}')

    # Yanit suresi kontrolu
    avg_response=$(tail -n 100 /var/log/nginx/blue-green/access.log 2>/dev/null | 
        awk '{sum += $NF; count++} END {if(count>0) printf "%.3f", sum/count; else print "0"}')

    log "Durum: HTTP=$http_code, 5xx_son_100=$error_count, Ort_Yanit=${avg_response}s (${elapsed}s/${MONITOR_DURATION}s)"

    if [ "$http_code" != "200" ] || [ "$error_count" -gt "$ERROR_THRESHOLD" ]; then
        ((consecutive_errors++))
        send_alert "Ortam: $active_env | HTTP: $http_code | 5xx: $error_count | Ard_Arda_Hata: $consecutive_errors"

        if [ $consecutive_errors -ge 3 ]; then
            send_alert "Otomatik rollback baslatiliyor!"
            /usr/local/bin/blue-green-rollback.sh
            exit 1
        fi
    else
        consecutive_errors=0
    fi

    sleep "$CHECK_INTERVAL"
done

Monitoring script’ini systemd timer ile otomatik başlatabilirsiniz:

# /etc/systemd/system/post-deploy-monitor.service
[Unit]
Description=Post Deploy Monitor
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/post-deploy-monitor.sh
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

CI/CD Pipeline Entegrasyonu

GitLab CI örneği ile tüm süreci otomatize edelim:

# .gitlab-ci.yml

stages:
  - build
  - test
  - deploy
  - verify

variables:
  IMAGE_TAG: $CI_COMMIT_SHA
  REGISTRY: registry.sirketiniz.com

build:
  stage: build
  script:
    - docker build -t $REGISTRY/api-service:$IMAGE_TAG .
    - docker push $REGISTRY/api-service:$IMAGE_TAG
  only:
    - main

integration-test:
  stage: test
  script:
    - ./scripts/run-integration-tests.sh
  only:
    - main

blue-green-deploy:
  stage: deploy
  script:
    - ssh deploy@prod-nginx "sudo /usr/local/bin/blue-green-deploy.sh $IMAGE_TAG"
  environment:
    name: production
    url: https://api.sirketiniz.com
  only:
    - main
  when: manual

post-deploy-verify:
  stage: verify
  script:
    - ssh deploy@prod-nginx "sudo /usr/local/bin/post-deploy-monitor.sh &"
    - sleep 60
    - curl -f https://api.sirketiniz.com/health
  only:
    - main

Sık Karşılaşılan Sorunlar ve Çözümleri

Veritabanı migration sorunları: En büyük tuzak burada. Yeni kod eski şemaya, eski kod yeni şemaya hitap edebilir. Çözüm olarak backward-compatible migration stratejisi kullanın. Kolon eklemek iyidir, kolon silmek veya rename etmek tehlikelidir. Büyük schema değişikliklerini ayrı bir migration deployment olarak yapın.

Sticky session problemi: Kullanıcı oturumları bir ortamda başladıysa diğerine geçince oturum kaybolabilir. Çözüm olarak session verilerini Redis gibi merkezi bir depoda tutun, hiçbir zaman uygulama sunucusunda.

Health check endpoint’i ne döndürmeli: Basit HTTP 200 yetmez. Veritabanı bağlantısı, cache bağlantısı ve kritik external servis kontrollerini de içermelidir. Ama dikkatli olun, çok agresif health check deployment’ı gereksiz yere başarısız yapabilir.

Nginx worker process ve keepalive: nginx -s reload mevcut bağlantıları kesmez ama worker process’ler değişir. Uzun süreli WebSocket bağlantılarınız varsa bu bir sorun olabilir. Bu durumda worker_shutdown_timeout direktifini kullanın:

# /etc/nginx/nginx.conf içinde
worker_shutdown_timeout 30s;

Disk alanı: Her deployment için yeni Docker image katmanları birikir. Düzenli temizlik için cron job ekleyin:

# Her gece eski image'leri temizle
0 2 * * * docker image prune -a --filter "until=72h" -f >> /var/log/docker-cleanup.log 2>&1

Sonuç

Blue-green deployment, production’da güven veren en sağlam deployment stratejilerinden biridir. Nginx ile bu stratejiyi uygulamak düşündüğünüzden çok daha basittir. Kritik noktalara bakalım:

  • Aktif ortamı her zaman bilinen bir dosyada tutun, script’ler arası tutarsızlık yaşamayın
  • Health check’i gerçekçi yazın, sahte bir 200 döndürmek en tehlikeli yanılgıdır
  • Rollback her zaman hazır olsun, deployment öncesi pasif ortamın sağlıklı olduğundan emin olun
  • Veritabanı migration’larını ayrı ele alın, bu konuyu pas geçmek felakete davet çıkarmaktır
  • Post-deployment monitoring’i otomatize edin, insan gözüyle izlemek yeterince hızlı değildir

Bu altyapıyı kurduktan sonra ekibiniz artık “deployment günü” korkusu yaşamayacak. Hatta iş saatlerinde bile rahatça deployment yapabilirsiniz. Gerçek sıfır downtime budur ve Nginx bunu kolaylaştırmak için tam olarak doğru araçtır.

Bir yanıt yazın

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