Week 9: Services, Networking & Service Discovery

Pods are disposable — a rollout, a crash, or a rescheduling event replaces them constantly, each replacement getting a brand-new IP. Nothing in your app should ever hardcode a Pod IP. A Service is a stable address in front of a constantly-changing set of Pods, and this week covers the three Service types and how the cluster's built-in DNS makes calling one Service from another as simple as calling it by name.

Module 9 of 17 Week 9 of 18 ~3.5 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain the difference between ClusterIP, NodePort and LoadBalancer Services
  • Trace how a Service's label selector finds its backing Pods via Endpoints
  • Call one Service from another Pod by name, using cluster DNS

1. Why Services Exist

Every rolling update in Week 8 replaced Pods with brand-new ones, each getting a new internal IP the moment it starts. If your frontend hardcoded an IP for the API Pod it talks to, that IP would be wrong within minutes of any deploy.

the problem, concretely
ReplicaSet manages 3 Pods for "api":
  api-7d9f8-abc12   10.244.1.5
  api-7d9f8-def34   10.244.2.8
  api-7d9f8-ghi56   10.244.1.9

# A rollout replaces all three. New IPs, every time:
  api-9c2e1-jkl78   10.244.2.14
  api-9c2e1-mno90   10.244.1.21
  api-9c2e1-pqr12   10.244.3.3

# Anything that hardcoded the old IPs is now broken.

A Service gives that constantly-shifting set of Pods one stable address — a fixed virtual IP and a DNS name — and load-balances traffic across whichever Pods currently match its selector, however many there are, wherever they're running.

This is exactly Week 4's problem, again

Docker's user-defined network DNS solved "find a container by name, not IP" on one machine. A Kubernetes Service solves the same problem at cluster scale, across many Pods and nodes — same underlying need, a more powerful mechanism to match a more dynamic environment.

2. ClusterIP, NodePort & LoadBalancer

Three Service types cover almost every scenario, differing only in how far the Service is reachable from:

service.yaml — ClusterIP (the default)
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: ClusterIP        # default — reachable only from INSIDE the cluster
  selector:
    app: api
  ports:
    - port: 80            # the Service's own port
      targetPort: 3000     # the port the container actually listens on
the three types compared
ClusterIP     # internal only. Default. Right choice for a database, an internal
              # API, anything another Pod calls but the outside world shouldn't.

NodePort      # opens a static port (30000-32767) on EVERY node's own IP. Mostly
              # a building block for LoadBalancer, or for bare-metal clusters
              # without a cloud load balancer available.

LoadBalancer  # provisions an actual cloud load balancer (AWS ELB, GCP LB, etc.)
              # with a public IP. The standard way to expose a Service to the
              # public internet on a managed cloud cluster.

In practice: internal-only services (databases, internal APIs) are ClusterIP, and public-facing entry points are usually fronted by an Ingress (Week 12) sitting behind a single LoadBalancer, rather than giving every public service its own cloud load balancer.

port vs. targetPort — the most common typo

port is what OTHER things use to call this Service. targetPort is what the CONTAINER actually listens on. Mixing these up is one of the most common causes of a Service that "exists" but returns nothing — always double-check targetPort matches your app's actual listening port.

3. Selectors & Endpoints

A Service finds its Pods the exact same way a ReplicaSet does: label selectors, not names. Kubernetes maintains an Endpoints (or EndpointSlice) object per Service, continuously updated with the IPs of every currently-matching, ready Pod.

watching the link between Service and Pods
kubectl get service api
kubectl get endpoints api
# NAME   ENDPOINTS
# api    10.244.1.5:3000,10.244.2.8:3000,10.244.1.9:3000

kubectl describe service api
# Selector: app=api          <- must match your Pods' labels EXACTLY

If kubectl get endpoints shows <none>, that's almost always a label mismatch — the Service's selector doesn't match any Pod's labels, character for character. It's the single most common Service debugging scenario you'll hit.

Only "Ready" Pods appear as Endpoints

A Pod that's Running but failing its readiness probe (Week 16) is automatically excluded from a Service's Endpoints — traffic never routes to a Pod that isn't ready to handle it, even though the Pod is technically alive. This is a core piece of how Kubernetes avoids sending traffic to broken instances.

4. Cluster DNS in Practice

Every Service automatically gets a DNS name, resolved by CoreDNS (the same kube-system Pod you may have spotted in Week 7's exercise):

the DNS naming pattern
<service-name>.<namespace>.svc.cluster.local

# From a Pod in the SAME namespace, the short form just works:
curl http://api/users

# From a Pod in a DIFFERENT namespace, include the namespace:
curl http://api.backend.svc.cluster.local/users
wiring a frontend to a backend by Service name
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - name: frontend
          image: myrepo/frontend:1.0.0
          env:
            - name: API_URL
              value: "http://api"    # resolves via cluster DNS, no IP anywhere

This is the payoff for the whole week: your application code never needs to know or care about Pod IPs, node placement, or how many replicas exist — it calls a stable name, and the Service + Endpoints + cluster DNS machinery handles the rest.

Looking ahead

The ConfigMaps and Secrets you'll meet in Week 10 are the real-world way to set values like API_URL without hardcoding them directly into every Deployment manifest — same idea shown here, made reusable across environments.

5. Hands-on Exercise

Hands-on

Expose your Week 8 Deployment with a Service, then wire up a second one that calls it by name

Turn last week's standalone Deployment into a properly service-discoverable component, then prove name-based calling works from a completely separate Deployment.

Part 1 — Expose it:

  1. Write a ClusterIP Service for your Week 8 Deployment, matching its Pod labels via selector.
  2. kubectl apply -f it, then confirm kubectl get endpoints shows all your Pods' IPs — if it shows none, fix the label mismatch before continuing.
  3. From inside a throwaway debug Pod (kubectl run debug --image=busybox -it --rm -- sh), curl your Service by name and confirm you get a response.
Hint

busybox doesn't have curl, but it does have wget -O- http://api — or use an image like nicolaka/netshoot which ships a fuller networking toolkit if you'd rather have curl, dig and friends available.

Part 2 — Chain a second service to it by name:

  1. Deploy a second, simple app (any small container that can make an outbound HTTP call, or reuse another one of your own images) as its own Deployment.
  2. Set an environment variable on it pointing at your Week 8 Service by its cluster-DNS name, and confirm it can successfully call it.
  3. Delete and recreate all the Pods behind your Week 8 Service (kubectl delete pods -l app=<your-label>) to force new IPs, and immediately re-test the call from your second app — confirm it still works without any config change, proving the Service absorbed the IP churn.
  4. Run kubectl get endpoints again right after the Pod deletion and note how quickly the Endpoints list updates to the new Pod IPs.
Hint

If the second app's call fails right after the Pod deletion, it's likely a timing issue — give new Pods a few seconds to reach Running and pass their default readiness state before retrying, since a Service only routes to Pods it considers ready.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why shouldn't application code ever hardcode a Pod's IP address?

Pod IPs are not stable — a rollout, a crash and restart, or rescheduling to another node all assign a brand-new IP. Any hardcoded reference breaks the moment the Pod is replaced, which is why Services (with a stable virtual IP and DNS name) exist as the correct abstraction to call instead.

Q2

Which Service type is the right default for an internal database that only other Pods should reach?

ClusterIP — the default type, reachable only from inside the cluster. NodePort and LoadBalancer both expose the Service beyond the cluster, which is unnecessary and increases exposure for something that should only ever be called by other internal Pods.

Q3

You created a Service but kubectl get endpoints shows none. What's the most likely cause?

The Service's label selector doesn't match any Pod's actual labels, character for character — this is the single most common Service misconfiguration. It's also possible matching Pods exist but none are passing their readiness probe, since only Ready Pods appear as Endpoints.

Q4

What's the difference between a Service's port and targetPort fields?

port is the port other things use when calling the Service. targetPort is the port the actual container is listening on inside the Pod. They can differ intentionally (e.g. Service port 80 forwarding to container port 3000) — mixing them up is a common cause of a Service that exists but returns nothing.