Ansible w praktyce
Po tej lekcji: kursant pisze inwentarz i playbook Ansible, używa komend ad-hoc i modułów apt, dnf, copy, template, lineinfile, user, service, firewalld, rozumie handlery i zmienne, chowa hasła w ansible-vault i przed każdą zmianą robi suchy przebieg --check --diff.
Wstęp
Po lewej skrypt, który instaluje nginx, wrzuca konfigurację, zakłada konta i otwiera port. Działa. Raz. Odpalisz drugi raz i dopisze tę samą linijkę do konfiguracji drugi raz. Na RHEL wywali się na apt. Po prawej to samo w Ansible. Możesz go uruchomić sto razy, a za drugim razem powie: „wszystko już jest, nic nie robię”.
Skrypt to przepis: „wlej, zamieszaj, piecz”. Ansible to zdjęcie gotowego ciasta z podpisem: „ma wyglądać tak”. Kucharz sam sprawdza, czego brakuje, i robi tylko to. Ansible to lista zadań opisująca stan docelowy. Dziś stawiamy nim dwa serwery WWW.
Instalacja i inwentarz
Ansible instalujesz tylko na swoim komputerze, czyli na węźle sterującym. Na serwerach nie instalujesz nic. Wystarczy im SSH i Python, który na Ubuntu i RHEL już jest.
sudo apt install ansible # [Ubuntu] razem z kolekcjami modułów
sudo dnf install ansible-core # [RHEL] sam rdzeń
ansible-galaxy collection install ansible.posix # [RHEL] tu jest moduł firewalld
ansible --versionU mnie ansible [core 2.21.6]. Wersja w repozytorium Twojej dystrybucji będzie starsza, ale wszystko z tej lekcji zadziała.
Inwentarz to lista gości z przydziałem do stolików. Kto jest kim i w jakiej grupie:
# inventory.ini
[web]
web1
web2
[db]
db1
[produkcja:children]
web
db
[lokalny]
localhost ansible_connection=localNazwy web1, web2, db1 biorą się z Twojego ~/.ssh/config z poprzedniej lekcji. Ansible łączy się zwykłym SSH, więc używa tej samej książki adresowej. Bez niej dopisujesz adres w inwentarzu: web1 ansible_host=192.168.122.11 ansible_user=eryk. Grupa produkcja składa się z grup, a ansible_connection=local znaczy „nie łącz się nigdzie, rób u mnie”. Świetne do ćwiczeń.
Obok kładziesz ansible.cfg, żeby nie podawać inwentarza za każdym razem:
[defaults]
inventory = inventory.iniansible-inventory --graph@all:
|--@ungrouped:
|--@produkcja:
| |--@web:
| | |--web1
| | |--web2
| |--@db:
| | |--db1
|--@lokalny:
| |--localhostAd-hoc, czyli jedno polecenie do wszystkich
Pierwsze, co robisz z nowym inwentarzem: sprawdzasz, czy wszyscy słyszą.
ansible produkcja -m pingweb1 | SUCCESS => {
"changed": false,
"ping": "pong"
}I tak samo web2 i db1. Uwaga, to nie jest ping z lekcji o sieci. Ten ping loguje się przez SSH i sprawdza, czy na serwerze działa Python. Jak sprawdzenie, czy kucharz nie tylko jest w kuchni, ale też umie zapalić kuchenkę.
-m to moduł, czyli narzędzie, którym Ansible coś robi. -a to argumenty. Twoja pętla z poprzedniej lekcji w jednej linijce:
ansible web -m shell -a 'uptime -p; df -h / | tail -1'web2 | CHANGED | rc=0 >>
up 6 hours, 26 minutes
/dev/vda1 25G 5.8G 18G 25% /
web1 | CHANGED | rc=0 >>
up 6 hours, 26 minutes
/dev/vda1 25G 6.1G 18G 26% /Równolegle, z podpisami. Domyślnie pięć serwerów naraz, zmienisz to przez -f 20. Moduł shell zawsze mówi CHANGED, bo nie wie, co Twoja komenda zrobiła. Ansible wie tylko tyle, że coś uruchomił. Teraz moduł, który wie:
ansible web1 -b -m apt -a 'name=figlet state=present'
ansible web1 -b -m apt -a 'name=figlet state=present'web1 | CHANGED => {
"changed": true,
web1 | SUCCESS => {
"changed": falseZa pierwszym razem zainstalował. Za drugim sprawdził, że jest, i nic nie zrobił. To jest idempotencja: ten sam efekt bez względu na to, ile razy klikniesz. Jak przycisk w windzie: naciśniesz pięć razy „3” i dalej jedziesz na trzecie, a nie na piętnaste. -b to „become”, czyli sudo. Jeśli sudo chce hasła, dodaj -K i Ansible raz o nie zapyta.
Kopiowanie pliku na serwery to moduł copy:
ansible db -b -m copy -a 'src=motd.txt dest=/etc/motd mode=0644'Playbook, czyli lista zadań w YAML-u
Ad-hoc jest dobre do pytań. Do budowania serwerów piszesz playbook: plik YAML z listą zadań. YAML to format, w którym wcięcia są częścią składni. Dwie spacje, nigdy tabulator. Jak w wojsku: krok w lewo, krok w prawo i masz błąd składni.
Zmienne grupy web w osobnym pliku group_vars/web/vars.yml. Ansible sam go wczyta dla każdego serwera z tej grupy:
---
http_port: 8080
strona_tytul: "Sklep z kapciami"
admini:
- ola
- bartekI playbook web.yml:
---
- name: Serwery WWW z nginx
hosts: web
become: true
tasks:
- name: Zainstaluj nginx (Ubuntu/Debian)
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
when: ansible_facts['os_family'] == 'Debian'
- name: Zainstaluj nginx (RHEL/Rocky/Alma)
ansible.builtin.dnf:
name: nginx
state: present
when: ansible_facts['os_family'] == 'RedHat'
- name: Ukryj wersję nginx w nagłówkach
ansible.builtin.lineinfile:
path: /etc/nginx/nginx.conf
regexp: '^\s*#?\s*server_tokens'
line: "\tserver_tokens off;"
insertafter: '^http \{'
notify: Przeładuj nginx
- name: Usuń domyślną stronę (Ubuntu/Debian)
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
when: ansible_facts['os_family'] == 'Debian'
notify: Przeładuj nginx
- name: Konfiguracja strony
ansible.builtin.template:
src: strona.conf.j2
dest: /etc/nginx/conf.d/strona.conf
owner: root
group: root
mode: "0644"
notify: Przeładuj nginx
- name: Folder strony
ansible.builtin.file:
path: /var/www/strona
state: directory
mode: "0755"
- name: Strona główna
ansible.builtin.template:
src: index.html.j2
dest: /var/www/strona/index.html
mode: "0644"
- name: Konta adminów
ansible.builtin.user:
name: "{{ item }}"
groups: "{{ 'sudo' if ansible_facts['os_family'] == 'Debian' else 'wheel' }}"
append: true
shell: /bin/bash
loop: "{{ admini }}"
- name: Otwórz port w zaporze (RHEL)
ansible.posix.firewalld:
port: "{{ http_port }}/tcp"
permanent: true
immediate: true
state: enabled
when: ansible_facts['os_family'] == 'RedHat'
- name: Nginx działa i startuje z systemem
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Przeładuj nginx
ansible.builtin.service:
name: nginx
state: reloadedCzytamy od góry. hosts: web to dla kogo. become: true to sudo dla całości. Każde zadanie ma name, czyli opis po ludzku, i jeden moduł z parametrami. Prawie wszystkie mówią o stanie, nie o czynności: state: present to „ma być zainstalowany”, state: absent to „ma go nie być”, state: started to „ma działać”.
whento bramkarz. Ansible na początku zbiera fakty o każdym serwerze, czyli system, wersję, adresy, pamięć.ansible_facts['os_family']toDebianna Ubuntu iRedHatna RHEL, Rocky i Almie. Jeden playbook, dwie rodziny.lineinfilepilnuje jednej linijki w pliku: znajdzie tę pasującą doregexpi podmieni ją naline, a jak nie znajdzie, dopisze. Nigdy dwa razy.userzloopzakłada każde konto z listy. Na Ubuntu admini są w grupiesudo, na RHEL wwheel.firewalldna RHEL otwiera port na stałe (permanent) i od razu (immediate). Na Ubuntu odpowiednikiem jestcommunity.general.ufw.servicepilnuje, żeby usługa działała i wstawała z systemem. Pod spodem na obu systemach rozmawia z systemd.
Pełna nazwa ansible.builtin.apt mówi, z której kolekcji jest moduł. Krótkie apt: też zadziała, ale linter będzie marudził.
Szablony i zmienne
copy wrzuca plik jak jest. template najpierw wypełnia luki. Jak formularz w urzędzie: ten sam druk, ale każdy serwer wpisuje swoje dane. Szablony pisze się w języku Jinja2, zmienne w podwójnych klamrach. templates/strona.conf.j2:
# {{ ansible_managed }}
server {
listen {{ http_port }};
server_name {{ inventory_hostname }};
root /var/www/strona;
index index.html;
}I templates/index.html.j2:
<h1>{{ strona_tytul }}</h1>
<p>Serwer: {{ inventory_hostname }}, system: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}</p>inventory_hostname to nazwa serwera z inwentarza, a ansible_managed wstawia ostrzeżenie „Ansible managed”. To tabliczka dla kolegów: nie poprawiaj ręcznie, bo i tak nadpiszę. Zmienne mogą przyjść z wielu miejsc: z group_vars, z host_vars/web1.yml dla jednego serwera, z sekcji vars: w playbooku albo z linii komend przez -e http_port=9090. Ta ostatnia wygrywa ze wszystkim.
--check --diff, czyli przymiarka przed szyciem
Zanim cokolwiek zmienisz na produkcji, robisz suchy przebieg. --check mówi „pokaż, co byś zrobił, ale nic nie rób”, a --diff dokłada różnice w plikach:
ansible-playbook web.yml --check --diffTASK [Ukryj wersję nginx w nagłówkach] ****************************************
@@ -18,7 +18,7 @@
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
- # server_tokens off;
+ server_tokens off;
TASK [Konfiguracja strony] *****************************************************
@@ -0,0 +1,7 @@
+# Ansible managed
+server {
+ listen 8080;
+ server_name web1;
...
TASK [Otwórz port w zaporze (RHEL)] ********************************************
skipping: [web1]
skipping: [web2]Wygląda dobrze? Dopiero wtedy prawdziwy przebieg:
ansible-playbook web.yml -l web1
curl http://web1:8080/TASK [Ukryj wersję nginx w nagłówkach] *****************************************
changed: [web1]
...
TASK [Konta adminów] ***********************************************************
changed: [web1] => (item=ola)
changed: [web1] => (item=bartek)
TASK [Otwórz port w zaporze (RHEL)] ********************************************
skipping: [web1]
TASK [Nginx działa i startuje z systemem] **************************************
changed: [web1]
RUNNING HANDLER [Przeładuj nginx] **********************************************
changed: [web1]
PLAY RECAP *********************************************************************
web1 : ok=10 changed=8 unreachable=0 failed=0 skipped=2 ...
<h1>Sklep z kapciami</h1>
<p>Serwer: web1, system: Ubuntu 24.04</p>-l web1 to „tylko ten jeden”. Na pierwszym serwerze sprawdzasz, potem puszczasz na resztę. U mnie nginx był już zainstalowany, więc to zadanie mówiło ok.
I najpiękniejsze zastosowanie --check --diff: wykrywanie samowolki. Ktoś poprawił stronę ręcznie na serwerze. Suchy przebieg od razu to pokazuje:
TASK [Strona główna] ***********************************************************
--- before: /var/www/strona/index.html
@@ -1,3 +1,2 @@
<h1>Sklep z kapciami</h1>
<p>Serwer: web1, system: Ubuntu 24.04</p>
-web1 ręcznie poprawionePrawdziwe uruchomienie przywróci stan z playbooka. Ansible jest jak mama, która wie, gdzie miały stać kapcie, i co wieczór je tam odstawia.
Handlery, czyli „przeładuj, ale tylko jeśli trzeba”
Przeładowanie nginx po każdym uruchomieniu playbooka byłoby jak restart routera co godzinę „na wszelki wypadek”. Handler to zadanie, które uruchamia się tylko wtedy, gdy ktoś go zawołał przez notify, i tylko wtedy, gdy to zadanie coś naprawdę zmieniło. Do tego raz, na końcu, nawet jeśli wołały go trzy zadania. Jak kelner, który zbiera zamówienia od całego stolika i idzie do kuchni raz, a nie po każdym gościu.
W przebiegu wyżej trzy zadania wołały Przeładuj nginx, a handler wykonał się raz, po wszystkich zadaniach. Przy drugim uruchomieniu nic się nie zmienia, więc nikt go nie woła i nginx nie jest niepotrzebnie szarpany.
ansible-vault, czyli sejf na hasła
Playbooki trzymasz w Gicie z lekcji 101. Hasło do bazy w zwykłym pliku YAML to znowu zdjęcie karty płatniczej na grupie. ansible-vault szyfruje plik hasłem:
ansible-vault create group_vars/web/vault.yml # nowy, od razu w edytorze
ansible-vault encrypt group_vars/web/vault.yml # albo istniejący
head -2 group_vars/web/vault.yml
ansible-vault view group_vars/web/vault.yml$ANSIBLE_VAULT;1.1;AES256
34313734643332323830306538363735613135613331643232666565666638646462376566623563
---
vault_db_haslo: "Kapc1e-2026!"Zaszyfrowany plik spokojnie commitujesz. Konwencja: w sejfie zmienne z przedrostkiem vault_, a w zwykłym vars.yml jawne db_haslo: "{{ vault_db_haslo }}". Dzięki temu grep db_haslo dalej znajdzie, skąd się bierze hasło. Użycie w zadaniu, z no_log, żeby hasło nie wyciekło na ekran ani do logów:
- name: Plik z hasłem do bazy
ansible.builtin.template:
src: db.conf.j2
dest: /etc/sklep/db.conf
owner: root
group: root
mode: "0600"
no_log: trueUruchomienie bez hasła do sejfu:
[ERROR]: Attempting to decrypt but no vault secrets found.Więc ansible-playbook baza.yml --ask-vault-pass albo --vault-password-file ~/.vault_pass z plikiem chmod 600, który nie trafia do repozytorium.
Uwaga, pułapka: sejf leży w group_vars/web/, więc Ansible próbuje go otworzyć przy każdym playbooku dla grupy web. U mnie po dodaniu sejfu nawet web.yml, który hasła do bazy w ogóle nie używa, zatrzymał się na tym samym błędzie. Hasło do sejfu podajesz więc zawsze albo ustawiasz vault_password_file w ansible.cfg.
Jak to działa pod spodem
Ansible nie ma na serwerach żadnego agenta. Podejrzyjmy z -vvv:
<db1> SSH: EXEC ssh ... db1 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo /home/eryk/.ansible/tmp `"...
<db1> PUT /home/eryk/.ansible/tmp/ansible-local-21756x13phrbg/tmpby7m0tg6 TO /home/eryk/.ansible/tmp/ansible-tmp-.../AnsiballZ_ping.py
<db1> SSH: EXEC ssh ... -tt db1 '/bin/sh -c '"'"'/usr/bin/python3.12 /home/eryk/.ansible/tmp/ansible-tmp-.../AnsiballZ_ping.py ...
<db1> SSH: EXEC ssh ... db1 '/bin/sh -c '"'"'rm -f -r /home/eryk/.ansible/tmp/ansible-tmp-.../ > /dev/null 2>&1'"'"''Dla każdego zadania: tworzy folder tymczasowy, wysyła przez SFTP mały program w Pythonie z modułem w środku, uruchamia go, odbiera wynik w JSON-ie i sprząta. Moduł sam porównuje stan obecny z docelowym i mówi changed: true albo false. Stamtąd biorą się kolory: zielony ok, żółty changed, czerwony failed. Zadania idą po kolei, ale każde na wszystkich serwerach naraz, po pięć (forks).
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
ansible | -m MODUŁ -a 'ARG' | ad-hoc: jeden moduł na grupie |
ansible | -b / -K | sudo / zapytaj o hasło do sudo |
ansible | -f 20 | 20 serwerów naraz zamiast 5 |
ansible-playbook | --check --diff | suchy przebieg z różnicami |
ansible-playbook | -l web1 | tylko wybrane serwery |
ansible-playbook | -e zmienna=wartość | zmienna z najwyższym priorytetem |
ansible-playbook | --syntax-check | tylko sprawdź składnię |
ansible-playbook | --force-handlers | uruchom handlery nawet po błędzie |
ansible-playbook | --ask-vault-pass | zapytaj o hasło do sejfu |
ansible-playbook | -v / -vvv | więcej szczegółów / wszystko, z SSH |
ansible-vault | create / edit / view | nowy / edycja / podgląd sejfu |
ansible-vault | encrypt / decrypt | zaszyfruj / odszyfruj plik |
ansible-inventory | --graph | drzewko grup i serwerów |
ansible-doc | MODUŁ | dokumentacja modułu z przykładami |
⚠ Typowy błąd
Zmieniasz konfigurację, playbook leci, a kilka zadań dalej coś się wysypuje. Naprawiasz, uruchamiasz ponownie, wszystko zielone. A nginx dalej działa ze starą konfiguracją. Sprawdziłem to na prostym playbooku:
TASK [Zmień konfigurację] ******************************************************
changed: [localhost]
TASK [Coś, co się wysypie] *****************************************************
fatal: [localhost]: FAILED! => {"changed": false, ... "rc": 1 ...}Drugie uruchomienie, już bez błędu w konfiguracji:
TASK [Zmień konfigurację] ******************************************************
ok: [localhost]I handler już się nie odpali. Nigdy.
Dlaczego? Handlery uruchamiają się na końcu, a przy błędzie Ansible na danym serwerze przerywa i do końca nie dochodzi. Zamówienie przepadło. Przy drugim uruchomieniu plik już jest poprawny, więc zadanie mówi ok, nikt nie woła handlera, a usługa cały czas pracuje na starej konfiguracji. Naprawa: ansible-playbook web.yml --force-handlers albo force_handlers: true w playbooku. A gdy już wpadłeś, przeładuj ręcznie: ansible web -b -m service -a 'name=nginx state=reloaded'.
Drugi klasyk: UNREACHABLE! z Permission denied (publickey). To nie Ansible, tylko zwykłe SSH. Sprawdź najpierw ssh web1 bez Ansible.
✎ Zadanie
Na dwóch maszynach wirtualnych (albo na jednej i localhost z ansible_connection=local) zrób inwentarz z grupą web. Napisz playbook, który: instaluje nginx (z when na Ubuntu i RHEL), wrzuca szablonem stronę z nazwą serwera i zmienną strona_tytul, zakłada użytkownika deploy i pilnuje, żeby nginx działał. Zaszyfruj ansible-vault plik ze zmienną vault_db_haslo i wrzuć ją do /etc/sklep/db.conf z prawami 0600. Uruchom najpierw --check --diff, potem naprawdę, potem drugi raz i sprawdź, czy w podsumowaniu jest changed=0. Zmień ręcznie plik strony i wykryj to suchym przebiegiem.
Pokaż rozwiązanie
Większość masz już w playbooku web.yml z poprzedniej lekcji. Do zadania dokładasz konto deploy i plik z hasłem:
- name: Konto deploy
ansible.builtin.user:
name: deploy
shell: /bin/bashansible-vault create group_vars/web/vault.yml # vault_db_haslo: "..."
echo 'db_haslo: "{{ vault_db_haslo }}"' >> group_vars/web/vars.yml
ansible-playbook web.yml --check --diff --ask-vault-pass
ansible-playbook web.yml --ask-vault-pass
ansible-playbook web.yml --ask-vault-pass # drugi razW drugim przebiegu w PLAY RECAP ma być changed=0. Jeśli coś dalej świeci się na żółto, to zwykle shell albo command bez changed_when, bo Ansible nie wie, czy Twoja komenda coś zmieniła, więc na wszelki wypadek mówi „tak”. Na koniec dopisz coś ręcznie w /var/www/strona/index.html i odpal --check --diff. Minusik przy Twojej linijce to Ansible, który już ostrzy nożyczki.
Co dalej
Ansible to luksus. Ale czasem dostajesz goły serwer, na którym masz tylko Basha, i skrypt musi zainstalować pakiety bez żadnych pytań. W następnej lekcji apt i dnf bez pytań, DEBIAN_FRONTEND=noninteractive i jak napisać w Bashu skrypt, który można uruchomić sto razy, tak jak playbook.