Demystifying Kubernetes Services

Why do we need Kubernetes Services?

In my earlier post, we have created a web server pod, but at the moment it is inaccessible apart from other pods in the container network. Even for pods in the container network it isn’t very convenient to access the web server as the pods need to find the IP address of the web server pod and keep track of any changes to it. Remember that kubernetes will reschedule pods onto other nodes, for example if the node fails. Once the pod is rescheduled it will be assigned an IP address from the available pool of addresses and not necessarily the same IP address it had before.

services

To overcome all of these challenges Kubernetes has services.

Thinking back to the Kubernetes definition for a service, a service defines networking rules for accessing pods in the cluster and from the internet.

You can declare a service to access a group of pods using labels.

In our webserver example we can use our app label to select the web server pod for the service target.

Clients can access the service at a fixed address and the service’s networking rules will direct client requests to a pod in the selected group of pods.

In our example there is only one pod but in general there can be many.

The service will also distribute the requests across the pods to balance the load.

services1

Let’s visualize how we’ll use the service to solve our problem of accessing the web server running in the pod. First, we’ll create a service that selects pods with the app=webserver label. That will cause the service to act as a kind of internal load balancer across those pods. The service will also be given a static IP address and port by Kubernetes that will allow us to access the service from outside of the container network and even outside of the cluster, like the Lab VM. Let’s see how to do it.

services2

apiVersion: v1
kind: Service
metadata:
  labels:
    app: webserver
  name: webserver
spec:
  ports:
  - port: 80
  selector:
    app: webserver
  type: NodePort

The first three fields are the same as before. The kind set to service. The metadata uses the same label as the pod since it is related to the same application. This isn’t required but it is a good practice to stay organized.

Now for the spec. The selector defines the labels to match pods against. This example targets pods labeled with app=webserver, which will select the pod we created. Services must also define port mappings. This service targets port 80. This is the value of the pods container port. Lastly, the optional type value defines how to actually expose the service. We’ll set the value to NodePort.

NodePort allocates a port for this service on each node in the cluster.

By doing this you can send a request to any node in the cluster on the designated port and be able to reach the service.

The designated port will be chosen from the set of available ports on the nodes, unless you specifiy a nodeport as part of the specs ports.

Usually it is better to let Kubernetes choose the node port from the available ports to avoid the chance of your specified port already being taken. That would cause the service will fail to create.

Future posts will cover alternate values of service types.

ubuntu@ip-10-0-128-5:~# pwd
/home/ubuntu
ubuntu@ip-10-0-128-5:~# kubectl get pods
No resources found.
ubuntu@ip-10-0-128-5:~# cd src/
ubuntu@ip-10-0-128-5:~/src# kubectl create -f 1.4-resources_pod.yaml
pod/mypod created
ubuntu@ip-10-0-128-5:~/src# kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
mypod   1/1     Running   0          11s
ubuntu@ip-10-0-128-5:~/src# kubectl create -f 2.1-web_service.yaml
service/webserver created
ubuntu@ip-10-0-128-5:~/src# kubectl get services
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        66m
webserver    NodePort    10.108.113.5   <none>        80:30009/TCP   19s
ubuntu@ip-10-0-128-5:~/src#

Notice both commands are both familiar. kubectl follows a simple design pattern which makes it easy to manage and explore different resources. kubectl displays the name, cluster IP, external IP, ports, and age of each service. CLUSTER-IP is the internal, AKA private IP, for each service. The external IP is not available for node port services. But if it were then this would be the public IP for a service. Note the ports column. Kubernetes automatically allocated a port in the port range allocated for node ports which is commonly port numbers between 30000 and 32767. Describe the service to see what other information is available.

ubuntu@ip-10-0-128-5:~/src# kubectl describe service
Name:              kubernetes
Namespace:         default
Labels:            component=apiserver
                   provider=kubernetes
Annotations:       <none>
Selector:          <none>
Type:              ClusterIP
IP:                10.96.0.1
Port:              https  443/TCP
TargetPort:        6443/TCP
Endpoints:         10.0.0.100:6443
Session Affinity:  None
Events:            <none>


Name:                     webserver
Namespace:                default
Labels:                   app=webserver
Annotations:              <none>
Selector:                 app=webserver
Type:                     NodePort
IP:                       10.108.113.5
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  30009/TCP
Endpoints:                192.168.30.65:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>
ubuntu@ip-10-0-128-5:~/src#

Notice, since we didn’t specify the label, we are seeing the other default service in our cluster, i.e. apiserver. Let’s narrow it down to the webserver.

ubuntu@ip-10-0-128-5:~/src# kubectl describe service webserver
Name:                     webserver
Namespace:                default
Labels:                   app=webserver
Annotations:              <none>
Selector:                 app=webserver
Type:                     NodePort
IP:                       10.108.113.5
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  30009/TCP
Endpoints:                192.168.30.65:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>
ubuntu@ip-10-0-128-5:~/src#

Just like before, you’ll see a bunch of useful debugging information. The port was shown in the get services output and also in this output. You can also see the Endpoints which is the address of each pod in the selected group along with the container port. If there were multiple pods selected by the label then you would see each of them listed here in the Endpoints.

Kubernetes automatically adds and removes the endpoints as matching pods are created and deleted. You don’t need to worry about that.

Now that we know the node port we need a node’s IP. It can be any node’s IP. If lets say you run kubectl get nodes, this will only show the nodes, but it won’t give us the node’s IP address.

ubuntu@ip-10-0-128-5:~/src# kubectl get nodes
NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-0-100.us-west-2.compute.internal    Ready    master   76m   v1.15.0
ip-10-0-22-203.us-west-2.compute.internal   Ready    <none>   74m   v1.15.0
ip-10-0-6-236.us-west-2.compute.internal    Ready    <none>   74m   v1.15.0
ubuntu@ip-10-0-128-5:~/src#

One way to list them is to grep for address in the describe node output and add the -A option to include lines after the match.

ubuntu@ip-10-0-128-5:~/src# kubectl describe nodes | grep -i address -A 1
Addresses:
  InternalIP:   10.0.0.100
--
Addresses:
  InternalIP:   10.0.22.203
--
Addresses:
  InternalIP:   10.0.6.236
ubuntu@ip-10-0-128-5:~/src#

Nodes are resources in the cluster just like pods and services so you can use get and describe on them.

You can check out all the information in the describe output on your own, right now we just need those IPs. The IP addresses are the internal or private IPs of the nodes in the cluster.

k15

The Bastion-host is in the same virtual network so it can reach the nodes using these addresses and I’ve allowed incoming traffic on the node port range from the bastion-host instance in the firewall rules to allow the request.

Choose any of the addresses and use curl to send an http request to the IP with the node port appended, which in this case is 30009

ubuntu@ip-10-0-128-5:~# kubectl get services
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        89m
webserver    NodePort    10.108.113.5   <none>        80:30009/TCP   23m
ubuntu@ip-10-0-128-5:~# curl 10.0.22.203:30009

Running the curl on any of the node’s IP address and Nodeport: 30009

ubuntu@ip-10-0-128-5:~# curl 10.0.22.203:30009
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
ubuntu@ip-10-0-128-5:~#

Try another node ip

ubuntu@ip-10-0-128-5:~# curl 10.0.0.100:30009
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
ubuntu@ip-10-0-128-5:~#

That is the raw html output being served up by nginx. You can try any of the node Ip’s and it would give the same result. All thanks to the Kubernetes service.

Summary

In this post, we learnt that:

  • Services allow us to expose pods using a static address even though the addresses of the underlying pods may be changing.
  • We also specifically used a nodeport service to gain access to the service from outside of the cluster on a static port that is reserved on each node in the cluster. This allowed us to access the service by sending a request to any of the nodes not only the node that’s running the nginx pod.

There is more to say about pods and services. I will try to use a more complex application to illustrate some of the remaining topics in a future post. Think microservices.