Skip to content

File Modules

  • In this lab we explore Ansible’s file management modules - the tools for copying, templating, modifying, and synchronizing files on managed hosts.
  • File modules are among the most frequently used modules in real-world playbooks.
  • We will cover copy, fetch, file, template, lineinfile, blockinfile, and synchronize.

What will we learn?

  • copy, fetch, file, template, lineinfile, blockinfile, synchronize
  • When to use each module and their key parameters
  • How to set permissions, ownership, and use Jinja2 templates

Prerequisites

  • Complete Lab 004 to understand how playbooks and tasks are structured.

01. copy Module - Transfer Files

tasks:
  # Copy a local file to remote hosts
  - name: Copy a local file
    ansible.builtin.copy:
      src: files/nginx.conf # Relative to playbook directory
      dest: /etc/nginx/nginx.conf
      owner: root
      group: root
      mode: "0644"
      backup: true # Create a backup before overwriting

  # Copy with inline content
  - name: Create a file with content
    ansible.builtin.copy:
      content: |
        # Generated by Ansible
        # Do not edit manually
        server_name={{ inventory_hostname }}
        port=8080
      dest: /etc/myapp/config.ini
      mode: "0600"

  # Copy directory recursively
  - name: Copy entire directory
    ansible.builtin.copy:
      src: files/website/ # Trailing slash copies contents
      dest: /var/www/html/
      mode: "0644"
      directory_mode: "0755"

02. fetch Module - Download Files from Remote Hosts

tasks:
  # Fetch a file from each managed host (creates subdirectories per host)
  - name: Fetch log files
    ansible.builtin.fetch:
      src: /var/log/nginx/access.log
      dest: logs/ # Creates: logs/<hostname>/var/log/nginx/access.log
      flat: false

  # Flat fetch (all files in one directory, renamed by hostname)
  - name: Fetch hostname file
    ansible.builtin.fetch:
      src: /etc/hostname
      dest: "fetched/{{ inventory_hostname }}-hostname"
      flat: true

03. file Module - Manage File/Directory State

tasks:
  # Create a directory
  - name: Create application directory
    ansible.builtin.file:
      path: /opt/myapp
      state: directory
      mode: "0755"
      owner: appuser
      group: appgroup

  # Create a file (empty)
  - name: Create empty file
    ansible.builtin.file:
      path: /var/log/myapp.log
      state: touch
      mode: "0640"

  # Create a symbolic link
  - name: Create symlink
    ansible.builtin.file:
      src: /opt/myapp-v2.1
      dest: /opt/myapp-current
      state: link

  # Delete a file or directory
  - name: Remove old files
    ansible.builtin.file:
      path: /tmp/old-deployment
      state: absent

  # Create a directory tree using a loop
  - name: Create nested directories
    ansible.builtin.file:
      path: /opt/app/{{ item }}
      state: directory
      mode: "0755"
    loop:
      - logs
      - config
      - data
      - temp

04. template Module - Jinja2 Templates

The template module processes Jinja2 template files (.j2) and copies the rendered result to managed hosts.

{# templates/nginx.conf.j2 #}
worker_processes {{ ansible_processor_vcpus }};

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
}

http {
    server_name {{ inventory_hostname }};
    listen {{ http_port | default(80) }};

    {% if ssl_enabled | default(false) %}
    listen 443 ssl;
    ssl_certificate /etc/ssl/{{ inventory_hostname }}.crt;
    {% endif %}

    {% for location in nginx_locations | default([]) %}
    location {{ location.path }} {
        proxy_pass {{ location.backend }};
    }
    {% endfor %}
}
tasks:
  - name: Deploy nginx configuration
    ansible.builtin.template:
      src: templates/nginx.conf.j2
      dest: /etc/nginx/nginx.conf
      owner: root
      group: root
      mode: "0644"
      validate: /usr/sbin/nginx -t -c %s # Validate before replacing
    notify: Reload nginx

05. lineinfile Module - Edit Single Lines

tasks:
  # Ensure a line exists in a file
  - name: Set max open files
    ansible.builtin.lineinfile:
      path: /etc/security/limits.conf
      line: "* soft nofile 65536"
      state: present

  # Replace a line matching a pattern
  - name: Set SSH port
    ansible.builtin.lineinfile:
      path: /etc/ssh/sshd_config
      regexp: "^#?Port "
      line: "Port 2222"
      backup: true
    notify: Restart sshd

  # Remove a line
  - name: Remove a line
    ansible.builtin.lineinfile:
      path: /etc/hosts
      regexp: "^192.168.1.100"
      state: absent

  # Insert after a specific line
  - name: Insert after pattern
    ansible.builtin.lineinfile:
      path: /etc/ssh/sshd_config
      insertafter: "^Port"
      line: "AllowUsers ansible admin"

06. blockinfile Module - Manage Blocks of Text

tasks:
  # Insert a block of text
  - name: Add hosts entries
    ansible.builtin.blockinfile:
      path: /etc/hosts
      block: |
        10.0.0.10 web1.internal
        10.0.0.11 web2.internal
        10.0.0.20 db1.internal
      marker: "# {mark} ANSIBLE MANAGED BLOCK"
      backup: true

  # Remove a managed block
  - name: Remove hosts entries
    ansible.builtin.blockinfile:
      path: /etc/hosts
      block: ""
      state: absent

07. synchronize Module - rsync Files

tasks:
  # Sync a local directory to remote hosts
  - name: Sync web content
    ansible.posix.synchronize:
      src: /local/website/
      dest: /var/www/html/
      delete: true # Remove files not in source
      recursive: true
      rsync_opts:
        - "--exclude=.git"
        - "--exclude=node_modules"
        - "--compress"

  # Pull files from remote to local
  - name: Pull logs from server
    ansible.posix.synchronize:
      mode: pull
      src: /var/log/nginx/
      dest: /local/logs/{{ inventory_hostname }}/

Practice

08. Hands-on

  1. Create a templates/ directory inside the controller and write a templates/config.ini.j2 Jinja2 template that includes inventory_hostname, ansible_default_ipv4.address, and an env variable with a default of lab.

??? success “Solution”

docker exec ansible-controller sh -c "cd /labs-scripts && mkdir -p templates files"
docker exec ansible-controller sh -c "cd /labs-scripts && cat > templates/config.ini.j2 << 'EOF'
# Configuration for {{ inventory_hostname }}
# Generated by Ansible on {{ ansible_date_time.date }}

[server]
hostname    = {{ inventory_hostname }}
ip_address  = {{ ansible_default_ipv4.address }}
environment = {{ env | default('lab') }}

[logging]
level       = {{ log_level | default('info') }}
path        = /var/log/myapp.log
EOF"
  1. Write a playbook lab016-files.yml that creates the directory structure /opt/myapp/{config,logs,data} on all hosts, deploys config.ini.j2 to /opt/myapp/config/app.ini, and creates a log file with initial content.

??? success “Solution”

docker exec ansible-controller sh -c "cd /labs-scripts && cat > lab016-files.yml << 'EOF'
---
- name: File Modules Practice
  hosts: all
  gather_facts: true
  vars:
    env: lab
    log_level: debug

  tasks:
    - name: Create application directory structure
      ansible.builtin.file:
        path: \"/opt/myapp/{{ item }}\"
        state: directory
        mode: \"0755\"
      loop:
        - config
        - logs
        - data

    - name: Deploy configuration from template
      ansible.builtin.template:
        src: templates/config.ini.j2
        dest: /opt/myapp/config/app.ini
        mode: \"0644\"

    - name: Create a log file
      ansible.builtin.copy:
        content: \"Application started\n\"
        dest: /opt/myapp/logs/app.log
        mode: \"0640\"
EOF"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-playbook lab016-files.yml"
  1. Add a task to lab016-files.yml that uses lineinfile to ensure the line 127.0.0.1 myapp.local is present in /etc/hosts on all servers.

??? success “Solution”

docker exec ansible-controller sh -c "cd /labs-scripts && ansible all -m lineinfile -a \"path=/etc/hosts line='127.0.0.1 myapp.local' state=present\" --become"
  1. Use the fetch module to pull the generated /opt/myapp/config/app.ini from all servers back to the controller under fetched/.

??? success “Solution”

docker exec ansible-controller sh -c "cd /labs-scripts && ansible all -m fetch -a 'src=/opt/myapp/config/app.ini dest=fetched/ flat=false'"
  1. Use blockinfile to add a block of custom entries to /etc/hosts on all servers, then verify the block was inserted.

??? success “Solution”

docker exec ansible-controller sh -c "cd /labs-scripts && ansible all -m blockinfile -a \"path=/etc/hosts block='10.0.0.10 web1.internal\n10.0.0.11 web2.internal' marker='# {mark} ANSIBLE MANAGED BLOCK'\" --become"

# Verify
docker exec ansible-controller sh -c "cd /labs-scripts && ansible all -m command -a 'grep -A5 ANSIBLE /etc/hosts'"
  1. Show the deployed config on all servers by reading /opt/myapp/config/app.ini.

??? success “Solution”

docker exec ansible-controller sh -c "cd /labs-scripts && ansible all -m command -a 'cat /opt/myapp/config/app.ini'"

### Output
# Configuration for linux-server-1
# Generated by Ansible on 2026-03-17
#
# [server]
# hostname    = linux-server-1
# ip_address  = 172.20.0.2
# environment = lab

09. Summary

  • copy transfers files from the control node to managed hosts; fetch does the reverse
  • file manages the state of files, directories, and symlinks with a single module
  • template renders Jinja2 (.j2) files using host variables before copying - ideal for config files
  • lineinfile makes precise single-line edits using regex matching
  • blockinfile manages entire blocks of text with auto-generated markers for idempotency
  • synchronize uses rsync for efficient bulk directory synchronization
  • Use validate: in template and copy to check files are valid before replacing them
  • Use backup: true to keep a copy of the original file before any change