π Kubernetes Services Deep Dive: Service Discovery, NodePort & Load Balancing (Practical Walkthrough)
When working with Kubernetes, deploying Pods is simple.
But understanding how traffic flows inside the cluster β and how applications are exposed β is where real learning begins.
In this practical deep dive, we will implement and understand:
π Pod IP volatility
π· Service Discovery using labels & selectors
π Exposing applications via NodePort
βοΈ LoadBalancer behavior in cloud environments
βοΈ Automatic load balancing
All steps are fully hands-on using this repository:
git clone https://github.com/Kunja-Ravikiran/k8s-service-deep-dive.git
cd k8s-service-deep-dive
π§Ή Step 1: Start with a Clean Minikube Cluster
Start Minikube:
minikube start
kubectl get nodes
β οΈ Important:
We are NOT deleting the Kubernetes cluster itself.
We are only deleting previous deployments/services if any exist.
Check existing resources:
kubectl get all
If needed, clean previous workloads:
kubectl delete deployment <name>
kubectl delete service <name>
π³ Step 2: Build the Docker Image & Understand the Deployment
Now that we have cloned the repository:
git clone https://github.com/Kunja-Ravikiran/k8s-service-deep-dive.git
cd k8s-service-deep-dive
We will first build the Docker image locally.
πΉ Build the Docker Image
Navigate into the app directory:
cd app
You should see:
ls
Dockerfile app.py requirements.txt
Now build the Docker image:
docker build -t flask-app:latest .
Once completed, verify:
docker images
You should see:
flask-app latest
Now go back to the root directory:
cd ..
πΉ Understanding deployment.yaml (Official vs Our Version)
Inside the repository, navigate to the k8s folder:
cd k8s
ls
You will see:
deployment.yaml
service-nodeport.yaml
service-loadbalancer.yaml
service-broken.yaml
We already have deployment.yaml in our project.
But instead of blindly using it, letβs understand what it contains.
To do that, weβll compare it with the official Kubernetes example.
π Official Kubernetes Deployment Example
From the official Kubernetes documentation:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
This is a generic example that deploys nginx.
π§ Our Modified Version (Inside This Repo)
Now open our file:
vim deployment.yaml
Youβll see something like:
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
labels:
app: flask-app
spec:
replicas: 2
selector:
matchLabels:
app: flask-app
template:
metadata:
labels:
app: flask-app
spec:
containers:
- name: flask-container
image: flask-app:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
π What Did We Change?
Field | Official Example | Our Version | Why We Changed It |
|---|---|---|---|
name | nginx-deployment | flask-app | Our application name |
labels | app: nginx | app: flask-app | Must match Service selector |
replicas | 3 | 2 | To demonstrate load balancing |
container name | nginx | flask-container | Clearer naming |
image | nginx:1.14.2 | flask-app:latest | Our Docker image |
containerPort | 80 | 8000 | Flask runs on port 8000 |
imagePullPolicy | not present | IfNotPresent | Use local image |
π§ Why This Comparison Is Important
This shows something very important:
You do NOT write Kubernetes YAML from scratch.
You:
Look at official examples
Understand the structure
Modify only what is required
Thatβs how real engineers work.
Kubernetes YAML becomes simple once you realize:
Itβs just a structured template that you adapt.
π Step 3: Apply the Deployment to Kubernetes
Now that we understand whatβs inside deployment.yaml, letβs create the Deployment in our cluster.
Make sure you are inside the k8s folder:
cd k8s
Apply the deployment:
kubectl apply -f deployment.yaml
You should see:
deployment.apps/flask-app created
π Verify the Deployment
Check if Pods are created:
kubectl get pods
Initially, you may see:
STATUS: ContainerCreating
Wait a few seconds and run again:
kubectl get pods
Now you should see:
NAME READY STATUS RESTARTS
flask-app-xxxxx 1/1 Running 0
flask-app-yyyyy 1/1 Running 0
Notice:
Two Pods are running
Because we set
replicas: 2
π Check Pod IP Addresses
Now check Pod IPs:
kubectl get pods -o wide
Example output:
NAME READY STATUS RESTARTS AGE IP NODE
flask-app-57686d9fdb-d9hcf 1/1 Running 0 3m56s 10.244.0.49 minikube
flask-app-57686d9fdb-x95jr 1/1 Running 0 3m56s 10.244.0.48 minikube
You will observe:
Each Pod has its own IP address
These IPs are assigned dynamically
π§ͺ Delete a Pod to Prove IP Volatility
Now delete one Pod:
kubectl delete pod flask-app-57686d9fdb-d9hcf
Wait a few seconds and check again:
kubectl get pods -o wide
You will see:
The deleted Pod is recreated
A new Pod appears
The new Pod has a different IP address
This proves:
Pod IP addresses are NOT stable.
π§ Important Observation
Right now:
Pods are running
But they are NOT accessible externally
And Pod IPs change when Pods are recreated
This is the exact problem Kubernetes Services are designed to solve.
In the next step, we will create a Service and see how Kubernetes provides a stable endpoint and load balancing.
π· Step 4: Service Discovery with Labels & Selectors
Now we have:
Two running Pods
Dynamic Pod IP addresses
No external access
We need a stable endpoint that:
Does not change when Pods are recreated
Routes traffic automatically
Exposes the application externally
That is the role of a Kubernetes Service.
πΉ Understanding service-nodeport.yaml (Official vs Our Version)
Inside the k8s folder:
cd k8s
ls
You will see:
deployment.yaml
service-nodeport.yaml
service-loadbalancer.yaml
service-broken.yaml
Open the NodePort service file:
vim service-nodeport.yaml
π Official Kubernetes NodePort Example
From official Kubernetes documentation:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app: MyApp
ports:
- port: 80
targetPort: 9376
This is a generic template.
π§ Our Modified Version
Inside our repo:
apiVersion: v1
kind: Service
metadata:
name: flask-service
spec:
type: NodePort
selector:
app: flask-app
ports:
- port: 80
targetPort: 8000
nodePort: 30007
π What Did We Modify?
Field | Official | Our Version | Why |
|---|---|---|---|
name | my-service | flask-service | Meaningful service name |
selector | app: MyApp | app: flask-app | Must match Deployment label |
targetPort | 9376 | 8000 | Flask runs on 8000 |
nodePort | not specified | 30007 | Explicit external port |
π Understanding the Ports
ports:
- port: 80
targetPort: 8000
nodePort: 30007
This means:
port: 80β Service port inside the clustertargetPort: 8000β Container portnodePort: 30007β External access port
Traffic flow:
Client β Node-IP:30007 β Service (80) β Pod (8000)
π Apply the Service
Make sure you are inside the k8s folder:
kubectl apply -f service-nodeport.yaml
Output:
service/flask-service created
π Check the Service
kubectl get svc
Example output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flask-service NodePort 10.103.214.221 <none> 80:30007/TCP 15s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10d
π Important Observation
PORT(S): 80:30007/TCP
This means:
Port 80 β Service Port (inside cluster)
Port 30007 β NodePort (external access)
πΉ Get Minikube IP
minikube ip
Example output:
192.168.49.2
πΉ Test the Service
curl http://192.168.49.2:30007
Example output:
<h1>Kubernetes Service Deep Dive π</h1>
<p><strong>Pod Name:</strong> flask-app-57686d9fdb-gfrv7</p>
<p>This response helps demonstrate load balancing.</p>
β Break the Selector (Failure Demo)
Apply intentionally broken service:
kubectl apply -f service-broken.yaml
Inside that file:
selector:
ap: flask-app # wrong key
Now test:
curl http://<minikube-ip>:30007
It fails.
Why?
Because the Service selector does not match Pod labels.
π§ How to Properly Fix It
We donβt just blindly reapply.
We FIX the selector.
Edit the service:
vim service-broken.yaml
Correct:
selector:
app: flask-app
Or simply reapply the correct service file:
kubectl apply -f service-nodeport.yaml
Now test again β traffic works.
This demonstrates the importance of label consistency.
π Step 5: Exposing Applications β NodePort vs LoadBalancer
NodePort
NodePort exposes the app on:
<Node-IP>:30007
Useful for:
Internal organization access
Testing environments
LoadBalancer
Before applying, just like we did with Deployment and NodePort Service,
we use the official Kubernetes Service syntax and modify only the required fields.
In our service-loadbalancer.yaml, we changed:
typeβLoadBalancerselectorβapp: flask-app(must match Deployment label)targetPortβ8000(Flask container port)
Again, we are not memorizing YAML β we modify only what is required.
Apply:
kubectl apply -f service-loadbalancer.yaml
kubectl get svc
Example output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flask-service LoadBalancer 10.103.214.221 <pending> 80:30007/TCP 34m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10d
In Minikube:
EXTERNAL-IP: <pending>
Because Minikube does not integrate with cloud provider APIs.
Minikube cannot:
Call AWS API
Create Azure Load Balancer
Allocate real public IP
So the external IP remains <pending>.
Important Observation
Even though the type is LoadBalancer, you still see:
PORT(S): 80:30007/TCP
This is because:
A LoadBalancer Service internally creates a NodePort.
Thatβs why this still works:
minikube ip
curl http://192.168.49.2:30007
Example output:
<h1>Kubernetes Service Deep Dive π</h1>
<p><strong>Pod Name:</strong> flask-app-57686d9fdb-gfrv7</p>
<p>This response helps demonstrate load balancing.</p>
In cloud environments (EKS, GKE, AKS):
Kubernetes provisions a cloud load balancer
Assigns a public IP or DNS
Routes traffic externally
In cloud, kubectl get svc would show something like:
EXTERNAL-IP: a1b2c3d4e5.elb.amazonaws.com
βοΈ Step 6: Load Balancing in Action
Our Deployment uses:
replicas: 2
This means:
Two Pods are running
Service should distribute traffic between them
Now letβs prove it practically.
πΉ Navigate to the Correct Directory
From inside the k8s folder, go back to the project root:
cd ..
Verify structure:
ls
You should see:
README.md app k8s scripts
Now enter the scripts folder:
cd scripts
ls
You will see:
test-loadbalance.sh
πΉ Make Script Executable (If Needed)
If you get permission issues:
chmod +x test-loadbalance.sh
πΉ Run the Script
Now execute:
./test-loadbalance.sh
π Example Output
You will see something like:
<h1>Kubernetes Service Deep Dive π</h1>
Pod Name: flask-app-57686d9fdb-gfrv7
-----------------------------
<h1>Kubernetes Service Deep Dive π</h1>
Pod Name: flask-app-57686d9fdb-x95jr
-----------------------------
<h1>Kubernetes Service Deep Dive π</h1>
Pod Name: flask-app-57686d9fdb-gfrv7
-----------------------------
<h1>Kubernetes Service Deep Dive π</h1>
Pod Name: flask-app-57686d9fdb-x95jr
-----------------------------
Notice:
Pod names alternate
Traffic is distributed between replicas
π§ What Just Happened?
Your script runs:
curl http://$MINIKUBE_IP:30007
multiple times.
Each request is handled by:
Either Pod A
Or Pod B
This confirms:
Kubernetes automatically distributes traffic
No additional configuration needed
Service handles load balancing internally
π₯ Important Understanding
We did NOT configure:
NGINX
HAProxy
External load balancer
The Kubernetes Service itself handles load balancing.
That is the power of Kubernetes networking.
π― Key Practical Takeaways
Never depend on Pod IPs
Always ensure labels & selectors match
NodePort exposes services on Node-IP:NodePort (for example, 192.168.49.2:30007).
LoadBalancer integrates with cloud
Services provide built-in load balancing
YAML syntax need not be memorized β just understand what fields to modify
π Conclusion
This practical walkthrough demonstrated the real mechanics behind Kubernetes Services:
Why they exist
How Service Discovery works
How exposure modes differ
How load balancing functions internally
How to verify traffic behavior using practical testing
Understanding this flow is essential for anyone serious about Kubernetes and DevOps.