Nginx map Modülü ile Dinamik Değişken Yönetimi

Nginx konfigürasyonunu yönetirken en çok canımızı sıkan şeylerden biri, benzer ama hafifçe farklı davranışlar için tekrarlayan if blokları yazmak zorunda kalmaktır. Bir isteğin nereye yönlendirileceğini, hangi başlığın ekleneceğini ya da hangi backend’in kullanılacağını belirlemek için onlarca if yazıyorsunuz ve bir bakıyorsunuz konfigürasyon dosyası okunaksız bir hal almış. İşte bu noktada map modülü devreye giriyor ve işleri dramatik biçimde sadeleştiriyor.

map Modülü Nedir ve Neden Kullanmalısınız?

Nginx’in ngx_http_map_module modülü, bir değişkenin değerine göre başka bir değişken üretmenizi sağlar. Basitçe söylemek gerekirse, bir giriş değeri alır ve buna karşılık gelen bir çıkış değeri döndürür. Bunu bir anahtar-değer tablosuna bakıyor gibi düşünebilirsiniz.

if bloklarının aksine, map direktifi http bloğu seviyesinde tanımlanır ve sadece gerçekten ihtiyaç duyulduğunda değerlendirilir. Bu lazy evaluation yaklaşımı performans açısından büyük avantaj sağlar. Üstelik map direktifleri server veya location bloklarının içinde değil, dışında tanımlandığı için konfigürasyonunuz çok daha temiz görünür.

Nginx dokümantasyonunda ünlü bir uyarı vardır: “if is evil” (if kötüdür). Bu tam doğru değil elbette, ama if bloklarının bazı durumlarda beklenmedik davranışlar sergilediği gerçek. map ise bu tuzaklardan kaçınmanın en temiz yollarından biri.

Temel Sözdizimi

map direktifinin temel yapısı oldukça sezgiseldir:

http {
    map $kaynak_degisken $hedef_degisken {
        varsayilan_deger    varsayilan_cikti;
        eslesme_degeri1     cikti1;
        eslesme_degeri2     cikti2;
    }
}

Birkaç önemli nokta:

  • map: Her zaman http bloğu içinde, server bloğunun dışında tanımlanır
  • $kaynak_degisken: Eşleştirme yapılacak girdi değişkeni
  • $hedef_degisken: Sonucun yazılacağı yeni değişken
  • default: Hiçbir kural eşleşmediğinde kullanılacak değer

Şimdi gerçek dünyadan senaryolara geçelim.

Senaryo 1: Cihaz Tipine Göre Yönlendirme

E-ticaret platformlarında mobil ve masaüstü kullanıcılara farklı deneyimler sunmak yaygın bir ihtiyaçtır. User-Agent başlığına bakarak cihaz tipini tespit edebilir ve buna göre farklı upstream’lere yönlendirebilirsiniz:

http {
    map $http_user_agent $mobil_mi {
        default          0;
        "~*mobile"       1;
        "~*android"      1;
        "~*iphone"       1;
        "~*ipad"         1;
        "~*tablet"       1;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://$mobil_mi == "1" ? mobile_backend : desktop_backend;
            # Veya daha temiz kullanim:
            set $backend "desktop_backend";
            if ($mobil_mi) {
                set $backend "mobile_backend";
            }
            proxy_pass http://$backend;
        }
    }
}

Daha temiz bir yaklaşım için map içinde doğrudan upstream adını üretmek daha iyidir:

http {
    map $http_user_agent $backend_pool {
        default          desktop_backend;
        "~*mobile"       mobile_backend;
        "~*android"      mobile_backend;
        "~*iphone"       mobile_backend;
        "~*ipad"         tablet_backend;
    }

    upstream desktop_backend {
        server 10.0.0.1:8080;
        server 10.0.0.2:8080;
    }

    upstream mobile_backend {
        server 10.0.0.3:8080;
        server 10.0.0.4:8080;
    }

    upstream tablet_backend {
        server 10.0.0.5:8080;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://$backend_pool;
            proxy_set_header X-Device-Type $backend_pool;
        }
    }
}

Bu yapıda User-Agent değeri otomatik olarak doğru backend’e yönlendiriyor. Yeni bir cihaz tipi eklemek istediğinizde sadece map bloğuna bir satır eklemeniz yeterli.

Senaryo 2: Ülkeye Göre İçerik Dili ve Yönlendirme

GeoIP modülüyle birlikte map kullanmak, coğrafi yönlendirme senaryolarında çok güçlü bir kombinasyon oluşturur. Diyelim ki $geoip_country_code değişkenine sahipsiniz:

http {
    # Ulke koduna gore dil belirleme
    map $geoip_country_code $site_dili {
        default    en;
        TR         tr;
        DE         de;
        FR         fr;
        ES         es;
        AR         ar;
        SA         ar;
        AE         ar;
    }

    # Ulke koduna gore para birimi
    map $geoip_country_code $para_birimi {
        default    USD;
        TR         TRY;
        DE         EUR;
        FR         EUR;
        GB         GBP;
        JP         JPY;
    }

    # Bazi ulkeler icin ozel domain'e yonlendirme
    map $geoip_country_code $yonlendirme_hedefi {
        default    "";
        CN         "https://cn.example.com";
        RU         "https://ru.example.com";
    }

    server {
        listen 80;
        server_name example.com;

        # Yonlendirme gerekiyorsa gonder
        if ($yonlendirme_hedefi) {
            return 301 $yonlendirme_hedefi$request_uri;
        }

        location / {
            proxy_pass http://app_backend;
            proxy_set_header X-Site-Language $site_dili;
            proxy_set_header X-Currency $para_birimi;
        }
    }
}

Burada önemli bir nokta: Birden fazla map bloğu tanımlayarak farklı amaçlar için farklı değişkenler üretiyorsunuz. Her map bağımsız çalışır ve sadece ihtiyaç duyulduğunda değerlendirilir.

Senaryo 3: Rate Limiting için Dinamik Sınırlar

API gateway senaryolarında farklı müşterilere farklı rate limit uygulamanız gerekebilir. Premium müşteriler daha yüksek limitlerden faydalanmalı:

http {
    # API anahtarina gore rate limit zone secimi
    map $http_x_api_key $rate_limit_zone {
        default              "standard_zone";
        "premium_key_abc123" "premium_zone";
        "premium_key_def456" "premium_zone";
        "enterprise_key_xyz" "enterprise_zone";
        "internal_service"   "internal_zone";
    }

    # Rate limit zone'larini tanimla
    limit_req_zone $binary_remote_addr zone=standard_zone:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=premium_zone:10m rate=100r/s;
    limit_req_zone $binary_remote_addr zone=enterprise_zone:10m rate=1000r/s;
    limit_req_zone $binary_remote_addr zone=internal_zone:10m rate=10000r/s;

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

        location /api/ {
            limit_req zone=$rate_limit_zone burst=20 nodelay;
            proxy_pass http://api_backend;
        }
    }
}

Bu yaklaşım tek tek müşterileri yönetmek için biraz kaba, ama burada asıl mesaj map ile dinamik zone seçimi yapılabildiği. Gerçek dünyada bunu daha çok müşteri tier’ına göre uygularsınız.

Senaryo 4: A/B Test Yönetimi

Canary deployment veya A/B test senaryolarında belirli kullanıcıları yeni versiyona yönlendirmek istersiniz. Cookie değerine göre bu kararı map ile verebilirsiniz:

http {
    # A/B test cookie'sine gore backend secimi
    map $cookie_ab_test $ab_backend {
        default    "production";
        "variant_a" "production";
        "variant_b" "canary";
        "beta"      "canary";
    }

    # Backend havuzlarini tanimla
    upstream production {
        server 10.0.1.1:8080 weight=9;
        server 10.0.1.2:8080 weight=9;
    }

    upstream canary {
        server 10.0.2.1:8080;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://$ab_backend;
            add_header X-Served-By $ab_backend;
            
            # Yoksa cookie set et
            add_header Set-Cookie "ab_test=variant_a; Path=/; Max-Age=86400" always;
        }
    }
}

Daha gelişmiş bir yaklaşımda split_clients modülüyle birlikte kullanarak yüzdesel dağılım da yapabilirsiniz, ama map tabanlı yaklaşım cookie veya başlık değerine dayalı deterministik yönlendirme için mükemmeldir.

Senaryo 5: Güvenlik Başlıklarının Dinamik Yönetimi

HTTPS ve HTTP istekler için farklı güvenlik başlıkları, farklı ortamlar için farklı CORS politikaları… Bunları map ile çok temiz yönetebilirsiniz:

http {
    # Izin verilen origin'lere gore CORS basligini ayarla
    map $http_origin $cors_origin {
        default                     "";
        "https://app.example.com"   "https://app.example.com";
        "https://admin.example.com" "https://admin.example.com";
        "https://partner.firma.com" "https://partner.firma.com";
        "~^https://.*.example.com$" $http_origin;
    }

    # HTTPS mi HTTP mi?
    map $scheme $hsts_baslik {
        default  "";
        https    "max-age=31536000; includeSubDomains; preload";
    }

    # Gelistirme ortami mi?
    map $host $csp_politikasi {
        default                 "default-src 'self'; script-src 'self'";
        "dev.example.com"       "default-src 'self' 'unsafe-inline' 'unsafe-eval'";
        "staging.example.com"   "default-src 'self' 'unsafe-inline'";
    }

    server {
        listen 443 ssl;
        server_name *.example.com;

        location / {
            proxy_pass http://app_backend;

            # Kosullu CORS basliklarini ekle
            if ($cors_origin) {
                add_header Access-Control-Allow-Origin $cors_origin always;
                add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
                add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
            }

            # Dinamik HSTS
            if ($hsts_baslik) {
                add_header Strict-Transport-Security $hsts_baslik always;
            }

            # Ortama ozgu CSP
            add_header Content-Security-Policy $csp_politikasi always;
        }
    }
}

Senaryo 6: Log Formatını Dinamik Olarak Ayarlama

Bazı endpoint’lerin çok daha detaylı loglanması, bazılarının ise log dosyasını şişirmemesi için hiç loglanmaması gerekebilir. Özellikle health check endpoint’leri buna güzel bir örnek:

http {
    # Bazi path'leri loglama
    map $request_uri $log_yap {
        default                     1;
        "~^/health"                 0;
        "~^/ping"                   0;
        "~^/metrics"                0;
        "~^/favicon.ico"           0;
        "~^/robots.txt"            0;
        "~*.(css|js|png|jpg|gif)" 0;
    }

    # Hassas endpoint'ler icin farkli log format
    map $request_uri $log_formati {
        default             "standart";
        "~^/api/auth"       "guvenlik";
        "~^/api/payment"    "guvenlik";
        "~^/admin"          "guvenlik";
    }

    log_format standart '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        '"$http_referer" "$http_user_agent"';

    log_format guvenlik '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        '"$http_referer" "$http_user_agent" '
                        'rt=$request_time upstream_rt=$upstream_response_time '
                        'x_forwarded_for="$http_x_forwarded_for"';

    server {
        listen 80;
        server_name example.com;

        access_log /var/log/nginx/access.log standart if=$log_yap;
        access_log /var/log/nginx/security.log guvenlik if=$log_formati;

        location / {
            proxy_pass http://app_backend;
        }
    }
}

Bu konfigürasyon sayesinde saniyede binlerce health check isteği log dosyanızı kirletmez ve güvenlik açısından kritik endpoint’ler ayrı bir log dosyasına yazılır.

Gelişmiş map Özellikleri

İç İçe map Kullanımı

Bir map çıktısını başka bir map‘e girdi olarak verebilirsiniz. Bu zincirleme yaklaşım karmaşık mantığı basamaklara bölmenizi sağlar:

http {
    # Once tarayici ailesini belirle
    map $http_user_agent $tarayici_ailesi {
        default     "other";
        "~*MSIE"    "ie";
        "~*Trident" "ie";
        "~*Firefox" "firefox";
        "~*Chrome"  "chrome";
        "~*Safari"  "safari";
    }

    # Sonra tarayici ailesine gore desteklenen ozellikleri belirle
    map $tarayici_ailesi $webp_destegi {
        default   1;
        ie        0;
        safari    0;
        other     0;
    }

    map $tarayici_ailesi $http2_push_aktif {
        default   1;
        ie        0;
        other     0;
    }

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

        location ~* .(jpg|jpeg|png)$ {
            # WebP destegi varsa WebP versiyonunu sun
            set $resim_uzantisi "jpg";
            if ($webp_destegi) {
                set $resim_uzantisi "webp";
            }
            try_files $uri.$resim_uzantisi $uri =404;
        }
    }
}

Regex Eşleştirme

map direktifinde ~ (büyük/küçük harf duyarlı) ve ~* (büyük/küçük harf duyarsız) regex kullanabilirsiniz. Eşleştirme sırası şöyledir:

  • Tam eşleşmeler önce kontrol edilir
  • ~ ve ~* ile başlayan regex’ler sırayla kontrol edilir
  • default en son devreye girer
http {
    map $uri $cache_suresi {
        default                     "no-cache";
        "~*.(css|js)$"            "public, max-age=31536000, immutable";
        "~*.(png|jpg|jpeg|gif|webp|svg)$" "public, max-age=2592000";
        "~*.(woff|woff2|ttf|eot)$" "public, max-age=31536000";
        "~^/api/"                   "no-store, no-cache";
        "/index.html"               "no-cache, must-revalidate";
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            root /var/www/html;
            add_header Cache-Control $cache_suresi;
            try_files $uri $uri/ /index.html;
        }
    }
}

hostnames Parametresi

map bloğuna hostnames parametresi ekleyerek server_name mantığına benzer wildcard domain eşleştirmesi yapabilirsiniz:

http {
    map $host $site_ortami {
        hostnames;
        default                 production;
        "*.dev.example.com"     development;
        "*.staging.example.com" staging;
        "localhost"             development;
        "127.0.0.1"             development;
    }

    map $site_ortami $hata_ayrinti {
        default      0;
        development  1;
        staging      1;
    }

    server {
        listen 80;
        server_name *.example.com localhost;

        location / {
            proxy_pass http://app_backend;
            proxy_set_header X-Environment $site_ortami;
            proxy_set_header X-Show-Debug $hata_ayrinti;
        }
    }
}

volatile ve include Kullanımı

Büyük projelerde map değerlerini harici dosyalara taşımak konfigürasyonu yönetilebilir kılar. Özellikle IP beyaz listeleri, API anahtarları gibi sık değişen veriler için idealdir:

http {
    # Buyuk IP listelerini disari al
    map $remote_addr $ip_izinli {
        default     0;
        include     /etc/nginx/maps/izinli_ipler.map;
        include     /etc/nginx/maps/engellenen_ipler.map;
    }
}

/etc/nginx/maps/izinli_ipler.map dosyasının içeriği:

# Bu dosya otomatik guncellenir, elle duzenlemeyin
10.0.0.0/8     1;
192.168.1.100  1;
172.16.0.0/12  1;

Bu yaklaşımla IP listelerini bir script ile güncelleyip nginx -s reload yapabilirsiniz. Ana konfigürasyon dosyasına dokunmanıza gerek kalmaz.

Yaygın Hatalar ve Dikkat Edilmesi Gerekenler

Nginx map kullanırken sıklıkla karşılaşılan bazı sorunlar var:

  • map bloğunu server içine koymak: map direktifi mutlaka http bloğu seviyesinde olmalı. server veya location içine koyarsanız Nginx başlamaz.
  • Boş string ile default karışıklığı: default "" tanımladığınızda değişken var ama boş, tanımlamadığınızda değişken hiç set edilmemiş demektir. if bloklarında bu iki durum farklı davranabilir.
  • Regex performansı: Çok sayıda regex içeren map bloklarında performans düşebilir. Mümkünse tam eşleşme kullanın, regex’leri kaçınılmaz durumlara saklayın.
  • Değişken adı çakışmaları: Mevcut Nginx değişkenlerinin adını map çıktısı için kullanmayın. $host, $uri gibi yerleşik değişkenler var ve bunları ezmek beklenmedik sonuçlar doğurur.

Konfigürasyonu test etmek için her zaman:

nginx -t

komutunu çalıştırın. map sözdizimi hatalarını bu şekilde yakalayabilirsiniz. Daha ayrıntılı debug için:

nginx -T | grep -A 20 "map $"

komutu tüm include’ları açarak birleşik konfigürasyonu gösterir.

Performans Notları

map modülünün lazy evaluation özelliği Nginx’in neden bu direktifi tercih ettiğini anlatır. Bir request geldiğinde tüm map blokları hemen değerlendirilmez. Bir map değişkeni ilk kez kullanıldığı anda hesaplanır ve o request boyunca cache’lenir. Tanımladığınız ama hiç kullanmadığınız map değişkenleri herhangi bir CPU zamanı harcamaz.

Bununla birlikte büyük map blokları için Nginx hash tablosu boyutunu ayarlamak gerekebilir:

http {
    map_hash_bucket_size 128;
    map_hash_max_size 4096;
    
    # Cok sayida giris iceren map
    map $http_x_customer_id $musteri_plani {
        default     "free";
        include     /etc/nginx/maps/musteri_planlari.map;
    }
}

map_hash_bucket_size değerini artırmak uzun string değerleri olan map blokları için gereklidir. Nginx bu konuda uyarı verir, log dosyalarını takip edin.

Sonuç

Nginx map modülü, web sunucusu konfigürasyonunuzu gereksiz if bloklarından arındırmanın ve dinamik davranışları temiz bir şekilde ifade etmenin en etkili yollarından biri. Cihaz tespiti, coğrafi yönlendirme, A/B test yönetimi, güvenlik başlıkları, log optimizasyonu gibi birbirinden farklı onlarca kullanım alanı var.

Özellikle şunu vurgulamak isterim: map sadece teknik bir araç değil, aynı zamanda bir mimari yaklaşım. Konfigürasyonunuzdaki karar mantığını veri olarak ifade edebiliyorsunuz. Bir müşterinin planını değiştirmek için konfigürasyon mantığına dokunmak yerine sadece map dosyasını güncelleyebiliyorsunuz. Bu ayrım, uzun vadede hem bakım kolaylığı hem de hata riskinin azalması anlamına geliyor.

Projenizde if bloklarından oluşan uzun zincirler görüyorsanız, büyük ihtimalle bunların önemli bir kısmı map ile çok daha temiz yazılabilir. Nginx dokümantasyonunu karıştırmak yerine mevcut konfigürasyonunuzu gözden geçirip dönüştürmeye başlayın. Birkaç saatlik bir çalışmayla hem okunabilirliği artırabilir hem de kendinize ileride çok teşekkür edeceksiniz.

Yorum yapın