DEV Community

Arnob
Arnob

Posted on

Provisioning a Kubernetes Cluster on Virtual Machines with Kubespray

Kubespray is an open-source tool that deploys production-ready Kubernetes clusters using Ansible. It supports cloud providers, on-premise environments, and hybrid setups.

captionless image

This document explains Kubespray architecture, requirements, installation steps, configuration, operations, and troubleshooting.

Kubespray Architecture

Kubespray uses Ansible playbooks to automate Kubernetes cluster setup.

Key Components

  • Ansible — Configuration management & orchestration
  • kubeadm — Bootstrap Kubernetes control plane
  • Container Runtime — containerd / CRI‑O / Docker (deprecated)
  • etcd — Distributed key‑value store
  • CNI Plugins — Calico, Flannel, Cilium, Weave

Node Types

  • Control Plane Nodes — API server, controller manager, scheduler
  • Worker Nodes — Run application workloads
  • etcd Nodes — Can be standalone or colocated

Supported Platforms

  • Bare metal
  • Virtual machines (VMware, Proxmox)
  • Cloud providers (AWS, GCP, Azure, OpenStack)
  • Hybrid & air‑gapped environments

Prerequisites

System Requirements

  • Ubuntu 20.04 / 22.04 / 24.04 (recommended) / 25.10
  • Minimum 2 CPU, 2 GB RAM per node
  • Root or passwordless sudo access

Networking

  • Unique hostname for each node
  • Full network connectivity between nodes
  • Required ports open (6443, 2379–2380, 10250, etc.)

Control Machine

  • Python = 3.11
  • Ansible ≥ 2.15–2.17
  • SSH access to all nodes

Make the hostname same as kubespray node name. If need to change the vm hostname then

hostnamectl set-hostname master-k8s-cluster
Enter fullscreen mode Exit fullscreen mode

Install required build dependencies

Run this once as root (or with sudo):

sudo apt update
sudo apt install -y \
  build-essential \
  gcc \
  make \
  pkg-config \
  libssl-dev \
  zlib1g-dev \
  libbz2-dev \
  libreadline-dev \
  libsqlite3-dev \
  libffi-dev \
  libncursesw5-dev \
  libgdbm-dev \
  liblzma-dev \
  uuid-dev \
  tk-dev \
  xz-utils \
  libxml2-dev \
  libxmlsec1-dev \
  curl
Enter fullscreen mode Exit fullscreen mode

Install Python 3.11 and Python virtual environment

# installing for ubuntu 25.10
curl https://pyenv.run | bash
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
exec "$SHELL"
pyenv --version
# Output
pyenv 2.6.25
pyenv install --list | grep 3.11
pyenv install 3.11.15
pyenv global 3.11.15
python --version
# Output
Python 3.11.15
Enter fullscreen mode Exit fullscreen mode

Now Cloning Kubespray

git clone https://github.com/kubernetes-sigs/kubespray.git
cd kubespray
Enter fullscreen mode Exit fullscreen mode

Install Kubespray requirements using pip

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Output will be

Collecting ansible==10.7.0 (from -r requirements.txt (line 1))
  Using cached ansible-10.7.0-py3-none-any.whl.metadata (8.0 kB)
Requirement already satisfied: cryptography==46.0.4 in /root/venv/lib/python3.11/site-packages (from -r requirements.txt (line 3)) (46.0.4)
Collecting jmespath==1.1.0 (from -r requirements.txt (line 5))
  Using cached jmespath-1.1.0-py3-none-any.whl.metadata (7.6 kB)
Collecting netaddr==1.3.0 (from -r requirements.txt (line 7))
  Using cached netaddr-1.3.0-py3-none-any.whl.metadata (5.0 kB)
Collecting ansible-core~=2.17.7 (from ansible==10.7.0->-r requirements.txt (line 1))
  Using cached ansible_core-2.17.14-py3-none-any.whl.metadata (7.0 kB)
Requirement already satisfied: cffi>=2.0.0 in /root/venv/lib/python3.11/site-packages (from cryptography==46.0.4->-r requirements.txt (line 3)) (2.0.0)
Requirement already satisfied: jinja2>=3.0.0 in /root/venv/lib/python3.11/site-packages (from ansible-core~=2.17.7->ansible==10.7.0->-r requirements.txt (line 1)) (3.1.6)
Requirement already satisfied: PyYAML>=5.1 in /root/venv/lib/python3.11/site-packages (from ansible-core~=2.17.7->ansible==10.7.0->-r requirements.txt (line 1)) (6.0.3)
Requirement already satisfied: packaging in /root/venv/lib/python3.11/site-packages (from ansible-core~=2.17.7->ansible==10.7.0->-r requirements.txt (line 1)) (26.0)
Requirement already satisfied: resolvelib<1.1.0,>=0.5.3 in /root/venv/lib/python3.11/site-packages (from ansible-core~=2.17.7->ansible==10.7.0->-r requirements.txt (line 1)) (1.0.1)
Requirement already satisfied: pycparser in /root/venv/lib/python3.11/site-packages (from cffi>=2.0.0->cryptography==46.0.4->-r requirements.txt (line 3)) (3.0)
Requirement already satisfied: MarkupSafe>=2.0 in /root/venv/lib/python3.11/site-packages (from jinja2>=3.0.0->ansible-core~=2.17.7->ansible==10.7.0->-r requirements.txt (line 1)) (3.0.3)
Using cached ansible-10.7.0-py3-none-any.whl (51.6 MB)
Using cached jmespath-1.1.0-py3-none-any.whl (20 kB)
Using cached netaddr-1.3.0-py3-none-any.whl (2.3 MB)
Using cached ansible_core-2.17.14-py3-none-any.whl (2.2 MB)
Installing collected packages: netaddr, jmespath, ansible-core, ansible
  Attempting uninstall: ansible-core
    Found existing installation: ansible-core 2.17.4
    Uninstalling ansible-core-2.17.4:
      Successfully uninstalled ansible-core-2.17.4
Successfully installed ansible-10.7.0 ansible-core-2.17.14 jmespath-1.1.0 netaddr-1.3.0
Enter fullscreen mode Exit fullscreen mode

Note: Some time pip can find ansible-core, then run this commend

pip install ansible-core==2.17.4
Enter fullscreen mode Exit fullscreen mode

Issue: Some time can be give this type or error

ERROR! couldn't resolve module/action 'community.general.ini_file'. This often indicates a misspelling, missing collection, or incorrect module path.
The error appears to be in '/root/kubespray/roles/kubernetes/preinstall/tasks/0063-networkmanager-dns.yml': line 2, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
---
- name: NetworkManager | Add nameservers to NM configuration
  ^ here
Enter fullscreen mode Exit fullscreen mode

Solution:

rm -rf venv
python3.11 -m venv venv
source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Hope issue will be resolve.

Issue: Some time error can be like _/var/lib/dpkg/lock-frontend_

failed: [master-k8s-cluster] (item=install) => {"ansible_loop_var": "item", "attempts": 4, "cache_update_time": 1770045867, "cache_updated": false, "changed": false, "item": {"action_label": "install", "packages": {"apparmor": [true], "apparmor-parser": [false], "apt-transport-https": [true], "aufs-tools": [true, false, true], "bash-completion": [], "chrony": [false, false], "conntrack": [true, true, true], "conntrack-tools": [false, true], "container-selinux": [false, true], "containers-basic": [false, true], "curl": [], "device-mapper": [false, true], "device-mapper-libs": [false, true], "e2fsprogs": [], "ebtables": [], "gnupg": [false, false, true], "iproute": [false], "iproute2": [true], "ipset": [false, true], "iptables": [true], "iputils": [false, false, true, true], "iputils-ping": [true, false, true], "ipvsadm": [true, true], "libseccomp": [false], "libseccomp2": [true, true], "libselinux-python": [false], "libselinux-python3": [false], "mergerfs": [false, false], "nftables": [false, true], "nss": [false], "ntp": [false, true], "ntpsec": [false, false], "openssl": [], "python-apt": [true, false], "python-cryptography": [false], "python3-apt": [true, true], "python3-cryptography": [false], "python3-libselinux": [false], "rsync": [], "socat": [], "software-properties-common": [true, true], "tar": [], "unzip": [], "xfsprogs": []}, "state": "present"}, "msg": "'/usr/bin/apt-get -y -o \"Dpkg::Options::=--force-confdef\" -o \"Dpkg::Options::=--force-confold\"       install 'apt-transport-https=3.1.6ubuntu2' 'conntrack=1:1.4.8-2' 'ebtables=2.0.11-6build1' 'ipvsadm=1:1.31-5' 'socat=1.8.0.3-1build1' 'unzip=6.0-28ubuntu7'' failed: E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)\nE: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?\n", "rc": 100, "stderr": "E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)\nE: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?\n", "stderr_lines": ["E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)", "E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?"], "stdout": "", "stdout_lines": []}
Enter fullscreen mode Exit fullscreen mode

Solution: Add user to _sudo visudo_. You must add this.

sudo visudo
Enter fullscreen mode Exit fullscreen mode

Here add

ubuntu ALL=(ALL) NOPASSWD:ALL
Enter fullscreen mode Exit fullscreen mode

Ensure no stuck apt processes
On master:

ps aux | grep -E 'apt|dpkg'
Enter fullscreen mode Exit fullscreen mode

If any running:

sudo killall apt apt-get dpkg || true
Enter fullscreen mode Exit fullscreen mode

Remove stale locks:

sudo rm -f /var/lib/dpkg/lock*
sudo rm -f /var/cache/apt/archives/lock
Enter fullscreen mode Exit fullscreen mode

Fix dpkg:

sudo dpkg --configure -a
Enter fullscreen mode Exit fullscreen mode

After installing Kubespray. Now prepare inventory

cp -rfp inventory/sample inventory/arnobcluster
Enter fullscreen mode Exit fullscreen mode

After that define your nodes name it hosts.yaml

Note: My laptop IP is 192.168.88.188, so this IP is used everywhere.

all:
  vars:
    ansible_user: ubuntu
    ansible_become: true
    ansible_become_user: root
    ansible_become_method: sudo
    ansible_python_interpreter: /usr/bin/python3
  hosts:
    master-k8s-cluster:
      ansible_host: 192.168.88.188
      ip: 192.168.88.188
      access_ip: 192.168.88.188
      ansible_user: ubuntu
  children:
    kube_control_plane:
      hosts:
        master-k8s-cluster:
    etcd:
      hosts:
        master-k8s-cluster:
    k8s_cluster:
      children:
        kube_control_plane:
        kube_node:
    calico_rr:
      hosts: {}
Enter fullscreen mode Exit fullscreen mode

Add or Edit the inventory file:

nano ./inventory/arnobcluster/inventory.ini
Enter fullscreen mode Exit fullscreen mode

Example:

[all]
master ansible_host=192.168.88.188 ip=192.168.88.188
[kube_control_plane]
master
[etcd]
master
[etcd:children]
kube_control_plane
[kube_node]
master
[k8s_cluster:children]
kube_control_plane
kube_node
Enter fullscreen mode Exit fullscreen mode

Configure SSH Access, You must be able to SSH without password.

ssh-keygen
# Output
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/ubuntu/.ssh/id_ed25519): 
Enter passphrase for "/home/ubuntu/.ssh/id_ed25519" (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/ubuntu/.ssh/id_ed25519
Your public key has been saved in /home/ubuntu/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:pE1so002OsHOveRbkNwjg7PEsmcYHqgdZfJTM2VbjOU ubuntu@master-k8s-cluster
The key's randomart image is:
+--[ED25519 256]--+
|        o+o      |
|     . +.+.      |
|  . o * @ E      |
|   * = ^ =       |
|  o * % S o      |
| o o O * = .     |
|. . + + o .      |
|     o   o       |
|        .        |
+----[SHA256]-----+
Enter fullscreen mode Exit fullscreen mode

Now ssh copy id

ssh-copy-id ubuntu@192.168.1.10
# Output
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/ubuntu/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
ubuntu@192.168.88.188's password: 
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'ubuntu@192.168.88.188'"
and check to make sure that only the key(s) you wanted were added.
Enter fullscreen mode Exit fullscreen mode

You can test the ssh

ssh ubuntu@192.168.88.188
Enter fullscreen mode Exit fullscreen mode

If you can access the host, then SSH is working. If not, find out what the issue is.

Now verify Ansible Connectivity

ansible all -i inventory/mycluster/inventory.ini -m ping
Enter fullscreen mode Exit fullscreen mode

Expected Output:

master | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.13"
    },
    "changed": false,
    "ping": "pong"
}
Enter fullscreen mode Exit fullscreen mode

Now Deploy Kubernetes Cluster

ansible-playbook -i inventory/arnobcluster/inventory.ini \
--become --become-user=root \
cluster.yml
Enter fullscreen mode Exit fullscreen mode

After checking many logs and finishing the installation, you need to check the nodes

ubuntu@master-k8s-cluster:~/kubespray$ sudo kubectl get ns
NAME              STATUS   AGE
default           Active   7m17s
kube-node-lease   Active   7m17s
kube-public       Active   7m17s
kube-system       Active   7m17s
Enter fullscreen mode Exit fullscreen mode

After Kubespray installs Kubernetes, the cluster access file (kubeconfig) is located on the control-plane node. You need to export it so kubectl can access the cluster

Copy kubeconfig from Master Node

On the master node, you can get the Kubespray admin configuration.

/etc/kubernetes/admin.conf
Enter fullscreen mode Exit fullscreen mode

So copy the admin.conf file to .kube/config and export it for your user.

mkdir -p $HOME/.kube
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $USER:$USER $HOME/.kube/config
Enter fullscreen mode Exit fullscreen mode

Now Test kubectl

Run:

kubectl get nodes
Enter fullscreen mode Exit fullscreen mode

Expected output:

NAME     STATUS   ROLES           AGE
master   Ready    control-plane   5m
Enter fullscreen mode Exit fullscreen mode

Convert the Kubernetes private IP to a public IP

When you export the Kubernetes kubeconfig, it usually contains the private IP of the control plane (e.g., 192.168.x.x).
To access the cluster from outside the network, you need to replace the private IP with the public IP.

Edit kubeconfig

nano ~/.kube/config
Enter fullscreen mode Exit fullscreen mode

Find the line like:

server: https://192.168.88.188:6443
Enter fullscreen mode Exit fullscreen mode

Change it to your public IP:

server: https://YOUR_PUBLIC_IP:6443
Enter fullscreen mode Exit fullscreen mode

Example:

server: https://34.124.55.90:6443
Enter fullscreen mode Exit fullscreen mode

Open Firewall Port

The Kubernetes API uses port 6443, so allow it.

Example with UFW:

sudo ufw allow 6443/tcp
Enter fullscreen mode Exit fullscreen mode

Verify Access

kubectl get nodes
Enter fullscreen mode Exit fullscreen mode

Output

NAME     STATUS   ROLES           AGE
master   Ready    control-plane   5m
Enter fullscreen mode Exit fullscreen mode

Installing Kubernetes using Kubespray on Ubuntu involves several layers of configuration, including preparing the operating system, ensuring compatible versions of Python and Ansible, configuring SSH access, and validating system requirements such as CPU, memory, and package permissions. Throughout the setup process, common issues may arise, such as Ansible version mismatches, missing collections, permission problems with package managers, and insufficient system resources.

By systematically resolving these issues using the correct Ansible version, installing required Ansible collections, configuring passwordless sudo, and ensuring adequate hardware resources the cluster deployment can proceed successfully. Kubespray simplifies Kubernetes deployment through automation, but it also enforces best practices and preflight checks to maintain cluster stability and reliability.

Overall, with proper environment preparation and dependency management, Kubespray provides a powerful and reproducible way to deploy and manage Kubernetes clusters in production or development environments

Top comments (0)