Ansible Dry Run — Check Mode and Diff Mode Explained
Introduction
Ansible's check mode (--check) previews what would change without making any modifications. Diff mode (--diff) shows the exact content differences. Together, they let you audit changes before applying them — essential for production deployments.
Quick Reference
# Check mode — show what would change (don't apply)
ansible-playbook site.yml --check
# Diff mode — show file content differences
ansible-playbook site.yml --diff
# Both together (most useful)
ansible-playbook site.yml --check --diff
# Short form
ansible-playbook site.yml -CD
Check Mode Output
$ ansible-playbook site.yml --check
TASK [Deploy config] *****
changed: [web01] => {"changed": true, "msg": "would copy..."}
TASK [Start nginx] *****
ok: [web01]
PLAY RECAP *****
web01: ok=3 changed=1 unreachable=0 failed=0
"changed" in check mode = would change if run for real.
Diff Mode Output
$ ansible-playbook site.yml --check --diff
TASK [Deploy nginx config] *****
--- before: /etc/nginx/nginx.conf
+++ after: /tmp/ansible-tmp/nginx.conf
@@ -1,5 +1,5 @@
worker_processes auto;
-worker_connections 1024;
+worker_connections 2048;
keepalive_timeout 65;
Per-Task Check Mode Control
# Force task to always run in check mode (for gathering info)
- name: Get current state (runs even in check mode)
ansible.builtin.command: systemctl status nginx
check_mode: false # Always executes
register: nginx_status
changed_when: false
# Force task to never run in check mode
- name: Dangerous operation (skip in check mode)
ansible.builtin.command: /opt/app/migrate.sh
when: not ansible_check_mode
check_mode Parameter
# Always run (useful for gathering facts)
- name: Gather info regardless of mode
ansible.builtin.stat:
path: /etc/myapp/config.conf
check_mode: false
register: config_exists
# Explicitly skip in check mode
- name: Run migration (never in dry run)
ansible.builtin.command: /opt/app/migrate.sh
check_mode: true # Always runs in "check" mode (reports would-change, never executes)
ansible_check_mode Variable
# Conditional logic based on check mode
- name: Show what would happen
ansible.builtin.debug:
msg: "Would restart nginx"
when: ansible_check_mode
- name: Actually restart nginx
ansible.builtin.service:
name: nginx
state: restarted
when: not ansible_check_mode
Modules That Support Check Mode
| Module | Check Mode Support |
|---|---|
copy | ✅ Full (shows diff) |
template | ✅ Full (shows diff) |
file | ✅ Full |
service | ✅ Full |
apt/yum | ✅ Full |
lineinfile | ✅ Full |
command/shell | ❌ Skipped (use check_mode: false) |
raw | ❌ Skipped |
script | ❌ Skipped |
Common Patterns
Pre-Flight Validation
# Always run in check mode first before applying
ansible-playbook site.yml -CD
# Review output...
# Then apply:
ansible-playbook site.yml
Configuration Audit (Drift Detection)
# Check if live config matches desired state
ansible-playbook site.yml --check --diff 2>&1 | grep "^[+-]"
# Any output = configuration drift detected
# Playbook for drift detection (CI/CD)
---
- name: Audit configuration drift
hosts: production
tasks:
- name: Check nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
check_mode: true
diff: true
register: nginx_drift
- name: Report drift
ansible.builtin.debug:
msg: "DRIFT DETECTED on {{ inventory_hostname }}"
when: nginx_drift.changed
Safe Partial Runs
# Check mode with tags — preview just the deployment part
ansible-playbook site.yml --check --diff --tags deploy
# Check mode with limit — preview one host only
ansible-playbook site.yml --check --diff --limit web01
CI/CD Integration
# GitHub Actions — check mode as PR validation
- name: Validate playbook (dry run)
run: |
ansible-playbook site.yml --check --diff 2>&1 | tee check-output.txt
if grep -q "^changed:" check-output.txt; then
echo "⚠️ Changes would be applied"
fi
Diff Mode Options
# ansible.cfg — always show diffs
[diff]
always = true
context = 3 # Lines of context around changes
# Suppress diff for specific playbook
ansible-playbook site.yml --diff --no-diff # (last flag wins)
Limitations
command/shellmodules are skipped — Ansible can't predict their output- Dependent tasks may fail — if task B depends on task A's changes
- Idempotency issues visible — modules that aren't idempotent show false "changed"
- No actual verification — check mode only predicts, doesn't guarantee
Workaround for Command Tasks
- name: Check what migration would do
ansible.builtin.command: /opt/app/migrate.sh --dry-run
check_mode: false # Run even in check mode
changed_when: false
register: migration_preview
- name: Show migration plan
ansible.builtin.debug:
var: migration_preview.stdout_lines
Troubleshooting
| Issue | Fix |
|---|---|
| Task skipped in check mode | Module doesn't support check mode — add check_mode: false |
| Dependent task fails | Previous task didn't actually run — use check_mode: false on prereqs |
| Diff shows entire file | File doesn't exist yet — this is expected |
| Always shows "changed" | Module isn't idempotent — consider changed_when |
Best Practices
- Always
--check --diffbefore production deploys — mandatory for critical systems - Use
check_mode: falsefor fact-gathering tasks — ensures data is available - Add
--diffto CI/CD validation — catches unexpected changes - Treat
changedin check mode as a flag — investigate before applying - Run check mode against production regularly — detect drift early
- Document which tasks skip check mode — team needs to know
Conclusion
Use --check to preview changes without applying them. Use --diff to see exact file content differences. Combine both (-CD) for full visibility. Run check mode before every production deployment. Use check_mode: false on tasks that gather information. Integrate into CI/CD for automated drift detection.