Nginx ile Geo-Based Yönlendirme: Ülke Bazlı Trafik Yönetimi

Ülke bazlı trafik yönetimi, global ölçekte hizmet veren sistemlerin vazgeçilmez bir parçası. Türkiye’den gelen kullanıcıları Türkçe içeriğe, Almanya’dan gelenleri Almanca versiyona yönlendirmek istiyorsun ama bunu uygulama katmanında yapmak istemiyorsun. Ya da daha kritik bir senaryo: belirli ülkelerden gelen trafiği tamamen engellemek ya da farklı API endpoint’lerine yönlendirmek gerekiyor. İşte tam burada Nginx’in ngx_http_geoip_module ve geoip2 modülleri devreye giriyor.

Bu yazıda gerçek dünya senaryoları üzerinden Nginx ile geo-based yönlendirmeyi nasıl kurarsın, hangi tuzaklardan kaçınman gerektiğini ve production ortamında ne gibi optimizasyonlar yapabileceğini anlatacağım.

Gereksinimler ve Temel Kavramlar

Başlamadan önce iki farklı yaklaşım var:

  • ngx_http_geoip_module: Nginx’in eski yerleşik modülü, MaxMind GeoIP legacy formatını (.dat) kullanır
  • ngx_http_geoip2_module: Yeni nesil, MaxMind GeoIP2 formatını (.mmdb) kullanır, daha güncel ve doğru

GeoIP2 modülünü kesinlikle tercih et. Legacy format artık MaxMind tarafından aktif olarak güncellenmemeye başlandı. Ücretsiz olarak kullanabileceğin GeoLite2 veritabanı her ay güncelleniyor ve kayıt olmak yeterli.

MaxMind GeoLite2 Veritabanını Edinmek

MaxMind’ın sitesinde ücretsiz hesap açıp lisans anahtarı alman gerekiyor. Ardından geoipupdate aracını kullanarak veritabanını otomatik güncel tutabilirsin.

# Ubuntu/Debian için geoipupdate kurulumu
sudo apt-get install geoipupdate

# /etc/GeoIP.conf dosyasını düzenle
sudo nano /etc/GeoIP.conf
# /etc/GeoIP.conf içeriği
AccountID 123456
LicenseKey your_license_key_here
EditionIDs GeoLite2-Country GeoLite2-City GeoLite2-ASN

# Veritabanını indir
sudo geoipupdate

# Aylık otomatik güncelleme için cron
echo "0 2 1 * * root /usr/bin/geoipupdate" | sudo tee /etc/cron.d/geoipupdate

Veritabanları genellikle /usr/share/GeoIP/ veya /var/lib/GeoIP/ altına düşer.

GeoIP2 Nginx Modülünü Kurma

Ubuntu/Debian sistemlerde modülü farklı yollarla kurabilirsin. En temiz yol libnginx-mod-http-geoip2 paketidir.

# Ubuntu 20.04/22.04
sudo apt-get install nginx libnginx-mod-http-geoip2

# CentOS/RHEL için
sudo yum install nginx-module-geoip

# Modülün yüklendiğini doğrula
nginx -V 2>&1 | grep -i geoip

# Modül dynamic ise nginx.conf'a yükle
# /etc/nginx/nginx.conf'un en başına ekle:
# load_module modules/ngx_http_geoip2_module.so;

Eğer Nginx’i kaynak koddan derliyorsan --with-http_geoip_module veya GeoIP2 için üçüncü taraf modülü şu şekilde eklersin:

# Kaynak derlemede GeoIP2 modülü
./configure 
    --prefix=/etc/nginx 
    --add-module=/path/to/ngx_http_geoip2_module 
    --with-compat 
    # ... diğer opsiyonlar
make && make install

Temel Yapılandırma: Ülke Tespiti

Modül yüklendikten sonra nginx.conf içinde geoip2 direktifini tanımlayacaksın. Bu direktif http bloğu içine giriyor.

# /etc/nginx/nginx.conf

load_module modules/ngx_http_geoip2_module.so;

http {
    # GeoIP2 veritabanı tanımlamaları
    geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
        $geoip2_metadata_country_build metadata build_epoch;
        $geoip2_data_country_code default=XX country iso_code;
        $geoip2_data_country_name country names en;
        $geoip2_data_country_name_tr country names tr;
    }

    geoip2 /usr/share/GeoIP/GeoLite2-City.mmdb {
        $geoip2_data_city_name default=Unknown city names en;
        $geoip2_data_region_name subdivisions 0 names en;
        $geoip2_data_region_code subdivisions 0 iso_code;
        $geoip2_data_timezone location time_zone;
        $geoip2_data_latitude location latitude;
        $geoip2_data_longitude location longitude;
    }

    geoip2 /usr/share/GeoIP/GeoLite2-ASN.mmdb {
        $geoip2_asn autonomous_system_number;
        $geoip2_org autonomous_system_organization;
    }

    # Ülke kodunu map ile kullan
    map $geoip2_data_country_code $allowed_country {
        default yes;
        RU no;
        CN no;
        KP no;
    }

    include /etc/nginx/conf.d/*.conf;
}

Burada dikkat etmen gereken nokta: $geoip2_data_country_code değişkeni IP adresini $remote_addr üzerinden alıyor. Eğer önünde bir load balancer veya CDN varsa gerçek IP’yi farklı bir başlıktan çekmen gerekecek. Bunu ileride ele alacağız.

Senaryo 1: İçerik Lokalizasyonu ve Dil Yönlendirmesi

Diyelim ki e-ticaret platformun var ve Türk kullanıcıları tr.example.com‘a, Almanya’dakileri de.example.com‘a yönlendirmek istiyorsun.

# /etc/nginx/conf.d/geo-redirect.conf

map $geoip2_data_country_code $target_domain {
    default         "www.example.com";
    TR              "tr.example.com";
    DE              "de.example.com";
    AT              "de.example.com";
    CH              "de.example.com";
    GB              "uk.example.com";
    US              "us.example.com";
    CA              "us.example.com";
}

server {
    listen 80;
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Zaten lokalize edilmiş domain'e gelenleri yönlendirme
    # Sadece ana domain'e gelenleri yönlendir
    set $do_redirect 1;

    # Cookie ile kullanıcı tercihi kaydet
    if ($cookie_preferred_region) {
        set $do_redirect 0;
    }

    # Bot trafiğini yönlendirme
    if ($http_user_agent ~* "(bot|crawler|spider|slurp|bingbot|googlebot)") {
        set $do_redirect 0;
    }

    location / {
        if ($do_redirect = 1) {
            return 302 https://$target_domain$request_uri;
        }

        proxy_pass http://backend_pool;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Country-Code $geoip2_data_country_code;
        proxy_set_header X-Country-Name $geoip2_data_country_name;
    }
}

Burada if bloklarının kullanımına dikkat et. Nginx’te if direktifi bazen beklenmedik davranışlar sergileyebilir. Nginx wiki’deki “If is Evil” makalesini mutlaka oku. Basit yönlendirme senaryolarında sorun çıkarmıyor ama karmaşık location bloklarıyla birleşince problem yaratabilir.

Senaryo 2: API Gateway ile Bölgesel Routing

Mikroservis mimarisinde farklı ülkelerdeki kullanıcıları farklı API cluster’larına yönlendirmek çok yaygın bir ihtiyaç. Mesela EU kullanıcılarının verilerinin AB sınırları içinde kalmasını zorunlu kılan GDPR uyumluluğu için bunu yapman gerekebilir.

# /etc/nginx/conf.d/api-geo-routing.conf

upstream api_eu {
    server api-eu-1.internal:8080 weight=5;
    server api-eu-2.internal:8080 weight=5;
    keepalive 32;
}

upstream api_us {
    server api-us-1.internal:8080 weight=5;
    server api-us-2.internal:8080 weight=5;
    keepalive 32;
}

upstream api_tr {
    server api-tr-1.internal:8080 weight=10;
    keepalive 16;
}

upstream api_default {
    server api-global-1.internal:8080;
    server api-global-2.internal:8080;
    keepalive 32;
}

# AB ülkelerini GDPR cluster'ına yönlendir
map $geoip2_data_country_code $api_backend {
    default     api_default;
    TR          api_tr;
    # EU ülkeleri
    AT          api_eu;
    BE          api_eu;
    BG          api_eu;
    CY          api_eu;
    CZ          api_eu;
    DE          api_eu;
    DK          api_eu;
    EE          api_eu;
    ES          api_eu;
    FI          api_eu;
    FR          api_eu;
    GR          api_eu;
    HR          api_eu;
    HU          api_eu;
    IE          api_eu;
    IT          api_eu;
    LT          api_eu;
    LU          api_eu;
    LV          api_eu;
    MT          api_eu;
    NL          api_eu;
    PL          api_eu;
    PT          api_eu;
    RO          api_eu;
    SE          api_eu;
    SI          api_eu;
    SK          api_eu;
    # Kuzey Amerika
    US          api_us;
    CA          api_us;
    MX          api_us;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    # Rate limiting zone'ları ülkeye göre farklılaştır
    limit_req_zone $binary_remote_addr zone=api_tr:10m rate=100r/s;
    limit_req_zone $binary_remote_addr zone=api_eu:10m rate=200r/s;

    location /api/ {
        proxy_pass http://$api_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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-Country-Code $geoip2_data_country_code;
        proxy_set_header X-Country-Name $geoip2_data_country_name;
        proxy_set_header X-Region $geoip2_data_region_name;
        proxy_set_header X-Timezone $geoip2_data_timezone;

        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;

        # GDPR compliance header
        add_header X-Data-Residency $api_backend always;
    }
}

Senaryo 3: Ülke Bazlı Erişim Kısıtlama

Bazı servisler için belirli ülkelerden gelen trafiği tamamen engellemen gerekebilir. Lisans kısıtlamaları, güvenlik politikaları ya da regülasyon gereklilikleri bunun başlıca nedenleri.

# /etc/nginx/conf.d/geo-block.conf

# Engellenen ülkeler listesi
geo $blocked_country {
    default 0;
    # Kuzey Kore
    # IP aralıkları manuel olarak da eklenebilir
    include /etc/nginx/geo/blocked_countries.conf;
}

# blocked_countries.conf örnek içeriği:
# 175.45.176.0/22 1;  # KP IP bloğu
# 210.52.109.0/24 1;

server {
    listen 443 ssl http2;
    server_name restricted.example.com;

    # Map ile engelleme kontrolü
    map $geoip2_data_country_code $is_blocked {
        default 0;
        KP 1;
        CU 1;
        IR 1;
    }

    location / {
        # Engellenen ülkelerden gelen trafiği reddet
        if ($is_blocked = 1) {
            return 451 "Unavailable For Legal Reasons";
        }

        # Sadece izin verilen ülkelere özel sayfayı göster
        proxy_pass http://backend;

        # Güvenlik loglaması
        access_log /var/log/nginx/geo_access.log combined;
    }

    # Engellenen ülkeler için custom error sayfası
    error_page 451 /geo-blocked.html;
    location = /geo-blocked.html {
        root /var/www/html;
        internal;
    }
}

HTTP 451 status kodu tam olarak bu senaryo için var: yasal nedenlerle erişilemeyen kaynaklar. 403 yerine 451 kullanmak hem daha semantik hem de şeffaflık açısından daha doğru bir pratik.

Load Balancer veya CDN Arkasındaki Gerçek IP Sorunu

Cloudflare, AWS ALB veya başka bir load balancer kullanıyorsan $remote_addr load balancer’ın IP’sini gösterecek. Gerçek istemci IP’sini almak için yapılandırmanı düzenlemelisin.

# /etc/nginx/nginx.conf içinde http bloğunda

http {
    # Güvenilir proxy IP'leri tanımla
    set_real_ip_from 103.21.244.0/22;   # Cloudflare
    set_real_ip_from 103.22.200.0/22;   # Cloudflare
    set_real_ip_from 103.31.4.0/22;     # Cloudflare
    set_real_ip_from 104.16.0.0/13;     # Cloudflare
    set_real_ip_from 104.24.0.0/14;     # Cloudflare
    set_real_ip_from 108.162.192.0/18;  # Cloudflare
    set_real_ip_from 131.0.72.0/22;     # Cloudflare
    set_real_ip_from 141.101.64.0/18;   # Cloudflare
    set_real_ip_from 162.158.0.0/15;    # Cloudflare
    set_real_ip_from 172.64.0.0/13;     # Cloudflare
    set_real_ip_from 173.245.48.0/20;   # Cloudflare
    set_real_ip_from 188.114.96.0/20;   # Cloudflare
    set_real_ip_from 190.93.240.0/20;   # Cloudflare
    set_real_ip_from 197.234.240.0/22;  # Cloudflare
    set_real_ip_from 198.41.128.0/17;   # Cloudflare

    # IPv6 Cloudflare
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2405:b500::/32;
    set_real_ip_from 2405:8100::/32;
    set_real_ip_from 2a06:98c0::/29;
    set_real_ip_from 2c0f:f248::/32;

    # AWS ALB için
    set_real_ip_from 10.0.0.0/8;
    set_real_ip_from 172.16.0.0/12;

    # Hangi header'dan okuyacağını belirt
    real_ip_header CF-Connecting-IP;
    # AWS ALB için: X-Forwarded-For
    # real_ip_header X-Forwarded-For;
    # real_ip_recursive on;

    geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
        $geoip2_data_country_code default=XX country iso_code;
        $geoip2_data_country_name country names en;
    }
}

Bu yapılandırmada real_ip_recursive on direktifi önemli: X-Forwarded-For başlığında birden fazla IP varsa en soldaki güvenilir olmayan IP’yi gerçek istemci IP’si olarak alır.

Performans Optimizasyonu ve Önbellekleme

GeoIP sorgularının performansa etkisi genellikle ihmal edilebilir seviyede olsa da yüksek trafikli sistemlerde bunu ciddiye almak gerekiyor.

# /etc/nginx/conf.d/geo-cache.conf

# Proxy önbelleğini ülkeye göre farklılaştır
proxy_cache_path /var/cache/nginx/tr levels=1:2 keys_zone=tr_cache:100m max_size=10g inactive=60m;
proxy_cache_path /var/cache/nginx/eu levels=1:2 keys_zone=eu_cache:100m max_size=10g inactive=60m;

map $geoip2_data_country_code $cache_zone {
    default eu_cache;
    TR      tr_cache;
}

map $geoip2_data_country_code $cache_key_prefix {
    default "EU";
    TR      "TR";
    US      "US";
}

server {
    listen 443 ssl http2;
    server_name content.example.com;

    location /static/ {
        proxy_pass http://backend;
        proxy_cache $cache_zone;
        # Cache key'e ülke kodunu ekle
        proxy_cache_key "$cache_key_prefix$request_uri";
        proxy_cache_valid 200 1h;
        proxy_cache_valid 404 1m;

        # Cache durumunu header'a ekle
        add_header X-Cache-Status $upstream_cache_status;
        add_header X-GeoIP-Country $geoip2_data_country_code;

        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
    }
}

Log Yapılandırması ve İzleme

Geo-based routing sorunlarını debug etmek için detaylı loglama şart.

# /etc/nginx/nginx.conf içinde

http {
    log_format geo_log '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        '"$http_referer" "$http_user_agent" '
                        'country="$geoip2_data_country_code" '
                        'city="$geoip2_data_city_name" '
                        'asn="$geoip2_asn" '
                        'org="$geoip2_org" '
                        'backend="$upstream_addr" '
                        'cache="$upstream_cache_status"';

    access_log /var/log/nginx/geo_access.log geo_log;

    # Ülke bazlı istatistik için ayrı log
    log_format country_stats '$geoip2_data_country_code $status $body_bytes_sent';
    access_log /var/log/nginx/country_stats.log country_stats;
}

Log analizini kolaylaştırmak için basit bir script:

#!/bin/bash
# /usr/local/bin/geo-stats.sh
# Son 1 saatin ülke bazlı trafik istatistiği

echo "=== Son 1 Saatin Ülke Bazlı İstatistikleri ==="
echo ""

# İstek sayısı ülke bazlı
echo "Top 15 Ülke (İstek Sayısı):"
awk '{print $1}' /var/log/nginx/country_stats.log | 
    sort | uniq -c | sort -rn | head -15 | 
    awk '{printf "  %-5s %s istekn", $2, $1}'

echo ""
echo "HTTP Durum Kodları Ülke Bazlı:"
awk '{print $1, $2}' /var/log/nginx/country_stats.log | 
    sort | uniq -c | sort -rn | head -20 | 
    awk '{printf "  %-5s %s -> %s istekn", $2, $3, $1}'

echo ""
echo "Toplam Trafik (Byte) Ülke Bazlı:"
awk '{bytes[$1]+=$3} END {for(c in bytes) print bytes[c], c}' 
    /var/log/nginx/country_stats.log | 
    sort -rn | head -10 | 
    awk '{printf "  %-5s %s bytesn", $2, $1}'

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

VPN ve Proxy Kullanıcıları

GeoIP veritabanları her zaman %100 doğru değil. Özellikle VPN kullanan kullanıcılar yanlış ülkede görünebilir. Bu durum için kullanıcılara manuel bölge seçimi seçeneği sun ve bu tercihi cookie’de sakla. Yukarıdaki $cookie_preferred_region kontrolü tam olarak bunun için.

IPv6 Adresleri

GeoLite2 veritabanları IPv6’yı destekliyor ama bazı IPv6 bloklarının coğrafi konumu belirsiz kalabiliyor. default=XX tanımını mutlaka yap, aksi halde değişken boş kalır ve beklenmedik davranışlar oluşur.

Veritabanı Güncellemeleri ve Nginx Reload

GeoIP veritabanı dosyalarını güncelledikten sonra Nginx’in yeni dosyayı kullanması için nginx -s reload komutunu çalıştırman gerekiyor. geoipupdate script’ine bunu otomatik olarak ekle:

#!/bin/bash
# /etc/cron.monthly/update-geoip.sh

/usr/bin/geoipupdate

# Güncelleme başarılı olduysa nginx'i yeniden yükle
if [ $? -eq 0 ]; then
    nginx -t && systemctl reload nginx
    echo "GeoIP veritabanı güncellendi ve Nginx yeniden yüklendi" | 
        logger -t geoip-update
else
    echo "GeoIP veritabanı güncelleme başarısız!" | 
        logger -t geoip-update -p user.err
fi

map vs if Tercihi

Performans açısından her zaman map direktifini tercih et. map direktifi lazy evaluation yapar ve sonuçları önbelleğe alır. if bloğu ise her istek için değerlendirilir. Büyük listeler için bu fark ciddi boyutlara ulaşabilir.

Sonuç

Nginx ile geo-based yönlendirme kurması görece kolay ama doğru yapılandırmak biraz incelik istiyor. Özetlemek gerekirse:

  • GeoIP2 modülünü kullan, legacy formatı bırak
  • Load balancer arkasındaysan gerçek IP tespitini mutlaka yapılandır
  • Yönlendirme mantığını if yerine map direktifiyle yönet
  • Kullanıcılara her zaman manuel bölge seçimi seçeneği sun, çünkü GeoIP yüzde yüz güvenilir değil
  • Bot trafiğini yönlendirme dışında tut, SEO açısından önemli
  • Veritabanı güncellemelerini otomatikleştir ve Nginx reload’ı buna dahil et
  • HTTP 451 kodunu yasal kısıtlamalar için doğru şekilde kullan

Production’a almadan önce birkaç farklı VPN ile test et, hangi ülkeden görünürsün kontrol et. Ayrıca curl -v -H "CF-Connecting-IP: 8.8.8.8" https://example.com gibi başlık manipülasyonuyla senaryolarını simüle edebilirsin. İyi yapılandırılmış bir geo-routing sistemi hem kullanıcı deneyimini iyileştirir hem de regülasyon uyumluluğunu kolaylaştırır.

Bir yanıt yazın

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