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.

Odhadovaná délka studia: 55 minut · Stav: nehotovo

Technologie: Ansible

Ú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

  1. Jaký je rozdíl mezi defaults/main.yml a vars/main.yml v roli
    z hlediska priority přepisování?
  2. Proč modul command sám o sobě není idempotentní a jak to lze
    opravit?
  3. Jak byste ověřili, že je playbook skutečně idempotentní, aniž byste
    četli jeho kód?
  4. 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í.