Kubernetes networking is the #1 thing that confuses beginners. You deploy your app, but how do users actually reach it? You hear terms like "ClusterIP," "NodePort," "LoadBalancer," "Ingress," and "ALB Controller" — and they all seem to do similar things. This guide explains each one using simple analogies, shows you when to use what, and gives you copy-paste YAML for every scenario.
The Big Picture: How Traffic Reaches Your App
Think of Kubernetes like a large office building. Your application pods are employees working in rooms. The question is: how does someone from outside the building find and talk to the right employee?
Kubernetes Services: The Foundation
A Service is the most fundamental networking concept in Kubernetes. Pods are temporary — they get created, destroyed, and rescheduled constantly. A Service gives you a stable address that always points to the right pods, no matter how many there are or where they're running.
ClusterIP: Internal Communication Only
Analogy: An internal phone extension. Only people inside the building can call it. Outsiders can't.
Use when: Service A needs to talk to Service B inside the cluster. Your API calling your database. Your backend calling a cache service.
# ClusterIP Service — the DEFAULT type
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
type: ClusterIP # This is the default, you can omit this line
selector:
app: backend # Find pods with label app=backend
ports:
- port: 8080 # Port the service listens on
targetPort: 8080 # Port the pod listens on
protocol: TCP
# Now any pod in the cluster can reach the backend at:
# http://backend-service:8080 (same namespace)
# http://backend-service.default:8080 (from another namespace)
# http://backend-service.default.svc.cluster.local:8080 (fully qualified)
# Kubernetes DNS automatically creates these names!
# You can also use it for your database:
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
# Now your app connects to: postgres://postgres:5432/mydb
# No IP addresses needed — just the service name!
NodePort: Quick External Access (Development Only)
Analogy: Punching a hole in the building wall. Anyone who knows the building's address and the hole number can reach in directly.
Use when: You need quick external access for testing/development. Never in production — it's insecure and limited.
# NodePort Service — opens a port on EVERY node
apiVersion: v1
kind: Service
metadata:
name: my-app-nodeport
spec:
type: NodePort
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # Port opened on every node (range: 30000-32767)
# Access your app at:
# http://<ANY-NODE-IP>:30080
# e.g., http://10.0.1.5:30080 or http://10.0.1.6:30080
# Problems with NodePort:
# 1. Ugly ports (30000-32767 range only)
# 2. Users need to know a node IP
# 3. If a node dies, that IP stops working
# 4. No SSL termination
# 5. No path-based routing
# Verdict: Fine for dev/testing, never for production
LoadBalancer: Cloud-Native External Access
Analogy: Hiring a professional doorman who stands at the main entrance. They have a public address that never changes, and they route visitors to the right place.
Use when: You need to expose ONE service to the internet with a stable public IP. Works on AWS (NLB/CLB), GCP, Azure.
# LoadBalancer Service — creates a cloud load balancer
apiVersion: v1
kind: Service
metadata:
name: my-app-lb
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
# On AWS, this creates a Classic Load Balancer (CLB) automatically
# kubectl get svc my-app-lb
# NAME TYPE EXTERNAL-IP PORT(S)
# my-app-lb LoadBalancer a1b2c3d4.us-east-1.elb.amazonaws.com 80:31234/TCP
# Point your DNS to the EXTERNAL-IP and you're live!
# Problem: Each LoadBalancer service creates a NEW cloud LB
# 10 services = 10 load balancers = 10x the cost!
# That's why we use Ingress...
Ingress: The Smart Router (Production Standard)
Analogy: A receptionist at the front desk. One entrance (one load balancer), but the receptionist reads the visitor's request and routes them to the right department:
- "I'm here for the API" → Route to API service
- "I'm here for the website" → Route to frontend service
- "I'm here for the admin panel" → Route to admin service
# Step 1: Install an Ingress Controller (runs once per cluster)
# The controller IS the actual reverse proxy (nginx, traefik, etc.)
# Option A: nginx Ingress Controller
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace
# Option B: AWS ALB Controller (see next section)
# Step 2: Create Ingress rules
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt # Auto TLS certs!
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
- app.example.com
secretName: my-tls-cert
rules:
# Route by hostname
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
# Route by path (same hostname, different paths)
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /admin
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
AWS ALB Controller: The AWS-Native Ingress
The AWS Load Balancer Controller (formerly ALB Ingress Controller) creates AWS Application Load Balancers directly from your Ingress resources. Instead of running nginx inside the cluster, it uses AWS-managed ALBs — which means AWS handles scaling, health checks, and SSL termination for you.
# Install AWS Load Balancer Controller on EKS
# Prerequisites: EKS cluster with IRSA (IAM Roles for Service Accounts)
# 1. Create IAM policy
curl -o iam-policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://iam-policy.json
# 2. Create service account with IAM role
eksctl create iamserviceaccount \
--cluster=my-cluster \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::ACCOUNT:policy/AWSLoadBalancerControllerIAMPolicy \
--approve
# 3. Install via Helm
helm install aws-load-balancer-controller \
eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=my-cluster \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller
# 4. Create Ingress with ALB annotations
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-alb-ingress
annotations:
# Tell K8s to use the ALB controller (not nginx)
kubernetes.io/ingress.class: alb
# Internet-facing (vs internal for private APIs)
alb.ingress.kubernetes.io/scheme: internet-facing
# Route directly to pod IPs (faster than NodePort)
alb.ingress.kubernetes.io/target-type: ip
# SSL: use ACM certificate
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:ACCOUNT:certificate/xxx
# Redirect HTTP to HTTPS
alb.ingress.kubernetes.io/ssl-redirect: "443"
# Health check path
alb.ingress.kubernetes.io/healthcheck-path: /health
# Enable WAF (Web Application Firewall)
# alb.ingress.kubernetes.io/waf-acl-id: your-waf-id
# Access logs to S3
# alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket=my-logs
spec:
ingressClassName: alb
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
nginx Ingress vs ALB Controller: When to Use Which
Network Policies: Firewall Rules for Pods
By default, every pod can talk to every other pod in the cluster. That's dangerous. Network Policies are Kubernetes's firewall — they control which pods can communicate with which.
# Default deny all ingress (lock down first, then whitelist)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: production
spec:
podSelector: {} # Apply to ALL pods in this namespace
policyTypes:
- Ingress
# No ingress rules = deny everything!
---
# Allow frontend to talk to backend (and nothing else)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend # Apply to backend pods
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # Only frontend pods can connect
ports:
- port: 8080
protocol: TCP
---
# Allow backend to talk to database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: postgres
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- port: 5432
The Complete Decision Guide
| I Want To... | Use This | Why |
|---|---|---|
| Connect microservices internally | ClusterIP Service | Stable DNS name, internal only, free |
| Quick test access from my laptop | NodePort or kubectl port-forward | Fast, no cloud resources needed |
| Expose ONE service to internet | LoadBalancer Service | Gets a public IP, simple setup |
| Expose MANY services on one IP | Ingress (nginx or ALB) | Host/path routing, SSL, one LB cost |
| Use AWS WAF, Cognito, ACM | AWS ALB Controller | Deep AWS integration |
| Run on any cloud or bare metal | nginx Ingress Controller | Cloud-agnostic, full control |
| Restrict which pods can communicate | NetworkPolicy | Pod-level firewall rules |
| gRPC, mTLS, traffic splitting | Service Mesh (Istio/Linkerd) | Advanced L7 features, observability |
| Expose a TCP/UDP service (not HTTP) | LoadBalancer (NLB on AWS) | Ingress is HTTP-only, NLB handles TCP/UDP |
Common Beginner Mistakes
- "My pod has an IP, why do I need a Service?" — Pod IPs change every time a pod restarts or moves to another node. Services give you a stable address. Never hardcode pod IPs.
- "I created a LoadBalancer for every service" — Each LoadBalancer creates a new cloud LB ($). Use one Ingress to route to many services behind a single LB.
- "I'm using NodePort in production" — NodePort exposes a random high port on every node. No SSL, no path routing, ugly URLs. Use Ingress instead.
- "My Ingress isn't working" — Most likely you forgot to install an Ingress Controller. Ingress resources are just rules — you need a controller (nginx, traefik, ALB) to actually execute them.
- "I can't connect from one namespace to another" — Use the full DNS name:
service-name.namespace.svc.cluster.local. Or check if a NetworkPolicy is blocking it.
Debugging Kubernetes Networking
# 1. Is my pod running and healthy?
kubectl get pods -o wide
kubectl logs my-pod
kubectl describe pod my-pod
# 2. Does my Service have endpoints?
kubectl get endpoints my-service
# If ENDPOINTS is empty: your selector labels don't match any pods!
# 3. Can I reach the service from inside the cluster?
kubectl run debug --image=nicolaka/netshoot -it --rm -- bash
curl http://my-service:8080/health # By service name
nslookup my-service # DNS resolution
# 4. Is the Ingress controller running?
kubectl get pods -n ingress-nginx
kubectl get ingress # Check ADDRESS column
# 5. What does the ALB look like?
kubectl describe ingress my-ingress
# Look for Events: "Successfully reconciled" = ALB created
# Check the ADDRESS field for the ALB URL
# 6. Network Policy blocking traffic?
kubectl get networkpolicy -A
kubectl describe networkpolicy deny-all
Kubernetes networking follows a simple progression: ClusterIP for internal communication, Ingress for external HTTP traffic (with nginx or ALB Controller), LoadBalancer for non-HTTP services, and NetworkPolicy for security. Start with ClusterIP + Ingress — that covers 90% of use cases. Add complexity only when you need it.