Alright, let’s get our hands dirty. You’ve got a Kubernetes cluster, and you’ve got services running inside it. The million-dollar question is: how do people outside the cluster actually talk to them? You don’t just wave a magic wand. You need a traffic cop, a bouncer, a translator—all rolled into one. That’s what an Ingress resource is, a set of rules. But the thing that actually does the work, the muscle behind the rules, is the Ingress Controller.

Think of it this way: the Ingress is the map, and the Ingress Controller is the car that actually drives the route. You can’t have one without the other, and frankly, the controller is the more complex, interesting piece.

The Usual Suspects: NGINX, Traefik, and HAProxy

The landscape is dominated by a few heavy hitters, each with its own philosophy.

The NGINX Ingress Controller is the old reliable, the battle-tested workhorse. It’s incredibly powerful, but its configuration is a bit of a… well, a journey. Its most “charming” quirk is that it doesn’t watch every single change in real-time. Instead, it reloads its entire configuration file when updates occur. For most setups, this is fine, but if you’re dealing with thousands of ingresses and need absolutely zero-downtime reloads, that reload process can make you sweat. You’ll often need to sprinkle in annotations like nginx.ingress.kubernetes.io/proxy-buffer-size to fine-tune its behavior, which feels a bit like whispering secrets to a machine.

Then there’s Traefik. Traefik is the cool, new kid who actually reads the manual. It’s built from the ground up for dynamic environments like Kubernetes. It watches the API server directly and updates its configuration instantly, without any config file reload nonsense. Its configuration is more native, often feeling cleaner than NGINX’s annotation soup. It’s a fantastic choice if you want something modern and easy to get along with.

Don’t sleep on HAProxy. It’s the absolute performance king when it comes to raw throughput and efficiency. If you’re serving an ungodly amount of traffic and every millisecond counts, HAProxy is your weapon of choice. It’s less common in general-purpose clusters, but when you see it, you know the team behind it is serious about performance.

Installing one is usually a helm command away. Here’s how you’d deploy the NGINX controller, for instance:

helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

This drops the controller into your cluster, along with a Service of type LoadBalancer that becomes the actual entrypoint for all your external traffic.

The Rough Edges and How to Not Cut Yourself

Here’s where I have to be direct: the basic Ingress resource is… limited. It’s a lowest-common-denominator API. Need to do advanced routing based on a header or cookie? Want to do canary deployments by shifting a percentage of traffic? The standard Ingress resource will have you bending over backwards using annotations specific to your chosen controller. This creates a nasty vendor lock-in situation. Your configuration is no longer portable.

For example, setting up a simple canary release with the NGINX controller means annotating your Ingress manifest like some kind of arcane ritual:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-canary
            port:
              number: 80

This works, but it’s only for NGINX. Try applying this YAML to a Traefik cluster and it’ll look at you like you’re speaking gibberish.

Enter the Gateway API: The Ingress We Should Have Had All Along

The Kubernetes community saw this mess and said, “We can do better.” The result is the Gateway API. This isn’t just a new version of Ingress; it’s a complete redesign that properly separates concerns.

Instead of one monolithic resource, you now have:

  • GatewayClass: Defines a type of load balancer (e.g., “we want an AWS NLB” or “a Traefik controller”).
  • Gateway: A concrete instance that requests a load balancer. It’s the replacement for the Ingress Controller’s service.
  • HTTPRoute: This is where you define your rules—matches, filters, and forwards. It attaches to a Gateway.

This separation is brilliant. It allows different teams to operate different parts of the stack. The infrastructure team can manage the Gateway, and the application team can manage their HTTPRoutes. The configuration is also far more expressive and portable.

Here’s what that same canary release looks like as an HTTPRoute:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: my-app-http-route
spec:
  parentRefs:
  - name: company-gateway # This is managed by another team
  rules:
  - matches:
    - path:
        value: /
    backendRefs:
    - name: my-app-stable
      port: 80
      weight: 90
    - name: my-app-canary
      port: 80
      weight: 10

See? Clean, explicit, portable, and built-in. No annotations. No magic. This is the future. While it’s still evolving, if you’re starting a new project today, you should be designing for the Gateway API. The old Ingress feels like a legacy interface in comparison.