Introduction
Ansible's stat module checks whether a file, directory, or symlink exists on remote hosts and returns detailed information about it. Use it to conditionally run tasks based on file existence — install only if a binary is missing, skip config if already deployed, or fail early if a required file isn't found.
Basic Check: Does a File Exist?
---
- name: Check if file exists
hosts: all
tasks:
- name: Check for config file
ansible.builtin.stat:
path: /etc/myapp/config.yml
register: config_file
- name: Show result
ansible.builtin.debug:
msg: "File exists: {{ config_file.stat.exists }}"
- name: Deploy config only if missing
ansible.builtin.template:
src: config.yml.j2
dest: /etc/myapp/config.yml
when: not config_file.stat.exists
Check If Directory Exists
- name: Check if directory exists
ansible.builtin.stat:
path: /opt/myapp
register: app_dir
- name: Create directory if missing
ansible.builtin.file:
path: /opt/myapp
state: directory
mode: "0755"
when: not app_dir.stat.exists
- name: Verify it's actually a directory (not a file)
ansible.builtin.debug:
msg: "/opt/myapp is a directory"
when: app_dir.stat.exists and app_dir.stat.isdir
Check File Type
- name: Check path type
ansible.builtin.stat:
path: /opt/myapp
register: path_info
- name: It's a regular file
ansible.builtin.debug:
msg: "Regular file"
when: path_info.stat.exists and path_info.stat.isreg
- name: It's a directory
ansible.builtin.debug:
msg: "Directory"
when: path_info.stat.exists and path_info.stat.isdir
- name: It's a symlink
ansible.builtin.debug:
msg: "Symbolic link → {{ path_info.stat.lnk_source }}"
when: path_info.stat.exists and path_info.stat.islnk
Common Patterns
Install Only If Binary Missing
---
- name: Install application if not present
hosts: all
become: true
tasks:
- name: Check if app is installed
ansible.builtin.stat:
path: /usr/local/bin/myapp
register: app_binary
- name: Download and install
when: not app_binary.stat.exists
block:
- name: Download archive
ansible.builtin.get_url:
url: "https://releases.example.com/myapp-2.0.tar.gz"
dest: /tmp/myapp.tar.gz
- name: Extract
ansible.builtin.unarchive:
src: /tmp/myapp.tar.gz
dest: /usr/local/bin/
remote_src: true
Skip If Already Configured
- name: Check if already configured
ansible.builtin.stat:
path: /etc/myapp/.configured
register: configured
- name: Run initial setup
ansible.builtin.command: /opt/myapp/setup.sh
when: not configured.stat.exists
- name: Mark as configured
ansible.builtin.file:
path: /etc/myapp/.configured
state: touch
when: not configured.stat.exists
Fail If Required File Missing
- name: Check for required SSL certificate
ansible.builtin.stat:
path: /etc/ssl/certs/myapp.pem
register: ssl_cert
- name: Fail if certificate missing
ansible.builtin.assert:
that:
- ssl_cert.stat.exists
fail_msg: "SSL certificate not found at /etc/ssl/certs/myapp.pem — deploy it first!"
# Or simpler with fail module
- name: Fail if missing
ansible.builtin.fail:
msg: "Required file /etc/ssl/certs/myapp.pem not found"
when: not ssl_cert.stat.exists
Backup Before Overwriting
- name: Check if config exists
ansible.builtin.stat:
path: /etc/nginx/nginx.conf
register: nginx_conf
- name: Backup existing config
ansible.builtin.copy:
src: /etc/nginx/nginx.conf
dest: "/etc/nginx/nginx.conf.bak.{{ ansible_date_time.iso8601_basic_short }}"
remote_src: true
when: nginx_conf.stat.exists
- name: Deploy new config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
Check Multiple Files
---
- name: Verify prerequisites
hosts: all
vars:
required_files:
- /etc/myapp/config.yml
- /etc/ssl/certs/myapp.pem
- /usr/local/bin/myapp
tasks:
- name: Check each file
ansible.builtin.stat:
path: "{{ item }}"
register: file_checks
loop: "{{ required_files }}"
- name: Report missing files
ansible.builtin.debug:
msg: "MISSING: {{ item.item }}"
loop: "{{ file_checks.results }}"
when: not item.stat.exists
- name: Fail if any missing
ansible.builtin.fail:
msg: "Missing files: {{ file_checks.results | rejectattr('stat.exists') | map(attribute='item') | list }}"
when: file_checks.results | rejectattr('stat.exists') | list | length > 0
File Properties from stat
The stat module returns much more than just existence:
- name: Get file details
ansible.builtin.stat:
path: /etc/myapp/config.yml
checksum_algorithm: sha256
register: file_info
- name: Show all properties
ansible.builtin.debug:
msg:
exists: "{{ file_info.stat.exists }}"
is_file: "{{ file_info.stat.isreg }}"
is_dir: "{{ file_info.stat.isdir }}"
is_link: "{{ file_info.stat.islnk }}"
size: "{{ file_info.stat.size }}" # bytes
mode: "{{ file_info.stat.mode }}" # "0644"
owner: "{{ file_info.stat.pw_name }}" # "root"
group: "{{ file_info.stat.gr_name }}" # "root"
uid: "{{ file_info.stat.uid }}"
gid: "{{ file_info.stat.gid }}"
checksum: "{{ file_info.stat.checksum }}" # sha256
mtime: "{{ file_info.stat.mtime }}" # modification time (epoch)
when: file_info.stat.exists
Check File Size
- name: Check log file size
ansible.builtin.stat:
path: /var/log/myapp.log
register: log_file
- name: Rotate if > 100MB
ansible.builtin.command: logrotate -f /etc/logrotate.d/myapp
when: log_file.stat.exists and log_file.stat.size > 104857600
Check File Permissions
- name: Check file permissions
ansible.builtin.stat:
path: /etc/shadow
register: shadow
- name: Warn if permissions are wrong
ansible.builtin.debug:
msg: "WARNING: /etc/shadow permissions are {{ shadow.stat.mode }}, should be 0640"
when: shadow.stat.exists and shadow.stat.mode != "0640"
Compare Checksums
- name: Check if file changed
ansible.builtin.stat:
path: /opt/app/current.jar
checksum_algorithm: sha256
register: current
- name: Download new version
ansible.builtin.get_url:
url: "https://releases.example.com/app-2.0.jar"
dest: /tmp/new-app.jar
checksum: "sha256:abc123..."
register: download
- name: Deploy if different
ansible.builtin.copy:
src: /tmp/new-app.jar
dest: /opt/app/current.jar
remote_src: true
when: not current.stat.exists or current.stat.checksum != "abc123..."
stat vs Other Approaches
# stat — explicit check, full control
- ansible.builtin.stat:
path: /opt/app
register: result
- ansible.builtin.debug:
msg: "exists"
when: result.stat.exists
# creates/removes — built into command/shell
- ansible.builtin.command: /opt/setup.sh
args:
creates: /opt/app/.installed # Skip if file exists
# file module — idempotent, no check needed
- ansible.builtin.file:
path: /opt/app
state: directory # Creates if missing, no-op if exists
Use stat when you need to branch logic based on existence. Use creates/removes for simple command guards. Use file/copy/template directly when the module is already idempotent.
Troubleshooting
"stat.exists is undefined"
The stat call itself failed (permissions, path syntax error). Check:
- name: Safe existence check
ansible.builtin.stat:
path: "{{ my_path }}"
register: check
ignore_errors: true
- name: Use with default
ansible.builtin.debug:
msg: "{{ check.stat.exists | default(false) }}"
Related Articles
- Ansible file Module — Create Files & Directories
- Ansible assert Module
- Ansible Create Directory Guide
- Ansible when Conditional
- Ansible copy Module
Conclusion
Use ansible.builtin.stat to check file/directory existence and properties. Register the result, then use when: result.stat.exists or when: not result.stat.exists for conditional logic. The module also returns size, permissions, owner, checksum, and timestamps. For simple "skip if file exists" with command/shell, prefer the creates argument instead. Remember: many Ansible modules (file, copy, template) are already idempotent — you don't always need a stat check first.