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
| Issue | Fix |
|---|---|
| Duplicate lines on re-run | Use lineinfile (idempotent) not shell: echo >> |
| Line added but not where expected | Use insertafter/insertbefore |
| Block appears twice | Check marker matches — use unique markers |
| File not created | Add create: true to lineinfile/blockinfile |
| Permissions wrong after write | Set mode explicitly |
| Content has extra blank lines | Use ` |
Best Practices
- Use
lineinfilefor single lines — idempotent, won't duplicate - Use
blockinfilefor sections — markers prevent duplication - Use
copy+contentto create/overwrite — full file replacement - Use
templatefor complex files — logic, loops, conditionals - Never use
shell: echo >>unless you want duplicates on re-run - Always add
\nat 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.