Гайдов и практик по написанию — куча. Все их можно легко найти — приводить их не буду. В данной статье я попытаюсь структурировать все мои шишки, полученные в рамках написания и эксплуатации ролей Ansible и рассказать как легко написать роль без регистрации и СМС.
Алгоритм написания роли в Ansible
Хорошая роль Ansible должна быть модульной, переиспользуемой и хорошо документированной. Вот пошаговый алгоритм создания роли:
Определение функционала роли
Проанализируй свою потребность и ответь на вопрос «Что должна делать роль»?
- Устанавливать и настраивать один сервис (Nginx, PostgreSQL, Docker и т. д.).
- Настраивать системные параметры (например,
sysctl
,limits.conf
). - Разворачивать приложение (например, WordPress, Prometheus).
- Конфигурировать отдельный сервис.
- Выдавать права и пр. и др.
При определении функционала не стоит смешивать несколько несвязанных задач (например, установка Nginx + настройка БД).
Нюансы написание ролюхи.
- Разбивать сложные задачи на подзадачи (
include_tasks
) и использовать теги (tags
) для выборочного запуска. - Безопасная работа с секретами в Ansible: no_log: true.
- Проверка пререквизитов и корректности переменных.
- Чтобы избежать конфликтов и повысить читаемость кода, все переменные (включая временные) должны начинаться с префикса имени роли.
- Обработка ошибок в Ansible (block/rescue).
- Неидемпотентная роль — выстрел в ногу.
- Не ленись — пиши модули.
- Помни о хендлерах.
- Хочешь мира — пиши документацию.
- Тестируй все изменения.
---
- name: Check variables
include_tasks:
file: pre_task.yml
apply:
tags: always
tags: always
- name: Install Nginx
apt:
name: nginx
state: present
tags: install
- name: Copy Nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
tags: config
- name: Flush handlers at end
meta: flush_handlers
tags: always
Разделение функционала по тегам
Теги — замечательный инструмент.
- Теги дают контроль над этапами:
install|update|config|certs|remove
. - Обеспечивают четкое разделение задач, что упрощает поддержку.
- Обеспечивают безопасность: Тег
remove
не конфликтует сinstall
. - Гибкость: Можно комбинировать (
deploy = install + config + certs
).
Примерная структура тегов:
Тег | Действие |
---|---|
install | Установка ПО |
update | Обновление (если версия изменилась) |
config | Настройка конфигов |
certs | Обновление TLS/SSL |
remove, never | Полное удаление ПО |
Особое внимание стоит уделить применению (apply) тегов, тут важно учесть, что роль должна проходить в dryrun (check_mode) перед установкой полностью, а после установки, с использованием любого из тегов. Если лениво подписывать каждую таску тегом можно применить вот такую конструкцию:
- name: Check variables
include_tasks:
file: pre_task.yml
apply:
tags: check # применяет тег ко всем таскам в файле
tags: check
Иногда получается вот такая структура:
my_app/
├── tasks/
│ ├── install.yml
│ ├── update.yml
│ ├── config.yml
│ ├── certs.yml
│ ├── remove.yml
│ └── main.yml # импорт всех задач с тегами
Безопасная работа с секретами в Ansible: no_log: true
Для защиты чувствительных данных (пароли, ключи, токены) в логах Ansible нужно использовать no_log: true
. Это предотвращает запись секретов в:
- Консольный вывод
- Файлы логов
- Системы мониторинга
Правильная реализация
# Для отдельных задач с секретами
- name: Set database password
ansible.builtin.lineinfile:
path: /etc/app.conf
line: "DB_PASSWORD={{ db_password }}"
no_log: true # ← Важно!
# Для целых блоков
- name: Secrets handling block
block:
- name: Create API key
ansible.builtin.command: generate-key.sh
register: api_key_result
- name: Deploy key to vault
ansible.builtin.uri:
url: "https://vault.example.com"
body: "{{ api_key_result.stdout }}"
no_log: true # Скрывает ВЕСЬ вывод блок
# Когда переменная содержит секрет:
- name: Configure secret token
ansible.builtin.template:
src: token.j2
dest: /etc/secrets/token
vars:
secret_token: "{{ vaulted_token }}" # Переменная из vault
no_log: true
# Для результатов выполненных задач (register):
- name: Get sensitive data
ansible.builtin.command: decrypt.sh
register: decrypted_data
no_log: true
- name: Use secured data
debug:
msg: "Data processed successfully"
when: decrypted_data.rc == 0
# Для модулей с секретами (например uri):
- name: Auth request
ansible.builtin.uri:
url: "https://api.example.com/login"
body:
username: admin
password: "{{ vaulted_pass }}"
no_log: true
# Комбинируйте с Ansible Vault:
vars:
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
643865...
# Комбинируйте с Hashicorp Vault
vars:
msql_password: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/data/hello token=my_vault_token url=http://myvault_url:8200') }}"
Как проверить защиту?
- Запустите плейбук с
-vvv
- Убедитесь что в выводе нет:
- "Changed": true, "DB_PASSWORD": "s3cr3t" + "output": "********"
Проверка пререквизитов в pre_tasks
Чтобы гарантировать корректную работу роли, все требования должны проверяться до её выполнения. Для этого используем pre_tasks
в плейбуке tasks/main.yml.
---
- name: "1. Check OS compatibility (Ubuntu/Debian)"
ansible.builtin.fail:
msg: "Unsupported OS. Required: Ubuntu/Debian"
when: ansible_facts['distribution'] not in ['Ubuntu', 'Debian']
- name: "2. Verify free disk space > 1GB"
ansible.builtin.command: df -BG /
register: disk_space
changed_when: false
failed_when:
- disk_space.rc != 0
or (disk_space.stdout | regex_search('\\d+G')) | int < 1
# Проверка доступности портов
- name: "3. Check ports 80/443 are available"
ansible.builtin.wait_for:
port: "{{ item }}"
state: stopped
timeout: 1
loop: [80, 443]
ignore_errors: true
register: ports_check
failed_when: ports_check.results | selectattr('failed') | list | length > 0
# Зависимости (установка)
- name: "Ensure curl is installed"
apt:
name: curl
state: present
# Блокирующие проверки
- name: "Fail if Docker not found"
ansible.builtin.command: docker --version
register: docker_check
failed_when: docker_check.rc != 0
# Неблокирующие предупреждения
- name: "Warn about low RAM"
ansible.builtin.debug:
msg: "Recommended: 4GB RAM (found {{ ansible_memtotal_mb }}MB)"
changed_when: false
when: ansible_memtotal_mb < 4096
# использование модуля assert
- name: After version 2.7 both O(msg) and O(fail_msg) can customize failing assertion message
ansible.builtin.assert:
that:
- my_param <= 100
- my_param >= 0
fail_msg: "'my_param' must be between 0 and 100"
success_msg: "'my_param' is between 0 and 100"
Нюансы реализации:
- Используйте параметры модулей
fail
иassert
для вывода понятных сообщений об ошибках. Старайтесь придерживаться одного из этих модулей в роли — будет красивее смотреться и быстрее дебажить. - Все проверки должны иметь
changed_when: false
. - Для «тяжелых» проверок добавляйте
run_once: true
. - Укажите все проверки в
README.md
Итог:
- Роль выполняется только при соблюдении всех условий
- Четкие сообщения об ошибках
- Нет «тихих» сбоев на этапе выполнения
Обработка ошибок в Ansible (block/rescue)
Чтобы роль не падала при некритических ошибках (например, если сервис временно недоступен), используем связку block
+ rescue
.
Это аналог try/catch
в других языках.
Допустим, мы копируем конфиг и перезапускаем сервис, но хотим:
- Продолжить выполнение, если конфиг скопировался, но сервис не перезапустился.
- Записать ошибку в лог, но не прерывать всю роль.
---
- name: Critical operations block
block:
- name: Task 1 - Copy config
ansible.builtin.template:
src: config.j2
dest: /etc/app/config.conf
register: taskresult
notify: restart app
- name: Task 2 - Validate config
ansible.builtin.command: app --validate
register: taskresult
changed_when: false
rescue:
- name: Add error to list
set_fact:
role_errors: "{{ role_errors | default([]) + [{
'task': ansible_failed_task.name,
'error': ansible_failed_result.msg
}] }}"
- name: Continue execution
meta: continue
- name: Print all errors (if any)
ansible.builtin.debug:
var: role_errors
failed_when: role_errors | length > 0
Как это работает:
- Основной блок (block)
- Каждая задача регистрирует результат в
taskresult
- При ошибке — переход в
rescue
- Каждая задача регистрирует результат в
- Обработка ошибок (rescue)
- Добавляем форматированную ошибку в список.
- Продолжаем выполнение (
meta: continue
)
- Финальный отчет
- После всех задач выводим список ошибок (если они есть) и если они есть выдаем ошибку.
Пример вывода при ошибках:
"role_errors": [
{
"task": "Task 2 - Validate config",
"error": "Command 'app --validate' returned 1: ERROR: Invalid config"
}
]
Преимущества такого подхода:
- Полная трассировка ошибок — видно какие именно задачи упали
- Аккуратный вывод — все ошибки собираются в одном месте
- Гибкость — можно добавить дополнительные поля (время, хост и т.д.)
- Скорость исправления и отладки — можно разом собрать все ошибки и попытаться их исправить.
Неидемпотентная роль — выстрел в ногу
Идемпотентность — ключевое требование к Ansible-ролям. Это означает, что:
✔ Повторный запуск роли не должен делать лишних изменений
✔ Система после каждого запуска должна приходить в одинаковое состояние
Как добиться идемпотентности?
Большинство модулей Ansible уже идемпотентны (apt, yum, template и др.). Но иногда (Для командных модулей (command
, shell
, raw
) всегда) нужно ручное управление через:
- changed_when: false # Всегда показывает «ok» (даже если что-то делал)
- changed_when: условие # Кастомное условие для «changed»
Примеры использования.
# Команды, которые всегда меняют состояние
- name: Check service stataus
command: systemctl is-active nginx
register: nginx_status
changed_when: false # ← Не влияет на систему, поэтому "ok"
- name: Force reload (если нужно)
command: systemctl reload nginx
when: nginx_status.stdout != "active"
# Кастомная проверка изменений
- name: Apply config if changed
template:
src: app.conf.j2
dest: /etc/app.conf
register: config_result
changed_when: config_result.changed # Стандартное поведение (можно опустить)
# Условный "changed" для скриптов
- name: Run database migration
command: /opt/app/migrate.py
register: migration_result
changed_when:
- "'Success' in migration_result.stdout" # ← "changed" только при успехе
- migration_result.rc == 0
# Для задач с always_run
- name: Validate config (выполняется всегда)
command: validate_config.sh
changed_when: false
check_mode: no
always_run: yes
# Для обработчиков (handlers) handlers/main.yml:
- name: migrate app
command: /opt/app/migrate.py
changed_when: false # ← Чтобы не показывал "changed" при каждом вызове
# Для сложных проверок: Используйте failed_when вместе с changed_when:
- name: Check license
command: check_license.sh
register: license_check
changed_when: false
failed_when:
- license_check.rc != 0
- "'Expired' in license_check.stdout"
Как проверить идемпотентность
- Запустите роль дважды
- ansible-playbook playbook.yml && ansible-playbook playbook.yml
- Ищите задачи с
changed!=0
при повторном запуске — это точки неидемпотентности.
Вынос сложной логики в кастомные модули Ansible
Когда в роли появляются сложные проверки, вычисления или работа с API, их лучше выносить в отдельные модули. Это:
- Упрощает поддержку кода
- Повышает производительность (модули выполняются на Python)
- Позволяет переиспользовать логику
Когда нужно выносить логику в модуль?
- Сложная валидация — Парсинг JSON/XML, Проверка сертификатов/подписей
- Работа с API — Запросы к Kubernetes, AWS, Database
- Громоздкие вычисления — Обработка больших данных, Математические операции
- Специфичная логика — Генерация конфигов со сложными условиями
Создаем кастомный модуль
roles/
└── my_role/
├── library/ # Сюда кладем модули
│ └── cert_validator.py
├── tasks/
│ └── main.yml
└── defaults/
└── main.yml
Пример модуля (library/cert_validator.py
):
#!/usr/bin/python3
# Используйте AnsibleModule
from ansible.module_utils.basic import AnsibleModule
import OpenSSL.crypto
from datetime import datetime
# Пишите документацию к модулю
DOCUMENTATION = r'''
module: cert_validator
description: Check SSL certificate expiry
options:
cert_path:
description: Path to PEM certificate
required: true
type: str
'''
def check_cert(cert_path):
# Обрабатывайте ошибки
try:
with open(cert_path, 'rb') as f:
cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, f.read()
)
expiry_date = datetime.strptime(
cert.get_notAfter().decode('utf-8'), '%Y%m%d%H%M%SZ'
)
return {
'valid': datetime.now() < expiry_date,
'expiry_date': expiry_date.isoformat()
}
except Exception as e:
return {'error': str(e)}
def main():
module = AnsibleModule(
argument_spec=dict(
cert_path=dict(type='str', required=True)
)
)
result = check_cert(module.params['cert_path'])
if 'error' in result:
module.fail_json(msg=result['error'])
module.exit_json(**result)
if __name__ == '__main__':
main()
Используем модуль в роли
---
- name: Validate SSL certificate
cert_validator:
cert_path: "{{ nginx__ssl_cert }}"
register: cert_check
- name: Fail if cert invalid
ansible.builtin.fail:
msg: "Certificate expires on {{ cert_check.expiry_date }}"
when: not cert_check.valid
Преимущества подхода
- Производительность — Модуль выполняет кучу команд со сложной логикой (в отличие от набора последовательных set_fact и циклов ansible).
- Безопасность — Нет риска инъекций (в отличие от сырых команд).
- Идемпотентность — Встроенная поддержка
changed
/failed
состояний. - Тестируемость — Модуль можно проверить отдельно от роли.
Советы по разработке модулей
- Добавляйте документацию
- Обрабатывайте ошибки
- Тестируйте локально
python library/cert_validator.py '{"cert_path":"/tmp/cert.pem"}'
Альтернативы для простых случаев
Если модуль — это overkill, используйте:
ansible.builtin.script
- name: Run validation script
ansible.builtin.script:
cmd: scripts/validate_cert.sh {{ cert_path }}- Фильтры Jinja2
- set_fact:
is_valid: "{{ cert_data | regex_search('VALID') }}"
Обработчики (handlers/main.yml)
Хендлеры – это «отложенные задачи», которые:
- Срабатывают только при изменениях (если был
notify
). - Выполняются один раз, даже если их вызвали несколько раз.
- Помогают избежать лишних действий (например, множественных перезапусков сервиса).
Когда использовать хендлеры?
- Перезапуск сервисов после изменения конфигов.
- Перечитывание конфигурации после изменения конфигов.
- Перезагрузка демонов после настройки параметров.
- Отправка уведомлений (например, оповещение в почту при изменениях).
И всегда добавь в конец роли принудительный вызов хендлеров. И конечно появляется логичный вопрос — а почему, зачем? Ответ: потому, что все хендлеры по умолчанию срабатывают не по завершению роли, а по завершению плея. И если в одном плее указано несколько ролей есть секция tasks и post_task, и они завязаны на результат выполнения конкретной роли, то тогда обязательно надо принудительно хендлеры в конце этой роли вызывать. ( Огромное спасибо за информацию FactorT )
- name: Flush handlers at end
meta: flush_handlers
tags: always
Документация Ansible-роли (README.md)
Хорошая документация помогает другим братьям-администраторам быстро понять, как использовать роль (и не приставать к тебе с глупыми вопросами, отвлекая от размышления о вечном). Вот структура README.md
:
- Название роли
- Ключевые переменные
- Теги
- Пререквизиты
- Сценарии использования
- Примеры переменных в defaults/main.yml
- Советы по использованию
Тестирование роли
Тестирование ролей Ansible должно проводиться как в кластерной среде, так и в отдельной (standalone) конфигурации. Это необходимо для обеспечения корректной работы ролей в различных условиях и на всех поддерживаемых операционных системах и конфигурациях.
Если в функционале ролей происходят изменения, перед слиянием (мержем) необходимо обновить тесты с использованием фреймворка Molecule и явно протестировать новый функционал. Это позволит гарантировать, что новые изменения не нарушают существующую функциональность и что роли продолжают работать корректно. Чувствую твой правомерный гнев — «зачем тестировать, ведь у меня локально на моей продуктивной инфраструктуре работает», но если роль ты пишешь не только для себя, то надо протестировать различные варианты.
Надеюсь, что вышеизложенное поможет тебе в трудовыебуднях.