Ansible: YAML i tworzenie Playbook-ów
 

W poprzednim artykule pokazaliśmy, jak użyteczne może być wykorzystanie poleceń ad-hoc. Umożliwia ono uruchamianie pojedynczych zadań na wielu węzłach. Polecenia ad-hoc stosuje się z natury doraźnie, stąd ich nazwa. Ich ograniczeniem jest to, że mogą one wykonywać tylko jeden moduł z jednym zestawem argumentów w danym momencie. Zdarza się, że potrzebujemy wykonać cały zestaw różnych zadań zarówno na dużej ilości węzłów działających w danej chwili, jak i nowych, które pojawią się w przyszłości.

Zadania te o wiele łatwiej jest wypisać, modyfikować i utrzymywać w formie pliku tekstowego, będącego tak zwanym playbook-iem. Playbook jest zbiorem zestawów zadań do wykonania na określonych zbiorach węzłów.

Dzięki wykorzystaniu języka YAML (YAML Ain’t Markup Language) do tworzenia playbook-ów, całość jest o wiele bardziej czytelna i zrozumiała od jednowierszowych poleceń ad-hoc, szczególnie tych z dużą ilością argumentów. Należy pamiętać, że im mamy do czynienia z większą skalą, tym większego znaczenia nabiera prostota, przejrzystość i czytelność. Ich brak przekłada się w negatywny sposób na bezpieczeństwo i awaryjność.

Playbook Ansible jest plikiem tekstowym z rozszerzeniem ".yml" lub ".yaml". To drugie jest zalecanym przez standard rozszerzeniem. Bardzo istotnym elementem struktury tego pliku są wcięcia (ang. indentation), będące spacjami. O ile ich dokładna ilość nie ma znaczenia, to dane na tym samym poziomie hierarchii muszą mieć taką samą ilość wcięć (spacji), a obiekty podrzędne (ang. children) muszą mieć więcej wcięć (spacji) od obiektu nadrzędnego (ang. parent).

Celowo piszę o znaku spacji, jako że znak tabulatora nie może być stosowany do wcięć. Powodem tego jest różny sposób interpretacji znaku tabulatora przez różne edytory tekstu. Pierwszy wiersz playbook-a musi zawierać trzy kreski "---", a ostatni wiersz powinien zawierać trzy kropki "...". Ten ostatni wiersz jest często pomijany.


Narzędzie Ansible dostępne jest w systemie RHEL bez żadnych dodatkowych opłat. Niemniej, Red Hat nie udziela dla niego wsparcia i nie daje dostępu do innych komponentów platformy Red Hat Ansible Automation bez wykupienia stosownych subskrypcji. Zainteresowanych ich zakupem This email address is being protected from spambots. You need JavaScript enabled to view it..


Miejsce na "plays", czyli zbiory zadań do odegrania na określonych grupach hostów, znajduje się pomiędzy "---", a "...". Przed przystąpieniem do pisania playbook-ów warto poświęcić trochę czasu na zapoznanie się ze składnią i strukturą języka YAML, jako iż ona będzie składnią i strukturą naszych playbook-ów.

YAML służy do opisu i reprezentowania danych w uporządkowany i ustrukturyzowany sposób. W porównaniu z XML, zapis w YAML jest bardziej zwięzły i przejrzysty, a także wygodniejszy dla człowieka przy tworzeniu i edycji.

Wewnątrz dokumentu można stosować komentarze. Stosowany jest do tego znak hash-a "#" na początku nowego wiersza lub znak spacji i hash-a " #" po danych. Wszystko na prawo od tego znaku traktowane jest jako komentarz. Komentarze mogą znajdować się także przed znakiem "---".

# To jest komentarz
product: 'Red Hat Ansible Automation' # To także jest komentarz


Zmienne, czy inaczej klucze są odseparowane od wartości za pomocą dwukropka i spacji ": ":

name: 'Marcin Ślęczek'
work: 'networkers.pl'


Dostępny jest także bardziej zwarty format mapowania zmiannych, zapisywany z użyciem nawiasów klamrowych "{}":

{name: 'Marcin Ślęczek', work: 'networkers.pl'}


Wartość tekstowa (ang. string) klucza nie musi być umieszczana wewnątrz cudzysłowów, nawet jeżeli zawiera spacje. Niemniej, trzeba wtedy uważać na znaki specjalne, będące składnią YAML, jak dla przykładu dwukropek ":", stąd zalecamy stosowanie cudzysłowów dla zmiennych tekstowych. Podwójny cudzysłów "" umożliwia użycie niektórych znaków specjalnych poprzez poprzedzenie ich znakiem backslash "\", jak dla przykładu znak nowego wiersza "\n". Pojedynczy cudzysłów '' uniemożliwia wykorzystanie znaków specjalnych.

Kiedy potrzebujemy zapisać długi tekst, składający się z wielu linii, to najlepiej do tego celu skorzystać z operatora "|" lub ">". Pierwszy z nich, to jest "|" zachowuje znak nowego wiersza i wszystkie końcowe spacje. Wiodące puste znaki każdej linii od lewej strony są usuwane. Przykład jego użycia widać poniżej:

data: |
Trzy linie tekstu,
każda zapisana w
oddzielnym wierszu.

address: |
networkers.pl Sp. z o.o.
os. Centrum B 7
31-927 Kraków


Drugi, to jest ">" konwertuje znaki nowego wiersza do spacji " " oraz usuwa wiodące puste znaki linii od lewej strony. Stosowany jest najczęściej tylko dla lepszej czytelności. Przykład jego użycia widać poniżej:

data: >
Jedna długa linia tekstu
zapisana w taki sposób
dla lepszej czytelności.

about: >
Jesteśmy dostawcą kompletnych rozwiązań do systemów IT,
centrów danych i środowisk DevOps, który działa od 2009 roku.
Sprzedajemy pojedyncze produkty, jak i kompletne rozwiązania.


Wartości zmiennych logicznych (ang. booleans) mogą być określane na różne sposoby, jak: True/False, true/false, yes/no czy 1/0. Warto zdecydować się na jeden sposób i konsekwentnie go stosować.


Każdy element tej samej listy posiada taką samą ilość wcięć, a następnie poprzedzony jest kreską i spacją "- ":

- firewall
- router
- switch
- hub


Dostępny jest także bardziej zwarty format zapisu listy z użyciem nawiasów kwadratowych "[]":

[firewall, router, switch, hub]


Na tym etapie, tyle z obszaru YAML nam wystarcz. Przeszliśmy przez najbardziej niezbędne podstawy. Nie opisywaliśmy wszystkich możliwych sposobów zapisu tych samych rzeczy, ani też jeszcze nie odnieśliśmy się do wszystkiego co będzie nam potrzebne dalej. Nie to jest naszym celem na ten moment.

O ile istnieje wiele różnych sposobów zapisu tej samej rzeczy, to warto dla porządku przyjąć jakiś standard w ramach projektu czy nawet całej organizacji, a następnie się go trzymać. Wtedy dla każdego całość będzie łatwiejsza w interpretacji i obsłudze, co przełoży się na sprawniejsze zarządzanie i mniejszą ilość ludzkich pomyłek.


Zanim zajmiemy się tworzeniem playbook-ów, przyjrzyjmy się jeszcze raz dwóm poleceniom ad-hoc:

$ ansible all -m command -a "df -h" 
$ ansible all -m copy -a "src=./environment dest=/etc/environment owner=root group=root mode=0644"


Widać, że nadają się one idealnie to sytuacji, kiedy potrzebujemy coś szybko sprawdzić lub doraźnie (łac. ad-hoc) zrobić. Zastosowanie ich do opisywania poszczególnych elementów infrastruktury jest raczej mało wygodne, a w dłuższej perspektywie trudne w utrzymaniu i podatne na błędy. Niemniej, mają one swoje zastosowanie i są potrzebne, dlatego warto umieć się nimi posługiwać.


Teraz zajmiemy się nieco innym wykorzystaniem Ansible, którym jest opisywanie poszczególnych elementów infrastruktury czy automatyzacja większej ilości zadań na różnych grupach węzłów. Do takich celów o wiele bardziej nadają się playbook-i.

Z punktu widzenia nazewnictwa stosowanego w Ansible, pojedynczy "play" jest zestawem zadań do odegrania (wykonania) na danej grupie węzłów, a plik w którym znajduje się jeden lub więcej takich "play", określany jest jako "playbook". Każdy "play" może wykonywać inne zadania, na innej grupie węzłów, ale sumarycznie tworzą one pewną całość, co ułatwia orkiestrowanie (ang. orchestrating) większych prac i bardziej złożonych projektów.

Każdy "play" może specyfikować inne ustawienia dotyczące sposobu nawiązywanie połączenia do zarządzanych węzłów, jak m.in. metoda połączenia i eskalacji uprawnień czy nazwa użytkownika. Jeżeli nie są one określone, to wykorzystywane są wartości z omówionego wcześniej pliku - ansible.cfg - konfiguracji Ansible lub wartości domyślne. Nazwy tych parametrów wewnątrz pliku playbook są dokładnie takie same. Parametry te definiowane są na tym samym poziomie (ta sama ilość wcięć) co parametry "hosts" i "tasks".

Parametr "hosts" przyjmuje za argument "pattern" identyfikujący węzły z inwentarza, na których będą wykonywane zadania. Omówiliśmy go szerzej w pierwszym artykule na temat Ansible.

Parametr "tasks" zawiera listę zadań do wykonania. Poszczególne zadania wykonywane są sekwencyjnie według kolejności ułożenia w ramach playbook-a. Natomiast mogą być one wykonywane równolegle na wielu węzłach w tym samym czasie. To na ilu, zależne jest od konfiguracji - domyślnie 5 (opcja "-f" lub "--fork"). Dla lepszej czytelności, pomiędzy kolejnymi zadaniami na liście można zostawiać pusty wiersz.

Parametr "name" jest etykietą, opisującą w zrozumiały dla nas sposób zadanie lub cały "play". Jako, że etykieta ta jest dla nas, a jednocześnie jest widoczna podczas późniejszego wykonywania playbook-a, warto zadbać by zawierała jasną i zwięzłą treść. Powinna być ona w miarę krótka, a jednocześnie oddawać cel czy przeznaczenie danego zadania. O ile jest ona opcjonalna, to zalecamy jej stosowanie.

Poniżej widać playbook z jednym "play", który odpowiada drugiemu z wyżej przywołanych poleceń ad-hoc.

---
- name: 'ALL servers file unification'
hosts: all
tasks:
- name: 'Copy ./environment file to /etc/environment'
copy:
src: ./environment
dest: /etc/environment
owner: root
group: root
mode: 0644
...


Do uruchamiania playbook-ów służy polecenie:

$ ansible-playbook <playbook_file> [-i INVENTORY]


Przed uruchomieniem playbook-a dobrą praktyką jest sprawdzenie czy jego składania jest prawidłowa. Dokonać tego można za pomocą opcji "--syntax-check". Jeżeli taka weryfikacja się nie powiedzie, to dostaniemy informację na temat błędu i jego lokalizacji.

$ ansible-playbook <playbook_file> [-i INVENTORY] --syntax-check


Jeżeli składania jest w porządku, można dodatkowo uruchomić playbook na sucho (ang. dry run). Powoduje to, że dostajemy na wyjściu wszystkie informacje na temat wyniku działania playbook-a, jak gdyby został on wykonany naprawdę. W ten sposób widać co zostało zmienione, a co nie. Oczywiście, w trybie tym nie dochodzi do żadnych modyfikacji na zarządzanych węzłach. Służy do tego opcja "-C" lub "--check".

$ ansible-playbook <playbook_file> [-i INVENTORY] --check


Dodatkowo, można zwiększyć ilość informacji generowanych na wyjściu poleceń "ansible" i "ansible-playbook". Służą do tego opcje "-v", "-vv", "-vvv" i "-vvvv". Każda z kolejnych zwiększa poziom debugowania o 1 i co za tym idzie ilość wyświetlanych informacji. Domyślnie poziom debugowania wynosi 0.

Poniżej znajduje się playbook zawierający dwa zestawy zadań do odegrania o nazwach "Play 1: Verification" oraz "Play 2: Unification". Każdy z nich może definiować wiele zadań, które mają zostać odegrane na różnych grupach hostów. W naszym przykładzie obydwa zestawy zadań będą wykonywane na wszystkich węzłach.

[msleczek@vm0-net projekt_A]$ cat playbook.yml 
---
- name: 'Play 1: Verification'
hosts: all
tasks:
- name: 'Verify connectivity'
ping:

- name: 'Play 2: Unification'
hosts: all
gather_facts: false
tasks:
- name: 'Copy ./environment file to /etc/environment'
copy:
src: ./environment
dest: /etc/environment
owner: root
group: root
mode: 0644
backup: yes
- name: 'Create backup destination'
file:
path: /root/backup
state: directory
mode: 0700

...

[msleczek@vm0-net projekt_A]$


Prześledzimy teraz wynik weryfikacji składni oraz uruchomienia powyższego playbook-a. Stworzyliśmy w nim trzy zadania, a u dołu widać wykonanie czterech. Początkowe zadanie "Gathering Facts" powstaje na skutek domyślnego uruchamiania modułu "setup", który zbiera dużą ilość informacji na temat każdego zarządzanego węzła. Pozwala to potem wykonywać pewne zadania w oparciu o zebrane wartości, które odzwierciedlają charakterystykę oraz konfigurację danego węzła. Dla przykładu, jeżeli system operacyjny węzła należy do rodziny "RedHat" to do zarządzania pakietami powinniśmy użyć modułu "yum", a jeżeli jest nią "Debian", to modułu "apt". W ten sposób możemy budować bardziej uniwersalne i dynamiczne playbook-i.

Informacje te nazywane są faktami (ang. facts). Jeżeli nie są one nam do niczego potrzebne, to warto wyłączyć ich zbieranie. W ten sposób nasz playbook będzie wykonywał się znacznie szybciej. Dokonać tego można za pomocą ustawienia parametru "gather_facts" na wartość "false". Zostało to zrobione w "Play 2: Unification". Należy pamiętać, że jeżeli jawnie nie zostanie to wyłączone, to informacje te domyślnie będą zbierane na początku każdego zestawu zadań do odegrania (ang. play).

[msleczek@vm0-net projekt_A]$ ansible-playbook --syntax-check playbook.yml

playbook: playbook.yml
[msleczek@vm0-net projekt_A]$ ansible-playbook playbook.yml

PLAY [Play 1: Verification] *****************************************************************

TASK [Gathering Facts] **********************************************************************
ok: [10.8.232.124]
ok: [10.8.232.123]
ok: [10.8.232.122]
ok: [10.8.232.121]

TASK [Verify connectivity] ******************************************************************
ok: [10.8.232.124]
ok: [10.8.232.121]
ok: [10.8.232.122]
ok: [10.8.232.123]

PLAY [Play 2: Unification] ******************************************************************

TASK [Copy ./environment file to /etc/environment] ******************************************
ok: [10.8.232.124]
ok: [10.8.232.122]
changed: [10.8.232.121]
changed: [10.8.232.123]

TASK [Create backup destination] ************************************************************
changed: [10.8.232.122]
ok: [10.8.232.123]
ok: [10.8.232.124]
changed: [10.8.232.121]

PLAY RECAP **********************************************************************************
10.8.232.121 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
10.8.232.122 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
10.8.232.123 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
10.8.232.124 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

[msleczek@vm0-net projekt_A]$


Wspominaliśmy już o idempotentności. Każde zadanie playbook-a powinno takie być i jeżeli tylko korzystamy z wyspecjalizowanych modułów, które są dostarczane z Ansible, to tak jest. Natomiast kiedy używamy jakiś własnych modułów lub stosujemy moduły bardziej uniwersalne, jak te typu "command", "shell" czy "raw", to sami powinniśmy o to zadbać. Dostępne w ramach playbook-a wyrażenia pozwalają na to, niemniej wymaga to dodatkowej pracy. Koniec końców, jeżeli tylko trzymaliśmy się zasad, to taki playbook może być uruchamiany wiele razy na tym samym zbiorze węzłów, bez żadnych negatywnych skutków.

To też widać po naszym playbook-u, który na części węzłów wykonał wszystkie zadania, na części tylko niektóre, a na części nie musiał w ogóle nic robić, gdyż były już one w odpowiednim stanie. Pomimo to, bez żadnych obaw został uruchomiony dla wszystkich węzłów.

Aby szybciej orientować się w tym co się wydarzyło oraz odpowiednio ukierunkować naszą uwagę, Ansible używa różnych kolorów w zwracanych wynikach. Do tych kolorów należy m.in.:

  • "nie udało się wykonać zadania",
  • "udało się wykonać zadanie",
  • "nie trzeba było nic robić".

Kiedy możliwe jest użycie kilku kolorów, to zostanie wykorzystany ten, który odnosi się do wydarzeń, na które powinniśmy bardziej ukierunkować naszą uwagę.

U samego dołu znajduje się podsumowanie dotyczące wykonania wszystkich zbiorów zadań - "PLAY RECAP".


Zapraszamy do kontaktu drogą mailową This email address is being protected from spambots. You need JavaScript enabled to view it. lub telefonicznie +48 797 004 932.