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: 8080ClusterIP (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: 30080NodePort 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: 8080On 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 webNAME 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: 80An 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
ClusterIPis internal-only, by design — it's the correct default for most Services in a real cluster.NodePortandLoadBalancerboth 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.
