Ansible Configuration Drift: Stop Servers Going Rogue with Idempotent Playbooks
Ansible configuration drift detection and remediation.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Working Ansible installation and inventory. Basic familiarity with Ansible playbooks and modules.
Use Ansible's --check mode to detect drift without applying changes, then run the playbook to remediate. For continuous compliance, schedule periodic runs via AWX/Tower or cron, and use assert modules to validate state. Always write idempotent playbooks — running them multiple times yields the same result.
Imagine you manage 100 identical vending machines. Over time, some get extra snacks stuffed in, others lose a spring, and a few have the wrong prices. That's drift. Compliance is checking every machine against a master blueprint and fixing any that deviate. Ansible is your wrench — it reads the blueprint and adjusts each machine back to spec, automatically.
Configuration drift is the silent killer of reliable infrastructure. I've seen a single manual chmod 777 on a config file cascade into a 3-hour outage because the monitoring agent stopped reporting. Drift is not a matter of if, but when — and it's almost always the result of someone 'just this once' SSHing in to fix something.
The problem is that most teams treat configuration as a one-time deployment. You push a playbook, servers look good, and you move on. But servers are living systems — patches, reboots, manual fixes, and time itself change them. Without continuous enforcement, your infrastructure becomes a snowflake farm where no two boxes are alike.
By the end of this article, you'll be able to detect configuration drift in your Ansible-managed servers, write playbooks that self-heal on every run, and build a compliance pipeline that alerts you the moment a server goes out of spec. No more mystery outages. No more 'it works on my machine'.
Why Drift Happens: The Three Enemies of Idempotence
Drift doesn't appear out of nowhere. It's caused by three things: manual intervention (someone SSHes in and tweaks a file), time-based configs (like SSL certs that expire), and failed automation runs (a playbook stops halfway, leaving partial state). Each of these breaks the idempotent promise — that running the same playbook twice yields the same result.
Without idempotence, you can't trust your automation. A playbook that works on Monday might break on Tuesday because a previous run left a file in an unexpected state. The fix is to write every task so it checks the current state before making changes. Ansible modules like copy, template, and lineinfile are idempotent by design — but only if you use them correctly. Never use shell or command to do something a module can do. That's where drift hides.
shell or command to set file permissions. The file module is idempotent and reports changes correctly. shell always reports 'changed' — breaking idempotence and hiding drift.Detecting Drift with Check Mode and Diff
Before you fix drift, you need to find it. Ansible's --check mode runs the playbook without making changes. It reports what would change. Combine it with --diff to see the exact lines that would be added, removed, or modified. This is your first line of defense against surprise changes.
But `--check` has a dirty secret: not all modules support it. The command and shell modules always report 'changed' in check mode because they can't predict the outcome. That's another reason to avoid them. Stick to modules that implement check mode properly — copy, template, file, lineinfile, service, package, user, group.
For continuous drift detection, run --check on a schedule (e.g., every 15 minutes via cron) and alert if any changes are reported. This catches manual tweaks before they cause issues.
ansible-playbook site.yml --check --diff | grep -E '^---|^\+\+\+|^[+-]' to see only the actual diff lines. Pipe to less -R for color.Continuous Compliance with Assert Modules
Check mode tells you what would change, but it doesn't enforce anything. For real compliance, you need tasks that validate the current state and fail if it doesn't match. Ansible's assert module is perfect for this. You can check file contents, package versions, user existence, service status — anything you can express as a condition.
Combine assert with fail to stop the playbook if a server is out of compliance. This is useful for audit trails: every run either passes (all asserts true) or fails with a clear message. You can also use assert in a separate compliance playbook that runs on a schedule and alerts on failure.
But be careful: assert only checks, it doesn't fix. For self-healing, use assert to detect drift and then run a remediation playbook. Or combine both in one playbook — check first, then fix.
assert in check mode first, then run a separate remediation playbook. Never auto-fix in production without human approval.Self-Healing Playbooks: Remediate Drift Automatically
Detection is useless without remediation. A self-healing playbook runs on a schedule (every hour, every day) and fixes any drift it finds. The key is idempotence: the playbook should only change what's wrong and leave everything else alone.
Design your playbooks in two phases: first, gather facts about the current state. Second, use modules that compare desired vs. actual state and make the minimum change. For example, lineinfile with regexp replaces only the matching line. template with validate checks syntax before overwriting.
But self-healing has a dark side: if the playbook itself has a bug, it can 'fix' servers into a broken state. Always run a new playbook in check mode on a canary server first. And version-control everything — you need to roll back a playbook change, not just a config change.
replace module with a regex that could match multiple lines. You'll corrupt the file. Always use lineinfile with a specific regexp that matches exactly one line.Handling Time-Based Drift: Certificates and Passwords
Some drift is inevitable — SSL certificates expire, passwords rotate, API keys get revoked. These are time-based drifts that no amount of idempotence can prevent. The solution is to manage them as variables that change over time, and trigger playbook runs when they change.
For SSL certs, use Ansible's openssl_certificate module to generate self-signed certs or fetch from a CA. Set a notify handler to restart the web server only when the cert changes. For passwords, use password_hash filter to store hashed passwords in variables, and run the playbook whenever the variable changes (e.g., via a CI/CD pipeline).
But the real trick is monitoring: use openssl_certificate_info to check expiry dates and alert before they expire. Don't wait for the playbook to fail because the cert is invalid.
ansible_date_time.epoch for timestamp comparisons. It's available on every host without extra facts.When Not to Use Ansible for Drift Management
Ansible is great for configuration drift, but it's not a silver bullet. If you need real-time compliance enforcement (millisecond-level), Ansible's push model is too slow. Use a tool like OPA (Open Policy Agent) or a service mesh sidecar that enforces policies at the request level.
Also, Ansible doesn't handle stateful services well. If a database's configuration drifts, you can't just overwrite the config file and restart — you might lose data. For databases, use dedicated tools like pt-config-diff or mysql_config_editor.
Finally, if your infrastructure is ephemeral (containers that live minutes), drift is less of a problem because you rebuild from scratch. Focus on image compliance instead of runtime drift.
The 4GB Container That Kept Dying
/etc/security/limits.conf had been manually edited to set nofile to 65536, but the Ansible playbook set it to 1024. On restart, the container picked up the Ansible-managed file, reducing file descriptor limits. The app opened 5000+ connections, hit the limit, and the kernel OOM-killed it because of memory pressure from retries.nofile to 65536 in the Ansible template and re-run the playbook. Added a wait_for task to ensure the service restarted cleanly. Also added a sysctl check to alert if fs.file-max was below 100000.- Never assume a config file is static.
- If Ansible manages it, Ansible owns it.
- Any manual change is a ticking time bomb.
ansible -m setup hostname to gather facts. 3. Compare desired vs actual state manually. 4. Add an assert task to validate the specific setting.stat module to check existence before using the file. 4. Order your plays correctly.ansible hostname -m stat -a 'path=/etc/ssh/sshd_config'ansible hostname -m fetch -a 'src=/etc/ssh/sshd_config dest=/tmp/ssh_config flat=yes'| File | Command / Code | Purpose |
|---|---|---|
| drift_example.yml | - name: Detect and fix SSH config drift | Why Drift Happens |
| check_mode.yml | - name: Check for drift in NTP config | Detecting Drift with Check Mode and Diff |
| compliance_check.yml | - name: Compliance audit | Continuous Compliance with Assert Modules |
| self_healing.yml | - name: Self-healing playbook for web servers | Self-Healing Playbooks |
| cert_renewal.yml | - name: Check and renew SSL certificates | Handling Time-Based Drift |
Key takeaways
Common mistakes to avoid
3 patternsOnly running drift detection manually
--check --diff every 30-60 minutes for continuous compliance.Not setting up drift notifications
Auto-remediating without audit trail
Interview Questions on This Topic
How does Ansible's `--check` mode work internally, and what are its limitations?
--check runs the playbook but sets a flag that modules can read. Most modules (file, copy, template) simulate changes without applying them. But command and shell always report 'changed' because they can't predict the outcome. Also, --check doesn't run handlers — so you won't see service restarts. Always combine with --diff to see what would change.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Ansible. Mark it forged?
3 min read · try the examples if you haven't