Ansible w Enterprise Linuksie – cz. IV: zbieranie faktów, Jinja2 oraz instrukcje w playbookach

Ansible_4

W tym miesiącu przeanalizujemy wyjątkowo istotny proces, jakim jest zbieranie faktów. Następnie pokrótce omówimy język szablonów Jinja2, by na końcu zobaczyć kilka przydatnych konstrukcji w playbookach.

Zbieranie faktów

Ansible przed wykonaniem zadań zawartych w playbooku wykonuje proces zwany zbieraniem faktów (fact gathering). Jest on bardzo istotny, gdyż wiele modułów korzysta z zebranych w ten sposób informacji. Zdarza się też, że zebrane fakty wykorzystujemy w playbookach, szczególnie w instrukcjach warunkowych.

By zobaczyć jakie informacje zbiera dla nas Ansible, wykonujemy znane nam już polecenie ansible.

ansible apps.local -m setup -u ansible > setup_log

Jedyną nowością w powyższym poleceniu jest przełącznik -u, po którym wskazujemy użytkownika, na którego Ansible ma się zalogować. Zgodnie z częścią 3 naszego cyklu jest to użytkownik ansible. Wyjście (stdout) przekierowujemy do pliku setup_log.

W ramach ćwiczenia proponuję wykonać tę samą komendę jednak z dodatkowym przełącznikiem -b:

ansible apps.local -m setup -u ansible -b > with_become_setup_log

Następnie porównać zawartość plików przy pomocy narzędzia diff, osobiście polecam użyć porównania w trybie side-by-side, czyli porównania dwukolumnowego. Służy do tego przełącznik -y:

diff -y setup_log with_become_setup_log

Po stwierdzeniu różnicy oczywistym staje się fakt, że użycie -b (become), nadaje nam przy pomocy sudo uprawnienia superużytkownika.

Najprostszy playbook, a zbieranie faktów

Stwórzmy teraz playbooka, który będzie jednym z najprostszych, jakie kiedykolwiek stworzymy – nadajmy mu nazwę popularnego powiedzenia: easy_peasy_lemon_squeezy.yml

- hosts: apps.local
  user:ansible
  become:true

Wykonajmy go:

ansible-playbook easy_peasy_lemon_squeezy.yml

Wykonując go, dostaniemy:

PLAY [apps.local] *****************************************************

TASK [Gathering Facts] ***********************************************

ok: [apps.local]

PLAY RECAP **********************************************************
apps.local : ok=1 changed=0 unreachable=0 failed=0

Jak widać, Ansible nawet dla całkowicie pustego playbooka wykona zbieranie faktów.

Fakty są dla nas dostępne w playbookach. Użyjemy teraz modułu debug, który służy między innymi do wypisywania zadanych informacji podczas wykonywania playbooka.
Do playbooka easy_peasy_lemon_squeezy.yml dopisujemy:

tasks:
- name: Print distro from facts
  debug:
    msg : '{{ ansible_distribution }} {{ ansible_distribution_major_version }}'

Wykonanie zadania 'Print distro from facts' da następujące wyjście:

TASK [Print distro from facts] ***************************************
ok: [apps.local] => {
    "msg": "EuroLinux 7"
}

Jak widać fakty zbierane przez Ansible, są przydatne nie tylko dla modułów, z których korzystamy, ale także dla nas. Na ich podstawie będziemy mogli stworzyć warunkowe playbooki – o tym jednak w dalszej części.

Jinja 2

Jinja 2 jest popularnym silnikiem generowania szablonów. Dzięki zastosowaniu silnika generowania szablonów możemy dynamicznie tworzyć pliki konfiguracyjne, oczywiście na podstawie zadanego szablonu.

Oficjalna strona projektu to http://jinja.pocoo.org/. Znajdziemy tam także dokładną dokumentację projektu.

Co istotne, Ansible znacząco rozszerza ilość dostępnych filtrów, funkcji testów, dodając także zapytania (lookups). Zapytania są jednak zaawansowanym tematem i nie będą tutaj omawiane. Ważne jednak, by pamiętać, że zapytania służą do pobierania danych z zewnętrznych źródeł. Jeżeli kiedykolwiek przyjdzie potrzeba wykorzystania takiej funkcjonalności, możemy skonsultować się z dokumentacją Ansible.

Postaram się teraz w pigułce przedstawić kilka wybranych podstawowych zagadnień związanych z Jinja2.

Dostęp do zmiennych

Dostęp do zmiennych wygląda podobnie jak w całym Ansible. Tym razem jednak nie stosujemy '' lub "" przy podwójnych nawiasach klamrowych.

{# To jest komentarz #}
{{ To_jest_nazwa_zmiennej }}

{{ Obiekt[klucz_lub_własność] }}
{# Da nam w 99.9% taki sam efekt jak #}
{{ Obiekt.klucz_lub_własności }}
{# Różnica jest w kolejności poszukiwania klucza lub własności obiekcie #}

Filtry – proste i przydatne narzędzia do manipulowania danymi

Czasami zmuszeni jesteśmy do manipulowania danymi. Jinja2 pozwala nam w prosty sposób przekształcać nasze zmienne. Zmiany mogą dotyczyć wielu płaszczyzn, a filtry zupełnie, jak Unixowe Pipe'y (Potoki) mogą się łączyć.
Poniżej bardzo prosty przykład.

{# zmienna= 'ala ma kota' #}
{{ zmienna | title}}
{# zmienna z filtrem title = 'Ala Ma Kota' #}
{{ zmienna | title | reject("Kota")}}
{# zmienna będzie równa 'Ala Ma'}

Jednym z często spotykanych filtrów jest filtr default, czyli wartość domyślna używana, gdy zmienna obiektu nie istnieje lub nie jest ustawiona.

{{ obiekt.flaga | default('False') }}
{{ storage.rozmiar | default('100 G') }}

Istnieje wiele przydatnych filtrów między innymi dla dat, liczb, czy nawet mapowania (słownikowania). W sposób znaczący zwiększają one możliwości szablonowania w Ansible.

Instrukcje warunkowe

Instrukcje warunkowe są chlebem powszednim każdego programisty. Postaram się jednak dla osób nietechnicznych trochę je wytłumaczyć. Osobom z większą wiedzą chciałbym przypomnieć, że pętle zawierają instrukcję warunkową + instrukcję skoku do odpowiedniego fragmentu kodu. Pozwolę więc sobie nie rozbijać tego akapitu na dwa osobne. Chciałbym także odnotować, iż pisząc warunek, mam na myśli wartość wyrażenia boolowskiego (wyrażenia przyjmującego wartość prawda (0) lub fałsz (1)).

Najprostszą instrukcją warunkową jest „if” (jeśli), jest to też najczęstsza instrukcja warunkowa. Z instrukcją „if” często jest stowarzyszona instrukcja „else” (w przeciwnym razie – jeśli nie zaszedł żaden z poprzednich warunków to). Wiele języków posiada słowo kluczowe „elif” (połączenie else (jeśli nie zaszedł poprzedni warunek) z „if” (jeśli zajedzie warunek)).

{% if ansible_distribution is equalto EuroLinux %} {# #}
    {# Jeśli dystrybucja to EuroLinux #}
{% elif ansible_distribution is equalto Ubuntu %}
    {# Jeśli poprzedni warunek nie został spełniony i dystrybucja to Ubuntu #}
{% else %}
    {# Jeśli nie zaszedł żaden z poprzednich warunków #}
{% endif %}

Oczywiście klauzule elif i else nie muszą występować.

Jak napisałem wcześniej, pętle są instrukcjami warunkowymi ze skokiem. Ich schemat w pseudokodzie można przedstawić następująco:

1. Sprawdź warunek pętli. Jeśli prawda to skocz do 2, jeśli fałsz to skocz do 4.
    2. Instrukcje pętli – mogą być oczywiście dłuższe niż 1 linia :).
    3. Skocz do 1.
4. Dalsze instrukcje programu.

Jest to schemat pętli „while”(dopóki). Inne pętle (for, do while) można zapisać przy pomocy tej pętli, dodając instrukcje przed pętlą i/lub delikatnie modyfikując zawartość pętli.

Poniżej przykład pętli „for”, która przechodzi po wszystkich dostępnych (odkrytych przez ansible) adresach ipv4.

{% for ipv4 in ansible_all_ipv4_addresses }
 {# coś dla każdej zmiennej ipv4 #}
{% endfor %}

Instrukcje w playbookach

Instrukcje w playbookach pozwalają nam na tworzenie elastyczniejszych, bardziej zwięzłych zdań, a co za tym idzie lepszych playbooków. Pierwszymi instrukcjami, jakie poznamy, są pętle.

Pętle w playbookach

Pierwszym znanym nam już typem pętli jest with_items. Użyliśmy go przy okazji instalacji pakietów. Należy pamiętać, że with_items wykonuje się sekwencyjnie (element po elemencie, bez zrównoleglenia). W przypadku dużych zbiorów danych lepszym (szybszym) rozwiązaniem może być wpisanie elementów pętli w jedną listę, którą podamy jako pojedynczy argument. Nie jest to jednak reguła, część modułów działa tak samo szybko z pętlą.

Przykład wykorzystania:

- name: Install additional software
  yum: state=present name={{ item }}
  with_items:
   - firewalld
   - libselinux-python
   - libsemanage-python
   - chrony

with_items wykonuje pętlę po liście. Możemy oczywiście dostać się do właściwości elementów listy.
Przykład z dokumentacji Ansible:

- name: add several users
  user:
    name: "{{ item.name }}"
    state: present
    groups: "{{ item.groups }}"
with_items:
  - { name: 'testuser1', groups: 'wheel' }
  - { name: 'testuser2', groups: 'root' }

Do przechodzenia po elementach słownika służy pętla with_dict. Przykład zaczerpnięty jest z oficjalnej dokumentacji.

users:
  alice:
    name: Alice Appleworth
    telephone: 123-456-7890
  bob:
    name: Bob Bananarama
    telephone: 987-654-3210
tasks:
  - name: Print phone records
    debug:
    msg: "User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
  with_dict:
    - "{{ users }}"

Moją ulubioną pętlą jest with_random_choice. Pozwala ona na stworzenie losowości. Wbrew powszechnej opinii zamierzona losowość wprowadza olbrzymią wartość. Przykłady użycia mogą być następujące:

  1. Testowanie losowe. Zdarzyło mi się implementować testy losowe oparte na wybraniu skryptów oraz odpowiednich argumentów. Testowanie losowe, mimo iż nie jest tak skuteczne, jak inne techniki testowania, ma jedną niepowtarzalną zaletę – wyśmienicie się automatyzuje, co pozwala często w krótkim czasie uzyskać satysfakcjonujący stopień pokrycia.
  2. Implementacja Chaos Monkeys – idea przedstawiona przez inżynierów Netflix'a – polega ona na wyłączeniu losowo wybranych maszyn w celu sprawdzenia, czy infrastruktura, aplikacje, bazy danych oraz inne elementy rzeczywiście sobie z tym radzą. Proaktywne sprawdzanie, czy nasze HA (High Availability) jest rzeczywiście HA, niesie z sobą olbrzymią wartość, gdyż w przypadku awarii wiemy, które maszyny (serwisy, aplikacje itp.) zostały wyłączone i gdzie należy szukać defektu. Co istotne nasze małpy chaosu działają w godzinach operowania zespołu i co oczywiste za naszą zgodą.

Poniżej przedstawiam bardzo prosty playbook. Skrypty są dostępne w tym samym katalogu co playbook. Powinny się one także uruchomić i przejść do tła (np. nohup z &) by ansible mogło normalnie zakończyć połączenie. Jak widać, oprócz samego wyłączenia maszyny możemy też skorzystać z innych skryptów, które mogą symulować różnego rodzaju niepożądane zjawiska np. wykorzystanie całego dostępnego ramu przez wyciek pamięci lub obciążenie procesora do granic możliwość.

---
- hosts: TODO
  name: Chaos Monkey.
  vars:
     scripts:
        - count_last_digit_of_pi.sh
        - kill_it_with_fire.sh
        - ramen_eater.sh
        - terminator.sh
        - disc_benchmark.sh
  tasks:
    - name: Print available scripts
      debug:
        msg: "{{scripts}}"

    - name: Choosing and executing script
      script: "{{item}}"
      with_random_choice:
        "{{ scripts }}

Oczywiście jest to tylko zarys. Do pełnej Chaos Monkey brakuje losowego wybierania hostów. Jestem jednak pewien, że czytelnik serii przy małym wsparciu internetu będzie w stanie sobie z tym poradzić. Zauważmy też, że nie jest to prosta implementacja tego słynnego konceptu – w oryginale mówiło się tylko o wyłączeniu instancji – przy odpowiednich skryptach jesteśmy w stanie osiągnąć większy chaos niż tylko wyłączenie maszyn.

Inne spotykane typy pętli to:

  1. with_fileglobs służący do iterowania po plikach w pojedynczym katalogu, które pasują do wyrażenia (np. pliki .conf).
  2. with_nested – służy do przechodzenia po zagnieżdżonych strukturach (np. Lista w liście).
  3. with_sequence – służy do wyliczania np. (1,2,3,4,5) lub (2,4,6,8).

Więcej na temat pętli możemy znaleźć w oficjalnej dokumentacji ansible – Ansible doc

„When” – ansiblowy „if”

Zdarza się, że potrzebujemy wykonać zadany task, tylko jeśli zachodzi jakiś warunek. Służy do tego instrukcja „when”.

Książkowy przykład dotyczy instalacji oprogramowania na Debianie i Enterprise Linuksie. Z powodu różnic w nazwach menadżerach pakietów oraz nazwach paczek chcielibyśmy inaczej skonfigurować repozytoria i instalować inne pakiety.

Poniżej przedstawiam kolejny prosty playbook, który przy pomocy Ansible powie nam, czy korzystamy z Debiana, EuroLinuksa lub innego systemu.

---
- hosts: localhost
  name: Debian, EuroLinux or Different
  tasks:
    - name: Debian
      when: ansible_os_family == "Debian"
      debug:
        msg: "Debian"
    - name: EuroLinux
      when: ansible_os_family == "EuroLinux"
      debug:
        msg: "EuroLinux"
    - name: Different
      when: ansible_os_family != "EuroLinux" and ansible_os_family != "Debian"
      debug:
        msg: "Different"

Zachęcam czytelnika do napisania (najlepiej z pamięci) playbooka, który w zależności od posiadanego systemu wyświetli inną wiadomość.

Oczywiście, zamiast prostego wyświetlania możemy użyć modułów yum lub apt, by zainstalować zadane oprogramowanie.

Innym przypadkiem użycia jest sprawdzenie, czy zmienna została zdefiniowana. W poniższym przypadku playbook posiada ustawioną tylko jedną zmienną do_backup.

---
- hosts: localhost
  name: Backup play
  vars:
    do_backup: False
  tasks:
    - name: Check variables
    fail: msg ="backup_dir or backuped_dir is undefnied!!!"
    when: backup_dir is undefined or backuped_dir is undefined

    - name: Do backup
    command: tar cvfz {{backup_dir}}/backup.tar.gz {{backuped_dir}}
    when: do_backup

Powyższy playbook nie wykona się, aż do momentu, w którym zmienne backup_dir i backuped_dir nie zostaną ustawione. Mogą zostać ustawione w playbooku (mogą być nawet puste!) przy pomocy dodatkowych argumentów z linii poleceń lub w pliku konfiguracyjnym. Co więcej, sama zmienna do_backup musi także przyjąć wartość True.

W innym przypadku możemy sprawdzić, czy poprzednia komenda zwróciła poprawny status. By zebrać fakty o komendzie, używamy klauzuli register. Poniższy przykład pochodzi z dokumentacji Ansible. Widzimy tutaj naturalne połączenie z filtrem z Jinja2.

---
- hosts: localhost
  name: Failing task
  tasks:
    - command: /bin/false
      register: result
      ignore_errors: True

    - command: /bin/true
      when: result|failed

Klauzula register zapisuje do zmiennej informacje zebrane z modułu. Sprawą oczywistą jest natomiast użycie filtra w celu sprawdzenia, czy zarejestrowany task się udał – /bin/something zostanie wykonane, tylko jeśli result się nie udał.

Podsumowanie i informacja o kolejnych artykułach

Pomimo iż w tym miesiącu nie dokonfigurowaliśmy żadnych maszyn (hostów), to znacząco zwiększyliśmy naszą wiedzę na bardzo istotne, z punktu widzenia tworzenia pełnej i elastycznej konfiguracji tematy.

Następna część będzie kolejnym workiem pełnym przydatnych informacji – poznamy między innymi „vault” (kryptę, skarbiec, sejf – usłyszałem już wszystkie wersje :)) – czyli mechanizm bezpiecznego trzymania sekretów w Ansible. Do tego dojdą nam nowe komendy, których możemy użyć, stosując moduły. Na zakończenie kolejnego materiału tej serii opowiemy sobie o rolach, by w następnym w końcu móc zacząć pisać nasze DevOpsowe środowisko oparte o Ansible.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *