Last active
August 16, 2026 14:18
-
-
Save bachmanity1/e9e791180aad0084fdf11b9c8f7c044f to your computer and use it in GitHub Desktop.
Install a single-node Kubernetes v1.35 cluster with Longhorn master-head
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 | |
| set -Eeuo pipefail | |
| KUBERNETES_MINOR="${KUBERNETES_MINOR:-v1.35}" | |
| POD_CIDR="${POD_CIDR:-10.244.0.0/16}" | |
| FLANNEL_VERSION="${FLANNEL_VERSION:-v0.28.9}" | |
| LONGHORNCTL_VERSION="${LONGHORNCTL_VERSION:-v1.12.1}" | |
| LONGHORN_UI_NODE_PORT="${LONGHORN_UI_NODE_PORT:-30080}" | |
| LONGHORN_NAMESPACE="longhorn-system" | |
| LONGHORN_MANAGER_IMAGE="${LONGHORN_MANAGER_IMAGE:-docker.io/dauren2495/longhorn-manager:localci-20260814-055609-m87b4c88-i183eea2}" | |
| LONGHORN_INSTANCE_MANAGER_IMAGE="${LONGHORN_INSTANCE_MANAGER_IMAGE:-docker.io/dauren2495/longhorn-instance-manager:localci-20260814-055609-m87b4c88-i183eea2}" | |
| if [[ ${EUID} -ne 0 ]]; then | |
| echo "Run this script as root, for example: curl ... | sudo bash" >&2 | |
| exit 1 | |
| fi | |
| if ! command -v dnf >/dev/null 2>&1; then | |
| echo "This script supports RHEL-compatible systems using dnf." >&2 | |
| exit 1 | |
| fi | |
| INSTALL_USER="${SUDO_USER:-root}" | |
| if ! id "${INSTALL_USER}" >/dev/null 2>&1; then | |
| INSTALL_USER=root | |
| fi | |
| INSTALL_HOME="$(getent passwd "${INSTALL_USER}" | cut -d: -f6)" | |
| NODE_IP="${NODE_IP:-$(ip -4 route get 1.1.1.1 | awk '{print $7; exit}')}" | |
| EXTERNAL_KUBECONFIG="${INSTALL_HOME}/kubeconfig-$(hostname -s).yaml" | |
| case "$(uname -m)" in | |
| x86_64) LONGHORNCTL_ARCH=amd64 ;; | |
| aarch64|arm64) LONGHORNCTL_ARCH=arm64 ;; | |
| *) | |
| echo "Unsupported architecture: $(uname -m)" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| echo "[1/10] Preparing the host" | |
| swapoff -a | |
| sed -ri '/^[^#].*[[:space:]]swap[[:space:]]/s/^/# disabled by Kubernetes installer: /' /etc/fstab | |
| cat >/etc/modules-load.d/k8s.conf <<'EOF' | |
| overlay | |
| br_netfilter | |
| EOF | |
| modprobe overlay | |
| modprobe br_netfilter | |
| cat >/etc/sysctl.d/99-kubernetes.conf <<'EOF' | |
| net.bridge.bridge-nf-call-iptables = 1 | |
| net.bridge.bridge-nf-call-ip6tables = 1 | |
| net.ipv4.ip_forward = 1 | |
| EOF | |
| sysctl --system >/dev/null | |
| echo "[2/10] Installing and configuring containerd" | |
| dnf install -y dnf-plugins-core curl ca-certificates iproute tar | |
| if ! dnf repolist --all 2>/dev/null | grep -q '^docker-ce-stable'; then | |
| dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo | |
| fi | |
| dnf install -y containerd.io | |
| mkdir -p /etc/containerd | |
| containerd config default >/etc/containerd/config.toml | |
| sed -ri 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml | |
| sed -ri 's/^disabled_plugins[[:space:]]*=.*$/disabled_plugins = []/' /etc/containerd/config.toml | |
| systemctl enable containerd | |
| systemctl restart containerd | |
| echo "[3/10] Installing Kubernetes ${KUBERNETES_MINOR}" | |
| cat >/etc/yum.repos.d/kubernetes.repo <<EOF | |
| [kubernetes] | |
| name=Kubernetes | |
| baseurl=https://pkgs.k8s.io/core:/stable:/${KUBERNETES_MINOR}/rpm/ | |
| enabled=1 | |
| gpgcheck=1 | |
| gpgkey=https://pkgs.k8s.io/core:/stable:/${KUBERNETES_MINOR}/rpm/repodata/repomd.xml.key | |
| exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni | |
| EOF | |
| dnf install -y kubelet kubeadm kubectl cri-tools --disableexcludes=kubernetes | |
| systemctl enable --now kubelet | |
| CRI_READY=false | |
| for _ in {1..30}; do | |
| if crictl --runtime-endpoint unix:///run/containerd/containerd.sock \ | |
| info >/dev/null 2>&1; then | |
| CRI_READY=true | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [[ "${CRI_READY}" != true ]]; then | |
| echo "Containerd CRI did not become ready:" >&2 | |
| crictl --runtime-endpoint unix:///run/containerd/containerd.sock info | |
| exit 1 | |
| fi | |
| echo "[4/10] Initializing the one-node cluster" | |
| if [[ ! -f /etc/kubernetes/admin.conf ]]; then | |
| kubeadm init \ | |
| --apiserver-advertise-address="${NODE_IP}" \ | |
| --pod-network-cidr="${POD_CIDR}" \ | |
| --kubernetes-version="$(kubeadm version -o short)" | |
| else | |
| echo "Existing kubeadm cluster detected; skipping kubeadm init" | |
| fi | |
| export KUBECONFIG=/etc/kubernetes/admin.conf | |
| mkdir -p "${INSTALL_HOME}/.kube" | |
| install -m 0600 /etc/kubernetes/admin.conf "${INSTALL_HOME}/.kube/config" | |
| chown -R "${INSTALL_USER}:${INSTALL_USER}" "${INSTALL_HOME}/.kube" | |
| # Create a self-contained kubeconfig whose API endpoint is reachable from | |
| # another machine. NODE_IP can be overridden when the detected address is not | |
| # the address clients should use. | |
| kubectl config view --raw --flatten --minify >"${EXTERNAL_KUBECONFIG}" | |
| kubectl config --kubeconfig="${EXTERNAL_KUBECONFIG}" \ | |
| set-cluster kubernetes --server="https://${NODE_IP}:6443" >/dev/null | |
| chmod 0600 "${EXTERNAL_KUBECONFIG}" | |
| chown "${INSTALL_USER}:${INSTALL_USER}" "${EXTERNAL_KUBECONFIG}" | |
| echo "[5/10] Installing Flannel ${FLANNEL_VERSION}" | |
| kubectl apply -f \ | |
| "https://github.com/flannel-io/flannel/releases/download/${FLANNEL_VERSION}/kube-flannel.yml" | |
| # A one-node cluster must allow workloads on its control-plane node. | |
| kubectl taint nodes --all node-role.kubernetes.io/control-plane- 2>/dev/null || true | |
| kubectl taint nodes --all node-role.kubernetes.io/master- 2>/dev/null || true | |
| kubectl wait --for=condition=Ready node --all --timeout=10m | |
| echo "[6/10] Installing longhornctl ${LONGHORNCTL_VERSION}" | |
| LONGHORNCTL_ASSET="longhornctl-linux-${LONGHORNCTL_ARCH}" | |
| LONGHORNCTL_URL="https://github.com/longhorn/cli/releases/download/${LONGHORNCTL_VERSION}" | |
| TMP_DIR="$(mktemp -d)" | |
| trap 'rm -rf "${TMP_DIR}"' EXIT | |
| curl -fsSL -o "${TMP_DIR}/${LONGHORNCTL_ASSET}" \ | |
| "${LONGHORNCTL_URL}/${LONGHORNCTL_ASSET}" | |
| curl -fsSL -o "${TMP_DIR}/${LONGHORNCTL_ASSET}.sha256" \ | |
| "${LONGHORNCTL_URL}/${LONGHORNCTL_ASSET}.sha256" | |
| ( | |
| cd "${TMP_DIR}" | |
| sha256sum -c "${LONGHORNCTL_ASSET}.sha256" | |
| ) | |
| LONGHORNCTL_BIN=/usr/local/bin/longhornctl | |
| install -m 0755 "${TMP_DIR}/${LONGHORNCTL_ASSET}" "${LONGHORNCTL_BIN}" | |
| echo "[7/10] Installing Longhorn prerequisites and restarting kubelet" | |
| kubectl create namespace "${LONGHORN_NAMESPACE}" \ | |
| --dry-run=client -o yaml | kubectl apply -f - | |
| "${LONGHORNCTL_BIN}" \ | |
| --kubeconfig "${KUBECONFIG}" \ | |
| --image "longhornio/longhorn-cli:${LONGHORNCTL_VERSION}" \ | |
| install preflight \ | |
| --enable-spdk \ | |
| --restart-kubelet \ | |
| --restart-kubelet-window=10s \ | |
| --update-packages=false | |
| kubectl wait --for=condition=Ready node --all --timeout=10m | |
| "${LONGHORNCTL_BIN}" \ | |
| --kubeconfig "${KUBECONFIG}" \ | |
| --image "longhornio/longhorn-cli:${LONGHORNCTL_VERSION}" \ | |
| check preflight --enable-spdk | |
| echo "[8/10] Installing Longhorn master-head" | |
| LONGHORN_MANIFEST="${TMP_DIR}/longhorn-master.yaml" | |
| curl -fsSL -o "${LONGHORN_MANIFEST}" \ | |
| https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml | |
| # Install the local-engine manager and instance-manager builds in place of the | |
| # upstream master-head images. | |
| sed -i \ | |
| "s|docker.io/longhornio/longhorn-manager:master-head|${LONGHORN_MANAGER_IMAGE}|g" \ | |
| "${LONGHORN_MANIFEST}" | |
| sed -i \ | |
| "s|docker.io/longhornio/longhorn-instance-manager:master-head|${LONGHORN_INSTANCE_MANAGER_IMAGE}|g" \ | |
| "${LONGHORN_MANIFEST}" | |
| grep -Fq "${LONGHORN_MANAGER_IMAGE}" "${LONGHORN_MANIFEST}" | |
| grep -Fq "${LONGHORN_INSTANCE_MANAGER_IMAGE}" "${LONGHORN_MANIFEST}" | |
| # The upstream default StorageClass requests three replicas. Use one replica | |
| # so dynamically provisioned volumes are schedulable on this one-node cluster. | |
| sed -i '0,/numberOfReplicas: "3"/s//numberOfReplicas: "1"/' "${LONGHORN_MANIFEST}" | |
| kubectl apply -f "${LONGHORN_MANIFEST}" | |
| kubectl rollout status daemonset/longhorn-manager \ | |
| -n "${LONGHORN_NAMESPACE}" --timeout=10m | |
| kubectl rollout status deployment/longhorn-driver-deployer \ | |
| -n "${LONGHORN_NAMESPACE}" --timeout=10m | |
| kubectl rollout status deployment/longhorn-ui \ | |
| -n "${LONGHORN_NAMESPACE}" --timeout=10m | |
| # Keep UI-created volumes at one replica as well. | |
| kubectl patch settings.longhorn.io default-replica-count \ | |
| -n "${LONGHORN_NAMESPACE}" --type=merge \ | |
| -p '{"value":"{\"v1\":\"1\",\"v2\":\"1\"}"}' | |
| echo "[9/10] Exposing the Longhorn UI on NodePort ${LONGHORN_UI_NODE_PORT}" | |
| kubectl patch service longhorn-frontend \ | |
| -n "${LONGHORN_NAMESPACE}" --type=merge \ | |
| -p "{\"spec\":{\"type\":\"NodePort\",\"ports\":[{\"name\":\"http\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":\"http\",\"nodePort\":${LONGHORN_UI_NODE_PORT}}]}}" | |
| if systemctl is-active --quiet firewalld 2>/dev/null; then | |
| firewall-cmd --permanent --add-port=6443/tcp | |
| firewall-cmd --permanent --add-port="${LONGHORN_UI_NODE_PORT}/tcp" | |
| firewall-cmd --reload | |
| fi | |
| echo "[10/10] Configuring shell convenience" | |
| BASHRC="${INSTALL_HOME}/.bashrc" | |
| touch "${BASHRC}" | |
| for line in \ | |
| 'export PATH=$PATH:/usr/local/bin:/usr/sbin' \ | |
| 'source <(kubectl completion bash)' \ | |
| 'alias k=kubectl' \ | |
| 'complete -o default -F __start_kubectl k'; do | |
| grep -Fqx "${line}" "${BASHRC}" || echo "${line}" >>"${BASHRC}" | |
| done | |
| chown "${INSTALL_USER}:${INSTALL_USER}" "${BASHRC}" | |
| echo | |
| echo "Kubernetes: $(kubectl version -o json | sed -n 's/.*\"gitVersion\": *\"\([^\"]*\)\".*/\1/p' | head -1)" | |
| echo "Node: $(kubectl get nodes -o name)" | |
| echo "Longhorn manifest: upstream master" | |
| echo "Manager image: ${LONGHORN_MANAGER_IMAGE}" | |
| echo "IM image: ${LONGHORN_INSTANCE_MANAGER_IMAGE}" | |
| echo "UI: http://${NODE_IP}:${LONGHORN_UI_NODE_PORT}" | |
| echo "Local kubeconfig: ${INSTALL_HOME}/.kube/config" | |
| echo "External kubeconfig: ${EXTERNAL_KUBECONFIG}" | |
| echo | |
| echo "Copy the external kubeconfig to your workstation:" | |
| echo " scp ${INSTALL_USER}@${NODE_IP}:${EXTERNAL_KUBECONFIG} ~/.kube/longhorn-$(hostname -s).yaml" | |
| echo " export KUBECONFIG=~/.kube/longhorn-$(hostname -s).yaml" | |
| echo | |
| echo "If this host is behind an external firewall or security group, allow TCP 6443 and ${LONGHORN_UI_NODE_PORT}." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment