AlmaLinux

Deploy HA Kubernetes Cluster on Rocky Linux 10 Using Kubespray

Kubespray is an Ansible-based tool that automates the deployment of production-ready Kubernetes clusters. It handles the full lifecycle – installing container runtimes, etcd, control plane components, and networking plugins across multiple nodes with a single playbook run.

This guide walks through deploying a highly available Kubernetes 1.34 cluster on Rocky Linux 10 / AlmaLinux 10 using Kubespray v2.30.0. The setup uses 3 control plane nodes with HAProxy and Keepalived for API server load balancing, plus 2 worker nodes. These steps also apply to RHEL 10.

Prerequisites

  • 5 servers running Rocky Linux 10 or AlmaLinux 10 with at least 4GB RAM and 2 vCPUs each
  • 1 deployment workstation (can be your laptop or a separate server) with Python 3.10+ and SSH access to all nodes
  • All nodes must have internet access to pull container images
  • SSH key-based authentication configured from the deployment workstation to all 5 nodes
  • A user with sudo privileges on all nodes (we use deploy in this guide)

Our cluster layout:

HostnameIP AddressRole
cp110.0.1.10Control Plane + etcd + HAProxy + Keepalived
cp210.0.1.11Control Plane + etcd + HAProxy + Keepalived
cp310.0.1.12Control Plane + etcd + HAProxy + Keepalived
w110.0.1.20Worker
w210.0.1.21Worker

Virtual IP (VIP) for Keepalived: 10.0.1.100

Step 1: Prepare All Nodes

Run these commands on all 5 nodes. Start by updating the system packages.

sudo dnf -y update

Set proper hostnames on each node.

sudo hostnamectl set-hostname cp1    # Run on 10.0.1.10
sudo hostnamectl set-hostname cp2    # Run on 10.0.1.11
sudo hostnamectl set-hostname cp3    # Run on 10.0.1.12
sudo hostnamectl set-hostname w1     # Run on 10.0.1.20
sudo hostnamectl set-hostname w2     # Run on 10.0.1.21

Add all nodes to /etc/hosts on every server.

sudo vi /etc/hosts

Add these entries:

10.0.1.10 cp1
10.0.1.11 cp2
10.0.1.12 cp3
10.0.1.20 w1
10.0.1.21 w2

Disable swap on all nodes. Kubernetes requires swap to be off.

sudo swapoff -a
sudo sed -i '/swap/d' /etc/fstab

Load required kernel modules and set sysctl parameters on all nodes.

sudo modprobe br_netfilter
sudo modprobe overlay

Make the modules persistent across reboots.

sudo tee /etc/modules-load.d/k8s.conf <<'EOF'
br_netfilter
overlay
EOF

Set the required sysctl parameters.

sudo tee /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
sudo sysctl --system

Step 2: Configure Firewall Rules

Open the required ports on each node. If you need a deeper understanding of firewalld on RHEL-based systems, check that guide first.

On the control plane nodes (cp1, cp2, cp3), open these ports:

sudo firewall-cmd --permanent --add-port=6443/tcp
sudo firewall-cmd --permanent --add-port=2379-2380/tcp
sudo firewall-cmd --permanent --add-port=10250-10252/tcp
sudo firewall-cmd --permanent --add-port=8443/tcp
sudo firewall-cmd --permanent --add-port=179/tcp
sudo firewall-cmd --permanent --add-port=4789/udp
sudo firewall-cmd --permanent --add-rich-rule='rule protocol value="vrrp" accept'
sudo firewall-cmd --reload

On the worker nodes (w1, w2), open these ports:

sudo firewall-cmd --permanent --add-port=10250/tcp
sudo firewall-cmd --permanent --add-port=30000-32767/tcp
sudo firewall-cmd --permanent --add-port=179/tcp
sudo firewall-cmd --permanent --add-port=4789/udp
sudo firewall-cmd --reload

Port reference for Kubernetes components:

PortProtocolPurpose
6443TCPKubernetes API server
2379-2380TCPetcd client and peer communication
10250TCPKubelet API
10251TCPkube-scheduler
10252TCPkube-controller-manager
8443TCPHAProxy frontend for API server
179TCPCalico BGP peering
4789UDPVXLAN overlay network
30000-32767TCPNodePort services

Step 3: Install and Configure Keepalived on Control Plane Nodes

Keepalived provides a floating virtual IP (VIP) that always points to a healthy control plane node. Install it on all three control plane nodes.

sudo dnf install -y keepalived

On cp1 (10.0.1.10) – this is the MASTER node.

sudo vi /etc/keepalived/keepalived.conf

Add the following configuration:

vrrp_script chk_haproxy {
  script "killall -0 haproxy"
  interval 2
  weight 2
}

vrrp_instance VI_1 {
  interface eth0
  state MASTER
  virtual_router_id 51
  priority 101
  advert_int 1
  unicast_src_ip 10.0.1.10
  unicast_peer {
    10.0.1.11
    10.0.1.12
  }
  virtual_ipaddress {
    10.0.1.100
  }
  track_script {
    chk_haproxy
  }
}

On cp2 (10.0.1.11) – BACKUP with priority 100.

sudo vi /etc/keepalived/keepalived.conf

Add the following configuration:

vrrp_script chk_haproxy {
  script "killall -0 haproxy"
  interval 2
  weight 2
}

vrrp_instance VI_1 {
  interface eth0
  state BACKUP
  virtual_router_id 51
  priority 100
  advert_int 1
  unicast_src_ip 10.0.1.11
  unicast_peer {
    10.0.1.10
    10.0.1.12
  }
  virtual_ipaddress {
    10.0.1.100
  }
  track_script {
    chk_haproxy
  }
}

On cp3 (10.0.1.12) – BACKUP with priority 99.

sudo vi /etc/keepalived/keepalived.conf

Add the following configuration:

vrrp_script chk_haproxy {
  script "killall -0 haproxy"
  interval 2
  weight 2
}

vrrp_instance VI_1 {
  interface eth0
  state BACKUP
  virtual_router_id 51
  priority 99
  advert_int 1
  unicast_src_ip 10.0.1.12
  unicast_peer {
    10.0.1.10
    10.0.1.11
  }
  virtual_ipaddress {
    10.0.1.100
  }
  track_script {
    chk_haproxy
  }
}

Replace eth0 with your actual network interface name if different. Check with ip link show.

Start and enable Keepalived on all three control plane nodes.

sudo systemctl enable --now keepalived

Verify the VIP is assigned on the MASTER node.

$ ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:f2:92:fd brd ff:ff:ff:ff:ff:ff
    inet 10.0.1.10/24 brd 10.0.1.255 scope global noprefixroute eth0
       valid_lft forever preferred_lft forever
    inet 10.0.1.100/32 scope global eth0
       valid_lft forever preferred_lft forever

Step 4: Install and Configure HAProxy on Control Plane Nodes

HAProxy load balances API server requests across all three control plane nodes. Install it on all three control plane nodes.

sudo dnf install -y haproxy

The HAProxy configuration is identical on all three control plane nodes. For more details on HAProxy setup, see our guide on installing HAProxy on Rocky Linux.

sudo vi /etc/haproxy/haproxy.cfg

Replace the entire file with this configuration:

global
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon
    stats socket /var/lib/haproxy/stats

defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option                  http-server-close
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000

# Kubernetes API server frontend
frontend apiserver
    bind *:8443
    mode tcp
    option tcplog
    default_backend apiserver

# Round-robin balancing for API server
backend apiserver
    option httpchk GET /healthz
    http-check expect status 200
    mode tcp
    option ssl-hello-chk
    balance roundrobin
    server cp1 10.0.1.10:6443 check
    server cp2 10.0.1.11:6443 check
    server cp3 10.0.1.12:6443 check

Allow HAProxy to bind to non-local addresses (needed for the VIP).

sudo setsebool -P haproxy_connect_any 1

Start and enable HAProxy on all three control plane nodes.

sudo systemctl enable --now haproxy

Verify HAProxy is listening on port 8443.

$ sudo ss -tlnp | grep 8443
LISTEN 0      3000         *:8443       *:*    users:(("haproxy",pid=12345,fd=7))

Step 5: Set Up the Deployment Workstation

All remaining steps run on your deployment workstation – the machine that will drive the Kubespray Ansible playbook. This can be your laptop or a separate server. Install Python 3, pip, and git.

sudo dnf install -y python3 python3-pip git

Generate an SSH key pair if you have not already, and copy it to all cluster nodes.

ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519

Copy the public key to each node. Enter the password when prompted.

for host in 10.0.1.10 10.0.1.11 10.0.1.12 10.0.1.20 10.0.1.21; do
  ssh-copy-id deploy@${host}
done

Verify you can SSH into each node without a password.

for host in 10.0.1.10 10.0.1.11 10.0.1.12 10.0.1.20 10.0.1.21; do
  ssh deploy@${host} hostname
done

Step 6: Clone Kubespray and Install Dependencies

Clone the Kubespray repository and check out the v2.30.0 release tag. If you are new to Ansible and need to install it, follow that guide first.

cd ~
git clone https://github.com/kubernetes-sigs/kubespray.git
cd kubespray
git checkout v2.30.0

Create a Python virtual environment and install the required dependencies.

python3 -m venv venv
source venv/bin/activate
pip install -U pip
pip install -r requirements.txt

This installs Ansible 10.7.0 along with cryptography, jmespath, and netaddr. Verify the installation.

$ ansible --version
ansible [core 2.17.x]
  config file = None
  configured module search path = ['/home/deploy/.ansible/plugins/modules']
  python version = 3.12.x

Step 7: Configure Kubespray Inventory

Copy the sample inventory to create your cluster configuration.

cp -rfp inventory/sample inventory/mycluster

Edit the inventory file to define your nodes.

vi inventory/mycluster/inventory.ini

Set the contents to:

[all]
cp1 ansible_host=10.0.1.10 ip=10.0.1.10
cp2 ansible_host=10.0.1.11 ip=10.0.1.11
cp3 ansible_host=10.0.1.12 ip=10.0.1.12
w1  ansible_host=10.0.1.20 ip=10.0.1.20
w2  ansible_host=10.0.1.21 ip=10.0.1.21

[kube_control_plane]
cp1
cp2
cp3

[etcd]
cp1
cp2
cp3

[kube_node]
w1
w2

[calico_rr]

[k8s_cluster:children]
kube_control_plane
kube_node
calico_rr

Step 8: Customize Kubespray Group Variables

Kubespray stores its configuration in inventory/mycluster/group_vars/. Edit the cluster configuration file first.

vi inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml

Set the container runtime to containerd (default in v2.30.0) and confirm these key settings:

# Container runtime - containerd is the default and recommended option
container_manager: containerd

# Network plugin - Calico provides network policy support
kube_network_plugin: calico

# Cluster name
cluster_name: cluster.local

# Pod and service CIDR ranges
kube_pods_subnet: 10.233.64.0/18
kube_service_addresses: 10.233.0.0/18

Now configure the external load balancer settings. Edit the all.yml file.

vi inventory/mycluster/group_vars/all/all.yml

Add or update these settings to point Kubespray at your HAProxy VIP:

# External load balancer for the API server
apiserver_loadbalancer_domain_name: "k8s-api.example.com"
loadbalancer_apiserver:
  address: 10.0.1.100
  port: 8443

# Disable internal load balancer since we use an external one
loadbalancer_apiserver_localhost: false

Make sure k8s-api.example.com resolves to 10.0.1.100 on all nodes. Add it to /etc/hosts on each node if you do not have DNS.

echo "10.0.1.100 k8s-api.example.com" | sudo tee -a /etc/hosts

Step 9: Deploy Kubernetes with Kubespray

Run the deployment playbook from your workstation. Replace deploy with the SSH user that has sudo access on all nodes.

ansible-playbook -i inventory/mycluster/inventory.ini \
  --become --user=deploy --become-user=root \
  cluster.yml

The deployment takes 15-30 minutes depending on network speed and hardware. Kubespray installs containerd, kubeadm, kubelet, kubectl, etcd, and Calico networking across all nodes. You should see zero failed tasks at the end.

PLAY RECAP *********************************************************************
cp1                        : ok=750  changed=148  unreachable=0    failed=0
cp2                        : ok=650  changed=130  unreachable=0    failed=0
cp3                        : ok=650  changed=130  unreachable=0    failed=0
w1                         : ok=480  changed=90   unreachable=0    failed=0
w2                         : ok=480  changed=90   unreachable=0    failed=0

Step 10: Verify the Kubernetes Cluster

SSH into the first control plane node and check the cluster status. The kubectl cheat sheet is a useful reference for common commands.

ssh [email protected]

Copy the kubeconfig to your user account so you can run kubectl without sudo.

mkdir -p ~/.kube
sudo cp /etc/kubernetes/admin.conf ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config

Check cluster information.

$ kubectl cluster-info
Kubernetes control plane is running at https://k8s-api.example.com:8443
CoreDNS is running at https://k8s-api.example.com:8443/api/v1/namespaces/kube-system/services/coredns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Verify all nodes are in Ready state.

$ kubectl get nodes -o wide
NAME   STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                        KERNEL-VERSION
cp1    Ready    control-plane   10m   v1.34.3   10.0.1.10     <none>        Rocky Linux 10 (Granite Ridge)  6.12.x-0.el10.x86_64
cp2    Ready    control-plane   9m    v1.34.3   10.0.1.11     <none>        Rocky Linux 10 (Granite Ridge)  6.12.x-0.el10.x86_64
cp3    Ready    control-plane   9m    v1.34.3   10.0.1.12     <none>        Rocky Linux 10 (Granite Ridge)  6.12.x-0.el10.x86_64
w1     Ready    <none>          8m    v1.34.3   10.0.1.20     <none>        Rocky Linux 10 (Granite Ridge)  6.12.x-0.el10.x86_64
w2     Ready    <none>          8m    v1.34.3   10.0.1.21     <none>        Rocky Linux 10 (Granite Ridge)  6.12.x-0.el10.x86_64

Check that all system pods are running.

$ kubectl get pods -n kube-system
NAME                                       READY   STATUS    RESTARTS   AGE
calico-kube-controllers-xxxxxxxxxx-xxxxx   1/1     Running   0          8m
calico-node-xxxxx                          1/1     Running   0          8m
calico-node-yyyyy                          1/1     Running   0          8m
calico-node-zzzzz                          1/1     Running   0          8m
coredns-xxxxxxxxxx-xxxxx                   1/1     Running   0          9m
coredns-xxxxxxxxxx-yyyyy                   1/1     Running   0          9m
etcd-cp1                                   1/1     Running   0          10m
etcd-cp2                                   1/1     Running   0          9m
etcd-cp3                                   1/1     Running   0          9m
kube-apiserver-cp1                         1/1     Running   0          10m
kube-apiserver-cp2                         1/1     Running   0          9m
kube-apiserver-cp3                         1/1     Running   0          9m
kube-controller-manager-cp1                1/1     Running   0          10m
kube-controller-manager-cp2                1/1     Running   0          9m
kube-controller-manager-cp3                1/1     Running   0          9m
kube-proxy-xxxxx                           1/1     Running   0          8m
kube-scheduler-cp1                         1/1     Running   0          10m
kube-scheduler-cp2                         1/1     Running   0          9m
kube-scheduler-cp3                         1/1     Running   0          9m
nodelocaldns-xxxxx                         1/1     Running   0          8m

Verify the etcd cluster health.

$ sudo etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/ssl/etcd/ssl/ca.pem \
  --cert=/etc/ssl/etcd/ssl/member-cp1.pem \
  --key=/etc/ssl/etcd/ssl/member-cp1-key.pem \
  endpoint health --cluster
https://10.0.1.10:2379 is healthy: successfully committed proposal: took = 10ms
https://10.0.1.11:2379 is healthy: successfully committed proposal: took = 12ms
https://10.0.1.12:2379 is healthy: successfully committed proposal: took = 11ms

Test a deployment to confirm workloads schedule correctly on worker nodes.

kubectl create deployment nginx-test --image=nginx:latest --replicas=2
kubectl get pods -o wide

The pods should land on w1 and w2. Clean up the test deployment after confirming.

kubectl delete deployment nginx-test

Step 11: Access the Cluster from Your Workstation

Copy the kubeconfig from a control plane node to your deployment workstation so you can manage the cluster remotely.

mkdir -p ~/.kube
scp [email protected]:/etc/kubernetes/admin.conf ~/.kube/config

Make sure k8s-api.example.com resolves to 10.0.1.100 on your workstation as well, then test access.

$ kubectl get nodes
NAME   STATUS   ROLES           AGE   VERSION
cp1    Ready    control-plane   15m   v1.34.3
cp2    Ready    control-plane   14m   v1.34.3
cp3    Ready    control-plane   14m   v1.34.3
w1     Ready    <none>          13m   v1.34.3
w2     Ready    <none>          13m   v1.34.3

Adding or Removing Nodes

Kubespray makes scaling easy. To add a new worker, update the inventory file with the new node details, then run the scale playbook.

ansible-playbook -i inventory/mycluster/inventory.ini \
  --become --user=deploy --become-user=root \
  scale.yml --limit=w3

To remove a node, use the remove-node playbook.

ansible-playbook -i inventory/mycluster/inventory.ini \
  --become --user=deploy --become-user=root \
  remove-node.yml -e "node=w3"

To upgrade the cluster to a newer Kubernetes version, update to a newer Kubespray release, then run the upgrade playbook. See the Kubernetes deployment with Rancher guide if you prefer a UI-based cluster manager instead.

ansible-playbook -i inventory/mycluster/inventory.ini \
  --become --user=deploy --become-user=root \
  upgrade-cluster.yml

Conclusion

You now have a highly available Kubernetes 1.34 cluster running on Rocky Linux 10 with 3 control plane nodes, 3 etcd members, and 2 worker nodes – all deployed through Kubespray. The HAProxy and Keepalived layer ensures API server access survives any single control plane node failure.

For production use, add TLS certificates for ingress, set up cluster monitoring with Prometheus and Grafana, configure persistent storage with a CSI driver, and implement regular etcd backup schedules. Consider enabling Kubernetes RBAC policies and network policies through Calico to enforce workload isolation.

Related Articles

Containers How To Run Graylog Server in Docker Containers Web Hosting Install Drupal on RHEL 10 / Rocky Linux 10 AlmaLinux Install Xfce Desktop on Rocky 8 / AlmaLinux 8 AlmaLinux How to Install Python 3.13 on RHEL 10 | AlmaLinux 10

Press ESC to close