Гайдов и практик по написанию - куча. Все их можно легко найти - приводить их не буду. В данной статье я попытаюсь структурировать все мои шишки, полученные в рамках написания и эксплуатации ролей 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
# использование модуля 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=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 и явно протестировать новый функционал. Это позволит гарантировать, что новые изменения не нарушают существующую функциональность и что роли продолжают работать корректно. Чувствую твой правомерный гнев - "зачем тестировать, ведь у меня локально на моей продуктивной инфраструктуре работает", но если роль ты пишешь не только для себя, то надо протестировать различные варианты.
Надеюсь, что вышеизложенное поможет тебе в трудовыебуднях.
Комментарии (20)
siferdj
16.05.2025 05:51Очень рекомендую делать снапшот чистой ВМ перед работой. Со временем создания роли/мультироли система обрастает мелочами, которые в последующих запусках не требуются, но на первом запуске - всегда нужны. А после каждого добавления нового функционала возвращать машину к первозданному виду.
Например, в ходе установки какого-то ПО понадобилось включить ip_forward. На первый раз решил включить руками, а потом это не требуется (можно запамятовать). Вот такие моменты помогает вычислить возвраты к первому состоянию ВМ.
Ещё рекомендую использовать мультироли. Каждая суброль должна выполнять маленький блок и быть независимой. Как раз теги помогут вызвать именно ту самую суброль, которая нужна. А meta поможет настроить между ними зависимости в случае необходимости.
Например, вам необходимо установить ПО, которое деплоится только через Docker Compose. Вот тут и пригодится зависимость через meta, которая устанавливает Docker.
Примеры очень упрощённые, но думаю суть вы поняли.
Yusmit Автор
16.05.2025 05:51Опять по порядку, на основании собственно опыта.
Например, в ходе установки какого-то ПО понадобилось включить ip_forward. На первый раз решил включить руками
Один раз включаю руками, второй отлаживаю плэйбук, третий качу новой версией роли. Любой чих должен быть описан - так документируется инфраструктура как код.
Например, вам необходимо установить ПО, которое деплоится только через Docker Compose
Значит ставим Compose как зависимость. Вызывать тегами отдельную роль - для меня странно. Я придерживаюсь подхода в котором в любой роли теги имеют одинаковое именование. При вашем подходе, вместо изолированного конфигурирования роли А ( тег config) я пройдусь по по всему кластеру.
Про мультироли наверное самый яркий пример - установка отказоустойчивого кластера postgres. В некоторых случаях наверное это более понятная концепция, нежели вязать через CI/tower последовательный деплой.
FactorT
16.05.2025 05:51no_log: true хорошо применять в проде. А как быть со средой дев, когда нам нужно проводить отладку и видеть чувствительные данные?
Yusmit Автор
16.05.2025 05:51Можно использовать переменные
no_log: "{{ rolename_env_nolog }}"
Меняем на деве - радуемся. На проде можно в CI проверку вставить.
FactorT
16.05.2025 05:51Используйте fail, а не assert для понятных сообщений об ошибках.
Позвольте не согласиться. Assert имеет функционал по выводу понятных сообщений как в случае успешной проверки, так и неуспешной.
Пример:
- name: Example assert 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"
Yusmit Автор
16.05.2025 05:51На вкус и цвет все фломастеры разные. При выборе из этих двух модулей я ориентируюсь на то что было написано до меня, стараясь писать понятные условия-проверки.
Учёл. Исправил.
FactorT
16.05.2025 05:51И всегда добавь в конец роли принудительный вызов хендлеров.
И конечно появляется логичный вопрос - а почему, зачем?
Ответ: потому, что все хендлеры по умолчанию срабатывают не по завершению роли, а по завершению плея. И если в одном плее указано несколько ролей есть секция tasks и post_task, и они завязаны на результат выполнения конкретной роли, то тогда обязательно надо принудительно хендлеры в конце этой роли вызывать.
slonopotamus
Расскажите, как это выглядит на практике. Допустим, в версии 1 роль делала apt-get install libfoo. В версии 2 роли libfoo больше не нужна и она перестала делать apt-get install. Вопрос. Как привести машины, на которых ранее прогонялась версия 1, в одинаковое состояние с машинами, на которых сразу выполняли версию 2? Удалять в роли пакет? Нельзя, может он другим ролям нужен, не?
andreymal
Добавить libfoo в эти самые другие роли, не?
slonopotamus
А если таких ролей не оказалось? Кто удалит libfoo?
trabl
Как вариант, делаем отдельный таск удаления пакета libfoo, если он установлен и прогоняем по нужной группе хостов. Навешиваем тэг таске для установки пакета libfoo, и при следующем запуске роли этот тэг игнорируем.
Yusmit Автор
Я бы не стал так делать. Когда на инфру иду - прогоняю роль драйраном - о том что кто-то, когда-то пакет удалял таской или скипом - я не знаю. А значит - неожиданные изменения в выводе, паника, ругань, проклятия в адрес того кто руками работает.
Yusmit Автор
Давайте разберемся.
Во первых речь зашла о запуске двух разных версий роли на инфраструктуру.
Во вторых если речь о вспомогальном пакете- библиотеке, напрямую не влияющием на функционал ПО из роли (например jq) - я бы грохнул, без зазрения совести. Если же речь о пакете - зависимости, то либо он не нужен, так как меняется версия, устанавливаемого ПО (а значит меняются переменные - версии и ни о какой идемпотентности после запусков не может быть речи), либо в новой версии у нас появился пересобранный deb пакет без зависимости. А если другим ролям всё таки этот пакет нужен - то смотри пункт про проверку зависимостей. И пошагово:
Роль А версии 1 установила зависимость.
Роль Б подтвердила наличие зависимости.
Роль А версии 2 удалила зависимость при apt install -y.
Роль Б установила зависимость.
Роль А версии 2 не внесла изменений при apt install -y.
slonopotamus
Эм в смысле, вы предлагаете вечно хранить все версии роли? Что будем делать когда древняя версия сломалась? Ну там сертификат протух или урл сменился или ещё какая хрень?
Yusmit Автор
Да. Именно версионирование я и исповедую. Поменялся урл - в новой версии инвентаря - задам новый. Хранить вечно - избыточно, но иметь возможность отката - необходимо.
Кстати, спасибо за идею.
siferdj
Не забывайте про SOLID. Принцип единой ответственности никто не отменял. Это существенная смена функционала. Т.е. в вашем случае пишем дополнительную проверку на наличие версии и формируем проведение в зависимости от потребностей (оставляем как есть/удаляем и ставим новую).
Yusmit Автор
Кстати, я обычно делаю versionlock для пакета - это прям про идемпотентность, если на тачке пасётся ещё кто нибудь с котом кроме меня. При управляемом обновлении поконтурно указываю новую.
Yusmit Автор
С рутом
slonopotamus
Как роли принять решение об удалении если она не знает о существовании других ролей, которые могут не хотеть удаления?
Yusmit Автор
Роль не знает ничего об окружении и других ролях. Про вспомогательный пакет я уже писал. apt yum dnf rmp не удаляют "чужие" используемые зависимости.