This type of service does not perform any load-balancing and only implements DNS Service Discovery, based on the Kubernetes DNS Spec. Although this is the simplest and the most basic type of Service, its use is mainly limited to stateful applications like databases and clusters. In these use case the assumption is that clients have some prior knowledge about the application they’re going to be communicating with, e.g. number of nodes, naming structure, and can handle failover and load-balancing on their own.

Some typical examples of stateful applications that use this kind of service are:

The only thing that makes a service “Headless” is the clusterIP: None which, on the one hand, tells dataplane agents to ignore this resource and, on the other hand, tells the DNS plugin that it needs special type of processing. The rest of the API parameters look similar to any other Service:

apiVersion: v1
kind: Service
  name: headless
  namespace: default
  clusterIP: None
  - name: http
    port: 8080
    app: database

The corresponding Endpoints resources are still creates for every healthy backend Pod, with the only notable distinction being the absence of hash in Pods name and presence of the hostname field.

apiVersion: v1
kind: Endpoints
    service.kubernetes.io/headless: ""
  name: headless
  namespace: default
- addresses:
  - hostname: database-0
    nodeName: k8s-guide-control-plane
      kind: Pod
      name: database-0
      namespace: default
  - name: http
    port: 8080
    protocol: TCP

In order to optimise the work of kube-proxy and other controllers that may need to read Endpoints, their Controller annotates all objects with the service.kubernetes.io/headless label.


This type of service is implemented entirely within a DNS plugin. The following is a simplified version of the actual code from CoreDNS’s kubernetes plugin:

CoreDNS builds an internal representation of Services, containing only the information that may be relevant to DNS (IPs, port numbers) and dropping all of the other details. This information is later used to build a DNS response.


Assuming that the lab is already setup, we can install a stateful application (consul) with the following command:

make headless

Check that the consul statefulset has been deployed:

$ kubect get sts
NAME            READY   AGE
consul-server   3/3     25m

Now we should be able to see one Headless Services in the default namespace:

$ kubect get svc consul-server
NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                                                                   AGE
consul-server   ClusterIP   None         <none>        8500/TCP,8301/TCP,8301/UDP,8302/TCP,8302/UDP,8300/TCP,8600/TCP,8600/UDP   29m

To interact with this service, we can do a DNS query from any of the net-tshoot Pods:

 kubectl exec -it net-tshoot-8kqh6 -- dig consul-server +search

; <<>> DiG 9.16.11 <<>> consul-server +search
;; global options: +cmd
;; Got answer:
;; WARNING: .local is reserved for Multicast DNS
;; You are currently testing what happens when an mDNS query is leaked to DNS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2841
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

; EDNS: version: 0, flags:; udp: 4096
; COOKIE: fe116ac7ab444725 (echoed)
;consul-server.default.svc.cluster.local. IN A

consul-server.default.svc.cluster.local. 13 IN A
consul-server.default.svc.cluster.local. 13 IN A
consul-server.default.svc.cluster.local. 13 IN A

;; Query time: 0 msec
;; WHEN: Sat Jun 05 15:30:09 UTC 2021
;; MSG SIZE  rcvd: 245

Application interacting with this StatefulSet can make use of DNS SRV lookup to find individual hostnames and port numbers exposed by the backend Pods:

$ kubectl exec -it net-tshoot-8kqh6 -- dig consul-server +search srv +short
0 4 8301 consul-server-2.consul-server.default.svc.cluster.local.
0 4 8600 consul-server-2.consul-server.default.svc.cluster.local.
0 4 8300 consul-server-2.consul-server.default.svc.cluster.local.
0 4 8500 consul-server-2.consul-server.default.svc.cluster.local.
0 4 8302 consul-server-2.consul-server.default.svc.cluster.local.
0 4 8301 consul-server-1.consul-server.default.svc.cluster.local.
0 4 8600 consul-server-1.consul-server.default.svc.cluster.local.
0 4 8300 consul-server-1.consul-server.default.svc.cluster.local.
0 4 8500 consul-server-1.consul-server.default.svc.cluster.local.
0 4 8302 consul-server-1.consul-server.default.svc.cluster.local.
0 4 8301 consul-server-0.consul-server.default.svc.cluster.local.
0 4 8600 consul-server-0.consul-server.default.svc.cluster.local.
0 4 8300 consul-server-0.consul-server.default.svc.cluster.local.
0 4 8500 consul-server-0.consul-server.default.svc.cluster.local.
0 4 8302 consul-server-0.consul-server.default.svc.cluster.local.