Created ADD_WORKER_NODE.md as a step-by-step checklist for adding new worker nodes to the k3s cluster. This complements the existing K3S_OPERATIONS.md with a focused, actionable guide. Key features: - Pre-flight checklist format - Hetzner Cloud VM creation steps (console and CLI) - SSH key requirements (cto-paperclip, silentmaxx) - Firewall configuration verification - Provisioning script usage - Health verification steps - Documentation update procedure - Rollback instructions - Troubleshooting guide Created for DEV-408 (adding worker node to resolve Stalwart scheduling). Co-Authored-By: Paperclip <noreply@paperclip.ing>
8.6 KiB
Adding a Worker Node to k3s Cluster - Quick Instruction
Purpose: Step-by-step checklist for adding a new worker node to the BasicStack k3s cluster.
Audience: CTO agent, infrastructure operators
Prerequisites:
- Hetzner Cloud access (console or hcloud CLI with credentials)
- kubectl access to the cluster
- SSH access to control plane (178.105.17.239)
Step 1: Create Hetzner Cloud VM
Via Hetzner Cloud Console
- Navigate to Hetzner Cloud Console
- Select the BasicStack project
- Click "Add Server"
- Configure:
- Name:
k3s-worker-N(use next available number) - Location: Match existing cluster (check with
kubectl get nodes -o wide) - Image: Ubuntu 24.04 LTS
- Type: CX32 or larger (minimum 4 vCPU, 8 GB RAM)
- Network: Attach to existing private network (
10.42.1.0/24) - SSH Keys: Select both
cto-paperclipANDsilentmaxx - Firewall: Attach
fw-k3s
- Name:
- Click "Create & Buy now"
- Record the IPs:
- Private IP:
________________(from Hetzner network tab) - Public IP:
________________(from server overview)
- Private IP:
Via hcloud CLI
# List available networks
hcloud network list
# List available SSH keys (verify both keys exist)
hcloud ssh-key list | grep -E 'cto-paperclip|silentmaxx'
# Create the server (adjust location as needed)
hcloud server create \
--name k3s-worker-4 \
--type cx32 \
--image ubuntu-24.04 \
--ssh-key cto-paperclip \
--ssh-key silentmaxx \
--network k3s-network \
--firewall fw-k3s \
--location nbg1
# Record the IPs from output
Step 2: Verify Firewall Configuration
Why: Flannel VXLAN uses public IPs for pod networking tunnels. Without the firewall rule, pods cannot communicate.
# Check if public IP is in fw-k3s
hcloud firewall describe fw-k3s | grep <public-ip>
# If missing, add the rule:
hcloud firewall add-rule fw-k3s \
--direction in \
--protocol tcp \
--source-ips <public-ip>/32 \
--port any
Checklist:
- Firewall rule exists for node's public IP
Step 3: Verify SSH Access
Test connectivity before proceeding:
# Test public IP
ssh root@<public-ip> 'hostname && date'
# Test private IP (if accessible)
ssh root@<private-ip> 'hostname && date'
Checklist:
- SSH access confirmed to public IP
- SSH access confirmed to private IP
Step 4: Run Provisioning Script
Location: infrastructure/scripts/provision-k3s-worker.sh
What it does:
- Detects current k3s cluster version
- Prompts for firewall confirmation
- Retrieves join token from control plane
- Installs k3s with version pinning
- Verifies successful cluster join
Execution:
cd /path/to/stack.basicstack.de/infrastructure/scripts
# Syntax: ./provision-k3s-worker.sh <private-ip> <public-ip>
./provision-k3s-worker.sh <PRIVATE-IP> <PUBLIC-IP>
# Example:
# ./provision-k3s-worker.sh 10.42.1.6 167.233.121.122
During execution:
- Confirm firewall update when prompted (press Enter after verifying Step 2)
- Wait for k3s installation (may take 5-10 minutes)
- Review the final node list output
Checklist:
- Script executed successfully
- No error messages in output
- Node appears in final
kubectl get nodesoutput
Step 5: Verify Node Health
Run these verification commands:
# 1. Check node status (should be Ready)
kubectl get nodes -o wide
# 2. Verify internal IP matches Hetzner private IP
kubectl get node k3s-worker-N -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}'
# Expected: 10.42.1.X (the private IP from Step 1)
# 3. Check k3s version consistency
kubectl get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion
# All nodes should have the same version
# 4. Verify flannel configuration
kubectl get node k3s-worker-N -o yaml | grep flannel
# Should see flannel.alpha.coreos.com annotations
# 5. Check pod CIDR assignment
kubectl get node k3s-worker-N -o jsonpath='{.spec.podCIDR}'
# Should return something like 10.244.X.0/24
Expected Results:
- Node status:
Ready - Internal IP: Matches private IP from Hetzner network
- k3s version: Matches existing nodes
- Flannel annotations: Present
- Pod CIDR: Assigned (10.244.X.0/24)
Checklist:
- Node status is Ready
- Internal IP is correct (Hetzner private IP)
- k3s version matches cluster
- Flannel annotations present
- Pod CIDR assigned
Step 6: Label the Node (Optional)
Add standard worker label:
kubectl label nodes k3s-worker-N node-role.kubernetes.io/worker=worker
Checklist:
- Node labeled
Step 7: Monitor Pod Scheduling
Watch for pods to be scheduled on the new node:
# Check overall pod distribution
kubectl get pods --all-namespaces -o wide | grep k3s-worker-N
# If adding capacity for specific workload (e.g., Stalwart):
kubectl get pod -n stalwart -l app=stalwart -o wide
# Watch for scheduling events
kubectl get events --sort-by='.lastTimestamp' | grep k3s-worker-N
Checklist:
- Pods are being scheduled on new node
- Target workload (if specified) successfully scheduled
Step 8: Update Documentation
Update the cluster architecture table in infrastructure/K3S_OPERATIONS.md:
-
Get the assigned pod CIDR:
kubectl get node k3s-worker-N -o jsonpath='{.spec.podCIDR}' -
Edit
infrastructure/K3S_OPERATIONS.mdand add to the "Node CIDRs" table:| k3s-worker-N | 10.244.X.0/24 | 10.42.1.Y | <public-ip> | -
Commit the change:
cd infrastructure git add K3S_OPERATIONS.md git commit -m "docs: add k3s-worker-N to cluster architecture table Added new worker node with Pod CIDR 10.244.X.0/24. Node added to resolve resource constraints for workload scheduling. Co-Authored-By: Paperclip <noreply@paperclip.ing>" git push
Checklist:
- K3S_OPERATIONS.md updated with new node
- Changes committed and pushed
Success Criteria
All checkboxes above should be completed:
- VM created with correct specifications
- Both SSH keys (cto-paperclip, silentmaxx) added
- Firewall rule configured
- SSH access verified
- Provisioning script executed successfully
- Node status: Ready
- Internal IP matches Hetzner private IP
- k3s version matches cluster
- Flannel annotations present
- Pod CIDR assigned
- Pods scheduling on new node
- Documentation updated
Rollback Procedure
If issues occur and the node needs to be removed:
# 1. Drain the node (move all pods off)
kubectl drain k3s-worker-N --ignore-daemonsets --delete-emptydir-data --timeout=5m
# 2. Delete from cluster
kubectl delete node k3s-worker-N
# 3. Remove firewall rule (get rule ID first)
hcloud firewall describe fw-k3s
hcloud firewall delete-rule fw-k3s <rule-id>
# 4. Delete Hetzner VM
hcloud server delete k3s-worker-N
Troubleshooting
Node Not Appearing in kubectl get nodes
Check:
- Firewall rule is configured (Step 2)
- k3s-agent service is running on the node:
ssh root@<node-ip> 'systemctl status k3s-agent' - Connectivity to control plane:
ssh root@<node-ip> 'curl -k https://10.42.1.1:6443'
Internal IP Shows Public IP Instead of Private IP
Cause: Node not properly connected to Hetzner private network.
Fix:
- Verify network attachment in Hetzner Cloud Console
- Ensure private network interface is up:
ssh root@<node-ip> 'ip addr show' - May need to recreate the VM with network properly attached
k3s Version Mismatch
Cause: Provisioning script couldn't detect cluster version, or manual installation was attempted.
Fix:
- Cordon the node:
kubectl cordon k3s-worker-N - Get correct version:
CLUSTER_VERSION=$(kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.kubeletVersion}' | sed 's/v//') - Reinstall with correct version:
K3S_TOKEN=$(ssh root@178.105.17.239 'cat /var/lib/rancher/k3s/server/node-token') ssh root@<node-ip> "curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION='v${CLUSTER_VERSION}' sh -s - agent \ --server https://10.42.1.1:6443 \ --token '${K3S_TOKEN}'" - Uncordon:
kubectl uncordon k3s-worker-N
Reference Links
- K3S_OPERATIONS.md - Full operational guide
- CLUSTER_ACCESS.md - Access and authentication
- Hetzner Cloud Docs
- k3s Documentation
Last Updated: 2026-07-26 Maintained By: CTO Agent