Kubernetes Kubernetes

Services: Stable Networking for Ephemeral Pods

Give Pods a stable address, expose them inside and outside the cluster, and restrict who can talk to whom.

14 min Intermediate

Every Pod gets its own IP address, but Pods come and go, and their addresses go with them. A Service provides a stable address in front of a changing set of Pods. This guide builds on the web Deployment from the previous guide and covers Services, exposing them outside the cluster, Ingress, and network policies.

The problem: Pod IPs change

kubectl get pods -l app=web -o wide
kubectl delete pod POD_NAME
kubectl get pods -l app=web -o wide

The replacement Pod has a different IP, and it may be on a different node. Anything that had the old address hard-coded is now broken, and with replicas that come and go, tracking addresses by hand isn’t possible.

Create a ClusterIP Service

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80

The selector picks the Pods that back the Service, using the same labels you saw with Deployments. port is the port the Service listens on, and targetPort is the port on the Pods it forwards to, which should match the container’s port. Because no type is set, this is a ClusterIP Service: reachable only from inside the cluster, on a stable virtual IP.

kubectl apply -f service.yaml
kubectl get service web
kubectl get endpointslices -l kubernetes.io/service-name=web

The EndpointSlice lists the Pod IPs currently behind the Service. Scale the Deployment up or down and it updates automatically, while the Service’s own address never changes. (Older clusters and tutorials use kubectl get endpoints web for the same information. Recent versions deprecate that API in favor of EndpointSlices.)

Inside the cluster, the Service also gets a DNS name: web from the same namespace, or web.NAMESPACE.svc.cluster.local from anywhere. Test it from a throwaway Pod:

kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- wget -qO- http://web

You should see the nginx welcome page. On each node, kube-proxy keeps the rules that spread connections across the Service’s ready Pods. In the common iptables mode this is effectively random rather than strict round-robin, and Pods that fail their readiness check are taken out of rotation.

Expose a Service outside the cluster

There are two more Service types, and each builds on the one before:

  • NodePort also opens a port (30000–32767) on every node. Anything that can reach a node’s IP can reach the Service on that port.
  • LoadBalancer additionally asks the environment for an external load balancer pointing at those node ports. On a cloud provider that happens automatically. On bare metal you need something like MetalLB.
apiVersion: v1
kind: Service
metadata:
  name: web-public
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080

Leave out nodePort and Kubernetes picks a free one. On minikube with the Docker driver, the node’s IP often isn’t reachable directly from your laptop, so let minikube open it for you:

minikube service web-public --url

For a LoadBalancer Service on minikube, run minikube tunnel in another terminal.

Route HTTP with Ingress

A LoadBalancer per Service gets expensive and only handles raw traffic. An Ingress lets you route HTTP(S) traffic to many Services by host and path, through a single entry point, and terminate TLS there. An Ingress only describes the rules. It needs an ingress controller running in the cluster to act on them (on minikube, minikube addons enable ingress).

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  ingressClassName: nginx
  rules:
    - host: web.example.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

Requests for web.example.local now go to the web Service. Certificates for the host can be automated with a tool like cert-manager.

Restrict traffic with NetworkPolicy

By default every Pod can talk to every other Pod. A NetworkPolicy selects Pods and lists what traffic is allowed. Once a Pod is selected for ingress by any policy, everything not explicitly allowed is denied. Start by denying all incoming traffic in a namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
    - Ingress

The empty podSelector selects every Pod in the namespace. Then allow only what you need, for instance traffic to the web Pods from Pods labeled role: client:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-client-to-web
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: client
      ports:
        - protocol: TCP
          port: 80

Network policies are namespaced, and they are only enforced if the cluster’s network plugin supports them, for example Calico or Cilium. Some minimal local setups accept the objects but silently ignore them, so test that a denied connection really is blocked.

Popular Guides