Skip to content

Instantly share code, notes, and snippets.

@bachmanity1
Last active August 16, 2026 14:18
Show Gist options
  • Select an option

  • Save bachmanity1/e9e791180aad0084fdf11b9c8f7c044f to your computer and use it in GitHub Desktop.

Select an option

Save bachmanity1/e9e791180aad0084fdf11b9c8f7c044f to your computer and use it in GitHub Desktop.
Install a single-node Kubernetes v1.35 cluster with Longhorn master-head
#!/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