Networking¶
Scope: how traffic actually reaches a Pod. The core-concepts page introduces kube-proxy as a node component; this page goes one layer deeper on the objects that sit in front of Pods — Service, Ingress, cluster DNS, and NetworkPolicy — and the path a request takes through all of them.
TL;DR — in 30 seconds:
- A Service gives a stable virtual IP + DNS name in front of a set of Pods (via label selector) so callers never chase individual Pod IPs that change every rollout.
- NodePort and LoadBalancer both build on ClusterIP rather than replacing it; Ingress is one entry point routing many Services by hostname/path through an Ingress controller — no controller running, no routing.
- By default every Pod can reach every other Pod; a NetworkPolicy is additive-deny — the moment any policy selects a Pod, only explicitly allowed traffic gets through.
1. The problem Services solve¶
Ship a rolling update and every replaced Pod comes back with a different IP — anything that cached the old address just started failing silently. Pods are disposable by design: a Deployment replaces a dead Pod with a new one, and nothing that depends on a Pod's IP directly can survive that churn. A Service gives a stable virtual IP and DNS name in front of a set of Pods (chosen by a label selector), and load-balances traffic across whichever Pods currently match — so callers never need to track individual Pod IPs.
2. Service types¶
| Type | Reachable from | Typical use |
|---|---|---|
| ClusterIP (default) | inside the cluster only | one Pod talking to another (the common case) |
| NodePort | any node's IP, on a fixed high port | quick external access without a cloud load balancer |
| LoadBalancer | outside the cluster, via a cloud LB | internet-facing traffic (provisions a Classic ELB or NLB on EKS — L4, no host/path routing) |
Headless (clusterIP: None) |
DNS returns each backing Pod's IP directly, no virtual IP | StatefulSet members that need to address each other individually |
NodePort and LoadBalancer both build on ClusterIP — each adds a layer of reachability rather than
replacing the one below it:
flowchart LR
lb["LoadBalancer<br/>(cloud LB, external)"] --> np["NodePort<br/>(any node, fixed port)"]
np --> ci["ClusterIP<br/>(virtual IP, internal)"]
ci --> p1["Pod"]
ci --> p2["Pod"]
A headless Service is the exception: it skips the virtual IP entirely so a client's DNS lookup resolves straight to the Pods' own IPs — what a StatefulSet needs when a caller must reach one specific replica rather than "any healthy one."
3. Ingress — one entry point for many Services¶
A LoadBalancer Service is exactly one external IP per Service — fine for one app, expensive and
unwieldy for dozens. Ingress is a single object that routes HTTP(S) by hostname and path to many
Services behind one entry point, so you provision one external load balancer instead of one per app.
Ingress is only a set of routing rules — an Ingress controller (a Pod running in the cluster, e.g.
NGINX or the AWS Load Balancer Controller) is what actually watches those rules and does the routing.
No controller running means an Ingress object does nothing. This is also where the L7 load balancer
lives: on EKS, the AWS Load Balancer Controller provisions an ALB for an Ingress (host/path routing)
— never for a plain LoadBalancer Service, which only ever gets the L4 ELB/NLB from the table above.
flowchart LR
client["Client"] --> ing["Ingress<br/>host/path rules"]
ing --> ctrl["Ingress controller<br/>(watches + enforces the rules)"]
ctrl --> svcA["Service A<br/>(ClusterIP)"]
ctrl --> svcB["Service B<br/>(ClusterIP)"]
svcA --> podsA["Pods"]
svcB --> podsB["Pods"]
4. Cluster DNS¶
Every Service gets a stable DNS name, resolved by the cluster's internal DNS (CoreDNS), of the form
<service>.<namespace>.svc.cluster.local — usually just <service> is enough from within the same
namespace. This is why Pods can hard-code a Service name instead of chasing IPs: the name resolves
correctly no matter how many times the backing Pods are replaced. The naming scheme is part of the
Kubernetes DNS spec, so it
holds across clusters and cloud providers, not just this one.
5. NetworkPolicy — controlling who can talk to whom¶
By default, every Pod can reach every other Pod in the cluster — there is no isolation until you add some. A NetworkPolicy is a firewall rule scoped to Pods (selected by labels, the same mechanism a Service uses): it allows or restricts traffic to and/or from matching Pods, by namespace, label selector, or IP block.
Note. A NetworkPolicy is additive-deny: the moment any policy selects a Pod, that Pod's traffic is restricted to only what the policies allow — everything not explicitly permitted is dropped. A Pod matched by zero policies stays fully open. This is documented behavior, not an implementation detail — see the NetworkPolicy reference.
6. kube-proxy, revisited¶
The core-concepts page introduced kube-proxy as the node agent that programs Service networking. Now that you know what a Service is: kube-proxy watches the API server for Services and their matching Pods, then writes node-local rules (iptables or IPVS) so a packet sent to a Service's virtual IP gets load-balanced to one of the current backing Pods — entirely in the kernel's networking path, no proxy process in the request's way despite the name.
7. How this connects¶
- Ingress and Service selection both key off the same label selector mechanism used by ReplicaSets — the object model is consistent top to bottom.
- EKS (module 05, AWS) is where
LoadBalancerServices and Ingress controllers actually provision real cloud load balancers. - Debugging "can't reach my Service" extends the core-concepts
get→describe→logs→eventsloop: also check the Service'sEndpoints(does it have any matching, Ready Pods?) and, if traffic should be restricted, whether a NetworkPolicy is blocking it.
Common gotchas
- Creating an Ingress object and expecting it to route traffic with nothing else installed. Fix: Ingress is only routing rules — an Ingress controller (a Pod running in the cluster) has to be installed to actually watch and enforce them; no controller means the Ingress object does nothing.
- Assuming a NetworkPolicy is an allow-list, cluster-wide, from the start. Fix: a Pod matched by zero NetworkPolicies stays fully open — isolation only starts the moment at least one policy selects that Pod, and from then on it's additive-deny (only explicitly allowed traffic gets through).
- Hard-coding a Pod IP instead of the Service's DNS name. Fix: Pod IPs change on every replace; a
Service's
<service>.<namespace>.svc.cluster.localname stays stable no matter how many times the backing Pods are replaced. - Debugging "can't reach my Service" by only checking the Service object. Fix: also check its
Endpoints — a Service with no matching, Ready Pods behind it looks fine on
kubectl get svcbut has nowhere to send traffic; and check whether a NetworkPolicy is blocking the path.
Check yourself — a NetworkPolicy allows traffic to Pod A from Pod B only. Can Pod C, which the policy never mentions, still reach Pod A?
No, once that policy selects Pod A. NetworkPolicy is additive-deny: the moment any policy selects a Pod, only what's explicitly allowed gets through, and everything else is dropped. A Pod matched by zero policies stays fully open, but as soon as one policy selects Pod A, Pod C needs its own explicit allow rule too.
Check yourself — an Ingress object's rules look correct, but requests still don't reach the app. What's the first thing to check?
Whether an Ingress controller is actually running in the cluster. Ingress is purely a set of routing rules — nothing routes traffic until a controller (NGINX, the AWS Load Balancer Controller, etc.) is watching and enforcing them.
You can defend this when you can say which Service type fits a given reachability requirement, trace a request from client through Ingress, Service, to Pod, explain why cluster DNS names outlive individual Pod IPs, and describe what a NetworkPolicy does when it selects a Pod.