Skip to content

Instantly share code, notes, and snippets.

@ilyesAj
Created February 1, 2026 18:28
Show Gist options
  • Select an option

  • Save ilyesAj/a91b995393613e837662f7377c9de2f0 to your computer and use it in GitHub Desktop.

Select an option

Save ilyesAj/a91b995393613e837662f7377c9de2f0 to your computer and use it in GitHub Desktop.
one command script k8s for ubuntu 22.04/
#!/usr/bin/env bash
# k8s-single-node-bootstrap.sh
# Single-node Kubernetes bootstrap on Ubuntu 22.04 (containerd + kubeadm)
# + optional MetalLB (L2) with configurable address pool (CIDR or range)
set -Eeuo pipefail
########################################
# Defaults (override via env or flags)
########################################
K8S_MINOR_DEFAULT="${K8S_MINOR:-v1.33}" # pkgs.k8s.io repo minor channel
K8S_POD_CIDR_DEFAULT="${POD_CIDR:-10.244.0.0/16}" # Flannel default
CNI_DEFAULT="${CNI:-calico}" # flannel|calico
K8S_VERSION_DEFAULT="${K8S_VERSION:-}" # e.g. "1.30.5-1.1" (apt version). Empty = latest in repo.
CRICTL_DEFAULT="${INSTALL_CRICTL:-true}" # true|false
DRYRUN_DEFAULT="${DRYRUN:-false}" # true|false
FORCE_RESET_DEFAULT="${FORCE_RESET:-false}" # true|false
UNTAINT_CONTROLPLANE_DEFAULT="${UNTAINT_CONTROLPLANE:-true}" # true|false
# MetalLB (L2)
METALLB_DEFAULT="${INSTALL_METALLB:-true}" # true|false
# You MUST set one of these (or default range below). Prefer a range on your LAN.
METALLB_ADDRS_DEFAULT="${METALLB_ADDRESSES:-192.168.8.240-192.168.8.250}" # "A.B.C.D-E.F.G.H" or "A.B.C.D/NN"
METALLB_POOL_NAME_DEFAULT="${METALLB_POOL_NAME:-default-pool}"
METALLB_ADV_NAME_DEFAULT="${METALLB_ADV_NAME:-default-adv}"
METALLB_NS_DEFAULT="metallb-system"
# k9s
INSTALL_K9S_DEFAULT="${INSTALL_K9S:-true}" # true|false
K9S_VERSION_DEFAULT="${K9S_VERSION:-latest}" # latest or v0.xx.x
########################################
# Helpers
########################################
log() { echo -e "[+] $*"; }
warn() { echo -e "[!] $*" >&2; }
die() { echo -e "[x] $*" >&2; exit 1; }
on_err() {
local ec=$?
warn "Error on line ${BASH_LINENO[0]}: '${BASH_COMMAND}' (exit code: $ec)"
warn "Tip: check 'sudo journalctl -u kubelet -xe' and 'kubectl -n kube-system get pods'"
exit "$ec"
}
trap on_err ERR
run() {
if [[ "${DRYRUN}" == "true" ]]; then
echo "[dryrun] $*"
else
eval "$@"
fi
}
need_root() { [[ $EUID -eq 0 ]] || die "Run as root (sudo)."; }
pkg_installed() { dpkg -s "$1" >/dev/null 2>&1; }
svc_enabled_running() { systemctl is-enabled --quiet "$1" && systemctl is-active --quiet "$1"; }
file_contains() { local f="$1" pat="$2"; [[ -f "$f" ]] && grep -qE "$pat" "$f"; }
cluster_exists() { [[ -f /etc/kubernetes/admin.conf ]] && [[ -d /etc/kubernetes/manifests ]]; }
########################################
# Args
########################################
usage() {
cat <<EOF
Usage: sudo $0 [options]
Kubernetes:
--k8s-minor v1.30 Repo channel minor. Default: ${K8S_MINOR_DEFAULT}
--k8s-version 1.30.5-1.1 Pin apt versions for kubelet/kubeadm/kubectl. Default: latest
--pod-cidr 10.244.0.0/16 Pod network CIDR. Default: ${K8S_POD_CIDR_DEFAULT}
--cni flannel|calico CNI plugin. Default: ${CNI_DEFAULT}
--[no-]untaint Allow scheduling on control-plane (single node). Default: ${UNTAINT_CONTROLPLANE_DEFAULT}
--force-reset If cluster exists, run kubeadm reset first. Default: ${FORCE_RESET_DEFAULT}
--install-crictl Install crictl. Default: ${CRICTL_DEFAULT}
MetalLB (LoadBalancer on bare metal):
--[no-]metallb Install MetalLB L2. Default: ${METALLB_DEFAULT}
--metallb-addresses "<range|cidr>" Address pool, e.g. "192.168.1.240-192.168.1.250" or "192.168.1.240/28"
Default: ${METALLB_ADDRS_DEFAULT}
--metallb-pool-name NAME Default: ${METALLB_POOL_NAME_DEFAULT}
--metallb-adv-name NAME Default: ${METALLB_ADV_NAME_DEFAULT}
k9s:
--[no-]k9s Install k9s (Kubernetes TUI). Default: ${INSTALL_K9S_DEFAULT}
--k9s-version VERSION k9s version (e.g. v0.32.5 or latest)
Misc:
--dryrun Print actions without executing. Default: ${DRYRUN_DEFAULT}
-h, --help Show help
Env equivalents:
K8S_MINOR, K8S_VERSION, POD_CIDR, CNI, UNTAINT_CONTROLPLANE, FORCE_RESET, INSTALL_CRICTL,
INSTALL_METALLB, METALLB_ADDRESSES, METALLB_POOL_NAME, METALLB_ADV_NAME, DRYRUN
EOF
}
K8S_MINOR="${K8S_MINOR_DEFAULT}"
K8S_VERSION="${K8S_VERSION_DEFAULT}"
POD_CIDR="${K8S_POD_CIDR_DEFAULT}"
CNI="${CNI_DEFAULT}"
UNTAINT_CONTROLPLANE="${UNTAINT_CONTROLPLANE_DEFAULT}"
FORCE_RESET="${FORCE_RESET_DEFAULT}"
INSTALL_CRICTL="${CRICTL_DEFAULT}"
DRYRUN="${DRYRUN_DEFAULT}"
INSTALL_METALLB="${METALLB_DEFAULT}"
METALLB_ADDRESSES="${METALLB_ADDRS_DEFAULT}"
METALLB_POOL_NAME="${METALLB_POOL_NAME_DEFAULT}"
METALLB_ADV_NAME="${METALLB_ADV_NAME_DEFAULT}"
INSTALL_K9S="${INSTALL_K9S_DEFAULT}"
K9S_VERSION="${K9S_VERSION_DEFAULT}"
while [[ $# -gt 0 ]]; do
case "$1" in
--k8s-minor) K8S_MINOR="$2"; shift 2;;
--k8s-version) K8S_VERSION="$2"; shift 2;;
--pod-cidr) POD_CIDR="$2"; shift 2;;
--cni) CNI="$2"; shift 2;;
--untaint) UNTAINT_CONTROLPLANE="true"; shift;;
--no-untaint) UNTAINT_CONTROLPLANE="false"; shift;;
--force-reset) FORCE_RESET="true"; shift;;
--install-crictl) INSTALL_CRICTL="true"; shift;;
--metallb) INSTALL_METALLB="true"; shift;;
--no-metallb) INSTALL_METALLB="false"; shift;;
--metallb-addresses) METALLB_ADDRESSES="$2"; shift 2;;
--metallb-pool-name) METALLB_POOL_NAME="$2"; shift 2;;
--metallb-adv-name) METALLB_ADV_NAME="$2"; shift 2;;
--k9s) INSTALL_K9S="true"; shift;;
--no-k9s) INSTALL_K9S="false"; shift;;
--k9s-version) K9S_VERSION="$2"; shift 2;;
--dryrun) DRYRUN="true"; shift;;
-h|--help) usage; exit 0;;
*) die "Unknown arg: $1 (use --help)";;
esac
done
need_root
case "${CNI}" in flannel|calico) ;; *) die "--cni must be flannel or calico (got: ${CNI})";; esac
if [[ "${INSTALL_METALLB}" == "true" ]]; then
[[ -n "${METALLB_ADDRESSES}" ]] || die "MetalLB enabled but METALLB_ADDRESSES is empty."
fi
########################################
# Steps
########################################
step_disable_swap() {
log "Disable swap (required by kubelet)"
if swapon --show | grep -q .; then run "swapoff -a"; else log "Swap already off"; fi
if [[ -f /etc/fstab ]] && grep -E '^\s*[^#].*\s+swap\s+' -q /etc/fstab; then
run "sed -i '/\\sswap\\s/ s/^/#/' /etc/fstab"
else
log "fstab swap entries already commented (or none)"
fi
}
step_kernel_modules_sysctl() {
log "Configure kernel modules + sysctl"
if ! file_contains /etc/modules-load.d/k8s.conf '^overlay$'; then
run "cat >/etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF"
else
log "modules-load config already present"
fi
run "modprobe overlay || true"
run "modprobe br_netfilter || true"
if ! file_contains /etc/sysctl.d/k8s.conf 'net.ipv4.ip_forward=1'; then
run "cat >/etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
EOF"
else
log "sysctl config already present"
fi
run "sysctl -p /etc/sysctl.d/k8s.conf"
}
step_install_containerd() {
log "Install/configure containerd"
if ! pkg_installed containerd; then
run "apt-get update -y"
run "apt-get install -y containerd"
else
log "containerd already installed"
fi
if [[ ! -f /etc/containerd/config.toml ]]; then
run "mkdir -p /etc/containerd"
run "containerd config default >/etc/containerd/config.toml"
fi
if grep -qE 'SystemdCgroup\s*=\s*false' /etc/containerd/config.toml; then
run "sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml"
fi
if ! svc_enabled_running containerd; then run "systemctl enable --now containerd"; else log "containerd already running"; fi
}
step_install_k8s_packages() {
log "Install kubeadm/kubelet/kubectl via pkgs.k8s.io (${K8S_MINOR})"
run "apt-get update -y"
run "apt-get install -y apt-transport-https ca-certificates curl gpg"
run "mkdir -p /etc/apt/keyrings"
if [[ ! -f /etc/apt/keyrings/kubernetes-apt-keyring.gpg ]]; then
run "curl -fsSL 'https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb/Release.key' | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg"
else
log "Kubernetes apt keyring already present"
fi
if [[ ! -f /etc/apt/sources.list.d/kubernetes.list ]] || ! grep -q "pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb" /etc/apt/sources.list.d/kubernetes.list; then
run "echo \"deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb/ /\" >/etc/apt/sources.list.d/kubernetes.list"
else
log "Kubernetes apt repo already configured"
fi
run "apt-get update -y"
if [[ -n "${K8S_VERSION}" ]]; then
run "apt-get install -y kubelet='${K8S_VERSION}' kubeadm='${K8S_VERSION}' kubectl='${K8S_VERSION}'"
else
run "apt-get install -y kubelet kubeadm kubectl"
fi
run "apt-mark hold kubelet kubeadm kubectl"
if [[ "${INSTALL_CRICTL}" == "true" ]] && ! pkg_installed cri-tools; then run "apt-get install -y cri-tools"; fi
run "systemctl enable kubelet"
}
step_kubeadm_init() {
log "Initialize single-node cluster (kubeadm)"
if cluster_exists; then
if [[ "${FORCE_RESET}" == "true" ]]; then
warn "Cluster appears to exist. Running kubeadm reset (FORCE_RESET=true)."
run "kubeadm reset -f"
run "rm -rf /etc/cni/net.d /var/lib/cni /var/lib/kubelet /etc/kubernetes || true"
else
log "Cluster already initialized. Skipping kubeadm init."
return 0
fi
fi
run "kubeadm init --pod-network-cidr='${POD_CIDR}'"
}
step_kubectl_adminconfig_for_root() {
log "Configure kubectl for root (/root/.kube/config)"
run "mkdir -p /root/.kube"
run "cp -f /etc/kubernetes/admin.conf /root/.kube/config"
run "chmod 600 /root/.kube/config"
}
wait_apiserver() {
export KUBECONFIG=/root/.kube/config
run "bash -lc 'for i in {1..60}; do kubectl get --raw=/healthz >/dev/null 2>&1 && exit 0; sleep 2; done; exit 1'"
}
cni_already_installed() {
export KUBECONFIG=/root/.kube/config
case "${CNI}" in
flannel)
# Flannel DaemonSet name is kube-flannel-ds in kube-system
kubectl -n kube-system get ds kube-flannel-ds >/dev/null 2>&1
;;
calico)
# Calico varies by version/manifest (calico-system is common)
kubectl -n calico-system get ds calico-node >/dev/null 2>&1 \
|| kubectl -n kube-system get ds calico-node >/dev/null 2>&1
;;
esac
}
step_install_cni() {
log "Install CNI: ${CNI}"
wait_apiserver
if cni_already_installed; then
log "CNI '${CNI}' already installed. Skipping apply."
return 0
fi
case "${CNI}" in
flannel)
run "kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml"
# Wait for flannel DS (best effort)
run "bash -lc 'kubectl -n kube-system rollout status ds/kube-flannel-ds --timeout=180s || true'"
;;
calico)
run "kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml"
# Wait for calico-node DS (namespace differs; best effort)
run "bash -lc 'kubectl -n calico-system rollout status ds/calico-node --timeout=180s || kubectl -n kube-system rollout status ds/calico-node --timeout=180s || true'"
;;
esac
}
step_untaint_controlplane() {
if [[ "${UNTAINT_CONTROLPLANE}" != "true" ]]; then
log "Skipping untaint (UNTAINT_CONTROLPLANE=false)"
return 0
fi
log "Ensure control-plane is schedulable (idempotent)"
export KUBECONFIG=/root/.kube/config
# Get all nodes
local nodes
nodes="$(kubectl get nodes -o name)"
for node in ${nodes}; do
# Remove control-plane taint if present
if kubectl get "${node}" -o jsonpath='{.spec.taints}' \
| grep -q 'node-role.kubernetes.io/control-plane'; then
run "kubectl taint ${node} node-role.kubernetes.io/control-plane-"
else
log "${node}: control-plane taint already absent"
fi
# Remove legacy master taint if present
if kubectl get "${node}" -o jsonpath='{.spec.taints}' \
| grep -q 'node-role.kubernetes.io/master'; then
run "kubectl taint ${node} node-role.kubernetes.io/master-"
else
log "${node}: master taint already absent"
fi
done
}
step_wait_ready() {
log "Wait for node to become Ready"
export KUBECONFIG=/root/.kube/config
for i in {1..120}; do
if kubectl get nodes -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}' \
2>/dev/null | grep -q True; then
log "Node is Ready"
return 0
fi
sleep 2
done
kubectl get nodes -o wide || true
kubectl get pods -A || true
die "Node did not become Ready in time"
}
step_install_metallb() {
if [[ "${INSTALL_METALLB}" != "true" ]]; then
log "Skipping MetalLB (INSTALL_METALLB=false)"
return 0
fi
log "Install MetalLB (L2) + configure address pool: ${METALLB_ADDRESSES}"
wait_apiserver
# Install manifests (idempotent via apply)
run "kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml"
# Wait for pods
run "bash -lc 'for i in {1..90}; do kubectl -n ${METALLB_NS_DEFAULT} get deploy controller >/dev/null 2>&1 && break; sleep 2; done'"
run "bash -lc 'kubectl -n ${METALLB_NS_DEFAULT} rollout status deploy/controller --timeout=180s'"
# speaker is a daemonset (may show 0/1 until node ready/untainted)
run "bash -lc 'kubectl -n ${METALLB_NS_DEFAULT} rollout status ds/speaker --timeout=180s || true'"
# Apply IPAddressPool + L2Advertisement
# MetalLB accepts ranges OR CIDR in addresses list.
export KUBECONFIG=/root/.kube/config
run "kubectl apply -f - <<EOF
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: ${METALLB_POOL_NAME}
namespace: ${METALLB_NS_DEFAULT}
spec:
addresses:
- ${METALLB_ADDRESSES}
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: ${METALLB_ADV_NAME}
namespace: ${METALLB_NS_DEFAULT}
spec:
ipAddressPools:
- ${METALLB_POOL_NAME}
EOF"
}
step_install_k9s() {
if [[ "${INSTALL_K9S}" != "true" ]]; then
log "Skipping k9s install (INSTALL_K9S=false)"
return 0
fi
if command -v k9s >/dev/null 2>&1; then
log "k9s already installed"
return 0
fi
log "Installing k9s"
local ver
if [[ "${K9S_VERSION}" == "latest" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/derailed/k9s/releases/latest \
| grep tag_name | cut -d '"' -f4)"
else
ver="${K9S_VERSION}"
fi
[[ -n "${ver}" ]] || die "Unable to determine k9s version"
run "curl -fsSL -o /tmp/k9s.tar.gz https://github.com/derailed/k9s/releases/download/${ver}/k9s_Linux_amd64.tar.gz"
run "tar -xzf /tmp/k9s.tar.gz -C /tmp k9s"
run "install -m 0755 /tmp/k9s /usr/local/bin/k9s"
run "rm -f /tmp/k9s /tmp/k9s.tar.gz"
log \"k9s ${ver} installed\"
}
step_summary() {
log "Done."
echo
echo "To use kubectl as your non-root user:"
echo " mkdir -p \$HOME/.kube"
echo " sudo cp -i /etc/kubernetes/admin.conf \$HOME/.kube/config"
echo " sudo chown \"\$(id -u):\$(id -g)\" \$HOME/.kube/config"
echo
echo "Checks:"
echo " kubectl get nodes -o wide"
echo " kubectl get pods -A"
echo
if [[ "${INSTALL_METALLB}" == "true" ]]; then
echo "MetalLB pool:"
echo " kubectl -n ${METALLB_NS_DEFAULT} get ipaddresspools,l2advertisements"
echo
echo "Quick test (creates a LoadBalancer service):"
echo " kubectl create deploy nginx --image=nginx"
echo " kubectl expose deploy nginx --port 80 --type LoadBalancer"
echo " kubectl get svc -w"
fi
}
########################################
# Execute
########################################
step_disable_swap
step_kernel_modules_sysctl
step_install_containerd
step_install_k8s_packages
step_kubeadm_init
step_kubectl_adminconfig_for_root
step_install_cni
step_untaint_controlplane
step_wait_ready
step_install_metallb
step_install_k9s
step_summary
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment