Services and EndpointSlices

A Kubernetes Pod IP is temporary. A rollout, node drain, crash, or scale event can replace Pods with new addresses, so clients need a stable name and load-balancing point that follows those changes. A Service provides that stable virtual front door. EndpointSlices are the discovery records behind it: they list the current network endpoints, their ports, address family, and readiness conditions in chunks that scale better than the older single Endpoints object.

In this Kubernetes and Helm course, this lesson sits at the point where charted workloads begin to communicate with each other. By the end, you should be able to design a Service selector, predict which Pods become backends, inspect the EndpointSlices Kubernetes creates, template a Service in Helm without breaking traffic during upgrades, and debug the common case where DNS resolves but connections fail.

Purpose and Outcome

A Service decouples clients from individual Pods. Inside the cluster, clients usually call http://service-name.namespace.svc or the shorter name when they are in the same namespace. Kubernetes DNS resolves that name to a stable Service address for normal ClusterIP Services. The data plane then forwards traffic to one of the ready endpoints published for that Service.

The outcome is not simply name resolution. The Service defines a contract: selected backends, exposed ports, protocol, optional external access, and traffic policy. EndpointSlices make the contract live by translating current Pod state into endpoint records. If a Pod is not ready, terminating, or missing a matching label, it should not be treated like a healthy backend for ordinary traffic.

How Services Work Internally

The Service object is stored in the Kubernetes API. For a selector-based Service, the EndpointSlice controller watches Services and Pods. When it sees a Service with a selector, it finds Pods in the same namespace whose labels match every selector key and value. It then writes one or more EndpointSlice objects labeled with kubernetes.io/service-name. Each slice contains endpoint addresses, readiness and termination conditions, topology hints where used, and a list of ports.

EndpointSlices are separate resources because one huge endpoints list becomes expensive in large clusters. Slices let Kubernetes update only the changed chunk instead of rewriting a single large object whenever one Pod changes. Consumers such as kube-proxy, cloud controllers, service meshes, and custom controllers can watch EndpointSlices to build routing tables or configuration.

kube-proxy watches Services and EndpointSlices on each node. Depending on the proxy mode and platform, it programs packet forwarding rules so traffic sent to a Service virtual IP and port is distributed across endpoint IPs and target ports. The Service IP is virtual; it is not normally bound to a process that accepts connections. It is a stable address interpreted by node networking rules.

DNS is another layer. CoreDNS watches Services and creates records. A normal ClusterIP Service gets an A or AAAA record for the Service IP. A headless Service, declared with clusterIP: None, returns endpoint addresses directly. That makes headless Services useful for clients that need to see individual peers, such as some databases, but it also moves balancing and retry behavior into the client.

Configuration Anatomy

The essential Service fields are small but precise. metadata.name becomes the DNS label. spec.type chooses the exposure model: ClusterIP for internal traffic, NodePort for a port on every node, LoadBalancer for cloud or platform load balancer integration, and ExternalName for DNS aliasing. spec.selector identifies backend Pods for selector-managed Services. spec.ports maps a stable Service port to a backend targetPort, which may be a number or a named container port.

EndpointSlice fields describe discovered backends. addressType is commonly IPv4 or IPv6. ports names the endpoint port and protocol. endpoints.addresses contains Pod or manually supplied IP addresses. conditions.ready says whether ordinary traffic should use the endpoint. Manual EndpointSlices are possible for Services without selectors, but then you own correctness; Kubernetes will not infer readiness from a remote system for you.

Example 1: A Minimal Internal Service

This first example creates two echo Pods and a ClusterIP Service. The selector has one key, app: echo, so every ready Pod in the namespace with that label becomes a backend. The Service listens on port 80 and forwards to container port 8080.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
        - name: echo
          image: registry.k8s.io/echoserver:1.10
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: echo
spec:
  selector:
    app: echo
  ports:
    - name: http
      port: 80
      targetPort: 8080

After applying it, kubectl get service echo shows a stable cluster IP. kubectl get endpointslice -l kubernetes.io/service-name=echo shows the generated slice. With two ready replicas, the deterministic expectation is one Service named echo and EndpointSlice data containing two endpoint addresses. The exact Pod IPs are cluster-assigned, so they should be inspected rather than hard-coded.

Example 2: Named Ports and Safer Selectors

Real charts should avoid selectors that accidentally include sidecars, jobs, or old releases. This example uses standard application labels and requires both name and component labels to match. It also uses a named targetPort, so the Service can keep exposing port 80 while the container declares the actual port by name.

apiVersion: v1
kind: Service
metadata:
  name: reports-api
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: reports
    app.kubernetes.io/component: api
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reports-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: reports
      app.kubernetes.io/component: api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: reports
        app.kubernetes.io/component: api
    spec:
      containers:
        - name: api
          image: registry.k8s.io/echoserver:1.10
          ports:
            - name: web
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /
              port: web

The named port is resolved on each selected Pod. If a later container image changes from 8080 to another number but keeps the port name web, the Service manifest can remain unchanged. That is useful in Helm charts because values for public Service ports and container internals often evolve at different speeds. The trade-off is that every selected Pod must define the named port consistently; otherwise traffic to that Pod cannot be routed as intended.

Example 3: A Service Without a Selector

Some Services front systems that are not ordinary Pods in the namespace: a database during migration, a legacy appliance reachable over private networking, or a workload represented by another controller. In that case, omit the Service selector and provide EndpointSlices yourself or through a controller.

apiVersion: v1
kind: Service
metadata:
  name: payments
spec:
  ports:
    - name: http
      port: 80
      targetPort: http
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: payments-manual-a
  labels:
    kubernetes.io/service-name: payments
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses:
      - 10.10.20.31
    conditions:
      ready: true
    hostname: payments-a
  - addresses:
      - 10.10.20.32
    conditions:
      ready: false
    hostname: payments-b

The key link is the EndpointSlice label kubernetes.io/service-name: payments. The first endpoint is marked ready and can receive ordinary traffic. The second endpoint is published but not ready, so consumers that respect readiness should avoid it. This pattern is powerful, but it bypasses Pod label selection and Pod readiness probes. Use it when you have another trustworthy source of endpoint health.

Helm Template Design

In Helm, the Service is usually one of the most stable objects in a chart because clients depend on its DNS name. A release rename changes generated names, so charts commonly use helper templates for predictable names and standard labels. Values should control exposure choices such as type and port, while selectors should stay tied to the release labels placed on the Pods.

apiVersion: v1
kind: Service
metadata:
  name: {{ include "chart.fullname" . }}
  labels:
    {{- include "chart.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  selector:
    app.kubernetes.io/name: {{ include "chart.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: http
      port: {{ .Values.service.port }}
      targetPort: http

Keep selectors narrow and stable. If a chart upgrade changes Pod template labels but not the Service selector, the old Service may point to no Pods. If it changes the Service selector before the new Pods are ready, traffic can drain early. A practical chart rule is to derive the Service selector and Pod labels from the same helper and review rendered YAML with helm template before applying it.

Design Choices and Trade-offs

ClusterIP is the default for service-to-service traffic because it is simple and internal. NodePort exposes every node and is usually a building block for other systems rather than the preferred user-facing endpoint. LoadBalancer delegates external reachability to the platform, which is convenient but may allocate billable infrastructure and cloud-specific annotations. ExternalName creates a DNS CNAME and does not create EndpointSlices or proxy traffic.

Headless Services trade the virtual IP for direct endpoint discovery. They are valuable when clients need stable peer identities or need to connect to every replica, but ordinary web APIs usually benefit from the simpler ClusterIP abstraction. Session affinity can keep a client address on the same backend, but it can hide uneven load and does not replace application-level session storage. externalTrafficPolicy: Local can preserve source IP for external traffic, but nodes without local endpoints must be handled correctly by the load balancer.

Failure Modes and Troubleshooting

A common symptom is curl: Could not resolve host. The likely cause is a wrong Service name, namespace, or DNS search path, not EndpointSlices. Check kubectl get service -n namespace, then query the fully qualified name service.namespace.svc.cluster.local from a debug Pod.

Another symptom is that DNS resolves but the connection times out. The cause is often an empty EndpointSlice set: the Service selector matches no Pods, Pods are not ready, or the Pods are in a different namespace. Run the following diagnostics and compare the Service selector with actual Pod labels.

kubectl get service echo -o wide
kubectl get endpointslice -l kubernetes.io/service-name=echo
kubectl describe endpointslice -l kubernetes.io/service-name=echo
kubectl get pod -l app=echo --show-labels
kubectl describe service echo

If endpoint addresses exist but requests fail with connection refused, the Service may target the wrong port or the container may not be listening. Inspect spec.ports[].targetPort, container port names, and application logs. If only some requests fail, look for mixed Pod versions, inconsistent named ports, or readiness probes that pass before the application can serve real traffic. Correct the label, readiness probe, or target port, then verify that the EndpointSlice conditions and addresses update.

Security, Performance, and Reliability

A Service is reachable according to cluster networking and policy, not according to intent. Use NetworkPolicy where the cluster supports it to limit which Pods may connect. Avoid exposing a LoadBalancer or broad NodePort from a Helm value without review, because the same chart may be installed in development and production with different network assumptions.

EndpointSlices improve API performance at scale, but they do not make poor selectors safe. A selector that captures thousands of Pods can create excessive fan-out and uneven application behavior. Readiness probes are reliability controls: they determine when endpoints become eligible. During shutdown, combine graceful termination with readiness changes so endpoints are removed before the process stops accepting requests.

Hands-on Lab

Prerequisites: a Kubernetes cluster you can modify, kubectl configured for it, permission to create a namespace, and a local file named service-lab.yaml containing the first example. The lab creates isolated objects, verifies Service discovery, makes one request through the Service name, and then removes everything.

kubectl create namespace svc-lab
kubectl apply -n svc-lab -f service-lab.yaml
kubectl rollout status -n svc-lab deployment/echo
kubectl get service,endpointslice -n svc-lab
kubectl run curl -n svc-lab --rm -i --restart=Never --image=curlimages/curl -- curl -sS http://echo
kubectl delete namespace svc-lab

Verification has three parts. The rollout command should report that deployment/echo successfully rolled out. The get service,endpointslice command should show the echo Service and at least one EndpointSlice labeled for it. The curl command should return an HTTP response from the echo server. Cleanup is the namespace deletion; if it hangs, inspect remaining finalizers with kubectl get namespace svc-lab -o yaml.

Assessment Exercises

  1. A Service selector is app=shop,component=api. There are five Pods with app=shop, but only two also have component=api. Predict how many Pods appear as endpoints and explain what happens to the other three.
  2. Change Example 2 so the container port is renamed from web to http but the Service still targets web. What symptoms would you expect, and which command would prove the cause?
  3. Choose between a headless Service and a ClusterIP Service for a stateless JSON API. Justify the choice using client behavior, load balancing, and operational simplicity.
  4. In a Helm chart, why is it risky to let users override arbitrary selector labels independently from Pod template labels? Propose a safer values structure.
  5. Design a troubleshooting checklist for a Service that works from one namespace but not another. Include DNS, NetworkPolicy, and endpoint checks.

Summary

Services give Kubernetes workloads a stable name, virtual address, and port contract. EndpointSlices hold the changing backend reality: which addresses exist, which ports they expose, and whether they are ready. Helm should render those objects predictably, especially names, labels, selectors, and exposure type. When traffic breaks, separate the layers: DNS record, Service selector, EndpointSlice contents, target port, Pod readiness, and network policy. That sequence turns Service debugging from guesswork into a repeatable inspection path.