SSD Nodes Learn
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-07-24

Як написати перший playbook для VPS на Ansible

Встановіть Ansible через pipx на Ubuntu 24.04 та налаштуйте захист VPS. Вирішення помилок Permission denied та sudo при використанні першого playbook.

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

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

За п'ятнадцять років налаштування VPS я помітив закономірність: всі налаштовують перші п'ять серверів вручну, а потім втрачають цілий вихідний на шостому, бо ніхто не пам'ятає, що саме було зроблено на перших п'яти. Цей посібник доповнює керування кількома Linux серверами — почніть читати, як тільки помітите, що вводите одну й ту саму apt install у три різні термінали.

Що таке Ansible насправді, одним абзацом

Ansible не потребує агентів. На серверах, якими він керує, не потрібно встановлювати жодних демонів: керуюча машина підключається через звичайний SSH, копіює невеликий Python-модуль на цільову систему, виконує його, зчитує виведення у форматі JSON і видаляє модуль. Від цільової системи потрібен лише python3, який вже є у кожному стандартному образі Ubuntu. Ключове поняття — idempotent (ідемпотентність), що означає просту річ: завдання описує стан, а не дію. state: present для пакета означає «переконатися, що він встановлено», а не «запустити інсталятор». Якщо стан уже досягнуто, Ansible нічого не змінює і повідомляє про ok замість changed. Ця властивість є основою продукту — саме вона робить повторний запуск playbook безпечним, а безпечні повторні запуски перетворюють shell-скрипт на інфраструктуру.

Prerequisites, and the gotchas up front

  • Control machine: ваш ноутбук або невеликий VPS. Я використовую Ubuntu 24.04; macOS працює так само після встановлення pipx через Homebrew.
  • Один або кілька цільових VPS з Ubuntu 24.04 на KVM, доступних через root. На них нічого не встановлюється.
  • SSH key аутентифікація для кожного цільового вузла. 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. Кожна ситуація з повною втратою доступу, у якій я допомагав клієнту, виникала через закриття останньої сесії для "чистого тестування".

Step 1: install Ansible on the control machine with pipx, not 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 позначений як externally managed (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

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

ansible --version

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

Це всі інструкції з встановлення. На цільових хостах нічого встановлювати не потрібно.

Step 2: SSH key access to every target

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

Цей рядок виконує дві функції: підтверджує роботу автентифікації за ключем без пароля та записує host key у known_hosts. Виконайте це зараз, оскільки Ansible видасть запит на підтвердження невідомого host key під час виконання сценарію, що виглядає як зависання системи.

Step 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 краще масштабується при двадцяти. Оберіть один варіант і не переймайтеся.

Step 4: ad-hoc commands — the green pong that proves everything

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

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

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

Step 5: перший playbook — checklist для нового 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

Рядки, які варто зрозуміти, а не просто скопіювати:

Variables знаходяться під vars: і посилаються через "{{ deploy_user }}" — беріть весь вираз у лапки, якщо значення починається з фігурної дужки, інакше YAML-парсер прочитає його некоректно. lookup('file', ...) зчитує ваш публічний ключ з control машини під час виконання, тому playbook не містить жодних ключів.

The loop. loop: "{{ baseline_services }}" виконує задачу сервісу один раз для кожного елемента, а вивід відображає кожен елемент окремим рядком. Зауважте, що задача apt приймає весь список пакетів одним махом — одна транзакція apt працює швидше, і це рекомендований шаблон для пакетів; цикли використовуються для модулів, які дійсно працюють з одним об'єктом за раз.

The handler — це концепція, яку слід засвоїти. notify: Restart ssh не означає "перезапустити ssh зараз". Він ставить handler у чергу, який виконується один раз наприкінці play, і тільки якщо задача, що викликає його, фактично повідомила про changed. Запустіть playbook завтра: drop-in файл уже буде правильним, задача копіювання повідомить про 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 в inventory і посильте налаштування до no під час наступного запуску. Посилюйте захист у такому порядку, щоб не втратити доступ до системи.

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

Порядок задач — це безпека фаєрвола. Allow OpenSSH виконується перед Enable ufw з політикою deny — Ansible виконує задачі суворо в зазначеному порядку, тому дірка в захисті з'являється до того, як зведеться стіна. fail2ban не потребує конфігурації для корисної роботи в цьому випадку; стандартні налаштування Ubuntu вже відстежують sshd, а те, що саме роблять jails — і що потрібно налаштувати — описано в fail2ban on Ubuntu 24.04 guide.

Step 6: dry run з прапором --check, потім запуск у реальному режимі

ansible-playbook site.yml --check

Режим перевірки (check mode) підключається до хостів, обчислює заплановані дії, але нічого не змінює. Перегляньте показник changed= у PLAY RECAP внизу — це кількість завдань, які змінять кожен хост. Важливе зауваження: режим перевірки має структурне обмеження, якщо наступне завдання залежить від змін, внесених попереднім завданням. Стандартний образ Ubuntu Server вже містить ufw, тому цей playbook проходить dry-run без помилок. Проте на мінімальному образі без цього пакету завдання ufw завершаться помилкою у режимі перевірки, оскільки режим перевірки не встановлює пакет, і модулю немає до чого звертатися. Це обмеження механізму dry run, а не помилка у вашому 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 мають дорівнювати 0. Примітка щодо become: true: це формальність, поки ви підключені як root, але як тільки ви зміните ansible_user на deploy, sudo почне працювати. Файл sudoers з параметром NOPASSWD, який встановлює цей playbook, запобігає появі -K у командному рядку. Без нього ви отримаєте Missing sudo password, про що описано нижче.

Step 7: запуск двічі — як виглядає ідемпотентність

Запустіть ту саму команду ще раз відразу:

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

changed=0 та ok зменшилися на одиницю, оскільки handler не був викликаний. Жодне ПЗ не було перевстановлено, sshd не перезапускався, ufw не змінювався. Це робить playbook не лише засобом налаштування, а й засобом аудиту: додайте web3 до inventory наступного місяця та запустіть знову — нова машина буде створена, а старі — перевірені. Ненульовий changed на машині, яку ви не змінювали, означає drift (відхилення конфігурації); це вказує на те, що хтось редагував вручну те, що має редагуватися через playbook.

Далі цей підхід масштабується. Наступний playbook, який варто написати, розгортає WireGuard VPN на тому ж VPS та посилює правило ufw, щоб SSH відповідав лише через тунель; після цього — playbook для встановлення Docker and Compose на кожен app server. Коли site.yml займає більше трьох екранів, розділіть його на roles — але не раніше.

Режими помилок та відповідні повідомлення

UNREACHABLE with 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.

Missing sudo password.

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 в inventory відповідає налаштованому обліковому запису, чи ви виконували ssh-copy-id до цього хоста, та чи дозволяє звичайний ssh user@host вхід без пароля. Будь-яке рішення, що виправляє звичайну команду ssh, виправить і Ansible, оскільки вони використовують один і той самий протокол передачі.

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

Завдання (task) описує бажаний стан — наприклад, "цей пакет встановлено" або "цей рядок є у цьому файлі" — а не дію для виконання. Якщо стан уже відповідає заданому, Ansible нічого не робить і замість changed повертає ok. Саме тому при повторному запуску 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 містить core разом із перевіреними спільнотними колекціями, включаючи ansible.posix (модуль authorized_key) та community.general (модуль ufw), які використовуються в цьому посібнику. Починайте з повного пакета; переходьте до версії core з окремими колекціями лише тоді, коли це дійсно необхідно.