Intro to kubernetes
Problem in learning kubernetes
Lots of people are considering learning Kubernetes but are struggling to take the first steps because the official docs are complete dogshit for beginners. Don’t get me wrong, the docs are great, but they serve a different purpose and are mostly used by experienced developers as a reference for things they are about to build or for explanations of some concepts that Kubernetes leverages.
Also official docs are mostly focus on how to deploy a big-ass production-ready system. Which is not something that you will target as a beginner. So instead I’m gonna show you how to deploy a single app.
Another problem is that people who don’t use Kubernetes or are not familiar with all the problems it solves say that for medium and small projects, it is overkill. Which is actually complete bullshit as well. Because of the fact that Kubernetes automates a lot of things, it makes it easier to use than to not use it. And it is not that hard to start.
What is Kubernetes? And how is better than Docker?
People that are aquite good with docker might ask, like why should I care? Because I can run docker in cluster mode with docker-swarm!
Weeeeell, here’s my response to you. The main problem with docker swarm is that it
might run applications on multiple nodes, but nothing more than that.
What about ingress management, automated certificate issuing, DNS updates, distributed volume provisioning? Docker swarm doesn’t have it,
and won’t have unless they change their approach for declaring workloads. Kubernetes on the other hand is easily extendable, is adopted by big-tech companies and
supported by a lot of cloud providers, which makes it a de-facto standard for container orchestration. Sad to admit,
but docker swarm is silently dying from the day it was born.
What’s inside of Kubernetes?
Since I wanted to give high-level overview of Kubernetes, I won’t go into details about each component, and networking, but I will give you a brief overview of the main components that make up Kubernetes.
- Container
- Pod
- Deployment
- Service
- Ingress
- Namespace
- Secret
- ConfigMap
Now let’s take a look at each of these components in more detail.
Container
Containers are not really something unique to Kubernetes. You might be familiar with containers from other systems like Docker. In context of kubernetes, containers are exactly same containers as they are in docker or containerd. Nothing brand new in here.
Pod
Pods are the smallest deployable units in Kubernetes.
It’s a logically connected group of containers.
You can think of them as a virtual machines running some applications. And there are several reasons for that. First of all, pods share same network and you can go from one container to another using localhost. Also, you can attach volumes to pod and then share it to multiple containers. Which seems a lot like a virtual machine, right?
Deployment
Pods are cool, but there are some limitations to them. First of all, the pod objects are immutable, which means that you can’t change them after they are created.
It might be a problem if you want to update your application, because you will have to delete the pod and create a new one. Also, scaling pods by yourself sounds weird. Of couese you can manually create ReplicaSet, but on average it’s not something you would want to do.
So that’s why we have Deployments. It allows you to describe a template for a pod and then Kubernetes will take care of creating, updating, and scaling pods for you. It will implicitly create a ReplicaSet for you, and will be maintaining it for you as well.
Deployment is a higher-level abstraction that allows you to manage pods more easily, scale them up and down, and update them without downtime.
There are some policies that you can set for deployments, like how many pods to keep running at the same time, update strategies, and so on.
Generally speaking, use deployments to manage your pods, and don’t create pods directly unless you have a good reason to do so.
Service
Service is a resource that allows you to set a single IP address and DNS name for a set of pods.
By default you can reach to service by it’s name from the same namespace. But if you’re in a different namespace, you can reach it by using domain name such as this:
<service-name>.<namespace-name>.svc.cluster.local
.
You can think of services as a load balancer that distributes traffic to a set of pods.
Ingress
Ingress is a resource that allows you to expose your services to the outside world with a single IP address and DNS name.
You can think of it as a reverse proxy that routes traffic to different services based on the request path or host. In order to use ingress, you need to have an ingress controller running in your cluster. K3S by default has Traefik ingress controller installed, which is a great choice for beginners. But for more control and features, I personally prefer to use NGINX Ingress Controller.
Ingress works this way: you create an ingress resource that defines the rules for routing traffic to different services, based on the request path or host.
Then the ingress controller will take care of routing the traffic to the appropriate service.
Namespace
I mentioned namespaces in the beginning, but I didn’t explain what they are. Actually it’s a very simple concept. It’s a way to group resources in Kubernetes and manage access. Most of the time you can think of namespaces as just a way to organise resources in your cluster.
I prefer using different namespaces for different apps. For example my mailserver is deployed in namespace mailserver
. But my blog is deployed in namespace s3rius
for personal projects.
Telegram bots, CI/CD pipelines, and other applications that I deploy in my cluster are also deployed in their own namespaces. Which makes it easier to manage resources and access control.
About access control, you can use Role-Based Access Control (RBAC) to manage access to resources in a namespace. It will only be useful if you have multiple users or teams working in the same cluster.
You can create roles and role bindings to grant permissions to users or groups in a namespace. But I won’t cover RBAC in this blog post. Just because it’s too complex and we’re only discussing basics.
ConfigMap and Secret
These two resources are used to store configuration data and sensitive information, respectively. For example, you can use ConfigMap to store environment variables, configuration files, or any other non-sensitive data that your application needs to run. And then mount them as files to your pod, or populate environment variables from it, etc.
In secrets you can store sensitive information like passwords, API keys, or any other data that you don’t want to expose in plain text. But please keep in mind that by default k8s encodes all secrets using base64, which is completely insecure. To make secrets actually secret, you better use Vault or External secrets operator or something similar. But for now let’s just use default base64 encoded secrets.
How to deploy kubernetes
For local development you have several options:
- K3D - k3s in Docker.
- Minikube - k8s in docker.
- Kind - Kubernetes in Docker, but with a focus on testing Kubernetes itself.
For production:
- Kubeadm - raw and barebones way to deploy Kubernetes.
- Kubespray - Ansible-based tool to deploy Kubernetes.
- K3S - Lightweight Kubernetes distribution that is easy to deploy and maintain.
- Rancher - A complete Kubernetes management platform
- DeckHouse - A complete Kubernetes management platform that is easy to deploy and maintain.
My personal recommendation is to use K3D for local development and K3S for production. For deploying K3S you can use k3sup, which is a simple script that allows you to deploy K3S on any Linux server with a single command.
Learning environment
Let’s use Docker-packed Kubernetes to start off. We’re going to use k3d to spin up a cluster. It’s an amazing tool that allows you to create a Kubernetes cluster and everything you need with a single command. Additionally, it spins up k3s instead of Google’s Kubernetes, which is a lightweight version of Kubernetes that is perfect for beginners.
It has all the features you need to get started and is much easier to set up than other distributions. I’m using this k3d configuration to create a cluster:
# yaml-language-server $schema=https://raw.githubusercontent.com/k3d-io/k3d/main/pkg/config/v1alpha5/schema.json
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
name: default
servers: 1
volumes:
- volume: /tmp/k3d-udev:/run/udev # For openebs
ports:
- port: 80:80 # HTTP ports mapping for cluster
nodeFilters:
- loadbalancer
- port: 443:443 # HTTPS ports mapping for cluster.
nodeFilters:
- loadbalancer
registries:
create:
name: registry.localhost # Registry for containers
host: "0.0.0.0" # Host for registry
hostPort: "5000" # Local port for registry
With this configuration, you can create a cluster with a single command:
k3d cluster create --config "k3d.yaml"
Connecting to the cluster
After you install kubectl you should be able to locate file ~/.kube/config
. This file contains all required infomration to connect to clusters. Tools like minikube, k3d or kind will automatically update this file when you create a cluster.
Let’s take a look at the contents of this file:
apiVersion: v1
kind: Config
preferences: {}
# Here's list of clusters that you can connect to.
# In this case we have only one cluster, which is k3d-default.
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://0.0.0.0:44755
name: k3d-default
# Users is a list of users that you can use to connect to clusters.
# Most of the time you will have each user for each cluster.
# But in some cases you might have multiple users for the same cluster.
users:
- name: admin@k3d-default
user:
client-certificate-data: DATA+OMITTED
client-key-data: DATA+OMITTED
# Context is a combination of cluster and user that you can use to connect to a cluster.
# You can create any combination of cluster and user,
# but most of the time you will have only one context for each cluster.
contexts:
- context:
cluster: k3d-default
user: admin@k3d-default
name: k3d-default
# Currently selected context. It will be used as a default one
# for all kubectl commands.
current-context: k3d-default
I will update my context to use k3d-default as my default context with this command:
kubectl config use-context k3d-default
Also, we can check if we are connected to the cluster by running:
kubectl cluster-info
Once we are connected to the cluster, we can start deploying applications and managing resources. But before that I want to mention Lens and K9S. These two things will help you a lot to get into kubernetes. Becuase kubectl is great, no shit, I’ll be using it for showing everything what is going on.
But I highly recommend you to install Lens
or K9S
to have better overview of your cluster and resources.
So you will be able to see what is going on in your cluster, what pods are running, what services are available,
and so on. I personally use k9s
and I think it’s much better, because it has everything you need and it’s fast as hell.
Lens is a bit more heavy, but it’s still a great tool for getting started with Kubernetes.
Deploying your first application
I’m going to use python to write a small server to deploy in the cluster. Here’s the application:
import os
from aiohttp import web
routes = web.RouteTableDef()
@routes.get("/")
async def index(req: web.Request) -> web.Response:
# We increment the request count and return the state
req.app["state"]["requests"] += 1
return web.json_response(req.app["state"])
app = web.Application()
app["state"] = {
"requests": 0,
"hostname": os.environ.get("HOSTNAME", "unknown"),
}
app.router.add_routes(routes)
if __name__ == "__main__":
web.run_app(app, port=8000, host="0.0.0.0")
As you can see, this is a simple aiohttp application that returns the number of requests and the hostname of the pod. This is a good example of an application that can be deployed in Kubernetes, because it is stateless and can be scaled easily.
Let’s create a Dockerfile for this application:
FROM python:3.11-alpine3.19
RUN pip install "aiohttp>=3.12,<4.0"
WORKDIR /app
COPY server.py .
CMD ["python", "server.py"]
Here’s a simple Dockerfile that installs aiohttp and runs the application. Let’s build an image and upload it to our running cluster.
docker build -t "registry.localhost:5000/small-app:latest" .
docker push "registry.localhost:5000/small-app:latest"
Now let’s deploy this application so it will become available from our host machine.
apiVersion: apps/v1
kind: Deployment
# Here's the name of the deployment, which is small-app.
metadata:
creationTimestamp: null
name: small-app
spec:
replicas: 1
# Here we define selector that will help this deployment
# to find pods that it manages. Usually it is the
# same as labels in pod template.
selector:
matchLabels:
app: small-app
strategy: {}
# Here we define the pod template that will be used to create
# pods for this deployment.
template:
metadata:
# Each pod create by this deployment will have these labels.
labels:
app: small-app
spec:
containers:
- image: registry.localhost:5000/small-app:latest
name: small-app
ports:
- containerPort: 8000
protocol: TCP
resources: {}
---
apiVersion: v1
kind: Service
metadata:
name: small-app
namespace: small-app
spec:
# This type of service will make it accessible only
# from inside the cluster.
# If you want to expose it to the outside world,
# you can use LoadBalancer or NodePort.
# But we don't need it for now.
type: ClusterIP
# Here we define ports available for this service.
# Port is the port that will be exposed by the service,
# targetPort is the port that the service
# will forward traffic to in target pods.
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8000
# Here we define where to route traffic for this service.
# In this case we route traffic to pods that have
# label app=small-app.
selector:
app: small-app
---
# Write ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: small-app
namespace: small-app
spec:
rules:
# Here's the host configuration for the Ingress
# It will route traffic for this host to the small-app service
- host: small-app.localhost
http:
paths:
# Here we define the path and the backend service
# The path is set to '/' which means all
# traffic to this host will be routed
# to the defined backend service
- path: /
pathType: Prefix
backend:
service:
name: small-app
port:
number: 80
resources: {}
Now let’s save this file as small-app.yaml
and apply it to our cluster:
kubectl apply -f small-app.yaml
Once the command is executed, you should see that the deployment and service are created successfully.
❯ kubectl get pods -n small-app
NAME READY STATUS RESTARTS AGE
small-app-54f455696b-rp8qw 1/1 Running 0 13m
If you were using my k3d configuration, you should be able to access the application at small-app.localhost.
Let’s check if it works. I’m gonna use curl and jq for that:
❯ curl -s http://small-app.localhost | jq
{
"requests": 1,
"hostname": "small-app-54f455696b-rp8qw"
}
Now you can scale up the application by changing the number of replicas in the deployment.
You can do it by editing the deployment and updating the replicas
field to 3, for example and then running
kubectl apply -f "small-app.yaml"
Or alternatively you can use kubectl scale
command:
kubectl scale deployment -n small-app small-app --replicas 3
Let’s verify that the application is scaled up:
❯ kubectl get pods -n small-app
NAME READY STATUS RESTARTS AGE
small-app-54f455696b-6tkxx 1/1 Running 0 21s
small-app-54f455696b-9sd7r 1/1 Running 0 21s
small-app-54f455696b-rp8qw 1/1 Running 0 25m
Now let’s fire some requests to the application and see how it works.
Works as expected. The application is scaled up and we can see that the requests are distributed between the pods.
I guess that is more than enough to get you started with Kubernetes. I might create some more posts on how to tune up your cluster, how to use volumes, how to use secrets and configmaps, and so on.
But now I’m tired and just want to publish it already. So please go easy on me. Stay tuned.