SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

Как написать первый playbook в Ansible для VPS

Установите Ansible через pipx на Ubuntu 24.04 и создайте первый playbook для базовой настройки VPS. Вы узнаете, как исправить ошибки Permission denied и sudo при доступе.

Что вы создаете

Один управляющий узел с установленным Ansible и один или несколько чистых VPS на базе Ubuntu 24.04, на которых нет ничего, кроме стандартного образа. В итоге вы получите файл инвентаризации с перечнем ваших серверов, ad-hoc команду ping для проверки аутентификации, а также playbook, который выполняет весь чек-лист настройки нового VPS в виде кода: создание пользователя для развертывания с вашим SSH-ключом, настройку безопасности sshd, установку fail2ban, автоматические обновления и межсетевой экран, который разрешает OpenSSH перед тем, как запретить всё остальное. Применяйте это к одному серверу или к двадцати. Запускайте playbook дважды — второй запуск не должен ничего менять, в этом и заключается основная цель.

За пятнадцать лет администрирования VPS я могу подтвердить закономерность: все настраивают первые пять серверов вручную, а на шестом теряют выходные, потому что никто не помнит, что именно делал с первыми пятью. Это руководство дополняет обзор по теме управления несколькими серверами Linux; обратитесь к нему в тот день, когда поймаете себя на вводе одной и той же команды apt install в трех разных терминалах.

Что такое Ansible на самом деле, в одном абзаце

Ansible не требует установки агентов. На управляемых серверах не нужно запускать никаких демонов: управляющая машина подключается по обычному SSH, копирует небольшой Python-модуль на целевой узел, выполняет его, считывает выведенные данные в формате JSON и удаляет модуль. Единственное требование к целевой системе — наличие python3, которое уже включено в любой стандартный образ Ubuntu. Ключевое понятие здесь — идемпотентность, и оно означает простую вещь: задача описывает состояние, а не действие. Использование state: present для пакета означает «убедиться, что он установлен», а не «запустить установщик». Если состояние уже достигнуто, Ansible ничего не меняет и сообщает о статусе ok вместо changed. Это свойство является основой продукта, именно оно делает повторный запуск playbook безопасным, а безопасные повторные запуски превращают набор shell-скриптов в полноценную инфраструктуру.

Предварительные требования и основные сложности

  • Управляющая машина: ваш ноутбук или небольшой VPS. Предполагается использование Ubuntu 24.04; macOS работает аналогично, если установить pipx через Homebrew.
  • Один или несколько целевых VPS под управлением Ubuntu 24.04 на базе KVM, доступных под пользователем root. На них ничего не устанавливается заранее.
  • Аутентификация по SSH-ключу для каждого целевого узла. Уровень прав Ansible в точности соответствует правам вашей команды ssh; если ssh root@host запрашивает пароль, Ansible завершится с ошибкой.
  • В Ubuntu 24.04 команда pip install ansible завершается с ошибкой error: externally-managed-environment. Это намеренная политика дистрибутива, а не поломка. Используйте pipx.
  • Пробелы в YAML являются частью синтаксиса. Неверный отступ приводит к mapping values are not allowed in this context, а символ табуляции в любом месте файла критичен.
  • Держите активную SSH-сессию открытой на каждом целевом узле во время выполнения playbook для настройки sshd. Каждый случай блокировки, с которым я помогал клиентам, был связан с закрытием последней сессии «для проверки с чистого листа».

Шаг 1: установка Ansible на управляющую машину через pipx, а не pip

Классический подход — это pip3 install ansible. На чистом образе 24.04 это приводит к ошибке на раннем этапе, Command 'pip3' not found, but can be installed with: sudo apt install python3-pip, а установка pip лишь приближает вас к настоящей проблеме:

pip3 install ansible
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

В Ubuntu 24.04 системный Python помечен как внешне управляемый (PEP 668), поэтому pip не может конфликтовать с apt за одни и те же файлы. Не используйте --break-system-packages; название флага говорит само за себя. Правильное решение — pipx, который создает для Ansible изолированное окружение virtualenv и добавляет бинарные файлы в ваш PATH:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

Откройте новую оболочку после pipx ensurepath, чтобы изменения в PATH вступили в силу. --include-deps — это не украшение: пакет ansible не содержит собственных консольных скриптов, ansible, ansible-playbook и остальные являются точками входа его зависимости ansible-core, поэтому без этого флага pipx отклонит установку с ошибкой No apps associated with package ansible or its dependencies. Устанавливайте пакет ansible, а не просто ansible-core: полный пакет включает в себя сообщества коллекций, а этот playbook использует модули из двух из них (ansible.posix и community.general).

ansible --version

Корректный результат начинается со строки вида ansible [core 2.19.x] и указывает версию Python, под которой он запущен; любая актуальная версия core подойдет для всех задач здесь. Ошибка ansible: command not found означает, что ~/.local/bin еще не добавлен в ваш PATH, требуется новая оболочка или source ~/.bashrc.

На этом установка завершена. На целевые машины ничего устанавливать не нужно.

Шаг 2: Настройка доступа по SSH-ключам для всех целевых узлов

ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20

Затем подтвердите доступ для каждого хоста:

ssh root@10.0.0.10 true && echo ok

Эта команда выполняет две задачи: проверяет работу аутентификации по ключу без ввода пароля и добавляет ключ хоста в known_hosts. Выполните это сейчас, так как Ansible при обнаружении неизвестного ключа хоста выводит интерактивный запрос в процессе выполнения, что выглядит как зависание программы.

Шаг 3: инвентарь, сначала INI, затем YAML по мере роста

Инвентарь — это текстовый файл со списком машин, к которым Ansible может подключаться. Создайте inventory.ini в директории нового проекта:

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 — это выбранный вами псевдоним, который будет отображаться в выводе и использоваться при обращении через --limit web1. ansible_host — это реальный адрес. [vps] — это группа, а [vps:vars] задает переменные для каждого хоста в ней; ansible_user — это пользователь, под которым Ansible выполняет вход. Рядом укажите ansible.cfg, чтобы больше не вводить -i:

[defaults]
inventory = inventory.ini

Ansible считывает ansible.cfg из текущей директории. Тот же инвентарь в формате YAML, сохраненный как inventory.yml (в этом случае укажите ansible.cfg на это имя), станет предпочтительным вариантом, когда у хостов появится по несколько переменных:

vps:
  hosts:
    web1:
      ansible_host: 10.0.0.10
    web2:
      ansible_host: 10.0.0.20
  vars:
    ansible_user: root

Они эквивалентны. INI удобнее просматривать для двух серверов; YAML лучше масштабируется для двадцати. Выберите один формат и не тратьте время на раздумья.

Шаг 4: ad-hoc команды, «зеленый пинг» как доказательство работоспособности

ansible all -m ping

Это не ICMP. Модуль ping — это полноценная генеральная репетиция: вход по SSH, копирование модуля, выполнение Python-кода на целевой машине и последующая очистка. Корректный результат отображается зеленым цветом, по одному блоку на каждый хост:

web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Зеленый цвет SUCCESS означает, что аутентификация, интерпретатор Python и транспорт работают корректно, а значит, и playbook отработает успешно. Красный цвет UNREACHABLE! означает, что транспорт не сработал до запуска модуля; точная строка ошибки и способы её устранения приведены в разделе режимов сбоя ниже. Стоит знать еще две ad-hoc команды:

ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --become

Ad-hoc команды предназначены для разовых задач и проверок. Все, что вы планируете запускать более одного раза, должно быть оформлено в виде playbook.

Шаг 5: первый playbook, чек-лист для нового VPS как код

Это всё, что вы делаете вручную в первые десять минут на новом сервере. Сохраните это как site.yml:

---
- name: Baseline a fresh Ubuntu VPS
  hosts: vps
  become: true

  vars:
    deploy_user: deploy
    deploy_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
    baseline_packages:
      - fail2ban
      - unattended-upgrades
      - ufw
    baseline_services:
      - fail2ban
      - unattended-upgrades

  tasks:
    - name: Create the deploy user
      ansible.builtin.user:
        name: "{{ deploy_user }}"
        groups: sudo
        append: true
        shell: /bin/bash

    - name: Install the deploy user's SSH key
      ansible.posix.authorized_key:
        user: "{{ deploy_user }}"
        key: "{{ deploy_pubkey }}"

    - name: Passwordless sudo for the deploy user
      ansible.builtin.copy:
        dest: /etc/sudoers.d/deploy
        content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s

    - name: Install baseline packages
      ansible.builtin.apt:
        name: "{{ baseline_packages }}"
        state: present
        update_cache: true

    - name: Enable and start baseline services
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
        enabled: true
      loop: "{{ baseline_services }}"

    - name: Harden sshd with a drop-in
      ansible.builtin.copy:
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        content: |
          PasswordAuthentication no
          KbdInteractiveAuthentication no
          PermitRootLogin prohibit-password
          X11Forwarding no
        mode: "0644"
        validate: /usr/sbin/sshd -t -f %s
      notify: Restart ssh

    - name: Allow OpenSSH through ufw
      community.general.ufw:
        rule: allow
        name: OpenSSH

    - name: Enable ufw with default deny
      community.general.ufw:
        state: enabled
        policy: deny

  handlers:
    - name: Restart ssh
      ansible.builtin.service:
        name: ssh
        state: restarted

Строки, которые стоит понять, а не просто скопировать:

Переменные находятся в vars: и вызываются через "{{ deploy_user }}". Если значение начинается с фигурной скобки, берите всё выражение в кавычки, иначе YAML-парсер считает его неверно. Модуль lookup('file', ...) считывает ваш публичный ключ с управляющей машины во время выполнения, поэтому в самом playbook нет никаких секретных ключей.

Цикл. loop: "{{ baseline_services }}" выполняет задачу для сервиса по одному разу для каждого элемента, и вывод показывает каждый элемент на отдельной строке. Заметьте, что задача apt принимает весь список пакетов сразу: одна транзакция apt быстрее, и это предпочтительный шаблон для пакетов. Циклы нужны для модулей, которые действительно работают с одним объектом за раз.

Обработчик (handler) — это концепция, которую нужно усвоить. notify: Restart ssh не означает «перезапустить ssh прямо сейчас». Это ставит обработчик в очередь, который сработает один раз в конце выполнения play, и только если уведомляющая задача действительно сообщила о changed. Запустите playbook завтра: файл конфигурации уже верный, задача copy сообщает о ok, и sshd не перезапускается. Строка validate: — это предохранитель: sshd проверяет файл перед тем, как заменить старый, поэтому опечатка приведет к ошибке задачи, а не к поломке демона.

PermitRootLogin prohibit-password, а не no, намеренно. Этот playbook входит как root по ключу. prohibit-password отключает вход по паролю для root, оставляя ваш доступ активным. Как только пользователь для развертывания проверен (ssh deploy@10.0.0.10 sudo true, обычный адрес, так как web1 — это псевдоним, известный только Ansible), переключите ansible_user=deploy в инвентаре и ужесточите настройки до no в следующем запуске. Усиливайте защиту в таком порядке, чтобы не потерять доступ к серверу.

Префикс 00- имеет значение. Для большинства параметров sshd учитывает первое вхождение, которое он считывает, а sshd_config в Ubuntu включает sshd_config.d/*.conf в лексикографическом порядке перед своим основным телом. Облачные образы Ubuntu 24.04 уже содержат 60-cloudimg-settings.conf в этой директории, а провайдеры, включающие вход по паролю через cloud-init, добавляют 50-cloud-init.conf с PasswordAuthentication yes. Называя наш файл 00-hardening.conf, мы добиваемся того, что он сортируется первым и перекрывает оба предыдущих.

Порядок задач обеспечивает безопасность межсетевого экрана. Allow OpenSSH выполняется до Enable ufw с политикой запрета по умолчанию. Ansible выполняет задачи строго в указанном порядке, поэтому «дыра» в правилах существует до того, как возводится «стена». fail2ban не требует настройки, чтобы быть полезным здесь: настройки по умолчанию в Ubuntu «из коробки» следят за sshd. О том, что именно делают jails и что стоит настроить, рассказано в руководстве по fail2ban в Ubuntu 24.04.

Шаг 6: пробный запуск с --check, затем выполнение в рабочем режиме

ansible-playbook site.yml --check

Режим проверки (check mode) устанавливает соединение, вычисляет, какие действия были бы выполнены, но не вносит никаких изменений. Изучите значение changed= в сводке PLAY RECAP в конце вывода: это количество задач, которые изменили бы состояние каждого хоста. Важное предостережение: режим проверки имеет структурное ограничение, если последующая задача зависит от изменений, внесенных предыдущей. Стандартный образ сервера Ubuntu поставляется с предустановленным ufw, поэтому этот playbook проходит проверку без ошибок, но на минимальном образе без ufw задачи по его настройке в режиме проверки завершатся с ошибкой. Это происходит потому, что в режиме проверки пакет не устанавливается, и модулю нечего вызывать. Это ограничение пробного запуска, а не ошибка в вашем playbook. Когда план выглядит корректно:

ansible-playbook site.yml

Каждая задача выводит строку для каждого хоста: желтый цвет changed, зеленый ok, а итоговая сводка должна выглядеть так:

PLAY RECAP *********************************************************************
web1 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
web2 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

Десять ok — это сбор фактов плюс восемь задач и обработчик (handler). Ваши значения changed могут отличаться от моих на единицу или два: стандартный образ Ubuntu содержит предустановленные ufw и unattended-upgrades, а fail2ban запускается сразу после установки через apt. Поэтому задача может вполне законно сообщить ok при самом первом запуске, так как целевое состояние уже достигнуто. Значения, которые должны быть равны нулю — это unreachable и failed. Примечание по поводу become: true: это формальность, пока вы подключаетесь как root, но как только вы измените ansible_user на deploy, sudo станет реальным. Файл sudoers с параметром NOPASSWD, который устанавливает этот playbook, — это именно то, что избавляет вас от ввода -K в командной строке. Без него вы получите Missing sudo password, о чем рассказано ниже.

Шаг 7: повторный запуск и идемпотентность

Сразу же выполните ту же команду еще раз:

web1 : ok=9  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0 и ok уменьшились на единицу, так как обработчик без уведомления не сработал. Ничего не было переустановлено, sshd не перезапускался, ufw не затрагивался. Именно это делает playbook инструментом для аудита в той же мере, что и для развертывания: добавьте web3 в inventory в следующем месяце и запустите снова — новый сервер будет настроен, а старые пройдут проверку. Ненулевой changed на сервере, который вы не трогали, означает отклонение конфигурации (drift); это сигнал о том, что кто-то внес ручные правки там, где изменения должны были вноситься через playbook.

Далее этот шаблон масштабируется. Следующий playbook, который стоит написать, должен развернуть WireGuard VPN на том же VPS и ужесточить правила ufw, чтобы SSH отвечал только через туннель; после этого — playbook, который устанавливает Docker и Compose на каждый сервер приложений. Когда site.yml станет длиннее трех экранов, разделите его на роли, но не раньше.

Типовые ошибки и соответствующие им сообщения

UNREACHABLE с ошибкой Permission denied.

web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: root@10.0.0.10: Permission denied (publickey).",
    "unreachable": true
}

Транспорт SSH завершился с ошибкой до запуска какого-либо модуля: неверно указан ansible_user, ключ не был скопирован на хост или предлагается неверный ключ. Воспроизведите ошибку с помощью обычной команды ssh root@10.0.0.10, затем используйте ssh -v, чтобы увидеть, какие ключи были предложены. Если SSH по паролю работает, а Ansible — нет, значит, вы пропустили ssh-copy-id.

Отсутствует пароль sudo.

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

Вы установили become: true, подключились как пользователь без прав root, и этому пользователю требуется пароль для sudo. Либо добавьте -K (--ask-become-pass) в командную строку, либо настройте для пользователя запись NOPASSWD в sudoers — именно поэтому playbook устанавливает такую запись для deploy до того, как вы переключитесь на него.

error: externally-managed-environment. Вы запустили pip для системного Python в Ubuntu 24.04. Решение описано в шаге 1: используйте pipx, а не pip, и не используйте --break-system-packages.

mapping values are not allowed in this context.

ERROR! Syntax Error while loading YAML.
  mapping values are not allowed in this context

Почти всегда это проблема с отступами: ключ находится на неверном уровне вложенности или пропущен пробел после двоеточия. Указанный номер строки указывает рядом с ошибкой, а не на саму ошибку, поэтому проверьте также строку выше. Родственная ошибка found character '\t' that cannot start any token означает, что в файл попал символ табуляции; YAML запрещает их использование. Сделайте выполнение ansible-playbook site.yml --syntax-check рефлексом перед каждым запуском и настройте в редакторе отступ в два пробела для YAML.

/usr/bin/python3: not found. Редко встречается в стандартных образах Ubuntu 24.04, но часто бывает в минимальных или netboot-образах: выполнение модуля завершается с ошибкой, так как на целевой системе отсутствует Python. Выполните начальную настройку с помощью модуля raw — это единственный модуль, который не требует ничего на целевой стороне: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, затем перезапустите playbook.

FAQ

Нужно ли устанавливать Ansible на управляемые серверы?

Нет. Ansible не требует установки агентов: управляющая машина передает небольшие модули на Python по SSH, выполняет их и удаляет. На целевом узле достаточно наличия python3 и доступа по SSH, которые уже есть в стандартных образах Ubuntu. Единственная установка в рамках этого руководства выполняется только на вашей управляющей машине.

Почему Ansible выдает ошибку "Permission denied (publickey)"?

Блок UNREACHABLE! с Permission denied (publickey) означает, что аутентификация SSH не прошла до того, как Ansible начал работу. Убедитесь, что ansible_user в инвентаре соответствует учетной записи, которую вы настроили, что вы выполнили ssh-copy-id для этого хоста и что обычная команда ssh user@host позволяет войти без пароля. Любое действие, исправляющее работу команды ssh, исправит и Ansible, так как они используют один и тот же транспорт.

Что означает идемпотентность в Ansible?

Задача описывает желаемое состояние, например «этот пакет должен быть установлен» или «эта строка должна присутствовать в файле», а не конкретное действие. Если состояние уже соответствует заданному, Ansible ничего не делает и сообщает ok вместо changed. Именно поэтому при повторном запуске плейбука вы видите changed=0, а сам повторный запуск является безопасной проверкой, а не рискованной переустановкой.

Что использовать для установки Ansible в Ubuntu 24.04: pip или pipx?

Используйте pipx. В Ubuntu 24.04 системный Python помечен как управляемый извне, поэтому команда pip install ansible завершается ошибкой error: externally-managed-environment по замыслу разработчиков дистрибутива. pipx install --include-deps ansible помещает Ansible в изолированное виртуальное окружение и корректно добавляет ansible, ansible-playbook и остальные компоненты в ваш PATH.

В чем разница между пакетами ansible и ansible-core?

ansible-core — это движок, содержащий только базовые модули ansible.builtin. Пакет ansible включает в себя core и отобранные сообществом коллекции, включая ansible.posix (модуль authorized_key) и community.general (модуль ufw), которые используются в этом руководстве. Начинайте с полного пакета; переходите на core с выборочными коллекциями только тогда, когда у вас появится конкретная причина для этого.