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:

  1. Client sends a request to the ingress controller’s public IP
  2. The controller reads the Host header and the URL path
  3. It matches against ingress rules and finds the correct backend Service
  4. 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

ControllerManaged byBest forNotes
NGINX Ingress ControllerYou (Helm)General purpose HTTP routingMost widely used, excellent docs
Application Gateway Ingress (AGIC)Azure (managed add-on)WAF, SSL offload, enterprise needsUses Azure Application Gateway
Azure NGINX Ingress ControllerAzure (managed)Managed NGINX without self-hostingNewer, preview features
TraefikYou (Helm)Dynamic routing, Let’s Encrypt auto-TLSGood 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 \
  --watch

Once 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_IP

Creating 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: 80
kubectl 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      1m

Requests 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. /api matches /api but not /api/users.
  • Prefix — matches the path and any sub-paths. /api matches /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 production
apiVersion: 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: 80

With 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
EOF

Add 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 this

cert-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.

Tip

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

  1. Missing ingressClassName field. In newer Kubernetes versions, the spec.ingressClassName: nginx field is required. Without it, the ingress resource is created but ignored by the controller. Old guides use the annotation approach which is now deprecated.
  2. 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.
  3. Path not matching because of trailing slash differences. /api and /api/ can behave differently depending on pathType. Test both forms during setup and use the rewrite-target annotation to normalize paths if needed.
  4. 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.
  5. Deploying only one ingress controller replica. A single-replica ingress controller is a single point of failure. Set controller.replicaCount=2 minimum for any production cluster, with pods spread across nodes.

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.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.