Zero-Downtime Migration from NGINX Ingress to Gateway API on Amazon EKS (Production Case Study)

Search for a command to run...

No comments yet. Be the first to comment.
The Ops Fix Hub is a dynamic series designed to tackle real-world challenges in the DevOps and CloudOps domains. "Breaking Down Barriers, One Ops Fix at a Time."
1. Overview This post documents how I designed and implemented cross-region disaster recovery for a production MySQL database running on Amazon RDS. The requirement was straightforward: If the primary region (ap-south-1) becomes unavailable, the data...
This migration was performed on a production workload where cost reduction was prioritized over zone-level high availability.

1. Overview What I Designed I designed a hybrid infrastructure architecture: Terraform → Foundation Layer Crossplane → Dynamic Lifecycle Layer ArgoCD → GitOps Enforcement This created a continuou

Cross-cloud VM migration is not a disk copy task. It is: An access model transformation A replication lifecycle management exercise A downtime control operation A cost boundary decision We execu

When AWS introduced AWS DevOps Agent, I was less interested in feature lists and more interested in one practical question. Can it actually reduce investigation time during real production-style failu

Migrating object storage across cloud providers is not a copy task.It is a cost, network, and security boundary problem. We migrated 10+ TB of object data from Google Cloud Storage to Amazon S3 under

In this post, we walk through a real production migration of a Kubernetes workload from NGINX Ingress Controller to Kubernetes Gateway API, implemented using Envoy Gateway, on Amazon EKS.
The key objective was to:
Migrate safely with zero downtime
Avoid introducing unnecessary cloud-specific complexity
Align the platform with Kubernetes’ future networking direction
This guide is written from a platform ownership perspective, not a lab or demo setup.
The application was already running in production and exposed using NGINX Ingress Controller.
While the setup was stable, the following risks were identified:
The NGINX Ingress Controller project has moved toward reduced long-term maintenance focus, increasing uncertainty around future support guarantees.
No long-term guarantees for:
Security patches
CVE fixes
Compatibility with future Kubernetes versions
Ingress sits at the cluster edge, making it a high-blast-radius component
Although there was no immediate outage, continuing with an edge component under reduced maintenance posed long-term operational and security risks.
User
↓
AWS LoadBalancer (auto-created by Service)
↓
NGINX Ingress Controller
↓
Application Service (ClusterIP)
↓
Application Pods
Stable and functional
Easy to operate
Tightly coupled to controller-specific annotations
Limited separation between platform and application ownership
Kubernetes Gateway API is positioned as the successor to Ingress, designed to solve long-standing limitations.
| Ingress | Gateway API |
| Single resource | Role-oriented resources |
| Annotation-driven | Spec-defined configuration |
| Weak ownership boundaries | Clear infra vs app separation |
| Controller-specific behavior | Standardized API |
Gateway API introduces:
GatewayClass – defines platform capability
Gateway – infrastructure-level entry point
HTTPRoute – application-level routing rules
This model is more scalable, auditable, and production-safe.
The cluster did not have AWS Load Balancer Controller installed.
Installing it mid-migration would have required:
IAM and IRSA setup
Additional operational complexity
Increased blast radius during a live migration
Instead, we chose Envoy Gateway, because it:
Is a first-class Gateway API implementation
Does not depend on AWS-specific controllers
Creates and manages its own dataplane
Is vendor-neutral and portable
Allows parallel validation with minimal risk
This decision was intentional, not a workaround.
I intentionally avoided introducing AWS Load Balancer Controller during migration to prevent IAM, IRSA, and cloud-controller changes from increasing the migration blast radius. The goal was to change one edge component at a time.
A direct replacement was not acceptable.
NGINX Ingress LoadBalancer → continues serving production traffic
Envoy Gateway LoadBalancer → used for validation
Traffic was cut over only after successful validation was completed.
The existing Ingress resource was left untouched to prevent configuration drift and unintended side effects during migration.
This ensured:
No user impact
Easy rollback
Controlled blast radius
The application was deployed with:
Kubernetes Deployment
Service of type ClusterIP
No changes were required at the application level.
NGINX Ingress Controller was already installed and exposed the application via an AWS LoadBalancer.
This remained untouched during the migration.
Gateway API resources must exist before any controller can operate.
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml
Envoy Gateway was installed using Helm via OCI registry.
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.0 \
-n envoy-gateway-system \
--create-namespace
The Envoy Gateway version was explicitly pinned to v1.7.0 after verifying compatibility with Gateway API v1.0.0 and the EKS cluster version.
Version pinning ensures deterministic deployments, reproducibility, and safe rollback capability in production environments.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
This explicitly defined Envoy Gateway as the cluster’s Gateway API implementation.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: app-gateway
namespace: default
spec:
gatewayClassName: envoy
listeners:
- protocol: HTTP
port: 80
This created a new AWS LoadBalancer, separate from the existing NGINX Ingress LB.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app
namespace: default
spec:
parentRefs:
- name: app-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app
port: 8088
This replaced the Ingress routing logic using Gateway API primitives.
At this stage:
NGINX LB → Production users
Gateway LB → Validation traffic
Validation was performed at multiple levels:
Verified HTTP 200 responses using curl
Tested authentication flows
Executed critical user workflows
Confirmed session persistence behavior
Checked LoadBalancer health check status
Verified readiness and liveness probes
Monitored pod logs for errors or unexpected restarts
Confirmed correct backend service port mapping
Reviewed Envoy Gateway metrics and controller logs to ensure no reconciliation errors or route attachment failures were present.
Compared response latency between both entry points
Monitored 4xx and 5xx error rates
Verified no increase in backend CPU or memory usage
Only after all validation checkpoints passed was production cutover approved.
Running NGINX Ingress and Envoy Gateway in parallel resulted in two active AWS LoadBalancers during the validation window, temporarily increasing infrastructure cost.
However:
The overlap period was intentionally short.
The additional cost was justified to eliminate downtime risk.
The parallel approach reduced blast radius during migration.
Cost was intentionally traded for reliability and controlled risk.
After all validation checks passed:
kubectl delete ingress app-ingress
Traffic shift was verified immediately after deletion by validating active connections on the Gateway LoadBalancer and confirming healthy backend responses.
The legacy NGINX Ingress was removed only after confirming stable traffic flow through the Gateway LoadBalancer.
Rollback plan:
Re-apply the Ingress resource if needed
Restore DNS if traffic switch involved domain update
The migration was reversible during the validation window.
Optionally, after a stability window:
helm uninstall ingress-nginx -n ingress-nginx
The Gateway API entry point became the sole production path.
User
↓
AWS LoadBalancer
↓
Envoy Gateway (Gateway API)
↓
Application Service
↓
Application Pods
Gateway without HTTPRoute does nothing — infrastructure and routing are intentionally separated
Gateway API enforces clearer ownership boundaries than Ingress
Parallel migration is the safest approach for production workloads
Envoy Gateway is an effective bridge when cloud-native controllers are not yet in place
In a later phase, once the platform is stable on the Gateway API.
Typical evolution:
NGINX Ingress
→ Envoy Gateway (Gateway API adoption)
→ AWS Load Balancer Controller (cloud-native optimization)
The following risks were evaluated before migration:
Gateway created without HTTPRoute (no traffic routing)
Incorrect backend service port reference
Namespace mismatch between Gateway and HTTPRoute
LoadBalancer health check failures
Controller crash or misconfiguration
Gateway API CRD and controller version mismatch
DNS TTL delays during traffic switch
By running both entry points in parallel, these risks were isolated and mitigated.
I designed and executed a zero-downtime migration from NGINX Ingress to Gateway API by running both entry points in parallel.
I validated routing behavior, health checks, infrastructure readiness, and traffic stability before shifting production traffic.
This approach reduced blast radius, preserved service availability, and aligned the platform with Kubernetes’ evolving networking model.