- Add k3s node provisioning script with version pinning - Add comprehensive K3S_OPERATIONS.md documentation - Add k3s system-upgrade-controller configuration This addresses DEV-221: prevents version skew issues by: 1. Enforcing version pinning when adding new nodes 2. Providing automated provisioning script 3. Setting up automated upgrades via upgrade controller 4. Documenting all cluster operations procedures Co-Authored-By: Paperclip <noreply@paperclip.ing>
9.8 KiB
k3s Cluster Operations
Current Cluster Status
- Version: v1.36.2+k3s1
- Last Updated: 2026-07-06
- Control Plane: k3s-cp-1 (178.105.17.239 / 10.42.1.1)
- Workers: k3s-worker-1, k3s-worker-2, k3s-worker-3, k3s-update-runner
- Update Method: k3s system-upgrade-controller (automated)
Quick Reference
# Check cluster version
kubectl get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion
# Check for version skew
VERSIONS=$(kubectl get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}' | tr ' ' '\n' | sort -u | wc -l)
if [ "$VERSIONS" -gt 1 ]; then echo "⚠️ Version skew detected!"; fi
# View current cluster state
kubectl get nodes -o wide
Adding a New Worker Node
Prerequisites
- Hetzner Cloud server running Ubuntu 24.04 LTS
- SSH access configured
hcloudCLI installed (for firewall management)
Procedure
Use the automated provisioning script:
cd infrastructure/scripts
./provision-k3s-worker.sh <new-node-private-ip> <new-node-public-ip>
Example:
./provision-k3s-worker.sh 10.42.1.6 167.233.121.122
The script will:
- Check current cluster version
- Prompt for Hetzner firewall update
- Verify SSH access
- Install k3s with matching version
- Verify node joined successfully
Manual Steps (if script unavailable)
-
Get cluster version:
CLUSTER_VERSION=$(ssh root@k3s-cp-1 'k3s --version' | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+\+k3s[0-9]+') echo "Cluster version: v${CLUSTER_VERSION}" -
Update Hetzner firewall
fw-k3s:# Add new node's public IP to firewall rules hcloud firewall add-rule fw-k3s --direction in --protocol tcp \ --source-ips <new-node-public-ip>/32 --port anyCritical: Flannel VXLAN uses public IPs for tunnels. If the public IP is not in the firewall, pods on that node cannot reach DNS or other pods.
-
Get K3S join token:
K3S_TOKEN=$(ssh root@178.105.17.239 'cat /var/lib/rancher/k3s/server/node-token') -
Install k3s on new node with version pinning:
ssh root@<new-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}'" -
Verify node joined:
kubectl get nodes kubectl get node <node-name> -o yaml | grep flannel
Upgrading the Cluster
Upgrade Strategy
We use k3s system-upgrade-controller for coordinated, automated upgrades.
The upgrade controller:
- Ensures version consistency across the cluster
- Performs rolling upgrades (one node at a time)
- Respects pod disruption budgets
- Automatically drains and cordons nodes during upgrade
- Prevents version skew
Planning an Upgrade
-
Review k3s release notes:
- Visit: https://github.com/k3s-io/k3s/releases
- Check for breaking changes
- Note any special upgrade instructions
-
Check current version:
kubectl get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion -
Schedule maintenance window (optional for minor upgrades)
Triggering an Upgrade
The system-upgrade-controller watches for Plan updates. To upgrade:
-
Edit the upgrade plan:
kubectl edit plan k3s-server -n system-upgrade kubectl edit plan k3s-agent -n system-upgrade -
Update the version field:
spec: version: v1.37.0+k3s1 # Change to desired version -
Monitor the upgrade:
# Watch nodes being upgraded kubectl get nodes -w # Check upgrade jobs kubectl get jobs -n system-upgrade # View upgrade controller logs kubectl logs -n system-upgrade -l upgrade.cattle.io/controller=system-upgrade-controller -f -
Verify completion:
kubectl get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion kubectl get pods --all-namespaces -o wide
Manual Upgrade (Emergency Only)
If the upgrade controller is unavailable:
Control Plane
ssh root@178.105.17.239
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="vX.Y.Z+k3s1" sh -
systemctl restart k3s
# Wait 5 minutes for control plane to stabilize
Worker Nodes (one at a time)
# Drain node
kubectl drain <worker-node> --ignore-daemonsets --delete-emptydir-data
# Upgrade node
ssh root@<worker-node>
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="vX.Y.Z+k3s1" sh -s - agent
systemctl restart k3s-agent
# Verify and uncordon
kubectl get nodes
kubectl uncordon <worker-node>
# Wait for pods to stabilize before proceeding to next node
kubectl get pods --all-namespaces -o wide | grep <worker-node>
Emergency Rollback
If issues occur after upgrade:
# On affected node
ssh root@<node>
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v1.36.2+k3s1" sh -s - agent
systemctl restart k3s-agent
Then investigate the issue before retrying the upgrade.
k3s System-Upgrade-Controller
Overview
The system-upgrade-controller manages automated k3s upgrades across the cluster.
- Namespace:
system-upgrade - Documentation: https://github.com/k3s-io/k3s-upgrade
- Plans:
k3s-server(control plane),k3s-agent(workers)
Checking Upgrade Status
# View current plans
kubectl get plans -n system-upgrade
# View upgrade jobs
kubectl get jobs -n system-upgrade
# View controller logs
kubectl logs -n system-upgrade -l upgrade.cattle.io/controller=system-upgrade-controller
Configuration
Upgrade plans are in: infrastructure/k3s-upgrade/
To modify upgrade behavior:
kubectl edit plan k3s-server -n system-upgrade
kubectl edit plan k3s-agent -n system-upgrade
Key configuration options:
version: Target k3s versionconcurrency: Number of nodes to upgrade simultaneously (default: 1)cordon: Cordon nodes before upgrade (default: true)drain: Drain pods before upgrade (default: true)
Monitoring and Alerts
Version Skew Detection
Check for version inconsistencies:
#!/bin/bash
VERSIONS=$(kubectl get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}' | tr ' ' '\n' | sort -u)
VERSION_COUNT=$(echo "$VERSIONS" | wc -l)
if [ "$VERSION_COUNT" -gt 1 ]; then
echo "❌ ERROR: k3s version mismatch detected!"
echo "Versions found:"
kubectl get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion
exit 1
fi
echo "✅ All nodes running same k3s version: $VERSIONS"
Prometheus Alerts
If Prometheus is deployed, version skew alerts are configured in:
infrastructure/monitoring/prometheus-alerts/k3s-version-skew.yaml
Troubleshooting
Node Not Joining Cluster
-
Check firewall rules:
hcloud firewall describe fw-k3s # Verify node's public IP is in allowed sources -
Check k3s-agent service:
ssh root@<node> systemctl status k3s-agent journalctl -u k3s-agent -n 100 -
Verify connectivity to control plane:
ssh root@<node> curl -k https://10.42.1.1:6443
Pods Not Scheduling on Node
-
Check node status:
kubectl describe node <node-name> -
Check for taints:
kubectl get node <node-name> -o jsonpath='{.spec.taints}' -
Check flannel:
kubectl get node <node-name> -o yaml | grep flannel kubectl logs -n kube-system -l app=flannel
Version Mismatch After Node Addition
Symptom: New node has different k3s version than cluster.
Fix:
# Cordon node
kubectl cordon <node-name>
# Reinstall with correct version
CLUSTER_VERSION=$(kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.kubeletVersion}' | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+\+k3s[0-9]+')
ssh root@<node>
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v${CLUSTER_VERSION}" sh -s - agent \
--server https://10.42.1.1:6443 \
--token '<token>'
systemctl restart k3s-agent
# Uncordon
kubectl uncordon <node-name>
Best Practices
- Always pin k3s version when adding nodes
- Use the provisioning script to ensure consistency
- Update firewall rules before adding nodes (Flannel requires public IP access)
- Test upgrades in a non-production environment first
- Monitor version consistency regularly
- Use system-upgrade-controller for all upgrades
- Never bypass version checks - version skew causes networking issues
Cluster Architecture
Network Configuration
- Pod CIDR: 10.244.0.0/16
- Service CIDR: 10.96.0.0/12
- CNI: Flannel (VXLAN mode)
- Flannel Backend: Uses public IPs for VXLAN tunnels (not private IPs)
Node CIDRs
| Node | Pod CIDR | Private IP | Public IP |
|---|---|---|---|
| k3s-cp-1 | 10.244.0.0/24 | 10.42.1.1 | 178.105.17.239 |
| k3s-worker-1 | 10.244.1.0/24 | 10.42.1.2 | - |
| k3s-worker-2 | 10.244.2.0/24 | 10.42.1.3 | - |
| k3s-update-runner | 10.244.3.0/24 | 167.233.79.65 | 167.233.79.65 |
| k3s-worker-3 | 10.244.5.0/24 | 10.42.1.5 | 167.233.121.121 |
Critical Firewall Rule
All nodes with public IPs must be in the fw-k3s firewall to allow Flannel VXLAN tunnels. Without this, pods cannot communicate across nodes.
Change History
| Date | Version | Changes | By |
|---|---|---|---|
| 2026-07-06 | v1.36.2+k3s1 | Initial documentation, upgraded from v1.35.5, installed system-upgrade-controller | CTO Agent |
| 2026-06-10 | v1.35.5+k3s1 | Original cluster deployment | - |