Monday 13 May 2024

Kubernetes NodePort Service

This article extends my notes from an Udemy course "Kubernetes for the Absolute Beginners - Hands-on". All course content rights belong to course creators. 

The previous article in the series was Introduction to Kubernetes Services | My Public Notepad.

NodePort Service


The goal is to make external access to the application running in the pod. This service enables this by mapping a port on the node to a port on the pod. NodePort service is in fact like a virtual server inside the node. Inside the cluster, it has its own IP address, and that IP address is called the cluster IP of the service (e.g. 10.106.1.12 as in our example).

There are three ports involved, from the viewpoint of the service:
  • Target port is the port on the pod where the actual web server is running is 80. That is where the service forwards the request to.
  • (Service) port is the port on the service itself. It is simply referred to as the port. 
  • Node port is the port on the node itself, which we use to access the web server externally. In our example it is set to 30008. Node ports can only be in a valid range, which by default is from 30000 to 32767.
Node: port 30008 <-- Node port - external requests come to it
   - NodePort Service (10.106.1.12): port 80
   - Pod (10.244.0.2): port 80 <-- target port


How to create NodePort Service?


Just like how we create a Deployment, ReplicaSet or Pod - via definition file:

service-definition.yaml:

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  typeNodePort
  ports:
    - targetPort: 80
      port: 80
      nodePort: 30008
  selector:
    app: myapp
    type: front-end

metadata contains the name of the service. It can also have labels. 

spec contains type which refers to the type of service we are creating (NodePort, Cluster IP or LoadBalancer). For NodePort type, we specify ports which is an array of port mappings as we can have multiple port mappings within a single service. Each port mapping is a dictionary and the only mandatory key is port. If targetPort is not specified, it is assumed to be the same as port. If nodePort is not specified, a free port in the valid range between 30000 and 32767 is automatically allocated.

There could be hundreds of pods with web services running on port 80. We need somehow to specify those that service wants to target. We'll use the approach frequently used in Kubernetes, the same one which is used by ReplicaSets to filter out those pods that it will be scaling up: pod labels and selectors.

Pods are created with labels and we'll use those labels in the service definition file, under the selector property, just like in the ReplicaSet and Deployment definition files selector provides a list of labels to identify pods. So to link service to pods, we'll pull the labels from the pod definition file (under metadata >> labels) and place them under the selector section.

To create the service:

kubectl create -f service-definition.yaml
service/myapp-service created

To see the created service:

kubectl get services
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes      ClusterIP   10.96.0.1        <none>        443/TCP        23d
myapp-service   NodePort    10.104.148.160   <none>        80:30008/TCP   49s


We should be able to use the node IP and port 30008 to access the web content served from the pod with labels as specified in the service definition file above. 

How to get the node IP address?

When working with minikube, to get the IP address of the node we can use either of these commands:

minikube node list
minikube        192.168.59.100

minikube ip 
192.168.59.100

minikube service list
|----------------------|---------------------------|--------------|-----------------------------|
|      NAMESPACE       |           NAME            | TARGET PORT  |             URL             |
|----------------------|---------------------------|--------------|-----------------------------|
| default              | kubernetes                | No node port |                             |
| default              | myapp-service             |           80 | http://192.168.59.100:30008 |
| kube-system          | kube-dns                  | No node port |                             |
| kubernetes-dashboard | dashboard-metrics-scraper | No node port |                             |
| kubernetes-dashboard | kubernetes-dashboard      | No node port |                             |
|----------------------|---------------------------|--------------|-----------------------------|

minikube service myapp-service --url
http://192.168.59.100:30008


Let's check what objects we have and whether pods are running and are ready:

kubectl get all
NAME                                   READY   STATUS    RESTARTS      AGE
pod/myapp-deployment-88c4d7667-76nhb   1/1     Running   1 (66m ago)   2d16h
pod/myapp-deployment-88c4d7667-7gktq   1/1     Running   1 (66m ago)   2d17h
pod/myapp-deployment-88c4d7667-hsg4h   1/1     Running   1 (66m ago)   2d17h
pod/myapp-deployment-88c4d7667-j6t7q   1/1     Running   1 (66m ago)   2d17h
pod/myapp-deployment-88c4d7667-qpf7k   1/1     Running   1 (66m ago)   2d17h
pod/myapp-deployment-88c4d7667-vhsrd   1/1     Running   1 (66m ago)   2d17h

NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/kubernetes      ClusterIP   10.96.0.1        <none>        443/TCP        24d
service/myapp-service   NodePort    10.104.148.160   <none>        80:30008/TCP   22h

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/myapp-deployment   6/6     6            6           3d

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/myapp-deployment-6866d9c964   0         0         0       2d23h
replicaset.apps/myapp-deployment-6bf7c4cbf    0         0         0       2d17h
replicaset.apps/myapp-deployment-759b778ddf   0         0         0       2d17h
replicaset.apps/myapp-deployment-75d76f4c78   0         0         0       2d17h
replicaset.apps/myapp-deployment-7b5bcfbfc6   0         0         0       2d17h
replicaset.apps/myapp-deployment-7b8958bfff   0         0         0       3d
replicaset.apps/myapp-deployment-88c4d7667    6         6         6       2d17h
replicaset.apps/myapp-deployment-b6c557d47    0         0         0       3d


Let's try to use curl:

$ curl http://192.168.59.100:30008 
curl: (7) Failed to connect to 192.168.59.100 port 30008 after 0 ms: Connection refused

Let's check whether labels of pods are matching those specified in the service definition.

To list all pods and their labels use:

kubectl get pods -A --show-labels
NAMESPACE              NAME                                         READY   STATUS    RESTARTS       AGE     LABELS
default                myapp-deployment-88c4d7667-76nhb             1/1     Running   1 (76m ago)    2d17h   app=myapp,pod-template-hash=88c4d7667
default                myapp-deployment-88c4d7667-7gktq             1/1     Running   1 (76m ago)    2d17h   app=myapp,pod-template-hash=88c4d7667
default                myapp-deployment-88c4d7667-hsg4h             1/1     Running   1 (76m ago)    2d17h   app=myapp,pod-template-hash=88c4d7667
default                myapp-deployment-88c4d7667-j6t7q             1/1     Running   1 (76m ago)    2d17h   app=myapp,pod-template-hash=88c4d7667
default                myapp-deployment-88c4d7667-qpf7k             1/1     Running   1 (76m ago)    2d17h   app=myapp,pod-template-hash=88c4d7667
default                myapp-deployment-88c4d7667-vhsrd             1/1     Running   1 (76m ago)    2d17h   app=myapp,pod-template-hash=88c4d7667
kube-system            coredns-5dd5756b68-zv66l                     1/1     Running   4 (76m ago)    24d     k8s-app=kube-dns,pod-template-hash=5dd5756b68
kube-system            etcd-minikube                                1/1     Running   4 (76m ago)    24d     component=etcd,tier=control-plane
kube-system            kube-apiserver-minikube                      1/1     Running   4 (76m ago)    24d     component=kube-apiserver,tier=control-plane
kube-system            kube-controller-manager-minikube             1/1     Running   4 (76m ago)    24d     component=kube-controller-manager,tier=control-plane
kube-system            kube-proxy-8cw9s                             1/1     Running   4 (76m ago)    24d     controller-revision-hash=dffc744c9,k8s-app=kube-proxy,pod-template-generation=1
kube-system            kube-scheduler-minikube                      1/1     Running   4 (76m ago)    24d     component=kube-scheduler,tier=control-plane
kube-system            storage-provisioner                          1/1     Running   11 (76m ago)   24d     addonmanager.kubernetes.io/mode=Reconcile,integration-test=storage-provisioner
kubernetes-dashboard   dashboard-metrics-scraper-7fd5cb4ddc-z5p5r   1/1     Running   2 (76m ago)    14d     k8s-app=dashboard-metrics-scraper,pod-template-hash=7fd5cb4ddc
kubernetes-dashboard   kubernetes-dashboard-8694d4445c-9td6g        1/1     Running   2 (76m ago)    14d     gcp-auth-skip-secret=true,k8s-app=kubernetes-dashboard,pod-template-hash=8694d4445c


Let's check the labels in the service again. We can check its definition file or we can get its YAML definition via:

kubectl get service myapp-service -o yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2024-05-11T23:39:57Z"
  name: myapp-service
  namespace: default
  resourceVersion: "326186"
  uid: 79da7881-9c43-4b5e-8826-b7bf3561f53c
spec:
  clusterIP: 10.104.148.160
  clusterIPs:
  - 10.104.148.160
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - nodePort: 30008
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: myapp
    type: front-end
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}


We can see that pods actually don't have label type: front-end which is specified in the service definition so let's remove it from the service.

$ kubectl edit service myapp-service
// delete line: type: front-end

Let's now try to access the web service:

curl http://192.168.59.100:30008
<!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>

Service can be mapped to a single or multiple pods. 

In the production environment, we have multiple instances of our web application running for high availability and load balancing purposes. In this case, we have multiple similar pods running our web application. They all have different internal IP addresses (e.g. 10.244.0.3, 10.244.0.2, 10.244.0.4) but the same labels with a key app and set to a value of myapp. The same label is used as a selector during the creation of the service.

When the service is created, it looks for a matching pod with the label. The service then automatically selects all matching pods as endpoints to forward the external request coming from the user. We don't have to do any additional configuration to make this happen.

Service uses uses a random algorithm to balance the load across these different pods. Thus the service acts as a built-in load balancer to distribute load across different parts.

That was an example when we have multiple pods on a single node. 


NodePort service in a Multi-node cluster


When pods are distributed across multiple nodes we have the web application on pod on separate nodes in the cluster. Each node has its own IP address e.g. 192.168.1.2, 192.168.1.3, 192.168.1.4. Each of them has isolated, independent internal network so the IP addresses of pods might be 10.244.0.3, 10.244.0.2, 10.244.0.4.

When we create a service, without us having to do any additional configuration, Kubernetes automatically creates a service that spans across all the nodes in the cluster and maps the targetPort to the same nodePort on all the nodes in the cluster. This way we can access our application using the IP of any node in the cluster and using the same port number, which in this case is 30008:


$ curl http://192.168.1.2:30008
$ curl http://192.168.1.3:30008
$ curl http://192.168.1.4:30008


How to delete service?


kubectl delete service myapp-service

Summary


In any case, whether it be a single pod on a single node, multiple pods on a single node or multiple pods on multiple nodes, the service is created exactly the same without usw having to do any additional steps during the service creation.

When pods are removed or added, the service is automatically updated, making it highly flexible and adaptive. Once created, we won't typically have to make any additional configuration changes.

No comments: