Deklaratywny język opisu infrastruktury

Zarówno Ansible jak i Terraform to narzędzia, w których stosuje się deklaratywne podejście do programowania infrastruktury. Co ono oznacza? Wyjaśnijmy to sobie na bardzo prostym przykładzie zadania, które zaprogramujemy w playbooku Ansible.

– name: Serwer WWW
  ansible.builtin.apt:
    name: [ nginx ]
    update_cache: yes
state: latest
  become: yes

Zastanów się teraz jaką czynność wykonujemy takim zadaniem? Jakie operacje w systemie są wykonywane? Jeżeli twoja odpowiedź brzmiała, że uruchamiając to zadanie instalujemy za pomocą managera pakietów apt oprogramowaniem nginx, to taka odpowiedź nie jest do końca poprawna. A dlaczego?

Rzeczywiście, w przypadku, jeżeli pakiet nginx nie jest w systemie zainstalowany, moduł ansible.builtin.apt przeprowadzi wszystkie czynności związane z jego instalacją. Zaprogramowane one są w logice wykonania samego modułu. Odtworzy on czynności podobne do tych, które wykonalibyśmy samodzielnie – połączy się ze zdalnym systemem i używając managera pakietów apt zainstaluje nginx. Ale przecież to nie jest jedyna sytuacja, którą możemy napotkać. Administrotor uruchamiając playbook może nie wiedzieć jaka jest konfiguracja systemu operacyjnego wskazanego serwera. Co się stanie, jeżeli pakiet już jest zainstalowany? W takiej sytuacji zostanie on zaktualizowany do najnowszej wersji lub jeżeli najnowsza wersja jest już zainstalowana to konfiguracja systemu nie ulegnie zmianie.

Skąd taka logika działania modułu i gdzie się podziało typowe programowanie znane choćby z języka Python?

Ansible czy Terraform to produkty, których działanie opiera się na języku deklaratywnym, natomiast pisanie programów w Pythonie na języku proceduralnym. W tym drugim definiujemy logikę wykonywania kolejnych operacji, aby osiągnąć wskazany stan. W modelu deklaratywnym jedynie opisujemy stan, który chcemy, aby został osiągnięty. To do zadań stosowanego przez nas narzędzia należy takie wykonanie operacji, aby uzyskać wskazany przez nas stan. Warto zaznaczyć, że taki stan może być określony przez cały zestaw parametrów. W powyższym przykładzie wskazaliśmy, że chcemy, aby w systemie zainstalowana była najnowsza dostępna wersja (state: latest) pakietu nginx (name: [ nginx ]) oraz uaktualniona o listę wszystkich najnowszych pakietów dostępnych w skonfigurowanych repozytoriach pamięć podręczna programu apt (update_cache: yes). W module ansible.builtin.apt w sposób już proceduralny zaprogramowana została przez twórców odpowiednia kolejność i logika wykonywania operacji, aby taki stan osiągnąć.

W Terraform jest także narzędziem, za pomocą którego infrastrukturę programujemy w sposób deklaratywny posługując się językiem HCL.

resource „aws_instance” „ec2_server” { 
ami           = „ami-12345”
  instance_type = „t2.micro”
  count         = 2
}

W przytoczonym przykładzie określamy stan, w którym w zarządzanej przez nas infrastrukturze uruchomione są dwa wirtualne serwery wskazanego typu.

Model deklaratywny pozwala na proste skalowanie infrastruktury. Jeżeli w naszej instalacji dwa serwery okazują się w pewnym momencie niewystarczające dla naszych potrzeb to w prosty sposób, korzystając z tego samego przepisu zawartego w kodzie, możemy zwiększyć ich liczbę do na przykład czterech. Wystarczy, że zmienimy wartość parametru count i ponownie wykonamy nasz skrypt.

resource „aws_instance” „ec2_server” {
  ami           = „ami-12345”
  instance_type = „t2.micro”
  count         = 4
}

Terraform dokona analizy uruchomionej infrastruktury i będzie starał się doprowadzić ją do nowego stanu, w którym w naszej usłudze uruchomione będą cztery wirtualne serwery.

Idempotencja

Z deklaratywnym podejściem do modelu opisu infrastruktury łączy się nierozerwalnie pojęcie idempotentności stanu końcowego. Jest ono zaczerpnięte z matematyki. Oznacza ono, że jeżeli stan opisany w zadaniu jest osiągnięty to nie wykonujemy żadnych operacji. Mówiąc prościej, oznacza to, że kiedy uruchamiamy jakiekolwiek zadanie, wymagane zmiany w stanie sytemu docelowego są dokonywane, a dalsze powtórzenia tego zadania na tym samym urządzeniu docelowym nie powodują żadnej zmiany jego stanu. Ponowne wykonanie zadania z pierwszego przykładu w tym artykule nie oznacza, że ponownie próbujemy zainstalować pakiet nginx, a tym bardziej nie odinstalowujemy go i nie instalujemy ponownie. Dlatego wykonanie zaprogramowanego przez nas playbooka może skutkować ominięciem wykonania poszczególnych zadań, jeżeli stan systemu nie ulega zmianie. W przykładzie z Terraforma jeżeli dwie maszyny wirtualne są już uruchomione, to nie usuwamy ich i nie powołujemy na ich miejsce dwóch nowych. Tutaj także stan infrastruktury nie ulega zmianie.

Większość modułów Ansible ma wbudowaną obsługę idempotencji, ale są pewne wyjątki. Do takich należą choćby często wykorzystywane ansible.builtin.command czy ansible.builtin.shell, za pomocą których wykonamy określone polecenia powłoki (shell). Ich wielokrotne wywołanie w naszym playbooku powoduje wielokrotne wykonanie wskazanego polecenia na zdalnym systemie. My, jako programiści playbooków musimy pamiętać o takich niuansach.

Idempotencję możemy też naruszyć nieumiejętnie lub nierozważnie określając parametry zadania. Oto przykład:

– name: Serwer WWW
  ansible.builtin.service:
    name: „nginx”

   state: restarted
  become: yes

Atrybut state jest w zadaniu ustawiony na restarted. Oznacza to wymuszenie ponownego uruchomienia wskazanej usługi. Idempotencja w tym przypadku nie zostanie zachowana, gdyż każdorazowe wykonanie tego zadania spowoduje restart usługi. Wynika to z natury działania określonego za pomocą parametru state stanu. Ansible sam z siebie nie ma żadnej możliwości sprawdzenia, że usługa dopiero co została zrestartowana. Dlatego, jeżeli celem zaprogramowanego zadania jest uruchomienie serwisu serwera nginx powinniśmy ustawić stan na started. Pamiętajmy, aby przed wykorzystaniem dowolnych budowanych lub pochodzących z kolekcji modułów Ansible przeczytać dokładnie ich dokumentację i zrozumieć działanie poszczególnych parametrów. W szczególności dotyczy to właśnie parametru state czy count.