In the previous post we managed to create a very simple deployment of REST Api and made the cluster execute it on our behalf. By looking it up in the dashboard, we saw it got deployed successfully. However, in no way we’ve been able to connect to it. How do we call this API?

What we did, is we created kubernetes resource of type Deployment. It is used to describe how the Pod should be created - besides others, it states what are the containers to be spawned, what resource quotas to apply (if any) and what ports should be exposed within the container’s namespace. Since namespaces are used to isolate network stack, nobody from outside of it can talk to it. To make them reachable, you need to use k8s abstraction called Service.

What are services?

Service is quite a handy Kubernetes resource used to define network topology. On the basic level, it allows you to expose port under which other services can reach your containers. This could be a port internal to the cluster, or exposed externally to the world. So, Services connect Pods together and expose them under single IP.

To learn it hands-on, let’s extend our Kubernetes deployment to include a basic Service:

# Required Kubernetes Resource API version
apiVersion: apps/v1
# specifies Kubernetes resource type
kind: Deployment
metadata:
  name: kubernetes-hello
spec:
  # we want single instance running at a given time
  replicas: 1
  selector:
    # replicaset will consist of pods matching label
    matchLabels:
      app: kubernetes-hello-label
  # recipe for a pod
  template:
    metadata:
      labels:
        # let's attach the label
        # so replicaset can find the containers
        app: kubernetes-hello-label
    spec:
      containers:
        - name: kubernetes-hello
          image: hello_asp_net_core:latest
          # don't pull images from the docker registry
          # as we are operating locally and this would fail
          imagePullPolicy: Never
          ports:
            # we'd like to expose port 80 (http)
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  # service will be registered in DNS under this name
  name: kubernetes-hello-service
spec:
  # only reachable within the cluster
  type: ClusterIP
  selector:
    # service consists of pods matching label
    app: kubernetes-hello-label
  ports:
    - protocol: TCP
      port: 80

This is how the deployment yaml file should look like by now. There are a couple important things to have in mind:

  • You need to define selector for the service.
    This tells Kubernetes which pods are part of this service. Then, the load will be evenly distributed among all the members, in a round-robin way. In our case, we attached app: kubernetes-hello-label to the pods and then used this label as a selector.
  • The name of the service is how the service will be registered in k8s internal dns
    Every service you define is by deault registed in kubernetes internal dns server under it’s name. This makes service discovery within the cluster possibile. In our case, other pods will be able to talk to this service by using kubernetes-hello-service as a hostname.
  • You can use --- in your Yaml file to separate different resources.
    That’s just a trick to have everything in a single file.

Now cd to the directory where deployment.yaml lives and:

$ kubectl apply -f deployment.yaml
$ kubectl describe svc kubernetes-hello-service
Name:              kubernetes-hello-service
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"kubernetes-hello-service","namespace":"default"},"spec":{"ports":...
Selector:          app=kubernetes-hello-label
Type:              ClusterIP
IP:                10.152.183.107
Port:              <unset>  5000/TCP
TargetPort:        80/TCP
Endpoints:         10.1.1.21:80
Session Affinity:  None
Events:            <none>

So, we applied the manifest and then queried for the definition of newly created kubernetes-hello-service using kubectl. Generally speaking, if you want to look anything up in the cluster, you can use kubectl to accomplish this.

Let’s grab the IP from the IP field and open it up in the browser, while appending /hello suffix. In my case, the full URL would be: http://10.152.183.107/hello.

Voila! Our API is reachable within the cluster network!

Time for a small experiment! Let’s first launch a script pining our API each second:

$ while true; do curl http://10.152.183.37/hello; echo ""; sleep 1; done

And now, adjust replicas property in deployment.yaml from 1 to 3. Having done so, while observing the terminal with curl, fire up in another terminal:

$ kubectl apply -f deployment.yaml

You should observe responses coming from brand-new instanceName! Without introducing any downtime, by changing a single integer we scaled up from one to three instances! Isn’t it awesome ?

Rolling updates and probes

I think it’s important to understand, at least on a high level, how and why does the scalling happen. When you adjust the number of replicas, or release diffent version of the image, Kubernetes will perform so called rollout update. This is the default deployment strategy in k8s.

Say you asked kubernetes to scale from a single instance to three instances:

  1. Kubernetes creates two new pods and then waits for the containers to start.
  2. Once the container started, Kubernetes will start performing readiness probe if it’s defined.
  3. Only after the two preceeding steps succeed, new pod will be attached to the service.

What are readiness probes, you might ask ?

Say you have some resources which need to be initialized before the service can fulfill requests, such as waiting for caches to be populated. You can easily make k8s wait for this to happen with readiness probe. This probe can be almost anything, but the most frequent one is surely an HTTP call.

Also, it’s a really good idea to define livenessProbe as well, which acts as an indicator on whether the service is still alive or not. You can even embbed some custom checks there or define timeouts based on your SLA. If you just skip it, Kubernetes will only observe whether the process crashed or not.

And, defining them is as simple as:

containers:
  - name: kubernetes-hello
    image: hello_asp_net_core:latest
    imagePullPolicy: Never
    ports:
      - containerPort: 80
    livenessProbe:
      httpGet:
        path: /hello
        port: 80
      # wait 3 seconds before the first chech
      initialDelaySeconds: 3
      # retry in 3 seconds
      periodSeconds: 3
      # One successful call means the probe suceeds
      successThreshold: 1
      # 3 failed calls in a row to consider it a failure
      failureThreshold: 3
      # After one second we consider the call a failure
      timeoutSeconds: 1
    readinessProbe:
      httpGet:
        path: /hello
        port: 80
      # wait 3 seconds before the first chech
      initialDelaySeconds: 3
      # retry in 3 seconds
      periodSeconds: 3
      # One successful call means the probe suceeds
      successThreshold: 1
      # 3 failed calls in a row to consider it a failure
      failureThreshold: 3
      # After one second we consider the call a failure
      timeoutSeconds: 1

Different types of Services

When we issued kubectl describe service you might have noticed that our service is of type ClusterIP. This is also the default one, if you don’t specify any. What does it mean?

Well, ClusterIP is the most basic type of a service in k8s. All it does, is it exposes your pods on a IP internal to the cluster and provides round-robin load balancing. However, no one from outside the cluster will be able to access this service. If you’d like to make it possibile, you might consider one of the types listed below:

  • NodePort
    Exposes the port on the kubernetes node, under IP of the node itself. While this way you can open up your service to the world, it’s not really the best way to achieve it. I suggest avoiding NodePort services, unless you have a really good reason to do so.
  • LoadBalancer
    Exposes the port under Load Balancer provided by your cloud provider. If you want your service to be accessibile from outside the cluster, this is far better option than NodePort.
    Note: this won’t work on local environments.
  • ExternalName
    This allows you to define aliases in the kubernetes DNS server. For example, you might define a service my-service and when asked for it’s IP, the DNS will return a CNAME pointing to another host (e.g. i-live-outside-cluster.com). This can be handy, for service discovery of external resources.
  • Ingress
    Actually, it’s not a ServiceType per se but a separate resouce kind instead. However, if you want to expose something to the world, this is probabbly your best bet. I’ll cover Ingress separately in the follow up posts.

Summary

Today, we’ve learnt the ABC of networking in Kubernetes. We met the Service resource, essential building block used to define Network topology in the cluster. We elaborated a bit on how ClusterIP is different from NodePort and that instead of the former, you should probabbly consider either LoadBalancer or Ingress.

Morover, we saw rolling updates in action and I explained to you briefly how does it work and how different probes are being used. Now that we are able to talk to our service, we’ll focus on how to provide runtime configuration to the service in the next post. Stay tuned!