Модуль 05 — Шаблоны и обработчики

Конфигурация собирается из шаблона, а служба перезагружается, только когда она изменилась

Прогресс курса Модуль 5 из 9

Что вы освоите в этом модуле

01

Шаблоны Jinja2

В модуле 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;
}

В шаблоне работают условия и циклы. Страница сервиса перечисляет все веб-узлы из инвентаря и отмечает текущий:

<!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 в цикле не украшение: без него порядок узлов может меняться между прогонами, файл будет каждый раз «другим», и задача потеряет идемпотентность.

02

Обработчики

Теперь решение проблемы из модуля 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
$ 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   
03

Изменение конфигурации

Переведём веб-узлы на порт 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, которая выполняет накопленные обработчики в нужный момент.

04

validate: не закрыть себе доступ

Ошибка в конфигурации 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), конфигурации, у которых есть своя команда проверки.

05

Часть файла: blockinfile и lineinfile

Не всякий файл стоит забирать целиком под шаблон. Если файл принадлежит пакету и обновляется вместе с ним, а вам нужна одна строка или один блок, используют:

Задача «Приветствие при входе» добавила в /etc/motd свой блок, не трогая текст Debian (выше в выводе cat /etc/motd). Но если одному файлу нужно больше двух-трёх правок, лучше взять его целиком под шаблон: десяток lineinfile на один файл трудно читать и легко сделать неидемпотентными.

Ключевые выводы модуля

Запомните главное

  • Конфигурации — в templates/*.j2, модуль template; в начале файла — {{ ansible_managed }}
  • В шаблонах — переменные, факты, groups, hostvars, циклы и фильтры; списки сортировать
  • Обработчик выполняется в конце play, один раз и только если вызвавшая задача что-то изменила
  • Перед перезагрузкой — проверка всей конфигурации; reloaded вместо restarted, где служба это умеет
  • validate — для всего, что может отрезать доступ: sshd, sudoers
  • Одна строка или блок — lineinfile/blockinfile; много правок — весь файл шаблоном
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 04: Переменные, факты, условия, циклы Все модули Модуль 06: Роли и коллекции