
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:
- 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
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
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"
Verify everything is in place:
kubectl api-resources | grep gateway.networking.k8s.io
kubectl api-resources | grep gateway.k8s.aws
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/...
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"
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
For NLB (L4):
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: aws-nlb
spec:
controllerName: gateway.k8s.aws/nlb
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/...
After applying this, the AWS Load Balancer Controller provisions an ALB. Check the status:
kubectl get gateway default-gateway -n platform
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
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
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
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
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)