MAATRIX GAMES / Блог / Ansible: автоматизация настройки нескольких игровых серверов

Ansible: автоматизация настройки нескольких игровых серверов

MAATRIX GAMES

Когда у тебя один сервер Rust или Minecraft — зашёл по SSH, обновил, перезапустил, забыл. Когда серверов пять, десять или тридцать (сеть сообществ, разные модпаки, тестовый и продовый стенды) — та же рутина превращается в вечер, убитый на копипасту команд в двадцать разных терминалов, и рано или поздно на одном из серверов ты забудешь применить патч или обновишь не ту версию мода. Ansible решает именно эту задачу: один 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.