Ansible для управления конфигурациями: практическое руководство по настройке серверов | AdminWiki

Ansible для управления конфигурациями: практическое руководство по настройке серверов

19 сентября 2026 12 мин. чтения
Содержание статьи

Что вы получите: рабочий шаблон Ansible за 20 минут

Ansible приводит серверы к заданному состоянию без агентов на управляемых хостах: подключение идёт по SSH, а описание состояния лежит в YAML-файлах и версионируется в Git. Один плейбук за 15-20 минут закрывает типовой набор: установка пакета, создание пользователя, доставка конфигурационного файла, открытие порта в firewall и запуск сервиса.

Карта материала: инвентарь и переменные, первый плейбук и идемпотентность, роли и порядок в репозитории, секреты через Ansible Vault, безопасная проверка на staging и разбор ошибок новичков. В финале собран сквозной пример роли Nginx с пользователем deploy и правилом ufw.

Примеры ориентированы на ansible-core 2.15 и новее, Ubuntu 22.04 и Debian 12. Синтаксис YAML и основы SSH здесь не разбираются: предполагается, что вы работаете с Linux-серверами и умеете подключаться к ним по ключу. Если нужно понять, где заканчивается зона Ansible и начинается зона Terraform, посмотрите разбор Ansible против Terraform: в этой статье речь только о конфигурационном менеджменте.

Итоговая структура репозитория, к которой мы придём:

ansible-repo/
├── ansible.cfg
├── inventory/
│   ├── staging.ini
│   └── production.ini
├── group_vars/
│   ├── all/
│   │   └── vault.yml
│   └── web.yml
├── host_vars/
│   └── web01.yml
├── playbooks/
│   └── site.yml
├── roles/
│   └── nginx/
├── requirements.yml
└── .gitignore

Кому подойдёт это руководство

DevOps-инженерам и системным администраторам, у которых Ansible ещё не выстроен в процесс: команды запускаются вручную, репозиторий ролей не структурирован, секреты лежат открытым текстом, а staging отсутствует как этап. Материал рассчитан и на тех, кто только переносит ручные операции в плейбуки.

Примеры закрывают реальные задачи: настройка Nginx, пользователи и SSH-доступ, правила ufw, доставка конфигов через template, перезапуск сервиса только при изменении файла. Теоретические разделы про принципы IaC не разбираются, зато поясняются места, где новички чаще всего ломают логику: приоритет переменных, режим прогноза, поведение shell и command.

Что понадобится для повторения примеров

  • Control node (рабочая станция или отдельная VM) с Python 3.10 и ansible-core 2.15 и выше.
  • SSH-доступ по ключу к одному-двум тестовым серверам. Подойдут Vagrant, Multipass или контейнеры с systemd.
  • Git для версионирования репозитория.
  • Python 3.8 и выше на управляемых хостах. В Ubuntu 22.04 и Debian 12 он уже установлен, а путь задаётся переменной ansible_python_interpreter.

Для staging достаточно одной виртуальной машины. Не запускайте первые прогоны на рабочих серверах: команды с become меняют системные файлы, а ошибка в шаблоне конфига роняет сервис.

Инвентарь Ansible: пример структуры для staging и production

Инвентарь - это список управляемых хостов, разбитых на группы, плюс переменные подключения. Окружения разделяются файлами: inventory/staging.ini и inventory/production.ini. Одна и та же роль применяется к разным стендам без правок кода.

[web]
staging-web01 ansible_host=10.10.1.11

[db]
staging-db01 ansible_host=10.10.1.21

[all:children]
web
db

[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_ed25519
ansible_python_interpreter=/usr/bin/python3

Файл ansible.cfg в корне репозитория убирает длинные ключи из команд:

[defaults]
inventory = ./inventory/staging.ini
host_key_checking = False
retry_files_enabled = False
vault_password_file = ~/.ansible/vault_pass

Строка host_key_checking = False отключает проверку отпечатка SSH-ключа. Оставляйте её только в тестовом окружении: она открывает путь для подмены хоста в сети. Если файл с паролем Vault отсутствует, Ansible сообщит об ошибке при первом обращении к зашифрованным данным, поэтому такой файл создаётся до прогона.

Проверка доступности всех хостов и работоспособности Python:

ansible all -m ping

Ответ "ping": "pong" на каждом хосте означает, что инвентарь и ключи настроены. Любой unreachable разбирается до того, как написан первый плейбук.

Статический и динамический инвентарь: когда что использовать

Статический INI или YAML подходит для парка из десятков серверов с редкими изменениями. Динамический инвентарь берёт список хостов из API облака: например, плагин amazon.aws.aws_ec2 собирает инстансы по тегам и регулярно обновляет список. Переход на него не требует переписывать плейбуки, если группы названы одинаково, потому что плейбук ссылается на группу, а не на файл. Для старта хватает статического файла, динамический добавляется, когда хосты начинают создаваться и удаляться автоматически.

Групповые и хостовые переменные: group_vars и host_vars

Правило приоритета: host_vars перекрывает group_vars, а group_vars перекрывает defaults роли. На практике group_vars/web.yml задаёт nginx_port: 80 для всей группы, а host_vars/web01.yml переопределяет его на 8080 только для одного хоста.

group_vars/web.yml
nginx_port: 80
ansible_user: deploy

host_vars/web01.yml
nginx_port: 8080

Facts (ansible_os_family, ansible_distribution, ansible_default_ipv4) собирает модуль setup, и они доступны только для чтения. Переопределить их в vars нельзя: запись ansible_os_family: RedHat будет проигнорирована без предупреждения. Для своих признаков берите отдельное имя, например my_os_family. Схема dev/prod с раздельными переменными и переопределениями для сети разобрана в материале про плейбуки Ansible для сетевой маршрутизации.

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

Плейбук описывает, каким должен быть сервер, а не последовательность команд. Файл playbooks/site.yml для группы web:

- name: Base configuration for web servers
  hosts: web
  become: true
  tasks:
    - name: Ensure nginx is installed
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Ensure deploy user exists
      ansible.builtin.user:
        name: deploy
        groups: sudo
        shell: /bin/bash
        state: present

    - name: Deploy nginx configuration
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: "0644"
      notify: restart nginx

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

  handlers:
    - name: restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

Запуск: ansible-playbook -i inventory/staging.ini playbooks/site.yml

В конце выводится PLAY RECAP: ok - успешные задачи, changed - задачи, изменившие систему, failed - ошибки, skipped - задачи, пропущенные по условию when, unreachable - недоступные хосты. Строка changed=4 при первом прогоне нормальна. Те же changed=4 при втором прогоне означают, что часть задач неидемпотентна.

Идемпотентность в Ansible: как проверить и почему это важно

Идемпотентность означает, что повторный запуск не меняет систему, если она уже в нужном состоянии. Модули apt, user, template, service идут с этим свойством по умолчанию. Модули command, shell и raw выполняются всегда и всегда возвращают changed, поэтому их либо заменяют профильным модулем, либо закрывают условием when.

Два режима проверки без изменения хостов:

ansible-playbook -i inventory/staging.ini playbooks/site.yml --check --diff

check_mode прогоняет плейбук в режиме прогноза, diff показывает построчную разницу для файлов, которые изменились бы. Ограничение: для command и shell прогноз тоже требует подсказок, иначе задача помечается как skipped.

Для команд, которые только читают состояние, ставится changed_when: false:

    - name: Validate nginx configuration syntax
      ansible.builtin.command: nginx -t
      changed_when: false

    - name: Check active state of nginx
      ansible.builtin.command: systemctl is-active nginx
      register: nginx_state
      changed_when: false
      failed_when: nginx_state.stdout != "active"

Такой блок оставляет PLAY RECAP чистым: прогон без изменений показывает changed=0, и любой неожиданный changed сразу указывает на проблемную задачу.

Handlers: перезапуск сервиса только при изменении конфига

Handler запускается один раз в конце play, даже если notify вызван десятью задачами. Если шаблон nginx.conf.j2 не менялся, notify не срабатывает и сервис не перезапускается, и на этом строится безопасная доставка конфигов. Принудительный запуск до конца play делается метазадачей ansible.builtin.meta: flush_handlers. Падение handler помечает хост как failed, и следующие play для него не выполняются.

Ansible роли: структура и организация репозитория

Роль упаковывает задачи, шаблоны, переменные и handlers в один переиспользуемый каталог. Скелет создаётся командой ansible-galaxy init nginx, дальше каталоги наполняются:

roles/nginx/
├── tasks/main.yml
├── handlers/main.yml
├── defaults/main.yml
├── vars/main.yml
├── templates/nginx.conf.j2
├── files/
└── meta/main.yml

Подключение в плейбуке сводится к списку:

- name: Apply web roles
  hosts: web
  become: true
  roles:
    - nginx

Внешние роли и коллекции фиксируются в requirements.yml и ставятся командой ansible-galaxy install -r requirements.yml. Файл в репозитории нужен, чтобы версии зависимостей совпадали у всех участников команды. Готовые шаблоны задач для повседневных операций собраны в подборке автоматизация инфраструктуры для DevOps и сисадминов.

Как избежать дублирования: defaults, vars и group_vars

defaults/main.yml хранит значения по умолчанию и легко перекрывается из inventory. vars/main.yml хранит константы роли и имеет приоритет выше group_vars и host_vars, поэтому не ставьте туда значения, которые планируете менять по окружению. Порядок перекрытия от высшего к низшему: host_vars, group_vars/web, group_vars/all, defaults роли.

roles/nginx/defaults/main.yml
nginx_port: 80
nginx_worker_processes: auto

group_vars/web.yml
nginx_port: 8080

Типичная ловушка: одна и та же переменная объявлена и в vars роли, и в group_vars. Приоритет у vars, переопределение из inventory молча не применяется, а конфиг на сервере остаётся со значением по умолчанию.

Теги и выборочный запуск задач

Теги позволяют прогнать только часть автоматизации. Пример задачи в роли nginx:

    - name: Deploy nginx configuration
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      tags:
        - nginx
        - config

Запуск только конфигурации: ansible-playbook playbooks/site.yml --tags config. Список доступных тегов: --list-tags. Исключение блока: --skip-tags packages. Без тегов на большом плейбуке нельзя безопасно применить одну правку: придётся гнать всё, включая задачи с перезапуском сервисов.

Ansible Vault: работа с секретами без утечек в Git

Пароли баз данных, токены API и приватные ключи хранятся в зашифрованных файлах. Создание файла секретов группы:

ansible-vault create group_vars/all/vault.yml

Шифрование одного значения без отдельного файла:

ansible-vault encrypt_string 'SuperSecret' --name 'db_password'

Пароль Vault указывается в ansible.cfg строкой vault_password_file = ~/.ansible/vault_pass, после чего ключ --vault-password-file в командах не нужен. Файл с паролем добавляется в .gitignore и не коммитится. Задача, работающая с секретом, помечается no_log: true, иначе значение попадёт в вывод терминала и в лог CI.

    - name: Create application database user
      community.postgresql.postgresql_user:
        name: app
        password: "{{ db_password }}"
      no_log: true

Vault в CI/CD: как передать пароль безопасно

Пароль передаётся через переменную окружения или файл из секретов пайплайна. Пример шага GitLab CI:

deploy:
  stage: deploy
  script:
    - echo "$VAULT_PASS" > vault_pass
    - ansible-playbook -i inventory/staging.ini playbooks/site.yml --vault-password-file vault_pass
    - rm -f vault_pass

Не передавайте пароль в неинтерактивных сценариях и не указывайте его в командной строке: значение оседает в истории shell, списке процессов и логах раннера. Полный пайплайн с Terraform, Ansible и GitLab CI разобран в статье инфраструктура как код и CI/CD.

Безопасная проверка результата: staging, --check и линтеры

Порядок применения: линт, прогноз на staging, полный прогон на staging, затем production с ограничением по хостам и тегам. Каждый шаг ловит свой класс ошибок.

ansible-lint playbooks/
ansible-playbook -i inventory/staging.ini playbooks/site.yml --check --diff
ansible-playbook -i inventory/staging.ini playbooks/site.yml
ansible-playbook -i inventory/production.ini playbooks/site.yml --limit web01 --tags nginx

ansible-lint отмечает устаревшие конструкции, команды вместо модулей и отсутствие режима у файлов. molecule прогоняет роль в контейнерах и проверяет идемпотентность: сценарий converge запускает роль дважды и падает, если второй прогон что-то меняет. Ограничение режима --check: задачи с command и shell без changed_when в прогнозе пропускаются, поэтому их поведение проверяется только реальным прогоном на staging.

Чек-лист перед запуском на production

  1. Резервная копия конфигов, которые меняет плейбук.
  2. Полный прогон на staging с нуля на чистом хосте.
  3. --check на production без failed и без неожиданных changed.
  4. Теги проставлены, прогон идёт с --limit и --tags, а не целиком.
  5. Vault подключён, секреты не выводятся в лог.
  6. ansible-lint без ошибок уровня critical.

Для критичных сервисов сначала выведите один хост из балансировщика, примените плейбук, проверьте работу и верните хост в пул. После успеха расширяйте --limit.

Типовые ошибки новичков в Ansible и как их избежать

Facts вместо vars. Запись vars: ansible_os_family: RedHat не сработает: факт собирает модуль setup, и переопределению он не подлежит. Свои признаки храните под своими именами (my_os_family) и сравнивайте с ними в when.

Отсутствие тегов. Без тегов нельзя запустить одну правку, поэтому растёт соблазн прогнать весь плейбук целиком на production. Минимум один тег на логический блок задач: packages, config, firewall.

Запуск сразу на production. Плейбук с become правит /etc и перезапускает сервисы. Порядок staging, прогноз, ограниченный прогон на одном хосте обязателен.

shell и command вместо модулей. Такие задачи всегда показывают changed и не умеют проверять состояние. Сначала проверьте, есть ли модуль под задачу (apt, copy, template, lineinfile, ufw), и только потом берите command с changed_when.

Секреты открытым текстом. Пароль в group_vars/all.yml уезжает в Git вместе с историей коммитов и остаётся там даже после удаления строки. Файлы с секретами шифруются Vault, а пароль Vault живёт вне репозитория.

become: true на всём плейбуке. Права root нужны только части задач. Ставьте become на уровне конкретной задачи и добавляйте become_user, когда действие выполняется от имени сервисного пользователя.

gather_facts: true без необходимости. Сбор фактов занимает секунды на каждом хосте. Если плейбук факты не использует, отключите их: gather_facts: false.

Сквозной пример: роль Nginx с пользователем и firewall

Файл roles/nginx/tasks/main.yml собирает все элементы вместе:

- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
  tags: packages

- name: Ensure deploy user exists
  ansible.builtin.user:
    name: deploy
    groups: sudo
    shell: /bin/bash
    state: present

- name: Deploy nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
  notify: restart nginx
  tags: config

- name: Allow HTTP port
  community.general.ufw:
    rule: allow
    port: "{{ nginx_port }}"
    proto: tcp
  tags: firewall

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

Плейбук подключения:

- name: Configure web servers
  hosts: web
  become: true
  roles:
    - role: nginx
      tags: nginx

Запуск: ansible-playbook -i inventory/staging.ini playbooks/site.yml. Роль сразу готова к production: порт, имя пользователя и содержимое шаблона переопределяются через group_vars соответствующего окружения. Готовый стек с Nginx, балансировкой и автомасштабированием описан в материале про IaC-стек для отказоустойчивого веб-приложения.

Проверка результата: curl, systemctl и повторный прогон

Проверка на staging:

curl -I http://staging-web01
systemctl status nginx
ansible-playbook -i inventory/staging.ini playbooks/site.yml

Ожидаемый результат: код 200 или 301 в первой строке ответа, active (running) в статусе сервиса и changed=0 в PLAY RECAP второго прогона. Если changed больше нуля, найдите задачи с changed и разберитесь с их идемпотентностью: чаще всего виноваты shell или command без changed_when. Handler при повторном прогоне не срабатывает: конфиг не менялся, значит и перезапуск не нужен.

Актуальность версий и совместимость примеров

Версия проверяется командой ansible --version. Примеры из статьи рассчитаны на ansible-core 2.15 и новее; модули ansible.builtin дают единый неймспейс и не конфликтуют с внешними коллекциями. На управляемых хостах нужен Python 3.8 и выше: Ubuntu 22.04 и Debian 12 подходят без дополнительных настроек, для минимальных образов путь к интерпретатору задаётся через ansible_python_interpreter.

Коллекции ставятся отдельно от ядра: ansible-galaxy collection install community.general community.postgresql. Модуль community.general.ufw живёт в коллекции community.general, поэтому в requirements.yml нужна секция collections с указанием версий. В ansible 2.9 и ниже структура другая: модули вызываются по коротким именам, часть параметров отсутствует, а сам механизм коллекций не используется.

Сверяйте совместимость модуля с вашей версией ansible-core и версией целевой ОС по официальной документации модуля перед прогоном на рабочих серверах. Поведение параметров и значения по умолчанию между мажорными версиями меняются, а проверка версии на хосте стоит одного лишнего запуска на staging.

Поделиться:
Сохранить гайд? В закладки браузера