Конфигурация собирается из шаблона, а служба перезагружается, только когда она изменилась
В модуле 03 конфигурация nginx была вписана в плейбук через copy: content. Для файла в десять строк это терпимо, для настоящей конфигурации — нет. Файлы конфигурации хранят отдельно, в каталоге templates/ рядом с плейбуком, а модуль template подставляет в них переменные и кладёт результат на узел.
# {{ ansible_managed }}
server {
listen {{ http_port }} default_server;
server_name {{ inventory_hostname }};
root /srv/www/price;
index index.html;
server_tokens off;
}
{{ http_port }} — переменная из group_vars/web.yml, {{ inventory_hostname }} — имя узла. У каждого узла получается свой файл из одного шаблона.{{ ansible_managed }} превращается в строку «Ansible managed». Она предупреждает того, кто откроет файл на сервере: правка руками будет затёрта следующим прогоном.В шаблоне работают условия и циклы. Страница сервиса перечисляет все веб-узлы из инвентаря и отмечает текущий:
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>{{ site_title }}</title></head>
<body>
<h1>{{ site_title }}</h1>
<p>Ответил узел {{ inventory_hostname }} ({{ ansible_facts['default_ipv4']['address'] }}).</p>
<p>Все веб-узлы:</p>
<ul>
{% for host in groups['web'] | sort %}
<li>{{ host }}{% if host == inventory_hostname %} — это я{% endif %}</li>
{% endfor %}
</ul>
</body>
</html>
| Запись | Что делает |
|---|---|
{{ выражение }} | Подставить значение |
{% for … %} … {% endfor %}, {% if … %} … {% endif %} | Цикл и условие |
groups['web'] | Список узлов группы из инвентаря |
hostvars['db1']['ansible_host'] | Переменные и факты другого узла |
| sort, | join(', '), | default(3), | upper | Фильтры: отсортировать, склеить, значение по умолчанию, регистр |
| sort в цикле не украшение: без него порядок узлов может меняться между прогонами, файл будет каждый раз «другим», и задача потеряет идемпотентность.
Теперь решение проблемы из модуля 03, где nginx продолжал работать со старой конфигурацией. Обработчик (handler) — задача, которая выполняется, только если её «позвали» через notify, и только если позвавшая задача что-то изменила:
---
- name: Веб-узлы — nginx из шаблонов
hosts: web
become: true
tasks:
- name: Пакет nginx установлен
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
- name: Каталог сайта
ansible.builtin.file:
path: /srv/www/price
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: Стартовая страница из шаблона
ansible.builtin.template:
src: index.html.j2
dest: /srv/www/price/index.html
owner: www-data
group: www-data
mode: "0644"
- name: Сайт nginx из шаблона
ansible.builtin.template:
src: nginx-site.conf.j2
dest: /etc/nginx/sites-available/default
mode: "0644"
notify: nginx перечитал конфигурацию
- name: Служба nginx запущена и в автозапуске
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
handlers:
- name: Конфигурация nginx проверена
ansible.builtin.command: nginx -t
changed_when: false
listen: nginx перечитал конфигурацию
- name: Служба nginx перезагружена
ansible.builtin.systemd:
name: nginx
state: reloaded
listen: nginx перечитал конфигурацию
- name: Все узлы — SSH и приветствие
hosts: all
become: true
tasks:
- name: Усиленные настройки SSH
ansible.builtin.template:
src: sshd-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/10-hardening.conf
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Служба sshd перечитала конфигурацию
- name: Приветствие при входе
ansible.builtin.blockinfile:
path: /etc/motd
marker: "# {mark} ANSIBLE: сведения об узле"
block: |
Узел {{ inventory_hostname }}, группы: {{ group_names | join(', ') }}.
Настраивается Ansible — правки руками будут перезаписаны.
handlers:
- name: Служба sshd перечитала конфигурацию
ansible.builtin.systemd:
name: ssh
state: reloaded
notify: nginx перечитал конфигурацию. Изменился файл — обработчики запланированы; не изменился — ничего не происходит.listen — общее имя для нескольких обработчиков. Здесь по одному сигналу выполняются два: сначала nginx -t проверяет всю конфигурацию, потом служба перезагружается. Ошибка в конфигурации остановит выполнение на проверке, и работающий nginx не получит сломанную конфигурацию.changed_when: false у nginx -t: проверка ничего не меняет, и так это отображается в выводе.reloaded, а не restarted: nginx перечитывает конфигурацию, не обрывая текущие соединения.$ ansible-playbook site.yml
PLAY [Веб-узлы — nginx из шаблонов] ********************************************
TASK [Gathering Facts] *********************************************************
ok: [web2]
ok: [web1]
TASK [Пакет nginx установлен] **************************************************
changed: [web1]
changed: [web2]
TASK [Каталог сайта] ***********************************************************
changed: [web1]
changed: [web2]
TASK [Стартовая страница из шаблона] *******************************************
changed: [web2]
changed: [web1]
TASK [Сайт nginx из шаблона] ***************************************************
changed: [web1]
changed: [web2]
TASK [Служба nginx запущена и в автозапуске] ***********************************
ok: [web1]
ok: [web2]
RUNNING HANDLER [Конфигурация nginx проверена] *********************************
ok: [web1]
ok: [web2]
RUNNING HANDLER [Служба nginx перезагружена] ***********************************
changed: [web2]
changed: [web1]
PLAY [Все узлы — SSH и приветствие] ********************************************
TASK [Gathering Facts] *********************************************************
ok: [web2]
ok: [web1]
ok: [db1]
TASK [Усиленные настройки SSH] *************************************************
changed: [db1]
changed: [web1]
changed: [web2]
TASK [Приветствие при входе] ***************************************************
changed: [db1]
changed: [web2]
changed: [web1]
RUNNING HANDLER [Служба sshd перечитала конфигурацию] **************************
changed: [web1]
changed: [web2]
changed: [db1]
PLAY RECAP *********************************************************************
db1 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web1 : ok=12 changed=8 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=12 changed=8 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
В выводе видны строки RUNNING HANDLER после задач каждого play. Результат на узлах:
$ ansible web1 -m command -a "cat /srv/www/price/index.html"
web1 | CHANGED | rc=0 >>
<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>Сервис цен</title></head>
<body>
<h1>Сервис цен</h1>
<p>Ответил узел web1 (172.30.50.11).</p>
<p>Все веб-узлы:</p>
<ul>
<li>web1 — это я</li>
<li>web2</li>
</ul>
</body>
</html>
$ ansible web -m shell -a "curl -s http://127.0.0.1/ | grep Ответил"
[ERROR]: Task failed: Module failed: non-zero return code
Origin: <adhoc 'shell' task>
{'action': 'shell', 'args': {'_raw_params': 'curl -s http://127.0.0.1/ | grep Ответил'}, 'timeout': 0, [...]
web1 | FAILED | rc=1 >>
/bin/sh: 1: curl: not foundnon-zero return code
web2 | FAILED | rc=1 >>
/bin/sh: 1: curl: not foundnon-zero return code
$ ansible web1 -m command -a "cat /etc/nginx/sites-available/default"
web1 | CHANGED | rc=0 >>
# Ansible managed
server {
listen 80 default_server;
server_name web1;
root /srv/www/price;
index index.html;
server_tokens off;
}
$ ansible db1 -m command -a "cat /etc/motd"
db1 | CHANGED | rc=0 >>
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
# BEGIN ANSIBLE: сведения об узле
Узел db1, группы: db.
Настраивается Ansible — правки руками будут перезаписаны.
# END ANSIBLE: сведения об узле
Второй прогон ничего не меняет — и обработчики не запускаются вовсе:
$ ansible-playbook site.yml | tail -5
PLAY RECAP *********************************************************************
db1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Переведём веб-узлы на порт 8080 — правкой одной переменной — и посмотрим на --diff:
$ sed -i "s/^http_port: 80$/http_port: 8080/" group_vars/web.yml
$ ansible-playbook site.yml --diff --limit web1
PLAY [Веб-узлы — nginx из шаблонов] ********************************************
TASK [Gathering Facts] *********************************************************
ok: [web1]
TASK [Пакет nginx установлен] **************************************************
ok: [web1]
TASK [Каталог сайта] ***********************************************************
ok: [web1]
TASK [Стартовая страница из шаблона] *******************************************
ok: [web1]
TASK [Сайт nginx из шаблона] ***************************************************
--- before: /etc/nginx/sites-available/default
+++ after: /tmp/anspage.CggADA/ex/.home/.ansible/tmp/ansible-local-1873995rf9kgb9c/tmplc1l9xwe/nginx-site.conf.j2
@@ -1,6 +1,6 @@
# Ansible managed
server {
- listen 80 default_server;
+ listen 8080 default_server;
server_name web1;
root /srv/www/price;
index index.html;
changed: [web1]
TASK [Служба nginx запущена и в автозапуске] ***********************************
ok: [web1]
RUNNING HANDLER [Конфигурация nginx проверена] *********************************
ok: [web1]
RUNNING HANDLER [Служба nginx перезагружена] ***********************************
changed: [web1]
PLAY [Все узлы — SSH и приветствие] ********************************************
TASK [Gathering Facts] *********************************************************
ok: [web1]
TASK [Усиленные настройки SSH] *************************************************
ok: [web1]
TASK [Приветствие при входе] ***************************************************
ok: [web1]
PLAY RECAP *********************************************************************
web1 : ok=11 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
$ ansible web1 -m uri -a "url=http://127.0.0.1:8080/" | grep -E "\| |\"status\""
web1 | SUCCESS => {
"status": 200,
Шаблон изменился, nginx проверен и перезагружен, на новом порту сервис отвечает. Задача «Служба nginx запущена» осталась ok — служба работала и работает. Разница в файле показана прямо в выводе, даже без --check: --diff полезен и при обычном прогоне, чтобы в журнале осталось, что именно поменялось.
Если play упал после того, как обработчик был запланирован, обработчик по умолчанию не выполнится, и при следующем прогоне файл уже будет «не изменён» — служба так и останется со старой конфигурацией. На этот случай есть ключ --force-handlers и задача ansible.builtin.meta: flush_handlers, которая выполняет накопленные обработчики в нужный момент.
Ошибка в конфигурации sshd опаснее ошибки в nginx: после перезапуска службы к узлу будет не подключиться — в том числе Ansible, который мог бы всё исправить. Параметр validate у template и copy сначала кладёт файл во временное место, проверяет его указанной командой (%s — путь к временному файлу) и только при успехе заменяет рабочий файл:
# {{ ansible_managed }}
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries {{ ssh_max_auth_tries | default(3) }}
ClientAliveIntervall 300
Сделаем опечатку в шаблоне — лишняя буква в имени параметра — и применим к db1:
$ sed -i "s/^ClientAliveInterval 300$/ClientAliveIntervall 300/" templates/sshd-hardening.conf.j2
$ ansible-playbook site.yml --limit db1 | grep -E -A6 "TASK \[Усиленные"
TASK [Усиленные настройки SSH] *************************************************
[ERROR]: Task failed: Module failed: failed to validate
Origin: /tmp/anspage.CggADA/ex/site.yml:58:7
56 become: true
57 tasks:
58 - name: Усиленные настройки SSH
$ ansible db1 -m command -a "grep ClientAlive /etc/ssh/sshd_config.d/10-hardening.conf"
db1 | CHANGED | rc=0 >>
ClientAliveInterval 300
$ ansible db1 -m ping | head -1
db1 | SUCCESS => {
Задача упала на проверке, рабочий файл на узле остался прежним, обработчик не вызывался, доступ к узлу есть. Без validate файл был бы заменён. Перезагрузка службы в Debian сама проверяет конфигурацию и упала бы, а работающий sshd остался бы со старыми настройками — но сломанный файл остался бы на диске, и при следующем перезапуске сервера sshd не запустится: узел станет недоступен по SSH. validate пишут для всего, что может отрезать доступ или уронить службу: sshd (sshd -t -f %s), sudoers (visudo -cf %s), конфигурации, у которых есть своя команда проверки.
Не всякий файл стоит забирать целиком под шаблон. Если файл принадлежит пакету и обновляется вместе с ним, а вам нужна одна строка или один блок, используют:
lineinfile — одна строка: найти по регулярному выражению regexp и заменить на line или добавить, если нет.blockinfile — блок между строками-маркерами. Повторный прогон заменяет содержимое между маркерами, а не добавляет блок второй раз.Задача «Приветствие при входе» добавила в /etc/motd свой блок, не трогая текст Debian (выше в выводе cat /etc/motd). Но если одному файлу нужно больше двух-трёх правок, лучше взять его целиком под шаблон: десяток lineinfile на один файл трудно читать и легко сделать неидемпотентными.
templates/*.j2, модуль template; в начале файла — {{ ansible_managed }}groups, hostvars, циклы и фильтры; списки сортироватьreloaded вместо restarted, где служба это умеетvalidate — для всего, что может отрезать доступ: sshd, sudoerslineinfile/blockinfile; много правок — весь файл шаблоном