Introduction

The ansible.builtin.file lookup plugin reads the contents of a file from the Ansible controller (the machine running ansible-playbook) and returns it as a string. This is useful for loading configuration snippets, certificates, SSH keys, or any file content into variables without copying the file to remote hosts first. This article covers basic usage, common patterns, error handling, and the difference between the file lookup and the slurp module.

How It Works

The file lookup runs on the controller, not on remote hosts. It reads the file during playbook parsing (for vars:) or task execution (for set_fact/debug) and returns the content as a string.

Controller (localhost)         Remote Host
┌─────────────────┐           ┌──────────────┐
│ ansible-playbook│           │              │
│                 │           │              │
│ lookup('file',  │           │              │
│   'data.txt')   │──read──→ data.txt (local)
│                 │           │              │
│ sends content   │──SSH──→   │ uses content │
│ to remote task  │           │              │
└─────────────────┘           └──────────────┘

Basic Usage

Read a File into a Variable

---
- name: Read file on controller
  hosts: all
  vars:
    file_contents: "{{ lookup('file', 'example.txt') }}"
  tasks:
    - name: Display file contents
      ansible.builtin.debug:
        msg: "Content: {{ file_contents }}"

Read with Absolute Path

vars:
  ssh_key: "{{ lookup('file', '/home/deploy/.ssh/id_rsa.pub') }}"
  ssl_cert: "{{ lookup('file', '/etc/ssl/certs/myapp.crt') }}"

Read Relative to Playbook

File lookup searches relative to the playbook directory, then the files/ directory:

project/
├── playbook.yml
├── files/
│   └── config.txt     ← found by lookup('file', 'config.txt')
└── config.txt         ← also found (playbook dir searched first)
# Both work — searches playbook dir, then files/ dir
vars:
  config: "{{ lookup('file', 'config.txt') }}"

Common Use Cases

1. Deploy SSH Authorized Keys

- name: Add SSH key to remote user
  ansible.posix.authorized_key:
    user: deploy
    key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
    state: present

2. Load SSL Certificates

- name: Deploy SSL certificate
  ansible.builtin.copy:
    content: "{{ lookup('file', 'certs/myapp.crt') }}"
    dest: /etc/ssl/certs/myapp.crt
    mode: "0644"

- name: Deploy SSL private key
  ansible.builtin.copy:
    content: "{{ lookup('file', 'certs/myapp.key') }}"
    dest: /etc/ssl/private/myapp.key
    mode: "0600"
  no_log: true

3. Load Configuration Snippets

- name: Add custom nginx config
  ansible.builtin.copy:
    content: "{{ lookup('file', 'nginx/custom.conf') }}"
    dest: /etc/nginx/conf.d/custom.conf
    mode: "0644"
  notify: Restart nginx

4. Read Multiple Files

- name: Read multiple config files
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ lookup('file', 'file1.txt', 'file2.txt', 'file3.txt', wantlist=True) }}"

5. Load JSON or YAML Data

# Read JSON file and parse it
vars:
  app_config: "{{ lookup('file', 'config.json') | from_json }}"
  
# Read YAML file and parse it
  db_config: "{{ lookup('file', 'database.yml') | from_yaml }}"
# Use parsed data
- name: Create database
  community.postgresql.postgresql_db:
    name: "{{ app_config.database.name }}"
    encoding: "{{ app_config.database.encoding }}"

6. Inject File Content into Templates

# In a Jinja2 template (nginx.conf.j2):
# server {
#     ssl_certificate_key {{ ssl_key }};
# }

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  vars:
    ssl_key: "{{ lookup('file', 'certs/server.key') }}"

7. Set Facts from File Content

- name: Load version from file
  ansible.builtin.set_fact:
    app_version: "{{ lookup('file', 'VERSION') | trim }}"

- name: Deploy versioned artifact
  ansible.builtin.get_url:
    url: "https://releases.example.com/app-{{ app_version }}.tar.gz"
    dest: /opt/app/

Error Handling

File Not Found

By default, a missing file causes a fatal error:

# This fails if secret.txt doesn't exist
vars:
  secret: "{{ lookup('file', 'secret.txt') }}"

Handle Missing Files Gracefully

# Use default filter
vars:
  config: "{{ lookup('file', 'custom.conf', errors='ignore') | default('# no custom config') }}"
# Check if file exists first
- name: Check for optional config
  ansible.builtin.stat:
    path: "{{ playbook_dir }}/files/custom.conf"
  register: custom_config
  delegate_to: localhost

- name: Load custom config if exists
  ansible.builtin.set_fact:
    extra_config: "{{ lookup('file', 'custom.conf') }}"
  when: custom_config.stat.exists

Errors Parameter

# errors='strict' (default) — fail on missing file
# errors='warn' — warn but return empty string
# errors='ignore' — silently return empty string

vars:
  optional: "{{ lookup('file', 'optional.txt', errors='warn') }}"

file Lookup vs slurp Module

Featurelookup('file')ansible.builtin.slurp
Runs onControllerRemote host
Use caseRead local filesRead remote files
ReturnsStringBase64-encoded string
TimingParse time (vars) or task timeTask time only
NetworkNo transfer neededTransfers over SSH

Read Remote File with slurp

# Use slurp to read files from remote hosts
- name: Read remote config
  ansible.builtin.slurp:
    src: /etc/nginx/nginx.conf
  register: remote_config

- name: Display remote file
  ansible.builtin.debug:
    msg: "{{ remote_config.content | b64decode }}"

Read Local File with file Lookup

# Use file lookup to read files from the controller
- name: Display local file
  ansible.builtin.debug:
    msg: "{{ lookup('file', '/etc/hosts') }}"
PluginPurpose
ansible.builtin.fileRead file contents
ansible.builtin.templateRead and render Jinja2 template
ansible.builtin.fileglobReturn list of files matching a pattern
ansible.builtin.csvfileRead data from CSV files
ansible.builtin.iniRead data from INI files
ansible.builtin.linesRead output of command as lines
ansible.builtin.urlRead content from a URL
# Template lookup — renders Jinja2 before returning
vars:
  rendered: "{{ lookup('template', 'config.j2') }}"

# Fileglob — list matching files
tasks:
  - name: Copy all configs
    ansible.builtin.copy:
      src: "{{ item }}"
      dest: /etc/myapp/conf.d/
    loop: "{{ lookup('fileglob', 'configs/*.conf', wantlist=True) }}"

Best Practices

  1. Use relative paths — place files in files/ directory for clean organization
  2. Handle missing files — use errors='warn' or default() for optional files
  3. Parse structured data — use | from_json or | from_yaml for data files
  4. Use no_log: true when reading sensitive files (keys, passwords)
  5. Prefer copy module over file lookup + copy content: when the file doesn't need transformation
  6. Use slurp when you need to read files from remote hosts

Conclusion

The ansible.builtin.file lookup plugin reads files from the controller machine into playbook variables. Use it for SSH keys, SSL certificates, configuration snippets, and structured data (JSON/YAML). For remote file reading, use the slurp module instead. Handle missing files with errors='warn' or the default() filter, and always use no_log: true when working with sensitive content.