Гайдов и практик по написанию - куча. Все их можно легко найти - приводить их не буду. В данной статье я попытаюсь структурировать все мои шишки, полученные в рамках написания и эксплуатации ролей 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 и явно протестировать новый функционал. Это позволит гарантировать, что новые изменения не нарушают существующую функциональность и что роли продолжают работать корректно. Чувствую твой правомерный гнев - "зачем тестировать, ведь у меня локально на моей продуктивной инфраструктуре работает", но если роль ты пишешь не только для себя, то надо протестировать различные варианты.
Надеюсь, что вышеизложенное поможет тебе в трудовыебуднях.