Skip to main content

Command Palette

Search for a command to run...

πŸš€ Kubernetes Services Deep Dive: Service Discovery, NodePort & Load Balancing (Practical Walkthrough)

Updated
β€’11 min read

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 cluster

  • targetPort: 8000 β†’ Container port

  • nodePort: 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 β†’ LoadBalancer

  • selector β†’ 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.


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.

More from this blog

codeops-labs.hashnode.dev

16 posts