Cluster drift detector: OpenTofu plan + Ansible check in one command
One script that answers 'has anything in my cluster changed that nobody wrote down?' — guest definitions via tofu plan, node config via ansible --check.
DistrosDebian 13, Any distro with OpenTofu + Ansible installed
Shell
bashUpdated
Script
#!/usr/bin/env bash
# Cluster drift detector — run from your infrastructure repo root.
# Exit 0 = no drift anywhere. Non-zero = something changed; read the output.
set -u
REPO="$HOME/projects/homelab-iac" # your repo path
SECRETS="$HOME/.my-secrets" # exports PROXMOX_VE_API_TOKEN
cd "$REPO"
set -a; source "$SECRETS"; set +a
echo "== guest definitions (OpenTofu) =="
( cd terraform && tofu plan -detailed-exitcode -no-color )
TOFU=$?
# -detailed-exitcode: 0 = no changes, 1 = error, 2 = drift
echo "== node configuration (Ansible check mode) =="
( cd ansible && ansible-playbook site.yml --check --diff )
ANSIBLE=$?
# check mode never changes the nodes; changed>0 shows in the recap
echo
if [ "$TOFU" -eq 0 ] && [ "$ANSIBLE" -eq 0 ]; then
echo "OK: cluster matches the record (plan clean, check clean)"
exit 0
fi
echo "DRIFT or ERROR: tofu exit=$TOFU ansible exit=$ANSIBLE"
exit 1
What this does
Runs both halves of a cluster-as-code drift check as one command, against a homelab codified the way the Cluster as Code series sets up:
- Guest definitions —
tofu plan -detailed-exitcodecompares every imported container/VM definition against the live cluster through the Proxmox API. Exit0means an empty diff,2means drift,1means the check itself failed. The flag makes a clean plan machine-checkable instead of something a human has to read. - Node configuration —
ansible-playbook site.yml --check --diffasks every node whether it still matches the captured roles. Check mode runs without making any changes on the remote systems, so this is safe to run as often as you like; a non-zerochanged=count in the recap means the record and a node disagree.
Both checks are read-only by construction: the OpenTofu provider holds a read-only (PVEAuditor) API token, and Ansible check mode never writes. The worst this script can do is tell you something you didn’t want to hear.
Prerequisites
- A cluster imported to plan-zero with OpenTofu — see Import a Live Proxmox Cluster Into OpenTofu With Zero Changes
- Node roles captured with Ansible and passing check mode — see Ansible for a Cluster You Built by Hand: Capture, Then Assert
- A secret file that exports
PROXMOX_VE_API_TOKENfor the read-only token, and SSH key access from this machine to your nodes
Notes
- Make these values your own:
$HOME/projects/homelab-iacis your repo path;$HOME/.my-secretsis your secret file (mode 0600, exportingPROXMOX_VE_API_TOKEN=user@pve!tokenid=UUID— the real value stays in your secret store, never in this script or your repo). Theterraform/andansible/subdirectory names match the series’ layout; adjust if yours differ. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy. - A non-zero exit is the tool working, not failing. Either the cluster drifted (fix the cluster) or you made an intentional change and didn’t update the code (fix the code). Both mean the record and reality disagree — which is exactly what you built this to catch. Never “fix” drift by adding
ignore_changesfor a real difference. - Scheduling: this is cron-friendly precisely because of the exit codes — mail yourself the output on non-zero and stay silent otherwise. Keep the secret file readable only by the user the job runs as.
- Ansible’s check is only meaningful for what your roles cover. Files and services no role asserts are invisible to it; the detector’s coverage is your roles’ coverage.
- Verified on the real four-node cluster this series documents: the OpenTofu half re-run 2026-07-30 (exit 0 against nine imported containers), the Ansible half proven
changed=0across all four nodes when the roles were captured.