In Kubernetes, services are an abstract way to expose application workloads to outside world as a network service. Services most commonly abstract access to Kubernetes pods, but they can also abstract other kinds of backends. The set of pods targeted by a service is usually determined by a selector.
Since pods are considered disposable or non-permanent resources, you would also need to use a controller resource like replicaset or deployment etc. to maintain desired number of pod replicas.
Defining a Service
As with other kubernetes objects, the service object can be defined using a service manifest. Below is the simple service manifest:
apiVersion: v1
kind: Service
metadata:
name: demoui-service
spec:
selector:
app: demoui
ports:
- protocol: TCP
port: 80
targetPort: 80
Like other objects, the mandatory properties are apiVersion
, kind
and metadata.name
. metadata.name
defines the name of the service object. This service target pods with label of app=demoui
. In port section, we match service port spec.ports.port
80 to container port spec.ports.targetPort
80.
To create the service, we can use kubectl apply command. We’ll also create a namespace so that we can segregate our work.
# create namespace demo cloud_user@d7bfd02ab81c:~/workspace$ kubectl create namespace demo namespace/demo created # create service named demoui-service cloud_user@d7bfd02ab81c:~/workspace$ kubectl apply -f service.yaml -n demo service/demoui-service created
Once service is created, we can see the service details using kubectl get service
or kubectl describe service
command:
cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoui-service ClusterIP 10.43.16.4 <none> 80/TCP 13s cloud_user@d7bfd02ab81c:~/workspace$ kubectl describe service demoui-service -n demo Name: demoui-service Namespace: demo Labels: <none> Annotations: <none> Selector: app=demoui Type: ClusterIP IP Family Policy: SingleStack IP Families: IPv4 IP: 10.43.16.4 IPs: 10.43.16.4 Port: <unset> 80/TCP TargetPort: 80/TCP Endpoints: <none> Session Affinity: None Events: <none>
Do note that we have not created any pods in our namespace demo. We can identify the pods mapped to our service using the value for the property Endpoints
in the kubectl describe service
output. The service object is not responsible for maintenance of pods and it does not take care of pods. So in our case, even though we have created a service named demoui-service
, we do not have any pods running, which can take care of the requests redirected through this service.
To create pods, we will use below deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-ui-deployment
labels:
version: "1.0.0"
app: demoui
spec:
replicas: 3
selector:
matchLabels:
app: demoui
template:
metadata:
labels:
app: demoui
spec:
containers:
- name: demoui
image: docker.io/mohitgoyal/demo-ui01:1.0.0
ports:
- name: http
containerPort: 80
protocol: TCP
Lets go ahead and create deployment object:
# create deployment controller cloud_user@d7bfd02ab81c:~/workspace$ kubectl apply -f deployment-simple.yaml -n demo deployment.apps/demo-ui-deployment created # verify deployment is created and pods are running cloud_user@d7bfd02ab81c:~/workspace$ kubectl get deployment -n demo NAME READY UP-TO-DATE AVAILABLE AGE demo-ui-deployment 3/3 3 3 19s
Now lets see the pods running and state of the service:
cloud_user@d7bfd02ab81c:~/workspace$ kubectl get pods -n demo -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demo-ui-deployment-5b8db9bb7b-7jchl 1/1 Running 0 53s 10.42.2.18 k3d-worker1-0 <none> <none> demo-ui-deployment-5b8db9bb7b-st9nd 1/1 Running 0 53s 10.42.1.48 k3d-worker-0 <none> <none> demo-ui-deployment-5b8db9bb7b-bqrxc 1/1 Running 0 53s 10.42.3.15 k3d-worker2-0 <none> <none> cloud_user@d7bfd02ab81c:~/workspace$ kubectl describe service demoui-service -n demo Name: demoui-service Namespace: demo Labels: <none> Annotations: <none> Selector: app=demoui Type: ClusterIP IP Family Policy: SingleStack IP Families: IPv4 IP: 10.43.16.4 IPs: 10.43.16.4 Port: <unset> 80/TCP TargetPort: 80/TCP Endpoints: 10.42.1.48:80,10.42.2.18:80,10.42.3.15:80 Session Affinity: None Events: <none>
In the output we can see the IP addresses mentioned in the Endpoints value, now matches the IP address of the pods created by the deployment. This means that the service demoui-service, has correctly identified all pods and pointing towards them.
Another important service property is Type
, which is cluster-ip in our case. We’ll discuss it later.
Multi Port Services
Some application requires to expose more than one port to their clients. Kubernetes lets you configure multiple port definitions on a Service object. When using multiple ports for a Service, you must give all of your ports names so that these are unambiguous. For example consider below manifest:
apiVersion: v1
kind: Service
metadata:
name: demoui-service
spec:
selector:
app: demoui
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
- name: https
protocol: TCP
port: 443
targetPort: 443
Lets deploy the service and check the service details:
cloud_user@d7bfd02ab81c:~/workspace$ kubectl apply -f service-multi-port.yaml -n demo service/demoui-service configured cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoui-service ClusterIP 10.43.16.4 <none> 80/TCP,443/TCP 37m cloud_user@d7bfd02ab81c:~/workspace$ kubectl describe service demoui-service -n demo Name: demoui-service Namespace: demo Labels: <none> Annotations: <none> Selector: app=demoui Type: ClusterIP IP Family Policy: SingleStack IP Families: IPv4 IP: 10.43.16.4 IPs: 10.43.16.4 Port: http 80/TCP TargetPort: 80/TCP Endpoints: 10.42.1.48:80,10.42.2.18:80,10.42.3.15:80 Port: https 443/TCP TargetPort: 443/TCP Endpoints: 10.42.1.48:443,10.42.2.18:443,10.42.3.15:443 Session Affinity: None Events: <none>
Using services
for Service Discovery
Kubernetes supports 2 primary modes of finding a Service – environment variables and DNS.
Environment Variables
When a pod is run on a Node, the kubelet adds a set of environment variables for each active service object. For example, we have below environment variables created for our service demoui-service:
cloud_user@d7bfd02ab81c:~/workspace$ kubectl get pods -n demo NAME READY STATUS RESTARTS AGE demo-ui-deployment-5b8db9bb7b-7jchl 1/1 Running 0 70m demo-ui-deployment-5b8db9bb7b-st9nd 1/1 Running 0 70m demo-ui-deployment-5b8db9bb7b-bqrxc 1/1 Running 0 70m cloud_user@d7bfd02ab81c:~/workspace$ kubectl exec demo-ui-deployment-5b8db9bb7b-7jchl -n demo -- env | grep -i service DEMOUI_SERVICE_PORT=tcp://10.43.16.4:80 DEMOUI_SERVICE_PORT_80_TCP=tcp://10.43.16.4:80 DEMOUI_SERVICE_SERVICE_HOST=10.43.16.4 DEMOUI_SERVICE_SERVICE_PORT=80 KUBERNETES_SERVICE_PORT=443 DEMOUI_SERVICE_PORT_80_TCP_PORT=80 DEMOUI_SERVICE_PORT_80_TCP_PROTO=tcp DEMOUI_SERVICE_PORT_80_TCP_ADDR=10.43.16.4 KUBERNETES_SERVICE_HOST=10.43.0.1 KUBERNETES_SERVICE_PORT_HTTPS=443
The environment variables start with service name as prefix.
Do note that you must create the Service before the application pods come into existence. Any application pods already existing, would not have these environment variables. For this reason, DNS is probably a better option.
DNS
A cluster-aware DNS server, such as CoreDNS, watches the Kubernetes API for new Services and creates a set of DNS records for each one. If DNS has been enabled throughout your cluster then all pods should automatically be able to resolve services by their DNS name.
Kubernetes provides a DNS service exposed to pods running in the cluster. This kubernetes DNS service was installed as a system component when the cluster was first created:
cloud_user@d7bfd02ab81c:~/workspace$ kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE helm-install-traefik-crd-dn54j 0/1 Completed 0 10d helm-install-traefik-j4hnx 0/1 Completed 1 10d metrics-server-86cbb8457f-xgbsw 1/1 Running 12 10d coredns-7448499f4d-tlcj5 1/1 Running 12 10d local-path-provisioner-5ff76fc89d-sk2l4 1/1 Running 13 10d traefik-97b44b794-jsk9r 1/1 Running 12 10d svclb-traefik-vcq66 2/2 Running 27 10d svclb-traefik-x422x 2/2 Running 16 9d svclb-traefik-2pwzp 2/2 Running 19 9d svclb-traefik-bqgtj 2/2 Running 32 10d
Check for the pod named coredns-7448499f4d-tlcj5 in above output.
To work with dns resolution or troublehshooting within our pod, we first need a container which has required binaries. For this, we can follow steps mentioned here. Below is TL;DR for instructions:
# create a simple pod with network binaries cloud_user@d7bfd02ab81c:~/workspace$ kubectl apply -f https://k8s.io/examples/admin/dns/dnsutils.yaml pod/dnsutils created # verify pod is up and running cloud_user@d7bfd02ab81c:~/workspace$ kubectl get pods dnsutils NAME READY STATUS RESTARTS AGE dnsutils 1/1 Running 0 23s # connect to the pod - dnsutils interactively and run dns queries cloud_user@d7bfd02ab81c:~/workspace$ kubectl exec -it dnsutils -- /bin/sh / # nslookup demoui-service.demo.svc.cluster.local Server: 10.43.0.10 Address: 10.43.0.10#53 Name: demoui-service.demo.svc.cluster.local Address: 10.43.16.4 / # nslookup demoui-service.demo Server: 10.43.0.10 Address: 10.43.0.10#53 Name: demoui-service.demo.svc.cluster.local Address: 10.43.16.4 / # quit /bin/sh: quit: not found / # exit command terminated with exit code 127
The full DNS name for our service is demoui-service.demo.svc.cluster.local where:
- demoui-service – is the name of the service in question
- demo – is the namespace that this service is in
- svc – recognizes that this is a service. This allows Kubernetes to expose other types of things as DNS in the future
- cluster.local – is the base domain name for the cluster. This is the default and what you will see for most clusters. Administrators may change this to allow unique DNS names across multiple clusters.
When referring to a service in your own namespace you can just use the service name as in demoui-service. You can also refer to a service in another namespace with {service-name}.{namespace}
as in demoui-service.demo. And, of course, you can and should always use the fully qualified service name.
Publishing a Service and Service Types
Kubernetes ServiceTypes
allow you to specify what kind of service you want. The default is ClusterIP
, which is what we have created till now.
Different kubernetes service types and their behaviors are:
- ClusterIP: Exposes the service on a cluster-internal IP address. Choosing this value makes the service only reachable from within the cluster. This is the default.
- NodePort: Exposes the service on each Node’s IP at a static port (the
NodePort
). AClusterIP
service, to which theNodePort
service routes, is automatically created. You’ll be able to contact theNodePort
service, from outside the cluster, by requesting<NodeIP>:<NodePort>
. - LoadBalancer: Exposes the Service externally using a cloud provider’s load balancer.
NodePort
andClusterIP
Services, to which the external load balancer routes, are automatically created. - ExternalName: Maps the service to the contents of the
externalName
field (e.g.foo.bar.example.com
), by returning aCNAME
record with its value. No proxying of any kind is set up.
You can also use Ingress to expose your application to outside world. Ingress is not a service type, but it acts as the entry point for your cluster. It lets you consolidate your routing rules into a single resource as it can expose multiple services under the same IP address.
NodePort Service
NodePort is special type of service which exposes application workloads on a static port on each node in the cluster. With this feature, if you can reach any node in the cluster you can contact a service. You use the NodePort without knowing where any of the Pods for that service are running. This can be integrated with hardware or software load balancers to expose the service further.
By default, the Kubernetes control plane allocates a port from a range specified by --service-node-port-range
flag (default: 30000-32767), if not specified. Else user is allowed to mention this port in the service manifest in the port range mentioned. This also means that you need to take care of possible port collisions yourself.
apiVersion: v1
kind: Service
metadata:
name: nodeport-demo-service
spec:
type: NodePort
selector:
app: demoui
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 31001
Lets go ahead and create this service:
cloud_user@d7bfd02ab81c:~/workspace$ kubectl apply -f service-nodeport.yaml -n demo service/nodeport-demo-service created cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoui-service ClusterIP 10.43.16.4 <none> 80/TCP,443/TCP 3h22m nodeport-demo-service NodePort 10.43.226.35 <none> 80:31001/TCP 16s cloud_user@d7bfd02ab81c:~/workspace$ kubectl describe service nodeport-demo-service -n demo Name: nodeport-demo-service Namespace: demo Labels: <none> Annotations: <none> Selector: app=demoui Type: NodePort IP Family Policy: SingleStack IP Families: IPv4 IP: 10.43.226.35 IPs: 10.43.226.35 Port: <unset> 80/TCP TargetPort: 80/TCP NodePort: <unset> 31001/TCP Endpoints: 10.42.1.48:80,10.42.2.18:80,10.42.3.15:80 Session Affinity: None External Traffic Policy: Cluster Events: <none>
LoadBalancer Service
On cloud providers which support external load balancers, setting the type
field to LoadBalancer
provisions a load balancer for your service. The actual creation of the load balancer happens asynchronously, and information about the provisioned balancer is published in the service’s .status.loadBalancer
field.
The traffic from the external load balancer is directed at the backend Pods. The cloud provider decides how it is load balanced.
ExternalName Service
Services of type ExternalName map a Service to a DNS name, not to a typical selector based on labels. We can create an external service using below-like manifest:
apiVersion: v1
kind: Service
metadata:
name: external-demoservice
spec:
type: ExternalName
externalName: mohitgoyal.co
cloud_user@d7bfd02ab81c:~/workspace$ kubectl apply -f service-external.yaml -n demo service/external-demoservice created cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoui-service ClusterIP 10.43.16.4 <none> 80/TCP,443/TCP 3h34m nodeport-demo-service NodePort 10.43.226.35 <none> 80:31001/TCP 11m external-demoservice ExternalName <none> mohitgoyal.co <none> 12s cloud_user@d7bfd02ab81c:~/workspace$ kubectl describe service external-demoservice -n demo Name: external-demoservice Namespace: demo Labels: <none> Annotations: <none> Selector: <none> Type: ExternalName IP Families: <none> IP: IPs: <none> External Name: mohitgoyal.co Session Affinity: None Events: <none>
When looking up the host external-demoservice.demo.svc.cluster.local, the cluster DNS service returns a CNAME
record with the value mohitgoyal.co. Accessing external-demoservice
works in the same way as other services but with the crucial difference that redirection happens at the DNS level rather than via proxying or forwarding:
cloud_user@d7bfd02ab81c:~/workspace$ kubectl exec -it dnsutils -- nslookup external-demoservice.demo.svc.cluster.local Server: 10.43.0.10 Address: 10.43.0.10#53 external-demoservice.demo.svc.cluster.local canonical name = mohitgoyal.co.
Deleting a Service
We can delete a service by passing the service manifest to the kubectl delete
command or using service name with the kubectl delete
command:
# get current list of services in namespace demo cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoui-service ClusterIP 10.43.16.4 <none> 80/TCP,443/TCP 7h8m nodeport-demo-service NodePort 10.43.226.35 <none> 80:31001/TCP 3h45m external-demoservice ExternalName <none> mohitgoyal.co <none> 3h34m # delete service external-demoservice using service manifest cloud_user@d7bfd02ab81c:~/workspace$ kubectl delete -f service-external.yaml -n demo service "external-demoservice" deleted # verify the service is deleted cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoui-service ClusterIP 10.43.16.4 <none> 80/TCP,443/TCP 7h9m nodeport-demo-service NodePort 10.43.226.35 <none> 80:31001/TCP 3h46m # delete service nodeport-demo-service imperatively cloud_user@d7bfd02ab81c:~/workspace$ kubectl delete service nodeport-demo-service -n demo service "nodeport-demo-service" deleted # verify service is deleted cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoui-service ClusterIP 10.43.16.4 <none> 80/TCP,443/TCP 7h9m # delete service demoui-service imperatively cloud_user@d7bfd02ab81c:~/workspace$ kubectl delete service demoui-service -n demo service "demoui-service" deleted # verify service is deleted cloud_user@d7bfd02ab81c:~/workspace$ kubectl get service -n demo No resources found in demo namespace. # deleting namespace since we don't need it anymore cloud_user@d7bfd02ab81c:~/workspace$ kubectl delete namespace demo namespace "demo" deleted