Nginx ile Lua Scripting ve OpenResty Kullanımı

Nginx’i saf haliyle kullanırken bir noktadan sonra “keşke burada biraz mantık ekleyebilseydim” diye düşünmüşsünüzdür. Rate limiting için, request manipülasyonu için ya da dinamik routing için ayrı bir uygulama katmanı çıkarmak zorunda kalmak can sıkıcı. İşte tam bu noktada OpenResty ve Lua devreye giriyor. Nginx’in performansını koruyarak içine gerçek bir scripting dili gömmüş oluyorsunuz.

OpenResty Nedir ve Neden Kullanılır?

OpenResty, Nginx’in üzerine inşa edilmiş, içine LuaJIT entegre edilmiş bir web platformudur. Çin’li geliştirici Yichun Zhang (agentzh) tarafından başlatılan bu proje, Nginx’in event-driven mimarisini bozmadan Lua scriptlerini doğrudan request lifecycle’ına dahil etmenizi sağlar.

Standart Nginx’ten farkı şudur: Nginx modül yazacaksanız C bilmeniz gerekir, derlemeniz gerekir ve production’da hata ayıklamanız bir kabus olur. OpenResty ile Lua yazıyorsunuz, interpreter zaten içeride, hot-reload benzeri workflows mümkün oluyor.

LuaJIT burada kritik bir detay. Standart Lua interpreter’ından 10-50 kat daha hızlı çalışabilen bu just-in-time compiler sayesinde Lua kodu neredeyse C hızında koşuyor. Bu yüzden OpenResty, yüksek trafikli sistemlerde bile tercih edilebilir bir seçenek.

Gerçek dünyada OpenResty şu senaryolarda çok işe yarıyor:

  • API Gateway olarak kullanım (rate limiting, auth, routing)
  • Request/response manipülasyonu
  • Redis ile entegre session yönetimi
  • Dinamik upstream seçimi
  • WAF (Web Application Firewall) ekleme
  • A/B testing altyapısı

OpenResty Kurulumu

Ubuntu/Debian üzerinde kurulum oldukça basit:

# OpenResty resmi reposunu ekle
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" 
    | sudo tee /etc/apt/sources.list.d/openresty.list

sudo apt update
sudo apt install openresty

# Servis durumunu kontrol et
sudo systemctl start openresty
sudo systemctl enable openresty
sudo systemctl status openresty

# Kurulum yolunu doğrula
/usr/local/openresty/nginx/sbin/nginx -v

CentOS/RHEL için:

# Repo ekle
sudo yum install yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

sudo yum install openresty openresty-resty

# PATH'e ekle
echo 'export PATH=/usr/local/openresty/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

OpenResty kurulduktan sonra yapılandırma dosyaları /usr/local/openresty/nginx/conf/ altında bulunur. Standart Nginx konfig sözdiziminin birebir aynısını kullanırsınız, sadece Lua direktifleri eklenir.

Lua Direktifleri ve Request Lifecycle

OpenResty, Nginx’in request processing aşamalarına karşılık gelen Lua direktifleri sunar. Bunları anlamak çok önemli:

  • init_by_lua_block: Worker process başlamadan önce, master process’te çalışır. Global değişken tanımları için idealdir
  • init_worker_by_lua_block: Her worker başladığında çalışır. Background timer’lar için kullanılır
  • set_by_lua_block: Nginx değişkeni set etmek için
  • rewrite_by_lua_block: URL rewrite aşamasında çalışır
  • access_by_lua_block: Erişim kontrolü için, upstream’e gitmeden önce
  • content_by_lua_block: Response içeriği üretmek için
  • header_filter_by_lua_block: Response header’larını modifiye etmek için
  • body_filter_by_lua_block: Response body’sini modifiye etmek için
  • log_by_lua_block: İstek tamamlandıktan sonra loglama için

İlk Lua Script: Hello World ve Temel Yapı

Basit bir başlangıç yapalım:

# /usr/local/openresty/nginx/conf/nginx.conf

worker_processes auto;
error_log /var/log/openresty/error.log;

events {
    worker_connections 1024;
}

http {
    # Lua modüllerinin aranacağı yollar
    lua_package_path '/usr/local/openresty/lualib/?.lua;;';
    lua_package_cpath '/usr/local/openresty/lualib/?.so;;';

    # Global Lua kodu - worker başlamadan önce çalışır
    init_by_lua_block {
        -- Lua yorumları iki tire ile başlar
        cjson = require "cjson"
        redis = require "resty.redis"
        ngx.log(ngx.INFO, "OpenResty başlatıldı, modüller yüklendi")
    }

    server {
        listen 80;
        server_name localhost;

        location /hello {
            content_by_lua_block {
                -- Request bilgilerini al
                local method = ngx.req.get_method()
                local uri = ngx.var.uri
                local remote_addr = ngx.var.remote_addr

                -- JSON response oluştur
                local response = {
                    message = "Merhaba OpenResty!",
                    method = method,
                    uri = uri,
                    client_ip = remote_addr,
                    timestamp = ngx.time()
                }

                ngx.header.content_type = "application/json"
                ngx.say(cjson.encode(response))
            }
        }
    }
}

Gerçek Senaryo 1: Rate Limiting ile API Koruması

Redis tabanlı sliding window rate limiting. Bu senaryoda her IP için dakikada maksimum istek sayısı koyuyoruz:

location /api/ {
    access_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        red:set_timeouts(1000, 1000, 1000) -- ms cinsinden

        -- Redis'e bağlan
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "Redis bağlantı hatası: ", err)
            -- Redis yoksa isteği geçir, sıkı davranmak istemiyorsak
            return
        end

        local client_ip = ngx.var.remote_addr
        local key = "rate_limit:" .. client_ip
        local limit = 60       -- dakikada maksimum istek
        local window = 60      -- saniye

        -- Atomic increment
        local count, err = red:incr(key)
        if err then
            ngx.log(ngx.ERR, "Redis INCR hatası: ", err)
            return
        end

        -- İlk istek ise expire set et
        if count == 1 then
            red:expire(key, window)
        end

        -- Kalan hakkı header'a yaz
        ngx.header["X-RateLimit-Limit"] = limit
        ngx.header["X-RateLimit-Remaining"] = math.max(0, limit - count)

        -- Limiti aştı mı?
        if count > limit then
            local ttl = red:ttl(key)
            ngx.header["Retry-After"] = ttl
            ngx.status = 429
            ngx.header.content_type = "application/json"
            ngx.say(cjson.encode({
                error = "Too Many Requests",
                retry_after = ttl,
                message = "Rate limit aşıldı, lütfen bekleyin"
            }))
            ngx.exit(429)
        end

        -- Connection pool'a geri koy
        red:set_keepalive(10000, 100)
    }

    proxy_pass http://backend_upstream;
}

Bu yaklaşımın güzel tarafı, ayrı bir rate limiting servisi çalıştırmak zorunda kalmıyorsunuz. Nginx’in kendisi, Redis ile konuşarak bu işi hallediyor.

Gerçek Senaryo 2: JWT Token Doğrulama

API gateway olarak kullanıldığında en sık ihtiyaç duyulan şeylerden biri JWT doğrulama. OpenResty ile bunu upstream uygulamaya bırakmadan Nginx katmanında yapabilirsiniz:

-- /usr/local/openresty/lualib/jwt_auth.lua

local cjson = require "cjson"
local hmac = require "resty.hmac"

local _M = {}

-- Base64 URL decode
local function base64url_decode(input)
    local remainder = #input % 4
    if remainder == 2 then
        input = input .. "=="
    elseif remainder == 3 then
        input = input .. "="
    end
    input = input:gsub("-", "+"):gsub("_", "/")
    return ngx.decode_base64(input)
end

function _M.verify(token, secret)
    if not token then
        return nil, "Token bulunamadı"
    end

    -- Bearer prefix'ini kaldır
    token = token:match("Bearer%s+(.+)") or token

    -- Token parçalarına ayır
    local parts = {}
    for part in token:gmatch("[^.]+") do
        table.insert(parts, part)
    end

    if #parts ~= 3 then
        return nil, "Geçersiz token formatı"
    end

    local header_str = base64url_decode(parts[1])
    local payload_str = base64url_decode(parts[2])

    if not header_str or not payload_str then
        return nil, "Base64 decode hatası"
    end

    -- İmzayı doğrula
    local signing_input = parts[1] .. "." .. parts[2]
    local h = hmac:new(secret, hmac.ALGOS.SHA256)
    local expected_sig = ngx.encode_base64(h:final(signing_input))
        :gsub("+", "-"):gsub("/", "_"):gsub("=", "")

    if expected_sig ~= parts[3] then
        return nil, "İmza geçersiz"
    end

    -- Payload'u parse et
    local ok, payload = pcall(cjson.decode, payload_str)
    if not ok then
        return nil, "Payload parse hatası"
    end

    -- Expiry kontrolü
    if payload.exp and payload.exp < ngx.time() then
        return nil, "Token süresi dolmuş"
    end

    return payload, nil
end

return _M

Bu modülü nginx.conf içinde şöyle kullanırsınız:

http {
    lua_package_path '/usr/local/openresty/lualib/?.lua;;';

    # JWT secret'ı environment'tan al
    env JWT_SECRET;

    init_by_lua_block {
        cjson = require "cjson"
        jwt_auth = require "jwt_auth"
        JWT_SECRET = os.getenv("JWT_SECRET") or "default-secret-degistir"
    }

    server {
        listen 80;

        # Korumalı endpoint
        location /api/protected/ {
            access_by_lua_block {
                local auth_header = ngx.req.get_headers()["Authorization"]

                local payload, err = jwt_auth.verify(auth_header, JWT_SECRET)

                if err then
                    ngx.status = 401
                    ngx.header.content_type = "application/json"
                    ngx.say(cjson.encode({
                        error = "Unauthorized",
                        message = err
                    }))
                    ngx.exit(401)
                end

                -- Kullanıcı bilgisini header olarak backend'e ilet
                ngx.req.set_header("X-User-ID", payload.sub)
                ngx.req.set_header("X-User-Role", payload.role or "user")
            }

            proxy_pass http://backend;
        }
    }
}

Gerçek Senaryo 3: Dinamik Upstream Seçimi

Canary deployment veya A/B testing için upstream’i dinamik olarak seçmek isteyebilirsiniz:

upstream backend_stable {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    keepalive 32;
}

upstream backend_canary {
    server 10.0.0.3:8080;
    keepalive 16;
}

server {
    listen 80;

    location / {
        set_by_lua_block $upstream_target {
            -- Canary cookie varsa canary'ye gönder
            local cookie_canary = ngx.var.cookie_canary
            if cookie_canary == "true" then
                return "backend_canary"
            end

            -- User-Agent'a göre karar ver (test araçları için)
            local user_agent = ngx.req.get_headers()["User-Agent"] or ""
            if user_agent:find("CanaryBot") then
                return "backend_canary"
            end

            -- %10 kullanıcıyı canary'ye at
            local user_id = ngx.var.cookie_user_id
            if user_id then
                -- Kullanıcı ID'sinin son rakamına bak
                local last_digit = tonumber(user_id:sub(-1)) or 0
                if last_digit == 0 then  -- %10 ihtimal
                    return "backend_canary"
                end
            end

            return "backend_stable"
        }

        proxy_pass http://$upstream_target;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Shared Memory ve Cache Yönetimi

OpenResty’nin güçlü özelliklerinden biri lua_shared_dict. Tüm worker process’leri arasında paylaşılan, in-memory dictionary. Redis olmadan basit cache senaryolarını burada çözebilirsiniz:

http {
    # Shared memory zone tanımla - 10MB
    lua_shared_dict app_cache 10m;
    lua_shared_dict rate_counters 5m;

    server {
        location /cached-data {
            content_by_lua_block {
                local cache = ngx.shared.app_cache
                local cache_key = "data:" .. ngx.var.uri

                -- Cache'den oku
                local cached = cache:get(cache_key)
                if cached then
                    ngx.header["X-Cache"] = "HIT"
                    ngx.header.content_type = "application/json"
                    ngx.say(cached)
                    return
                end

                -- Cache miss, veriyi hesapla/çek
                ngx.header["X-Cache"] = "MISS"

                -- Burada normalde DB sorgusu veya API çağrısı olurdu
                local data = {
                    result = "hesaplanmış veri",
                    generated_at = ngx.time(),
                    worker_pid = ngx.worker.pid()
                }

                local json_data = cjson.encode(data)

                -- 30 saniye cache'le
                local success, err, forcible = cache:set(cache_key, json_data, 30)
                if not success then
                    ngx.log(ngx.WARN, "Cache set hatası: ", err)
                end

                -- forcible true ise eski veriler silindi (memory dolu)
                if forcible then
                    ngx.log(ngx.WARN, "Cache memory doldu, eski veriler silindi")
                end

                ngx.header.content_type = "application/json"
                ngx.say(json_data)
            }
        }
    }
}

Loglama ve Debugging

Production’da debug etmek için özel log formatları ve Lua tabanlı loglama çok işe yarıyor:

http {
    # Özel log formatı - JSON
    log_format json_log escape=json
        '{'
            '"time":"$time_iso8601",'
            '"remote_addr":"$remote_addr",'
            '"method":"$request_method",'
            '"uri":"$uri",'
            '"status":$status,'
            '"bytes_sent":$bytes_sent,'
            '"request_time":$request_time,'
            '"upstream_time":"$upstream_response_time",'
            '"user_agent":"$http_user_agent",'
            '"request_id":"$request_id"'
        '}';

    server {
        # Her isteğe unique ID ekle
        set_by_lua_block $request_id {
            return string.format("%016x", math.random(0, 2^53))
        }

        location / {
            # Response header'a request ID ekle
            header_filter_by_lua_block {
                ngx.header["X-Request-ID"] = ngx.var.request_id
            }

            # Request tamamlandıktan sonra detaylı log
            log_by_lua_block {
                local latency = tonumber(ngx.var.request_time) or 0

                -- Yavaş requestleri ayrıca logla
                if latency > 1.0 then
                    ngx.log(ngx.WARN, string.format(
                        "YAVAŞ REQUEST - ID: %s, URI: %s, Süre: %.3fs, Status: %s",
                        ngx.var.request_id,
                        ngx.var.uri,
                        latency,
                        ngx.var.status
                    ))
                end

                -- 5xx hataları için alert log
                local status = tonumber(ngx.var.status) or 0
                if status >= 500 then
                    ngx.log(ngx.ERR, string.format(
                        "SERVER ERROR - ID: %s, URI: %s, Status: %d",
                        ngx.var.request_id,
                        ngx.var.uri,
                        status
                    ))
                end
            }

            proxy_pass http://backend;
        }
    }
}

OPM ile Lua Modül Yönetimi

OpenResty Package Manager (OPM), Lua modüllerini yönetmek için kullanılır. npm veya pip benzeri bir araç:

# OPM kurulumu (OpenResty ile genellikle gelir)
which opm

# Popüler modülleri kur
opm get bungle/lua-resty-http          # HTTP client
opm get ledgetech/lua-resty-redis-connector
opm get SkyLothar/lua-resty-jwt        # JWT kütüphanesi
opm get pintsized/lua-resty-crypto     # Kriptografi

# Kurulu modülleri listele
opm list

# Alternatif: LuaRocks paket yöneticisi
luarocks install lua-cjson
luarocks install luasocket

Popüler OpenResty kütüphaneleri şunlar:

  • resty.http: Lua’dan HTTP istekleri atmak için
  • resty.redis: Redis bağlantısı için
  • resty.mysql: MySQL bağlantısı için
  • resty.jwt: JWT işlemleri için
  • resty.template: HTML template rendering için
  • resty.lock: Distributed locking için

Performans İpuçları ve Yaygın Hatalar

Connection pool kullanmayı unutmayın. Redis veya MySQL’e her istekte yeni bağlantı açmak performansı mahveder:

# Yanlış
local red = redis:new()
red:connect("127.0.0.1", 6379)
-- ... kullan
red:close()  # Bağlantıyı kapat - YANLIŞ

# Doğru
local red = redis:new()
red:connect("127.0.0.1", 6379)
-- ... kullan
red:set_keepalive(10000, 100)  # Pool'a geri koy - DOĞRU

Blocking I/O yapmayın. Standart Lua io kütüphanesi, dosya okuma gibi işlemler Nginx’in event loop’unu bloklar. OpenResty’nin ngx.io fonksiyonlarını kullanın veya bunları init_by_lua_block içinde yapın.

pcall ile hata yönetimi yapın. Lua’da unhandled exception worker’ı çökertemez ama isteği mahveder:

# Güvenli JSON parse
local ok, data = pcall(cjson.decode, raw_json)
if not ok then
    ngx.log(ngx.ERR, "JSON parse hatası: ", data)
    ngx.exit(400)
    return
end

ngx.shared.dict boyutlandırmasına dikkat edin. Çok küçük tutarsanız sürekli eviction olur ve cache verimliliği düşer. ngx.shared.dict:info() ile doluluk oranını takip edin.

Test Ortamı ve CI/CD Entegrasyonu

OpenResty tabanlı scriptleri test etmek için Test::Nginx framework’ü kullanılır:

# Test::Nginx kurulumu
cpan Test::Nginx

# Basit bir test dosyası
# t/rate_limit.t
use Test::Nginx::Socket 'no_plan';
run_tests();

__DATA__

=== TEST 1: Normal istek rate limit aşmıyor
--- config
location /api {
    access_by_lua_file /path/to/rate_limit.lua;
    return 200 "ok";
}
--- request
GET /api
--- response_headers
X-RateLimit-Limit: 60
--- error_code: 200
# Testleri çalıştır
prove -r t/

# OpenResty ile birlikte gelen resty CLI ile script test et
resty -e "
    local cjson = require 'cjson'
    local data = {name='test', value=42}
    print(cjson.encode(data))
"

Sonuç

OpenResty ve Lua kombinasyonu, Nginx’i statik bir web sunucusu ya da basit bir reverse proxy olmaktan çıkarıp gerçek anlamda programlanabilir bir platform haline getiriyor. Rate limiting, authentication, caching, A/B testing gibi cross-cutting concern’leri uygulama kodundan çekip infrastructure seviyesine taşıyabiliyorsunuz.

Özellikle mikroservis mimarilerinde API gateway ihtiyacı için ayrı bir servis ayağa kaldırmak yerine OpenResty tabanlı bir çözüm ciddi kaynak tasarrufu sağlayabilir. Kong ve APISIX gibi popüler API gateway ürünleri de tam olarak bu OpenResty altyapısı üzerine kurulu.

Başlangıç için önerim şu yolu izleyin: Önce mevcut Nginx kurulumlarınızdan birini OpenResty’ye taşıyın, basit bir rate limiting veya JWT doğrulama scripti yazın, shared dict ile bir şeyler cache’leyin. LuaJIT’in hızını, kodun okunabilirliğini ve deployment kolaylığını görünce neden bu kadar kullanıcısının olduğunu anlayacaksınız.

Lua öğrenmek için fazla zaman harcamanıza gerek yok, söz dizimi çok temiz. OpenResty wiki’si ve GitHub’daki lua-resty-* repoları başlamak için mükemmel kaynaklar.

Yorum yapın