Разложить плейбук на переиспользуемые части с понятными параметрами
defaults) от её внутренних констант (vars)Плейбук модуля 05 уже длинный, а нам нужна ещё база данных. Если всё держать в одном файле, то настройку nginx для второго проекта придётся копировать — а потом исправлять ошибку в двух местах. Роль — каталог с задачами, шаблонами, обработчиками и параметрами для одной задачи: «nginx с сайтом», «PostgreSQL с базой приложения». Плейбук превращается в список: каким узлам какие роли.
---
- name: Общая настройка всех узлов
hosts: all
become: true
roles:
- common
- name: Веб-узлы
hosts: web
become: true
roles:
- role: nginx_site
vars:
nginx_site_title: Сервис цен
- name: База данных
hosts: db
become: true
roles:
- postgres
Три роли проекта:
$ find roles -type f | sort
roles/common/defaults/main.yml
roles/common/tasks/main.yml
roles/nginx_site/defaults/main.yml
roles/nginx_site/handlers/main.yml
roles/nginx_site/meta/main.yml
roles/nginx_site/tasks/main.yml
roles/nginx_site/templates/index.html.j2
roles/nginx_site/templates/site.conf.j2
roles/postgres/defaults/main.yml
roles/postgres/meta/main.yml
roles/postgres/tasks/main.yml
Каталоги роли фиксированы, и Ansible ищет в них файлы сам — путь к шаблону или обработчику указывать не нужно. Заготовку создаёт ansible-galaxy role init:
$ ansible-galaxy role init --init-path /tmp/skel demo >/dev/null && (cd /tmp/skel && find demo -type f | sort) && rm -rf /tmp/skel
demo/defaults/main.yml
demo/handlers/main.yml
demo/meta/main.yml
demo/README.md
demo/tasks/main.yml
demo/tests/inventory
demo/tests/test.yml
demo/vars/main.yml
| Каталог | Что внутри |
|---|---|
tasks/main.yml | Задачи — точка входа роли |
handlers/main.yml | Обработчики |
templates/, files/ | Шаблоны для template и файлы для copy |
defaults/main.yml | Параметры роли со значениями по умолчанию — самый слабый приоритет, их переопределяет кто угодно |
vars/main.yml | Внутренние значения роли — сильнее инвентаря; то, что пользователю роли менять не надо |
meta/main.yml | Описание, поддерживаемые системы и зависимости от других ролей |
README.md | Какие параметры есть и пример использования |
Ненужные каталоги удаляют — у наших ролей нет tests и vars. Роли ищутся в каталогах из roles_path (у нас roles рядом с плейбуком):
[defaults]
inventory = inventory.ini
remote_user = ansible
private_key_file = ~/.ssh/ansible_ed25519
interpreter_python = auto_silent
roles_path = roles
[ssh_connection]
# модули передаются через stdin одного SSH-сеанса, без временных файлов на узле:
# быстрее и не нужен пакет acl для become_user (модуль 06)
pipelining = True
Это задачи и шаблоны модуля 05, разложенные по каталогам роли. Главное — параметры: всё, что может отличаться у разных пользователей роли, вынесено в defaults:
---
# Всё, что пользователь роли может поменять
nginx_site_title: Сайт
nginx_site_port: 80
nginx_site_root: /srv/www/site
---
- name: Пакет nginx установлен
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
- name: Каталог сайта
ansible.builtin.file:
path: "{{ nginx_site_root }}"
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: Стартовая страница
ansible.builtin.template:
src: index.html.j2
dest: "{{ nginx_site_root }}/index.html"
owner: www-data
group: www-data
mode: "0644"
- name: Конфигурация сайта
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-available/default
mode: "0644"
notify: nginx перечитал конфигурацию
- name: Служба nginx запущена и в автозапуске
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
В плейбуке роль подключена с параметром: role: nginx_site и vars: nginx_site_title: Сервис цен. Порт и каталог остались по умолчанию. Хорошая роль работает «из коробки» с разумными значениями и не требует от пользователя знать, что у неё внутри.
Имена переменных роли начинают с имени роли: nginx_site_port, postgres_password, common_admin_users. Переменные в Ansible общие для всего прогона, и две роли с переменной port будут мешать друг другу. Линтер (модуль 08) проверяет это правило.
---
- name: Пароль базы задан
ansible.builtin.assert:
that: postgres_password | length >= 12
fail_msg: "Задайте postgres_password (не короче 12 символов) — лучше в Ansible Vault"
quiet: true
- name: PostgreSQL и драйвер для модулей установлены
ansible.builtin.apt:
name: [postgresql, python3-psycopg2]
state: present
update_cache: true
cache_valid_time: 3600
- name: PostgreSQL запущен
ansible.builtin.systemd:
name: postgresql
state: started
enabled: true
- name: Пользователь приложения
community.postgresql.postgresql_user:
name: "{{ postgres_user }}"
password: "{{ postgres_password }}"
become: true
become_user: postgres
no_log: true
- name: База приложения
community.postgresql.postgresql_db:
name: "{{ postgres_database }}"
owner: "{{ postgres_user }}"
encoding: UTF-8
become: true
become_user: postgres
postgres_password нет разумного значения по умолчанию, и роль не должна молча создать пользователя с пустым паролем. assert в начале роли останавливает прогон с понятным сообщением.community.postgresql.postgresql_user и postgresql_db из коллекции, которая пришла с пакетом ansible. Они идемпотентны: создадут пользователя и базу, если их нет, и поменяют пароль, если он другой. Модулям нужен драйвер python3-psycopg2 на узле — он ставится той же задачей.become_user: postgres. Управлять базой может системный пользователь postgres, и задача выполняется от его имени через sudo -u postgres.no_log: true у задачи с паролем — Ansible не выведет её параметры в журнал даже с -v.Что есть в коллекции и какая её версия установлена:
$ ansible-galaxy collection list community.postgresql -p /usr/lib/python3/dist-packages 2>/dev/null | tail -2
-------------------- -------
community.postgresql 4.1.0
$ ansible-doc -l community.postgresql 2>/dev/null | head -6
community.postgresql.postgresql_alter_system Change a PostgreSQL serv...
community.postgresql.postgresql_copy Copy data between a file...
community.postgresql.postgresql_db Add or remove PostgreSQL...
community.postgresql.postgresql_ext Add or remove PostgreSQL...
community.postgresql.postgresql_idx Create or drop indexes f...
community.postgresql.postgresql_info Gather information about...
Если коллекции нет в пакете или нужна другая версия, её ставят командой ansible-galaxy collection install по списку из файла requirements.yml в репозитории проекта — так у всех участников одинаковые версии. Из России Galaxy доступен не всегда; запасной путь — скачать архив коллекции (.tar.gz) там, где доступ есть, и ставить из файла.
Перед прогоном одна новая настройка в ansible.cfg — pipelining = True. Без неё Ansible копирует модуль во временный файл на узле, и при become_user обычному пользователю postgres нужно дать права на чужой временный файл — на Debian для этого требуется пакет acl. С pipelining модуль передаётся через тот же SSH-сеанс без файла: проблема исчезает, а прогон заметно ускоряется. Условие одно — в sudoers узлов не должно быть requiretty (в Debian его нет).
$ ansible-playbook site.yml
PLAY [Общая настройка всех узлов] **********************************************
TASK [Gathering Facts] *********************************************************
ok: [db1]
ok: [web2]
ok: [web1]
TASK [common : Общие пакеты установлены] ***************************************
changed: [web1]
changed: [db1]
changed: [web2]
TASK [common : Учётные записи администраторов] *********************************
changed: [web1] => (item=ivanova)
changed: [db1] => (item=ivanova)
changed: [web2] => (item=ivanova)
PLAY [Веб-узлы] ****************************************************************
TASK [Gathering Facts] *********************************************************
ok: [web1]
ok: [web2]
TASK [nginx_site : Пакет nginx установлен] *************************************
changed: [web1]
changed: [web2]
TASK [nginx_site : Каталог сайта] **********************************************
changed: [web1]
changed: [web2]
TASK [nginx_site : Стартовая страница] *****************************************
changed: [web2]
changed: [web1]
TASK [nginx_site : Конфигурация сайта] *****************************************
changed: [web1]
changed: [web2]
TASK [nginx_site : Служба nginx запущена и в автозапуске] **********************
ok: [web1]
ok: [web2]
RUNNING HANDLER [nginx_site : Конфигурация nginx проверена] ********************
ok: [web1]
ok: [web2]
RUNNING HANDLER [nginx_site : Служба nginx перезагружена] **********************
changed: [web1]
changed: [web2]
PLAY [База данных] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [db1]
TASK [postgres : Пароль базы задан] ********************************************
ok: [db1]
TASK [postgres : PostgreSQL и драйвер для модулей установлены] *****************
changed: [db1]
TASK [postgres : PostgreSQL запущен] *******************************************
ok: [db1]
TASK [postgres : Пользователь приложения] **************************************
[WARNING]: Module remote_tmp /var/lib/postgresql/.ansible/tmp did not exist and was created with a mode of 0700, this may cause issues when running as another user. To avoid this, create the remote_tmp dir with the correct permissions manually
changed: [db1]
TASK [postgres : База приложения] **********************************************
changed: [db1]
PLAY RECAP *********************************************************************
db1 : ok=9 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web1 : ok=11 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=11 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Задачи в выводе подписаны именем роли: nginx_site : Конфигурация сайта. Предупреждение про remote_tmp выводится один раз: Ansible создал для пользователя postgres служебный каталог.
$ ansible-playbook site.yml | tail -5
PLAY RECAP *********************************************************************
db1 : ok=9 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
$ ansible db1 -b --become-user postgres -m command -a "psql -Atc \"select datname, pg_get_userbyid(datdba) from pg_database where datname = 'price'\""
db1 | CHANGED | rc=0 >>
price|price_app
База price принадлежит price_app. Без пароля роль не работает:
$ ansible-playbook site.yml --limit db1 -e postgres_password=12345 | grep -E -A3 "TASK \[postgres : Пароль"
TASK [postgres : Пароль базы задан] ********************************************
[ERROR]: Task failed: Action failed: Задайте postgres_password (не короче 12 символов) — лучше в Ansible Vault
Origin: /tmp/anspage.VcFGqF/ex/roles/postgres/tasks/main.yml:2:3
В meta/main.yml можно указать dependencies — роли, которые выполнятся перед этой. Пользуются этим осторожно: зависимость выполняется неявно, и по плейбуку уже не видно, что происходит. Чаще проще и понятнее перечислить роли в плейбуке по порядку.
Готовые роли и коллекции сообщества экономят время, но это чужой код, который выполняется на ваших серверах с правами root. Перед использованием читают задачи роли, фиксируют версию в requirements.yml и обновляют её осознанно — как любую другую зависимость.
tasks, handlers, templates, defaults, meta; плейбук — какие роли каким узламdefaults с разумными значениями; имена с префиксом ролиassert в начале ролиrequirements.ymlbecome_user + pipelining = True; задачи с секретами — no_log: true