Гайдов и практик по написанию — куча. Все их можно легко найти — приводить их не буду. В данной статье я попытаюсь структурировать все мои шишки, полученные в рамках написания и эксплуатации ролей Ansible.
Содержание
Алгоритм написания роли в Ansible
Хорошая роль Ansible должна быть модульной, переиспользуемой и хорошо документированной. Вот пошаговый алгоритм создания роли:
Определение функционала роли
Проанализируй свою потребность и ответь на вопрос «Что должна делать роль»?
-
Устанавливать и настраивать один сервис (Nginx, PostgreSQL, Docker и т. д.).
-
Настраивать системные параметры (например,
sysctl,limits.conf). -
Разворачивать приложение (например, WordPress, Prometheus).
-
Конфигурировать отдельный сервис.
-
Выдавать права и пр. и др.
При определении функционала не стоит смешивать несколько несвязанных задач (например, установка Nginx + настройка БД).
Нюансы написание ролюхи.
-
Разбивать сложные задачи на подзадачи (
include_tasks) и использовать теги (tags) для выборочного запуска. -
Чтобы избежать конфликтов и повысить читаемость кода, все переменные (включая временные) должны начинаться с префикса имени роли.
--- - 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).
Примерная структура тегов:
|
Тег |
Действие |
|---|---|
|
|
Установка ПО |
|
|
Обновление (если версия изменилась) |
|
|
Настройка конфигов |
|
|
Обновление TLS/SSL |
|
|
Полное удаление ПО |
Особое внимание стоит уделить применению (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
Нюансы реализации:
-
Используйте
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=1при повторном запуске — это точки неидемпотентности.
Вынос сложной логики в кастомные модули 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
Преимущества подхода
-
Производительность — Модуль выполняется 1 раз (в отличие от
command/shell). -
Безопасность — Нет риска инъекций (в отличие от сырых команд).
-
Идемпотентность — Встроенная поддержка
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). -
Выполняются один раз, даже если их вызвали несколько раз.
-
Помогают избежать лишних действий (например, множественных перезапусков сервиса).
Когда использовать хендлеры?
-
Перезапуск сервисов после изменения конфигов.
-
Перечитывание конфигурации после изменения конфигов.
-
Перезагрузка демонов после настройки параметров.
-
Отправка уведомлений (например, оповещение в почту при изменениях).
И всегда добавь в конец роли
- name: Flush handlers at end meta: flush_handlers tags: always
Документация Ansible-роли (README.md)
Хорошая документация помогает другим братьям-администраторам быстро понять, как использовать роль (и не приставать к тебе с глупыми вопросами, отвлекая от размышления о вечном). Вот структура README.md:
-
Название роли
-
Ключевые переменные
-
Теги
-
Пререквизиты
-
Сценарии использования
-
Примеры переменных в defaults/main.yml
-
Советы по использованию
Тестирование роли
Тестирование ролей Ansible должно проводиться как в кластерной среде, так и в отдельной (standalone) конфигурации. Это необходимо для обеспечения корректной работы ролей в различных условиях и на всех поддерживаемых операционных системах и конфигурациях.
Если в функционале ролей происходят изменения, перед слиянием (мержем) необходимо обновить тесты с использованием фреймворка Molecule и явно протестировать новый функционал. Это позволит гарантировать, что новые изменения не нарушают существующую функциональность и что роли продолжают работать корректно.
Надеюсь, что вышеизложенное поможет Вам в трудовыебуднях.
ссылка на оригинал статьи https://habr.com/ru/articles/909636/
Добавить комментарий