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. Bezserialpołożysz wszystkie maszyny jednocześnie. Zserial: 1problem 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- odpowiednikapt dist-upgrade, czyli aktualizacja z możliwością instalacji nowych zależności. Do wyboru masz teżsafe(odpowiednik zwykłegoapt upgrade) orazfull.register: reboot_required- zapisuje wynik zadania do zmiennej. Modułstatsprawdza, 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 duetregister+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: 600daje jej na to 10 minut.
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.
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.
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.
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 zgit diff. W duecie z--checkdostajesz 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 whosts. Nowy playbook testuję zawsze najpierw na jednej, najmniej ważnej maszynie.--tags users- wykonuje tylko zadania oznaczone danym tagiem. Tagi dodajesz do zadania parametremtags: ['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,--limiti--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.