- match the global README.md convention; update the reference in k3s.sh - pad the options table so it reads cleanly in raw and rendered views, kept under the 80-col markdownlint limit
K3S HA Deploy (k3sup + kube-vip + MetalLB)
Deploys a highly-available k3s cluster over SSH with k3sup, a floating
control-plane VIP via kube-vip, and type: LoadBalancer support via
MetalLB.
Following the YouTube tutorial? This script has been modernized since the video — see What changed below.
What it builds
- A 3-server (control-plane) + 2-agent (worker) k3s cluster with embedded etcd.
- kube-vip advertises a single virtual IP for the Kubernetes API across all control-plane nodes (survives a node failure).
- MetalLB hands out real IPs to
type: LoadBalancerservices.
Why both kube-vip and MetalLB?
They solve different problems. kube-vip provides the control-plane VIP
(one address for the API server, HA across masters). MetalLB provides
service load balancing (type: LoadBalancer for your apps). The older
kube-vip cloud-provider that overlapped MetalLB has been removed to avoid
duplicate LoadBalancer controllers.
Prerequisites
-
Ubuntu/Debian nodes (the script installs prerequisites with
apt; on a non-apt distro it exits with a clear message). -
Passwordless sudo for the SSH user on every node. Example cloud-init:
#cloud-config users: - name: <your-user> sudo: ["ALL=(ALL) NOPASSWD:ALL"] groups: [sudo] -
An SSH key pair you can use to reach the nodes (the script distributes the public key and never clobbers your
~/.ssh/config). -
At least 3 control-plane nodes for real HA (etcd needs a quorum).
-
Run the script from an Ubuntu/Debian admin box (it installs
k3supandkubectllocally if missing).
Usage
- Snapshot your VMs.
- Copy your SSH key into your home directory (or into
~/.ssh). - Edit the "YOU SHOULD ONLY NEED TO EDIT THIS SECTION" block in
k3s.sh— node IPs,user,interface,vip,lbrange,certName. chmod +x k3s.sh && ./k3s.sh- Review the pre-flight summary and confirm. Grab a coffee.
Options (environment variables)
| Variable | Purpose |
|---|---|
ASSUME_YES=1 |
Skip the pre-flight [y/N] prompt (unattended runs). |
NO_COLOR=1 |
Disable colored output. |
RAW_BASE=<url> |
Base URL for the sibling manifests (default: main). |
Tracking the latest k3s instead of a pinned version
In the config block set k3sChannel="stable" and leave k3sVersion="".
The script then always installs the current stable k3s release.
Upgrading kube-vip
The kube-vip manifest is generated from the pinned image (its env schema
changes between releases). To bump:
docker run --rm ghcr.io/kube-vip/kube-vip:<version> manifest daemonset \
--interface eth0 --address 10.0.0.254 \
--controlplane --arp --leaderElection --taint --inCluster > kube-vip
sed -i 's/value: eth0/value: REPLACE_INTERFACE/' kube-vip
sed -i 's/value: 10.0.0.254/value: REPLACE_VIP/' kube-vip
--inCluster is required on k3s: it makes kube-vip use the kube-vip
ServiceAccount (created by the RBAC the script applies) instead of the
kubeadm /etc/kubernetes/admin.conf kubeconfig, which does not exist on k3s.
Then update KVVERSION in k3s.sh to match.
What changed from the video
- Versions: k3s
v1.35.6+k3s1, kube-vipv1.2.1, MetalLBv0.16.0. - kube-vip now runs on all control-plane nodes, and your kubeconfig
points at the VIP (not master1) — fixes
localhost:8080/API errors. A readiness check confirms the VIP is answering before the script continues. - Removed the redundant kube-vip cloud-provider; MetalLB alone handles
type: LoadBalancer. - MetalLB installs from a single native manifest (which creates its own
metallb-systemnamespace) — the old separate namespace apply and its mismatched MetalLB versions are gone. - k3sup: the join token is fetched once and reused across all nodes, and
k3sup readywaits for the cluster instead of a hand-rolled poll loop. - Safer SSH: host keys are added via
ssh-keyscan; the script no longer overwrites~/.ssh/config. - Hardening:
set -euo pipefail, per-node time sync,aptprerequisite install with a clear message on unsupported distros, arch-awarekubectl, and it is safe to re-run. - Cleaner output: non-blinking step-by-step logging (only the banner
still blinks — for old times' sake), a pre-flight config summary with a
[y/N]confirmation (ASSUME_YES=1to skip), and a final summary listing the API VIP and the assigned LoadBalancer IP.