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.
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.
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.
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.
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.