Your First Kubernetes Cluster the Hard Way with Vagrant

If infrastructure-as-code is your priority — everything in a Vagrantfile, committable to Git, reproducible with one command — Vagrant is the right choice for your first Kubernetes cluster. The same 6-VM architecture as the UTM and OrbStack versions, but defined declaratively with the QEMU provider and socket_vmnet for Apple Silicon networking.

This post covers the Vagrant Simple setup. Want to understand the architecture and design decisions before diving in? Read the Vagrant Simple overview first. For help choosing between tools, see the UTM vs Vagrant vs OrbStack comparison. For the full learning path from Simple to HA, see From Simple to HA.

What You’re Building

6 full QEMU-backed VMs defined in a single Vagrantfile, running on the socket_vmnet subnet (auto-detected, typically 192.168.105.0/24). Same architecture as UTM and OrbStack: Vault server with 3-tier PKI, jump/bastion server, dedicated etcd node, one master, two workers. Kubernetes installed the hard way from ARM64 binaries.

VMRoleIP SuffixvCPURAM
vaultPKI & Secrets.1124 GB
jumpBastion / Ansible.1224 GB
etcd-1Key-value store.2122 GB
master-1Control plane.3124 GB
worker-1Worker node.4126 GB
worker-2Worker node.4226 GB
Vagrant dual-NIC architecture: eth1 vmnet carries all cluster traffic, eth0 NAT is internet-only
Each VM gets two interfaces: eth1 (vmnet, 192.168.105.0/24) for all cluster traffic, eth0 (NAT) for internet only. Binding to the wrong interface is the #1 source of mysterious failures.

Total footprint: 12 vCPUs, ~26 GB RAM. You’ll need a Mac with 32 GB+ total system memory.

Why Vagrant

Vagrant’s value is declarative infrastructure. The entire cluster topology lives in a single Vagrantfile — VM names, IPs, resource allocations, provisioning scripts. Commit it to Git, share it with a teammate, and vagrant up --provider=qemu produces an identical cluster on any Apple Silicon Mac with the same prerequisites installed.

The vagrant-qemu plugin provides a QEMU provider that runs natively on ARM64. socket_vmnet (from the Lima project) gives each VM a routable IP on the host network via macOS’s vmnet.framework. Together, they create full VMs with genuine kernel isolation — the same as UTM under the hood, but managed through Vagrant’s declarative abstraction.

The tradeoff: every VM gets two network interfaces (one NAT for internet, one vmnet for inter-VM communication). This dual-NIC design means the Vagrantfile and Ansible inventory must be explicit about which interface to use. Getting this wrong is the #1 source of mysterious failures — nodes appear to join the cluster but can’t communicate. The automation handles this correctly, but it’s worth understanding.

Prerequisites

Mac with Apple Silicon and these installed via Homebrew:

brew install vagrant qemu socket_vmnet ansible
vagrant plugin install vagrant-qemu
sudo brew services start socket_vmnet
pip3 install hvac

socket_vmnet runs as a LaunchDaemon (requires root) to provide vmnet.framework networking for QEMU. The QEMU process itself stays unprivileged.

Deploy

git clone https://github.com/labitlearnit/k8s-vagrant-simple-homelab.git
cd k8s-vagrant-simple-homelab
./k8s-vagrant-simple-homelab.sh

The script runs vagrant up --provider=qemu to create all 6 VMs, configures the jump server as bastion and Ansible controller, then executes the full Ansible deployment from jump: Vault bootstrap, PKI setup, certificate issuance, etcd, control plane, workers, and Calico CNI.

Total time: approximately 6 minutes 33 seconds. The vagrant up phase alone takes about 2 minutes 25 seconds.

What Makes Vagrant Different

Declarative Vagrantfile. All 6 VMs are defined in one file with their roles, IP suffixes, CPU, and RAM allocations. The network prefix is auto-detected from the socket_vmnet LaunchDaemon plist. vagrant up creates, vagrant destroy -f tears down. The Vagrantfile is version-controllable infrastructure.

Dual network interfaces. Each VM gets eth0 (NAT for internet, managed by the QEMU provider) and eth1 (vmnet for inter-VM communication, provided by socket_vmnet). All cluster communication — etcd, API server, kubelet registration — happens on eth1. The Ansible inventory and all systemd unit files are configured to bind to the vmnet IP, not the NAT IP.

Shell provisioner. Unlike UTM (cloud-init ISOs) and OrbStack (cloud-init passed directly), Vagrant uses its built-in shell provisioner to configure each VM after boot — setting hostnames, injecting SSH keys, writing /etc/hosts, configuring the static IP on the vmnet interface via Netplan, and installing role-specific packages.

Accessing Your Cluster

ssh jump
kubectl get nodes -o wide

From jump, reach any other VM: ssh vault, ssh etcd-1, ssh master-1, ssh worker-1. The Vault UI is accessible at http://vault:8200 from your Mac’s browser.

Vagrant-Specific Things to Know

Host key cleanup after recreation. When VMs are destroyed and recreated, they generate new SSH host keys. Stale keys cached on the Mac will cause SSH to reject connections. The deploy script handles this automatically, but manual Ansible runs may need cleanup: remove cached host keys for all VM IPs and delete Ansible’s SSH control sockets at ~/.ansible/cp/.

Wrong interface binding. If Kubernetes components bind to eth0 (NAT) instead of eth1 (vmnet), nodes will appear to join the cluster but can’t communicate. Always verify bind addresses in systemd unit files point to the vmnet IPs.

socket_vmnet bridge. If vagrant status shows VMs as running but SSH fails with “No route to host”, the socket_vmnet bridge may need a restart: sudo brew services restart socket_vmnet. Check that bridge100 exists with ifconfig bridge100.

Duplicate SSH config entries. Both the HA and Simple Vagrant projects add a Host jump block to ~/.ssh/config. Running both creates duplicates. SSH uses the first match, so it still works, but clean up with grep -c "Host jump" ~/.ssh/config if needed.

What’s Next

Once you’ve explored the simple cluster, the Simple to HA learning path maps the full progression. The Vagrant HA setup scales to 11 VMs — the Vagrant HA deep dive covers the Vagrantfile, dual-NIC architecture, and deployment timing in detail.

To understand what the simple cluster is missing, read Why Your Homelab K8s Cluster Isn’t Production-Ready. For the Vault PKI deep dive, see Vault PKI for Kubernetes: 3-Tier CA the Right Way.

The full source code is at github.com/labitlearnit/k8s-vagrant-simple-homelab.

Big tech, small lab. One reel at a time.

Questions, corrections, or want to share how you’re using these repos?

labitlearnit@gmail.com

Enjoyed this post?

Want homelab configs to your email?

Leave a Reply

Discover more from Lab it, learn it

Subscribe now to keep reading and get access to the full archive.

Continue reading