Introduction

Writing content to files is one of the most common Ansible operations — deploying configs, appending log entries, generating environment files, or building dynamic content from variables. This guide covers all the ways to write to files: create, overwrite, append, insert at specific positions, and manage blocks of content.

Create/Overwrite File

# Create or overwrite entire file
- name: Write config file
  ansible.builtin.copy:
    content: |
      [app]
      name = myapp
      port = 8080
      debug = false
    dest: /etc/myapp/config.ini
    mode: '0644'

Append a Line

# Add line at end of file (if not already present)
- name: Add entry to /etc/hosts
  ansible.builtin.lineinfile:
    path: /etc/hosts
    line: "192.168.1.100 myserver.local"
    state: present

# Append without idempotency check (always adds)
- name: Append timestamp to log
  ansible.builtin.shell: echo "$(date): Deployment completed" >> /var/log/deploy.log
  changed_when: true

Insert at Specific Position

# Insert after a pattern
- name: Add config after [global] section
  ansible.builtin.lineinfile:
    path: /etc/myapp/config.ini
    insertafter: '^\[global\]'
    line: "max_connections = 1000"

# Insert before a pattern
- name: Add comment before critical setting
  ansible.builtin.lineinfile:
    path: /etc/sysctl.conf
    insertbefore: '^net.ipv4.ip_forward'
    line: "# Required for container networking"

# Insert at beginning of file
- name: Add shebang to script
  ansible.builtin.lineinfile:
    path: /opt/scripts/run.sh
    insertbefore: BOF
    line: "#!/bin/bash"

# Insert at end of file
- name: Add to end
  ansible.builtin.lineinfile:
    path: /etc/profile.d/custom.sh
    insertafter: EOF
    line: "export PATH=$PATH:/opt/bin"

Write a Block of Lines

# Insert or update a managed block
- name: Add firewall rules
  ansible.builtin.blockinfile:
    path: /etc/iptables/rules.v4
    marker: "# {mark} ANSIBLE MANAGED - App Rules"
    block: |
      -A INPUT -p tcp --dport 8080 -j ACCEPT
      -A INPUT -p tcp --dport 8443 -j ACCEPT
      -A INPUT -p tcp --dport 9090 -j ACCEPT
    insertafter: "^-A INPUT -p tcp --dport 22"

Write Variable to File

# Write a single variable
- name: Save discovered IP
  ansible.builtin.copy:
    content: "{{ ansible_default_ipv4.address }}\n"
    dest: /etc/myapp/server_ip.txt

# Write dictionary as JSON
- name: Save server facts
  ansible.builtin.copy:
    content: "{{ server_info | to_nice_json }}\n"
    dest: /opt/app/server-facts.json
  vars:
    server_info:
      hostname: "{{ ansible_hostname }}"
      ip: "{{ ansible_default_ipv4.address }}"
      os: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
      memory_mb: "{{ ansible_memtotal_mb }}"

# Write list as one-per-line
- name: Write server list
  ansible.builtin.copy:
    content: "{{ groups['webservers'] | join('\n') }}\n"
    dest: /etc/myapp/servers.txt

Replace Lines (Find and Replace)

# Replace a line matching regex
- name: Change listen port
  ansible.builtin.lineinfile:
    path: /etc/nginx/nginx.conf
    regexp: '^\s*listen\s+\d+'
    line: "    listen 8080;"

# Replace with backreference
- name: Comment out a line
  ansible.builtin.replace:
    path: /etc/myapp/config.conf
    regexp: '^(dangerous_setting.*)'
    replace: '# \1  # Disabled by Ansible'

Build File from Template

# templates/env.j2
{% for key, value in env_vars.items() %}
{{ key }}={{ value }}
{% endfor %}
- name: Generate .env file from variables
  ansible.builtin.template:
    src: env.j2
    dest: /opt/app/.env
    mode: '0600'
  vars:
    env_vars:
      DATABASE_URL: "postgresql://{{ db_host }}/{{ db_name }}"
      REDIS_URL: "redis://{{ redis_host }}:6379"
      SECRET_KEY: "{{ app_secret }}"
      LOG_LEVEL: "info"

Append to File (Idempotent)

# lineinfile is idempotent — won't duplicate
- name: Ensure entries in cron.allow
  ansible.builtin.lineinfile:
    path: /etc/cron.allow
    line: "{{ item }}"
    create: true  # Create file if missing
  loop:
    - root
    - deploy
    - backup

Write Command Output to File

- name: Capture system info
  ansible.builtin.shell: |
    echo "=== System Info ==="
    uname -a
    echo "=== Disk Usage ==="
    df -h
    echo "=== Memory ==="
    free -h
  register: sysinfo
  changed_when: false

- name: Save to file
  ansible.builtin.copy:
    content: "{{ sysinfo.stdout }}\n"
    dest: /opt/reports/system-report.txt

Truncate and Rewrite

# Completely replace file content
- name: Reset log file
  ansible.builtin.copy:
    content: ""
    dest: /var/log/myapp/app.log
    mode: '0644'
    owner: app

Troubleshooting

IssueFix
Duplicate lines on re-runUse lineinfile (idempotent) not shell: echo >>
Line added but not where expectedUse insertafter/insertbefore
Block appears twiceCheck marker matches — use unique markers
File not createdAdd create: true to lineinfile/blockinfile
Permissions wrong after writeSet mode explicitly
Content has extra blank linesUse `

Best Practices

  1. Use lineinfile for single lines — idempotent, won't duplicate
  2. Use blockinfile for sections — markers prevent duplication
  3. Use copy + content to create/overwrite — full file replacement
  4. Use template for complex files — logic, loops, conditionals
  5. Never use shell: echo >> unless you want duplicates on re-run
  6. Always add \n at end — POSIX files need trailing newlines

Conclusion

Use copy with content to create or overwrite files. Use lineinfile to append or modify single lines idempotently. Use blockinfile for managed multi-line sections. Use template for complex dynamic content. Avoid raw shell appends — they're not idempotent. Always end file content with a newline character.