Ansible playbook czy rola: kiedy wybrać dane rozwiązanie
Dowiedz się, kiedy stosować płaski plik YAML, a kiedy przejść na strukturę katalogów roli. Analiza różnic w reużywalności kodu, kolejności zmiennych i organizacji zadań.
Ansible playbook a rola: jaka jest różnica
Ansible playbook to plik uruchamiany za pomocą ansible-playbook. Mapuje on grupę hostów na zadania, które mają zostać wykonane. Ansible role to katalog o ustalonej strukturze, zawierający zadania, szablony, handlery oraz domyślne zmienne, wywoływany w playbooku po nazwie. Składnia zadań w obu przypadkach jest identyczna, więc nie jest to kwestia możliwości wyrażenia określonej logiki. Jest to kwestia ponownego wykorzystania kodu.
Należy zacząć od płaskiego playbooka. Jeden site.yml zawierający listę tasks: to właściwa forma dla pierwszej automatyzacji i sprawdza się ona lepiej, niż większość osób zakłada. Na rolę warto przejść, gdy ten sam blok zadań musi zostać wykonany dla drugiej grupy hostów lub gdy plik przekroczy około 100 linii i odnalezienie zadania poprzez przewijanie staje się utrudnione.
Jeśli jeszcze nie napisano żadnego playbooka, należy zacząć od pierwszego playbooka dla pojedynczego VPS i wrócić do tego zagadnienia, gdy projekt zacznie się rozrastać.
Kiedy płaski playbook jest właściwym rozwiązaniem
Płaski playbook jest odpowiedni, gdy zadanie jest wykonywane jednorazowo, na jednym hoście lub gdy nikt inny nie będzie go czytał. Prowizjonowanie pojedynczego serwera aplikacji lub aktualizacja systemu przed oknem serwisowym nie uzasadniają tworzenia struktury katalogów. Rola dodaje siedem katalogów i jeden poziom pośredni. Jeśli jedynym wywołującym jest playbook znajdujący się obok, to pośrednictwo nie przynosi korzyści, a jedynie wymusza dodatkowy skok za każdym razem, gdy użytkownik chce sprawdzić, co faktycznie jest wykonywane.
Płaski playbook przestaje być właściwym rozwiązaniem w konkretnym momencie, który łatwo zidentyfikować. Jest to chwila, w której kopiuje się blok zadań do drugiego playbooka. Ta kopia jest sygnałem ostrzegawczym. Od tego momentu każda poprawka musi być wprowadzana dwukrotnie, a pewnego dnia zostanie 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.ymlto punkt wejścia. Ansible uruchamia ten plik, gdy rola jest wywoływana; każdy inny katalog jest opcjonalny.defaults/main.ymlprzechowuje zmienne, które wywołujący powinien nadpisać. Jest to źródło o najniższym priorytecie w Ansible, więc niemal wszystko inne ma nad nim pierwszeństwo.vars/main.ymlprzechowuje zmienne, których wywołujący nie powinien nadpisywać. Pod względem priorytetu znajduje się powyżej inwentarza, co jest silnym założeniem. Należy używać go rzadko.handlers/main.ymlprzechowuje zadania wyzwalane przeznotify. Handler uruchamia się raz, na końcu play, niezależnie od tego, ile zadań wysłało do niego powiadomienie.files/przechowuje pliki kopiowane w niezmienionej formie przez modułcopy, atemplates/przechowuje szablony Jinja2 renderowane przez modułtemplate. Wewnątrz roli odwołujesz się do obu za pomocą samej nazwy pliku, bez ścieżki, ponieważ Ansible przeszukuje najpierw własne katalogi roli.meta/main.ymldeklaruje zależności roli oraz metadane odczytywane przez Ansible Galaxy.
Ten układ nie jest kwestią preferencji stylistycznych. Ansible szuka plików w tych konkretnych ścieżkach, więc szablon umieszczony w roles/common/template/ (liczba pojedyncza) po prostu nigdy nie zostanie znaleziony.
Budowa wspólnej roli za pomocą ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonPolecenie to tworzy pełny szkielet w roles/common, w tym katalogi, które nie będą używane, oraz pliki main.yml zawierające jedynie ---. Należy usunąć te, które pozostają puste. Pusty plik vars/main.yml nie wpływa negatywnie na działanie Ansible, jednak utrudnia identyfikację plików, które faktycznie mają znaczenie dla roli.
Teraz należy wypełnić pliki realizujące zadania. Najpierw należy zdefiniować 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 użyć cudzysłowów dla "no" oraz "yes". Ansible analizuje YAML za pomocą PyYAML, który interpretuje surowe no jako wartość logiczną false, przez co wygenerowana linia konfiguracji przyjmuje postać PermitRootLogin False, co powoduje jej odrzucenie przez sshd. Cudzysłowy wymuszają traktowanie wartości jako ciągu 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, natomiast w systemach z rodziny RHEL jest to sshd. Handler wskazujący na błędną nazwę zawiedzie dopiero w momencie, gdy zmiana w szablonie wymusi jego uruchomienie; dlatego problem ten ujawnia się zazwyczaj po kilku tygodniach.
Linia validate jest najbardziej użytecznym elementem tego zadania. Ansible renderuje szablon do pliku tymczasowego, podstawia ścieżkę do tego pliku w miejsce %s i wykonuje polecenie. Plik docelowy jest nadpisywany tylko wtedy, gdy polecenie zakończy się kodem 0. W przypadku umieszczenia błędnej dyrektywy w szablonie i ponownego uruchomienia, zadanie zakończy się błędem failed to validate, rzeczywisty plik /etc/ssh/sshd_config.d/99-hardening.conf pozostanie nienaruszony, a dostęp do serwera zostanie zachowany. Należy pamiętać, że test sprawdza więcej niż tylko poprawność składni. Jeśli sshd -t nie może odczytać kluczy hosta, kończy działanie z błędem sshd: no hostkeys available -- exiting., a Ansible raportuje ten sam błąd failed to validate. Przed uznaniem szablonu za błędny należy zapoznać się z msg danego 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ę sekcją failed=0 w podsumowaniu. Parametry należy przekazywać w miejscu wywołania przy użyciu rozszerzonej formy; w ten sposób jedna rola obsługuje dwie grupy hostów:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordIstnieje jedna zasada kolejności, która zaskakuje niemal każdego. Play może zawierać pre_tasks, roles, tasks oraz post_tasks, a Ansible uruchomi je w tej właśnie kolejności, niezależnie od tego, w jakiej kolejności zostały zapisane w pliku. Umieszczenie tasks: powyżej roles: nie zmienia faktu, że role i tak uruchomią się jako pierwsze. Jeśli więc coś musi wydarzyć się 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 poziomu listy zadań zamiast używać klucza roles:, należy skorzystać z 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ę w momencie parsowania, a jej zadania stają się częścią play, dlatego ansible-playbook --list-tasks site.yml wyświetla je na liście, a tag nałożony na import dotyczy każdego zadania wewnątrz. include_role jest dynamiczne. Nic nie jest odczytywane, dopóki zadanie nie zostanie uruchomione, co pozwala na sterowanie nazwą roli za pomocą zmiennej lub pętli. Kosztem tego rozwiązania jest niewidoczność tych zadań dla --list-tasks oraz --start-at-task.
W tym miejscu kryje się pułapka. Warunek when: w zadaniu include_role jest sprawdzany, zanim defaults/main.yml dołączanej roli znajdzie się w zasięgu. Zapisanie when: common_packages | length > 0 przy include spowoduje zatrzymanie wykonania z błędem 'common_packages' is undefined, nawet jeśli zmienna ta jest zdefiniowana w samej dołączanej roli. Rozwiązaniem jest przeniesienie przełącznika poza rolę: należy umieścić go w group_vars/all.yml, gdzie jest dostępny w każdym zasięgu, a wartości domyślne roli pozostawić dla parametrów, które rola wykorzystuje wewnętrznie.
Która zmienna wygrywa: defaults, group_vars, vars, extra vars
Dokumentacja Ansible wymienia ponad dwadzieścia poziomów pierwszeństwa zmiennych. Cztery z nich rozstrzygają niemal każdy praktyczny spór; poniżej przedstawiono je od najsłabszego do najsilniejszego.
roles/<name>/defaults/main.ymlznajduje się niemal na samym dole. Prawie każda wartość ustawiona w innym miejscu ma nad nią pierwszeństwo, dlatego jest to właściwe miejsce dla parametrów konfiguracyjnych roli.group_vars/orazhost_vars/znajdują się pośrodku. To tutaj należy umieszczać specyficzne dla danej infrastruktury wartości, które skutecznie nadpisują domyślne ustawienia roli.roles/<name>/vars/main.ymlznajduje się powyżejhost_vars. Wartość zdefiniowana w tym miejscu nie może zostać nadpisana z poziomu inwentarza. Należy ją rezerwować dla elementów, które muszą zachować spójność wewnętrzną roli, takich jak nazwa pakietu, która musi być zgodna z nazwą usługi.- Parametr roli przekazany w miejscu jej wywołania ma pierwszeństwo przed
vars/main.yml, a-ew wierszu poleceń wygrywa ze wszystkim, w tym z parametrami roli.
Proces rozstrzygania pierwszeństwa można prześledzić w około minutę. Należy zdefiniować w prostej roli jedną wartość 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 wypisuje tunable=from-hostvars internal=from-rolevars. Inwentarz okazał się ważniejszy niż wartość domyślna roli, ale przegrał ze zmienną roli. Drugie uruchomienie wypisuje internal=from-cli, ponieważ extra vars znajdują się na samym szczycie hierarchii i nic poniżej nie może ich zmienić. Dlatego właśnie -e sprawdza się przy jednorazowym uruchomieniu, ale jest błędem w skrypcie przeznaczonym do stałego użytku: bezgłośnie wygrywa z każdą przemyślaną decyzją zapisaną w repozytorium.
Zasada robocza: jeśli wartość ma być konfigurowalna, należy umieścić ją w defaults/. Umieszczenie jej w vars/ informuje każdego przyszłego użytkownika roli, że inwentarz nie może jej zmienić. Czasami jest to zamierzone działanie, jednak zazwyczaj wynika z błędu.
Weryfikacja idempotentności roli: dwukrotne uruchomienie
Godne zaufania uruchomienie Ansible daje ten sam rezultat za drugim razem i raportuje brak zmian. Uruchom playbook dwukrotnie i przeanalizuj 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 uznał zadanie za wykonane. changed=2 przy drugim uruchomieniu oznacza, że dwa zadania nie potrafią rozpoznać stanu, więc będą bez końca nadpisywać pliki i restartować usługi. Typową przyczyną jest command lub shell, ponieważ Ansible nie ma możliwości sprawdzenia, co wykonał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.txtUruchom ten playbook dwukrotnie, a następnie policz linie z wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt zawiera dwie linie, a /tmp/guarded.txt jedną. Przy drugim uruchomieniu zabezpieczone zadanie nie wykonało się wcale, a jego wynik zawiera komunikat skipped, since /tmp/guarded.txt exists, ponieważ creates dostarcza modułowi widoczny produkt, który jest sprawdzany w pierwszej kolejności. Jeśli polecenie nie pozostawia takiego produktu, zarejestruj jego wyjście i podejmij decyzję samodzielnie za pomocą changed_when.
ansible-playbook --check --diff site.yml przewiduje zmiany bez ich wprowadzania, a --diff drukuje dokładne linie, które zostałyby nadpisane w szablonie. Analizując wyjście, należy pamiętać o jednym zastrzeżeniu: zadania shell oraz command są pomijane w trybie sprawdzania, więc plan wyglądający na poprawny może nadal ukrywać pewne operacje.
Jeszcze jedna kolumna w podsumowaniu wymaga uwagi: host, z którym Ansible nie mógł się połączyć, jest liczony w unreachable, a nie w failed, i żadne z jego zadań nie zostało wykonane. Dlatego zdecyduj z wyprzedzeniem, czy jeden nieosiągalny host powinien zatrzymać cały proces, zanim uruchomisz tę rolę na większej liczbie maszyn.
Dlaczego Ansible zgłasza, że rola nie została znaleziona
Ansible szuka katalogu roles/ obok pliku playbooka, a następnie w roles_path. Wyszukiwanie odbywa się w oparciu o lokalizację playbooka, a nie bieżący katalog powłoki.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyTen komunikat oznacza, że site.yml oraz roles/ przestały być ze sobą zgodne; narzędzie wyświetla ścieżki, które zostały sprawdzone. Należy przechowywać oba elementy w tym samym katalogu. Uruchamianie z katalogu nadrzędnego jest poprawne, ponieważ kluczowa jest ścieżka do playbooka:
ansible-playbook -i infra/inventory.ini infra/site.ymlIstnieje mniej oczywista przyczyna tego samego problemu. Ansible ignoruje plik ansible.cfg w bieżącym katalogu, jeśli jest on zapisywalny przez wszystkich (world writable). Każdy użytkownik w systemie mógłby umieścić tam konfigurację i zmienić sposób działania uruchamianego procesu.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.Ustawienia roles_path oraz inventory są wtedy pomijane bez ostrzeżenia, a wyszukiwanie roli kończy się niepowodzeniem z przyczyn niezwiązanych z samymi rolami. Polecenie ansible --version wyświetla plik config file, który został faktycznie wczytany, a ansible-config dump --only-changed wypisuje wszystkie ustawienia różniące się od wartości domyślnych. Należy sprawdzić oba te polecenia, gdy działanie skryptu sugeruje, że konfiguracja nie istnieje.
Udostępnianie ról: requirements.yml i przypięta wersja
Rola napisana przez kogoś innego jest instalowana, a nie kopiowana. Należy ją zadeklarować jednokrotnie:
# 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 ustawiać version. Bez tego parametru pobierana jest domyślna gałąź dostępna w dniu uruchomienia polecenia, co powoduje, że wdrożenie działające w poprzednim miesiącu może przestać działać bez żadnych zmian w repozytorium. Należy wskazać roles_path na katalog pobierania i wykluczyć ten katalog z systemu git:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesRole znajdujące się w roles/ obok playbooka są nadal wykrywane, ponieważ ta ścieżka jest zawsze przeszukiwana dodatkowo obok roles_path. Dzięki temu własne role pozostają w repozytorium i podlegają weryfikacji, podczas gdy role zewnętrzne są powtarzalnymi pobraniami przypiętymi do konkretnego tagu.
Kiedy role przestają być rozwiązaniem
Rola jest jednostką wielokrotnego użytku w ramach jednego uruchomienia Ansible. Nie tworzy ona serwerów ani rekordów DNS u dostawcy, a próby wymuszenia takiego działania prowadzą do powstania playbooków, których nikt nie chce utrzymywać. Warto przeczytać podział pracy między Ansible a Terraform przed rozpoczęciem działań. Rola nie zastępuje również projektu inwentarza: po przekroczeniu kilku maszyn, sposób grupowania i łączenia się z serwerami ma większe znaczenie niż sposób przechowywania zadań.
Hardening, który instaluje ta rola common, również wymaga podjęcia własnych decyzji. Powyższy plik drop-in ustawia tylko dwie dyrektywy, więc przed podjęciem decyzji, co powinno znaleźć się w roli dla każdego posiadanego hosta, należy przeczytać które ustawienia SSH faktycznie warto zmieniać oraz jak skonfigurować Ubuntu, aby samodzielnie stosowało aktualizacje bezpieczeństwa.
FAQ
Kiedy należy przekształcić playbook Ansible w rolę?
Gdy ten sam blok zadań musi zostać wykonany w drugim playu lub dla drugiej grupy hostów. Kopiowanie zadań między playbookami jest sygnałem do zmiany, ponieważ od tego momentu każda poprawka musi być nanoszona dwukrotnie, co prędzej czy później doprowadzi do niespójności. Pojedynczy playbook o długości około 100 linii, który zawsze dotyczy tylko jednej grupy, nie zyskuje na przekształceniu w rolę, a dodatkowe katalogi jedynie utrudniają jego czytelność.
Czy role są uruchamiane przed zadaniami w tym samym playu?
Tak. Ansible wykonuje pre_tasks, następnie wszystko wymienione w roles:, potem tasks:, a na końcu post_tasks:. Kolejność występowania tych kluczy w pliku nie ma znaczenia. Umieszczenie tasks: powyżej roles: nie sprawi, że te zadania zostaną wykonane jako pierwsze. Jeśli coś musi nastąpić przed rolą, należy umieścić to w pre_tasks:.
Dlaczego wartość z group_vars nie nadpisuje roli?
Należy sprawdzić, czy zmienna została ustawiona w vars/main.yml roli, zamiast w defaults/main.yml. vars/ znajduje się wyżej niż group_vars oraz host_vars w hierarchii pierwszeństwa Ansible, dlatego inwentarz nie może jej nadpisać. Należy przenieść zmienną do defaults/main.yml, który znajduje się blisko końca listy pierwszeństwa i jest właściwym miejscem dla parametrów, które użytkownik powinien móc zmieniać. Aby potwierdzić, że przyczyną jest pierwszeństwo, a nie literówka, należy uruchomić playbook z flagą -e name=value, która ma najwyższy priorytet ze wszystkich źródeł.
Dlaczego Ansible zgłasza, że rola nie została znaleziona?
Wyszukiwanie rozpoczyna się w lokalizacji pliku playbooka, dlatego site.yml oraz roles/ muszą znajdować się w tym samym katalogu. Komunikat o błędzie zawiera ścieżki, które zostały sprawdzone, jak w the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Uruchamianie playbooka z katalogu nadrzędnego jest poprawne, ponieważ wyszukiwanie podąża za ścieżką playbooka, a nie za bieżącym katalogiem roboczym powłoki. Jeśli zależność opiera się na roles_path z ansible.cfg, należy upewnić się, że plik został załadowany za pomocą ansible --version, ponieważ katalog roboczy z uprawnieniami zapisu dla wszystkich (world writable) powoduje, że Ansible go ignoruje.
Czy do utworzenia roli wymagane jest użycie ansible-galaxy init?
Nie. Rola to jedynie katalogi o określonych nazwach, więc mkdir -p roles/common/tasks wraz z plikiem tasks/main.yml stanowi już działającą rolę. ansible-galaxy init --init-path roles common oszczędza czas i dostarcza pełny szkielet, w tym meta/main.yml oraz szablon README. Należy usunąć puste katalogi, ponieważ nieużywany vars/main.yml utrudnia identyfikację plików, które faktycznie wykonują operacje w roli.