Skip to content
C Codeloom
Kubernetes

Kubernetes Ingress vs LoadBalancer vs NodePort Explained

Understand the three ways to expose a Kubernetes service to the outside world, the tradeoffs of each, and how to pick the right one for your workload.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What ClusterIP, NodePort, and LoadBalancer actually do
  • How an Ingress controller fits on top of Services
  • When to pay for cloud LBs vs sharing one Ingress
  • TLS termination patterns
  • How to debug stuck external traffic

Prerequisites

  • Familiarity with kubectl and basic Kubernetes objects

What and Why

A Service in Kubernetes is a stable virtual IP and DNS name in front of a set of pods. By itself, a Service is only reachable inside the cluster. To accept traffic from outside, you choose one of three exposure strategies: NodePort, LoadBalancer, or Ingress. Each one solves a different problem, and mixing them up leads to expensive cloud bills or unreachable apps.

Mental Model

  • ClusterIP (default): internal only. Pods reach the service by DNS.
  • NodePort: opens the same high port (30000–32767) on every node. External clients hit any node IP on that port.
  • LoadBalancer: provisions a cloud load balancer (ELB, NLB, GLB) that forwards to NodePorts. One external IP per service.
  • Ingress: a single shared entry point that routes HTTP(S) traffic by host and path to many backend services. Implemented by an Ingress controller (NGINX, Traefik, ALB, Istio).
NodePort:
client -> nodeIP:31234 -> kube-proxy -> pod

LoadBalancer:
client -> cloud LB -> nodeIP:31234 -> kube-proxy -> pod

Ingress:
client -> cloud LB -> ingress-controller pods -> Service ClusterIP -> pod
                        (routes by Host/path)
External traffic paths

Hands-on Example

Start with a Deployment and a ClusterIP Service:

apiVersion: apps/v1
kind: Deployment
metadata: { name: web }
spec:
  replicas: 2
  selector: { matchLabels: { app: web } }
  template:
    metadata: { labels: { app: web } }
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports: [{ containerPort: 80 }]
---
apiVersion: v1
kind: Service
metadata: { name: web }
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: 80 }]

To expose via NodePort:

spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 31080

Now curl http://<any-node-ip>:31080 reaches the pods. Cheap and simple, but you must know node IPs and open the high port in firewalls.

To expose via LoadBalancer (on a cloud cluster):

spec:
  type: LoadBalancer
  ports: [{ port: 80, targetPort: 80 }]

The cloud provisions a real load balancer and writes its external IP into status.loadBalancer.ingress. Each LoadBalancer service costs roughly the price of one ELB or NLB per month.

For many services, use a single Ingress instead:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com]
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port: { number: 80 }
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port: { number: 80 }

One cloud LB now fronts the Ingress controller, which routes by host and path to many ClusterIP services.

Common Pitfalls

Using LoadBalancer per service. Five services equal five cloud LBs and five bills. Use an Ingress for HTTP traffic.

Ingress without a controller. Creating an Ingress object does nothing on its own. You must install a controller (ingress-nginx, traefik, AWS Load Balancer Controller) and reference it via ingressClassName.

externalTrafficPolicy confusion. With the default Cluster, source IPs are SNAT’d to the node IP. Set externalTrafficPolicy: Local to preserve client IPs, but be aware it skips nodes without local pods and breaks even balancing.

NodePort port collisions. Two services cannot share the same NodePort. Let Kubernetes assign one unless you really need a fixed value.

TLS at the wrong layer. If your LB terminates TLS, the Ingress controller sees plain HTTP. If both terminate, you double-encrypt. Pick one termination point and document it.

Practical Tips

For non-HTTP protocols (gRPC streaming, raw TCP, UDP), use Service type: LoadBalancer with NLB-style annotations, or a Gateway API implementation. Classic Ingress is HTTP-only.

Inspect what is actually exposed:

kubectl get svc -A
kubectl describe ingress web-ingress
kubectl get endpoints web

endpoints is the most useful debug command — if it is empty, your Service selector does not match any pods, and no amount of LoadBalancer money will fix it.

Use cert-manager for automatic Let’s Encrypt certificates. Annotate the Ingress and let it issue and renew silently. Pair with external-dns to auto-create Route 53 or Cloud DNS records.

For multi-tenant clusters, give each team its own Ingress class so tenants do not stomp on shared controller config.

Wrap-up

Pick NodePort for quick experiments and on-prem clusters without a cloud LB. Pick LoadBalancer when a single service truly needs its own dedicated external IP, often for non-HTTP traffic. Pick Ingress for everything HTTP, because one shared entry point with host and path routing is cheaper, simpler, and more powerful. Always pair Ingress with a real controller, mind your TLS termination point, and let kubectl get endpoints be your first stop when traffic disappears into the void.