Welcome to the second part of my Learning Kubernetes series. This second post builds on the concepts introduced in the previous one 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! 😄
The brave "new" declarative world!
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:
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. When we created the Pod, we did so by running the following (imperative) command:
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.
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:
- Kind: specifies the kind of Kubernetes object to be created.
- Name: defines the name for the object.
- 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).
- Image is the container's image to be executed.
- (Containers) Name is the name for the container in the pod (it must be unique).
- 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:
# 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
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.
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
- This first field specifies the number of seconds to delay the probe's first execution.
- Specifies that the probe must respond within the X-second timeout (in our case: 1-second timeout).
- Specifies that the probe will be called every X seconds (in our case: 10 seconds).
- 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:
# 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):
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, we will create different versions of our 2 Pods by adding labels. Here you have the manifests:
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
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:
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.
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.
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:
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:
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:
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 with additional resource examples.
Top comments (2)
I really liked the first part os the series, and this post complements very well all the information you presented last time. Good work, can't wait for the next part 💻
Thank you João for your words! It has been a very interesting experience!