Master Kubernetes Services: Easily Understand ClusterIP, NodePort, and LoadBalancer 2025
Post 9 of 70 in “Mastering Kubernetes: A Practical Journey from Beginner to CKA”
Table of Contents
🔥 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:
curlorwgetfor 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.

How Services Work Under the Hood:
- Service Controller watches for new Services and assigns them cluster-unique virtual IPs
- Endpoints Controller continuously monitors which Pods match the Service’s label selector
- kube-proxy (running on every node) creates iptables/ipvs rules to route traffic from Service IPs to healthy Pod IPs
- 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
| Feature | ClusterIP | NodePort | LoadBalancer |
|---|---|---|---|
| External Access | ❌ Internal only | ✅ Via node IP:port | ✅ Via cloud LB IP |
| Use Case | Pod-to-pod communication | Development/testing | Production external |
| IP Assignment | Cluster virtual IP | Cluster IP + Node ports | Cluster IP + Node ports + External IP |
| Port Range | Any port | 30000-32767 | Any port (cloud managed) |
| Cost | Free | Free | Cloud provider charges |
| Cloud Requirement | None | None | Requires cloud provider |
| Typical Production Usage | 95% of services | <1% of services | 4% 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:
- Client Pod sends request to
backend-service:80 - CoreDNS resolves service name to ClusterIP (10.96.123.45)
- Kernel routes packet to Service IP
- kube-proxy iptables rules DNAT packet to healthy Pod IP
- 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-servicenotservice1 - 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
| Problem | Quick Check | Solution |
|---|---|---|
| “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-labels | Fix label selectors or Pod readiness |
| NodePort Blocked | Test internally first | Check firewall, security groups |
| LoadBalancer Pending | Cloud 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 servicefor spec details, or visit - Networking guide:
- Service API reference:
- Community: #networking channel in Kubernetes Slack
- Debugging:
kubectl get events --sort-by=.metadata.creationTimestampfor 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/users → user-service and /api/v1/products → product-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:
- Beginner: Create a multi-tier application (frontend → backend → database) using only ClusterIP services for internal communication
- Intermediate: Implement blue-green deployment using different Service selectors to switch traffic between versions
- Advanced: Set up cross-namespace service communication and monitor traffic flows using
tcpdumpon 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.

One Comment