Learning Kubernetes - Part II: Pods, Labels, and Services
Tags: kubernetes, devops, cloud, beginners
Published: 2021-03-22 00:00:55

> Welcome to the second part of my Learning Kubernetes series. This second post builds on the concepts introduced in the [previous one](https://dev.to/vascoalramos/learning-kubernetes-concepts-part-1-pb0) and explores a new (*better*) way of defining objects. We will also explore what **Services** are and how they can help you better expose your applications.

## But first... a quick recap!

> Kubernetes is an orchestration tool that allows us to manage containerized applications across a group of nodes.

In the previous post, we talked about:
- What is Kubernetes
- What it's used for
- Some simple concepts and tools around it: *Pods* and *kubectl*.

To review what a Pod is, this is what we saw in the previous post:

> A pod is the smallest unit inside the Kubernetes cluster, and it represents a collection of application containers and volumes running in the same isolated execution environment.

Also, not to forget that all containers inside the same Pod share:
- IP address
- Namespace
- Storage

One last thing, we saw how to create objects (more precisely, Pods) using **kubectl**. In this post, I want to introduce you to the *de facto* way of creating and managing objects in Kubernetes and some new concepts.

So, let's jump right in! :smile:

## The brave "new" declarative world!
![Brave new world banner](https://imgur.com/0u5glcX.png)
The declarative approach to Infrastructure is natural to DevOps and has gained even more relevance with the surge of GitOps. Immutability is a core concept in the declarative approach. In the case of Kubernetes, as it's said in [Kubernetes: Up and Running](https://www.amazon.com/Kubernetes-Running-Dive-Future-Infrastructure/dp/1492046531/):

> Immutable container images are at the core of everything that you will build in Kubernetes. It is possible to imperatively change running containers, but this is an **anti-pattern** [...] And even then, the changes must also be recorded through a declarative configuration update later, after the fire is out.

So... how do we do this? Let's go back to the example in [Part I](https://dev.to/vascoalramos/learning-kubernetes-concepts-part-1-pb0). When we created the Pod, we did so by running the following (imperative) command:

```bash
kubectl run kubernetes-hello-world --image=paulbouwer/hello-kubernetes:1.9 --port=8080
```

Now, we will do the same thing but with a declarative configuration.

```YAML
apiVersion: v1
kind: Pod                                            # 1
metadata:
  name: kubernetes-hello-world                       # 2
spec:                                                # 3
  containers:
    - image: paulbouwer/hello-kubernetes:1.9         # 4
      name: hello-kubernetes                         # 5
      ports:
        - containerPort: 8080                        # 6
```

As you can see, this YAML manifest is equivalent to the previous command. Now let's see the most important concepts in this definition:

1. **Kind:** specifies the kind of Kubernetes object to be created.
2. **Name**: defines the name for the object.
3. **Spec** is the specification of the object's desired state. The main specification here, at least in the case of Pods, is the array of containers (in our case, there's only 1 container in that array).
4. **Image** is the container's image to be executed.
5. **(Containers) Name** is the name for the container in the pod (it must be unique).
6. **Container Port** is the port on which the container is listening.

Now, to create the Pod we need to "send" the manifest to the Kubernetes API. We do this by running the following command:
```bash
# assume the manifest is stored in a file named 'pod.yml'
kubectl apply -f pod.yml
```

You should see an output stating the object was created. If you describe the object with

```bash
kubectl describe pod/kubernetes-hello-world
```

and compare it to the description of the object created with the previous method, you will see they are similar in every way. 

### Health Checks
When you run your app as a container, Kubernetes automatically keeps it alive through a health check. This process guarantees that your app is always running, and if the app fails, Kubernetes restarts it immediately.

This default behavior is helpful in simple scenarios. However, in most cases, it's not enough. 

That's the utility of health checks for application liveness. It allows you to run customized health checks to verify that your app is running and working.

The following manifest builds on the previous one by adding a custom health check capability, using what is called a **Liveness Probe**.

```YAML
apiVersion: v1
kind: Pod
metadata:
  name: kubernetes-hello-world-health
spec:                                       
  containers:
    - image: paulbouwer/hello-kubernetes:1.9
      name: hello-kubernetes                         
      livenessProbe:
        httpGet:
          path: /
          port: 8080
        initialDelaySeconds: 5                       # 1
        timeoutSeconds: 1                            # 2
        periodSeconds: 10                            # 3
        failureThreshold: 3                          # 4
      ports:
        - containerPort: 8080 
```

1. This first field specifies the number of seconds to delay the probe's first execution.
2. Specifies that the probe must respond within the X-second timeout (in our case: 1-second timeout).
3. Specifies that the probe will be called every X seconds (in our case: 10 seconds).
4. Specifies that the container will fail and restart if the probe fails more than X times in a row (in our case: 3 times).

Now we create the pod by running:
```bash
# assume the manifest is stored in a file named 'pod-health.yml'
kubectl apply -f pod-health.yml
```

If you run `kubectl get pods`, you will have the following output (or something similar):
![Pods list](https://imgur.com/zY9a54J.png)

This shows you the two pods we created. the first one without a custom health check, and the second one with a custom health check that restarts the Pod if the container fails more than 3x in a row.


## Labels and Annotations
**Labels** and **Annotations** are cornerstone concepts in Kubernetes that let you work in sets of objects that represent how *you* think about your app.

**Labels** are key/value pairs that can be attached to Kubernetes objects such as Pods and Deployments. They can be arbitrary and are useful for attaching semantic information used to group Kubernetes objects.

**Annotations** are key/value pairs designed to hold non-semantic information that can be used by tools and libraries.

As we will see, labels are essential to the definition of some Kubernetes objects such as Services and Deployments.

To exemplify the usage of labels, and following [the recommend usage of labels in Kubernetes](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/), we will create different versions of our 2 Pods by adding labels. Here you have the manifests:

```YAML
apiVersion: v1
kind: Pod
metadata:
  name: kubernetes-hello-world-labeled
  labels:                                     # label section
    name: hello-world
    part-of: hello-world
    version: labeled
spec:
  containers:
    - image: paulbouwer/hello-kubernetes:1.9
      name: hello-kubernetes
      ports:
        - containerPort: 8080
```

```YAML
apiVersion: v1
kind: Pod
metadata:
  name: kubernetes-hello-world-health-labeled
  labels:                                     # label section
    name: hello-world-health
    part-of: hello-world
    version: labeled
spec:
  containers:
    - image: paulbouwer/hello-kubernetes:1.9
      name: hello-kubernetes
      livenessProbe:
        httpGet:
          path: /
          port: 8080
        initialDelaySeconds: 5
        timeoutSeconds: 1
        periodSeconds: 10
        failureThreshold: 3
      ports:
        - containerPort: 8080
```

By creating these two pods following a similar approach as in the others, we get this output:
![Get all pods](https://imgur.com/pSzfJSS.png)

### Label Selectors
#### Show Labels
The first interaction with labels is to quickly see all the labels associated with the objects when we list them, just as we can see in the following image. 

![Get Pods (show labels)](https://imgur.com/mDjDW8U.png)

#### Selector
The other, more handy interaction, is to list the objects that have the specified labels. The following image shows two different ways to do this.

![Label selector](https://imgur.com/k9cmBjd.png)

## Services
As we saw, we currently have four Pods: two of them are labeled and the other two are not.

Imagine that we want to expose the two pods that are labeled. How do we do that? The answer is **Services**!

Services provide an abstract way to **expose** an app running on a **logical set of Pods** and a policy by which to access them. The set of Pods targeted by a Service is usually determined by a **LabelSelector**.

Services provide three policies to expose your app:
- **ClusterIP** (default type): exposes the service on an internal IP in the cluster. This makes the Service only reachable from within the cluster. 
- **NodePort**: exposes the service on the same port of each node by forwarding traffic to that port to the service. It's a superset (an expansion) of ClusterIP. 
- **LoadBalancer**: creates an external load balancer in the current cloud (if supported) and assigns a fixed, external IP to the Service. The load balancer directs traffic to the nodes in your cluster using NodePort, so it's a superset of NodePort.

As my Kubernetes Cluster is in a cloud setup (GCP), I will expose my application with a service of type LoadBalancer by creating and applying the following YAML:

```YAML
apiVersion: v1
kind: Service
metadata:
  name: hello-world
spec:
  type: LoadBalancer      # service type: ClusterIP (default), NodePort, LoadBalancer
  selector:               # label selector (of pods)
    part-of: hello-world
    version: labeled
  ports:
    - port: 80            # the port in which the service gets requests
      protocol: TCP       # the communication protocol
      targetPort: 8080    # the port at which incoming requests are forward
```

And you will have the following output:
![Service](https://imgur.com/RaIhYHs.png)

As you can see, the service is exposed in IP `35.197.19.160`, at port 80. So if I access `http://35.197.19.160:80`, I see my app up and running:
![app view](https://imgur.com/g3KD7dA.png)

## Wrap Up
That's all for this post! As of a summary, we:
- transitioned from imperative object definitions to declarative ones;
- explored how to define custom health checks to our pods;
- learned the basics of how labels and annotations work;
- understood how to expose applications using services.

In the next post, I will address how to create true deployments with dynamic pod creation and destruction, and also how to use Ingress as a way to expose and load balance our applications.

----

If you liked my post, you can follow me, see my other posts or check my [Kubernetes repo](https://github.com/vascoalramos/kubernetes-playground) with additional resource examples.