Kubernetes Ingress Controllers - NGINX, Alternatives, and the Gateway API
Deploying ingress-nginx, configuring TLS with cert-manager, and understanding where the Gateway API fits as Ingress's successor
Introduction
An Ingress resource by itself does nothing - it’s a routing specification that requires an Ingress controller running in the cluster to actually watch it and program a load balancer or proxy. This guide covers installing ingress-nginx (still the most widely deployed controller), TLS with cert-manager, and the Gateway API, which is gradually superseding Ingress for anything beyond basic HTTP routing.
Installing ingress-nginx
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.service.type=LoadBalancer
kubectl get svc -n ingress-nginx ingress-nginx-controller
The controller’s LoadBalancer Service is what actually gets a cloud load balancer provisioned (an ALB-equivalent on AWS, a Load Balancer on Azure/GCP) - every Ingress resource in the cluster then routes through that single entry point based on hostname and path, rather than each service getting its own load balancer.
A Basic Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
ingressClassName: nginx is the modern way to target a specific controller when more than one is installed in a cluster - it replaced the older kubernetes.io/ingress.class annotation, which is deprecated (still honored by most controllers for compatibility, but new manifests should use the field, not the annotation).
TLS with cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
ingressClassName: nginx
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls-cert
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
The cert-manager.io/cluster-issuer annotation on the Ingress is what triggers cert-manager to watch it, request a certificate from Let’s Encrypt via the HTTP-01 challenge (which cert-manager solves by temporarily programming its own routes through the same ingress controller), and store the result in the app-tls-cert Secret the tls block references - no separate Certificate resource is required for this common case, though creating one explicitly is also supported when you need more control over renewal timing or key algorithm.
Where the Gateway API Fits
Ingress was deliberately kept minimal - host/path routing and not much else - so every controller invented its own annotations (like nginx.ingress.kubernetes.io/rewrite-target above) for anything beyond that, meaning Ingress manifests are rarely portable between controllers without editing annotations. The Gateway API is the community’s role-oriented successor, with core resources (Gateway, HTTPRoute) that graduated to v1 and are implemented consistently across controllers (including ingress-nginx, via a separate Gateway API-specific installation):
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: app-gateway
spec:
gatewayClassName: nginx
listeners:
- name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- name: app-tls-cert
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-route
spec:
parentRefs:
- name: app-gateway
hostnames:
- app.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-service
port: 80
Ingress isn’t deprecated and remains fully supported - it’s still the simpler choice for basic host/path routing to a single controller. Reach for the Gateway API when you need traffic splitting, header-based routing, or a routing configuration that needs to work identically across multiple controller implementations without controller-specific annotations.
Best Practices
- Pin
ingressClassNameexplicitly on every Ingress once more than one controller might ever run in the cluster - an Ingress with no class can be picked up by an unintended controller if a second one is later installed. - Terminate TLS at the ingress controller, not in application pods, unless you have a specific mutual-TLS requirement - it centralizes certificate management and keeps application code free of TLS termination logic.
- Set resource requests/limits on the controller’s own pods - it’s a shared, cluster-critical component; an under-provisioned controller becomes a single point of failure for every application behind it.
- Use
cert-managerfor automated renewal rather than manually rotating certificates - Let’s Encrypt certificates are short-lived (90 days) by design specifically to make automation the only sane operational path.
Conclusion
ingress-nginx plus cert-manager covers the overwhelming majority of “route HTTPS traffic into the cluster” use cases with minimal moving parts. The Gateway API is worth adopting once controller-specific annotations become a real portability or expressiveness problem, not as a default replacement for every basic Ingress.