SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-01

Ansible для VPS: перший playbook на Ubuntu 24.04

Встановіть Ansible через pipx, створіть inventory і playbook для захисту VPS на Ubuntu 24.04. Розберіть помилки Permission denied та sudo.

Що ви створюєте

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

Після п’ятнадцяти років підготовки 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, а символ табуляції в будь-якому місці робить файл некоректним.
  • Під час посилення конфігурації sshd за допомогою playbook залишайте відкритим робочий SSH-сеанс на кожній цільовій системі. У всіх випадках блокування доступу, з якими мені доводилося допомагати клієнтам, останній сеанс закривали, щоб «перевірити з чистого середовища».

Крок 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, під яким запускається Ansible. Будь-яка поточна версія 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: inventory: спочатку INI, YAML — коли він розростеться

Inventory — це текстовий файл зі списком машин, до яких 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 з поточного каталогу. Той самий inventory у 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: спеціальні команди, зелений pong, який підтверджує все

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! означає, що транспорт не працював ще до запуску будь-якого модуля. Точний текст повідомлення та спосіб виправлення наведено в розділі про типові помилки нижче. Варто знати ще дві спеціальні команди:

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

Спеціальні команди призначені для одноразових дій і перевірок. Усе, що потрібно виконати двічі, слід додати до playbook.

Крок 5: перший playbook, контрольний список для нового VPS у вигляді коду

Це все, що ви виконали б вручну протягом перших 10 хвилин на новому сервері. Збережіть це як 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 parser неправильно його інтерпретує. lookup('file', ...) під час виконання зчитує ваш public key з керувальної машини, тому playbook не містить ключового матеріалу.

Цикл. loop: "{{ baseline_services }}" виконує завдання для сервісу один раз для кожного елемента, а у виводі кожен елемент показано в окремому рядку. Зверніть увагу: apt task натомість передає весь список пакетів одним викликом. Одна apt transaction швидша й є рекомендованим підходом для пакетів. Цикли призначені для модулів, які справді обробляють лише один об’єкт за раз.

Handler — це концепція, яку потрібно засвоїти. notify: Restart ssh не означає «негайно перезапустити ssh». Він ставить handler у чергу. Handler виконується один раз наприкінці play і лише якщо task, що його викликав, справді повідомив про changed. Запустіть playbook знову завтра: drop-in file уже буде правильним, copy task повідомить про ok, а sshd не буде перезапущено. Рядок validate: забезпечує безпеку тригера: sshd перевіряє файл перед заміною старого, тому помилка в ньому призведе до невдалого task, а не до зупинки daemon.

PermitRootLogin prohibit-password, а не no, навмисно. Цей playbook підключається як root за допомогою ключа. prohibit-password вимикає вхід root за паролем, зберігаючи ваш поточний доступ. Після перевірки deploy user (ssh deploy@10.0.0.10 sudo true, звичайна адреса, оскільки web1 є псевдонімом, який знає лише Ansible) змініть ansible_user=deploy в inventory і під час наступного запуску обмежте його до no. Виконуйте hardening у такому порядку, щоб не втратити доступ до сервера.

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

Порядок task забезпечує безпеку firewall. Allow OpenSSH виконується перед Enable ufw із політикою deny. Ansible виконує tasks суворо в наведеному порядку, тому доступ залишається відкритим до встановлення блокування. Для корисної роботи fail2ban не потребує додаткової конфігурації: його стандартні параметри Ubuntu одразу відстежують sshd. Принцип роботи jail і параметри, які слід налаштувати, описано в посібнику з fail2ban в Ubuntu 24.04.

Крок 6: виконайте пробний запуск із --check, а потім запустіть по-справжньому

ansible-playbook site.yml --check

Режим перевірки встановлює з’єднання, обчислює, які дії виконав би, і нічого не змінює. Перегляньте кількість changed= у PLAY RECAP внизу. Це кількість завдань, які змінили б кожен вузол. Є важливе обмеження: режим перевірки має структурне обмеження, якщо пізніше завдання залежить від змін, внесених раніше. Стандартний серверний образ Ubuntu уже містить ufw, тому цей playbook проходить пробний запуск без помилок. Але в мінімальному образі без ufw завдання 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 — це збирання фактів, вісім завдань і обробник. Ваш 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 до інвентарю та повторно запустіть playbook — новий сервер буде налаштовано, а старі сервери перевірено. Ненульове значення changed на сервері, якого ви не змінювали, означає розбіжність конфігурації. Це вказує, що хтось уніс вручну зміни, які слід було внести до 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) до командного рядка або надайте користувачеві запис sudoers із параметром NOPASSWD. Саме тому 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 встановлюється лише на керувальній машині.

Чому Ansible повідомляє "Permission denied (publickey)"?

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

Що означає ідемпотентність в Ansible?

Завдання оголошує бажаний стан: "цей пакет встановлено" або "цей рядок є у цьому файлі", а не дію, яку потрібно виконати. Якщо стан уже відповідає вимогам, Ansible нічого не робить і повідомляє ok замість changed. Тому під час повторного запуску playbook вдруге відображається changed=0, а повторний запуск є безпечною перевіркою, а не ризикованим повторним встановленням.

Чи слід використовувати pip або pipx для встановлення Ansible в Ubuntu 24.04?

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

У чому різниця між пакетами ansible і ansible-core?

ansible-core — це ядро та лише модулі ansible.builtin. Пакет ansible містить ядро і відібрані спільнотою collections, зокрема ansible.posix (модуль authorized_key) та community.general (модуль ufw), які використовуються в цьому посібнику. Почніть із повного пакета. Переходьте на core із вибраними вручну collections лише за потреби.