SSD Nodes Learn
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-07-24

Instalacja Ansible i pierwszy playbook na VPS

Instalacja Ansible przez pipx na Ubuntu 24.04. Tworzenie inventory oraz playbooka do hardeningu VPS. Rozwiązanie błędów Permission denied i sudo errors.

Cel prac

Jedna maszyna sterująca z zainstalowanym Ansible oraz jeden lub więcej nowych serwerów VPS z systemem Ubuntu 24.04 (tylko obraz systemowy). Wynikiem końcowym będzie plik inventory zawierający nazwy serwerów, polecenie ad-hoc ping potwierdzające działanie uwierzytelniania oraz playbook realizujący pełną listę kontrolną dla nowego VPS jako kod: użytkownik do wdrażania z kluczem SSH, utwardzony proces sshd, fail2ban, unattended upgrades oraz firewall zezwalający na ruch OpenSSH przed zablokowaniem pozostałego ruchu. Rozwiązanie działa dla jednego serwera lub dwudziestu. Ponowne uruchomienie playbooka nie wprowadza żadnych zmian — jest to kluczowa cecha tego rozwiązania.

Po piętnastu latach provisioningu serwerów VPS można stwierdzić stały schemat: użytkownicy konfigurują pierwsze pięć serwerów ręcznie, a przy szóstym tracą cały weekend, ponieważ nikt nie pamięta wykonanych wcześniej kroków. Niniejszy poradnik stanowi rozwinięcie tematu zarządzania wieloma serwerami Linux — należy sięgnąć po niego w momencie, gdy zaczniesz wpisywać tę samą apt install w trzech różnych terminalach.

Czym w rzeczywistości jest Ansible, w jednym akapicie

Ansible jest bezagentowy. Na zarządzanych serwerach nie należy instalować żadnego demona: maszyna sterująca łączy się przez standardowy protokół SSH, kopiuje mały moduł Python na cel, wykonuje go, odczytuje wydrukowany format JSON, a następnie usuwa moduł. Cel wymaga jedynie python3, który znajduje się w każdym standardowym obrazie Ubuntu. Kluczowym terminem jest idempotentność, co oznacza prostą rzecz: zadanie opisuje stan, a nie czynność. state: present dla pakietu oznacza „upewnij się, że jest on zainstalowany”, a nie „uruchom instalator”. Jeśli stan jest już osiągnięty, Ansible nie wykonuje żadnych operacji i raportuje ok zamiast changed. Ta właściwość stanowi istotę produktu — to ona sprawia, że ponowne uruchomienie playbooka jest bezpieczne, a bezpieczne ponowne uruchomienia zmieniają skrypt powłoki w infrastrukturę.

Wymagania wstępne i potencjalne problemy

  • Maszyna sterująca: laptop lub mały VPS. Zakładany system to Ubuntu 24.04; macOS działa identycznie po zainstalowaniu pipx za pomocą Homebrew.
  • Jeden lub więcej serwerów docelowych VPS z systemem Ubuntu 24.04 na KVM, dostępnych przez konto root. Na serwerach nie są instalowane żadne dodatkowe pakiety.
  • Uwierzytelnianie za pomocą klucza SSH do każdego serwera docelowego. Poziom uprawnień Ansible jest identyczny jak w komendzie ssh — jeśli ssh root@host wymaga podania hasła, Ansible zakończy działanie błędem.
  • W systemie Ubuntu 24.04 pip install ansible kończy działanie z błędem error: externally-managed-environment. Jest to celowa polityka dystrybucji, a nie błąd systemu. Należy użyć pipx.
  • Formatowanie białych znaków w YAML ma znaczenie składniowe. Błędne wcięcie powoduje błąd mapping values are not allowed in this context, a użycie znaku tabulacji skutkuje krytycznym błędem.
  • Należy pozostawić otwartą aktywną sesję SSH na każdym serwerze docelowym podczas gdy playbook konfiguruje zabezpieczenia sshd. Każda sytuacja utraty dostępu, w której pomogłem klientowi, wynikała z zamknięcia ostatniej sesji w celu "przeprowadzenia testu na czysto".

Krok 1: instalacja Ansible na maszynie sterującej za pomocą pipx, a nie pip

Typowym błędem jest użycie pip3 install ansible. Na czystym obrazie systemu 24.04, który napotyka błąd na wczesnym etapie — Command 'pip3' not found, but can be installed with: sudo apt install python3-pip — instalacja pip prowadzi do problemów z zarządzaniem pakietami:

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.

W systemie Ubuntu 24.04 Python systemowy jest oznaczony jako zarządzany zewnętrznie (PEP 668), co uniemożliwia konflikt pip z apt o te same pliki. Nie należy używać --break-system-packages; nazwa flagi jest jednoznaczna. Poprawnym rozwiązaniem jest pipx, który tworzy dla Ansible izolowane środowisko wirtualne (virtualenv) i dodaje binarne pliki wykonywalne do zmiennej PATH:

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

Po wykonaniu pipx ensurepath należy otworzyć nową powłokę, aby zmiany w zmiennej PATH zostały zaaplikowane. --include-deps nie jest opcjonalne: pakiet ansible nie zawiera własnych skryptów konsolowych — ansible, ansible-playbook i pozostałe to punkty wejścia (entry points) zależności ansible-core — dlatego bez tej flagi pipx zgłosi błąd instalacji: No apps associated with package ansible or its dependencies. Należy zainstalować pakiet ansible, a nie sam pakiet ansible-core — pełny pakiet zawiera kolekcje społeczności, a niniejszy playbook wykorzystuje moduły z dwóch z nich (ansible.posix oraz community.general).

ansible --version

Poprawny wynik instalacji zaczyna się od linii typu ansible [core 2.19.x] i wskazuje wersję Pythona, na której uruchomiono proces; każda aktualna wersja podstawowa jest odpowiednia. Komunikat ansible: command not found oznacza, że ~/.local/bin nie znajduje się jeszcze w zmiennej PATH — należy otworzyć nową powłokę lub użyć source ~/.bashrc.

To koniec procesu instalacji. Cele (targets) nie otrzymują żadnych pakietów.

Step 2: Dostęp przez klucz SSH do każdego celu

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

Następnie należy to zweryfikować dla każdego hosta:

ssh root@10.0.0.10 true && echo ok

Ta jedna linia pełni dwie funkcje: potwierdza działanie uwierzytelniania kluczem bez hasła oraz zapisuje klucz hosta w known_hosts. Należy to wykonać natychmiast, ponieważ Ansible wyświetla nieznany klucz hosta jako interaktywny komunikat w trakcie działania, co jest błędnie interpretowane jako zawieszenie procesu.

Step 3: inwentarz — najpierw INI, potem YAML przy większej skali

Inwentarz to plik tekstowy zawierający listę maszyn, którymi może zarządzać Ansible. Należy utworzyć inventory.ini w nowym katalogu projektu:

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 to wybrany przez użytkownika alias — jest on wyświetlany w wyjściu i stanowi cel dla --limit web1. ansible_host to rzeczywisty adres. [vps] to grupa, a [vps:vars] definiuje zmienne dla każdego hosta w tej grupie; ansible_user określa użytkownika logowania Ansible. Obok znajduje się ansible.cfg, aby uniknąć ponownego wpisywania -i:

[defaults]
inventory = inventory.ini

Ansible odczytuje ansible.cfg z bieżącego katalogu. Inwentarz w formacie YAML — należy zapisać go jako inventory.yml i wskazać tę nazwę w ansible.cfg — będzie preferowany, gdy każdy host będzie posiadał wiele zmiennych:

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

Oba formaty są równoważne. Format INI jest czytelniejszy przy dwóch serwerach; format YAML lepiej skaluje się przy dwudziestu. Należy wybrać jeden format i stosować go konsekwentnie.

Step 4: polecenia ad-hoc — zielony wynik potwierdzający poprawność działania

ansible all -m ping

Nie jest to ICMP. Moduł ping stanowi pełną próbę generalną: logowanie przez SSH, kopiowanie modułu, wykonanie kodu Python na docelowym hoście oraz sprzątanie. Poprawny wynik to kolor zielony, jeden blok na każdy host:

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

Zielony SUCCESS oznacza, że uwierzytelnianie, interpreter Python oraz protokół transportowy działają poprawnie — playbook również zadziała. Czerwony UNREACHABLE! oznacza błąd protokołu transportowego przed uruchomieniem modułu; dokładny komunikat błędu oraz rozwiązanie znajdują się w sekcji failure modes poniżej. Dwa dodatkowe polecenia ad-hoc, które warto znać:

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

Polecenia ad-hoc służą do jednorazowych zadań i weryfikacji. Każda operacja wykonywana wielokrotnie powinna zostać umieszczona w playbooku.

Step 5: pierwszy playbook — lista kontrolna new-VPS jako kod

Zawiera ona wszystkie czynności wykonywane ręcznie w ciągu pierwszych dziesięciu minut na nowym serwerze. Należy zapisać plik jako 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

Elementy wymagające zrozumienia, a nie tylko skopiowania:

Zmienne znajdują się w vars: i są odwoływane za pomocą "{{ deploy_user }}" — należy użyć cudzysłowu dla całego wyrażenia, jeśli wartość zaczyna się od klamry, aby uniknąć błędów parsera YAML. lookup('file', ...) pobiera klucz publiczny z maszyny control podczas uruchamiania, dzięki czemu playbook nie zawiera danych klucza.

Pętla. loop: "{{ baseline_services }}" wykonuje zadanie usługi raz dla każdego elementu, a wyjście wyświetla każdy element w osobnej linii. Zadanie apt przetwarza całą listę pakietów jednocześnie — jedna transakcja apt jest szybsza i jest zalecanym wzorcem dla pakietów; pętle stosuje się dla modułów operujących na jednym obiekcie naraz.

Handler to kluczowe pojęcie. notify: Restart ssh nie oznacza "zrestartuj ssh teraz". Kolejkuje on handler, który uruchamia się raz na końcu playbooka, tylko jeśli zadanie powiadamiające zgłosi changed. Ponowne uruchomienie playbooka jutro: plik konfiguracyjny jest już poprawny, zadanie kopiowania zgłasza ok, a usługa sshd nie zostaje zrestartowana. Linia validate: stanowi zabezpieczenie — sshd sprawdza plik przed zastąpieniem starego, więc błąd składniowy spowoduje niepowodzenie zadania zamiast awarii demona.

PermitRootLogin prohibit-password, a nie no — celowo. Ten playbook loguje się jako root przy użyciu klucza. prohibit-password wyłącza logowanie na roota za pomocą hasła, zachowując dostęp kluczem. Po zweryfikowaniu użytkownika do wdrażania (ssh deploy@10.0.0.10 sudo true — czysty adres, ponieważ web1 to tylko alias znany Ansible), należy zmienić ansible_user=deploy w inventory i ograniczyć go do no podczas późniejszego uruchomienia. Proces utwardzania systemu należy przeprowadzać w kolejności, która nie odetnie dostępu do serwera.

Prefiks 00- ma znaczenie. Dla większości słów kluczowych sshd honoruje pierwsze napotkane wystąpienie, a ubuntu sshd_config zawiera sshd_config.d/*.conf w kolejności leksykalnej przed własną treścią. Obrazy cloud Ubuntu 24.04 zawierają już 60-cloudimg-settings.conf w tym katalogu, a dostawcy umożliwiający logowanie hasłem przez cloud-init dodają 50-cloud-init.conf z PasswordAuthentication yes; nadanie naszemu plikowi nazwy 00-hardening.conf sprawia, że jest on sortowany jako pierwszy i ma pierwszeństwo.

Kolejność zadań stanowi zabezpieczenie. Allow OpenSSH uruchamia się przed Enable ufw z polityką deny — Ansible wykonuje zadania ściśle w podanej kolejności, więc luka powstaje przed postawieniem zapory. fail2ban nie wymaga konfiguracji, aby być użytecznym w tym przypadku; domyślne ustawienia Ubuntu monitorują sshd od razu, a szczegóły działania jaili oraz metody ich dostrajania opisano w przewodniku fail2ban na Ubuntu 24.04.

Step 6: dry run z flag --check, a następnie uruchomienie właściwe

ansible-playbook site.yml --check

Tryb check łączy się z hostem, oblicza planowane działania i nie wprowadza żadnych zmian. Należy sprawdzić liczbę changed= w sekcji PLAY RECAP na dole strony — określa ona liczbę zadań, które zmodyfikowałyby każdy host. Ważne zastrzeżenie: tryb check posiada ograniczenie strukturalne w sytuacjach, gdy późniejsze zadanie zależy od zmian wprowadzonych przez wcześniejsze zadanie. Standardowy obraz serwerowy Ubuntu zawiera pakiet ufw, więc playbook przechodzi test dry-run bez błędów. Na minimalnym obrazie bez tego pakietu zadania ufw zakończą się błędem w trybie check, ponieważ tryb ten nie instaluje pakietów — moduł nie ma wówczas obiektu, który mógłby wywołać. Jest to ograniczenie trybu dry-run, a nie błąd w playbooku. Po zweryfikowaniu planu:

ansible-playbook site.yml

Każde zadanie wypisuje jedną linię na host — żółty changed, zielony ok — a podsumowanie powinno wyglądać następująco:

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

Dziesięć ok to zbieranie danych, osiem zadań oraz handler. Wartość changed może różnić się od mojej o jeden lub dwa punkty: standardowy obraz Ubuntu zawiera ufw oraz unattended-upgrades, a usługa fail2ban uruchamia się automatycznie podczas instalacji przez apt, więc zadanie może poprawnie zaraportować ok przy pierwszym uruchomieniu — jako stan już osiągnięty. Wartościami, które muszą wynosić 0, są unreachable oraz failed. Uwaga dotycząca become: true: jest to formalność podczas połączenia jako root, ale po zmianie ansible_user na deploy usługa sudo staje się aktywna — plik sudoers z parametrem NOPASSWD, który instaluje ten playbook, zapobiega wyświetlaniu -K w linii komend. Bez tego wystąpi błąd Missing sudo password, opisany poniżej.

Step 7: uruchomienie dwukrotne — przykład idempotencji

Należy natychmiast ponownie uruchomić tę samą komendę:

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

Wartości changed=0 oraz ok zmniejszyły się o jeden, ponieważ niepowiadomiony handler nie został wywołany. Żaden pakiet nie został przeinstalowany, usługa sshd nie została zrestartowana, a konfiguracja ufw nie została zmieniona. Dzięki temu playbook pełni funkcję zarówno narzędzia do provisioningu, jak i audytu: po dodaniu web3 do inventory w przyszłym miesiącu i ponownym uruchomieniu, nowa maszyna zostanie skonfigurowana, a istniejące zostaną zweryfikowane. Niezerowa wartość changed na maszynie, która nie była modyfikowana, oznacza dryft konfiguracji. Informuje to o ręcznej zmianie ustawień, które powinny zostać zmienione w playbooku.

W tym miejscu wzorzec zaczyna się kumulować. Kolejny przydatny playbook może instalować WireGuard VPN na tym samym VPS i zaostrzać regułę ufw, aby SSH odpowiadał wyłącznie przez tunel; następnie można stworzyć playbook instalujący Docker i Compose na każdym serwerze aplikacji. Dopiero gdy site.yml zajmuje więcej niż trzy ekrany, należy podzielić go na role.

Tryby awarii oraz napisy, które zostaną wyświetlone

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
}

Transport SSH zakończył się niepowodzeniem przed uruchomieniem jakiegokolwiek modułu: ansible_user jest błędne, klucz nie został skopiowany na dany host lub przesyłany jest niewłaściwy klucz. Aby odtworzyć błąd, użyj polecenia ssh root@10.0.0.10, a następnie ssh -v, aby sprawdzić przesyłane klucze. Jeśli logowanie przez SSH za pomocą hasła działa, a Ansible nie, oznacza to pominięcie ssh-copy-id.

Missing sudo password.

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

Ustawiono become: true i nawiązano połączenie jako użytkownik bez uprawnień roota, a użytkownik ten wymaga hasła do sudo. Należy dodać -K (--ask-become-pass) do linii poleceń lub nadać użytkownikowi wpis NOPASSWD w pliku sudoers — właśnie dlatego playbook instaluje go dla deploy przed przełączeniem się na tego użytkownika.

error: externally-managed-environment. Uruchomiono pip względem systemowego Pythona w Ubuntu 24.04. Omówiono to w kroku 1: należy używać pipx, a nie pip, oraz nie należy używać --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

Przyczyną jest prawie zawsze błędne wcięcie: klucz znajduje się na niewłaściwym poziomie lub brakuje spacji po dwukropku. Podany numer linii wskazuje na obszar w pobliżu błędu, a nie bezpośrednio na niego — należy sprawdzić również linię powyżej. Podobny błąd found character '\t' that cannot start any token oznacza użycie tabulatora; format YAML ich zabrania. Należy traktować ansible-playbook site.yml --syntax-check jako nawyk przed każdym uruchomieniem i ustawić edytor na dwu-spazowe wcięcia dla plików YAML.

/usr/bin/python3: not found. Rzadkie w standardowych obrazach Ubuntu 24.04, częste w obrazach minimalnych lub netboot: wykonanie modułu kończy się niepowodzeniem, ponieważ docelowy system nie posiada Pythona. Należy zainstalować Pythona za pomocą modułu raw, który jako jedyny nie wymaga niczego po stronie docelowej: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, a następnie ponownie uruchomić playbook.

FAQ

Czy muszę instalować Ansible na zarządzanych serwerach?

Nie. Ansible jest bezagentowy: maszyna sterująca przesyła małe moduły Python przez SSH, wykonuje je, a następnie usuwa. Serwer docelowy wymaga jedynie python3 oraz dostępu SSH, które są standardem w obrazach Ubuntu. Jedyną instalacją w całym tym poradniku jest ta na maszynie sterującej.

Dlaczego Ansible wyświetla błąd "Permission denied (publickey)"?

Blok UNREACHABLE! z komunikatem Permission denied (publickey) oznacza, że uwierzytelnianie SSH nie powiodło się przed uruchomieniem jakichkolwiek operacji przez Ansible. Należy sprawdzić, czy ansible_user w pliku inventory zgadza się z utworzonym kontem, czy wykonano ssh-copy-id do tego hosta oraz czy zwykłe polecenie ssh user@host loguje się bez hasła. Rozwiązanie problemu z poleceniem ssh rozwiązuje problem z Ansible, ponieważ oba korzystają z tego samego protokołu transportowego.

Co oznacza termin idempotentność w Ansible?

Zadanie definiuje pożądany stan — np. "ten pakiet jest zainstalowany" lub "ta linia znajduje się w tym pliku" — zamiast konkretnej akcji do wykonania. Jeśli stan jest już osiągnięty, Ansible nie wykonuje żadnych działań i zamiast changed raportuje ok. Dlatego ponowne uruchomienie playbooka skutkuje komunikatem changed=0 za drugim razem, a ponowna instalacja jest bezpiecznym audytem, a nie ryzykowną reinstalacją.

Czy do instalacji Ansible na Ubuntu 24.04 należy użyć pip czy pipx?

pipx. Ubuntu 24.04 oznacza systemowy Python jako zarządzany zewnętrznie, przez co pip install ansible kończy się błędem error: externally-managed-environment zgodnie z założeniami systemu. pipx install --include-deps ansible umieszcza Ansible w izolowanym środowisku virtualenv i poprawnie udostępnia ansible, ansible-playbook oraz pozostałe elementy w zmiennej PATH.

Jaka jest różnica między pakietami ansible a ansible-core?

ansible-core to silnik oraz wyłącznie moduły ansible.builtin. Pakiet ansible zawiera rdzeń wraz z wyselekcjonowanymi kolekcjami społeczności — w tym ansible.posix (moduł authorized_key) oraz community.general (moduł ufw), które są używane w tym poradniku. Zaleca się rozpoczęcie od pełnego pakietu; przejście do wersji core wraz z wybranymi kolekcjami należy rozważyć tylko w uzasadnionych przypadkach.