Ansible часто выбирают, когда нужно быстро и предсказуемо привести сервер «в нужный вид»: поставить пакеты, разложить конфиги, открыть порты, перезапустить сервисы — и при этом не бояться повторного запуска. В этой инструкции разберём базовые кирпичики Ansible (inventory, playbook, roles) и то, как на практике добиваться идемпотентности при provisioning одного сервера.
Что вы получите от Ansible на одном сервере
Даже если у вас один хост, Ansible полезен по трём причинам:
Повторяемость: вы описываете состояние, а не последовательность ручных действий.
Идемпотентность: второй (и десятый) запуск не ломает конфиги и не делает лишних действий.
Документирование: репозиторий с ролями и переменными часто заменяет разрозненную вики.
Ключевой сдвиг мышления: Ansible не «настраивает сервер», а «приводит сервер к желаемому состоянию». Чем точнее описано состояние, тем ближе вы к идемпотентному provisioning.
Inventory: минимальная база для single server
Inventory — это список управляемых хостов и связанных с ними переменных. Для одного сервера удобно держать inventory в виде INI или YAML.
Пример inventory в INI
[web]
myserver ansible_host=203.0.113.10 ansible_user=admin
[web:vars]
ansible_python_interpreter=/usr/bin/python3
Группа web здесь одна, но она уже даёт масштабирование «без переделки плейбука»: появится второй сервер — просто добавите его в группу.
Пример inventory в YAML
all:
children:
web:
hosts:
myserver:
ansible_host: 203.0.113.10
ansible_user: admin
vars:
ansible_python_interpreter: /usr/bin/python3
SSH-параметры и «безболезненный старт»
На практике проблемы начинаются не с ролей, а с подключения. Полезные переменные inventory:
ansible_user— каким пользователем подключаться.ansible_port— если SSH не на 22.ansible_ssh_private_key_file— путь к ключу (при большом количестве ключей удобнее задавать черезansible.cfgили окружение).ansible_become,ansible_become_method— если нужны привилегии root.
Для первого прогона полезно быстро проверить доступность хоста:
ansible -i inventory.ini web -m ping
Если сервер у вас пока «голый», часто проще стартовать с чистой машины на VDS: одинаковое окружение, предсказуемые пакеты, меньше сюрпризов при подключении и правах.

Playbook: скелет provisioning
Playbook описывает: на каких хостах выполнять задачи и какие роли/таски применять. Для single server обычно достаточно одного плейбука, который включает роли.
---
- name: Provision single server
hosts: web
become: true
gather_facts: true
roles:
- common
- nginx
- app
Почему удобно включать роли, а не писать всё задачами прямо в playbook:
Роли проще тестировать и переиспользовать.
Структура становится очевидной: базовая настройка, веб-сервер, приложение.
Переменные и handlers изолированы по смыслу.
Roles: структура, которая удерживает проект от хаоса
Role — это соглашение о том, где лежат tasks, templates, files, defaults, handlers и т.д. Минимальная структура роли выглядит так:
roles/
nginx/
defaults/
main.yml
tasks/
main.yml
handlers/
main.yml
templates/
nginx.conf.j2
defaults vs vars: где держать переменные
Частая ошибка новичков — складывать все переменные в одно место. Практичное правило:
defaults/main.yml— безопасные значения «по умолчанию», которые можно переопределять.vars/main.yml— значения, которые почти никогда не должны переопределяться (используйте редко).
Пример roles/nginx/defaults/main.yml:
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_sites:
- server_name: example.local
root: /var/www/example
tasks и handlers: перезапускайте сервисы только когда надо
Идемпотентность в Ansible держится на двух вещах:
модуль сам понимает, нужно ли менять состояние (например, пакет уже установлен — действий нет);
handlers срабатывают только по уведомлению, когда что-то реально изменилось.
Пример roles/nginx/tasks/main.yml:
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Deploy nginx.conf
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Reload nginx
- name: Ensure nginx is enabled and running
ansible.builtin.service:
name: nginx
state: started
enabled: true
И roles/nginx/handlers/main.yml:
---
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Если шаблон не изменился — handler не вызовется. Это и есть практическая идемпотентность: повторный запуск playbook не делает лишних перезапусков.
Идемпотентность: что это в терминах Ansible
Идемпотентность означает: вы можете прогонять provisioning сколько угодно раз, а результат будет одинаковым, без накопления побочных эффектов. В Ansible это обычно означает:
таска меняет систему только при необходимости и возвращает
changedтолько при реальном изменении;таска корректно обрабатывает «уже сделано»;
перезапуски/перечитки конфигов происходят только после изменений;
нет «всегда выполняющихся» shell-команд, которые что-то перетирают или добавляют повторно.
Простой тест: второй запуск playbook на уже настроенном сервере должен завершаться быстро и почти без изменений.
Типовые ловушки, которые ломают идемпотентность
1) Использование shell/command вместо модулей
Команда вроде «создать пользователя» через shell почти всегда неидемпотентна: либо упадёт, либо будет выполнять лишнее. Лучше модуль:
- name: Create deploy user
ansible.builtin.user:
name: deploy
shell: /bin/bash
create_home: true
state: present
То же самое относится к пакетам (используйте ansible.builtin.apt/ansible.builtin.dnf), файлам (ansible.builtin.copy/ansible.builtin.template), systemd (ansible.builtin.service, ansible.builtin.systemd), git (ansible.builtin.git).
2) Не зафиксированы права/владельцы
Если вы не задаёте owner/group/mode, то ручное изменение или другая задача может привести к «вечному changed». Для конфигов и важных файлов лучше всегда фиксировать права явно.
3) Нестабильные шаблоны
Если шаблон генерирует меняющееся содержимое (например, вставляет время генерации), каждый запуск будет менять файл и триггерить handler. Избегайте динамики без необходимости.
4) «Всегда обновлять кеш пакетов» без причины
На Debian/Ubuntu update_cache: true у apt удобно, но может замедлять повторные прогоны. На single server часто достаточно обновлять кеш реже (например, отдельной задачей в роли common, либо поддерживать обновления через systemd timers), а в «частых» ролях не трогать кеш каждый раз.
Переменные и окружения: dev/stage/prod даже на одном сервере
Даже в single server сценарии полезно отделять переменные, потому что «один сервер» сегодня легко превращается в «один прод и один стейдж» завтра. Минимальный подход:
group_vars/web.yml— общие для группы web;host_vars/myserver.yml— специфичные для конкретного хоста;секреты — отдельно (как минимум через Ansible Vault).
Пример group_vars/web.yml:
app_user: deploy
app_root: /var/www/myapp
app_env: production
Если завтра вы добавите ещё один хост в группу, переменные применятся автоматически — без переписывания playbook.
Практика: роль для приложения (деплой без «магии»)
Для примера возьмём роль app, которая создаёт директории, кладёт конфиг и запускает systemd-сервис. Идея: меньше «скриптов, которые всё делают», больше управляемых ресурсов.
---
- name: Create app directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0755"
loop:
- "{{ app_root }}"
- "{{ app_root }}/shared"
- "{{ app_root }}/releases"
- name: Deploy app env file
ansible.builtin.template:
src: app.env.j2
dest: "{{ app_root }}/shared/app.env"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
notify: Restart app
- name: Install systemd unit
ansible.builtin.template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
owner: root
group: root
mode: "0644"
notify:
- Systemd daemon-reload
- Restart app
- name: Enable and start app
ansible.builtin.service:
name: myapp
state: started
enabled: true
Handlers для этой роли:
---
- name: Systemd daemon-reload
ansible.builtin.systemd:
daemon_reload: true
- name: Restart app
ansible.builtin.service:
name: myapp
state: restarted
Почему это идемпотентно:
директории создаются только если их нет или не те права;
шаблоны меняют файлы только при изменении содержимого;
daemon_reloadвызывается только при изменении unit-файла;перезапуск приложения — только при реальной необходимости.
Если это прод-сервер с доменом, логично сразу планировать схему «домен → веб → TLS»: пригодится отдельная статья про переносы и HTTPS-настройки, чтобы не наступить на HSTS и редиректы: перенос домена: 301, HSTS и SSL на практике.
Как проверять идемпотентность и качество provisioning
1) Прогон «два раза подряд»
Самая простая проверка: запустите playbook, затем сразу повторите. Во втором прогоне ожидайте минимум changed и отсутствие лишних перезапусков.
2) Check mode и diff
Для безопасной оценки изменений используйте режим проверки — особенно в проде, когда вы хотите сначала понять, что именно изменится.
ansible-playbook -i inventory.ini site.yml --check --diff
Учтите: не все модули идеально поддерживают --check, поэтому результаты трактуйте как «приблизительную картину».
3) Ограничение хостов и теги
Даже на одном сервере полезно помечать задачи тегами, чтобы применять только часть provisioning (например, только nginx или только app).
ansible-playbook -i inventory.ini site.yml --tags nginx
ansible-playbook -i inventory.ini site.yml --tags app

Полезные приёмы, чтобы roles были удобными
Делайте роли параметризуемыми
Если роль жёстко «зашита» под один домен и один путь — она быстро превратится в одноразовый набор тасок. Лучше выносить существенные параметры в defaults и переопределять их в group_vars/host_vars.
Делите роль на логические файлы tasks
Когда задач становится много, проще поддерживать структуру:
roles/nginx/tasks/
main.yml
install.yml
config.yml
sites.yml
А в main.yml подключать:
---
- ansible.builtin.import_tasks: install.yml
- ansible.builtin.import_tasks: config.yml
- ansible.builtin.import_tasks: sites.yml
Не бойтесь assert и явных проверок
Если без переменной роль не должна работать — лучше упасть сразу понятной ошибкой, чем получить «тихий» кривой provisioning.
- name: Validate required vars
ansible.builtin.assert:
that:
- app_root is defined
- app_user is defined
fail_msg: "Define app_root and app_user"
Короткий чек-лист: Ansible для single server без боли
Начните с простого inventory и одной группы (например,
web).Соберите provisioning из ролей:
common,nginx,app.Максимум используйте встроенные модули, минимум
shell/command.Все конфиги — через
template, перезапуски — через handlers.Фиксируйте права файлов и директории явно.
Проверяйте идемпотентность вторым прогоном и через
--check --diff.
Если подходить к Ansible как к инструменту управления состоянием, то даже single server превращается в предсказуемую платформу: вы можете быстро пересоздать машину, восстановиться после ошибок и безопасно разворачивать изменения, сохраняя контроль над тем, что именно меняется.


