Chapter 11 of 47 · Networking, part 1

Service: one stable address for many Pods

Deployments keep Pods alive, but every replacement Pod has a new IP. A Service gives the group one name and one IP that never change, and spreads requests across whichever Pods are ready right now.

The problem

Your frontend needs to call posts. Which of the three Pods? And what happens when that one is replaced? Send requests, crash a Pod, scale up, and keep sending:

The client only ever uses posts-service. It never learns a Pod IP, so Pods can come and go freely.

How a Service finds Pods

Exactly like a ReplicaSet: with a label selector. The Service keeps a live list of the IPs of every ready Pod with matching labels, stored in an object called an EndpointSlice.

A Service selects Pods by labelposts-service with selector app=posts sends traffic to the three Pods labelled app=posts, but not to the Pod labelled app=users. posts-service · 10.96.40.12selector: app=posts Pod 10.244.1.5app=posts Pod 10.244.2.9app=posts Pod 10.244.1.7app=posts Pod 10.244.2.3app=users ✗ Endpoints: 10.244.1.5:4000, 10.244.2.9:4000, 10.244.1.7:4000
The Service does not know about Deployments at all. It only matches labels.
The #1 Service bug: the selector does not match the Pod labels (a typo, or app: post vs app: posts). The Service exists, has an IP, and sends traffic nowhere. Check with kubectl get endpointslices -l kubernetes.io/service-name=posts-service; empty means no match.

The YAML and its three ports

apiVersion: v1
kind: Service
metadata:
  name: posts-service
spec:
  type: ClusterIP
  selector:
    app: posts
  ports:
    - port: 80
      targetPort: 4000
      protocol: TCP

selector

Which Pods get the traffic: every ready Pod labelled app=posts. Must match the labels in your Deployment's Pod template.

client ──► posts-service:80 ──► Pod IP:4000 (targetPort) ──► app.listen(4000)

Calling it by name

Kubernetes runs a DNS server (CoreDNS). Every Service gets a name that any Pod can use:

FromUse
A Pod in the same namespacehttp://posts-service
A Pod in another namespacehttp://posts-service.default
Fully written outhttp://posts-service.default.svc.cluster.local

So your comments service calls posts like this, with no IP anywhere:

// comments service
const res = await fetch('http://posts-service/posts');

Service types

Each type builds on the previous one, adding a way in from further away.

ClusterIP: reachable only inside the clusterclusterInternet ✗comments Podposts-service10.96.40.12:80posts Pods

The default. A virtual IP inside the cluster only. Use it for service-to-service calls: posts, users, databases. Most of your Services will be this.

spec:
  type: ClusterIP
  selector: { app: posts }
  ports: [{ port: 80, targetPort: 4000 }]

The kubectl commands

  1. $ kubectl apply -f service.yaml
    service/posts-service created
    $ kubectl get svc
    NAME            TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)
    kubernetes      ClusterIP   10.96.0.1     <none>        443/TCP
    posts-service   ClusterIP   10.96.40.12   <none>        80/TCP

    The Service has its own stable IP. (kubernetes is the API server's own Service; ignore it.)

  2. $ kubectl get endpointslices -l kubernetes.io/service-name=posts-service
    NAME                  ADDRESSTYPE   PORTS   ENDPOINTS
    posts-service-x7k2d   IPv4          4000    10.244.1.5,10.244.2.9,10.244.1.7

    The Pods it currently sends to. Crash one and run this again: the list updates.

  3. $ kubectl run tmp --rm -it --image=busybox:1.36 -- sh
    / # wget -qO- http://posts-service/posts
    [{"id":1,"title":"Hello Kubernetes"}]
    / # exit

    Test from inside the cluster with a throwaway Pod. This is exactly what your other services do.

  4. $ kubectl port-forward service/posts-service 8080:80
    Forwarding from 127.0.0.1:8080 -> 4000
    # in another terminal:
    $ curl http://localhost:8080/posts

    Reach a ClusterIP Service from your laptop while developing, without changing its type.

  5. $ kubectl expose deployment posts --name=posts-service --port=80 --target-port=4000
    service/posts-service exposed

    The imperative shortcut: creates the same Service, copying the selector from the Deployment.

  1. spec:
      selector:
        app: post        # typo: Pods are labelled app=posts

    A one-letter mistake.

  2. $ kubectl get svc posts-service
    NAME            TYPE        CLUSTER-IP    PORT(S)
    posts-service   ClusterIP   10.96.40.12   80/TCP

    Looks perfectly healthy…

  3. / # wget -qO- http://posts-service/posts
    wget: can't connect to remote host (10.96.40.12): Connection refused

    …but nothing answers.

  4. $ kubectl get endpointslices -l kubernetes.io/service-name=posts-service
    NAME                  ADDRESSTYPE   PORTS     ENDPOINTS
    posts-service-x7k2d   IPv4          <unset>   <unset>
    $ kubectl get pods --show-labels
    NAME                     LABELS
    posts-7d9f8c6b5d-4kq2p   app=posts,pod-template-hash=7d9f8c6b5d

    No endpoints. Compare the selector with the Pod labels, fix app: posts, apply, and the endpoints appear.

Recap

Quick check

Your app listens on 4000. The Service has port: 80 and no targetPort. Does http://posts-service work?
Show answer

No. targetPort defaults to port, so traffic goes to Pod port 80, where nothing listens. Set targetPort: 4000.

Services: common questions

What is the difference between port and targetPort?

port is the port the Service listens on. targetPort is the port on the Pods it forwards to, where your app listens. targetPort defaults to port.