Caddy ile Handle ve Route Direktifleri ile Gelişmiş Yönlendirme

Modern web altyapılarında tek bir sunucu üzerinde onlarca farklı servis çalıştırmak artık olağan bir durum. Microservice mimarileri, API gateway’ler, statik site hosting ve reverse proxy kombinasyonları… Bunların hepsini tek bir yapılandırma dosyasında yönetmek ciddi bir karmaşıklık yaratıyor. İşte tam bu noktada Caddy’nin handle ve route direktifleri devreye giriyor. Bu iki direktif, trafik yönlendirme mantığını son derece okunabilir ve esnek bir şekilde kurmanıza imkan tanıyor. Bu yazıda gerçek dünya senaryoları üzerinden bu direktifleri derinlemesine inceleyeceğiz.

Caddy’nin Yönlendirme Mantığını Anlamak

Caddy’yi ilk kez yapılandırmaya başlayanların en çok kafasının karıştığı konu, direktiflerin nasıl önceliklendirildiğidir. Nginx’te location bloklarına alışkın olanlar için Caddy’nin yaklaşımı başta biraz garip gelebilir.

Caddy, Caddyfile’daki direktifleri yazıldıkları sıraya göre değil, kendi iç öncelik sırasına göre işler. Bu tasarım kararı aslında çok mantıklı; çünkü yazım sırasına bağımlı yapılandırmalar bakımı zorlaştırır ve ince hatalara davetiye çıkarır. Ancak bu durum, bazen istediğiniz sırayı zorla belirlemeniz gerektiğinde sorun yaratır. İşte route direktifi tam olarak bu problemi çözüyor.

handle direktifi ise bir tür “eğer bu path’e gelirse, şunu yap ve dur” mantığı kurmanızı sağlıyor. Nginx’teki location bloklarına en yakın Caddy karşılığı olarak düşünebilirsiniz.

Handle Direktifi: Temel Kullanım

handle direktifinin en basit formu bir path matcher ile birlikte kullanılır. Bir isteği yakaladığınızda, o blok içindeki direktifler uygulanır ve Caddy başka bir handle bloğuna geçmez.

# /etc/caddy/Caddyfile

example.com {
    handle /api/* {
        reverse_proxy localhost:8080
    }

    handle /static/* {
        root * /var/www/static
        file_server
    }

    handle {
        reverse_proxy localhost:3000
    }
}

Burada dikkat edilmesi gereken önemli bir nokta var: En sonda yer alan handle bloğu herhangi bir path matcher almamış. Bu, “diğer hiçbir blok bu isteği yakalamamışsa buraya gel” anlamına geliyor. Nginx’teki location / ile aynı rolü üstleniyor.

Handle ile Varsayılan Fallback Mantığı

Gerçek bir senaryoya bakalım. Diyelim ki bir e-ticaret sitesi yönetiyorsunuz. Ana frontend React uygulaması, /api altı Go ile yazılmış bir backend, /admin ise ayrı bir Node.js uygulaması. Tüm bunları tek bir Caddyfile’da şöyle yönetebilirsiniz:

shop.example.com {
    # Admin paneli - sadece belirli IP'lerden erişilebilir
    handle /admin* {
        @allowed_ips remote_ip 10.0.0.0/8 192.168.1.0/24
        handle @allowed_ips {
            reverse_proxy localhost:4000
        }
        handle {
            respond "Erisim Reddedildi" 403
        }
    }

    # API backend
    handle /api/* {
        header Cache-Control no-store
        reverse_proxy localhost:8080
    }

    # Statik dosyalar
    handle /assets/* {
        root * /var/www/shop/dist
        file_server {
            precompressed gzip br
        }
    }

    # React SPA - tüm diğer istekler
    handle {
        root * /var/www/shop/dist
        try_files {path} /index.html
        file_server
    }
}

Bu yapıda /admin bloğu içinde iç içe handle kullandığımıza dikkat edin. Bu Caddy’nin güçlü özelliklerinden biri; handle bloklarını birbirine geçirebiliyorsunuz.

Route Direktifi: Sıralı İşleme

route direktifi, içindeki direktiflerin kesinlikle yazıldıkları sırada işlenmesini garantiler. Bu, Caddy’nin normal öncelik sistemini bypass etmenin resmi yolu.

Neden buna ihtiyaç duyarsınız? Şöyle bir senaryo düşünün: Hem rewrite hem de reverse_proxy kullanmak istiyorsunuz. Caddy’nin normal akışında reverse_proxy‘nin önce çalışması gerekebilir, ama siz URL’i önce rewrite edip sonra proxy’lemek istiyorsunuz.

api-gateway.example.com {
    route /v1/* {
        # Önce URL'i dönüştür
        rewrite /v1/* /api/v1{path}
        # Sonra proxy'le
        reverse_proxy localhost:8081
    }

    route /v2/* {
        rewrite /v2/* /api/v2{path}
        reverse_proxy localhost:8082
    }

    route /v3/* {
        rewrite /v3/* /api/v3{path}
        reverse_proxy localhost:8083
    }
}

Route ile Middleware Zinciri

route direktifinin gerçek gücü, middleware zinciri oluştururken ortaya çıkıyor. Bir request’in belirli adımlardan sırayla geçmesini istediğinizde bu yapı çok işe yarıyor:

secure-api.example.com {
    route /api/* {
        # 1. Adım: Rate limiting
        rate_limit {
            zone api_zone {
                key {remote_host}
                events 100
                window 1m
            }
        }

        # 2. Adım: JWT doğrulama header'ı ekle
        header X-Real-IP {remote_host}
        header X-Forwarded-Proto {scheme}

        # 3. Adım: İsteği backend'e ilet
        reverse_proxy {
            to localhost:9000
            lb_policy round_robin
            health_uri /health
            health_interval 10s
        }
    }

    # API dışındaki her şeyi reddet
    route {
        respond "Bu endpoint mevcut degil" 404
    }
}

Handle vs Route: Hangisini Ne Zaman Kullanmalı?

Bu iki direktif arasındaki seçim çoğu zaman kafa karıştırıcı olabiliyor. Temel ayrımı şöyle özetleyebilirim:

  • handle: Birbirini dışlayan (mutually exclusive) path’ler için kullanın. Bir blok eşleştiğinde diğerleri çalışmaz.
  • route: Sıralı işleme garantisi istediğinizde ve birden fazla direktifin belirli bir sırada çalışması gerektiğinde kullanın.

Pratik bir karşılaştırma yapalım. Aynı senaryoyu her iki yöntemle yazalım:

# Handle ile yaklaşım
example.com {
    handle /api/* {
        reverse_proxy localhost:8080
    }

    handle /docs/* {
        reverse_proxy localhost:8081
    }

    handle {
        file_server
    }
}
# Route ile aynı senaryo
example.com {
    route {
        handle /api/* {
            reverse_proxy localhost:8080
        }

        handle /docs/* {
            reverse_proxy localhost:8081
        }

        handle {
            file_server
        }
    }
}

İkinci yaklaşımda route içine handle blokları koyduk. Bu kombinasyon çok yaygın kullanılıyor ve sıralama ile karşılıklı dışlama özelliklerini birleştiriyor.

Gerçek Dünya Senaryosu: Multi-Tenant SaaS Uygulaması

Şimdi daha karmaşık bir senaryoya geçelim. Bir SaaS platformu yönettiğinizi düşünün. Her müşterinin farklı bir subdomain’i var, bazı müşteriler premium plan kullanıyor ve farklı servislere erişebiliyor.

# Ana uygulama
app.saas.com {
    route {
        # Sağlık kontrolü - her zaman önce
        handle /health {
            respond "OK" 200
        }

        # Premium API endpoint'leri
        @premium_api {
            path /api/premium/*
            header X-Customer-Plan premium
        }
        handle @premium_api {
            reverse_proxy premium-api-cluster:8080
        }

        # Standart API
        handle /api/* {
            reverse_proxy standard-api-cluster:8080
        }

        # WebSocket bağlantıları
        @websocket {
            path /ws/*
            header Connection *Upgrade*
            header Upgrade websocket
        }
        handle @websocket {
            reverse_proxy ws-server:8090 {
                transport http {
                    versions h1
                }
            }
        }

        # Frontend uygulaması
        handle {
            root * /var/www/saas/dist
            try_files {path} /index.html
            file_server
        }
    }
}

# Müşteri subdomain'leri
*.saas.com {
    route {
        handle /api/* {
            reverse_proxy tenant-api:8080 {
                header_up X-Tenant-ID {labels.3}
            }
        }

        handle {
            root * /var/www/saas/dist
            file_server
        }
    }
}

Bu yapıda {labels.3} placeholder’ı subdomain adını alıp backend’e header olarak iletir. Tenant izolasyonunu bu şekilde Caddy seviyesinde yapabilirsiniz.

Hata Yönetimi ile Handle_Errors

handle_errors direktifi, yönlendirme hataları oluştuğunda devreye giren özel bir handle türüdür. Bu direktif sayesinde custom hata sayfaları sunabilir veya hataları başka bir servise yönlendirebilirsiniz.

example.com {
    handle /api/* {
        reverse_proxy localhost:8080
    }

    handle {
        root * /var/www/html
        file_server
    }

    handle_errors {
        @404 expression {err.status_code} == 404
        handle @404 {
            root * /var/www/html
            rewrite * /404.html
            file_server
        }

        @5xx expression {err.status_code} >= 500
        handle @5xx {
            root * /var/www/html
            rewrite * /500.html
            file_server
        }

        handle {
            respond "Bir hata olustu: {err.status_code}" {err.status_code}
        }
    }
}

Named Matcher’lar ile Handle Kombinasyonu

Caddy’de @isim syntax’ıyla named matcher tanımlayabilirsiniz. Bu matcher’ları handle direktifleriyle kombinleyince çok güçlü kurallar yazabilirsiniz:

media.example.com {
    # Resim optimizasyonu isteyen istekler
    @image_request {
        path *.jpg *.jpeg *.png *.webp
        query optimize=true
    }

    # Bot trafiği
    @bot_traffic {
        header_regexp User-Agent (bot|crawler|spider|scraper)
    }

    # Mobil cihazlar
    @mobile {
        header_regexp User-Agent (Android|iPhone|iPad|Mobile)
    }

    route {
        # Bot trafiğini hızlıca yanıtla
        handle @bot_traffic {
            respond "Robot erisimi kisitlidir" 403
        }

        # Resim optimizasyonu
        handle @image_request {
            reverse_proxy image-optimizer:3000
        }

        # Mobil CDN
        handle @mobile {
            reverse_proxy mobile-cdn:8080
        }

        # Varsayılan
        handle {
            root * /var/www/media
            file_server {
                precompressed gzip br
            }
        }
    }
}

Karmaşık Reverse Proxy Senaryosu: Blue-Green Deployment

Blue-green deployment yaparken Caddy’nin yönlendirme direktifleri hayat kurtarıcı olabiliyor. Traffic’i yeni sürüme kademeli olarak geçirebilirsiniz:

app.example.com {
    # Yeni sürümü test eden kullanıcılar
    @beta_testers {
        cookie beta_access=true
    }

    # Canary release - %10 traffic
    @canary_traffic {
        expression {rand.float} < 0.1
    }

    route {
        # Beta test kullanıcıları yeni sürüme
        handle @beta_testers {
            header X-App-Version green
            reverse_proxy green-app:8080
        }

        # Canary traffic yeni sürüme
        handle @canary_traffic {
            header X-App-Version green
            reverse_proxy green-app:8080
        }

        # Geri kalan tüm traffic eski sürüme
        handle {
            header X-App-Version blue
            reverse_proxy blue-app:8080
        }
    }
}

Bu yapıda {rand.float} placeholder’ı 0 ile 1 arasında rastgele bir float değer üretiyor. Böylece yaklaşık %10 traffic yeni sürüme yönlendiriliyor.

Path Traversal ve Güvenlik Yapılandırması

Güvenlik açısından kritik uygulamalarda handle direktifleri ile katmanlı koruma oluşturabilirsiniz:

secure.example.com {
    route {
        # Hassas dosyalara erişimi engelle
        @sensitive_files {
            path *.env *.git* *.htaccess */.DS_Store */config.yml */secrets*
        }
        handle @sensitive_files {
            respond "Yasakli dosya" 403
        }

        # Yönetim paneli - sadece VPN üzerinden
        @admin_no_vpn {
            path /admin/*
            not remote_ip 10.8.0.0/24
        }
        handle @admin_no_vpn {
            respond "VPN baglantisi gerekli" 403
        }

        # Normal admin erişimi
        handle /admin/* {
            basicauth {
                admin $2a$14$hashlenmisBcryptSifresi
            }
            reverse_proxy admin-panel:4000
        }

        # Public API - rate limit ile
        handle /api/public/* {
            reverse_proxy public-api:8080
        }

        # Geri kalan
        handle {
            root * /var/www/html
            file_server
        }
    }
}

Performans Optimizasyonu: Caching Katmanı

Handle direktiflerini cache mantığıyla birleştirerek performans optimizasyonu yapabilirsiniz:

cdn.example.com {
    route {
        # Statik assetler - uzun süreli cache
        @static_assets {
            path /assets/* /fonts/* /images/*
            file
        }
        handle @static_assets {
            header Cache-Control "public, max-age=31536000, immutable"
            header Vary Accept-Encoding
            root * /var/www/static
            file_server {
                precompressed gzip br
            }
        }

        # API yanıtları - kısa süreli cache
        @api_cacheable {
            method GET
            path /api/public/*
        }
        handle @api_cacheable {
            header Cache-Control "public, max-age=60, stale-while-revalidate=30"
            reverse_proxy api-server:8080
        }

        # Dinamik içerik - cache yok
        handle {
            header Cache-Control no-store
            reverse_proxy app-server:3000
        }
    }
}

Logging ve Debugging için Handle Yapılandırması

Production ortamında sorun giderirken spesifik path’ler için detaylı loglama aktif etmeniz gerekebilir:

example.com {
    log {
        output file /var/log/caddy/access.log
        format json
        level INFO
    }

    route {
        # Sorunlu endpoint için debug loglama
        handle /problematic-endpoint* {
            log {
                output file /var/log/caddy/debug.log
                format json
                level DEBUG
            }
            reverse_proxy debug-backend:8080
        }

        # API için ayrı log dosyası
        handle /api/* {
            log {
                output file /var/log/caddy/api.log
                format json
            }
            reverse_proxy api-backend:8080
        }

        handle {
            file_server
        }
    }
}

Yaygın Hatalar ve Çözümleri

Handle ve route direktiflerini kullanırken en sık karşılaşılan sorunları bilmek, hata ayıklama sürecinizi çok kısaltır.

Hatalı sıralama sorunu: handle bloklarında daha spesifik path’leri önce yazın. /api/v1/users path’ini /api/‘dan önce tanımlarsanız Caddy doğru şekilde çalışır, ama tersi durumda /api/ her şeyi yakalayacaktır.

Matcher çakışması: İki handle bloğu aynı path’i yakalıyorsa ilk eşleşen kazanır. Bu nedenle route içinde handle kullanırken sıralamaya dikkat edin.

handle_path vs handle farkı: handle_path direktifi path’i otomatik olarak strip eder. Yani /api/users isteği handle_path /api/* bloğuna girdiğinde backend /users olarak görür. Bunu bilinçli kullanın:

example.com {
    # handle_path - prefix'i siler, backend /users görür
    handle_path /api/* {
        reverse_proxy localhost:8080
    }

    # handle - prefix kalır, backend /api/users görür
    handle /static/* {
        root * /var/www
        file_server
    }
}

Sonuç

Caddy’nin handle ve route direktifleri, modern web altyapılarının karmaşık yönlendirme ihtiyaçlarını karşılamak için tasarlanmış güçlü araçlardır. handle ile birbirini dışlayan path kuralları oluştururken, route ile işlem sırası üzerinde tam kontrol sahibi olursunuz.

Bu yazıda ele aldığımız senaryolar, gerçek prodüksiyon ortamlarında karşılaşacağınız durumların büyük bölümünü kapsıyor. Blue-green deployment’tan multi-tenant SaaS mimarisine, güvenlik katmanlamadan performans optimizasyonuna kadar bu direktifler her durumda esneklik sunuyor.

Pratik tavsiye olarak şunu söyleyeyim: Basit senaryolarda sadece handle kullanın, yeterli. Direktif sırasının önemli olduğu durumlarda route ekleyin. Karmaşık iç içe kurallarda route içinde handle kombinasyonu en okunabilir ve bakımı en kolay yapıyı oluşturuyor. Caddy’nin caddy validate komutunu yapılandırma değişikliklerinizden önce mutlaka çalıştırın; sözdizimi hatalarını üretime almadan yakalamanın en kolay yolu bu.

Yorum yapın