Writing Idempotent Ansible Playbooks for Kubernetes Clusters

The Challenge: Mixing Ansible with Kubernetes
Kubernetes handles cluster state declaratively through its API, but relying on Ansible for cluster bootstrap, secret distribution, or external infrastructure configuration can regularly lead to imperative patterns. A common pitfall is falling back on Ansible's command or shell modules to execute raw kubectl apply invocations. Executing arbitrary shell commands breaks idempotency because Ansible runs them on every execution, regardless of whether the target cluster state requires updates.
Using kubernetes.core.k8s Instead of Shell Commands
To make playbooks truly idempotent, interact directly with the Kubernetes API using the kubernetes.core.k8s module. Rather than calling external binaries, this module queries the API server to evaluate your target specification against live cluster resources, making modifications only when live state drifts from desired configuration.
- name: Ensure Nginx deployment is in the desired state
kubernetes.core.k8s:
state: present
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.24.0 # Pin specific tag for idempotency
ports:
- containerPort: 80
Notice the explicit container image tag: nginx:1.24.0. Avoid using mutable tags such as :latest in idempotent playbooks. Using mutable tags prevents Ansible from verifying whether the underlying container image changed, which can lead to unnecessary workload restarts or unhandled configuration drift.
Idempotent Secret Management with slurp
Dynamic secret handling frequently causes playbooks to report false positive changes. If you generate or read TLS certificates on every execution, Ansible may re-apply the Kubernetes secret even when values remain identical.
Using ansible.builtin.slurp helps preserve idempotency when syncing local TLS certificates into Kubernetes secrets:
- name: Read TLS certificate contents
ansible.builtin.slurp:
src: /etc/ssl/certs/app.crt
register: tls_cert
- name: Read TLS key contents ansible.builtin.slurp: src: /etc/ssl/private/app.key register: tls_key
- name: Apply TLS Secret to Kubernetes kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret type: kubernetes.io/tls metadata: name: app-tls namespace: production data: tls.crt: "{{ tls_cert['content'] }}" # slurp provides base64-encoded strings tls.key: "{{ tls_key['content'] }}"
The slurp module outputs file contents pre-encoded in Base64, matching what Kubernetes Secret data fields expect. When kubernetes.core.k8s runs, it compares the payload hash with the active secret in the cluster. If local files haven't changed, Ansible reports ok instead of changed, preventing unnecessary object updates.