Master Kubernetes Services: Easily Understand ClusterIP, NodePort, and LoadBalancer 2025

Post 9 of 70 in “Mastering Kubernetes: A Practical Journey from Beginner to CKA”

🔥 TL;DR

  • Services provide stable network endpoints for ephemeral Pods using label selectors and virtual IPs
  • ClusterIP exposes apps internally, NodePort opens external access on every node, LoadBalancer integrates with cloud providers
  • kube-proxy handles traffic routing using iptables/ipvs rules across all cluster nodes
  • Service discovery works through DNS (my-service.my-namespace.svc.cluster.local) and environment variables
  • Real traffic flows through virtual IPs to Pod endpoints with automatic load balancing and health checking

Introduction: Kubernetes Services

Picture this: You’ve got a web application running across three Pods, and suddenly one Pod crashes and gets replaced with a completely different IP address. How do your frontend Pods know where to send requests now? What happens when you scale your backend from 3 to 10 replicas?

This is exactly the problem I ran into during my first production Kubernetes deployment. I had perfectly working Pods, but they couldn’t reliably talk to each other because Pod IPs kept changing. That’s when I discovered Services – the networking glue that makes everything work seamlessly.

In our previous post, we explored how namespaces organize your cluster resources. Today, we’re diving into Services – the networking abstraction that solves one of the most fundamental challenges in distributed systems: how do you maintain stable communication between constantly changing components?

What we’ll learn today: • How Services provide stable networking for dynamic Pod environments • The three main Service types and when to use each one • Real network traffic flows and packet routing inside your cluster
• Hands-on service discovery with DNS and environment variables

Why this matters: Without Services, your microservices architecture would be chaos. Services are what transform a collection of individual containers into a cohesive, resilient application. Master this concept, and you’ll understand how Netflix serves millions of users, how Spotify handles music streaming at scale, and how your own applications can achieve enterprise-grade reliability.

By the end of this post, you’ll trace actual network packets through your cluster, understand exactly how kube-proxy routes traffic, and build a complete service discovery demo that you can deploy in any Kubernetes environment.


Prerequisites

What you need to know:

  • Pod networking basics from our earlier posts (Posts #3-4)
  • Labels and selectors for resource targeting (covered in Post #6)
  • Namespace organization from our previous post (Post #8)
  • Basic kubectl commands and YAML structure

📌 Quick Refresher: Remember that Pods get dynamic IP addresses that change when they restart. Labels are key-value pairs that help us select and group resources. Namespaces provide logical separation of resources within a cluster.

Tools required:

  • A running Kubernetes cluster (local or cloud) with containerd runtime (recommended over Docker)
  • kubectl configured and connected
  • Optional: curl or wget for testing network connectivity
  • Cluster with at least 2GB RAM and 2 CPU cores for the demo applications

Previous posts to review:

  • Post #8: “Kubernetes Namespaces” – for resource organization context
  • Post #6: “Labels and Selectors” – essential for understanding how Services target Pods

Estimated time: 45-60 minutes for complete walkthrough with hands-on examples


Understanding Services: The Network Foundation

Theory First: Why Services Exist

Here’s what I love about Kubernetes Services – they solve a problem that’s obvious in hindsight but tricky to implement correctly. Think of Services like the phone system for your cluster. Instead of memorizing everyone’s constantly changing cell phone numbers (Pod IPs), you call the main office number (Service IP), and the receptionist (kube-proxy) routes your call to whoever can help you.

Every Service creates a virtual IP address (VIP) that never changes, even when the Pods behind it are constantly being created, destroyed, and replaced. This stable endpoint is what makes microservices architecture actually work in practice.

Kubernetes Service abstraction comparison - Master Kubernetes Services Easily Understand ClusterIP, NodePort, and LoadBalancer
Kubernetes Service abstraction comparison – Master Kubernetes Services Easily Understand ClusterIP, NodePort, and LoadBalancer

How Services Work Under the Hood:

  1. Service Controller watches for new Services and assigns them cluster-unique virtual IPs
  2. Endpoints Controller continuously monitors which Pods match the Service’s label selector
  3. kube-proxy (running on every node) creates iptables/ipvs rules to route traffic from Service IPs to healthy Pod IPs
  4. DNS Controller creates DNS records so you can reach services by name instead of IP

⚠️ Production Alert: Services are NOT load balancers in the traditional sense. They’re packet routing abstractions. The actual load balancing happens at the network layer through kube-proxy’s routing rules.

The Three Service Types You’ll Actually Use

ClusterIP (Internal Only)

  • Purpose: Internal communication between Pods within the cluster
  • Access: Only reachable from inside the cluster
  • Use Case: Database connections, internal APIs, microservice communication
  • Traffic Flow: Pod → ClusterIP → kube-proxy → Target Pod

NodePort (External Access)

  • Purpose: Expose services outside the cluster through node IPs
  • Access: External traffic can reach any node IP on the specified port
  • Use Case: Development environments, simple external access
  • Traffic Flow: External Client → Node IP:NodePort → ClusterIP → Target Pod

LoadBalancer (Cloud Integration)

  • Purpose: Cloud provider integration for external load balancers
  • Access: External traffic through cloud provider’s load balancer
  • Use Case: Production external services, automatic cloud integration
  • Traffic Flow: External Client → Cloud LB → Node IP:NodePort → ClusterIP → Target Pod

Service Type Comparison Table

FeatureClusterIPNodePortLoadBalancer
External Access❌ Internal only✅ Via node IP:port✅ Via cloud LB IP
Use CasePod-to-pod communicationDevelopment/testingProduction external
IP AssignmentCluster virtual IPCluster IP + Node portsCluster IP + Node ports + External IP
Port RangeAny port30000-32767Any port (cloud managed)
CostFreeFreeCloud provider charges
Cloud RequirementNoneNoneRequires cloud provider
Typical Production Usage95% of services<1% of services4% of services

When to choose each type:

  • ClusterIP: Databases, internal APIs, microservice communication, caching layers
  • NodePort: Local development, CI/CD pipelines, simple external access needs
  • LoadBalancer: Public-facing APIs, web applications, services requiring high availability

💡 Pro Tip: For development, use kubectl port-forward svc/my-service 8080:80 instead of NodePort – it’s more secure and doesn’t require firewall changes.

Quick Reference Commands:

# Service discovery verification
kubectl get endpoints <service-name>
kubectl describe svc <service-name>

# Test connectivity from any pod
kubectl run debug --image=nicolaka/netshoot --rm -it -- curl <service-name>

# Secure local access to ClusterIP services
kubectl port-forward svc/<service-name> 8080:80


Hands-On Implementation: Building Real Services

Let’s build a complete example that demonstrates all three Service types with real network traffic flows. We’ll create a web application with a backend database and expose it using different Service configurations.

Step 1: Create the Application Namespace and Pods

First, let’s set up our demo environment in the namespace we learned about in the previous post:

# demo-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: service-demo
  labels:
    environment: learning
---
# Backend application - simulates a database
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-app
  namespace: service-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
      containers:
      - name: backend
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "64Mi"
            cpu: "10m"
          limits:
            memory: "128Mi"
            cpu: "50m"
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5
        env:
        - name: BACKEND_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        command: ["/bin/sh"]
        args:
        - -c
        - |
          echo "<h1>Backend Pod: $BACKEND_ID</h1><p>Pod IP: $(hostname -i)</p>" > /usr/share/nginx/html/index.html
          exec nginx -g 'daemon off;'
---
# Frontend application
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-app
  namespace: service-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
      containers:
      - name: frontend
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "64Mi"
            cpu: "10m"
          limits:
            memory: "128Mi"
            cpu: "50m"
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5
        env:
        - name: FRONTEND_ID
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        command: ["/bin/sh"]
        args:
        - -c
        - |
          echo "<h1>Frontend Pod: $FRONTEND_ID</h1><p>Pod IP: $(hostname -i)</p>" > /usr/share/nginx/html/index.html
          exec nginx -g 'daemon off;'

Deploy and verify:

kubectl apply -f demo-namespace.yaml

# Quick verification 
kubectl get pods -n service-demo -o wide
kubectl wait --for=condition=ready pod --all -n service-demo --timeout=60s

Expected Output:

NAME                           READY   STATUS    RESTARTS   AGE   IP           NODE
backend-app-7d4b8f9c8d-abcde   1/1     Running   0          30s   10.244.1.4   node1
backend-app-7d4b8f9c8d-fghij   1/1     Running   0          30s   10.244.2.5   node2
backend-app-7d4b8f9c8d-klmno   1/1     Running   0          30s   10.244.1.6   node1
frontend-app-6b5c7d8e9f-pqrst  1/1     Running   0          30s   10.244.2.7   node2
frontend-app-6b5c7d8e9f-uvwxy  1/1     Running   0          30s   10.244.1.8   node1

Notice how each Pod gets a different IP address. This is exactly the problem Services solve!

Step 2: Create a ClusterIP Service for Internal Communication

# backend-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: backend-service
  namespace: service-demo
spec:
  type: ClusterIP  # Default, can be omitted
  selector:
    app: backend    # Simplified selector
  ports:
  - name: http
    port: 80
    targetPort: 80

Deploy and verify endpoints:

kubectl apply -f backend-service.yaml

# Verification commands (copy-paste ready)
kubectl get service backend-service -n service-demo
kubectl get endpoints backend-service -n service-demo
kubectl describe service backend-service -n service-demo

Expected Output:

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
backend-service   ClusterIP   10.96.123.45    <none>        80/TCP    15s

NAME              ENDPOINTS                               AGE
backend-service   10.244.1.4:80,10.244.1.6:80,10.244.2.5:80   15s

Step 3: Test Service Discovery (Quick Method)

The fastest way to test service discovery is using a debug pod:

# Create temporary debug pod for testing
kubectl run debug -n service-demo --image=nicolaka/netshoot --rm -it -- bash

# Inside the debug pod, test service discovery:
curl backend-service                    # Short name (same namespace)
curl backend-service.service-demo       # With namespace  
nslookup backend-service                # DNS resolution

# Test load balancing - run multiple times
for i in {1..5}; do curl -s backend-service | grep "Backend Pod"; done

Check Understanding: You should see different Pod names in the responses, demonstrating automatic load balancing across all backend Pods.

Step 4: Create NodePort for External Access

# frontend-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: frontend-nodeport
  namespace: service-demo
spec:
  type: NodePort
  selector:
    app: frontend
  ports:
  - name: http
    port: 80
    targetPort: 80
    nodePort: 30001  # Choose port in 30000-32767 range
  externalTrafficPolicy: Local  # Preserve source IP, reduce hops

kubectl apply -f frontend-nodeport.yaml

# Verification commands
kubectl get service frontend-nodeport -n service-demo
kubectl get nodes -o wide  # Get node IPs for testing

External Access Test:

# Get node external IP and test from outside cluster
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}')
curl http://$NODE_IP:30001

Step 5: LoadBalancer Service (Cloud Environments Only)

⚠️ Important: LoadBalancer services only work with cloud providers (AWS, GCP, Azure) or on-premises load balancer integrations.

# frontend-loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
  name: frontend-loadbalancer
  namespace: service-demo
spec:
  type: LoadBalancer
  selector:
    app: frontend
  ports:
  - name: http
    port: 80
    targetPort: 80

kubectl apply -f frontend-loadbalancer.yaml

# Watch for external IP assignment (takes 1-3 minutes in cloud)
kubectl get service frontend-loadbalancer -n service-demo -w

# Test once external IP is assigned
EXTERNAL_IP=$(kubectl get service frontend-loadbalancer -n service-demo -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$EXTERNAL_IP

<details> <summary>🔍 **Deep Dive: How Traffic Actually Flows**</summary>

Understanding the packet flow helps with troubleshooting:

ClusterIP Traffic Flow:

  1. Client Pod sends request to backend-service:80
  2. CoreDNS resolves service name to ClusterIP (10.96.123.45)
  3. Kernel routes packet to Service IP
  4. kube-proxy iptables rules DNAT packet to healthy Pod IP
  5. Backend Pod responds directly to client Pod

Real Network Analysis:

# View iptables rules created by kube-proxy
sudo iptables -t nat -L KUBE-SERVICES | grep backend-service

# Check service endpoints
kubectl get endpoints backend-service -n service-demo -o yaml

# Monitor DNS queries (from debug pod)
dig +trace backend-service.service-demo.svc.cluster.local

This explains why Services aren’t traditional load balancers – they’re packet routing abstractions! </details>

Quick Troubleshooting

Issue: “Connection Refused”

# Check service and endpoints
kubectl get svc,endpoints backend-service -n service-demo

# Verify pod labels match service selector  
kubectl get pods -n service-demo --show-labels
kubectl describe service backend-service -n service-demo

Issue: “Service Not Found”

# Check namespace and DNS
kubectl get services --all-namespaces | grep backend
nslookup backend-service.service-demo.svc.cluster.local

Issue: “NodePort Not Accessible”

# Check firewall and node status
kubectl get nodes -o wide
kubectl get service frontend-nodeport -n service-demo

# Test from inside cluster first
kubectl run debug --image=nicolaka/netshoot --rm -it -- curl [NODE-INTERNAL-IP]:30001


Real-World Scenarios

E-commerce Platform Pattern

Traffic Flow: Internet → LoadBalancer → Frontend → ClusterIP Services → Backend Pods

# Typical production setup
Frontend Service (LoadBalancer)    # Handles 50K+ requests/minute  
├── API Gateway (ClusterIP)        # Routes to microservices
├── User Service (ClusterIP)       # Authentication 
├── Product Catalog (ClusterIP)    # Inventory management
└── Database (ClusterIP)           # Persistent storage

Key Production Insights:

  • LoadBalancer: Only for services that need external access (frontend, APIs)
  • ClusterIP: 95% of your services will be internal microservice communication
  • Service discovery latency: <1ms DNS resolution, <5ms routing in production clusters

Multi-Tenant SaaS Pattern

Each customer namespace has isolated services, with shared infrastructure accessed cross-namespace:

# Customer services in isolated namespaces
kubectl get services -n tenant-customer-a
kubectl get services -n tenant-customer-b

# Access shared monitoring across namespaces  
curl prometheus.monitoring.svc.cluster.local:9090/metrics

Production naming conventions:

  • Descriptive names: user-api-service not service1
  • Environment indicators: user-api-prod, user-api-staging
  • Version targeting: Use specific labels for blue-green deployments

Troubleshooting Quick Reference

Service Discovery Debug Commands

# Essential debugging toolkit
kubectl get svc,endpoints [service-name] -n [namespace]
kubectl describe service [service-name] -n [namespace]
kubectl run debug --image=nicolaka/netshoot --rm -it -- bash

# Inside debug pod
nslookup [service-name].[namespace].svc.cluster.local
curl -v [service-name]:[port]
dig +short [service-name]

Common Issues & Fast Fixes

ProblemQuick CheckSolution
“Connection Refused”kubectl get endpoints [svc]Verify Pod labels match service selector
“Service Not Found”kubectl get svc -A | grep [name]Check namespace, verify service exists
“No Endpoints”kubectl get pods --show-labelsFix label selectors or Pod readiness
NodePort BlockedTest internally firstCheck firewall, security groups
LoadBalancer PendingCloud environment?Requires cloud provider integration

Advanced Debugging

# Check kube-proxy logs for routing issues
kubectl logs -n kube-system -l k8s-app=kube-proxy --tail=20

# Verify CoreDNS is working
kubectl get pods -n kube-system -l k8s-app=kube-dns

# Check recent events
kubectl get events --sort-by=.metadata.creationTimestamp -n [namespace]

Where to get help:

  • Official docs: kubectl explain service for spec details, or visit
  • Networking guide:
  • Service API reference:
  • Community: #networking channel in Kubernetes Slack
  • Debugging: kubectl get events --sort-by=.metadata.creationTimestamp for recent cluster events

📂 Complete Service Discovery Template

# complete-service-demo.yaml
# Deploy this entire configuration to see all Service types in action
---
apiVersion: v1
kind: Namespace
metadata:
  name: service-complete-demo
  labels:
    environment: learning
    series: mastering-k8s
    post: "09-services"
---
# Backend Application (Database Simulator)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-database
  namespace: service-complete-demo
  labels:
    app: database
    tier: data
    component: backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: database
      tier: data
  template:
    metadata:
      labels:
        app: database
        tier: data
        component: backend
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
        fsGroup: 101
      containers:
      - name: database
        image: nginx:1.25
        ports:
        - containerPort: 80
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: false
          capabilities:
            drop:
            - ALL
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        command: ["/bin/sh"]
        args:
        - -c
        - |
          # Create health check endpoints
          mkdir -p /usr/share/nginx/html
          echo "OK" > /usr/share/nginx/html/health
          echo "READY" > /usr/share/nginx/html/ready
          echo "<h1>Database Pod: $POD_NAME</h1><p>Pod IP: $POD_IP</p><p>Service: ClusterIP</p><p>Timestamp: $(date)</p>" > /usr/share/nginx/html/index.html
          exec nginx -g 'daemon off;'
---
# API Application (Middle Tier)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
  namespace: service-complete-demo
  labels:
    app: api
    tier: application
    component: backend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
      tier: application
  template:
    metadata:
      labels:
        app: api
        tier: application
        component: backend
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
        fsGroup: 101
      containers:
      - name: api
        image: nginx:1.25
        ports:
        - containerPort: 80
        - containerPort: 9090  # Metrics port
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: DATABASE_URL
          value: "database-service.service-complete-demo.svc.cluster.local"
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: false
          capabilities:
            drop:
            - ALL
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        command: ["/bin/sh"]
        args:
        - -c
        - |
          mkdir -p /usr/share/nginx/html
          echo "OK" > /usr/share/nginx/html/health
          echo "READY" > /usr/share/nginx/html/ready
          echo "<h1>API Pod: $POD_NAME</h1><p>Database URL: $DATABASE_URL</p><p>Service: NodePort</p><p>Timestamp: $(date)</p>" > /usr/share/nginx/html/index.html
          echo "api_requests_total 42" > /usr/share/nginx/html/metrics
          exec nginx -g 'daemon off;'
---
# Frontend Application
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-web
  namespace: service-complete-demo
  labels:
    app: frontend
    tier: web
    component: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
      tier: web
  template:
    metadata:
      labels:
        app: frontend
        tier: web
        component: frontend
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
        fsGroup: 101
      containers:
      - name: frontend
        image: nginx:1.25
        ports:
        - containerPort: 80
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: API_URL
          value: "api-service.service-complete-demo.svc.cluster.local"
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "100m"
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: false
          capabilities:
            drop:
            - ALL
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        command: ["/bin/sh"]
        args:
        - -c
        - |
          mkdir -p /usr/share/nginx/html
          echo "OK" > /usr/share/nginx/html/health
          echo "READY" > /usr/share/nginx/html/ready
          echo "<h1>Frontend Pod: $POD_NAME</h1><p>API URL: $API_URL</p><p>Service: LoadBalancer</p><p>Timestamp: $(date)</p>" > /usr/share/nginx/html/index.html
          exec nginx -g 'daemon off;'
---
# ClusterIP Service for Database (Internal Only)
apiVersion: v1
kind: Service
metadata:
  name: database-service
  namespace: service-complete-demo
  labels:
    app: database
    tier: data
    service-type: clusterip
spec:
  type: ClusterIP
  selector:
    app: database
    tier: data
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
---
# NodePort Service for API (External Development Access)
apiVersion: v1
kind: Service
metadata:
  name: api-service
  namespace: service-complete-demo
  labels:
    app: api
    tier: application
    service-type: nodeport
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "9090"
    prometheus.io/path: "/metrics"
spec:
  type: NodePort
  selector:
    app: api
    tier: application
  ports:
  - name: http
    port: 80
    targetPort: 80
    nodePort: 30080
    protocol: TCP
  - name: metrics
    port: 9090
    targetPort: 9090
    nodePort: 30090
    protocol: TCP
---
# LoadBalancer Service for Frontend (Production External Access)
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
  namespace: service-complete-demo
  labels:
    app: frontend
    tier: web
    service-type: loadbalancer
  annotations:
    # AWS Load Balancer Controller annotations
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
    # GCP Load Balancer annotations
    cloud.google.com/l4-rbs: "enabled"
    # Azure Load Balancer annotations  
    service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: "/health"
spec:
  type: LoadBalancer
  selector:
    app: frontend
    tier: web
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  # Enable session affinity for sticky sessions if needed
  # sessionAffinity: ClientIP
  # sessionAffinityConfig:
  #   clientIP:
  #     timeoutSeconds: 300
---
# Service for testing ClusterIP service discovery
apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-tester
  namespace: service-complete-demo
  labels:
    app: tester
    component: debug
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tester
  template:
    metadata:
      labels:
        app: tester
        component: debug
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
        fsGroup: 101
      containers:
      - name: tester
        image: nicolaka/netshoot:latest
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "100m"
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
            add:
            - NET_RAW  # Needed for ping
        command: ["/bin/bash"]
        args:
        - -c
        - |
          echo "Service Discovery Tester Pod Ready"
          echo "Test commands:"
          echo "curl database-service"
          echo "curl api-service" 
          echo "curl frontend-service"
          echo "nslookup database-service.service-complete-demo.svc.cluster.local"
          sleep infinity

Deploy the complete demo:

# Deploy everything
kubectl apply -f complete-service-demo.yaml

# Wait for all pods to be ready
kubectl wait --for=condition=ready pod --all -n service-complete-demo --timeout=300s

# Check all services
kubectl get services -n service-complete-demo -o wide

# Test service discovery from inside the cluster
kubectl exec -it deployment/service-tester -n service-complete-demo -- bash

# Inside the tester pod, run these commands:
curl database-service          # ClusterIP - internal only
curl api-service              # NodePort - also available externally  
curl frontend-service         # LoadBalancer - external access
nslookup database-service.service-complete-demo.svc.cluster.local

# Test external access (from outside cluster)
kubectl get nodes -o wide  # Get node external IPs
curl http://[NODE-EXTERNAL-IP]:30080  # NodePort access
kubectl get service frontend-service -n service-complete-demo  # Get LoadBalancer IP
curl http://[LOADBALANCER-EXTERNAL-IP]  # LoadBalancer access

# Clean up when done
kubectl delete namespace service-complete-demo

What this template demonstrates:

  • ✅ Complete 3-tier application (frontend → api → database)
  • ✅ All three Service types (ClusterIP, NodePort, LoadBalancer)
  • ✅ Production-ready security contexts and resource limits
  • ✅ Health checks and monitoring integration
  • ✅ Service discovery testing with dedicated debug pod
  • ✅ Cross-namespace DNS resolution examples
  • ✅ Cloud provider LoadBalancer annotations
  • ✅ Realistic environment variables and pod communication

Next Steps

Ready to take your networking skills to the next level? In our next post, we’re diving into “Ingress Controllers: The Gateway to Your Applications” – where we’ll discover how companies like Netflix and GitHub handle millions of external requests without creating hundreds of LoadBalancer services.

Here’s what’s coming: You’ll learn how Ingress controllers provide HTTP/HTTPS routing, SSL termination, and virtual hosting – essentially turning your cluster into a sophisticated web server that can handle complex routing rules like /api/v1/usersuser-service and /api/v1/productsproduct-service, all through a single external IP address.

Additional learning paths:

  • Explore Headless Services for direct Pod discovery (StatefulSets preview)
  • Research ExternalName Services for integrating external dependencies
  • Investigate Service Mesh solutions like Istio for advanced traffic management

Practice challenges:

  1. Beginner: Create a multi-tier application (frontend → backend → database) using only ClusterIP services for internal communication
  2. Intermediate: Implement blue-green deployment using different Service selectors to switch traffic between versions
  3. Advanced: Set up cross-namespace service communication and monitor traffic flows using tcpdump on cluster nodes

Community engagement: Share your service discovery setup! What naming conventions work best for your team? Have you discovered any interesting patterns when working with Services at scale?

Remember, you’re now 13% through our Kubernetes mastery journey. You can create Pods, organize them with namespaces, and connect them reliably with Services. Next, we’ll add the final networking piece that makes everything accessible from the internet!


FAQ Section

What happens when a Service’s target Pod becomes unhealthy or crashes?

The Endpoints controller automatically removes unhealthy Pods from the Service’s endpoint list based on readiness probe failures. kube-proxy updates its routing rules within seconds, so traffic stops flowing to failed Pods immediately. New requests automatically route to remaining healthy Pods.

Can I access a ClusterIP Service from outside the cluster?

No, ClusterIP Services are only accessible from within the cluster network. The ClusterIP range (typically 10.96.0.0/12) exists only in the cluster’s virtual network. To provide external access, you need NodePort, LoadBalancer, or Ingress (covered in the next post).

How does Kubernetes load balance traffic across multiple Pods behind a Service?

kube-proxy uses iptables rules (or ipvs in newer versions) to distribute traffic using round-robin algorithm by default. It’s not traditional load balancing – it’s packet-level routing at the network layer. Each connection gets routed to a different Pod, but existing connections maintain their Pod assignment.

What’s the difference between a Service port and targetPort?

The Service port is what clients connect to (the Service’s virtual port), while targetPort is the actual port the Pod container is listening on. For example, your Service might expose port 80, but your container actually runs on port 8080 – you’d set port: 80 and targetPort: 8080.

Why would I choose NodePort over LoadBalancer for external access?

NodePort is simpler and works in any environment (cloud or on-premises), while LoadBalancer requires cloud provider integration and typically costs more. Use NodePort for development, testing, or when you have your own external load balancer. Use LoadBalancer for production cloud deployments where you want automatic cloud integration and don’t want to manage external load balancers yourself.

Similar Posts

One Comment

Leave a Reply