Gateway API no EKS com o AWS Load Balancer Controller e ALB

A Gateway API é a sucessora do Ingress no Kubernetes. Em vez de um único objeto cheio de anotações de cada fornecedor, ela divide a publicação de uma aplicação em papéis. Um define o tipo de load balancer, outro abre as portas e um terceiro cuida das rotas. No EKS, o AWS Load Balancer Controller transforma esses objetos num Application Load Balancer de verdade.

Este artigo continua a série artigos-eks. No artigo do AWS Load Balancer Controller, a aplicação saiu num ALB a partir de um Ingress. Agora o mesmo resultado sai de um Gateway e de um HTTPRoute, num cluster 1.37 criado com Terraform. Tudo o que aparece aqui foi executado num laboratório real, com os prints do console e a saída do kubectl, e destruído no fim.

Segurança: os comandos deste guia usam o perfil do AWS CLI para autenticar. Nunca coloque access keys no código ou em .tfvars versionado; prefira aws sso login ou um perfil com credenciais temporárias.

Sumário

Pré-requisitos

AWS CLI v2 autenticado, Terraform >= 1.5, kubectl e, de preferência, o helm para consultar versões. As versões usadas no laboratório:

  • Kubernetes 1.37 no EKS;
  • chart eks/aws-load-balancer-controller 3.6.0;
  • CRDs da Gateway API v1.6.0, canal standard;
  • providers hashicorp/aws ~> 6.0 e hashicorp/helm ~> 3.0.

Ingress × Gateway API: o que muda

Com Ingress, tudo fica num objeto só: o esquema do ALB, o tipo de target, os caminhos, o certificado. O que a especificação não cobre vira anotação, e cada controller inventa as suas (alb.ingress.kubernetes.io/..., nginx.ingress.kubernetes.io/...). Trocar de controller significa reescrever as anotações.

A Gateway API divide a mesma informação em recursos com donos diferentes:

RecursoQuem cuidaO que vira no ALB
GatewayClassTime de plataformaQual controller atende (aqui, o ALB)
GatewayPlataforma ou operação do clusterO ALB e os listeners (porta e protocolo)
HTTPRouteTime da aplicaçãoRegras do listener e target groups
LoadBalancerConfiguration, TargetGroupConfiguration, ListenerRuleConfigurationQuem precisa de ajuste específico da AWSO que antes era anotação: esquema, tipo de target, atributos

Na prática, o time de plataforma publica um Gateway e cada time de aplicação pendura nele as suas rotas, sem tocar no ALB. O que é específico da AWS deixa de ser anotação solta e passa a ser um recurso tipado, validado pela API do Kubernetes.

O ambiente com Terraform

A infraestrutura é a mesma do artigo do controller, composta pelos módulos da série: VPC com as tags de subnet, cluster, node group, add-ons e o controller instalado com Helm e permissão via EKS Pod Identity. Só mudam as versões: Kubernetes 1.37 e chart 3.6.0.

module "lb_controller" {
  source = "../../terraform-eks-lb-controller-modules"

  cluster_name  = module.eks.name
  region        = var.region
  vpc_id        = module.vpc.vpc_id
  chart_version = var.lb_controller_chart_version   # "3.6.0"

  depends_on = [module.addons]
}

Ao subir o chart, a regra é trocar também o iam_policy.json do módulo pela versão correspondente. Conferi a política da tag v3.6.0 contra a da v3.4.0, que o módulo já tinha: são idênticas, então a role não precisou mudar.

terraform init
terraform plan  -var 'public_access_cidrs=["SEU_IP/32"]'   # 32 recursos
terraform apply -var 'public_access_cidrs=["SEU_IP/32"]'

aws eks update-kubeconfig --region us-east-1 --name eks-blog-castilho

No laboratório, o apply levou cerca de 13 minutos e meio, quase todo gasto no control plane e no node group.

Gateway API no EKS: cluster eks-blog-castilho ativo na versão 1.37
Cluster eks-blog-castilho ativo na versão 1.37
Node group default com 2 nós e AMI 1.37 no console do EKS
Node group default com 2 nós t3.medium
Add-ons do EKS ativos: Pod Identity Agent, VPC CNI e CoreDNS
Add-ons ativos, entre eles o Pod Identity Agent usado pelo controller

Os dois nós aparecem no kubectl já na versão do control plane:

$ kubectl get nodes
NAME                          STATUS   ROLES    AGE   VERSION
ip-10-0-41-27.ec2.internal    Ready    <none>   20m   v1.37.1-eks-117f323
ip-10-0-48-165.ec2.internal   Ready    <none>   20m   v1.37.1-eks-117f323
Instâncias EC2 dos nós do cluster EKS em execução
As instâncias EC2 dos nós, em subnets privadas

Instalando os CRDs da Gateway API

Existem dois conjuntos de CRDs, e é aqui que mora a primeira pegadinha. O chart 3.6.0 já instala os CRDs do próprio controller: LoadBalancerConfiguration, TargetGroupConfiguration e ListenerRuleConfiguration. Os CRDs padrão da Gateway API (GatewayClass, Gateway, HTTPRoute e outros) não vêm com o EKS nem com o chart. Eles saem do projeto upstream:

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.0/standard-install.yaml

kubectl get crd | grep gateway

Se o controller subiu antes desses CRDs existirem, como acontece quando o Terraform instala o chart e só depois você aplica os CRDs, ele desliga o suporte à Gateway API na inicialização e registra no log:

Disabling ALBGatewayAPI: missing required CRDs

A verificação só acontece no início. Aplicar os CRDs depois não religa nada: o Gateway fica sem endereço e sem nenhum evento explicando o motivo. A saída é reiniciar o controller depois de aplicar os CRDs:

kubectl rollout restart deployment aws-load-balancer-controller -n kube-system
kubectl logs -n kube-system deploy/aws-load-balancer-controller | grep -i gateway

Em pipeline, a forma de evitar isso é garantir a ordem: CRDs primeiro, chart depois. Ou acrescentar o restart como passo fixo logo após a aplicação dos CRDs.

GatewayClass e LoadBalancerConfiguration

A GatewayClass é um recurso de cluster que diz qual controller atende os Gateway daquela classe. Para ALB, o controllerName é gateway.k8s.aws/alb (para NLB existe o gateway.k8s.aws/nlb):

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: aws-alb
spec:
  controllerName: gateway.k8s.aws/alb

O esquema do ALB, que no Ingress era a anotação alb.ingress.kubernetes.io/scheme, agora fica numa LoadBalancerConfiguration. Os valores são os mesmos do Ingress: internal, nas subnets com a tag kubernetes.io/role/internal-elb, ou internet-facing, nas subnets com kubernetes.io/role/elb. Para um ALB acessível pela internet, declare o esquema de forma explícita:

apiVersion: gateway.k8s.aws/v1
kind: LoadBalancerConfiguration
metadata:
  name: loja-alb
  namespace: loja
spec:
  scheme: internet-facing

A aplicação de exemplo tem dois Deployments no namespace loja: web (nginx) e api (Apache httpd), cada um com dois pods e um Service ClusterIP na porta 80. O manifesto completo está no repositório do GitHub, no fim do artigo.

Gateway: o ALB e o listener

O Gateway aponta para a classe, liga a LoadBalancerConfiguration pelo infrastructure.parametersRef e declara os listeners. Cada listener vira um listener do ALB:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: loja
  namespace: loja
spec:
  gatewayClassName: aws-alb
  infrastructure:
    parametersRef:
      group: gateway.k8s.aws
      kind: LoadBalancerConfiguration
      name: loja-alb
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Same

O allowedRoutes.namespaces.from: Same é o controle de acesso que faltava no Ingress: só rotas do namespace loja podem se pendurar nesse listener. Com Ingress e group.name, qualquer namespace que repetisse o nome do grupo acrescentava regras ao ALB. Aqui, para liberar outros namespaces, o dono do Gateway precisa trocar para All ou para um Selector por label.

TargetGroupConfiguration: target-type ip

O tipo de target, que no Ingress era alb.ingress.kubernetes.io/target-type: ip, vai para uma TargetGroupConfiguration ligada a cada Service. Com ip, o ALB registra direto os IPs dos pods, que no EKS são IPs da VPC graças ao VPC CNI, e o tráfego não passa por NodePort:

apiVersion: gateway.k8s.aws/v1
kind: TargetGroupConfiguration
metadata:
  name: api-ip
  namespace: loja
spec:
  targetReference:
    name: api
  defaultConfiguration:
    targetType: ip

O Service web recebe uma configuração igual, com name: web-ip e targetReference.name: web.

HTTPRoute: rotas por caminho e reescrita de URL

O HTTPRoute é a parte do time da aplicação. A ideia do exemplo: /api vai para o httpd e todo o resto vai para o nginx. A primeira versão do lab foi a tradução direta de um Ingress:

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /api
    backendRefs:
      - name: api
        port: 80

O resultado foi 404 do httpd em /api. O PathPrefix só escolhe o destino; ele não remove o prefixo. A requisição chegou ao Apache como /api, e o Apache não tem nada nesse caminho. Com Ingress acontece o mesmo, mas lá a correção dependia de recurso de cada controller. Na Gateway API, a reescrita faz parte da especificação, com o filtro URLRewrite:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: loja
  namespace: loja
spec:
  parentRefs:
    - name: loja
      sectionName: http
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
      backendRefs:
        - name: api
          port: 80
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: web
          port: 80

O controller traduz o filtro para o recurso nativo de transformação de URL das regras do ALB: uma expressão regular (^/api(/)?) trocada por /. A reescrita acontece no próprio load balancer, sem sidecar nem proxy extra. Depois do ajuste, /api respondeu com a página padrão do httpd.

kubectl apply -f app.yaml
kubectl apply -f gateway.yaml

kubectl get gatewayclass
kubectl get gateway,httproute -n loja

O que o controller criou na AWS

O ALB apareceu no console cerca de 15 segundos depois do apply. Já o PROGRAMMED=True no Gateway e a primeira resposta 200 levaram de 4 a 5 minutos. É o tempo de o ALB terminar o provisionamento, de o DNS propagar e de os pods passarem no health check.

$ kubectl get gatewayclass
NAME      CONTROLLER            ACCEPTED   AGE
aws-alb   gateway.k8s.aws/alb   True       16m

$ kubectl get gateway -n loja
NAME   CLASS     ADDRESS                                                     PROGRAMMED   AGE
loja   aws-alb   k8s-loja-loja-xxxxxxxxxx-nnnnnnnnnn.us-east-1.elb.amazonaws.com   True         16m

$ kubectl get httproute -n loja
NAME   HOSTNAMES   AGE
loja               16m

O nome segue o padrão k8s-<namespace>-<gateway>-<hash>. O esquema veio da LoadBalancerConfiguration e as subnets públicas foram descobertas pelas tags:

ALB internet-facing criado pela Gateway API no EKS
ALB internet-facing criado a partir do Gateway, em duas zonas de disponibilidade

As regras do listener HTTP:80 mostram a tradução do HTTPRoute. A regra 1 casa /api e /api/* e aplica a transformação de URL; a regra 2 manda o resto para o nginx. A ação padrão, quando nenhuma regra casa, é uma resposta fixa 404 gerada pelo próprio ALB:

Regras do listener do ALB geradas pelo HTTPRoute com transformação de URL
Regras do listener: o filtro URLRewrite virou uma transformação de URL do ALB

E cada Service virou um target group do tipo IP, com os pods registrados direto:

Target group do tipo IP com os pods saudáveis
Target group do tipo IP com os pods saudáveis

Os IPs no target group são os mesmos dos pods:

$ kubectl get pods -n loja -o wide
NAME                       STATUS    IP
api-767f6c7bfc-6tkkd       Running   10.0.39.37
api-767f6c7bfc-wz955       Running   10.0.53.135
web-5f7f65895b-ld49z       Running   10.0.62.152
web-5f7f65895b-qxwdv       Running   10.0.41.152

$ ALB=$(kubectl get gateway loja -n loja -o jsonpath='{.status.addresses[0].value}')
$ curl -s -o /dev/null -w "%{http_code}\n" http://$ALB/
200
$ curl -s http://$ALB/api | grep title
<title>It works! Apache httpd</title>

Uma observação sobre os prints: com uma sessão federada só de leitura, o console do EKS mostra “Unauthorized” nas abas de recursos do Kubernetes, porque ela não tem access entry no cluster. Por isso os pods e os nós aparecem aqui pela saída do kubectl. Quem precisa ver workloads no console deve ganhar uma access entry com política de leitura, como mostrado no artigo de Access Entries.

Ingress ou Gateway API?

O Ingress não foi removido do Kubernetes e o AWS Load Balancer Controller continua atendendo os dois. Os mesmos pods podem ser publicados por um Ingress e por um Gateway ao mesmo tempo, cada um com o seu ALB. Isso permite migrar aplicação por aplicação, trocando o DNS no fim.

  • Fique no Ingress se o cluster tem poucas aplicações, um time só, e as anotações atuais resolvem. Nesse cenário, a migração dá trabalho e traz pouco ganho.
  • Vá para a Gateway API se vários times dividem o mesmo load balancer, se você quer separar quem cria o ALB de quem cria as rotas, ou se depende de recursos que no Ingress eram anotação específica: reescrita de URL, divisão de tráfego por peso entre backendRefs, roteamento por header.
  • Projetos novos já podem começar pela Gateway API: a especificação é estável no canal standard e o suporte do controller é GA desde a v3.0.0.

Custos e limpeza

  • Gateway API e controller: sem custo próprio; os pods do controller rodam nos nós.
  • ALB: cobrança por hora e por LCU. Cada Gateway é um ALB. Compartilhar um Gateway entre vários times custa menos do que criar um por aplicação.
  • Cluster base: control plane, NAT Gateway e nós, como no primeiro artigo da série.

A ordem da limpeza é a mesma do Ingress: o ALB, os target groups e os security groups foram criados pelo controller, fora do state do Terraform. Apague primeiro os objetos do Kubernetes e espere o ALB sumir; só depois rode o destroy:

kubectl delete -f gateway.yaml
kubectl delete -f app.yaml
aws elbv2 describe-load-balancers --query 'LoadBalancers[?starts_with(LoadBalancerName,`k8s-loja`)].LoadBalancerName'
# espere a lista ficar vazia

terraform destroy -var 'public_access_cidrs=["SEU_IP/32"]'

# o EKS cria este log group sozinho e o Terraform não o remove
aws logs delete-log-group --log-group-name /aws/eks/eks-blog-castilho/cluster

No laboratório, o destroy removeu os 32 recursos. O log group /aws/eks/<cluster>/cluster é criado pelo próprio EKS quando há logs do control plane ativos; como não está no state, ficou para trás e precisou ser apagado à mão. Vale conferir também se não sobrou ALB, target group ou security group com prefixo k8s-.

Conclusão

A Gateway API no EKS entrega o mesmo ALB que o Ingress, mas com papéis separados: a GatewayClass escolhe o controller, o Gateway abre o load balancer e controla quem pode usá-lo, e o HTTPRoute fica com o time da aplicação. As anotações viraram recursos tipados (LoadBalancerConfiguration, TargetGroupConfiguration) e a reescrita de URL virou parte da especificação, traduzida para um recurso nativo do ALB.

Os dois pontos que mais custaram tempo no laboratório valem como checklist: aplicar os CRDs padrão antes do controller (ou reiniciá-lo depois) e lembrar que PathPrefix não tira o prefixo sem um URLRewrite.

Referências oficiais

Interessado em saber mais sobre esse assunto?

Deixe uma resposta

Descubra mais sobre Blog do Castilho - Tecnologia | FinOps | DevOps | Cloud

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continuar lendo