Ansible: playbook czy rola? Kiedy użyć każdego
Sprawdź, kiedy wystarczy płaski playbook, a kiedy potrzebna jest rola: struktura katalogów, ansible-galaxy init, wywołania ról i pierwszeństwo zmiennych.
Ansible: playbook a rola — jaka jest różnica
Ansible playbook to plik uruchamiany za pomocą ansible-playbook. Przypisuje grupę hostów do zadań, które należy na nich wykonać. Rola Ansible to katalog o ustalonej strukturze, zawierający zadania, szablony, handlery i zmienne domyślne. Playbook wywołuje rolę według nazwy. Składnia zadań jest w obu przypadkach identyczna, więc nie chodzi o zakres możliwych operacji. Chodzi o możliwość ponownego użycia.
Należy zacząć od płaskiego playbooka. Jeden plik site.yml zawierający listę tasks: to właściwa struktura dla pierwszej automatyzacji. Taka struktura pozostaje właściwa dłużej, niż zwykle się oczekuje. Na rolę należy przejść, gdy ten sam blok zadań musi zostać uruchomiony dla drugiej grupy hostów albo gdy plik przekroczy około 100 wierszy i nie można już znaleźć zadania przez przewijanie.
Jeśli nie utworzono jeszcze playbooka, zacznij od pierwszego playbooka dla pojedynczego VPS i wróć do tego tematu, gdy zacznie się rozrastać.
Kiedy płaski playbook jest właściwym rozwiązaniem
Płaski playbook jest właściwym rozwiązaniem, gdy zadanie jest wykonywane raz, na jednym hoście albo nikt inny nie będzie go czytać. Inicjowanie pojedynczego serwera aplikacji lub instalowanie poprawek na serwerze przed oknem serwisowym nie uzasadnia tworzenia drzewa katalogów. Rola dodaje siedem katalogów i jedną warstwę pośrednią. Jeśli jedynym elementem wywołującym jest playbook znajdujący się obok, ta warstwa niczego nie zapewnia, a za każdym razem, gdy trzeba sprawdzić rzeczywiście wykonywane zadania, wymaga dodatkowego przejścia.
Płaski playbook przestaje być właściwym rozwiązaniem w określonym momencie. Łatwo go rozpoznać. Blok zadań zostaje skopiowany do drugiego playbooka. Jest to sygnał, że od tej chwili każdą poprawkę trzeba wprowadzać dwukrotnie, a pewnego dnia zostanie ona wprowadzona tylko raz.
Co faktycznie zawiera katalog roli
roles/common/
defaults/main.yml
vars/main.yml
tasks/main.yml
handlers/main.yml
templates/99-hardening.conf.j2
files/
meta/main.ymltasks/main.ymljest punktem wejścia. Ansible uruchamia ten plik, gdy rola jest wywoływana. Wszystkie pozostałe katalogi są opcjonalne.defaults/main.ymlzawiera zmienne, które wywołujący powinien nadpisać. Jest to źródło o najniższym priorytecie w Ansible, dlatego niemal każde inne źródło ma pierwszeństwo.vars/main.ymlzawiera zmienne, których wywołujący nie powinien nadpisywać. Mają wyższy priorytet niż inventory. To zdecydowane założenie. Należy używać tego katalogu rzadko.handlers/main.ymlzawiera zadania wywoływane przeznotify. Handler jest uruchamiany na końcu play, jeden raz, niezależnie od liczby zadań, które go powiadomiły.files/zawiera pliki kopiowane bez zmian przez modułcopy, atemplates/zawiera szablony Jinja2 przetwarzane przez modułtemplate. W obrębie roli do obu odwołuje się za pomocą samej nazwy pliku, bez ścieżki, ponieważ Ansible w pierwszej kolejności przeszukuje własne katalogi roli.meta/main.ymldeklaruje zależności roli oraz metadane odczytywane przez Ansible Galaxy.
Układ nie jest kwestią preferencji dotyczących stylu. Ansible szuka plików dokładnie w tych ścieżkach, dlatego szablon umieszczony w roles/common/template/ (liczba pojedyncza) po prostu nie zostanie znaleziony.
Utworzenie wspólnej roli za pomocą ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonPolecenie tworzy cały szkielet w katalogu roles/common, w tym katalogi, które nie będą używane, oraz puste pliki main.yml zawierające tylko ---. Należy usunąć te elementy, które pozostaną puste. Pusty katalog vars/main.yml nie powoduje problemów w Ansible, ale utrudnia ustalenie, które pliki w roli mają znaczenie.
Następnie należy uzupełnić pliki wykonujące właściwe działania. Najpierw należy skonfigurować wartości domyślne, ponieważ stanowią one publiczny interfejs roli.
# roles/common/defaults/main.yml
---
common_packages:
- ufw
- fail2ban
- unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"Należy ująć "no" i "yes" w cudzysłowy. Ansible analizuje YAML za pomocą PyYAML, który interpretuje niecytowany zapis no jako wartość logiczną false. W rezultacie wygenerowany wiersz konfiguracji ma postać PermitRootLogin False, a sshd go odrzuca. Cudzysłowy zachowują tę wartość jako ciąg znaków.
# roles/common/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Create the admin group
ansible.builtin.group:
name: "{{ common_admin_group }}"
state: present
- name: Install the sshd hardening drop-in
ansible.builtin.template:
src: 99-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/99-hardening.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd# roles/common/handlers/main.yml
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}W systemach Debian i Ubuntu jednostka systemd nosi nazwę ssh, a w systemach z rodziny RHEL — sshd. Handler z nieprawidłową nazwą jednostki kończy się błędem dopiero wtedy, gdy coś rzeczywiście zmieni szablon. Z tego powodu problem zwykle ujawnia się dopiero po kilku tygodniach.
Wiersz validate jest najważniejszym elementem tego zadania. Ansible renderuje szablon do pliku tymczasowego, podstawia ścieżkę tego pliku w miejsce %s i uruchamia polecenie. Plik docelowy zostaje zastąpiony tylko wtedy, gdy polecenie zakończy się kodem 0. Należy dodać do szablonu nieprawidłową dyrektywę i uruchomić zadanie ponownie. Zadanie zakończy się błędem z komunikatem failed to validate, rzeczywisty plik /etc/ssh/sshd_config.d/99-hardening.conf pozostanie niezmieniony, a dostęp do serwera nadal będzie możliwy. Należy pamiętać, że sprawdzenie obejmuje więcej niż poprawność składni. Jeśli sshd -t nie może odczytać kluczy hosta, kończy działanie z kodem sshd: no hostkeys available -- exiting., a Ansible zgłasza ten sam failed to validate. Przed uznaniem szablonu za przyczynę problemu należy zapoznać się z sekcją msg dokumentacji modułu.
Jak playbook wywołuje rolę
# site.yml
---
- name: Base configuration for every server
hosts: all
become: true
roles:
- common# inventory.ini
[local]
localhost ansible_connection=localansible-playbook -i inventory.ini site.ymlPlay powinien kończyć się wpisem failed=0 w sekcji podsumowania. Parametry należy przekazywać w miejscu wywołania w rozwiniętej formie. Dzięki temu jedna rola może obsługiwać dwie grupy hostów:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordIstnieje jedna reguła kolejności, która zaskakuje prawie wszystkich. Play może zawierać pre_tasks, roles, tasks i post_tasks, a Ansible uruchomi je w tej kolejności, niezależnie od kolejności zapisania w pliku. Umieszczenie tasks: przed roles: nie zmieni faktu, że role zostaną uruchomione najpierw. Jeśli coś musi nastąpić przed rolą, należy umieścić to w pre_tasks:, a nie na początku tasks:.
- name: Ordering demonstration
hosts: local
gather_facts: false
pre_tasks:
- name: Runs first
ansible.builtin.debug:
msg: pre
roles:
- common
tasks:
- name: Runs after the role
ansible.builtin.debug:
msg: task
post_tasks:
- name: Runs last
ansible.builtin.debug:
msg: postAby wywołać rolę z listy zadań zamiast użycia klucza roles:, należy użyć import_role lub include_role.
tasks:
- name: Static, read when the playbook is parsed
ansible.builtin.import_role:
name: common
- name: Dynamic, resolved when the task runs
ansible.builtin.include_role:
name: postgres
when: "'db' in group_names"import_role jest statyczne. Ansible odczytuje rolę podczas analizy składni, a jej zadania stają się częścią play, dlatego ansible-playbook --list-tasks site.yml wyświetla te zadania, a znacznik przypisany do importu ma zastosowanie do każdego zadania wewnątrz roli. include_role jest dynamiczne. Odczyt następuje dopiero podczas wykonywania zadania. Umożliwia to określenie nazwy roli na podstawie zmiennej lub pętli. Wadą jest to, że te zadania nie są widoczne dla --list-tasks ani dla --start-at-task.
W tym miejscu występuje jedna pułapka. when: w zadaniu include_role jest oceniane, zanim defaults/main.yml dołączonej roli znajdzie się w zakresie. Umieszczenie when: common_packages | length > 0 w wywołaniu kończy wykonanie błędem 'common_packages' is undefined, mimo że ta zmienna jest zdefiniowana w dołączanej roli. Należy przenieść przełącznik poza rolę: umieścić go w group_vars/all.yml, gdzie będzie dostępny w każdym miejscu, a wartości używane przez samą rolę pozostawić w jej wartościach domyślnych.
Która zmienna ma pierwszeństwo: defaults, group_vars, vars, extra vars
Ansible opisuje ponad dwadzieścia poziomów pierwszeństwa zmiennych. Cztery z nich rozstrzygają niemal każdy rzeczywisty spór. Poniżej przedstawiono je od najsłabszego do najsilniejszego.
roles/<name>/defaults/main.ymlznajduje się niemal na dole. Prawie wszystko ustawione w innym miejscu ma przed nim pierwszeństwo. Dlatego jest właściwym miejscem na parametry konfiguracyjne roli.group_vars/ihost_vars/znajdują się pośrodku. Tu należy umieszczać ustawienia właściwe dla danego środowiska. Wartości te prawidłowo zastępują wartości domyślne roli.roles/<name>/vars/main.ymlma pierwszeństwo przedhost_vars. Wartość umieszczonej tu zmiennej nie może zostać zastąpiona z poziomu inventory. Należy zachować to miejsce dla wartości, które muszą pozostać wewnętrznie spójne w roli, na przykład nazwy pakietu, która musi odpowiadać nazwie usługi.- Parametr roli przekazany w miejscu jej wywołania ma pierwszeństwo przed
vars/main.yml, a-eprzekazane w wierszu poleceń ma pierwszeństwo przed wszystkim, w tym przed parametrami roli.
Można to sprawdzić w około minutę. Należy utworzyć niewielką rolę z jedną wartością domyślną i jedną zmienną roli, a następnie ustawić te same nazwy w host_vars.
# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults# roles/prec/vars/main.yml
---
prec_internal: from-rolevars# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars# roles/prec/tasks/main.yml
---
- name: Show which value survived
ansible.builtin.debug:
msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cliPierwsze uruchomienie wyświetla tunable=from-hostvars internal=from-rolevars. Inventory ma pierwszeństwo przed wartością domyślną roli, ale ustępuje zmiennej roli. Drugie uruchomienie wyświetla internal=from-cli, ponieważ extra vars znajdują się na samym szczycie hierarchii i żadna niższa warstwa nie może ich zastąpić. Z tego samego powodu -e jest właściwe podczas jednorazowego uruchomienia, ale nie w utrwalanym skrypcie: ma niejawnie pierwszeństwo przed każdą rozważaną decyzją zapisaną w repozytorium.
Praktyczna zasada jest następująca: jeśli wartość ma podlegać konfiguracji, należy umieścić ją w defaults/. Umieszczenie jej w vars/ informuje każdego przyszłego użytkownika roli, że inventory nie może jej zmienić. Czasami jest to zamierzone, ale zazwyczaj wynika z pomyłki.
Sprawdzenie idempotencji roli: uruchomienie jej dwukrotnie
Godne zaufania wykonanie Ansible daje ten sam wynik za drugim razem i zgłasza, że niczego nie zmieniono. Należy uruchomić playbook dwukrotnie i odczytać podsumowanie.
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.ymlDrugie podsumowanie powinno wyglądać następująco:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 oznacza, że każdy moduł sprawdził bieżący stan i stwierdził, że zadanie zostało już wykonane. changed=2 podczas drugiego uruchomienia oznacza, że dwa zadania nie potrafią rozpoznać różnicy, więc będą stale ponownie zapisywać pliki i uruchamiać usługi. Zwykle przyczyną jest command lub shell, ponieważ Ansible nie ma sposobu, aby ustalić, co zrobiło dowolne polecenie.
# traps.yml
---
- name: Command modules do not know what they changed
hosts: local
gather_facts: false
tasks:
- name: This appends a line on every run
ansible.builtin.shell: "echo run >> /tmp/grow.txt"
- name: This appends a line only once
ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
args:
creates: /tmp/guarded.txtNależy uruchomić ten playbook dwukrotnie, a następnie policzyć wiersze zawierające wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt zawiera dwa wiersze, a /tmp/guarded.txt zawiera jeden. Podczas drugiego uruchomienia zadanie z warunkiem ochronnym w ogóle się nie wykonało, a jego wynik zawiera komunikat skipped, since /tmp/guarded.txt exists, ponieważ creates zapewnia modułowi widoczny rezultat, którego może najpierw szukać. Jeśli polecenie nie pozostawia takiego rezultatu, należy zarejestrować jego dane wyjściowe i samodzielnie podjąć decyzję za pomocą changed_when.
ansible-playbook --check --diff site.yml przewiduje zmiany bez ich wprowadzania, a --diff wyświetla dokładne wiersze, które szablon przepisałby ponownie. Dane wyjściowe należy interpretować z uwzględnieniem jednego zastrzeżenia: zadania shell i command są pomijane w trybie check mode, dlatego plan, który wygląda na pozbawiony zmian, może nadal ukrywać działania do wykonania.
Dlaczego Ansible informuje, że rola nie została znaleziona
Ansible szuka katalogu roles/ obok pliku playbooka, a następnie w roles_path. Wyszukiwanie odbywa się względem playbooka, a nie powłoki.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyTen komunikat oznacza, że site.yml i roles/ przestały być ze sobą zgodne. Komunikat zawiera także ścieżki, które zostały sprawdzone. Należy umieścić oba elementy w tym samym katalogu. Uruchomienie polecenia z katalogu nadrzędnego jest prawidłowe, ponieważ decydująca jest ścieżka playbooka:
ansible-playbook -i infra/inventory.ini infra/site.ymlIstnieje także mniej widoczna wersja tego samego problemu. Ansible ignoruje plik ansible.cfg w bieżącym katalogu, jeżeli katalog ten jest zapisywalny dla wszystkich użytkowników. Każdy użytkownik serwera mógłby wówczas umieścić w nim konfigurację i zmienić działanie uruchomienia.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.Ustawienia roles_path i inventory są wtedy niejawnie pomijane, a wyszukiwanie roli kończy się niepowodzeniem z przyczyny niezwiązanej z rolami. Polecenie ansible --version wyświetla config file, które zostało faktycznie wczytane, a polecenie ansible-config dump --only-changed wyświetla każde ustawienie różniące się od wartości domyślnych. Należy sprawdzić oba wyniki zawsze, gdy uruchomienie przebiega tak, jakby konfiguracja nie istniała.
Udostępnianie ról: requirements.yml i przypięta wersja
Rola napisana przez inną osobę jest instalowana, a nie kopiowana. Należy zadeklarować ją raz:
# requirements.yml
---
roles:
- name: postgres
src: https://github.com/example/ansible-role-postgres
scm: git
version: v1.4.0ansible-galaxy install -r requirements.yml -p galaxy_rolesZawsze należy ustawić version. Bez tego używana jest zawartość domyślnej gałęzi dostępna w dniu wykonania polecenia. W efekcie wdrożenie, które działało w zeszłym miesiącu, przestaje działać bez żadnych zmian we własnym repozytorium. roles_path należy wskazać jako katalog pobierania. Katalog ten należy wykluczyć z git:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesRole w roles/, znajdującym się obok playbooka, nadal są wykrywane, ponieważ ta ścieżka jest zawsze przeszukiwana dodatkowo względem roles_path. Dzięki temu własne role pozostają zatwierdzone i poddawane przeglądom, a role innych dostawców są powtarzalnymi pobraniami przypiętymi do znacznika.
Gdy role przestają być właściwym rozwiązaniem
Rola jest jednostką ponownego użycia w ramach jednego uruchomienia Ansible. Nie tworzy serwerów ani rekordów DNS u dostawcy. Próba wykorzystania jej do tego celu prowadzi do powstania playbooków, których nikt nie chce utrzymywać. Przed rozpoczęciem warto przeczytać podział zadań między Ansible i Terraform. Rola nie zastępuje również projektu inventory: po przekroczeniu kilku maszyn sposób grupowania tych serwerów i uzyskiwania do nich dostępu ma większe znaczenie niż sposób uporządkowania zadań.
Zabezpieczenia instalowane przez tę rolę common również wymagają osobnych decyzji. Powyższy plik drop-in ustawia tylko dwie dyrektywy. Przed określeniem, co powinno znaleźć się w roli dla każdego zarządzanego hosta, należy przeczytać które ustawienia SSH rzeczywiście warto zmienić oraz jak skonfigurować Ubuntu, aby samodzielnie stosował aktualizacje zabezpieczeń.
FAQ
Kiedy należy przekształcić playbook Ansible w rolę?
Gdy ten sam blok zadań musi zostać uruchomiony w drugim playu lub dla drugiej grupy hostów. Kopiowanie zadań między playbookami jest sygnałem ostrzegawczym, ponieważ od tego momentu każdą poprawkę trzeba stosować dwukrotnie, a pewnego dnia zostanie ona zastosowana tylko raz. Pojedynczy playbook o długości około 100 wierszy, który jest zawsze kierowany tylko do jednej grupy, nic nie zyskuje dzięki roli. Dodatkowe katalogi utrudniają jego odczytanie.
Czy role są uruchamiane przed zadaniami w tym samym playu?
Tak. Ansible uruchamia pre_tasks, następnie wszystkie elementy wymienione w roles:, potem tasks:, a na końcu post_tasks:. Kolejność tych kluczy w pliku nie ma znaczenia. Umieszczenie tasks: przed roles: nie powoduje wcześniejszego uruchomienia tych zadań. Jeśli coś musi zostać wykonane przed rolą, należy umieścić to w pre_tasks:.
Dlaczego wartość group_vars nie zastępuje wartości z roli?
Należy sprawdzić, czy zmienna jest ustawiona w vars/main.yml roli, a nie w defaults/main.yml. vars/ ma wyższy priorytet niż group_vars i host_vars w kolejności pierwszeństwa Ansible, dlatego inventory nie może zastąpić tej wartości. Zmienną należy przenieść do defaults/main.yml. Ten poziom znajduje się blisko dołu kolejności i jest właściwym miejscem dla wartości, którą powinien móc zmieniać wywołujący. Aby potwierdzić, że przyczyną jest kolejność pierwszeństwa, a nie literówka, należy jednorazowo uruchomić playbook z -e name=value, który ma wyższy priorytet niż każde inne źródło.
Dlaczego Ansible informuje, że nie znaleziono roli?
Wyszukiwanie rozpoczyna się obok pliku playbooka, dlatego site.yml i roles/ muszą znajdować się w tym samym katalogu. Komunikat o błędzie zawiera wypróbowane ścieżki, jak w the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Uruchomienie playbooka z katalogu nadrzędnego jest prawidłowe, ponieważ wyszukiwanie odbywa się na podstawie ścieżki playbooka, a nie bieżącego katalogu roboczego powłoki. Jeśli używane jest roles_path z ansible.cfg, należy potwierdzić załadowanie tego pliku za pomocą ansible --version, ponieważ Ansible ignoruje plik znajdujący się w katalogu z prawem zapisu dla wszystkich użytkowników.
Czy do utworzenia roli potrzebne jest ansible-galaxy init?
Nie. Rola składa się wyłącznie z katalogów o oczekiwanych nazwach, dlatego mkdir -p roles/common/tasks wraz z tasks/main.yml tworzy już działającą rolę. ansible-galaxy init --init-path roles common ogranicza liczbę czynności wymaganych do wpisania i tworzy pełny szkielet, w tym meta/main.yml oraz szablon pliku README. Należy usuwać puste katalogi, ponieważ pusty vars/main.yml utrudnia ustalenie, które pliki w roli faktycznie wykonują działania.