Migrare da ingress-nginx alla Kubernetes Gateway API

2026-08-31 · 13 min read · gen:2m 39s · tok:15320
#kubernetes #gateway-api #ingress-nginx #devops #advanced-tutorial #italiano

ingress-nginx è archiviato. Guida pratica per migrare il traffico reale alla Gateway API con Envoy Gateway o Cilium: HTTPRoute, filtri, canary, TLS e un cutover sicuro.

Migrare da ingress-nginx alla Kubernetes Gateway API: Guida Pratica al Cutover

Il progetto ingress-nginx è stato archiviato all’inizio del 2026. Niente più release, niente più bugfix e — la parte che deve preoccuparti — niente più patch di sicurezza. Se hai Kubernetes in produzione c’è una buona probabilità che ti riguardi direttamente: i sondaggi recenti danno l’uso di ingress-nginx intorno alla metà di tutti i cluster. “Non fare niente” è ormai un incident al rallentatore.

La community ha convergito sulla Gateway API come sostituto a lungo termine di Ingress. Non è un nuovo controller: è un nuovo set di risorse Kubernetes (GatewayClass, Gateway, HTTPRoute) che diversi controller implementano — Envoy Gateway, Cilium, Istio, Kong e altri. Questa guida segue una migrazione reale: installi un controller Gateway accanto all’ingress-nginx esistente, converti gli oggetti Ingress, traduci le annotation che non si convertono da sole e sposti il traffico host per host senza una grossa finestra di manutenzione.

Prerequisiti

  • Un cluster Kubernetes 1.29+ con accesso kubectl e cluster-admin per i passi di installazione
  • Helm 3.14+
  • Un deployment ingress-nginx esistente con veri oggetti Ingress da migrare
  • cert-manager già in esecuzione se termini il TLS nel cluster (questa guida lo assume)
  • ingress2gateway v0.4.0+ installato in locale (go install github.com/kubernetes-sigs/ingress2gateway@latest)
  • Familiarità con il comportamento delle tue annotation Ingress attuali (rewrite, redirect, timeout, auth)

Devi essere a tuo agio nel leggere e fare diff di manifest YAML e nel modificare record DNS o load balancer.

Architettura e Concetti Chiave

Ingress impacchettava tutto — listener, hostname, path, TLS e comportamenti vendor-specific — in un solo oggetto più una zuppa di annotation. La Gateway API divide quella superficie in tre risorse, di proprietà di tre ruoli diversi.

flowchart TD
    subgraph Infra["Team infrastruttura"]
        GC[GatewayClass<br/>controllerName: envoy / cilium]
    end
    subgraph Cluster["Cluster operator"]
        GW[Gateway<br/>listener :80 / :443<br/>riferimenti cert TLS]
    end
    subgraph Dev["Team applicativi"]
        R1[HTTPRoute<br/>shop.example.com]
        R2[HTTPRoute<br/>api.example.com]
    end
    GC --> GW
    GW --> R1
    GW --> R2
    R1 --> S1[(Service: storefront)]
    R2 --> S2[(Service: api)]
    R2 -. peso 90/10 .-> S3[(Service: api-canary)]
  • GatewayClass nomina il controller (spec.controllerName). Ne installi uno per controller e non lo tocchi quasi più.
  • Gateway definisce i listener: porte, protocolli, hostname e riferimenti ai certificati TLS. È di proprietà dei cluster operator. allowedRoutes decide quali namespace possono attaccare route.
  • HTTPRoute contiene le regole di routing: hostnames, matches (path, header, method, query), filters (mutazione header, redirect, rewrite, mirroring) e backendRefs con pesi opzionali per il traffic splitting. Sono di proprietà dei team applicativi, nei loro namespace.

I vantaggi pratici: le annotation diventano campi tipizzati e validati; il canary è nativo invece di un secondo Ingress con canary-weight; e uno sviluppatore non può cambiare per sbaglio la config TLS o l’hostname di un altro team, perché vivono in oggetti che non controlla.

Scegliere un controller

ControllerData planeQuando conviene
Envoy GatewayEnvoy (user-space)Vuoi l’implementazione di riferimento, alta conformance alla Gateway API e portabilità tra cloud
CiliumeBPF per L4, Envoy per L7Cilium è già la tua CNI; vuoi load balancing L4 in-kernel vicino al line rate
IstioEnvoyHai già Istio e vuoi che mesh ed edge condividano la config

Questa guida usa Envoy Gateway negli esempi perché è autosufficiente e cloud-neutral. Ogni manifest mostrato è Gateway API v1 standard — cambiano solo il controllerName della GatewayClass e l’installazione per Cilium o Istio.

Implementazione Passo-Passo

Installare un controller Gateway accanto a ingress-nginx

I due possono coesistere. ingress-nginx osserva gli oggetti Ingress; il controller Gateway osserva Gateway/HTTPRoute. Installa le CRD della Gateway API ed Envoy Gateway:

1
2
3
4
5
6
7
8
9
# CRD della Gateway API (canale standard)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml

# Envoy Gateway
helm install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
  --version v1.3.0 \
  --namespace envoy-gateway-system --create-namespace

kubectl -n envoy-gateway-system rollout status deploy/envoy-gateway

Crea la GatewayClass e un Gateway condiviso. Il Gateway ottiene un suo IP esterno / load balancer, separato da quello che ingress-nginx ha già — è proprio quella separazione a rendere sicuro il cutover:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shared-gateway
  namespace: gateway-system
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "*.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: example-com-wildcard-tls
      allowedRoutes:
        namespaces:
          from: All
1
2
3
4
kubectl create namespace gateway-system
kubectl apply -f gateway.yaml
kubectl get gateway -n gateway-system shared-gateway \
  -o jsonpath='{.status.addresses[0].value}'   # annota questo IP/hostname per il DNS più avanti

💡 Tieni i certificati TLS nel Gateway, non in ogni route. Un solo certificateRef wildcard sul listener copre ogni HTTPRoute *.example.com. Se ti servono certificati per-host, aggiungi altri listener con valori hostname specifici.

Conversione di massa degli Ingress con ingress2gateway

Non scrivere a mano decine di route. ingress2gateway legge i tuoi oggetti Ingress in esecuzione (e le annotation ingress-nginx che conosce) e produce YAML Gateway API:

1
2
3
4
ingress2gateway print \
  --providers ingress-nginx \
  --namespace shop \
  > shop-routes.generated.yaml

Per un Ingress tipico come questo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# PRIMA: ingress-nginx
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: storefront
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
spec:
  ingressClassName: nginx
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /app(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: storefront
                port:
                  number: 8080

…lo strumento produce lo scheletro dell’HTTPRoute. Lo rivedi comunque: ingress2gateway converte ciò che mappa in modo pulito e lascia commenti dove non ci riesce:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# DOPO: Gateway API (rivisto)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: storefront
  namespace: shop
spec:
  parentRefs:
    - name: shared-gateway
      namespace: gateway-system
  hostnames:
    - shop.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /app
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
      timeouts:
        backendRequest: 30s
      backendRefs:
        - name: storefront
          port: 8080

Tradurre le annotation che non si convertono da sole

La maggior parte dei comportamenti di ingress-nginx mappa su un filtro o un campo della Gateway API. Quelli che incontrerai più spesso:

Annotation ingress-nginxEquivalente Gateway API
rewrite-targetfiltro URLRewritepath.type: ReplacePrefixMatch
ssl-redirect / force-ssl-redirectfiltro RequestRedirect con scheme: https, statusCode: 301 su una route del listener HTTP
permanent-redirectfiltro RequestRedirect (hostname, path, statusCode: 301)
proxy-read-timeout / proxy-send-timeoutrules[].timeouts.backendRequest
configuration-snippet che aggiunge headerfiltri RequestHeaderModifier / ResponseHeaderModifier
canary + canary-weightpiù backendRefs con weight
enable-cors + cors-*ResponseHeaderModifier (statico) o la CRD di policy CORS del controller
auth-url / auth-signin (auth esterna)dipende dal controller: SecurityPolicy di Envoy Gateway, CiliumEnvoyConfig di Cilium

Il redirect HTTP→HTTPS vale la pena mostrarlo perché sta su quasi ogni Ingress. Attacca una route di solo redirect al listener HTTP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: https-redirect
  namespace: gateway-system
spec:
  parentRefs:
    - name: shared-gateway
      sectionName: http           # solo il listener :80
  hostnames:
    - "*.example.com"
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301

Tutto ciò che è guidato da annotation e non ha ancora un campo — mTLS verso il backend, Lua complesso, rate limiting — è dove usi le CRD di policy del controller (BackendTrafficPolicy, SecurityPolicy, ClientTrafficPolicy in Envoy Gateway). Fai l’inventario di queste prima di iniziare: sono la coda lunga della migrazione.

TLS e cert-manager

cert-manager capisce la Gateway API. Annoti il Gateway invece di un Ingress e lui gestisce il certificato del listener:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "shop.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: shop-example-com-tls   # cert-manager lo crea

Abilita l’integrazione Gateway API di cert-manager con --set config.enableGatewayAPI=true sul suo chart Helm.

Configurazione per Produzione

Canary e traffic splitting

È nativo. Nessun secondo oggetto, nessuna annotation — solo pesi sui backendRefs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
rules:
  - matches:
      - path:
          type: PathPrefix
          value: /
    backendRefs:
      - name: api
        port: 8080
        weight: 90
      - name: api-canary
        port: 8080
        weight: 10

I pesi sono relativi, non percentuali — 9 e 1 si comportano come 90 e 10. I tool di progressive delivery (Argo Rollouts, Flagger) guidano questi numeri in automatico ed entrambi supportano la Gateway API come provider di prima classe.

Route cross-namespace con ReferenceGrant

Di default un HTTPRoute può referenziare solo Service nel suo namespace, e un Gateway accetta route solo dai namespace che i suoi allowedRoutes permettono. Per instradare verso un Service in un altro namespace, il namespace proprietario deve pubblicare un ReferenceGrant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-gateway-routes
  namespace: payments
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      namespace: shop
  to:
    - group: ""
      kind: Service

È una feature, non un attrito: l’accesso cross-namespace ora è esplicito e verificabile invece che implicito.

Il cutover affiancato

Fai girare entrambi gli stack e sposta il traffico per hostname:

  1. Deploya il Gateway con il suo IP di load balancer. Ancora nessuno ci punta.
  2. Crea gli HTTPRoute per un hostname. Verifica con un header Host direttamente contro l’IP del Gateway:
    1
    2
    
    curl -H "Host: shop.example.com" -kso /dev/null -w "%{http_code}\n" \
      https://<gateway-ip>/healthz
    
  3. Sposta il DNS di shop.example.com dal LB di ingress-nginx al LB del Gateway. Abbassa il TTL a 60s il giorno prima, così il rollback è rapido.
  4. Osserva entrambi i set di access log. Il traffico ingress-nginx per quell’host scende a zero man mano che le cache scadono.
  5. Ripeti per hostname. Quando l’ultimo è spostato e i log di ingress-nginx sono silenziosi per un giorno intero, cancella gli oggetti Ingress e poi disinstalla ingress-nginx.

Il rollback a qualsiasi passo è un cambio DNS all’indietro. Nessuna route è mai servita da entrambi gli stack contemporaneamente, quindi non c’è rischio di doppio routing.

Errori Comuni e Troubleshooting

L’HTTPRoute mostra Accepted: False con reason NotAllowedByListeners. L’allowedRoutes.namespaces del listener del Gateway non include il namespace della route, oppure gli hostnames della route non intersecano l’hostname del listener. Controlla kubectl describe httproute: le condizioni di status ti dicono esattamente quale.

404 da Envoy ma la route sembra corretta. In parentRefs manca il namespace (di default è quello della route, non quello del Gateway), oppure sectionName punta a un listener che non esiste.

Le catture regex di rewrite-target non si traducono. I rewrite della Gateway API sono basati su prefisso, non regex. path: /app(/|$)(.*)rewrite /$2 diventa PathPrefix: /app + ReplacePrefixMatch: /. I rewrite davvero regex-dipendenti richiedono il path match RegularExpression (implementation-specific) o una piccola modifica al routing dell’app.

L’handshake TLS fallisce dopo il cutover. Il Secret del certificateRef è nel namespace del Gateway ma il Gateway è altrove, oppure cert-manager non ha ancora emesso. kubectl get certificate -A e controlla la condizione Programmed nello status del Gateway.

L’auth esterna / OAuth2 proxy ha smesso di funzionare. auth-url non ha un equivalente core nella Gateway API. Ti serve la SecurityPolicy del controller (Envoy Gateway) o equivalente, collegata allo stesso provider OIDC. Non spostare questo hostname finché non è testato.

L’IP del client ora è quello del load balancer. Imposta externalTrafficPolicy: Local sul Service del Gateway, oppure configura la ClientTrafficPolicy del controller per fidarsi di X-Forwarded-For dal tuo LB.

Performance e Scalabilità

  • Il data plane eBPF di Cilium gestisce L4 nel kernel e passa a Envoy solo il L7 (modifiche header HTTP, gRPC). Se sei già su Cilium come CNI, questo significa latenza e CPU misurabilmente più basse di un proxy solo user-space a conteggi di connessione alti.
  • Envoy Gateway provisiona un deployment Envoy per Gateway (o per GatewayClass, configurabile). Dimensionalo con una risorsa EnvoyProxy e un HPA; le due repliche di default sono un punto di partenza, non un numero di produzione.
  • Meno reload. ingress-nginx ricaricava e ri-forkava NGINX a ogni cambio Ingress. Envoy applica i cambi di route via xDS senza drop di connessione, quindi gli ambienti ad alto churn smettono di pagare la tassa del reload.
  • Un Gateway, molte route. Non creare un Gateway per team — sarebbe un load balancer per team. Condividi un Gateway, isola con namespace e allowedRoutes.
  • Tieni piccoli gli oggetti route. Qualche centinaio di HTTPRoute va bene; una singola route da 5.000 regole no. Dividi per hostname o gruppo di path.

Conclusioni e Next Steps

L’archiviazione di ingress-nginx forza una decisione, ma la Gateway API è un upgrade vero, non uno spostamento laterale: configurazione tipizzata invece di stringhe di annotation, canary nativo, vera separazione dei ruoli e una spec che più controller implementano, così non sei di nuovo legato al ciclo di vita di un solo progetto.

La migrazione in sé è meccanica se la fai a tappe: installi un controller accanto a ingress-nginx, converti con ingress2gateway, traduci la coda lunga delle annotation in filtri e CRD di policy, poi sposti il traffico un hostname alla volta dietro il DNS.

Prossimi passi:

  • Lancia ingress2gateway print su ogni namespace adesso, solo per vedere quanto è grande la tua coda lunga di annotation.
  • Scegli il controller che si adatta al tuo stack (Cilium se è la tua CNI, altrimenti Envoy Gateway) e tira su un Gateway non-prod.
  • Collega Argo Rollouts o Flagger alla Gateway API così i canary sono automatizzati dal primo giorno.
  • Aggiungi BackendTrafficPolicy per retry, timeout e circuit breaking che prima vivevano nelle annotation.

Risorse Aggiuntive