Ansible: pierwszy playbook dla VPS na Ubuntu 24.04
Instalacja Ansible przez pipx, inventory i playbook do zabezpieczenia VPS z Ubuntu 24.04. Uwzględniono błędy Permission denied oraz sudo i bezpieczną idempotencję.
Co jest tworzone
Jedna maszyna sterująca z zainstalowanym Ansible oraz co najmniej jeden nowy VPS z Ubuntu 24.04, zawierający wyłącznie standardowy obraz systemu. Na koniec będzie dostępny plik inventory z nazwami serwerów, polecenie ad-hoc ping potwierdzające poprawne uwierzytelnianie od początku do końca oraz playbook, który jako kod realizuje pełną listę czynności dla nowego VPS: tworzy użytkownika deploy z kluczem SSH, wzmacnia konfigurację sshd, konfiguruje fail2ban i unattended upgrades oraz ustawia zaporę sieciową, która najpierw zezwala na OpenSSH, a następnie odrzuca cały pozostały ruch. Można wskazać jeden serwer lub dwadzieścia. Drugie uruchomienie nie wprowadzi żadnych zmian. Na tym polega całe rozwiązanie.
Po piętnastu latach konfigurowania VPS można wskazać powtarzający się schemat: każdy konfiguruje ręcznie pierwszych pięć serwerów, a następnie traci cały weekend na szóstym, ponieważ nikt nie pamięta, co zostało wykonane na pierwszych pięciu. Ten przewodnik rozwija omówienie zarządzania wieloma serwerami Linux. Warto po niego sięgnąć, gdy pojawi się potrzeba wpisywania tego samego apt install do trzech terminali.
Czym właściwie jest Ansible — w jednym akapicie
Ansible nie wymaga agentów. Na zarządzanych serwerach nie trzeba instalować żadnego demona. Maszyna sterująca łączy się przez standardowy SSH, kopiuje mały moduł Python do systemu docelowego, wykonuje go, odczytuje wypisywany przez niego kod JSON i usuwa moduł. Jedynym wymaganiem po stronie systemu docelowego jest python3, który znajduje się już w każdym standardowym obrazie Ubuntu. Najważniejsze pojęcie to idempotentność. Oznacza ono prostą zasadę: zadanie opisuje stan, a nie działanie. state: present dla pakietu oznacza „upewnij się, że pakiet jest zainstalowany”, a nie „uruchom instalator”. Jeśli stan jest już osiągnięty, Ansible niczego nie zmienia i zgłasza ok zamiast changed. Ta właściwość stanowi istotę produktu. Dzięki niej ponowne uruchomienie playbooka jest bezpieczne. Bezpieczne ponowne uruchamianie skryptów powłoki pozwala przekształcić je w kod infrastruktury.
Wymagania wstępne i najważniejsze problemy
- Maszyna sterująca: laptop lub mały VPS. Przyjęto Ubuntu 24.04; system macOS działa identycznie po zainstalowaniu pipx za pomocą Homebrew.
- Co najmniej jeden docelowy VPS z systemem Ubuntu 24.04 na KVM, dostępny jako root. Na tych maszynach nic nie zostanie zainstalowane.
- Uwierzytelnianie za pomocą klucza SSH na każdym serwerze docelowym. Ansible korzysta dokładnie z tego samego uwierzytelniania co polecenie
ssh. Jeślissh root@hostwyświetla monit o hasło, Ansible zakończy działanie błędem. - W systemie Ubuntu 24.04 polecenie
pip install ansiblekończy działanie błędemerror: externally-managed-environment. Jest to zamierzone działanie wynikające z zasad dystrybucji, a nie usterka. Należy użyć pipx. - Białe znaki w YAML są elementem składni. Nieprawidłowe wcięcie powoduje błąd
mapping values are not allowed in this context, a użycie znaku tabulacji w dowolnym miejscu kończy się błędem. - Podczas wzmacniania konfiguracji sshd przez playbook należy pozostawić otwartą działającą sesję SSH na każdym serwerze docelowym. Każdy przypadek utraty dostępu, przy którego przywracaniu pomagałem klientowi, obejmował zamknięcie ostatniej sesji „w celu sprawdzenia z czystego środowiska”.
Krok 1: zainstalowanie Ansible na maszynie sterującej za pomocą pipx, a nie pip
Pierwszym odruchem jest pip3 install ansible. Na całkowicie nowym obrazie 24.04 polecenie to kończy się niepowodzeniem o jeden krok wcześniej, Command 'pip3' not found, but can be installed with: sudo apt install python3-pip, a zainstalowanie pip prowadzi tylko do rzeczywistej przeszkody:
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 oznacza systemowy Python jako zarządzany zewnętrznie (PEP 668), dlatego pip nie może konkurować z apt o te same pliki. Nie należy używać --break-system-packages; nazwa tej flagi dokładnie opisuje jej działanie. Właściwym rozwiązaniem jest pipx, który tworzy dla Ansible własne odizolowane środowisko wirtualne i umieszcza pliki wykonywalne w PATH:
sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansiblePo wykonaniu pipx ensurepath należy otworzyć nową powłokę, aby zmiana PATH zaczęła obowiązywać. --include-deps nie jest ozdobnikiem: pakiet ansible nie dostarcza własnych skryptów konsolowych, ansible, ansible-playbook i pozostałe elementy są punktami wejścia jego zależności ansible-core, dlatego bez tej flagi pipx odmawia instalacji z komunikatem No apps associated with package ansible or its dependencies. Należy zainstalować pakiet ansible, a nie samo ansible-core. Pełny pakiet zawiera kolekcje społecznościowe, a ten playbook korzysta z modułów z dwóch z nich (ansible.posix i community.general).
ansible --versionPrawidłowy wynik zaczyna się od wiersza podobnego do ansible [core 2.19.x] i wskazuje wersję języka Python, w której program jest uruchamiany; dowolna aktualna wersja core jest odpowiednia do wszystkich opisanych tutaj czynności. ansible: command not found oznacza natomiast, że ~/.local/bin nie znajduje się jeszcze w PATH; należy otworzyć nową powłokę albo wykonać source ~/.bashrc.
To cała instalacja. Na komputerach docelowych nie instaluje się niczego.
Krok 2: Dostęp do każdego celu za pomocą klucza SSH
ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20Następnie należy to sprawdzić raz dla każdego hosta:
ssh root@10.0.0.10 true && echo okTen jeden wiersz wykonuje dwa zadania: potwierdza, że uwierzytelnianie za pomocą klucza działa bez hasła, oraz zapisuje klucz hosta w known_hosts. Należy zrobić to teraz, ponieważ Ansible wyświetla niezapisany klucz hosta jako interaktywny monit ukryty w środku wykonywania, co wygląda dokładnie jak zawieszenie.
Krok 3: plik inventory — najpierw INI, YAML po zwiększeniu złożoności
Plik inventory zawiera listę maszyn, z którymi Ansible może wykonywać operacje. Utwórz 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=rootweb1 to wybrany alias. Jest wyświetlany w danych wyjściowych i służy do wskazywania celu za pomocą --limit web1. ansible_host to rzeczywisty adres. [vps] to grupa, a [vps:vars] ustawia zmienne dla każdego należącego do niej hosta; ansible_user określa konto, za pomocą którego Ansible się loguje. Obok tego dodaj ansible.cfg, aby nie wpisywać ponownie -i:
[defaults]
inventory = inventory.iniAnsible odczytuje ansible.cfg z bieżącego katalogu. Ten sam plik inventory w formacie YAML należy zapisać jako inventory.yml i zamiast tego wskazać tę nazwę za pomocą ansible.cfg. Taki format będzie preferowany, gdy każdy host będzie miał kilka zmiennych:
vps:
hosts:
web1:
ansible_host: 10.0.0.10
web2:
ansible_host: 10.0.0.20
vars:
ansible_user: rootOba formaty są równoważne. INI łatwiej przejrzeć przy dwóch serwerach, natomiast YAML lepiej skaluje się przy dwudziestu. Należy wybrać jeden format i nie poświęcać temu więcej uwagi.
Krok 4: polecenia ad-hoc — zielony wynik pong potwierdzający poprawne działanie
ansible all -m pingNie jest to ICMP. Moduł ping wykonuje pełny test: logowanie przez SSH, kopiowanie modułu, wykonanie kodu Python na hoście docelowym oraz czyszczenie. Prawidłowy wynik jest zielony i zawiera jeden blok dla każdego hosta:
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}Zielony wynik SUCCESS oznacza, że uwierzytelnianie, interpreter Python oraz transport działają poprawnie. Playbook również zadziała. Czerwony wynik UNREACHABLE! oznacza, że transport zakończył się niepowodzeniem, zanim uruchomiono jakikolwiek moduł. Dokładny komunikat i sposób naprawy opisano w poniższej sekcji dotyczącej trybów awarii. Warto znać jeszcze dwa polecenia ad-hoc:
ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --becomePolecenia ad-hoc służą do jednorazowych operacji i kontroli. Wszystko, co ma być uruchamiane dwukrotnie, powinno trafić do playbooka.
Krok 5: pierwszy playbook, lista kontrolna nowego VPS zapisana jako kod
To wszystko, co należałoby wykonać ręcznie w ciągu pierwszych dziesięciu minut na nowym serwerze. Należy zapisać to 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: restartedWarto zrozumieć poniższe wiersze, zamiast kopiować je bez analizy:
Zmienne znajdują się w vars: i są odwoływane za pomocą "{{ deploy_user }}". Całe wyrażenie należy ująć w cudzysłowy, gdy wartość zaczyna się od nawiasu klamrowego. W przeciwnym razie parser YAML może odczytać je nieprawidłowo. lookup('file', ...) odczytuje klucz publiczny z maszyny sterującej podczas działania, dlatego playbook nie zawiera materiału klucza.
Pętla. loop: "{{ baseline_services }}" wykonuje zadanie usługi raz dla każdego elementu, a dane wyjściowe pokazują każdy element w osobnym wierszu. Należy zwrócić uwagę, że zadanie apt przekazuje całą listę pakietów jednocześnie. Jedna transakcja apt jest szybsza i stanowi zalecany wzorzec dla pakietów. Pętle służą modułom, które rzeczywiście wykonują operację tylko na jednym elemencie naraz.
Handler to pojęcie, które należy dobrze przyswoić. notify: Restart ssh nie oznacza „uruchom teraz ponownie ssh”. Zamiast tego dodaje handler do kolejki. Handler zostaje uruchomiony raz na końcu odtwarzania i tylko wtedy, gdy zadanie wywołujące rzeczywiście zgłosiło changed. Po ponownym uruchomieniu playbooka następnego dnia plik drop-in jest już prawidłowy, zadanie kopiowania zgłasza ok, a sshd nie jest ponownie uruchamiany. Wiersz validate: zapewnia bezpieczeństwo wyzwalacza. sshd sprawdza plik przed zastąpieniem starego pliku, dlatego literówka powoduje niepowodzenie zadania zamiast awarii demona.
PermitRootLogin prohibit-password, nie no, celowo. Ten playbook loguje się jako root za pomocą klucza. prohibit-password wyłącza logowanie root za pomocą hasła, pozostawiając aktywne bieżące logowanie. Po potwierdzeniu działania użytkownika wdrożeniowego (ssh deploy@10.0.0.10 sudo true, zwykły adres, ponieważ web1 jest aliasem znanym tylko Ansible) należy zmienić ansible_user=deploy w inventory i zaostrzyć ustawienie do no podczas późniejszego uruchomienia. Zabezpieczenia należy wprowadzać w kolejności, która nie doprowadzi do utraty dostępu.
Prefiks 00- ma znaczenie. W przypadku większości słów kluczowych sshd uwzględnia pierwsze wystąpienie odczytane podczas analizy konfiguracji, a Ubuntu's sshd_config dołącza sshd_config.d/*.conf w kolejności leksykalnej przed własną treścią. Obrazy chmurowe Ubuntu 24.04 zawierają już plik 60-cloudimg-settings.conf w tym katalogu, a dostawcy włączający logowanie za pomocą hasła przez cloud-init dodają plik 50-cloud-init.conf z ustawieniem PasswordAuthentication yes. Nadanie naszemu plikowi nazwy 00-hardening.conf sprawia, że jest sortowany jako pierwszy i ma pierwszeństwo przed oboma plikami.
Kolejność zadań zapewnia bezpieczeństwo zapory. Allow OpenSSH jest wykonywane przed Enable ufw z polityką deny. Ansible wykonuje zadania ściśle w podanej kolejności, dlatego luka istnieje, zanim zostanie włączona blokada. fail2ban nie wymaga tutaj konfiguracji, aby był użyteczny. Jego ustawienia domyślne Ubuntu monitorują sshd od razu po instalacji. Opis działania jaili i ustawień, które można dostosować, znajduje się w przewodniku dotyczącym fail2ban na Ubuntu 24.04.
Krok 6: uruchomienie testowe z opcją --check, a następnie właściwe wykonanie
ansible-playbook site.yml --checkTryb sprawdzania nawiązuje połączenie i oblicza działania, które zostałyby wykonane, ale niczego nie zmienia. Odczytaj liczbę changed= w PLAY RECAP na dole. Jest to liczba zadań, które zmodyfikowałyby każdy host. Należy uwzględnić jedno ograniczenie: tryb sprawdzania ma ograniczenia strukturalne, gdy późniejsze zadanie zależy od zmian wprowadzonych przez wcześniejsze zadanie. Standardowy obraz serwera Ubuntu zawiera już ufw, dlatego ten playbook przechodzi uruchomienie testowe bez błędów. Jednak na minimalnym obrazie, który go nie zawiera, zadania ufw kończą się błędem w trybie sprawdzania, ponieważ ten tryb nigdy faktycznie nie zainstalował pakietu, a moduł nie ma wtedy niczego, co mógłby wywołać. Jest to ograniczenie uruchomień testowych, a nie błąd w playbooku. Gdy plan wygląda poprawnie:
ansible-playbook site.ymlKażde zadanie wyświetla wiersz dla każdego hosta: żół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=0Dziesięć ok obejmuje zbieranie faktów, osiem zadań i handler. Wartość changed może różnić się od podanej tutaj o jeden lub dwa: standardowy obraz Ubuntu zawiera już ufw i unattended-upgrades, a fail2ban uruchamia się natychmiast po zainstalowaniu przez apt. Dlatego zadanie może prawidłowo zgłosić ok już przy pierwszym uruchomieniu, ponieważ deklarowany stan jest już ustawiony. Wartości, które muszą wynosić zero, to unreachable i failed. Uwaga dotycząca become: true: podczas łączenia jako root jest to formalność, ale po zmianie ansible_user na deploy sudo zaczyna faktycznie obowiązywać, a instalowany przez ten playbook plik sudoers z NOPASSWD dokładnie zapobiega pojawianiu się -K w wierszu poleceń. Bez niego pojawi się Missing sudo password, co opisano poniżej.
7. krok: uruchomienie po raz drugi — jak wygląda idempotentność
Natychmiast uruchom ponownie to samo polecenie:
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 i ok zmniejszyły się o jeden, ponieważ niepowiadomiony handler nie został uruchomiony. Niczego nie zainstalowano ponownie, sshd nie został zrestartowany, a ufw nie został zmodyfikowany. Dzięki temu playbook jest zarówno narzędziem audytowym, jak i narzędziem do wdrażania: w przyszłym miesiącu dodaj web3 do inventory i uruchom playbook ponownie. Nowy serwer zostanie skonfigurowany, a stare serwery zostaną zweryfikowane. Niezerowa wartość changed na serwerze, którego nie modyfikowano, oznacza rozbieżność konfiguracji. Wskazuje to, że ktoś ręcznie zmienił ustawienie, które powinno zostać zmienione w playbooku.
Od tego momentu schemat można rozszerzać. Warto przygotować następny playbook, który skonfiguruje VPN WireGuard na tym samym VPS i zaostrzy regułę ufw, aby SSH odpowiadał tylko przez tunel. Następnie można przygotować playbook instalujący Docker i Compose na każdym serwerze aplikacyjnym. Gdy site.yml przekroczy trzy ekrany, należy podzielić go na role, ale nie wcześniej.
Tryby awarii wraz z komunikatami wyświetlanymi w takich sytuacjach
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, zanim uruchomiono jakikolwiek moduł: ansible_user jest nieprawidłowe, klucz nie został skopiowany na ten host albo używany jest niewłaściwy klucz. Należy odtworzyć problem za pomocą zwykłego ssh root@10.0.0.10, a następnie użyć ssh -v, aby sprawdzić, które klucze zostały zaoferowane. Jeśli logowanie SSH za pomocą hasła działa, ale Ansible nie działa, pominięto ssh-copy-id.
Brak hasła sudo.
web1 | FAILED! => {
"msg": "Missing sudo password"
}Ustawiono become: true, nawiązano połączenie jako użytkownik inny niż root, a ten użytkownik musi podać hasło do sudo. Należy dodać -K (--ask-become-pass) do wiersza poleceń albo nadać użytkownikowi wpis NOPASSWD w sudoers. Właśnie dlatego playbook instaluje taki wpis dla deploy, zanim nastąpi przełączenie na to konto.
error: externally-managed-environment. Uruchomiono pip względem systemowego środowiska Python w Ubuntu 24.04. Zostało to omówione w kroku 1: pipx, a nie pip ani --break-system-packages.
mapping values are not allowed in this context.
ERROR! Syntax Error while loading YAML.
mapping values are not allowed in this contextPrawie zawsze przyczyną jest nieprawidłowe wcięcie: klucz znajduje się na niewłaściwym poziomie albo po dwukropku brakuje spacji. Zgłoszony numer wiersza wskazuje miejsce w pobliżu błędu, a niekoniecznie sam błąd; należy sprawdzić również poprzedni wiersz. Pokrewny komunikat found character '\t' that cannot start any token oznacza, że w tekście pojawił się znak tabulacji. YAML ich nie dopuszcza. Przed każdym uruchomieniem należy odruchowo wykonać ansible-playbook site.yml --syntax-check oraz ustawić w edytorze wcięcia o szerokości dwóch spacji dla YAML.
/usr/bin/python3: not found. Rzadko występuje to na standardowych obrazach Ubuntu 24.04, ale często na obrazach minimalnych lub netboot. Wykonanie modułu kończy się niepowodzeniem, ponieważ na hoście docelowym nie ma Python. Należy zainstalować go za pomocą modułu raw, jedynego modułu, który nie wymaga niczego po stronie zdalnej: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, a następnie ponownie uruchomić playbook.
FAQ
Czy należy zainstalować Ansible na serwerach, którymi zarządza?
Nie. Ansible nie wymaga agenta: komputer sterujący przesyła przez SSH niewielkie moduły Python, uruchamia je, a następnie usuwa. Host docelowy potrzebuje tylko python3 i dostępu SSH. Oba te elementy są już dostępne w standardowych obrazach Ubuntu. Jedyna instalacja w całym tym przewodniku odbywa się na komputerze sterującym.
Dlaczego Ansible zgłasza „Permission denied (publickey)”?
Blok UNREACHABLE! z Permission denied (publickey) oznacza, że uwierzytelnianie SSH nie powiodło się, zanim Ansible cokolwiek uruchomił. Należy sprawdzić, czy ansible_user w spisie zasobów odpowiada faktycznie skonfigurowanemu kontu, czy wykonano ssh-copy-id dla tego hosta oraz czy zwykłe ssh user@host loguje się bez hasła. Usunięcie problemu ze zwykłym poleceniem ssh usuwa również problem z Ansible, ponieważ oba używają tego samego transportu.
Co oznacza idempotentność w Ansible?
Zadanie deklaruje stan docelowy, na przykład „ten pakiet jest zainstalowany” lub „ten wiersz znajduje się w tym pliku”, zamiast deklarować czynność do wykonania. Jeśli stan jest już zgodny z deklaracją, Ansible nie wykonuje żadnych zmian i zgłasza ok zamiast changed. Dlatego przy drugim uruchomieniu playbooka pojawia się changed=0, a ponowne uruchomienie jest bezpiecznym sprawdzeniem, a nie ryzykowną ponowną instalacją.
Czy w celu zainstalowania Ansible na Ubuntu 24.04 należy użyć pip czy pipx?
pipx. Ubuntu 24.04 oznacza systemowy Python jako zarządzany zewnętrznie, dlatego pip install ansible kończy się błędem error: externally-managed-environment. pipx install --include-deps ansible umieszcza Ansible w odizolowanym środowisku virtualenv i prawidłowo udostępnia ansible, ansible-playbook oraz pozostałe polecenia w zmiennej PATH.
Jaka jest różnica między pakietami ansible i ansible-core?
ansible-core to silnik zawierający tylko moduły ansible.builtin. Pakiet ansible zawiera rdzeń oraz wybrane kolekcje społeczności, w tym ansible.posix (moduł authorized_key) i community.general (moduł ufw), używane w tym przewodniku. Na początku należy użyć pełnego pakietu. Do ansible-core i ręcznie wybranych kolekcji należy przejść tylko wtedy, gdy istnieje ku temu uzasadnienie.