Introduction
Need to create a file with specific content on remote hosts? Ansible offers several approaches: copy with content for simple text, template for dynamic files with variables, lineinfile for single-line edits, and blockinfile for multi-line insertions. This guide shows when to use each.
Method 1: copy with content
# Create file with static content
- name: Create environment file
ansible.builtin.copy:
content: |
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
dest: /etc/myapp/.env
owner: app
group: app
mode: '0600'
With Variables
- name: Create config with variables
ansible.builtin.copy:
content: |
[database]
host = {{ db_host }}
port = {{ db_port }}
name = {{ db_name }}
user = {{ db_user }}
dest: /etc/myapp/database.ini
mode: '0640'
JSON Content
- name: Create JSON config
ansible.builtin.copy:
content: "{{ config | to_nice_json }}\n"
dest: /etc/myapp/config.json
vars:
config:
server:
host: "0.0.0.0"
port: 8080
database:
url: "postgresql://{{ db_host }}:5432/{{ db_name }}"
logging:
level: info
file: /var/log/myapp/app.log
YAML Content
- name: Create YAML config
ansible.builtin.copy:
content: "{{ config | to_nice_yaml }}"
dest: /etc/myapp/config.yml
vars:
config:
redis:
host: redis.internal
port: 6379
workers: 4
Method 2: template Module
# templates/app.conf.j2
server {
listen {{ http_port }};
server_name {{ server_name }};
location / {
proxy_pass http://127.0.0.1:{{ app_port }};
}
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
}
- name: Deploy from template
ansible.builtin.template:
src: app.conf.j2
dest: /etc/nginx/conf.d/app.conf
owner: root
mode: '0644'
notify: Reload nginx
Method 3: lineinfile (Single Line)
# Add or modify a single line
- name: Set hostname in /etc/hosts
ansible.builtin.lineinfile:
path: /etc/hosts
line: "{{ ansible_default_ipv4.address }} {{ inventory_hostname }}"
regexp: "^.*{{ inventory_hostname }}$"
state: present
# Ensure a line exists
- name: Enable IP forwarding
ansible.builtin.lineinfile:
path: /etc/sysctl.conf
line: "net.ipv4.ip_forward = 1"
regexp: "^net.ipv4.ip_forward"
Method 4: blockinfile (Multi-line Block)
- name: Add SSH config block
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
block: |
Match Group developers
AllowTcpForwarding yes
X11Forwarding no
PermitTunnel no
marker: "# {mark} ANSIBLE MANAGED - developers access"
notify: Restart sshd
Method 5: Create Empty File
# Touch (create if missing, update timestamp if exists)
- name: Create empty file
ansible.builtin.file:
path: /var/log/myapp/app.log
state: touch
owner: app
mode: '0644'
# Create only if missing (don't update timestamp)
- name: Create file only if absent
ansible.builtin.copy:
content: ""
dest: /etc/myapp/custom.conf
force: false # Won't overwrite existing
mode: '0644'
Comparison: When to Use Each
| Method | Use Case |
|---|---|
copy + content | Simple text, env files, small configs |
template | Complex files with logic (loops, conditionals) |
lineinfile | Modify one line in existing file |
blockinfile | Insert/update a multi-line section |
file + state: touch | Create empty file |
Advanced Patterns
Dynamic File from Loop
- name: Generate hosts file entries
ansible.builtin.copy:
content: |
# Managed by Ansible
{% for host in groups['webservers'] %}
{{ hostvars[host].ansible_host }} {{ host }}
{% endfor %}
dest: /etc/hosts.d/webservers
Multiline String Formats
# Literal block (preserves newlines)
- ansible.builtin.copy:
content: |
line 1
line 2
line 3
dest: /tmp/literal.txt
# Result: "line 1\nline 2\nline 3\n"
# Folded block (joins lines)
- ansible.builtin.copy:
content: >
This is a single
long line that gets
folded together.
dest: /tmp/folded.txt
# Result: "This is a single long line that gets folded together.\n"
# Strip trailing newline
- ansible.builtin.copy:
content: |-
no trailing newline
dest: /tmp/no-newline.txt
Create File from Command Output
- name: Get system info
ansible.builtin.command: uname -a
register: system_info
changed_when: false
- name: Write system info to file
ansible.builtin.copy:
content: "{{ system_info.stdout }}\n"
dest: /etc/system-info.txt
Atomic File Creation (Validate First)
- name: Create sudoers entry (validated)
ansible.builtin.copy:
content: "deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp\n"
dest: /etc/sudoers.d/deploy
mode: '0440'
validate: "visudo -cf %s"
Troubleshooting
| Issue | Fix |
|---|---|
| File always "changed" | Check for trailing newlines — add \n to content |
| Variables not expanded | Use template module, not copy for Jinja2 logic |
| Permission denied | Add become: true |
| Directory doesn't exist | Create with file: state=directory first |
| Content has wrong encoding | Set content as UTF-8, check source encoding |
Best Practices
- Use
templatefor anything complex — Jinja2 logic belongs in.j2files - Always end content with
\n— POSIX requires trailing newline - Set explicit permissions —
mode: '0644'prevents umask surprises - Use
validatefor critical files — sudoers, nginx, sshd configs - Use
blockinfilefor managed sections — markers prevent duplicates on re-run - Prefer
copy+contentover shell echo — idempotent and auditable
Conclusion
For simple files, use copy with content. For files with variables and logic, use template. For editing existing files, use lineinfile (single line) or blockinfile (sections). Always set permissions, end content with a newline, and use validate for syntax-critical files.