CKA — Services & Networking Lab

5 minute read

Lab Overview

This hands-on lab covers the core Services & Networking concepts you need for the CKA exam. You’ll create different Service types, test DNS resolution, inspect Endpoints, and debug Pod-to-Pod connectivity.

What you’ll practice:

  • Creating ClusterIP, NodePort, and Headless Services
  • Testing DNS resolution from inside Pods
  • Inspecting Endpoints and watching them change
  • Verifying Pod-to-Pod communication

Prerequisites:

  • A running Kubernetes cluster (minikube, kind, or kubeadm)
  • kubectl configured and connected to the cluster

Task 1: Create a ClusterIP Service

Goal: Deploy an nginx application and expose it as a ClusterIP Service.

Step 1: Create the Deployment:

kubectl create deployment web --image=nginx --replicas=3

Step 2: Verify the Pods are running:

kubectl get pods -l app=web -o wide
Expected Output
NAME                   READY   STATUS    RESTARTS   AGE   IP            NODE
web-96d5df5c8-abc12    1/1     Running   0          30s   10.244.1.5    node1
web-96d5df5c8-def34    1/1     Running   0          30s   10.244.2.8    node2
web-96d5df5c8-ghi56    1/1     Running   0          30s   10.244.1.9    node1

Step 3: Expose the Deployment as a ClusterIP Service:

kubectl expose deployment web --port=80 --target-port=80 --name=web-clusterip

Step 4: Verify the Service:

kubectl get svc web-clusterip
Expected Output
NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
web-clusterip    ClusterIP   10.96.45.123   <none>        80/TCP    10s

Step 5: Test the Service from a temporary Pod:

kubectl run test-pod --image=busybox:1.28 --rm -it --restart=Never -- wget -qO- http://web-clusterip
Expected Output
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

You should see the default nginx welcome page HTML.


Task 2: Create a NodePort Service

Goal: Expose the same Deployment as a NodePort Service and access it from outside the cluster.

Step 1: Create a NodePort Service:

kubectl expose deployment web --port=80 --target-port=80 --type=NodePort --name=web-nodeport

Step 2: Check the assigned NodePort:

kubectl get svc web-nodeport
Expected Output
NAME            TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
web-nodeport    NodePort   10.96.78.210    <none>        80:31245/TCP   5s

Note the NodePort (31245 in this example — yours will differ).

Step 3: Get a node IP:

kubectl get nodes -o wide

Step 4: Access the Service via NodePort:

# Replace <node-ip> and <node-port> with actual values
curl http://<node-ip>:<node-port>
Expected Output
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

You should see the nginx welcome page, confirming external access works.


Task 3: Test DNS Resolution

Goal: Deploy a DNS utility Pod and test Service DNS resolution within and across namespaces.

Step 1: Create a namespace for cross-namespace testing:

kubectl create namespace other-ns

Step 2: Create a Deployment and Service in the other-ns namespace:

kubectl create deployment web-other --image=nginx --replicas=2 -n other-ns
kubectl expose deployment web-other --port=80 --target-port=80 --name=web-other-svc -n other-ns

Step 3: Deploy a DNS utility Pod in the default namespace:

kubectl run dnsutils --image=gcr.io/kubernetes-e2e-test-images/dnsutils:1.3 --command -- sleep 3600

Wait for it to be ready:

kubectl wait --for=condition=Ready pod/dnsutils --timeout=60s

Step 4: Test same-namespace DNS resolution:

kubectl exec -it dnsutils -- nslookup web-clusterip
Expected Output
Server:    10.96.0.10
Address:   10.96.0.10#53

Name:      web-clusterip.default.svc.cluster.local
Address:   10.96.45.123

The short name resolves because both the Pod and Service are in the default namespace.

Step 5: Test cross-namespace DNS resolution:

kubectl exec -it dnsutils -- nslookup web-other-svc.other-ns
Expected Output
Server:    10.96.0.10
Address:   10.96.0.10#53

Name:      web-other-svc.other-ns.svc.cluster.local
Address:   10.96.88.45

Step 6: Test full FQDN resolution:

kubectl exec -it dnsutils -- nslookup web-other-svc.other-ns.svc.cluster.local
Expected Output
Server:    10.96.0.10
Address:   10.96.0.10#53

Name:      web-other-svc.other-ns.svc.cluster.local
Address:   10.96.88.45

Task 4: Headless Service

Goal: Create a Headless Service and verify that DNS returns individual Pod IPs.

Step 1: Create a Headless Service for the web Deployment:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: web-headless
spec:
  clusterIP: None
  selector:
    app: web
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
EOF

Step 2: Verify the Service has no ClusterIP:

kubectl get svc web-headless
Expected Output
NAME           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
web-headless   ClusterIP   None         <none>        80/TCP    5s

Note that CLUSTER-IP is None.

Step 3: Test DNS resolution — it should return Pod IPs:

kubectl exec -it dnsutils -- nslookup web-headless
Expected Output
Server:    10.96.0.10
Address:   10.96.0.10#53

Name:      web-headless.default.svc.cluster.local
Address:   10.244.1.5
Name:      web-headless.default.svc.cluster.local
Address:   10.244.2.8
Name:      web-headless.default.svc.cluster.local
Address:   10.244.1.9

Notice that DNS returns multiple A records — one for each Pod’s IP. Compare this to the ClusterIP Service which returns a single virtual IP.


Task 5: Endpoints Investigation

Goal: View Endpoints for a Service, scale the Deployment, and watch Endpoints change.

Step 1: View current Endpoints for the web-clusterip Service:

kubectl get endpoints web-clusterip
Expected Output
NAME             ENDPOINTS                                      AGE
web-clusterip    10.244.1.5:80,10.244.1.9:80,10.244.2.8:80     5m

Three endpoints — one for each replica.

Step 2: Scale the Deployment to 5 replicas:

kubectl scale deployment web --replicas=5

Step 3: Watch Endpoints update:

kubectl get endpoints web-clusterip
Expected Output
NAME             ENDPOINTS                                                        AGE
web-clusterip    10.244.1.5:80,10.244.1.9:80,10.244.1.12:80,10.244.2.8:80,...   6m

You should now see 5 endpoints — Kubernetes automatically updated the Endpoints object.

Step 4: Scale down to 1 replica and check again:

kubectl scale deployment web --replicas=1
kubectl get endpoints web-clusterip
Expected Output
NAME             ENDPOINTS        AGE
web-clusterip    10.244.1.5:80    7m

Only one endpoint remains.

Step 5: View EndpointSlices:

kubectl get endpointslices -l kubernetes.io/service-name=web-clusterip
Expected Output
NAME                    ADDRESSTYPE   PORTS   ENDPOINTS      AGE
web-clusterip-abc12     IPv4          80      10.244.1.5     7m

Step 6: Scale back up for the next task:

kubectl scale deployment web --replicas=3

Task 6: Pod-to-Pod Communication

Goal: Create two Deployments and test Pod-to-Pod connectivity by IP and Service name.

Step 1: Create a second Deployment:

kubectl create deployment backend --image=nginx --replicas=2
kubectl expose deployment backend --port=80 --target-port=80 --name=backend-svc

Step 2: Get Pod IPs for both Deployments:

kubectl get pods -o wide -l app=web
kubectl get pods -o wide -l app=backend
Expected Output
# web Pods
NAME                   READY   STATUS    IP            NODE
web-96d5df5c8-abc12    1/1     Running   10.244.1.5    node1
web-96d5df5c8-def34    1/1     Running   10.244.2.8    node2
web-96d5df5c8-ghi56    1/1     Running   10.244.1.9    node1

# backend Pods
NAME                       READY   STATUS    IP            NODE
backend-7f8b9c6d4-jkl78    1/1     Running   10.244.2.11   node2
backend-7f8b9c6d4-mno90    1/1     Running   10.244.1.14   node1

Step 3: Test Pod-to-Pod communication by IP (from a web Pod to a backend Pod):

# Get a web Pod name
WEB_POD=$(kubectl get pods -l app=web -o jsonpath='{.items[0].metadata.name}')

# Get a backend Pod IP
BACKEND_IP=$(kubectl get pods -l app=backend -o jsonpath='{.items[0].status.podIP}')

# Test connectivity by IP
kubectl exec -it $WEB_POD -- wget -qO- http://$BACKEND_IP
Expected Output
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

Direct Pod-to-Pod communication by IP works.

Step 4: Test Pod-to-Pod communication via Service name:

kubectl exec -it $WEB_POD -- wget -qO- http://backend-svc
Expected Output
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

Communication via Service DNS name works — this is the recommended approach.

Step 5: Test cross-namespace connectivity:

kubectl exec -it $WEB_POD -- wget -qO- http://web-other-svc.other-ns
Expected Output
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

Cross-namespace communication works using <service>.<namespace> format.


Cleanup

Remove all resources created during this lab:

# Delete Services
kubectl delete svc web-clusterip web-nodeport web-headless backend-svc

# Delete Deployments
kubectl delete deployment web backend

# Delete the dnsutils Pod
kubectl delete pod dnsutils

# Delete resources in other-ns
kubectl delete deployment web-other -n other-ns
kubectl delete svc web-other-svc -n other-ns
kubectl delete namespace other-ns

Verify everything is cleaned up:

kubectl get all
Expected Output
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   1d

Only the default kubernetes Service should remain.


Congratulations! You’ve completed the Services & Networking hands-on lab. You’ve practiced creating different Service types, testing DNS resolution, inspecting Endpoints, and verifying Pod-to-Pod communication — all essential skills for the CKA exam.

Updated: