Ingress Controllers in AKS
When you have five microservices that all need to be reachable over HTTP, creating five LoadBalancer Services means five public IP addresses and five Azure load balancers. An Ingress controller collapses that into one. It sits in front of all your services and routes requests based on the hostname and URL path the client requested.
How Ingress works
An Ingress controller is a reverse proxy running as a pod in your cluster. It watches the Kubernetes API for Ingress resources — routing rule objects — and reconfigures itself whenever rules change.
The flow for an incoming request:
- Client sends a request to the ingress controller’s public IP
- The controller reads the
Hostheader and the URL path - It matches against ingress rules and finds the correct backend Service
- It proxies the request to a pod behind that Service
The ingress controller itself is exposed via one LoadBalancer Service. All routing happens inside the cluster.
Ingress controller options in AKS
| Controller | Managed by | Best for | Notes |
|---|---|---|---|
| NGINX Ingress Controller | You (Helm) | General purpose HTTP routing | Most widely used, excellent docs |
| Application Gateway Ingress (AGIC) | Azure (managed add-on) | WAF, SSL offload, enterprise needs | Uses Azure Application Gateway |
| Azure NGINX Ingress Controller | Azure (managed) | Managed NGINX without self-hosting | Newer, preview features |
| Traefik | You (Helm) | Dynamic routing, Let’s Encrypt auto-TLS | Good for microservices |
NGINX Ingress Controller is the most commonly used option. This page focuses on it.
Installing NGINX Ingress Controller with Helm
The standard way to install NGINX Ingress in AKS is via Helm:
# Add the NGINX Helm repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
# Create a dedicated namespace for the ingress controller
kubectl create namespace ingress-nginx
# Install NGINX Ingress Controller
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--set controller.replicaCount=2 \
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set defaultBackend.nodeSelector."kubernetes\.io/os"=linux
# Wait for the load balancer IP to be assigned
kubectl get service ingress-nginx-controller \
--namespace ingress-nginx \
--watchOnce the EXTERNAL-IP is assigned, that is your single entry point for all Ingress-routed traffic.
# Save the IP for use in DNS or testing
INGRESS_IP=$(kubectl get service ingress-nginx-controller \
--namespace ingress-nginx \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $INGRESS_IPCreating an Ingress resource
With the controller running, define routing rules with Ingress resources. Suppose you have two services: web-frontend and api-service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-frontend
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80kubectl apply -f app-ingress.yaml
# Verify the ingress was configured
kubectl get ingress app-ingress -n production
# NAME CLASS HOSTS ADDRESS PORTS AGE
# app-ingress nginx myapp.example.com 20.50.200.11 80 1mRequests to myapp.example.com/ go to web-frontend. Requests to myapp.example.com/api go to api-service. One IP, one load balancer, multiple services.
Path types explained
Ingress path matching has three modes:
- Exact — only the exact path matches.
/apimatches/apibut not/api/users. - Prefix — matches the path and any sub-paths.
/apimatches/api,/api/users,/api/users/123. - ImplementationSpecific — controller decides. For NGINX, this allows regex patterns in the path.
Use Prefix for most API routing. Use Exact for specific endpoints that should not accidentally match sub-paths.
TLS termination
To serve HTTPS, create a Kubernetes Secret with your TLS certificate and key, then reference it in the Ingress resource:
# Create a TLS secret from certificate files
kubectl create secret tls myapp-tls \
--cert=myapp.crt \
--key=myapp.key \
--namespace productionapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress-tls
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-frontend
port:
number: 80With ssl-redirect: “true”, HTTP requests are automatically redirected to HTTPS. The ingress controller terminates TLS and forwards plain HTTP to the backend services.
Automating certificates with cert-manager
Manually managing certificates is tedious. cert-manager automates certificate issuance and renewal from Let’s Encrypt or other certificate authorities.
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
# Create a Let's Encrypt ClusterIssuer
cat <<EOF | kubectl apply -f -
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
solvers:
- http01:
ingress:
class: nginx
EOFAdd an annotation to your Ingress resource to trigger automatic certificate issuance:
metadata:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls-auto # cert-manager creates thiscert-manager watches for this annotation, requests a certificate from Let’s Encrypt, stores it in the Secret, and renews it before it expires. You never touch TLS certificates manually again.
Use the Let’s Encrypt staging server during testing to avoid rate limits. Replace acme-v02.api.letsencrypt.org/directory with acme-staging-v02.api.letsencrypt.org/directory. Staging certificates are not trusted by browsers but let you test the workflow without hitting production limits.
Useful NGINX Ingress annotations
NGINX Ingress Controller behavior is customized via annotations on Ingress resources:
metadata:
annotations:
# Require HTTPS (redirect HTTP to HTTPS)
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Increase upload size limit
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# Rate limiting: 100 requests per minute per IP
nginx.ingress.kubernetes.io/limit-rpm: "100"
# Enable CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
# Sticky sessions (route same client to same pod)
nginx.ingress.kubernetes.io/affinity: "cookie"
# Custom timeout
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"Common mistakes
- Missing ingressClassName field. In newer Kubernetes versions, the
spec.ingressClassName: nginxfield is required. Without it, the ingress resource is created but ignored by the controller. Old guides use the annotation approach which is now deprecated. - Backend Services using ClusterIP type. Ingress controllers route to Services, not pods. The backend Services should be ClusterIP type — they do not need to be LoadBalancer. Using LoadBalancer for backends wastes Azure public IPs.
- Path not matching because of trailing slash differences.
/apiand/api/can behave differently depending on pathType. Test both forms during setup and use the rewrite-target annotation to normalize paths if needed. - Let’s Encrypt rate limits during testing. Let’s Encrypt limits certificate issuance to 50 per domain per week. Repeatedly deleting and recreating certificates during testing quickly hits the limit. Always use the staging server for testing.
- Deploying only one ingress controller replica. A single-replica ingress controller is a single point of failure. Set
controller.replicaCount=2minimum for any production cluster, with pods spread across nodes.
Summary
- An Ingress controller is a reverse proxy that routes HTTP/HTTPS traffic to multiple backend services using a single load balancer IP.
- NGINX Ingress Controller is the most common choice, installed via Helm. Azure Application Gateway Ingress is available as a managed AKS add-on.
- Ingress resources define routing rules based on hostname and URL path. One Ingress can route to many services.
- TLS termination is configured by referencing a TLS Secret in the Ingress spec. cert-manager automates certificate issuance and renewal from Let’s Encrypt.
- Always run at least 2 ingress controller replicas in production and use the ingressClassName field (not the deprecated annotation) in Kubernetes 1.18+.
Frequently asked questions
Why use an Ingress controller instead of LoadBalancer services?
A LoadBalancer Service creates one Azure Load Balancer per service, each with its own public IP and cost. An Ingress controller shares one load balancer across all services, routing based on hostname and URL path. It is cheaper and easier to manage.
What is the difference between an Ingress resource and an Ingress controller?
An Ingress resource is the Kubernetes YAML object that defines routing rules. An Ingress controller is the pod that reads those rules and actually implements them — for example, by configuring NGINX. You need both.
Can I use HTTPS with an Ingress controller?
Yes. You store your TLS certificate as a Kubernetes Secret and reference it in the Ingress resource. The controller terminates TLS and forwards plain HTTP to the backend services. cert-manager can automate certificate issuance from Let's Encrypt.