Вот здесь и выходят на сцену два модуля для хирургического редактирования файла ansible: ansible.builtin.lineinfile (одна строка) и ansible.builtin.blockinfile (управляемый блок строк). Плюс примкнувший к ним ansible.builtin.replace для замены по всему файлу. Разберём, как ими пользоваться так, чтобы повторный запуск ничего не ломал, и где они обманчиво опасны.
Как работает ansible lineinfile: regexp решает всё
Ключевая идея lineinfile - не "добавь строку", а "обеспечь, чтобы строка существовала в нужном виде". Модуль ищет в файле совпадение по regexp, и дальше логика такая:
- совпадение нашлось - последняя совпавшая строка заменяется на значение line;
- совпадения нет - строка из line добавляется (в конец файла или туда, куда укажешь через insertafter/insertbefore);
- строка уже совпадает с line дословно - ничего не меняется, задача зелёная (ok, не changed).
Код: Выделить всё
- name: Запретить вход root по SSH
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?\s*PermitRootLogin'
line: 'PermitRootLogin no'
validate: '/usr/sbin/sshd -t -f %s'
notify: restart sshd
Обрати внимание на validate. Параметр прогоняет твою команду на ВРЕМЕННОЙ копии файла перед тем, как заменить оригинал. %s - это плейсхолдер, куда Ansible подставит путь к временному файлу. Для sshd это sshd -t -f %s. Если конфиг невалиден - модуль падает с ошибкой, а боевой /etc/ssh/sshd_config остаётся нетронутым. На RHCE это буквально спасает: сломанный sshd_config и перезапущенный sshd - и ты потерял доступ к экзаменационной машине. Для sudoers используют validate: 'visudo -cf %s', для nginx - 'nginx -t -c %s'.

insertafter, insertbefore и удаление строк
Когда строки в файле ещё нет и её надо поставить в конкретное место, добавляй insertafter или insertbefore. Значение - это тоже regexp (либо спецслова EOF/BOF - конец/начало файла).
Код: Выделить всё
- name: Добавить алиас в /etc/hosts после строки localhost
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '\sdb01\.local$'
line: '10.0.0.5 db01.local db01'
insertafter: '^127\.0\.0\.1'
Удаление строки - это state: absent вместе с regexp. line при этом не нужен:
Код: Выделить всё
- name: Убрать старую запись из /etc/fstab
ansible.builtin.lineinfile:
path: /etc/fstab
regexp: '^/dev/sdb1\s'
state: absent
ansible blockinfile: управляемый блок между маркерами
Когда надо вставить не одну строку, а кусок из нескольких (блок в /etc/hosts, секция конфига, набор переменных) - тут lineinfile неудобен. Берём ansible.builtin.blockinfile. Он оборачивает твой текст в пару маркеров-комментариев и при следующих запусках заменяет всё, что между ними. То есть блок остаётся "под управлением" Ansible: меняешь содержимое в плейбуке - меняется и кусок в файле.
Код: Выделить всё
- name: Управляемый блок sysctl для форвардинга
ansible.builtin.blockinfile:
path: /etc/hosts
marker: "# {mark} ANSIBLE app-серверы"
block: |
10.0.0.11 app01.local app01
10.0.0.12 app02.local app02
10.0.0.13 app03.local app03
Код: Выделить всё
# BEGIN ANSIBLE app-серверы
10.0.0.11 app01.local app01
10.0.0.12 app02.local app02
10.0.0.13 app03.local app03
# END ANSIBLE app-серверы
replace, template и когда что выбирать
ansible.builtin.replace - это sed по всему файлу: заменяет ВСЕ вхождения regexp на replace. В отличие от lineinfile он не "обеспечивает одну строку", а правит все совпадения. Удобно для массовых замен:
Код: Выделить всё
- name: Поднять worker_connections во всех vhost
ansible.builtin.replace:
path: /etc/nginx/nginx.conf
regexp: 'worker_connections\s+\d+'
replace: 'worker_connections 2048'
validate: 'nginx -t -c %s'
Маленькая пометка про дистрибутивы: пути отличаются. В RHEL/Fedora SSH-демон - sshd с конфигом /etc/ssh/sshd_config, в Debian/Ubuntu конфиг там же, но в свежих версиях ещё и /etc/ssh/sshd_config.d/*.conf. В RED OS и Astra Linux всё по схеме RHEL/Debian соответственно. Сами модули кросс-платформенны, меняются только пути и validate-команды.
Типичные грабли
- regexp ловит не то. Спецсимволы regex (точка, скобки, слэши) надо экранировать. '^/dev/sda' и '^/dev/sda1' - разные вещи; незаякоренный паттерн '.local' совпадёт с серединой строки.
- lineinfile меняет последнее совпадение, а не первое. Если под regexp попало несколько строк, правится последняя. Сужай паттерн якорями ^ и $.
- Меняешь конфиг сервиса - не забыл validate и notify. Без validate можно положить sshd; без handler-перезапуска изменение не вступит в силу.
- Несколько blockinfile в один файл с одинаковым marker. Дай каждому уникальную метку в {mark}.
- insertafter не двигает существующую строку. Он работает только при ВСТАВКЕ новой. Хочешь переставить - сначала state: absent, потом добавить.
Руками на тестовой RHEL/Fedora-машине (или контейнере):
- Напиши задачу lineinfile, которая ставит PermitRootLogin no в /etc/ssh/sshd_config с validate: '/usr/sbin/sshd -t -f %s'. Запусти дважды - убедись, что второй прогон ok, а не changed.
- Намеренно подсунь битый line (например, PermitRootLogin maybe) - проверь, что validate ловит ошибку и файл не меняется.
- Через blockinfile добавь блок из трёх записей в /etc/hosts с уникальным marker. Поменяй одну запись в плейбуке, прогони - блок обновился, дубликатов нет.
- Через replace замени значение во всех вхождениях в тестовом конфиге и сравни поведение с lineinfile.
- Сделай state: absent для строки и для блока - убедись, что они исчезают чисто.
- За счёт какого параметра lineinfile остаётся идемпотентным и почему echo >> file так не умеет?
- Что делает validate и почему без него опасно править sshd_config?
- Зачем в marker модуля blockinfile нужен {mark} и что будет, если в одном файле два блока с одинаковым marker?
- В каком случае правильнее взять template вместо lineinfile/blockinfile?
lineinfile правит одну строку по regexp, blockinfile - управляемый блок между маркерами, replace - все вхождения по всему файлу. Идемпотентность держится на грамотном regexp, а validate бережёт тебя от выстрела в ногу при правке конфигов сервисов. Но если файл целиком твой - не мельчи точечными задачами, бери template. На EX294 правка конфигов через lineinfile с regexp и validate - типовая задача, отработай её до автоматизма.