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) }}"

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.