DEV Community

Douglas Alves
Douglas Alves

Posted on Originally published at Medium on

Migrating from Ingress NGINX to Gateway API on EKS

Photo by Shamin Haky on Unsplash
Photo by Shamin Haky on Unsplash

The ingress-nginx controller was retired in March 2026. That means no more security updates or bug fixes, so if you rely on NGINX as the ingress controller for your cluster, you need to act.

Worth mentioning : Only the ingress-nginx controller was retired. NGINX as a web server and the Kubernetes Ingress API remain supported.

Why not take the opportunity to modernize your EKS cluster with Gateway API? By the end of this article, you’ll understand what Gateway API is, how to configure its core and AWS-specific components, and how to migrate your existing workloads without any downtime.

Prerequisites

Before you start the migration, make sure you have:

  • AWS Load Balancer Controller v3.0 or later installed. GA support for Gateway API was released in v3.0.
  • EKS cluster running the Amazon VPC CNI plugin. IP-mode target groups require native VPC networking — if you’re using an overlay CNI like Calico in VXLAN mode, this won’t work.
  • The Gateway API CRDs installed in the cluster (upstream CRDs from gateway-api.sigs.k8s.io).
  • The AWS-specific CRDs installed: TargetGroupConfiguration, LoadBalancerConfiguration, and ListenerRuleConfiguration.

Understanding the Gateway API Features

Applying Gateway API to your cluster gives you clear ownership and portability. As an example, platform teams can manage infrastructure while app teams manage application routing. It also gives you portability, as core resources work across providers. That means no more vendor-specific annotation soup.

The basic components of Gateway API in EKS are the following:

Basic components of Gateway API

  • GatewayClass  — Defines which controller manages gateways of this type. Owned by the infrastructure provider (in our case, AWS Load Balancer Controller).
  • Gateway  — Represents the actual load balancer instance. It declares listeners (ports, protocols, TLS), and references a GatewayClass (in our case, also a LoadBalancerConfiguration).
  • HTTPRoute  — Defines the routing rules (paths, headers, weights). It references a Gateway.

AWS Custom Resources:

  • LoadBalancerConfiguration  — Defines all aspects of a load balancer, including scheme (internet-facing or internal), default SSL certificate, WAF protections and others.
  • TargetGroupConfiguration  — Defines all aspects of a target group, including target type (IP or node) and health check configurations. It references a Service and can define per-route health check.

Step 1 — Enable Gateway API support in the AWS Load Balancer Controller

The first step is to enable Gateway API support in the AWS Load Balancer Controller. If you’re using a helm installation (recommended), just change the values as follows and upgrade:

aws-load-balancer-controller:
  clusterName: <your-cluster-name>
  serviceAccount:
    create: true
    name: aws-load-balancer-controller
  controllerConfig:
    featureGates:
      NLBGatewayAPI: true # Enables L4 (TCP/UDP) routing via NLB
      ALBGatewayAPI: true # Enables L7 (HTTP/gRPC) routing via ALB
Enter fullscreen mode Exit fullscreen mode

Step 2 — Install the CRDs

You need two sets of CRDs: the upstream Kubernetes Gateway API CRDs, and the AWS-specific ones.

Gateway API CRDs:

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml
Enter fullscreen mode Exit fullscreen mode

AWS-specific CRDs (install from the AWS Load Balancer Controller repository — check the latest release tag):

kubectl apply -k "github.com/aws/eks-charts/stable/aws-load-balancer-controller/crds?ref=master"
Enter fullscreen mode Exit fullscreen mode

Verify everything is in place:

kubectl api-resources | grep gateway.networking.k8s.io
kubectl api-resources | grep gateway.k8s.aws
Enter fullscreen mode Exit fullscreen mode

You should see GatewayClass, Gateway, HTTPRoute, GRPCRoute in the first output, and LoadBalancerConfiguration, TargetGroupConfiguration, ListenerRuleConfiguration in the second.

Step 3 — The new AWS-specific CRDs

This is the biggest change when coming from Ingress on AWS. Everything you used to configure via annotations is now expressed through typed CRDs. This is strictly better — configuration errors surface at kubectl apply time instead of failing silently at runtime.

LoadBalancerConfiguration

Replaces annotations like alb.ingress.kubernetes.io/scheme, alb.ingress.kubernetes.io/subnets, alb.ingress.kubernetes.io/security-groups. It configures the ALB or NLB that backs your Gateway.

apiVersion: gateway.k8s.aws/v1beta1
kind: LoadBalancerConfiguration
metadata:
  name: internet-facing
  namespace: platform # Must be in the same namespace as your Gateway
spec:
  scheme: internet-facing
  listenerConfigurations:
    defaultCertificate: arn:aws:acm:us-east-1:xxxxxxxxxxxx:certificate/...
    protocolPort: HTTPS:443
  loadBalancerAttributes:
  - key: idle_timeout.timeout_seconds
    value: "305"
  wafV2:
    webACL: arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:regional/webacl/...
Enter fullscreen mode Exit fullscreen mode

TargetGroupConfiguration

Replaces alb.ingress.kubernetes.io/target-group-attributes, alb.ingress.kubernetes.io/healthcheck, and similar annotations. Attached to a Service.

apiVersion: gateway.k8s.aws/v1beta1
kind: TargetGroupConfiguration
metadata:
  name: api-tg-config
  namespace: my-app-ns
spec:
  targetReference:
    kind: Service
    name: api-service
  defaultConfiguration:
    targetType: ip
  routeConfigurations:
    - routeIdentifier:
        kind: HTTPRoute
        namespace: my-app-ns
        name: my-app-route
      targetGroupProps:
        healthCheckConfig:
          healthCheckPath: /healthz
          healthCheckPort: 8080
          healthCheckProtocol: HTTP
          healthyThresholdCount: 2
          unhealthyThresholdCount: 2
          healthCheckTimeout: 5
          healthCheckInterval: 15
          matcher:
            httpCode: "200"
Enter fullscreen mode Exit fullscreen mode

Step 4 — Create the GatewayClass

The GatewayClass tells Kubernetes which controller manages gateways of this type. It’s a cluster-scoped resource — your platform team creates it once, and app teams reference it by name.

For ALB (L7):

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

For NLB (L4):

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: aws-nlb
spec:
  controllerName: gateway.k8s.aws/nlb
Enter fullscreen mode Exit fullscreen mode

Step 5 — Create the Gateway

The Gateway represents your actual load balancer. It defines listeners and references a LoadBalancerConfiguration for AWS-specific settings.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: default-gateway
  namespace: platform # Must be in the same namespace as your LoadBalancerConfiguration
spec:
  gatewayClassName: aws-alb
  infrastructure:
    parametersRef:
      group: gateway.k8s.aws
      kind: LoadBalancerConfiguration
      name: internet-facing # Your LoadBalancerConfiguration
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All
  - name: https
    protocol: HTTPS
    port: 443
    allowedRoutes:
      namespaces:
        from: All
    tls:
      mode: Terminate
      options:
        aws.amazon.com/acm-certificate-arn: arn:aws:acm:us-east-1:xxxxxxxxxxxx:certificate/...
Enter fullscreen mode Exit fullscreen mode

After applying this, the AWS Load Balancer Controller provisions an ALB. Check the status:

kubectl get gateway default-gateway -n platform
Enter fullscreen mode Exit fullscreen mode

The ADDRESS field will populate with the ALB's DNS name once it's PROGRAMMED: True. This takes 1-2 minutes.

Step 6 — Create HTTPRoutes for your applications

HTTPRoute is where your application teams live. It maps to the routing rules you had in your Ingress spec, but without the annotation noise.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app-route
  namespace: my-app-ns
spec:
  parentRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: default-gateway
      namespace: platform # Cross-namespace reference to the shared Gateway
      sectionName: https # Attach to the HTTPS listener specifically
  hostnames:
    - "myapp.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-service
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: frontend-service
          port: 3000
Enter fullscreen mode Exit fullscreen mode

Traffic splitting is now native. Just set a weight on each backendRef:

rules:
    - backendRefs:
        - name: my-app-v1
          port: 8080
          weight: 90
        - name: my-app-v2
          port: 8080
          weight: 10
Enter fullscreen mode Exit fullscreen mode

The migration strategy: no downtime required

Don’t delete your NGINX Ingress resources first. Here’s the safe path:

1. Run both controllers in parallel

Deploy the Gateway API resources alongside your existing NGINX Ingress. They’ll get different AWS load balancer endpoints. Your NGINX Ingress keeps serving production traffic.

2. Test the Gateway endpoint

After your Gateway and HTTPRoute resources are applied and the ALB is PROGRAMMED, test it directly by hitting the ALB DNS name with a Host header:

curl -H "Host: myapp.example.com" http://<alb-dns-name>/api/healthz
Enter fullscreen mode Exit fullscreen mode

3. Shift traffic progressively via weighted DNS

Update your DNS record to use weighted routing (Route 53 weighted records work perfectly here). Start with 5–10% of traffic going to the new ALB, watch your metrics and error rates, then gradually shift to 100%.

4. Clean up NGINX resources

Once 100% of traffic is on the Gateway API path and you’ve monitored for a stability period, remove your Ingress resources and eventually the NGINX controller itself.

Common pitfalls

  • Internal ALB instead of internet-facing. If you don’t explicitly set scheme: internet-facing in your LoadBalancerConfiguration, you get an internal ALB that's not reachable from the internet. This is the single most common "why isn't it working" issue.
  • LoadBalancerConfiguration in the wrong namespace. When attaching a LoadBalancerConfiguration to a Gateway, it must be in the same namespace as the Gateway. Cross-namespace references are not supported here.
  • Feature gates not enabled. If your Gateway stays in a Pending state and the controller logs show nothing about it, check that ALBGatewayAPI: true is in your feature gates. The controller silently ignores Gateway resources when the feature is disabled.
  • VPC CNI required for IP mode. If you’re using a non-VPC-native CNI, IP-mode target groups won’t work. You can use NodePort services with instance target type, but you lose the pod-level routing precision.
  • Always check status conditions. Check the status field of your Gateway and HTTPRoute resources — the controller writes detailed conditions there, including the ALB DNS name, ARN, and any reconciliation errors.
kubectl describe gateway default-gateway -n platform
kubectl describe httproute my-app-route -n my-app
Enter fullscreen mode Exit fullscreen mode

Conclusions

Gateway API on EKS is genuinely better than Ingress. The role separation between GatewayClass, Gateway, and HTTPRoute maps cleanly to how platform teams and application teams actually work. The typed CRDs (LoadBalancerConfiguration, TargetGroupConfiguration) replace annotation soup with something you can actually validate. And the routing capabilities — header-based routing, traffic splitting, gRPC support — are built in rather than bolted on.

The migration itself is less scary than it looks. Run both in parallel, shift DNS progressively, and clean up when you’re confident. The NGINX controller retirement is a forcing function, but it’s also a good excuse to modernize your cluster networking in a way you’ll appreciate for years.

Have questions or suggestions? Let me hear your thoughts in the comments — I’m happy to help and discuss!

Resources:

Top comments (0)