Kubespray is an open-source tool that deploys production-ready Kubernetes clusters using Ansible. It supports cloud providers, on-premise environments, and hybrid setups.
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
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
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
Now Cloning Kubespray
git clone https://github.com/kubernetes-sigs/kubespray.git
cd kubespray
Install Kubespray requirements using pip
pip install -r requirements.txt
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
Note: Some time pip can find ansible-core, then run this commend
pip install ansible-core==2.17.4
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
Solution:
rm -rf venv
python3.11 -m venv venv
source venv/bin/activate
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": []}
Solution: Add user to _sudo visudo_. You must add this.
sudo visudo
Here add
ubuntu ALL=(ALL) NOPASSWD:ALL
Ensure no stuck apt processes
On master:
ps aux | grep -E 'apt|dpkg'
If any running:
sudo killall apt apt-get dpkg || true
Remove stale locks:
sudo rm -f /var/lib/dpkg/lock*
sudo rm -f /var/cache/apt/archives/lock
Fix dpkg:
sudo dpkg --configure -a
After installing Kubespray. Now prepare inventory
cp -rfp inventory/sample inventory/arnobcluster
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: {}
Add or Edit the inventory file:
nano ./inventory/arnobcluster/inventory.ini
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
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]-----+
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.
You can test the ssh
ssh ubuntu@192.168.88.188
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
Expected Output:
master | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
Now Deploy Kubernetes Cluster
ansible-playbook -i inventory/arnobcluster/inventory.ini \
--become --become-user=root \
cluster.yml
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
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
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
Now Test kubectl
Run:
kubectl get nodes
Expected output:
NAME STATUS ROLES AGE
master Ready control-plane 5m
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
Find the line like:
server: https://192.168.88.188:6443
Change it to your public IP:
server: https://YOUR_PUBLIC_IP:6443
Example:
server: https://34.124.55.90:6443
Open Firewall Port
The Kubernetes API uses port 6443, so allow it.
Example with UFW:
sudo ufw allow 6443/tcp
Verify Access
kubectl get nodes
Output
NAME STATUS ROLES AGE
master Ready control-plane 5m
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)