Editing Files You Do Not Own

Scenario: A tuning file has grown a fresh copy of the same setting on every run for two months, and two managed blocks keep overwriting each other so the file is never the same twice.

New words, in plain English

lineinfile is the module most often used incorrectly, and the failure is always the same shape.

Its regexp is not a search filter - it is an ownership claim. It tells Ansible "the line matching this pattern is mine; replace it". If the regexp matches nothing, lineinfile appends line to the file. Run it again, and if the regexp still does not match the line it just wrote, it appends again. That is the duplicate-forever bug, and it is why the regexp must match both the old value you are replacing and the new value you are writing.

The reliable pattern is to anchor on the setting name, never on its value:

regexp: '^net\.core\.somaxconn\s*='
line: 'net.core.somaxconn = 4096'

That matches the existing = 128 line (so it is replaced, not duplicated) and matches the new line on the next run (so nothing happens).

blockinfile has a parallel trap: every block in the same file must have a distinct marker. Two tasks using the default marker will each believe the other's block is theirs, and will overwrite it in turn - producing a file that oscillates forever and never converges.

Analogy: regexp is the name badge on a locker, not a search for a locker. If your badge does not match any locker, you do not get told - you are simply given a brand-new locker, every single time you visit.

A worked example

# WRONG - regexp never matches, so it appends on every run
- ansible.builtin.lineinfile:
    path: /etc/sysctl.d/99-tuning.conf
    regexp: '^#somaxconn'
    line: 'net.core.somaxconn = 4096'

# RIGHT - anchored on the key, so it replaces and then converges
- ansible.builtin.lineinfile:
    path: /etc/sysctl.d/99-tuning.conf
    regexp: '^net\.core\.somaxconn\s*='
    line: 'net.core.somaxconn = 4096'

# Two blocks in ONE file need two DISTINCT markers
- ansible.builtin.blockinfile:
    path: /etc/sysctl.d/99-tuning.conf
    marker: "# {mark} ANSIBLE MANAGED BLOCK: limits"
    block: |
      fs.nr_open = 1048576
      kernel.pid_max = 65536

- ansible.builtin.blockinfile:
    path: /etc/sysctl.d/99-tuning.conf
    marker: "# {mark} ANSIBLE MANAGED BLOCK: logging"
    block: |
      kernel.printk = 3 4 1 3

Both modules support validate:, which runs a command against a temporary copy before the real file is replaced - validate: 'visudo -cf %s', validate: 'nginx -t -c %s'. For any file where a syntax error locks you out or takes a service down, this is not optional.

And the strategic point: if your automation owns the whole file, template is simpler, safer and fully diffable. Reach for lineinfile and blockinfile only when you must coexist with a package or a human inside the same file.

Tip: The two-run test catches every one of these bugs in seconds. Run it twice; if the second run is not changed=0, your regexp or your marker is wrong.
Goal: Put this to work in the ansible-lineinfile-duplicates lab. Open /labs/ansible, pick ansible-lineinfile-duplicates, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.