Nginx ile Dynamic Upstream: Otomatik Backend Yönetimi

Mikroservis mimarisine geçiş yaptığınızda ya da container tabanlı bir ortamda çalışmaya başladığınızda, backend sunucularınızın IP adresleri ve portları sürekli değişmeye başlar. Docker Swarm scale ediyorsunuz, Kubernetes pod’ları yeniden başlıyor, yeni instance’lar devreye giriyor. Klasik statik upstream konfigürasyonu bu dinamizmle başa çıkamaz ve her değişiklikte Nginx’i yeniden başlatmak zorunda kalırsınız. Bu yazıda Nginx’i dinamik backend değişikliklerine nasıl adapte edeceğimizi, hem açık kaynak hem de ticari çözümleri ele alarak detaylıca inceleyeceğiz.

Statik Upstream’in Sorunu

Klasik Nginx konfigürasyonunda upstream bloğu şöyle görünür:

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

Bu konfigürasyonda her backend değişikliğinde /etc/nginx/nginx.conf dosyasını düzenleyip nginx -s reload komutu çalıştırmanız gerekir. Küçük ortamlarda bu kabul edilebilir, ancak günde onlarca kez scale olan bir sistemde bu yaklaşım hem operasyonel yük hem de kısa süreli de olsa servis kesintisi riski oluşturur. Reload işlemi sırasında aktif bağlantılar kesilmese de yeni upstream bilgisinin yüklenmesi arasındaki gecikme, hatalı yönlendirmelere neden olabilir.

DNS Tabanlı Dinamik Upstream

Nginx’in en basit dinamik upstream yöntemi DNS tabanlı çözümdür. Nginx Plus’ta bu özellik doğrudan desteklenirken, açık kaynak versiyonda birkaç trick gerekmektedir.

Resolver ile DNS Güncellemesi

Açık kaynak Nginx’te resolver direktifi ile DNS tabanlı dinamik çözümleme yapabilirsiniz:

http {
    resolver 127.0.0.1 8.8.8.8 valid=30s ipv6=off;
    resolver_timeout 5s;

    server {
        listen 80;

        location /api/ {
            set $backend "backend.service.consul:8080";
            proxy_pass http://$backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_connect_timeout 5s;
            proxy_read_timeout 60s;
        }
    }
}

Buradaki kritik nokta şu: upstream bloğu kullanmak yerine değişken üzerinden proxy_pass tanımladığınızda, Nginx her istek geldiğinde DNS çözümlemesi yapar. valid=30s parametresi DNS cache süresini belirler. Bu sayede Consul, etcd veya herhangi bir service discovery sisteminin güncellediği DNS kayıtları otomatik olarak alınır.

Dikkat edilmesi gereken bir durum var: Değişken kullanıldığında Nginx upstream health check mekanizmaları devre dışı kalır. Bu nedenle backend’in sağlıklı olup olmadığını başka bir yöntemle takip etmeniz gerekir.

Consul ile Service Discovery Entegrasyonu

Gerçek dünya senaryolarında en yaygın kombinasyonlardan biri Nginx + Consul’dur. Consul, servis kayıtlarını DNS üzerinden sunar ve Nginx bu kayıtları doğrudan kullanabilir.

Consul DNS Konfigürasyonu

Önce Consul’un DNS portunu Nginx’in kullanabileceği şekilde ayarlayalım:

# /etc/nginx/conf.d/consul-upstream.conf

http {
    # Consul'un DNS arayüzünü resolver olarak tanımla
    resolver 127.0.0.1:8600 valid=5s ipv6=off;

    upstream api_backend {
        # Bu statik tanım sadece fallback içindir
        server 127.0.0.1:8081 backup;
    }

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

        location / {
            # Consul'daki servis adıyla dinamik çözümleme
            set $service_url "api-service.service.consul";
            proxy_pass http://$service_url:8080;

            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # Hata durumunda yeniden deneme
            proxy_next_upstream error timeout invalid_header;
            proxy_next_upstream_tries 3;
        }
    }
}

Bu konfigürasyonda api-service.service.consul adresi Consul tarafından çözümlenir ve sağlıklı olan backend’lerden biri döner.

nginx-upsync ile Gerçek Zamanlı Upstream Yönetimi

nginx-upsync modülü, Nginx upstream havuzunu Consul veya etcd üzerinden gerçek zamanlı olarak güncellemenizi sağlar. Nginx reload gerekmeksizin backend ekleme ve çıkarma yapabilirsiniz.

Kurulum

# nginx-upsync modülünü derlemek için
cd /usr/local/src
git clone https://github.com/weibocom/nginx-upsync-module.git

# Nginx'i modülle birlikte derle
./configure 
    --prefix=/etc/nginx 
    --with-http_ssl_module 
    --with-http_v2_module 
    --add-module=../nginx-upsync-module

make && make install

nginx-upsync Konfigürasyonu

# /etc/nginx/nginx.conf

upstream backend_api {
    # Başlangıç noktası olarak dummy server
    server 127.0.0.1:11111;

    # Consul ile senkronizasyon
    upsync 127.0.0.1:8500/v1/health/service/api-backend
           upsync_timeout=6m
           upsync_interval=500ms
           upsync_type=consul_health
           strong_dependency=off;

    # Upstream durumunu dosyaya yaz (Nginx restart sonrası kurtarma için)
    upsync_dump_path /etc/nginx/upstream_cache/api_backend.conf;
}

server {
    listen 80;

    location /api/ {
        proxy_pass http://backend_api;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
    }

    # Upstream yönetim endpoint'i
    location /upstream_show {
        upstream_show;
    }
}

Bu konfigürasyonla Nginx, her 500 milisaniyede bir Consul’u sorgular ve sağlıklı backend listesini günceller. /upstream_show endpoint’i üzerinden mevcut upstream durumunu görebilirsiniz.

Lua ile Dinamik Upstream: OpenResty Yaklaşımı

OpenResty (Nginx + LuaJIT), en esnek dinamik upstream çözümünü sunar. Redis veya paylaşımlı bellek üzerinden backend listesini yönetebilir, custom logic ekleyebilirsiniz.

# /etc/nginx/nginx.conf - OpenResty ile dinamik upstream

http {
    lua_shared_dict upstream_list 1m;
    lua_shared_dict upstream_locks 100k;

    # Başlangıçta backend listesini yükle
    init_by_lua_block {
        local upstreams = ngx.shared.upstream_list
        upstreams:set("backend_1", "192.168.1.10:8080")
        upstreams:set("backend_2", "192.168.1.11:8080")
        upstreams:set("backend_3", "192.168.1.12:8080")
    }

    upstream dynamic_backend {
        server 0.0.0.1;  # Dummy, Lua tarafından override edilecek

        balancer_by_lua_block {
            local balancer = require "ngx.balancer"
            local upstreams = ngx.shared.upstream_list
            local keys = upstreams:get_keys()

            if #keys == 0 then
                ngx.log(ngx.ERR, "No backends available!")
                return ngx.exit(502)
            end

            -- Basit round-robin
            local idx = math.random(#keys)
            local backend = upstreams:get(keys[idx])
            local host, port = backend:match("(.+):(%d+)")

            local ok, err = balancer.set_current_peer(host, tonumber(port))
            if not ok then
                ngx.log(ngx.ERR, "Failed to set peer: ", err)
                return ngx.exit(502)
            end
        }
    }

    server {
        listen 80;

        # Backend ekleme endpoint'i
        location /admin/upstream/add {
            content_by_lua_block {
                local args = ngx.req.get_uri_args()
                local key = args["key"]
                local addr = args["addr"]

                if not key or not addr then
                    ngx.status = 400
                    ngx.say('{"error": "key and addr required"}')
                    return
                end

                local upstreams = ngx.shared.upstream_list
                upstreams:set(key, addr)
                ngx.say('{"status": "added", "key": "' .. key .. '"}')
            }
        }

        # Backend silme endpoint'i
        location /admin/upstream/remove {
            content_by_lua_block {
                local args = ngx.req.get_uri_args()
                local key = args["key"]

                if not key then
                    ngx.status = 400
                    ngx.say('{"error": "key required"}')
                    return
                end

                local upstreams = ngx.shared.upstream_list
                upstreams:delete(key)
                ngx.say('{"status": "removed"}')
            }
        }

        location /api/ {
            proxy_pass http://dynamic_backend;
        }
    }
}

Bu yaklaşımla Nginx reload gerektirmeksizin HTTP API üzerinden backend ekleyip çıkarabilirsiniz:

# Yeni backend ekle
curl "http://localhost/admin/upstream/add?key=backend_4&addr=192.168.1.13:8080"

# Backend çıkar
curl "http://localhost/admin/upstream/remove?key=backend_2"

Kubernetes Ortamında Dinamik Upstream

Kubernetes kullanıyorsanız, Nginx Ingress Controller zaten dinamik upstream yönetimini sizin için halleder. Ancak Nginx’i Kubernetes dışında yönetiyorsanız ve Kubernetes servislerine proxy yapıyorsanız, farklı bir yaklaşım gerekir.

# Kubernetes ExternalName servisi ile Nginx upstream
# /etc/nginx/conf.d/k8s-upstream.conf

http {
    # Kubernetes CoreDNS
    resolver 10.96.0.10 valid=10s ipv6=off;

    server {
        listen 80;

        location /api/users/ {
            set $k8s_svc "user-service.default.svc.cluster.local";
            proxy_pass http://$k8s_svc:8080;
            proxy_set_header Host $host;

            # Kubernetes pod yeniden başlamalarına karşı retry
            proxy_next_upstream error timeout;
            proxy_next_upstream_tries 2;
            proxy_next_upstream_timeout 10s;
        }

        location /api/orders/ {
            set $k8s_svc "order-service.default.svc.cluster.local";
            proxy_pass http://$k8s_svc:8080;
            proxy_set_header Host $host;
        }
    }
}

Otomatik Health Check ve Failover

Dinamik upstream yönetiminin olmazsa olmazı health check mekanizmasıdır. Nginx Plus’ta bu built-in gelir, açık kaynak versiyonda nginx_upstream_check_module kullanılır.

# nginx_upstream_check_module ile health check
# /etc/nginx/conf.d/upstream-health.conf

upstream api_cluster {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;

    # Health check parametreleri
    check interval=3000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "HEAD /health HTTP/1.0rnHost: localhostrnrn";
    check_http_expect_alive http_2xx http_3xx;
}

server {
    listen 80;

    location / {
        proxy_pass http://api_cluster;
    }

    # Health check durumunu görüntüle
    location /nginx_status/upstream {
        check_status;
        access_log off;
        # Sadece dahili ağdan erişime izin ver
        allow 10.0.0.0/8;
        allow 192.168.0.0/16;
        deny all;
    }
}

check: Kontrol aralığı (ms), kaç başarılı kontrolde aktif, kaç başarısızda pasif, timeout

Shell Script ile Otomatik Upstream Güncelleme

Nginx Plus veya özel modül kullanmıyorsanız, basit bir shell script ile upstream dosyasını dinamik olarak yönetebilirsiniz. Bu yaklaşım özellikle Consul veya etcd yokken işe yarar.

#!/bin/bash
# /usr/local/bin/nginx-upstream-update.sh

UPSTREAM_FILE="/etc/nginx/conf.d/dynamic_upstream.conf"
TEMP_FILE="/tmp/upstream_new.conf"
NGINX_PID_FILE="/var/run/nginx.pid"

# Backend listesini bir kaynaktan çek (örnek: AWS ECS Task API)
get_backends() {
    # Bu örnekte AWS ECS'den task IP'lerini çekiyoruz
    aws ecs list-tasks 
        --cluster production 
        --service-name api-service 
        --query 'taskArns[]' 
        --output text | 
    xargs -I {} aws ecs describe-tasks 
        --cluster production 
        --tasks {} 
        --query 'tasks[].attachments[].details[?name==`privateIPv4Address`].value' 
        --output text
}

generate_upstream_config() {
    local backends=("$@")

    cat > "$TEMP_FILE" << 'HEADER'
# Bu dosya otomatik olarak üretilmektedir
# Son güncelleme: $(date)
upstream api_dynamic {
HEADER

    for backend in "${backends[@]}"; do
        echo "    server ${backend}:8080 max_fails=3 fail_timeout=30s;" >> "$TEMP_FILE"
    done

    echo "    keepalive 32;" >> "$TEMP_FILE"
    echo "}" >> "$TEMP_FILE"
}

# Mevcut ve yeni konfigürasyonu karşılaştır
update_if_changed() {
    if ! diff -q "$UPSTREAM_FILE" "$TEMP_FILE" > /dev/null 2>&1; then
        cp "$TEMP_FILE" "$UPSTREAM_FILE"

        # Konfigürasyonu test et
        if nginx -t 2>/dev/null; then
            nginx -s reload
            echo "[$(date)] Upstream güncellendi ve Nginx yeniden yüklendi"
        else
            echo "[$(date)] HATA: Nginx konfigürasyon testi başarısız!"
            # Önceki konfigürasyonu geri yükle
            cp "${UPSTREAM_FILE}.bak" "$UPSTREAM_FILE"
        fi
    else
        echo "[$(date)] Değişiklik yok, güncelleme atlandı"
    fi
}

# Backup al
cp "$UPSTREAM_FILE" "${UPSTREAM_FILE}.bak" 2>/dev/null

# Backend listesini al ve konfigürasyon oluştur
BACKEND_LIST=($(get_backends))

if [ ${#BACKEND_LIST[@]} -eq 0 ]; then
    echo "[$(date)] UYARI: Hiç backend bulunamadı, güncelleme atlandı"
    exit 1
fi

generate_upstream_config "${BACKEND_LIST[@]}"
update_if_changed

Bu script’i cron ile her dakika çalıştırabilirsiniz:

# /etc/cron.d/nginx-upstream-update
* * * * * root /usr/local/bin/nginx-upstream-update.sh >> /var/log/nginx/upstream-update.log 2>&1

Nginx Plus ile Dinamik Upstream API

Nginx Plus kullanıyorsanız, resmi upstream API ile doğrudan upstream yönetimi yapabilirsiniz:

# Nginx Plus upstream API konfigürasyonu
upstream api_backend {
    zone api_backend 64k;  # Paylaşımlı bellek zone'u zorunlu
    server 192.168.1.10:8080;
}

server {
    listen 8080;

    location /api/ {
        api write=on;
        # Sadece dahili erişime izin ver
        allow 127.0.0.1;
        allow 10.0.0.0/8;
        deny all;
    }
}

Ardından API üzerinden backend yönetimi:

# Mevcut upstream'leri listele
curl http://localhost:8080/api/6/http/upstreams/api_backend/servers

# Yeni backend ekle
curl -X POST http://localhost:8080/api/6/http/upstreams/api_backend/servers 
  -H "Content-Type: application/json" 
  -d '{"server": "192.168.1.13:8080", "weight": 2}'

# Backend'i drain moduna al (graceful çıkarma)
curl -X PATCH http://localhost:8080/api/6/http/upstreams/api_backend/servers/3 
  -H "Content-Type: application/json" 
  -d '{"drain": true}'

# Backend'i tamamen kaldır
curl -X DELETE http://localhost:8080/api/6/http/upstreams/api_backend/servers/3

Drain modu, Nginx Plus’ın en değerli özelliklerinden biridir. Bir backend’i drain’e aldığınızda yeni istekler oraya yönlendirilmez ancak mevcut aktif bağlantılar tamamlanır. Bu sayede zero-downtime deployment yapabilirsiniz.

Monitoring ve Logging

Dinamik upstream sistemlerde hangi backend’in ne kadar trafik aldığını, hangi backend’lerin fail ettiğini izlemek kritik önem taşır:

# /etc/nginx/nginx.conf - upstream durumunu logla

log_format upstream_log '$time_local '
                        'upstream: $upstream_addr '
                        'status: $upstream_status '
                        'response_time: $upstream_response_time '
                        'request: "$request" '
                        'client: $remote_addr';

server {
    listen 80;
    access_log /var/log/nginx/upstream_access.log upstream_log;

    location / {
        proxy_pass http://api_backend;

        # Upstream header'larını response'a ekle (debug için)
        add_header X-Upstream-Addr $upstream_addr always;
        add_header X-Upstream-Status $upstream_status always;
        add_header X-Response-Time $upstream_response_time always;
    }
}

Upstream loglarını analiz etmek için:

# En çok trafik alan backend'leri bul
awk '{print $3}' /var/log/nginx/upstream_access.log | sort | uniq -c | sort -rn

# Hata veren backend'leri filtrele
grep "upstream_status: [45]" /var/log/nginx/upstream_access.log | 
    awk '{print $3, $5}' | sort | uniq -c | sort -rn

# Ortalama response time hesapla
awk '{sum+=$7; count++} END {print "Avg:", sum/count "s"}' 
    /var/log/nginx/upstream_access.log

Pratik Tavsiyeler

Dinamik upstream yönetimi kurarken dikkat etmeniz gereken birkaç önemli nokta var:

  • Keepalive bağlantısını ihmal etmeyin: Dinamik ortamlarda her istek için yeni TCP bağlantısı açmak büyük yük oluşturur. keepalive 32 gibi bir değer her upstream havuzu için tanımlayın.
  • DNS TTL ile Nginx resolver’ı uyumlu ayarlayın: DNS TTL değeriniz 30 saniyeyse, valid=25s gibi biraz daha düşük bir değer kullanın. Aksi halde eski IP’lere istek gitmeye devam edebilir.
  • Upstream dosya backup’larını otomatikleştirin: Script tabanlı çözümlerde backup mekanizması olmazsa olmaz. Hatalı bir güncelleme tüm servisi çökertebilir.
  • Staging’de test edin: Dinamik upstream değişikliklerini production’da doğrudan denememek gerekir. Özellikle Lua tabanlı çözümlerde bir syntax hatası tüm Nginx’i durdurabilir.
  • Circuit breaker ekleyin: Backend’lerden sürekli hata geliyorsa, proxy_next_upstream ve max_fails değerlerini dikkatli ayarlayın. Hatalı backend’e sürekli gitmek toplam latency’yi artırır.
  • Zone direktifini unutmayın: Nginx Plus veya upsync kullanıyorsanız, zone direktifi olmadan worker process’ler arası upstream durumu paylaşılamaz.

Sonuç

Nginx ile dinamik upstream yönetimi, kullandığınız altyapıya ve ihtiyaçlarınıza göre birden fazla yoldan yapılabilir. Consul veya etcd gibi bir service discovery sisteminiz varsa DNS tabanlı çözüm en az karmaşıklıkla en iyi sonucu verir. Daha granüler kontrol istiyorsanız OpenResty + Lua kombinasyonu güçlü ama bakım maliyeti yüksek bir seçenek sunar. Basit ortamlar için shell script + cron tabanlı yaklaşım çoğu durumda yeterlidir. Nginx Plus kullanıyorsanız ise upstream API ile kurumsal düzeyde bir çözüme sahipsiniz demektir.

Hangi yolu seçerseniz seçin, sağlam bir monitoring, iyi test edilmiş bir fallback mekanizması ve detaylı logging olmadan dinamik upstream yönetimi tehlikeli bir macera olabilir. Önce staging’de deneyin, log’ları iyi kurun ve ancak ondan sonra production’a taşıyın.

Bir yanıt yazın

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