Разработка собственного модуля Ansible на Python

Рейтинг: 78.5% · 14 голосов
Подробный курс по Ansible с прицелом на экзамен RHCE EX294: установка и инвентарь, плейбуки, переменные и факты, Vault, циклы и условия, шаблоны Jinja2, роли и коллекции, Execution Environments и ansible-navigator, управление системами (диски, LVM, cron, SELinux, firewalld). Примеры на RHEL/Fedora. Актуально на 2026.
Ответить
Аватара пользователя
Maksim_DevOps
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Разработка собственного модуля Ansible на Python

Сообщение Maksim_DevOps »

Оглавление курса (47)
  1. Что такое Ansible и зачем он нужен: автоматизация без агентов
  2. Сертификация RHCE и экзамен EX294: что внутри и как готовиться
  3. Архитектура Ansible: control node, узлы, модули и плагины
  4. Установка Ansible на control node: dnf, pip и версии ansible-core
  5. Подготовка управляемых узлов: SSH, пользователь и sudo
  6. Инвентарь Ansible: hosts, группы и переменные
  7. Настройка Ansible: файл ansible.cfg и приоритеты конфигурации
  8. Host patterns и инструменты командной строки Ansible
  9. Ad hoc команды Ansible: быстрые задачи без плейбука
  10. Ansible playbook: что это такое и как устроен
  11. Структура плейбука: play, task, модули и переменные
  12. Запуск плейбуков: проверка, теги, ограничения и отладка
  13. Параллелизм и порядок выполнения: forks, serial, strategy
  14. Факты Ansible: сбор информации об узлах
  15. Переменные Ansible: типы, объявление и приоритет
  16. Регистрация результатов и специальные переменные
  17. Ansible Vault: шифрование паролей и секретов
  18. Организация инвентаря: group_vars, host_vars и вложенные группы
  19. Динамический инвентарь и инвентарные плагины
  20. Циклы в Ansible: loop, списки и словари
  21. Повтор задачи до условия: until, retries и delay
  22. Условия в Ansible: директива when и логика
  23. Jinja2 в Ansible: выражения, фильтры и подстановки
  24. Обработчики Ansible: handlers, notify и flush_handlers
  25. Обработка ошибок: failed_when, changed_when и ignore_errors
  26. Блоки в Ansible: block, rescue и always
  27. Управление файлами: модули file, copy, fetch и stat
  28. Архивы и сборка файлов: archive, unarchive, assemble
  29. Точечное редактирование файлов: lineinfile и blockinfile
  30. Шаблоны Jinja2: модуль template и динамические конфиги
  31. Jinja2 продвинуто: фильтры, циклы, макросы и lookup
  32. Модули Ansible: ansible-doc и поиск нужного модуля
  33. Разработка собственного модуля Ansible на Python (вы здесь)
  34. Роли Ansible и Ansible Galaxy: переиспользование
  35. Разработка роли Ansible: создание с нуля
  36. Зависимости ролей и requirements.yml
  37. Коллекции Ansible: ansible.posix, community и своя коллекция
  38. Execution Environments: контейнеры для запуска Ansible
  39. Сборка Execution Environment с ansible-builder
  40. ansible-navigator: запуск плейбуков в Execution Environment
  41. Управление SSH-ключами через Ansible: authorized_key
  42. Управление дисками: filesystem, mount и parted
  43. LVM через Ansible: тома, группы и расширение
  44. Планировщик задач: cron и systemd timers через Ansible
  45. Безопасность: hardening SSH и репозитории dnf/yum
  46. SELinux через Ansible: булевы, контексты и порты
  47. Сквозной проект и подготовка к экзамену EX294
Рано или поздно ты упрёшься в стену: задача нетипичная, а готового модуля под неё нет. Дёргать чужой REST API через uri, парсить ответ через json_query, городить пять задач с set_fact и условиями - и всё равно получается хрупкая конструкция, которая ломается при первом же check-режиме. В такие моменты проще написать свой модуль на Python, чем городить костыли из shell и command с register. Этот урок про то, как делается разработка модуля Ansible с нуля: анатомия, идемпотентность, поддержка check_mode и документация, которую подхватит ansible-doc.

Когда вообще нужен свой модуль Ansible

Сначала честный фильтр. Свой модуль - это не первый инструмент, а последний. Сперва ищи готовое: коллекций в Ansible Galaxy тысячи, и почти всё, что нужно админу, уже покрыто (ansible.builtin, ansible.posix, community.general и сотни вендорских). Свой код имеет смысл писать, когда:
  • нет подходящего готового модуля, а задача повторяется из плейбука в плейбук;
  • ты обвязываешь внутренний API или самописную утилиту, про которую никто, кроме тебя, не знает;
  • связка из command/shell с register и кучей when превратилась в нечитаемое месиво и не умеет в check_mode;
  • нужна настоящая идемпотентность и понятный changed, а не угадывание по тексту вывода.
Сразу про развилку. Если тебе нужно преобразовать данные внутри Jinja2 (превратить строку в список, посчитать хеш) - это filter-плагин. Если нужно притащить данные откуда-то снаружи во время рендеринга (прочитать файл, сходить в Vault) - это lookup-плагин. А модуль - это то, что выполняет действие на управляемом узле и меняет его состояние: ставит пакет, правит конфиг, дёргает API. Когда у тебя глагол "сделай на хосте" - это модуль. Когда "дай мне значение для шаблона" - это плагин. Не путай, иначе будешь писать модуль там, где хватило бы трёх строк фильтра.

Изображение

Анатомия: как написать модуль Ansible на Python

Любой ansible python module строится вокруг класса AnsibleModule из ansible.module_utils.basic. Он берёт на себя грязную работу: парсит и валидирует аргументы по схеме argument_spec, понимает check_mode, форматирует вывод в JSON. Тебе остаётся бизнес-логика.

Каркас всегда один и тот же. Объявляешь, какие параметры принимаешь (argument_spec), создаёшь AnsibleModule с supports_check_mode=True, читаешь module.params, что-то делаешь и завершаешься через exit_json (успех) или fail_json (ошибка). Никаких print и sys.exit - только эти два метода, иначе Ansible не поймёт результат.

Минимальный, но рабочий пример. Модуль управляет строкой "ключ=значение" в простом ini-подобном файле. Положи его в library/keyval.py рядом с плейбуком:

Код: Выделить всё

#!/usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import annotations

DOCUMENTATION = r'''
---
module: keyval
short_description: Управляет строкой key=value в простом конфиге
description:
  - Гарантирует наличие или отсутствие строки вида key=value в файле.
  - Идемпотентен и поддерживает режим проверки (--check).
options:
  path:
    description: Путь к файлу конфигурации.
    required: true
    type: path
  key:
    description: Имя ключа.
    required: true
    type: str
  value:
    description: Значение ключа. Игнорируется при state=absent.
    required: false
    type: str
  state:
    description: Должна ли строка присутствовать.
    default: present
    choices: [present, absent]
    type: str
author:
  - Cyberlake Academy
'''

EXAMPLES = r'''
- name: Прописать порт в конфиг
  keyval:
    path: /etc/myapp/app.conf
    key: port
    value: "8080"
    state: present

- name: Убрать устаревший ключ
  keyval:
    path: /etc/myapp/app.conf
    key: legacy_mode
    state: absent
'''

RETURN = r'''
changed:
  description: Был ли изменён файл.
  type: bool
  returned: always
line:
  description: Итоговая строка key=value (при state=present).
  type: str
  returned: when state is present
'''

from ansible.module_utils.basic import AnsibleModule
import os


def run_module():
    argument_spec = dict(
        path=dict(type='path', required=True),
        key=dict(type='str', required=True),
        value=dict(type='str', required=False),
        state=dict(type='str', default='present',
                   choices=['present', 'absent']),
    )

    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
        required_if=[('state', 'present', ['value'])],
    )

    path = module.params['path']
    key = module.params['key']
    value = module.params['value']
    state = module.params['state']
    desired = "{0}={1}".format(key, value)

    # читаем текущее состояние
    lines = []
    if os.path.exists(path):
        with open(path, 'r', encoding='utf-8') as fh:
            lines = fh.read().splitlines()

    new_lines = [ln for ln in lines if not ln.startswith(key + '=')]
    changed = False
    result = {'changed': False}

    if state == 'present':
        new_lines.append(desired)
        result['line'] = desired
        changed = (new_lines != lines)
    else:  # absent
        changed = (new_lines != lines)

    result['changed'] = changed

    # в check-режиме ничего не пишем, только сообщаем намерение
    if module.check_mode or not changed:
        module.exit_json(**result)

    try:
        with open(path, 'w', encoding='utf-8') as fh:
            fh.write("\n".join(new_lines) + "\n")
    except OSError as exc:
        module.fail_json(msg="Не удалось записать {0}: {1}".format(path, exc),
                         **result)

    module.exit_json(**result)


def main():
    run_module()


if __name__ == '__main__':
    main()
Разбор ключевых мест. argument_spec - это контракт: тип, required, default, choices. Ansible сам провалидирует ввод до того, как отработает твой код, и сам вернёт внятную ошибку, если тип не тот. required_if - один из готовых валидаторов (есть ещё mutually_exclusive, required_together, required_one_of): здесь он требует value, только когда state=present. supports_check_mode=True - обещание, что модуль умеет холостой прогон; без него Ansible в режиме --check просто пропустит задачу. И главное правило идемпотентности: сначала вычисли, нужно ли что-то менять (changed), и только потом, если не check_mode и changed, реально пиши на диск.

Где лежит модуль, тестирование и ansible-doc

Самый простой способ размещения - каталог library/ рядом с плейбуком. Ansible автоматически подхватывает оттуда модули:

Код: Выделить всё

project/
  ansible.cfg
  site.yml
  library/
    keyval.py
Для роли это roles/myrole/library/, и модуль виден только внутри роли. По-взрослому, когда модуль перерос один проект, его пакуют в коллекцию (namespace.collection в plugins/modules/) и вызывают по FQCN вроде mynamespace.mycoll.keyval. Это же путь к публикации в Galaxy. Но для начала и для экзамена library/ полностью достаточно.

Прогнать модуль в обычном плейбуке:

Код: Выделить всё

- name: Проверяем свой модуль
  hosts: web
  become: true
  tasks:
    - name: Прописать порт
      keyval:
        path: /etc/myapp/app.conf
        key: port
        value: "8080"
      register: out

    - ansible.builtin.debug:
        var: out.line
Сначала всегда гоняй с --check, потом по-настоящему, потом повторно - вторая полная прогонка должна дать changed: false. Это и есть проверка идемпотентности. Для быстрой отладки без плейбука удобен test-module из исходников ansible или прямой запуск с подсунутым JSON-аргументом, но на практике большинству хватает обычного ansible-playbook с -vvv.

Отдельно про документацию. Те три блока DOCUMENTATION, EXAMPLES, RETURN в начале файла - не украшение. Их читает встроенный движок, и твой модуль сразу получает справку наравне со штатными:

Код: Выделить всё

ansible-doc -M ./library keyval
Если YAML внутри блоков битый - ansible-doc выругается, а в CI коллекции это будет красным тестом. Так что валидный YAML в DOCUMENTATION - это и документация, и бесплатный линтер схемы.

Пара слов про окружение по современному стандарту RHCE. На свежих вариантах экзамена (EX294v9 это RHEL 9, EX294v10 это RHEL 10) акцент сместился на ansible-navigator и execution environments - модули там запускаются внутри контейнерного образа. Это значит, что твой самописный модуль из library/ поедет в контейнер вместе с проектом, а его зависимости (например, нестандартные python-библиотеки) должны быть в самом образе EE, а не на хосте. На голом ansible-core (старый стиль, RHEL 8) всё проще: интерпретатор Python берётся прямо с управляемого узла.

Типичные грабли
  • print вместо exit_json. Любой посторонний вывод в stdout ломает парсинг результата. Только exit_json/fail_json, и никаких отладочных print - для отладки есть -vvv и module.debug.
  • Изменения в check_mode. Забыл проверку module.check_mode перед записью - и твой "безопасный" --check молча правит прод. Проверяй check_mode всегда, до любой записи.
  • Вечный changed: true. Если модуль пишет файл при каждом запуске, не сравнивая с текущим состоянием, идемпотентности нет. Сначала вычисляй diff, потом решай, менять ли.
  • Версии Python. ansible-core 2.14 и новее хочет Python 3.10+ на control node и Python 3.6+ на управляемых узлах. Код модуля исполняется на целевом хосте - не тащи туда f-string-фишки, которых нет в его Python, если узлы старые.
  • Кривой YAML в DOCUMENTATION. Лишний таб или неэкранированный символ - и ansible-doc/sanity-тесты падают. Держи блоки в r'''...''' и проверяй ansible-doc сразу.
Мини-лаба

Повтори руками, без копипаста:
  • Создай каталог проекта с ansible.cfg и подкаталогом library/, положи туда keyval.py из урока.
  • Напиши плейбук, который через свой модуль добавляет два ключа в /tmp/app.conf, прогони его трижды: --check, обычный, ещё раз обычный. Убедись, что третий прогон даёт changed: false.
  • Поменяй задачу на state=absent для одного ключа и проверь, что строка исчезла, а повторный запуск снова даёт changed: false.
  • Сломай намеренно один из блоков (убери двоеточие в DOCUMENTATION) и посмотри, как ругнётся ansible-doc -M ./library keyval. Почини.
  • Бонус: добавь параметр backup: bool с default=false и логику резервной копии файла перед записью.
Контрольные вопросы
  • Чем модуль отличается от filter- и lookup-плагина, и в каком случае писать каждый из них?
  • Что делает supports_check_mode=True и где в коде должна стоять проверка module.check_mode?
  • Как обеспечить идемпотентность, чтобы повторный запуск давал changed: false?
  • Зачем нужны блоки DOCUMENTATION/EXAMPLES/RETURN и какая команда показывает справку по своему модулю?
Итог

Свой модуль на Python - это AnsibleModule, честный argument_spec, идемпотентная логика, аккуратный check_mode и завершение через exit_json/fail_json. Положи его в library/ рядом с плейбуком, опиши тремя блоками для ansible-doc, прогони трижды и убедись в changed: false. На экзамене EX294 эта тема всплывает редко, но в программу гл.7 входит, и понимание устройства модуля делает тебя сильно увереннее в чтении чужого кода и отладке штатных модулей. Пиши свой код только когда готового реально нет - но когда нужно, теперь ты умеешь.
👍2 ❤️1 🔥1 😄 🤔1
Аватара пользователя
lagger
Сообщения: 1
Зарегистрирован: 31 май 2026, 08:27

Re: Разработка собственного модуля Ansible на Python

Сообщение lagger »

Долго пихал свой API через uri и json_query, пока не дошло что проще модуль написать. Спасибо за пример с required_if, как раз не знал что валидацию можно туда вынести.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
vault2010
Сообщения: 1
Зарегистрирован: 13 май 2026, 04:22

Re: Разработка собственного модуля Ansible на Python

Сообщение vault2010 »

Вопрос: если зависимость модуля это сторонняя python-либа, её ставить на managed node или внутрь execution environment? У меня на navigator модуль падает с ModuleNotFoundError хотя на хосте либа стоит.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Модули Ansible: ansible-doc и поиск нужного модуля
Следующая глава →
Роли Ansible и Ansible Galaxy: переиспользование

Все главы курса «Ansible: автоматизация и подготовка к RHCE (EX294)»

Поделиться темой: ✈ Telegram VK
Похожие запросы: что такое ansible простыми словамиansible для начинающих с чего начатьустановка ansible на linuxansible inventory и hosts файлчто такое playbook в ansibleструктура ansible playbook из чего состоит

Вернуться в «Ansible: автоматизация и подготовка к RHCE (EX294)»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость