← Back to the section

Networking in Kubernetes often feels like magic: the service exists but doesn't respond; the address resolves but the connection hangs; after a deploy the neighboring team starts catching 503s. All of this comes down to four mechanisms: Service, DNS, Ingress, NetworkPolicy. Let's go through them one by one.

Why you can't use a pod's IP directly

In Kubernetes every pod has its own IP address, and all pods see each other on one flat network — no NAT. It seems like you could just talk over the IP and be done with it.

The problem: pods are short-lived. A restart, an update, a node failure — and the pod comes back up with a new IP. Hardcoding the address in a config means breaking the system on the very first recreation.

That is exactly what Service exists for: an abstraction that gives a stable network name to a shifting set of pods.

Service types: what to pick and when

A Service is described in YAML and specifies which pods it groups (via a selector on labels) and how they are reachable from the outside.

apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: payments
spec:
  selector:
    app: order          # groups all pods with the label app=order
  ports:
    - port: 80
      targetPort: 8080

There are four types:

ClusterIP — the default type. Gives a virtual IP reachable only inside the cluster. For connecting one microservice to another — this is the one, and almost always the only one.

NodePort — opens a port on every node in the cluster (30000–32767). A technical primitive; it isn't used directly in production, but the LoadBalancer type is built on top of it.

LoadBalancer — provisions an external balancer from the cloud provider and wires it to the service. It's expensive to attach one to every service: usually a single LoadBalancer sits in front of the Ingress controller, and services are published through Ingress (more on this below).

Headless (clusterIP: None) — no virtual IP. The DNS name resolves straight into the list of IPs of the concrete pods. Needed when the client cares about each instance individually: database clusters, Kafka clients, StatefulSets.

DNS: how services find each other by name

Every Service automatically gets a DNS name:

service-name.namespace.svc.cluster.local

Within the same namespace the short name is enough:

http://order-service

From another namespace you need the namespace prefix:

http://order-service.payments

This is the correct way to reach a neighboring service — never an IP, always the DNS name. And always paired with timeouts: a DNS name doesn't make the neighbor reliable.

app:
  clients:
    order:
      base-url: http://order-service.payments
      connect-timeout: 1s
      read-timeout: 3s

Endpoints: where networking meets readiness

Behind every Service Kubernetes maintains a list of endpoints — the real pod IPs that passed two checks: a match on the selector labels and a successful readiness probe.

This is the key link:

  • A pod started but is still warming up → readiness fails → it's not in endpoints → no traffic goes to it.
  • A pod is overloaded, readiness failed → it drops out of endpoints until it recovers.
  • During a rolling update, old pods are removed from endpoints before they stop — which is why a proper graceful shutdown doesn't lose requests.

The first thing to check when "the service exists but doesn't respond":

kubectl -n payments get endpoints order-service

An empty list while the pods are alive means: all replicas are failing the readiness probe. That's now a concrete question about probes, not "something's wrong with the network".

Ingress: one entry point for all services

Exposing every service to the outside via LoadBalancer is wasteful: a separate balancer and a separate IP for each. The solution — one LoadBalancer in front of the Ingress controller, with routing described in Ingress resources.

Ingress is a declaration of "which host/path routes where":

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api
spec:
  tls:
    - hosts: [api.example.com]
      secretName: api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port: { number: 80 }
          - path: /payments
            pathType: Prefix
            backend:
              service:
                name: payment-service
                port: { number: 80 }

TLS is terminated at the Ingress level; inside the cluster traffic flows as HTTP (or over mTLS if you have a service mesh).

The Ingress controller is a separate component you need to install (nginx, Traefik, cloud variants). The Ingress resource by itself, without a controller, does nothing.

Gateway API — the successor to Ingress

Gateway API is a newer standard for the same task. The main differences:

  • a clear separation of roles: the platform manages the Gateway (the entry point), teams manage their own HTTPRoute;
  • support for TCP, gRPC, and WebSocket out of the box;
  • no zoo of vendor annotations that Ingress runs into for complex scenarios.

New platforms build on Gateway API; existing Ingress resources keep working.

NetworkPolicy: who is allowed to talk to whom

By default in Kubernetes any pod can reach any other. For an environment with payment services this is unacceptable.

NetworkPolicy is a declarative firewall at the pod level. It describes which traffic is allowed, and everything else is denied:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: limits-ingress
  namespace: risk
spec:
  podSelector:
    matchLabels: { app: limits }
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { team: payments }
      ports:
        - port: 8080

This manifest says: pods app=limits in the namespace risk accept incoming traffic only from a namespace labeled team=payments, and only on port 8080.

A good practice is deny-by-default per namespace, then explicit allows by consumer. If you can't reach a neighboring service — kubectl describe networkpolicy gets checked before you write to the platform team.

Service mesh: when you need more

A service mesh (Istio, Linkerd) is an extra layer: a proxy container runs next to each pod and intercepts all traffic. This lets you get, without changing code:

  • mTLS between all services automatically;
  • retries and timeouts at the network level;
  • canary traffic distribution by weight;
  • unified telemetry for all calls.

A mesh is justified with dozens of services and a regular practice of canary rollouts. With a small number of services it adds noticeable complexity to debugging — every request gains one more proxy.

An important rule when using a mesh: retries are configured in one place — either in the mesh or in the application, not both. A retry in the application plus a retry in the mesh is an avalanche of repeats when a neighboring service degrades.

Common mistakes

MistakeWhat happensThe right way
Talking to a pod's IPBreaks on the first recreationThe Service DNS name
LoadBalancer per serviceExtra cost, a zoo of entry pointsOne LB in front of the Ingress controller
NodePort in productionNon-standard ports, bypassing TLSIngress / Gateway API
Hardcoded FQDN with cluster.local in codeBreaks when you change clustersShort name + namespace in config
A client with no timeouts, "it's right next door"A stuck neighbor hangs the callerA timeout on every client
Retries in both the mesh and the appAn avalanche of requests on degradationOne place for the retry policy

In short

  • Every pod has its own IP, but pods change IPs — so you need a Service as a stable name.
  • ClusterIP — for connecting services inside the cluster (the main type). LoadBalancer — for entry from the outside, better one in front of Ingress. Headless — when the client cares about each instance individually.
  • The service DNS name: name.namespace.svc.cluster.local. Within a namespace — just name.
  • Endpoints are the real pod IPs behind a Service. A pod lands there only if it passed the readiness probe. Empty endpoints with live pods — look at the probes.
  • Ingress routes external traffic by host/path to the right Service. Gateway API is a newer standard with a separation of roles.
  • NetworkPolicy is a firewall between pods. By default everything is open; deny-by-default + explicit allows is a good practice.
  • A service mesh moves retries, timeouts, and mTLS into the infrastructure. Retries — either in the mesh or in the application, not both.
  • K8s Fundamentals — pod, Deployment, Service in the overall cluster model.
  • Spring Boot in Kubernetes — the readiness probe that drives endpoints.
  • Operations and debugging — diagnosing the Ingress → Service → endpoints → pod chain.
  • Resilience patterns — timeouts and retries that a DNS name doesn't replace.