WhatsApp

☸️ Kubernetes · Ansible · IaC

Automating a Production Kubernetes Cluster with Ansible & Vagrant — Beyond minikube

⚠️ Active Development — core cluster automation complete

Most Kubernetes tutorials point you at minikube or k3s — single-binary tools that hide everything Kubernetes actually does. This project takes a different approach: a production-style multi-node cluster provisioned from scratch using Vagrant and configured end-to-end with Ansible, exposing every step that managed services abstract away.

The result is a three-node Kubernetes cluster — one control plane, two workers — bootstrapped with kubeadm, running containerd as the container runtime and Calico for pod networking. Every step is automated. No manual kubectl commands, no hand-editing config files.

Why Not minikube or k3s?

minikube and k3s are excellent for getting something running fast. They are not excellent for learning what Kubernetes actually does. They hide:

Building a cluster with kubeadm forces you to understand all of it. This project automates that process so it's reproducible — but it doesn't hide it.

Cluster Architecture

Ansible Workstation | SSH | ┌─────┴──────┬──────────────┐ ▼ ▼ ▼ k8s-master worker-1 worker-2 192.168.56.10 192.168.56.11 192.168.56.12 │ API Server │ kubelet │ kubelet │ etcd │ kube-proxy │ kube-proxy │ Scheduler │ containerd │ containerd │ Controller └──────────────┘ │ containerd │ ▼ Calico CNI — Pod CIDR: 192.168.0.0/16
NodeIP AddressRole
k8s-master192.168.56.10Control Plane (API Server, etcd, Scheduler, Controller)
worker-1192.168.56.11Worker (kubelet, kube-proxy, containerd)
worker-2192.168.56.12Worker (kubelet, kube-proxy, containerd)

Ansible Role Architecture

The automation is structured into five purpose-built roles, applied in a deliberate sequence so dependencies are always satisfied before they're needed:

common — OS preparation

Runs on all nodes before anything Kubernetes-related. Disables swap (Kubernetes requires it off), loads necessary kernel modules (br_netfilter, overlay), and sets sysctl parameters for container networking:

# Kernel modules required by Kubernetes
- name: Load kernel modules
  modprobe:
    name: "{{ item }}"
    state: present
  loop:
    - overlay
    - br_netfilter

# Required sysctl settings
- name: Set network sysctl params
  sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    sysctl_set: yes
  loop:
    - {key: "net.bridge.bridge-nf-call-iptables",  value: "1"}
    - {key: "net.bridge.bridge-nf-call-ip6tables", value: "1"}
    - {key: "net.ipv4.ip_forward",                  value: "1"}

containerd — container runtime

Installs and configures containerd with SystemdCgroup = true — the setting most tutorials skip, and the one that causes the most silent failures. Without it, kubelet and containerd disagree on cgroup management and the cluster behaves unpredictably.

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true

kubernetes — tooling installation

Adds the Kubernetes apt repository, installs kubeadm, kubelet, and kubectl at a pinned version, and holds them to prevent unintended upgrades that would break the cluster.

- name: Install Kubernetes packages
  apt:
    name:
      - kubeadm={{ k8s_version }}-*
      - kubelet={{ k8s_version }}-*
      - kubectl={{ k8s_version }}-*
    state: present

- name: Hold Kubernetes packages at version
  dpkg_selections:
    name: "{{ item }}"
    selection: hold
  loop: [kubeadm, kubelet, kubectl]

master — control plane bootstrap

Runs kubeadm init with the correct pod CIDR for Calico, generates the kubeconfig, deploys the Calico CNI manifest, and extracts the join token — which gets passed to the worker role automatically via Ansible facts.

- name: Initialize control plane
  command: >
    kubeadm init
    --apiserver-advertise-address={{ ansible_host }}
    --pod-network-cidr=192.168.0.0/16
    --node-name=k8s-master

- name: Extract join command
  command: kubeadm token create --print-join-command
  register: join_command

- name: Store join command for workers
  set_fact:
    k8s_join_command: "{{ join_command.stdout }}"

worker — node enrollment

Uses the join command stored as an Ansible fact to attach each worker to the control plane. Fully automated — no manual token copy-paste.

- name: Join worker to cluster
  command: "{{ hostvars['k8s-master']['k8s_join_command'] }}"

The Hardest Bug — Node IP Misconfiguration

Every node initially registered with the same NAT IP address (10.0.2.15) instead of its private network address. The cluster appeared to form correctly, then failed silently — pods scheduled to workers couldn't communicate, and kubectl get nodes showed all nodes at the same IP.

The root cause: Vagrant creates two network interfaces per VM — a NAT interface (eth0) and a private network interface (eth1). By default, kubelet advertises eth0. The fix is to force kubelet to use the correct interface explicitly:

KUBELET_EXTRA_ARGS=--node-ip={{ ansible_host }}

This is passed via the kubelet configuration file in the Ansible role. It's one of the most common issues in multi-node Vagrant + Kubernetes setups and barely documented in official guides.

Deployment — Three Commands

# 1. Provision the VMs
make up

# 2. Configure the entire cluster
cd ansible && ansible-playbook -i inventory/hosts.ini playbooks/site.yml

# 3. Verify
kubectl get nodes
# NAME         STATUS   ROLES
# k8s-master   Ready    control-plane
# worker-1     Ready    <none>
# worker-2     Ready    <none>

Demo Application

An NGINX deployment runs across both worker nodes, exposed via a Kubernetes Service. This validates the full stack: scheduling, pod networking across nodes (via Calico), and service discovery.

kubectl apply -f kubernetes/demo-app/deployment.yaml
kubectl apply -f kubernetes/demo-app/service.yaml
kubectl get pods -o wide
# Shows pods distributed across worker-1 and worker-2

What's Already Working

Roadmap — What's Coming

Key Lessons

Work With Me

Need help implementing any of this?

I offer consulting for Kubernetes, Linux, AWS, CI/CD pipelines, Docker, Terraform, and monitoring. Let's talk.

Book Free Consultation WhatsApp →