Networking
Services, Ingress, DNS, and NetworkPolicies - how pods talk to each other and the outside world
Networking
Pods come and go. Their IPs change. Networking in Kubernetes is the layer that gives you stable endpoints despite that churn.
The Model
Three rules to internalize:
- Every pod gets its own cluster-routable IP. Containers in the same pod share
localhost. - Pods can reach any other pod by IP — no NAT. (The network plugin / CNI makes this true.)
- Services are stable virtual IPs in front of pods. Clients talk to the Service, not the pods.
Services
A Service is a stable name + IP that load-balances across a set of pods selected by labels.
ClusterIP — internal (default)
apiVersion: v1
kind: Service
metadata:
name: api-server
spec:
type: ClusterIP # the default
selector:
app: api-server # picks pods with this label
ports:
- port: 80 # the Service's port
targetPort: 3000 # the pod's port
protocol: TCPOther pods reach it as api-server (same namespace) or api-server.production.svc.cluster.local (anywhere).
NodePort — exposes on every node's port
spec:
type: NodePort
ports:
- port: 80
targetPort: 3000
nodePort: 30080 # 30000-32767 rangeReachable at http://<any-node-ip>:30080. Fine for dev; ugly for production.
LoadBalancer — provisions a real LB
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 3000On a managed cloud, this provisions a cloud load balancer (AWS ALB/NLB, GCP LB, etc.) and gives you a public IP/hostname. One LB per Service — expensive at scale. Use Ingress instead for HTTP.
Service Types Compared
| Type | Reachable from | Use case |
|---|---|---|
ClusterIP | Other pods | Service-to-service |
NodePort | Anyone who can reach a node | Dev clusters, niche routing |
LoadBalancer | The internet | One-off external services |
ExternalName | Other pods (CNAME) | Aliases for external DNS |
DNS
The cluster runs CoreDNS automatically. Names resolve as:
| Name | Resolves to |
|---|---|
api-server | The Service in the same namespace |
api-server.production | The Service in namespace production |
api-server.production.svc.cluster.local | Fully qualified — works anywhere |
postgres-0.postgres.production.svc.cluster.local | Specific pod in a StatefulSet (headless Service) |
/etc/resolv.conf inside a pod is auto-configured so short names work.
Ingress
Putting one cloud LB in front of every service is silly. Ingress is a single entry point that does HTTP host/path routing to many Services.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
spec:
ingressClassName: nginx
tls:
- hosts: [api.example.com]
secretName: api-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-server
port: { number: 80 }
- path: /admin
pathType: Prefix
backend:
service:
name: admin-portal
port: { number: 80 }Ingress doesn't run itself — you install an Ingress controller (the actual reverse proxy) in the cluster:
| Controller | Notes |
|---|---|
| ingress-nginx | Most popular; nginx under the hood |
| Traefik | Auto-discovery, dynamic config |
| AWS Load Balancer Controller | Provisions AWS ALBs from Ingress |
| Gateway API | The successor to Ingress; richer model |
Gateway API is the modern replacement for Ingress (GA in 2023). It separates the "infra owner" view (GatewayClass, Gateway) from the "app owner" view (HTTPRoute) and handles TCP/UDP, not just HTTP. New projects should consider Gateway API; existing Ingress works fine and isn't going away.
NetworkPolicies
By default, every pod can talk to every other pod. NetworkPolicies add a firewall on top:
# Default-deny ingress for the production namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # all pods in the namespace
policyTypes: [Ingress]Then allow what you need:
# Only the API can talk to the database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-api
namespace: production
spec:
podSelector:
matchLabels: { app: postgres }
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: { app: api-server }
ports:
- { protocol: TCP, port: 5432 }# API can call external HTTPS but nothing else
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-egress-https
namespace: production
spec:
podSelector:
matchLabels: { app: api-server }
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system } # DNS
ports:
- { protocol: UDP, port: 53 }
- to:
- ipBlock: { cidr: 0.0.0.0/0 }
ports:
- { protocol: TCP, port: 443 }NetworkPolicies only work if your CNI plugin enforces them. Most do (Calico, Cilium, Weave); some (like flat-config Flannel) silently ignore them. Verify with kubectl describe networkpolicy and an actual connectivity test.
What's Next
Pods can find each other and reach the world. Next, give them their config and storage → Config & Storage.