·2 min read

Building Production-Ready Ansible Playbooks for an HA Kubeadm Cluster

A hands-on guide to building modular, reproducible Ansible playbooks for provisioning a highly available Kubernetes cluster with kubeadm, HAProxy, containerd, and Calico

ansiblekubernetesDevOps

Building Production-Ready Ansible Playbooks for an HA Kubeadm Cluster

Writing Ansible playbooks for Kubernetes looks simple: run a few commands, install binaries, and join nodes. But turning a manual installation into a reproducible, High-Availability (HA) cluster automation tool is a completely different challenge.

I set out to build a hands-on lab environment on Proxmox to experiment with Kubernetes internals using kubeadm. This is the story of how I transitioned from manual node provisioning to a modular Ansible framework.


Phase 1: The Manual Prototype

Before writing a single line of YAML, I needed to understand every manual step required to assemble a cluster.

Using Proxmox, I spun up virtual machines for control planes, workers, and a load balancer node. Following Kubernetes documentation and community guides, I went through the entire lifecycle manually:

  • Disabling swap and configuring kernel modules (overlay, br_netfilter).
  • Setting up containerd with systemd cgroups.
  • Installing pinned versions of kubeadm, kubelet, and kubectl.
  • Provisioning an HAProxy instance to load-balance API server requests across control planes.
  • Running kubeadm init, applying the Calico CNI, and manually executing kubeadm join commands.

Doing it by hand revealed all the edge cases—like race conditions with background apt/dpkg locks and transient networking issues—that my automation would eventually need to handle.


Phase 2: Structuring the Playbooks

Once the manual steps worked, I began converting my shell history into Ansible playbooks. My goal was clear: maintainability and modularity.

Instead of dumping everything into one monolithic playbook file, I researched community best practices, pulled inspiration from Ansible Galaxy, and structured the repository using a role-based architecture.

text
├── ansible.cfg                  # Ansible defaults (inventory path, roles path, fact caching)
├── inventory/                   # Cluster inventory configuration
│   ├── host.ini                 # Active inventory file (referenced by ansible.cfg)
│   ├── hosts.ini.example        # Example inventory file layout
│   └── group_vars/all/          # Global playbook configuration variables
│       ├── calico.yml           # Tigera operator & CNI settings
│       ├── haproxy.yml          # Frontend/backend load balancer configuration
│       ├── hosts.yml            # Host-to-IP/domain mapping definitions
│       ├── kubeadm.yml          # Kubeadm versions, CIDRs, API endpoint
│       └── project.yml          # Project name, domain, kubeconfig paths
├── manifests/                   # Kubernetes resource manifests
│   ├── calico_api_server.yml    # Calico API server extension manifest
│   ├── calico_installation.yml  # Tigera Installation custom resource
│   ├── goldmane.yml             # Calico flow aggregation component
│   └── whisker.yml              # Calico observability component
├── playbooks/                   # Orchestration entry points
│   └── site.yml                 # Master playbook for end-to-end cluster setup
├── roles/                       # Modular Ansible execution tasks
│   ├── bootstrap/               # SSH key deployment, passwordless sudo setup
│   ├── node_prep/               # Kernel modules, sysctl, swap/firewall disable, host entries
│   ├── container_runtime/       # Containerd installation and systemd cgroup config
│   ├── kubernetes/              # Kubernetes APT repository setup, pinned binary install
│   ├── haproxy/                 # HAProxy load balancer for control-plane API HA
│   ├── control_plane/           # Kubeadm init, kubectl config, join token generation
│   ├── cni/                     # Tigera operator & Calico CNI deployment
│   └── join_node/               # Worker and secondary control-plane join routines
├── scripts/
│   └── cleanup.sh               # Full teardown script (reset nodes to pre-install state)
└── tests/
    └── requirements.txt         # Pinned Python dependencies (ansible-core, molecule, etc.)

Key Design & Architecture Choices

1. Granular Role Separation

Every major layer lives in its own role:

  • node_prep: Handles baseline OS setup, kernel parameters, and disables swap.
  • container_runtime: Installs and configures containerd with systemd cgroups (SystemdCgroup = true).
  • haproxy: Sets up a dedicated load balancer on port 6443 to ensure control plane high availability.
  • cni: Deploys the Tigera Operator to manage Calico lifecycle and custom resources.

2. Variable Isolation in group_vars/all

Hardcoding configuration values leads to brittle playbooks. I separated concern variables into dedicated files inside group_vars/all/:

  • Network parameters (Pod CIDR 192.168.0.0/16, Service CIDRs) reside in kubeadm.yml.
  • Versions for kubeadm (v1.30.14) and calico (v3.27) are explicitly pinned to ensure reproducible builds.

Code Quality & Testing

Writing Ansible code is one thing; keeping it clean and error-free is another. To maintain high code quality, I integrated ansible-lint into my workflow.

Running static analysis caught several issues early on:

  • Unnamed tasks and missing module parameters.
  • Deprecated syntax usage.
  • Improper handler triggers for service restarts.
bash
# Running quality checks
ansible-lint playbooks/site.yml

Key Takeaways

  1. Do it manually first: You cannot automate what you don't fully understand. Manual trial runs on Proxmox saved me hours of Ansible debugging.
  2. Lint early, lint often: Tools like ansible-lint enforce consistency and catch syntax gotchas before execution.
  3. Keep variables organized: Centralizing configuration settings makes updating cluster versions or changing subnets effortless.

check out my repo

https://github.com/sajit21/KubeAdm-setup-Ansible