How-to 6 min read

Ingress, LoadBalancer or NodePort: exposing a service without guessing

Three ways to get traffic into a Kubernetes cluster, what each one actually costs, and the decision rule that stops you paying for twelve load balancers.

Ingress, LoadBalancer or NodePort: exposing a service without guessing

A pod that nobody can reach is not a deployment, it is a science experiment. Kubernetes gives you three mechanisms for getting outside traffic to a workload, and teams routinely pick the wrong one — usually the one that works first, which is rarely the one that should stay.

Here is what each does, what it costs, and the rule for choosing.

The three, in one line each

  • NodePort — opens the same high-numbered port on every node. Traffic to any node IP on that port reaches the service.
  • LoadBalancer — asks the cloud provider for a real load balancer with its own IP, pointed at the service.
  • Ingress — one load balancer in front of an HTTP router that fans traffic out to many services by hostname and path.

They are not alternatives at the same level. LoadBalancer builds on NodePort, and Ingress builds on LoadBalancer. Understanding the stacking is most of the decision.

NodePort: useful, rarely the answer

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: NodePort
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080

Now every node listens on 30080. That is genuinely handy for a quick test, for a cluster behind an appliance you already own, or for a service consumed only from inside the private network.

As a public entry point it is poor. The port is in the 30000–32767 range, so users would need :30080 in the URL. Node IPs change when pools are replaced, so you would need something in front anyway — which is exactly what a load balancer is. And there is no TLS termination, no hostname routing, no health-based removal of a broken node.

LoadBalancer: one service, one IP, one bill

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
    - port: 443
      targetPort: 8080

Apply that and the cluster provisions a load balancer, gives it an IP, and points it at the service's NodePort on the healthy nodes. It is the right answer for anything that is not HTTP — a Postgres proxy, an MQTT broker, a game server, a gRPC service where you want L4 pass-through.

The trap is using it for every HTTP service. Ten microservices with type: LoadBalancer is ten load balancers, ten public IPs, ten TLS certificates to manage and ten line items on the invoice, to serve what is architecturally one website. On our managed Kubernetes the load balancer is a separately billed resource, so this is a real number rather than an aesthetic complaint.

Ingress: one door, many rooms

An Ingress controller runs in the cluster, gets exactly one LoadBalancer service of its own, and routes HTTP by host and path.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: public
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com, api.example.com]
      secretName: public-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port: { number: 80 }
    - host: api.example.com
      http:
        paths:
          - path: /v1
            pathType: Prefix
            backend:
              service:
                name: api
                port: { number: 8080 }

One load balancer, one certificate pipeline, as many services as you like. Add a service and you add nine lines of YAML rather than a new billed resource. This is the default answer for HTTP and it should be boring.

The decision rule

  1. Is the traffic HTTP or HTTPS, from the public internet? Ingress.
  2. Is it another protocol, or do you need the client IP and TLS to pass through untouched? LoadBalancer.
  3. Is it only reachable from inside the cluster? ClusterIP — the default, and the one nobody remembers is also an option.
  4. Is it only reachable from inside your private network, through something you already operate? NodePort.

That covers essentially every case, and it means the number of load balancers in your account tracks the number of protocols you serve rather than the number of services you run.

What actually goes wrong

The client IP disappears. By default traffic is SNATed between nodes, so your application logs the node address instead of the user. For an Ingress, read X-Forwarded-For. For a LoadBalancer service where the real IP matters, set externalTrafficPolicy: Local — and understand the trade: traffic is only sent to nodes actually running a pod, so you need reasonable pod spread or some nodes fail health checks.

Websockets and long requests get cut. Load balancer and Ingress idle timeouts are usually 30 to 60 seconds. Long-polling, server-sent events, large uploads and slow reports all break at exactly that boundary. Raise the timeout on both the LB and the controller, not just one.

Certificates fail silently. cert-manager needs the HTTP-01 challenge path reachable, or DNS-01 credentials. A misconfigured issuer produces a working site on the old certificate until the day it expires — alert on certificate expiry independently of the renewal machinery.

Readiness probes decide your uptime. The load balancer removes a node when its health check fails; the Ingress removes a pod when its readiness probe fails. A service with no readiness probe receives traffic while it is still booting, and a rollout becomes a burst of 502s.

Path rewriting is controller-specific. pathType: Prefix matching and the rewrite annotations differ between nginx, Traefik and HAProxy. Pin the controller and read its documentation rather than a generic tutorial.

A note on Gateway API

Gateway API is the successor to Ingress, and it is worth knowing about: route ownership is split from infrastructure ownership, so a platform team can own the gateway while application teams own their routes, and it handles non-HTTP protocols in the same model. If you are starting fresh and your controller supports it, it is a reasonable choice. If you have working Ingress resources, there is no urgency — Ingress is not going anywhere soon.

The short version

HTTP from the internet gets one Ingress. Anything else that needs an IP gets a LoadBalancer. Internal traffic gets ClusterIP. NodePort is for the specific case where you already own the thing in front. Count the load balancers in your account, and if that number looks like your service count, you have found this month's easiest saving.

Antyxsoft Cloud

Written by the engineers who operate the Antyxsoft platform.

Talk to the team

Infrastructure notes, once a month

Release notes, capacity updates and the occasional deep dive. No fluff, unsubscribe any time.

We store your email in HubSpot and never share it.