Mert Özen Her Satırda Daha İleri
DevOps Kubernetes — #06

Kubernetes'te Dış Trafik: Ingress, ingress-nginx'in Emekliliği ve Gateway API ile HTTPRoute

Mert Özen 8 Kas 2026 10 dk 4 görüntülenme
Kubernetes'te Dış Trafik: Ingress, ingress-nginx'in Emekliliği ve Gateway API ile HTTPRoute

Ingress API'si donduruldu, ingress-nginx Mart 2026'da emekli oldu; yeni yol GatewayClass, Gateway ve HTTPRoute'a bölünmüş Gateway API. kind'da Envoy Gateway kuruyor, /api isteklerini .NET'e, geri kalanını Angular'a yönlendiriyor, PathPrefix'in /apifoo'yu neden yakalamadığını, route status'larını, TLS ve forwarded header notlarını ve ingress2gateway ile geçişin uyarılarını gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler kind 0.33 ile Kubernetes 1.37, kubectl 1.37, Helm 4.3, Envoy Gateway 1.10, ingress2gateway 1.2, .NET 10 ve Angular 22 ile denenmiştir.

Önce Tahmin Et

Bookstore'un Angular uygulaması ve .NET API'si (Application Programming Interface) aynı alan adının arkasında: /api ile başlayan istekler API'ye, geri kalanı Angular'ı sunan nginx'e gidecek. HTTPRoute'ta /api için PathPrefix kullanıyorsun. /apifoo isteği nereye gider: API'ye mi, Angular'a mı? Bir de şu: şirketin cluster'ında yıllardır sorunsuz çalışan ingress-nginx var. Mart 2026'dan sonra o kurulum çalışmayı durdurur mu?

Ingress ve Controller Kavramı

Bir önceki yazıda ClusterIP Service'lerin yalnız cluster içinden erişilebildiğini, LoadBalancer Service'inin ise her servis için ayrı bir dış IP (Internet Protocol) açtığını gördük. Web uygulamalarında asıl ihtiyaç farklı: tek bir giriş noktası, host ve path'e göre yönlendirme, TLS (Transport Layer Security) sonlandırma. Kubernetes'in bu iş için ilk cevabı Ingress resource'uydu:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: bookstore
  namespace: bookstore
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
  ingressClassName: nginx
  rules:
    - host: bookstore.localtest.me
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

Ingress bir kural listesidir; kendi başına hiçbir şey yapmaz. Kuralları okuyup gerçek bir proxy'yi (nginx, Envoy, HAProxy, bir bulut load balancer'ı) yapılandıran bir Ingress controller kurman gerekir. Kubernetes bu controller'ı kendisi getirmez. Ingress API'si host, path ve TLS'in ötesinde pek bir şey tanımlamadığı için her controller eksikleri kendi annotation'larıyla kapattı: yukarıdaki nginx.ingress.kubernetes.io/proxy-body-size yalnız ingress-nginx'te anlam taşır. Sonuç, bir controller'dan diğerine taşınamayan, annotation'larla dolu manifest'ler oldu.

Ingress Dondu, ingress-nginx Emekli Oldu

Kubernetes dokümantasyonunun Ingress sayfası artık şöyle başlıyor: proje Ingress yerine Gateway kullanılmasını öneriyor ve Ingress API'si donduruldu. Dondurulmuş olması kaldırılacağı anlamına gelmiyor; API stable ve GA (General Availability) garantileri sürüyor, ama yeni özellik ve değişiklik gelmeyecek.

İkinci haber daha somut. Kubernetes SIG (Special Interest Group) Network ve Security Response Committee, 11 Kasım 2025'te ingress-nginx'in emekliye ayrılacağını duyurdu: Mart 2026'ya kadar best-effort bakım, sonrasında yeni sürüm yok, bugfix yok, keşfedilecek güvenlik açıkları için güncelleme yok; GitHub repository'leri read-only. Duyuruya göre projeyi yıllarca bir iki kişi mesai dışında ayakta tutmuş, "snippets" annotation'larıyla keyfi nginx configuration'ı ekleyebilmek gibi bir zamanlar esneklik sayılan özellikler bugün ciddi güvenlik açığı olarak görülüyor. Not: bu, Kubernetes topluluğunun ingress-nginx projesi; F5'in (nginx'in arkasındaki şirket) ayrı projesi nginxinc/kubernetes-ingress bu duyurunun kapsamında değil.

Baştaki ikinci sorunun cevabı: hayır, durmaz. Duyuru açıkça mevcut kurulumların bozulmayacağını, Helm chart'larının ve image'ların erişilebilir kalacağını söylüyor. Sorun çalışmaya devam etmesi: internete açık, artık yama almayan bir proxy. Kendi cluster'ında kullanıp kullanmadığını duyurudaki komutla kontrol edebilirsin:

$ kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx
No resources found
$ kubectl get ingressclass
No resources found

Bizim cluster'da yok. Sende çıktı doluysa, geçiş planı bu yazının asıl konusu.

Gateway API: Rollere Ayrılmış Üç Katman

Gateway API, Ingress'in yerine tasarlanan ve Kubernetes çekirdeğine değil CRD (Custom Resource Definition) olarak kurulan bir API ailesi. Dört tasarım ilkesi var: role göre ayrılmış, taşınabilir, ifade gücü yüksek ve genişletilebilir. En çok fark yaratan ilki: tek bir Ingress object'i yerine sorumluluk üç resource'a bölünüyor.

06-ingress-vs-gateway-tr.png

GatewayClass, altyapı sağlayıcının tanımladığı "gateway türü": hangi controller'ın (Envoy Gateway, NGINX Gateway Fabric, Istio, bir bulut load balancer'ı) bu gateway'leri yöneteceğini söyler. Gateway, cluster operatörünün açtığı somut giriş noktası: hangi port, hangi protokol, hangi hostname, hangi sertifika, hangi namespace'lerden route kabul edileceği. HTTPRoute ise uygulama ekibinin işi: hangi path ve header hangi Service'e gidecek. Bookstore ekibi kendi HTTPRoute'unu yazar ama Gateway'in TLS ayarına ya da başka bir takımın route'una dokunamaz; RBAC (Role-Based Access Control) bu sınırları resource türüne göre çizebilir.

kind'da Envoy Gateway Kurmak

Gateway API'nin birden çok implementation'ı var; bu seride Envoy Gateway kullanıyoruz (NGINX Gateway Fabric ya da Traefik ile de aynı manifest'ler çalışır, değişen yalnız GatewayClass). Kurulum Helm (serinin dışında; Helm serisinde ayrıntılı) ile:

helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.10.0 \
  -n envoy-gateway-system --create-namespace
kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available

Chart, Gateway API CRD'lerini de beraberinde kuruyor. Kurduğu sürüme bakalım:

$ kubectl get crd gateways.gateway.networking.k8s.io -o jsonpath='{.metadata.annotations.gateway\.networking\.k8s\.io/bundle-version} {.metadata.annotations.gateway\.networking\.k8s\.io/channel}'
v1.6.1 experimental

Envoy Gateway 1.10, Gateway API'nin 1.6.1 sürümünü experimental kanalıyla getiriyor; Gateway API'nin kendi son sürümü ise 29 Ekim 2026'da çıkan 1.7.0. CRD'ler cluster genelinde tek kopya olduğu için, bir cluster'da birden çok implementation varsa CRD sürümünü kimin yöneteceğine baştan karar ver. Bir not daha: yoğun yük altındaki lokal cluster'ımda chart'ın CRD kurulumu API server timeout'una takıldı; CRD dosyalarını kubectl apply --server-side ile tek tek uygulayıp chart'ı --skip-crds ile kurmak sorunu çözdü.

kind'da LoadBalancer IP'si olmadığı için Envoy'un Service'ini NodePort'a çeviriyoruz. Bu, Envoy Gateway'e özgü EnvoyProxy resource'u ile yapılıyor ve GatewayClass ona parametersRef ile bağlanıyor. Gateway'i de aynı dosyada tanımlıyoruz:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: kind-nodeport
  namespace: envoy-gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: NodePort
        externalTrafficPolicy: Cluster
        patch:
          type: StrategicMerge
          value:
            spec:
              ports:
                - port: 80
                  nodePort: 30080
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
  parametersRef:
    group: gateway.envoyproxy.io
    kind: EnvoyProxy
    name: kind-nodeport
    namespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: bookstore-gw
  namespace: bookstore
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      hostname: "bookstore.localtest.me"
      allowedRoutes:
        namespaces:
          from: Same

Port 30080'i seçmemizin nedeni ikinci yazıdaki kind config'i: control plane container'ının 30080'i makinenin 18780'ine bağlı. bookstore.localtest.me ise herkese açık DNS'te (Domain Name System) 127.0.0.1'e çözülen bir ad; /etc/hosts'a dokunmadan host tabanlı yönlendirmeyi denemeyi sağlıyor. allowedRoutes.namespaces.from: Same bu Gateway'e yalnız bookstore namespace'indeki route'ların bağlanabileceğini söylüyor.

HTTPRoute ile /api → .NET, / → Angular

Angular tarafı ayrı bir Deployment: bookstore-web:1.0.0 image'ı Angular 22 build çıktısını non-root bir nginx (port 8080) ile sunuyor, önünde web adında bir ClusterIP Service var. Runtime configuration'ı (config.json) bir ConfigMap'ten mount ediliyor; onu yedinci yazıda açacağız. Route:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: bookstore
  namespace: bookstore
spec:
  parentRefs:
    - name: bookstore-gw
  hostnames:
    - "bookstore.localtest.me"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api
          port: 80
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: web
          port: 80
$ kubectl get gatewayclass
NAME   CONTROLLER                                      ACCEPTED   AGE
eg     gateway.envoyproxy.io/gatewayclass-controller   True       21s
$ kubectl get gateway
NAME           CLASS   ADDRESS      PROGRAMMED   AGE
bookstore-gw   eg      172.20.0.2   True         21s
$ kubectl get svc -n envoy-gateway-system
NAME                                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                                            AGE
envoy-bookstore-bookstore-gw-ee7dcdcd   NodePort    10.96.214.153   <none>        80:30080/TCP                                       21s
envoy-gateway                           ClusterIP   10.96.224.23    <none>        18000/TCP,18001/TCP,18002/TCP,19001/TCP,9443/TCP   42s
$ kubectl get httproute bookstore -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'
Accepted=True (Accepted)
ResolvedRefs=True (ResolvedRefs)

Gateway'i uygulayınca Envoy Gateway controller'ı envoy-gateway-system namespace'inde bu Gateway'e özel bir Envoy Deployment'ı ve NodePort Service'i açtı. HTTPRoute'un status'u iki şeyi söylüyor: Gateway route'u kabul etti (Accepted) ve referans verdiği Service'ler bulundu (ResolvedRefs). Bir route çalışmıyorsa ilk bakılacak yer burası; örneğin Gateway from: Same derken başka namespace'ten bağlanan bir route burada Accepted=False olur. Şimdi makineden istek atalım:

$ curl -s -i -H "Host: bookstore.localtest.me" localhost:18780/api/info
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
server: Kestrel

{"pod":"api-75dcdbd96-np2nj","version":"1.0.0","environment":"Production","storeName":"Bookstore","discountPercent":0,"database":"postgres"}
$ curl -s -H "Host: bookstore.localtest.me" localhost:18780/config/config.json
{
  "apiBaseUrl": "",
  "storeName": "Bookstore"
}
$ curl -s -H "Host: bookstore.localtest.me" localhost:18780/ | head -5
<!doctype html>
<html lang="en" data-beasties-container>
<head>
  <meta charset="utf-8">
  <title>BookstoreWeb</title>
$ curl -s -i -H "Host: other.localtest.me" localhost:18780/api/info
HTTP/1.1 404 Not Found
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://bookstore.localtest.me:18780/apifoo
200 text/html

/api/info Kestrel'den, kök path Angular'ın index.html'inden geldi; tanımsız bir host 404 aldı. Baştaki ilk sorunun cevabı son satırda: /apifoo Angular'a gitti. Gateway API'de PathPrefix karakter karakter değil, path element'i bazında eşleşir: /api prefix'i /api ve /api/books ile eşleşir ama /apifoo ile eşleşmez. İstek bir sonraki kurala, /'e düştü ve nginx'in SPA (Single Page Application) fallback'i index.html döndü. Angular uygulamasının apiBaseUrl'ü bu yüzden boş: tarayıcı API'ye aynı origin'den, /api/... yoluyla gidiyor ve CORS (Cross-Origin Resource Sharing) ayarına gerek kalmıyor.

TLS, cert-manager ve Forwarded Header'lar

Production'da Gateway'e bir HTTPS (Hypertext Transfer Protocol Secure) listener'ı eklersin: protocol: HTTPS, port: 443 ve tls.certificateRefs ile bir Secret'a referans. Sertifikayı elle yenilemek yerine cert-manager kullanmak yaygın: Gateway API desteği açıldığında Gateway'e eklenen bir annotation'dan (örneğin cert-manager.io/cluster-issuer) sertifikayı üretir, Secret'a yazar ve süresi dolmadan yeniler. Sertifika, ACME (Automatic Certificate Management Environment) ve TLS ayrıntıları ilerideki SSL (Secure Sockets Layer)/TLS serisinin konusu.

.NET tarafında unutulmaması gereken bir ayrıntı var: TLS Gateway'de sonlanınca API isteği düz HTTP (Hypertext Transfer Protocol) olarak ve Envoy'un IP'sinden alır. Doğru client IP'si ve şeması X-Forwarded-For ve X-Forwarded-Proto header'larında gelir; ASP.NET Core'un bunlara güvenmesi için Forwarded Headers middleware'ini ve güvenilir proxy ağlarını ayarlaman gerekir. Bunu ilerideki Web Server'lar ve Hosting serisinin Forwarded Headers yazısında ayrıntılı göreceğiz; Kubernetes'e özgü kısmı, güvenilir ağın tek bir IP değil pod CIDR'ı (Classless Inter-Domain Routing) olması.

Ingress'ten Gateway API'ye Geçiş: ingress2gateway

Kubernetes SIG Network'ün ingress2gateway aracı mevcut Ingress'leri Gateway API resource'larına çevirir. Yukarıdaki Ingress dosyasını verdim:

$ ingress2gateway print --input-file ingress.yaml --providers ingress-nginx
WARN  Ingress NGINX serves TLS traffic for host "bookstore.localtest.me" with a self-signed certificate. This behavior will not be translated and the host will not be accessible via HTTPS.
WARN  Failed to apply bookstore.bookstore.metadata.annotations."nginx.ingress.kubernetes.io/proxy-body-size" from bookstore/bookstore: Most Gateway API implementations have reasonable body size and buffering defaults
WARN  Gateway API does not support configuring URL normalization (RFC 3986, Section 6). Please check if this matters for your use case and consult implementation-specific details.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  annotations:
    gateway.networking.k8s.io/generator: ingress2gateway-1.2.0
  name: nginx
  namespace: bookstore
spec:
  gatewayClassName: nginx
  listeners:
  - hostname: bookstore.localtest.me
    name: bookstore-localtest-me-http
    port: 80
    protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  annotations:
    gateway.networking.k8s.io/generator: ingress2gateway-1.2.0
  name: bookstore-bookstore-localtest-me
  namespace: bookstore
spec:
  hostnames:
  - bookstore.localtest.me
  parentRefs:
  - name: nginx
  rules:
  - backendRefs:
    - name: api
      port: 80
    matches:
    - path:
        type: PathPrefix
        value: /api
...

(Uyarıları renk kodlarından arındırıp tek satıra indirdim; iki kez basılan body size uyarısını bir kez gösterdim.) Çıktı bizim elle yazdığımız route'un neredeyse aynısı. Asıl değerli kısım uyarılar: annotation'ların hangilerinin çevrilemediğini ve ingress-nginx'in hangi örtük davranışlarının (self-signed sertifika, URL (Uniform Resource Locator) normalizasyonu) kaybolacağını tek tek söylüyor. Gerçek bir geçişte bu listeyi bir checklist gibi kullan: her uyarı için ya Gateway API'nin standart bir karşılığını, ya implementation'ın policy resource'unu (Envoy Gateway'de ClientTrafficPolicy, BackendTrafficPolicy) bul, ya da davranışın gerçekten gerekmediğine karar ver. Aracın --emitter envoy-gateway gibi seçenekleri implementation'a özgü resource'lar da üretebiliyor.

Üretimde Dikkat

Gateway'i paylaşılan altyapı olarak düşün. Tek bir Gateway'e birçok takımın HTTPRoute'u bağlanabilir; allowedRoutes ile hangi namespace'lerin bağlanabileceğini açıkça sınırla ve Gateway'i yalnız platform ekibinin değiştirebileceği bir namespace'te tut.

Gateway'in kendisi de bir uygulama: Envoy pod'larının replica sayısı, resource'ları, PodDisruptionBudget'ı ve sürüm yükseltmeleri senin sorumluluğunda. Bütün dış trafiğin geçtiği bileşenin tek replica'yla çalışması, API'nin on replica'sını anlamsız kılar.

Geçişi paralel yürüt. Eski Ingress controller'ı kapatmadan Gateway'i ayrı bir IP'de aç, trafiği DNS düzeyinde kademeli taşı, metrikleri karşılaştır. Rollback yolu DNS kaydını geri almak olsun.

Sık Düşülen Tuzaklar

CRD sürümü uyuşmazlığı. Gateway API CRD'leri cluster genelinde tek kopya; iki farklı implementation farklı sürümler getiriyorsa biri diğerinin CRD'sini ezebilir. Experimental ve standard kanalları karıştırmak da sürpriz field'lara yol açar.

Route'un status'una bakmamak. HTTPRoute uygulanınca "created" der, ama Gateway onu kabul etmemiş olabilir. Accepted ve ResolvedRefs koşullarını kontrol et.

Prefix eşleşmesini string gibi düşünmek. Yukarıda gördüğün gibi /api prefix'i /apifoo'yu yakalamaz. Ingress'teki pathType: Prefix da element bazındaydı; ama bazı controller'ların annotation'larla açılan regex davranışları geçişte değişir.

Annotation'ları körlemesine taşımak. ingress-nginx'e özgü bir annotation'ın Gateway dünyasında karşılığı yoksa, belki o davranışa hiç ihtiyacın yoktur; belki de bir policy resource'u gerekir. ingress2gateway'in uyarılarını atlama.

Küçük Bir Deneme

★☆☆ HTTPRoute'a /api kuralını silip yeniden uygula ve curl http://bookstore.localtest.me:18780/api/info çıktısının neden Angular'ın index.html'i olduğunu açıkla.

★★☆ bookstore-staging adında ikinci bir namespace'te aynı API'yi çalıştır ve oradan bookstore-gw'ye bağlanan bir HTTPRoute yaz. Route'un status'unda ne gördüğünü not et; sonra Gateway'in allowedRoutes'unu bir namespace selector'ıyla genişletip tekrar dene.

★★★ /api kuralında iki backendRefs kullan: api Service'ine weight: 90, 1.1.0 sürümünü çalıştıran api-canary Service'ine weight: 10. 100 istek atıp version alanına göre dağılımı ölç. Bunun canary release'in ilk adımı olduğunu ve ilerideki Deployment Stratejileri serisinde metriklerle nasıl otomatikleştirileceğini düşün.

Trafik artık dışarıdan içeri doğru yerlere akıyor. Ama API'nin mağaza adı, indirim oranı ve veritabanı şifresi hâlâ image'a ya da manifest'e gömülü. Bir sonraki yazıda ConfigMap ve Secret ile .NET configuration'ını Kubernetes'ten beslemeyi, değişikliklerin pod'a nasıl ve ne zaman yansıdığını ölçüyoruz.