Connect application with outside world with Services in Kubernetes

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). A ClusterIP service, to which the NodePort service routes, is automatically created. You’ll be able to contact the NodePort service, from outside the cluster, by requesting <NodeIP>:<NodePort>.
  • LoadBalancer: Exposes the Service externally using a cloud provider’s load balancer. NodePort and ClusterIP 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 a CNAME 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s