
In the previous blog I covered Deployments, rolling updates and rollbacks. In this blog I will be covering Services - how pods inside the cluster talk to each other, and how traffic from outside the cluster reaches them.
Why we need a Service
Every pod gets its own IP address, and any pod in the cluster can reach any other pod using that IP. So the first question is why we need anything more.
The problem is that pod IPs are not stable. The moment a pod restarts, gets rescheduled or gets scaled, the IP changes. So if our frontend has the backend's pod IP written in its config, it breaks the next time that pod is replaced.
A Service fixes this by giving us a stable name and a stable IP in front of a group of pods. The pods behind it can come and go, and the Service keeps pointing at whichever ones currently exist.
The Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: nginx:1.23.4-alpine
ports:
- containerPort: 80
I started this with 1 replica and scaled it to 2 afterwards.
kubectl scale deployment myapp --replicas=2
The important part in this file is the label app: myapp on the pod template. That label is what the Service is going to use to find these pods.
ClusterIP
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
type: ClusterIP
selector:
app: myapp
ports:
- port: 80
targetPort: 80
ClusterIP is the default service type, so the type line can even be left out. It gives the Service an IP and a DNS name that only work from inside the cluster.
Two things here that confused me at first:
The selector matches pods, not the Deployment. The Service has no idea the Deployment exists. It only looks for pods with the label
app: myapp. Whatever created those pods does not matter.portandtargetPortare different things.portis what callers use to reach the Service, andtargetPortis the port the container is actually listening on. Here both are 80, but they do not have to be the same.
Testing from inside the cluster
To check the Service works, I created a temporary pod with busybox and ran wget against it.
apiVersion: v1
kind: Pod
metadata:
name: temp-pod
labels:
env: dev
spec:
restartPolicy: Never
containers:
- name: busybox-test
image: busybox:1.36
command: ["wget", "-O-", "myapp:80"]
restartPolicy: Never is there because this pod only has one job - run wget once and exit. Without it Kubernetes would keep restarting the pod every time it finishes.
Notice that I used myapp:80 and not an IP. The service name resolves through the cluster DNS, so this keeps working even if the Service IP changes. To see the output:
kubectl logs temp-pod
It printed the nginx welcome page, which means the request went from the busybox pod, through the Service, to one of the nginx pods.
Testing from outside the cluster
Running the same thing from my laptop failed. The ClusterIP address only exists inside the cluster's network, and my machine has no route to it. The DNS name myapp does not resolve outside the cluster either.
That is exactly the point of ClusterIP. It is for traffic between components inside the cluster, like a backend talking to a database that should never be reachable from outside.
NodePort
To make the pods reachable from outside, I changed the service type to NodePort.
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
type: NodePort
selector:
app: myapp
ports:
- port: 80
targetPort: 80
nodePort: 30001
A NodePort opens the given port on every node in the cluster, and anything that arrives on that port gets forwarded to one of the matching pods. It opens on every node, not just the nodes where the pods are running. If traffic lands on a node with no pod on it, that node still forwards it to a pod on another node.
Since I am using kind, the nodes are themselves Docker containers, so there is one more step. The port has to be mapped from the node container to my machine when the cluster is created:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30001
hostPort: 30001
- role: worker
- role: worker
This is the same idea as docker run -p from the earlier days. With that in place, the app was reachable from my laptop:
wget -O- localhost:30001
Can we expose pods as a Service without a Deployment?
Yes. A Service only selects pods by label. If I create a single pod with the label app: myapp, the Service picks it up exactly the same way.
The Deployment is not needed for the Service to work. It is only there to keep the pods alive and manage their updates. Without it, if that one pod dies, nothing brings it back, and the Service is left pointing at nothing.
When to use which service type
ClusterIP: For anything that only needs to be reached from inside the cluster. Backends, databases, caches, internal APIs. This is the default and it is what most services should be.
NodePort: For exposing something outside the cluster in a simple way, mostly for learning, testing, or when there is already a load balancer outside the cluster that we manage ourselves. The port range is limited to 30000-32767 and it exposes the node IPs directly, so it is rarely used on its own in production.
LoadBalancer: For exposing a service to the internet on a cloud provider. It asks the cloud to create a real load balancer in front of the nodes, which gives a proper external IP and health checks. Under the hood it still uses a NodePort, the cloud load balancer just forwards to it.
ExternalName: For giving a service inside the cluster a name that points to something outside it, like a managed database. It has no selector and no pods behind it. It just returns a DNS alias, so our pods can call database and actually reach something like mydb.xyz.rds.amazonaws.com. If that database ever moves, only the Service changes, not the application config.
Conclusion
The thing I took away from Day 9 is that a Service is not about the pods themselves, it is about giving them a stable address. Pods are temporary and their IPs keep changing, and the Service is the thing that stays put.
The other big one for me was that everything is tied together through labels. The Service does not know about the Deployment, the Deployment does not know about the Service. They only work together because the pod template and the selector happen to use the same label.





