Skip to content

Phase 3 — K3s

Objective

Set up a K3s (lightweight Kubernetes distribution) management cluster on 3 nodes (control plane on deborah, workers on invincible and oliver), with Argo CD controller ready for GitOps.

What You Learn

K3s as a lightweight bare-metal Kubernetes: server/agent, CNI (Container Network Interface) configurable (Flannel by default in K3s; in this homelab, Calico), disable Traefik/ServiceLB to control the Gateway and LoadBalancer via GitOps later.

Learn this phase

In simple terms: Kubernetes is the manager of a restaurant: you tell it "I want three cooks working at all times" and it takes care of replacing any that are missing. The control plane is the management (decides) and the workers are the kitchen (work). K3s is a version of Kubernetes designed for small teams.

Key concepts

  • Node: a machine in the cluster; can be control plane (decides) or worker (executes).
  • Pod: the smallest unit of execution: one or more containers that live together.
  • Deployment: declares how many copies of a pod should exist and keeps them running.
  • CNI: the component that provides network to pods; here, Calico.
  • kubectl and kubeconfig: the command-line tool and the file with the cluster access data.

Practical challenge (safe): create a test deployment, delete one of its pods, and watch Kubernetes replace it.

kubectl get nodes -o wide
kubectl create deployment demo --image=nginx --replicas=2
kubectl get pods -l app=demo
kubectl delete pod -l app=demo --wait=false
# aparecen pods nuevos
kubectl get pods -l app=demo
# limpieza
kubectl delete deployment demo
What is the difference between a pod and a deployment?

A pod is a running copy; a deployment is the rule that says how many copies should exist and recreates them if they disappear.

What happens if a worker fails?

Kubernetes detects that the pods have stopped responding and recreates them on other healthy nodes.

Why is a CNI required?

Because Kubernetes does not come with its own pod network in this setup: the CNI provides addresses and inter-node communication.

Have questions? Use the Learn with AI button next to each title, or the page Learn.

Stack for this phase

flowchart TB
  subgraph cp [Control_plane]
    DEB[deborah_server]
    VIP[kube_vip_opcional]
  end
  subgraph workers [Workers]
    INV[invincible_agent]
    NOL[oliver_agent]
  end
  DEB --> API[API_6443]
  VIP -.-> API
  INV --> API
  NOL --> API
  DEB --> ArgoCD[ArgoCD_controller]
  • K3s — Management cluster; Phase 3.
  • CNI — Flannel (default), Canal, Calico, Cilium; Phase 3.
  • kube-vip — Optional VIP (virtual IP) :6443; fork Phase 3.
  • Argo CD — Controller bootstrap; Phase 3 → GitOps Phase 4.
  • Ansible — playbook-k3s.yml.

Further reading: Playbook options · Ansible role catalog · K3s manual (legacy)

This decision in other environments

A single control plane on deborah, with the default K3s datastore (SQLite), and two workers. It is simple, uses little memory, and is sufficient for learning; if deborah goes down, the pods continue running but you cannot make changes until it comes back up. If you want high availability later, K3s can switch to embedded etcd.

  • Benefits: Kubernetes provides a unique way to deploy, scale, and recover applications on any server; a small team operates many services with the same manifests, and K3s achieves this with a lightweight binary that installs and updates in minutes.
  • High availability: three control planes are used (an odd number to maintain quorum in etcd), with embedded or external etcd, behind a virtual IP or a load balancer. SQLite does not work with multiple servers.
  • Zero-downtime updates: with multiple control planes, one is updated at a time and the API never goes down.
  • Commercial support: it is common to pay for a distribution with support and security patches, such as RKE2 within SUSE Rancher Prime or Red Hat OpenShift.
  • When K3s is suitable: at the edge (stores, branches, factories, IoT), where there is little hardware, little technical staff on-site, or intermittent connectivity. There, a small and easy-to-replace cluster is worth more than a large one.
Cloud Equivalent service
GCP Google Kubernetes Engine (GKE)
AWS Amazon Elastic Kubernetes Service (EKS)
Azure Azure Kubernetes Service (AKS)
Oracle Cloud (OCI) OCI Kubernetes Engine (OKE)

Gain a managed control plane provided by the cloud provider: high availability, patches, and SLA without you having to manage etcd. You lose control over that control plane, pay for the service, and become more tied to that cloud's integrations.

Before You Start

  • Phases 0, 1, and 2 completed.
  • Healthy Incus cluster (optional but recommended before CAPN —Cluster API Provider for Incus—).

Run

Playbook options are activated in group_vars/incus_cluster/k3s_install.yml, not via command line. Today, Argo CD (gitops_argocd), node labels (node_labels), and local kubeconfig (fetch_kubeconfig) are enabled, with CNI Calico. Review the file and run:

cd ansible
ansible-playbook -i inventory.ini playbook-k3s.yml

Do not use -e k3s_install.<opción>=…

Ansible ignores it: k3s_install is a dictionary and a key with dots in -e does not modify it (the default CNI would be installed without warning). Passing -e with JSON is worse: it replaces the entire dictionary and loses core.version, cluster_cidr, etc. That's why the playbook fails in both cases. To change the CNI use -e k3s_cni=canal (or edit the file); see playbook options.

Manual curl steps in K3s — manual installation. Use only if Ansible is not available; the supported path is the playbook.

Options

If you need… In k3s_install.yml Today Doc
Stable API VIP kube_vip.enabled: true disabled playbook-options
Longhorn/arch labels node_labels.enabled: true enabled playbook-options
Local kubeconfig fetch_kubeconfig.enabled: true enabled playbook-options
OS hardening os_hardening.enabled: true disabled playbook-options
Decision Option A (default) Option B
K3s API Name k3s.homelab.local (variable k3s_api_endpoint) to the control plane IP, today deborah 192.168.20.5 kube-vip 192.168.20.4
CNI Calico (configured in this repo) Flannel vxlan (default of K3s), Canal or Cilium (k3s_install.core.cni)

CNI

CNI Variable Notes
Flannel (default of K3s) cni: flannel Embedded in K3s; flannel_backend: vxlan or host-gw
Canal cni: canal Flannel overlay + Calico policies; role k3s_cni
Calico cni: calico NetworkPolicy, optional BGP; --flannel-backend=none
Cilium cni: cilium eBPF, Hubble; validate on ARM64 (deborah)

The CNI is chosen in k3s_install.core.cni (today: calico), before installing K3s: changing it in an already installed cluster requires reinstalling K3s.

# group_vars/incus_cluster/k3s_install.yml
k3s_install:
  core:
    cni: calico  # flannel | canal | calico | cilium

Details: playbook-options — CNI.

Trade-off analysis
Ansible (playbook-k3s.yml)
Flags in group_vars; role k3s_cni
Requires SSH and order server → CNI → agents
Manual curl
Escape hatch
No idempotence; easy to forget --disable traefik
Fixed IP 192.168.20.5
No extra components
If the CP migrates, agents need reconfiguration
kube-vip 192.168.20.4
Stable API VIP
Additional daemon; different from MetalLB
Flannel
Zero friction; low consumption
No native NetworkPolicy
Canal
Calico policies over overlay
Legacy project; two stacks
Calico
Mature NetworkPolicy; BGP at L2
More pods/RAM; IP pools
Cilium
eBPF; L3–L7 policies; Hubble
More CPU; kernel/BPF; test on deborah
node_labels / fetch_kubeconfig
Longhorn and local kubeconfig ready
Longer play; misapplied labels affect storage

Verify

# En deborah o con kubeconfig
sudo k3s kubectl get nodes
# 3 nodos Ready
sudo k3s kubectl get pods -n argocd
# argocd-server Running (si gitops_argocd.enabled=true)

Wait for pods argocd-server in Running before Phase 4.

If it fails

Symptom Check
Agent does not join Token, K3S_URL, firewall :6443
Residual Traefik --tags traefik or role k3s_cleanup_traefik
CP down after reboot systemctl enable --now k3s in deborah

Next

→ Phase 4 — GitOps