Created
February 1, 2026 18:28
-
-
Save ilyesAj/a91b995393613e837662f7377c9de2f0 to your computer and use it in GitHub Desktop.
one command script k8s for ubuntu 22.04/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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