Ansible: автоматизация настройки нескольких игровых серверов
Когда у тебя один сервер Rust или Minecraft — зашёл по SSH, обновил, перезапустил, забыл. Когда серверов пять, десять или тридцать (сеть сообществ, разные модпаки, тестовый и продовый стенды) — та же рутина превращается в вечер, убитый на копипасту команд в двадцать разных терминалов, и рано или поздно на одном из серверов ты забудешь применить патч или обновишь не ту версию мода. Ansible решает именно эту задачу: один playbook — и одна и та же операция прилетает на весь парк серверов одинаково, предсказуемо и без ручной беготни.
Содержание
- Зачем Ansible игровому серверу и когда он того стоит
- Как устроен Ansible: инвентарь, playbook, роли
- Подготовка: SSH-доступ, sudo и ansible.cfg
- Playbook для установки и обновления игрового сервера
- Idempotent-подход: почему playbook можно гонять хоть каждый час
- Переменные и конфиги: одна роль — разные сервера
- Практические сценарии для сети серверов
Зачем Ansible игровому серверу и когда он того стоит
Ansible — это оркестрация без агентов: на управляемых серверах не нужно ничего ставить, кроме Python, всё происходит по SSH с управляющей машины (control node). Ты описываешь желаемое состояние — «на сервере должна стоять такая-то версия SteamCMD-приложения, такие-то конфиги, сервис должен быть запущен» — а Ansible сам разбирается, что нужно сделать, чтобы к этому состоянию прийти.
Если у тебя один-два сервера, Ansible, скорее всего, избыточен — проще зайти руками. Порог окупаемости обычно наступает на 3-5 серверах или раньше, если есть повторяющиеся операции: регулярные обновления модов, синхронные рестарты по расписанию, раскатка одинаковых конфигов на кластер из нескольких инстансов одной игры. Также он оправдан для гибридной сети — часть серверов на своём железе, часть арендована, и нужен единый способ ими управлять независимо от хостера.
Не путай Ansible с Docker/Kubernetes — это разные уровни задачи. Ansible отлично ложится на «голые» VPS с systemd-юнитами для запуска игровых процессов — он настраивает то, что уже развёрнуто, а не поднимает инфраструктуру с нуля.
Как устроен Ansible: инвентарь, playbook, роли
Три базовых понятия, без которых дальше не разобраться:
- Inventory (инвентарь) — список серверов, с которыми ты работаешь, сгруппированных по смыслу (
rust_servers,minecraft_servers,staging). - Playbook — YAML-файл с последовательностью задач (tasks), которые нужно выполнить на выбранной группе серверов.
- Role (роль) — переиспользуемый набор задач, шаблонов и переменных, оформленный как модуль. Роль
steamcmd_gameможно применить и к Rust, и к CS2, и к ARK — меняются только переменные.
Минимальная структура проекта на диске:
gameservers-ansible/
├── ansible.cfg
├── inventory/
│ └── hosts.yml
├── group_vars/
│ ├── all.yml
│ └── rust_servers.yml
├── roles/
│ └── steamcmd_game/
│ ├── tasks/main.yml
│ ├── templates/
│ ├── handlers/main.yml
│ └── defaults/main.yml
└── site.yml
Пример инвентаря в YAML-формате:
# inventory/hosts.yml
all:
children:
rust_servers:
hosts:
rust-eu-1: { ansible_host: 203.0.113.10 }
rust-eu-2: { ansible_host: 203.0.113.11 }
minecraft_servers:
hosts:
mc-survival: { ansible_host: 203.0.113.30 }
vars:
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверПодготовка: SSH-доступ, sudo и ansible.cfg
Ansible ходит по SSH тем же пользователем, каким ты обычно администрируешь сервер, поэтому первым делом — ключи, а не пароли:
ssh-keygen -t ed25519 -f ~/.ssh/gameservers_ed25519 -C "ansible-deploy"
ssh-copy-id -i ~/.ssh/gameservers_ed25519.pub deploy@203.0.113.10
Повтори для каждого хоста (или пропиши ключ через cloud-init хостера сразу при заказе сервера). Дальше — ansible.cfg в корне проекта, чтобы не таскать флаги в каждой команде:
[defaults]
inventory = inventory/hosts.yml
remote_user = deploy
private_key_file = ~/.ssh/gameservers_ed25519
host_key_checking = False
retry_files_enabled = False
[privilege_escalation]
become = True
become_method = sudo
Проверка связи со всей сетью одной командой:
ansible all -m ping
Для базовой закалки нового хоста (пользователь, зависимости, ufw) удобно завести отдельный playbook bootstrap.yml, который прогоняется один раз — а дальше уже игровые роли. Про сами правила файрвола см. настройку портов и файрвола для игрового сервера — модуль ufw укладывает те же правила в playbook, чтобы не открывать порты руками на каждом новом узле.
Playbook для установки и обновления игрового сервера
Рабочий пример для SteamCMD-игр (Rust, CS2, ARK, 7 Days to Die — принцип одинаковый, разбор самого SteamCMD см. в статье про установку и обновление серверов через SteamCMD). Роль steamcmd_game с переменными по умолчанию:
# roles/steamcmd_game/defaults/main.yml
steam_app_id: "258550" # Rust Dedicated Server
install_dir: "/home/deploy/gameserver"
service_name: "rust-server"
Задачи роли:
# roles/steamcmd_game/tasks/main.yml
- name: Установить зависимости для SteamCMD
ansible.builtin.apt:
name: [lib32gcc-s1, curl, unzip]
state: present
update_cache: true
become: true
- name: Создать директорию для сервера
ansible.builtin.file:
path: "{{ install_dir }}"
state: directory
owner: deploy
mode: "0755"
- name: Установить/обновить сервер через SteamCMD
ansible.builtin.shell: >
/home/deploy/steamcmd/steamcmd.sh
+force_install_dir {{ install_dir }}
+login anonymous
+app_update {{ steam_app_id }} validate
+quit
register: steamcmd_result
changed_when: "'Success! App' in steamcmd_result.stdout"
notify: restart gameserver
- name: Развернуть unit-файл systemd
ansible.builtin.template:
src: gameserver.service.j2
dest: "/etc/systemd/system/{{ service_name }}.service"
mode: "0644"
become: true
notify: reload systemd and restart
- name: Убедиться, что сервис включён и запущен
ansible.builtin.systemd:
name: "{{ service_name }}"
enabled: true
state: started
become: true
Шаблон systemd-юнита (templates/gameserver.service.j2) параметризуется переменными хоста, так что для каждой группы серверов можно задать свои параметры запуска: ExecStart={{ install_dir }}/{{ start_script }}, User=deploy, Restart=on-failure. Запуск на всю группу Rust-серверов:
ansible-playbook site.yml --limit rust_servers
Idempotent-подход: почему playbook можно гонять хоть каждый час
Ключевое свойство хорошего playbook — идемпотентность: повторный прогон без изменений в окружении не должен ничего ломать и не должен рестартовать сервис, если менять нечего. Это критично для игровых серверов, потому что лишний рестарт — это дисконнект всех игроков на сервере прямо посреди боя или стройки.
Три вещи, которые обеспечивают идемпотентность в примере выше:
changed_when— модульshell/commandсам по себе всегда считается «изменённым», поэтому мы явно говорим Ansible смотреть на вывод SteamCMD и считать шаг изменением только если реально было обновление. Без этого хендлерrestart gameserverсрабатывал бы при каждом запуске playbook, даже если версия игры не менялась.- Handlers вместо прямых рестартов — обработчик из
handlers/main.ymlвыполняется один раз в конце playbook, даже если его вызвали несколько задач, и только если было реальное изменение (тот же паттернname+ansible.builtin.systemd: state: restarted, что и в задаче выше). - Декларативные модули вместо shell, где возможно —
ansible.builtin.file,ansible.builtin.template,ansible.builtin.systemd,ansible.builtin.aptсами проверяют текущее состояние и меняют только то, что расходится с описанным.shell/commandиспользуй только там, где нет штатного модуля (как со SteamCMD, под который нет модуля в ядре Ansible).
Проверить идемпотентность просто: прогони playbook дважды подряд и посмотри на summary — второй прогон должен показать changed=0:
PLAY RECAP *********************************************************
rust-eu-1 : ok=6 changed=0 unreachable=0 failed=0
rust-eu-2 : ok=6 changed=0 unreachable=0 failed=0
Переменные и конфиги: одна роль — разные сервера
Сила Ansible для сети серверов — не в том, что все серверы становятся одинаковыми, а в том, что различия между ними описаны явно и лежат в git, а не в голове админа. Для этого используются group_vars и host_vars.
# group_vars/rust_servers.yml
steam_app_id: "258550"
start_script: "RustDedicated"
server_max_players: 150
server_tickrate: 30
# host_vars/rust-eu-1.yml
server_name: "MAATRIX | EU Vanilla | x3"
server_seed: 84271
Общее для группы (версия, тикрейт, набор плагинов) уходит в group_vars, уникальное для конкретного сервера (имя, seed, порт) — в host_vars. Дальше конфиг сервера собирается из шаблона Jinja2, а не редактируется руками на каждом хосте:
# templates/server.cfg.j2
server.hostname "{{ server_name }}"
server.maxplayers {{ server_max_players }}
server.tickrate {{ server_tickrate }}
server.seed {{ server_seed }}
Что обычно выносят в переменные при управлении сетью из 5+ серверов:
| Переменная | Где задаётся |
|---|---|
| Версия игры/сборки | group_vars или роль |
| RAM для JVM/сервиса | group_vars по типу игры |
| Имя сервера, seed, слоты | host_vars (уникально на хост) |
| Список модов/плагинов | group_vars или файл mods.yml |
| Порты | host_vars, если разные на одной машине |
| Секреты (RCON, токены) | только Ansible Vault |
Для секретов — RCON-пароль, токены дискорд-бота, ключи API хостинга (см. автоматизацию через API хостинга — она хорошо комбинируется с Ansible: API поднимает сервер, Ansible донастраивает софт внутри) — не клади их в открытый YAML. Используй Ansible Vault:
ansible-vault create group_vars/rust_servers/vault.yml
# внутри: rcon_password: "оченьСлож ныйПароль"
ansible-playbook site.yml --ask-vault-pass
Практические сценарии для сети серверов
Массовое обновление модов с сохранением совместимости. Если у тебя несколько сборок Paper/Fabric с разным набором плагинов, playbook может пройтись по mods.yml с фиксированными версиями и скачать конкретные джарники, а не «последнюю версию» — чтобы не словить несовместимость (проблема разобрана в статье про автообновление модов на сервере; Ansible закрывает случай, когда обновление нужно контролируемо прогнать сразу на весь парк).
Синхронный рестарт по расписанию на всей группе — через cron-модуль Ansible раскатывает одно и то же расписание сразу на все хосты группы:
- name: Еженедельный рестарт по воскресеньям в 05:00 UTC
ansible.builtin.cron:
name: "weekly restart {{ service_name }}"
weekday: "0"
hour: "5"
job: "systemctl restart {{ service_name }}"
become: true
Откат на предыдущую версию. Если версия хранится в переменной, откат — это смена значения и повторный прогон: ansible-playbook site.yml --limit rust-eu-1 -e "steam_app_id_manifest=1234567890". SteamCMD поддерживает установку по конкретному manifest ID (+app_update <id> -beta <branch>) — держи под рукой рабочий manifest перед крупными обновлениями важного сервера.
Добавление нового сервера в сеть. Заказал VPS, добавил его в inventory/hosts.yml в нужную группу, прогнал bootstrap.yml, затем site.yml --limit new-host — новый узел получает ту же конфигурацию, что и остальные серверы группы, без расхождений по мелочам.
Проверка дрейфа конфигурации. Раз в неделю полезно прогнать playbook в dry-run — ansible-playbook site.yml --check --diff — чтобы увидеть, не менял ли кто-то конфиги руками в обход Ansible; флаг --diff построчно покажет, что изменится при реальном применении.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Нужен ли Ansible Tower/AWX для сети из нескольких серверов?
Нет, обычного ansible-playbook с CLI и cron достаточно вплоть до пары десятков серверов. Tower/AWX имеет смысл, когда нужен веб-интерфейс, RBAC для нескольких админов и журнал запусков.
Можно ли использовать Ansible на Windows-серверах (например, ARK на Windows)?
Да, через WinRM вместо SSH, но настройка тяжелее, а модулей под Windows меньше. В смешанном парке для Linux используй Ansible, а Windows-узлы проще администрировать через PowerShell или панель хостинга.
Что делать, если один сервер из группы недоступен во время прогона?
Ansible остановит выполнение на упавшем хосте, но продолжит на остальных (если не стоит any_errors_fatal: true). PLAY RECAP покажет failed/unreachable хосты — их можно перепрогнать флагом --limit @site.retry.
Ansible ломает сервер, если запустить playbook посреди игровой сессии?
Сам playbook — нет, если он идемпотентен: без реальных изменений changed=0 и рестарта не будет. Но если меняешь версию игры или конфиг, рестарт неизбежен — планируй такие прогоны на технические окна и предупреждай игроков заранее, например через MOTD.
Стоит ли хранить playbook и inventory в приватном git-репозитории?
Да — это даёт историю изменений, откат командой git revert и код-ревью перед раскаткой на прод. Секреты держи только через Ansible Vault.