K3s - An Ally For Fast Deployment And Orchestration
Over the last articles, we prepared everything a cluster needs. We have something to orchestrate (a containerized GitHub runner), a homelab with three ThinkCentre machines, a router that isolates them in their own network, Ubuntu Server installed on each of them, and Ansible to configure them all from one place. Time to turn these three machines into our first orchestrated cluster.
This is also what I do every day as the “DevOps guy” at work: deploying tools and applications for the development team, such as Jenkins, GitHub runners, a LAVA web instance, and dashboard or monitoring tools like Grafana and Prometheus.
If you have access to multiple machines or VMs, Kubernetes is often the right tool for the job. And if your needs are not too specific, its lightweight distribution, K3s, is usually a great fit.
Why K3s specifically? It keeps the Kubernetes experience with a smaller footprint and a very simple install process. It also bundles useful components out of the box, which makes it ideal for homelabs like ours, edge environments, and small to medium internal platforms such as a company lab.
What we will end up with:
- the key Kubernetes concepts, explained in a few lines each,
- a K3s cluster on our three homelab nodes: one control plane and two agents,
- a handful of
kubectlcommands to find our way around the cluster, - the whole setup as an Ansible playbook, versioned with the rest of our infrastructure.
What is Kubernetes in a nutshell?
Instead of running an application directly on a bare machine, in the dev world we usually use a container, which creates a “fake computer environment” isolated from our system.
We do that for several reasons:
- Isolation (containing the app inside a “box” to limit contamination, corruption or destruction of our system. Containers share the host kernel though, so they are not a strong security boundary on their own).
- Disposability (we can trash this “box” and make a new one on the spot).
- Scalability (we can deploy one or many identical “boxes” on demand).
- Consistency (we can consistently reproduce an identical dev environment in that “box” on any machine, anywhere, anytime).
For that, we usually use Docker. Then comes Docker Compose which allows us to define, run and manage several containers together on a single machine, making an “app cluster” locally, connecting them through networks, volumes, or devices.
But when you want to change scale and do that with multiple machines at the same time, here comes Kubernetes. A useful way to picture it is as an evolution from the same ideas as Docker Compose, but at a larger scale, with orchestration, scheduling, recovery, and networking built in.
In one sentence: Kubernetes is an automatic app babysitter and traffic manager that keeps apps running, healthy, and scalable across many computers.
Note
K3s doesn’t use Docker: it ships its own embedded containerd runtime. If you installed Docker
on the nodes (as in the Ansible article), it stays useful to build and test images, but the two
don’t share images: an image built with docker build is not visible to K3s until it is pushed
to a registry (or imported with k3s ctr images import).
Key concepts of Kubernetes
Here are some of the main concepts to know in Kubernetes:
On infrastructure:
- Cluster: A fleet of computers working together.
- Node: One computer in that fleet.
- Control Plane: The brain of Kubernetes, a set of components (API server, scheduler, controllers, datastore) that manages deployment and orchestration. In K3s, it runs on a server node, which also runs workloads by default; the other nodes are agents.
On resources:
- Namespace: a logical partition inside a cluster used to organize and isolate resources.
- Pod: the smallest deployable unit, a group of one or more containers that share the same network and storage (usually one app per Pod), deployed on nodes.
- Service: an abstraction that exposes one or more Pods as a stable network endpoint.
- Secret: a Kubernetes object used to store sensitive data such as tokens, passwords, or keys.
- Ingress: an object that manages external HTTP/HTTPS access and routes traffic to Services.
- ConfigMap: a Kubernetes object used to store non-sensitive configuration values.
- PersistentVolume (PV): a storage resource in the cluster used to keep data beyond Pod lifecycle.
On operation:
- Deployment: an object that declares how many identical Pods of an app should run, and manages their lifecycle (through ReplicaSets), including creation, updates, and scaling.
- Desired State: The target configuration declared by the user that Kubernetes continuously tries to maintain.
- Self-healing: Kubernetes’ ability to automatically replace or restart failed containers and pods to keep applications running.
- Scaling: The process of adjusting the number of running application instances up or down based on demand.
- Rolling Update: A strategy that gradually replaces old application versions with new ones to minimize downtime.
Real-life workflow example: I deploy an app as a Deployment, expose it internally with a Service, store credentials in a Secret, inject app settings with a ConfigMap, keep data with a PersistentVolume, and publish it externally through an Ingress.
Small architecture diagram
flowchart LR
User((User / CI)) --> Ingress[Ingress]
Ingress --> Service[Service]
subgraph Cluster[K3s Cluster]
CP[Control Plane]
subgraph W1[Worker Node 1]
Pod1[Pod A]
end
subgraph W2[Worker Node 2]
Pod2[Pod B]
end
end
Service --> Pod1
Service --> Pod2
CP -. schedules .-> Pod1
CP -. schedules .-> Pod2
K3s cluster setup
To set up a K3s cluster, we need to list the machines we want to add to the cluster and have proper access to them (login and admin rights). We’ll need to set one (or more) of them as a control plane (the strategic center of the cluster) to centralize information and manage the other machines set as nodes.
Note
For machine provisioning at bare-metal scale, including package installation, updates, and configuration management, please refer to my Ansible article. The commands below are shown by hand for clarity, but they could (and should) live in a playbook: see the Ansible alternative at the end of this article.
Caution
A single control plane is a single point of failure. For production setups, plan for control plane resilience, backups, storage strategy, and network architecture.
On all machines, we need to run:
# Install dependencies
sudo apt install -y curl vim gnupg lsb-release ca-certificates
# Disable all active swap spaces (recommended: it keeps the memory behavior of the nodes predictable)
sudo swapoff -a
# Disable swap at boot
sudo sed -i '/ swap / s/^/#/' /etc/fstab
On the control plane:
# Install k3s
curl -sfL https://get.k3s.io | sh -s - server --write-kubeconfig-mode 644
# Verify with k3s command to get the state of nodes
sudo k3s kubectl get nodes
# Get the control plane token NODE_TOKEN
sudo cat /var/lib/rancher/k3s/server/node-token
Warning
--write-kubeconfig-mode 644 makes /etc/rancher/k3s/k3s.yaml readable by every user on the
control plane. This file holds cluster-admin credentials: anyone who can read it controls the
whole cluster. It’s convenient for a homelab where only we log in, but on a shared machine, drop
the flag and copy the file to your own ~/.kube/config with chmod 600 instead.
We also need the control plane IP, MASTER_IP: the address the worker nodes use to reach it.
Be careful with hostname -I, it lists all the addresses of the machine, including the ones of
Docker (172.17.0.1) and, once the cluster runs, of the K3s internal network. Use the address of
the machine on your LAN, ideally a fixed one (a static DHCP lease on your router, for instance).
On worker nodes:
# Set MASTER_IP and NODE_TOKEN with the values from the control plane
export MASTER_IP=<control plane IP>
export NODE_TOKEN=<control plane token>
# Install the K3s agent
curl -sfL https://get.k3s.io | K3S_URL="https://${MASTER_IP}:6443" K3S_TOKEN="${NODE_TOKEN}" sh -s - agent
Note
We can verify that the worker nodes joined correctly by running kubectl get nodes on the control plane.
Useful commands
Now that the cluster is up and running, here are a few useful commands to navigate in K3s. First, kubectl is the main command-line tool used to interact with the cluster from the control plane.
Some common actions are:
- get: list one or more Kubernetes objects.
- describe: show detailed information about a specific object.
- create: create a new object.
- delete: delete an existing object.
Here are a few useful examples:
- get nodes:
kubectl get nodes - get namespaces:
kubectl get namespaces - create a namespace:
kubectl create namespace <mynamespace> - create a secret in a namespace from a
.envfile:kubectl create secret generic <mysecret> --from-env-file=<.env> -n <mynamespace> - describe a Pod:
kubectl describe pod <mypod> -n <mynamespace> - open an interactive shell inside a Pod:
kubectl exec -it -n <mynamespace> <mypod> -- bash - list Pods in a namespace with their status:
kubectl get pods -n <mynamespace> -o wide - run a debug Pod:
kubectl run <myapp-debug> -n <mynamespace> --rm -it --image=<mydockerimage> --command -- sh - view Pod logs:
kubectl logs -n <mynamespace> <mypod> - check node CPU and memory usage:
kubectl top nodes
Ansible alternative: the full playbook
Typing the same commands on every machine works for a first try, but it’s exactly the kind of
manual change our Ansible article tells us to avoid.
Here is the whole setup as a single playbook, reusing the inventory of that article: the
controlplane group gets the K3s server, the nodes group gets the agents, and linux
contains them all.
playbook/install_k3s.yaml:
---
- name: Prepare the nodes for K3s
hosts: linux
become: true
tags: prep
tasks:
- name: Install curl
ansible.builtin.apt:
name: curl
state: present
update_cache: true
- name: Disable active swap
ansible.builtin.command: swapoff -a
when: ansible_swaptotal_mb > 0
- name: Disable swap at boot
ansible.builtin.replace:
path: /etc/fstab
regexp: '^([^#].*\sswap\s.*)$'
replace: '# \1'
- name: Install the K3s server
hosts: controlplane
become: true
tasks:
- name: Install K3s server
ansible.builtin.shell: curl -sfL https://get.k3s.io | sh -s - server --write-kubeconfig-mode 644
args:
creates: /usr/local/bin/k3s
- name: Read the node token
ansible.builtin.slurp:
src: /var/lib/rancher/k3s/server/node-token
register: node_token
- name: Join the agents
hosts: nodes
become: true
vars:
k3s_server: "{{ groups['controlplane'][0] }}"
tasks:
- name: Install K3s agent
ansible.builtin.shell: curl -sfL https://get.k3s.io | sh -s - agent
environment:
K3S_URL: "https://{{ k3s_server }}:6443"
K3S_TOKEN: "{{ hostvars[k3s_server].node_token.content | b64decode | trim }}"
args:
creates: /usr/local/bin/k3s
no_log: true
A few things worth noticing:
- Swap is only turned off when the node actually has some (
ansible_swaptotal_mbis a fact gathered by Ansible), and thereplacemodule comments out the swap line of/etc/fstabonly once, so a second run reportschanged=0. - The token never leaves Ansible’s memory:
slurpreads it on the control plane, and the agents get it fromhostvars.no_log: truekeeps it out of the terminal output. - The control plane IP comes from the inventory (
groups['controlplane'][0]), so there is no address to look up by hand. - The install scripts are run with
shellandcreates, with the same limits as the Docker install of the Ansible article: Ansible knows K3s is installed, not which version.
Then, as usual, a dry run first and the real run after:
ansible-playbook playbook/install_k3s.yaml --ask-vault-pass --check --diff --tags prep
ansible-playbook playbook/install_k3s.yaml --ask-vault-pass
ansible controlplane -a "k3s kubectl get nodes" --ask-vault-pass
Note
The dry run is limited to the preparation play (--tags prep). In --check mode, the
shell tasks don’t run, so on fresh nodes K3s is never installed and there is no token for
slurp to read: the server and agent plays can only be tested for real.
Next steps
This post was an introduction to K3s core concepts and I hope it was useful. In the next blog articles I am going to talk more about actual deployment use cases, such as GitHub runners over a fleet, with some additional tools like ArgoCD. Stay tuned! 🚀
Where this post fits
This post, its tags, and everything they connect to. Open the full graph →
Nothing to map yet — tag a post, or link two posts together, to see them here.
No posts or tags match your search.