Перейти к содержанию

Введение в Ansible: инвентари, плейбуки и идемпотентность

Главное ценностное предложение Ansible по сравнению с shell-скриптом — идемпотентность: повторный запуск того же плейбука против уже настроенного хоста должен сообщить «ничего не изменилось» во второй раз, а не упасть с ошибкой или задублировать работу. Эта статья — о том, что именно делает это правдой.

Как это устроено

Инвентарь: какие хосты вы администрируете

# inventory.ini
[webservers]
web1.example.com
web2.example.com ansible_host=10.0.0.12

[databases]
db1.example.com ansible_user=deploy

[production:children]
webservers
databases

Группы ([webservers]) можно вкладывать через :children. ansible_host/ansible_user — переменные соединения — они говорят Ansible, как реально достучаться до этого хоста, отдельно от его отображаемого имени в инвентаре.

ansible webservers -i inventory.ini -m ping

-m ping — собственный модуль Ansible (не ICMP) — он подтверждает, что Python доступен и работает по SSH на цели, самая базовая проверка связности перед запуском чего-то реального.

Плейбуки: декларативные, не процедурные

# site.yml
- name: Configure web servers
  hosts: webservers
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Deploy site configuration
      ansible.builtin.template:
        src: templates/site.conf.j2
        dest: /etc/nginx/conf.d/site.conf
        mode: "0644"
      notify: Reload nginx

    - name: Ensure nginx is enabled and running
      ansible.builtin.systemd:
        name: nginx
        enabled: true
        state: started

  handlers:
    - name: Reload nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded
ansible-playbook -i inventory.ini site.yml

Это декларативно, а не shell-скрипт: state: present означает «убедиться, что пакет установлен» — если он уже установлен, ничего не происходит (отчитывается как ok, не changed); если нет — Ansible его устанавливает. Тот же плейбук, запущенный против уже полностью настроенного хоста, должен показать все задачи как ok с нулём changed — именно так проверяется реальная идемпотентность плейбука.

Хендлеры запускаются только при изменении, и только один раз

notify: Reload nginx срабатывает только если задача template реально изменила файл — и даже если десять разных задач в одном play уведомляют один и тот же хендлер, он всё равно запустится один раз, в конце play. Это избегает, например, десятикратного reload nginx только потому, что десять отдельных конфигов были сгенерированы в одном плейбуке.

Переменные и шаблоны

{# templates/site.conf.j2 #}
server {
    listen {{ http_port | default(80) }};
    server_name {{ inventory_hostname }};
}

{{ inventory_hostname }} — встроенная переменная — имя хоста, как оно указано в инвентаре. | default(80) даёт запасное значение, если http_port нигде не определена для этого хоста, избегая жёсткого падения на отсутствующей переменной.

Проверка, что изменилось бы, без реального изменения

ansible-playbook -i inventory.ini site.yml --check --diff

--check («dry run») сообщает, что было бы изменено, не применяя это на самом деле; --diff показывает реальные различия содержимого для задач с файлами (template, copy, lineinfile). Это самая полезная привычка, которую стоит выработать перед запуском незнакомого или изменённого плейбука против продакшна — всегда сначала --check --diff.

Различия Ubuntu/Debian и RHEL-подобных систем

Сам Ansible не зависит от дистрибутива (это Python, работающий с управляющей машины, подключаясь по SSH — на целевой машине Ansible вообще не нужен, только Python). Практическая разница — в используемых модулях: ansible.builtin.apt против ansible.builtin.dnf/yum, либо независимый от дистрибутива модуль ansible.builtin.package (как в примере выше), который сам выбирает нужный бэкенд по определённому семейству ОС цели. Пример разворачивает файл в /etc/nginx/conf.d/, который пакеты nginx обеих семейств подключают по умолчанию; для каталога sites-available/ в стиле Debian дополнительно понадобилась бы символьная ссылка в sites-enabled/.

Практическое задание

  1. Напишите инвентарь с единственной группой [test], содержащей localhost ansible_connection=local (работает локально, SSH не нужен — безопасно для практики).
  2. Напишите плейбук, гарантирующий существование файла /tmp/ansible-test.txt с конкретным содержимым, через модуль copy с content:.
  3. Запустите его дважды подряд и убедитесь, что второй запуск отчитывается changed=0 — доказывая идемпотентность.
  4. Измените содержимое файла вручную (echo "tampered" > /tmp/ansible-test.txt), перезапустите плейбук и убедитесь, что он обнаруживает и исправляет расхождение (changed=1).

Как проверить, что всё сделано правильно

  • Второй запуск шага 3: итоговая строка PLAY RECAP вывода ansible-playbook должна показать changed=0, failed=0.
  • Шаг 4: recap должен показать changed=1, а cat /tmp/ansible-test.txt — снова ваше исходное содержимое, не «tampered».

Типичные ошибки

  • Написание плейбуков как последовательности задач shell/command вместо использования специализированных модулей (package, template, systemd, copy, ...). Сырые shell-команды по умолчанию не идемпотентны — задача shell: useradd deploy падает при втором запуске, потому что пользователь уже существует, тогда как модуль ansible.builtin.user корректно обрабатывает «убедиться, что этот пользователь существует» независимо от текущего состояния.
  • Никогда не запускать --check --diff перед применением незнакомого плейбука к продакшн- хостам.
  • Жёстко прописанные значения, которые должны быть переменными (хосты, порты, пути), делая один и тот же плейбук непригодным для второго, чуть отличающегося окружения без правки самого плейбука.

⚠ Опасные команды

  • Любой плейбук с ansible.builtin.file: state=absent или command: rm -rf <path> против пути, выведенного из переменной, запущенный на большом инвентаре без --check сначала — одна неверная переменная разрешается в одно и то же разрушительное действие одновременно на каждом целевом хосте, что и есть одновременно сила и опасность управления конфигурацией в масштабе.
  • ansible all -i inventory.ini -m <module> (группа all) запускается против каждого хоста в инвентаре — опечатанный или слишком широкий файл инвентаря превращает тестовую команду в действие на весь парк.

Связанные статьи

Контрольные вопросы

  1. Почему задача shell: useradd deploy не идемпотентна, а модуль ansible.builtin.user достигает той же цели безопасно?
  2. Что на самом деле делает --check --diff, и почему это должно стать привычкой перед запуском любого незнакомого плейбука?
  3. Почему хендлеры запускаются только один раз за play, даже если уведомлены несколькими задачами?
  4. В чём практический риск запуска плейбука против группы инвентаря all без предварительного сужения до меньшей тестовой группы?