Kubernetes · lesson 4 of 10
Intermediate
6Advanced
1Also part of Kubernetes
5- + Deploying FastAPI with Docker and Kubernetes: Images, Signals, and Probes from FastAPI
- + Deployment Strategies Compared: Rolling, Blue-Green, and Canary from DevOps
- + GitOps in Practice: Git as the Source of Truth from DevOps
- + Kubernetes Pod Security Admission: Enforcing the Restricted Profile from Cloud Security
- + CIS Kubernetes Benchmark Checks with kube-bench in kind from Cloud Security
Services and Networking
Put a real backend behind ClusterIP, NodePort and LoadBalancer Services, then trace DNS across namespaces, EndpointSlices and headless Services.
By SkillByExample · Updated
Why Services exist
Pods are ephemeral: when one is replaced, it comes back with a new IP. A Service puts a stable virtual IP and DNS name in front of a set of Pods, chosen by a label selector, and load-balances across whichever of those Pods are Ready.
The outputs below come from a single-node minikube cluster started with
minikube start -p rw-k8s --driver=docker. The profile name becomes the node name, so
NODE shows rw-k8s rather than minikube. Pod hashes, IPs and ages will differ on yours.
A backend to route to
A Service with nothing behind it has nothing to forward to. This Deployment runs two copies of
hashicorp/http-echo, a tiny HTTP server, on port 8080. Each copy replies with its own Pod
name, so you can see which Pod answered.
# api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: echo
image: hashicorp/http-echo:1.0
args: ["-listen=:8080", "-text=hello from $(POD_NAME)"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- containerPort: 8080
$ kubectl apply -f api.yaml
deployment.apps/api created
$ kubectl rollout status deployment/api
Waiting for deployment "api" rollout to finish: 0 of 2 updated replicas are available...
Waiting for deployment "api" rollout to finish: 1 of 2 updated replicas are available...
deployment "api" successfully rolled out
$ kubectl get pods -l app=api -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
api-785f67cc54-7mkfv 1/1 Running 0 3s 10.244.0.4 rw-k8s <none> <none>
api-785f67cc54-mbwl4 1/1 Running 0 3s 10.244.0.3 rw-k8s <none> <none>
The $(POD_NAME) in args is expanded by Kubernetes from the env entry, which the downward
API fills with each Pod’s own name. The two Pod IPs, 10.244.0.3 and 10.244.0.4, are what
every Service below forwards to, and they would change if these Pods were replaced.
ClusterIP (the default)
A ClusterIP Service is reachable only from inside the cluster. It is the usual way for one workload to call another.
# service-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api # routes to pods with this label
ports:
- protocol: TCP
port: 80 # port on the Service
targetPort: 8080 # port on the Pod
type: ClusterIP
$ kubectl apply -f service-clusterip.yaml
service/api-service created
$ kubectl get service api-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-service ClusterIP 10.102.76.117 <none> 80/TCP 0s
$ kubectl describe service api-service
Name: api-service
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=api
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.102.76.117
IPs: 10.102.76.117
Port: <unset> 80/TCP
TargetPort: 8080/TCP
Endpoints: 10.244.0.3:8080,10.244.0.4:8080
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>
The Service got a virtual IP, 10.102.76.117, that no Pod owns. Endpoints is the part that
matters: the two Pod IPs from the previous step, each paired with targetPort 8080. Clients
connect to port 80 on the ClusterIP, and the node forwards each connection to one of those
addresses.
To call it, you need a Pod inside the cluster. A long-running curl Pod is handy for the rest of this lesson:
kubectl run client --image=curlimages/curl:8.10.1 -- sleep 3600
kubectl wait --for=condition=Ready pod/client
for i in 1 2 3 4; do kubectl exec client -- curl -sS http://api-service; done
kubectl exec client -- curl -sS http://api-service.default.svc.cluster.local
$ kubectl run client --image=curlimages/curl:8.10.1 -- sleep 3600
pod/client created
$ kubectl wait --for=condition=Ready pod/client
pod/client condition met
$ for i in 1 2 3 4; do kubectl exec client -- curl -sS http://api-service; done
hello from api-785f67cc54-mbwl4
hello from api-785f67cc54-mbwl4
hello from api-785f67cc54-7mkfv
hello from api-785f67cc54-mbwl4
$ kubectl exec client -- curl -sS http://api-service.default.svc.cluster.local
hello from api-785f67cc54-7mkfv
Both Pods answered, but not in strict turns: mbwl4 served three of the four calls. kube-proxy
in its default iptables mode picks a backend at random for each new connection, so the split
evens out over many requests, not over four. The short name api-service and the full name
api-service.default.svc.cluster.local reach the same Service.
NodePort
A NodePort Service also opens a fixed port, from 30000-32767, on every node. Anything that can reach a node’s IP can reach the Service.
# service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: api-nodeport
spec:
selector:
app: api
ports:
- protocol: TCP
port: 80 # internal cluster port
targetPort: 8080 # pod port
nodePort: 30080 # port exposed on every node (30000-32767)
type: NodePort
kubectl apply -f service-nodeport.yaml
kubectl get service api-nodeport
kubectl get nodes -o wide
kubectl exec client -- curl -sS http://192.168.49.2:30080
kubectl exec client -- curl -sS --retry 5 --retry-connrefused http://192.168.49.2:30080
$ kubectl apply -f service-nodeport.yaml
service/api-nodeport created
$ kubectl get service api-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-nodeport NodePort 10.111.71.240 <none> 80:30080/TCP 0s
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
rw-k8s Ready control-plane 3m3s v1.35.1 192.168.49.2 <none> Debian GNU/Linux 12 (bookworm) 5.15.153.1-microsoft-standard-WSL2 docker://29.2.1
$ kubectl exec client -- curl -sS http://192.168.49.2:30080
curl: (7) Failed to connect to 192.168.49.2 port 30080 after 0 ms: Could not connect to server
command terminated with exit code 7
$ kubectl exec client -- curl -sS --retry 5 --retry-connrefused http://192.168.49.2:30080
curl: (7) Failed to connect to 192.168.49.2 port 30080 after 0 ms: Could not connect to server
hello from api-785f67cc54-mbwl4
80:30080/TCP means port 80 on the ClusterIP and port 30080 on the node, so a NodePort Service
is a ClusterIP Service with an extra door on each node. The first call was refused because the commands ran back-to-back in a script (typed by hand,
the first call usually succeeds): apply returns as soon as the API server stores the Service. kube-proxy then programs the
node’s port separately, a moment later. --retry-connrefused retries until the port is open,
and here it answered on the second attempt. Use your own node’s
INTERNAL-IP in the URL.
On Docker Desktop for Windows or macOS, 192.168.49.2 is not reachable from your own machine,
only from inside the cluster. minikube service opens a tunnel to the node port for you:
$ minikube -p rw-k8s service api-nodeport --url
http://127.0.0.1:51584
! Because you are using a Docker driver on windows, the terminal needs to be open to run it.
$ curl -sS http://127.0.0.1:51584
hello from api-785f67cc54-mbwl4
The first command keeps running and holds the tunnel open. The second is from another
terminal. The local port, 51584 here, is chosen at random each time.
LoadBalancer
On a cloud cluster, a LoadBalancer Service asks the provider for an external load balancer
with its own public IP. On minikube, nothing provides one until you start minikube tunnel.
# service-lb.yaml
apiVersion: v1
kind: Service
metadata:
name: api-lb
spec:
selector:
app: api
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
$ kubectl apply -f service-lb.yaml
service/api-lb created
$ kubectl get service api-lb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-lb LoadBalancer 10.96.226.205 <pending> 80:31551/TCP 0s
EXTERNAL-IP stays <pending> until something assigns an address. It also got a ClusterIP and
a random node port, 31551, so a LoadBalancer Service is a NodePort Service with an external
address in front. Start the tunnel in a second terminal and leave it running:
$ minikube -p rw-k8s tunnel
* Tunnel successfully started
* NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...
! Access to ports below 1024 may fail on Windows with OpenSSH clients older than v8.1. For more information, see: https://minikube.sigs.k8s.io/docs/handbook/accessing/#access-to-ports-1024-on-windows-requires-root-permission
* Starting tunnel for service api-lb.
$ kubectl get service api-lb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-lb LoadBalancer 10.96.226.205 127.0.0.1 80:31551/TCP 17s
$ curl -sS http://127.0.0.1
hello from api-785f67cc54-mbwl4
With the Docker driver on Windows or macOS, the tunnel gives the Service 127.0.0.1 and forwards its port 80 from
your machine. On EKS, GKE or AKS, the same manifest gets a real cloud load balancer address
there instead, usually within a minute or two.
Service types summary
| Type | Reachable from | Use case |
|---|---|---|
ClusterIP | Inside the cluster only | Workload-to-workload calls |
NodePort | Every node’s IP on a fixed port, plus the ClusterIP | Dev/testing, your own external load balancer |
LoadBalancer | An external IP from the cloud, plus node port and ClusterIP | Production internet-facing apps |
ExternalName | Inside the cluster, as a DNS alias (CNAME) to an outside name | Giving an external API a cluster name |
DNS across namespaces
Every Service is in DNS as <service>.<namespace>.svc.cluster.local. The short name works
only from the same namespace. Here the same curl image is run from a second namespace:
kubectl create namespace production
kubectl run client -n production --image=curlimages/curl:8.10.1 -- sleep 3600
kubectl wait -n production --for=condition=Ready pod/client
kubectl exec -n production client -- curl -sS http://api-service
kubectl exec -n production client -- curl -sS http://api-service.default
kubectl exec -n production client -- cat /etc/resolv.conf
$ kubectl create namespace production
namespace/production created
$ kubectl run client -n production --image=curlimages/curl:8.10.1 -- sleep 3600
pod/client created
$ kubectl wait -n production --for=condition=Ready pod/client
pod/client condition met
$ kubectl exec -n production client -- curl -sS http://api-service
curl: (6) Could not resolve host: api-service
command terminated with exit code 6
$ kubectl exec -n production client -- curl -sS http://api-service.default
hello from api-785f67cc54-7mkfv
$ kubectl exec -n production client -- cat /etc/resolv.conf
nameserver 10.96.0.10
search production.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
resolv.conf explains both results. A short name is tried against each search domain in
turn. From production, api-service becomes api-service.production.svc.cluster.local,
which doesn’t exist, and the other search domains miss too, so curl fails with exit 6, “could not resolve”.
api-service.default becomes api-service.default.svc.cluster.local on the second search
domain, which does exist. 10.96.0.10 is the cluster’s CoreDNS Service.
Endpoints and EndpointSlices
A Service’s backend list is kept in EndpointSlice objects, which the control plane
rewrites whenever a matching Pod becomes Ready or goes away. The older Endpoints API still
works but prints a deprecation warning (see Practice task 1).
$ kubectl get endpointslices -l kubernetes.io/service-name=api-service
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-service-v952m IPv4 8080 10.244.0.3,10.244.0.4 106s
$ kubectl scale deployment api --replicas=3
deployment.apps/api scaled
$ kubectl rollout status deployment/api
Waiting for deployment "api" rollout to finish: 2 of 3 updated replicas are available...
deployment "api" successfully rolled out
$ kubectl get endpointslices -l kubernetes.io/service-name=api-service
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-service-v952m IPv4 8080 10.244.0.3,10.244.0.4,10.244.0.7 108s
The Service was not touched, but its slice picked up the third Pod, 10.244.0.7, as soon as
it was Ready. The slice’s age, 108s, shows that it was updated in place, not recreated.
When a Service has no endpoints, the selector is almost always the cause. This one differs from
api-service only in case:
# service-typo.yaml
apiVersion: v1
kind: Service
metadata:
name: api-typo
spec:
selector:
app: API # labels are case-sensitive
ports:
- port: 80
targetPort: 8080
$ kubectl apply -f service-typo.yaml
service/api-typo created
$ kubectl get endpointslices -l kubernetes.io/service-name=api-typo
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-typo-bmsmn IPv4 <unset> <unset> 0s
$ kubectl exec client -- curl -sS -m 3 http://api-typo
curl: (7) Failed to connect to api-typo port 80 after 1 ms: Could not connect to server
command terminated with exit code 7
$ kubectl describe service api-typo | grep Selector
Selector: app=API
$ kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
api-785f67cc54-7mkfv 1/1 Running 0 2m3s app=api,pod-template-hash=785f67cc54
api-785f67cc54-9pk9r 1/1 Running 0 10s app=api,pod-template-hash=785f67cc54
api-785f67cc54-mbwl4 1/1 Running 0 2m3s app=api,pod-template-hash=785f67cc54
client 1/1 Running 0 113s run=client
The Service exists and its name resolves, so curl got as far as connecting and was refused
(exit 7, not 6). The slice is empty, and the two diagnostic commands show why: the selector
wants app=API and every Pod has app=api. Delete it before moving on:
$ kubectl delete -f service-typo.yaml
service "api-typo" deleted from default namespace
Headless Services (StatefulSets)
Setting clusterIP: None makes a Service headless: it gets no virtual IP, and its DNS name
returns the Pod IPs themselves. Paired with a StatefulSet, each Pod also gets its own DNS
name. The StatefulSet below runs sleep as a stand-in for a real database. Only the DNS
records matter here.
# db-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: db-headless
spec:
clusterIP: None # headless: no virtual IP
selector:
app: db
ports:
- port: 5432
# db.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db-headless # gives each Pod a DNS name under this Service
replicas: 2
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: busybox:1.36
command: ["sleep", "3600"] # stands in for a real database
kubectl apply -f db-headless.yaml -f db.yaml
kubectl rollout status statefulset/db
kubectl get service db-headless
kubectl get pods -l app=db -o wide
kubectl exec client -- nslookup db-headless.default.svc.cluster.local
kubectl exec client -- nslookup db-0.db-headless.default.svc.cluster.local
$ kubectl apply -f db-headless.yaml -f db.yaml
service/db-headless created
statefulset.apps/db created
$ kubectl rollout status statefulset/db
Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
$ kubectl get service db-headless
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
db-headless ClusterIP None <none> 5432/TCP 2s
$ kubectl get pods -l app=db -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
db-0 1/1 Running 0 3s 10.244.0.8 rw-k8s <none> <none>
db-1 1/1 Running 0 2s 10.244.0.9 rw-k8s <none> <none>
$ kubectl exec client -- nslookup db-headless.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: db-headless.default.svc.cluster.local
Address: 10.244.0.8
Name: db-headless.default.svc.cluster.local
Address: 10.244.0.9
$ kubectl exec client -- nslookup db-0.db-headless.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: db-0.db-headless.default.svc.cluster.local
Address: 10.244.0.8
CLUSTER-IP None is what makes it headless. The Service name returns both Pod IPs, and
db-0.db-headless returns only db-0’s. That per-Pod name is what lets a database replica
find “the primary” or “replica 1” by a name that survives restarts. The StatefulSet section of
the controllers lesson covers the rest.
Learning outcomes
You can:
- Write ClusterIP, NodePort and LoadBalancer Service manifests and reach each from the right place
- Explain how a selector becomes EndpointSlice entries, and why
portandtargetPortdiffer - Use DNS for service discovery within and across namespaces
- Debug a Service with
kubectl describe,kubectl get endpointslicesand--show-labels
Practice
These tasks continue from the cluster state the lesson leaves: api (scaled to 3),
api-service, api-nodeport, api-lb, the db StatefulSet, and the client Pod. The outputs
come from the same rw-k8s profile.
- Change
api.yamlto runnginx:1.25, which listens on port 80, not 8080. Apply it and check the Service’s endpoints and the Pods.
Solution
# api.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: web
image: nginx:1.25
ports:
- containerPort: 80
$ kubectl apply -f api.yaml
deployment.apps/api configured
$ kubectl rollout status deployment/api
Waiting for deployment "api" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "api" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "api" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "api" rollout to finish: 1 out of 2 new replicas have been updated...
Waiting for deployment "api" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "api" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "api" rollout to finish: 1 old replicas are pending termination...
deployment "api" successfully rolled out
$ kubectl get endpoints api-service
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME ENDPOINTS AGE
api-service 10.244.0.10:8080,10.244.0.11:8080 2m30s
$ kubectl get pods -l app=api -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
api-5f9cf776d6-2nx2h 1/1 Running 0 1s 10.244.0.11 rw-k8s <none> <none>
api-5f9cf776d6-tr5hv 1/1 Running 0 3s 10.244.0.10 rw-k8s <none> <none>
api-785f67cc54-mbwl4 1/1 Terminating 0 2m36s 10.244.0.3 rw-k8s <none> <none>
Applying the file also took replicas back from 3 to 2: the earlier kubectl scale was never
written into api.yaml. The endpoints already list only the two new Pods, although the old
mbwl4 Pod is still shutting down, because a Terminating Pod is dropped from the legacy
Endpoints list first. EndpointSlices keep it listed with ready=false, terminating=true. Notice the port: 8080, from targetPort, although nginx listens on 80. The warning
says the newer EndpointSlice API replaces Endpoints, but the command still works.
- Call
api-servicefrom theclientPod. It fails. Explain why from task 1’s output, fixservice-clusterip.yaml, and call it again.
Solution
Change targetPort: 8080 to targetPort: 80 in service-clusterip.yaml, then:
kubectl exec client -- curl -sS -m 3 http://api-service
kubectl apply -f service-clusterip.yaml
kubectl get endpoints api-service
kubectl exec client -- curl -sS -m 3 http://api-service | grep '<title>'
$ kubectl exec client -- curl -sS -m 3 http://api-service
curl: (7) Failed to connect to api-service port 80 after 1 ms: Could not connect to server
command terminated with exit code 7
$ kubectl apply -f service-clusterip.yaml
service/api-service configured
$ kubectl get endpoints api-service
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME ENDPOINTS AGE
api-service 10.244.0.10:80,10.244.0.11:80 2m38s
$ kubectl exec client -- curl -sS -m 3 http://api-service | grep '<title>'
<title>Welcome to nginx!</title>
The name resolved and the Service had endpoints, but it forwarded to port 8080, where nothing listens, so the connection was refused (curl exit 7). Non-empty endpoints only prove the selector matched. The port still has to be right.
- From a
busybox:1.36Pod, look upapi-serviceby its short name, by its full name, and as if it lived in theproductionnamespace.
Solution
kubectl run dns-test --image=busybox:1.36 -- sleep 3600
kubectl wait --for=condition=Ready pod/dns-test
kubectl get service api-service
kubectl exec dns-test -- nslookup api-service
kubectl exec dns-test -- nslookup api-service.default.svc.cluster.local
kubectl exec dns-test -- nslookup api-service.production.svc.cluster.local
$ kubectl run dns-test --image=busybox:1.36 -- sleep 3600
pod/dns-test created
$ kubectl wait --for=condition=Ready pod/dns-test
pod/dns-test condition met
$ kubectl get service api-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-service ClusterIP 10.102.76.117 <none> 80/TCP 2m44s
$ kubectl exec dns-test -- nslookup api-service
Server: 10.96.0.10
Address: 10.96.0.10:53
** server can't find api-service.cluster.local: NXDOMAIN
** server can't find api-service.cluster.local: NXDOMAIN
Name: api-service.default.svc.cluster.local
Address: 10.102.76.117
** server can't find api-service.svc.cluster.local: NXDOMAIN
** server can't find api-service.svc.cluster.local: NXDOMAIN
command terminated with exit code 1
$ kubectl exec dns-test -- nslookup api-service.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: api-service.default.svc.cluster.local
Address: 10.102.76.117
$ kubectl exec dns-test -- nslookup api-service.production.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
** server can't find api-service.production.svc.cluster.local: NXDOMAIN
** server can't find api-service.production.svc.cluster.local: NXDOMAIN
command terminated with exit code 1
Both working lookups return the ClusterIP, not a Pod IP. busybox’s nslookup tries every
search suffix, printing an NXDOMAIN for each one that misses and exiting 1 even though
default.svc.cluster.local matched. The full name avoids that noise. The production
namespace exists, but there is no api-service in it, so that name doesn’t exist at all.
- The NodePort Service has the same problem as task 2. Show that it is broken, fix
service-nodeport.yamlthe same way, and call the NodePort from the curl Pod again.
Solution
Change targetPort: 8080 to targetPort: 80 in service-nodeport.yaml, then:
kubectl exec client -- curl -sS -m 3 http://192.168.49.2:30080
kubectl apply -f service-nodeport.yaml
kubectl get service api-nodeport
kubectl exec client -- curl -sS -m 3 http://192.168.49.2:30080 | grep '<title>'
$ kubectl exec client -- curl -sS -m 3 http://192.168.49.2:30080
curl: (7) Failed to connect to 192.168.49.2 port 30080 after 0 ms: Could not connect to server
command terminated with exit code 7
$ kubectl apply -f service-nodeport.yaml
service/api-nodeport configured
$ kubectl get service api-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-nodeport NodePort 10.111.71.240 <none> 80:30080/TCP 116s
$ kubectl exec client -- curl -sS -m 3 http://192.168.49.2:30080 | grep '<title>'
<title>Welcome to nginx!</title>
Each Service carries its own targetPort, so fixing api-service did nothing for
api-nodeport. The ClusterIP and node port did not change on apply, because only
targetPort was updated. Use your own node’s INTERNAL-IP in the URL.
- Write
api-headless, a headless version ofapi-service, and compare what DNS returns for it with task 3.
Solution
# api-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: api-headless
spec:
clusterIP: None # headless — no virtual IP
selector:
app: api
ports:
- port: 80
$ kubectl apply -f api-headless.yaml
service/api-headless created
$ kubectl get service api-headless
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-headless ClusterIP None <none> 80/TCP 0s
$ kubectl exec dns-test -- nslookup api-headless.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: api-headless.default.svc.cluster.local
Address: 10.244.0.10
Name: api-headless.default.svc.cluster.local
Address: 10.244.0.11
$ kubectl get pods -l app=api -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
api-5f9cf776d6-2nx2h 1/1 Running 0 30s 10.244.0.11 rw-k8s <none> <none>
api-5f9cf776d6-tr5hv 1/1 Running 0 32s 10.244.0.10 rw-k8s <none> <none>
With no ClusterIP, DNS returns one record per ready Pod, and the addresses match the Pod IPs. The client picks one and connects to it directly, with no kube-proxy balancing in between.
Clean up with:
kubectl delete -f api-headless.yaml -f db.yaml -f db-headless.yaml -f service-lb.yaml -f service-nodeport.yaml -f service-clusterip.yaml -f api.yaml
kubectl delete pod client dns-test
kubectl delete namespace production
$ kubectl delete -f api-headless.yaml -f db.yaml -f db-headless.yaml -f service-lb.yaml -f service-nodeport.yaml -f service-clusterip.yaml -f api.yaml
service "api-headless" deleted from default namespace
statefulset.apps "db" deleted from default namespace
service "db-headless" deleted from default namespace
service "api-lb" deleted from default namespace
service "api-nodeport" deleted from default namespace
service "api-service" deleted from default namespace
deployment.apps "api" deleted from default namespace
$ kubectl delete pod client dns-test
pod "client" deleted from default namespace
pod "dns-test" deleted from default namespace
$ kubectl delete namespace production
namespace "production" deleted
Deleting the namespace also deletes the client Pod inside it.