Ansible Check Mode — Dry Run Playbooks Safely
Introduction
Check mode (--check) runs your playbook without making any changes — it predicts what would happen. Combined with --diff, it shows exactly what lines in files would change. This is essential for auditing, change management, and building confidence before applying changes to production.
Basic Usage
# Dry run a playbook
ansible-playbook site.yml --check
# Dry run with diff output
ansible-playbook site.yml --check --diff
# Diff only (shows changes but still applies them)
ansible-playbook site.yml --diff
How Check Mode Works
In check mode, Ansible:
- Connects to hosts normally
- Gathers facts normally
- Evaluates conditions normally
- Simulates each task — reports
changedorokwithout executing - Returns what would change
---
- name: Deploy web server (safe with --check)
hosts: webservers
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
# --check: Reports "changed" if nginx is not installed
# Does NOT actually install nginx
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
# --check --diff: Shows exact line changes
Force Check Mode Per Task
# Always run in check mode (even without --check flag)
- name: Audit file permissions
ansible.builtin.file:
path: /etc/shadow
mode: '0640'
check_mode: true
register: shadow_check
- name: Alert if permissions wrong
ansible.builtin.debug:
msg: "WARNING: /etc/shadow permissions need fixing!"
when: shadow_check.changed
Skip Check Mode Per Task
# Always execute (even in --check mode)
- name: Gather package facts (must run for later tasks)
ansible.builtin.package_facts:
check_mode: false
# Tasks that depend on gathered data
- name: Report installed nginx version
ansible.builtin.debug:
msg: "nginx {{ ansible_facts.packages['nginx'][0].version }}"
when: "'nginx' in ansible_facts.packages"
Diff Mode
Diff mode shows line-by-line changes for file-modifying modules:
ansible-playbook site.yml --diff
Output example:
TASK [Deploy nginx config] *****
--- before: /etc/nginx/nginx.conf
+++ after: /home/user/.ansible/tmp/nginx.conf
@@ -1,5 +1,5 @@
worker_processes auto;
-worker_connections 768;
+worker_connections 1024;
keepalive_timeout 65;
-server_tokens on;
+server_tokens off;
Diff with Sensitive Data
- name: Deploy config with secrets
ansible.builtin.template:
src: db.conf.j2
dest: /etc/app/db.conf
diff: false # Suppress diff output (contains passwords)
Check Mode in CI/CD
# .github/workflows/ansible-check.yml
name: Ansible Dry Run
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dry run playbook
run: ansible-playbook site.yml --check --diff
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
Handling Check Mode Limitations
Some modules don't support check mode — they skip entirely:
- name: Run database migration
ansible.builtin.command:
cmd: /opt/app/migrate.sh
# command/shell modules SKIP in check mode
# They can't predict what a script would do
- name: Run migration (check-aware)
ansible.builtin.command:
cmd: /opt/app/migrate.sh --dry-run
when: ansible_check_mode
changed_when: false
- name: Run migration (real)
ansible.builtin.command:
cmd: /opt/app/migrate.sh
when: not ansible_check_mode
The ansible_check_mode Variable
- name: Different behavior in check mode
ansible.builtin.debug:
msg: "{{ 'DRY RUN — no changes' if ansible_check_mode else 'LIVE — applying changes' }}"
- name: Skip destructive tasks in check mode
ansible.builtin.file:
path: /tmp/old-data
state: absent
when: not ansible_check_mode
Compliance Auditing Pattern
---
- name: Audit security compliance
hosts: all
become: true
tasks:
- name: Check SSH root login disabled
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
check_mode: true
register: ssh_root
- name: Check password auth disabled
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PasswordAuthentication'
line: 'PasswordAuthentication no'
check_mode: true
register: ssh_password
- name: Compliance report
ansible.builtin.debug:
msg: |
Host: {{ inventory_hostname }}
SSH Root Login: {{ 'FAIL' if ssh_root.changed else 'PASS' }}
Password Auth: {{ 'FAIL' if ssh_password.changed else 'PASS' }}
Troubleshooting
| Issue | Solution |
|---|---|
| Task skipped in check mode | Module doesn't support check; use check_mode: false |
| False "changed" in check | Module is check-mode aware but state differs |
| Dependent tasks fail | Earlier task didn't run; use check_mode: false for data-gathering |
| No diff output | Module doesn't support diff; works best with file/template/lineinfile |
Best Practices
- Always
--check --diffbefore production runs — review every change - Use in CI/CD — catch playbook errors before merging
- Mark data-gathering tasks with
check_mode: falseso dependent tasks work - Suppress diffs on secrets with
diff: false - Use for compliance auditing —
check_mode: true+registerto detect drift
Conclusion
Check mode is your safety net. Run --check --diff before every production deployment, use it in CI/CD pipelines, and build compliance audits with per-task check_mode: true. It turns Ansible from a configuration tool into an auditing tool — without changing a single line of your playbooks.