Lesson 04 of 32 · Level 1 — Foundations
Services, DNS & basic networking
Pods come and go and their IPs change. Services give them a stable name and address, and CoreDNS makes that name findable. Learn the Service types, how traffic really flows, and how to debug 'can't connect'.
The problem Services solve
Every pod gets its own IP address, and any pod can reach any other pod across nodes (that's the CNI plugin's job). So why not just use pod IPs?
Because pods are temporary. A rollout, a crash, or a node drain replaces pods, and every new pod gets a new IP. Hard-coding pod IPs is like writing a friend's hotel room number in your address book.
A pizza shop has lots of staff, and they change shifts all day. You don't call Sam's personal phone. You call the shop's number, and whoever is working picks up. A Service is the shop's phone number: it never changes, and it rings whichever pods are working right now. DNS is the phone book that turns the name "pizza-shop" into that number.
Create a Service
Use the web Deployment from the previous lesson (label app: web):
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web # send traffic to Ready pods with this label
ports:
- port: 80 # the Service's port
targetPort: 80 # the container's port
$ kubectl apply -f web-svc.yaml
service/web created
$ kubectl get svc web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.96.142.17 <none> 80/TCP 5s
$ kubectl get endpointslices -l kubernetes.io/service-name=web
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web-x7k2p IPv4 80 10.244.1.7,10.244.2.4,10.244.2.5 5s
The EndpointSlice is the live list of pod IPs behind the Service. Scale the Deployment and watch the list change: the Service IP stays the same.
Talk to it from inside the cluster
Start a throwaway debug pod and call the Service by name:
$ kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- sh
/ # nslookup web
Name: web.default.svc.cluster.local
Address: 10.96.142.17
/ # wget -qO- http://web | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
/ # exit
And from your laptop, without exposing anything publicly:
$ kubectl port-forward svc/web 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Open http://localhost:8080 in a browser.
How DNS names work
CoreDNS (running in kube-system) gives every Service a name:
<service>.<namespace>.svc.cluster.local
web . default .svc.cluster.local
- Same namespace: just
web - Another namespace:
web.shop - Always works:
web.shop.svc.cluster.local
The Service types
| Type | Reachable from | Typical use |
|---|---|---|
| ClusterIP (default) | Inside the cluster only | Service-to-service traffic (most Services) |
| NodePort | <any node IP>:30000–32767 |
Simple external access, labs, behind your own LB |
| LoadBalancer | A cloud/external load balancer IP | Exposing apps on EKS/GKE/AKS; MetalLB on bare metal |
| ExternalName | DNS alias to an outside name | Pointing db at mydb.example.com |
Headless (clusterIP: None) |
DNS returns pod IPs directly | StatefulSets: db-0.db, db-1.db |
HTTP from the outside world
For websites and APIs you'll usually put an Ingress or Gateway API route in front of ClusterIP Services, so one load balancer serves many apps by hostname and path, with TLS. The next lesson sets one up step by step; TLS is covered in lesson 19.
How traffic actually flows
Nothing actually "listens" on a ClusterIP. kube-proxy on every node watches Services and EndpointSlices, and programs iptables/IPVS rules (or an eBPF dataplane such as Cilium does it instead). A packet to 10.96.142.17:80 is rewritten on the spot to one of the pod IPs.
client pod ──► 10.96.142.17:80 (virtual) ──[kube-proxy rules: pick one]──► 10.244.2.4:80 (real pod)
Try it: break a Service and fix it
- Edit the Service and change the selector to
app: webb(a typo):kubectl patch svc web -p '{"spec":{"selector":{"app":"webb"}}}' - From the debug pod,
wget -qO- http://webnow hangs or fails, yet DNS still resolves. Why? - Check
kubectl describe svc web: Endpoints: <none>. That's your clue. - Fix it:
kubectl patch svc web -p '{"spec":{"selector":{"app":"web"}}}'
Rule of thumb: DNS works but the connection fails → check endpoints. No endpoints → selector/labels or readiness.
Going deeper: the debugging ladder and DNS gotchas
Work up the layers, and stop at the first one that fails:
- Are the pods Ready? (
kubectl get pods -l app=web) - Does the Service have endpoints? (
kubectl get endpointslices) - Does calling a pod IP directly work? (Tests the CNI and the app itself.)
- Does the ClusterIP work? (Tests kube-proxy / dataplane rules.)
- Does the name resolve? (Tests CoreDNS and
/etc/resolv.conf.)
DNS gotcha: pods default to ndots:5, so a name like api.example.com is first tried with each search domain appended, causing extra lookups and latency on busy clusters. Use a trailing dot (api.example.com.) or tune dnsConfig for external-heavy workloads. Also, a ClusterIP not answering ping is normal, not a fault.
Recap
- Pods get new IPs all the time. A Service gives a stable IP and name in front of matching Ready pods.
- EndpointSlices hold the live pod list. Empty endpoints is the most common "can't connect" cause.
- DNS name:
service.namespace.svc.cluster.local. Short names work inside the same namespace. - ClusterIP inside, NodePort / LoadBalancer outside, Ingress / Gateway for HTTP.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.