AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible copy vs template vs lineinfile: When to Use Each Module

By Luca Berton · Published 2024-01-01 · Category: troubleshooting

Comparison of Ansible copy, template, and lineinfile modules. When to use each for file management, templates, single-line edits.

Three Ansible modules manage file content: copy, template, and lineinfile. Each solves a different problem. Using the wrong one creates brittle playbooks. This guide explains when to use each, with real examples and a decision flowchart.

Quick Comparison

| Feature | copy | template | lineinfile | |---------|------|----------|------------| | Use case | Static files | Dynamic files | Edit single lines | | Jinja2 support | content: only | Full Jinja2 | line:/regexp: only | | Source | Local file or inline | Jinja2 .j2 file | Existing remote file | | Creates file | Yes | Yes | Configurable | | Idempotent | Yes | Yes | Yes | | Replaces entire file | Yes | Yes | No | | Preserves unknown content | No | No | Yes | | Speed | Fastest | Fast | Moderate |

See also: ansible.builtin.file Module: Manage Files, Directories & Symlinks (Complete Guide)

The Decision Flowchart

Do you control the ENTIRE file content?
├── YES → Is the content dynamic (variables, loops, conditions)?
│   ├── YES → Use TEMPLATE
│   └── NO  → Use COPY
└── NO → Do you need to change specific lines in an existing file?
    ├── YES → Does the file have a predictable format?
    │   ├── YES → Use LINEINFILE (or blockinfile for multi-line)
    │   └── NO  → Use TEMPLATE with careful defaults
    └── NO → Use COPY for a new file, TEMPLATE if dynamic

Module 1: copy

Use copy when you have a static file or inline content with no variables.

Copy a Local File

- name: Deploy static configuration
  ansible.builtin.copy:
    src: files/motd.txt
    dest: /etc/motd
    owner: root
    group: root
    mode: '0644'

Inline Content

- name: Create static config
  ansible.builtin.copy:
    content: |
      # Application Configuration
      LOG_LEVEL=info
      MAX_CONNECTIONS=100
      TIMEOUT=30
    dest: /etc/myapp/config.env
    mode: '0644'

When to Use copy

• Static configuration files (no variables) • Binary files (images, certificates, archives) • Simple text files that don't change between hosts • Scripts that don't need host-specific values

When NOT to Use copy

# ❌ WRONG — hardcoded values that differ per host
- name: Deploy host config
  ansible.builtin.copy:
    content: |
      HOSTNAME=webserver01
      IP=192.168.1.10
    dest: /etc/myapp/host.conf

# ✅ RIGHT — use template instead - name: Deploy host config ansible.builtin.template: src: host.conf.j2 dest: /etc/myapp/host.conf

See also: Ansible Write to File: 5 Methods with Practical Examples (2026)

Module 2: template

Use template when file content needs variables, conditionals, or loops.

Basic Template

# playbook
- name: Deploy dynamic config
  ansible.builtin.template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    mode: '0644'
  notify: reload nginx
{# templates/nginx.conf.j2 #}
worker_processes {{ ansible_processor_vcpus }};

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

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

{% for upstream in nginx_upstreams | default([]) %} location {{ upstream.path }} { proxy_pass http://{{ upstream.host }}:{{ upstream.port }}; } {% endfor %} } }

Template with Conditionals

{# templates/sshd_config.j2 #}
Port {{ ssh_port | default(22) }}
PermitRootLogin {{ 'yes' if allow_root_login | default(false) else 'no' }}
PasswordAuthentication {{ 'yes' if allow_password_auth | default(false) else 'no' }}

{% if ssh_allowed_users is defined %} AllowUsers {{ ssh_allowed_users | join(' ') }} {% endif %}

{% if ssh_banner_enabled | default(true) %} Banner /etc/ssh/banner.txt {% endif %}

When to Use template

• Configuration files with host-specific values • Files that change based on group membership • Files with conditional sections • Files with repeated blocks (loops) • Any file where you want Ansible variables resolved

Template vs copy with content

# copy with content — works for simple cases
- ansible.builtin.copy:
    content: "HOSTNAME={{ inventory_hostname }}\n"
    dest: /etc/hostname.conf

# template — better for anything complex - ansible.builtin.template: src: hostname.conf.j2 dest: /etc/hostname.conf

# Rule of thumb: if you have more than 2 variables # or any conditional, use template

Module 3: lineinfile

Use lineinfile when you need to change ONE line in an existing file without touching the rest.

Ensure a Line Exists

- name: Enable IP forwarding in sysctl
  ansible.builtin.lineinfile:
    path: /etc/sysctl.conf
    regexp: '^net\.ipv4\.ip_forward'
    line: 'net.ipv4.ip_forward = 1'
    state: present

Modify Existing Line

- name: Change SSH port
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?Port\s+'
    line: 'Port 2222'
  notify: restart sshd

Add Line After Match

- name: Add nameserver after existing ones
  ansible.builtin.lineinfile:
    path: /etc/resolv.conf
    insertafter: '^nameserver'
    line: 'nameserver 8.8.8.8'

Remove a Line

- name: Remove obsolete config
  ansible.builtin.lineinfile:
    path: /etc/myapp.conf
    regexp: '^DEPRECATED_OPTION='
    state: absent

When to Use lineinfile

• Modifying system files you don't fully control (/etc/sysctl.conf, /etc/fstab) • Toggling a single setting in a large config • Adding entries to files managed by other tools • Files where you only care about one specific line

When NOT to Use lineinfile

# ❌ WRONG — managing many lines with lineinfile
- name: Configure app
  ansible.builtin.lineinfile:
    path: /etc/myapp.conf
    regexp: "^{{ item.key }}="
    line: "{{ item.key }}={{ item.value }}"
  loop:
    - { key: DB_HOST, value: "db.example.com" }
    - { key: DB_PORT, value: "5432" }
    - { key: DB_NAME, value: "myapp" }
    - { key: DB_USER, value: "appuser" }
    - { key: LOG_LEVEL, value: "info" }
    # ^^^ If managing 5+ lines, use template instead

# ✅ RIGHT — use template for the whole file - name: Deploy app config ansible.builtin.template: src: myapp.conf.j2 dest: /etc/myapp.conf

See also: Automating File Extension Validation with Ansible

blockinfile: The Middle Ground

When you need to manage multiple lines but can't own the whole file:

- name: Add custom rules to iptables
  ansible.builtin.blockinfile:
    path: /etc/sysconfig/iptables
    marker: "# {mark} ANSIBLE MANAGED - custom 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
    insertbefore: "^COMMIT"

Performance Considerations

# Fastest → slowest for deploying a full config file:
# 1. copy (src: file)     — raw file transfer, no processing
# 2. copy (content: ...)  — inline, minimal processing
# 3. template             — Jinja2 rendering + transfer
# 4. lineinfile (×N)      — reads file N times for N lines

# For 100 hosts deploying a config: # copy: ~2s # template: ~3s # lineinfile: ~5s (single line), ~15s (5 lines in loop)

Real-World Examples

Example 1: /etc/hosts

# Use lineinfile — you need to add entries without removing existing ones
- name: Add application hosts
  ansible.builtin.lineinfile:
    path: /etc/hosts
    regexp: '.*{{ item.hostname }}$'
    line: "{{ item.ip }}  {{ item.hostname }}"
  loop:
    - { ip: "10.0.1.10", hostname: "db.internal" }
    - { ip: "10.0.1.20", hostname: "cache.internal" }

Example 2: Application Config File

# Use template — you control the whole file
- name: Deploy application config
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/myapp/app.conf
    validate: "/opt/myapp/bin/validate-config %s"

Example 3: SSL Certificate

# Use copy — binary/static file
- name: Deploy SSL certificate
  ansible.builtin.copy:
    src: "certs/{{ inventory_hostname }}.pem"
    dest: /etc/ssl/certs/app.pem
    mode: '0644'

FAQ

When should I use copy content instead of template?

Use copy content: for simple inline strings with 1-2 variables and no conditionals. Use template when you have complex logic, loops, conditionals, or more than a few variables. The threshold is roughly 5 lines of content or 2+ variables.

Can lineinfile create a file?

Yes, by default. If the file doesn't exist and create: true (which is the default when state: present), lineinfile creates it. Set create: false to fail if the file is missing.

Is lineinfile idempotent?

Yes — if the line already matches, no change is made. The regexp parameter finds the line to replace, and line sets the desired content. Running it twice produces the same result.

Should I use lineinfile or template for /etc/sysctl.conf?

For 1-3 settings, lineinfile is fine. For comprehensive sysctl hardening (10+ settings), consider the ansible.posix.sysctl module or a template that owns the entire file (e.g., a drop-in file in /etc/sysctl.d/).

What about the replace module?

ansible.builtin.replace uses regex to find and replace text anywhere in a file, not just complete lines. Use it when you need to change part of a line (e.g., update a version number inside a URL).

Conclusion

copy for static files. template for dynamic files. lineinfile for surgical single-line edits. When in doubt, use template — it's the most flexible and keeps your configuration fully version-controlled.

Related Articles

Ansible Write to File: Complete GuideAnsible Jinja2 Filters Complete ReferenceAnsible Variable Precedence GuideAnsible register: Save Task Output

Category: troubleshooting

Browse all Ansible tutorials · AnsiblePilot Home