Article

Table of Contents
Theme:
Was this article helpful?

1  out of  1 found this helpful

Try Vultr Today with

$50 Free on Us!

Want to contribute?

You could earn up to $600 by adding new articles.

An Overview of Kubernetes Service Types

Author: David Dymko

Last Updated: Thu, Nov 10, 2022
Kubernetes System Admin

What is a Service in Kubernetes?

In a Kubernetes environment, you can have hundreds, if not thousands, of pods that are ephemeral. Whether it is because nodes are being scaled down, pod replicas being scaled down, or pods being rescheduled to a new node, the IP address of a pod is never guaranteed. The pods IP address is assigned after it has been scheduled to a specific node and before it has been booted.

Given that we are in a cloud native environment, we want the ability to scale pods horizontally. Thus, it would be quite difficult to track all the IP addresses for all of our applications. Kubernetes has a resource type that solves this problem of ever-changing pod IPs called Services. A Kubernetes service allows you to create a single constant IP address that contains an IP address for a group of pods that contain the same service. This is very useful as the service IP remains static while the pod IPs can constantly be changing. You never have to worry about not having the proper IP address.

How do Services work?

How does a service work? How do I configure one?

Let us start by creating a deployment with three instances of an Nginx pod.

apiVersion: apps/v1

  kind: Deployment

metadata:

  labels:

    app: nginx

  name: nginx

spec:

  replicas: 3

  selector:

    matchLabels:

      app: nginx

    spec:

      containers:

      - image: nginx

        name: nginx

After applying the deployment manifest, we can see we have three instances of nginx running with the IPs 10.244.242.66, 10.244.242.67, 10.244.230,199. Also, take note of the labels we have assigned to these pods app=nginx. This will be crucial to how the service monitors these pods.

$ kubectl get pods -owide --show-labels

NAME                    READY   STATUS    RESTARTS   AGE     IP               NODE                      LABELS

nginx-8f458dc5b-6vcxt   1/1     Running   0          2m27s   10.244.242.66    nodepool-a-95e9c8e86208   app=nginx,pod-template-hash=8f458dc5b

nginx-8f458dc5b-ktvtf   1/1     Running   0          2m27s   10.244.242.67    nodepool-a-95e9c8e86208   app=nginx,pod-template-hash=8f458dc5b

nginx-8f458dc5b-mlkwd   1/1     Running   0          2m27s   10.244.230.199   nodepool-a-11aa1dc199fa   app=nginx,pod-template-hash=8f458dc5b

Now, let us define our service manifest, which is defined below with a breakdown of what is defined.

apiVersion: v1

kind: Service

metadata:

  name: nginx

  labels:

    app: nginx

spec:

  selector:

    app: nginx

  type: ClusterIP

  ports:

  - name: 80-80

    port: 80

    protocol: TCP

    targetPort: 80

As we start to break down this manifest, we notice that the kind is of Service. Next, the metadata section should be familiar as it is the same as the metadata fields on other Kubernetes resources. The spec section defines how and what our service will interact with.

The selector field. This field is where you define which pods you want this service to monitor. This is done through label matching. You will notice that this selector has a definition for app: nginx. If you remember, these are the labels we had defined for our pods in the deployment manifest. With this selector definition, we are stating that any pods with the labels app=nginx will be part of this service.

Next, we have the type field. This defines what kind of service we want this to be. We will dive deeper into service types later on, but we have just defined this now as a ClusterIP.

Finally, we have the ports field. Here, we define how and where the traffic will flow through the service. Note that this field takes in an array so that you can define multiple entries.

Let's break down each field within ports:

  • name: You can assign a specific name to the given port entry.

  • port: This is the port the service will listen on.

  • targetPort: This is the port the service will forward requests to. This should match the port the pods are listening on.

  • protocol: The specific protocol you wish the service to listen to and interact with.

Now that we have a basic understanding of what the service manifest does and how it interacts with pods, we deploy the manifest and inspect the service.

$ k get svc

NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE

kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   90m

nginx        ClusterIP   10.108.176.62   <none>        80/TCP    18m



$ kubectl describe svc nginx

Name:              nginx

Namespace:         default

Labels:            app=nginx

Annotations:       <none>

Selector:          app=nginx

Type:              ClusterIP

IP Family Policy:  SingleStack

IP Families:       IPv4

IP:                10.108.176.62

IPs:               10.108.176.62

Port:              80-80  80/TCP

TargetPort:        80/TCP

Endpoints:         10.244.230.199:80,10.244.242.66:80,10.244.242.67:80

Session Affinity:  None

Events:            <none>

Everything in the describe command should align up with the manifest definition except for two fields IP and Endpoints.

The IP field defines the service's IP address. This is the static IP that can be used for accessing the pods behind the service.

The Endpoints field defines the pod IPs that are currently assigned to this service. You notice that these IPs are the ones from our Nginx deployment.

Before we continue to the other service types, let's show scaling down the Nginx deployment and see how the service will handle this change.

$ kubectl scale deployment/nginx --replicas 1

deployment.apps/nginx scaled



$ kubectl get pods -owide

NAME                    READY   STATUS    RESTARTS   AGE   IP               NODE                      NOMINATED NODE   READINESS GATES

nginx-8f458dc5b-mlkwd   1/1     Running   0          43m   10.244.230.199   nodepool-a-11aa1dc199fa   <none>           <none>



$ kubectl describe service nginx | grep Endpoints  

Endpoints:         10.244.230.199:80



$ kubectl scale deployment/nginx --replicas 5

deployment.apps/nginx scaled



$ kubectl get pods -owide

NAME                    READY   STATUS    RESTARTS   AGE    IP               NODE                      NOMINATED NODE   READINESS GATES

nginx-8f458dc5b-9pmhb   1/1     Running   0          108s   10.244.242.69    nodepool-a-95e9c8e86208   <none>           <none>

nginx-8f458dc5b-mlkwd   1/1     Running   0          47m    10.244.230.199   nodepool-a-11aa1dc199fa   <none>           <none>

nginx-8f458dc5b-r2pcp   1/1     Running   0          108s   10.244.242.70    nodepool-a-95e9c8e86208   <none>           <none>

nginx-8f458dc5b-vgwv6   1/1     Running   0          108s   10.244.242.68    nodepool-a-95e9c8e86208   <none>           <none>

nginx-8f458dc5b-x6thl   1/1     Running   0          108s   10.244.230.200   nodepool-a-11aa1dc199fa   <none>           <none>



$ kubectl describe service nginx | grep Endpoints

Endpoints:         10.244.230.199:80,10.244.230.200:80,10.244.242.68:80 + 2 more...

You can see that scaling the pods up and down are immediately reflected in the service allowing for a single static IP to access the pods.

Types of Services

Within the Service resources, there are three different types. They are as follows:

  1. ClusterIP

  2. NodePort

  3. LoadBalancer

    These all have their own behaviors and specific uses cases, so it is important to know when to use each one. In the following sections, we'll do some deeper dives into each of these.

ClusterIP

The ClusterIP is the most basic" of the service types. It will create a static IP address that is on a separate subnet from the pod IP range and will monitor a group of pods through label selectors.

cluster-ip

ClusterIPs allow pods to communicate easily with one another as pods can either send requests to the static IP or use the Kubernetes DNS to send requests to {service-name}.{namespace}. In the prior example we deployed, the DNS would have been nginx.default. There are some limitations to using ClusterIP. The biggest one being there is no way to expose this service to the outside world. This is where the service types nodePort and loadBalancer should be used.

NodePort

The NodePort service builds on top of the ClusterIP type. It exposes the ClusterIP to the outside world by opening a port on the worker nodes to forward traffic. This means that if you have 50 worker nodes, each worker node will listen on the assigned port even if the pods do not live on that worker node.

nodeport

To create a NodePort service the only difference is to define the Type as NodePort

kind: Service

metadata:

  labels:

    app: nginx

  name: nginx

spec:

  ports:

  - name: 80-80

    port: 80

    protocol: TCP

    targetPort: 80

    # nodePort: 30001

  selector:

    app: nginx

  type: NodePort

You may notice that nodePort is commented out. This is because if you do not define nodePort within the ports section, Kubernetes will automatically assign a random port from the range 30000-32767. If you wish to define a specific port, it must be done from this range.

Deploy the NodePort service manifest and have us review it.

$ kubectl get svc

NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE

kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        135m

nginx        NodePort    10.98.246.253   <none>        80:30828/TCP   5s



$ kubectl describe svc nginx

Name:                     nginx

Namespace:                default

Labels:                   app=nginx

Annotations:              <none>

Selector:                 app=nginx

Type:                     NodePort

IP Family Policy:         SingleStack

IP Families:              IPv4

IP:                       10.98.246.253

IPs:                      10.98.246.253

Port:                     80-80  80/TCP

TargetPort:               80/TCP

NodePort:                 80-80  30828/TCP

Endpoints:                10.244.230.199:80,10.244.230.200:80,10.244.242.68:80 + 2 more...

Session Affinity:         None

External Traffic Policy:  Cluster

Events:                   <none>

Here we see a new field called NodePort with the value of 30828/TCP. This is the port and protocol that is assigned to this service. Now, if we were to send requests to any of our worker nodes IP addresses with that port, the request is sent to the service, which then is sent to our pods.

$ kubectl get nodes -owide

NAME                      STATUS   ROLES    AGE    VERSION   INTERNAL-IP   EXTERNAL-IP     OS-IMAGE                       KERNEL-VERSION    CONTAINER-RUNTIME

nodepool-a-11aa1dc199fa   Ready    <none>   140m   v1.24.4   10.1.96.5     45.77.152.173   Debian GNU/Linux 10 (buster)   4.19.0-22-amd64   containerd://1.6.8

nodepool-a-95e9c8e86208   Ready    <none>   140m   v1.24.4   10.1.96.4     140.82.44.105   Debian GNU/Linux 10 (buster)   4.19.0-22-amd64   containerd://1.6.8

$ curl 140.82.44.105:30828

<!DOCTYPE html>

<html>

<head>

<title>Welcome to nginx!</title>



$ curl 45.77.152.173:30828

<!DOCTYPE html>

<html>

<head>

<title>Welcome to nginx!</title>

While this is a useful way to expose our services to the outside world, it doesn't scale well. In a highly auto-scaling Kubernetes cluster, worker nodes may be ephemeral, as they may be constantly being spun up and down. This leaves us in a similar problem we have with pods where we can find out what the IPs are, but that doesn't mean they will always be there. If you require a static external IP address for your services, then the LoadBalancer service type will fit that need.

LoadBalancer

Similarly to how NodePort builds on top of ClusterIP, the LoadBalancer service builds on top of NodePort. With the LoadBalancer service, each worker node continues to have a unique port that is assigned to the specific service. However, now an L4 is deployed and configured to route to the specific worker nodes and ports. A high-level request flow is as follows: a request is sent to the Load Balancer, which is forwarded to a worker node on the specific nodeport, which then gets passed to the specific service and finally sent to a pod.

loadbalancer

Configuring a LoadBalancer service is a bit more involved if needed. A very simple configuration can be as follows, with just the type field being changed to LoadBalancer.

apiVersion: v1

kind: Service

metadata:

  labels:

    app: nginx

  name: nginx

spec:

  ports:

  - name: 80-80

    port: 80

    protocol: TCP

    targetPort: 80

  selector:

    app: nginx

  type: LoadBalancer

For example, if you wish to change your Load Balancer to force SSL redirect, configure firewall rules, to use HTTPS instead of HTTP, or to use a different balancing algorithm, then this will require defining Annotations on your service.

Here is one such example LoadBalancer manifest.

apiVersion: v1

kind: Service

metadata:

  annotations:

    service.beta.kubernetes.io/vultr-loadbalancer-protocol: "http"

    service.beta.kubernetes.io/vultr-loadbalancer-https-ports: "443"

    # You will need to have created a TLS Secret and pass in the name as the value

    service.beta.kubernetes.io/vultr-loadbalancer-ssl: "ssl-secret"

    service.beta.kubernetes.io/vultr-loadbalancer-algorithm: "least_connections"

    service.beta.kubernetes.io/vultr-loadbalancer-ssl-redirect: "true"

    service.beta.kubernetes.io/vultr-loadbalancer-firewall-rules: "0.0.0.0/80;0.0.0.0/443"

  labels:

    app: nginx

  name: nginx

spec:

  ports:

    - port: 80

      name: "http"

    - port: 443

      name: "https"

  selector:

    app: nginx

  type: LoadBalancer

For more details, see our list of all available annotations that can be used with Vultr Load Balancers.

When you first deploy your LoadBalancer service you will see that the EXTERNAL-IP section is pending

$ portfolio [talk-postman] âš¡  k get svc

NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE

kubernetes   ClusterIP      10.96.0.1        <none>        443/TCP        150m

nginx        LoadBalancer   10.103.252.120   <pending>     80:30092/TCP   4s



$ portfolio [talk-postman] âš¡  kubectl describe service nginx

Name:                     nginx

Namespace:                default

Labels:                   app=nginx

Annotations:              <none>

Selector:                 app=nginx

Type:                     LoadBalancer

IP Family Policy:         SingleStack

IP Families:              IPv4

IP:                       10.103.252.120

IPs:                      10.103.252.120

Port:                     80-80  80/TCP

TargetPort:               80/TCP

NodePort:                 80-80  30092/TCP

Endpoints:                10.244.230.199:80,10.244.230.200:80,10.244.242.68:80 + 2 more...

Session Affinity:         None

External Traffic Policy:  Cluster

Events:

  Type     Reason                  Age               From                Message

  ----     ------                  ----              ----                -------

  Normal   EnsuringLoadBalancer    8s (x4 over 44s)  service-controller  Ensuring load balancer

  Warning  SyncLoadBalancerFailed  8s (x4 over 44s)  service-controller  Error syncing load balancer: failed to ensure load balancer: load-balancer is not yet active - current status: pending

You notice that here Kubernetes has deployed a Vultr Load Balancer and is in constant communication to check if the load balancer has been provisioned and assigned an IP.

Once the Vultr Load Balancer has been provisioned and is ready for traffic, you will see an assigned IP address in the EXTERNAL-IP field and that the LB has been ensured". This means Kubernetes now have control to monitor and update the Load Balancer accordingly. Also, take note that a unique nodeport is assigned.

$ portfolio [talk-postman] âš¡  k get svc

NAME         TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)        AGE

kubernetes   ClusterIP      10.96.0.1        <none>         443/TCP        154m

nginx        LoadBalancer   10.103.252.120   45.63.15.140   80:30092/TCP   4m9s



$ kubectl describe service nginx

Name:                     nginx

Namespace:                default

Labels:                   app=nginx

Annotations:              <none>

Selector:                 app=nginx

Type:                     LoadBalancer

IP Family Policy:         SingleStack

IP Families:              IPv4

IP:                       10.103.252.120

IPs:                      10.103.252.120

LoadBalancer Ingress:     45.63.15.140

Port:                     80-80  80/TCP

TargetPort:               80/TCP

NodePort:                 80-80  30092/TCP

Endpoints:                10.244.230.199:80,10.244.230.200:80,10.244.242.68:80 + 2 more...

Session Affinity:         None

External Traffic Policy:  Cluster

Events:

  Type     Reason                  Age                    From                Message

  ----     ------                  ----                   ----                -------

  Warning  SyncLoadBalancerFailed  2m21s (x5 over 3m37s)  service-controller  Error syncing load balancer: failed to ensure load balancer: load-balancer is not yet active - current status: pending

  Normal   EnsuringLoadBalancer    61s (x6 over 3m37s)    service-controller  Ensuring load balancer

  Normal   EnsuredLoadBalancer     60s                    service-controller  Ensured load balancer

To validate that our Vultr Load Balancer is working, we can send a request to the EXTERNAL-IP, which is our Load balancers public IP address.

$ curl 45.63.15.140

<!DOCTYPE html>

<html>

<head>

<title>Welcome to nginx!</title>

You may look at the LoadBalancer service type as a fantastic solution. However, there is one drawback to them; you cannot define any kind of URL Paths, which doesn't allow for flexibility or have it route to multiple services. The LoadBalancer service is tied to a single service, so if you want to expose another application from your cluster, you would deploy another Vultr Load Balancer which can add quickly add up in cost if you have multiple applications.

The solution to this problem is not another service type but a different Kubernetes resource called ingress.

Ingress

An ingress resource is a much more flexible way to define your services to be reached by the outside world. One of the biggest selling points here is you can define a single ingress resource and have this route to all of your services under subdomains, ports, or URL paths that you define. They also offer a plethora of other features, such as rate-limiting, authentication, automatic TLS, weighted routing, and much more. All of this can be done with a single load balancer as the Ingress resource works with the LoadBalancer service type, the only difference being the deploy load balancer acts as a proxy to send all traffic to the Ingress resource, which acts as an L7 load balancer within the cluster.

mermaid diagram

The only caveat here is that Kubernetes does not come with an in-house ingress controller. There are various ingress controllers out there, all with varying types of features. Ultimately, this falls to your needs and wants before deciding which ingress controller to install. Refer to the ingress controllers' documentation for configuration guides.

Some of the more popular Ingress controllers are:

Wrapping up

While this post may give you a good understanding of the different types of Kubernetes Services and Ingress controllers, this just scratched the service. There are many more configuration options for these services and some complimentary features such as network policies. Please check out some of the links listed below for more information.

Some useful links:

Want to contribute?

You could earn up to $600 by adding new articles.