Skip to main content
RunBook Academy

AnsibleXVIII · Files and Configuration ManagementFiles and configuration management

file and the meaning of state

Intermediate⏱ ~16 minansible-playbookansible-doc

What you'll learn

  • Distinguish the six documented values of state and the operation each performs
  • Explain why state: file never creates a file and what to use instead
  • Predict what recurse does and does not apply to
  • Use state: absent safely, including what it will silently remove
  • Verify a symlink is a symlink rather than a copy of its target

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

ansible.builtin.file does not write file contents. It manages everything around the contents: existence, type, ownership, permissions, SELinux context, timestamps, and extended attributes. One option decides which of those operations you get.

Read-only / Safethe documented choices
$ ansible-doc ansible.builtin.file | grep -A2 'choices:'
        choices: [absent, directory, file, hard, link, touch]
      default: null
      type: str

Read that list as six verbs, not six adjectives.

stateOperationCreates?Can destroy?
fileSet attributes on an existing pathNoNo
directoryCreate the path and all intermediate directoriesYesNo
linkCreate or repoint a symbolic linkYesWith force
hardCreate or change a hard linkYesWith force
touchCreate empty, or update access and modification timesYesNo
absentUnlink a file or link; recursively delete a directoryNoYes

state: file does not create a file

This is the one that surprises people, and the documentation is blunt about it:

If file, even with other options (such as mode), the file will be modified if it exists but will NOT be created if it does not exist. Set to touch or use the ansible.builtin.copy or ansible.builtin.template module if you want to create the file if it does not exist.

The task fails if the path is missing. That is deliberate: state: file means “this path is a regular file and has these attributes”, and a missing path cannot satisfy that with an empty file you never asked for.

directory and what recurse covers

state: directory creates every intermediate directory, like mkdir -p, and applies the supplied permissions to the ones it creates. recurse: true extends the attribute application to the directory’s existing contents — and the documentation constrains it tightly:

Recursively set the specified file attributes on directory contents. This applies only when state is set to directory.

Two consequences worth stating plainly.

recurse never deletes anything. It is not rsync --delete. Files in the directory that your automation did not create keep existing; they just acquire your ownership and mode.

recurse applies one mode to files and directories alike. That is usually wrong. mode: '0644' on a recursed tree strips the execute bit from every directory, which makes the directory untraversable. The symbolic form solves it, because capital X sets the execute bit only where it is already set on something, or on directories.

Configuration changerecursive permissions that do not break directories
# Wrong: 0644 on a tree makes every directory non-traversable.
- name: Fix ownership of the app tree
ansible.builtin.file:
  path: /opt/app
  state: directory
  owner: appsvc
  group: appsvc
  mode: '0644'
  recurse: true

# Right: u=rwX,g=rX,o= - the capital X only touches directories and
# files that already carried an execute bit.
- name: Fix ownership of the app tree
ansible.builtin.file:
  path: /opt/app
  state: directory
  owner: appsvc
  group: appsvc
  mode: u=rwX,g=rX,o=
  recurse: true

state: link creates a symbolic link at path pointing at src. The option that decides how aggressive it is:

force: Force the creation of the links in two cases: if the link type is symbolic and the source file does not exist (but will appear later); the destination exists and is a file (so, we need to unlink the path file and create a link to the src file in place of it).

That second clause is the one to read twice. force: true will unlink a real file and replace it with a symlink. That is exactly what you want when converting a config file into a link to a shared one, and exactly what you do not want when the path was a real file holding data.

Configuration changethe release-directory symlink pattern
- name: Point current at the new release
ansible.builtin.file:
  src: /opt/app/releases/{{ app_release }}
  dest: /opt/app/current
  state: link
  owner: appsvc
  group: appsvc
notify: Reload the application

follow interacts with this and defaults to true on the file module. With follow: true and state: link, setting mode can modify the target of the link rather than the link itself, which the documentation names explicitly. When you are creating a link to a destination that does not exist yet, set follow: false to avoid the permission warning the module raises about a path it cannot stat.

absent, and the size of the hole it makes

state: absent is the only value on this module that destroys data, and what it destroys depends on what the path turned out to be:

If absent, directories will be recursively deleted, and files or symlinks will be unlinked.

A templated path that resolves one level higher than intended does not fail — it succeeds, recursively, and reports changed. There is no confirmation and no --force gate.

The module also documents that absent does not fail when the path does not exist, because the state did not change. That is correct idempotency and it also means a typo in the path is invisible: the task reports ok forever against a path that was never there.

touch and the check-mode footnote

state: touch creates an empty file if the path is missing, and updates the access and modification times if it is not. Because updating a timestamp is a change by definition, a touch task reports changed on every run unless you constrain it with modification_time: preserve and access_time: preserve.

That perpetual changed is not cosmetic. It notifies handlers, it inflates the change count an auditor reads, and it means a run that should have been quiet never is. If you are using touch to create a log file, copy with content: '' and force: false states the intent better and stays ok.

Verifying the result

Whatever the play claims, stat on the host is the fact. It is read-only and it is the right first move in any diagnosis.

Read-only / Safewhat the path actually is
ansible -i inventories/prod app -m ansible.builtin.stat \
-a "path=/opt/app/current follow=false" \
| grep -E '"islnk"|"isdir"|"isreg"|"lnk_target"|"nlink"|"mode"'
Read-only / Safea symlink, and a directory pretending to be one
$ ansible -i inventories/prod app -m ansible.builtin.stat -a 'path=/opt/app/current follow=false'
app01.example.com | SUCCESS => {
  "stat": {
      "islnk": true,
      "isdir": false,
      "lnk_target": "/opt/app/releases/2026-08-04-9f3c1ab",
      "mode": "0777"
  }
}
app02.example.com | SUCCESS => {
  "stat": {
      "islnk": false,
      "isdir": true,
      "mode": "0755"
  }
}

Illustrative output

The 0777 on the symlink is normal and not a finding: on Linux, symlink permission bits are not consulted, and stat reports them as 0777. The finding is app02, where islnk is false.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task uses state: file with mode 0600 against /etc/app/token. On twelve of forty hosts the application has never started and the file does not exist. What happens on those twelve?

  2. Q2. state: absent against a path that turns out to be a directory deletes the directory and everything inside it, without any additional flag.

  3. Q3. A task sets mode: 0644 with recurse: true on /opt/app to fix ownership. What breaks?

  4. Q4. Which statements about state: link with force: true are correct? Select all that apply.

Passing score: 75%. Answers are checked in this browser.