Expose Python Apps with ClusterIP
Expose Python apps with a ClusterIP service — Kubernetes for Python Developers.
Focus: expose python apps with a clusterip service
You've built a Python Flask app, containerized it, and deployed it as a Kubernetes Deployment. But now the real question hits: can other Pods inside your cluster talk to your app? By default, each Pod gets its own internal IP, but those IPs are ephemeral — they change on restarts and scale events. Exposing Python apps with a ClusterIP service solves this by giving your app a stable virtual IP and DNS name inside the cluster. Without it, your microservices would have to chase moving IPs, making any form of service-to-service communication fragile and brittle. In this lesson, you'll learn to create a ClusterIP service, test it from within the cluster, and understand exactly when this internal networking pattern is the right choice.
The problem this lesson solves
Think about the last time you called a Python API from another Python service. You probably used requests.get("http://my-service:5000/health"). The URL contains a hostname — my-service — that you expect to resolve to a stable IP. Kubernetes makes that promise possible with a Service. But why is it a problem without one?
When you create a Deployment, Kubernetes launches Pods. Each Pod receives an internal IP address from the cluster's network range. These IPs are ephemeral: they change whenever the Pod is recreated (after a crash, a rollout, or a scaling event). If your other components hardcode these IPs, they'll break as soon as anything restarts.
Here's another wrinkle: your Deployment might have multiple replicas. Which Pod should the caller hit? Without a Service, you'd have to pick one of several IPs, and you'd have no load balancing. The result is a mess of brittle configuration and manual IP management.
So the pain is real: unstable internal addressing and no built-in load balancing. Both are solved cleanly by creating a Kubernetes Service of type ClusterIP.
By the end of this lesson, you'll be able to:
- Explain how a ClusterIP service provides a stable network endpoint.
- Create a Service object for your Python app using kubectl and YAML.
- Test the service from inside a Pod using curl.
- Choose between ClusterIP, NodePort, and LoadBalancer based on your use case.
Core concept / mental model
A ClusterIP service is a stable virtual IP and DNS name inside your Kubernetes cluster. It acts as a front door to your Pods, receiving traffic and forwarding it to one of the selected Pods. The name is a bit misleading: the IP is not assigned to a specific Pod — it's a virtual IP managed by the cluster's control plane.
Here's the mental model: imagine your Pods are apartment units in a building. Each unit has an internal room number (Pod IP). The ClusterIP service is the building's front desk. Outsiders (other services) know the building's address and phone number (the service's cluster IP and DNS name). They call, and the front desk directs the call to an available unit. If a tenant moves out (Pod restarts), the front desk still knows the building's address — no one outside needs to know the individual room numbers.
Conceptually, a Service object has three critical parts:
- Selector: a set of labels that match your Pods (e.g.,
app: my-python-app). The service only sends traffic to Pods that have those labels. - Ports: the mapping between the service's listening port and the target port on the Pod. For example, your Flask app might listen on port 5000 inside the container.
- Type:
ClusterIPis the default; it exposes the service only inside the cluster network.
Kubernetes automatically creates a DNS record for the service using the format <service-name>.<namespace>.svc.cluster.local. Within the same namespace, you can shorten that to just <service-name>. So a Pod can call http://my-flask-svc:5000 without any configuration.
Key definitions
- Endpoint: an IP:port pair that the service routes to. These are updated automatically when Pods change.
- kube-proxy: the component on each node that implements the virtual IP forwarding rules.
- Selector: the label query that links the service to its backing Pods.
How it works step by step
When you create a Service, here's what happens under the hood (simplified but accurate):
- You apply a Service manifest — you define
apiVersion: v1,kind: Service, metadata, and aspecblock withselector,ports, andtype: ClusterIP. - Control plane assigns a virtual IP — Kubernetes picks an IP from your cluster's service CIDR range (e.g.,
10.96.0.0/12). This IP is stable for the life of the Service. - Endpoints are created — the control plane watches Pods that match your selector and populates an Endpoints object (or EndpointSlices in newer versions) with the current Pod IPs.
- kube-proxy installs rules — on every node, kube-proxy programs iptables (or IPVS) rules that capture traffic to the service IP and forward it to one of the endpoints.
- Load balancing happens automatically — Kubernetes ships with a default round-robin strategy (the actual algorithm depends on kube-proxy mode).
- DNS is registered — CoreDNS creates a record for the service name, so DNS queries resolve to the virtual IP.
This all happens within seconds and is fully automated — you don't need to manage IPs or proxies yourself.
Why not just use Pod IPs?
If you're skeptical, here's a quick comparison: - Pod IPs: tied to a Pod lifecycle — a Pod restart changes IP. - ClusterIP: stable across Pod restarts, updates, and scaling events. - Pod IPs: not automatically load balanced. - ClusterIP: balances across all healthy Pods matching the selector. - Pod IPs: not reachable via DNS by default. - ClusterIP: has a built-in DNS name.
That's why Services are fundamental to Kubernetes networking: they abstract away the volatility of Pods.
Hands-on walkthrough
Time to get your hands dirty. In this section, you'll deploy a minimal Flask app, expose it with a ClusterIP service, and then verify it works from a test Pod. We'll use a simple HTTP endpoint that returns JSON.
Step 1: Create a Python app
Create a file named app.py:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({"message": "Hello from Python in Kubernetes!"})
@app.route("/health")
def health():
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Step 2: Containerize the app
Add a Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
And a requirements.txt:
flask==2.3.3
Build and push the image:
# Build the image (replace your-dockerhub with your Docker Hub username)
docker build -t your-dockerhub/my-python-api:1.0 .
docker push your-dockerhub/my-python-api:1.0
Note: If you're using Minikube or a local cluster, you can skip the push and load the image directly into the cluster with
minikube image load my-python-api:1.0.
Step 3: Deploy the app
Create deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-python-api
spec:
replicas: 2
selector:
matchLabels:
app: my-python-api
template:
metadata:
labels:
app: my-python-api
spec:
containers:
- name: api
image: your-dockerhub/my-python-api:1.0
ports:
- containerPort: 5000
Apply it:
kubectl apply -f deployment.yaml
Your app is now running with two replicas.
Step 4: Create the ClusterIP service
Create service.yaml:
apiVersion: v1
kind: Service
metadata:
name: my-python-api-svc
spec:
selector:
app: my-python-api
ports:
- port: 80
targetPort: 5000
type: ClusterIP
Here's the breakdown:
- selector matches Pods with app: my-python-api.
- port: 80 is the port that other services will use to reach the service.
- targetPort: 5000 is the port the container listens on.
- type: ClusterIP is the default, but we're being explicit.
Apply it:
kubectl apply -f service.yaml
Step 5: Verify the service
Get the service details:
kubectl get svc my-python-api-svc
Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-python-api-svc ClusterIP 10.96.123.45 <none> 80/TCP 10s
You'll see a cluster IP like 10.96.123.45 and no external IP. Now, to test the service from inside the cluster, run a temporary curl Pod:
kubectl run curlpod --image=curlimages/curl -it --rm -- sh
Inside that shell, hit the service:
curl http://my-python-api-svc:80/health
Expected output:
{"status":"ok"}
Type exit to leave the pod. That's it — your Python app is now exposed to other components inside the cluster under a stable DNS name.
Bonus: Check the Endpoints
kubectl get endpoints my-python-api-svc
You'll see both Pod IPs listed, proving the service is load balancing across replicas.
Compare options / when to choose what
You now have a solid grasp of ClusterIP, but Kubernetes offers three main Service types. Here's a quick comparison to help you choose correctly:
| Service Type | Accessibility | Use Case | Typical Python Example |
|---|---|---|---|
| ClusterIP | Only inside the cluster | Internal service-to-service communication | A Flask backend calling a Django or FastAPI microservice |
| NodePort | On each node's IP via a high port (e.g., 30000+) | Quick external access for development or debugging | Exposing your API on http://<node-ip>:30080 for a quick test |
| LoadBalancer | On a cloud load balancer with a public IP | Production internet-facing services that need a stable public IP | Serving a user-facing API behind a cloud LB (like AWS ELB) |
When to choose ClusterIP
- When your Python app is meant to be consumed only by other services in the cluster — e.g., a backend API called by a frontend service.
- When you want to load balance across multiple replicas without exposing any public IP.
- When you're inside a microservices architecture and want a clean, internal dependency graph.
Alternatives and variations
- Headless Service (setting
clusterIP: None) if you need the DNS to return Pod IPs directly — handy for stateful apps like a database cluster. - ExternalName Service to point to an external hostname — useful when you intentionally want to proxy to an outside service.
- Network Policies (not a Service type, but related) — if you want to restrict which Pods can even talk to your ClusterIP service, combine it with Kubernetes NetworkPolicies.
Pro tip: Don't use a NodePort or LoadBalancer for internal services — it's a security risk and violates least-privilege. Stick with ClusterIP unless you have a clear need.
Troubleshooting & edge cases
Even with such a simple concept, things can go wrong. Here are the most common issues and how to fix them:
1. Service has no endpoints
If kubectl get endpoints my-svc shows <none>, it means your selector doesn't match any Pods.
kubectl get svc my-svc
kubectl get pods --show-labels
Compare the labels of your Pods with the selector in your Service YAML. Fix the selector and re-apply.
2. curl times out from inside a Pod
This often happens because the target port is wrong. Check that targetPort matches the port your container actually listens on (e.g., 5000 for Flask). A common mistake is pointing targetPort to 80 when your Flask app runs on 5000.
3. DNS name doesn't resolve
If you can't reach http://my-svc:80, verify that CoreDNS is running:
kubectl get pods -n kube-system -l k8s-app=kube-dns
If it's not, check your cluster's DNS add-on (e.g., on Minikube, minikube addons enable coredns).
4. Your app is unreachable because you're using a NodePort already
If you see NodePort as the type and you can't access it, check the port range (default 30000-32767). You may also need to use the node's IP, not localhost.
5. Load balancing seems uneven
Kubernetes uses round-robin by default with kube-proxy in iptables mode. If you have sticky sessions enabled, you may see skewed distribution. For most cases, this is fine. If you need true load distribution metrics, consider istio or linkerd.
Pro tip: Always check
kubectl describe svc my-svcfor events — it will often tell you exactly what's wrong (e.g., "no endpoints").
What you learned & what's next
Awesome — let's recap what you've mastered in this lesson:
- You can now explain the core idea behind a ClusterIP service: it's a stable, virtual network endpoint that load balances and provides DNS for internal access to your Python app.
- You completed a practical exercise: you deployed a Flask app, exposed it with a ClusterIP service, verified endpoints, and tested it using
curlfrom a temporary pod. - You understand when to choose ClusterIP vs. NodePort vs. LoadBalancer, based on accessibility and use case.
- You've practiced troubleshooting common issues like mismatched selectors, wrong target ports, and DNS resolution problems.
Now that your app is internally accessible, the next logical step is to expose it to the outside world. You're ready to move on to learning about NodePort services — how to reach your Python app from outside the cluster using a node's IP and a high port, and why you'd only use that for quick testing. After that, you'll dive into Ingress controllers as the production-grade way to route both internal and external traffic to your services.
Keep building — every Kubernetes abstraction you master brings you closer to running Python microservices confidently in production.
Practice recap
Test your understanding by creating a second Python service (e.g., a FastAPI app) with two replicas and expose it with a ClusterIP service. Then, from a curl pod, call the /health endpoint of both services using their DNS names. Finally, deliberately break the selector and watch the endpoints disappear, then fix it — this reinforces the troubleshooting skills you just learned.
Common mistakes
- Typo in the selector: a single wrong label key/value silently means zero endpoints — always run
kubectl get endpointsto confirm. - Setting
targetPortto the serviceportinstead of the container's actual listening port (e.g., 5000 for Flask) causes connection refused. - Forgetting that ClusterIP is only reachable inside the cluster — trying to curl it from your laptop will time out; use
kubectl port-forwardor a NodePort for external access. - Not specifying
type: ClusterIPexplicitly and assuming a NodePort was created — the default is ClusterIP, which is often what you want, but be aware. - Scaling to zero replicas: a ClusterIP service with no backing Pods returns connection errors — keep at least one replica for availability.
Variations
- Use a headless service (
clusterIP: None) for stateful workloads where DNS should return Pod IPs directly — ideal for a Cassandra or Kafka cluster. - Use an ExternalName service to alias an external hostname within your cluster — handy for migrating off external dependencies later.
- Pair your ClusterIP service with a NetworkPolicy to restrict which Pods can talk to it, enforcing zero-trust networking inside the cluster.
Real-world use cases
- A Python FastAPI backend serves as an internal API for a Django frontend running in the same cluster — both talk over a ClusterIP service.
- A Python Celery worker needs to reach a Redis cache and a database — both exposed as ClusterIP services for stable internal endpoints.
- A machine learning service (e.g., a Flask model server) is called only by other Python microservices inside the cluster, never directly from outside.
Key takeaways
- ClusterIP is the default and most common Service type — it provides a stable virtual IP and DNS name for internal traffic.
- The service's
selectormust match the Pod labels exactly; otherwise no endpoints are created. - The
portfield is the service's listening port;targetPortmust match the container's actual application port. - Always test a ClusterIP service from inside the cluster using a temporary pod with
kubectl run. - Choose NodePort or LoadBalancer only when external access is needed; ClusterIP keeps your services secure and internal.
- Troubleshoot systematically: check endpoints, service description, and CoreDNS logs in that order.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.