Se você seguiu o nosso guia de GKE Autopilot com external-dns e cert-manager, o seu cluster está rodando o ingress-nginx como porta de entrada. Precisamos conversar sobre isso.

Em janeiro de 2026, os comitês de Steering e de Segurança do Kubernetes anunciaram a aposentadoria do ingress-nginx. Em 24 de março o repositório ficou somente leitura. O comunicado é explícito: “sem mais releases para correção de bugs, patches de segurança ou qualquer tipo de atualização”. E isso aconteceu depois de quatro CVEs de severidade alta em fevereiro, além do IngressNightmare (CVSS 9.8) do ano anterior — vulnerabilidades que agora nunca mais serão corrigidas.

Estimativas apontam que cerca de metade dos ambientes cloud-native dependiam do ingress-nginx, e uma pesquisa recente ainda encontrou 44% dos times usando — o que faz de você parte de uma multidão. A boa notícia: a migração para a Gateway API no GKE é mais simples do que parece, dá para fazer sem downtime e, no final, você ganha um Load Balancer melhor. Este guia é o roteiro, passo a passo, para quem tem exatamente o setup do tutorial anterior.

O que muda: Ingress vs. Gateway API

O Ingress foi projetado como um mínimo denominador comum, e cada controlador (nginx, traefik, GCE) estendia o padrão com dezenas de annotations proprietárias. A Gateway API é o sucessor oficial: um padrão do Kubernetes, expressivo e orientado a papéis, que separa infraestrutura de roteamento:

RecursoQuem gerenciaO que faz
GatewayClassGoogle (no GKE)Define o tipo de Load Balancer
GatewayTime de plataformaProvisiona o LB, os listeners (80/443), o IP e o TLS
HTTPRouteTime da aplicaçãoRoteia hostnames/paths para os Services

A mudança arquitetural que importa no GKE: com o ingress-nginx você tinha um Load Balancer L4 regional (passthrough) mandando tráfego para pods do nginx dentro do cluster, que faziam o TLS e o roteamento. Com a Gateway API gerenciada, o GKE provisiona um Application Load Balancer L7 global (anycast) do Google Cloud: o TLS termina na borda da rede do Google, o roteamento é feito pelo próprio LB, e os pods do nginx deixam de existir — nem vCPU/RAM do Autopilot eles consomem mais.

Pré-requisitos

  • O cluster GKE Autopilot e o setup do tutorial anterior (ingress-nginx, cert-manager, external-dns no Cloud DNS, GitHub Actions).
  • gcloud, kubectl e helm instalados.
  • O domínio já delegado ao Cloud DNS (feito no tutorial anterior).

Os placeholders são os mesmos de antes: meu-projeto, meu-cluster, us-central1, exemplo.com / app.exemplo.com.

💸 Custo: o Application Load Balancer global é cobrado por hora de forwarding rule e por GB processado (tier Premium). Em troca, você elimina os pods do ingress-nginx e o Load Balancer L4 antigo. Para a maioria dos workloads a conta fica parecida — mas confira a tabela de preços de rede para o seu volume.

A estratégia: migrar sem downtime

A recomendação oficial do Google é rodar os dois lado a lado, e é o que vamos fazer:

  1. Subir o Gateway e os HTTPRoute em paralelo ao Ingress atual (nada muda para os usuários).
  2. Testar a nova rota direto pelo IP do Gateway, sem mexer no DNS.
  3. Fazer o cutover do DNS (o external-dns faz isso por nós).
  4. Só então remover o Ingress e desinstalar o ingress-nginx.

Em nenhum momento o site fica fora do ar.

Passo 1 — Confirmar a Gateway API no cluster

No Autopilot, a Gateway API vem sempre habilitada — não há nada para ligar. Confirme as classes disponíveis:

kubectl get gatewayclass

Você verá, entre outras, a gke-l7-global-external-managed — é a que o Google recomenda para aplicações públicas HTTPS, e a que usaremos. (Em clusters Standard, se não aparecer, habilite com gcloud container clusters update meu-cluster --location=us-central1 --gateway-api=standard.)

Evite a gke-l7-gxlb: é a classe legada, não suporta redirect HTTP→HTTPS nem paths por regex. O próprio Google recomenda a global-external-managed no lugar dela.

Passo 2 — Reservar um IP estático GLOBAL

Atenção aqui, porque é diferente do tutorial anterior. O ingress-nginx usava um IP regional (Load Balancer L4). O Application Load Balancer global exige um IP global:

gcloud compute addresses create app-gateway-ip --global

# anote o IP — vamos usar para testar antes do DNS
gcloud compute addresses describe app-gateway-ip --global --format="value(address)"

Não apague o IP regional antigo ainda — ele continua servindo o ingress-nginx até o cutover.

Passo 3 — TLS gerenciado com o Certificate Manager

Aqui está a maior mudança conceitual em relação ao cert-manager + Let’s Encrypt. Como o TLS agora termina no Load Balancer do Google, a forma recomendada é deixar o próprio Google emitir e renovar o certificado, via Certificate Manager. Zero componente para operar no cluster.

O fluxo é: autorização DNS → certificado gerenciado → certificate map (que o Gateway referencia).

# 1) autorização DNS para o hostname
gcloud certificate-manager dns-authorizations create app-exemplo-com-auth \
  --domain="app.exemplo.com"

# 2) veja o registro CNAME que o Google pede
gcloud certificate-manager dns-authorizations describe app-exemplo-com-auth
# dnsResourceRecord:
#   name: _acme-challenge.app.exemplo.com.
#   type: CNAME
#   data: xxxxxxxx.authorize.certificatemanager.goog.

Crie esse CNAME na sua zona do Cloud DNS (substitua data pelo valor retornado):

gcloud dns record-sets create _acme-challenge.app.exemplo.com. \
  --zone=exemplo-com --type=CNAME --ttl=30 \
  --rrdatas="xxxxxxxx.authorize.certificatemanager.goog."

Agora o certificado, o map e a entrada que liga o hostname ao certificado:

# 3) certificado gerenciado pelo Google (ele emite e renova sozinho)
gcloud certificate-manager certificates create app-exemplo-com-cert \
  --domains="app.exemplo.com" \
  --dns-authorizations="app-exemplo-com-auth"

# 4) aguarde ficar ACTIVE (pode levar alguns minutos após o CNAME propagar)
gcloud certificate-manager certificates describe app-exemplo-com-cert \
  --format="value(managed.state)"

# 5) o map é o que o Gateway vai referenciar
gcloud certificate-manager maps create app-exemplo-com-map
gcloud certificate-manager maps entries create app-exemplo-com-entry \
  --map="app-exemplo-com-map" \
  --certificates="app-exemplo-com-cert" \
  --hostname="app.exemplo.com"

Wildcard? Use --domains="exemplo.com,*.exemplo.com" com uma única autorização no domínio raiz.

”E o meu cert-manager?”

Uma dúvida comum — e uma confusão que circula por aí — é que “o cert-manager não funciona com o Gateway do GKE”. Não é verdade. Secrets TLS do Kubernetes são suportados em todas as GatewayClasses do GKE: o controlador copia o Secret para um certificado SSL do Compute Engine. Você pode manter o cert-manager, mas duas coisas mudam:

  1. Não existe mais Ingress para o desafio HTTP-01. O solver precisa ser o de Gateway API, habilitado no cert-manager (≥ 1.15):
helm upgrade --install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --set crds.enabled=true \
  --set config.gatewayAPI.enabled=true
# ClusterIssuer com o solver de Gateway API
solvers:
  - http01:
      gatewayHTTPRoute:
        parentRefs:
          - name: external-gateway
            namespace: demo
            kind: Gateway
  1. No Gateway, o listener HTTPS referencia o Secret com tls.certificateRefs em vez da annotation certmap — os dois não podem coexistir no mesmo Gateway.

Cada desafio HTTP-01 espera o LB ser reprogramado (minutos), então é mais lento. Se for manter o cert-manager, o solver DNS-01 com Cloud DNS é o caminho mais tranquilo. Ainda assim, para o GKE, o Certificate Manager é a opção que recomendamos: menos peças, renovação automática pelo Google.

Passo 4 — O Gateway

Crie k8s/core/gateway/gateway.yaml. Ele referencia o IP global pelo nome e o certificado pelo map:

# k8s/core/gateway/gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: external-gateway
  namespace: demo
  annotations:
    networking.gke.io/certmap: app-exemplo-com-map   # TLS pelo Certificate Manager
spec:
  gatewayClassName: gke-l7-global-external-managed
  addresses:
    - type: NamedAddress
      value: app-gateway-ip                            # o IP GLOBAL reservado no Passo 2
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Same
    - name: https
      protocol: HTTPS
      port: 443
      allowedRoutes:
        namespaces:
          from: Same

Repare que o listener HTTPS não tem bloco tls: com o certmap, o certificado vem da annotation. O listener HTTP fica só para redirecionar.

Passo 5 — As rotas (HTTPRoute)

Duas rotas: a da aplicação (no listener HTTPS) e o redirect (no listener HTTP). Crie em k8s/apps/whoami/httproute.yaml:

# rota da aplicação, ligada ao listener https
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: whoami
  namespace: demo
spec:
  parentRefs:
    - name: external-gateway
      sectionName: https
  hostnames:
    - "app.exemplo.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: whoami
          port: 80
---
# HTTP → HTTPS, ligado ao listener http
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: redirect-http-to-https
  namespace: demo
spec:
  parentRefs:
    - name: external-gateway
      sectionName: http
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301

O sectionName amarra cada rota ao listener certo. O redirect usa o filtro padrão RequestRedirect — suportado na classe global-external-managed (e é justamente o que a classe legada gxlb não faz).

Passo 6 — Aplicar e testar SEM mexer no DNS

Aplique o Gateway e as rotas. O Ingress antigo continua no ar, intocado:

kubectl apply -f k8s/core/gateway/gateway.yaml
kubectl apply -f k8s/apps/whoami/httproute.yaml

O GKE leva alguns minutos para provisionar o Load Balancer. Acompanhe até o Gateway ficar Programmed e receber o IP:

kubectl get gateway external-gateway -n demo \
  -o=jsonpath="{.status.addresses[0].value}" --watch

kubectl describe gateway external-gateway -n demo   # procure Programmed: True
kubectl get httproute -n demo                       # as rotas devem estar Accepted

Agora o teste decisivo — bater no IP do Gateway simulando o hostname, sem tocar no DNS:

GW_IP=$(gcloud compute addresses describe app-gateway-ip --global --format="value(address)")

# HTTPS pela nova rota (certificado do Google)
curl -sI --resolve app.exemplo.com:443:$GW_IP https://app.exemplo.com/

# redirect HTTP → HTTPS
curl -sI --resolve app.exemplo.com:80:$GW_IP http://app.exemplo.com/
# HTTP/1.1 301 Moved Permanently
# location: https://app.exemplo.com/

Se o HTTPS responder 200 com certificado válido e o HTTP devolver 301, o novo caminho está pronto. Os usuários ainda estão no ingress-nginx — e vão continuar até você decidir.

Passo 7 — Cutover do DNS (o external-dns faz o trabalho)

No tutorial anterior o external-dns lia sources: [ingress, service]. Para ele passar a apontar app.exemplo.com para o Gateway, troque a fonte para as rotas da Gateway API. Edite k8s/core/external-dns/values.yaml:

sources:
  - gateway-httproute   # lê hostnames dos HTTPRoute; o IP vem do status do Gateway
  - service

O chart do external-dns (≥ 1.20) adiciona sozinho as permissões RBAC de gateways/httproutes quando detecta uma fonte gateway-*. Aplique e remova o Ingress antigo — com policy: sync e o mesmo txtOwnerId, o external-dns reescreve o registro A para o IP do Gateway:

helm upgrade --install external-dns external-dns/external-dns \
  --namespace external-dns \
  --values k8s/core/external-dns/values.yaml

kubectl delete ingress whoami -n demo

# acompanhe a troca
kubectl logs -n external-dns deploy/external-dns | grep app.exemplo.com
dig +short app.exemplo.com        # deve virar o IP global do Gateway

TTL: clientes com o registro antigo em cache continuam batendo no ingress-nginx até o TTL expirar. É exatamente por isso que só desinstalamos o nginx depois — mantenha os dois por um tempo e observe o tráfego do antigo cair a zero.

Passo 8 — Limpeza

Quando o tráfego no ingress-nginx zerar:

# desinstala o controlador aposentado
helm uninstall ingress-nginx -n ingress-nginx
kubectl delete namespace ingress-nginx

# libera o IP regional antigo (o do L4)
gcloud compute addresses delete nginx-ingress-ip --region=us-central1

# se não for manter o cert-manager, remova-o também
helm uninstall cert-manager -n cert-manager

E no repositório: apague o workflow do ingress-nginx e o values.yaml dele. O workflow de deploy das apps continua igual — só troca ingress.yaml por httproute.yaml no kubectl apply. Vale um workflow para o gateway.yaml em k8s/core/gateway/, no mesmo molde dos outros.

Tem muitos Ingress? Use o ingress2gateway

Para quem tem dezenas de Ingress, a ferramenta oficial ingress2gateway (v1.0 saiu em março junto com a aposentadoria; já está na v1.2) converte manifests automaticamente, traduzindo mais de 30 annotations do ingress-nginx:

brew install ingress2gateway

# lê os Ingress do cluster e imprime Gateway + HTTPRoute equivalentes
ingress2gateway print --providers=ingress-nginx -n demo

# ou a partir de um arquivo
ingress2gateway print --providers=ingress-nginx --input-file=ingress.yaml

Ele converte rewrite-target, redirects, canary por peso/header, timeouts, CORS e ssl-redirect. Revise a saída: troque o gatewayClassName para gke-l7-global-external-managed, e leia os avisos — o que não tem equivalente ele sinaliza, em vez de ignorar em silêncio.

O que não migra 1:1

Algumas coisas do nginx não têm tradução direta, e é bom saber antes:

  • ModSecurity / WAF — o equivalente no GCP é o Cloud Armor, anexado ao backend com um GCPBackendPolicy (networking.gke.io/v1). Mais robusto que o ModSecurity dentro do pod.
  • Rewrites complexos — o GKE Gateway só faz substituição de prefixo; regex de rewrite não existe.
  • Autenticação via annotation (auth-url, basic auth) — o caminho recomendado é o Identity-Aware Proxy.
  • Annotations nginx.ingress.kubernetes.io/* em geral — não existem mais. Timeouts, health checks e políticas de SSL viram recursos próprios do GKE (HealthCheckPolicy, GCPGatewayPolicy).

Como fazemos na DevPlus

Nossos clusters seguiam exatamente o setup do tutorial anterior — ingress-nginx com IP fixo, cert-manager e external-dns. Este guia é o roteiro de migração que adotamos, e a lógica por trás dele é simples: um componente aposentado, com CVEs abertas e sem patches, não pode ser a porta de entrada de um sistema em produção. A Gateway API resolve isso e ainda entrega um Load Balancer global, com TLS na borda e sem pods para manter.

Conclusão

A aposentadoria do ingress-nginx não é uma questão de “se”, é de “quando” — e cada dia com ele na frente do cluster é um dia exposto a vulnerabilidades que não serão corrigidas. A migração para a Gateway API no GKE cabe numa tarde: IP global, certificado gerenciado, um Gateway, dois HTTPRoute, cutover pelo external-dns e limpeza. Rodando os dois em paralelo, os usuários nem percebem.

Precisa migrar um cluster em produção com segurança? A DevPlus planeja e executa migrações de infraestrutura Kubernetes sem downtime. Fale com a gente.

Veja também: GKE Autopilot do zero: DNS e HTTPS automáticos, Guia de sobrevivência: .NET no GKE Autopilot em produção e Health Checks no ASP.NET Core: do básico ao Kubernetes.