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.
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.
~/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:
playbooks/
edit-demo.yml
files/demo.conf
templates/demo-app.conf.j2Base files/demo.conf:
# demo application config
listen_port=8080
debug=false
log_level=infoPrepare 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:
- 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: falseforce: 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
- name: Set listen port
ansible.builtin.lineinfile:
path: /opt/demo-app/demo.conf
regexp: ^listen_port=
line: listen_port=9090
backup: trueRun from the project root:
cd ~/ansible-project && ansible-playbook playbooks/edit-demo.ymlSample 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=0The 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
- 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: trueAfter the play, the file contains:
ansible rocky2 -m shell -a "cat /opt/demo-app/demo.conf" -bSample 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-appAnsible 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:
- name: Raise log level
ansible.builtin.replace:
path: /opt/demo-app/demo.conf
regexp: ^log_level=info$
replace: log_level=warning
backup: trueThe ^ 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:
# managed by ansible template
listen_port={{ app_port | default(8080) }}
debug={{ app_debug | default(false) | lower }}
log_level={{ app_log_level | default("info") }}- 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 %sRendered output on the host:
ansible rocky2 -m shell -a "cat /opt/demo-app/demo-managed.conf" -bSample output:
rocky2 | CHANGED | rc=0 >>
# managed by ansible template
listen_port=9090
debug=false
log_level=warningUse 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.
- 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: truebackup: 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:
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 appFirst run with new content fires the handler; a second identical run reports ok and skips the handler.
Preview edits before applying:
ansible-playbook playbooks/edit-demo.yml --check --diffSample 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=infoThe 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

