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.
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
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 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.
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
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 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
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.
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.
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
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.