Ansible w praktyce - piszemy playbooki

Wstęp

W poprzednim wpisie o Ansible przygotowaliśmy sobie warsztat: instalacja przez pipx, układ katalogów projektu, plik inventory i pierwsza rola common. Wszystko ładnie, tylko że rola instalująca vim i htop to trochę jak kupić wiertarkę i powiesić na niej ręcznik. Czas w końcu coś tym Ansible zrobić.

W tym wpisie napiszemy wspólnie trzy playbooki, których sam regularnie używam w homelabie. Każdy z nich przy okazji nauczy Cię jednego z mechanizmów, które w poprzedniej części tylko zapowiedziałem: pracy z faktami i warunkami, pętli oraz szablonów Jinja2. Na koniec pokażę Ci jeszcze Ansible Vault, czyli co zrobić z hasłami, żeby nie leżały w repozytorium w plaintext, oraz kilka flag ansible-playbook, które nie raz uratowały mnie przed zrobieniem sobie krzywdy.

Zakładam, że masz już strukturę projektu z poprzedniego wpisu. Jeśli nie - zajrzyj tam najpierw, to dosłownie kilka minut.

Playbook 1: Aktualizacja wszystkich serwerów

Zacznijmy od czegoś, co każdy z nas robi (a przynajmniej powinien robić) regularnie. Ile razy logowałeś się po kolei na każdą maszynę tylko po to, żeby wklepać apt update && apt upgrade? U mnie, przy kilku VM-kach na Proxmoxie i paru VPS-ach, potrafiło to zjeść pół wieczoru. A przecież dokładnie od tego mamy Ansible.

Tworzymy plik playbooks/update.yml:

# playbooks/update.yml
---
- name: Update and upgrade all servers
  hosts: all
  become: yes
  serial: 1

  tasks:
    - name: Update apt cache and upgrade packages
      ansible.builtin.apt:
        update_cache: yes
        upgrade: dist
        autoremove: yes

    - name: Check if reboot is required
      ansible.builtin.stat:
        path: /var/run/reboot-required
      register: reboot_required

    - name: Reboot server if required
      ansible.builtin.reboot:
        msg: "Reboot wykonany przez Ansible po aktualizacji pakietów"
        reboot_timeout: 600
      when: reboot_required.stat.exists

Rozbierzmy to na części, bo dzieje się tutaj kilka ciekawych rzeczy:

  • serial: 1 - instruuje Ansible, aby przechodził przez hosty pojedynczo, zamiast aktualizować wszystkie naraz. Po co? Wyobraź sobie, że nowa wersja jakiegoś pakietu okazuje się zepsuta. Bez serial położysz wszystkie maszyny jednocześnie. Z serial: 1 problem zauważysz po pierwszej i zdążysz przerwać. Na produkcji ta jedna linijka to różnica między “mieliśmy chwilową degradację” a “mieliśmy awarię”.
  • upgrade: dist - odpowiednik apt dist-upgrade, czyli aktualizacja z możliwością instalacji nowych zależności. Do wyboru masz też safe (odpowiednik zwykłego apt upgrade) oraz full.
  • register: reboot_required - zapisuje wynik zadania do zmiennej. Moduł stat sprawdza, czy istnieje plik /var/run/reboot-required - to właśnie ten plik systemy Debianowe tworzą, kiedy aktualizacja (najczęściej jądra) wymaga restartu.
  • when: reboot_required.stat.exists - warunek. Zadanie rebootu wykona się tylko wtedy, kiedy plik istnieje. To jest właśnie duet register + when, którego będziesz używać nieustannie: jedno zadanie coś sprawdza, drugie reaguje na wynik.
  • reboot - moduł, który restartuje maszynę i - co najlepsze - sam czeka, aż wróci do życia i będzie osiągalna po SSH. reboot_timeout: 600 daje jej na to 10 minut.
ℹ️
Zwróć uwagę na pełne nazwy modułów, np. ansible.builtin.apt zamiast samego apt jak w poprzednim wpisie. Obie formy działają, ale pełna nazwa (FQCN - Fully Qualified Collection Name) jest obecnie rekomendowana przez dokumentację i jednoznacznie wskazuje, skąd pochodzi moduł. Warto się przyzwyczajać od początku.

Uruchamiamy:

ansible-playbook playbooks/update.yml

I to tyle. Od dziś aktualizacja całego homelabu to jedno polecenie i można iść zrobić herbatę.

Playbook 2: Użytkownicy i klucze SSH, czyli pętle

Drugi scenariusz z życia. Dokładnie z taką sytuacją miałem do czynienia, kiedy po którejś reinstalacji maszyny okazało się, że na trzech serwerach mam użytkownika ze swoim kluczem SSH, na dwóch nie mam, a na jednym jest klucz ze starego laptopa, którego nie widziałem od roku. Spójność konfiguracji, mówili.

Zamiast klepać useradd po serwerach, opiszmy stan docelowy w zmiennych. Do pliku inventory/group_vars/all.yml dodajemy listę:

# inventory/group_vars/all.yml
---
managed_users:
  - name: przeq
    groups: sudo
    ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... przeq@laptop"
  - name: deploy
    groups: ""
    ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... deploy@ci"

Plik w group_vars/all oznacza, że zmienne będą dostępne dla wszystkich hostów z inventory. Gdybyś chciał innych użytkowników tylko na serwerach z grupy web, tworzysz analogicznie group_vars/web.yml - tak jak opisywałem układ katalogów w poprzedniej części.

Teraz playbook playbooks/users.yml:

# playbooks/users.yml
---
- name: Manage users and SSH keys
  hosts: all
  become: yes

  tasks:
    - name: Ensure users exist
      ansible.builtin.user:
        name: "{{ item.name }}"
        groups: "{{ item.groups }}"
        append: yes
        shell: /bin/bash
        state: present
      loop: "{{ managed_users }}"

    - name: Ensure SSH keys are present
      ansible.posix.authorized_key:
        user: "{{ item.name }}"
        key: "{{ item.ssh_key }}"
        state: present
      loop: "{{ managed_users }}"

Nowością jest tutaj loop. Ansible wykona zadanie tyle razy, ile elementów ma lista managed_users, a w każdej iteracji bieżący element będzie dostępny jako item. Dlatego w parametrach odwołujemy się do item.name czy item.ssh_key. Jedna definicja zadania, dowolna liczba użytkowników - dopisanie kolejnego to jedna pozycja na liście w group_vars, a nie kolejna sesja SSH.

Warto też zwrócić uwagę na append: yes w module user. Bez tego parametru Ansible ustawiłby użytkownikowi dokładnie te grupy, które podałeś, usuwając go ze wszystkich pozostałych. Z append: yes grupy są dopisywane. Ten parametr potrafi zaoszczędzić nieprzyjemnych niespodzianek.

⚠️
Moduł authorized_key pochodzi z kolekcji ansible.posix. Jeśli instalowałeś Ansible przez pipx install ansible tak jak w poprzednim wpisie, masz ją już na pokładzie. Jeśli jednak wybrałeś minimalny pakiet ansible-core, doinstalujesz ją poleceniem ansible-galaxy collection install ansible.posix.

Zanim uruchomisz playbook na ostro, sprawdź co się wydarzy:

ansible-playbook playbooks/users.yml --check --diff

O tych dwóch flagach więcej pod koniec wpisu, ale już teraz zdradzę: to najlepsi przyjaciele początkującego (i nie tylko początkującego).

Playbook 3: Szablony Jinja2, czyli MOTD z faktami

Trzeci playbook będzie najprostszy, ale nauczy Cię mechanizmu, na którym opiera się połowa prawdziwych zastosowań Ansible: szablonów. Szablon to plik z dziurami, które Ansible wypełnia zmiennymi. A skoro tak, to wykorzystamy do tego zmienne, które Ansible zbiera zupełnie za darmo - tak zwane fakty.

Fakty (facts) to informacje o hoście, które Ansible zbiera na początku każdego playbooka: nazwa hosta, wersja systemu, adresy IP, ilość RAM i dziesiątki innych. Możesz je podejrzeć poleceniem:

ansible web1 -m setup

Przygotujmy szablon wiadomości powitalnej, którą zobaczysz po zalogowaniu na serwer. Tworzymy plik playbooks/templates/motd.j2:

##################################################
#  {{ ansible_facts['hostname'] }}
#  System: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
#  IP: {{ ansible_facts['default_ipv4']['address'] }}
#
#  Maszyna zarządzana przez Ansible.
#  Zmiany wprowadzone ręcznie zostaną nadpisane!
##################################################

I playbook playbooks/motd.yml, który go rozpropaguje:

# playbooks/motd.yml
---
- name: Deploy MOTD banner
  hosts: all
  become: yes

  tasks:
    - name: Template MOTD file
      ansible.builtin.template:
        src: templates/motd.j2
        dest: /etc/motd
        owner: root
        group: root
        mode: '0644'

Po uruchomieniu playbooka każda maszyna dostanie swój własny, spersonalizowany baner - z własnym hostname, wersją systemu i adresem IP. Ten sam szablon, różne wyniki na każdym hoście. Dokładnie tak samo działa generowanie konfiguracji nginx, konfiguracji agenta Zabbix czy czegokolwiek innego - różnica polega tylko na tym, że zamiast faktów podstawiasz swoje zmienne z group_vars albo defaults roli.

A ostatnia linijka szablonu to nie żart. Jeżeli plik jest zarządzany przez Ansible, to Ansible jest jedynym miejscem, w którym się go edytuje. Ręczna poprawka na serwerze zniknie przy następnym uruchomieniu playbooka - i dobrze, na tym polega utrzymywanie spójności.

ℹ️
Gdy szablonujesz konfigurację usługi (a nie zwykły plik jak MOTD), pamiętaj o dwóch rzeczach z poprzedniego wpisu: dodaj notify wskazujący handler restartujący usługę oraz - jeśli program to umożliwia - parametr validate, np. validate: 'nginx -t -c %s'. Dzięki temu Ansible odrzuci zepsutą konfigurację, zanim ta trafi na swoje miejsce.

Ansible Vault, czyli co z hasłami

Prędzej czy później w Twoich zmiennych pojawi się coś wrażliwego: hasło do bazy, token API, cokolwiek. Trzymanie tego w repozytorium otwartym tekstem to proszenie się o kłopoty. Odpowiedzią Ansible jest Vault - wbudowane szyfrowanie plików ze zmiennymi.

Zanim utworzymy zaszyfrowany plik, mała zmiana organizacyjna. Chcemy trzymać zmienne jawne i zaszyfrowane osobno, a Ansible pozwala zamiast pojedynczego pliku group_vars/all.yml użyć katalogu group_vars/all/ i wczytać z niego wszystkie pliki. Uwaga: nie można mieć obu naraz - plik all.yml i katalog all/ obok siebie skończą się błędem. Dlatego najpierw przenosimy dotychczasowe zmienne:

mkdir inventory/group_vars/all
mv inventory/group_vars/all.yml inventory/group_vars/all/vars.yml

Teraz tworzymy zaszyfrowany plik obok:

ansible-vault create inventory/group_vars/all/vault.yml

Ansible zapyta o hasło, a następnie otworzy edytor. Wpisujemy zmienne jak do każdego innego pliku:

vault_grafana_admin_password: "SuperTajneHaslo123"

Po zapisaniu plik na dysku wygląda tak:

$ANSIBLE_VAULT;1.1;AES256
36613831356362376361643230623232616139383564383936633463323...

Możesz go spokojnie commitować do repozytorium - bez hasła nikt go nie odczyta. W playbookach używasz zmiennej normalnie, natomiast przy uruchamianiu dodajesz flagę:

ansible-playbook playbooks/site.yml --ask-vault-pass

Do edycji zaszyfrowanego pliku służy ansible-vault edit, a do podejrzenia ansible-vault view.

⚠️
Przyjęła się konwencja, aby zmienne z Vault poprzedzać prefixem vault_. Kiedy w playbooku widzisz {{ vault_grafana_admin_password }}, od razu wiesz, gdzie szukać jej definicji i że jest wrażliwa. Drobiazg, a przy większym projekcie robi ogromną różnicę.

Flagi, które uratują Ci skórę

Na koniec obiecany zestaw flag ansible-playbook, których używam praktycznie przy każdym uruchomieniu:

  • --check - tryb “na sucho”. Ansible przechodzi przez wszystkie zadania i raportuje, co by zmienił, ale niczego nie dotyka. Idealne przed pierwszym uruchomieniem nowego playbooka.
  • --diff - pokazuje różnice w modyfikowanych plikach, w formacie znanym z git diff. W duecie z --check dostajesz pełny podgląd zmian, zanim się na nie zgodzisz.
  • --limit web1 - ogranicza wykonanie do wskazanego hosta lub grupy, niezależnie od tego, co playbook ma w hosts. Nowy playbook testuję zawsze najpierw na jednej, najmniej ważnej maszynie.
  • --tags users - wykonuje tylko zadania oznaczone danym tagiem. Tagi dodajesz do zadania parametrem tags: ['users']. Przy dużym playbooku oszczędza to mnóstwo czasu, bo nie musisz przechodzić przez wszystko, żeby poprawić jedną rzecz.
  • --syntax-check - błyskawiczna weryfikacja składni playbooka, bez łączenia się z hostami. YAML bywa złośliwy w kwestii wcięć, więc warto odpalać ją nawykowo.

Mój typowy cykl pracy z nowym playbookiem wygląda więc tak:

ansible-playbook playbooks/users.yml --syntax-check
ansible-playbook playbooks/users.yml --check --diff --limit web1
ansible-playbook playbooks/users.yml --limit web1
ansible-playbook playbooks/users.yml

Od składni, przez próbę generalną na jednej maszynie, aż po całą infrastrukturę. Nudno? Może trochę. Ale jeszcze nigdy tym sposobem nie położyłem sobie całego homelabu naraz, a zdarzało mi się to w czasach, kiedy flagi --check używałem wyłącznie od święta.

Podsumowanie

W tym wpisie przeszliśmy od teorii do praktyki:

  • Playbook aktualizujący wszystkie serwery z kontrolowanym restartem (register, when, serial)
  • Zarządzanie użytkownikami i kluczami SSH z jednej listy zmiennych (loop, group_vars)
  • Szablony Jinja2 wypełniane faktami hosta (template, facts)
  • Szyfrowanie wrażliwych zmiennych z Ansible Vault
  • Flagi --check, --diff, --limit i --tags, czyli siatka bezpieczeństwa przy każdym uruchomieniu

Te trzy playbooki możesz potraktować jako szkielet i rozbudowywać o własne potrzeby - dorzucić instalację agenta Zabbix do monitoringu, konfigurację WireGuard czy cokolwiek, co do tej pory klepałeś ręcznie. Zasada jest zawsze ta sama: opisujesz stan docelowy, a Ansible martwi się resztą.

W kolejnych krokach warto zerknąć na przenoszenie playbooków do ról (o strukturze ról pisałem w części pierwszej), a docelowo na uruchamianie ich automatycznie z CI. Ale to już materiał na osobny wpis.

Powodzenia w automatyzacji!

Comments powered by Talkyard.