KVM Deployment of Kubernetes Cluster Operations Manual
Preface
This manual provides detailed steps for deploying a complete Kubernetes cluster using KVM on Ubuntu 22.04 LTS system.
Environment Requirements
Hardware Configuration
CPU: Supports hardware virtualization (Intel VT-x or AMD-V must be enabled)
Memory: Minimum 8GB (2GB per node)
Storage: Minimum 120GB free space
Network: Stable network connection supporting virtual bridges
Software Versions
OS: Ubuntu 22.04 LTS
Kubernetes: v1.35.6
Container Runtime: containerd (latest stable version)
Note on version support : The Kubernetes project only maintains the latest three minor releases.
As of 2026-08 those are v1.34-v1.36. The v1.32 pin in earlier versions of this manual is end-of-life and
no longer receives security updates. This manual targets v1.35 (supported until 2027-02). Adjust the
v1.35 in the apt repo paths and --kubernetes-version below if you track a newer release.
1. Basic Environment Preparation
1.1 Environment Variable Configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export X_DIR="/opt/k8s" export X_DOWNLOAD_DIR="${X_DIR} /download" export X_IMG_DIR="${X_DIR} /images" export X_CFG_DIR="${X_DIR} /configs" export X_NET="k8s-net" export X_OS_VARIANT="ubuntu22.04" export X_BASE_IMG="${X_DOWNLOAD_DIR} /jammy-server-cloudimg-amd64.img" export QCOW2_URL="https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" sudo mkdir -p $X_DOWNLOAD_DIR $X_IMG_DIR $X_CFG_DIR sudo chown -R $USER :$USER $X_DIR [[ ! -f ${X_BASE_IMG} ]] && curl -fsSL -o "${X_BASE_IMG} " "${QCOW2_URL} " sudo virsh net-destroy ${X_NET} 2>/dev/nullsudo virsh net-undefine ${X_NET} 2>/dev/nullsudo virsh destroy k8s-cp-01 2>/dev/nullsudo virsh destroy k8s-worker-01 2>/dev/nullsudo virsh destroy k8s-worker-02 2>/dev/nullsudo virsh undefine k8s-cp-01 --remove-all-storage 2>/dev/nullsudo virsh undefine k8s-worker-01 --remove-all-storage 2>/dev/nullsudo virsh undefine k8s-worker-02 --remove-all-storage 2>/dev/nullsudo rm -rf ${X_CFG_DIR} /* ${X_IMG_DIR} /*
1.2 Install Required Components
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 sudo apt-get update && sudo apt-get upgrade -ysudo apt-get install -y \ qemu-system-x86 \ libvirt-daemon-system \ libvirt-clients \ bridge-utils \ virt-manager \ virtinst \ cloud-image-utils \ wget \ curl kvm-ok sudo systemctl enable --now libvirtdsudo systemctl status libvirtdlsmod | grep kvm
2. Virtual Network Configuration
2.1 Create Dedicated Network
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cat << EOF > ${X_CFG_DIR}/k8s-network.xml <network> <name>${X_NET}</name> <forward mode="nat"/> <bridge name="virbr-k8s" stp="on" delay="0"/> <ip address="192.168.122.1" netmask="255.255.255.0"> <dhcp> <range start="192.168.122.2" end="192.168.122.254"/> </dhcp> </ip> </network> EOF sudo virsh net-define ${X_CFG_DIR} /k8s-network.xmlsudo virsh net-start ${X_NET} sudo virsh net-autostart ${X_NET} sudo virsh net-list --all
3. Virtual Machine Deployment
3.1 Create Cloud-Init Configuration File
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 [[ ! -f ~/.ssh/id_ed25519 ]] && ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" cat << EOF > ${X_CFG_DIR}/cloud-init.yml #cloud-config # Basic system configuration hostname: myhost fqdn: myhost.example.com # User setup configuration users: - name: ubuntu sudo: ALL=(ALL) NOPASSWD:ALL groups: sudo homedir: /home/ubuntu shell: /bin/bash ssh_authorized_keys: - $(cat ~/.ssh/id_ed25519.pub) # Password setup password: ubuntu chpasswd: expire: false ssh_pwauth: true # Package management package_update: true package_upgrade: true packages: - curl - apt-transport-https - ca-certificates - gnupg - containerd.io write_files: - path: /etc/sysctl.d/k8s.conf content: | net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 - path: /etc/modules-load.d/k8s.conf content: | overlay br_netfilter - path: /etc/containerd/certs.d/docker.io/hosts.toml content: | server = "https://docker.io" [host."https://docker.m.daocloud.io"] capabilities = ["pull", "resolve"] [host."https://dockerproxy.net"] capabilities = ["pull", "resolve"] - path: /etc/containerd/certs.d/registry.k8s.io/hosts.toml content: | server = "https://registry.k8s.io" [host."https://k8s.m.daocloud.io"] capabilities = ["pull", "resolve"] [host."https://k8s.dockerproxy.net"] capabilities = ["pull", "resolve"] - path: /etc/containerd/certs.d/gcr.io/hosts.toml content: | server = "https://gcr.io" [host."https://gcr.m.daocloud.io"] capabilities = ["pull", "resolve"] [host."https://gcr.dockerproxy.net"] capabilities = ["pull", "resolve"] - path: /etc/containerd/certs.d/ghcr.io/hosts.toml content: | server = "https://ghcr.io" [host."https://ghcr.m.daocloud.io"] capabilities = ["pull", "resolve"] [host."https://ghcr.dockerproxy.net"] capabilities = ["pull", "resolve"] - path: /etc/containerd/config.toml content: | version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] [plugins."io.containerd.grpc.v1.cri".containerd] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true [plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d" # Commands to run at the end of the cloud-init process runcmd: - curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg - sudo chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg - sudo apt-get update - sudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni - sudo apt-mark hold kubelet kubeadm kubectl - sudo systemctl enable --now containerd - sudo systemctl enable --now kubelet - sudo kubeadm config images pull --image-repository=registry.aliyuncs.com/google_containers # Configure apt sources apt: primary: - arches: [default] uri: https://mirrors.aliyun.com/ubuntu/ search: - https://repo.huaweicloud.com/ubuntu/ - https://mirrors.cloud.tencent.com/ubuntu/ - https://mirrors.cernet.edu.cn/ubuntu/ - https://archive.ubuntu.com sources: docker.list: source: deb [arch=amd64] https://mirrors.cernet.edu.cn/docker-ce/linux/ubuntu jammy stable keyid: 0EBFCD88 kubernetes.list: source: deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.35/deb/ / keyid: 0D811D58 power_state: mode: reboot EOF
3.2 Deploy Virtual Machines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 cp ${X_BASE_IMG} ${X_IMG_DIR} /k8s-cp-01.imgcp ${X_BASE_IMG} ${X_IMG_DIR} /k8s-worker-01.imgcp ${X_BASE_IMG} ${X_IMG_DIR} /k8s-worker-02.imgqemu-img resize ${X_IMG_DIR} /k8s-cp-01.img 20G qemu-img resize ${X_IMG_DIR} /k8s-worker-01.img 20G qemu-img resize ${X_IMG_DIR} /k8s-worker-02.img 20G sed -i "s/^hostname: .*/hostname: k8s-cp-01/g" ${X_CFG_DIR} /cloud-init.yml sed -i "s/^fqdn: .*/fqdn: k8s-cp-01.example.com/g" ${X_CFG_DIR} /cloud-init.yml cloud-localds ${X_IMG_DIR} /k8s-cp-01.img.seed ${X_CFG_DIR} /cloud-init.yml sed -i "s/^hostname: .*/hostname: k8s-worker-01/g" ${X_CFG_DIR} /cloud-init.yml sed -i "s/^fqdn: .*/fqdn: k8s-worker-01.example.com/g" ${X_CFG_DIR} /cloud-init.yml cloud-localds ${X_IMG_DIR} /k8s-worker-01.img.seed ${X_CFG_DIR} /cloud-init.yml sed -i "s/^hostname: .*/hostname: k8s-worker-02/g" ${X_CFG_DIR} /cloud-init.yml sed -i "s/^fqdn: .*/fqdn: k8s-worker-02.example.com/g" ${X_CFG_DIR} /cloud-init.yml cloud-localds ${X_IMG_DIR} /k8s-worker-02.img.seed ${X_CFG_DIR} /cloud-init.yml sudo virt-install \ --name k8s-cp-01 \ --memory 2048 \ --vcpus 2 \ --disk path=${X_IMG_DIR} /k8s-cp-01.img,size=20 \ --disk path=${X_IMG_DIR} /k8s-cp-01.img.seed,device=cdrom \ --network network=${X_NET} \ --os-variant ${X_OS_VARIANT} \ --import \ --graphics none \ --noautoconsole sudo virt-install \ --name k8s-worker-01 \ --memory 2048 \ --vcpus 2 \ --disk path=${X_IMG_DIR} /k8s-worker-01.img,size=20 \ --disk path=${X_IMG_DIR} /k8s-worker-01.img.seed,device=cdrom \ --network network=${X_NET} \ --os-variant ${X_OS_VARIANT} \ --import \ --graphics none \ --noautoconsole sudo virt-install \ --name k8s-worker-02 \ --memory 2048 \ --vcpus 2 \ --disk path=${X_IMG_DIR} /k8s-worker-02.img,size=20 \ --disk path=${X_IMG_DIR} /k8s-worker-02.img.seed,device=cdrom \ --network network=${X_NET} \ --os-variant ${X_OS_VARIANT} \ --import \ --graphics none \ --noautoconsole
3.3 Wait for VM Boot
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 K8S_CP_IP=$(sudo virsh domifaddr "k8s-cp-01" | awk '/ipv4/ {print $4}' | cut -d'/' -f1) K8S_WORKER_01_IP=$(sudo virsh domifaddr "k8s-worker-01" | awk '/ipv4/ {print $4}' | cut -d'/' -f1) K8S_WORKER_02_IP=$(sudo virsh domifaddr "k8s-worker-02" | awk '/ipv4/ {print $4}' | cut -d'/' -f1) echo "K8S_CP_IP: ${K8S_CP_IP} " echo "K8S_WORKER_01_IP: ${K8S_WORKER_01_IP} " echo "K8S_WORKER_02_IP: ${K8S_WORKER_02_IP} " sudo sed -i "/k8s-cp-01/d" /etc/hostssudo sed -i "/k8s-worker-01/d" /etc/hostssudo sed -i "/k8s-worker-02/d" /etc/hostsecho "${K8S_CP_IP} k8s-cp-01" | sudo tee -a /etc/hosts >/dev/nullecho "${K8S_WORKER_01_IP} k8s-worker-01" | sudo tee -a /etc/hosts >/dev/nullecho "${K8S_WORKER_02_IP} k8s-worker-02" | sudo tee -a /etc/hosts >/dev/nullssh-keygen -R "k8s-cp-01" >/dev/null 2>&1 ssh-keygen -R "k8s-worker-01" >/dev/null 2>&1 ssh-keygen -R "k8s-worker-02" >/dev/null 2>&1 ssh-keygen -R "${K8S_CP_IP} " >/dev/null 2>&1 ssh-keygen -R "${K8S_WORKER_01_IP} " >/dev/null 2>&1 ssh-keygen -R "${K8S_WORKER_02_IP} " >/dev/null 2>&1 ssh ubuntu@k8s-cp-01 "test -f /var/lib/cloud/instance/boot-finished && echo 'cloud-init completed'" ssh ubuntu@k8s-worker-01 "test -f /var/lib/cloud/instance/boot-finished && echo 'cloud-init completed'" ssh ubuntu@k8s-worker-02 "test -f /var/lib/cloud/instance/boot-finished && echo 'cloud-init completed'" ssh ubuntu@k8s-cp-01 "sudo systemctl is-active containerd" ssh ubuntu@k8s-worker-01 "sudo systemctl is-active containerd" ssh ubuntu@k8s-worker-02 "sudo systemctl is-active containerd"
4. Kubernetes Cluster Initialization
4.1 Control Plane Initialization
NOTES:
EOF with single quote to avoid variable expansion, i.e. it will keep the raw string
EOF with no single quote to expand variables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ssh ubuntu@k8s-cp-01 << EOF sudo kubeadm init \ --image-repository=registry.aliyuncs.com/google_containers \ --kubernetes-version=v1.35.6 \ --apiserver-advertise-address=${K8S_CP_IP} \ --apiserver-bind-port=6443 \ --pod-network-cidr=10.244.0.0/16 \ --service-cidr=169.169.0.0/16 \ --token=abcdef.0123456789abcdef \ --token-ttl=0 EOF ssh ubuntu@k8s-cp-01 << 'EOF' >/dev/null 2>&1 mkdir -p $HOME /.kubesudo cp -i /etc/kubernetes/admin.conf $HOME /.kube/configsudo chown $(id -u):$(id -g) $HOME /.kube/configEOF kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/v0.28.9/Documentation/kube-flannel.yml
4.2 Add Worker Nodes
Join worker nodes:
1 2 3 4 5 6 7 ssh ubuntu@k8s-worker-01 << EOF sudo $(ssh ubuntu@k8s-cp-01 kubeadm token create --print-join-command) EOF ssh ubuntu@k8s-worker-02 << EOF sudo $(ssh ubuntu@k8s-cp-01 kubeadm token create --print-join-command) EOF
(Optional) If kubectl access is required on worker nodes:
1 2 3 4 mkdir -p $HOME /.kubescp control-plane-node:/etc/kubernetes/admin.conf $HOME /.kube/config sudo chown $(id -u):$(id -g) $HOME /.kube/config
5. Cluster Verification
5.1 Basic Functionality Verification
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 kubectl get nodes -o wide kubectl get pods -n kube-system kubectl get pods -n kube-flannel kubectl create deployment nginx-test --image=nginx kubectl expose deployment nginx-test --port=80 --type =NodePort kubectl get pods,deployment,svc -o wide curl -s $(kubectl get svc nginx-test -o jsonpath='{.spec.clusterIP}' ):80 kubectl delete service,deployment nginx-test
6. Maintenance Operations
6.1 Cluster Reset
To redeploy, execute on all nodes:
1 2 3 sudo kubeadm reset -fsudo rm -rf $HOME /.kubesudo rm -rf /etc/cni/net.d
6.2 Clean Virtual Environment
1 2 3 4 5 6 7 8 9 10 11 sudo virsh destroy k8s-cp-01sudo virsh destroy k8s-worker-01sudo virsh destroy k8s-worker-02sudo virsh undefine k8s-cp-01 --remove-all-storagesudo virsh undefine k8s-worker-01 --remove-all-storagesudo virsh undefine k8s-worker-02 --remove-all-storagesudo virsh net-destroy ${X_NET} sudo virsh net-undefine ${X_NET}