Introduction
The ansible.builtin.file module with state: absent removes files and directories on remote hosts. It's idempotent — running it again on an already-deleted path doesn't fail or report "changed". This guide covers single files, directories, wildcards, conditional deletion, and cleanup patterns.
Quick Reference
# Delete a file
- ansible.builtin.file:
path: /tmp/old-config.conf
state: absent
# Delete a directory recursively
- ansible.builtin.file:
path: /opt/old-release
state: absent
# Delete multiple files
- ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /tmp/file1.txt
- /tmp/file2.txt
- /var/log/old-app.log
Delete Patterns (Wildcards)
# Find and delete files matching a pattern
- name: Find old log files
ansible.builtin.find:
paths: /var/log/myapp
patterns: "*.log.gz"
age: "30d" # Older than 30 days
register: old_logs
- name: Delete old logs
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_logs.files }}"
Delete All Files in Directory (Keep Directory)
- name: Find all files in temp dir
ansible.builtin.find:
paths: /tmp/build-artifacts
file_type: any
register: artifacts
- name: Remove all artifacts
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ artifacts.files }}"
Conditional Deletion
# Delete only if file exists (always safe — state: absent is already idempotent)
- name: Remove config if migration complete
ansible.builtin.file:
path: /etc/myapp/old-config.yml
state: absent
when: migration_complete | default(false)
# Delete based on file age
- name: Find stale PID files
ansible.builtin.find:
paths: /var/run
patterns: "myapp-*.pid"
age: "1h"
register: stale_pids
- name: Remove stale PIDs
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ stale_pids.files }}"
when: stale_pids.matched > 0
Common Cleanup Patterns
Post-Deployment Cleanup
- name: Clean up deployment artifacts
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /tmp/deploy-package.tar.gz
- /tmp/deploy-checksum.sha256
- /opt/app/previous-release
- name: Remove old releases (keep last 3)
block:
- name: List all releases
ansible.builtin.find:
paths: /opt/app/releases
file_type: directory
register: releases
- name: Sort and identify old releases
ansible.builtin.set_fact:
old_releases: "{{ releases.files | sort(attribute='mtime') | map(attribute='path') | list | reverse | list | slice(3) | last | default([]) }}"
- name: Remove old releases
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop: "{{ old_releases }}"
Clean Temp Files by Age
- name: Find temp files older than 7 days
ansible.builtin.find:
paths: /tmp
age: "7d"
recurse: false
file_type: file
register: old_temp
- name: Remove old temp files
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_temp.files }}"
loop_control:
label: "{{ item.path }}"
Remove Package and Config
- name: Remove application completely
block:
- name: Stop service
ansible.builtin.service:
name: myapp
state: stopped
ignore_errors: true
- name: Remove package
ansible.builtin.apt:
name: myapp
state: absent
purge: true
- name: Remove config and data directories
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/myapp
- /var/lib/myapp
- /var/log/myapp
- /opt/myapp
Delete Symlinks
# Remove symlink (not target)
- name: Remove symlink
ansible.builtin.file:
path: /usr/local/bin/myapp
state: absent
# Only removes the link, not what it points to
Safe Deletion with Backup
# Move to trash instead of delete
- name: Move to trash before deleting
ansible.builtin.command:
cmd: "mv {{ item }} /tmp/trash/{{ item | basename }}.{{ ansible_date_time.epoch }}"
removes: "{{ item }}"
loop:
- /etc/myapp/old-config.yml
# Or use copy + delete pattern
- name: Backup before delete
ansible.builtin.fetch:
src: /etc/myapp/config.old
dest: ./backups/
ignore_errors: true
- name: Then delete
ansible.builtin.file:
path: /etc/myapp/config.old
state: absent
Troubleshooting
| Issue | Fix |
|---|---|
| Permission denied | Use become: true |
| Directory not empty | state: absent removes recursively — this always works |
| Symlink target deleted instead | This doesn't happen — file: absent removes the path itself |
| Need to delete by pattern | Use find module first, then loop over results |
| Task shows "ok" not "changed" | File already absent — this is correct idempotent behavior |
file (absent) vs shell rm
| Feature | file: absent | shell: rm |
|---|---|---|
| Idempotent | ✅ | ❌ (fails if missing without -f) |
| Check mode | ✅ | ❌ |
| Diff mode | ✅ | ❌ |
| Wildcards | ❌ (use find) | ✅ (rm *.log) |
| Recursive | ✅ | Needs -rf |
Best Practices
- Use
state: absent— idempotent, safe, check-mode compatible - Use
find+ loop for patterns — don't shell out torm - Backup important files first — fetch before deleting
- Use
loop_control: label— for readable output with many files - Add
ignore_errorsonly when the file truly might not exist AND you don't care - Clean up in handlers — delete temp files after deployment succeeds
Conclusion
Use ansible.builtin.file with state: absent to delete files and directories. It's idempotent and recursive by default. For pattern-based deletion, use find to locate files then loop with file: absent. Always prefer this over shell: rm for idempotency and check-mode support. Back up critical files with fetch before deletion.