Ansible – skuteczne i proste konfigurowanie infrastruktury

Ansible – skuteczne i proste konfigurowanie infrastruktury

W dzisiejszym wpisie znajdziecie odpowiedzi na pytania czym jest Ansible oraz w jakich sytuacjach powinniśmy z niego korzystać. Omówię podstawowe elementy tego narzędzia takie jak pliki inventory, Palybooki i moduły. Pobawimy się sterowaniem przepływem zadań dzięki zmiennym i warunkom wykonania. Poruszę temat strategii, debugowania i używania ansiblowych Ról. Na koniec wykorzystamy zdobytą wiedzę by wdrożyć prostą aplikację napisaną w Javie.

Co to jest Ansible?

Ansible został napisany przez Michael DeHaan, ale prawa autorskie do owej marki w 2015 roku nabył Red Hat. Narzędzie służy do automatyzowania konfiguracji i zarządzania zdalnymi maszynami. Dzięki niemu możemy wprowadzić podejście IaC do provisioningu, z którym wiąże się wiele zalet. Ansible jest prosty w użyciu i łatwy do przyswojenia ponieważ instrukcje zapisujemy w postaci plików yaml. Nie potrzebujemy zaawansowanej wiedzy o programowaniu obiektowym by móc się nim sprawnie posługiwać. Dodatkowo Ansible posiada bardzo dobrą dokumentację z dziesiątkami przykładów.

Poza konfiguracją jest też wykorzystywany jako narzędzie wspomagające procesy ciągłego dostarczania i wdrażania oprogramowania. W związku z powyższym w niemalże każdej ofercie pracy dla devopsa znajdziecie znajomość Ansibla jako jedną z wymaganych umiejętności.

Podstawy

Ansible jest programem napisanym w języku Python. Jest to proste narzędzie, a większość jego funkcjonalności będziemy w stanie poznać w ciągu dwóch, trzech dni. Mimo swojej prostoty ma potężne możliwości automatyzacyjne.

Zadania możemy wykonywać zarówno na systemach operacyjnych z rodziny linux jak i windows. Ansible w porównaniu do innych narzędzi konfiguracyjnych nie wymaga by na hostach docelowych znajdował się jego agent, a operacje może wykonywać równolegle na wielu maszynach. Jedynym ograniczeniem są możliwości naszego łączą i moc procesora.

Procedury dla Ansibla piszemy używając Playbooków czyli plików yaml. Dzięki temu deklarowanie zadań jest przejrzyste i intuicyjne. Nie potrzebujemy wiedzy z zakresu różnych dziedzin programowania by przewidzieć co dany fragment kodu wykona. Poniższy przykład zawiera fragment procedury, która tworzy nowego uzytkownika w bazie mysql i nadaje mu dość obszerne uprawnienia.

- name: Create database user with name 'red-devops' and password with all database privileges
  mysql_user:
    name: red-devops
    password: '{{ user_password }}'
    priv: '*.*:ALL'
    state: present

Następna część kodu jest skryptem SQL. Z pozoru wydaje się prostrzy bo zajmuje tylko dwie linijki kodu. Jeśli nie mamy wiedzy o różnych magicznych znaczkach problemem będzie dla nas zrozumienie jego działania. Przykładowo tylko dzięki zmiennej user_password możemy się domyśleć, że IDENTIFIED BY oznacza wskazanie hasła do użycia. Rzadko ktoś niezwiązany z bazami danych widzi tego typu składnię.

CREATE USER 'red-devops'@'%' IDENTIFIED BY '{{ user_password }}';
GRANT ALL PRIVILEGES ON *.* TO '*'@'%' WITH GRANT OPTION;

W porównaniu do poprzedniego pliku taki skrypt musimy sami aktualizować w miarę upływu czasu i zmian składni. A jeśli taki użytkownik już istnieje w bazie danych to w odpowiedzi skrypt zwróci błąd. W kodzie użytym przez Ansibla pod spodem zaszyta jest dość rozbudowana logika, która przed wprowadzeniem zmian sprawdza czy taki użytkownik już istnieje.

W powyższym przyładzie klucz – wartość state: present w pliku yaml oznacza jaki stan chcemy osiągnąć. Jeżeli zatem użytkownik z dokładnie takimi uprawnieniami już istnieje wówczas program nie wprowadzi zmian. W innym wypadku doprowadzi do stanu zadeklarowanego. Tym sposobem dochodzimy do jednej z najistotniejszych zalet Ansibla.

Idempotentność

Ansible jest idempotentny czyli możemy wielokrotnie wykonywać Playbooka, a zmiany na hostach zajdą tylko jeden raz (z drobnymi wyjątkami o których za chwilę). Jeśli jakaś instancja będzie spełniała wszystkie narzucone wymagania Ansible tylko to sprawdzi i nie wykonana na niej żadnych zmian. W innym wypadku narzędzie wprowadzi zmiany na hoście zgodnie z instrukcjami. Dzięki temu stan na wszystkich maszynach będzie jednolity.

Oczywiście wszystko ma swoje ograniczenia. Ansible posiada bardzo rozbudowany zakres zadań, które może wykonać. Niestety nie każde zadanie jest w stanie sprawdzić czy stan zadeklarowany został już osiągnięty. W przypadku instalacji oprogramowania jest to proste, ale co jeżeli chcielibyśmy wykonać komendę wypisującą tekst np. echo. Program nie będzie w stanie powiedzieć czy zadanie zostało wcześniej wyzwolone, więc za każdym razem zostanie wykonane ponownie.

Oto inny przykład: za każdym razem gdy zrealizujemy poniższą instrukcję Ansible stworzy kolejną instancję AWS EC2.

- name: Start an instance
  amazon.aws.ec2_instance:
    name: "AWS Instance"
    key_name: "ssh-key"
    vpc_subnet_id: subnet-xxxxx
    instance_type: t3.micro
    security_group: default
    network:
      assign_public_ip: true
    image_id: ami-123456

Ansible nie obsługuje stanu infrastruktury, więc nie może sprawdzić czy dane zadanie tworzenia nowej instancji zostało już wykonane czy nie. W tym wypadku lepiej będzie posłużyć się innymi bardziej odpowiednimi narzędziami do provisioningu infrastruktury np. CloudFormation lub Terraform.

Pliki Inventory

Jak wcześniej wspomniałem Ansible potrafi w jednej chwili wykonywać instrukcje na wielu maszynach. W przypadku maszyn z rodziny linux używa protokołu SSH, a dla windowsów posługuje się WinRM. Pliki inventory służą mu do uzyskiwania informacji potrzebnych do połączenia się z instancjami. Pliki te mogą być w dwóch formatach: YAML lub INI. Najprostrzy plik inventory może wyglądać jak niżej: zwykły adres IP lub DNS hosta.

3.126.153.234
ec2-3-126-153-234.eu-central-1.compute.amazonaws.com

Możemy też pogrupować instancje i wykonywać zadania na wszystkich lub wybranych grupach.

[frontend]
frontend-01.red-devops.com
frontend-02.red-devops.com

[backend]
backend-01.red-devops.com
backend-02.red-devops.com

[db]
database-01.red-devops.com

W tych plikach możemy umieścić dane potrzebne do połaczenia lub zmienne.

ansible_host=frontend-01.red-devops.com ansible_connections=ssh     ansible_user=root   ansible_ssh_pass=XXXX
ansible_host=database-01.red-devops.com ansible_connections=winrm   ansible_user=admin  ansible_password=XXXX

Po szczegóły odsyłam do dokumentacji.

Zazwyczaj jednak pliki inventory nie są statyczne, a adresy serwerów docelowych cały czas się zmieniają. Bardziej rozsądne jest zatem użycie dynamicznych inventory. Poniżej przykład inventory używającego plugina aws_ec2.

---
plugin: aws_ec2
regions:
  - "eu-central-1"
filters:
  instance-state-name: running
  tag:environmet: Dev
  tag:ostype: linux
compose:
  ansible_host: public_ip_address
hostnames:
  - public-ip-address

Dzięki temu rozszerzeniu Ansible wyszuka publiczne adresy IP instancji dla danego konta AWS w danym regionie, które aktualnie są w stanie running i posiadają wybrane tagi. Jest to dużo bezpieczniejsze i uniwersalne rozwiązanie.

Ansible Playbooks

Główną siłą Ansibla są tzw. Playbooki czyli pliki w formacie yaml, w których definujemy zadania do wykonania. Dzięki nim możemy instalować oprogramowanie, pracować z plikami, wykonywać skrypty, komendy, uruchamiać lub zatrzymywać procesy, konfigurować reguły sieciowe i wiele więcej.

Poniżej umieszczam pełny Playbook. Nie przestraszcie się jego składni. Tak naprawdę to prosty program. Na jego podstawie będziemy krok po kroku poznawać wszystkie najważniejsze elementy Playbooków w następnych podrozdziałach.

---

- name: Get files *.war checksum and save result
  hosts: all
  remote_user: ubuntu
  vars:
    - files_list: []
  tasks:
  -  name: Find files ending with .war
     ansible.builtin.find:
       paths: /home/ubuntu
       patterns: '*.war'
       recurse: yes
     register: output 

  - name: Remove files with "angular" in path name
    loop: "{{ output.files | map(attribute='path') | list }}"
    when: not( item | regex_search("angular") )
    ansible.builtin.set_fact:
      files_list: "{{ files_list + [item] }}"

  - name: Get stat from files
    loop: "{{ files_list }}"
    ansible.builtin.stat:
      path: "{{ item }}"
    register: files_stats

  - name: Save result on localhost
    delegate_to: localhost
    loop: "{{ files_stats.results }}"
    ansible.builtin.lineinfile:
      create: true
      path: "{{ playbook_dir }}/result.txt"
      owner: ec2-user
      line:
      - "host:{{ inventory_hostname }} checksum: {{ item.stat.checksum }} path: {{ item.item }}"

By uruchomić powyższy Playbook wykonujemy komendę:
ansible-playbook -i inventory.ini get-checksum-playbook.yaml --private-key ansible-key.pem
Poza plikiem inventory i samym Playbookiem musimy jeszcze przekazać klucz do komunikacji z instancjami. Robimy to umieszczając klucz z flagą --private-key

Struktura Playbooka jest prosta. Na samej górze widnieje nazwa playbooka „Get files *.war checksum and save result”. Następnie hosts: all co w naszym przypadku oznacza, że Playbook zostanie wykonany na wszystkich instancjach zdefiniowanych w pliku inventory. Do połączenia będziemy używać użytkownika ubuntu remote_user: ubuntu. Dodatkowo deklarujemy jedną zmienną files_list: [] i przypisujemy jej pustą tablicę. W sekcji tasks mamy cztery zadania, którę zostaną wykonane kolejno od góry.

Moduły

Moduły to pogrupowane w kategorie akcje, których możemy używać w Playbookach. Na przykład operacje na plikach są możliwe dzięki modułowi ansible.builtin.file. Moduł amazon.aws.ec2_instance pozwala na tworzenie instancji w chmurze AWS. Wcześniej przy pomocy modułu mysql_user stworzyliśmy użytkownika bazy danych. Lista wszystkich modułów jest przepoteżna. Tutaj link do strony, na której znajdziecie całą listę. Moduły rozpoczynające się od ansible.builtin możemy podawać w formie skróconej np. dla ansible.builtin.find: wystarczy tylko find:.

W naszym Playbooku posiadamy cztery moduły find, set_fact, stat i lineinfile. Każdy z nich przyjmuje pewne parametry i zwraca odpowiedź lub wykonuje operację. Niektóre parametry są wymagane.

-  name: Find files ending with .war
     ansible.builtin.find:
       paths: /home/ubuntu
       patterns: '*.war'
       recurse: yes
   register: output 

W tym przypadku moduł ansible.builtin.find wymaga informacji gdzie ma szukać plików paths: /home/ubuntu oraz jaki ma być szablon szukanych plików patterns: '*.war'. Parametrem opcjonalnym jest recurse, który domyślnie przyjmuje wartość no. Oznacza to, że moduł domyślnie sprawdziłby tylko folder wskazany w paths. Wszystkie parametry przyjmowane przez moduł wraz z przykładami można znaleźć w dokumentacji. Dodatkowo przy tym module dodaliśmy klucz – wartość register: output dzięki czemu wartości przez niego zwrócone przypisujemy zmiennej output.

Zmienne, warunki wykonania i pętle

W Playbookach Ansibla w celu dokonania dynamicznej ewaluacji zmiennych wykorzystujemy framework Jinja2. Najprostrze podstawienie zmiennej do stringa wygląda tak:

---
- name: Hello Playbook
  hosts: all
  vars:
  - user: Red-DevOps
  tasks:
    - name: Say hello
      ansible.builtin.debug:
        msg: Hello {{ user }}

W momencie wykonywania modułu debug Ansible podstawi w miejsce {{ user }} wartość „Red-DevOps”. W naszym przykładnie mamy bardziej wymagjącą konwersję wymieszaną z pętlą. Drugie zadanie różni się nieco od pozostałych modułów ponieważ set_fact wykonuje się lokalnie. Naistotniejesze jest zrozumienie linijki loop.

- name: Remove files with "angular" in path name
  loop: "{{ output.files | map(attribute='path') | list }}"
  when: not( item | regex_search("angular") )
  ansible.builtin.set_fact:
    files_list: "{{ files_list + [item] }}"

Dzięki Jinja2 dokonujemy tutaj transformacji. Wyciągamy ze zmiennej outputs dostarczonej z poprzedniego zadania atrybut files. Jest to lista różnych argumentów. Nas jednak interesuje tylko path więc filtrujemy to przez map(attribute='path') i w ostatnim kroku umieszczamy daną w liście | list. Listę możemy użyć w loop by po niej iterować.

Iterowanie w pętli wykonujemy używając słowa kluczowego item. W linijce when: not( item | regex_search("angular") ) jest warunek, który określa, dla którego item moduł set_fact doda go do listy. Sprawdzamy każdy element za pomocą wyrażenia regularnego item | regex_search("angular"). Jeśli zawiera słowo „angular” funkcja zwraca true, ale dzięki użyciu zaprzeczenia dla całej funkcji not elementy te zostaną pominięte w pętli. Na koniec files_list: "{{ files_list + [item] }}" elementy z listy, które spełniaja warunek są przypisywane do pustej tablicy zadeklarowanej na samym początku Playbooka.

W porównaniu do poprzedniego zadania „Task Get checksum from list” i „Save results on localhost” są mniej skomplikowane.

- name: Get stat from files
    loop: "{{ files_list }}"
    ansible.builtin.stat:
      path: "{{ item }}"
    register: files_stats

Wykonujemy pętlę na tablicy dostarczonej z poprzedniego zadania i z każdego zawartego tam pliku pobieramy statystyki po czym zapisujemy do zmiennej files_stats. Ostatnie zadanie też używa pętli.

- name: Save result on localhost
    delegate_to: localhost
    loop: "{{ files_stats.results }}"
    ansible.builtin.lineinfile:
      create: true
      path: "{{ playbook_dir }}/result.txt"
      owner: ec2-user
      line:
      - "host:{{ inventory_hostname }} checksum: {{ item.stat.checksum }} path: {{ item.item }}"

Jedyną nowością w tym wypadku jest słowo kluczowe delegate_to: localhost, które sprawi, że to zadanie wykona się na maszynie lokalnej. Używamy tutaj dwóch zmiennych, które są dostarczane przez Ansibla: playbook_dir i inventory_hostname. Moduł lineinfile zapisuje do pliku result.txt, informacje o adresie hosta, pełnej ścieżce do wszystkich plików z rozszerzeniem .war i checksumą tych plików.

Przed wykonaniem polecenia ansible-playbook -i inventory.ini playbook.yaml --private-key ansible-key.pem stworzyłem dwie instancje ubuntu i dodałem w nich kilka testowych plików.

Po rozpoczęciu wykonywania Playbooka możemy zauważyć dodatkowe zadanie, które nie zostało przez nas zdefiniowane. „Gathering Facts” jest do task domyślnie dodawany przez Ansibla. Jego rolą jest zebranie użytecznych informacji z naszych hostów. Mamy możliwość by to zachowanie wyłączyć. W naszym wypadku wyłączenie zbierania faktów tylko przyspieszyłoby pracę programu, ale niektóre moduły potrzebują tych dodatkowych danych do prawidłowej pracy. Należy zatem mieć ten fakt na uwadze.

Kolorem zielonym oznaczone są operacje, które zakończyły sie poprawnie i nie wprowadziły żadnych zmian. W innym odcieniu zielonego widzimy jedną operację skipping. Jest to spowodowane nie spełnieniem warunku wykonania tej operacji dla danego elementu.

W dalszej części widzimy ostatnie zadanie „Save result on localhost”. Wrzucam tylko wycinek ponieważ ilość danych jest na tyle duża, że trudno znaleźć w nich szukany element. Zaznaczyłem skąd po wybraniu item.item w ostatnim zadaniu dostajemy pełną ścieżkę do pliku.

Na koniec Ansible wyświetla podsumowanie swoich działań. Dla naszego przykładu wykonał pięć zadań na każdym z hostów i dokonał jednej modyfikacji. Zmiana dokonała się tylko na naszej maszynie. Program zapisał dane do pliku result.txt. Mimo to na zdjęciu niżej widać napis changed przy adresach zdalnych instancji. Może być to trochę mylące ponieważ na hostach nie została wprowadzona żadna zmiana, ale w ten sposób Ansible przekazuje informację, że dokonał zmiany na naszym hoście lokalnym dla ostatniego zadania.

Zawartośc pliku result.txt prezentuje się następująco:

['host:3.126.240.54 checksum: f127c7952ffec3217ba2b8073bd732097629cdeb path: /home/ubuntu/app/backend/backend.war']
['host:172.31.23.31 checksum: ae6655eb723920cba7183a8d1773448d9ae57af6 path: /home/ubuntu/app/level1/backend.war']
['host:172.31.23.31 checksum: da39a3ee5e6b4b0d3255bfef95601890afd80709 path: /home/ubuntu/app/level1/empty.war']

Strategie, Ansible Tower, debuggowanie i inne

Za pomocą strategii jesteśmy w stanie kontrolować sposób wykonywania zadań. Domyślnie Ansible czeka aż dane zadanie zostanie wykonane na wszystkich hostach by przejść do kolejnego. Jest to strategia liniowa. Możemy też wybrać strategię free co sprawi, że hosty nie będą czekać na siebie i wykonają następne zadania najszybciej jak potrafią. Istnieje dużo więcej możliwych konfiguracji. Szczegóły i przykłady znajdziecie w dokumentacji.

Inną ważną informacją jest to, że Ansible domyślnie wykonuje zadania na pięciu hostach jednocześnie. Dopiero po wykonaniu wszystkich zadań na tych hostach przejdzie do następnej grupy 5 hostów by pracować z nimi. Nadpisujac parametr forks możemy zmienić tę liczbę. Wszelkie konfigurację możemy ustawić w pliku ansible.cfg

[defaults]
forks = 30
strategy = free

Niektóre z konfiguracji możemy też ustawiać w Playbooku lub przypisywać do konkretnych zadań. Ansible pozwala też na konfigururację za pomocą zmiennych środowiskowych.

Jako ciekawostkę dodam, że istnieje graficzny interfejs użytkownika Ansible Tower pomagający w koordynowaniu i nadzorowaniu pracy zespołowej z użyciem tego narzędzia.

Naturalnie jak w wielu innych narzędziach Ansible umożliwia zwiększanie lub zmniejszanie poziomu logowanych informacji. Dodając do polecenia ansible-playbook -i inventory.yaml playbook.yaml flagę -v. Mamy możliwość ustawienia czterech poziomów logowania od najmniej dokładnego -v do czterech -vvvv, który jest najdokładniejszy.

Warta zapamiętania jest flaga --check, która dodana do polecenia powoduje, że Ansible wykona Playbook bez wprowadzania na hostach zmian. Przydaje się jeśli chcemy zobaczyć co może się stać.

Szczególnie wartym uwagi poleceniem przy pracy z dynamicznym inventory jest ansible-inventory np. ansible-inventory -i inventory_aws_ec2.yml --graph. Te polecenie wyświetli listę instancji, która zostanie zwrócona do Playbooka. Możemy ją też wykorzystać ze statycznymi plikami inventory.

Role w Ansiblu

Jest to kluczowy element narzędzia jakim jest Ansible. Daje on ogromne możliwości enkapsulacji kodu i zwiększania jego przenośności. Jego cel jest podobny do Akcji z GitHub Actions czy modułów z HasiCorp Terraform. Jeśli jednak nie spotkaliście się z wyżej wymienionymi narzędziami to wspomnę, że Role służą do grupowania zadań w większe komponenty, wydzielania ich z kodu i wykorzystywania w wielu Playbookach. Zamiast wykonywania ciągu powtarzalnych zadań i zaśmiecania Playbooka dziesiątkami linijek kodu możemy w prosty sposób wskazać Ansiblowi Role jaką ma wykonać za pomocą kilku deklaracji.

Społeczność Ansibla stworzyła ogromną liczbę gotowych do użycia Roli z dobrą dokumentacją i przykładami. Największą bazą Ról jest Ansible Galaxy. Wykorzystam prostą Rolę do instalacji javy na zdalnych hostach.

Wybrałem Rolę od użytkownika geerlingguy, a ilość jej pobrań i wynik świadczą o wysokiej jakości. Rolę możemy pobrać za pomocą prostego polecenia ansible-galaxy install geerlingguy.java. W ten sposób Rola zostanie pobrana do folderu /etc/ansible/roles gdzie domyślnie Ansible będzie jej szukał. Po przeczytaniu dokumentacji dowiemy się, że domyśla instalowana wersja to java 18.

---
- name: Install java 18
  hosts: servers
  roles:
    - role: geerlingguy.java
      become: yes

Opisane elementy narzędzia Ansible w dużym stopniu pokrywają, ale nie wyczerpują wszystkich kluczowych zagadnień. Warto zapoznać się z możliwościami rozszerzania funkcjonalności Ansibla o pluginy. Nie wspomniałem też o poleceniu ansible-vault, które pozwala na szyfrowanie i odszyfrowywanie plików.

Wdrożenie z Ansible i Github Actions

Wykorzystajmy zdobytą wiedzę w praktyce. Naszym zadaniem będzie użycie wcześniej zbudowanego artefaktu z repozytorium red-devops/CharlenesCoffeeCorner. Jest to prosta aplikacja napisana w Javie. Wdrożymy ją na dwie instancje AWS EC2 z obrazem Ubuntu. Instancje wyszukamy dynamicznie po tagach używając pluginu aws_ec2. Deployment przeprowadzimy z wykorzystaniem GitHub Actions.

Jeśli nie używałeś wcześniej GitHub Actions to zapraszam do mojego poprzedniego wpisu, z którego dowiesz się wszystkiego czego potrzebujesz.

By spełnić powyższe wymagania potrzebujemy kilku głównych plików.

  1. Plik z definicją workflow dla GitHub Actions
  2. Playbook z instrukcjami do wykonania na hostach
  3. Inventory dostarczające informację o adresach hostów

Oraz kilka dodatkowych z konfiguracją. Zacznijmy od omówienia pliku inventory.

---
plugin: aws_ec2
regions:
  - "eu-central-1"
filters:
  instance-state-name: running
  tag:environmet: Dev
  tag:ostype: linux
compose:
  ansible_host: public_ip_address
hostnames:
  - ip-address

Używamy pluginu aws_ec2. Instancje znajdują się w regionie eu-central-1. Dodajemy też stosowne tagi by wyszukać tylko te instancje, które spełniają wymagania. Domyślnie plugin aws_ec2 nie jest włączony w konfiguracji Ansibla dlatego musimy dodać odpowiednią linijkę w pliku ansible.cfg

[defaults]
remote_user = ubuntu
host_key_checking = False
enable_plugins = host_list, virtualbox, yaml, constructed, aws_ec2, script, ini, auto, toml

Dodatkowo umieszczam w konfiguracji opcję host_key_checking = False by nie sprawdzać host fingerprint przy połączeniu. W innym wypadku najpierw musielibyśmy dodać te instancje do known_host. Wpis ma za zadanie pokazać możliwości Ansible dlatego w ten sposób ułatwiłem sobie konfigurację by nie przytłaczać ilością detali.

GitHub Actions workflow wygląda nastepująco:

name: Ansible CD - CharlenesCoffeeCorner 
on: 
  workflow_dispatch:

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  AWS_DEFAULT_REGION: eu-central-1

jobs:
  Deploy-CharlenesCoffeeCorner:
    runs-on: ubuntu-latest
    steps:
      - name: Set ssh private key
        uses: webfactory/ssh-agent@v0.7.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Clone repository
        uses: actions/checkout@v3

      - name: Download artifact
        uses: dawidd6/action-download-artifact@v2
        with: 
          workflow: CharlenesCoffeeCorner-CI.yaml
          repo: red-devops/CharlenesCoffeeCorner
          
      - name: Install required software
        run: |
          sudo pip install boto3 ansible 
          ansible-galaxy install -r requirements.yaml
      
      - name: Run Ansible Playbook
        run: |
          /usr/local/bin/ansible-playbook -i inventory_aws_ec2.yaml deploy-playbook.yaml --extra-vars="workspace_path=$GITHUB_WORKSPACE" -v

Ustawiamy w naszym workflow zmienne środowiskowe AWS_ACCESS_KEY_ID i AWS_SECRET_ACCESS_KEY. Będą one potrzebne wtyczce aws_ec2 w inventory by pobrać dane o tagach z instancji i ich publiczne adresy IP.

UWAGA!!!

W powyższym przykładzie w celu komunikacji z kontem AWS używamy AWS_ACCESS_KEY_ID i AWS_SECRET_ACCESS_KEY Nie jest to zalecane podejście. Zgodnie z najnowszymi dobrymi praktykami prawidłowym rozwiązaniem jest użycie tymczasowych dostępów za pomocą np. assuming roles lub innego serwisu mogącego dostarczyć nam tymczasowe dostępy do serwisów AWS np. HashiCorp Vault. Wybrałem takie rozwiązanie ze względu na charakter szkoleniowy wpisu.

Następnie mamy pięć zadań. Pierwsze „Set ssh private key” ustawia klucz dostępu wykorzystywany do połaczenia z instancjami, dlatego w poleceniu ansible-playbook nie widzicie flagi --private-key. Następnie pobieramy repozytorium z wszystkimi wymaganymi plikami. Zadanie „Download artifact” używa akcji w celu pobrania ostatniego artefaktu z repozytorium red-devops/CharlenesCoffeeCorner.

Instalujemy potrzebne oprogramowanie, boto3 i samego Ansibla. Biblioteka boto3 jest nam potrzebne do pracy pluginu aws_ec2, a nie występuje domyślnie w obrazie ubuntu_latest dostarczonym przez GitHub Actions. Dalej widzimy polecenie ansible-galaxy install -r requirements.yaml, które instaluje Role wykorzystywane w Playbooku. Jest to jedna Rola geerlingguy.java

Na koniec wykonujemy Playbooka i przekazujemy mu zmienną workspace_path= $GITHUB_WORKSPACE, którą będziemy potrzebować przy kopiowaniu artefaktu. Dodajemy flagę -v by w logach pojawiły się logi z odpowiedzą z naszej aplikacji.

---

- name: Deploy CharlenesCoffeeCorner and run
  hosts: all
  become: yes
  vars:
    - workspace_path: ''
  tasks:
  - name: Only run "update_cache=yes" if the last one is more than 3600 seconds ago
    ansible.builtin.apt:
      update_cache: yes
      cache_valid_time: 3600

  - name: Install java
    include_role:
      name: geerlingguy.java

  - name: Create a directory if it does not exist
    ansible.builtin.file:
      path: /app/backend
      state: directory
      mode: '0755'

  - name: Copy file with owner and permissions
    ansible.builtin.copy:
      src: "{{ workspace_path }}/CharlenesCoffeeCorner-Artifact/CharlenesCoffeeCorner-1.0-SNAPSHOT.jar"
      dest: /app/backend/CharlenesCoffeeCorner-1.0-SNAPSHOT.jar

  - name: Run artifact
    ansible.builtin.command:
      chdir: /app/backend
      cmd: java -jar CharlenesCoffeeCorner-1.0-SNAPSHOT.jar s-coffee add-em bacon-roll orange-juice
    

Pozostało nam omówić sam Playbook. Łączymy się z instancjami za pomocą użytkownika ubuntu zadeklarowanego w pliku ansible.cfg. Klucz – wartość become:yes oznacza, że wszystkie nasze zadania zostaną wykonane z uprawnieniami sudo.

Pierwsze zadanie odświeża nam cache, który inaczej wyrzuci błąd przy instalacji Javy w Roli geerlingguy.java. W drugim zadaniu widzimy coś nowego. W Ansiblu najpierw wykonują się Roles, a dopiero później Tasks. Dzięki metodzie include_role możemy jednak umieścić Role pomiędzy innymi zadaniami i sterować ich kolejnością. Rola ta instaluje Jave na zdalnych hostach. Potrzebujemy jej do wystartowania artefaktu.

W punkcie trzecim tworzymy ścieżkę dla naszego artefaktu, a w czwartym kopujemy plik jar z lokalnego środowiska do zdalnych maszyn podstawiając za zmienną workspace_path ścieżkę dostarczoną z flagą --extra-vars.

Ostatnie zadanie używa modułu command. Wykonujemy na artefakcie polecenie, a dzięki fladze -v logujemy odpowiedź do konsoli.

Workflow zostanie wyzwolony za pomocą akcji workflow_dispatch. Musimy zatem ręcznie rozkazać GitHub Actions by rozpoczął wykonywanie tego pipeline.

Logi z ostatniego zadania pokazują, że aplikacja obliczyła należność prawidłowo. Poniżej widzimy podsumowanie całego Playbooka. Wszystkie pliki znajdziecie w repozytorium red-devops/Ansible-guide na GitHubie.

Powyższy przykład może nie być zbyt użyteczny ponieważ aplikacja jest desktopowa, a jej instalacja na zdalnych maszynach jest pozbawiona sensu. Niemniej w taki sam sposób postępowalibyśmy z wdrażaniem innych aplikacji, więc jak najbardziej szkielet i sposób deploymentu jest prawidłowy.

UWAGA NA ZMIANY W REPOZYTORIACH !!!

Proszę zwrócić uwagę na daty commitów, które znajdziecie we wszystkich repozytoriach dostępnych na https://github.com/red-devops. Często korzystam z wcześniejszego kodu w kolejnych wpisach, dostosowując go do bieżących zadań. W związku z tym może się zdarzyć, że kod prezentowany w tym wpisie będzie różnił się od tego, który znajdziecie w najnowszym commicie repozytorium. Niemniej jednak wszystkie główne commity zostawiam, dlatego proszę porównywać daty publikacji wpisu z datami commitów.

Podsumowanie

Ansible jest wszechstronnym, uniwersalnym i prostym narzędziem do zarządzania konfiguracją. Każdy Devops powinien rozważyć naukę tego programu ponieważ w znacznym stopniu ułatwia automatyzację powtarzalnych czynności. Jest przy tym wyjątkowo bezpieczny dzięki idempotentnemu zachowaniu. Brak agenta sprawia, że nie musimy się przejmować monitorowaniem i jego aktualizacją. Z czystym sumieniem mogę powiedzieć, że jest to jeden z lepszych softów na rynku służących do utrzymywania podejścia Infrastructure as as code.

Comments are closed.