Modul 10 — Infrastructure as Code
51. Ansible role a idempotence v praxi
Jak strukturovat větší Ansible projekty do znovupoužitelných rolí a proč je rozdíl mezi idempotentními moduly a příkazy shell/command zásadní pro bezpečnou automatizaci.
Úvod a kontext
Minulá lekce ukázala jeden playbook s několika tasky přímo v jednom
souboru. To funguje pro malý projekt, ale s rostoucím počtem
spravovaných serverů a služeb (webserver, databáze, monitoring agent,
...) se stejný vzor tasků opakuje napříč playbooky. Ansible na to má
stejnou odpověď jako Terraform na opakující se zdroje — role. Tato
lekce také podrobně rozebírá idempotenci, kterou lekce o principech
IaC jen zmínila, protože je to nejčastější zdroj chyb u začátečníků
v Ansible.
Teorie
Role
Role je standardizovaná adresářová struktura balící dohromady
tasky, proměnné, šablony a soubory pro jednu ucelenou funkci (např.
„nakonfiguruj nginx", „nainstaluj a nastav PostgreSQL"). Ansible
očekává pevně danou strukturu adresářů, kterou lze vygenerovat příkazem
ansible-galaxy init nginx:
roles/
└── nginx/
├── tasks/
│ └── main.yml # hlavní seznam úkolů role
├── handlers/
│ └── main.yml # handlery specifické pro roli
├── templates/
│ └── nginx.conf.j2 # Jinja2 šablony
├── files/
│ └── favicon.ico # statické soubory ke zkopírování beze změny
├── vars/
│ └── main.yml # proměnné specifické pro roli (vysoká priorita)
├── defaults/
│ └── main.yml # výchozí hodnoty proměnných (nízká priorita, snadno přepsatelné)
└── meta/
└── main.yml # metadata, závislosti na jiných rolích
Obsah tasks/main.yml role je téměř identický s tím, co bylo dřív
přímo v playbooku:
# roles/nginx/tasks/main.yml
- name: Nainstalovat nginx
apt:
name: nginx
state: present
- name: Nahrát konfiguraci
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: Restartovat nginx
- name: Spustit a povolit nginx
service:
name: nginx
state: started
enabled: true
Playbook pak roli jen zavolá:
# playbook.yml
---
- name: Nastavení webových serverů
hosts: webservers
become: true
roles:
- nginx
- monitoring-agent
Výhody rolí: znovupoužitelnost napříč projekty (stejná role
nginx se dá použít v deseti různých playbookách), jasná struktura,
kterou znají všichni uživatelé Ansible, snadné sdílení přes
Ansible Galaxy (veřejný registr rolí, obdoba Terraform Registry
nebo Docker Hub).
Proměnné a jejich priorita
Role obvykle definuje rozumné výchozí hodnoty v defaults/main.yml
(nejnižší priorita, snadno přepsatelné voláním role) a případně pevné,
záměrně obtížně přepsatelné hodnoty v vars/main.yml:
# roles/nginx/defaults/main.yml
nginx_port: 80
nginx_worker_processes: auto
Volající playbook může výchozí hodnotu přepsat:
roles:
- role: nginx
vars:
nginx_port: 8080
Idempotence: moduly vs. shell/command
Idempotence znamená, že opakované spuštění stejného tasku nezmění nic,
pokud je systém už v požadovaném stavu. Většina vestavěných modulů
(apt, service, template, file, user, ...) je navržena
idempotentně — modul si sám ověří aktuální stav a hlásí changed jen
tehdy, když skutečně něco upravil, jinak ok.
Problém nastává u modulů command a shell, které jen spustí
libovolný příkaz — ty nejsou idempotentní samy o sobě, protože
Ansible nemá způsob, jak zjistit, zda příkaz už byl proveden. Bez
dalšího ošetření se spustí pokaždé a vždy se nahlásí jako changed,
i kdyby dělal přesně to samé podruhé:
# ŠPATNĚ — spustí se při každém běhu, i když adresář už existuje
- name: Vytvořit adresář pro aplikaci
command: mkdir /opt/moje-appka
Pokud pro daný úkol existuje vestavěný modul, je vždy lepší použít jej
místo command/shell, protože je idempotentní přirozeně:
# SPRÁVNĚ — modul file je idempotentní, hlásí "ok" pokud adresář už existuje
- name: Vytvořit adresář pro aplikaci
file:
path: /opt/moje-appka
state: directory
mode: '0755'
Když je command/shell opravdu nutný (např. spuštění specifického
skriptu, pro který neexistuje modul), dá se idempotence zajistit ručně
pomocí creates (task se přeskočí, pokud daný soubor/adresář už
existuje) nebo changed_when/when:
- name: Stáhnout a rozbalit aplikaci, jen pokud ještě není rozbalená
command: tar -xzf /tmp/app.tar.gz -C /opt/moje-appka
args:
creates: /opt/moje-appka/bin/app # pokud tento soubor existuje, task se přeskočí
- name: Spustit databázovou migraci, jen pokud je potřeba
command: /opt/moje-appka/bin/migrate --check
register: migrace_stav
changed_when: "'pending migrations' in migrace_stav.stdout"
Tagy
Tagy umožňují spustit jen podmnožinu tasků z playbooku, což se hodí
u dlouhých playbooků při ladění nebo částečném nasazení:
- name: Nahrát konfiguraci
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
tags: [konfigurace]
ansible-playbook playbook.yml --tags konfigurace
Praktický příklad
Ukázka ověření idempotence v praxi — dvojí spuštění stejného playbooku
proti již nakonfigurovanému serveru:
$ ansible-playbook -i inventory.ini playbook.yml
PLAY RECAP *************************************************************
web1.example.cz : ok=4 changed=3 unreachable=0 failed=0
# druhé spuštění, bez jakékoliv změny na serveru mezi tím
$ ansible-playbook -i inventory.ini playbook.yml
PLAY RECAP *************************************************************
web1.example.cz : ok=4 changed=0 unreachable=0 failed=0
Při druhém běhu je changed=0 — to je důkaz, že playbook je skutečně
idempotentní. Pokud by po druhém běhu changed bylo znovu nenulové
(a nic mezitím server jinak nezměnilo), signalizuje to problém — typicky
nesprávně použitý command/shell bez creates/changed_when.
Shrnutí
Role balí tasky, proměnné, šablony a handlery do znovupoužitelné,
standardizované struktury, kterou lze sdílet přes Ansible Galaxy a
volat z libovolného playbooku. Idempotence je vlastnost, díky které je
bezpečné playbook spouštět opakovaně — vestavěné moduly ji mají
přirozeně, zatímco command/shell ji vyžadují explicitně ošetřit
přes creates nebo changed_when. Opakovaný běh se stejným výsledkem
changed=0 je praktickým ověřením, že je playbook napsán správně.
Kontrolní otázky
- Jaký je rozdíl mezi
defaults/main.ymlavars/main.ymlv roli
z hlediska priority přepisování? - Proč modul
commandsám o sobě není idempotentní a jak to lze
opravit? - Jak byste ověřili, že je playbook skutečně idempotentní, aniž byste
četli jeho kód? - Mini cvičení: přepište následující task tak, aby byl idempotentní,
pomocícreates:command: touch /opt/app/.initialized.
Lekce na sebe nejsou zamčené — libovolnou lekci můžete otevřít i označit jako hotovou v jakémkoliv pořadí.