NovuSpark
All articles

November 7, 2025 · NovuSpark Team

Kubernetes Networking: ClusterIP, NodePort, and Ingress

This is the second post in our Kubernetes fundamentals series. Start with Pods, Deployments, and Services if you're joining partway through.

The previous post in this series introduced Services as a stable address for a set of Pods, without covering the detail that actually matters once you need traffic from outside the cluster to reach one: a Service has a type, and the default type is deliberately not reachable from outside the cluster at all.

ClusterIP: the default, and internal-only

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

ClusterIP (the default if type is omitted entirely) gives a Service a stable IP address that's only routable from within the cluster. This is correct, and exactly what you want, for the large majority of Services in a real system — an internal API, a database, anything that only other things inside the cluster should ever talk to directly.

NodePort: opening a specific port on every node

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

NodePort opens a specific port (30000–32767 by convention) on every node in the cluster, forwarding traffic on that port to the Service. <any-node-ip>:30080 reaches this Service from outside the cluster, from any node.

This is genuinely useful for local development and quick testing, and genuinely awkward for real production traffic: it exposes a raw port directly on every node, ties clients to knowing a specific node's IP (which is exactly the kind of "stable address for something that changes underneath it" problem Services exist to solve — NodePort partially reintroduces it at the node level), and provides no HTTP-level routing at all.

LoadBalancer: asking the cloud provider for a real one

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

On a cloud-managed cluster (EKS, GKE, AKS), type: LoadBalancer provisions an actual cloud load balancer (an AWS Network Load Balancer, for EKS) pointed at the Service, with its own stable external IP or DNS name:

kubectl get service web
NAME   TYPE           CLUSTER-IP     EXTERNAL-IP                              PORT(S)
web    LoadBalancer   10.100.34.12   a1b2c3d4.elb.eu-west-2.amazonaws.com    80:31234/TCP

The real limitation: one cloud load balancer per Service, each with its own genuinely non-trivial cost. A cluster running twenty separate HTTP services, each with its own LoadBalancer Service, means twenty separate load balancers billed independently — appropriate for a handful of genuinely distinct entry points, expensive and unnecessary as the default way to expose every HTTP service in a cluster.

Ingress: HTTP-aware routing, one entry point for many services

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

An Ingress routes HTTP(S) traffic to different Services based on hostname and path — app.example.com/api to one Service, everything else to another — through a single load balancer, regardless of how many Services it fronts. This is the mechanism that actually scales: adding a tenth internal service means adding a rule to the Ingress, not provisioning a tenth cloud load balancer.

An Ingress resource on its own does nothing — it requires an Ingress Controller (nginx-ingress and AWS Load Balancer Controller are common choices) actually running in the cluster to read Ingress objects and configure real routing accordingly. This is a detail that trips people up constantly: creating an Ingress object with no controller installed produces no error and no routing — just a resource sitting there, doing nothing, because nothing is watching it.

A rough guide for choosing

  • ClusterIP: the default for anything that's only ever called from inside the cluster — which, in most real systems, is most Services.
  • NodePort: local development and quick manual testing, rarely appropriate for real production traffic.
  • LoadBalancer: a genuinely distinct external entry point that needs its own dedicated load balancer — a handful per cluster, not one per Service.
  • Ingress: the standard way to expose multiple HTTP services externally through one shared entry point, and almost always the right choice once there's more than one or two external-facing HTTP services.

What to actually remember from this post

  • ClusterIP is internal-only, by design — it's the correct default for most Services in a real cluster.
  • NodePort and LoadBalancer both expose a Service externally, but at genuinely different cost and abstraction levels — one raw port per node, versus one real cloud load balancer per Service.
  • Ingress is the HTTP-aware routing layer that lets many Services share a single external entry point.
  • An Ingress Controller must actually be installed for Ingress objects to do anything at all — the object alone is inert.

Next in the series: ConfigMaps, Secrets, and Environment Configuration in Kubernetes, where we cover how configuration and secrets actually get into a running Pod.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.