Wybierz swój język

Podstawy automatyzacji z 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, którymi zajmowaliśmy się do tej pory. Szczególnie tych z większą 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". 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 mówimy 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.

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ż to właśnie ona będzie składnią i strukturą naszych playbooków.

YAML służy do opisu oraz 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 znajdujących się w danym miejscu 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


Słownik (ang. dictionary) i przypisywanie wartości

Słownik składa się z klucza i przypisanej mu wartości.

Zmienne, czy też 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) przypisana do 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 znak dwukropka ":".

Dlatego też 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 ' ' nie daje możliwości używania 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, sieci,
  centrów danych i środowisk DevOps, który działa od 2009 roku.
  Sprzedajemy pojedyncze produkty, jak i kompletne rozwiązania.


Zgodnie ze standarem YAML 1.1, na którym aktualnie bazują pliki Ansible, wartości zmiennych logicznych (ang. booleans) mogą być określane na różne sposoby, to jest poprzez: True/False, true/false, yes/no czy też 1/0. Najlepiej zdecydować się na jeden sposób i konsekwentnie go stosować. Przy czym warto też mieć na uwadze, że standard YAML 1.2 wymaga, aby wartości zmiennych logicznych przyjmowały tylko wartość true/false. Stąd też i my zalecamy korzystanie właśnie z tych wartości.


Lista (ang. lists).

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 danych, 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.


Red Hat Ansible Automation Platform zawiera w sobie szereg dodatkowych komponentów, które sprawiają, iż nawet najbardziej złożona automatyzacja może być wdrażana wygodnie, efektywnie i skalowalnie w ramach średnich i większych organizacji oraz przedsiębiorstw. Zainteresowanych tą platformą Ten adres pocztowy jest chroniony przed spamowaniem. Aby go zobaczyć, konieczne jest włączenie w przeglądarce obsługi JavaScript..

Jesteśmy oficjalnym partnerem firmy Red Hat i za naszym pośrednictwem można zakupić ich produkty na polskim rynku.


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

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

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ć.


Zastosowanie playbooków

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ę playbooki.

Z punktu widzenia nazewnictwa stosowanego w Ansible, pojedynczy "play" jest zestawem zadań do odegrania (wykonania) na danej czy też wskazanej 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 automatyzację oraz 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, eskalacji uprawnień czy też nazwa użytkownika. Jeżeli nie są one określone, to stosowane 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ą 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 w ramach danego "play". Poszczególne zadania wykonywane są sekwencyjnie, według kolejności ułożenia w ramach playbooka. Niemniej, 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" oraz zmienna "forks" w pliku konfiguracji ansible, w sekcji "defaults"). Dla lepszej czytelności, pomiędzy kolejnymi zadaniami na liście można zostawiać pusty wiersz.

Parametr "name" jest opcjonalną etykietą, służąca do opisu w zrozumiały dla nas sposób zarówno poszczególnych zadań, jak i całych zbiorów zadań ("play"). Jako, że etykieta ta jest widoczna podczas późniejszego wykonywania playbooka, warto zadbać o to, aby zawierała ona 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 zaleca się 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'
    ansible.builtin.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 playbooka 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). Służy do tego opcja "--check" lub "-C", skutkiem której dostaniemy na wyjściu wszystkie informacje na temat wyniku działania playbooka, jak gdyby został on wykonany naprawdę. W ten sposób widać co zostanie zmienione, a co nie. Niemniej, w trybie tym nie dochodzi do żadnych modyfikacji na zarządzanych węzłach. Zatem, jeżeli jakieś zadanie jest powiązane czy też zależne od wykonania wcześniejszego, to może się takie wywołanie na sucho nie powieść i zawierać błędy, których nie będzie przy normalnym uruchomieniu playbooka. Przykładem mogą być dwa zadania, pierwsze instalujące pakiet wybranej usługi i drugie uruchamiające tą usługę. W trybie na sucho, drugie zadanie nie powiedzie się, choć przy normalnym wywołaniu playbooka nie otrzymalibyśmy błędu.

$ 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 jeden (1) i co za tym idzie ilość wyświetlanych informacji. Domyślnie poziom debugowania wynosi zero (0).

Poniżej znajduje się przykładowy playbook, zawierający dwa zestawy zadań (tak zwane "plays") do odegrania o nazwach: "Play 1: Verification" oraz "Play 2: Unification". Każdy z zestawów 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'
    ansible.builtin.ping:

- name: 'Play 2: Unification'

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

...
[msleczek@vm0-net projekt_A]

Prześledzimy teraz wynik weryfikacji składni oraz uruchomienia powyższego playbooka. 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 "ansible.builtin.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 zarządzanego węzła należy do rodziny "RedHat", to do zarządzania pakietami powinniśmy użyć modułu "ansible.builtin.dnf", a jeżeli jest nią "Debian", to modułu "ansible.builtin.apt".

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 ("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 playbooka powinno właśnie takie być. Jeżeli tylko korzystamy z wyspecjalizowanych modułów, które są dostarczane w ramach Ansible, to tak jest. Natomiast, kiedy używamy jakiś własnych modułów lub stosujemy moduły bardziej uniwersalne, jak te typu "ansible.builtin.command", "ansible.builtin.shell" czy "ansible.builtin.raw", to sami powinniśmy o to zadbać. Dostępne w ramach playbooka 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 playbooku, 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 Ten adres pocztowy jest chroniony przed spamowaniem. Aby go zobaczyć, konieczne jest włączenie w przeglądarce obsługi JavaScript. zainteresowanych rozwiązaniami Red Hat, w tym w szczególności Red Hat Enterprise Linux, Red Hat Satellite, Red Hat OpenShift Container Platform, Red Hat Ansible Automation Platform oraz Red Hat OpenStack Platform. Jesteśmy partnerem firmy Red Hat i za naszym pośrednictwem można zakupić ich produkty na polskim rynku.


Przed kolejną porcją wiedzy zachęcamy do przećwiczenia i utrwalenia tej poznanej tutaj. Skorzystaj z naszych ćwiczeń!