AnsibleXVIII · Files and Configuration ManagementFiles and configuration management
Owning a region of a file you do not own
What you'll learn
- Design a marker that survives multiple blocks in one file and multiple runs
- State the three documented marker rules that cause a block to be inserted repeatedly
- Predict what happens to a human edit made inside a managed region
- Place a block deterministically with insertafter and insertbefore
- Remove a managed region cleanly, including its markers
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
blockinfile is the middle position in this part’s argument. template
declares an entire file and needs to own it. lineinfile declares one
line and declares nothing about the rest. blockinfile declares a
named region, and everything outside that region belongs to somebody
else.
That makes it the right tool for a specific and very common situation: a file that a package owns, that a human also edits, and into which your automation needs to contribute several related lines that belong together.
The markers are the contract
The default marker is # {mark} ANSIBLE MANAGED BLOCK, with {mark}
replaced by BEGIN and END. Rendered into a file, a managed region
looks like this:
# BEGIN ANSIBLE MANAGED BLOCK
Match User deploy
PasswordAuthentication no
# END ANSIBLE MANAGED BLOCK
The module locates its region by finding those two lines. It replaces everything between them and touches nothing else. That is the whole mechanism, and every failure mode below is a consequence of it.
The three marker rules that cause repeated insertion
All three are documented, and all three produce the same symptom: the block is added again on every run, and the file grows.
One — the marker must contain {mark}.
Using a custom marker without the
{mark}variable may result in the block being repeatedly inserted on subsequent playbook runs.
Without {mark}, the begin and end markers are the same string, so the
module cannot delimit a region and fails to find its own previous block.
Two — markers cannot be multi-line.
Multi-line markers are not supported and will result in the block being repeatedly inserted on subsequent playbook runs.
A marker written with a YAML block scalar, or containing a \n, is
not a marker. The module appends a newline to marker_begin and
marker_end itself.
Three — one marker per block, per file.
When more than one block should be handled in one file you must change the
markerper task.
Two tasks with the default marker against the same file do not create
two regions. The second task finds the first task’s markers and
overwrites the region between them. Each run, both tasks fight over one
region, and both report changed forever.
- name: Manage the SSH match block for the deploy account
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} ANSIBLE MANAGED - deploy account"
block: |
Match User deploy
PasswordAuthentication no
AllowTcpForwarding no
validate: /usr/sbin/sshd -t -f %s
- name: Manage the SSH match block for the backup account
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} ANSIBLE MANAGED - backup account"
block: |
Match User backup
PasswordAuthentication no
ForceCommand /usr/local/bin/backup-only
validate: /usr/sbin/sshd -t -f %sThe same rule applies inside a loop. The module notes it explicitly for
the legacy with_* form, and it holds for loop just as much: if the
marker is constant across iterations, each iteration overwrites the
last, and the file ends up holding only the final item.
- name: Manage per-service host entries
ansible.builtin.blockinfile:
path: /etc/hosts
marker: "# {mark} ANSIBLE MANAGED - {{ item.name }}"
block: |
{{ item.ip }} {{ item.name }} {{ item.name }}.example.com
loop:
- { name: cache01, ip: 192.0.2.31 }
- { name: cache02, ip: 192.0.2.32 }Comment syntax is per-file, not universal
The default marker begins with #, which is a comment in shell, YAML,
INI, most config formats and no XML or HTML file anywhere. A marker that
is not a comment in the target file’s syntax is a syntax error you
wrote deliberately, and the module will happily install it.
| File type | Marker |
|---|---|
Shell, INI, most .conf | # {mark} ANSIBLE MANAGED - <owner> |
| HTML, XML | <!-- {mark} ANSIBLE MANAGED - <owner> --> |
| C-family, JSON5, some agent configs | // {mark} ANSIBLE MANAGED - <owner> |
| SQL | -- {mark} ANSIBLE MANAGED - <owner> |
| Strict JSON | none exists — do not use this module |
JSON has no comment syntax. There is no marker that a JSON parser will
accept, so a managed region cannot be expressed. Template the whole
file, or build the structure as a variable and render it with
to_nice_json.
Placement: insertafter and insertbefore
These are consulted only when the markers are not already present — that is, on the first run, or after someone deleted the region. Once the markers exist, the block stays exactly where they are, which is the behaviour you want: a human who moved your region to a more sensible place in the file does not get it moved back.
If insertafter finds no match, the block goes to EOF. If
insertbefore finds no match, the block goes to the end of the file as
well. Both fall back to the end silently — the same failure shape as
lineinfile, with the same consequence for section-scoped settings.
Since ansible-core 2.14 the anchor regex honours the multiline flag:
(?m) in the pattern makes the match run line by line, and without it
the pattern is applied across the file as one string. That is what makes
a multi-line anchor possible.
append_newline and prepend_newline (both default false) add a
blank line after and before the inserted block if one is not already
there. They matter more than they look: in files where a directive must
be separated from a preceding stanza, a block welded onto the previous
line changes its meaning.
What happens when a human edits inside the region
They lose the edit, silently, on the next run.
That is the correct behaviour and it is the point of the markers, but the practical failure is worth naming: the human did not know the region was managed, because the only signal was a comment they read as decoration. When the change reverts, the usual first theory is that the edit did not save.
Removing a region
Two forms do the same thing, and both remove the markers along with the content:
state: absent
or, equivalently per the documentation, block: "" — “if it is missing
or an empty string, the block will be removed as if state were
specified to absent”.
That equivalence is a trap in one specific case: a block built from a
variable that renders empty removes the region rather than writing an
empty one. A block fed by {{ some_list | join('\n') }} where the list
is empty deletes the managed region across the fleet and reports
changed, which looks exactly like a successful update in the recap.
If a region may legitimately be empty, guard the task with a when on
the variable rather than letting an empty render mean deletion.
Knowledge check
Knowledge check · 4 questions
Q1. Two blockinfile tasks in the same role write to /etc/ssh/sshd_config, both using the default marker. What happens?
Q2. A blockinfile task whose block renders to an empty string removes the managed region and its markers entirely.
Q3. A managed region needs to go into an HTML file. What must change from the default configuration?
Q4. Which of these cause a blockinfile task to insert its block again on every run? Select all that apply.
Passing score: 75%. Answers are checked in this browser.