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.

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.