Skip to content
Published on

Understanding Ingress Controllers Completely — From the Ingress Resource to How Controllers Actually Work

Authors

Introduction: Why Ingress Is Confusing From the Start

One of the most common questions when operating Kubernetes for the first time is: "I can just create a LoadBalancer Service, so why do I need Ingress?" In a small cluster with one or two services, a LoadBalancer-type Service is genuinely enough. But the story changes once you have dozens of services. If you spin up one cloud load balancer per service, cost grows linearly, and you have to manage a separate public IP and TLS certificate for each load balancer.

Ingress solves this. Behind a single entry point (usually one load balancer), it routes traffic to many backend services based on host name and URL path. Because it operates at L7 (HTTP/HTTPS), it can handle path-based routing, host-based virtual hosting, and TLS termination all in one place.

The biggest source of confusion in practice, however, is that "Ingress" refers to two things at once. One is the Ingress resource (an API object), and the other is the Ingress Controller (the actual running proxy). If you cannot distinguish these two, you fall into traps like "I created an Ingress but nothing happens." This article starts from that distinction and works through the operating principle, the resource spec, TLS, a controller comparison, and the 2026 shift toward Gateway API.

Ingress API vs Ingress Controller — The Most Important Distinction

Let us separate the two concepts clearly.

  • Ingress resource (API object): A declarative YAML spec describing routing rules such as "send the /api path of example.com to api-service." On its own it is just data stored in etcd; it processes no traffic.
  • Ingress Controller: A program (usually a reverse proxy) actually running in the cluster. It watches Ingress resources, and when it detects a change, it generates its own configuration accordingly and routes incoming traffic per the rules.

The crucial point is that creating an Ingress resource does nothing if no controller is installed in the cluster. When you create a Deployment, the control plane brings up pods on its own; but Ingress only takes effect once a separately installed third-party controller is present. This is what fundamentally distinguishes it from other resources.

By analogy, the Ingress resource is the "order ticket" and the Ingress Controller is the "kitchen." Pile up order tickets with no kitchen, and no food comes out.

Representative controllers include ingress-nginx, Traefik, HAProxy Ingress, Contour (Envoy-based), Kong, and APISIX. Cloud providers also offer their own controllers (AWS Load Balancer Controller, GKE Ingress, and so on).

The Control Loop: From API Watch to Reload

The internal behavior of an Ingress Controller mostly follows this control loop.

                        Kubernetes API Server
                  (1) watch Ingress / Service / Endpoints / Secret
        ┌───────────────────────────────────────────┐
        │            Ingress Controller               │
        │                                             │
        │   (2) detect change → update internal model │
        │   (3) generate routing config/object        │
        │       (e.g. render nginx.conf template)      │
        │   (4) proxy reload or dynamic reconfigure    │
        │                                             │
        │   ┌─────────────┐      ┌──────────────┐     │
        │   │ Controller   │ ───▶ │  Data Plane   │     │
        │   │  (watcher)   │ cfg  │ (nginx/envoy) │     │
        │   └─────────────┘      └──────────────┘     │
        └───────────────────────────────────────────┘
                       (5) route external traffic
   Client ─▶ LoadBalancer ─▶ Controller Pod ─▶ Backend Service ─▶ Pod

Step by step:

  1. Watch: The controller opens watch connections to the API server, subscribing in real time to changes in Ingress, Service, Endpoints (or EndpointSlice), TLS Secrets, and so on.
  2. Model update: When a change event arrives, the controller rebuilds its internal routing model.
  3. Config generation: It converts that model into a form the data plane understands. ingress-nginx renders nginx.conf from a template; an Envoy-based controller produces xDS configuration.
  4. Apply (reload/reconfigure): For file-based proxies like nginx, it performs a reload; for proxies that support dynamic configuration like Envoy, it reconfigures without downtime. ingress-nginx also handles dynamic backend changes (endpoint add/remove) via a Lua module without a reload.
  5. Traffic routing: The data plane forwards actual HTTP requests to backend services per the rules.

An important point here is that controllers often send traffic directly to pod endpoints rather than to the backend Service's ClusterIP. ingress-nginx watches Endpoints/EndpointSlice by default and load-balances across individual pod IPs. This lets it bypass kube-proxy's iptables round-robin and apply its own load balancing (for example, ewma or round_robin).

Ingress Resource Spec in Detail

Let us examine the Ingress resource spec field by field. The following is a typical example based on networking.k8s.io/v1.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress
  namespace: shop
spec:
  ingressClassName: nginx
  defaultBackend:
    service:
      name: fallback-service
      port:
        number: 80
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
          - path: /static
            pathType: Prefix
            backend:
              service:
                name: static-service
                port:
                  number: 80
    - host: admin.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: admin-service
                port:
                  number: 3000

rules and host

rules is an array of routing rules. Each rule may optionally have a host. When host is set, the rule applies only when the request's Host header matches. Omitting host applies it to all hosts. Wildcard hosts (*.example.com) are also supported, but with the constraint that they match a single label only. That is, *.example.com matches a.example.com but not a.b.example.com.

paths and pathType

Under each host, multiple paths may appear. pathType determines the matching mode and has three values.

  • Prefix: Splits the path into slash-separated elements and matches by prefix. /api matches /api and /api/v1 but not /apixyz.
  • Exact: The path must match exactly. It is case-sensitive.
  • ImplementationSpecific: Delegates the matching mode to the controller implementation. ingress-nginx may apply its own rules such as regular expressions in this case.

pathType is a required field in the v1 API. Omitting it causes Ingress creation to be rejected.

backend service and defaultBackend

backend specifies where to send traffic, naming a service and a port (by number or name). defaultBackend is the fallback that receives requests matching no rule. If unset, unmatched requests fall to the controller's default 404 page.

resource backend

You can also specify a resource (for example, a custom resource pointing to object storage) as the backend instead of a service. backend.service and backend.resource are mutually exclusive.

IngressClass and Multiple Controllers

A cluster may have more than one controller installed (for example, internal and external). The mechanism that decides "who handles this Ingress resource" is IngressClass.

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: nginx
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
spec:
  controller: k8s.io/ingress-nginx

On the Ingress resource, you specify which class to use with spec.ingressClassName: nginx. A controller only handles Ingresses for the class it owns and ignores the rest. If an IngressClass has the is-default-class annotation set to true, Ingresses that omit ingressClassName are automatically assigned to that default class.

In the past, the class was specified via the kubernetes.io/ingress.class annotation, but that approach is deprecated. Use spec.ingressClassName in new environments. Multi-controller operation is covered in detail in a separate article.

Configuring TLS

To terminate HTTPS at the Ingress, use a TLS Secret and spec.tls.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress-tls
  namespace: shop
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - shop.example.com
      secretName: shop-tls
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: shop-service
                port:
                  number: 80

The Secret referenced by secretName must be of type kubernetes.io/tls and contain tls.crt and tls.key. The Secret must live in the same namespace as the Ingress.

apiVersion: v1
kind: Secret
metadata:
  name: shop-tls
  namespace: shop
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded-cert>
  tls.key: <base64-encoded-key>

In practice, you do not manage certificates by hand; you use cert-manager to automatically issue and renew them from Let's Encrypt and the like. cert-manager handles the ACME challenge and stores the issued certificate as a Secret in the form above. The Ingress Controller then watches that Secret and uses it for TLS termination.

Controller Types Overview Comparison

A comparison of representative Ingress Controllers follows.

ControllerData planeConfig update methodCharacteristicsNotes
ingress-nginxNGINXnginx.conf reload + Lua dynamic endpointMost widely used, rich annotationsTrending toward maintenance mode, security-patch focused
NGINX Ingress (F5)NGINX / NGINX Plusconfig reloadCommercial support (Plus), separate projectDifferent project from ingress-nginx
TraefikTraefikdynamic hot reconfigureCRDs/annotations, auto service discoveryDashboard included
HAProxy IngressHAProxyruntime API + reloadHigh-performance L4/L7, precise health checksFine tuning possible
ContourEnvoyxDS dynamicHTTPProxy CRD, delegation modelCNCF project
KongKong/NGINXDB-less dynamicAPI gateway features, pluginsRich auth/rate limit
APISIXAPISIXetcd-based dynamicPlugin ecosystem, high performanceApache project
EmissaryEnvoydynamicMapping CRD basedFormerly Ambassador

Selection criteria are typically as follows. For simple routing with broad references, use ingress-nginx; when dynamic service discovery and operational convenience matter, Traefik; when you need API-gateway-grade auth/plugins, Kong/APISIX; when you need fine-grained Envoy-based policy, consider Contour.

2026 Context: Ingress API Frozen and the Gateway API Succession

There is a 2026 trend that must be addressed here. The Ingress API is frozen. That is, the Ingress in networking.k8s.io/v1 is stable and keeps working, but no new features are being added. Advanced capabilities like header-based routing, weighted traffic splitting, and explicit method matching all had to be worked around with controller-specific annotations, and this "annotation hell" has been called out as the fundamental limitation of the Ingress API.

Its successor is Gateway API. Gateway API offers a role-separated resource model — GatewayClass, Gateway, HTTPRoute — and supports header matching, weighted traffic splitting, and cross-namespace routing within the standard spec. The big difference is that it is designed to separate the responsibilities of infrastructure operators (who manage Gateways) and application developers (who manage Routes).

In addition, ingress-nginx, long used as a de facto standard, is operating closer to maintenance mode, focused on security patches rather than new features. So for a new project, it is reasonable to consider Gateway API and a controller that implements it (for example, Envoy Gateway, Traefik, or Contour's Gateway support) from the start.

This does not mean you should throw away all Ingress right now. Ingress still works broadly and is plenty for simple routing. But for a fresh design, the recommended strategy is to put Gateway API first and migrate existing assets gradually.

First Deployment Walkthrough

Let us deploy a first Ingress with ingress-nginx. First install the controller (using Helm).

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

After installation, check whether the controller received an external IP via a LoadBalancer-type Service.

kubectl get svc -n ingress-nginx
kubectl get pods -n ingress-nginx

Now deploy a test application and service.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - name: hello
          image: hashicorp/http-echo
          args:
            - "-text=hello from ingress"
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: hello-service
spec:
  selector:
    app: hello
  ports:
    - port: 80
      targetPort: 5678

Then create the Ingress.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: hello.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello-service
                port:
                  number: 80

To test locally, map the controller's external IP to hello.example.com in your hosts file, or emulate it with curl's Host header.

curl -H "Host: hello.example.com" http://<EXTERNAL-IP>/

If you see hello from ingress, it works.

Operations and Tuning Points

Here are the settings you touch most often in operations. Most are ingress-nginx annotations or ConfigMap entries.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
  • proxy-body-size: Upload size limit. The first thing to check when a file-upload service returns 413.
  • proxy-read-timeout: Time to wait for a backend response. Increase it when long-running work is cut off with 504.
  • ssl-redirect: Force HTTP to redirect to HTTPS.
  • rewrite-target: Path rewriting, used with regex capture groups.

Via ConfigMap, you adjust global settings (worker process count, keepalive, gzip, and so on). The controller itself should run with resource requests/limits, an HPA, and a PodDisruptionBudget for stability. Since the controller is the single point through which all traffic passes, sufficient replicas and node spreading (topologySpreadConstraints) matter.

Pitfalls and Troubleshooting

Here are problems frequently encountered in practice.

1. You created an Ingress but ADDRESS is empty. Either no controller is installed, or ingressClassName is wrong so no controller claims the Ingress. Check the ADDRESS column with kubectl get ingress and look at the events from kubectl describe ingress.

kubectl get ingress
kubectl describe ingress hello-ingress

2. 503 Service Temporarily Unavailable. Most likely the backend service's selector does not match the pods, so Endpoints is empty. Check next.

kubectl get endpoints hello-service

If empty, inspect the service selector, the pod labels, and the pod readiness probe state.

3. The controller returns 404 Not Found. This happens when the Host header differs from the rule's host, or when pathType/path matching differs from intent. Re-check the difference between Prefix and Exact, and the single-label constraint of wildcard hosts.

4. rewrite-target does not work as intended. The capture-group regex and path definition are misaligned. When using rewrite-target in ingress-nginx, define the path in regex-capture form and validate together with the use-regex annotation.

5. TLS is not applied. The Secret type is not kubernetes.io/tls, the Secret is in a different namespace from the Ingress, or spec.tls.hosts and rules.host disagree. If you use cert-manager, trace the state of the Certificate, CertificateRequest, and Order resources.

6. Frequent controller reloads cause latency. Frequent Ingress/Service/Endpoints changes can trigger frequent config reloads. ingress-nginx avoids most reloads via dynamic endpoint updates, but annotation or host-rule changes do trigger reloads. Batch your changes and monitor reload frequency in the controller logs.

kubectl logs -n ingress-nginx deploy/ingress-nginx-controller | grep -i reload

Closing

To handle Ingress properly, you must first internalize the distinction between the Ingress resource (declaration) and the Ingress Controller (execution). On top of that, once you understand the control loop of API watch → config generation → reload, you can reason structurally about why routing works or does not.

Once you master the resource spec (rules/paths/pathType/backend/defaultBackend), controller selection via IngressClass, and TLS Secret integration, you can handle basic operations confidently. That said, 2026 is a transition period in which the Ingress API is frozen and Gateway API is establishing itself as the successor standard. Keep existing Ingress assets stable, but for new designs I recommend actively considering Gateway API. As next steps, it is worth looking at migration to Gateway API and operating multiple controllers.

References