Ansible: Zmienne i szablony Jinja2
 

Zmienne służą do przechowywania różnego rodzaju danych. Ich wartości zwykle są zależne od położenia lub kontekstu w jakim się znajdują. Zmienne mogą być całkiem niezależnymi bytami lub wspólnie z innymi zmiennymi w grupie opisywać czy parametryzować dany obiekt. Do tego celu możemy wykorzystać strukturę słownika (ang. dictionary), który grupuje zmienne nazywane kluczami i umożliwia przypisywanie do nich wartości.

Przykład takiej struktury słownika w języku YAML możemy zobaczyć poniżej:

device:
name: R1
ipv4_address: 10.8.104.1
ipv6_address: 2001:db8:c1sc0::a


Zmienne muszą zaczynać się od litery, a w nazwie mogą posiadać dodatkowo cyfry oraz znak podkreślenia "_". YAML umożliwia odwoływanie się do zmiennych wewnątrz słownika przy użyciu kropki "." lub nawiasów kwadratowych "[]":

device.ipv6_address
device['ipv6_address']


Powyżej, w obu przypadkach odwołaliśmy się do wartości "2001:db8:c1sc0::a". O ile drugi zapis jest bezpieczniejszy, to pierwszy jest częściej stosowany ze względu na wygodę. Jeżeli zdecydujemy się na stosowanie zapisu z kropką ".", to warto zapoznać się ograniczeniami opisanymi w dokumentacji Ansible.

Ze zmiennych najczęściej korzysta się do parametryzowania hostów i grup inwentarza. W ten sposób, mogą być one obsługiwane przez Ansible, w oparciu o wartości przypisane do różnych zmiennych. Jako, że najczęściej statyczny inwentarz tworzony jest w formacie pliku INI, to też tylko do niego się na ten moment odnosimy. Kiedy chcemy przypisać zmienne bezpośrednio do hosta, to podaje się je w formacie "key=value", w tym samym wierszu co definicja hosta. Oto przykład inwentarza ze zmiennymi przypisanymi bezpośrednio do hostów:

[web_servers]
web1 ansible_host=10.8.232.121 ansible_port=2222
web2:2222 ansible_host=10.8.232.122
10.8.232.123:2222
10.8.232.124 ansible_port=2222


W naszym przykładzie warto zwrócić uwagę na nazwę "web1" i "web2". Jeżeli podane nazwy hostów nie są nazwami FQDN (Fully Qualified Domain Name) czy nazwami rozwiązywalnymi na adres IP, to powinniśmy skorzystać ze zmiennej "ansible_host". Wskazuje ona adres IP, do którego będzie zestawiane połączenie. Domyślnie zostanie podjęta próba nawiązania połączenia SSH na port 22. Jeżeli chcemy zmienić numer portu, to trzeba dołączyć go po dwukropku ":" do nazwy lub adresu IP hosta albo posłużyć się zmienną "ansible_port".

W praktyce, zmienne najczęściej definiuje się w ramach grup. Są one wtedy ulokowane w specjalnej sekcji o nazwie grupy z sufiksem ":vars". W sekcji dla zmiennych podaje się jedną zmienną per wiersz. Używanie zmiennych na poziomie grupy nie wyklucza w żaden sposób możliwości wykorzystania innych zmiennych czy nawet nadpisania tych samych zmiennych na poziomie hosta. Zostało to pokazane poniżej:

[web_servers:vars]
ansible_port=2222

[web_servers]
web1 ansible_host=10.8.232.121
web2 ansible_host=10.8.232.122
10.8.232.123
10.8.232.124:22


W naszym przykładzie widać definicję zmiennej "ansible_port" dla wszystkich hostów grupy "web_servers". Niemniej, dla hosta "10.8.232.124" zostanie użyty inny port, jako iż zostało to nadpisane na poziomie hosta.

Definicje zmiennych mogą obejmować różne zakresy, to jest wszystkich "all", grup nadrzędnych czy podrzędnych, a także pojedynczego hosta. Przy czym wyższy priorytet ma definicja zmiennej w ramach bardziej specyficznego zakresu. Dopiero kiedy jej nie ma, sprawdzane są dostępne definicje w szerszych zakresach. Inaczej mówiąc, o ile grupy podrzędne dziedziczą wartości zmiennych grup nadrzędnych, to zawsze obowiązuje bardziej precyzyjna definicja. Stąd, na poziomie grupy podrzędnej można nadpisać zmienne grup nadrzędnych. W przypadku, kiedy ten sam host należy do wielu grup tego samego poziomu, grupy układane są alfabetycznie i każda kolejna grupa na liście nadpisuje zmienne wcześniejszych. Da się też dla każdej z grup ręcznie ustawić priorytet.

Nie można zapomnieć o wielu inwentarzach. Tam również mogą powielać się definicje hostów oraz grup, a także ich zmiennych. Zarówno polecenie "ansible", jak i "ansible-playbook" umożliwia podanie kilku opcji "-i" lub "--inventory", a jako ich argument można wskazać katalog z wieloma plikami inwentarzy (statycznych i dynamicznych jednocześnie). W przypadku, kiedy stosujemy kilka inwentarzy, każdy kolejny inwentarz na liście nadpisuje zmienne poprzedniego. Kiedy podany jest katalog, to dla jego plików stosowana jest kolejność alfabetyczna, jak w przypadku grup. Stąd, zalecane jest wtedy rozpoczynanie nazw plików inwentarzy od numerów, dzięki czemu całość jest bardziej klarowna.

W przypadku złożonych środowisk oraz dużej ilości zarządzanych węzłów zalecane jest trzymanie zmiennych poza inwentarzem, w oddzielnych plikach per grupa oraz per host. Do tego celu stosowany jest odpowiednio katalog "group_vars/" (zmienne grup) i katalog "host_vars/" (zmienne hostów). Ansible stara się znaleźć te katalogi w oparciu o lokalizację pliku inwentarza (polecenie "ansible") lub bieżącego katalogu (polecenie "ansible-playbook"). Nazwy tych plików odpowiadają nazwom grup lub hostów. Zawartość plików musi być zgodna ze składnią YAML. Pliki nie muszą posiadać żadnego rozszerzenia lub posiadać rozszerzenie ".yml", ".yaml" lub ".json".

W naszym przykładzie plik "group_vars/web_servers" powinien mieć zawartość:

---
ansible_port: 2222


Warto zwrócić uwagę na trzy kreski "---" oraz typowy dla YAML sposób mapowania "key: value". W ten sam sposób mogą być także tworzone zmienne wewnątrz playbook-a. Da się także w jego wnętrzu wskazywać całe pliki i katalogi ze zmiennymi. Też dodatkowe zmienne mogą zostać przekazane jako argumenty dla opcji "-e" lub "--extra-vars" poleceń "ansible" i "ansible-playbook". Możliwości jest dużo, ale na szczęście ich biegła znajomość nie jest nam na ten moment potrzebna. Pełna lista źródeł zmiennych i ich priorytetów dostępna jest w dokumentacji Ansible.

Odwoływanie się do zmiennych realizowane jest poprzez system szablonów Jinja2, znany wielu z języka Python. Jest on bardzo elastyczny, obsługuje wewnątrz zarówno wyrażenia warunkowe, jak i pętle. Do wartości zmiennej odwołujemy się w Jinja2 poprzez umieszczenie jej nazwy we wnętrzu podwójnych nawiasów klamrowych "{{ }}".

W poprzednim artykule wspomnieliśmy o możliwości wykorzystania zebranych informacji na temat zarządzanych węzłów do budowy bardziej uniwersalnych playbook-ów. Informacje te nazywane są faktami (ang. facts). Są one zdobywane w trakcie wykonywania zadania "Gathering Facts". Jest ono domyślnie zawsze uruchamiane na początku każdego play-a. W jego trakcie Ansible tworzy dla każdego z węzłów odpowiednie zmienne, do których potem można się odwoływać wewnątrz playbook-a i szablonów Jinja2. Ilość tych zmiennych jest bardzo duża. Najłatwiej wyświetlić je wszystkie za pomocą omówionego wcześniej modułu "setup", polecenia ad-hoc.

W playbook-u zwykle odnosimy się do wybranych faktów, aby uwarunkować wykonanie danego zadania od wartości, jaka się w nich znajduje. Zdarza się też, że zebrane fakty wykorzystywane są jako wartości konfiguracyjne dla różnych usług lub do budowy plików. Fakty można także wykorzystać do generowania powiadomień zarówno na wyjściu playbook-a, jak i takich wysyłanych drogą e-mail czy do pokoju Cisco Webex Teams.

Do wyświetlania wartości zmiennych, w tym faktów, w trakcie działania playbook-a służy moduł "debug".

[msleczek@vm0-net projekt_A]$ cat playbook.yaml 
---
- hosts: all
tasks:
- name: "Informacje o hostach"
debug: "msg='Host: {{ansible_hostname}}, OS: {{ansible_os_family}}, IPv4: {{ansible_default_ipv4.address}}.'"
...

[msleczek@vm0-net projekt_A]$


Do wyświetlania informacji możemy użyć jednej z dwóch opcji modułu "debug": "var" lub "msg". Wykluczają się one wzajemnie, więc trzeba wybrać:

  • "var" wyświetla tylko wartość podanej zmiennej i nie wymaga jawnego użycia nawiasów klamrowych "{{ }}" dookoła nazwy zmiennej.
  • "msg" wyświetla przygotowaną przez nas wiadomość, w której jeżeli używane są zmienne, to powinniśmy je jawnie umieścić wewnątrz podwójnych nawiasów klamrowych "{{ }}" - zgodnie z formatem Jinja2.

Moduł "debug" udostępnia też parametr "verbosity", który określa przy jakim minimalnym poziomie debugowania, informacja ta zostanie dla nas wyświetlona. Domyślnie "verbosity" ustawione jest na 0, co powoduje wyświetlanie informacji przy każdym wywołaniu playbook-a.

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

PLAY [all] **********************************************************************************

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

TASK [Informacje o hostach] *****************************************************************
ok: [10.8.232.121] => {
"msg": "Host: vm1-net, OS: RedHat, IPv4: 10.8.232.121."
}
ok: [10.8.232.122] => {
"msg": "Host: vm2-net, OS: RedHat, IPv4: 10.8.232.122."
}
ok: [10.8.232.123] => {
"msg": "Host: vm3-net, OS: RedHat, IPv4: 10.8.232.123."
}
ok: [10.8.232.124] => {
"msg": "Host: vm4-net, OS: RedHat, IPv4: 10.8.232.124."
}

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

[msleczek@vm0-net projekt_A]$


Powyżej widać wynik wykonania bardzo prostego i krótkiego playbook-a, którego treść widać nieco ponad nim. Wykorzystuje on omówiony moduł "debug" do wyświetlania zmiennych w trakcie wykonywania playbook-a.

Dalej prześledzimy bardziej złożony przypadek wykorzystania zmiennych oraz szablonu Jinja2. Będziemy pracować na bardzo prostym inwentarzu, który zawiera definicję tylko 4 hostów.

[msleczek@vm0-net projekt_A]$ cat inventory
10.8.232.121
10.8.232.122
10.8.232.123
10.8.232.124
[msleczek@vm0-net projekt_A]$


Żaden z tych 4 hostów nie ma uruchomionej usługi HTTP:

[msleczek@vm0-net projekt_A]$ for IP_ADDRESS in `cat inventory`; do curl http://$IP_ADDRESS; echo; done;
curl: (7) Failed to connect to 10.8.232.121 port 80: No route to host

curl: (7) Failed to connect to 10.8.232.122 port 80: No route to host

curl: (7) Failed to connect to 10.8.232.123 port 80: No route to host

curl: (7) Failed to connect to 10.8.232.124 port 80: No route to host

[msleczek@vm0-net projekt_A]$


Zadaniem naszego playbook-a będzie konfiguracja tych 4 hostów do pracy jako serwery webowe. Aby to było możliwe, muszą one posiadać oprogramowanie "httpd", odpowiednio sparametryzowany plik "index.html", włączoną usługę "httpd" oraz otwarty port dla tej usługi w systemowej zaporze sieciowej. Plik "index.html" jest parametryzowany, jako że każdy z serwerów webowych ma udostępniać nieco inną treść. Do tego celu wykorzystaliśmy szablon Jinja2. Pliki szablonów Jinja2 powinny posiadać rozszerzenie ".j2".

[msleczek@vm0-net projekt_A]$ cat index.html.j2 
Strona www na serwerze {{ ansible_default_ipv4.address }} z systemem {{ ansible_os_family }}.
Serwer ten nazywa się {{ ansible_hostname }}.
[msleczek@vm0-net projekt_A]$


W miejscu zmiennych szablonu Jinja2 "index.html.j2" powinny docelowo znaleźć się odpowiednie wartości, które zostaną zebrane w trakcie zadania "Gathering Facts". Dla każdego z hostów mogą być one różne. Do obsługi szablonów Jinja2 stosowany jest moduł "template", do instalacji potrzebnego oprogramowania moduł "yum", do uruchomienia usługi moduł "service", a do otwarcia stosownego portu w zaporze sieciowej moduł "firewalld". Zachęcam na tym etapie do zapoznania się w wynikiem polecenia "ansible-doc" dla tych modułów.

[msleczek@vm0-net projekt_A]$ cat web_servers.yaml 
---
- name: START WEB SERVERS
hosts: all
tasks:
- name: PACKAGE INSTALLATION
yum:
name: httpd
state: present
- name: ADD index.html file
template:
src: index.html.j2
dest: /var/www/html/index.html
- name: START HTTPD SERVICE
service:
name: httpd
state: started
enabled: true
- name: OPEN HTTP TRAFFIC
firewalld:
service: http
permanent: true
immediate: true
state: enabled

- name: TEST WEB SERVERS
hosts: localhost
become: false
gather_facts: false
tasks:
- name: GET WEB SERVERS LIST
command: "/usr/bin/cat ./inventory"
register: web_servers
- name: CONNECT TO WEBPAGE
uri:
url: http://{{item}}
return_content: true
status_code: 200
loop: "{{web_servers.stdout_lines}}"

[msleczek@vm0-net projekt_A]$


Playbook składa się z dwóch zbiorów zadań ("play-ów"). Pierwszy zajmuje się konfiguracją serwerów webowych, a drugi weryfikacją tego. Drugi zbiór wykonywany jest na "localhost" i ma wyłączone zadanie "Gathering Facts". W pierwszym zadaniu przypisaliśmy do zmiennej "web_servers" zawartość naszego pliku inwentarza "./inventory". W tym celu zarejestrowaliśmy wynik odpowiedniego polecenia pod tą zmienną. Służy do tego parametr "register".

W drugim zadaniu, drugiego zbioru zadań używamy modułu "uri" w celu nawiązania połączenia HTTP po kolei, do każdego z naszych serwerów webowych. Sukces tego zadania jest uwarunkowany otrzymanym kodem HTTP. Domyślnie, kod 200 oznacza sukces. Wykonanie tych testów ze stacji zarządzającej "localhost" daje pewność, że usługa "httpd" działa i otwarty został odpowiedni port na zaporze sieciowej.

Nowo zarejestrowana zmienna "web_servers" składa się z wielu wierszy. Do każdego z nich jest dostęp poprzez listę "web_servers.stdout_lines". Kiedy podamy tą listę jako argument wyrażenia "loop", to moduł "uri" zostanie wykonany tyle razy, ile na liście znajduje się wartości. W każdym kolejnym wykonaniu modułu "uri", zmienna "item" będzie przyjmowała wartość kolejnej wartości z listy, będącej argumentem wyrażenia "loop". Jest to pierwszy raz, kiedy wykorzystaliśmy w playbook-u pętlę (ang. loops). Pętlami zajmiemy się bardziej w kolejnym artykule.

[msleczek@vm0-net projekt_A]$ ansible-playbook web_servers.yaml

PLAY [START WEB SERVERS] ********************************************************************

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

TASK [PACKAGE INSTALLATION] *****************************************************************
changed: [10.8.232.122]
changed: [10.8.232.123]
changed: [10.8.232.124]
changed: [10.8.232.121]

TASK [ADD index.html file] ******************************************************************
changed: [10.8.232.122]
changed: [10.8.232.124]
changed: [10.8.232.121]
changed: [10.8.232.123]

TASK [START HTTPD SERVICE] ******************************************************************
changed: [10.8.232.122]
changed: [10.8.232.124]
changed: [10.8.232.121]
changed: [10.8.232.123]

TASK [OPEN HTTP TRAFFIC] ********************************************************************
changed: [10.8.232.122]
changed: [10.8.232.123]
changed: [10.8.232.124]
changed: [10.8.232.121]

PLAY [TEST WEB SERVERS] *********************************************************************

TASK [GET WEB SERVERS LIST] *****************************************************************
changed: [localhost]

TASK [CONNECT TO WEBPAGE] *******************************************************************
ok: [localhost] => (item=10.8.232.121)
ok: [localhost] => (item=10.8.232.122)
ok: [localhost] => (item=10.8.232.123)
ok: [localhost] => (item=10.8.232.124)

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

[msleczek@vm0-net projekt_A]$


Widać w "PLAY RECAP", że wszystkie zadania zakończyły się sukcesem.

Zatem możemy powtórzyć nasz test z początku i sprawdzić zwracaną przez serwery webowe treść.

[msleczek@vm0-net projekt_A]$ for IP_ADDRESS in `cat inventory`; do curl http://$IP_ADDRESS; echo; done;
Strona www na serwerze 10.8.232.121 z systemem RedHat.
Serwer ten nazywa się vm1-net.

Strona www na serwerze 10.8.232.122 z systemem RedHat.
Serwer ten nazywa się vm2-net.

Strona www na serwerze 10.8.232.123 z systemem RedHat.
Serwer ten nazywa się vm3-net.

Strona www na serwerze 10.8.232.124 z systemem RedHat.
Serwer ten nazywa się vm4-net.

[msleczek@vm0-net projekt_A]$


Przybliżyliśmy tutaj zaledwie podstawy zmiennych i szablonów Jinja2. Są one kluczowym elementem playbook-ów i będziemy do nich często powracać w kolejnych artykułach.


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


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.