lineinfile vs blockinfile vs replace vs template in Ansible

Choose Ansible lineinfile, blockinfile, replace, or template for config edits—with idempotency rules, risk comparison, validate, and tested examples on sshd-style and app config files.

Published

Updated

Read time 13 min read

Reviewed byDeepak Prasad

Ansible lineinfile blockinfile replace and template on Rocky Linux 10

Ansible gives you four common ways to change text files on managed hosts: lineinfile for one line, blockinfile for a marked multi-line section, replace for regex substitution, and template for a full rendered file. All four solve “put the right text in the config,” but at different scope and risk levels.

This guide helps you pick the right module, stay idempotent, and avoid broken sshd_config, sudoers, nginx.conf, or application .conf files. It assumes you can run a basic playbook from playbook structure and know when to use become. For pushing static bytes or pulling files back, see file, copy and fetch. For Jinja2 variables, filters, and loops inside .j2 files, see the template module guide—here we only explain when to choose template.

IMPORTANT
Headline rule: when Ansible owns a file end to end—no vendor defaults you must preserve around the edges, and content varies per host or environment—prefer template over stacking lineinfile, blockinfile, or replace tasks. Patch modules fit vendor-shipped files you only tune at the margins.

Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.

What Problem Do These Ansible Modules Solve?

Vendor packages ship default configs you must tune: enable a setting, inject a managed block, swap a value, or deploy a file that varies per host. Shell sed works once but is hard to make safe and repeatable.

These modules edit or generate text in place on managed hosts:

Module What it changes
lineinfile One complete line
blockinfile A multi-line block between markers
replace Text matching a regexp
template The whole file from a Jinja2 source

They complement copy—use copy for static files you do not need to render or patch line by line.


Quick Difference Between lineinfile, blockinfile, replace and template

Decision rule: one line → lineinfile; marked section → blockinfile; pattern swap → replace; whole file you own → template.

Module Best for Scope Risk Main caution
lineinfile Ensure one line exists or is absent Single line Low to medium Regexp must stay idempotent
blockinfile Manage a multi-line block Marked block Medium Markers must be stable and include {mark}
replace Regex-based substitution Matching text Medium to high Broad patterns hit unintended text
template Generate full config from variables Whole file Low if you own the file Can overwrite manual local edits

When to use which

Requirement Recommended module
Add or update one config line lineinfile
Remove one matching line lineinfile with state: absent
Add several related lines together blockinfile
Manage a section with begin/end markers blockinfile
Replace part of a line using regex replace
Replace every matching pattern replace
Generate a complete file from variables template
File has many variables, loops, or conditionals template
You own the complete config file template
You do not own the whole file lineinfile or blockinfile
Critical config needs a syntax check validate on the module you choose

What is lineinfile in Ansible?

ansible.builtin.lineinfile ensures a single line is present, updated, or absent in a file. Use it for one key-value setting, one directive, or one comment line—not for whole sections.

Common parameters:

Parameter Role
path File to edit
line Exact line to insert or enforce
regexp Pattern that identifies the line to replace (or anchor for insertion)
search_string Literal string search—mutually exclusive with regexp and backrefs
create Create the file if missing. Default is false; without it, the task fails when the file does not exist.
backrefs Reuse capture groups from regexp in line
state present (default) or absent
insertafter / insertbefore Where to place a new line when no regexp match exists
validate Command run against temp file before commit (%s = path)
backup Timestamped copy on the managed host before change—not a rollback plan by itself

Use search_string when you only need a literal match and want to avoid regex escaping. Use regexp when you need anchors, alternatives, or capture groups.

When you set create: true, also set mode, owner, and group explicitly for production files so the new file does not depend only on the remote user’s umask.

Idempotency rules for lineinfile

The official docs recommend a regexp that matches both the old and final line states—for example ^listen_port= rather than ^listen_port=8080$ when the desired value is 9090. That way the task updates an existing line instead of appending a duplicate on the next run.

Use state: absent with a regexp to remove a matching line without touching the rest of the file.

Use backrefs: true when you want to preserve part of a matched line and replace only one captured portion—for example keeping a key name while swapping the value. With backrefs: true, insertbefore and insertafter are ignored, and if the regexp does not match, the file is left unchanged. Do not rely on backrefs when Ansible must add a new line when no match exists.


What is blockinfile in Ansible?

ansible.builtin.blockinfile inserts, updates, or removes a multi-line block surrounded by marker lines. Ansible finds the block by those markers on later runs.

Default markers look like # BEGIN ANSIBLE MANAGED BLOCK and # END ANSIBLE MANAGED BLOCK. Custom markers must include {mark}—Ansible substitutes BEGIN or END. If {mark} is missing, Ansible may not recognize an existing block and can insert duplicates.

blockinfile beats three or four lineinfile tasks when settings belong together: related flags, a snippet of sshd overrides, or an application stanza you want to replace as one unit.

Use insertafter or insertbefore to place a new block relative to an existing line when the file has no marker yet. Set state: absent with the same marker to remove a previously managed block. An empty or omitted block removes the block the same way.

When one file holds more than one managed block, each task needs a unique marker. Reusing the same marker can make later tasks update or overwrite the wrong block.


What is replace in Ansible?

ansible.builtin.replace substitutes text that matches regexp with replace. It can change part of a line, multiple lines, or every match in the file.

Useful parameters:

Parameter Role
regexp Pattern to find
replace Replacement text (supports backreferences like \1)
after / before Limit matches to content after or before a matched line
backup Timestamped copy on the managed host before change

The module replaces all matches. You must design the pattern so it does not match the replacement again—otherwise the task may report changed every run or alter text you did not intend. Prefer lineinfile when a full-line regexp and line is clearer than partial regex surgery.

Use after and before when the same pattern appears in multiple sections but only one region should change—they limit replacement to content following or preceding a matched line.

replace uses multiline regexp mode: ^ and $ match line boundaries, but . does not match newlines (DOTALL is not enabled). Do not assume . spans multiple lines; test with --check --diff, or move complex structure edits to template.


What is template in Ansible?

ansible.builtin.template renders a .j2 file on the control node with Jinja2, then writes the result to dest. Choose it when you own the file or when variables, conditionals, and loops belong in the config body.

template supports validate and backup like the other modules. A template task can notify a handler so a service reloads only when rendered content changes—we show that briefly in the examples below, not a full handlers walkthrough.

When the same file needs more than two or three lineinfile or replace operations, move the logic into a template source you can review in one place.


lineinfile vs blockinfile vs replace vs template: Comparison Table

Need lineinfile blockinfile replace template
One setting line Best fit Overkill Possible Overkill
Managed multi-line section Awkward Best fit Risky Possible if you own file
Partial line edit Possible with backrefs for one matching line Poor fit Best for all matching patterns Overkill
Per-host variables in many places Poor fit Poor fit Poor fit Best fit
Vendor file you must not fully own Good Good Careful Avoid full replace
Syntax check before apply validate validate validate validate

Risk and Idempotency Comparison

This is where module choice matters most for production configs.

Module Idempotency behavior Risk if misused
lineinfile Good when regexp matches old and final line Duplicate lines when regexp is too narrow
blockinfile Good when markers are stable and unique Duplicate blocks when markers change or {mark} is omitted
replace Depends entirely on regexp design Repeated replacements or unintended matches
template Good when rendered output is stable Overwrites unmanaged manual edits on the host

Rule of thumb: if you need more than two or three lineinfile or replace tasks on the same file, consider template instead—or move the file into an Ansible role templates/ directory when the same config ships to many hosts.

template is safest when Ansible owns the entire file. replace is the most regex-sensitive—reach for it only when line-based editing cannot express the change.


Common Real-World Examples

Lab layout:

Sample output:

output
playbooks/
  edit-demo.yml
  files/demo.conf
  templates/demo-app.conf.j2

Base files/demo.conf:

output
# demo application config
listen_port=8080
debug=false
log_level=info

Prepare the lab file

Before the edit tasks run, create the target directory and copy the base config. The examples below assume /opt/demo-app/demo.conf already exists:

yaml
- name: Ensure demo directory exists
      ansible.builtin.file:
        path: /opt/demo-app
        state: directory
        mode: "0755"

    - name: Copy base config for edits
      ansible.builtin.copy:
        src: files/demo.conf
        dest: /opt/demo-app/demo.conf
        mode: "0644"
        force: false

force: false keeps an existing demo.conf if you re-run the lab; set force: true only when you need to reset the file to the seed content.

Change one config setting with lineinfile

yaml
- name: Set listen port
      ansible.builtin.lineinfile:
        path: /opt/demo-app/demo.conf
        regexp: ^listen_port=
        line: listen_port=9090
        backup: true

Run from the project root:

bash
cd ~/ansible-project && ansible-playbook playbooks/edit-demo.yml

Sample output:

output
TASK [Change one config setting with lineinfile] *******************************
changed: [rocky2]

PLAY RECAP *********************************************************************
rocky2                     : ok=8    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The listen_port line moves from 8080 to 9090 without duplicating the setting on a second run. A repeat play reports changed=0 when regexps, markers, and template variables are stable.

Add a managed config block with blockinfile

yaml
- name: Add feature flags block
      ansible.builtin.blockinfile:
        path: /opt/demo-app/demo.conf
        marker: "# {mark} ANSIBLE MANAGED BLOCK demo-app"
        block: |
          feature_x=true
          feature_y=false
        create: true

After the play, the file contains:

bash
ansible rocky2 -m shell -a "cat /opt/demo-app/demo.conf" -b

Sample output:

output
rocky2 | CHANGED | rc=0 >>
# demo application config
listen_port=9090
debug=false
log_level=warning
# BEGIN ANSIBLE MANAGED BLOCK demo-app
feature_x=true
feature_y=false
# END ANSIBLE MANAGED BLOCK demo-app

Ansible updates the block in place on the next run because the markers match.

Replace a value pattern with replace

When the whole line should change but lineinfile is not the clearest tool—for example swapping only when the old value is still present:

yaml
- name: Raise log level
      ansible.builtin.replace:
        path: /opt/demo-app/demo.conf
        regexp: ^log_level=info$
        replace: log_level=warning
        backup: true

The ^ and $ anchors keep the pattern from matching log_level=warning again, so the task stays idempotent.

Generate a complete config file with template

templates/demo-app.conf.j2:

jinja2
# managed by ansible template
listen_port={{ app_port | default(8080) }}
debug={{ app_debug | default(false) | lower }}
log_level={{ app_log_level | default("info") }}
yaml
- name: Render full application config
      ansible.builtin.template:
        src: templates/demo-app.conf.j2
        dest: /opt/demo-app/demo-managed.conf
        mode: "0644"
        validate: test -f %s

Rendered output on the host:

bash
ansible rocky2 -m shell -a "cat /opt/demo-app/demo-managed.conf" -b

Sample output:

output
rocky2 | CHANGED | rc=0 >>
# managed by ansible template
listen_port=9090
debug=false
log_level=warning

Use template when the file is yours end to end; use lineinfile or blockinfile when vendor defaults must stay around the edges.

Validate config before applying changes

validate runs a command against the temporary file Ansible creates. The temp path must be passed as %s. Ansible executes the command securely—shell pipes and expansion do not work directly.

Match the validator to the file shape you deploy:

Config type Example validate
Sudoers fragment visudo -cf %s
Full nginx main config nginx -t -c %s
Full sshd config sshd -t -f %s
App config with a checker myapp --config-test %s

A random conf.d snippet may not validate as a standalone file—use a workflow that matches how the daemon loads includes, or validate after assembly.

yaml
- name: Ensure debug flag with syntax check
      ansible.builtin.lineinfile:
        path: /opt/demo-app/demo.conf
        regexp: ^debug=
        line: debug=false
        validate: test -f %s
        backup: true

backup: true leaves a timestamped copy on the managed host next to the live file before Ansible overwrites it. That is local recovery aid—not a full rollback strategy. Pair with version control or fetch when you need a copy on the control node.

Restart a service only when the file changes

Pair template (or any file module that reports changed) with notify so reload work runs only when content actually changes:

yaml
handlers:
    - name: restart demo app
      ansible.builtin.debug:
        msg: demo app would restart here

  tasks:
    - name: Deploy config and notify handler
      ansible.builtin.template:
        src: templates/demo-app.conf.j2
        dest: /opt/demo-app/demo-handler.conf
        mode: "0644"
      notify: restart demo app

First run with new content fires the handler; a second identical run reports ok and skips the handler.

Preview edits before applying:

bash
ansible-playbook playbooks/edit-demo.yml --check --diff

Sample output:

output
TASK [Change one config setting with lineinfile] *******************************
--- before: /opt/demo-app/demo.conf (content)
+++ after: /opt/demo-app/demo.conf (content)
@@ -1,4 +1,4 @@
 # demo application config
-listen_port=8080
+listen_port=9090
 debug=false
 log_level=info

The diff shows the exact line change before you commit it.


Common Mistakes

Mistake Why it is a problem
Using lineinfile for many related lines Independent line edits drift and are hard to review
Writing a regexp that only matches the old value Task may append duplicates after the first run
Expecting lineinfile to manage a full section blockinfile keeps related lines together
Changing blockinfile markers after deployment Ansible may not find the old block and inserts another
Removing {mark} from blockinfile marker Block can be inserted repeatedly
Reusing the same marker for multiple blocks Later tasks can overwrite or update the wrong block
Using replace with a broad regexp Unintended text changes across the file
Using replace when lineinfile is enough Extra regex risk without benefit
Using lineinfile or replace for a fully managed file template is cleaner when you control the whole file
Not using validate on critical config Broken sshd, sudoers, or nginx config can lock you out
Forgetting backup before risky edits Recovery is harder when a bad edit lands on the host

When Not to Use These Modules

Need Better approach
Edit structured YAML/JSON safely Data-aware module or template
Edit XML XML-aware module where available
Append command output Avoid shell redirection; use a proper module workflow
Make many related config changes template

Best Practices

Best practice Why
Prefer template when you own the whole file One reviewed source, stable idempotency
Prefer blockinfile for managed multi-line sections Related settings stay in one block
Prefer lineinfile for one clear line Simple when regexp is correct
Use replace only when pattern substitution is required Regex is easier to overmatch
Use precise regex Prevents duplicate lines and collateral edits
Use validate on critical config Stops invalid files from going live
Use backup: true on risky edits Timestamped rollback copy on the host
Use handlers for service reload/restart Service runs only when file content changes
Avoid heavy logic in ad-hoc file edits Move complex config to template
Test with --check --diff Shows expected changes before apply

Summary

lineinfile owns one line, blockinfile owns a marked section, replace swaps matching text with regex, and template renders a whole file from Jinja2. Match regexp and markers for idempotency, use validate on configs that can break services, and move to template when edits multiply. Preview with --check --diff, and link out to the template and file/copy/fetch guides for topics outside module selection.


References

  • ansible.builtin.lineinfile
  • ansible.builtin.blockinfile
  • ansible.builtin.replace
  • ansible.builtin.template

Frequently Asked Questions

1. When should I use lineinfile instead of template?

Use lineinfile when you do not own the whole file and need one complete line added, updated, or removed. Use template when you control the full file or it needs many variables and conditionals.

2. Why is blockinfile safer than several lineinfile tasks?

blockinfile manages a multi-line section between stable markers as one unit. Multiple lineinfile tasks can drift, duplicate lines, or miss related settings when the file changes.

3. Is replace idempotent in Ansible?

Only if you design the regexp so it does not match the replacement text again. replace changes every match on each run when the pattern still applies.

4. Do lineinfile and blockinfile support validate?

Yes. Use validate when the validator can check the exact temporary file Ansible creates. It works well for sudoers fragments with visudo -cf %s, full sshd/nginx main configs, or app config files with a validator. Snippets/includes may need a different validation workflow.

5. When should I stop editing and switch to template?

When the same file needs more than two or three lineinfile or replace operations, or when variables, loops, and conditionals belong in the file body. See the template guide for Jinja2 syntax.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …