Как написать первый 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 ansibleerror: 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=rootweb1 — это выбранный вами псевдоним, который будет отображаться в выводе и использоваться при обращении через --limit web1. ansible_host — это реальный адрес. [vps] — это группа, а [vps:vars] задает переменные для каждого хоста в ней; ansible_user — это пользователь, под которым Ansible выполняет вход. Рядом укажите ansible.cfg, чтобы больше не вводить -i:
[defaults]
inventory = inventory.iniAnsible считывает 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" --becomeAd-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=0changed=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 с выборочными коллекциями только тогда, когда у вас появится конкретная причина для этого.