LinuxautomationTested on real hardware

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
Shellbash
Updated
Script
bash
#!/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:

  1. Guest definitionstofu plan -detailed-exitcode compares every imported container/VM definition against the live cluster through the Proxmox API. Exit 0 means an empty diff, 2 means drift, 1 means the check itself failed. The flag makes a clean plan machine-checkable instead of something a human has to read.
  2. Node configurationansible-playbook site.yml --check --diff asks 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-zero changed= 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

Notes

  • Make these values your own: $HOME/projects/homelab-iac is your repo path; $HOME/.my-secrets is your secret file (mode 0600, exporting PROXMOX_VE_API_TOKEN=user@pve!tokenid=UUID — the real value stays in your secret store, never in this script or your repo). The terraform/ and ansible/ 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_changes for 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=0 across all four nodes when the roles were captured.