Add a Node¶
Add a new machine to the HomeLab as a member of the Incus cluster and as a
K3s worker (lightweight Kubernetes distribution). It's the same sequence as the initial setup
(Stages 0 to 3), but limited to a single node with
--limit, without touching the ones that are already running.
Before you start
- Debian installed on the new machine, with your admin user created
during installation (see which user gets
sudo), and connected to the LAN (local network). - A free IP address in
192.168.20.0/22for the node. - The current cluster running: Incus and K3s with the 3 healthy nodes.
- A name for the node (the current ones follow the series Invincible:
invincible,oliver,deborah). In the examples:nolan.
All commands are run from ansible/.
Day 0: python3, sudo, and your SSH key¶
A freshly installed Debian doesn't have what Ansible needs. Add the current IP
address of the node (the one assigned by DHCP) to bootstrap/inventory.ini, in the
group servidores_debian, and run:
ansible-playbook -i bootstrap/inventory.ini bootstrap/setup_sudo.yml --ask-pass --limit <ip-actual>
ssh-copy-id <usuario-admin>@<ip-actual>
Details: Stage 0 — Day 0.
Node Inventory and Variables¶
In inventory.ini, add the node to its architecture group
(x86_nodes or arm64_nodes) and to the group k3s_agent:
[x86_nodes]
# ... nodos existentes ...
nolan ansible_host=<ip-actual> static_ip=192.168.20.8 ip_static=false bridge_iface=eno1 ram_gb=16 cpu_model="..." k3s_role=agent
[k3s_agent]
invincible
oliver
nolan
bridge_iface is the physical network interface: confirm it on the node with
ip -br link. The groups incus_cluster and the K3s groups are separated on purpose: a new node is agent, never control plane, unless you add it to k3s_server intentionally.
Create host_vars/nolan.yml (copy an existing one as a base):
---
incus_role: "Miembro"
incus_cluster_role: member
incus_cluster_groups: [x86-nodes, default] # arm64-nodes si es ARM64
k3s_role: agent
ip_static: true
k3s_node_labels:
node.longhorn.io/create-default-disk: "false" # "true" si aporta disco a Longhorn
ansible_python_interpreter: /usr/bin/python3
No playbook edits
The join to Incus, the cluster groups, the manual scheduler, the K3s labels, and the DNS are calculated from inventory.ini and host_vars/. No playbook edits are needed for a new node.
If you use automatic OS updates (k3s_install.os_hardening), also add its reboot schedule in group_vars/incus_cluster/k3s_install.yml
(reboot_schedule), different from the others.
Discovery (optional, recommended)¶
Measure the real hardware of the node (RAM, disks, network, temperature) before deciding its role:
The report is saved in reports/nolan.md.
Fixed IP Address¶
First review what will change and then apply it. The playbook has a safety pause because it can cut the SSH connection:
ansible-playbook -i inventory.ini playbook-set-static-ip.yml --limit nolan --check --diff
ansible-playbook -i inventory.ini playbook-set-static-ip.yml --limit nolan
After that, in inventory.ini, change ansible_host to the fixed IP address and
ip_static=true.
Incus Package and Hardening¶
ansible-playbook -i inventory.ini playbook-bootstrap.yml --limit nolan
ansible-playbook -i inventory.ini playbook-hardening.yml --limit nolan --check --diff
ansible-playbook -i inventory.ini playbook-hardening.yml --limit nolan
The hardening disables password login via SSH: if step 1 didn't load your key, the role stops before leaving you out.
Join the Node to the Incus Cluster¶
Include invincible in the --limit: it's the leader that generates the join token and assigns the cluster groups.
Join the Node to K3s¶
Include the control plane node (deborah) in the --limit: from there comes the token
with which the agent joins. With only --limit nolan fails with Missing k3s_token.
K3s Version
The SUC (System Upgrade Controller) updates the
cluster only, but Ansible installs the fixed version of
k3s_install.core.version (group_vars/incus_cluster/k3s_install.yml).
Before this step, put there the version the nodes have today
(kubectl get nodes → column VERSION); if not, the new node joins
with an older version.
# el primero es el control plane
ansible-playbook -i inventory.ini playbook-k3s.yml --limit deborah,nolan
The same playbook applies the k3s_node_labels labels.
DNS and Extras¶
Add the nolan.mco.local record to DNS (the zones are generated from the inventory):
If the node has WiFi (has_wifi: true and wifi_iface in its host_vars),
configure the WiFi backup:
Verify¶
The ssh log in with SSO access: run opkssh login first (requires step
4.12).
# nolan en Ready, misma VERSION que el resto
kubectl get nodes -o wide
# etiquetas de k3s_node_labels
kubectl get node nolan --show-labels
# nolan ONLINE
ssh devops@deborah.homelab.local sudo incus cluster list
ssh devops@deborah.homelab.local sudo incus cluster group show x86-nodes
# 192.168.20.8
dig +short nolan.mco.local @192.168.20.5
If It Fails¶
| Symptom | Check |
|---|---|
setup_sudo.yml doesn't connect |
Current IP in bootstrap/inventory.ini; that Debian still allows root login via SSH with password |
| Lose SSH after fixed IP | Connect with the new IP; check bridge_iface with ip -br link on the machine console |
| Join to Incus fails | --limit without invincible; free space (≥ 20 GB) on the node |
| K3s: Missing k3s_token | --limit without the control plane node (deborah) |
| The node joins with a different K3s version | k3s_install.core.version outdated (see the warning in step 7) |