Ansible playbooks to provision and configure a production-style Kubernetes cluster on Rocky Linux using containerd as the container runtime and Flannel as the CNI plugin.
Built with Ansible | Kubernetes 1.32.x | containerd | Flannel CNI
| Machine | Hostname | Role |
|---|---|---|
| Ansible Control Node | sam.example.com |
Ansible, kubectl |
| Control Plane | kube-master01.example.com |
Kubernetes Control Plane |
| Worker Node 1 | kube-worker01.example.com |
Kubernetes Worker |
| Worker Node 2 | kube-worker02.example.com |
Kubernetes Worker |
| Component | Choice | Reason |
|---|---|---|
| Kubernetes | 1.32.x | Latest stable |
| Container Runtime | containerd | Recommended for K8s 1.32 |
| CNI Plugin | Flannel | Simple, reliable for small clusters |
| OS | Rocky Linux 9 | RHEL-compatible, enterprise grade |
k8s-cluster/
├── ansible.cfg
├── site.yml # Master playbook — runs everything in order
├── common.yml
├── containerd.yml
├── kubernetes.yml
├── master.yml
├── worker.yml
├── inventory/
│ └── hosts.ini
├── group_vars/
│ └── k8s_cluster.yml
└── roles/
├── common/tasks/main.yml
├── containerd/tasks/main.yml
├── containerd/handlers/main.yml
├── kubernetes/tasks/main.yml
├── master/tasks/main.yml
└── worker/tasks/main.yml
On each Kubernetes node, configure passwordless sudo for the Ansible user:
echo "ansible ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/ansible
sudo chmod 0440 /etc/sudoers.d/ansibleCopy the SSH key from the control node to each Kubernetes node:
ssh-copy-id ansible@kube-master01.example.com
ssh-copy-id ansible@kube-worker01.example.com
ssh-copy-id ansible@kube-worker02.example.comansible k8s_cluster -m pingExpected: pong from all nodes with no errors.
Update with your actual hostnames or IPs:
[k8s_master]
kube-master01.example.com
[k8s_workers]
kube-worker01.example.com
kube-worker02.example.com
[k8s_cluster:children]
k8s_master
k8s_workersThe group names in
[k8s_cluster:children]must exactly match the group names defined above.
---
kubernetes_version: "1.32"
pod_network_cidr: "10.244.0.0/16"
flannel_manifest_url: "https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml"
pod_network_cidrmust be10.244.0.0/16for Flannel. Do not change this value without also reconfiguring Flannel itself.
| Option | Purpose |
|---|---|
remote_user |
SSH user on all target hosts |
host_key_checking = False |
Skips SSH fingerprint prompts |
result_format = yaml |
Cleaner, more readable output |
callbacks_enabled |
timer prints total runtime; profile_tasks shows time per task |
pipelining = True |
Combines SSH operations for faster runs |
ssh_args |
Reuses SSH connections for 60 seconds per task |
ansible-playbook site.ymlansible-playbook common.yml # Phase 1 — baseline OS config
ansible-playbook containerd.yml # Phase 2 — container runtime
ansible-playbook kubernetes.yml # Phase 3 — K8s packages
ansible-playbook master.yml worker.yml # Phase 4 — init cluster and join workers
master.ymlandworker.ymlmust be run together so the join token fact is available to the worker role.
kubectl get nodesExpected output:
NAME STATUS ROLES AGE VERSION
kube-master01 Ready control-plane Xm v1.32.x
kube-worker01.example.com Ready worker Xm v1.32.x
kube-worker02.example.com Ready worker Xm v1.32.x
kubectl get pods -ARuns on all nodes. Loads required kernel modules (overlay, br_netfilter), sets sysctl parameters, confirms swap is disabled, opens firewall ports, adds cluster nodes to /etc/hosts, and configures firewalld masquerading and trusted CNI interfaces.
Adds the Docker CE repository, installs containerd.io, generates the default config, and enables SystemdCgroup = true — required for Kubernetes 1.32 on Rocky Linux.
Adds the Kubernetes yum repository, installs kubelet, kubeadm, and kubectl, enables kubelet, and configures crictl to use the containerd socket.
Initializes the control plane with kubeadm init, sets up kubeconfig for root and the Ansible user, fetches kubeconfig to the local control node, applies the Flannel CNI manifest, waits for the node to become Ready, and generates the worker join token.
Joins each worker node to the cluster using the join command stored as a fact on the master, confirms kubelet is running, and labels the node with node-role.kubernetes.io/worker=worker.
| Port | Protocol | Purpose |
|---|---|---|
| 6443 | TCP | Kubernetes API Server |
| 2379 | TCP | etcd client |
| 2380 | TCP | etcd peer |
| 10250 | TCP | Kubelet API |
| 10251 | TCP | kube-scheduler |
| 10252 | TCP | kube-controller-manager |
| 8472 | UDP | Flannel VXLAN |
| Port | Protocol | Purpose |
|---|---|---|
| 10250 | TCP | Kubelet API |
| 8472 | UDP | Flannel VXLAN |
| 30000-32767 | TCP | NodePort Services |
If pods cannot reach other pods on different nodes, DNS lookups time out from inside pods, or the kube-dns service IP is unreachable, the most common causes are:
Masquerade disabled — firewalld NAT was off. Without it return traffic from the service network cannot get back to the originating pod:
ansible k8s_cluster -m command -a "firewall-cmd --add-masquerade --permanent" --become
ansible k8s_cluster -m command -a "firewall-cmd --reload" --becomeCNI interfaces not in trusted zone — cni0 and flannel.1 were unassigned, causing firewalld to block pod traffic crossing those interfaces:
ansible k8s_cluster -m command -a "firewall-cmd --zone=trusted --add-interface=cni0 --permanent" --become
ansible k8s_cluster -m command -a "firewall-cmd --zone=trusted --add-interface=flannel.1 --permanent" --become
ansible k8s_cluster -m command -a "firewall-cmd --reload" --becomeVerify DNS resolution:
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- nslookup kubernetes.default.svc.cluster.localBoth fixes are now incorporated into the common role and run automatically.
MIT