Как стать автором
Обновить

Как запилить годную ролюху в Ansible

Уровень сложностиПростой
Время на прочтение10 мин
Количество просмотров595

Гайдов и практик по написанию - куча. Все их можно легко найти - приводить их не буду. В данной статье я попытаюсь структурировать все мои шишки, полученные в рамках написания и эксплуатации ролей Ansible и рассказать как легко написать роль без регистрации и СМС.

Содержание

Алгоритм написания роли в Ansible

Хорошая роль Ansible должна быть модульной, переиспользуемой и хорошо документированной. Вот пошаговый алгоритм создания роли:

Определение функционала роли

Проанализируй свою потребность и ответь на вопрос "Что должна делать роль"?

  • Устанавливать и настраивать один сервис (Nginx, PostgreSQL, Docker и т. д.).

  • Настраивать системные параметры (например, sysctllimits.conf).

  • Разворачивать приложение (например, WordPress, Prometheus).

  • Конфигурировать отдельный сервис.

  • Выдавать права и пр. и др.

При определении функционала не стоит смешивать несколько несвязанных задач (например, установка Nginx + настройка БД).

Нюансы написание ролюхи.

---
- 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') }}"

Как проверить защиту?

  1. Запустите плейбук с -vvv

  2. Убедитесь что в выводе нет:

    - "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

Нюансы реализации:

  1. Используйте fail, а не assert для понятных сообщений об ошибках.

  2. Все проверки должны иметь changed_when: false.

  3. Для "тяжелых" проверок добавляйте run_once: true.

  4. Укажите все проверки в README.md

Итог:

  • Роль выполняется только при соблюдении всех условий

  • Четкие сообщения об ошибках

  • Нет "тихих" сбоев на этапе выполнения

Обработка ошибок в Ansible (block/rescue)

Чтобы роль не падала при некритических ошибках (например, если сервис временно недоступен), используем связку block + rescue.
Это аналог try/catch в других языках.

Допустим, мы копируем конфиг и перезапускаем сервис, но хотим:

  1. Продолжить выполнение, если конфиг скопировался, но сервис не перезапустился.

  2. Записать ошибку в лог, но не прерывать всю роль.

---
- 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

Как это работает:

  1. Основной блок (block)

    • Каждая задача регистрирует результат в taskresult

    • При ошибке - переход в rescue

  2. Обработка ошибок (rescue)

    • Добавляем форматированную ошибку в список.

    • Продолжаем выполнение (meta: continue)

  3. Финальный отчет

    • После всех задач выводим список ошибок (если они есть) и если они есть выдаем ошибку.

Пример вывода при ошибках:

"role_errors": [
  {
    "task": "Task 2 - Validate config",
    "error": "Command 'app --validate' returned 1: ERROR: Invalid config"
  }
]

Преимущества такого подхода:

  1. Полная трассировка ошибок - видно какие именно задачи упали

  2. Аккуратный вывод - все ошибки собираются в одном месте

  3. Гибкость - можно добавить дополнительные поля (время, хост и т.д.)

  4. Скорость исправления и отладки - можно разом собрать все ошибки и попытаться их исправить.

Неидемпотентная роль - выстрел в ногу

Идемпотентность — ключевое требование к Ansible-ролям. Это означает, что:
✔ Повторный запуск роли не должен делать лишних изменений
✔ Система после каждого запуска должна приходить в одинаковое состояние

Как добиться идемпотентности?

Большинство модулей Ansible уже идемпотентны (apt, yum, template и др.). Но иногда (Для командных модулей (commandshellraw) всегда) нужно ручное управление через:

  • 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)

  • Позволяет переиспользовать логику

Когда нужно выносить логику в модуль?

  1. Сложная валидация - Парсинг JSON/XML, Проверка сертификатов/подписей

  2. Работа с API - Запросы к Kubernetes, AWS, Database

  3. Громоздкие вычисления - Обработка больших данных, Математические операции

  4. Специфичная логика - Генерация конфигов со сложными условиями

Создаем кастомный модуль

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, используйте:

  1. ansible.builtin.script

    - name: Run validation script
      ansible.builtin.script:
        cmd: scripts/validate_cert.sh {{ cert_path }}
  2. Фильтры 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 и явно протестировать новый функционал. Это позволит гарантировать, что новые изменения не нарушают существующую функциональность и что роли продолжают работать корректно. Чувствую твой правомерный гнев - "зачем тестировать, ведь у меня локально на моей продуктивной инфраструктуре работает", но если роль ты пишешь не только для себя, то надо протестировать различные варианты.

Надеюсь, что вышеизложенное поможет тебе в трудовыебуднях.

Теги:
Хабы:
+3
Комментарии0

Публикации

Работа

Ближайшие события