9. Best Practices: production‑grade Ansible (Middle+)
Как писать production‑grade Ansible‑код: балансировать DRY и читаемость, минимизировать «магию», обеспечивать предсказуемость выполнения, разделять логику и данные. В конце — список типичных антипаттернов и способы их исправления. В разделе — небольшие примеры кода с комментариями.
DRY vs читаемость
DRY (Don’t Repeat Yourself) полезен, но в Ansible легко перейти грань и сделать код «магическим» и нечитаемым.
Хороший DRY
- повторяемые шаги вынесены в роль
- повторяемые значения вынесены в vars с понятными именами
# Хорошо: переиспользуемая роль
- hosts: web
roles:
- nginx
- app
Плохой DRY (магия ради DRY)
# Плохо: сложная Jinja-логика прямо в task (трудно читать и дебажить)
- name: Render configs
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop: "{{ lookup('vars', env ~ '_templates') | dict2items }}"
Если так нужно — лучше вынести структуры данных в group_vars, а преобразования — в filter plugin (см. раздел 6).
Минимизация magic
Под magic обычно попадает:
- скрытые зависимости между ролями (роль A «ожидает», что роль B уже поставила пакет)
- неявные значения переменных
- динамические include без ясной структуры
Production best practices:
- у ролей должен быть README (что делает, какие vars нужны)
- зависимости фиксировать в
meta/main.yml - defaults должны быть безопасными (не ломать системы «из коробки»)
Предсказуемость выполнения
Идемпотентность и handlers
Основное правило: playbook должен давать корректный changed и не делать лишних restarts.
- name: Deploy config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart myapp
handlers:
- name: Restart myapp
ansible.builtin.service:
name: myapp
state: restarted
Rolling updates
Для production‑сервисов применять serial и healthchecks, чтобы не «выключить всё сразу».
- hosts: web
serial: "20%"
tasks:
- name: Deploy config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart myapp
Разделение логики и данных
Логика — в ролях/задачах, данные — в inventories/*/group_vars и host_vars.
roles/app/tasks/main.yml # логика
inventories/prod/group_vars/all.yml # данные окружения (не секреты)
inventories/prod/group_vars/all/vault.yml # секреты (Vault)
Production best practices:
- окружения отличаются данными, а не копипастой playbook’ов
- секреты хранить отдельно (Ansible Vault / внешнее хранилище), задачи с секретами —
no_log: true
Антипаттерны и как их избегать
1) Giant playbooks (1000+ строк)
Проблема: трудно ревьюить и сопровождать, нет переиспользования.
Решение:
- разнести на роли (
common,nginx,app,monitoring) - собрать в
site.yml(см. раздел 3)
2) Хардкод
Проблема: нельзя переиспользовать между окружениями.
Решение: вынести в vars.
# Плохо
dest: /etc/myapp/prod.conf
# Хорошо
dest: "{{ myapp_config_path }}"
3) Отсутствие idempotency
Проблема: повторный прогон ломает систему или даёт ложный changed.
Решение: использовать модули и корректный changed_when.
4) shell/command вместо модулей
Проблема: меньше идемпотентности и переносимости.
Решение: использовать модули или писать custom module.
# Плохо
- ansible.builtin.shell: useradd ops
# Хорошо
- ansible.builtin.user:
name: ops
state: present
Production best practices (чеклист)
- [ ] Роли маленькие и сфокусированные (KISS)
- [ ] Минимум
shell/command - [ ] Идемпотентность: повторный прогон не меняет систему без необходимости
- [ ] Handlers для restart/reload
- [ ] Данные окружений в
inventories/*/group_vars - [ ] Секреты: Vault IDs,
no_log, внешние secret stores при необходимости - [ ] CI: ansible-lint + syntax-check, тесты Molecule для ключевых ролей